695 lines
21 KiB
Go
695 lines
21 KiB
Go
package agentconfig
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestLoadRuntimeConfigLocalWinsAndReplacesOrderedArrays(t *testing.T) {
|
|
root := t.TempDir()
|
|
repoGlobal := runtimeGlobalFixture()
|
|
userLocal := runtimeLocalFixture(
|
|
root,
|
|
"local-profile",
|
|
` selection:
|
|
rules:
|
|
- id: local-only
|
|
target:
|
|
profile: local-profile
|
|
`,
|
|
` isolation:
|
|
fallback_modes: [clone]
|
|
`,
|
|
` retention:
|
|
completed_days: 5
|
|
`,
|
|
` alpha:
|
|
workspace: `+yamlQuote(filepath.Join(root, "workspace"))+`
|
|
selected_milestone: milestone-a
|
|
override:
|
|
defaults:
|
|
default_profile: project-profile
|
|
profile_aliases:
|
|
shared: project-profile
|
|
selection:
|
|
rules:
|
|
- id: project-only
|
|
target:
|
|
profile: project-profile
|
|
`,
|
|
)
|
|
|
|
snapshot, err := LoadRuntimeConfigBytes([]byte(repoGlobal), []byte(userLocal))
|
|
if err != nil {
|
|
t.Fatalf("LoadRuntimeConfigBytes: %v", err)
|
|
}
|
|
config := snapshot.Config()
|
|
|
|
if got, want := config.Defaults.DefaultProfile, "local-profile"; got != want {
|
|
t.Fatalf("default profile = %q, want %q", got, want)
|
|
}
|
|
if config.Defaults.AutoResumeInterrupted {
|
|
t.Fatal("auto_resume_interrupted = true, want explicit local false")
|
|
}
|
|
if got, want := config.Defaults.ProfileAliases, map[string]string{
|
|
"global": "global-profile",
|
|
"local": "local-profile",
|
|
"shared": "local-profile",
|
|
}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("profile aliases = %#v, want %#v", got, want)
|
|
}
|
|
if got, want := selectionRuleIDs(config.Selection.Rules), []string{"local-only"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("selection rule IDs = %v, want whole replacement %v", got, want)
|
|
}
|
|
if got, want := config.Isolation.FallbackModes, []string{"clone"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("fallback modes = %v, want whole replacement %v", got, want)
|
|
}
|
|
if got, want := config.Retention, (RetentionPolicy{
|
|
CompletedDays: 5,
|
|
BlockedDays: 30,
|
|
MaxProjectLogRecords: 500,
|
|
}); got != want {
|
|
t.Fatalf("retention = %#v, want %#v", got, want)
|
|
}
|
|
|
|
project, ok := snapshot.Project("alpha")
|
|
if !ok {
|
|
t.Fatal("project alpha is missing")
|
|
}
|
|
if got, want := project.Defaults.DefaultProfile, "project-profile"; got != want {
|
|
t.Fatalf("project default profile = %q, want %q", got, want)
|
|
}
|
|
if got, want := project.Defaults.ProfileAliases["global"], "global-profile"; got != want {
|
|
t.Fatalf("project inherited alias = %q, want %q", got, want)
|
|
}
|
|
if got, want := project.Defaults.ProfileAliases["shared"], "project-profile"; got != want {
|
|
t.Fatalf("project local-wins alias = %q, want %q", got, want)
|
|
}
|
|
if got, want := selectionRuleIDs(project.Selection.Rules), []string{"project-only"}; !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("project selection rule IDs = %v, want %v", got, want)
|
|
}
|
|
if project.AutoResumeInterrupted {
|
|
t.Fatal("project auto resume did not inherit explicit local false")
|
|
}
|
|
}
|
|
|
|
func TestLoadRuntimeConfigRejectsInvalidDocumentsAndValues(t *testing.T) {
|
|
root := t.TempDir()
|
|
validGlobal := runtimeGlobalFixture()
|
|
validLocal := runtimeLocalFixture(root, "local-profile", "", "", "", "")
|
|
|
|
tests := []struct {
|
|
name string
|
|
global string
|
|
local string
|
|
want string
|
|
}{
|
|
{
|
|
name: "unknown repo field",
|
|
global: validGlobal + "unexpected: true\n",
|
|
local: validLocal,
|
|
want: "field unexpected not found",
|
|
},
|
|
{
|
|
name: "unknown local credential field",
|
|
global: validGlobal,
|
|
local: validLocal + "token: forbidden\n",
|
|
want: "field token not found",
|
|
},
|
|
{
|
|
name: "multiple local documents",
|
|
global: validGlobal,
|
|
local: validLocal + "---\nversion: \"1\"\n",
|
|
want: "exactly one YAML document",
|
|
},
|
|
{
|
|
name: "unsupported version",
|
|
global: strings.Replace(validGlobal, `version: "1"`, `version: "2"`, 1),
|
|
local: validLocal,
|
|
want: "unsupported repo-global runtime config version",
|
|
},
|
|
{
|
|
name: "relative local root",
|
|
global: validGlobal,
|
|
local: strings.Replace(validLocal, yamlQuote(filepath.Join(root, "state")), "relative/state", 1),
|
|
want: "absolute clean path",
|
|
},
|
|
{
|
|
name: "invalid isolation mode",
|
|
global: strings.Replace(validGlobal, "default_mode: overlay", "default_mode: direct", 1),
|
|
local: validLocal,
|
|
want: "unsupported mode",
|
|
},
|
|
{
|
|
name: "duplicate ordered rule",
|
|
global: strings.Replace(
|
|
validGlobal,
|
|
" - id: global-second",
|
|
" - id: global-first",
|
|
1,
|
|
),
|
|
local: validLocal,
|
|
want: "repeats rule id",
|
|
},
|
|
{
|
|
name: "negative retention",
|
|
global: strings.Replace(validGlobal, "completed_days: 14", "completed_days: -1", 1),
|
|
local: validLocal,
|
|
want: "must be non-negative",
|
|
},
|
|
{
|
|
name: "local target missing from configured catalog",
|
|
global: runtimeGlobalWithCatalogFixture(),
|
|
local: validLocal,
|
|
want: `references unknown profile "local-profile"`,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := LoadRuntimeConfigBytes([]byte(test.global), []byte(test.local))
|
|
if err == nil || !strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("LoadRuntimeConfigBytes error = %v, want containing %q", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLoadRuntimeConfigRejectsPartialSelectionDefaultTargets(t *testing.T) {
|
|
root := t.TempDir()
|
|
catalogGlobal := runtimeGlobalWithCatalogFixture()
|
|
// Use a catalog-known profile so the local config itself is valid.
|
|
validLocal := runtimeLocalFixture(root, "global-profile", "", "", "", "")
|
|
|
|
tests := []struct {
|
|
name string
|
|
global string
|
|
local string
|
|
want string
|
|
}{
|
|
{
|
|
name: "provider-only default selection rejects",
|
|
global: strings.Replace(catalogGlobal, " profile: global-profile\n", " provider: codex\n", 1),
|
|
local: validLocal,
|
|
want: "profile is required when provider or model is set",
|
|
},
|
|
{
|
|
name: "model-only default selection rejects",
|
|
global: strings.Replace(catalogGlobal, " profile: global-profile\n", " model: global-model\n", 1),
|
|
local: validLocal,
|
|
want: "profile is required when provider or model is set",
|
|
},
|
|
{
|
|
name: "provider and model without profile rejects",
|
|
global: strings.Replace(catalogGlobal, " profile: global-profile\n", " provider: codex\n model: global-model\n", 1),
|
|
local: validLocal,
|
|
want: "profile is required when provider or model is set",
|
|
},
|
|
{
|
|
name: "fully empty default selection remains valid",
|
|
global: strings.Replace(catalogGlobal, " profile: global-profile\n", "", 1),
|
|
local: validLocal,
|
|
want: "",
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := LoadRuntimeConfigBytes([]byte(test.global), []byte(test.local))
|
|
if test.want == "" {
|
|
if err != nil {
|
|
t.Fatalf("LoadRuntimeConfigBytes error = %v, want nil", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("LoadRuntimeConfigBytes error = %v, want containing %q", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuntimeSnapshotAccessorsAreImmutableAndRevisioned(t *testing.T) {
|
|
root := t.TempDir()
|
|
global := runtimeGlobalFixture()
|
|
localA := runtimeLocalFixture(root, "local-a", "", "", "", "")
|
|
|
|
snapshotA, err := LoadRuntimeConfigBytes([]byte(global), []byte(localA))
|
|
if err != nil {
|
|
t.Fatalf("load snapshot A: %v", err)
|
|
}
|
|
if !strings.HasPrefix(snapshotA.Revision(), "sha256:") {
|
|
t.Fatalf("revision = %q, want sha256 prefix", snapshotA.Revision())
|
|
}
|
|
sourcesA := snapshotA.SourceRevisions()
|
|
if !strings.HasPrefix(sourcesA.RepoGlobal, "sha256:") || !strings.HasPrefix(sourcesA.UserLocal, "sha256:") {
|
|
t.Fatalf("source revisions = %#v, want sha256 prefixes", sourcesA)
|
|
}
|
|
initialConfig := snapshotA.Config()
|
|
if initialConfig.Revision != snapshotA.Revision() {
|
|
t.Fatalf("config revision = %q, want snapshot revision %q", initialConfig.Revision, snapshotA.Revision())
|
|
}
|
|
if initialConfig.SourceRevisions != sourcesA {
|
|
t.Fatalf("config source revisions = %#v, want %#v", initialConfig.SourceRevisions, sourcesA)
|
|
}
|
|
if !strings.HasPrefix(initialConfig.Selection.Revision, "sha256:") {
|
|
t.Fatalf("selection revision = %q, want sha256 prefix", initialConfig.Selection.Revision)
|
|
}
|
|
|
|
mutated := snapshotA.Config()
|
|
mutated.Defaults.ProfileAliases["global"] = "mutated"
|
|
mutated.Selection.Rules[0].ID = "mutated"
|
|
mutated.Isolation.FallbackModes[0] = "mutated"
|
|
mutated.Projects["injected"] = ProjectRegistration{ID: "injected"}
|
|
fresh := snapshotA.Config()
|
|
if got, want := fresh.Defaults.ProfileAliases["global"], "global-profile"; got != want {
|
|
t.Fatalf("fresh alias = %q, want %q", got, want)
|
|
}
|
|
if got, want := fresh.Selection.Rules[0].ID, "global-first"; got != want {
|
|
t.Fatalf("fresh rule ID = %q, want %q", got, want)
|
|
}
|
|
if got, want := fresh.Isolation.FallbackModes[0], "worktree"; got != want {
|
|
t.Fatalf("fresh fallback = %q, want %q", got, want)
|
|
}
|
|
if _, exists := fresh.Projects["injected"]; exists {
|
|
t.Fatal("mutation leaked into immutable snapshot projects")
|
|
}
|
|
|
|
localB := strings.Replace(localA, "local-a", "local-b", 1)
|
|
snapshotB, err := LoadRuntimeConfigBytes([]byte(global), []byte(localB))
|
|
if err != nil {
|
|
t.Fatalf("load snapshot B: %v", err)
|
|
}
|
|
if snapshotA.Revision() == snapshotB.Revision() {
|
|
t.Fatalf("runtime revisions did not change: %q", snapshotA.Revision())
|
|
}
|
|
sourcesB := snapshotB.SourceRevisions()
|
|
if sourcesA.RepoGlobal != sourcesB.RepoGlobal {
|
|
t.Fatalf("repo-global revision changed: A=%q B=%q", sourcesA.RepoGlobal, sourcesB.RepoGlobal)
|
|
}
|
|
if sourcesA.UserLocal == sourcesB.UserLocal {
|
|
t.Fatalf("user-local revision did not change: %q", sourcesA.UserLocal)
|
|
}
|
|
if got, want := snapshotA.Config().Defaults.DefaultProfile, "local-a"; got != want {
|
|
t.Fatalf("snapshot A changed after loading B: got %q, want %q", got, want)
|
|
}
|
|
}
|
|
|
|
func TestLoadRuntimeConfigDoesNotWriteRepoGlobalInput(t *testing.T) {
|
|
root := t.TempDir()
|
|
repoPath := filepath.Join(root, "repo-runtime.yaml")
|
|
localPath := filepath.Join(root, "local-runtime.yaml")
|
|
repoBytes := []byte(runtimeGlobalFixture())
|
|
if err := os.WriteFile(repoPath, repoBytes, 0o444); err != nil {
|
|
t.Fatalf("write repo fixture: %v", err)
|
|
}
|
|
if err := os.WriteFile(localPath, []byte(runtimeLocalFixture(root, "local-profile", "", "", "", "")), 0o600); err != nil {
|
|
t.Fatalf("write local fixture: %v", err)
|
|
}
|
|
before := sha256.Sum256(repoBytes)
|
|
|
|
if _, err := LoadRuntimeConfig(repoPath, localPath); err != nil {
|
|
t.Fatalf("LoadRuntimeConfig: %v", err)
|
|
}
|
|
afterBytes, err := os.ReadFile(repoPath)
|
|
if err != nil {
|
|
t.Fatalf("read repo fixture after load: %v", err)
|
|
}
|
|
after := sha256.Sum256(afterBytes)
|
|
if before != after {
|
|
t.Fatalf("repo-global digest changed: before=%x after=%x", before, after)
|
|
}
|
|
info, err := os.Stat(repoPath)
|
|
if err != nil {
|
|
t.Fatalf("stat repo fixture: %v", err)
|
|
}
|
|
if got, want := info.Mode().Perm(), os.FileMode(0o444); got != want {
|
|
t.Fatalf("repo-global mode = %o, want %o", got, want)
|
|
}
|
|
}
|
|
|
|
func TestClientProcessSpecsAreUserLocalAndImmutable(t *testing.T) {
|
|
root := t.TempDir()
|
|
local := runtimeLocalWithClientsFixture(root, `
|
|
clients:
|
|
flutter:
|
|
executable: /opt/iop/flutter
|
|
args: [--socket, local.sock]
|
|
working_directory: /opt/iop
|
|
launch_on_start: true
|
|
restart_on_crash: true
|
|
restart_limit: 3
|
|
restart_backoff_millis: 25
|
|
focus_args: [--focus-existing]
|
|
unity:
|
|
executable: /opt/iop/unity
|
|
args: [--character]
|
|
working_directory: /opt/iop
|
|
`)
|
|
|
|
snapshot, err := LoadRuntimeConfigBytes(
|
|
[]byte(runtimeGlobalFixture()),
|
|
[]byte(local),
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("LoadRuntimeConfigBytes: %v", err)
|
|
}
|
|
config := snapshot.Config()
|
|
if got, want := config.Clients["flutter"].RestartLimit, 3; got != want {
|
|
t.Fatalf("flutter restart limit = %d, want %d", got, want)
|
|
}
|
|
if got, want := config.Clients["unity"].Executable, "/opt/iop/unity"; got != want {
|
|
t.Fatalf("unity executable = %q, want %q", got, want)
|
|
}
|
|
|
|
flutter := config.Clients["flutter"]
|
|
flutter.Args[0] = "mutated"
|
|
flutter.FocusArgs[0] = "mutated"
|
|
config.Clients["flutter"] = flutter
|
|
delete(config.Clients, "unity")
|
|
|
|
fresh := snapshot.Config()
|
|
if got, want := fresh.Clients["flutter"].Args[0], "--socket"; got != want {
|
|
t.Fatalf("client args mutation leaked: got %q, want %q", got, want)
|
|
}
|
|
if got, want := fresh.Clients["flutter"].FocusArgs[0], "--focus-existing"; got != want {
|
|
t.Fatalf("focus args mutation leaked: got %q, want %q", got, want)
|
|
}
|
|
if _, ok := fresh.Clients["unity"]; !ok {
|
|
t.Fatal("client map mutation leaked into snapshot")
|
|
}
|
|
}
|
|
|
|
func TestClientProcessSpecValidationMatrix(t *testing.T) {
|
|
root := t.TempDir()
|
|
validGlobal := runtimeGlobalFixture()
|
|
validLocal := runtimeLocalWithClientsFixture(root, `
|
|
clients:
|
|
flutter:
|
|
executable: /opt/iop/flutter
|
|
working_directory: /opt/iop
|
|
restart_on_crash: true
|
|
restart_limit: 2
|
|
focus_args: [--focus-existing]
|
|
`)
|
|
tests := []struct {
|
|
name string
|
|
global string
|
|
local string
|
|
want string
|
|
}{
|
|
{
|
|
name: "valid",
|
|
global: validGlobal,
|
|
local: validLocal,
|
|
},
|
|
{
|
|
name: "repo-global clients rejected",
|
|
global: validGlobal + "clients: {}\n",
|
|
local: validLocal,
|
|
want: "field clients not found",
|
|
},
|
|
{
|
|
name: "unknown kind",
|
|
global: validGlobal,
|
|
local: strings.Replace(validLocal, " flutter:\n", " electron:\n", 1),
|
|
want: `unsupported client kind "electron"`,
|
|
},
|
|
{
|
|
name: "relative executable",
|
|
global: validGlobal,
|
|
local: strings.Replace(validLocal, "/opt/iop/flutter", "bin/flutter", 1),
|
|
want: "executable must be an absolute clean path",
|
|
},
|
|
{
|
|
name: "relative working directory",
|
|
global: validGlobal,
|
|
local: strings.Replace(validLocal, "/opt/iop\n", "relative\n", 1),
|
|
want: "working_directory must be an absolute clean path",
|
|
},
|
|
{
|
|
name: "negative restart limit",
|
|
global: validGlobal,
|
|
local: strings.Replace(validLocal, "restart_limit: 2", "restart_limit: -1", 1),
|
|
want: "restart limits must be non-negative",
|
|
},
|
|
{
|
|
name: "restart requires positive limit",
|
|
global: validGlobal,
|
|
local: strings.Replace(validLocal, "restart_limit: 2", "restart_limit: 0", 1),
|
|
want: "restart_limit must be positive",
|
|
},
|
|
{
|
|
name: "disabled restart rejects policy",
|
|
global: validGlobal,
|
|
local: strings.Replace(validLocal, "restart_on_crash: true", "restart_on_crash: false", 1),
|
|
want: "restart policy requires restart_on_crash",
|
|
},
|
|
{
|
|
name: "unity cannot bypass flutter focus",
|
|
global: validGlobal,
|
|
local: strings.Replace(
|
|
validLocal,
|
|
" flutter:\n",
|
|
" unity:\n",
|
|
1,
|
|
),
|
|
want: "cannot configure Flutter focus arguments",
|
|
},
|
|
{
|
|
name: "environment field rejected",
|
|
global: validGlobal,
|
|
local: strings.Replace(
|
|
validLocal,
|
|
" working_directory: /opt/iop\n",
|
|
" working_directory: /opt/iop\n environment:\n TOKEN: forbidden\n",
|
|
1,
|
|
),
|
|
want: "field environment not found",
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
_, err := LoadRuntimeConfigBytes(
|
|
[]byte(test.global),
|
|
[]byte(test.local),
|
|
)
|
|
if test.want == "" {
|
|
if err != nil {
|
|
t.Fatalf("LoadRuntimeConfigBytes: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if err == nil || !strings.Contains(err.Error(), test.want) {
|
|
t.Fatalf("error = %v, want containing %q", err, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRuntimeConfigWatcherPublishesOnlyValidNextInvocationRevision(t *testing.T) {
|
|
root := t.TempDir()
|
|
repoPath := filepath.Join(root, "repo-runtime.yaml")
|
|
localPath := filepath.Join(root, "local-runtime.yaml")
|
|
repoBytes := []byte(runtimeGlobalFixture())
|
|
if err := os.WriteFile(repoPath, repoBytes, 0o444); err != nil {
|
|
t.Fatalf("write repo fixture: %v", err)
|
|
}
|
|
localA := runtimeLocalFixture(root, "local-a", "", "", "", "")
|
|
if err := os.WriteFile(localPath, []byte(localA), 0o600); err != nil {
|
|
t.Fatalf("write local fixture A: %v", err)
|
|
}
|
|
repoDigest := sha256.Sum256(repoBytes)
|
|
|
|
watcher, err := NewRuntimeConfigWatcher(context.Background(), repoPath, localPath, 10*time.Millisecond)
|
|
if err != nil {
|
|
t.Fatalf("NewRuntimeConfigWatcher: %v", err)
|
|
}
|
|
defer watcher.Close()
|
|
|
|
invocationA := watcher.Snapshot()
|
|
if got, want := invocationA.Config().Defaults.DefaultProfile, "local-a"; got != want {
|
|
t.Fatalf("initial profile = %q, want %q", got, want)
|
|
}
|
|
|
|
writeAtomicFixture(t, localPath, strings.Replace(localA, "version:", "unknown: true\nversion:", 1))
|
|
select {
|
|
case reloadErr := <-watcher.Errors():
|
|
if reloadErr == nil || !strings.Contains(reloadErr.Error(), "field unknown not found") {
|
|
t.Fatalf("watcher error = %v, want strict unknown-field error", reloadErr)
|
|
}
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timed out waiting for invalid reload error")
|
|
}
|
|
if got, want := watcher.Snapshot().Revision(), invocationA.Revision(); got != want {
|
|
t.Fatalf("invalid edit published revision %q, want retained %q", got, want)
|
|
}
|
|
|
|
localB := strings.Replace(localA, "local-a", "local-b", 1)
|
|
writeAtomicFixture(t, localPath, localB)
|
|
var invocationB RuntimeSnapshot
|
|
select {
|
|
case invocationB = <-watcher.Updates():
|
|
case <-time.After(2 * time.Second):
|
|
t.Fatal("timed out waiting for valid revision B")
|
|
}
|
|
if invocationB.Revision() == invocationA.Revision() {
|
|
t.Fatalf("watcher revisions are equal: %q", invocationB.Revision())
|
|
}
|
|
if got, want := invocationB.Config().Defaults.DefaultProfile, "local-b"; got != want {
|
|
t.Fatalf("revision B profile = %q, want %q", got, want)
|
|
}
|
|
if got, want := watcher.Snapshot().Revision(), invocationB.Revision(); got != want {
|
|
t.Fatalf("current watcher revision = %q, want B %q", got, want)
|
|
}
|
|
if got, want := invocationA.Config().Defaults.DefaultProfile, "local-a"; got != want {
|
|
t.Fatalf("pinned invocation A changed to %q, want %q", got, want)
|
|
}
|
|
|
|
afterRepoBytes, err := os.ReadFile(repoPath)
|
|
if err != nil {
|
|
t.Fatalf("read repo fixture after watch: %v", err)
|
|
}
|
|
if after := sha256.Sum256(afterRepoBytes); after != repoDigest {
|
|
t.Fatalf("watcher mutated repo-global input: before=%x after=%x", repoDigest, after)
|
|
}
|
|
}
|
|
|
|
func runtimeGlobalFixture() string {
|
|
return `version: "1"
|
|
defaults:
|
|
default_profile: global-profile
|
|
auto_resume_interrupted: true
|
|
profile_aliases:
|
|
global: global-profile
|
|
shared: global-profile
|
|
selection:
|
|
timezone: UTC
|
|
default:
|
|
profile: global-profile
|
|
rules:
|
|
- id: global-first
|
|
match:
|
|
stages: [worker]
|
|
min_grade: 1
|
|
max_grade: 10
|
|
target:
|
|
profile: global-profile
|
|
- id: global-second
|
|
target:
|
|
profile: backup-profile
|
|
isolation:
|
|
default_mode: overlay
|
|
fallback_modes: [worktree, clone]
|
|
retention:
|
|
completed_days: 14
|
|
blocked_days: 30
|
|
max_project_log_records: 500
|
|
`
|
|
}
|
|
|
|
func runtimeGlobalWithCatalogFixture() string {
|
|
catalog := `catalog:
|
|
version: "1"
|
|
providers:
|
|
- id: codex
|
|
command: codex
|
|
version_probe:
|
|
args: [--version]
|
|
authentication:
|
|
args: [login, status]
|
|
capabilities: [run]
|
|
models:
|
|
- id: global-model
|
|
provider: codex
|
|
target: native-global
|
|
profiles:
|
|
- id: global-profile
|
|
provider: codex
|
|
model: global-model
|
|
capabilities: [run]
|
|
- id: backup-profile
|
|
provider: codex
|
|
model: global-model
|
|
capabilities: [run]
|
|
`
|
|
return strings.Replace(runtimeGlobalFixture(), "version: \"1\"\n", "version: \"1\"\n"+catalog, 1)
|
|
}
|
|
|
|
func runtimeLocalFixture(
|
|
root string,
|
|
defaultProfile string,
|
|
selectionOverride string,
|
|
isolationOverride string,
|
|
retentionOverride string,
|
|
projects string,
|
|
) string {
|
|
return fmt.Sprintf(`version: "1"
|
|
device:
|
|
state_root: %s
|
|
overlay_root: %s
|
|
log_root: %s
|
|
temp_root: %s
|
|
cache_root: %s
|
|
override:
|
|
defaults:
|
|
default_profile: %s
|
|
auto_resume_interrupted: false
|
|
profile_aliases:
|
|
local: %s
|
|
shared: %s
|
|
%s%s%sprojects:
|
|
%s`,
|
|
yamlQuote(filepath.Join(root, "state")),
|
|
yamlQuote(filepath.Join(root, "overlays")),
|
|
yamlQuote(filepath.Join(root, "logs")),
|
|
yamlQuote(filepath.Join(root, "tmp")),
|
|
yamlQuote(filepath.Join(root, "cache")),
|
|
defaultProfile,
|
|
defaultProfile,
|
|
defaultProfile,
|
|
selectionOverride,
|
|
isolationOverride,
|
|
retentionOverride,
|
|
projects,
|
|
)
|
|
}
|
|
|
|
func runtimeLocalWithClientsFixture(root, clients string) string {
|
|
return strings.Replace(
|
|
runtimeLocalFixture(root, "local-profile", "", "", "", ""),
|
|
"override:\n",
|
|
strings.TrimPrefix(clients, "\n")+"\noverride:\n",
|
|
1,
|
|
)
|
|
}
|
|
|
|
func yamlQuote(value string) string {
|
|
return fmt.Sprintf("%q", value)
|
|
}
|
|
|
|
func selectionRuleIDs(rules []SelectionRule) []string {
|
|
ids := make([]string, len(rules))
|
|
for index, rule := range rules {
|
|
ids[index] = rule.ID
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func writeAtomicFixture(t *testing.T, path, content string) {
|
|
t.Helper()
|
|
nextPath := path + ".next"
|
|
if err := os.WriteFile(nextPath, []byte(content), 0o600); err != nil {
|
|
t.Fatalf("write next fixture: %v", err)
|
|
}
|
|
if err := os.Rename(nextPath, path); err != nil {
|
|
t.Fatalf("replace fixture: %v", err)
|
|
}
|
|
}
|