| 1 | package acp |
| 2 | |
| 3 | import ( |
| 4 | "encoding/json" |
| 5 | "testing" |
| 6 | "time" |
| 7 | |
| 8 | "reasonix/internal/event" |
| 9 | ) |
| 10 | |
| 11 | func TestUpdateSinkWriteAccessOptionsMapScopes(t *testing.T) { |
| 12 | fn := &fakeNotifier{onReq: func(_ string, params any) (json.RawMessage, error) { |
| 13 | raw, _ := json.Marshal(params) |
| 14 | var p PermissionRequestParams |
| 15 | if err := json.Unmarshal(raw, &p); err != nil { |
| 16 | t.Fatalf("permission params: %v", err) |
| 17 | } |
| 18 | assertACPv1PermissionOptionKinds(t, p.Options) |
| 19 | if len(p.Options) != 3 || |
| 20 | p.Options[0].OptionID != "reasonix_write_once" || p.Options[0].Kind != OptAllowOnce || |
| 21 | p.Options[1].OptionID != "reasonix_write_session" || p.Options[1].Kind != OptAllowAlways || |
| 22 | p.Options[2].OptionID != "reasonix_write_deny" || p.Options[2].Kind != OptRejectOnce { |
| 23 | t.Fatalf("write-access options = %+v", p.Options) |
| 24 | } |
| 25 | meta, _ := p.ToolCall.Meta["reasonix.io"].(map[string]any) |
| 26 | if meta["kind"] != event.ApprovalKindWriteAccess { |
| 27 | t.Fatalf("meta = %+v", meta) |
| 28 | } |
| 29 | res, _ := json.Marshal(PermissionRequestResult{ |
| 30 | Outcome: PermissionOutcome{Outcome: "selected", OptionID: "reasonix_write_session"}, |
| 31 | }) |
| 32 | return res, nil |
| 33 | }} |
| 34 | sink := newUpdateSink(fn, "sess-1") |
| 35 | got := make(chan approveCall, 1) |
| 36 | sink.bindApprove(func(id string, allow, session, persist bool) { got <- approveCall{id, allow, session, persist} }) |
| 37 | sink.Emit(event.Event{Kind: event.ApprovalRequest, Approval: event.Approval{ |
| 38 | ID: "wa-1", Tool: "bash", Subject: "install", Kind: event.ApprovalKindWriteAccess, |
| 39 | WriteAccess: event.NormalizeWriteAccessApproval(&event.WriteAccessApproval{ |
| 40 | Directories: []string{"/tmp/out"}, DisplayDirectories: []string{"~/.local"}, Justification: "install tool", |
| 41 | }), |
| 42 | }}) |
| 43 | select { |
| 44 | case c := <-got: |
| 45 | if c != (approveCall{id: "wa-1", allow: true, session: true}) { |
| 46 | t.Fatalf("approve = %+v", c) |
| 47 | } |
| 48 | case <-time.After(2 * time.Second): |
| 49 | t.Fatal("write-access approve was never called") |
| 50 | } |
| 51 | } |
| 52 | |
| 53 | func TestUpdateSinkWriteAccessLegacyAllowOnce(t *testing.T) { |
| 54 | fn := &fakeNotifier{onReq: func(_ string, _ any) (json.RawMessage, error) { |
| 55 | res, _ := json.Marshal(PermissionRequestResult{Outcome: PermissionOutcome{Outcome: "selected", OptionID: string(OptAllowOnce)}}) |
| 56 | return res, nil |
| 57 | }} |
| 58 | sink := newUpdateSink(fn, "sess-1") |
| 59 | got := make(chan approveCall, 1) |
| 60 | sink.bindApprove(func(id string, allow, session, persist bool) { got <- approveCall{id, allow, session, persist} }) |
| 61 | sink.Emit(event.Event{Kind: event.ApprovalRequest, Approval: event.Approval{ |
| 62 | ID: "wa-2", Tool: "write_file", Kind: event.ApprovalKindWriteAccess, |
| 63 | WriteAccess: &event.WriteAccessApproval{Directories: []string{"/tmp/out"}}, |
| 64 | }}) |
| 65 | select { |
| 66 | case c := <-got: |
| 67 | if c != (approveCall{id: "wa-2", allow: true}) { |
| 68 | t.Fatalf("legacy allow_once = %+v", c) |
| 69 | } |
| 70 | case <-time.After(2 * time.Second): |
| 71 | t.Fatal("legacy write-access approve was never called") |
| 72 | } |
| 73 | } |
| 74 |