@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,217 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { PiAcpAgent } from '../../src/acp/agent.js'
|
|
4
|
+
import { FakeAgentSideConnection, asAgentConn } from '../helpers/fakes.js'
|
|
5
|
+
|
|
6
|
+
class FakeSessions {
|
|
7
|
+
constructor(private readonly session: any) {}
|
|
8
|
+
|
|
9
|
+
async create() {
|
|
10
|
+
return this.session
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
maybeGet(sessionId: string) {
|
|
14
|
+
if (sessionId !== this.session.sessionId) return undefined
|
|
15
|
+
return this.session
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get(sessionId: string) {
|
|
19
|
+
if (sessionId !== this.session.sessionId) {
|
|
20
|
+
throw new Error(`Unknown sessionId: ${sessionId}`)
|
|
21
|
+
}
|
|
22
|
+
return this.session
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
test('PiAcpAgent: newSession returns configOptions for model and thinking selectors', async () => {
|
|
27
|
+
const realSetTimeout = globalThis.setTimeout
|
|
28
|
+
;(globalThis as any).setTimeout = () => 0 as any
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
const conn = new FakeAgentSideConnection()
|
|
32
|
+
const session = {
|
|
33
|
+
sessionId: 's1',
|
|
34
|
+
cwd: process.cwd(),
|
|
35
|
+
proc: {
|
|
36
|
+
async getAvailableThinkingLevels() {
|
|
37
|
+
return ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
38
|
+
},
|
|
39
|
+
async getAvailableModels() {
|
|
40
|
+
return {
|
|
41
|
+
models: [
|
|
42
|
+
{ provider: 'test', id: 'alpha', name: 'Alpha' },
|
|
43
|
+
{ provider: 'test', id: 'beta', name: 'Beta' }
|
|
44
|
+
]
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
async getState() {
|
|
48
|
+
return {
|
|
49
|
+
thinkingLevel: 'high',
|
|
50
|
+
model: { provider: 'test', id: 'beta' }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
setStartupInfo() {},
|
|
55
|
+
sendStartupInfoIfPending() {}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
59
|
+
;(agent as any).sessions = new FakeSessions(session) as any
|
|
60
|
+
|
|
61
|
+
const result = await agent.newSession({ cwd: process.cwd(), mcpServers: [] } as any)
|
|
62
|
+
|
|
63
|
+
assert.equal(result.models?.currentModelId, 'test/beta')
|
|
64
|
+
assert.equal(result.modes?.currentModeId, 'high')
|
|
65
|
+
assert.deepEqual(result.configOptions, [
|
|
66
|
+
{
|
|
67
|
+
type: 'select',
|
|
68
|
+
id: 'model',
|
|
69
|
+
category: 'model',
|
|
70
|
+
name: 'Model',
|
|
71
|
+
description: 'Select the model for this session',
|
|
72
|
+
currentValue: 'test/beta',
|
|
73
|
+
options: [
|
|
74
|
+
{ value: 'test/alpha', name: 'test/Alpha', description: null },
|
|
75
|
+
{ value: 'test/beta', name: 'test/Beta', description: null }
|
|
76
|
+
]
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'select',
|
|
80
|
+
id: 'thought_level',
|
|
81
|
+
category: 'thought_level',
|
|
82
|
+
name: 'Thinking',
|
|
83
|
+
description: 'Set the reasoning effort for this session',
|
|
84
|
+
currentValue: 'high',
|
|
85
|
+
options: [
|
|
86
|
+
{ value: 'off', name: 'Thinking: off', description: null },
|
|
87
|
+
{ value: 'minimal', name: 'Thinking: minimal', description: null },
|
|
88
|
+
{ value: 'low', name: 'Thinking: low', description: null },
|
|
89
|
+
{ value: 'medium', name: 'Thinking: medium', description: null },
|
|
90
|
+
{ value: 'high', name: 'Thinking: high', description: null },
|
|
91
|
+
{ value: 'xhigh', name: 'Thinking: xhigh', description: null },
|
|
92
|
+
{ value: 'max', name: 'Thinking: max', description: null }
|
|
93
|
+
]
|
|
94
|
+
}
|
|
95
|
+
])
|
|
96
|
+
} finally {
|
|
97
|
+
;(globalThis as any).setTimeout = realSetTimeout
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
test('PiAcpAgent: setSessionConfigOption maps model changes to pi and emits config_option_update', async () => {
|
|
102
|
+
const conn = new FakeAgentSideConnection()
|
|
103
|
+
const state = {
|
|
104
|
+
thinkingLevel: 'medium',
|
|
105
|
+
model: { provider: 'test', id: 'alpha' }
|
|
106
|
+
}
|
|
107
|
+
const setModelCalls: Array<{ provider: string; modelId: string }> = []
|
|
108
|
+
|
|
109
|
+
const session = {
|
|
110
|
+
sessionId: 's1',
|
|
111
|
+
cwd: process.cwd(),
|
|
112
|
+
proc: {
|
|
113
|
+
async getAvailableThinkingLevels() {
|
|
114
|
+
return ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
115
|
+
},
|
|
116
|
+
async getAvailableModels() {
|
|
117
|
+
return {
|
|
118
|
+
models: [
|
|
119
|
+
{ provider: 'test', id: 'alpha', name: 'Alpha' },
|
|
120
|
+
{ provider: 'test', id: 'beta', name: 'Beta' }
|
|
121
|
+
]
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
async getState() {
|
|
125
|
+
return state
|
|
126
|
+
},
|
|
127
|
+
async setModel(provider: string, modelId: string) {
|
|
128
|
+
setModelCalls.push({ provider, modelId })
|
|
129
|
+
state.model = { provider, id: modelId }
|
|
130
|
+
}
|
|
131
|
+
},
|
|
132
|
+
async publishContextUsage() {
|
|
133
|
+
// Context usage publishing is covered in test/unit/context-usage.test.ts.
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
138
|
+
;(agent as any).sessions = new FakeSessions(session) as any
|
|
139
|
+
|
|
140
|
+
const result = await agent.setSessionConfigOption({
|
|
141
|
+
sessionId: 's1',
|
|
142
|
+
configId: 'model',
|
|
143
|
+
value: 'test/beta'
|
|
144
|
+
} as any)
|
|
145
|
+
|
|
146
|
+
assert.deepEqual(setModelCalls, [{ provider: 'test', modelId: 'beta' }])
|
|
147
|
+
assert.equal(result.configOptions.find(option => option.id === 'model')?.currentValue, 'test/beta')
|
|
148
|
+
assert.deepEqual(conn.updates, [
|
|
149
|
+
{ sessionId: 's1', update: { sessionUpdate: 'current_mode_update', currentModeId: 'medium' } },
|
|
150
|
+
{
|
|
151
|
+
sessionId: 's1',
|
|
152
|
+
update: {
|
|
153
|
+
sessionUpdate: 'config_option_update',
|
|
154
|
+
configOptions: result.configOptions
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
])
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
test('PiAcpAgent: setSessionConfigOption maps thought level changes to pi and emits sync updates', async () => {
|
|
161
|
+
const conn = new FakeAgentSideConnection()
|
|
162
|
+
const state = {
|
|
163
|
+
thinkingLevel: 'medium',
|
|
164
|
+
model: { provider: 'test', id: 'alpha' }
|
|
165
|
+
}
|
|
166
|
+
const thinkingLevels: string[] = []
|
|
167
|
+
|
|
168
|
+
const session = {
|
|
169
|
+
sessionId: 's1',
|
|
170
|
+
cwd: process.cwd(),
|
|
171
|
+
proc: {
|
|
172
|
+
async getAvailableThinkingLevels() {
|
|
173
|
+
return ['off', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
|
174
|
+
},
|
|
175
|
+
async getAvailableModels() {
|
|
176
|
+
return {
|
|
177
|
+
models: [{ provider: 'test', id: 'alpha', name: 'Alpha' }]
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
async getState() {
|
|
181
|
+
return state
|
|
182
|
+
},
|
|
183
|
+
async setThinkingLevel(level: string) {
|
|
184
|
+
thinkingLevels.push(level)
|
|
185
|
+
state.thinkingLevel = level
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
191
|
+
;(agent as any).sessions = new FakeSessions(session) as any
|
|
192
|
+
|
|
193
|
+
const result = await agent.setSessionConfigOption({
|
|
194
|
+
sessionId: 's1',
|
|
195
|
+
configId: 'thought_level',
|
|
196
|
+
value: 'xhigh'
|
|
197
|
+
} as any)
|
|
198
|
+
|
|
199
|
+
assert.deepEqual(thinkingLevels, ['xhigh'])
|
|
200
|
+
assert.equal(result.configOptions.find(option => option.id === 'thought_level')?.currentValue, 'xhigh')
|
|
201
|
+
assert.deepEqual(conn.updates, [
|
|
202
|
+
{
|
|
203
|
+
sessionId: 's1',
|
|
204
|
+
update: {
|
|
205
|
+
sessionUpdate: 'current_mode_update',
|
|
206
|
+
currentModeId: 'xhigh'
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
sessionId: 's1',
|
|
211
|
+
update: {
|
|
212
|
+
sessionUpdate: 'config_option_update',
|
|
213
|
+
configOptions: result.configOptions
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
])
|
|
217
|
+
})
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, writeFileSync, mkdirSync, existsSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { PiAcpAgent } from '../../src/acp/agent.js'
|
|
7
|
+
import { FakeAgentSideConnection, asAgentConn } from '../helpers/fakes.js'
|
|
8
|
+
|
|
9
|
+
test('PiAcpAgent: deleteSession removes stored session and session file', async () => {
|
|
10
|
+
const root = mkdtempSync(join(tmpdir(), 'pi-acp-delete-test-'))
|
|
11
|
+
const sessionsDir = join(root, 'sessions', '--tmp--delete-project--')
|
|
12
|
+
const sessionFile = join(sessionsDir, '0000_delete_me.jsonl')
|
|
13
|
+
mkdirSync(sessionsDir, { recursive: true })
|
|
14
|
+
writeFileSync(
|
|
15
|
+
sessionFile,
|
|
16
|
+
'{"type":"session","version":3,"id":"sess-del-store","timestamp":"2026-06-16T00:00:00.000Z","cwd":"/tmp/delete-project"}\n',
|
|
17
|
+
'utf-8'
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
const oldEnv = process.env.PI_CODING_AGENT_DIR
|
|
21
|
+
process.env.PI_CODING_AGENT_DIR = root
|
|
22
|
+
|
|
23
|
+
const conn = new FakeAgentSideConnection()
|
|
24
|
+
const agent = new PiAcpAgent(asAgentConn(conn))
|
|
25
|
+
|
|
26
|
+
const storedSessionId = 'stored-session'
|
|
27
|
+
const storeDeletes: string[] = []
|
|
28
|
+
|
|
29
|
+
// Inject a SessionStore that tracks calls.
|
|
30
|
+
;(agent as any).store = {
|
|
31
|
+
get(sessionId: string) {
|
|
32
|
+
if (sessionId !== storedSessionId) return null
|
|
33
|
+
return { sessionId, cwd: '/tmp/delete-project', sessionFile, updatedAt: new Date().toISOString() }
|
|
34
|
+
},
|
|
35
|
+
delete(sessionId: string) {
|
|
36
|
+
storeDeletes.push(sessionId)
|
|
37
|
+
},
|
|
38
|
+
upsert() {}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const response = await agent.deleteSession({ sessionId: storedSessionId } as any)
|
|
43
|
+
assert.deepEqual(response, {})
|
|
44
|
+
assert.deepEqual(storeDeletes, [storedSessionId])
|
|
45
|
+
assert.equal(existsSync(sessionFile), false)
|
|
46
|
+
} finally {
|
|
47
|
+
if (oldEnv === undefined) delete process.env.PI_CODING_AGENT_DIR
|
|
48
|
+
else process.env.PI_CODING_AGENT_DIR = oldEnv
|
|
49
|
+
}
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
test('PiAcpAgent: deleteSession finds session via pi discovery when SessionStore misses', async () => {
|
|
53
|
+
const root = mkdtempSync(join(tmpdir(), 'pi-acp-delete-discovery-'))
|
|
54
|
+
const sessionsDir = join(root, 'sessions', '--tmp--delete-discovery--')
|
|
55
|
+
const sessionFile = join(sessionsDir, '0000_pi_discovery.jsonl')
|
|
56
|
+
mkdirSync(sessionsDir, { recursive: true })
|
|
57
|
+
writeFileSync(
|
|
58
|
+
sessionFile,
|
|
59
|
+
JSON.stringify({
|
|
60
|
+
type: 'session',
|
|
61
|
+
version: 3,
|
|
62
|
+
id: 'pi-discovered-session',
|
|
63
|
+
timestamp: '2026-06-16T00:00:00.000Z',
|
|
64
|
+
cwd: '/tmp/delete-discovery'
|
|
65
|
+
}) + '\n',
|
|
66
|
+
'utf-8'
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
const oldEnv = process.env.PI_CODING_AGENT_DIR
|
|
70
|
+
process.env.PI_CODING_AGENT_DIR = root
|
|
71
|
+
|
|
72
|
+
const conn = new FakeAgentSideConnection()
|
|
73
|
+
const agent = new PiAcpAgent(asAgentConn(conn))
|
|
74
|
+
|
|
75
|
+
const storeDeletes: string[] = []
|
|
76
|
+
|
|
77
|
+
;(agent as any).store = {
|
|
78
|
+
get() {
|
|
79
|
+
return null
|
|
80
|
+
},
|
|
81
|
+
delete(sessionId: string) {
|
|
82
|
+
storeDeletes.push(sessionId)
|
|
83
|
+
},
|
|
84
|
+
upsert() {}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const response = await agent.deleteSession({ sessionId: 'pi-discovered-session' } as any)
|
|
89
|
+
assert.deepEqual(response, {})
|
|
90
|
+
assert.deepEqual(storeDeletes, ['pi-discovered-session'])
|
|
91
|
+
assert.equal(existsSync(sessionFile), false)
|
|
92
|
+
} finally {
|
|
93
|
+
if (oldEnv === undefined) delete process.env.PI_CODING_AGENT_DIR
|
|
94
|
+
else process.env.PI_CODING_AGENT_DIR = oldEnv
|
|
95
|
+
}
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('PiAcpAgent: deleteSession succeeds idempotently for unknown sessionId', async () => {
|
|
99
|
+
const root = mkdtempSync(join(tmpdir(), 'pi-acp-delete-unknown-'))
|
|
100
|
+
const sessionsDir = join(root, 'sessions', '--tmp--delete-unknown--')
|
|
101
|
+
mkdirSync(sessionsDir, { recursive: true })
|
|
102
|
+
|
|
103
|
+
const oldEnv = process.env.PI_CODING_AGENT_DIR
|
|
104
|
+
process.env.PI_CODING_AGENT_DIR = root
|
|
105
|
+
|
|
106
|
+
const conn = new FakeAgentSideConnection()
|
|
107
|
+
const agent = new PiAcpAgent(asAgentConn(conn))
|
|
108
|
+
|
|
109
|
+
// Per ACP session/delete semantics, deleting a non-existent session
|
|
110
|
+
// should succeed idempotently (return {} without error).
|
|
111
|
+
const storeDeletes: string[] = []
|
|
112
|
+
;(agent as any).store = {
|
|
113
|
+
get() {
|
|
114
|
+
return null
|
|
115
|
+
},
|
|
116
|
+
delete(sessionId: string) {
|
|
117
|
+
storeDeletes.push(sessionId)
|
|
118
|
+
},
|
|
119
|
+
upsert() {}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const response = await agent.deleteSession({ sessionId: 'non-existent-session' } as any)
|
|
124
|
+
assert.deepEqual(response, {})
|
|
125
|
+
assert.deepEqual(storeDeletes, [])
|
|
126
|
+
} finally {
|
|
127
|
+
if (oldEnv === undefined) delete process.env.PI_CODING_AGENT_DIR
|
|
128
|
+
else process.env.PI_CODING_AGENT_DIR = oldEnv
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test('PiAcpAgent: deleteSession survives missing session file', async () => {
|
|
133
|
+
const root = mkdtempSync(join(tmpdir(), 'pi-acp-delete-missingfile-'))
|
|
134
|
+
const sessionsDir = join(root, 'sessions', '--tmp--delete-missingfile--')
|
|
135
|
+
mkdirSync(sessionsDir, { recursive: true })
|
|
136
|
+
const nonExistentFile = join(sessionsDir, '0000_non_existent.jsonl')
|
|
137
|
+
|
|
138
|
+
const oldEnv = process.env.PI_CODING_AGENT_DIR
|
|
139
|
+
process.env.PI_CODING_AGENT_DIR = root
|
|
140
|
+
|
|
141
|
+
const conn = new FakeAgentSideConnection()
|
|
142
|
+
const agent = new PiAcpAgent(asAgentConn(conn))
|
|
143
|
+
|
|
144
|
+
const storeDeletes: string[] = []
|
|
145
|
+
|
|
146
|
+
;(agent as any).store = {
|
|
147
|
+
get(sessionId: string) {
|
|
148
|
+
if (sessionId !== 'missing-file-session') return null
|
|
149
|
+
return {
|
|
150
|
+
sessionId,
|
|
151
|
+
cwd: '/tmp/delete-missingfile',
|
|
152
|
+
sessionFile: nonExistentFile,
|
|
153
|
+
updatedAt: new Date().toISOString()
|
|
154
|
+
}
|
|
155
|
+
},
|
|
156
|
+
delete(sessionId: string) {
|
|
157
|
+
storeDeletes.push(sessionId)
|
|
158
|
+
},
|
|
159
|
+
upsert() {}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const response = await agent.deleteSession({ sessionId: 'missing-file-session' } as any)
|
|
164
|
+
assert.deepEqual(response, {})
|
|
165
|
+
assert.deepEqual(storeDeletes, ['missing-file-session'])
|
|
166
|
+
} finally {
|
|
167
|
+
if (oldEnv === undefined) delete process.env.PI_CODING_AGENT_DIR
|
|
168
|
+
else process.env.PI_CODING_AGENT_DIR = oldEnv
|
|
169
|
+
}
|
|
170
|
+
})
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { PiAcpAgent } from '../../src/acp/agent.js'
|
|
7
|
+
import { PiRpcProcess } from '../../src/pi-rpc/process.js'
|
|
8
|
+
import { FakeAgentSideConnection, asAgentConn } from '../helpers/fakes.js'
|
|
9
|
+
|
|
10
|
+
class FakeSessions {
|
|
11
|
+
restoredSession: any = null
|
|
12
|
+
|
|
13
|
+
constructor(private readonly buildSession: (sessionId: string, params: any) => any) {}
|
|
14
|
+
|
|
15
|
+
maybeGet(sessionId: string) {
|
|
16
|
+
return this.restoredSession?.sessionId === sessionId ? this.restoredSession : undefined
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
getOrCreate(sessionId: string, params: any) {
|
|
20
|
+
if (!this.restoredSession) {
|
|
21
|
+
this.restoredSession = this.buildSession(sessionId, params)
|
|
22
|
+
}
|
|
23
|
+
return this.restoredSession
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test('PiAcpAgent: prompt auto-restores a missing session from SessionStore', async () => {
|
|
28
|
+
const conn = new FakeAgentSideConnection()
|
|
29
|
+
const promptCalls: Array<{ message: string; images: unknown[] }> = []
|
|
30
|
+
const spawnCalls: any[] = []
|
|
31
|
+
const storeUpserts: any[] = []
|
|
32
|
+
|
|
33
|
+
const sessions = new FakeSessions((sessionId, params) => ({
|
|
34
|
+
sessionId,
|
|
35
|
+
cwd: params.cwd,
|
|
36
|
+
proc: params.proc,
|
|
37
|
+
async prompt(message: string, images: unknown[]) {
|
|
38
|
+
promptCalls.push({ message, images })
|
|
39
|
+
return 'end_turn'
|
|
40
|
+
},
|
|
41
|
+
async cancel() {},
|
|
42
|
+
wasCancelRequested() {
|
|
43
|
+
return false
|
|
44
|
+
}
|
|
45
|
+
}))
|
|
46
|
+
|
|
47
|
+
const originalSpawn = PiRpcProcess.spawn
|
|
48
|
+
;(PiRpcProcess as any).spawn = async (params: any) => {
|
|
49
|
+
spawnCalls.push(params)
|
|
50
|
+
return {
|
|
51
|
+
onEvent: () => () => {}
|
|
52
|
+
} as any
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
57
|
+
;(agent as any).sessions = sessions as any
|
|
58
|
+
;(agent as any).store = {
|
|
59
|
+
get(sessionId: string) {
|
|
60
|
+
if (sessionId !== 'stored-session') return null
|
|
61
|
+
return {
|
|
62
|
+
sessionId,
|
|
63
|
+
cwd: '/tmp/store-project',
|
|
64
|
+
sessionFile: '/tmp/store-project/session.jsonl',
|
|
65
|
+
updatedAt: new Date().toISOString()
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
upsert(entry: any) {
|
|
69
|
+
storeUpserts.push(entry)
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = await agent.prompt({
|
|
74
|
+
sessionId: 'stored-session',
|
|
75
|
+
prompt: [{ type: 'text', text: 'hello again' }]
|
|
76
|
+
} as any)
|
|
77
|
+
|
|
78
|
+
assert.equal(result.stopReason, 'end_turn')
|
|
79
|
+
assert.deepEqual(spawnCalls, [
|
|
80
|
+
{
|
|
81
|
+
cwd: '/tmp/store-project',
|
|
82
|
+
sessionPath: '/tmp/store-project/session.jsonl',
|
|
83
|
+
piCommand: process.env.PI_ACP_PI_COMMAND
|
|
84
|
+
}
|
|
85
|
+
])
|
|
86
|
+
assert.deepEqual(promptCalls, [{ message: 'hello again', images: [] }])
|
|
87
|
+
assert.deepEqual(storeUpserts, [
|
|
88
|
+
{
|
|
89
|
+
sessionId: 'stored-session',
|
|
90
|
+
cwd: '/tmp/store-project',
|
|
91
|
+
sessionFile: '/tmp/store-project/session.jsonl'
|
|
92
|
+
}
|
|
93
|
+
])
|
|
94
|
+
} finally {
|
|
95
|
+
PiRpcProcess.spawn = originalSpawn
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('PiAcpAgent: setSessionConfigOption auto-restores via pi session discovery when SessionStore misses', async () => {
|
|
100
|
+
const conn = new FakeAgentSideConnection()
|
|
101
|
+
const root = mkdtempSync(join(tmpdir(), 'pi-acp-restore-fallback-'))
|
|
102
|
+
const sessionsDir = join(root, 'sessions', '--tmp--fallback-project--')
|
|
103
|
+
const sessionFile = join(sessionsDir, '0000_restore_fallback.jsonl')
|
|
104
|
+
const prevAgentDir = process.env.PI_CODING_AGENT_DIR
|
|
105
|
+
|
|
106
|
+
mkdirSync(sessionsDir, { recursive: true })
|
|
107
|
+
writeFileSync(
|
|
108
|
+
sessionFile,
|
|
109
|
+
JSON.stringify({
|
|
110
|
+
type: 'session',
|
|
111
|
+
version: 3,
|
|
112
|
+
id: 'fallback-session',
|
|
113
|
+
timestamp: '2026-06-16T00:00:00.000Z',
|
|
114
|
+
cwd: '/tmp/fallback-project'
|
|
115
|
+
}) + '\n',
|
|
116
|
+
'utf-8'
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
process.env.PI_CODING_AGENT_DIR = root
|
|
120
|
+
|
|
121
|
+
const storeUpserts: any[] = []
|
|
122
|
+
const setModelCalls: Array<{ provider: string; modelId: string }> = []
|
|
123
|
+
const spawnCalls: any[] = []
|
|
124
|
+
const state = {
|
|
125
|
+
thinkingLevel: 'medium',
|
|
126
|
+
model: { provider: 'test', id: 'alpha' }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const sessions = new FakeSessions((sessionId, params) => ({
|
|
130
|
+
sessionId,
|
|
131
|
+
cwd: params.cwd,
|
|
132
|
+
proc: params.proc,
|
|
133
|
+
async publishContextUsage() {
|
|
134
|
+
// Context usage publishing is covered in test/unit/context-usage.test.ts.
|
|
135
|
+
}
|
|
136
|
+
}))
|
|
137
|
+
|
|
138
|
+
const originalSpawn = PiRpcProcess.spawn
|
|
139
|
+
;(PiRpcProcess as any).spawn = async (params: any) => {
|
|
140
|
+
spawnCalls.push(params)
|
|
141
|
+
return {
|
|
142
|
+
onEvent: () => () => {},
|
|
143
|
+
getAvailableThinkingLevels: async () => ['medium'],
|
|
144
|
+
getAvailableModels: async () => ({
|
|
145
|
+
models: [
|
|
146
|
+
{ provider: 'test', id: 'alpha', name: 'Alpha' },
|
|
147
|
+
{ provider: 'test', id: 'beta', name: 'Beta' }
|
|
148
|
+
]
|
|
149
|
+
}),
|
|
150
|
+
getState: async () => state,
|
|
151
|
+
async setModel(provider: string, modelId: string) {
|
|
152
|
+
setModelCalls.push({ provider, modelId })
|
|
153
|
+
state.model = { provider, id: modelId }
|
|
154
|
+
}
|
|
155
|
+
} as any
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
160
|
+
;(agent as any).sessions = sessions as any
|
|
161
|
+
;(agent as any).store = {
|
|
162
|
+
get() {
|
|
163
|
+
return null
|
|
164
|
+
},
|
|
165
|
+
upsert(entry: any) {
|
|
166
|
+
storeUpserts.push(entry)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const result = await agent.setSessionConfigOption({
|
|
171
|
+
sessionId: 'fallback-session',
|
|
172
|
+
configId: 'model',
|
|
173
|
+
value: 'test/beta'
|
|
174
|
+
} as any)
|
|
175
|
+
|
|
176
|
+
assert.deepEqual(spawnCalls, [
|
|
177
|
+
{
|
|
178
|
+
cwd: '/tmp/fallback-project',
|
|
179
|
+
sessionPath: sessionFile,
|
|
180
|
+
piCommand: process.env.PI_ACP_PI_COMMAND
|
|
181
|
+
}
|
|
182
|
+
])
|
|
183
|
+
assert.deepEqual(setModelCalls, [{ provider: 'test', modelId: 'beta' }])
|
|
184
|
+
assert.equal(result.configOptions.find(option => option.id === 'model')?.currentValue, 'test/beta')
|
|
185
|
+
assert.deepEqual(storeUpserts, [
|
|
186
|
+
{
|
|
187
|
+
sessionId: 'fallback-session',
|
|
188
|
+
cwd: '/tmp/fallback-project',
|
|
189
|
+
sessionFile
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
sessionId: 'fallback-session',
|
|
193
|
+
cwd: '/tmp/fallback-project',
|
|
194
|
+
sessionFile
|
|
195
|
+
}
|
|
196
|
+
])
|
|
197
|
+
assert.deepEqual(conn.updates, [
|
|
198
|
+
{ sessionId: 'fallback-session', update: { sessionUpdate: 'current_mode_update', currentModeId: 'medium' } },
|
|
199
|
+
{
|
|
200
|
+
sessionId: 'fallback-session',
|
|
201
|
+
update: {
|
|
202
|
+
sessionUpdate: 'config_option_update',
|
|
203
|
+
configOptions: result.configOptions
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
])
|
|
207
|
+
} finally {
|
|
208
|
+
PiRpcProcess.spawn = originalSpawn
|
|
209
|
+
if (prevAgentDir === undefined) delete process.env.PI_CODING_AGENT_DIR
|
|
210
|
+
else process.env.PI_CODING_AGENT_DIR = prevAgentDir
|
|
211
|
+
}
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
test('PiAcpAgent: cancel ignores stale session IDs without spawning a restore process', async () => {
|
|
215
|
+
const conn = new FakeAgentSideConnection()
|
|
216
|
+
const spawnCalls: any[] = []
|
|
217
|
+
|
|
218
|
+
const originalSpawn = PiRpcProcess.spawn
|
|
219
|
+
;(PiRpcProcess as any).spawn = async (params: any) => {
|
|
220
|
+
spawnCalls.push(params)
|
|
221
|
+
return {
|
|
222
|
+
onEvent: () => () => {}
|
|
223
|
+
} as any
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const agent = new PiAcpAgent(asAgentConn(conn), {} as any)
|
|
228
|
+
;(agent as any).sessions = new FakeSessions(() => {
|
|
229
|
+
throw new Error('cancel should not restore a missing session')
|
|
230
|
+
}) as any
|
|
231
|
+
|
|
232
|
+
await agent.cancel({ sessionId: 'stale-session' } as any)
|
|
233
|
+
|
|
234
|
+
assert.deepEqual(spawnCalls, [])
|
|
235
|
+
assert.deepEqual(conn.updates, [])
|
|
236
|
+
} finally {
|
|
237
|
+
PiRpcProcess.spawn = originalSpawn
|
|
238
|
+
}
|
|
239
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import test from 'node:test'
|
|
2
|
+
import assert from 'node:assert/strict'
|
|
3
|
+
import {
|
|
4
|
+
expandSlashCommand,
|
|
5
|
+
parseCommandArgs,
|
|
6
|
+
substituteArgs,
|
|
7
|
+
toAvailableCommands
|
|
8
|
+
} from '../../src/acp/slash-commands.js'
|
|
9
|
+
|
|
10
|
+
test('parseCommandArgs: handles quotes', () => {
|
|
11
|
+
assert.deepEqual(parseCommandArgs('a b'), ['a', 'b'])
|
|
12
|
+
assert.deepEqual(parseCommandArgs("'a b' c"), ['a b', 'c'])
|
|
13
|
+
assert.deepEqual(parseCommandArgs('"a b" c'), ['a b', 'c'])
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
test('substituteArgs: replaces $1.. and $@', () => {
|
|
17
|
+
assert.equal(substituteArgs('x=$1 y=$2 all=$@', ['one', 'two']).trim(), 'x=one y=two all=one two')
|
|
18
|
+
assert.equal(substituteArgs('$3', ['one']).trim(), '')
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
test('expandSlashCommand: expands known command', () => {
|
|
22
|
+
const cmds = [{ name: 'hello', description: '(user)', content: 'Say hi to $1', source: '(user)' }]
|
|
23
|
+
|
|
24
|
+
assert.equal(expandSlashCommand('/hello world', cmds as any), 'Say hi to world')
|
|
25
|
+
assert.equal(expandSlashCommand('/unknown world', cmds as any), '/unknown world')
|
|
26
|
+
assert.equal(expandSlashCommand('not a command', cmds as any), 'not a command')
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
test('toAvailableCommands: de-dupes by name (first wins)', () => {
|
|
30
|
+
const cmds = [
|
|
31
|
+
{ name: 'x', description: 'first', content: '1', source: '(user)' },
|
|
32
|
+
{ name: 'x', description: 'second', content: '2', source: '(project)' }
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
assert.deepEqual(toAvailableCommands(cmds as any), [{ name: 'x', description: 'first' }])
|
|
36
|
+
})
|