Skip to content

Commit b5d9963

Browse files
Merge pull request #428 from rileywhite/chore/option-defaults-e2e-tests
test: add end-to-end tests for option_defaults through resolve path
2 parents fb8cb17 + 0d4142a commit b5d9963

1 file changed

Lines changed: 278 additions & 0 deletions

File tree

internal/config/resolve_test.go

Lines changed: 278 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"reflect"
66
"testing"
7+
8+
"github.com/gastownhall/gascity/internal/fsys"
79
)
810

911
// --- helper lookPath functions ---
@@ -862,3 +864,279 @@ func TestMergeProviderOverBuiltinFieldSync(t *testing.T) {
862864
}
863865
}
864866
}
867+
868+
// TestOptionDefaultsTOMLThroughResolve exercises the full path:
869+
// TOML config → LoadWithIncludes (parses + applies patches) → ResolveProvider → EffectiveDefaults.
870+
//
871+
// Three merge layers are verified:
872+
//
873+
// Layer 1: schema-declared default (permission_mode → "plan")
874+
// Layer 2: provider-level option_defaults (model → "sonnet", overriding schema "opus")
875+
// Layer 3: agent-level option_defaults (permission_mode → "unrestricted", model → "haiku" via patch)
876+
func TestOptionDefaultsTOMLThroughResolve(t *testing.T) {
877+
fs := fsys.NewFake()
878+
879+
// city.toml: custom provider with options_schema + option_defaults,
880+
// an agent with its own option_defaults, and a patch that adds more.
881+
fs.Files["/city/city.toml"] = []byte(`
882+
include = ["overrides.toml"]
883+
884+
[workspace]
885+
name = "test"
886+
887+
[providers.testprov]
888+
command = "testprov"
889+
prompt_mode = "arg"
890+
891+
[[providers.testprov.options_schema]]
892+
key = "model"
893+
label = "Model"
894+
type = "select"
895+
default = "opus"
896+
897+
[[providers.testprov.options_schema.choices]]
898+
value = "opus"
899+
label = "Opus"
900+
flag_args = ["--model", "opus"]
901+
902+
[[providers.testprov.options_schema.choices]]
903+
value = "sonnet"
904+
label = "Sonnet"
905+
flag_args = ["--model", "sonnet"]
906+
907+
[[providers.testprov.options_schema.choices]]
908+
value = "haiku"
909+
label = "Haiku"
910+
flag_args = ["--model", "haiku"]
911+
912+
[[providers.testprov.options_schema]]
913+
key = "permission_mode"
914+
label = "Permission Mode"
915+
type = "select"
916+
default = "plan"
917+
918+
[[providers.testprov.options_schema.choices]]
919+
value = "plan"
920+
label = "Plan"
921+
flag_args = ["--permission-mode", "plan"]
922+
923+
[[providers.testprov.options_schema.choices]]
924+
value = "unrestricted"
925+
label = "Unrestricted"
926+
flag_args = ["--dangerously-skip-permissions"]
927+
928+
[[providers.testprov.options_schema]]
929+
key = "output_format"
930+
label = "Output Format"
931+
type = "select"
932+
default = "text"
933+
934+
[[providers.testprov.options_schema.choices]]
935+
value = "text"
936+
label = "Text"
937+
flag_args = ["--output", "text"]
938+
939+
[[providers.testprov.options_schema.choices]]
940+
value = "json"
941+
label = "JSON"
942+
flag_args = ["--output", "json"]
943+
944+
# Provider-level overrides: model "sonnet" (instead of schema "opus"),
945+
# output_format "json" (instead of schema "text").
946+
# output_format is provider-only — no agent overrides it, proving the
947+
# provider layer independently participates in the merge.
948+
[providers.testprov.option_defaults]
949+
model = "sonnet"
950+
output_format = "json"
951+
952+
[[agent]]
953+
name = "worker"
954+
provider = "testprov"
955+
956+
# Agent-level overrides: permission_mode and model.
957+
# model = "sonnet" here will be overwritten by the patch (model = "haiku"),
958+
# proving patch-wins-over-agent overwrite semantics (not just additive insertion).
959+
[agent.option_defaults]
960+
permission_mode = "unrestricted"
961+
model = "sonnet"
962+
`)
963+
964+
// Patch fragment: override agent's model to "haiku".
965+
fs.Files["/city/overrides.toml"] = []byte(`
966+
[[patches.agent]]
967+
name = "worker"
968+
969+
[patches.agent.option_defaults]
970+
model = "haiku"
971+
`)
972+
973+
cfg, _, err := LoadWithIncludes(fs, "/city/city.toml")
974+
if err != nil {
975+
t.Fatalf("LoadWithIncludes: %v", err)
976+
}
977+
978+
// Find the worker agent.
979+
var worker *Agent
980+
for i := range cfg.Agents {
981+
if cfg.Agents[i].Name == "worker" {
982+
worker = &cfg.Agents[i]
983+
break
984+
}
985+
}
986+
if worker == nil {
987+
t.Fatal("worker agent not found in loaded config")
988+
}
989+
990+
// After patching, agent.OptionDefaults should have both keys.
991+
if got := worker.OptionDefaults["permission_mode"]; got != "unrestricted" {
992+
t.Errorf("after patch: agent.OptionDefaults[permission_mode] = %q, want %q", got, "unrestricted")
993+
}
994+
if got := worker.OptionDefaults["model"]; got != "haiku" {
995+
t.Errorf("after patch: agent.OptionDefaults[model] = %q, want %q", got, "haiku")
996+
}
997+
998+
// Resolve the provider — this merges all three layers into EffectiveDefaults.
999+
rp, err := ResolveProvider(worker, &cfg.Workspace, cfg.Providers, lookPathOnly("testprov"))
1000+
if err != nil {
1001+
t.Fatalf("ResolveProvider: %v", err)
1002+
}
1003+
1004+
// Layer 1 (schema default "opus") overridden by Layer 2 (provider "sonnet"),
1005+
// then overridden by Layer 3 (agent "haiku" via patch).
1006+
// This also proves overwrite semantics: agent inline had model = "sonnet",
1007+
// but the patch overwrites it to "haiku".
1008+
if got := rp.EffectiveDefaults["model"]; got != "haiku" {
1009+
t.Errorf("EffectiveDefaults[model] = %q, want %q (agent patch should override agent inline and provider default)", got, "haiku")
1010+
}
1011+
1012+
// Layer 1 (schema default "plan") overridden by Layer 3 (agent "unrestricted").
1013+
if got := rp.EffectiveDefaults["permission_mode"]; got != "unrestricted" {
1014+
t.Errorf("EffectiveDefaults[permission_mode] = %q, want %q (agent default should override schema default)", got, "unrestricted")
1015+
}
1016+
1017+
// Layer 2 (provider "json") is NOT overridden by any agent-level source.
1018+
// This proves the provider layer independently participates in the merge —
1019+
// without it, output_format would remain at schema default "text".
1020+
if got := rp.EffectiveDefaults["output_format"]; got != "json" {
1021+
t.Errorf("EffectiveDefaults[output_format] = %q, want %q (provider default should override schema default)", got, "json")
1022+
}
1023+
}
1024+
1025+
// TestOptionDefaultsRigOverrideThroughResolve exercises the rig-level override
1026+
// path: TOML config → LoadWithIncludes (which internally calls ExpandPacks,
1027+
// applying AgentOverride) → ResolveProvider → EffectiveDefaults.
1028+
//
1029+
// This complements TestOptionDefaultsTOMLThroughResolve which tests the patch path.
1030+
// The rig override path is a separate code flow through applyAgentOverride (pack.go).
1031+
func TestOptionDefaultsRigOverrideThroughResolve(t *testing.T) {
1032+
fs := fsys.NewFake()
1033+
1034+
// Pack defines an agent with no option_defaults.
1035+
fs.Files["/city/packs/svc/pack.toml"] = []byte(`[pack]
1036+
name = "svc"
1037+
schema = 1
1038+
1039+
[[agent]]
1040+
name = "coder"
1041+
provider = "testprov"
1042+
`)
1043+
1044+
// city.toml: provider with options_schema + rig with override option_defaults.
1045+
// No provider-level option_defaults — only schema defaults + agent overrides.
1046+
fs.Files["/city/city.toml"] = []byte(`
1047+
[workspace]
1048+
name = "test"
1049+
1050+
[providers.testprov]
1051+
command = "testprov"
1052+
prompt_mode = "arg"
1053+
1054+
[[providers.testprov.options_schema]]
1055+
key = "model"
1056+
label = "Model"
1057+
type = "select"
1058+
default = "opus"
1059+
1060+
[[providers.testprov.options_schema.choices]]
1061+
value = "opus"
1062+
label = "Opus"
1063+
flag_args = ["--model", "opus"]
1064+
1065+
[[providers.testprov.options_schema.choices]]
1066+
value = "haiku"
1067+
label = "Haiku"
1068+
flag_args = ["--model", "haiku"]
1069+
1070+
[[providers.testprov.options_schema]]
1071+
key = "permission_mode"
1072+
label = "Permission Mode"
1073+
type = "select"
1074+
default = "plan"
1075+
1076+
[[providers.testprov.options_schema.choices]]
1077+
value = "plan"
1078+
label = "Plan"
1079+
flag_args = ["--permission-mode", "plan"]
1080+
1081+
[[providers.testprov.options_schema.choices]]
1082+
value = "unrestricted"
1083+
label = "Unrestricted"
1084+
flag_args = ["--dangerously-skip-permissions"]
1085+
1086+
[[rigs]]
1087+
name = "myrig"
1088+
path = "/repo"
1089+
includes = ["packs/svc"]
1090+
1091+
[[rigs.overrides]]
1092+
agent = "coder"
1093+
1094+
[rigs.overrides.option_defaults]
1095+
model = "haiku"
1096+
permission_mode = "unrestricted"
1097+
`)
1098+
1099+
// LoadWithIncludes handles the full pipeline: parse TOML → apply patches →
1100+
// ExpandPacks (which applies rig overrides). No separate ExpandPacks call needed.
1101+
cfg, _, err := LoadWithIncludes(fs, "/city/city.toml")
1102+
if err != nil {
1103+
t.Fatalf("LoadWithIncludes: %v", err)
1104+
}
1105+
1106+
// Find the expanded agent — verify exactly one exists (LoadWithIncludes
1107+
// already expanded packs; a duplicate would indicate double expansion).
1108+
var coder *Agent
1109+
coderCount := 0
1110+
for i := range cfg.Agents {
1111+
if cfg.Agents[i].Name == "coder" {
1112+
coder = &cfg.Agents[i]
1113+
coderCount++
1114+
}
1115+
}
1116+
if coder == nil {
1117+
t.Fatal("coder agent not found after expansion")
1118+
}
1119+
if coderCount != 1 {
1120+
t.Fatalf("expected exactly 1 coder agent, got %d (double expansion?)", coderCount)
1121+
}
1122+
1123+
// Override should have set agent.OptionDefaults.
1124+
if got := coder.OptionDefaults["model"]; got != "haiku" {
1125+
t.Errorf("after override: agent.OptionDefaults[model] = %q, want %q", got, "haiku")
1126+
}
1127+
1128+
// Resolve: no provider option_defaults, so only schema defaults + agent overrides.
1129+
rp, err := ResolveProvider(coder, &cfg.Workspace, cfg.Providers, lookPathOnly("testprov"))
1130+
if err != nil {
1131+
t.Fatalf("ResolveProvider: %v", err)
1132+
}
1133+
1134+
// Schema default "opus" overridden by agent override "haiku".
1135+
if got := rp.EffectiveDefaults["model"]; got != "haiku" {
1136+
t.Errorf("EffectiveDefaults[model] = %q, want %q", got, "haiku")
1137+
}
1138+
// Schema default "plan" overridden by agent override "unrestricted".
1139+
if got := rp.EffectiveDefaults["permission_mode"]; got != "unrestricted" {
1140+
t.Errorf("EffectiveDefaults[permission_mode] = %q, want %q", got, "unrestricted")
1141+
}
1142+
}

0 commit comments

Comments
 (0)