@cueloop/daemon 0.1.0-alpha.10 → 0.1.0-alpha.100
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 +7 -0
- package/SDK.md +102 -0
- package/package.json +43 -22
- package/src/agent-tools.ts +98 -0
- package/src/agent-turn.test.ts +24 -0
- package/src/agent-turn.ts +34 -0
- package/src/api.test.ts +1081 -65
- package/src/api.ts +1425 -89
- package/src/capabilities.test.ts +57 -0
- package/src/capabilities.ts +95 -0
- package/src/client-errors.ts +24 -0
- package/src/client-reliability.test.ts +189 -0
- package/src/client.test.ts +37 -0
- package/src/client.ts +820 -94
- package/src/curate.ts +109 -0
- package/src/daemon-sdk-effect.ts +113 -0
- package/src/daemon-sdk.test.ts +306 -0
- package/src/daemon-sdk.ts +397 -0
- package/src/daemon-version.test.ts +142 -0
- package/src/diff-hot-reload.test.ts +419 -0
- package/src/diff-watcher.test.ts +117 -0
- package/src/diff-watcher.ts +326 -0
- package/src/examples/daemon-sdk.ts +55 -0
- package/src/gh.test.ts +59 -0
- package/src/gh.ts +74 -0
- package/src/ghostty-thread-surface-store.test.ts +49 -0
- package/src/ghostty-thread-surface-store.ts +101 -0
- package/src/ghostty-thread-surface.test.ts +398 -0
- package/src/ghostty-thread-surface.ts +267 -0
- package/src/harness-delivery.test.ts +269 -0
- package/src/harness-diagnostics.test.ts +26 -0
- package/src/harness-diagnostics.ts +30 -0
- package/src/harness-state-store.test.ts +93 -0
- package/src/harness-state-store.ts +213 -0
- package/src/herdr-split.test.ts +106 -0
- package/src/herdr-split.ts +85 -0
- package/src/herdr-thread-surface-store.test.ts +81 -0
- package/src/herdr-thread-surface-store.ts +66 -0
- package/src/herdr-thread-surface.test.ts +465 -0
- package/src/herdr-thread-surface.ts +378 -0
- package/src/index.ts +8 -9
- package/src/interruptible-wait.test.ts +127 -0
- package/src/interruptible-wait.ts +66 -0
- package/src/jj-working-tree.ts +131 -0
- package/src/main.ts +4 -1
- package/src/operation-receipts.test.ts +111 -0
- package/src/operation-receipts.ts +91 -0
- package/src/paths.ts +49 -0
- package/src/pr-poller.test.ts +102 -0
- package/src/pr-poller.ts +73 -0
- package/src/project-files.test.ts +134 -0
- package/src/project-files.ts +68 -0
- package/src/protocol.ts +78 -7
- package/src/retention.test.ts +191 -0
- package/src/retention.ts +123 -0
- package/src/sdk-identities.ts +35 -0
- package/src/server.test.ts +456 -42
- package/src/server.ts +601 -88
- package/src/share-blob.test.ts +119 -0
- package/src/share-blob.ts +57 -0
- package/src/single-instance.test.ts +162 -13
- package/src/store.conformance.test.ts +41 -0
- package/src/store.jsonl.test.ts +173 -0
- package/src/store.ts +267 -34
- package/src/testing/store-conformance.ts +192 -0
- package/src/thread-agent-validation.ts +119 -0
- package/src/thread-agent.test.ts +412 -0
- package/src/thread-agent.ts +713 -0
- package/src/thread-review.test.ts +483 -0
- package/src/thread-review.ts +382 -0
- package/src/thread-subscription.test.ts +174 -0
- package/src/thread-subscription.ts +129 -0
- package/src/thread-surface-config.test.ts +63 -0
- package/src/thread-surface-config.ts +61 -0
- package/src/validate.test.ts +279 -37
- package/src/validate.ts +418 -33
- package/src/vcs-source.test.ts +180 -0
- package/src/vcs-source.ts +229 -0
- package/src/version.ts +6 -0
- package/src/working-tree.ts +150 -0
- package/src/review.test.ts +0 -167
- package/src/review.ts +0 -174
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { SCHEMA_VERSION, type Thread, type AgentHarnessAdapter } from "@cueloop/schema";
|
|
6
|
+
import { ThreadAgentManager } from "./thread-agent";
|
|
7
|
+
|
|
8
|
+
function createTestThread(home: string): Thread {
|
|
9
|
+
return {
|
|
10
|
+
schemaVersion: SCHEMA_VERSION,
|
|
11
|
+
id: "adapter-test",
|
|
12
|
+
workspace: { repoRoot: home, branch: "main" },
|
|
13
|
+
artifact: { type: "plan", content: "Review the retry", meta: {} },
|
|
14
|
+
revisions: [],
|
|
15
|
+
annotations: [],
|
|
16
|
+
message: null,
|
|
17
|
+
status: "pending",
|
|
18
|
+
createdAt: "2026-10-04",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
test("a non-ACP harness streams and resumes through the same Thread manager", async () => {
|
|
23
|
+
const home = mkdtempSync(join(tmpdir(), "cueloop-harness-"));
|
|
24
|
+
const thread = createTestThread(home);
|
|
25
|
+
const sessions: (string | undefined)[] = [];
|
|
26
|
+
const adapter: AgentHarnessAdapter = {
|
|
27
|
+
id: "test-native",
|
|
28
|
+
label: "Native agent",
|
|
29
|
+
connect(options) {
|
|
30
|
+
sessions.push(options.sessionId);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
start: async () => options.sessionId ?? "native-session",
|
|
34
|
+
prompt: async () => {
|
|
35
|
+
options.onEvent({
|
|
36
|
+
kind: "tool",
|
|
37
|
+
id: "read",
|
|
38
|
+
title: "Read retry",
|
|
39
|
+
toolKind: "read",
|
|
40
|
+
status: "completed",
|
|
41
|
+
});
|
|
42
|
+
options.onEvent({ kind: "message", id: crypto.randomUUID(), text: "Native answer" });
|
|
43
|
+
|
|
44
|
+
return { outcome: "completed" as const };
|
|
45
|
+
},
|
|
46
|
+
cancel() {},
|
|
47
|
+
permission() {},
|
|
48
|
+
close() {},
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
let manager = new ThreadAgentManager({
|
|
53
|
+
home,
|
|
54
|
+
enabled: true,
|
|
55
|
+
adapter,
|
|
56
|
+
getThread: () => thread,
|
|
57
|
+
onChange() {},
|
|
58
|
+
});
|
|
59
|
+
const finish = async () => {
|
|
60
|
+
for (let attempt = 0; manager.get(thread.id).phase.kind !== "idle" && attempt < 100; attempt++)
|
|
61
|
+
await Bun.sleep(1);
|
|
62
|
+
expect(manager.get(thread.id).phase).toEqual({ kind: "idle" });
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
manager.prompt({ id: thread.id, text: "Explain" });
|
|
67
|
+
await finish();
|
|
68
|
+
expect(manager.get(thread.id).harness).toEqual({
|
|
69
|
+
id: "test-native",
|
|
70
|
+
label: "Native agent",
|
|
71
|
+
sessionId: "native-session",
|
|
72
|
+
});
|
|
73
|
+
expect(manager.get(thread.id).messages[1]?.text).toBe("Native answer");
|
|
74
|
+
const promptDiscussion = { id: manager.get(thread.id).submissions![0]!.id };
|
|
75
|
+
|
|
76
|
+
expect(manager.isReadOnly(thread.id, promptDiscussion.id)).toBe(true);
|
|
77
|
+
const promptReply = manager.reply(thread.id, promptDiscussion.id, "The prompt is clear");
|
|
78
|
+
|
|
79
|
+
expect(promptReply.comments.at(-1)?.replyTo).toBe(promptDiscussion.id);
|
|
80
|
+
const answer = manager.get(thread.id).messages[1]!;
|
|
81
|
+
|
|
82
|
+
manager.comment({
|
|
83
|
+
id: thread.id,
|
|
84
|
+
comment: {
|
|
85
|
+
id: "answer-question",
|
|
86
|
+
messageId: answer.id,
|
|
87
|
+
anchor: { quote: "Native", prefix: "", suffix: " answer", blockIndex: 0, start: 0, end: 6 },
|
|
88
|
+
body: "Explain this",
|
|
89
|
+
sent: false,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
const replied = manager.reply(thread.id, "answer-question", "This is the explanation");
|
|
93
|
+
|
|
94
|
+
expect(replied.comments.at(-1)).toMatchObject({
|
|
95
|
+
replyTo: "answer-question",
|
|
96
|
+
messageId: answer.id,
|
|
97
|
+
sent: true,
|
|
98
|
+
author: "embedded-agent",
|
|
99
|
+
});
|
|
100
|
+
manager.comment({
|
|
101
|
+
id: thread.id,
|
|
102
|
+
comment: {
|
|
103
|
+
...replied.comments.find((entry) => entry.id === "answer-question")!,
|
|
104
|
+
id: "answer-followup",
|
|
105
|
+
replyTo: "answer-question",
|
|
106
|
+
body: "Why?",
|
|
107
|
+
sent: false,
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
manager.prompt({ id: thread.id, text: "" });
|
|
111
|
+
expect(
|
|
112
|
+
manager.get(thread.id).submissions?.find((entry) => entry.commentId === "answer-followup")
|
|
113
|
+
?.context,
|
|
114
|
+
).toContain("This is the explanation");
|
|
115
|
+
await finish();
|
|
116
|
+
manager.dispose();
|
|
117
|
+
manager = new ThreadAgentManager({
|
|
118
|
+
home,
|
|
119
|
+
enabled: true,
|
|
120
|
+
adapter,
|
|
121
|
+
getThread: () => thread,
|
|
122
|
+
onChange() {},
|
|
123
|
+
});
|
|
124
|
+
manager.prompt({ id: thread.id, text: "Continue" });
|
|
125
|
+
await finish();
|
|
126
|
+
expect(sessions).toEqual([undefined, "native-session"]);
|
|
127
|
+
const other = new ThreadAgentManager({
|
|
128
|
+
home,
|
|
129
|
+
enabled: true,
|
|
130
|
+
adapter: { ...adapter, id: "another-agent" },
|
|
131
|
+
getThread: () => thread,
|
|
132
|
+
onChange() {},
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(() => other.prompt({ id: thread.id, text: "Mix sessions" })).toThrow(
|
|
136
|
+
"different harness",
|
|
137
|
+
);
|
|
138
|
+
other.dispose();
|
|
139
|
+
} finally {
|
|
140
|
+
manager.dispose();
|
|
141
|
+
rmSync(home, { recursive: true, force: true });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("the first prototype's fx records migrate without losing session identity", () => {
|
|
146
|
+
const home = mkdtempSync(join(tmpdir(), "cueloop-harness-migrate-"));
|
|
147
|
+
const thread = createTestThread(home);
|
|
148
|
+
const path = join(home, "thread-agents");
|
|
149
|
+
|
|
150
|
+
mkdirSync(path);
|
|
151
|
+
writeFileSync(
|
|
152
|
+
join(path, `${thread.id}.json`),
|
|
153
|
+
JSON.stringify({
|
|
154
|
+
threadId: thread.id,
|
|
155
|
+
fxSessionId: "saved-fx-session",
|
|
156
|
+
phase: { kind: "idle" },
|
|
157
|
+
messages: [],
|
|
158
|
+
tools: [],
|
|
159
|
+
comments: [],
|
|
160
|
+
}),
|
|
161
|
+
);
|
|
162
|
+
const manager = new ThreadAgentManager({
|
|
163
|
+
home,
|
|
164
|
+
enabled: false,
|
|
165
|
+
getThread: () => thread,
|
|
166
|
+
onChange() {},
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
expect(manager.get(thread.id).harness).toEqual({
|
|
171
|
+
id: "fx",
|
|
172
|
+
label: "fx",
|
|
173
|
+
sessionId: "saved-fx-session",
|
|
174
|
+
});
|
|
175
|
+
} finally {
|
|
176
|
+
manager.dispose();
|
|
177
|
+
rmSync(home, { recursive: true, force: true });
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("empty input is a no-op; queued comments freeze once and retries reuse their mirrors", async () => {
|
|
182
|
+
const home = mkdtempSync(join(tmpdir(), "cueloop-agent-queue-"));
|
|
183
|
+
const thread = createTestThread(home);
|
|
184
|
+
const prompts: string[] = [];
|
|
185
|
+
const finishes: ((outcome: "completed" | "cancelled") => void)[] = [];
|
|
186
|
+
const adapter: AgentHarnessAdapter = {
|
|
187
|
+
id: "test",
|
|
188
|
+
label: "Test",
|
|
189
|
+
connect(options) {
|
|
190
|
+
return {
|
|
191
|
+
start: async () => "session",
|
|
192
|
+
prompt: (text) => {
|
|
193
|
+
prompts.push(text);
|
|
194
|
+
options.onEvent({
|
|
195
|
+
kind: "message",
|
|
196
|
+
id: `answer-${prompts.length}`,
|
|
197
|
+
text: `Reply ${prompts.length}`,
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
return new Promise((resolve) => finishes.push((outcome) => resolve({ outcome })));
|
|
201
|
+
},
|
|
202
|
+
cancel() {
|
|
203
|
+
finishes.at(-1)?.("cancelled");
|
|
204
|
+
},
|
|
205
|
+
permission() {},
|
|
206
|
+
close() {},
|
|
207
|
+
};
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
const manager = new ThreadAgentManager({
|
|
211
|
+
home,
|
|
212
|
+
enabled: true,
|
|
213
|
+
adapter,
|
|
214
|
+
getThread: () => thread,
|
|
215
|
+
onChange() {},
|
|
216
|
+
});
|
|
217
|
+
const tick = () => new Promise<void>((resolve) => setImmediate(resolve));
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
manager.prompt({ id: thread.id, text: "" });
|
|
221
|
+
expect(prompts).toEqual([]);
|
|
222
|
+
expect(manager.get(thread.id).messages).toEqual([]);
|
|
223
|
+
thread.annotations.push({
|
|
224
|
+
id: "first",
|
|
225
|
+
kind: "comment",
|
|
226
|
+
body: "Explain first",
|
|
227
|
+
anchor: {
|
|
228
|
+
quote: "Review",
|
|
229
|
+
prefix: "",
|
|
230
|
+
suffix: " the retry",
|
|
231
|
+
blockIndex: 0,
|
|
232
|
+
start: 0,
|
|
233
|
+
end: 6,
|
|
234
|
+
},
|
|
235
|
+
createdAt: thread.createdAt,
|
|
236
|
+
});
|
|
237
|
+
manager.prompt({ id: thread.id, text: "" });
|
|
238
|
+
await tick();
|
|
239
|
+
expect(manager.isReadOnly(thread.id, "first")).toBe(true);
|
|
240
|
+
thread.annotations.push({ ...thread.annotations[0]!, id: "second", body: "Explain second" });
|
|
241
|
+
manager.prompt({ id: thread.id, text: "" });
|
|
242
|
+
expect(prompts).toHaveLength(1);
|
|
243
|
+
expect(manager.get(thread.id).submissions?.map((entry) => entry.status)).toEqual([
|
|
244
|
+
"running",
|
|
245
|
+
"queued",
|
|
246
|
+
]);
|
|
247
|
+
finishes[0]!("completed");
|
|
248
|
+
await tick();
|
|
249
|
+
expect(prompts).toHaveLength(2);
|
|
250
|
+
expect(prompts[1]).toContain("Input: Explain second");
|
|
251
|
+
expect(prompts[1]).toContain("Reply 1");
|
|
252
|
+
finishes[1]!("cancelled");
|
|
253
|
+
await tick();
|
|
254
|
+
const failed = manager.get(thread.id).submissions![1]!;
|
|
255
|
+
const mirrors = manager.get(thread.id).messages.filter((entry) => entry.role === "user");
|
|
256
|
+
|
|
257
|
+
manager.prompt({ id: thread.id, text: "", retry: failed.id });
|
|
258
|
+
await tick();
|
|
259
|
+
expect(manager.get(thread.id).messages.filter((entry) => entry.role === "user")).toEqual(
|
|
260
|
+
mirrors,
|
|
261
|
+
);
|
|
262
|
+
expect(manager.isReadOnly(thread.id, "second")).toBe(true);
|
|
263
|
+
finishes[2]!("completed");
|
|
264
|
+
await tick();
|
|
265
|
+
expect(manager.get(thread.id).submissions?.map((entry) => entry.status)).toEqual([
|
|
266
|
+
"completed",
|
|
267
|
+
"completed",
|
|
268
|
+
]);
|
|
269
|
+
manager.prompt({ id: thread.id, text: "" });
|
|
270
|
+
expect(prompts).toHaveLength(3);
|
|
271
|
+
} finally {
|
|
272
|
+
manager.dispose();
|
|
273
|
+
rmSync(home, { recursive: true, force: true });
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
test("rejected first batches leave originals editable and create no queued turn", () => {
|
|
278
|
+
const home = mkdtempSync(join(tmpdir(), "cueloop-agent-rejected-"));
|
|
279
|
+
const thread = createTestThread(home);
|
|
280
|
+
const manager = new ThreadAgentManager({
|
|
281
|
+
home,
|
|
282
|
+
enabled: true,
|
|
283
|
+
adapter: {
|
|
284
|
+
id: "test",
|
|
285
|
+
label: "Test",
|
|
286
|
+
connect() {
|
|
287
|
+
throw new Error("A rejected batch must not start");
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
getThread: () => thread,
|
|
291
|
+
onChange() {},
|
|
292
|
+
});
|
|
293
|
+
const original = {
|
|
294
|
+
id: "valid",
|
|
295
|
+
kind: "comment",
|
|
296
|
+
body: "Explain",
|
|
297
|
+
anchor: { quote: "Review", prefix: "", suffix: " the retry", blockIndex: 0, start: 0, end: 6 },
|
|
298
|
+
createdAt: thread.createdAt,
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
thread.annotations.push(original, { ...original, id: "oversized", body: "x".repeat(320001) });
|
|
303
|
+
expect(() => manager.prompt({ id: thread.id, text: "" })).toThrow("320 KiB");
|
|
304
|
+
expect(manager.get(thread.id).messages).toEqual([]);
|
|
305
|
+
expect(manager.get(thread.id).submissions).toBeUndefined();
|
|
306
|
+
expect(manager.isReadOnly(thread.id, "valid")).toBe(false);
|
|
307
|
+
} finally {
|
|
308
|
+
manager.dispose();
|
|
309
|
+
rmSync(home, { recursive: true, force: true });
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("prompt operation receipts survive restart without requeueing accepted inputs", () => {
|
|
314
|
+
const home = mkdtempSync(join(tmpdir(), "cueloop-agent-receipt-"));
|
|
315
|
+
const thread = createTestThread(home);
|
|
316
|
+
const adapter: AgentHarnessAdapter = {
|
|
317
|
+
id: "receipt-test",
|
|
318
|
+
label: "Receipt test",
|
|
319
|
+
connect: () => ({
|
|
320
|
+
start: async () => "session",
|
|
321
|
+
prompt: () => new Promise(() => {}),
|
|
322
|
+
cancel() {},
|
|
323
|
+
permission() {},
|
|
324
|
+
close() {},
|
|
325
|
+
}),
|
|
326
|
+
};
|
|
327
|
+
const options = { home, enabled: true, adapter, getThread: () => thread, onChange() {} };
|
|
328
|
+
let manager = new ThreadAgentManager(options);
|
|
329
|
+
|
|
330
|
+
try {
|
|
331
|
+
const input = { id: thread.id, text: "Once", operationId: "once" };
|
|
332
|
+
const accepted = manager.prompt(input);
|
|
333
|
+
|
|
334
|
+
manager.dispose();
|
|
335
|
+
manager = new ThreadAgentManager(options);
|
|
336
|
+
const replay = manager.prompt(input);
|
|
337
|
+
|
|
338
|
+
expect(replay.promptOperations?.[0]?.result).toEqual(accepted.promptOperations?.[0]?.result);
|
|
339
|
+
expect(replay.submissions).toHaveLength(1);
|
|
340
|
+
expect(replay.promptOperations?.[0]?.outcome).toBe("failed");
|
|
341
|
+
expect(() => manager.prompt({ ...input, text: "Different" })).toThrow(
|
|
342
|
+
"Operation payload conflict",
|
|
343
|
+
);
|
|
344
|
+
manager.prompt({
|
|
345
|
+
id: thread.id,
|
|
346
|
+
text: "",
|
|
347
|
+
retry: replay.submissions![0]!.id,
|
|
348
|
+
operationId: "retry",
|
|
349
|
+
});
|
|
350
|
+
expect(manager.get(thread.id).promptOperations?.[0]?.outcome).toBe("failed");
|
|
351
|
+
} finally {
|
|
352
|
+
manager.dispose();
|
|
353
|
+
rmSync(home, { recursive: true, force: true });
|
|
354
|
+
}
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("retrying accepted work after a failed running-state save starts the harness once", async () => {
|
|
358
|
+
const home = mkdtempSync(join(tmpdir(), "cueloop-agent-start-save-"));
|
|
359
|
+
const thread = createTestThread(home);
|
|
360
|
+
const blockedPath = join(home, "thread-agents", `${thread.id}.json.tmp`);
|
|
361
|
+
let saves = 0;
|
|
362
|
+
let prompts = 0;
|
|
363
|
+
const started = Promise.withResolvers<void>();
|
|
364
|
+
const adapter: AgentHarnessAdapter = {
|
|
365
|
+
id: "start-save-test",
|
|
366
|
+
label: "Start save test",
|
|
367
|
+
connect: () => ({
|
|
368
|
+
start: async () => "session",
|
|
369
|
+
prompt: () => {
|
|
370
|
+
prompts++;
|
|
371
|
+
started.resolve();
|
|
372
|
+
|
|
373
|
+
return new Promise(() => {});
|
|
374
|
+
},
|
|
375
|
+
cancel() {},
|
|
376
|
+
permission() {},
|
|
377
|
+
close() {},
|
|
378
|
+
}),
|
|
379
|
+
};
|
|
380
|
+
const manager = new ThreadAgentManager({
|
|
381
|
+
home,
|
|
382
|
+
enabled: true,
|
|
383
|
+
adapter,
|
|
384
|
+
getThread: () => thread,
|
|
385
|
+
onChange() {
|
|
386
|
+
if (++saves === 1) mkdirSync(blockedPath);
|
|
387
|
+
},
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
try {
|
|
391
|
+
const input = { id: thread.id, text: "Start once", operationId: "start-once" };
|
|
392
|
+
|
|
393
|
+
expect(() => manager.prompt(input)).toThrow();
|
|
394
|
+
expect(manager.get(thread.id).phase.kind).toBe("idle");
|
|
395
|
+
expect(manager.get(thread.id).submissions?.[0]?.status).toBe("queued");
|
|
396
|
+
expect(prompts).toBe(0);
|
|
397
|
+
const acceptedIds = manager.get(thread.id).promptOperations?.[0]?.result;
|
|
398
|
+
|
|
399
|
+
rmSync(blockedPath, { recursive: true });
|
|
400
|
+
const replay = manager.prompt(input);
|
|
401
|
+
|
|
402
|
+
expect(replay.promptOperations?.[0]?.result).toEqual(acceptedIds);
|
|
403
|
+
expect(replay.submissions).toHaveLength(1);
|
|
404
|
+
await started.promise;
|
|
405
|
+
expect(prompts).toBe(1);
|
|
406
|
+
manager.prompt(input);
|
|
407
|
+
expect(prompts).toBe(1);
|
|
408
|
+
} finally {
|
|
409
|
+
manager.dispose();
|
|
410
|
+
rmSync(home, { recursive: true, force: true });
|
|
411
|
+
}
|
|
412
|
+
});
|