Files
Agent-Native/runtime/policy/policy_test.go
T
weichen 9758a8d8d0
ci / vet-and-test (pull_request) Successful in 48s
ci / docker-checkout-hostgateway (pull_request) Successful in 1s
Add permission ask loop, lifecycle hook emissions, and Policy type
Why: The N1 harness phase needs the kernel to resolve ask decisions
through the host (paired HITL), emit the full hook lifecycle the plan
promises, and carry mode/rules/session-allow in one named object.

Evidence: go vet clean; go test ./... 24 packages ok, including new
loop_hooks_lifecycle_test.go (lifecycle order, prompt text, ask
allow/deny/legacy paths, stop continuation counts) and three new
policy tests (struct-form parity, session-allow store, nil store).

Impact: hooks adds PostToolBatch and SessionEnd events; loop emits
SessionStart/UserPromptSubmit/PostToolBatch/SessionEnd and PreCompact
lands in AgentSession.Compact; gateTool consults PermissionAsk on ask
and keeps ask=reject when the port is unset; policy gains Policy,
SessionAllows, and MemoryAllows. SubagentStart/Stop emissions are
deferred to the adapters assembly increment (factory site has no bus
access yet).

Refs: Documents proposal 2026-10-07-agent-stack-n1q1i1-plans.md WP2/WP3
2026-10-07 03:01:17 +00:00

91 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package policy
import "testing"
func TestPlanModeDeniesWrites(t *testing.T) {
effect := Decide(Request{Mode: ModePlan, Name: "write_file", Args: `{"path":"a.md"}`}, nil)
if effect != EffectDeny {
t.Fatalf("effect %q", effect)
}
if !Visible(string(ModePlan), "update_plan", false) {
t.Fatal("plan tool hidden")
}
if Visible(string(ModePlan), "write_file", false) {
t.Fatal("write visible in plan mode")
}
}
func TestDefaultModeStaysQuiet(t *testing.T) {
if got := Decide(Request{Mode: ModeDefault, Name: "write_file"}, nil); got != EffectNone {
t.Fatalf("default write %q", got)
}
if got := Decide(Request{Mode: ModeDefault, Name: "run_shell", Args: `{"command":"rm -rf /"}`}, nil); got != EffectAsk {
t.Fatalf("high shell %q", got)
}
}
func TestRulePrecedenceAndSessionAllow(t *testing.T) {
rules := []Rule{{Effect: EffectAllow, Tool: "write_file"}, {Effect: EffectDeny, Tool: "write_*", ArgGlob: "*secret*"}}
if got := Decide(Request{Name: "write_file", Args: `{"path":"secret.txt"}`}, rules); got != EffectDeny {
t.Fatalf("deny lost: %q", got)
}
if got := Decide(Request{Name: "http.fetch", SessionAllow: true}, []Rule{{Effect: EffectAsk, Tool: "http.*"}}); got != EffectAllow {
t.Fatalf("session allow %q", got)
}
}
func TestAutoEditAllowsWorkspaceWrites(t *testing.T) {
if got := Decide(Request{Mode: ModeAutoEdit, Name: "fs_write_file"}, nil); got != EffectAllow {
t.Fatalf("auto edit %q", got)
}
}
// TestPolicyDecideMatchesFunctionForm:Policy 结构体形态与函数形态判定一致。
func TestPolicyDecideMatchesFunctionForm(t *testing.T) {
rules := []Rule{{Effect: EffectAllow, Tool: "write_file"}, {Effect: EffectDeny, Tool: "write_*", ArgGlob: "*secret*"}}
cases := []struct {
p Policy
name string
args string
want Effect
}{
{Policy{Mode: ModePlan}, "write_file", `{"path":"a.md"}`, EffectDeny},
{Policy{Mode: ModeDefault, Rules: rules}, "write_file", `{"path":"a.md"}`, EffectAllow},
{Policy{Mode: ModeDefault, Rules: rules}, "write_file", `{"path":"secret.txt"}`, EffectDeny},
{Policy{Mode: ModeDefault}, "run_shell", `{"command":"rm -rf /"}`, EffectAsk},
}
for i, c := range cases {
if got := c.p.Decide(c.name, c.args, Traits{}); got != c.want {
t.Fatalf("case %d: effect %q, want %q", i, got, c.want)
}
}
}
// TestPolicySessionAllowsStore:会话放行经存储短路 ask,且跨调用存活。
func TestPolicySessionAllowsStore(t *testing.T) {
store := NewMemoryAllows()
p := Policy{Mode: ModeDefault, Rules: []Rule{{Effect: EffectAsk, Tool: "run_shell"}}, SessionAllows: store}
if got := p.Decide("run_shell", `{"command":"ls"}`, Traits{}); got != EffectAsk {
t.Fatalf("before remember: %q, want ask", got)
}
store.Remember("run_shell", `{"command":"ls"}`)
if got := p.Decide("run_shell", `{"command":"ls"}`, Traits{}); got != EffectAllow {
t.Fatalf("after remember: %q, want allow", got)
}
if got := p.Decide("run_shell", `{"command":"rm -rf /"}`, Traits{}); got != EffectAsk {
t.Fatalf("other args: %q, want ask(精确匹配不放行)", got)
}
}
// TestPolicyNilStoreAndVisible:nil 存储等价于 SessionAllow 恒 false;Visible 方法可用。
func TestPolicyNilStoreAndVisible(t *testing.T) {
p := Policy{Mode: ModePlan}
if got := p.Decide("write_file", `{}`, Traits{}); got != EffectDeny {
t.Fatalf("plan deny: %q", got)
}
if !p.Visible("update_plan") || p.Visible("write_file") {
t.Fatal("policy visible mismatch")
}
}