Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
348 changes: 348 additions & 0 deletions internal/policy/loader_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,348 @@
package policy

import (
"os"
"path/filepath"
"testing"
)
Comment on lines +1 to +7
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file adds extensive new tests for internal/policy (Load/Find/Parse), which is not mentioned in the PR summary or Issue #22 (which is scoped to chitin status hook checks). Unless this is intentional, consider moving these policy tests into a separate PR to keep the change set focused and reduce review/merge risk.

Copilot uses AI. Check for mistakes.

func TestLoad_ValidFile(t *testing.T) {
tmpDir := t.TempDir()
policyFile := filepath.Join(tmpDir, "chitin.yaml")
policyContent := `mode: enforce
rules:
- action: read
effect: allow
target: "*.txt"
reason: "Allow text files"
- action: write
effect: deny
target: "*.secret"
reason: "Deny secret files"
`
if err := os.WriteFile(policyFile, []byte(policyContent), 0644); err != nil {
t.Fatal(err)
}

policy, err := Load(policyFile)
if err != nil {
t.Fatalf("Load failed: %v", err)
}

if policy.Mode != "enforce" {
t.Errorf("Expected mode 'enforce', got %q", policy.Mode)
}
if len(policy.Rules) != 2 {
t.Errorf("Expected 2 rules, got %d", len(policy.Rules))
}
if policy.Rules[0].Effect != "allow" {
t.Errorf("First rule should be allow, got %q", policy.Rules[0].Effect)
}
if policy.Rules[1].Effect != "deny" {
t.Errorf("Second rule should be deny, got %q", policy.Rules[1].Effect)
}
}

func TestLoad_DirectoryPath(t *testing.T) {
tmpDir := t.TempDir()
policyFile := filepath.Join(tmpDir, "chitin.yaml")
policyContent := `mode: monitor
rules:
- action: "*"
effect: allow
target: "**"
reason: "Allow everything in monitor mode"
`
if err := os.WriteFile(policyFile, []byte(policyContent), 0644); err != nil {
t.Fatal(err)
}

policy, err := Load(tmpDir)
if err != nil {
t.Fatalf("Load failed: %v", err)
}

if policy.Mode != "monitor" {
t.Errorf("Expected mode 'monitor', got %q", policy.Mode)
}
if len(policy.Rules) != 1 {
t.Errorf("Expected 1 rule, got %d", len(policy.Rules))
}
}

func TestLoad_MissingFile(t *testing.T) {
tmpDir := t.TempDir()
nonExistentFile := filepath.Join(tmpDir, "nonexistent.yaml")

_, err := Load(nonExistentFile)
if err == nil {
t.Fatal("Expected error for missing file")
}
}

func TestFind_InCurrentDirectory(t *testing.T) {
tmpDir := t.TempDir()
policyFile := filepath.Join(tmpDir, "chitin.yaml")
policyContent := `mode: enforce
rules:
- action: read
effect: allow
target: "*.go"
reason: "Allow Go files"
`
if err := os.WriteFile(policyFile, []byte(policyContent), 0644); err != nil {
t.Fatal(err)
}

policy, foundDir, err := Find(tmpDir)
if err != nil {
t.Fatalf("Find failed: %v", err)
}

if foundDir != tmpDir {
t.Errorf("Expected found directory %q, got %q", tmpDir, foundDir)
}
if policy.Mode != "enforce" {
t.Errorf("Expected mode 'enforce', got %q", policy.Mode)
}
if len(policy.Rules) != 1 {
t.Errorf("Expected 1 rule, got %d", len(policy.Rules))
}
}

func TestFind_WalksUpParentDirectories(t *testing.T) {
tmpDir := t.TempDir()
policyFile := filepath.Join(tmpDir, "chitin.yaml")
subDir := filepath.Join(tmpDir, "sub", "nested", "deep")

policyContent := `mode: monitor
rules:
- action: write
effect: allow
target: "*.md"
reason: "Allow markdown files"
`
if err := os.WriteFile(policyFile, []byte(policyContent), 0644); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(subDir, 0755); err != nil {
t.Fatal(err)
}

policy, foundDir, err := Find(subDir)
if err != nil {
t.Fatalf("Find failed: %v", err)
}

if foundDir != tmpDir {
t.Errorf("Expected found directory %q, got %q", tmpDir, foundDir)
}
if policy.Mode != "monitor" {
t.Errorf("Expected mode 'monitor', got %q", policy.Mode)
}
}

func TestFind_NoChitinYaml(t *testing.T) {
tmpDir := t.TempDir()
subDir := filepath.Join(tmpDir, "empty", "directory")
if err := os.MkdirAll(subDir, 0755); err != nil {
t.Fatal(err)
}

_, _, err := Find(subDir)
if err == nil {
t.Fatal("Expected error when no chitin.yaml exists")
}
}

func TestParse_DefaultsModeToEnforce(t *testing.T) {
yamlContent := `rules:
- action: read
effect: allow
target: "*.txt"
reason: "Allow text files"
`
policy, err := Parse([]byte(yamlContent))
if err != nil {
t.Fatalf("Parse failed: %v", err)
}

if policy.Mode != "enforce" {
t.Errorf("Expected default mode 'enforce', got %q", policy.Mode)
}
}

func TestParse_RejectsInvalidMode(t *testing.T) {
yamlContent := `mode: invalid
rules:
- action: read
effect: allow
target: "*.txt"
reason: "Allow text files"
`
_, err := Parse([]byte(yamlContent))
if err == nil {
t.Fatal("Expected error for invalid mode")
}
}

func TestParse_AcceptsMonitorMode(t *testing.T) {
yamlContent := `mode: monitor
rules:
- action: read
effect: allow
target: "*.txt"
reason: "Allow text files"
`
policy, err := Parse([]byte(yamlContent))
if err != nil {
t.Fatalf("Parse failed: %v", err)
}

if policy.Mode != "monitor" {
t.Errorf("Expected mode 'monitor', got %q", policy.Mode)
}
}

func TestParse_RejectsMissingAction(t *testing.T) {
yamlContent := `mode: enforce
rules:
- effect: allow
target: "*.txt"
reason: "Missing action"
`
_, err := Parse([]byte(yamlContent))
if err == nil {
t.Fatal("Expected error for missing action")
}
}

func TestParse_RejectsInvalidEffect(t *testing.T) {
yamlContent := `mode: enforce
rules:
- action: read
effect: maybe
target: "*.txt"
reason: "Invalid effect"
`
_, err := Parse([]byte(yamlContent))
if err == nil {
t.Fatal("Expected error for invalid effect")
}
}

func TestParse_AcceptsValidEffects(t *testing.T) {
testCases := []struct {
name string
effect string
}{
{"allow", "allow"},
{"deny", "deny"},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
yamlContent := `mode: enforce
rules:
- action: read
effect: ` + tc.effect + `
target: "*.txt"
reason: "Test"
`
policy, err := Parse([]byte(yamlContent))
if err != nil {
t.Fatalf("Parse failed for effect %q: %v", tc.effect, err)
}
if len(policy.Rules) != 1 {
t.Fatalf("Expected 1 rule, got %d", len(policy.Rules))
}
if policy.Rules[0].Effect != tc.effect {
t.Errorf("Expected effect %q, got %q", tc.effect, policy.Rules[0].Effect)
}
})
}
}

func TestParse_WithInvariantModes(t *testing.T) {
yamlContent := `mode: enforce
invariantModes:
git_clean: monitor
no_secrets: enforce
rules:
- action: read
effect: allow
target: "*.go"
reason: "Allow Go files"
`
policy, err := Parse([]byte(yamlContent))
if err != nil {
t.Fatalf("Parse failed: %v", err)
}

if len(policy.InvariantModes) != 2 {
t.Errorf("Expected 2 invariant modes, got %d", len(policy.InvariantModes))
}
if policy.InvariantModes["git_clean"] != "monitor" {
t.Errorf("Expected git_clean mode 'monitor', got %q", policy.InvariantModes["git_clean"])
}
if policy.InvariantModes["no_secrets"] != "enforce" {
t.Errorf("Expected no_secrets mode 'enforce', got %q", policy.InvariantModes["no_secrets"])
}
}

func TestParse_WithRequiresField(t *testing.T) {
yamlContent := `mode: enforce
rules:
- action: write
effect: allow
target: "*.go"
reason: "Allow Go file writes after reading"
requires:
prior_action: read_file
on_same: path
`
policy, err := Parse([]byte(yamlContent))
if err != nil {
t.Fatalf("Parse failed: %v", err)
}

if len(policy.Rules) != 1 {
t.Fatalf("Expected 1 rule, got %d", len(policy.Rules))
}
if policy.Rules[0].Requires == nil {
t.Fatal("Expected requires field to be set")
}
if policy.Rules[0].Requires.PriorAction != "read_file" {
t.Errorf("Expected prior_action 'read_file', got %q", policy.Rules[0].Requires.PriorAction)
}
if policy.Rules[0].Requires.OnSame != "path" {
t.Errorf("Expected on_same 'path', got %q", policy.Rules[0].Requires.OnSame)
}
}

func TestParse_WithArrayAction(t *testing.T) {
yamlContent := `mode: enforce
rules:
- action:
- read
- write
effect: allow
target: "*.go"
reason: "Allow read and write on Go files"
`
policy, err := Parse([]byte(yamlContent))
if err != nil {
t.Fatalf("Parse failed: %v", err)
}

if len(policy.Rules) != 1 {
t.Fatalf("Expected 1 rule, got %d", len(policy.Rules))
}
actions := policy.Rules[0].Actions()
if len(actions) != 2 {
t.Fatalf("Expected 2 actions, got %d", len(actions))
}
if actions[0] != "read" || actions[1] != "write" {
t.Errorf("Expected actions ['read', 'write'], got %v", actions)
}
}
Loading
Loading