@pi-in-go/pigpen-acp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CREDITS.md +23 -0
- package/LICENSE +22 -0
- package/README.md +110 -0
- package/extensions/acp/cmd/pig-acp/e2e_llm_test.go +168 -0
- package/extensions/acp/cmd/pig-acp/e2e_test.go +541 -0
- package/extensions/acp/cmd/pig-acp/go.mod +6 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent.go +784 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_builtin_test.go +536 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_commands_test.go +199 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_config_test.go +530 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_helpers_test.go +215 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_load_test.go +333 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_prompt.go +289 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/agent_session_test.go +429 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/auth.go +61 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/bench_test.go +47 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/helpers_test.go +421 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/main_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/mutation_extra_test.go +142 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/paths.go +77 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/pisessions.go +317 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/protocol.go +188 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/server.go +218 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/server_test.go +386 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session.go +1068 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_diff_test.go +183 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_events_test.go +443 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_extra_test.go +547 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/session_usage_test.go +198 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/settings.go +79 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/slash.go +278 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/slash_test.go +128 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/startup.go +156 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/store.go +118 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/store_paths_test.go +466 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/translate.go +352 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/translate_test.go +105 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/acp/types.go +202 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/bench_test.go +24 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc.go +302 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/jsonrpc_test.go +320 -0
- package/extensions/acp/cmd/pig-acp/internal/jsonrpc/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_unix.go +19 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/kill_windows.go +11 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/process.go +530 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/process_test.go +355 -0
- package/extensions/acp/cmd/pig-acp/internal/pirpc/twin_test.go +28 -0
- package/extensions/acp/cmd/pig-acp/main.go +177 -0
- package/extensions/acp/cmd/pig-acp/main_test.go +213 -0
- package/extensions/acp/cmd/pig-acp/sigpipe_unix.go +10 -0
- package/extensions/acp/cmd/pig-acp/sigpipe_windows.go +5 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/extension.go +28 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/go.mod +7 -0
- package/extensions/acp/cmd/pig-acp/testdata/ask/go.sum +2 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/extension.go +37 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/go.mod +7 -0
- package/extensions/acp/cmd/pig-acp/testdata/gate/go.sum +2 -0
- package/extensions/acp/cmd/pig-acp/twin_test.go +28 -0
- package/extensions/acp/extension.go +63 -0
- package/extensions/acp/extension_test.go +111 -0
- package/extensions/acp/fakehost_test.go +548 -0
- package/extensions/acp/go.mod +6 -0
- package/extensions/acp/go.sum +2 -0
- package/package.json +41 -0
- package/port/PORT.md +147 -0
- package/port/interop/package-lock.json +89 -0
- package/port/interop/package.json +13 -0
- package/port/interop/run.mjs +229 -0
- package/port/mutate.mjs +110 -0
- package/port/mutation-results.txt +107 -0
- package/port/mutations.json +748 -0
- package/port/oracle/pi-acp/LICENSE +21 -0
- package/port/oracle/pi-acp/README.md +209 -0
- package/port/oracle/pi-acp/package.json +65 -0
- package/port/oracle/pi-acp/src/acp/agent.ts +1661 -0
- package/port/oracle/pi-acp/src/acp/auth-required.ts +37 -0
- package/port/oracle/pi-acp/src/acp/auth.ts +59 -0
- package/port/oracle/pi-acp/src/acp/paths.ts +15 -0
- package/port/oracle/pi-acp/src/acp/pi-commands.ts +59 -0
- package/port/oracle/pi-acp/src/acp/pi-sessions.ts +333 -0
- package/port/oracle/pi-acp/src/acp/pi-settings.ts +75 -0
- package/port/oracle/pi-acp/src/acp/session-store.ts +68 -0
- package/port/oracle/pi-acp/src/acp/session.ts +1078 -0
- package/port/oracle/pi-acp/src/acp/slash-commands.ts +197 -0
- package/port/oracle/pi-acp/src/acp/translate/bash.ts +100 -0
- package/port/oracle/pi-acp/src/acp/translate/pi-messages.ts +17 -0
- package/port/oracle/pi-acp/src/acp/translate/pi-tools.ts +51 -0
- package/port/oracle/pi-acp/src/acp/translate/prompt.ts +71 -0
- package/port/oracle/pi-acp/src/index.ts +82 -0
- package/port/oracle/pi-acp/src/pi-rpc/command.ts +16 -0
- package/port/oracle/pi-acp/src/pi-rpc/process.ts +432 -0
- package/port/oracle/pi-acp/test/component/agent-steering-followup-modes.test.ts +143 -0
- package/port/oracle/pi-acp/test/component/session-diff.test.ts +214 -0
- package/port/oracle/pi-acp/test/component/session-events.test.ts +1102 -0
- package/port/oracle/pi-acp/test/component/session-list-and-load.test.ts +129 -0
- package/port/oracle/pi-acp/test/component/session-list-custom-session-dir.test.ts +52 -0
- package/port/oracle/pi-acp/test/component/session-list-scoped.test.ts +76 -0
- package/port/oracle/pi-acp/test/component/session-load-toolresult.test.ts +68 -0
- package/port/oracle/pi-acp/test/component/session-queue-cancel.test.ts +43 -0
- package/port/oracle/pi-acp/test/component/session-slash-commands.test.ts +37 -0
- package/port/oracle/pi-acp/test/component/session-thinking-modes.test.ts +22 -0
- package/port/oracle/pi-acp/test/component/session-title-long-session.test.ts +56 -0
- package/port/oracle/pi-acp/test/component/session-updatedAt-message-only.test.ts +57 -0
- package/port/oracle/pi-acp/test/helpers/fakes.ts +87 -0
- package/port/oracle/pi-acp/test/unit/auth-methods-terminal-auth-meta.test.ts +22 -0
- package/port/oracle/pi-acp/test/unit/builtin-commands.test.ts +60 -0
- package/port/oracle/pi-acp/test/unit/context-usage.test.ts +204 -0
- package/port/oracle/pi-acp/test/unit/merge-commands.test.ts +19 -0
- package/port/oracle/pi-acp/test/unit/model-thinking-levels.test.ts +268 -0
- package/port/oracle/pi-acp/test/unit/new-session-auth-required-when-no-models.test.ts +51 -0
- package/port/oracle/pi-acp/test/unit/new-session-pi-not-found.test.ts +26 -0
- package/port/oracle/pi-acp/test/unit/new-session-runtime-startup-errors.test.ts +148 -0
- package/port/oracle/pi-acp/test/unit/pi-command.test.ts +44 -0
- package/port/oracle/pi-acp/test/unit/pi-commands.test.ts +32 -0
- package/port/oracle/pi-acp/test/unit/pi-enable-embed-context-flag.test.ts +35 -0
- package/port/oracle/pi-acp/test/unit/pi-messages.test.ts +29 -0
- package/port/oracle/pi-acp/test/unit/pi-rpc-request-timeout.test.ts +137 -0
- package/port/oracle/pi-acp/test/unit/pi-rpc-session-path.test.ts +93 -0
- package/port/oracle/pi-acp/test/unit/pi-tools.test.ts +40 -0
- package/port/oracle/pi-acp/test/unit/prompt-to-pi-message.test.ts +74 -0
- package/port/oracle/pi-acp/test/unit/session-config-options.test.ts +217 -0
- package/port/oracle/pi-acp/test/unit/session-delete.test.ts +170 -0
- package/port/oracle/pi-acp/test/unit/session-restore.test.ts +239 -0
- package/port/oracle/pi-acp/test/unit/slash-commands.test.ts +36 -0
- package/port/oracle/pi-acp/test/unit/startup-info-env.test.ts +84 -0
- package/port/oracle/pi-acp/test/unit/startup-info-load-session.test.ts +53 -0
- package/port/oracle/pi-acp/test/unit/startup-info-project-packages.test.ts +74 -0
- package/port/oracle/pi-acp/test/unit/stdout-destroyed-does-not-crash.test.ts +32 -0
- package/port/oracle/pi-acp/test/unit/thinking-level-rpc.test.ts +43 -0
- package/port/schema/LICENSE +191 -0
- package/port/schema/schema.json +8015 -0
- package/port/upstream-tests.json +201 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
package acp
|
|
2
|
+
|
|
3
|
+
// Twins of test/component/session-diff.test.ts.
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"testing"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func completedToolUpdate(c *fakeConn, id string) Update {
|
|
12
|
+
for _, u := range c.all() {
|
|
13
|
+
if u.Update["toolCallId"] == id && u.Update["sessionUpdate"] == "tool_call_update" && u.Update["status"] == "completed" {
|
|
14
|
+
return u.Update
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return nil
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
func findDiff(update Update) map[string]any {
|
|
21
|
+
items, _ := update["content"].([]any)
|
|
22
|
+
for _, i := range items {
|
|
23
|
+
if m, _ := i.(map[string]any); m["type"] == "diff" {
|
|
24
|
+
return m
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return nil
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func okResult(text string) Event {
|
|
31
|
+
return Event{"type": "tool_execution_end", "toolCallId": "t1", "isError": false, "result": map[string]any{"content": []any{map[string]any{"type": "text", "text": text}}}}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
func write(t *testing.T, p, content string) {
|
|
35
|
+
t.Helper()
|
|
36
|
+
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
37
|
+
t.Fatal(err)
|
|
38
|
+
}
|
|
39
|
+
if err := os.WriteFile(p, []byte(content), 0o644); err != nil {
|
|
40
|
+
t.Fatal(err)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
func TestSessionDiff(t *testing.T) {
|
|
45
|
+
newSession := func(t *testing.T, dir string) (*fakeConn, *fakeProc, *Session) {
|
|
46
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
47
|
+
return conn, proc, newTestSession(dir, proc, conn)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
tw(t, "component/session-diff", "PiAcpSession: emits ACP diff content for edit tool from actual before/after file contents", func(t *testing.T) {
|
|
51
|
+
dir := tmp(t)
|
|
52
|
+
file := filepath.Join(dir, "a.txt")
|
|
53
|
+
write(t, file, "before\n")
|
|
54
|
+
conn, proc, s := newSession(t, dir)
|
|
55
|
+
proc.emit(Event{"type": "tool_execution_start", "toolCallId": "t1", "toolName": "edit", "args": map[string]any{"path": "a.txt"}})
|
|
56
|
+
write(t, file, "after\n")
|
|
57
|
+
proc.emit(okResult("ok"))
|
|
58
|
+
settle(t, s)
|
|
59
|
+
end := completedToolUpdate(conn, "t1")
|
|
60
|
+
if end == nil {
|
|
61
|
+
t.Fatal("expected completed tool_call_update")
|
|
62
|
+
}
|
|
63
|
+
if _, ok := end["content"].([]any); !ok {
|
|
64
|
+
t.Fatal("expected content array")
|
|
65
|
+
}
|
|
66
|
+
d := findDiff(end)
|
|
67
|
+
if d == nil {
|
|
68
|
+
t.Fatal("expected diff content item")
|
|
69
|
+
}
|
|
70
|
+
if d["path"] != "a.txt" || d["oldText"] != "before\n" || d["newText"] != "after\n" {
|
|
71
|
+
t.Errorf("diff = %v", d)
|
|
72
|
+
}
|
|
73
|
+
if _, ok := end["rawOutput"]; ok {
|
|
74
|
+
t.Error("expected raw output to be suppressed when diff is emitted")
|
|
75
|
+
}
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
tw(t, "component/session-diff", "PiAcpSession: does not turn requested edit args into finalized ACP diffs at tool start", func(t *testing.T) {
|
|
79
|
+
dir := tmp(t)
|
|
80
|
+
file := filepath.Join(dir, "a.txt")
|
|
81
|
+
write(t, file, "before\n")
|
|
82
|
+
conn, proc, s := newSession(t, dir)
|
|
83
|
+
proc.emit(Event{"type": "tool_execution_start", "toolCallId": "t1", "toolName": "edit",
|
|
84
|
+
"args": map[string]any{"path": "a.txt", "edits": []any{map[string]any{"oldText": "before", "newText": "after"}}}})
|
|
85
|
+
settle(t, s)
|
|
86
|
+
var start Update
|
|
87
|
+
for _, u := range conn.all() {
|
|
88
|
+
if u.Update["toolCallId"] == "t1" && u.Update["sessionUpdate"] == "tool_call" {
|
|
89
|
+
start = u.Update
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if start == nil {
|
|
93
|
+
t.Fatal("expected tool_call for edit start")
|
|
94
|
+
}
|
|
95
|
+
if _, ok := start["content"]; ok {
|
|
96
|
+
t.Error("expected no start-time diff from requested edit args")
|
|
97
|
+
}
|
|
98
|
+
write(t, file, "after\n")
|
|
99
|
+
proc.emit(okResult("ok"))
|
|
100
|
+
settle(t, s)
|
|
101
|
+
end := completedToolUpdate(conn, "t1")
|
|
102
|
+
if end == nil {
|
|
103
|
+
t.Fatal("expected completed tool_call_update")
|
|
104
|
+
}
|
|
105
|
+
d := findDiff(end)
|
|
106
|
+
if d == nil || d["oldText"] != "before\n" || d["newText"] != "after\n" {
|
|
107
|
+
t.Errorf("diff = %v", d)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
tw(t, "component/session-diff", "PiAcpSession: edit diff uses realized fuzzy-match file contents instead of requested args", func(t *testing.T) {
|
|
112
|
+
dir := tmp(t)
|
|
113
|
+
file := filepath.Join(dir, "fuzzy.txt")
|
|
114
|
+
write(t, file, "FULLWIDTH: ABC123\n")
|
|
115
|
+
conn, proc, s := newSession(t, dir)
|
|
116
|
+
proc.emit(Event{"type": "tool_execution_start", "toolCallId": "t1", "toolName": "edit",
|
|
117
|
+
"args": map[string]any{"path": "fuzzy.txt", "edits": []any{map[string]any{"oldText": "FULLWIDTH: ABC123", "newText": "FULLWIDTH: ascii replacement"}}}})
|
|
118
|
+
write(t, file, "FULLWIDTH: ascii replacement\n")
|
|
119
|
+
proc.emit(okResult("Successfully replaced 1 block(s) in fuzzy.txt."))
|
|
120
|
+
settle(t, s)
|
|
121
|
+
end := completedToolUpdate(conn, "t1")
|
|
122
|
+
if end == nil {
|
|
123
|
+
t.Fatal("expected completed tool_call_update")
|
|
124
|
+
}
|
|
125
|
+
d := findDiff(end)
|
|
126
|
+
if d == nil || d["oldText"] != "FULLWIDTH: ABC123\n" || d["newText"] != "FULLWIDTH: ascii replacement\n" {
|
|
127
|
+
t.Errorf("diff = %v", d)
|
|
128
|
+
}
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
tw(t, "component/session-diff", "PiAcpSession: emits write diff content from actual before/after file contents on completion", func(t *testing.T) {
|
|
132
|
+
dir := tmp(t)
|
|
133
|
+
file := filepath.Join(dir, "a.txt")
|
|
134
|
+
write(t, file, "before\n")
|
|
135
|
+
conn, proc, s := newSession(t, dir)
|
|
136
|
+
proc.emit(Event{"type": "tool_execution_start", "toolCallId": "t1", "toolName": "write", "args": map[string]any{"path": "a.txt", "content": "after\n"}})
|
|
137
|
+
settle(t, s)
|
|
138
|
+
var start Update
|
|
139
|
+
for _, u := range conn.all() {
|
|
140
|
+
if u.Update["toolCallId"] == "t1" && u.Update["sessionUpdate"] == "tool_call" {
|
|
141
|
+
start = u.Update
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
if start == nil {
|
|
145
|
+
t.Fatal("expected tool_call for write start")
|
|
146
|
+
}
|
|
147
|
+
if _, ok := start["content"]; ok {
|
|
148
|
+
t.Error("expected no start-time diff for write")
|
|
149
|
+
}
|
|
150
|
+
write(t, file, "after\n")
|
|
151
|
+
proc.emit(okResult("Successfully wrote 6 bytes to a.txt"))
|
|
152
|
+
settle(t, s)
|
|
153
|
+
end := completedToolUpdate(conn, "t1")
|
|
154
|
+
if end == nil {
|
|
155
|
+
t.Fatal("expected completed tool_call_update")
|
|
156
|
+
}
|
|
157
|
+
d := findDiff(end)
|
|
158
|
+
if d == nil || d["path"] != "a.txt" || d["oldText"] != "before\n" || d["newText"] != "after\n" {
|
|
159
|
+
t.Errorf("diff = %v", d)
|
|
160
|
+
}
|
|
161
|
+
if _, ok := end["rawOutput"]; ok {
|
|
162
|
+
t.Error("expected raw output to be suppressed when diff is emitted")
|
|
163
|
+
}
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
tw(t, "component/session-diff", "PiAcpSession: emits write diff content for new files on completion", func(t *testing.T) {
|
|
167
|
+
dir := tmp(t)
|
|
168
|
+
file := filepath.Join(dir, "new.txt")
|
|
169
|
+
conn, proc, s := newSession(t, dir)
|
|
170
|
+
proc.emit(Event{"type": "tool_execution_start", "toolCallId": "t1", "toolName": "write", "args": map[string]any{"path": "new.txt", "content": "created\n"}})
|
|
171
|
+
write(t, file, "created\n")
|
|
172
|
+
proc.emit(okResult("Successfully wrote 8 bytes to new.txt"))
|
|
173
|
+
settle(t, s)
|
|
174
|
+
end := completedToolUpdate(conn, "t1")
|
|
175
|
+
if end == nil {
|
|
176
|
+
t.Fatal("expected completed tool_call_update")
|
|
177
|
+
}
|
|
178
|
+
d := findDiff(end)
|
|
179
|
+
if d == nil || d["path"] != "new.txt" || d["oldText"] != nil || d["newText"] != "created\n" {
|
|
180
|
+
t.Errorf("diff = %v", d)
|
|
181
|
+
}
|
|
182
|
+
})
|
|
183
|
+
}
|
|
@@ -0,0 +1,443 @@
|
|
|
1
|
+
package acp
|
|
2
|
+
|
|
3
|
+
// Twins of test/component/session-events.test.ts. Each test keeps the upstream test name.
|
|
4
|
+
|
|
5
|
+
import (
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
func cwdNow(t testing.TB) string {
|
|
13
|
+
t.Helper()
|
|
14
|
+
d, err := os.Getwd()
|
|
15
|
+
if err != nil {
|
|
16
|
+
t.Fatal(err)
|
|
17
|
+
}
|
|
18
|
+
return d
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func textDelta(s string) Event {
|
|
22
|
+
return Event{"type": "message_update", "assistantMessageEvent": map[string]any{"type": "text_delta", "delta": s}}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
func chunk(text string) Update {
|
|
26
|
+
return Update{"sessionUpdate": "agent_message_chunk", "content": map[string]any{"type": "text", "text": text}}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
func TestSessionEvents(t *testing.T) {
|
|
30
|
+
tw(t, "component/session-events", "PiAcpSession: emits agent_message_chunk for text_delta", func(t *testing.T) {
|
|
31
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
32
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
33
|
+
proc.emit(textDelta("hi"))
|
|
34
|
+
settle(t, s)
|
|
35
|
+
ups := conn.all()
|
|
36
|
+
if len(ups) != 1 || ups[0].SessionID != "s1" {
|
|
37
|
+
t.Fatalf("updates = %+v", ups)
|
|
38
|
+
}
|
|
39
|
+
jsonEqual(t, ups[0].Update, chunk("hi"))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
tw(t, "component/session-events", "PiAcpSession: emits agent_thought_chunk for thinking_delta", func(t *testing.T) {
|
|
43
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
44
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
45
|
+
proc.emit(Event{"type": "message_update", "assistantMessageEvent": map[string]any{"type": "thinking_delta", "delta": "thinking..."}})
|
|
46
|
+
settle(t, s)
|
|
47
|
+
ups := conn.all()
|
|
48
|
+
if len(ups) != 1 || ups[0].SessionID != "s1" {
|
|
49
|
+
t.Fatalf("updates = %+v", ups)
|
|
50
|
+
}
|
|
51
|
+
jsonEqual(t, ups[0].Update, Update{"sessionUpdate": "agent_thought_chunk", "content": map[string]any{"type": "text", "text": "thinking..."}})
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
tw(t, "component/session-events", "PiAcpSession: emits tool_call + tool_call_update + completes", func(t *testing.T) {
|
|
55
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
56
|
+
cwd := cwdNow(t)
|
|
57
|
+
s := newTestSession(cwd, proc, conn)
|
|
58
|
+
proc.emit(Event{"type": "tool_execution_start", "toolCallId": "t1", "toolName": "bash", "args": map[string]any{"command": "ls"}})
|
|
59
|
+
proc.emit(Event{"type": "tool_execution_update", "toolCallId": "t1", "partialResult": map[string]any{"content": []any{map[string]any{"type": "text", "text": "running"}}}})
|
|
60
|
+
proc.emit(Event{"type": "tool_execution_end", "toolCallId": "t1", "isError": false, "result": map[string]any{"content": []any{map[string]any{"type": "text", "text": "done"}}}})
|
|
61
|
+
settle(t, s)
|
|
62
|
+
ups := conn.all()
|
|
63
|
+
if len(ups) != 3 {
|
|
64
|
+
t.Fatalf("got %d updates: %+v", len(ups), ups)
|
|
65
|
+
}
|
|
66
|
+
u0 := ups[0].Update
|
|
67
|
+
if u0["sessionUpdate"] != "tool_call" || u0["toolCallId"] != "t1" || u0["title"] != "ls" || u0["kind"] != "execute" || u0["status"] != "in_progress" {
|
|
68
|
+
t.Errorf("update 0 = %v", u0)
|
|
69
|
+
}
|
|
70
|
+
if _, ok := u0["locations"]; ok {
|
|
71
|
+
t.Error("locations must be undefined for a bash call without a path")
|
|
72
|
+
}
|
|
73
|
+
jsonEqual(t, u0["content"], []any{map[string]any{"type": "terminal", "terminalId": "t1"}})
|
|
74
|
+
jsonEqual(t, u0["_meta"], map[string]any{"terminal_info": map[string]any{"terminal_id": "t1", "cwd": cwd}})
|
|
75
|
+
if _, ok := u0["rawInput"]; ok {
|
|
76
|
+
t.Error("rawInput must be undefined")
|
|
77
|
+
}
|
|
78
|
+
u1 := ups[1].Update
|
|
79
|
+
if u1["sessionUpdate"] != "tool_call_update" || u1["toolCallId"] != "t1" || u1["status"] != "in_progress" {
|
|
80
|
+
t.Errorf("update 1 = %v", u1)
|
|
81
|
+
}
|
|
82
|
+
if _, ok := u1["content"]; ok {
|
|
83
|
+
t.Error("content must be undefined")
|
|
84
|
+
}
|
|
85
|
+
jsonEqual(t, u1["_meta"], map[string]any{"terminal_output": map[string]any{"terminal_id": "t1", "data": "running"}})
|
|
86
|
+
if _, ok := u1["rawOutput"]; ok {
|
|
87
|
+
t.Error("rawOutput must be undefined")
|
|
88
|
+
}
|
|
89
|
+
u2 := ups[2].Update
|
|
90
|
+
if u2["sessionUpdate"] != "tool_call_update" || u2["toolCallId"] != "t1" || u2["status"] != "completed" {
|
|
91
|
+
t.Errorf("update 2 = %v", u2)
|
|
92
|
+
}
|
|
93
|
+
if _, ok := u2["content"]; ok {
|
|
94
|
+
t.Error("content must be undefined")
|
|
95
|
+
}
|
|
96
|
+
jsonEqual(t, u2["_meta"], map[string]any{
|
|
97
|
+
"terminal_output": map[string]any{"terminal_id": "t1", "data": "done"},
|
|
98
|
+
"terminal_exit": map[string]any{"terminal_id": "t1", "exit_code": 0, "signal": nil},
|
|
99
|
+
})
|
|
100
|
+
if _, ok := u2["rawOutput"]; ok {
|
|
101
|
+
t.Error("rawOutput must be undefined")
|
|
102
|
+
}
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
tw(t, "component/session-events", "PiAcpSession: emits tool locations from pi path args", func(t *testing.T) {
|
|
106
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
107
|
+
cwd := cwdNow(t)
|
|
108
|
+
s := newTestSession(cwd, proc, conn)
|
|
109
|
+
proc.emit(Event{"type": "tool_execution_start", "toolCallId": "t1", "toolName": "read", "args": map[string]any{"path": "src/acp/session.ts"}})
|
|
110
|
+
settle(t, s)
|
|
111
|
+
ups := conn.all()
|
|
112
|
+
if len(ups) != 1 || ups[0].Update["sessionUpdate"] != "tool_call" {
|
|
113
|
+
t.Fatalf("updates = %+v", ups)
|
|
114
|
+
}
|
|
115
|
+
jsonEqual(t, ups[0].Update["locations"], []any{map[string]any{"path": cwd + "/src/acp/session.ts"}})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
tw(t, "component/session-events", "PiAcpSession: handles extension select via ACP permission request", func(t *testing.T) {
|
|
119
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
120
|
+
conn.nextPermission = PermissionResponse{Outcome: PermissionOutcome{Outcome: "selected", OptionID: "choice-1"}}
|
|
121
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
122
|
+
proc.emit(Event{"type": "extension_ui_request", "id": "ui-1", "method": "select", "title": "Pick one", "options": []any{"Alpha", "Beta"}})
|
|
123
|
+
eventually(t, "the extension UI response", func() bool { return len(proc.uiResponses()) == 1 })
|
|
124
|
+
settle(t, s)
|
|
125
|
+
reqs := conn.permissions()
|
|
126
|
+
if len(reqs) != 1 {
|
|
127
|
+
t.Fatalf("%d permission requests", len(reqs))
|
|
128
|
+
}
|
|
129
|
+
jsonEqual(t, reqs[0], map[string]any{
|
|
130
|
+
"sessionId": "s1",
|
|
131
|
+
"toolCall": map[string]any{
|
|
132
|
+
"toolCallId": "pi-ui-ui-1", "title": "Pick one", "kind": "other", "status": "pending",
|
|
133
|
+
"rawInput": map[string]any{"method": "select", "title": "Pick one", "options": []any{"Alpha", "Beta"}},
|
|
134
|
+
},
|
|
135
|
+
"options": []any{
|
|
136
|
+
map[string]any{"optionId": "choice-0", "name": "Alpha", "kind": "allow_once"},
|
|
137
|
+
map[string]any{"optionId": "choice-1", "name": "Beta", "kind": "allow_once"},
|
|
138
|
+
},
|
|
139
|
+
})
|
|
140
|
+
jsonEqual(t, proc.uiResponses(), []any{map[string]any{"id": "ui-1", "value": "Beta"}})
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
tw(t, "component/session-events", "PiAcpSession: handles extension confirm via ACP permission request", func(t *testing.T) {
|
|
144
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
145
|
+
conn.nextPermission = PermissionResponse{Outcome: PermissionOutcome{Outcome: "selected", OptionID: "no"}}
|
|
146
|
+
newTestSession(cwdNow(t), proc, conn)
|
|
147
|
+
proc.emit(Event{"type": "extension_ui_request", "id": "ui-2", "method": "confirm", "title": "Clear session?", "message": "All messages will be lost."})
|
|
148
|
+
eventually(t, "the extension UI response", func() bool { return len(proc.uiResponses()) == 1 })
|
|
149
|
+
reqs := conn.permissions()
|
|
150
|
+
if len(reqs) != 1 {
|
|
151
|
+
t.Fatalf("%d permission requests", len(reqs))
|
|
152
|
+
}
|
|
153
|
+
jsonEqual(t, reqs[0].Options, []any{
|
|
154
|
+
map[string]any{"optionId": "yes", "name": "Yes", "kind": "allow_once"},
|
|
155
|
+
map[string]any{"optionId": "no", "name": "No", "kind": "reject_once"},
|
|
156
|
+
})
|
|
157
|
+
jsonEqual(t, proc.uiResponses(), []any{map[string]any{"id": "ui-2", "confirmed": false}})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
tw(t, "component/session-events", "PiAcpSession: sends cancelled response when ACP confirm is cancelled", func(t *testing.T) {
|
|
161
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
162
|
+
conn.nextPermission = PermissionResponse{Outcome: PermissionOutcome{Outcome: "cancelled"}}
|
|
163
|
+
newTestSession(cwdNow(t), proc, conn)
|
|
164
|
+
proc.emit(Event{"type": "extension_ui_request", "id": "ui-5", "method": "confirm", "title": "Continue?"})
|
|
165
|
+
eventually(t, "the extension UI response", func() bool { return len(proc.uiResponses()) == 1 })
|
|
166
|
+
jsonEqual(t, proc.uiResponses(), []any{map[string]any{"id": "ui-5", "cancelled": true}})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
tw(t, "component/session-events", "PiAcpSession: cancels unsupported input and editor extension UI requests with visible fallback", func(t *testing.T) {
|
|
170
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
171
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
172
|
+
proc.emit(Event{"type": "extension_ui_request", "id": "ui-3", "method": "input", "title": "Enter name"})
|
|
173
|
+
proc.emit(Event{"type": "extension_ui_request", "id": "ui-4", "method": "editor", "title": "Edit text"})
|
|
174
|
+
eventually(t, "both responses", func() bool { return len(proc.uiResponses()) == 2 })
|
|
175
|
+
settle(t, s)
|
|
176
|
+
jsonEqual(t, proc.uiResponses(), []any{
|
|
177
|
+
map[string]any{"id": "ui-3", "cancelled": true},
|
|
178
|
+
map[string]any{"id": "ui-4", "cancelled": true},
|
|
179
|
+
})
|
|
180
|
+
ups := conn.all()
|
|
181
|
+
if len(ups) != 2 {
|
|
182
|
+
t.Fatalf("%d updates", len(ups))
|
|
183
|
+
}
|
|
184
|
+
if !strings.Contains(str(ups[0].Update["content"].(map[string]any)["text"]), "input UI request is not supported") {
|
|
185
|
+
t.Errorf("update 0 = %v", ups[0].Update)
|
|
186
|
+
}
|
|
187
|
+
if !strings.Contains(str(ups[1].Update["content"].(map[string]any)["text"]), "editor UI request is not supported") {
|
|
188
|
+
t.Errorf("update 1 = %v", ups[1].Update)
|
|
189
|
+
}
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
retry := func(ev Event, want string) func(*testing.T) {
|
|
193
|
+
return func(t *testing.T) {
|
|
194
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
195
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
196
|
+
proc.emit(ev)
|
|
197
|
+
settle(t, s)
|
|
198
|
+
ups := conn.all()
|
|
199
|
+
if len(ups) != 1 {
|
|
200
|
+
t.Fatalf("%d updates", len(ups))
|
|
201
|
+
}
|
|
202
|
+
jsonEqual(t, ups[0].Update, chunk(want))
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
tw(t, "component/session-events", "PiAcpSession: emits agent_message_chunk for auto_retry_start with attempt/maxAttempts and rounded delay", retry(
|
|
206
|
+
Event{"type": "auto_retry_start", "attempt": 2, "maxAttempts": 5, "delayMs": 2400}, "Retrying (attempt 2/5, waiting 2s)..."))
|
|
207
|
+
tw(t, "component/session-events", "PiAcpSession: formats a positive sub-second auto_retry_start delay as waiting 1s", retry(
|
|
208
|
+
Event{"type": "auto_retry_start", "attempt": 1, "maxAttempts": 3, "delayMs": 1}, "Retrying (attempt 1/3, waiting 1s)..."))
|
|
209
|
+
tw(t, "component/session-events", "PiAcpSession: falls back to a generic retry message when auto_retry_start fields are missing or malformed", retry(
|
|
210
|
+
Event{"type": "auto_retry_start", "attempt": "oops", "maxAttempts": nil, "delayMs": "bad"}, "Retrying..."))
|
|
211
|
+
tw(t, "component/session-events", "PiAcpSession: omits raw errorMessage content from surfaced auto_retry_start status text", retry(
|
|
212
|
+
Event{"type": "auto_retry_start", "attempt": 1, "maxAttempts": 4, "delayMs": 1500, "errorMessage": "provider overloaded: 529"}, "Retrying (attempt 1/4, waiting 2s)..."))
|
|
213
|
+
tw(t, "component/session-events", "PiAcpSession: emits agent_message_chunk for auto_retry_end", retry(
|
|
214
|
+
Event{"type": "auto_retry_end"}, "Retry finished, resuming."))
|
|
215
|
+
tw(t, "component/session-events", "PiAcpSession: emits agent_message_chunk for auto_compaction_start", retry(
|
|
216
|
+
Event{"type": "auto_compaction_start"}, "Context nearing limit, running automatic compaction..."))
|
|
217
|
+
tw(t, "component/session-events", "PiAcpSession: emits agent_message_chunk for auto_compaction_end", retry(
|
|
218
|
+
Event{"type": "auto_compaction_end"}, "Automatic compaction finished; context was summarized to continue the session."))
|
|
219
|
+
|
|
220
|
+
t.Run("gap F1: compaction_start and compaction_end (pi 0.84+ and PiG) produce the compaction notices", func(t *testing.T) {
|
|
221
|
+
t.Skip("upstream defect kept as ported: pi-acp handles auto_compaction_start/auto_compaction_end, but Pi 0.84 to 0.99 and PiG 0.3.0 emit " +
|
|
222
|
+
"compaction_start (reason manual, threshold or overflow) and compaction_end, so the two notices never appear against these hosts. " +
|
|
223
|
+
"Fixing it changes observable behavior and needs an owner decision (port/PORT.md, F1). The twins above keep the upstream event names.")
|
|
224
|
+
})
|
|
225
|
+
|
|
226
|
+
tw(t, "component/session-events", "PiAcpSession: preserves ordering when auto_retry_start is interleaved with text_delta events", func(t *testing.T) {
|
|
227
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
228
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
229
|
+
proc.emit(textDelta("before "))
|
|
230
|
+
proc.emit(Event{"type": "auto_retry_start", "attempt": 1, "maxAttempts": 2, "delayMs": 2000})
|
|
231
|
+
proc.emit(textDelta("after"))
|
|
232
|
+
settle(t, s)
|
|
233
|
+
var got []Update
|
|
234
|
+
for _, u := range conn.all() {
|
|
235
|
+
got = append(got, u.Update)
|
|
236
|
+
}
|
|
237
|
+
jsonEqual(t, got, []Update{chunk("before "), chunk("Retrying (attempt 1/2, waiting 2s)..."), chunk("after")})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
tw(t, "component/session-events", "PiAcpSession: emits streamed tool locations from pi path args", func(t *testing.T) {
|
|
241
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
242
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
243
|
+
proc.emit(Event{"type": "message_update", "assistantMessageEvent": map[string]any{
|
|
244
|
+
"type": "toolcall_start",
|
|
245
|
+
"toolCall": map[string]any{"id": "t1", "name": "write", "arguments": map[string]any{"path": "/tmp/test.txt", "content": "hello"}},
|
|
246
|
+
}})
|
|
247
|
+
settle(t, s)
|
|
248
|
+
ups := conn.all()
|
|
249
|
+
if len(ups) != 1 || ups[0].Update["sessionUpdate"] != "tool_call" {
|
|
250
|
+
t.Fatalf("updates = %+v", ups)
|
|
251
|
+
}
|
|
252
|
+
jsonEqual(t, ups[0].Update["locations"], []any{map[string]any{"path": "/tmp/test.txt"}})
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
editLine := func(file string, args map[string]any, wantLine int) func(*testing.T) {
|
|
256
|
+
return func(t *testing.T) {
|
|
257
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
258
|
+
cwd := tmp(t)
|
|
259
|
+
path := filepath.Join(cwd, "a.txt")
|
|
260
|
+
if err := os.WriteFile(path, []byte(file), 0o644); err != nil {
|
|
261
|
+
t.Fatal(err)
|
|
262
|
+
}
|
|
263
|
+
s := newTestSession(cwd, proc, conn)
|
|
264
|
+
proc.emit(Event{"type": "tool_execution_start", "toolCallId": "t1", "toolName": "edit", "args": args})
|
|
265
|
+
settle(t, s)
|
|
266
|
+
ups := conn.all()
|
|
267
|
+
if len(ups) != 1 || ups[0].Update["sessionUpdate"] != "tool_call" {
|
|
268
|
+
t.Fatalf("updates = %+v", ups)
|
|
269
|
+
}
|
|
270
|
+
loc := map[string]any{"path": path}
|
|
271
|
+
if wantLine > 0 {
|
|
272
|
+
loc["line"] = wantLine
|
|
273
|
+
}
|
|
274
|
+
jsonEqual(t, ups[0].Update["locations"], []any{loc})
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
tw(t, "component/session-events", "PiAcpSession: emits edit tool line when oldText matches uniquely", editLine("one\ntwo\nneedle\nthree\n",
|
|
278
|
+
map[string]any{"path": "a.txt", "oldText": "needle"}, 3))
|
|
279
|
+
tw(t, "component/session-events", "PiAcpSession: emits edit tool line from edits array when oldText matches uniquely", editLine("one\ntwo\nneedle\nthree\n",
|
|
280
|
+
map[string]any{"path": "a.txt", "edits": []any{map[string]any{"oldText": "needle", "newText": "replacement"}}}, 3))
|
|
281
|
+
tw(t, "component/session-events", "PiAcpSession: emits edit tool line from stringified edits array", editLine("one\ntwo\nneedle\nthree\n",
|
|
282
|
+
map[string]any{"path": "a.txt", "edits": `[{"oldText":"needle","newText":"replacement"}]`}, 3))
|
|
283
|
+
tw(t, "component/session-events", "PiAcpSession: omits edit tool line when oldText matches multiple times", editLine("one\nneedle\ntwo\nneedle\n",
|
|
284
|
+
map[string]any{"path": "a.txt", "oldText": "needle"}, 0))
|
|
285
|
+
|
|
286
|
+
tw(t, "component/session-events", "PiAcpSession: prompt stays open through retry runs until agent_settled", func(t *testing.T) {
|
|
287
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
288
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
289
|
+
p := s.Prompt("hello", nil)
|
|
290
|
+
proc.emit(Event{"type": "agent_start"})
|
|
291
|
+
proc.emit(Event{"type": "auto_retry_start", "attempt": 1, "maxAttempts": 3, "delayMs": 2000})
|
|
292
|
+
proc.emit(Event{"type": "agent_end", "willRetry": true})
|
|
293
|
+
settle(t, s)
|
|
294
|
+
select {
|
|
295
|
+
case <-p:
|
|
296
|
+
t.Fatal("resolved before agent_settled")
|
|
297
|
+
default:
|
|
298
|
+
}
|
|
299
|
+
proc.emit(Event{"type": "agent_start"})
|
|
300
|
+
proc.emit(Event{"type": "turn_end"})
|
|
301
|
+
proc.emit(Event{"type": "agent_end", "willRetry": false})
|
|
302
|
+
settle(t, s)
|
|
303
|
+
select {
|
|
304
|
+
case <-p:
|
|
305
|
+
t.Fatal("resolved before agent_settled")
|
|
306
|
+
default:
|
|
307
|
+
}
|
|
308
|
+
proc.emit(Event{"type": "agent_settled"})
|
|
309
|
+
if r := wait(t, p); r.Reason != StopEndTurn {
|
|
310
|
+
t.Fatalf("reason = %v", r)
|
|
311
|
+
}
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
tw(t, "component/session-events", "PiAcpSession: does not re-emit startup info on first prompt after it was already sent", func(t *testing.T) {
|
|
315
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
316
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
317
|
+
notice := "New version available: v0.74.0 (installed v0.73.1)."
|
|
318
|
+
s.SetStartupInfo(notice)
|
|
319
|
+
s.SendStartupInfoIfPending()
|
|
320
|
+
settle(t, s)
|
|
321
|
+
p := s.Prompt("hello", nil)
|
|
322
|
+
eventually(t, "the prompt", func() bool { return len(proc.promptList()) == 1 })
|
|
323
|
+
if got := promptsOf(t, proc, 1); got[0].Message != "hello" {
|
|
324
|
+
t.Errorf("prompt = %v", got)
|
|
325
|
+
}
|
|
326
|
+
n := 0
|
|
327
|
+
for _, u := range conn.all() {
|
|
328
|
+
if u.Update["sessionUpdate"] == "agent_message_chunk" {
|
|
329
|
+
if c, _ := u.Update["content"].(map[string]any); c["type"] == "text" && c["text"] == notice {
|
|
330
|
+
n++
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if n != 1 {
|
|
335
|
+
t.Errorf("startup info sent %d times", n)
|
|
336
|
+
}
|
|
337
|
+
proc.emit(Event{"type": "agent_start"})
|
|
338
|
+
proc.emit(Event{"type": "turn_end"})
|
|
339
|
+
proc.emit(Event{"type": "agent_end"})
|
|
340
|
+
proc.emit(Event{"type": "agent_settled"})
|
|
341
|
+
if r := wait(t, p); r.Reason != StopEndTurn {
|
|
342
|
+
t.Fatalf("reason = %v", r)
|
|
343
|
+
}
|
|
344
|
+
})
|
|
345
|
+
|
|
346
|
+
tw(t, "component/session-events", "PiAcpSession: cancel flips stopReason to cancelled", func(t *testing.T) {
|
|
347
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
348
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
349
|
+
p := s.Prompt("hello", nil)
|
|
350
|
+
if err := s.Cancel(); err != nil {
|
|
351
|
+
t.Fatal(err)
|
|
352
|
+
}
|
|
353
|
+
settledTurn(proc)
|
|
354
|
+
r := wait(t, p)
|
|
355
|
+
if proc.aborts() != 1 || r.Reason != StopCancelled {
|
|
356
|
+
t.Fatalf("aborts=%d reason=%v", proc.aborts(), r)
|
|
357
|
+
}
|
|
358
|
+
})
|
|
359
|
+
|
|
360
|
+
tw(t, "component/session-events", "PiAcpSession: queues concurrent prompt and starts it after agent_settled", func(t *testing.T) {
|
|
361
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
362
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
363
|
+
first := s.Prompt("one", nil)
|
|
364
|
+
second := s.Prompt("two", nil)
|
|
365
|
+
eventually(t, "the first prompt", func() bool { return len(proc.promptList()) >= 1 })
|
|
366
|
+
if got := promptsOf(t, proc, 1); len(got) != 1 || got[0].Message != "one" {
|
|
367
|
+
t.Fatalf("prompts = %v", got)
|
|
368
|
+
}
|
|
369
|
+
settledTurn(proc)
|
|
370
|
+
if r := wait(t, first); r.Reason != StopEndTurn {
|
|
371
|
+
t.Fatalf("first = %v", r)
|
|
372
|
+
}
|
|
373
|
+
eventually(t, "the queued prompt", func() bool { return len(proc.promptList()) == 2 })
|
|
374
|
+
if got := promptsOf(t, proc, 2); got[1].Message != "two" {
|
|
375
|
+
t.Fatalf("prompts = %v", got)
|
|
376
|
+
}
|
|
377
|
+
settledTurn(proc)
|
|
378
|
+
if r := wait(t, second); r.Reason != StopEndTurn {
|
|
379
|
+
t.Fatalf("second = %v", r)
|
|
380
|
+
}
|
|
381
|
+
})
|
|
382
|
+
|
|
383
|
+
tw(t, "component/session-events", "PiAcpSession: cancel clears queued prompts", func(t *testing.T) {
|
|
384
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
385
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
386
|
+
first := s.Prompt("one", nil)
|
|
387
|
+
second := s.Prompt("two", nil)
|
|
388
|
+
eventually(t, "the first prompt", func() bool { return len(proc.promptList()) == 1 })
|
|
389
|
+
if err := s.Cancel(); err != nil {
|
|
390
|
+
t.Fatal(err)
|
|
391
|
+
}
|
|
392
|
+
settledTurn(proc)
|
|
393
|
+
r1, r2 := wait(t, first), wait(t, second)
|
|
394
|
+
if r1.Reason != StopCancelled || r2.Reason != StopCancelled {
|
|
395
|
+
t.Fatalf("r1=%v r2=%v", r1, r2)
|
|
396
|
+
}
|
|
397
|
+
})
|
|
398
|
+
|
|
399
|
+
tw(t, "component/session-events", "PiAcpSession: expands /command before sending to pi", func(t *testing.T) {
|
|
400
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
401
|
+
s := newTestSession(cwdNow(t), proc, conn, FileSlashCommand{Name: "hello", Description: "test", Content: "Say hello to $1", Source: "(project)"})
|
|
402
|
+
p := s.Prompt("/hello world", nil)
|
|
403
|
+
eventually(t, "the prompt", func() bool { return len(proc.promptList()) == 1 })
|
|
404
|
+
if got := promptsOf(t, proc, 1); got[0].Message != "Say hello to world" {
|
|
405
|
+
t.Errorf("prompt = %q", got[0].Message)
|
|
406
|
+
}
|
|
407
|
+
settledTurn(proc)
|
|
408
|
+
if r := wait(t, p); r.Reason != StopEndTurn {
|
|
409
|
+
t.Fatalf("reason = %v", r)
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
tw(t, "component/session-events", "PiAcpSession: tags extension notify chunks with severity in _meta", func(t *testing.T) {
|
|
414
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
415
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
416
|
+
proc.emit(Event{"type": "extension_ui_request", "id": "n1", "method": "notify", "message": "MCP: connection failed", "notifyType": "error"})
|
|
417
|
+
eventually(t, "the acknowledgement", func() bool { return len(proc.uiResponses()) == 1 })
|
|
418
|
+
settle(t, s)
|
|
419
|
+
ups := conn.all()
|
|
420
|
+
if len(ups) != 1 {
|
|
421
|
+
t.Fatalf("%d updates", len(ups))
|
|
422
|
+
}
|
|
423
|
+
jsonEqual(t, ups[0].Update, Update{
|
|
424
|
+
"sessionUpdate": "agent_message_chunk",
|
|
425
|
+
"content": map[string]any{"type": "text", "text": "MCP: connection failed"},
|
|
426
|
+
"_meta": map[string]any{"piAcp": map[string]any{"notify": map[string]any{"level": "error"}}},
|
|
427
|
+
})
|
|
428
|
+
jsonEqual(t, proc.uiResponses()[0], map[string]any{"id": "n1", "cancelled": true})
|
|
429
|
+
})
|
|
430
|
+
|
|
431
|
+
tw(t, "component/session-events", "PiAcpSession: defaults notify severity to info when notifyType is absent", func(t *testing.T) {
|
|
432
|
+
conn, proc := newFakeConn(), newFakeProc()
|
|
433
|
+
s := newTestSession(cwdNow(t), proc, conn)
|
|
434
|
+
proc.emit(Event{"type": "extension_ui_request", "id": "n2", "method": "notify", "message": "heads up"})
|
|
435
|
+
eventually(t, "the acknowledgement", func() bool { return len(proc.uiResponses()) == 1 })
|
|
436
|
+
settle(t, s)
|
|
437
|
+
ups := conn.all()
|
|
438
|
+
if len(ups) != 1 {
|
|
439
|
+
t.Fatalf("%d updates", len(ups))
|
|
440
|
+
}
|
|
441
|
+
jsonEqual(t, ups[0].Update["_meta"], map[string]any{"piAcp": map[string]any{"notify": map[string]any{"level": "info"}}})
|
|
442
|
+
})
|
|
443
|
+
}
|