Files
2026-07-13 13:00:08 +08:00

1077 lines
36 KiB
Go

package memorycompiler
import (
"context"
"encoding/json"
"os"
"path/filepath"
"runtime"
"strings"
"testing"
"time"
)
func TestStartTurnEmptyStateDoesNotInjectIR(t *testing.T) {
rt := New(t.TempDir())
ctx, turn := rt.StartTurn(context.Background(), "fix a bug", nil)
if ctx != "" {
t.Fatalf("empty compiler state injected context:\n%s", ctx)
}
if turn == nil {
t.Fatal("StartTurn returned nil turn")
}
turn.Finish(nil)
}
func TestFailureTraceCreatesConstraintIR(t *testing.T) {
dir := t.TempDir()
rt := New(dir)
_, turn := rt.StartTurn(context.Background(), "fix a bug", nil)
turn.RecordToolResults([]ToolRecord{
{Name: "bash", Error: "exit status 1"},
{Name: "bash", Error: "exit status 1"},
})
turn.Finish(nil)
ctx, _ := rt.StartTurn(context.Background(), "continue", nil)
if !strings.Contains(ctx, "<memory-compiler-execution>") {
t.Fatalf("expected learned IR context, got:\n%s", ctx)
}
if !strings.Contains(ctx, "memory_v5_execution_contract") {
t.Fatalf("expected compiled execution contract, got:\n%s", ctx)
}
if !strings.Contains(ctx, `"source_event":"continue"`) {
t.Fatalf("expected source event to be compiled into the contract, got:\n%s", ctx)
}
if !strings.Contains(ctx, "avoid repeating bash") {
t.Fatalf("expected repeated-error constraint, got:\n%s", ctx)
}
if !strings.Contains(ctx, "bugfix-reproduce-first") {
t.Fatalf("expected matching strategy in IR, got:\n%s", ctx)
}
st := readState(t, dir)
if len(st.Learnings) != 1 {
t.Fatalf("learnings = %d, want 1", len(st.Learnings))
}
if len(st.Mutations) == 0 {
t.Fatal("expected structured compiler mutations")
}
if st.Mutations[0].Status != "testing" || !st.Mutations[0].Applied {
t.Fatalf("mutation = %+v, want applied testing mutation", st.Mutations[0])
}
if len(st.NoisyRefs) == 0 {
t.Fatal("expected noisy memory pattern to be tracked")
}
if st.ExecutionState.CurrentPhase != "needs_followup" {
t.Fatalf("phase = %q, want needs_followup", st.ExecutionState.CurrentPhase)
}
if len(st.ExecutionState.ActiveConstraints) == 0 {
t.Fatal("expected active constraints from learning")
}
assertNode(t, st.Nodes, func(n MemoryNode) bool {
return n.Type == "tool_result" && n.TruthLocked && n.Constraint != nil
}, "truth-locked failed tool result")
assertNode(t, st.Nodes, func(n MemoryNode) bool {
return n.Quality == QualityCorrupted && strings.HasPrefix(n.ID, "noise:")
}, "corrupted noise node")
assertEdge(t, st.Edges, "contradicts")
traces := readTraces(t, dir)
last := traces[len(traces)-1]
if len(last.CausalEdges) != 0 || len(last.ToolResults) != 0 || len(last.DecisionBranches) != 0 {
t.Fatalf("execution trace should stay minimal, got %+v", last)
}
if last.Cost.ToolCalls != 2 || last.Cost.ToolErrors != 2 {
t.Fatalf("cost metrics = %+v, want two tool calls and errors", last.Cost)
}
learningTraces := readLearningTraces(t, dir)
learningLast := learningTraces[len(learningTraces)-1]
if len(learningLast.CausalEdges) == 0 {
t.Fatalf("expected causal learning trace edges, got %+v", learningLast)
}
}
func TestStartTurnExposesMemoryCitations(t *testing.T) {
dir := t.TempDir()
rt := New(dir)
_, seed := rt.StartTurn(context.Background(), "fix a bug", nil)
seed.RecordToolResults([]ToolRecord{
{Name: "bash", Error: "exit status 1"},
{Name: "bash", Error: "exit status 1"},
})
seed.Finish(nil)
_, turn := rt.StartTurn(context.Background(), "continue", nil)
citations := turn.MemoryCitations()
if len(citations) == 0 {
t.Fatal("expected memory citations for learned compiler state")
}
found := false
for _, c := range citations {
if c.Source != "Memory v5" {
t.Fatalf("citation source = %q, want Memory v5", c.Source)
}
if c.Kind != "compiler_reference" && c.Kind != "constraint" && c.Kind != "risk_note" {
t.Fatalf("citation kind should expose compiler observability semantics: %+v", c)
}
if strings.Contains(c.Note, "avoid repeating bash") {
found = true
}
}
if !found {
t.Fatalf("expected repeated bash learning in citations: %+v", citations)
}
citations[0].Note = "mutated"
if turn.MemoryCitations()[0].Note == "mutated" {
t.Fatal("MemoryCitations returned mutable backing slice")
}
}
func TestMemoryCitationsHideEvidenceReferences(t *testing.T) {
citations := memoryCitationsForIR(PlannerIR{
Version: version,
Goal: "fix a bug",
SourceEvent: "fix a bug",
MemoryReferences: []MemoryRef{
{ID: "tool-1", Content: "bash succeeded", Quality: string(QualityHighSignal), Influence: "evidence"},
{ID: "mem-1", Content: "prefer focused tests", Quality: string(QualityHighSignal), Influence: "reference"},
},
Constraints: []Constraint{{Type: "avoid", Text: "repeat failed command", Source: "constraint-1"}},
RiskNotes: []string{"verify before final answer"},
})
if len(citations) == 0 {
t.Fatal("expected non-evidence compiler citations to remain visible")
}
for _, c := range citations {
if strings.Contains(c.Note, "bash succeeded") || strings.Contains(c.Note, "evidence:") {
t.Fatalf("tool-result evidence citation leaked into user-facing citations: %+v", c)
}
}
foundReference := false
foundConstraint := false
for _, c := range citations {
if strings.Contains(c.Note, "reference: prefer focused tests") {
foundReference = true
}
if strings.Contains(c.Note, "avoid: repeat failed command") {
foundConstraint = true
}
}
if !foundReference || !foundConstraint {
t.Fatalf("expected reference and constraint citations to remain, got %+v", citations)
}
}
func TestStrategyPreconditionShortTokenMatchesOnWordBoundary(t *testing.T) {
// "ui" must match the real frontend keyword but not arbitrary substrings —
// otherwise the synthetic "Continue pursuing the active goal." turn (and any
// goal/build/quiet text) misroutes to frontend-visual-verify (#5342).
matches := []string{"fix the ui", "UI layout broken", "tweak the ui.", "ui/ux pass"}
for _, g := range matches {
if !strategyPreconditionMatches(g, "ui") {
t.Errorf("strategyPreconditionMatches(%q, \"ui\") = false, want true", g)
}
}
nonMatches := []string{"Continue pursuing the active goal.", "build the parser", "quiet mode", "guidance notes"}
for _, g := range nonMatches {
if strategyPreconditionMatches(g, "ui") {
t.Errorf("strategyPreconditionMatches(%q, \"ui\") = true, want false (substring false positive)", g)
}
}
// Longer preconditions keep substring semantics.
if !strategyPreconditionMatches("optimize the frontend pipeline", "frontend") {
t.Errorf("long precondition lost substring match")
}
}
func TestClassifyStrategyDoesNotRouteGoalContinuationToFrontend(t *testing.T) {
goal := "Continue pursuing the active goal. If it is complete, end with [goal:complete]."
if got := classifyStrategy(goal); got == "frontend-visual-verify" {
t.Fatalf("goal-continuation classified as %q (the \"ui\" in \"pursuing\" leaked)", got)
}
if got := classifyStrategy("redesign the ui layout"); got != "frontend-visual-verify" {
t.Fatalf("genuine ui goal classified as %q, want frontend-visual-verify", got)
}
}
func TestSuccessTraceFeedsReusableStrategyAndGraph(t *testing.T) {
dir := t.TempDir()
rt := New(dir)
_, turn := rt.StartTurn(context.Background(), "frontend ui setting", nil)
turn.RecordToolResults([]ToolRecord{{Name: "go test", Output: "ok"}})
turn.Finish(nil)
ctx, _ := rt.StartTurn(context.Background(), "frontend ui setting", nil)
if !strings.Contains(ctx, "frontend-visual-verify") {
t.Fatalf("expected learned frontend strategy, got:\n%s", ctx)
}
if !strings.Contains(ctx, "go test succeeded") {
t.Fatalf("expected tool-result memory reference, got:\n%s", ctx)
}
if !strings.Contains(ctx, "verify-ui") {
t.Fatalf("expected strategy execution plan, got:\n%s", ctx)
}
st := readState(t, dir)
assertNode(t, st.Nodes, func(n MemoryNode) bool {
return n.Type == "decision" && strings.Contains(n.Content, "frontend-visual-verify")
}, "decision node for selected strategy")
assertEdge(t, st.Edges, "derived_from")
}
func TestGraphTraversalFiltersCorruptedMemoryAndExpandsConnectedNodes(t *testing.T) {
now := time.Now().UTC()
st := state{
Nodes: []MemoryNode{
{
ID: "seed",
Type: "tool_result",
Content: "validated source result",
Timestamp: now,
Confidence: 0.9,
Quality: QualityHighSignal,
},
{
ID: "connected",
Type: "fact",
Content: "connected supporting constraint",
Timestamp: now,
Confidence: 0.8,
Quality: QualityMediumSignal,
Constraint: &Constraint{Type: "must_use", Text: "use connected graph evidence", Source: "connected"},
},
{
ID: "corrupted",
Type: "fact",
Content: "must never appear",
Timestamp: now,
Confidence: 1,
Quality: QualityCorrupted,
Constraint: &Constraint{Type: "must_use", Text: "bad constraint", Source: "corrupted"},
},
},
Edges: []MemoryEdge{{From: "seed", To: "connected", Relation: "supports"}},
}
ir := buildIR("fix a bug", "fix a bug", st)
got, err := json.Marshal(ir)
if err != nil {
t.Fatal(err)
}
text := string(got)
if !strings.Contains(text, "use connected graph evidence") {
t.Fatalf("expected connected graph constraint, got:\n%s", text)
}
if strings.Contains(text, "must never appear") || strings.Contains(text, "bad constraint") {
t.Fatalf("corrupted memory leaked into IR:\n%s", text)
}
}
func TestMutationEvaluationLoopAcceptsAppliedMutation(t *testing.T) {
dir := t.TempDir()
rt := New(dir)
_, turn := rt.StartTurn(context.Background(), "fix a bug", nil)
turn.RecordToolResults([]ToolRecord{
{Name: "bash", Error: "exit status 1"},
{Name: "bash", Error: "exit status 1"},
})
turn.Finish(nil)
ctx, turn := rt.StartTurn(context.Background(), "fix a bug", nil)
if !strings.Contains(ctx, "avoid repeating bash") {
t.Fatalf("expected mutation to affect next compiled contract, got:\n%s", ctx)
}
turn.RecordToolResults([]ToolRecord{{Name: "go test", Output: "ok"}})
turn.Finish(nil)
_, turn = rt.StartTurn(context.Background(), "fix a bug", nil)
turn.RecordToolResults([]ToolRecord{{Name: "go test", Output: "ok"}})
turn.Finish(nil)
st := readState(t, dir)
found := false
for _, m := range st.Mutations {
if strings.Contains(m.Reason, "avoid repeating bash") {
found = true
if m.Status != "accepted" {
t.Fatalf("mutation status = %q, want accepted: %+v", m.Status, m)
}
if len(m.EvaluationTraceIDs) == 0 {
t.Fatalf("mutation missing evaluation trace: %+v", m)
}
if len(m.EvaluationTraceIDs) < mutationMinEvalTrials {
t.Fatalf("mutation accepted before minimum validation trials: %+v", m)
}
}
}
if !found {
t.Fatalf("missing repeated bash mutation: %+v", st.Mutations)
}
traces := readTraces(t, dir)
last := traces[len(traces)-1]
if len(last.MutationEvaluations) != 0 {
t.Fatalf("execution trace should not carry mutation telemetry: %+v", last)
}
learningTraces := readLearningTraces(t, dir)
learningLast := learningTraces[len(learningTraces)-1]
if len(learningLast.MutationEvaluations) == 0 {
t.Fatalf("expected mutation evaluation in learning trace: %+v", learningLast)
}
}
func TestRecoveredToolFailureCountsAsSuccessfulOutcome(t *testing.T) {
dir := t.TempDir()
rt := New(dir)
_, turn := rt.StartTurn(context.Background(), "fix a bug", nil)
turn.RecordToolResults([]ToolRecord{
{Name: "go test", Error: "failed before fix"},
{Name: "go test", Output: "ok"},
})
turn.Finish(nil)
traces := readTraces(t, dir)
if got := traces[len(traces)-1].Outcome; got != "success" {
t.Fatalf("outcome = %q, want success for recovered failure", got)
}
st := readState(t, dir)
for _, s := range st.Strategies {
if s.ID == "bugfix-reproduce-first" {
if s.Successes != 1 || s.Failures != 0 {
t.Fatalf("strategy counters = successes:%d failures:%d, want 1/0", s.Successes, s.Failures)
}
return
}
}
t.Fatalf("bugfix strategy not found: %+v", st.Strategies)
}
func TestRuntimeStateUsesPrivateFilesAndSharedDirLock(t *testing.T) {
dir := t.TempDir()
rt1 := New(dir)
rt2 := New(dir)
if rt1.mu != rt2.mu {
t.Fatal("runtimes for the same dir must share a lock")
}
_, turn := rt1.StartTurn(context.Background(), "fix a bug", nil)
turn.RecordToolResults([]ToolRecord{{Name: "go test", Output: "ok"}})
turn.Finish(nil)
if runtime.GOOS == "windows" {
return
}
for _, name := range []string{stateFile, tracesFile} {
info, err := os.Stat(filepath.Join(dir, name))
if err != nil {
t.Fatal(err)
}
if info.Mode().Perm() != 0o600 {
t.Fatalf("%s permissions = %o, want 0600", name, info.Mode().Perm())
}
}
}
func TestCompilerContractOrderingIsStable(t *testing.T) {
st := state{
NoisyRefs: map[string]int{
"z-noise": 2,
"a-noise": 2,
},
Mutations: []CompilerMutation{
{Target: "noise_filter", Change: "quarantine_pattern", Reason: "noise mutation", Applied: true, Status: "accepted"},
},
}
ir1 := buildIR("fix a bug", "fix a bug", st)
ir2 := buildIR("fix a bug", "fix a bug", st)
got1, err := compileExecutionContract(ir1)
if err != nil {
t.Fatal(err)
}
got2, err := compileExecutionContract(ir2)
if err != nil {
t.Fatal(err)
}
if got1 != got2 {
t.Fatalf("compiled contract is not stable:\n%s\n---\n%s", got1, got2)
}
if strings.Index(got1, "a-noise") > strings.Index(got1, "z-noise") {
t.Fatalf("noisy refs were not emitted in stable order:\n%s", got1)
}
}
func TestCompilerContractCanonicalizesSemanticOrder(t *testing.T) {
ir1 := PlannerIR{
Version: version,
Goal: "fix a bug",
SourceEvent: "fix a bug",
RuntimeMode: "control",
Constraints: []Constraint{
{Type: "reference", Text: "z", Source: "z"},
{Type: "avoid", Text: "a", Source: "a"},
},
RiskNotes: []string{"z risk", "a risk"},
MemoryReferences: []MemoryRef{
{ID: "m2", Content: "memory two", Influence: "reference"},
{ID: "m1", Content: "memory one", Influence: "evidence"},
},
StrategySelection: &StrategyPick{Selected: "general", Reason: "default", Rejected: []RejectedStrategy{
{ID: "z", Score: 0.1},
{ID: "a", Score: 0.9},
}},
}
ir2 := PlannerIR{
Version: version,
Goal: "fix a bug",
SourceEvent: "fix a bug",
RuntimeMode: "control",
Constraints: []Constraint{
{Type: "avoid", Text: "a", Source: "a"},
{Type: "reference", Text: "z", Source: "z"},
},
RiskNotes: []string{"a risk", "z risk"},
MemoryReferences: []MemoryRef{
{ID: "m1", Content: "memory one", Influence: "evidence"},
{ID: "m2", Content: "memory two", Influence: "reference"},
},
StrategySelection: &StrategyPick{Selected: "general", Reason: "default", Rejected: []RejectedStrategy{
{ID: "a", Score: 0.9},
{ID: "z", Score: 0.1},
}},
}
got1, err := compileExecutionContract(ir1)
if err != nil {
t.Fatal(err)
}
got2, err := compileExecutionContract(ir2)
if err != nil {
t.Fatal(err)
}
if got1 != got2 {
t.Fatalf("canonical contracts differ:\n%s\n---\n%s", got1, got2)
}
if !strings.Contains(got1, `"constraints":[`) || !strings.Contains(got1, `"memory_references":[`) {
t.Fatalf("expected explicit canonical IR arrays, got:\n%s", got1)
}
}
func TestCompilerContractIsCompact(t *testing.T) {
ir := PlannerIR{
Version: version,
Goal: "optimize a workflow",
SourceEvent: "optimize a workflow",
RuntimeMode: "control",
Constraints: []Constraint{
{Type: "must_use", Text: "prefer low latency", Source: "memory:latency"},
},
MemoryReferences: []MemoryRef{
{ID: "memory:latency", Content: "user prefers low latency", Quality: string(QualityHighSignal), Influence: "constraint"},
},
AvailableStrategies: []StrategyRef{
{ID: "general", SuccessRate: 0.9, Samples: 10, Score: 0.8, Reason: "ranked top"},
{ID: "bugfix-reproduce-first", SuccessRate: 0.5, Samples: 4},
},
StrategySelection: &StrategyPick{Selected: "general", Reason: "matched prior low-latency pattern", Score: 0.8, Rejected: []RejectedStrategy{{ID: "z", Score: 0.1}}},
}
contract, err := compileExecutionContract(ir)
if err != nil {
t.Fatal(err)
}
// The load-bearing fields the model acts on must survive compaction.
if !strings.Contains(contract, `"source_event":"optimize a workflow"`) {
t.Fatalf("compiled contract dropped source_event:\n%s", contract)
}
if !strings.Contains(contract, `"prefer low latency"`) {
t.Fatalf("compiled contract dropped the active constraint:\n%s", contract)
}
if !strings.Contains(contract, `"selected":"general"`) {
t.Fatalf("compiled contract dropped the selected strategy:\n%s", contract)
}
if !strings.Contains(contract, `"memory:latency"`) {
t.Fatalf("compiled contract dropped the high-signal memory reference:\n%s", contract)
}
// The compacted-away fields (prose explanation, ranked candidate table,
// rejected strategies) must NOT bloat the per-turn contract.
for _, banned := range []string{`"ir_explanation"`, `"available_strategies"`, `"rejected"`, `"constraint_mapping"`} {
if strings.Contains(contract, banned) {
t.Fatalf("compiled contract still carries bloat field %s:\n%s", banned, contract)
}
}
}
func TestStrategyExplorationIsDeterministicAndBounded(t *testing.T) {
strategies := ensureBuiltInStrategies(nil)
var exploredGoal string
var firstPick StrategyPick
for i := 0; i < 200; i++ {
goal := "fix a bug variant " + string(rune('a'+i%26)) + "-" + string(rune('a'+(i/26)%26))
ranked := rankStrategies(goal, strategies)
pick := selectStrategy(goal, ranked)
if pick.Mode == "explore" {
exploredGoal = goal
firstPick = pick
break
}
}
if exploredGoal == "" {
t.Fatal("expected at least one deterministic exploration goal")
}
for i := 0; i < 5; i++ {
got := selectStrategy(exploredGoal, rankStrategies(exploredGoal, strategies))
if got.Selected != firstPick.Selected || got.Mode != firstPick.Mode {
t.Fatalf("exploration selection is not deterministic: first=%+v got=%+v", firstPick, got)
}
}
if firstPick.ExplorationRate != 0.1 {
t.Fatalf("exploration rate = %v, want 0.1", firstPick.ExplorationRate)
}
}
func TestEquilibriumExplorationRateAdaptsToLearningState(t *testing.T) {
stable := state{Learnings: []SystemLearning{
{TraceID: "1", GoodPatterns: []string{"general"}},
{TraceID: "2", GoodPatterns: []string{"general"}},
{TraceID: "3", GoodPatterns: []string{"general"}},
}}
if got := equilibriumExplorationRatePercent(stable, DriftReport{}); got != maxExplorationRatePercent {
t.Fatalf("stable exploration rate = %d, want %d", got, maxExplorationRatePercent)
}
unstable := state{Learnings: []SystemLearning{
{TraceID: "1", GoodPatterns: []string{"general"}},
{TraceID: "2", BadStrategies: []string{"general"}},
}}
if got := equilibriumExplorationRatePercent(unstable, DriftReport{}); got != minExplorationRatePercent {
t.Fatalf("unstable exploration rate = %d, want %d", got, minExplorationRatePercent)
}
oscillating := state{Learnings: []SystemLearning{
{TraceID: "1", GoodPatterns: []string{"general"}},
{TraceID: "2", GoodPatterns: []string{"low-latency-optimization"}},
{TraceID: "3", BadStrategies: []string{"bugfix-reproduce-first"}},
{TraceID: "4", GoodPatterns: []string{"general"}},
}}
if got := equilibriumExplorationRatePercent(oscillating, DriftReport{}); got != minExplorationRatePercent {
t.Fatalf("oscillating exploration rate = %d, want damped %d", got, minExplorationRatePercent)
}
if got := equilibriumExplorationRatePercent(state{}, DriftReport{}); got != explorationRatePercent {
t.Fatalf("neutral exploration rate = %d, want %d", got, explorationRatePercent)
}
}
func TestControlPolicyHierarchyPrioritizesSemanticShift(t *testing.T) {
st := state{Learnings: []SystemLearning{
{TraceID: "1", GoodPatterns: []string{"general"}, CausalFindings: []string{"IR execution semantic variation: execution steps varied from planner IR"}},
{TraceID: "2", GoodPatterns: []string{"general"}, CausalFindings: []string{"IR execution semantic variation: tool calls exceeded IR step budget"}},
{TraceID: "3", GoodPatterns: []string{"general"}, CausalFindings: []string{"IR execution semantic variation: execution steps varied from planner IR"}},
}}
policy := controlPolicyForState(st, DriftReport{})
if policy.Mode != "stabilize" {
t.Fatalf("policy mode = %q, want stabilize: %+v", policy.Mode, policy)
}
if policy.ExplorationRatePercent != minExplorationRatePercent {
t.Fatalf("semantic shift exploration rate = %d, want %d", policy.ExplorationRatePercent, minExplorationRatePercent)
}
if policy.Gain >= 1 {
t.Fatalf("semantic shift should lower control gain, got %+v", policy)
}
if len(policy.SemanticShift) == 0 {
t.Fatalf("semantic shift monitor did not emit signals: %+v", policy)
}
}
func TestControlPolicyAdaptiveGainChangesMutationCooldown(t *testing.T) {
stable := controlPolicyForState(state{Learnings: []SystemLearning{
{TraceID: "1", GoodPatterns: []string{"general"}},
{TraceID: "2", GoodPatterns: []string{"general"}},
{TraceID: "3", GoodPatterns: []string{"general"}},
}}, DriftReport{})
unstable := controlPolicyForState(state{Learnings: []SystemLearning{
{TraceID: "1", BadStrategies: []string{"general"}},
}}, DriftReport{})
if stable.Gain <= unstable.Gain {
t.Fatalf("stable gain should exceed unstable gain: stable=%+v unstable=%+v", stable, unstable)
}
if stable.MutationCooldown >= mutationFeedbackCooldown {
t.Fatalf("stable policy should shorten cooldown for plasticity: %+v", stable)
}
if unstable.MutationCooldown <= mutationFeedbackCooldown {
t.Fatalf("unstable policy should lengthen cooldown for damping: %+v", unstable)
}
}
func TestStrategyDebiasRewardsNovelContextFit(t *testing.T) {
oldDominant := Strategy{ID: "old-dominant", Successes: 30, Preconditions: []string{"unrelated"}}
freshFit := Strategy{ID: "fresh-fit", Preconditions: []string{"latency"}}
ranked := rankStrategies("optimize latency", []Strategy{oldDominant, freshFit})
if len(ranked) < 2 {
t.Fatalf("ranked strategies = %+v", ranked)
}
if ranked[0].strategy.ID != "fresh-fit" {
t.Fatalf("fresh context-fit strategy should outrank old dominant strategy: %+v", ranked)
}
if !strings.Contains(ranked[0].reason, "novelty bonus") || !strings.Contains(ranked[1].reason, "usage penalty") {
t.Fatalf("strategy reasons should expose debias factors: %+v", ranked)
}
}
func TestIRExecutionValidatorSplitsHardAndSoftSemanticDrift(t *testing.T) {
ir := PlannerIR{
Version: version,
Goal: "fix a bug",
SourceEvent: "fix a bug",
MemoryReferences: []MemoryRef{
{ID: "memory:source", Content: "use source"},
},
ExecutionSteps: []Step{{ID: "reproduce", Action: "Reproduce the bug."}},
StrategySelection: &StrategyPick{
Selected: "bugfix-reproduce-first",
Reason: "matched bugfix",
ExplorationRate: 0.1,
},
}
trace := ExecutionTrace{
Goal: "fix a bug",
Steps: []Step{{ID: "patch", Action: "Patch without reproducing."}},
StrategyUsed: []string{"general"},
MemoryUsed: []string{},
Cost: CostMetrics{ToolCalls: 7},
}
got := validateIRExecution(ir, trace)
if !got.Reject {
t.Fatalf("expected validator to reject hard semantic drift: %+v", got)
}
hard := strings.Join(got.HardFindings, "\n")
for _, want := range []string{"selected strategy drift", "memory references drifted"} {
if !strings.Contains(hard, want) {
t.Fatalf("validator hard findings missing %q: %+v", want, got.HardFindings)
}
}
soft := strings.Join(got.SoftFindings, "\n")
for _, want := range []string{"execution steps varied", "tool calls exceeded IR step budget"} {
if !strings.Contains(soft, want) {
t.Fatalf("validator soft findings missing %q: %+v", want, got.SoftFindings)
}
}
joined := strings.Join(got.Findings, "\n")
for _, want := range []string{"selected strategy drift", "execution steps varied", "memory references drifted", "tool calls exceeded IR step budget"} {
if !strings.Contains(joined, want) {
t.Fatalf("validator findings missing %q: %+v", want, got.Findings)
}
}
learning := analyzeTrace(ExecutionTrace{ID: "trace-1", Goal: "fix", Outcome: "partial_success", SemanticDrift: got.Findings, SemanticDriftHard: got.HardFindings, SemanticDriftSoft: got.SoftFindings}, "general")
improvements := strings.Join(learning.CompilerImprovements, "\n")
if !strings.Contains(improvements, "enforce IR execution contract") {
t.Fatalf("semantic drift did not feed compiler improvements: %+v", learning)
}
if strings.Contains(improvements, "execution steps varied") {
t.Fatalf("soft execution variation should not be enforced as a compiler mutation: %+v", learning)
}
}
func TestMemoryCompilerSuppressesCompilerFeedbackNoise(t *testing.T) {
overhead := compilerIROverheadSelfFeedback
planModeBlocked := planModeBlockedToolError
planModeConstraint := "avoid repeating bash after repeated error: " + planModeBlocked
planModeNode := "bash repeated error: " + planModeBlocked
learning := analyzeTrace(ExecutionTrace{
ID: "trace-overhead",
Goal: "fix memory compiler",
Outcome: "success",
Cost: CostMetrics{
EstimatedInputTokens: 10,
EstimatedCompiledTokens: 950,
EstimatedIROverheadTokens: 940,
},
}, "general")
if strings.Contains(strings.Join(learning.CompilerImprovements, "\n"), overhead) {
t.Fatalf("IR overhead metric should not become compiler self-feedback: %+v", learning.CompilerImprovements)
}
learning = analyzeTrace(ExecutionTrace{
ID: "trace-plan-mode",
Goal: "draft a plan",
Outcome: "partial_success",
ToolResults: []ToolRecord{
{Name: "bash", Error: planModeBlocked},
{Name: "bash", Error: planModeBlocked},
},
}, "general")
if len(learning.BadStrategies) > 0 ||
strings.Contains(strings.Join(learning.MemoryNoisePatterns, "\n"), planModeBlocked) ||
strings.Contains(strings.Join(learning.CompilerImprovements, "\n"), planModeBlocked) {
t.Fatalf("plan-mode policy blocks should not become cross-session learning: %+v", learning)
}
realConstraint := Constraint{Type: "must_use", Text: "keep source-of-truth verification", Source: "policy:real"}
now := time.Now().UTC()
ir := buildIR("fix memory compiler", "fix memory compiler", state{
ExecutionState: ExecutionState{ActiveConstraints: []Constraint{
{Type: "reference", Text: overhead, Source: "learning:old"},
{Type: "avoid", Text: planModeConstraint, Source: "learning:plan-mode"},
realConstraint,
}},
Nodes: []MemoryNode{
{
ID: "learning:old-node",
Type: "fact",
Content: overhead,
Timestamp: now,
Confidence: 1,
Quality: QualityHighSignal,
Constraint: &Constraint{Type: "reference", Text: overhead, Source: "learning:old-node"},
TruthLocked: false,
},
{
ID: "noise:plan-mode",
Type: "state",
Content: planModeNode,
Timestamp: now,
Confidence: 1,
Quality: QualityHighSignal,
Constraint: &Constraint{Type: "avoid", Text: planModeNode, Source: "noise:plan-mode"},
TruthLocked: false,
},
{
ID: "memory:real",
Type: "fact",
Content: "use the issue body as source",
Timestamp: now,
Confidence: 1,
Quality: QualityHighSignal,
Constraint: &realConstraint,
},
},
Mutations: []CompilerMutation{
{Target: "strategy_selector", Change: "add_constraint", Reason: overhead, Applied: true},
{Target: "strategy_selector", Change: "add_constraint", Reason: planModeConstraint, Applied: true},
{Target: "strategy_selector", Change: "add_constraint", Reason: "keep focused regression check", Applied: true},
},
NoisyRefs: map[string]int{
overhead: 2,
planModeNode: 2,
"real transient IO error": 2,
},
})
compiled, err := compileExecutionContract(ir)
if err != nil {
t.Fatal(err)
}
if strings.Contains(compiled, overhead) {
t.Fatalf("compiled contract leaked IR-overhead self-feedback:\n%s", compiled)
}
if strings.Contains(compiled, planModeBlocked) {
t.Fatalf("compiled contract leaked plan-mode policy noise:\n%s", compiled)
}
for _, want := range []string{realConstraint.Text, "use the issue body as source", "keep focused regression check", "real transient IO error"} {
if !strings.Contains(compiled, want) {
t.Fatalf("compiled contract lost actionable signal %q:\n%s", want, compiled)
}
}
}
func TestStrategyExplorationEntropyStaysAboveFloor(t *testing.T) {
strategies := ensureBuiltInStrategies(nil)
explored := 0
total := 400
for i := 0; i < total; i++ {
goal := "strategy entropy sample " + string(rune('a'+i%26)) + "-" + string(rune('a'+(i/26)%26))
if pick := selectStrategy(goal, rankStrategies(goal, strategies)); pick.Mode == "explore" {
explored++
}
}
rate := float64(explored) / float64(total)
if rate <= 0.03 {
t.Fatalf("exploration rate = %.3f, want > 0.03", rate)
}
if rate > 0.15 {
t.Fatalf("exploration rate = %.3f, want bounded near configured rate", rate)
}
}
func TestTraceSplitterKeepsRuntimeTraceSmall(t *testing.T) {
tr := ExecutionTrace{
ID: "trace-large",
IRVersion: version,
Goal: "debug trace split",
Steps: []Step{{ID: "run", Action: "Run a large command"}},
Outcome: "success",
ToolResults: []ToolRecord{{
Name: "bash",
Output: strings.Repeat("large-output ", 2000),
}},
CausalEdges: []CausalEdge{{From: "tool:trace-large:0", To: "outcome:trace-large", Relation: "supported_outcome"}},
}
bundle := splitTrace(tr, SystemLearning{TraceID: tr.ID, CausalFindings: []string{"tool result supported success"}}, true)
runtimeBytes, err := json.Marshal(bundle.RuntimeTrace)
if err != nil {
t.Fatal(err)
}
debugBytes, err := json.Marshal(bundle.DebugTrace)
if err != nil {
t.Fatal(err)
}
if len(runtimeBytes)*100 >= len(debugBytes)*30 {
t.Fatalf("runtime trace is too large: runtime=%d debug=%d", len(runtimeBytes), len(debugBytes))
}
if len(bundle.RuntimeTrace.ToolResults) != 0 || len(bundle.RuntimeTrace.CausalEdges) != 0 {
t.Fatalf("runtime trace leaked debug telemetry: %+v", bundle.RuntimeTrace)
}
if bundle.LearningTrace == nil || len(bundle.LearningTrace.CausalFindings) == 0 {
t.Fatalf("learning trace missing structured signal: %+v", bundle.LearningTrace)
}
}
func TestAppendBoundedJSONLPrunesOldLines(t *testing.T) {
path := filepath.Join(t.TempDir(), learningTracesFile)
for i := 0; i < 5; i++ {
if err := appendBoundedJSONL(path, map[string]int{"i": i}, 3); err != nil {
t.Fatalf("appendBoundedJSONL(%d): %v", i, err)
}
}
b, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
lines := strings.Split(strings.TrimSpace(string(b)), "\n")
if len(lines) != 3 {
t.Fatalf("lines = %d, want 3:\n%s", len(lines), string(b))
}
for idx, line := range lines {
var got map[string]int
if err := json.Unmarshal([]byte(line), &got); err != nil {
t.Fatalf("line %d json: %v", idx, err)
}
if want := idx + 2; got["i"] != want {
t.Fatalf("line %d i = %d, want %d", idx, got["i"], want)
}
}
}
func TestMutationFeedbackDampingSkipsCooldownResonance(t *testing.T) {
now := time.Now().UTC()
existing := []CompilerMutation{{
Target: "strategy_selector",
Change: "add_constraint",
Reason: "first signal",
Status: "testing",
Applied: true,
CreatedAt: now.Add(-time.Minute),
UpdatedAt: now.Add(-time.Minute),
}}
next := CompilerMutation{
Target: "strategy_selector",
Change: "add_constraint",
Reason: "second nearby signal",
Status: "testing",
Applied: true,
CreatedAt: now,
UpdatedAt: now,
}
got := mergeMutations(existing, next)
if len(got) != 1 {
t.Fatalf("cooldown should damp same target/change resonance, got %+v", got)
}
later := next
later.Reason = "later independent signal"
later.CreatedAt = now.Add(mutationFeedbackCooldown + time.Minute)
later.UpdatedAt = later.CreatedAt
got = mergeMutations(existing, later)
if len(got) != 2 {
t.Fatalf("cooldown should allow later mutation signal, got %+v", got)
}
lowGainPolicy := defaultControlPolicy()
lowGainPolicy.Gain = 0.5
lowGainPolicy.MutationCooldown = controlMutationCooldown(lowGainPolicy.Gain)
lowGainPolicy.MutationCooldownMs = lowGainPolicy.MutationCooldown.Milliseconds()
damped := next
damped.Reason = "low gain damped signal"
damped.CreatedAt = now.Add(45 * time.Minute)
damped.UpdatedAt = damped.CreatedAt
got = mergeMutationsWithPolicy(lowGainPolicy, existing, damped)
if len(got) != 1 {
t.Fatalf("low gain policy should extend cooldown and damp feedback, got %+v", got)
}
}
func TestDriftControlReportsStaleConflictingAndOverusedState(t *testing.T) {
now := time.Now().UTC()
st := state{
Strategies: []Strategy{{ID: "general", Successes: 10, Failures: 0}},
Nodes: []MemoryNode{
{
ID: "old-fact",
Type: "fact",
Content: "old preference",
Timestamp: now.AddDate(0, 0, -365),
Confidence: 0.1,
Quality: QualityMediumSignal,
},
{
ID: "tool-success",
Type: "tool_result",
Content: "go test succeeded",
Timestamp: now,
Confidence: 0.9,
Quality: QualityHighSignal,
TruthLocked: true,
},
{
ID: "tool-fail",
Type: "tool_result",
Content: "go test failed: timeout",
Timestamp: now,
Confidence: 0.9,
Quality: QualityMediumSignal,
TruthLocked: true,
},
},
}
next, report := applyDriftControl(st, now, "trace-drift")
if !containsString(report.OverusedStrategies, "general") {
t.Fatalf("missing overused strategy report: %+v", report)
}
if !containsString(report.StaleMemoryNodes, "old-fact") {
t.Fatalf("missing stale memory report: %+v", report)
}
if len(report.ConflictingFacts) == 0 {
t.Fatalf("missing conflicting fact report: %+v", report)
}
assertEdge(t, next.Edges, "contradicts")
assertNode(t, next.Nodes, func(n MemoryNode) bool {
return n.ID == "old-fact" && n.Quality == QualityNoise
}, "stale node marked as noise")
assertNode(t, next.Nodes, func(n MemoryNode) bool {
return n.ID == "old-fact" && n.Confidence == 0.1
}, "drift control should not persist decayed confidence")
}
func TestTruthLockedNodeCannotBeOverwritten(t *testing.T) {
now := time.Now().UTC()
nodes := []MemoryNode{{
ID: "tool:trace:0",
Type: "tool_result",
Content: "original result",
Timestamp: now,
Confidence: 0.95,
Quality: QualityHighSignal,
TruthLocked: true,
}}
nodes = upsertNode(nodes, MemoryNode{
ID: "tool:trace:0",
Type: "tool_result",
Content: "overwritten result",
Timestamp: now.Add(time.Second),
Confidence: 0.1,
Quality: QualityNoise,
})
if nodes[0].Content != "original result" {
t.Fatalf("truth-locked node was overwritten: %+v", nodes[0])
}
}
func TestLoadStateBacksUpCorruptState(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, stateFile)
if err := os.WriteFile(path, []byte("{not-json"), 0o600); err != nil {
t.Fatal(err)
}
r := New(dir)
st := r.loadState()
if st.NoisyRefs == nil {
t.Fatal("loadState should return initialized empty state")
}
matches, err := filepath.Glob(path + ".corrupt-*")
if err != nil {
t.Fatal(err)
}
if len(matches) != 1 {
t.Fatalf("corrupt backup count = %d, want 1", len(matches))
}
b, err := os.ReadFile(matches[0])
if err != nil {
t.Fatal(err)
}
if string(b) != "{not-json" {
t.Fatalf("backup content = %q", b)
}
}
func TestTraceIDIncludesCollisionGuard(t *testing.T) {
now := time.Unix(123, 456).UTC()
a := traceID(now)
b := traceID(now)
prefix := now.Format("20060102T150405.000000000") + "-"
if !strings.HasPrefix(a, prefix) || !strings.HasPrefix(b, prefix) {
t.Fatalf("trace IDs missing timestamp prefix: %q %q", a, b)
}
if a == b {
t.Fatalf("trace IDs collided for the same timestamp: %q", a)
}
}
func readState(t *testing.T, dir string) state {
t.Helper()
b, err := os.ReadFile(filepath.Join(dir, stateFile))
if err != nil {
t.Fatal(err)
}
var st state
if err := json.Unmarshal(b, &st); err != nil {
t.Fatal(err)
}
return st
}
func readTraces(t *testing.T, dir string) []ExecutionTrace {
t.Helper()
b, err := os.ReadFile(filepath.Join(dir, tracesFile))
if err != nil {
t.Fatal(err)
}
var traces []ExecutionTrace
for _, line := range strings.Split(strings.TrimSpace(string(b)), "\n") {
var tr ExecutionTrace
if err := json.Unmarshal([]byte(line), &tr); err != nil {
t.Fatal(err)
}
traces = append(traces, tr)
}
return traces
}
func readLearningTraces(t *testing.T, dir string) []LearningTrace {
t.Helper()
b, err := os.ReadFile(filepath.Join(dir, learningTracesFile))
if err != nil {
t.Fatal(err)
}
var traces []LearningTrace
for _, line := range strings.Split(strings.TrimSpace(string(b)), "\n") {
var tr LearningTrace
if err := json.Unmarshal([]byte(line), &tr); err != nil {
t.Fatal(err)
}
traces = append(traces, tr)
}
return traces
}
func assertNode(t *testing.T, nodes []MemoryNode, pred func(MemoryNode) bool, desc string) {
t.Helper()
for _, n := range nodes {
if pred(n) {
return
}
}
t.Fatalf("missing node: %s\nnodes=%+v", desc, nodes)
}
func assertEdge(t *testing.T, edges []MemoryEdge, relation string) {
t.Helper()
for _, e := range edges {
if e.Relation == relation {
return
}
}
t.Fatalf("missing %s edge: %+v", relation, edges)
}