





















@@ -183,6 +183,62 @@ describe("embedded attempt session lock lifecycle", () => {
183183expect(release).toHaveBeenCalledTimes(2);
184184});
185185186+it("refreshes the prompt fence after an owned write throws", async () => {
187+const sessionFile = await createTempSessionFile();
188+const release = vi.fn(async () => {});
189+const acquireSessionWriteLock = vi.fn(async () => ({ release }));
190+const controller = await createEmbeddedAttemptSessionLockController({
191+ acquireSessionWriteLock,
192+lockOptions: { ...lockOptions, sessionFile },
193+});
194+195+await controller.releaseForPrompt();
196+await expect(
197+controller.withSessionWriteLock(async () => {
198+await fs.appendFile(sessionFile, '{"type":"message","id":"owned-before-error"}\n', "utf8");
199+throw new Error("downstream event handler failed");
200+}),
201+).rejects.toThrow("downstream event handler failed");
202+await expect(controller.withSessionWriteLock(() => "finalize")).resolves.toBe("finalize");
203+204+expect(controller.hasSessionTakeover()).toBe(false);
205+expect(acquireSessionWriteLock).toHaveBeenCalledTimes(3);
206+expect(release).toHaveBeenCalledTimes(3);
207+});
208+209+it("does not reuse a released lock from inherited async context", async () => {
210+const sessionFile = await createTempSessionFile();
211+let resumeDetached!: () => void;
212+const detachedGate = new Promise<void>((resolve) => {
213+resumeDetached = resolve;
214+});
215+const release = vi.fn(async () => {});
216+const acquireSessionWriteLock = vi.fn(async () => ({ release }));
217+const controller = await createEmbeddedAttemptSessionLockController({
218+ acquireSessionWriteLock,
219+lockOptions: { ...lockOptions, sessionFile },
220+});
221+222+await controller.releaseForPrompt();
223+let detachedWrite!: Promise<void>;
224+await controller.withSessionWriteLock(async () => {
225+detachedWrite = (async () => {
226+await detachedGate;
227+await controller.withSessionWriteLock(async () => {
228+await fs.appendFile(sessionFile, '{"type":"message","id":"detached-owned"}\n', "utf8");
229+});
230+})();
231+});
232+233+resumeDetached();
234+await detachedWrite;
235+await expect(controller.withSessionWriteLock(() => "finalize")).resolves.toBe("finalize");
236+237+expect(controller.hasSessionTakeover()).toBe(false);
238+expect(acquireSessionWriteLock).toHaveBeenCalledTimes(4);
239+expect(release).toHaveBeenCalledTimes(4);
240+});
241+186242it("refreshes the prompt fence after an owned transcript mirror append", async () => {
187243const sessionFile = await createTempSessionFile();
188244const release = vi.fn(async () => {});
@@ -214,6 +270,23 @@ describe("embedded attempt session lock lifecycle", () => {
214270expect(release).toHaveBeenCalledTimes(3);
215271});
216272273+it("refreshes the prompt fence after an owned session manager append", async () => {
274+const sessionFile = await createTempSessionFile();
275+const release = vi.fn(async () => {});
276+const acquireSessionWriteLock = vi.fn(async () => ({ release }));
277+const controller = await createEmbeddedAttemptSessionLockController({
278+ acquireSessionWriteLock,
279+lockOptions: { ...lockOptions, sessionFile },
280+});
281+282+await controller.releaseForPrompt();
283+await fs.appendFile(sessionFile, '{"type":"message","id":"owned-session-manager"}\n', "utf8");
284+controller.refreshAfterOwnedSessionWrite();
285+286+await expect(controller.withSessionWriteLock(() => "finalize")).resolves.toBe("finalize");
287+expect(controller.hasSessionTakeover()).toBe(false);
288+});
289+217290it("returns a no-op cleanup lock after prompt lock reacquisition times out", async () => {
218291const releases: string[] = [];
219292const acquireSessionWriteLock = vi
@@ -379,6 +452,49 @@ describe("embedded attempt session lock lifecycle", () => {
379452expect(releases).toEqual(["released", "released", "released"]);
380453});
381454455+it("makes the Pi event listener await locked session event processing", async () => {
456+const events: string[] = [];
457+const session = {
458+_agentEventQueue: Promise.resolve(),
459+_disconnectFromAgent: vi.fn(() => events.push("disconnect")),
460+_reconnectToAgent: vi.fn(() => events.push("reconnect")),
461+_processAgentEvent: vi.fn(async (event: { type?: string }) => {
462+events.push(`process:${event.type}`);
463+}),
464+_handleAgentEvent(event: { type?: string }) {
465+events.push(`handle:${event.type}`);
466+session["_agentEventQueue"] = session["_agentEventQueue"].then(() =>
467+session["_processAgentEvent"](event),
468+);
469+session["_agentEventQueue"].catch(() => {});
470+},
471+};
472+473+installSessionEventWriteLock({
474+ session,
475+withSessionWriteLock: async (run) => {
476+events.push("lock");
477+return await run();
478+},
479+});
480+481+const handleAgentEvent = session["_handleAgentEvent"];
482+const result = handleAgentEvent({ type: "message_end" }) as unknown as Promise<unknown>;
483+484+expect(result).toHaveProperty("then");
485+expect(events).toEqual(["disconnect", "reconnect", "handle:message_end"]);
486+487+await result;
488+489+expect(events).toEqual([
490+"disconnect",
491+"reconnect",
492+"handle:message_end",
493+"lock",
494+"process:message_end",
495+]);
496+});
497+382498it("locks Pi extension hooks that can mutate the session outside agent events", async () => {
383499const locked: string[] = [];
384500const called: string[] = [];
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。