package permission import ( "context" "sync" "testing" ) // mockEngine 是用于测试的权限引擎 mock. type mockEngine struct { mu sync.Mutex mode Mode rules []Rule } func newMockEngine() *mockEngine { return &mockEngine{ mode: ModeDefault, rules: make([]Rule, 0), } } func (m *mockEngine) Check(_ context.Context, _ *Request) (*Response, error) { return &Response{Decision: DecisionAllow}, nil } func (m *mockEngine) Mode() Mode { return m.mode } func (m *mockEngine) SetMode(mode Mode) { m.mode = mode } func (m *mockEngine) AddRule(rule Rule) { m.mu.Lock() defer m.mu.Unlock() m.rules = append(m.rules, rule) } func (m *mockEngine) ruleCount() int { m.mu.Lock() defer m.mu.Unlock() return len(m.rules) } // --- 测试 --- func TestRuleApplier_ApplySuggestions(t *testing.T) { eng := newMockEngine() learner := NewLearningTracker(2) // 阈值设为 2,方便测试 applier := NewRuleApplier(eng, learner, 0) // 模拟用户多次允许 npm 命令 learner.TrackDecision("Bash", map[string]any{"command": "npm install"}, DecisionAllow) learner.TrackDecision("Bash", map[string]any{"command": "npm test"}, DecisionAllow) // 应用建议 applied := applier.ApplySuggestions() if len(applied) != 1 { t.Fatalf("期望应用 1 条规则,实际 %d 条", len(applied)) } if applied[0].ToolName != "Bash" { t.Errorf("期望工具名 Bash,实际 %s", applied[0].ToolName) } if applied[0].Source != SourceSession { t.Errorf("期望来源 session,实际 %s", applied[0].Source) } if applied[0].Content != "prefix:npm" { t.Errorf("期望内容 prefix:npm,实际 %s", applied[0].Content) } // 引擎应该收到规则 if eng.ruleCount() != 1 { t.Errorf("引擎应有 1 条规则,实际 %d 条", eng.ruleCount()) } if applier.SessionRuleCount() != 1 { t.Errorf("session 规则数应为 1,实际 %d", applier.SessionRuleCount()) } } func TestRuleApplier_DuplicateSuggestionsSkipped(t *testing.T) { eng := newMockEngine() learner := NewLearningTracker(2) applier := NewRuleApplier(eng, learner, 0) // 模拟 npm 被允许多次 learner.TrackDecision("Bash", map[string]any{"command": "npm install"}, DecisionAllow) learner.TrackDecision("Bash", map[string]any{"command": "npm test"}, DecisionAllow) // 第一次应用 applied1 := applier.ApplySuggestions() if len(applied1) != 1 { t.Fatalf("第一次应用期望 1 条,实际 %d", len(applied1)) } // 继续允许更多 npm 命令(learner 已标记建议为 suggested,不会再次返回) learner.TrackDecision("Bash", map[string]any{"command": "npm run build"}, DecisionAllow) // 第二次应用--不应有新规则(因为 learner 已标记 suggested) applied2 := applier.ApplySuggestions() if len(applied2) != 0 { t.Errorf("第二次应用期望 0 条新规则,实际 %d", len(applied2)) } // 引擎仍然只有 1 条规则 if eng.ruleCount() != 1 { t.Errorf("引擎应有 1 条规则,实际 %d", eng.ruleCount()) } } func TestRuleApplier_MaxSessionRulesLimit(t *testing.T) { eng := newMockEngine() learner := NewLearningTracker(1) // 阈值 1,一次就够 maxRules := 3 applier := NewRuleApplier(eng, learner, maxRules) // 手动应用规则达到上限 for i := 0; i < maxRules; i++ { rule := Rule{ Behavior: DecisionAllow, ToolName: "Bash", Content: "prefix:cmd" + string(rune('A'+i)), } ok := applier.ApplyRule(rule) if !ok { t.Fatalf("第 %d 条规则应该成功应用", i+1) } } if applier.SessionRuleCount() != maxRules { t.Fatalf("session 规则数应为 %d,实际 %d", maxRules, applier.SessionRuleCount()) } // 超过上限的规则应该被拒绝 extraRule := Rule{ Behavior: DecisionAllow, ToolName: "Bash", Content: "prefix:extra", } ok := applier.ApplyRule(extraRule) if ok { t.Error("超过上限的规则应该被拒绝") } if applier.SessionRuleCount() != maxRules { t.Errorf("session 规则数应保持 %d,实际 %d", maxRules, applier.SessionRuleCount()) } } func TestRuleApplier_ApplyRuleDuplicate(t *testing.T) { eng := newMockEngine() learner := NewLearningTracker(0) applier := NewRuleApplier(eng, learner, 0) rule := Rule{ Behavior: DecisionAllow, ToolName: "Bash", Content: "prefix:npm", } // 第一次应用 ok := applier.ApplyRule(rule) if !ok { t.Error("第一次应用应成功") } // 重复应用同一规则 ok = applier.ApplyRule(rule) if ok { t.Error("重复规则应被拒绝") } if applier.SessionRuleCount() != 1 { t.Errorf("session 规则数应为 1,实际 %d", applier.SessionRuleCount()) } } func TestRuleApplier_Reset(t *testing.T) { eng := newMockEngine() learner := NewLearningTracker(0) applier := NewRuleApplier(eng, learner, 0) // 应用几条规则 applier.ApplyRule(Rule{Behavior: DecisionAllow, ToolName: "Bash", Content: "prefix:npm"}) applier.ApplyRule(Rule{Behavior: DecisionAllow, ToolName: "Bash", Content: "prefix:git"}) if applier.SessionRuleCount() != 2 { t.Fatalf("reset 前 session 规则数应为 2,实际 %d", applier.SessionRuleCount()) } // Reset applier.Reset() if applier.SessionRuleCount() != 0 { t.Errorf("reset 后 session 规则数应为 0,实际 %d", applier.SessionRuleCount()) } // reset 后可以重新应用之前的规则 ok := applier.ApplyRule(Rule{Behavior: DecisionAllow, ToolName: "Bash", Content: "prefix:npm"}) if !ok { t.Error("reset 后应该可以重新应用之前的规则") } }