package restrict import ( "context" "errors" "testing" ) // reviewStub 是 Reviewer 的桩:记录调用、按配置返回结论或错误。 type reviewStub struct { allowed bool err error calls []reviewCall } type reviewCall struct{ actor, action, describe string } func (s *reviewStub) ReviewAndApprove(_ context.Context, actor, action, describe string) (bool, error) { s.calls = append(s.calls, reviewCall{actor: actor, action: action, describe: describe}) if s.err != nil { return false, s.err } return s.allowed, nil } // staticResolver 固定返回一遍归属。 func staticResolver(sessionID, actor string) Resolver { return func(context.Context) (string, string) { return sessionID, actor } } // TestGate_NonSensitivePassesWithoutApproval 锁定「非敏感自主」:读文件/检索 // 不进代审、不产生记录。 func TestGate_NonSensitivePassesWithoutApproval(t *testing.T) { t.Parallel() rv := &reviewStub{allowed: true} g := NewGate(rv, staticResolver("web", "昔涟")) d := g.Admit(context.Background(), "file", map[string]any{"action": "read", "path": "/data/a.md"}) if !d.Allow { t.Fatalf("读文件应自主放行,得到 %+v", d) } if d.Approval != nil { t.Fatalf("非敏感调用不应产生代审记录: %+v", d.Approval) } if len(rv.calls) != 0 { t.Fatalf("非敏感调用不应触发代审,代审者被调用 %d 次", len(rv.calls)) } } // TestGate_SensitiveToolTriggersApproval 是裁决 13.4.3 的反向用例(loop 侧): // terminal 不再自主执行,而是触发主代理代审,并把记录交给调用方落审计。 func TestGate_SensitiveToolTriggersApproval(t *testing.T) { t.Parallel() rv := &reviewStub{allowed: true} g := NewGate(rv, staticResolver("web", "昔涟")) d := g.Admit(context.Background(), "terminal", map[string]any{"command": "echo hi"}) if !d.Allow { t.Fatalf("代审批准后应放行,得到 %+v", d) } if len(rv.calls) != 1 { t.Fatalf("代审者调用次数 = %d, want 1", len(rv.calls)) } if rv.calls[0].actor != "昔涟" { t.Fatalf("代审归属 actor = %q, want 昔涟", rv.calls[0].actor) } if rv.calls[0].action != "执行命令:terminal echo hi" { t.Fatalf("代审动作 = %q, want 执行命令:terminal echo hi", rv.calls[0].action) } ev := d.Approval if ev == nil { t.Fatal("敏感调用放行也必须留下代审记录(design/11 §8:批准/拒绝各落一行)") } if !ev.Allowed || ev.SessionID != "web" || ev.Actor != "昔涟" || ev.Tool != "terminal" || ev.Op != SensitiveExecCommand { t.Fatalf("代审记录 = %+v", ev) } if ev.Explain != "「昔涟 想要干什么,我审查后批准了」" { t.Fatalf("前端小字 = %q, want design/11 §4 的固定句式", ev.Explain) } if ev.At.IsZero() { t.Fatal("代审记录应带时间戳") } } // TestGate_DeniedSensitiveCallIsBlocked 锁定拒绝路径:代审不放行 → 调用被拦截、 // 回灌软提示、记录里 Allowed=false 且小字换成「拒绝了」。 func TestGate_DeniedSensitiveCallIsBlocked(t *testing.T) { t.Parallel() rv := &reviewStub{allowed: false} g := NewGate(rv, staticResolver("web", "昔涟")) d := g.Admit(context.Background(), "terminal", map[string]any{"command": "rm -rf /"}) if d.Allow { t.Fatalf("代审未放行却被放行: %+v", d) } if d.Reason == "" { t.Fatal("拦截必须给出回灌给模型的说明") } if d.Approval == nil || d.Approval.Allowed { t.Fatalf("拒绝也要落记录且 Allowed=false: %+v", d.Approval) } if d.Approval.Explain != "「昔涟 想要干什么,我审查后拒绝了」" { t.Fatalf("拒绝小字 = %q", d.Approval.Explain) } } // TestGate_ReviewerFailureIsFailClosed 锁定 fail-closed:代审者报错(或压根 // 没配)时敏感操作一律拦截并留下拒绝记录,绝不静默放行。 func TestGate_ReviewerFailureIsFailClosed(t *testing.T) { t.Parallel() ctx := context.Background() for name, g := range map[string]*Gate{ "reviewer error": NewGate(&reviewStub{err: errors.New("approver down")}, staticResolver("web", "昔涟")), "no reviewer": NewGate(nil, staticResolver("web", "昔涟")), } { d := g.Admit(ctx, "terminal", map[string]any{"command": "echo hi"}) if d.Allow { t.Fatalf("%s: 敏感调用被放行,应 fail-closed 拦截", name) } if d.Approval == nil || d.Approval.Allowed { t.Fatalf("%s: 应留下 Allowed=false 的代审记录: %+v", name, d.Approval) } } } // TestGate_NilGateDeniesSensitive 锁定零值安全:未装配的门(nil 接收者)碰到 // 敏感调用按拦截处理,而不是 panic 或放行。 func TestGate_NilGateDeniesSensitive(t *testing.T) { t.Parallel() var g *Gate if d := g.Admit(context.Background(), "terminal", map[string]any{"command": "echo hi"}); d.Allow { t.Fatalf("nil 门应拦截敏感调用: %+v", d) } if d := g.Admit(context.Background(), "calculator", nil); !d.Allow { t.Fatalf("nil 门的非敏感调用仍应放行: %+v", d) } }