mirror of
https://github.com/boshu2/agentops.git
synced 2026-09-14 15:08:13 +08:00
b7d11812bd
* feat(eval): plumb DisableHooks toggle for LID baseline-A/B (W1.1)
Wire SuiteEnvironment.DisableHooks (suite-declared) and
LiveRuntimeOptions.OverrideDisableHooks (caller-toggled) through
liveEnvironmentRecord, liveRuntimeEnv, and liveRuntimePrompt via the
new effectiveDisableHooks helper. When effective:
- liveEnvironmentRecord populates the existing HooksDisabled placeholder
- liveRuntimeEnv appends AGENTOPS_HOOKS_DISABLED=1 (already honored at
hooks/session-start.sh:7)
- liveRuntimePrompt appends a "do not load skills" constraint sentence
Round-trip JSON snapshot tests for SuiteEnvironment + EnvironmentRecord
per f-2026-04-26-001. Internal helpers for the prompt/env wiring.
Issue: soc-x5yz / W1.1
Plan: .agents/plans/2026-05-02-lid-primitives-eval-integration.md
* feat(eval): --baseline-mode flag + DeltaScorecard runner (W1.2)
Add `ao eval run --baseline-mode={skill-on,skill-off,both}`. When `both`,
runs the suite twice (skill-on, then skill-off via OverrideDisableHooks)
and synthesises a DeltaScorecard with per-case pass/fail delta + aggregate
score delta. cobra ValidArgs reject invalid mode values. New `--delta-out`
writes the scorecard JSON. Per-leg run records get `-skill-on`/`-skill-off`
suffixed paths and run-ids when caller supplies --out / --run-id, so the
two leg artifacts are preserved alongside the scorecard.
Wires OverrideDisableHooks through RunOptions → environmentRecord so the
deterministic SIL path mirrors the live path's HooksDisabled provenance
(W1.1 wired the live path; this commit completes the deterministic side).
Tests: AllBaselineModes/IsValidBaselineMode, deltaSign triad, computeDelta
with mixed pass/fail/missing cases, appendBaselineSuffix path-mangling.
Issue: soc-x5yz / W1.2
Plan: .agents/plans/2026-05-02-lid-primitives-eval-integration.md
* feat(eval): wire DisableHooks into deterministic engine + LID demo fixture (W1.3)
Engine: when OverrideDisableHooks is set, mutate the local suite copy so
runtimeRecord, environmentRecord, and the case command env all see the same
effective DisableHooks state. Inject AGENTOPS_HOOKS_DISABLED=1 into the case
command env (parallels the live-runtime path from W1.1) so SIL fixtures can
gate behavior on the kill-switch.
Demo fixture: evals/agentops-core/lid-primitives-demo.json — two shell cases
that produce the smallest measurable delta proving the baseline-A/B wiring
works end-to-end. Smoke run reports +0.5 aggregate delta:
ao eval run evals/agentops-core/lid-primitives-demo.json --baseline-mode=both
→ skill-on=1.0000 (pass) skill-off=0.5000 (fail) delta=+0.5000 cases=2
Issue: soc-x5yz / W1.3
Plan: .agents/plans/2026-05-02-lid-primitives-eval-integration.md
* docs(eval): contract + code-map for LID baseline-A/B (W1.4)
- docs/contracts/eval-baseline-ab.md: --baseline-mode semantics, what
skill suppression actually disables (and what it doesn't), DeltaScorecard
schema, when to use which mode, SIL vs HIL/VIL scope (HIL/VIL deferred),
backward-compat guarantees, regression guards.
- docs/code-map/eval-lid-primitives.md: file inventory of every Wave 1
surface, test coverage map, live smoke recipe, recipe for adding new
baseline-A/B-eligible suites, open follow-ups (Wave 2 anchor namespace,
HIL/VIL extension, release-readiness gate promotion).
Hook audit confirmed only hooks/session-start.sh:7 auto-honors
AGENTOPS_HOOKS_DISABLED today; future skill-loading hooks must honor it
or future baseline-A/B measurements will under-report.
Issue: soc-x5yz / W1.4
Plan: .agents/plans/2026-05-02-lid-primitives-eval-integration.md
* fix(daemon): Wave A durability — fsync prop, orphan-tmp sweep, snapshot dir-fsync
Three audit follow-ups from .agents/research/2026-05-02-bug-daemon-audit-and-chaos.md:
- W-A-04 (soc-58q5.1): AppendLedgerEvent fsync error propagation. Audit was
STALE — code already returned wrapped Sync error at store.go:211. Added
L2 regression test TestAppendLedgerEvent_SyncErrorPropagates via a
test-only ledgerSyncFn function-variable seam. Caught by reverting line
211 and observing the new test fail.
- W-A-09 (soc-58q5.2): rotateLedger orphan-tmp cleanup. Sweeps
ledger.*.jsonl.gz.tmp files older than 5 min at rotation start (grace
prevents racing concurrent in-flight gzip work). Cleanup failures log at
warn and do not fail rotation. L2 test TestRotation_CleansOrphanedGzTmp
pre-seeds stale + fresh tmps via os.Chtimes, asserts only stale removed.
- W-A-21 (soc-58q5.3): WriteProjectionSnapshot syncDir after rename.
Adds private syncDir helper; calls it after os.Rename(tmp, dst).
syncDir failure DOES fail the function (durability contract). L2 test
TestWriteProjectionSnapshot_SyncsParentDir confirms happy-path remains
green; injection-based negative test deferred (would require interface
threading that costs more than defense-in-depth value).
go test -race -count=1 ./internal/daemon/... green (26.7s).
* fix(ci): close out PR #212 CI failures (W1.x parity + pre-mortem ref link)
- skills/pre-mortem/SKILL.md: add the missing scope-predicate-positive-
negative-cases.md to References (fixes go-build TestSkillContract_
ReferencesLinkedInSKILLMD failure that's been carrying forward from
commit 12fa13b4).
- docs/documentation-index.md: register docs/contracts/eval-baseline-ab.md
(fixes contract-compatibility-gate orphan-contract failure).
- cli/docs/COMMANDS.md: regenerate to capture --baseline-mode and
--delta-out flags added in W1.2 (fixes cli-docs-parity diff failure).
- AGENTS.md: add the hook-output-schema-lint job introduced by 12fa13b4
to the CI jobs table (fixes validate-ci-policy-parity drift).
Local: 11899 tests pass. heal-skill --strict clean. validate-ci-policy-
parity PASS.
Issue: soc-x5yz / W1 CI closure
451 lines
15 KiB
Go
451 lines
15 KiB
Go
package eval
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestRunLiveRuntimeSkipsWhenExecutableUnavailable(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name string
|
|
runtime Runtime
|
|
executable string
|
|
}{
|
|
{name: "claude", runtime: RuntimeClaude, executable: "claude"},
|
|
{name: "codex", runtime: RuntimeCodex, executable: "codex"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
suite := liveRuntimeSuite(tc.runtime)
|
|
|
|
run, err := RunLiveRuntime(context.Background(), LiveRuntimeOptions{
|
|
Suite: suite,
|
|
RunID: "live-skip",
|
|
Runtime: tc.runtime,
|
|
Enabled: true,
|
|
LookPath: func(name string) (string, error) {
|
|
if name != tc.executable {
|
|
t.Fatalf("lookPath name = %q, want %s", name, tc.executable)
|
|
}
|
|
return "", exec.ErrNotFound
|
|
},
|
|
Now: fixedEvalTime,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RunLiveRuntime returned error: %v", err)
|
|
}
|
|
|
|
wantReason := tc.executable + " executable not found: executable file not found in $PATH"
|
|
if run.Status != StatusSkipped {
|
|
t.Fatalf("status = %s, want skipped", run.Status)
|
|
}
|
|
if run.Verdict != VerdictInconclusive {
|
|
t.Fatalf("verdict = %s, want inconclusive", run.Verdict)
|
|
}
|
|
if run.Runtime.Name != tc.runtime || !run.Runtime.Live {
|
|
t.Fatalf("runtime = %+v, want live %s", run.Runtime, tc.runtime)
|
|
}
|
|
if run.Runtime.Attempts != 1 {
|
|
t.Fatalf("attempts = %d, want 1", run.Runtime.Attempts)
|
|
}
|
|
if run.Runtime.TimeoutSeconds != 45 {
|
|
t.Fatalf("timeout = %d, want 45", run.Runtime.TimeoutSeconds)
|
|
}
|
|
if run.Runtime.SkippedReason != wantReason {
|
|
t.Fatalf("skipped_reason = %q, want %q", run.Runtime.SkippedReason, wantReason)
|
|
}
|
|
if len(run.CaseResults) != 1 || run.CaseResults[0].Status != StatusSkipped {
|
|
t.Fatalf("case results = %+v, want one skipped case", run.CaseResults)
|
|
}
|
|
if run.CaseResults[0].FailureMessage != wantReason {
|
|
t.Fatalf("case failure = %q, want skip reason", run.CaseResults[0].FailureMessage)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRunLiveRuntimeDisabledSkipsBeforeExternalProbe(t *testing.T) {
|
|
run, err := RunLiveRuntime(context.Background(), LiveRuntimeOptions{
|
|
Suite: liveRuntimeSuite(RuntimeCodex),
|
|
RunID: "live-disabled",
|
|
Runtime: RuntimeCodex,
|
|
LookPath: func(name string) (string, error) {
|
|
t.Fatalf("lookPath should not be called when live runtime is disabled")
|
|
return "", nil
|
|
},
|
|
Runner: func(ctx context.Context, cmd RuntimeCommand) (RuntimeExecutionResult, error) {
|
|
t.Fatalf("runner should not be called when live runtime is disabled")
|
|
return RuntimeExecutionResult{}, nil
|
|
},
|
|
Now: fixedEvalTime,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RunLiveRuntime returned error: %v", err)
|
|
}
|
|
wantReason := "live runtime disabled; set LiveRuntimeOptions.Enabled to true"
|
|
if run.Status != StatusSkipped {
|
|
t.Fatalf("status = %s, want skipped", run.Status)
|
|
}
|
|
if run.Runtime.SkippedReason != wantReason {
|
|
t.Fatalf("skipped_reason = %q, want %q", run.Runtime.SkippedReason, wantReason)
|
|
}
|
|
if !run.Runtime.Live {
|
|
t.Fatalf("runtime live = false, want true for attempted live tier")
|
|
}
|
|
}
|
|
|
|
func TestRunLiveRuntimeIsolatesAndScrubsCodexEnvironment(t *testing.T) {
|
|
suite := liveRuntimeSuite(RuntimeCodex)
|
|
suite.Environment.ScrubEnvPrefixes = []string{"SECRET_"}
|
|
suite.Environment.IsolateHome = true
|
|
suite.Environment.IsolateCodexHome = true
|
|
suite.Environment.Network = "allowed"
|
|
suite.Environment.MaxAttempts = 2
|
|
isolationRoot := t.TempDir()
|
|
var got RuntimeCommand
|
|
|
|
run, err := RunLiveRuntime(context.Background(), LiveRuntimeOptions{
|
|
Suite: suite,
|
|
RunID: "live-codex",
|
|
Runtime: RuntimeCodex,
|
|
RuntimeCommand: "codex --profile ci",
|
|
Enabled: true,
|
|
Env: []string{
|
|
"PATH=/bin",
|
|
"SECRET_TOKEN=redacted",
|
|
"AGENTOPS_RPI_RUNTIME=direct",
|
|
"KEEP=1",
|
|
},
|
|
IsolationRoot: isolationRoot,
|
|
LookPath: func(name string) (string, error) {
|
|
if name != "codex" {
|
|
t.Fatalf("lookPath name = %q, want codex", name)
|
|
}
|
|
return "/fake/bin/codex", nil
|
|
},
|
|
VersionRunner: func(ctx context.Context, cmd RuntimeCommand) (string, error) {
|
|
if cmd.Executable != "/fake/bin/codex" {
|
|
t.Fatalf("version executable = %q, want /fake/bin/codex", cmd.Executable)
|
|
}
|
|
return "codex 0.115.0", nil
|
|
},
|
|
Runner: func(ctx context.Context, cmd RuntimeCommand) (RuntimeExecutionResult, error) {
|
|
got = cmd
|
|
return RuntimeExecutionResult{
|
|
Status: StatusInconclusive,
|
|
Verdict: VerdictInconclusive,
|
|
TranscriptPath: filepath.Join(isolationRoot, "transcript.jsonl"),
|
|
ScorecardPath: filepath.Join(isolationRoot, "scorecard.json"),
|
|
}, nil
|
|
},
|
|
Now: fixedEvalTime,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RunLiveRuntime returned error: %v", err)
|
|
}
|
|
|
|
if got.Executable != "/fake/bin/codex" {
|
|
t.Fatalf("executable = %q, want /fake/bin/codex", got.Executable)
|
|
}
|
|
if want := []string{"--profile", "ci", "exec", "Respond with ok."}; !slices.Equal(got.Args, want) {
|
|
t.Fatalf("args = %v, want %v", got.Args, want)
|
|
}
|
|
if got.TimeoutSeconds != 45 {
|
|
t.Fatalf("command timeout = %d, want 45", got.TimeoutSeconds)
|
|
}
|
|
assertEnvMissingPrefix(t, got.Env, "SECRET_")
|
|
assertEnvMissingPrefix(t, got.Env, "AGENTOPS_RPI_RUNTIME")
|
|
assertEnvContains(t, got.Env, "KEEP=1")
|
|
assertEnvContainsPrefix(t, got.Env, "HOME="+filepath.Join(isolationRoot, "home"))
|
|
assertEnvContainsPrefix(t, got.Env, "CODEX_HOME="+filepath.Join(isolationRoot, "codex-home"))
|
|
|
|
if run.Environment.NetworkAccess != NetworkEnabled {
|
|
t.Fatalf("network = %s, want enabled", run.Environment.NetworkAccess)
|
|
}
|
|
if !run.Environment.IsolatedHome || !run.Environment.IsolatedCodexHome {
|
|
t.Fatalf("environment isolation = %+v, want home and codex home isolated", run.Environment)
|
|
}
|
|
wantPrefixes := []string{"AGENTOPS_RPI_RUNTIME", "CLAUDECODE", "CLAUDE_CODE_", "SECRET_"}
|
|
if !slices.Equal(run.Environment.ScrubbedEnvPrefixes, wantPrefixes) {
|
|
t.Fatalf("scrubbed prefixes = %v, want %v", run.Environment.ScrubbedEnvPrefixes, wantPrefixes)
|
|
}
|
|
if run.Runtime.Version != "codex 0.115.0" {
|
|
t.Fatalf("version = %q, want codex 0.115.0", run.Runtime.Version)
|
|
}
|
|
if run.Runtime.Profile != "ci" {
|
|
t.Fatalf("profile = %q, want ci", run.Runtime.Profile)
|
|
}
|
|
if run.Runtime.Attempts != 1 {
|
|
t.Fatalf("attempts = %d, want 1", run.Runtime.Attempts)
|
|
}
|
|
}
|
|
|
|
func TestRunLiveRuntimeCapturesTranscriptAndScorecardArtifacts(t *testing.T) {
|
|
suite := liveRuntimeSuite(RuntimeClaude)
|
|
transcriptPath := filepath.Join(t.TempDir(), "claude-transcript.jsonl")
|
|
scorecardPath := filepath.Join(t.TempDir(), "scorecard.json")
|
|
|
|
run, err := RunLiveRuntime(context.Background(), LiveRuntimeOptions{
|
|
Suite: suite,
|
|
RunID: "live-artifacts",
|
|
Runtime: RuntimeClaude,
|
|
RuntimeCommand: "claude --model sonnet",
|
|
Enabled: true,
|
|
LookPath: func(name string) (string, error) {
|
|
if name != "claude" {
|
|
t.Fatalf("lookPath name = %q, want claude", name)
|
|
}
|
|
return "/fake/bin/claude", nil
|
|
},
|
|
VersionRunner: func(ctx context.Context, cmd RuntimeCommand) (string, error) {
|
|
return "claude 2.0.0", nil
|
|
},
|
|
Runner: func(ctx context.Context, cmd RuntimeCommand) (RuntimeExecutionResult, error) {
|
|
return RuntimeExecutionResult{
|
|
Status: StatusInconclusive,
|
|
Verdict: VerdictInconclusive,
|
|
TranscriptPath: transcriptPath,
|
|
ScorecardPath: scorecardPath,
|
|
Artifacts: []Artifact{
|
|
{Path: filepath.Join(filepath.Dir(scorecardPath), "extra.txt"), Purpose: "runtime note", Kind: "note"},
|
|
},
|
|
}, nil
|
|
},
|
|
Now: fixedEvalTime,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RunLiveRuntime returned error: %v", err)
|
|
}
|
|
|
|
if run.Runtime.Version != "claude 2.0.0" {
|
|
t.Fatalf("version = %q, want claude 2.0.0", run.Runtime.Version)
|
|
}
|
|
if run.Runtime.Model != "sonnet" {
|
|
t.Fatalf("model = %q, want sonnet", run.Runtime.Model)
|
|
}
|
|
assertArtifact(t, run.Artifacts, Artifact{Path: transcriptPath, Purpose: "runtime transcript", Kind: "transcript"})
|
|
assertArtifact(t, run.Artifacts, Artifact{Path: scorecardPath, Purpose: "runtime scorecard", Kind: "scorecard"})
|
|
assertArtifact(t, run.Artifacts, Artifact{Path: filepath.Join(filepath.Dir(scorecardPath), "extra.txt"), Purpose: "runtime note", Kind: "note"})
|
|
}
|
|
|
|
func liveRuntimeSuite(runtimeName Runtime) *Suite {
|
|
return &Suite{
|
|
SchemaVersion: 1,
|
|
ID: "live.runtime",
|
|
Name: "Live runtime",
|
|
Domain: "runtime",
|
|
Visibility: VisibilityPublicCanary,
|
|
Tier: TierLive,
|
|
Allowed: []Runtime{runtimeName},
|
|
Environment: SuiteEnvironment{
|
|
TimeoutSeconds: 45,
|
|
},
|
|
Scoring: Scoring{
|
|
AggregateThreshold: 1,
|
|
Dimensions: []ScoringDimension{
|
|
{Name: DimensionRuntimeCompatibility, Weight: 1, Threshold: 1},
|
|
},
|
|
},
|
|
BaselinePolicy: BaselinePolicy{Mode: "none"},
|
|
Cases: []Case{
|
|
{
|
|
ID: "prompt",
|
|
Title: "runtime prompt",
|
|
Kind: "runtime_prompt",
|
|
Objective: "Exercise an optional live runtime adapter.",
|
|
Runtime: runtimeName,
|
|
Inputs: map[string]any{
|
|
"prompt": "Respond with ok.",
|
|
},
|
|
Expectations: []Expectation{{Type: "manual_review"}},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func assertEnvMissingPrefix(t *testing.T, env []string, prefix string) {
|
|
t.Helper()
|
|
for _, entry := range env {
|
|
if strings.HasPrefix(entry, prefix) {
|
|
t.Fatalf("env contains scrubbed prefix %q in %q", prefix, entry)
|
|
}
|
|
}
|
|
}
|
|
|
|
func assertEnvContains(t *testing.T, env []string, value string) {
|
|
t.Helper()
|
|
for _, entry := range env {
|
|
if entry == value {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("env missing %q; got %v", value, env)
|
|
}
|
|
|
|
func assertEnvContainsPrefix(t *testing.T, env []string, prefix string) {
|
|
t.Helper()
|
|
for _, entry := range env {
|
|
if strings.HasPrefix(entry, prefix) {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("env missing prefix %q; got %v", prefix, env)
|
|
}
|
|
|
|
func assertArtifact(t *testing.T, artifacts []Artifact, want Artifact) {
|
|
t.Helper()
|
|
for _, artifact := range artifacts {
|
|
if artifact == want {
|
|
return
|
|
}
|
|
}
|
|
t.Fatalf("artifacts missing %+v; got %+v", want, artifacts)
|
|
}
|
|
|
|
func TestRunLiveRuntimePropagatesRunnerErrors(t *testing.T) {
|
|
suite := liveRuntimeSuite(RuntimeClaude)
|
|
run, err := RunLiveRuntime(context.Background(), LiveRuntimeOptions{
|
|
Suite: suite,
|
|
RunID: "live-error",
|
|
Runtime: RuntimeClaude,
|
|
Enabled: true,
|
|
LookPath: func(name string) (string, error) {
|
|
return "/fake/bin/claude", nil
|
|
},
|
|
VersionRunner: func(ctx context.Context, cmd RuntimeCommand) (string, error) {
|
|
return "", nil
|
|
},
|
|
Runner: func(ctx context.Context, cmd RuntimeCommand) (RuntimeExecutionResult, error) {
|
|
return RuntimeExecutionResult{}, errors.New("runtime failed")
|
|
},
|
|
Now: fixedEvalTime,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("RunLiveRuntime returned error: %v", err)
|
|
}
|
|
if run.Status != StatusError {
|
|
t.Fatalf("status = %s, want error", run.Status)
|
|
}
|
|
if run.CaseResults[0].FailureMessage != "runtime failed" {
|
|
t.Fatalf("failure = %q, want runtime failed", run.CaseResults[0].FailureMessage)
|
|
}
|
|
}
|
|
|
|
// W1.1 — DisableHooks plumbing. Verifies the hook-suppression toggle propagates
|
|
// through liveEnvironmentRecord, liveRuntimeEnv, and liveRuntimePrompt whether
|
|
// it comes from the suite or the LiveRuntimeOptions override.
|
|
|
|
func TestEffectiveDisableHooksOrsSuiteAndOverride(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
suiteFlag bool
|
|
overrideFlag bool
|
|
want bool
|
|
}{
|
|
{name: "neither", suiteFlag: false, overrideFlag: false, want: false},
|
|
{name: "suite only", suiteFlag: true, overrideFlag: false, want: true},
|
|
{name: "override only", suiteFlag: false, overrideFlag: true, want: true},
|
|
{name: "both", suiteFlag: true, overrideFlag: true, want: true},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
suite := Suite{Environment: SuiteEnvironment{DisableHooks: tc.suiteFlag}}
|
|
opts := LiveRuntimeOptions{OverrideDisableHooks: tc.overrideFlag}
|
|
if got := effectiveDisableHooks(opts, suite); got != tc.want {
|
|
t.Fatalf("effectiveDisableHooks: got %v, want %v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestLiveRuntimeEnvEmitsAgentopsHooksDisabled(t *testing.T) {
|
|
suite := Suite{Environment: SuiteEnvironment{DisableHooks: true}}
|
|
opts := LiveRuntimeOptions{Env: []string{}}
|
|
env, notes, err := liveRuntimeEnv(opts, suite)
|
|
if err != nil {
|
|
t.Fatalf("liveRuntimeEnv: %v", err)
|
|
}
|
|
assertEnvContains(t, env, "AGENTOPS_HOOKS_DISABLED=1")
|
|
if !slices.ContainsFunc(notes, func(n string) bool { return strings.Contains(n, "hooks disabled") }) {
|
|
t.Fatalf("notes missing hooks-disabled entry: %v", notes)
|
|
}
|
|
}
|
|
|
|
func TestLiveRuntimeEnvOmitsHooksDisabledWhenInactive(t *testing.T) {
|
|
suite := Suite{Environment: SuiteEnvironment{}}
|
|
opts := LiveRuntimeOptions{Env: []string{}}
|
|
env, _, err := liveRuntimeEnv(opts, suite)
|
|
if err != nil {
|
|
t.Fatalf("liveRuntimeEnv: %v", err)
|
|
}
|
|
for _, e := range env {
|
|
if strings.HasPrefix(e, "AGENTOPS_HOOKS_DISABLED=") {
|
|
t.Fatalf("env unexpectedly contains AGENTOPS_HOOKS_DISABLED entry: %q", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLiveRuntimeEnvHonorsOverrideDisableHooks(t *testing.T) {
|
|
suite := Suite{Environment: SuiteEnvironment{}}
|
|
opts := LiveRuntimeOptions{Env: []string{}, OverrideDisableHooks: true}
|
|
env, _, err := liveRuntimeEnv(opts, suite)
|
|
if err != nil {
|
|
t.Fatalf("liveRuntimeEnv: %v", err)
|
|
}
|
|
assertEnvContains(t, env, "AGENTOPS_HOOKS_DISABLED=1")
|
|
}
|
|
|
|
func TestLiveRuntimePromptAppendsNegationWhenDisabled(t *testing.T) {
|
|
suite := Suite{
|
|
Name: "demo",
|
|
Description: "Run something.",
|
|
Cases: []Case{{Inputs: map[string]any{"prompt": "Do X."}}},
|
|
Environment: SuiteEnvironment{DisableHooks: true},
|
|
}
|
|
prompt := liveRuntimePrompt(LiveRuntimeOptions{}, suite)
|
|
if !strings.Contains(prompt, "Do X.") {
|
|
t.Fatalf("prompt missing case prompt: %q", prompt)
|
|
}
|
|
if !strings.Contains(prompt, "Do NOT load additional skills or plugins") {
|
|
t.Fatalf("prompt missing negation constraint: %q", prompt)
|
|
}
|
|
}
|
|
|
|
func TestLiveRuntimePromptOmitsNegationWhenEnabled(t *testing.T) {
|
|
suite := Suite{
|
|
Name: "demo",
|
|
Cases: []Case{{Inputs: map[string]any{"prompt": "Do X."}}},
|
|
}
|
|
prompt := liveRuntimePrompt(LiveRuntimeOptions{}, suite)
|
|
if strings.Contains(prompt, "Do NOT load additional skills") {
|
|
t.Fatalf("prompt unexpectedly contains negation constraint: %q", prompt)
|
|
}
|
|
}
|
|
|
|
func TestLiveEnvironmentRecordReflectsEffectiveDisableHooks(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
suiteFlag bool
|
|
overrideFlag bool
|
|
want bool
|
|
}{
|
|
{name: "default false", suiteFlag: false, overrideFlag: false, want: false},
|
|
{name: "suite true", suiteFlag: true, overrideFlag: false, want: true},
|
|
{name: "override true", suiteFlag: false, overrideFlag: true, want: true},
|
|
}
|
|
for _, tc := range tests {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
suite := Suite{Environment: SuiteEnvironment{DisableHooks: tc.suiteFlag}}
|
|
opts := LiveRuntimeOptions{OverrideDisableHooks: tc.overrideFlag}
|
|
rec := liveEnvironmentRecord(opts, suite)
|
|
if rec.HooksDisabled != tc.want {
|
|
t.Fatalf("HooksDisabled: got %v, want %v", rec.HooksDisabled, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|