



























@@ -93,6 +93,17 @@ const pluginRuntimeState = vi.hoisted(
9393}) satisfies { registry: PluginRegistry },
9494);
959596+const jsonFileMockState = vi.hoisted(() => ({
97+writeJsonOverride: null as
98+| null
99+| ((
100+actualWriteJson: (filePath: string, value: unknown, options?: unknown) => Promise<void>,
101+filePath: string,
102+value: unknown,
103+options?: unknown,
104+) => Promise<void>),
105+}));
106+96107vi.mock("../infra/home-dir.js", async () => {
97108const actual =
98109await vi.importActual<typeof import("../infra/home-dir.js")>("../infra/home-dir.js");
@@ -107,6 +118,33 @@ vi.mock("../infra/home-dir.js", async () => {
107118};
108119});
109120121+vi.mock("../infra/json-files.js", async () => {
122+const actual =
123+await vi.importActual<typeof import("../infra/json-files.js")>("../infra/json-files.js");
124+return {
125+ ...actual,
126+writeJson: async (
127+filePath: string,
128+value: unknown,
129+options?: Parameters<typeof actual.writeJson>[2],
130+) => {
131+if (jsonFileMockState.writeJsonOverride) {
132+return await jsonFileMockState.writeJsonOverride(
133+actual.writeJson as (
134+filePath: string,
135+value: unknown,
136+options?: unknown,
137+) => Promise<void>,
138+filePath,
139+value,
140+options,
141+);
142+}
143+return await actual.writeJson(filePath, value, options);
144+},
145+};
146+});
147+110148vi.mock("./runtime.js", async () => {
111149const actual = await vi.importActual<typeof import("./runtime.js")>("./runtime.js");
112150return {
@@ -485,6 +523,86 @@ describe("plugin conversation binding approvals", () => {
485523expect(differentAccount.status).toBe("pending");
486524});
487525526+it("serializes overlapping always-allow approval writes", async () => {
527+const firstWriteGate = createDeferredVoid();
528+const firstWriteStarted = createDeferredVoid();
529+let writeCount = 0;
530+jsonFileMockState.writeJsonOverride = async (actualWriteJson, filePath, value, options) => {
531+writeCount += 1;
532+if (writeCount === 1) {
533+firstWriteStarted.resolve();
534+await firstWriteGate.promise;
535+}
536+await actualWriteJson(filePath, value, options);
537+};
538+539+try {
540+const firstRequest = await requestPendingBinding(
541+createDiscordCodexBindRequest(
542+"channel:race-1",
543+"Bind this conversation to Codex thread race-1.",
544+"default",
545+),
546+);
547+const firstApproval = resolvePluginConversationBindingApproval({
548+approvalId: firstRequest.approvalId,
549+decision: "allow-always",
550+senderId: "user-1",
551+});
552+await firstWriteStarted.promise;
553+554+const secondRequest = await requestPendingBinding(
555+createDiscordCodexBindRequest(
556+"channel:race-2",
557+"Bind this conversation to Codex thread race-2.",
558+"work",
559+),
560+);
561+let secondSettled = false;
562+const secondApproval = resolvePluginConversationBindingApproval({
563+approvalId: secondRequest.approvalId,
564+decision: "allow-always",
565+senderId: "user-1",
566+}).then((result) => {
567+secondSettled = true;
568+return result;
569+});
570+571+await flushMicrotasks();
572+expect(secondSettled).toBe(false);
573+expect(writeCount).toBe(1);
574+575+firstWriteGate.resolve();
576+const [firstResult, secondResult] = await Promise.all([firstApproval, secondApproval]);
577+578+expect(firstResult.status).toBe("approved");
579+expect(secondResult.status).toBe("approved");
580+expect(writeCount).toBe(2);
581+582+const persisted = JSON.parse(fs.readFileSync(approvalsPath, "utf8")) as {
583+approvals: Array<{ accountId: string; channel: string; pluginRoot: string }>;
584+};
585+expect(persisted.approvals).toEqual(
586+expect.arrayContaining([
587+expect.objectContaining({
588+accountId: "default",
589+channel: "discord",
590+pluginRoot: "/plugins/codex-a",
591+}),
592+expect.objectContaining({
593+accountId: "work",
594+channel: "discord",
595+pluginRoot: "/plugins/codex-a",
596+}),
597+]),
598+);
599+expect(persisted.approvals).toHaveLength(2);
600+} finally {
601+firstWriteGate.resolve();
602+jsonFileMockState.writeJsonOverride = null;
603+}
604+});
605+488606it("shares pending bind approvals across duplicate module instances", async () => {
489607const { first, second } = await importDuplicateConversationBindingModules();
490608const request = await requestPendingBinding(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。