Files
Assistant/cmd/core-runtime/development_gate_test.go
T
m 067facdfa4
CI / test (push) Successful in 47s
CI / test (pull_request) Successful in 52s
feat(governance): connect verifier to development gate
2026-08-23 21:29:53 +03:00

154 lines
5.2 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 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")
}
}