mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-09 18:08:31 -10:00
* perf(agent): execution profiles, --exec-profile, bench passthrough, TUI /profile (PR10b+10c) * fix(execprofile): harden profiles against review findings - fast escalates on critical-risk mutations, not high: the sandbox marks every shell command high before command analysis, so a high trigger spent the one-shot on the first go test of any task - escalation can now restore the provider-default effort a profile displaced (PostureEscalation.RestoreDefaultEffort; a plain empty target means leave untouched) - TUI /turns while a profile is active pins the budget: disarms the escalation turn target (mirroring exec's explicit --max-turns rule) and profile reverts use touched bits instead of value equality, so explicit choices survive even when they coincide with profile values - spec-draft runs get the same escalation policy as the main run instead of the displaced budget with no safety net - bench validates and canonicalizes --exec-profile before spawning: an unknown name previously produced an exit-0 report full of millisecond usage-error walls - turn summary prints exec-profile alongside mode; zero exec rejects an inline-empty --exec-profile=; balanced trace label documented and pinned as deliberate attribution * fix(execprofile): explicit effort choices disarm the escalation effort restore An explicit effort set AFTER profile selection could be cleared by a mid-run escalation (the effort analog of the /turns pin, introduced by RestoreDefaultEffort): TUI /effort and Ctrl+T now disarm the restore on a policy clone, and the spec-draft path only arms it when no explicit --spec-reasoning-effort governs the draft. * fix(execprofile): address review findings on the profile surfaces - --use-spec projects away a profile's self-correct knob instead of silently arming an option the draft runner ignores (the explicit --self-correct --use-spec combination is already a parse-time error) - RunTurnBench canonicalizes and validates ExecProfile at the library boundary, so a direct caller's casing/whitespace cannot fork report labels and an unknown name fails before anything runs - reverting a profile whose displaced budget was zero clears ZERO_MAX_TURNS (SetMaxTurnsEnv ignores zero, so sub-agents would have kept the removed profile's budget) - the TUI effort-disarm test fails instead of skipping when the catalog stops filling low effort * fix(tui): reconcile an active profile's effort fill on model switch The fill is per-model, not a session preference: fast selected on a model without low fills nothing, and switching to a model that supports it should behave like selecting the profile there; the reverse switch must not keep sending a level the destination does not support. Both switch paths (the /model command and the cross-provider picker) now re-derive the fill, leaving explicitly touched efforts alone and keeping the escalation's effort restore in step. Also: the revert-env test asserts key removal via LookupEnv, and applyProfileTurnBudget's pinned-budget parameter is named and documented as covering mode presets, not just the flag. * fix(tui): void profile effort bookkeeping when the model-switch drop erases it The /model path's pre-existing unsupported-effort drop ran before the profile reconciliation and left contradictory state behind: a dropped touched override kept blocking the profile from governing, and a dropped profile fill left a stale applied value with the effort restore still armed. The drop itself is load-bearing (the TUI run path forwards the effort to the provider unfiltered), so instead of preserving a known-unsupported level, the voided choice now resets the profile's effort bookkeeping and the reconcile re-derives from scratch. The whole sequence lives in one ring-parameterized helper driven by the switch target's authoritative ring, unit-tested against synthetic rings for both the void-then-refill arc and the supported-override survival. * fix(tui): only drop an explicit effort when the target ring is authoritative A nil ring meant both a catalog model with no effort controls and a live-discovered custom model the catalog cannot vouch for. The drop now requires an authoritative ring (a resolved catalog entry, whose ring may legitimately be empty); an unknown ring preserves an explicit preference, matching the cross-provider picker's behavior, while the profile's own fill still applies only where support is known.
132 lines
4.8 KiB
Go
132 lines
4.8 KiB
Go
package execprofile
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"github.com/Gitlawb/zero/internal/sandbox"
|
|
)
|
|
|
|
// The no-regression invariant of the whole feature: balanced must be the empty
|
|
// profile (nothing but a name), so selecting it cannot change a run.
|
|
func TestBalancedProfileIsEmpty(t *testing.T) {
|
|
profile, ok := Lookup("balanced")
|
|
if !ok {
|
|
t.Fatal("balanced must exist in the catalog")
|
|
}
|
|
if profile.Name != "balanced" {
|
|
t.Fatalf("Name = %q, want %q", profile.Name, "balanced")
|
|
}
|
|
profile.Name = ""
|
|
if !reflect.DeepEqual(profile, Profile{}) {
|
|
t.Fatalf("balanced must be zero-valued apart from its name, got %+v", profile)
|
|
}
|
|
if policy := Balanced.Policy(80, false); policy != nil {
|
|
t.Fatalf("balanced Policy must be nil (byte-identical loop), got %+v", policy)
|
|
}
|
|
}
|
|
|
|
func TestLookupIsCaseAndSpaceInsensitive(t *testing.T) {
|
|
for _, name := range []string{"fast", "Fast", " FAST "} {
|
|
if _, ok := Lookup(name); !ok {
|
|
t.Fatalf("Lookup(%q) should resolve", name)
|
|
}
|
|
}
|
|
if _, ok := Lookup("turbo"); ok {
|
|
t.Fatal("Lookup(turbo) should not resolve")
|
|
}
|
|
}
|
|
|
|
func TestNamesAreSorted(t *testing.T) {
|
|
want := []string{"balanced", "fast", "thorough"}
|
|
if got := Names(); !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("Names() = %v, want %v", got, want)
|
|
}
|
|
}
|
|
|
|
// Escalation targets must be the displaced values only — the catalog carries
|
|
// triggers, the call site supplies what was displaced, and nothing else may
|
|
// appear in the escalation.
|
|
func TestFastPolicyTargetsDisplacedValuesOnly(t *testing.T) {
|
|
policy := Fast.Policy(80, true)
|
|
if policy == nil {
|
|
t.Fatal("fast must arm an escalation policy")
|
|
}
|
|
if policy.Name != "fast" {
|
|
t.Fatalf("policy.Name = %q, want fast", policy.Name)
|
|
}
|
|
esc := policy.Escalate
|
|
if esc == nil {
|
|
t.Fatal("fast policy must carry an escalation")
|
|
}
|
|
if esc.MaxTurns != 80 {
|
|
t.Fatalf("Escalate.MaxTurns = %d, want the displaced 80", esc.MaxTurns)
|
|
}
|
|
if esc.ReasoningEffort != "" {
|
|
t.Fatalf("Escalate.ReasoningEffort = %q, want empty (displaced effort is the provider default by construction)", esc.ReasoningEffort)
|
|
}
|
|
if !esc.RestoreDefaultEffort {
|
|
t.Fatal("effort was profile-filled, so escalation must restore the provider default")
|
|
}
|
|
if esc.RestoreCompletionGate {
|
|
t.Fatal("fast does not touch the completion gate, so escalation must not either")
|
|
}
|
|
if esc.OnToolFailureStreak != Fast.EscalateOnToolFailureStreak ||
|
|
esc.OnCompletionUncertain != Fast.EscalateOnCompletionUncertain ||
|
|
esc.OnSelfCorrectFailure != Fast.EscalateOnSelfCorrectFailure ||
|
|
esc.OnRiskyMutation != Fast.EscalateOnRiskyMutation {
|
|
t.Fatalf("escalation triggers must mirror the catalog, got %+v", esc)
|
|
}
|
|
}
|
|
|
|
// The effort restore arms only when the profile actually filled the effort:
|
|
// with an explicit user effort the profile backed off, so escalation must not
|
|
// clear the user's choice.
|
|
func TestFastPolicyExplicitEffortIsNeverRestored(t *testing.T) {
|
|
policy := Fast.Policy(80, false)
|
|
if policy == nil || policy.Escalate == nil {
|
|
t.Fatal("fast must still arm its triggers")
|
|
}
|
|
if policy.Escalate.RestoreDefaultEffort {
|
|
t.Fatal("the profile did not fill the effort, so escalation must leave it alone")
|
|
}
|
|
}
|
|
|
|
// A profile that did not displace the budget (explicit --max-turns pinned it)
|
|
// must not let escalation move the ceiling at all.
|
|
func TestFastPolicyZeroDisplacedLeavesCeilingUntouched(t *testing.T) {
|
|
policy := Fast.Policy(0, false)
|
|
if policy == nil || policy.Escalate == nil {
|
|
t.Fatal("fast must still arm its triggers with a zero displaced budget")
|
|
}
|
|
if policy.Escalate.MaxTurns != 0 {
|
|
t.Fatalf("Escalate.MaxTurns = %d, want 0 (no displaced value to restore)", policy.Escalate.MaxTurns)
|
|
}
|
|
}
|
|
|
|
// Thorough is already the maximum posture: no triggers, so no policy — the
|
|
// loop must stay byte-identical to the same knobs set by hand.
|
|
func TestThoroughPolicyIsNil(t *testing.T) {
|
|
if policy := Thorough.Policy(80, true); policy != nil {
|
|
t.Fatalf("thorough Policy must be nil, got %+v", policy)
|
|
}
|
|
}
|
|
|
|
// Pin the catalog's provisional values so a retune is a deliberate,
|
|
// test-visible diff (these floors came from the Phase 0 baseline's read-class
|
|
// evidence and are expected to move after the post-oracle-fix re-capture).
|
|
func TestCatalogProvisionalValues(t *testing.T) {
|
|
if Fast.MaxTurns != 30 || Fast.ReasoningEffort != "low" || Fast.SelfCorrect {
|
|
t.Fatalf("fast knobs changed: %+v", Fast)
|
|
}
|
|
// Critical, not High: the sandbox classifies every shell command as at
|
|
// least high risk, so a High trigger would spend the one-shot escalation
|
|
// on the first benign `go test` of any coding task.
|
|
if Fast.EscalateOnRiskyMutation != sandbox.RiskCritical {
|
|
t.Fatalf("fast risky-mutation trigger = %q, want %q", Fast.EscalateOnRiskyMutation, sandbox.RiskCritical)
|
|
}
|
|
if Thorough.MaxTurns != 160 || Thorough.ReasoningEffort != "high" || !Thorough.SelfCorrect {
|
|
t.Fatalf("thorough knobs changed: %+v", Thorough)
|
|
}
|
|
}
|