返回 DeepSeek-Reasonix
inbox_app_review_test.go
根目录 / desktop / inbox_app_review_test.go
1 package main
2
3 import (
4 "errors"
5 "fmt"
6 "path/filepath"
7 "testing"
8
9 "reasonix/internal/control"
10 "reasonix/internal/event"
11 "reasonix/internal/sessioninbox"
12 )
13
14 func TestInboxWailsErrorsUseStableCodes(t *testing.T) {
15 tests := []struct {
16 name string
17 err error
18 code string
19 }{
20 {name: "paused", err: sessioninbox.ErrPaused, code: "inbox_paused"},
21 {name: "item capacity", err: sessioninbox.ErrCapacityItems, code: "inbox_capacity_items"},
22 {name: "byte capacity", err: sessioninbox.ErrCapacityBytes, code: "inbox_capacity_bytes"},
23 {name: "item too large", err: sessioninbox.ErrItemTooLarge, code: "inbox_item_too_large"},
24 {name: "not found", err: sessioninbox.ErrNotFound, code: "inbox_item_not_found"},
25 {name: "invalid state", err: sessioninbox.ErrInvalidState, code: "inbox_invalid_state"},
26 {name: "newer schema", err: sessioninbox.ErrSchemaReadonly, code: "inbox_schema_readonly"},
27 {name: "closed", err: sessioninbox.ErrClosed, code: "inbox_closed"},
28 {name: "empty", err: sessioninbox.ErrEmpty, code: "inbox_empty"},
29 {name: "idempotency conflict", err: sessioninbox.ErrIdempotencyConflict, code: "inbox_idempotency_conflict"},
30 {name: "wrapped sentinel", err: fmt.Errorf("steer: %w", sessioninbox.ErrPaused), code: "inbox_paused"},
31 {name: "read only channel", err: errors.New("channel session is read-only"), code: "channel_read_only"},
32 {name: "workspace starting", err: errors.New("workspace is still starting"), code: "workspace_starting"},
33 {name: "workspace start failed", err: errors.New("workspace failed to start: internal detail"), code: "workspace_start_failed"},
34 }
35 for _, tt := range tests {
36 t.Run(tt.name, func(t *testing.T) {
37 got := inboxBridgeError(tt.err)
38 if got.Error() != "reasonix_error:"+tt.code {
39 t.Fatalf("error = %q, want stable code %q", got, tt.code)
40 }
41 if !errors.Is(got, tt.err) {
42 t.Fatalf("wrapped error no longer preserves errors.Is for %v", tt.err)
43 }
44 })
45 }
46
47 unknown := errors.New("filesystem detail: /private/example")
48 //nolint:errorlint // Identity is the contract: unknown diagnostics must not be wrapped.
49 if got := inboxBridgeError(unknown); got != unknown {
50 t.Fatalf("unknown diagnostic error = %q, want original error", got)
51 }
52 }
53
54 func TestSteerInboxItemPausedReturnsStableCode(t *testing.T) {
55 dir := t.TempDir()
56 ctrl := control.New(control.Options{
57 SessionDir: dir, SessionPath: filepath.Join(dir, "session.jsonl"), Sink: event.Discard,
58 })
59 t.Cleanup(ctrl.Close)
60 if err := ctrl.SetInboxPaused(true); err != nil {
61 t.Fatal(err)
62 }
63 app := NewApp()
64 app.setTestCtrl(ctrl, "")
65 receipt, err := app.EnqueueInboxFollowup("test", "guide the turn", "guide the turn", "paused-steer")
66 if err != nil {
67 t.Fatal(err)
68 }
69 failedReceipt, err := app.SteerInboxItem("test", receipt.ItemID)
70 if err == nil || err.Error() != "reasonix_error:inbox_paused" {
71 t.Fatalf("SteerInboxItem error = %v, want stable paused code", err)
72 }
73 if failedReceipt.Error != "reasonix_error:inbox_paused" {
74 t.Fatalf("SteerInboxItem receipt error = %q, want stable paused code", failedReceipt.Error)
75 }
76 }
77
78 func TestEnqueueInboxSteerWhenPausedQueuesFollowup(t *testing.T) {
79 dir := t.TempDir()
80 ctrl := control.New(control.Options{
81 SessionDir: dir, SessionPath: filepath.Join(dir, "session.jsonl"), Sink: event.Discard,
82 })
83 t.Cleanup(ctrl.Close)
84 if err := ctrl.SetInboxPaused(true); err != nil {
85 t.Fatal(err)
86 }
87 app := NewApp()
88 app.setTestCtrl(ctrl, "")
89 receipt, err := app.EnqueueInboxSteer("test", "guide later", "guide later", "paused-enqueue-steer")
90 if err != nil {
91 t.Fatal(err)
92 }
93 if receipt.Disposition != string(sessioninbox.DispositionQueuedFollowup) || !receipt.Paused {
94 t.Fatalf("EnqueueInboxSteer receipt = %+v, want queued_followup while paused", receipt)
95 }
96 }
97
98 func TestDurableInvocationFollowupPreservesEmptyExplicitTask(t *testing.T) {
99 dir := t.TempDir()
100 ctrl := control.New(control.Options{
101 SessionDir: dir, SessionPath: filepath.Join(dir, "session.jsonl"), Sink: event.Discard,
102 })
103 t.Cleanup(ctrl.Close)
104 if err := ctrl.SetInboxPaused(true); err != nil {
105 t.Fatal(err)
106 }
107 app := NewApp()
108 app.setTestCtrl(ctrl, "")
109
110 rec, err := app.EnqueueInboxFollowupWithInvocations(
111 "test", "/init", "", []InvocationRequest{{Name: "init", Kind: "skill"}}, "desktop-submit-1",
112 )
113 if err != nil {
114 t.Fatal(err)
115 }
116 _, env, err := ctrl.ReadInboxItem(rec.ItemID)
117 if err != nil {
118 t.Fatal(err)
119 }
120 if env.SubmitText != "" || env.RawText != "" {
121 t.Fatalf("entity-only durable task degraded to text: submit=%q raw=%q", env.SubmitText, env.RawText)
122 }
123 if len(env.Invocations) != 1 || env.Invocations[0].Name != "init" || env.Invocations[0].Kind != "skill" {
124 t.Fatalf("durable invocation metadata = %+v", env.Invocations)
125 }
126 }
127
127 lines GO