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.
225 lines
8.5 KiB
Go
225 lines
8.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/Gitlawb/zero/internal/execprofile"
|
|
)
|
|
|
|
func TestApplyExecProfileFillsOnlyUnset(t *testing.T) {
|
|
// Unset effort: the profile fills it and reports the fill, so the
|
|
// escalation policy knows the displaced effort was the provider default.
|
|
options := execOptions{execProfile: "fast"}
|
|
profile, effortFilled, err := applyExecProfile(&options)
|
|
if err != nil {
|
|
t.Fatalf("applyExecProfile: %v", err)
|
|
}
|
|
if profile.Name != "fast" {
|
|
t.Fatalf("profile.Name = %q, want fast", profile.Name)
|
|
}
|
|
if options.reasoningEffort != "low" {
|
|
t.Fatalf("reasoningEffort = %q, want the profile's low", options.reasoningEffort)
|
|
}
|
|
if !effortFilled {
|
|
t.Fatal("the profile filled the effort, so effortFilled must report it")
|
|
}
|
|
|
|
// Set effort (explicit flag or a mode's fill): the profile backs off.
|
|
options = execOptions{execProfile: "fast", reasoningEffort: "high"}
|
|
_, effortFilled, err = applyExecProfile(&options)
|
|
if err != nil {
|
|
t.Fatalf("applyExecProfile: %v", err)
|
|
}
|
|
if options.reasoningEffort != "high" {
|
|
t.Fatalf("reasoningEffort = %q, an explicit value must win over the profile", options.reasoningEffort)
|
|
}
|
|
if effortFilled {
|
|
t.Fatal("the profile backed off, so effortFilled must be false")
|
|
}
|
|
}
|
|
|
|
func TestApplyExecProfileThoroughArmsSelfCorrect(t *testing.T) {
|
|
options := execOptions{execProfile: "thorough"}
|
|
if _, _, err := applyExecProfile(&options); err != nil {
|
|
t.Fatalf("applyExecProfile: %v", err)
|
|
}
|
|
if !options.selfCorrect {
|
|
t.Fatal("thorough must arm the post-edit self-correct loop")
|
|
}
|
|
if options.reasoningEffort != "high" {
|
|
t.Fatalf("reasoningEffort = %q, want high", options.reasoningEffort)
|
|
}
|
|
}
|
|
|
|
// The spec-draft path wires no self-corrector (explicit --self-correct
|
|
// --use-spec is rejected at parse time, before profiles apply), so a profile's
|
|
// self-correct knob is projected away under --use-spec instead of silently
|
|
// arming a knob the draft runner ignores. The other knobs still apply.
|
|
func TestApplyExecProfileSpecProjectsAwaySelfCorrect(t *testing.T) {
|
|
options := execOptions{execProfile: "thorough", useSpec: true}
|
|
if _, _, err := applyExecProfile(&options); err != nil {
|
|
t.Fatalf("applyExecProfile: %v", err)
|
|
}
|
|
if options.selfCorrect {
|
|
t.Fatal("thorough under --use-spec must not arm self-correct (the draft runner has none)")
|
|
}
|
|
if options.reasoningEffort != "high" {
|
|
t.Fatalf("reasoningEffort = %q, the profile's other knobs must still apply", options.reasoningEffort)
|
|
}
|
|
}
|
|
|
|
func TestApplyExecProfileUnknownIsUsageError(t *testing.T) {
|
|
options := execOptions{execProfile: "turbo"}
|
|
_, _, err := applyExecProfile(&options)
|
|
if err == nil {
|
|
t.Fatal("expected a usage error for an unknown profile")
|
|
}
|
|
if _, ok := err.(execUsageError); !ok {
|
|
t.Fatalf("expected execUsageError, got %T: %v", err, err)
|
|
}
|
|
for _, name := range execprofile.Names() {
|
|
if !strings.Contains(err.Error(), name) {
|
|
t.Fatalf("usage error must list %q, got %q", name, err.Error())
|
|
}
|
|
}
|
|
}
|
|
|
|
// Precedence: explicit flag > --mode > --exec-profile. The deep mode fills
|
|
// effort=high and max-turns=160; the fast profile must not override either,
|
|
// because a mode-filled field counts as set by the time the profile runs.
|
|
func TestExecProfilePrecedenceFlagOverModeOverProfile(t *testing.T) {
|
|
options := execOptions{mode: "deep", execProfile: "fast"}
|
|
if err := applyExecMode(&options); err != nil {
|
|
t.Fatalf("applyExecMode: %v", err)
|
|
}
|
|
profile, _, err := applyExecProfile(&options)
|
|
if err != nil {
|
|
t.Fatalf("applyExecProfile: %v", err)
|
|
}
|
|
if options.reasoningEffort != "high" {
|
|
t.Fatalf("reasoningEffort = %q, the mode's high must win over the profile's low", options.reasoningEffort)
|
|
}
|
|
// The mode filled options.maxTurns, so the turn budget must not be displaced.
|
|
effective, displaced := applyProfileTurnBudget(profile, options.maxTurns, options.maxTurns)
|
|
if effective != 160 || displaced != 0 {
|
|
t.Fatalf("turn budget = (%d, displaced %d), the mode's 160 must win with nothing displaced", effective, displaced)
|
|
}
|
|
}
|
|
|
|
func TestApplyProfileTurnBudget(t *testing.T) {
|
|
fast, _ := execprofile.Lookup("fast")
|
|
balanced, _ := execprofile.Lookup("balanced")
|
|
|
|
// No explicit flag: the profile displaces the resolved budget.
|
|
if effective, displaced := applyProfileTurnBudget(fast, 0, 80); effective != 30 || displaced != 80 {
|
|
t.Fatalf("fast over resolved 80 = (%d, %d), want (30, 80)", effective, displaced)
|
|
}
|
|
// Explicit --max-turns pins the budget; the profile backs off entirely.
|
|
if effective, displaced := applyProfileTurnBudget(fast, 50, 50); effective != 50 || displaced != 0 {
|
|
t.Fatalf("fast with explicit 50 = (%d, %d), want (50, 0)", effective, displaced)
|
|
}
|
|
// Balanced never displaces anything.
|
|
if effective, displaced := applyProfileTurnBudget(balanced, 0, 80); effective != 80 || displaced != 0 {
|
|
t.Fatalf("balanced over resolved 80 = (%d, %d), want (80, 0)", effective, displaced)
|
|
}
|
|
}
|
|
|
|
// The no-regression invariant at the options level: selecting balanced leaves
|
|
// the options byte-identical to not selecting a profile at all, and produces
|
|
// no loop policy.
|
|
func TestExecProfileBalancedLeavesOptionsUntouched(t *testing.T) {
|
|
options := execOptions{execProfile: "balanced"}
|
|
reference := options
|
|
profile, _, err := applyExecProfile(&options)
|
|
if err != nil {
|
|
t.Fatalf("applyExecProfile: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(options, reference) {
|
|
t.Fatalf("balanced changed the options: %+v vs %+v", options, reference)
|
|
}
|
|
if policy := profile.Policy(80, false); policy != nil {
|
|
t.Fatalf("balanced must produce a nil policy, got %+v", policy)
|
|
}
|
|
}
|
|
|
|
// --exec-profile= with an inline empty value must error like its two-token
|
|
// form and the sibling --mode=, not silently run the default posture (the
|
|
// classic shell hazard: an unset variable expanding into the inline form).
|
|
func TestParseExecProfileInlineEmptyIsError(t *testing.T) {
|
|
_, _, err := parseExecArgs([]string{"--exec-profile=", "hello"})
|
|
if err == nil || !strings.Contains(err.Error(), "--exec-profile requires a value") {
|
|
t.Fatalf("expected a required-value error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// Full echo-provider run: the selected profile must be stamped into the
|
|
// per-turn trace so benchmark attribution can group runs by posture.
|
|
func TestRunExecTraceRecordsSelectedProfile(t *testing.T) {
|
|
tracePath := filepath.Join(t.TempDir(), "trace.ndjson")
|
|
exitCode, _, stderr := runExecWithEcho(t, []string{
|
|
"exec", "--exec-profile", "fast", "--trace", tracePath, "hello",
|
|
})
|
|
if exitCode != exitSuccess {
|
|
t.Fatalf("expected exit %d, got %d: %s", exitSuccess, exitCode, stderr)
|
|
}
|
|
raw, err := os.ReadFile(tracePath)
|
|
if err != nil {
|
|
t.Fatalf("read trace: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"profile":"fast"`) {
|
|
t.Fatalf("trace must record the selected profile, got %s", raw)
|
|
}
|
|
}
|
|
|
|
// An explicit --spec-reasoning-effort governs the spec draft's effort, so the
|
|
// escalation effort-restore must not arm there even when the profile filled
|
|
// the (unused) main effort.
|
|
func TestSpecProfileEffortFilled(t *testing.T) {
|
|
if !specProfileEffortFilled(true, "") {
|
|
t.Fatal("no explicit spec effort: the profile's fill governs, restore must arm")
|
|
}
|
|
if specProfileEffortFilled(true, "high") {
|
|
t.Fatal("an explicit spec effort must disarm the effort restore")
|
|
}
|
|
if specProfileEffortFilled(false, "") {
|
|
t.Fatal("nothing filled, nothing to restore")
|
|
}
|
|
}
|
|
|
|
// The trace label deliberately records the SELECTED profile, balanced
|
|
// included, so captures are self-describing. Run behavior stays identical
|
|
// (balanced fills nothing and arms no policy); the label is the one intended
|
|
// difference in the opt-in trace artifact.
|
|
func TestRunExecTraceRecordsBalancedSelection(t *testing.T) {
|
|
tracePath := filepath.Join(t.TempDir(), "trace.ndjson")
|
|
exitCode, _, stderr := runExecWithEcho(t, []string{
|
|
"exec", "--exec-profile", "balanced", "--trace", tracePath, "hello",
|
|
})
|
|
if exitCode != exitSuccess {
|
|
t.Fatalf("expected exit %d, got %d: %s", exitSuccess, exitCode, stderr)
|
|
}
|
|
raw, err := os.ReadFile(tracePath)
|
|
if err != nil {
|
|
t.Fatalf("read trace: %v", err)
|
|
}
|
|
if !strings.Contains(string(raw), `"profile":"balanced"`) {
|
|
t.Fatalf("trace must record the balanced selection, got %s", raw)
|
|
}
|
|
}
|
|
|
|
func TestRunExecUnknownExecProfileIsUsageError(t *testing.T) {
|
|
exitCode, _, stderr := runExecWithEcho(t, []string{
|
|
"exec", "--exec-profile", "turbo", "hello",
|
|
})
|
|
if exitCode != exitUsage {
|
|
t.Fatalf("expected usage exit %d, got %d", exitUsage, exitCode)
|
|
}
|
|
if !strings.Contains(stderr, "unknown execution profile") {
|
|
t.Fatalf("expected an unknown-profile usage error, got %q", stderr)
|
|
}
|
|
}
|