mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-12 08:41:32 -10:00
* modelregistry: add UpgradeTargetID and Registry.UpgradeTarget escalation lookup * modelregistry: seed escalation chains in catalog decoration * modelregistry: verification gate for escalation upgrade map * agent: add ModelSwitcher option and ToolResult.RequestedModel field * agent: lift escalate_to_model meta into ToolResult.RequestedModel * agent: perform at-most-one mid-run model switch in the turn loop * tools: add SideEffectNone for control-only tools * tools: add escalate_model control tool (metadata + args) * cli: parse --allow-escalation exec flag * cli: document --allow-escalation in exec help * cli: register escalate_model tool under --allow-escalation * cli: wire model switcher for exec escalation * cli: guard byte-identical exec run without escalation flag * cli: attribute post-switch usage to the escalated model * tools: accept an explicit empty escalate_model reason reason is an optional, informational-only arg, but the previous arg parsing rejected an explicit reason:"" (non-empty-string check), failing the whole escalation. Switch to stringArgWithEmpty so an absent OR empty reason is accepted while a non-string value is still rejected. Add a test covering both the empty and absent forms. * agent: test (nil,nil) escalation switcher keeps original provider When a ModelSwitcher returns (nil, nil) — no error, no replacement — the loop's `else if newProvider != nil` guard already keeps the active provider, so the run continues on the original model. Add an agent-layer test that proves this (it panics if the nil guard is dropped), closing the gap the adversarial review flagged. Also note the deferred compaction limitation near the switch: the compactor's context-window budget is fixed at run start and not updated on a mid-run switch (needs a ModelSwitcher contract change to fix). * cli: gate usage model attribution on the escalation flag and harden tests FIX A (back-compat): the OnUsage closure wrote a "model" key into every persisted EventUsage payload unconditionally, diverging a non-escalation run from the byte-identical origin/main baseline. The model can only change mid-run under --allow-escalation, so include the "model" key only then. A new flag-OFF test asserts the persisted usage payload carries no "model" key (mutation-proven). Test hardening for the CLI escalation boundary: - Wire test: each newProvider build returns a DISTINCT provider with its own turn counter; assert the source provider served exactly the escalation turn and the escalated provider served exactly the post-switch answer. Fails if `provider = newProvider` is dropped. - Mixed usage attribution: emit usage pre- and post-switch and assert the first attributes to the original model, the second to the escalated target (the loop's ordering guarantee). - Switch-error path: a failed rebuild is non-fatal and post-error usage stays attributed to the original model (currentModel unchanged). - Top-tier decline end-to-end: a flag-on run that escalates while already top-tier performs no switch (newProvider built exactly once) and succeeds. * modelregistry: validate UpgradeTargetID resolves at registry construction * cli: update currentModel only when the switched provider is non-nil * sandbox: make 'none' a first-class side effect (control-only tools) --------- Co-authored-by: Gnanam <[email protected]>
43 lines
1.6 KiB
Go
43 lines
1.6 KiB
Go
package sandbox
|
|
|
|
import "testing"
|
|
|
|
func TestAutonomyRankOrdering(t *testing.T) {
|
|
if !(autonomyRank[AutonomyLow] < autonomyRank[AutonomyMedium] && autonomyRank[AutonomyMedium] < autonomyRank[AutonomyHigh]) {
|
|
t.Fatalf("autonomyRank not strictly ordered: %v", autonomyRank)
|
|
}
|
|
}
|
|
|
|
func TestAutonomyAllowedNormalizesBothOperands(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
requested Autonomy
|
|
max Autonomy
|
|
want bool
|
|
}{
|
|
{"low under high", AutonomyLow, AutonomyHigh, true},
|
|
{"high under medium ceiling", AutonomyHigh, AutonomyMedium, false},
|
|
{"equal medium", AutonomyMedium, AutonomyMedium, true},
|
|
{"uppercase requested still ranked", Autonomy("HIGH"), AutonomyMedium, false},
|
|
{"unknown requested fails closed against high ceiling", Autonomy("bogus"), AutonomyHigh, false},
|
|
{"unknown ceiling fails closed against high request", AutonomyHigh, Autonomy("bogus"), false},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
if got := autonomyAllowed(tc.requested, tc.max); got != tc.want {
|
|
t.Fatalf("autonomyAllowed(%q, %q) = %v, want %v", tc.requested, tc.max, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeSideEffectPreservesNone(t *testing.T) {
|
|
if got := NormalizeSideEffect(SideEffectNone); got != SideEffectNone {
|
|
t.Fatalf("NormalizeSideEffect(none) = %q, want none (must not collapse to out_of_workspace)", got)
|
|
}
|
|
// An unrecognized value still fails closed to out_of_workspace.
|
|
if got := NormalizeSideEffect(SideEffect("bogus")); got != SideEffectOutOfWorkspace {
|
|
t.Fatalf("NormalizeSideEffect(bogus) = %q, want out_of_workspace", got)
|
|
}
|
|
}
|