package main import ( "context" "path/filepath" "testing" "time" "git.cin.su/m/Assistant/adapters/developmentexecutor" "git.cin.su/m/Assistant/contracts/work" ) type developmentGateStoreStub struct { gate work.DevelopmentGate saved bool expected int64 } func (s *developmentGateStoreStub) LoadDevelopmentGate(context.Context, string) (work.DevelopmentGate, error) { return s.gate, nil } func (s *developmentGateStoreStub) SaveDevelopmentGate(_ context.Context, gate work.DevelopmentGate, expected int64) error { s.gate = gate s.saved = true s.expected = expected return nil } type developmentCommandRunnerStub struct { exitCodes []int commands int } func (r *developmentCommandRunnerStub) Run(context.Context, string, string, []string) int { code := 0 if r.commands < len(r.exitCodes) { code = r.exitCodes[r.commands] } r.commands++ return code } func validDevelopmentVerificationPlan(t *testing.T, worktreeID string) developmentexecutor.Plan { t.Helper() worktree, err := filepath.Abs(".") if err != nil { t.Fatal(err) } return developmentexecutor.Plan{ Worktree: worktree, WorktreeID: worktreeID, BaseRepoRevision: "base-revision", ProtectedMainAccess: "DENIED", ProductionAccess: "DENIED", SecretAccess: "DENIED", Checks: []developmentexecutor.Check{{Kind: developmentexecutor.CheckGitDiffCheck}}, } } func isolatedDevelopmentGate() work.DevelopmentGate { return work.DevelopmentGate{ ID: "gate-1", Revision: 4, Stage: work.DevelopmentIsolated, EvidenceRefs: []string{"evidence-1"}, ProposalRef: "proposal-1", TaskPacketRef: "task-1", WorktreeRef: "sandbox-1", ProducerRunRefs: []work.AgentRunID{"producer-1"}, VerifierRunRefs: nil, VerificationEvidence: "", } } func TestVerifyDevelopmentGatePersistsVerifiedWithIndependentEvidence(t *testing.T) { store := &developmentGateStoreStub{gate: isolatedDevelopmentGate()} runner := &developmentCommandRunnerStub{exitCodes: []int{0}} gate, result, err := verifyDevelopmentGate(context.Background(), store, developmentGateVerificationRequest{ GateID: "gate-1", ExpectedRevision: 4, VerifierRunID: "verifier-1", Plan: validDevelopmentVerificationPlan(t, "sandbox-1"), }, time.UnixMilli(20), runner) if err != nil { t.Fatal(err) } if !result.Passed || gate.Stage != work.DevelopmentVerified || gate.Revision != 5 { t.Fatalf("verification did not advance gate: gate=%#v result=%#v", gate, result) } if !store.saved || store.expected != 4 || gate.VerificationEvidence == "" { t.Fatalf("verification was not durably persisted: store=%#v gate=%#v", store, gate) } } func TestVerifyDevelopmentGateLeavesIsolatedOnFailedChecks(t *testing.T) { store := &developmentGateStoreStub{gate: isolatedDevelopmentGate()} gate, result, err := verifyDevelopmentGate(context.Background(), store, developmentGateVerificationRequest{ GateID: "gate-1", ExpectedRevision: 4, VerifierRunID: "verifier-1", Plan: validDevelopmentVerificationPlan(t, "sandbox-1"), }, time.UnixMilli(20), &developmentCommandRunnerStub{exitCodes: []int{1}}) if err != errDevelopmentChecksFailed || result.Passed || gate.Stage != work.DevelopmentIsolated || store.saved { t.Fatalf("failed checks changed gate unexpectedly: gate=%#v result=%#v err=%v store=%#v", gate, result, err, store) } } func TestVerifyDevelopmentGateRejectsProducerAsVerifier(t *testing.T) { store := &developmentGateStoreStub{gate: isolatedDevelopmentGate()} _, _, err := verifyDevelopmentGate(context.Background(), store, developmentGateVerificationRequest{ GateID: "gate-1", ExpectedRevision: 4, VerifierRunID: "producer-1", Plan: validDevelopmentVerificationPlan(t, "sandbox-1"), }, time.UnixMilli(20), &developmentCommandRunnerStub{exitCodes: []int{0}}) if err != errDevelopmentVerifierNotIndependent { t.Fatalf("producer was accepted as verifier: %v", err) } } func TestRecordDevelopmentGateEvidenceAdvancesCanaryAndPromotion(t *testing.T) { store := &developmentGateStoreStub{gate: work.DevelopmentGate{ ID: "gate-1", Revision: 6, Stage: work.DevelopmentVerified, EvidenceRefs: []string{"evidence-1"}, ProposalRef: "proposal-1", TaskPacketRef: "task-1", WorktreeRef: "sandbox-1", ProducerRunRefs: []work.AgentRunID{"producer-1"}, VerifierRunRefs: []work.AgentRunID{"verifier-1"}, VerificationEvidence: "verification-1", }} gate, err := recordDevelopmentGateEvidence(context.Background(), store, developmentGateEvidenceRequest{GateID: "gate-1", ExpectedRevision: 6, NextStage: work.DevelopmentCanary, EvidenceRef: "canary-1"}, time.UnixMilli(30)) if err != nil || gate.Stage != work.DevelopmentCanary || gate.Revision != 7 || gate.CanaryEvidence != "canary-1" { t.Fatalf("canary transition failed: gate=%#v err=%v", gate, err) } gate, err = recordDevelopmentGateEvidence(context.Background(), store, developmentGateEvidenceRequest{GateID: "gate-1", ExpectedRevision: 7, NextStage: work.DevelopmentPromoted, EvidenceRef: "promotion-1"}, time.UnixMilli(31)) if err != nil || gate.Stage != work.DevelopmentPromoted || gate.Revision != 8 || gate.PromotionRef != "promotion-1" { t.Fatalf("promotion transition failed: gate=%#v err=%v", gate, err) } } func TestRecordDevelopmentGateEvidenceSupportsCanaryRollbackAndRejectsMissingEvidence(t *testing.T) { store := &developmentGateStoreStub{gate: work.DevelopmentGate{ID: "gate-1", Revision: 2, Stage: work.DevelopmentCanary, EvidenceRefs: []string{"evidence-1"}, CanaryEvidence: "canary-1"}} gate, err := recordDevelopmentGateEvidence(context.Background(), store, developmentGateEvidenceRequest{GateID: "gate-1", ExpectedRevision: 2, NextStage: work.DevelopmentRolledBack, EvidenceRef: "rollback-1"}, time.UnixMilli(40)) if err != nil || gate.Stage != work.DevelopmentRolledBack || gate.RollbackRef != "rollback-1" { t.Fatalf("rollback transition failed: gate=%#v err=%v", gate, err) } store = &developmentGateStoreStub{gate: work.DevelopmentGate{ID: "gate-2", Revision: 2, Stage: work.DevelopmentCanary, EvidenceRefs: []string{"evidence-1"}, CanaryEvidence: "canary-1"}} gate, err = recordDevelopmentGateEvidence(context.Background(), store, developmentGateEvidenceRequest{GateID: "gate-2", ExpectedRevision: 2, NextStage: work.DevelopmentRolledBack}, time.UnixMilli(40)) if err != errDevelopmentEvidenceRequired || gate.Stage != work.DevelopmentCanary || store.saved { t.Fatalf("missing rollback evidence changed gate: gate=%#v err=%v store=%#v", gate, err, store) } } func TestDevelopmentGateControllerRequiresSequentialVerifiedEvidence(t *testing.T) { now := time.UnixMilli(10) gate, err := prepareDevelopmentGate(work.DevelopmentGate{ID: "gate-1", Revision: 1, Stage: work.DevelopmentEvidenceCollected, EvidenceRefs: []string{"evidence-1"}}, now) if err != nil { t.Fatal(err) } gate.ProposalRef = "proposal-1" gate, err = advanceDevelopmentGate(gate, work.DevelopmentProposalCreated, now) if err != nil || gate.Revision != 2 { t.Fatalf("proposal transition failed: %#v %v", gate, err) } gate.TaskPacketRef = "task-1" gate, err = advanceDevelopmentGate(gate, work.DevelopmentTaskAdmitted, now) if err != nil { t.Fatal(err) } gate.WorktreeRef = "isolated-worktree" gate, err = advanceDevelopmentGate(gate, work.DevelopmentIsolated, now) if err != nil { t.Fatal(err) } gate.ProducerRunRefs = []work.AgentRunID{"producer"} gate.VerifierRunRefs = []work.AgentRunID{"verifier"} gate.VerificationEvidence = "verification-1" gate, err = advanceDevelopmentGate(gate, work.DevelopmentVerified, now) if err != nil { t.Fatal(err) } if _, err := advanceDevelopmentGate(gate, work.DevelopmentPromoted, now); err == nil { t.Fatal("promotion bypassed canary") } }