package harness import ( "encoding/json" "fmt" "os" "os/exec" "path/filepath" "runtime" "strings" "github.com/hjbdev/patterm/internal/projectkey" "github.com/hjbdev/patterm/internal/trust" ) type Options struct { PattermBin string Scenario *Scenario } type testEnv struct { Root string `json:"root"` ConfigHome string `json:"xdg_config_home"` DataHome string `json:"xdg_data_home"` RuntimeDir string `json:"xdg_runtime_dir"` BinDir string `json:"bin_dir"` ProjectDir string `json:"project_dir"` PattermBin string `json:"patterm_bin"` Cols uint16 `json:"cols"` Rows uint16 `json:"rows"` } func prepareEnv(opts Options) (*testEnv, []string, error) { sc := opts.Scenario root, err := os.MkdirTemp("", "patterm-harness-*") if err != nil { return nil, nil, err } env := &testEnv{ Root: root, ConfigHome: filepath.Join(root, "config"), DataHome: filepath.Join(root, "data"), RuntimeDir: filepath.Join(root, "runtime"), BinDir: filepath.Join(root, "bin"), Cols: sc.Cols, Rows: sc.Rows, PattermBin: opts.PattermBin, } if env.ProjectDir = sc.ProjectDir; env.ProjectDir == "" { env.ProjectDir = filepath.Join(root, "project") } for _, dir := range []string{env.ConfigHome, env.DataHome, env.RuntimeDir, env.BinDir, env.ProjectDir} { if err := os.MkdirAll(dir, 0o700); err != nil { return nil, nil, err } } if env.PattermBin == "" { env.PattermBin, err = defaultPattermBin() if err != nil { return nil, nil, err } } if err := writeScripts(env.BinDir, sc.Scripts); err != nil { return nil, nil, err } if err := writePresets(env.ConfigHome, sc.Presets); err != nil { return nil, nil, err } if err := seedTrust(env, sc.Trust); err != nil { return nil, nil, err } childEnv := append(os.Environ(), "XDG_CONFIG_HOME="+env.ConfigHome, "XDG_DATA_HOME="+env.DataHome, "XDG_RUNTIME_DIR="+env.RuntimeDir, "PATTERM_HARNESS=1", "PATH="+env.BinDir+string(os.PathListSeparator)+os.Getenv("PATH"), ) for k, v := range sc.Env { childEnv = append(childEnv, k+"="+v) } return env, childEnv, nil } func writeScripts(bin string, scripts []ScenarioScript) error { for _, script := range scripts { if script.Name == "" { return fmt.Errorf("script missing name") } if strings.Contains(script.Name, "/") { return fmt.Errorf("script name %q must not contain /", script.Name) } path := filepath.Join(bin, script.Name) if err := os.WriteFile(path, []byte(script.Body), 0o700); err != nil { return err } } return nil } func writePresets(configHome string, presets ScenarioPresets) error { base := filepath.Join(configHome, "patterm", "presets") if err := os.MkdirAll(filepath.Join(base, "agents"), 0o700); err != nil { return err } if err := os.MkdirAll(filepath.Join(base, "processes"), 0o700); err != nil { return err } for _, p := range presets.Agents { if err := writePreset(filepath.Join(base, "agents", p.Name+".json"), p); err != nil { return err } } for _, p := range presets.Processes { if err := writePreset(filepath.Join(base, "processes", p.Name+".json"), p); err != nil { return err } } return nil } func writePreset(path string, p ScenarioPreset) error { if p.Name == "" { return fmt.Errorf("preset missing name") } b, err := json.MarshalIndent(p, "", " ") if err != nil { return err } b = append(b, '\n') return os.WriteFile(path, b, 0o600) } func seedTrust(env *testEnv, presets []string) error { if len(presets) == 0 { return nil } key, err := projectkey.Key(env.ProjectDir) if err != nil { return err } restore := setenv(map[string]string{"XDG_DATA_HOME": env.DataHome}) defer restore() store, err := trust.Open(key) if err != nil { return err } for _, p := range presets { if err := store.Grant(p); err != nil { return err } } return nil } func setenv(vals map[string]string) func() { type old struct { v string ok bool } prev := map[string]old{} for k, v := range vals { ov, ok := os.LookupEnv(k) prev[k] = old{v: ov, ok: ok} _ = os.Setenv(k, v) } return func() { for k, ov := range prev { if ov.ok { _ = os.Setenv(k, ov.v) } else { _ = os.Unsetenv(k) } } } } func defaultPattermBin() (string, error) { if p := os.Getenv("PATTERM_BIN"); p != "" { return p, nil } return buildPattermBinary() } func buildPattermBinary() (string, error) { root, err := repoRoot() if err != nil { return "", err } out := filepath.Join(os.TempDir(), "patterm-harness-bin", "patterm") if err := os.MkdirAll(filepath.Dir(out), 0o700); err != nil { return "", err } cmd := exec.Command("go", "build", "-o", out, "./cmd/patterm") cmd.Dir = root cmd.Env = os.Environ() if b, err := cmd.CombinedOutput(); err != nil { return "", fmt.Errorf("build patterm: %w\n%s", err, string(b)) } return out, nil } func repoRoot() (string, error) { _, file, _, ok := runtime.Caller(0) if !ok { return "", fmt.Errorf("runtime.Caller failed") } return filepath.Clean(filepath.Join(filepath.Dir(file), "..", "..")), nil }