iop/apps/agent/internal/taskloop/parity_test.go

194 lines
7 KiB
Go

package taskloop
import (
"crypto/sha256"
"encoding/hex"
"os"
"path/filepath"
"strings"
"testing"
)
func TestParityEmbeddedManifestIsCompleteAndCurrent(t *testing.T) {
manifest, err := LoadEmbeddedParityManifest()
if err != nil {
t.Fatal(err)
}
if err := ValidateParityManifest(repoRoot(t), manifest); err != nil {
t.Fatal(err)
}
}
func TestParityManifestRejectsMissingDuplicateUnclassifiedAndStaleEvidence(t *testing.T) {
root := parityFixtureRoot(t)
manifest := validParityFixture(t, root)
cases := []struct {
name string
mutate func(*ParityManifest)
want string
}{
{"missing disposal", func(m *ParityManifest) { m.Disposal.ReferenceFiles = m.Disposal.ReferenceFiles[:3] }, "disposal inventory is missing classified reference"},
{"duplicate behavior", func(m *ParityManifest) { m.Behaviors = append(m.Behaviors, m.Behaviors[0]) }, "duplicate behavior"},
{"unclassified", func(m *ParityManifest) { m.Behaviors[0].Disposition = "" }, "unclassified disposition"},
{"stale source", func(m *ParityManifest) { m.Behaviors[0].GoSource[0] = "missing.go" }, "stale path"},
{"python caller", func(m *ParityManifest) { m.Behaviors[0].ProductionCaller = "python" }, "lacks Go ownership evidence"},
{"hash drift", func(m *ParityManifest) { m.Disposal.ReferenceFiles[0].SHA256 = strings.Repeat("0", 64) }, "checksum drift"},
{"unknown disposal state", func(m *ParityManifest) { m.Disposal.State = "transitioning" }, "unknown disposal state"},
}
for _, tt := range cases {
t.Run(tt.name, func(t *testing.T) {
copy := manifest
copy.Behaviors = append([]ParityBehavior(nil), manifest.Behaviors...)
copy.Disposal.ReferenceFiles = append([]ParityReferenceFile(nil), manifest.Disposal.ReferenceFiles...)
for i := range copy.Behaviors {
copy.Behaviors[i].GoSource = append([]string(nil), copy.Behaviors[i].GoSource...)
}
tt.mutate(&copy)
if err := ValidateParityManifest(root, copy); err == nil || !strings.Contains(err.Error(), tt.want) {
t.Fatalf("ValidateParityManifest error = %v, want %q", err, tt.want)
}
})
}
}
func TestDispositionReportIsStable(t *testing.T) {
manifest, err := LoadEmbeddedParityManifest()
if err != nil {
t.Fatal(err)
}
report := FormatParityReport(manifest)
for _, want := range []string{"parity: ok", "behaviors: 13", "disposal_state: retained", "disposal_files: 9", "milestone-completion-transition"} {
if !strings.Contains(report, want) {
t.Fatalf("report %q does not contain %q", report, want)
}
}
}
func parityFixtureRoot(t *testing.T) string {
t.Helper()
root := t.TempDir()
for _, path := range append(append([]string{}, parityFixtureReferenceFiles...), "owner.go") {
fullPath := filepath.Join(root, filepath.FromSlash(path))
if err := os.MkdirAll(filepath.Dir(fullPath), 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(fullPath, []byte(path), 0600); err != nil {
t.Fatal(err)
}
}
return root
}
func validParityFixture(t *testing.T, root string) ParityManifest {
t.Helper()
manifest := ParityManifest{SchemaVersion: paritySchemaVersion, ReferenceRoot: "references", DeletionGate: "completion"}
for index, path := range parityFixtureReferenceFiles {
contents, err := os.ReadFile(filepath.Join(root, filepath.FromSlash(path)))
if err != nil {
t.Fatal(err)
}
manifest.Behaviors = append(manifest.Behaviors, ParityBehavior{
ID: "behavior-" + string(rune('a'+index)),
ReferenceSources: []string{path}, Disposition: "replace", GoOwner: "Go", GoSource: []string{"owner.go"}, GoTest: []string{"TestOwner"}, Invariants: "bounded", ProductionCaller: "go-only",
})
manifest.Disposal.ReferenceFiles = append(manifest.Disposal.ReferenceFiles, ParityReferenceFile{
Path: path, SHA256: sha256Hex(contents), Disposition: "replace", CoveredBy: []string{"behavior-" + string(rune('a'+index))},
})
}
manifest.Disposal.State = "retained"
manifest.Disposal.Procedure = "retain"
return manifest
}
var parityFixtureReferenceFiles = []string{
"references/dispatch.py",
"references/observer.py",
"references/tests/test_dispatch.py",
"references/tests/test_dispatch.py.orig",
}
func TestParityManifestRejectsUnrecordedRetainedFixture(t *testing.T) {
root := parityFixtureRoot(t)
manifest := validParityFixture(t, root)
extra := filepath.Join(root, "references", "tests", "extra.py")
if err := os.WriteFile(extra, []byte("fixture\n"), 0600); err != nil {
t.Fatal(err)
}
if err := ValidateParityManifest(root, manifest); err == nil || !strings.Contains(err.Error(), "unrecorded retained fixture") {
t.Fatalf("ValidateParityManifest error = %v, want unrecorded retained fixture", err)
}
}
func TestParityManifestDisposalLifecycle(t *testing.T) {
root := parityFixtureRoot(t)
manifest := validParityFixture(t, root)
if err := ValidateParityManifest(root, manifest); err != nil {
t.Fatal(err)
}
for _, reference := range manifest.Disposal.ReferenceFiles {
if err := os.Remove(filepath.Join(root, filepath.FromSlash(reference.Path))); err != nil {
t.Fatal(err)
}
}
manifest.Disposal.State = "disposed"
if err := ValidateParityManifest(root, manifest); err != nil {
t.Fatalf("disposed manifest: %v", err)
}
root = parityFixtureRoot(t)
manifest = validParityFixture(t, root)
manifest.Disposal.State = "disposed"
if err := ValidateParityManifest(root, manifest); err == nil || !strings.Contains(err.Error(), "retained fixture") {
t.Fatalf("disposed manifest with survivors error = %v", err)
}
root = parityFixtureRoot(t)
manifest = validParityFixture(t, root)
if err := os.Remove(filepath.Join(root, filepath.FromSlash(manifest.Disposal.ReferenceFiles[0].Path))); err != nil {
t.Fatal(err)
}
if err := ValidateParityManifest(root, manifest); err == nil || !strings.Contains(err.Error(), "missing fixture") {
t.Fatalf("retained manifest with missing fixture error = %v", err)
}
}
func TestParityManifestDisposedRejectsMissingHistoricalInventory(t *testing.T) {
root := parityFixtureRoot(t)
manifest := validParityFixture(t, root)
for _, reference := range manifest.Disposal.ReferenceFiles {
if err := os.Remove(filepath.Join(root, filepath.FromSlash(reference.Path))); err != nil {
t.Fatal(err)
}
}
manifest.Disposal.State = "disposed"
if err := ValidateParityManifest(root, manifest); err != nil {
t.Fatalf("complete disposed manifest: %v", err)
}
manifest.Disposal.ReferenceFiles = manifest.Disposal.ReferenceFiles[:len(manifest.Disposal.ReferenceFiles)-1]
if err := ValidateParityManifest(root, manifest); err == nil || !strings.Contains(err.Error(), "disposal inventory is missing classified reference") {
t.Fatalf("disposed manifest with truncated history error = %v", err)
}
}
func sha256Hex(contents []byte) string {
digest := sha256.Sum256(contents)
return hex.EncodeToString(digest[:])
}
func repoRoot(t *testing.T) string {
t.Helper()
directory, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
for {
if _, err := os.Stat(filepath.Join(directory, "go.mod")); err == nil {
return directory
}
parent := filepath.Dir(directory)
if parent == directory {
t.Fatal("repository root not found")
}
directory = parent
}
}