pi-extended-teams 2.1.16
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 +105 -0
- package/assets/pi-extended-teams-agent-navigation.png +0 -0
- package/assets/pi-extended-teams-in-action.png +0 -0
- package/extensions/agents/read-agent-report.ts +181 -0
- package/extensions/agents/read-agent-session-lifecycle.test.ts +605 -0
- package/extensions/agents/read-agent-session-lifecycle.ts +676 -0
- package/extensions/agents/read-agent.test.ts +3077 -0
- package/extensions/agents/read-agent.ts +1119 -0
- package/extensions/agents/write-agent.test.ts +513 -0
- package/extensions/agents/write-agent.ts +392 -0
- package/extensions/events/register-events.test.ts +465 -0
- package/extensions/events/register-events.ts +409 -0
- package/extensions/index.test.ts +1659 -0
- package/extensions/index.ts +1228 -0
- package/extensions/internal/agent-session-files.test.ts +164 -0
- package/extensions/internal/agent-session-files.ts +320 -0
- package/extensions/internal/debug.ts +44 -0
- package/extensions/internal/model-selection.ts +82 -0
- package/extensions/internal/pi-command.test.ts +191 -0
- package/extensions/internal/pi-command.ts +224 -0
- package/extensions/internal/pi-runtime-api.test.ts +43 -0
- package/extensions/internal/pi-runtime-api.ts +78 -0
- package/extensions/internal/schema.ts +19 -0
- package/extensions/internal/session-context-reference.test.ts +290 -0
- package/extensions/internal/session-context-reference.ts +430 -0
- package/extensions/internal/session-files.test.ts +225 -0
- package/extensions/internal/session-files.ts +273 -0
- package/extensions/internal/session-usage.ts +59 -0
- package/extensions/resources/spawn-resource-plan.test.ts +233 -0
- package/extensions/resources/spawn-resource-plan.ts +247 -0
- package/extensions/runtime/active-agent-sleep.test.ts +157 -0
- package/extensions/runtime/active-agent-sleep.ts +117 -0
- package/extensions/runtime/nested-read-agents.ts +22 -0
- package/extensions/runtime/pending-child-controller.test.ts +169 -0
- package/extensions/runtime/pending-child-controller.ts +280 -0
- package/extensions/runtime/types.ts +68 -0
- package/extensions/team/contracts.test.ts +111 -0
- package/extensions/team/lifecycle.test.ts +1095 -0
- package/extensions/team/lifecycle.ts +502 -0
- package/extensions/team/recipient-closure.test.ts +97 -0
- package/extensions/team/recipient-closure.ts +120 -0
- package/extensions/team/roster.test.ts +129 -0
- package/extensions/team/roster.ts +165 -0
- package/extensions/team/team-contracts.json +43 -0
- package/extensions/team/writer-screens.test.ts +50 -0
- package/extensions/team/writer-screens.ts +156 -0
- package/extensions/tools/agent-communication-tools.test.ts +306 -0
- package/extensions/tools/agent-communication-tools.ts +183 -0
- package/extensions/tools/coordination-tools.test.ts +670 -0
- package/extensions/tools/coordination-tools.ts +304 -0
- package/extensions/tools/delegation-guard.test.ts +95 -0
- package/extensions/tools/delegation-guard.ts +65 -0
- package/extensions/tools/file-claim-tools.test.ts +104 -0
- package/extensions/tools/file-claim-tools.ts +68 -0
- package/extensions/tools/model-tools.ts +53 -0
- package/extensions/tools/predefined-tools.test.ts +647 -0
- package/extensions/tools/predefined-tools.ts +331 -0
- package/extensions/tools/read-helper.test.ts +35 -0
- package/extensions/tools/task-runtime-tools.test.ts +334 -0
- package/extensions/tools/task-runtime-tools.ts +227 -0
- package/extensions/tools/team-tools.read-agent.test.ts +1924 -0
- package/extensions/tools/team-tools.ts +1270 -0
- package/extensions/ui/agent-follow-view.test.ts +520 -0
- package/extensions/ui/agent-follow-view.ts +779 -0
- package/extensions/ui/agent-navigation.test.ts +52 -0
- package/extensions/ui/agent-navigation.ts +55 -0
- package/extensions/ui/ansi.ts +16 -0
- package/extensions/ui/extensions-command.test.ts +336 -0
- package/extensions/ui/extensions-command.ts +282 -0
- package/extensions/ui/favorite-models-command.test.ts +280 -0
- package/extensions/ui/favorite-models-command.ts +423 -0
- package/extensions/ui/frame.ts +77 -0
- package/extensions/ui/input.ts +17 -0
- package/extensions/ui/read-agent-status.test.ts +117 -0
- package/extensions/ui/read-agent-status.ts +125 -0
- package/extensions/ui/renderers.ts +243 -0
- package/extensions/ui/status-widget.test.ts +143 -0
- package/extensions/ui/status-widget.ts +440 -0
- package/extensions/ui-frame.test.ts +69 -0
- package/package.json +75 -0
- package/skills/teams.md +256 -0
- package/src/adapters/terminal-registry.ts +78 -0
- package/src/adapters/tmux-adapter.test.ts +276 -0
- package/src/adapters/tmux-adapter.ts +200 -0
- package/src/orchestration/index.ts +437 -0
- package/src/orchestration/orchestrator.test.ts +396 -0
- package/src/orchestration/types.ts +125 -0
- package/src/utils/atomic-json.ts +21 -0
- package/src/utils/claims.test.ts +137 -0
- package/src/utils/claims.ts +169 -0
- package/src/utils/hooks.test.ts +75 -0
- package/src/utils/hooks.ts +35 -0
- package/src/utils/lifecycle-tombstone.test.ts +105 -0
- package/src/utils/lifecycle-tombstone.ts +268 -0
- package/src/utils/lock.race.child.ts +44 -0
- package/src/utils/lock.race.test.ts +198 -0
- package/src/utils/lock.test.ts +90 -0
- package/src/utils/lock.ts +186 -0
- package/src/utils/messaging.test.ts +337 -0
- package/src/utils/messaging.ts +441 -0
- package/src/utils/model-resolution.test.ts +231 -0
- package/src/utils/model-resolution.ts +322 -0
- package/src/utils/models.test.ts +8 -0
- package/src/utils/models.ts +115 -0
- package/src/utils/paths.ts +81 -0
- package/src/utils/predefined-teams/types.ts +48 -0
- package/src/utils/predefined-teams.save-template.test.ts +74 -0
- package/src/utils/predefined-teams.test.ts +441 -0
- package/src/utils/predefined-teams.ts +471 -0
- package/src/utils/read-helper-queue.ts +99 -0
- package/src/utils/report-events.test.ts +154 -0
- package/src/utils/report-events.ts +222 -0
- package/src/utils/runtime.test.ts +314 -0
- package/src/utils/runtime.ts +261 -0
- package/src/utils/security.test.ts +43 -0
- package/src/utils/settings.test.ts +480 -0
- package/src/utils/settings.ts +645 -0
- package/src/utils/shared-memory.test.ts +80 -0
- package/src/utils/shared-memory.ts +81 -0
- package/src/utils/tasks.race.test.ts +44 -0
- package/src/utils/tasks.test.ts +229 -0
- package/src/utils/tasks.ts +396 -0
- package/src/utils/teams.ts +231 -0
- package/src/utils/terminal-adapter.ts +103 -0
- package/src/utils/thinking-levels.test.ts +56 -0
- package/src/utils/thinking-levels.ts +47 -0
- package/src/utils/workflow-metadata.test.ts +11 -0
- package/src/utils/workflow-metadata.ts +88 -0
- package/src/utils/write-queue.test.ts +251 -0
- package/src/utils/write-queue.ts +202 -0
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
NESTED_SESSION_TEARDOWN_TIMEOUT_MS,
|
|
4
|
+
closeReadAgentMessageDelivery,
|
|
5
|
+
enqueueReadAgentMessageDelivery,
|
|
6
|
+
installReadAgentSessionLifecycle,
|
|
7
|
+
requestReadAgentTeardown,
|
|
8
|
+
type ManagedReadAgentLifecycleState,
|
|
9
|
+
} from "./read-agent-session-lifecycle.js";
|
|
10
|
+
|
|
11
|
+
function deferred<T = void>() {
|
|
12
|
+
let resolve!: (value: T) => void;
|
|
13
|
+
let reject!: (error: unknown) => void;
|
|
14
|
+
const promise = new Promise<T>((done, fail) => {
|
|
15
|
+
resolve = done;
|
|
16
|
+
reject = fail;
|
|
17
|
+
});
|
|
18
|
+
return { promise, resolve, reject };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeSession(options: { handlers?: boolean } = {}) {
|
|
22
|
+
const order: string[] = [];
|
|
23
|
+
const session = {
|
|
24
|
+
hasExtensionHandlers: vi.fn(() => options.handlers ?? true),
|
|
25
|
+
extensionRunner: {
|
|
26
|
+
emit: vi.fn(async (event: any) => { order.push(`emit:${event.reason}`); }),
|
|
27
|
+
},
|
|
28
|
+
clearQueue: vi.fn(() => { order.push("clearQueue"); return { steering: [], followUp: [] }; }),
|
|
29
|
+
abort: vi.fn(async () => { order.push("abort"); }),
|
|
30
|
+
dispose: vi.fn(() => { order.push("dispose"); }),
|
|
31
|
+
};
|
|
32
|
+
return { session: session as any, order };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
afterEach(() => {
|
|
36
|
+
vi.useRealTimers();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("nested read-agent delivery lifecycle", () => {
|
|
40
|
+
it("cancels sender-visible waits and drops a delayed second delivery after close", async () => {
|
|
41
|
+
const firstRaw = deferred();
|
|
42
|
+
const sends = vi.fn(() => firstRaw.promise);
|
|
43
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
44
|
+
acceptingMessages: true,
|
|
45
|
+
messageDeliveryClosed: false,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const first = enqueueReadAgentMessageDelivery(state, "reader", sends);
|
|
49
|
+
await vi.waitFor(() => expect(sends).toHaveBeenCalledOnce());
|
|
50
|
+
const second = enqueueReadAgentMessageDelivery(state, "reader", sends);
|
|
51
|
+
const firstOutcome = first.then(() => "delivered", (error: Error) => error.message);
|
|
52
|
+
const secondOutcome = second.then(() => "delivered", (error: Error) => error.message);
|
|
53
|
+
|
|
54
|
+
const close = closeReadAgentMessageDelivery(state);
|
|
55
|
+
expect(close.cancelledDeliveries).toBe(2);
|
|
56
|
+
await expect(firstOutcome).resolves.toContain("was cancelled");
|
|
57
|
+
await expect(secondOutcome).resolves.toContain("was cancelled");
|
|
58
|
+
expect(sends).toHaveBeenCalledOnce();
|
|
59
|
+
|
|
60
|
+
firstRaw.resolve();
|
|
61
|
+
await close.rawDeliverySettlement;
|
|
62
|
+
expect(sends).toHaveBeenCalledOnce();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("nested selected-extension session shutdown", () => {
|
|
67
|
+
it("invokes shutdown before clear/abort and uses the first concurrent reason exactly once", async () => {
|
|
68
|
+
const { session, order } = makeSession();
|
|
69
|
+
const handler = deferred();
|
|
70
|
+
session.extensionRunner.emit.mockImplementation(async (event: any) => {
|
|
71
|
+
order.push(`emit:${event.reason}`);
|
|
72
|
+
await handler.promise;
|
|
73
|
+
});
|
|
74
|
+
const lifecycle = installReadAgentSessionLifecycle(session);
|
|
75
|
+
|
|
76
|
+
const first = lifecycle.requestShutdown("quit", Promise.resolve());
|
|
77
|
+
const concurrent = lifecycle.requestShutdown("reload", Promise.resolve());
|
|
78
|
+
await Promise.resolve();
|
|
79
|
+
|
|
80
|
+
expect(order).toEqual(["emit:quit", "clearQueue", "abort"]);
|
|
81
|
+
expect(session.clearQueue).toHaveBeenCalledOnce();
|
|
82
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
83
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
84
|
+
|
|
85
|
+
handler.resolve();
|
|
86
|
+
await expect(first).resolves.toMatchObject({ status: "settled", reason: "quit", extensionShutdown: "emitted" });
|
|
87
|
+
await expect(concurrent).resolves.toMatchObject({ status: "settled", reason: "quit" });
|
|
88
|
+
expect(order).toEqual(["emit:quit", "clearQueue", "abort", "dispose"]);
|
|
89
|
+
expect(session.extensionRunner.emit).toHaveBeenCalledOnce();
|
|
90
|
+
expect(session.dispose).toHaveBeenCalledOnce();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it("times out a permanently pending shutdown handler and disposes exactly once only after it settles", async () => {
|
|
94
|
+
vi.useFakeTimers();
|
|
95
|
+
const { session, order } = makeSession();
|
|
96
|
+
const handler = deferred();
|
|
97
|
+
session.extensionRunner.emit.mockImplementation(async (event: any) => {
|
|
98
|
+
order.push(`emit:${event.reason}`);
|
|
99
|
+
await handler.promise;
|
|
100
|
+
});
|
|
101
|
+
const lifecycle = installReadAgentSessionLifecycle(session);
|
|
102
|
+
|
|
103
|
+
const shutdown = lifecycle.requestShutdown("reload", Promise.resolve());
|
|
104
|
+
await vi.advanceTimersByTimeAsync(NESTED_SESSION_TEARDOWN_TIMEOUT_MS);
|
|
105
|
+
await expect(shutdown).resolves.toMatchObject({
|
|
106
|
+
status: "timed_out",
|
|
107
|
+
reason: "reload",
|
|
108
|
+
extensionShutdown: "emitted",
|
|
109
|
+
abort: "settled",
|
|
110
|
+
delivery: "settled",
|
|
111
|
+
dispose: "deferred",
|
|
112
|
+
});
|
|
113
|
+
expect(order).toEqual(["emit:reload", "clearQueue", "abort"]);
|
|
114
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
115
|
+
|
|
116
|
+
handler.resolve();
|
|
117
|
+
await lifecycle.finalized;
|
|
118
|
+
lifecycle.requestShutdown("quit", Promise.resolve());
|
|
119
|
+
await Promise.resolve();
|
|
120
|
+
|
|
121
|
+
expect(session.extensionRunner.emit).toHaveBeenCalledOnce();
|
|
122
|
+
expect(session.clearQueue).toHaveBeenCalledOnce();
|
|
123
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
124
|
+
expect(session.dispose).toHaveBeenCalledOnce();
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("continues through throwing and absent shutdown handlers", async () => {
|
|
128
|
+
const throwing = makeSession();
|
|
129
|
+
throwing.session.extensionRunner.emit.mockRejectedValue(new Error("handler failed"));
|
|
130
|
+
const failedHandlerResult = await installReadAgentSessionLifecycle(throwing.session)
|
|
131
|
+
.requestShutdown("quit", Promise.resolve());
|
|
132
|
+
|
|
133
|
+
expect(failedHandlerResult).toMatchObject({ status: "settled", extensionShutdown: "failed" });
|
|
134
|
+
expect(throwing.order).toEqual(["clearQueue", "abort", "dispose"]);
|
|
135
|
+
|
|
136
|
+
const absent = makeSession({ handlers: false });
|
|
137
|
+
const absentResult = await installReadAgentSessionLifecycle(absent.session)
|
|
138
|
+
.requestShutdown("quit", Promise.resolve());
|
|
139
|
+
|
|
140
|
+
expect(absentResult).toMatchObject({ status: "settled", extensionShutdown: "no_handlers" });
|
|
141
|
+
expect(absent.session.extensionRunner.emit).not.toHaveBeenCalled();
|
|
142
|
+
expect(absent.order).toEqual(["clearQueue", "abort", "dispose"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("quarantines timed-out raw operations and disposes exactly once only after both settle", async () => {
|
|
146
|
+
vi.useFakeTimers();
|
|
147
|
+
const { session } = makeSession();
|
|
148
|
+
const delivery = deferred();
|
|
149
|
+
const abort = deferred();
|
|
150
|
+
session.abort.mockImplementation(() => abort.promise);
|
|
151
|
+
const lifecycle = installReadAgentSessionLifecycle(session);
|
|
152
|
+
|
|
153
|
+
const shutdown = lifecycle.requestShutdown("reload", delivery.promise);
|
|
154
|
+
await vi.advanceTimersByTimeAsync(NESTED_SESSION_TEARDOWN_TIMEOUT_MS);
|
|
155
|
+
await expect(shutdown).resolves.toMatchObject({
|
|
156
|
+
status: "timed_out",
|
|
157
|
+
reason: "reload",
|
|
158
|
+
abort: "timed_out",
|
|
159
|
+
delivery: "timed_out",
|
|
160
|
+
dispose: "deferred",
|
|
161
|
+
});
|
|
162
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
163
|
+
|
|
164
|
+
delivery.resolve();
|
|
165
|
+
await Promise.resolve();
|
|
166
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
167
|
+
abort.resolve();
|
|
168
|
+
await lifecycle.finalized;
|
|
169
|
+
|
|
170
|
+
expect(session.extensionRunner.emit).toHaveBeenCalledOnce();
|
|
171
|
+
expect(session.clearQueue).toHaveBeenCalledOnce();
|
|
172
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
173
|
+
expect(session.dispose).toHaveBeenCalledOnce();
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("quarantines startup pending past the shared deadline and finalizes only after the late session settles", async () => {
|
|
177
|
+
vi.useFakeTimers();
|
|
178
|
+
const creation = deferred<any | undefined>();
|
|
179
|
+
const abort = deferred();
|
|
180
|
+
const { session } = makeSession();
|
|
181
|
+
session.abort.mockImplementation(() => abort.promise);
|
|
182
|
+
const finalize = vi.fn(async () => {});
|
|
183
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
184
|
+
startupState: "pending",
|
|
185
|
+
sessionCreation: creation.promise,
|
|
186
|
+
acceptingMessages: true,
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
const teardown = requestReadAgentTeardown(state, {
|
|
190
|
+
reason: "quit",
|
|
191
|
+
closePersistence: vi.fn(async () => {}),
|
|
192
|
+
finalize,
|
|
193
|
+
});
|
|
194
|
+
await vi.advanceTimersByTimeAsync(NESTED_SESSION_TEARDOWN_TIMEOUT_MS);
|
|
195
|
+
await expect(teardown).resolves.toMatchObject({
|
|
196
|
+
status: "timed_out",
|
|
197
|
+
abort: "timed_out",
|
|
198
|
+
dispose: "deferred",
|
|
199
|
+
});
|
|
200
|
+
expect(state.teardownState).toBe("quarantined");
|
|
201
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
202
|
+
|
|
203
|
+
state.session = session;
|
|
204
|
+
creation.resolve(session);
|
|
205
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
206
|
+
expect(session.extensionRunner.emit).toHaveBeenCalledOnce();
|
|
207
|
+
expect(session.clearQueue).toHaveBeenCalledOnce();
|
|
208
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
209
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
210
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
211
|
+
|
|
212
|
+
abort.resolve();
|
|
213
|
+
await state.teardownFinalizationPromise;
|
|
214
|
+
expect(session.dispose).toHaveBeenCalledOnce();
|
|
215
|
+
expect(finalize).toHaveBeenCalledOnce();
|
|
216
|
+
expect(state.teardownState).toBe("finalized");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("finalizes safely when pending session creation rejects and no session can exist", async () => {
|
|
220
|
+
const creation = deferred<any | undefined>();
|
|
221
|
+
const finalize = vi.fn(async () => {});
|
|
222
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
223
|
+
startupState: "pending",
|
|
224
|
+
sessionCreation: creation.promise,
|
|
225
|
+
acceptingMessages: true,
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const teardown = requestReadAgentTeardown(state, {
|
|
229
|
+
reason: "quit",
|
|
230
|
+
closePersistence: vi.fn(async () => {}),
|
|
231
|
+
finalize,
|
|
232
|
+
});
|
|
233
|
+
creation.reject(new Error("creation failed"));
|
|
234
|
+
|
|
235
|
+
await expect(teardown).resolves.toMatchObject({
|
|
236
|
+
status: "settled",
|
|
237
|
+
extensionShutdown: "no_handlers",
|
|
238
|
+
abort: "unavailable",
|
|
239
|
+
dispose: "settled",
|
|
240
|
+
});
|
|
241
|
+
expect(finalize).toHaveBeenCalledOnce();
|
|
242
|
+
expect(state.teardownState).toBe("finalized");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("does not emit or destroy a session when persisted closure cannot be proven", async () => {
|
|
246
|
+
const { session } = makeSession();
|
|
247
|
+
const finalize = vi.fn(async () => {});
|
|
248
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
249
|
+
session,
|
|
250
|
+
acceptingMessages: true,
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
await expect(requestReadAgentTeardown(state, {
|
|
254
|
+
reason: "quit",
|
|
255
|
+
closePersistence: async () => { throw new Error("persistence unavailable"); },
|
|
256
|
+
finalize,
|
|
257
|
+
})).resolves.toMatchObject({
|
|
258
|
+
status: "persistence_failed",
|
|
259
|
+
persistenceClosed: false,
|
|
260
|
+
finalized: false,
|
|
261
|
+
removedMember: false,
|
|
262
|
+
releasedClaims: [],
|
|
263
|
+
error: "persistence unavailable",
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
expect(state.teardownState).toBe("persistence_failed");
|
|
267
|
+
expect(session.extensionRunner.emit).not.toHaveBeenCalled();
|
|
268
|
+
expect(session.clearQueue).not.toHaveBeenCalled();
|
|
269
|
+
expect(session.abort).not.toHaveBeenCalled();
|
|
270
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
271
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("bounds a never-settling persistence close without any destructive cleanup", async () => {
|
|
275
|
+
vi.useFakeTimers();
|
|
276
|
+
const persistence = deferred();
|
|
277
|
+
const { session } = makeSession();
|
|
278
|
+
const finalize = vi.fn(async () => {});
|
|
279
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
280
|
+
session,
|
|
281
|
+
acceptingMessages: true,
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
const teardown = requestReadAgentTeardown(state, {
|
|
285
|
+
reason: "quit",
|
|
286
|
+
closePersistence: vi.fn(() => persistence.promise),
|
|
287
|
+
finalize,
|
|
288
|
+
});
|
|
289
|
+
await vi.advanceTimersByTimeAsync(NESTED_SESSION_TEARDOWN_TIMEOUT_MS);
|
|
290
|
+
|
|
291
|
+
await expect(teardown).resolves.toMatchObject({
|
|
292
|
+
status: "persistence_failed",
|
|
293
|
+
persistenceClosed: false,
|
|
294
|
+
finalized: false,
|
|
295
|
+
removedMember: false,
|
|
296
|
+
releasedClaims: [],
|
|
297
|
+
error: `Persistence closure timed out after ${NESTED_SESSION_TEARDOWN_TIMEOUT_MS}ms.`,
|
|
298
|
+
});
|
|
299
|
+
expect(state.teardownState).toBe("persistence_failed");
|
|
300
|
+
expect(state.acceptingMessages).toBe(false);
|
|
301
|
+
expect(state.messageDeliveryClosed).toBe(true);
|
|
302
|
+
expect(state.persistedRecipientClosed).not.toBe(true);
|
|
303
|
+
expect(session.extensionRunner.emit).not.toHaveBeenCalled();
|
|
304
|
+
expect(session.clearQueue).not.toHaveBeenCalled();
|
|
305
|
+
expect(session.abort).not.toHaveBeenCalled();
|
|
306
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
307
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("observes a late persistence rejection without cleanup or an unhandled rejection", async () => {
|
|
311
|
+
vi.useFakeTimers();
|
|
312
|
+
const persistence = deferred();
|
|
313
|
+
const { session } = makeSession();
|
|
314
|
+
const finalize = vi.fn(async () => {});
|
|
315
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
316
|
+
session,
|
|
317
|
+
acceptingMessages: true,
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const teardown = requestReadAgentTeardown(state, {
|
|
321
|
+
closePersistence: vi.fn(() => persistence.promise),
|
|
322
|
+
finalize,
|
|
323
|
+
});
|
|
324
|
+
await vi.advanceTimersByTimeAsync(NESTED_SESSION_TEARDOWN_TIMEOUT_MS);
|
|
325
|
+
await expect(teardown).resolves.toMatchObject({
|
|
326
|
+
status: "persistence_failed",
|
|
327
|
+
error: `Persistence closure timed out after ${NESTED_SESSION_TEARDOWN_TIMEOUT_MS}ms.`,
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
persistence.reject(new Error("late persistence failure"));
|
|
331
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
332
|
+
await expect(state.teardownFinalizationPromise).resolves.toMatchObject({
|
|
333
|
+
status: "persistence_failed",
|
|
334
|
+
persistenceClosed: false,
|
|
335
|
+
error: "late persistence failure",
|
|
336
|
+
});
|
|
337
|
+
expect(state.teardownState).toBe("persistence_failed");
|
|
338
|
+
expect(session.extensionRunner.emit).not.toHaveBeenCalled();
|
|
339
|
+
expect(session.clearQueue).not.toHaveBeenCalled();
|
|
340
|
+
expect(session.abort).not.toHaveBeenCalled();
|
|
341
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
342
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("continues exactly once after late persistence success and defers finalization for raw settlement", async () => {
|
|
346
|
+
vi.useFakeTimers();
|
|
347
|
+
const persistence = deferred();
|
|
348
|
+
const shutdownHandler = deferred();
|
|
349
|
+
const abort = deferred();
|
|
350
|
+
const { session } = makeSession();
|
|
351
|
+
session.extensionRunner.emit.mockImplementation(() => shutdownHandler.promise);
|
|
352
|
+
session.abort.mockImplementation(() => abort.promise);
|
|
353
|
+
const finalize = vi.fn(async () => {});
|
|
354
|
+
const closePersistence = vi.fn(() => persistence.promise);
|
|
355
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
356
|
+
session,
|
|
357
|
+
acceptingMessages: true,
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const teardown = requestReadAgentTeardown(state, { closePersistence, finalize });
|
|
361
|
+
await vi.advanceTimersByTimeAsync(NESTED_SESSION_TEARDOWN_TIMEOUT_MS);
|
|
362
|
+
await expect(teardown).resolves.toMatchObject({ status: "persistence_failed" });
|
|
363
|
+
const eventualFromPersistence = state.teardownFinalizationPromise;
|
|
364
|
+
const concurrentContinuation = requestReadAgentTeardown(state, {
|
|
365
|
+
closePersistence: vi.fn(async () => {}),
|
|
366
|
+
finalize: vi.fn(async () => {}),
|
|
367
|
+
});
|
|
368
|
+
await expect(concurrentContinuation).resolves.toMatchObject({ status: "persistence_failed" });
|
|
369
|
+
|
|
370
|
+
persistence.resolve();
|
|
371
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
372
|
+
expect(state.persistedRecipientClosed).toBe(true);
|
|
373
|
+
expect(session.extensionRunner.emit).toHaveBeenCalledOnce();
|
|
374
|
+
expect(session.clearQueue).toHaveBeenCalledOnce();
|
|
375
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
376
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
377
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
378
|
+
|
|
379
|
+
shutdownHandler.resolve();
|
|
380
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
381
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
382
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
383
|
+
|
|
384
|
+
abort.resolve();
|
|
385
|
+
const eventualResult = await eventualFromPersistence;
|
|
386
|
+
expect(eventualResult).toMatchObject({
|
|
387
|
+
status: "settled",
|
|
388
|
+
persistenceClosed: true,
|
|
389
|
+
finalized: true,
|
|
390
|
+
});
|
|
391
|
+
expect(state.teardownResult).toEqual(eventualResult);
|
|
392
|
+
expect(state.teardownState).toBe("finalized");
|
|
393
|
+
expect(closePersistence).toHaveBeenCalledOnce();
|
|
394
|
+
expect(session.extensionRunner.emit).toHaveBeenCalledOnce();
|
|
395
|
+
expect(session.clearQueue).toHaveBeenCalledOnce();
|
|
396
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
397
|
+
expect(session.dispose).toHaveBeenCalledOnce();
|
|
398
|
+
expect(finalize).toHaveBeenCalledOnce();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
it("uses only the persistence deadline remainder for nested shutdown", async () => {
|
|
402
|
+
vi.useFakeTimers();
|
|
403
|
+
const abort = deferred();
|
|
404
|
+
const { session } = makeSession();
|
|
405
|
+
session.abort.mockImplementation(() => abort.promise);
|
|
406
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
407
|
+
session,
|
|
408
|
+
acceptingMessages: true,
|
|
409
|
+
};
|
|
410
|
+
const closePersistence = vi.fn(() => new Promise<void>((resolve) => {
|
|
411
|
+
setTimeout(resolve, 1000);
|
|
412
|
+
}));
|
|
413
|
+
|
|
414
|
+
const teardown = requestReadAgentTeardown(state, {
|
|
415
|
+
closePersistence,
|
|
416
|
+
finalize: vi.fn(async () => {}),
|
|
417
|
+
});
|
|
418
|
+
let settled = false;
|
|
419
|
+
void teardown.then(() => { settled = true; });
|
|
420
|
+
|
|
421
|
+
await vi.advanceTimersByTimeAsync(2499);
|
|
422
|
+
expect(settled).toBe(false);
|
|
423
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
424
|
+
|
|
425
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
426
|
+
await expect(teardown).resolves.toMatchObject({
|
|
427
|
+
status: "timed_out",
|
|
428
|
+
persistenceClosed: true,
|
|
429
|
+
abort: "timed_out",
|
|
430
|
+
});
|
|
431
|
+
expect(closePersistence).toHaveBeenCalledOnce();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("shares concurrent close and continuation, including exact-deadline fulfillment", async () => {
|
|
435
|
+
vi.useFakeTimers();
|
|
436
|
+
const { session } = makeSession();
|
|
437
|
+
const finalize = vi.fn(async () => {});
|
|
438
|
+
const closePersistence = vi.fn(() => new Promise<void>((resolve) => {
|
|
439
|
+
setTimeout(resolve, NESTED_SESSION_TEARDOWN_TIMEOUT_MS);
|
|
440
|
+
}));
|
|
441
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
442
|
+
session,
|
|
443
|
+
acceptingMessages: true,
|
|
444
|
+
};
|
|
445
|
+
const options = { closePersistence, finalize };
|
|
446
|
+
|
|
447
|
+
const first = requestReadAgentTeardown(state, options);
|
|
448
|
+
const concurrent = requestReadAgentTeardown(state, options);
|
|
449
|
+
expect(concurrent).toBe(first);
|
|
450
|
+
expect(closePersistence).toHaveBeenCalledOnce();
|
|
451
|
+
|
|
452
|
+
await vi.advanceTimersByTimeAsync(NESTED_SESSION_TEARDOWN_TIMEOUT_MS);
|
|
453
|
+
const boundaryResult = await first;
|
|
454
|
+
expect(boundaryResult.persistenceClosed).toBe(true);
|
|
455
|
+
expect(boundaryResult.status).not.toBe("persistence_failed");
|
|
456
|
+
expect(state.persistedRecipientClosed).toBe(true);
|
|
457
|
+
expect(session.extensionRunner.emit).toHaveBeenCalledOnce();
|
|
458
|
+
expect(session.clearQueue).toHaveBeenCalledOnce();
|
|
459
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
460
|
+
|
|
461
|
+
const eventual = await state.teardownFinalizationPromise;
|
|
462
|
+
expect(eventual).toMatchObject({ status: "settled", finalized: true });
|
|
463
|
+
const subsequent = requestReadAgentTeardown(state, {
|
|
464
|
+
closePersistence: vi.fn(async () => {}),
|
|
465
|
+
finalize: vi.fn(async () => {}),
|
|
466
|
+
});
|
|
467
|
+
await expect(subsequent).resolves.toEqual(eventual);
|
|
468
|
+
expect(closePersistence).toHaveBeenCalledOnce();
|
|
469
|
+
expect(session.extensionRunner.emit).toHaveBeenCalledOnce();
|
|
470
|
+
expect(session.clearQueue).toHaveBeenCalledOnce();
|
|
471
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
472
|
+
expect(session.dispose).toHaveBeenCalledOnce();
|
|
473
|
+
expect(finalize).toHaveBeenCalledOnce();
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
it("defers all external cleanup for a quarantined state until raw operations settle", async () => {
|
|
477
|
+
vi.useFakeTimers();
|
|
478
|
+
const { session } = makeSession();
|
|
479
|
+
const delivery = deferred();
|
|
480
|
+
const abort = deferred();
|
|
481
|
+
session.abort.mockImplementation(() => abort.promise);
|
|
482
|
+
const finalize = vi.fn(async () => {});
|
|
483
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
484
|
+
session,
|
|
485
|
+
acceptingMessages: true,
|
|
486
|
+
messageDeliveryTail: delivery.promise,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
const closePersistence = vi.fn(async () => {});
|
|
490
|
+
const teardown = requestReadAgentTeardown(state, {
|
|
491
|
+
reason: "watchdog",
|
|
492
|
+
closePersistence,
|
|
493
|
+
finalize,
|
|
494
|
+
});
|
|
495
|
+
await vi.advanceTimersByTimeAsync(NESTED_SESSION_TEARDOWN_TIMEOUT_MS);
|
|
496
|
+
const bounded = await teardown;
|
|
497
|
+
expect(bounded).toMatchObject({ status: "timed_out", reason: "quit" });
|
|
498
|
+
expect(state.teardownState).toBe("quarantined");
|
|
499
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
500
|
+
expect(session.dispose).not.toHaveBeenCalled();
|
|
501
|
+
|
|
502
|
+
const unusedClose = vi.fn(async () => {});
|
|
503
|
+
const unusedFinalize = vi.fn(async () => {});
|
|
504
|
+
const repeated = requestReadAgentTeardown(state, {
|
|
505
|
+
reason: "reload",
|
|
506
|
+
closePersistence: unusedClose,
|
|
507
|
+
finalize: unusedFinalize,
|
|
508
|
+
});
|
|
509
|
+
await expect(repeated).resolves.toBe(bounded);
|
|
510
|
+
expect(closePersistence).toHaveBeenCalledOnce();
|
|
511
|
+
expect(unusedClose).not.toHaveBeenCalled();
|
|
512
|
+
expect(unusedFinalize).not.toHaveBeenCalled();
|
|
513
|
+
expect(session.extensionRunner.emit).toHaveBeenCalledOnce();
|
|
514
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
515
|
+
|
|
516
|
+
delivery.resolve();
|
|
517
|
+
abort.resolve();
|
|
518
|
+
const finalization = await state.teardownFinalizationPromise;
|
|
519
|
+
|
|
520
|
+
expect(finalization).toMatchObject({ status: "settled", finalized: true });
|
|
521
|
+
expect(state.teardownResult).toEqual(finalization);
|
|
522
|
+
expect(finalize).toHaveBeenCalledOnce();
|
|
523
|
+
expect(session.extensionRunner.emit).toHaveBeenCalledOnce();
|
|
524
|
+
expect(session.abort).toHaveBeenCalledOnce();
|
|
525
|
+
expect(session.dispose).toHaveBeenCalledOnce();
|
|
526
|
+
expect(state.teardownState).toBe("finalized");
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
it("keeps cleanup quarantined when nested session disposal fails", async () => {
|
|
530
|
+
const { session } = makeSession();
|
|
531
|
+
session.dispose.mockImplementation(() => { throw new Error("dispose failed"); });
|
|
532
|
+
const finalize = vi.fn(async () => {});
|
|
533
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
534
|
+
session,
|
|
535
|
+
acceptingMessages: true,
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
await expect(requestReadAgentTeardown(state, {
|
|
539
|
+
closePersistence: vi.fn(async () => {}),
|
|
540
|
+
finalize,
|
|
541
|
+
})).resolves.toMatchObject({
|
|
542
|
+
status: "cleanup_failed",
|
|
543
|
+
dispose: "failed",
|
|
544
|
+
finalized: false,
|
|
545
|
+
error: "Nested session disposal failed.",
|
|
546
|
+
});
|
|
547
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
548
|
+
expect(state.teardownState).toBe("quarantined");
|
|
549
|
+
expect(session.dispose).toHaveBeenCalledOnce();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it("caches authoritative cleanup failure proof after a bounded timeout", async () => {
|
|
553
|
+
vi.useFakeTimers();
|
|
554
|
+
const { session } = makeSession();
|
|
555
|
+
const delivery = deferred();
|
|
556
|
+
const finalize = vi.fn(async () => ({
|
|
557
|
+
finalized: false,
|
|
558
|
+
removedMember: false,
|
|
559
|
+
releasedClaims: ["src/released.ts"],
|
|
560
|
+
error: "member cleanup failed",
|
|
561
|
+
}));
|
|
562
|
+
const state: ManagedReadAgentLifecycleState = {
|
|
563
|
+
session,
|
|
564
|
+
acceptingMessages: true,
|
|
565
|
+
messageDeliveryTail: delivery.promise,
|
|
566
|
+
};
|
|
567
|
+
const closePersistence = vi.fn(async () => {});
|
|
568
|
+
|
|
569
|
+
const teardown = requestReadAgentTeardown(state, {
|
|
570
|
+
reason: "quit",
|
|
571
|
+
closePersistence,
|
|
572
|
+
finalize,
|
|
573
|
+
});
|
|
574
|
+
await vi.advanceTimersByTimeAsync(NESTED_SESSION_TEARDOWN_TIMEOUT_MS);
|
|
575
|
+
await expect(teardown).resolves.toMatchObject({
|
|
576
|
+
status: "timed_out",
|
|
577
|
+
finalized: false,
|
|
578
|
+
delivery: "timed_out",
|
|
579
|
+
});
|
|
580
|
+
expect(finalize).not.toHaveBeenCalled();
|
|
581
|
+
|
|
582
|
+
delivery.resolve();
|
|
583
|
+
const lateResult = await state.teardownFinalizationPromise;
|
|
584
|
+
expect(lateResult).toMatchObject({
|
|
585
|
+
status: "cleanup_failed",
|
|
586
|
+
finalized: false,
|
|
587
|
+
removedMember: false,
|
|
588
|
+
releasedClaims: ["src/released.ts"],
|
|
589
|
+
error: "member cleanup failed",
|
|
590
|
+
delivery: "timed_out",
|
|
591
|
+
});
|
|
592
|
+
expect(state.teardownResult).toEqual(lateResult);
|
|
593
|
+
|
|
594
|
+
const unusedClose = vi.fn(async () => {});
|
|
595
|
+
const unusedFinalize = vi.fn(async () => {});
|
|
596
|
+
await expect(requestReadAgentTeardown(state, {
|
|
597
|
+
closePersistence: unusedClose,
|
|
598
|
+
finalize: unusedFinalize,
|
|
599
|
+
})).resolves.toEqual(lateResult);
|
|
600
|
+
expect(closePersistence).toHaveBeenCalledOnce();
|
|
601
|
+
expect(finalize).toHaveBeenCalledOnce();
|
|
602
|
+
expect(unusedClose).not.toHaveBeenCalled();
|
|
603
|
+
expect(unusedFinalize).not.toHaveBeenCalled();
|
|
604
|
+
});
|
|
605
|
+
});
|