package cockpit import ( "os" "path/filepath" "strings" "testing" ) // flashOSAnswerKey is the operator's hand-built FlashOS handover skill — // the living structural answer key for C1 dogfood parity. It is read-only // and may be absent (CI, or a fresh clone), so the parity test skips rather // than fails when it is missing. const flashOSAnswerKey = "/Users/antonhahn/FlashOS/ajhahnde/.claude/skills/handover/SKILL.md" func TestScratchRegenerate_WritesToScratchOnly(t *testing.T) { pb := loadHandover(t) scratch := t.TempDir() path, err := ScratchRegenerate(pb, "claude", scratch) if err != nil { t.Fatalf("ScratchRegenerate: %v", err) } if filepath.Dir(filepath.Dir(filepath.Dir(filepath.Dir(path)))) != scratch { t.Errorf("scratch path %q not under scratch root %q", path, scratch) } b, err := os.ReadFile(path) if err != nil { t.Fatalf("read scratch artifact: %v", err) } shape := parseSkillShape(b) if shape.stepCount < 5 || !shape.hasOutput { t.Errorf("scratch artifact shape off: steps=%d output=%v", shape.stepCount, shape.hasOutput) } } func TestParity_FlashOSAnswerKey(t *testing.T) { if _, err := os.Stat(flashOSAnswerKey); err != nil { t.Skipf("FlashOS answer key absent (%v) — parity check skipped", err) } pb := loadHandover(t) res, err := Parity(pb, "claude", flashOSAnswerKey) if err != nil { t.Fatalf("Parity: %v", err) } if !res.LayerOK { t.Errorf("layer parity failed: %v", res.Notes) } if !res.CapOK { t.Errorf("capability parity failed: %v", res.Notes) } if !res.SafetyOK { t.Errorf("safety parity failed: %v", res.Notes) } } func TestParity_UnknownTarget(t *testing.T) { pb := loadHandover(t) if _, err := Parity(pb, "nosuchharness", flashOSAnswerKey); err == nil { t.Error("expected an error for an unknown target") } if _, err := ScratchRegenerate(pb, "nosuchharness", t.TempDir()); err == nil { t.Error("expected ScratchRegenerate to reject an unknown target") } } func TestParity_SafetyTierWarnsOnOverGrantingKey(t *testing.T) { // A hand-built answer key that over-grants a write-git verb is NOT eeco's // artifact, so it no longer hard-fails the safety tier — Tier 3 scopes to // the emitted allowlist. eeco's emit is clean, so SafetyOK holds; the // key's over-grant is surfaced as a warning Note instead. dir := t.TempDir() key := filepath.Join(dir, ".claude", "skills", "handover", "SKILL.md") if err := os.MkdirAll(filepath.Dir(key), 0o755); err != nil { t.Fatal(err) } poisoned := "---\nname: handover\ndescription: x\nallowed-tools: Read, Bash(git commit:*)\n---\n# Handover\n" if err := os.WriteFile(key, []byte(poisoned), 0o644); err != nil { t.Fatal(err) } pb := loadHandover(t) res, err := Parity(pb, "claude", key) if err != nil { t.Fatalf("Parity: %v", err) } if !res.SafetyOK { t.Errorf("over-granting answer key hard-failed the safety tier; should warn (Tier 3 is emitted-only): %v", res.Notes) } if !hasNote(res.Notes, "answer key over-grants") { t.Errorf("expected an over-grant warning Note, got %v", res.Notes) } } func TestParity_SafetyTierFailsOnEmittedForbiddenVerb(t *testing.T) { // The invariant that matters: a forbidden write-git verb in the EMITTED // allowlist hard-fails the safety tier. ScratchRegenerate renders without // the generation gate, so a poisoned playbook reaches the parity scan. dir := t.TempDir() key := filepath.Join(dir, ".claude", "skills", "handover", "SKILL.md") if err := os.MkdirAll(filepath.Dir(key), 0o755); err != nil { t.Fatal(err) } clean := "---\nname: handover\ndescription: x\nallowed-tools: Read\n---\n# Handover\n" if err := os.WriteFile(key, []byte(clean), 0o644); err != nil { t.Fatal(err) } pb := loadHandover(t) pb.Capabilities = append(pb.Capabilities, Capability{Kind: "bash", Verb: "git commit", Scope: "*"}) res, err := Parity(pb, "claude", key) if err != nil { t.Fatalf("Parity: %v", err) } if res.SafetyOK { t.Error("safety tier passed an emitted allowlist that grants git commit") } } func TestParity_GitHeadCoverageAndKeyOverGrantWarns(t *testing.T) { // Tier 2: emitted "git branch --show-current" covers an answer key that // grants the broader "git branch" head — a scope refinement is not a // capability gap. Tier 3: the bare "git branch" in the key is a forbidden // over-grant → a warning Note, but SafetyOK stays true because the emitted // allowlist is clean. dir := t.TempDir() key := filepath.Join(dir, ".claude", "skills", "handover", "SKILL.md") if err := os.MkdirAll(filepath.Dir(key), 0o755); err != nil { t.Fatal(err) } keyBody := "---\nname: handover\ndescription: x\n" + "allowed-tools: Read, Write, Bash(git status:*), Bash(git log:*), Bash(git diff:*), Bash(git describe:*), Bash(git branch:*)\n" + "---\n# Handover\n" if err := os.WriteFile(key, []byte(keyBody), 0o644); err != nil { t.Fatal(err) } pb := loadHandover(t) res, err := Parity(pb, "claude", key) if err != nil { t.Fatalf("Parity: %v", err) } if !res.CapOK { t.Errorf("capability tier failed; head coverage should treat emitted \"git branch --show-current\" as covering key \"git branch\": %v", res.Notes) } if !res.SafetyOK { t.Errorf("safety tier should hold (emitted clean): %v", res.Notes) } if !hasNote(res.Notes, "answer key over-grants") { t.Errorf("expected an over-grant warning for the key's bare git branch, got %v", res.Notes) } } // hasNote reports whether any note contains substr. func hasNote(notes []string, substr string) bool { for _, n := range notes { if strings.Contains(n, substr) { return true } } return false }