183 lines
7.7 KiB
Go
183 lines
7.7 KiB
Go
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")
|
|
}
|
|
}
|