@pickle-pee/genesis-cli 0.0.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/README.md +39 -0
- package/dist/bootstrap.d.ts +18 -0
- package/dist/bootstrap.js +89 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/input-loop.d.ts +42 -0
- package/dist/input-loop.js +344 -0
- package/dist/input-loop.js.map +1 -0
- package/dist/main.d.ts +35 -0
- package/dist/main.js +321 -0
- package/dist/main.js.map +1 -0
- package/dist/mode-dispatch.d.ts +100 -0
- package/dist/mode-dispatch.js +1830 -0
- package/dist/mode-dispatch.js.map +1 -0
- package/dist/rpc-server.d.ts +16 -0
- package/dist/rpc-server.js +339 -0
- package/dist/rpc-server.js.map +1 -0
- package/dist/session-store.d.ts +12 -0
- package/dist/session-store.js +48 -0
- package/dist/session-store.js.map +1 -0
- package/dist/terminal-display-width.d.ts +1 -0
- package/dist/terminal-display-width.js +33 -0
- package/dist/terminal-display-width.js.map +1 -0
- package/dist/test/bootstrap.test.d.ts +1 -0
- package/dist/test/bootstrap.test.js +50 -0
- package/dist/test/bootstrap.test.js.map +1 -0
- package/dist/test/input-loop-raw.test.d.ts +1 -0
- package/dist/test/input-loop-raw.test.js +204 -0
- package/dist/test/input-loop-raw.test.js.map +1 -0
- package/dist/test/interactive-tty-workbench.test.d.ts +1 -0
- package/dist/test/interactive-tty-workbench.test.js +647 -0
- package/dist/test/interactive-tty-workbench.test.js.map +1 -0
- package/dist/test/main.test.d.ts +1 -0
- package/dist/test/main.test.js +42 -0
- package/dist/test/main.test.js.map +1 -0
- package/dist/test/mode-dispatch.test.d.ts +1 -0
- package/dist/test/mode-dispatch.test.js +315 -0
- package/dist/test/mode-dispatch.test.js.map +1 -0
- package/dist/test/permission-flow.test.d.ts +7 -0
- package/dist/test/permission-flow.test.js +191 -0
- package/dist/test/permission-flow.test.js.map +1 -0
- package/dist/test/rpc-server.test.d.ts +7 -0
- package/dist/test/rpc-server.test.js +285 -0
- package/dist/test/rpc-server.test.js.map +1 -0
- package/dist/test/session-store.test.d.ts +1 -0
- package/dist/test/session-store.test.js +57 -0
- package/dist/test/session-store.test.js.map +1 -0
- package/dist/test/terminal-display-width.test.d.ts +1 -0
- package/dist/test/terminal-display-width.test.js +25 -0
- package/dist/test/terminal-display-width.test.js.map +1 -0
- package/dist/test/tty-session.test.d.ts +1 -0
- package/dist/test/tty-session.test.js +114 -0
- package/dist/test/tty-session.test.js.map +1 -0
- package/dist/theme.d.ts +16 -0
- package/dist/theme.js +20 -0
- package/dist/theme.js.map +1 -0
- package/dist/tty-session.d.ts +26 -0
- package/dist/tty-session.js +116 -0
- package/dist/tty-session.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +26 -0
|
@@ -0,0 +1,647 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const node_stream_1 = require("node:stream");
|
|
4
|
+
const runtime_1 = require("@pickle-pee/runtime");
|
|
5
|
+
const vitest_1 = require("vitest");
|
|
6
|
+
const mode_dispatch_js_1 = require("../mode-dispatch.js");
|
|
7
|
+
class FakeTtyInput extends node_stream_1.PassThrough {
|
|
8
|
+
isTTY = true;
|
|
9
|
+
setRawMode(_enabled) { }
|
|
10
|
+
resume() {
|
|
11
|
+
return super.resume();
|
|
12
|
+
}
|
|
13
|
+
pause() {
|
|
14
|
+
return super.pause();
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
class FakeTtyOutput extends node_stream_1.PassThrough {
|
|
18
|
+
isTTY = true;
|
|
19
|
+
columns = 80;
|
|
20
|
+
rows = 24;
|
|
21
|
+
}
|
|
22
|
+
class VirtualScreen {
|
|
23
|
+
width;
|
|
24
|
+
height;
|
|
25
|
+
lines;
|
|
26
|
+
row = 1;
|
|
27
|
+
column = 1;
|
|
28
|
+
constructor(width, height) {
|
|
29
|
+
this.width = width;
|
|
30
|
+
this.height = height;
|
|
31
|
+
this.lines = Array.from({ length: height }, () => Array.from({ length: width }, () => " "));
|
|
32
|
+
}
|
|
33
|
+
consume(chunk) {
|
|
34
|
+
for (let index = 0; index < chunk.length;) {
|
|
35
|
+
const char = chunk[index];
|
|
36
|
+
if (char === "\x1b") {
|
|
37
|
+
const consumed = this.consumeEscape(chunk, index);
|
|
38
|
+
index += consumed;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (char === "\r") {
|
|
42
|
+
this.column = 1;
|
|
43
|
+
index += 1;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (char === "\n") {
|
|
47
|
+
this.row = Math.min(this.height, this.row + 1);
|
|
48
|
+
index += 1;
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
this.writeChar(char);
|
|
52
|
+
index += 1;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
snapshot() {
|
|
56
|
+
return this.lines.map((line) => line.join("").replace(/\s+$/g, "")).join("\n");
|
|
57
|
+
}
|
|
58
|
+
consumeEscape(text, start) {
|
|
59
|
+
if (text[start + 1] !== "[") {
|
|
60
|
+
return 1;
|
|
61
|
+
}
|
|
62
|
+
let end = start + 2;
|
|
63
|
+
while (end < text.length && !/[A-Za-z]/.test(text[end] ?? "")) {
|
|
64
|
+
end += 1;
|
|
65
|
+
}
|
|
66
|
+
if (end >= text.length) {
|
|
67
|
+
return text.length - start;
|
|
68
|
+
}
|
|
69
|
+
const final = text[end] ?? "";
|
|
70
|
+
const body = text.slice(start + 2, end);
|
|
71
|
+
this.applyCsi(body, final);
|
|
72
|
+
return end - start + 1;
|
|
73
|
+
}
|
|
74
|
+
applyCsi(body, final) {
|
|
75
|
+
if (final === "m" || final === "h" || final === "l" || final === "r") {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (final === "H") {
|
|
79
|
+
if (body.length === 0) {
|
|
80
|
+
this.row = 1;
|
|
81
|
+
this.column = 1;
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const [row, column] = body.split(";");
|
|
85
|
+
this.row = Math.max(1, Number.parseInt(row ?? "1", 10) || 1);
|
|
86
|
+
this.column = Math.max(1, Number.parseInt(column ?? "1", 10) || 1);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (final === "J") {
|
|
90
|
+
for (let row = 0; row < this.height; row += 1) {
|
|
91
|
+
this.lines[row]?.fill(" ");
|
|
92
|
+
}
|
|
93
|
+
this.row = 1;
|
|
94
|
+
this.column = 1;
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (final === "K") {
|
|
98
|
+
this.lines[this.row - 1]?.fill(" ");
|
|
99
|
+
this.column = 1;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (final === "A") {
|
|
103
|
+
const amount = Math.max(1, Number.parseInt(body || "1", 10) || 1);
|
|
104
|
+
this.row = Math.max(1, this.row - amount);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (final === "C") {
|
|
108
|
+
const amount = Math.max(1, Number.parseInt(body || "1", 10) || 1);
|
|
109
|
+
this.column = Math.min(this.width, this.column + amount);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
writeChar(char) {
|
|
114
|
+
if (this.row < 1 || this.row > this.height || this.column < 1 || this.column > this.width) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.lines[this.row - 1][this.column - 1] = char;
|
|
118
|
+
this.column = Math.min(this.width, this.column + 1);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function findLineIndexContaining(snapshot, needle) {
|
|
122
|
+
return snapshot.split("\n").findIndex((line) => line.includes(needle));
|
|
123
|
+
}
|
|
124
|
+
function countOccurrences(snapshot, needle) {
|
|
125
|
+
return snapshot.split(needle).length - 1;
|
|
126
|
+
}
|
|
127
|
+
class FakeInteractiveSession {
|
|
128
|
+
id = { value: "tty-test-session" };
|
|
129
|
+
state = {
|
|
130
|
+
id: this.id,
|
|
131
|
+
model: { id: "glm-5.1", displayName: "GLM 5.1", provider: "zai" },
|
|
132
|
+
toolSet: [],
|
|
133
|
+
};
|
|
134
|
+
context = {
|
|
135
|
+
sessionId: this.id,
|
|
136
|
+
workingDirectory: "/tmp",
|
|
137
|
+
mode: "interactive",
|
|
138
|
+
model: this.state.model,
|
|
139
|
+
toolSet: new Set(["bash"]),
|
|
140
|
+
taskState: { status: "idle", currentTaskId: null, startedAt: null },
|
|
141
|
+
};
|
|
142
|
+
events = (0, runtime_1.createEventBus)();
|
|
143
|
+
plan = null;
|
|
144
|
+
sessionApprovedCommands = new Set();
|
|
145
|
+
pendingPermission = null;
|
|
146
|
+
isWaitingForPermission() {
|
|
147
|
+
return this.pendingPermission !== null;
|
|
148
|
+
}
|
|
149
|
+
async prompt(input) {
|
|
150
|
+
if (input === "hello") {
|
|
151
|
+
this.emit({
|
|
152
|
+
id: "thinking-1",
|
|
153
|
+
timestamp: Date.now(),
|
|
154
|
+
sessionId: this.id,
|
|
155
|
+
category: "text",
|
|
156
|
+
type: "thinking_delta",
|
|
157
|
+
content: "...",
|
|
158
|
+
});
|
|
159
|
+
this.emit({
|
|
160
|
+
id: "text-1",
|
|
161
|
+
timestamp: Date.now(),
|
|
162
|
+
sessionId: this.id,
|
|
163
|
+
category: "text",
|
|
164
|
+
type: "text_delta",
|
|
165
|
+
content: "Hi from Genesis",
|
|
166
|
+
});
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (input === "bash echo hello") {
|
|
170
|
+
if (this.sessionApprovedCommands.has("echo hello")) {
|
|
171
|
+
this.emitBashExecution("bash-echo-2", "echo hello", "Echo: hello");
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
this.emit({
|
|
175
|
+
id: "perm-1",
|
|
176
|
+
timestamp: Date.now(),
|
|
177
|
+
sessionId: this.id,
|
|
178
|
+
category: "permission",
|
|
179
|
+
type: "permission_requested",
|
|
180
|
+
toolName: "bash",
|
|
181
|
+
toolCallId: "bash-echo-1",
|
|
182
|
+
riskLevel: "L3",
|
|
183
|
+
});
|
|
184
|
+
await new Promise((resolve) => {
|
|
185
|
+
this.pendingPermission = {
|
|
186
|
+
callId: "bash-echo-1",
|
|
187
|
+
toolName: "bash",
|
|
188
|
+
command: "echo hello",
|
|
189
|
+
resolve,
|
|
190
|
+
};
|
|
191
|
+
});
|
|
192
|
+
this.emitBashExecution("bash-echo-1", "echo hello", "Echo: hello");
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
if (input === "bash pwd") {
|
|
196
|
+
this.emitBashExecution("bash-pwd", "pwd", "/tmp");
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
if (input === "bash ls") {
|
|
200
|
+
this.emitBashExecution("bash-ls", "ls", "README.md\npackages");
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (input === "bash cat README.md") {
|
|
204
|
+
this.emitBashExecution("bash-cat", "cat README.md", "# Genesis CLI");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (input === "bash head -n 5 README.md") {
|
|
208
|
+
this.emitBashExecution("bash-head", "head -n 5 README.md", "# Genesis CLI");
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (input === 'bash tail -f "logs/app.log"') {
|
|
212
|
+
this.emitBashExecution("bash-tail", 'tail -f "logs/app.log"', "tailing logs/app.log");
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
if (input === "bash wc -l README.md") {
|
|
216
|
+
this.emitBashExecution("bash-wc", "wc -l README.md", "42 README.md");
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (input === 'bash grep -n "Genesis CLI" README.md') {
|
|
220
|
+
this.emitBashExecution("bash-grep", 'grep -n "Genesis CLI" README.md', "1:# Genesis CLI");
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (input === 'bash rg -n "createToolGovernor" packages') {
|
|
224
|
+
this.emitBashExecution("bash-rg", 'rg -n "createToolGovernor" packages', "packages/app-runtime/src/governance/tool-governor.ts:1:createToolGovernor");
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (input === 'bash find . -name "*.ts" -type f') {
|
|
228
|
+
this.emitBashExecution("bash-find", 'find . -name "*.ts" -type f', "./packages/app-cli/src/mode-dispatch.ts");
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
if (input === 'bash fd -t f "governor" packages') {
|
|
232
|
+
this.emitBashExecution("bash-fd", 'fd -t f "governor" packages', "packages/app-runtime/src/governance/tool-governor.ts");
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
if (input === "write file") {
|
|
236
|
+
this.emit({
|
|
237
|
+
id: "perm-write-1",
|
|
238
|
+
timestamp: Date.now(),
|
|
239
|
+
sessionId: this.id,
|
|
240
|
+
category: "permission",
|
|
241
|
+
type: "permission_requested",
|
|
242
|
+
toolName: "write",
|
|
243
|
+
toolCallId: "write-file-1",
|
|
244
|
+
riskLevel: "L3",
|
|
245
|
+
});
|
|
246
|
+
await new Promise((resolve) => {
|
|
247
|
+
this.pendingPermission = { callId: "write-file-1", toolName: "write", resolve };
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (input === "edit file") {
|
|
252
|
+
this.emit({
|
|
253
|
+
id: "perm-edit-1",
|
|
254
|
+
timestamp: Date.now(),
|
|
255
|
+
sessionId: this.id,
|
|
256
|
+
category: "permission",
|
|
257
|
+
type: "permission_requested",
|
|
258
|
+
toolName: "edit",
|
|
259
|
+
toolCallId: "edit-file-1",
|
|
260
|
+
riskLevel: "L3",
|
|
261
|
+
});
|
|
262
|
+
await new Promise((resolve) => {
|
|
263
|
+
this.pendingPermission = { callId: "edit-file-1", toolName: "edit", resolve };
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
async continue(input) {
|
|
269
|
+
return this.prompt(input);
|
|
270
|
+
}
|
|
271
|
+
abort() { }
|
|
272
|
+
async close() { }
|
|
273
|
+
async resolvePermission(callId, decision) {
|
|
274
|
+
if (!this.pendingPermission || this.pendingPermission.callId !== callId) {
|
|
275
|
+
throw new Error(`Unexpected permission resolution: ${callId}`);
|
|
276
|
+
}
|
|
277
|
+
if (decision === "allow_for_session" &&
|
|
278
|
+
this.pendingPermission.toolName === "bash" &&
|
|
279
|
+
this.pendingPermission.command) {
|
|
280
|
+
this.sessionApprovedCommands.add(this.pendingPermission.command);
|
|
281
|
+
}
|
|
282
|
+
const toolName = this.pendingPermission.toolName;
|
|
283
|
+
this.emit({
|
|
284
|
+
id: `resolved-${callId}`,
|
|
285
|
+
timestamp: Date.now(),
|
|
286
|
+
sessionId: this.id,
|
|
287
|
+
category: "permission",
|
|
288
|
+
type: "permission_resolved",
|
|
289
|
+
toolName,
|
|
290
|
+
toolCallId: callId,
|
|
291
|
+
decision,
|
|
292
|
+
});
|
|
293
|
+
const pending = this.pendingPermission;
|
|
294
|
+
this.pendingPermission = null;
|
|
295
|
+
pending.resolve();
|
|
296
|
+
}
|
|
297
|
+
onStateChange() {
|
|
298
|
+
return () => { };
|
|
299
|
+
}
|
|
300
|
+
async compact() { }
|
|
301
|
+
emit(event) {
|
|
302
|
+
this.events.emit(event);
|
|
303
|
+
}
|
|
304
|
+
emitBashExecution(callId, command, result) {
|
|
305
|
+
this.emit({
|
|
306
|
+
id: `tool-start-${callId}`,
|
|
307
|
+
timestamp: Date.now(),
|
|
308
|
+
sessionId: this.id,
|
|
309
|
+
category: "tool",
|
|
310
|
+
type: "tool_started",
|
|
311
|
+
toolName: "bash",
|
|
312
|
+
toolCallId: callId,
|
|
313
|
+
parameters: { command },
|
|
314
|
+
});
|
|
315
|
+
this.emit({
|
|
316
|
+
id: `tool-end-${callId}`,
|
|
317
|
+
timestamp: Date.now(),
|
|
318
|
+
sessionId: this.id,
|
|
319
|
+
category: "tool",
|
|
320
|
+
type: "tool_completed",
|
|
321
|
+
toolName: "bash",
|
|
322
|
+
toolCallId: callId,
|
|
323
|
+
status: "success",
|
|
324
|
+
durationMs: 10,
|
|
325
|
+
result,
|
|
326
|
+
});
|
|
327
|
+
this.emit({
|
|
328
|
+
id: `tool-text-${callId}`,
|
|
329
|
+
timestamp: Date.now(),
|
|
330
|
+
sessionId: this.id,
|
|
331
|
+
category: "text",
|
|
332
|
+
type: "text_delta",
|
|
333
|
+
content: result,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
queueMultiChunkHello() {
|
|
337
|
+
this.prompt = async (input) => {
|
|
338
|
+
if (input === "hello") {
|
|
339
|
+
this.emit({
|
|
340
|
+
id: "thinking-1",
|
|
341
|
+
timestamp: Date.now(),
|
|
342
|
+
sessionId: this.id,
|
|
343
|
+
category: "text",
|
|
344
|
+
type: "thinking_delta",
|
|
345
|
+
content: "...",
|
|
346
|
+
});
|
|
347
|
+
for (const [index, chunk] of ["Hi", "Hi from", "Hi from Genesis"].entries()) {
|
|
348
|
+
this.emit({
|
|
349
|
+
id: `text-chunk-${index}`,
|
|
350
|
+
timestamp: Date.now(),
|
|
351
|
+
sessionId: this.id,
|
|
352
|
+
category: "text",
|
|
353
|
+
type: "text_delta",
|
|
354
|
+
content: chunk,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
return FakeInteractiveSession.prototype.prompt.call(this, input);
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function createFakeRuntime(session) {
|
|
364
|
+
const events = (0, runtime_1.createEventBus)();
|
|
365
|
+
return {
|
|
366
|
+
createSession: () => session,
|
|
367
|
+
recoverSession: () => session,
|
|
368
|
+
events,
|
|
369
|
+
governor: (0, runtime_1.createToolGovernor)(),
|
|
370
|
+
planEngine: (0, runtime_1.createPlanEngine)(),
|
|
371
|
+
shutdown: async () => { },
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
async function waitFor(check, timeoutMs = 1000) {
|
|
375
|
+
const start = Date.now();
|
|
376
|
+
while (!check()) {
|
|
377
|
+
if (Date.now() - start > timeoutMs) {
|
|
378
|
+
throw new Error("Timed out waiting for condition");
|
|
379
|
+
}
|
|
380
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
async function withPatchedProcessTty(input, output, run) {
|
|
384
|
+
const screen = new VirtualScreen(output.columns, output.rows);
|
|
385
|
+
output.on("data", (chunk) => {
|
|
386
|
+
screen.consume(chunk.toString("utf8"));
|
|
387
|
+
});
|
|
388
|
+
const stdinDescriptor = Object.getOwnPropertyDescriptor(process, "stdin");
|
|
389
|
+
const stdoutDescriptor = Object.getOwnPropertyDescriptor(process, "stdout");
|
|
390
|
+
Object.defineProperty(process, "stdin", { value: input, configurable: true });
|
|
391
|
+
Object.defineProperty(process, "stdout", { value: output, configurable: true });
|
|
392
|
+
try {
|
|
393
|
+
return await run(screen);
|
|
394
|
+
}
|
|
395
|
+
finally {
|
|
396
|
+
if (stdinDescriptor) {
|
|
397
|
+
Object.defineProperty(process, "stdin", stdinDescriptor);
|
|
398
|
+
}
|
|
399
|
+
if (stdoutDescriptor) {
|
|
400
|
+
Object.defineProperty(process, "stdout", stdoutDescriptor);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
(0, vitest_1.afterEach)(() => {
|
|
405
|
+
// defensive no-op; process descriptors are restored per-test
|
|
406
|
+
});
|
|
407
|
+
(0, vitest_1.describe)("interactive workbench TTY", () => {
|
|
408
|
+
(0, vitest_1.it)("renders a completed assistant reply and keeps the composer visible", async () => {
|
|
409
|
+
const session = new FakeInteractiveSession();
|
|
410
|
+
const runtime = createFakeRuntime(session);
|
|
411
|
+
const input = new FakeTtyInput();
|
|
412
|
+
const output = new FakeTtyOutput();
|
|
413
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
414
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
415
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
416
|
+
input.write("hello\r");
|
|
417
|
+
await waitFor(() => screen.snapshot().includes("Hi from Genesis"));
|
|
418
|
+
const snapshot = screen.snapshot();
|
|
419
|
+
(0, vitest_1.expect)(snapshot).toContain("hello");
|
|
420
|
+
(0, vitest_1.expect)(snapshot).toContain("Hi from Genesis");
|
|
421
|
+
(0, vitest_1.expect)(snapshot).toContain("❯");
|
|
422
|
+
const userLine = findLineIndexContaining(snapshot, "hello");
|
|
423
|
+
const assistantLine = findLineIndexContaining(snapshot, "Hi from Genesis");
|
|
424
|
+
const footerSeparatorLine = findLineIndexContaining(snapshot, "────────────────");
|
|
425
|
+
(0, vitest_1.expect)(assistantLine - userLine).toBeGreaterThanOrEqual(2);
|
|
426
|
+
(0, vitest_1.expect)(footerSeparatorLine - assistantLine).toBeLessThanOrEqual(2);
|
|
427
|
+
input.write("/exit\r");
|
|
428
|
+
await startPromise;
|
|
429
|
+
});
|
|
430
|
+
}, 10000);
|
|
431
|
+
(0, vitest_1.it)("does not leave duplicate assistant lines behind across streaming updates", async () => {
|
|
432
|
+
const session = new FakeInteractiveSession();
|
|
433
|
+
session.queueMultiChunkHello();
|
|
434
|
+
const runtime = createFakeRuntime(session);
|
|
435
|
+
const input = new FakeTtyInput();
|
|
436
|
+
const output = new FakeTtyOutput();
|
|
437
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
438
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
439
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
440
|
+
input.write("hello\r");
|
|
441
|
+
await waitFor(() => screen.snapshot().includes("Hi from Genesis"));
|
|
442
|
+
const snapshot = screen.snapshot();
|
|
443
|
+
(0, vitest_1.expect)(countOccurrences(snapshot, "⏺ Hi from Genesis")).toBe(1);
|
|
444
|
+
input.write("/exit\r");
|
|
445
|
+
await startPromise;
|
|
446
|
+
});
|
|
447
|
+
}, 10000);
|
|
448
|
+
(0, vitest_1.it)("shows permission UI and clears it after allow-for-session approval", async () => {
|
|
449
|
+
const session = new FakeInteractiveSession();
|
|
450
|
+
const runtime = createFakeRuntime(session);
|
|
451
|
+
const input = new FakeTtyInput();
|
|
452
|
+
const output = new FakeTtyOutput();
|
|
453
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
454
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
455
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
456
|
+
input.write("bash echo hello\r");
|
|
457
|
+
await waitFor(() => session.isWaitingForPermission());
|
|
458
|
+
await waitFor(() => screen.snapshot().includes("choice [Enter/1/2/3]>"));
|
|
459
|
+
(0, vitest_1.expect)(screen.snapshot()).toContain("bash echo hello");
|
|
460
|
+
input.write("2\r");
|
|
461
|
+
await waitFor(() => screen.snapshot().includes("Echo: hello"));
|
|
462
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
463
|
+
input.write("bash echo hello\r");
|
|
464
|
+
await waitFor(() => countOccurrences(screen.snapshot(), "Echo: hello") >= 2);
|
|
465
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
466
|
+
input.write("/exit\r");
|
|
467
|
+
await startPromise;
|
|
468
|
+
});
|
|
469
|
+
}, 10000);
|
|
470
|
+
(0, vitest_1.it)("does not show a permission prompt for bash pwd", async () => {
|
|
471
|
+
const session = new FakeInteractiveSession();
|
|
472
|
+
const runtime = createFakeRuntime(session);
|
|
473
|
+
const input = new FakeTtyInput();
|
|
474
|
+
const output = new FakeTtyOutput();
|
|
475
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
476
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
477
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
478
|
+
input.write("bash pwd\r");
|
|
479
|
+
await waitFor(() => screen.snapshot().includes("/tmp"));
|
|
480
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
481
|
+
input.write("/exit\r");
|
|
482
|
+
await startPromise;
|
|
483
|
+
});
|
|
484
|
+
}, 10000);
|
|
485
|
+
(0, vitest_1.it)("does not show a permission prompt for bash ls", async () => {
|
|
486
|
+
const session = new FakeInteractiveSession();
|
|
487
|
+
const runtime = createFakeRuntime(session);
|
|
488
|
+
const input = new FakeTtyInput();
|
|
489
|
+
const output = new FakeTtyOutput();
|
|
490
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
491
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
492
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
493
|
+
input.write("bash ls\r");
|
|
494
|
+
await waitFor(() => screen.snapshot().includes("README.md"));
|
|
495
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
496
|
+
input.write("/exit\r");
|
|
497
|
+
await startPromise;
|
|
498
|
+
});
|
|
499
|
+
}, 10000);
|
|
500
|
+
(0, vitest_1.it)("does not show a permission prompt for bash cat", async () => {
|
|
501
|
+
const session = new FakeInteractiveSession();
|
|
502
|
+
const runtime = createFakeRuntime(session);
|
|
503
|
+
const input = new FakeTtyInput();
|
|
504
|
+
const output = new FakeTtyOutput();
|
|
505
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
506
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
507
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
508
|
+
input.write("bash cat README.md\r");
|
|
509
|
+
await waitFor(() => screen.snapshot().includes("# Genesis CLI"));
|
|
510
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
511
|
+
input.write("/exit\r");
|
|
512
|
+
await startPromise;
|
|
513
|
+
});
|
|
514
|
+
}, 10000);
|
|
515
|
+
(0, vitest_1.it)("does not show a permission prompt for bash head", async () => {
|
|
516
|
+
const session = new FakeInteractiveSession();
|
|
517
|
+
const runtime = createFakeRuntime(session);
|
|
518
|
+
const input = new FakeTtyInput();
|
|
519
|
+
const output = new FakeTtyOutput();
|
|
520
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
521
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
522
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
523
|
+
input.write("bash head -n 5 README.md\r");
|
|
524
|
+
await waitFor(() => screen.snapshot().includes("# Genesis CLI"));
|
|
525
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
526
|
+
input.write("/exit\r");
|
|
527
|
+
await startPromise;
|
|
528
|
+
});
|
|
529
|
+
}, 10000);
|
|
530
|
+
(0, vitest_1.it)("does not show a permission prompt for bash tail", async () => {
|
|
531
|
+
const session = new FakeInteractiveSession();
|
|
532
|
+
const runtime = createFakeRuntime(session);
|
|
533
|
+
const input = new FakeTtyInput();
|
|
534
|
+
const output = new FakeTtyOutput();
|
|
535
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
536
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
537
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
538
|
+
input.write('bash tail -f "logs/app.log"\r');
|
|
539
|
+
await waitFor(() => screen.snapshot().includes("tailing logs/app.log"));
|
|
540
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
541
|
+
input.write("/exit\r");
|
|
542
|
+
await startPromise;
|
|
543
|
+
});
|
|
544
|
+
}, 10000);
|
|
545
|
+
(0, vitest_1.it)("does not show a permission prompt for bash wc", async () => {
|
|
546
|
+
const session = new FakeInteractiveSession();
|
|
547
|
+
const runtime = createFakeRuntime(session);
|
|
548
|
+
const input = new FakeTtyInput();
|
|
549
|
+
const output = new FakeTtyOutput();
|
|
550
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
551
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
552
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
553
|
+
input.write("bash wc -l README.md\r");
|
|
554
|
+
await waitFor(() => screen.snapshot().includes("42 README.md"));
|
|
555
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
556
|
+
input.write("/exit\r");
|
|
557
|
+
await startPromise;
|
|
558
|
+
});
|
|
559
|
+
}, 10000);
|
|
560
|
+
(0, vitest_1.it)("does not show a permission prompt for bash grep", async () => {
|
|
561
|
+
const session = new FakeInteractiveSession();
|
|
562
|
+
const runtime = createFakeRuntime(session);
|
|
563
|
+
const input = new FakeTtyInput();
|
|
564
|
+
const output = new FakeTtyOutput();
|
|
565
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
566
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
567
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
568
|
+
input.write('bash grep -n "Genesis CLI" README.md\r');
|
|
569
|
+
await waitFor(() => screen.snapshot().includes("1:# Genesis CLI"));
|
|
570
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
571
|
+
input.write("/exit\r");
|
|
572
|
+
await startPromise;
|
|
573
|
+
});
|
|
574
|
+
}, 10000);
|
|
575
|
+
(0, vitest_1.it)("does not show a permission prompt for bash rg", async () => {
|
|
576
|
+
const session = new FakeInteractiveSession();
|
|
577
|
+
const runtime = createFakeRuntime(session);
|
|
578
|
+
const input = new FakeTtyInput();
|
|
579
|
+
const output = new FakeTtyOutput();
|
|
580
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
581
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
582
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
583
|
+
input.write('bash rg -n "createToolGovernor" packages\r');
|
|
584
|
+
await waitFor(() => screen.snapshot().includes("createToolGovernor"));
|
|
585
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
586
|
+
input.write("/exit\r");
|
|
587
|
+
await startPromise;
|
|
588
|
+
});
|
|
589
|
+
}, 10000);
|
|
590
|
+
(0, vitest_1.it)("does not show a permission prompt for bash find", async () => {
|
|
591
|
+
const session = new FakeInteractiveSession();
|
|
592
|
+
const runtime = createFakeRuntime(session);
|
|
593
|
+
const input = new FakeTtyInput();
|
|
594
|
+
const output = new FakeTtyOutput();
|
|
595
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
596
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
597
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
598
|
+
input.write('bash find . -name "*.ts" -type f\r');
|
|
599
|
+
await waitFor(() => screen.snapshot().includes("./packages/app-cli/src/mode-dispatch.ts"));
|
|
600
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
601
|
+
input.write("/exit\r");
|
|
602
|
+
await startPromise;
|
|
603
|
+
});
|
|
604
|
+
}, 10000);
|
|
605
|
+
(0, vitest_1.it)("does not show a permission prompt for bash fd", async () => {
|
|
606
|
+
const session = new FakeInteractiveSession();
|
|
607
|
+
const runtime = createFakeRuntime(session);
|
|
608
|
+
const input = new FakeTtyInput();
|
|
609
|
+
const output = new FakeTtyOutput();
|
|
610
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
611
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
612
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
613
|
+
input.write('bash fd -t f "governor" packages\r');
|
|
614
|
+
await waitFor(() => screen.snapshot().includes("tool-governor.ts"));
|
|
615
|
+
(0, vitest_1.expect)(screen.snapshot()).not.toContain("choice [Enter/1/2/3]>");
|
|
616
|
+
input.write("/exit\r");
|
|
617
|
+
await startPromise;
|
|
618
|
+
});
|
|
619
|
+
}, 10000);
|
|
620
|
+
(0, vitest_1.it)("keeps the user prompt visible when write/edit permissions are requested", async () => {
|
|
621
|
+
const session = new FakeInteractiveSession();
|
|
622
|
+
const runtime = createFakeRuntime(session);
|
|
623
|
+
const input = new FakeTtyInput();
|
|
624
|
+
const output = new FakeTtyOutput();
|
|
625
|
+
await withPatchedProcessTty(input, output, async (screen) => {
|
|
626
|
+
const startPromise = (0, mode_dispatch_js_1.createModeHandler)("interactive").start(runtime);
|
|
627
|
+
await waitFor(() => screen.snapshot().includes("❯"));
|
|
628
|
+
input.write("write file\r");
|
|
629
|
+
await waitFor(() => session.isWaitingForPermission());
|
|
630
|
+
await waitFor(() => screen.snapshot().includes("choice [Enter/1/2/3]>"));
|
|
631
|
+
(0, vitest_1.expect)(screen.snapshot()).toContain("write file");
|
|
632
|
+
(0, vitest_1.expect)(screen.snapshot()).toContain("Write");
|
|
633
|
+
input.write("3\r");
|
|
634
|
+
await waitFor(() => !screen.snapshot().includes("choice [Enter/1/2/3]>"));
|
|
635
|
+
input.write("edit file\r");
|
|
636
|
+
await waitFor(() => session.isWaitingForPermission());
|
|
637
|
+
await waitFor(() => screen.snapshot().includes("choice [Enter/1/2/3]>"));
|
|
638
|
+
(0, vitest_1.expect)(screen.snapshot()).toContain("edit file");
|
|
639
|
+
(0, vitest_1.expect)(screen.snapshot()).toContain("Edit");
|
|
640
|
+
input.write("3\r");
|
|
641
|
+
await waitFor(() => !screen.snapshot().includes("choice [Enter/1/2/3]>"));
|
|
642
|
+
input.write("/exit\r");
|
|
643
|
+
await startPromise;
|
|
644
|
+
});
|
|
645
|
+
}, 10000);
|
|
646
|
+
});
|
|
647
|
+
//# sourceMappingURL=interactive-tty-workbench.test.js.map
|