Go 159 lines
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
}