-
Notifications
You must be signed in to change notification settings - Fork 0
fix: feat: add Codex and Gemini hook checks to chitin status #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,348 @@ | ||
| package policy | ||
|
|
||
| import ( | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| ) | ||
|
|
||
| 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) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 tochitin statushook 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.