package ccsettings import ( "os" "path/filepath" "testing" ) func TestFingerprint(t *testing.T) { base := Grant{ Servers: map[string]ServerDef{"a": {Command: "x", Env: map[string]string{"K": "1", "J": "2"}}, "b": {Command: "y"}}, Allow: []string{"Bash(make:*)", "Edit"}, } same := Grant{ Servers: map[string]ServerDef{"b": {Command: "y"}, "a": {Command: "x", Env: map[string]string{"J": "2", "K": "1"}}}, Allow: []string{"Edit", "Bash(make:*)"}, } if base.Fingerprint() != same.Fingerprint() { t.Error("the same definitions in another order should be the same fingerprint") } changed := []Grant{ {Servers: map[string]ServerDef{"a": {Command: "x", Env: map[string]string{"K": "1", "J": "2"}}, "b": {Command: "y", Args: []string{"--evil"}}}, Allow: base.Allow}, {Servers: map[string]ServerDef{"a": {Command: "x", Env: map[string]string{"K": "1", "J": "2"}}}, Allow: base.Allow}, {Servers: base.Servers, Allow: []string{"Bash(make:*)", "Edit", "Bash"}}, {Servers: base.Servers, Allow: []string{"Bash(make:*)", "Edit"}, AllowFrom: []string{"elsewhere"}}, } for i, g := range changed[:3] { if g.Fingerprint() == base.Fingerprint() { t.Errorf("change %d did not change the fingerprint", i) } } // Which file a rule came from is not part of what is trusted. if changed[3].Fingerprint() != base.Fingerprint() { t.Error("AllowFrom should not affect the fingerprint") } if !(Grant{}).Empty() || base.Empty() { t.Error("Empty is wrong") } } func TestNewGrantTakesOnlyProjectScope(t *testing.T) { mcp := []MCPFile{ {Scope: ScopeLocal, Servers: map[string]ServerDef{"l": {Command: "l"}}}, {Scope: ScopeProject, Servers: map[string]ServerDef{"p": {Command: "p"}}}, {Scope: ScopeUser, Servers: map[string]ServerDef{"u": {Command: "u"}}}, } perms := []Permissions{ {Scope: ScopeUser, Allow: []string{"Bash"}}, {Scope: ScopeProject, Shown: ".claude/settings.json", Allow: []string{"Edit"}, Deny: []string{"Read(.env)"}}, {Scope: ScopeLocal, Allow: []string{"Write"}}, } g := NewGrant(mcp, perms) if len(g.Servers) != 1 || g.Servers["p"].Command != "p" { t.Errorf("servers %v", g.Servers) } if len(g.Allow) != 1 || g.Allow[0] != "Edit" || g.AllowFrom[0] != ".claude/settings.json" { t.Errorf("allow %v from %v", g.Allow, g.AllowFrom) } if len(g.Describe()) != 4 { t.Errorf("describe: %q", g.Describe()) } } func TestTrustStore(t *testing.T) { s := TrustStore{Path: filepath.Join(t.TempDir(), "codey", TrustFilename)} root := "/src/proj" if decided, _, err := s.Lookup(root, "fp1"); decided || err != nil { t.Fatalf("a fresh store decided=%v err=%v", decided, err) } if err := s.Record(root, "fp1", true); err != nil { t.Fatal(err) } if decided, trusted, _ := s.Lookup(root, "fp1"); !decided || !trusted { t.Error("the recorded decision was not found") } // Changed definitions are a new question. if decided, _, _ := s.Lookup(root, "fp2"); decided { t.Error("a decision about other definitions counted") } // Another checkout is another question. if decided, _, _ := s.Lookup("/src/other", "fp1"); decided { t.Error("a decision about another root counted") } // A refusal is remembered as a refusal. if err := s.Record(root, "fp2", false); err != nil { t.Fatal(err) } if decided, trusted, _ := s.Lookup(root, "fp2"); !decided || trusted { t.Error("the refusal was not remembered") } if decided, _, _ := s.Lookup(root, "fp1"); decided { t.Error("the old decision should have been replaced") } // Other roots' decisions survive a write. s.Record("/src/other", "x", true) if decided, _, _ := s.Lookup(root, "fp2"); !decided { t.Error("recording one root lost another") } } // A store that cannot be read is an error, not "nobody decided": the caller // treats it as untrusted, and must not overwrite it. func TestTrustStoreCorrupt(t *testing.T) { path := filepath.Join(t.TempDir(), TrustFilename) os.WriteFile(path, []byte("{"), 0o644) s := TrustStore{Path: path} if _, _, err := s.Lookup("/r", "fp"); err == nil { t.Error("want an error") } if err := s.Record("/r", "fp", true); err == nil { t.Error("recording over a corrupt store should fail rather than replace it") } }