From 01678e94897a410905dbbdf5f3a25db063416e0d Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 12 Oct 2026 13:49:34 +1100 Subject: [PATCH] fix(canvas): handle board startup requests Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 545df520-6557-4211-a110-53c60f4b9f3a --- .../extension.mjs | 2 ++ eng/board-state-storage.test.mjs | 35 +++++++++++++++++-- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/.github/extensions/external-plugin-review-board/extension.mjs b/.github/extensions/external-plugin-review-board/extension.mjs index ad1006c35..c65797e17 100644 --- a/.github/extensions/external-plugin-review-board/extension.mjs +++ b/.github/extensions/external-plugin-review-board/extension.mjs @@ -213,6 +213,7 @@ function routesFor(instanceId) { function wrap(fn) { return async (ctx) => { try { + if (!store) throw new CanvasError("not_ready", "The extension is still starting. Retry the action."); return await fn(ctx); } catch (error) { if (error instanceof CanvasError) throw error; @@ -316,6 +317,7 @@ session = await joinSession({ }, ], open: async (ctx) => { + if (!store) throw new CanvasError("not_ready", "The extension is still starting. Retry opening it."); let entry = servers.get(ctx.instanceId); if (!entry) { entry = await startBoardServer({ diff --git a/eng/board-state-storage.test.mjs b/eng/board-state-storage.test.mjs index 29f0d8ef0..3db0f568b 100644 --- a/eng/board-state-storage.test.mjs +++ b/eng/board-state-storage.test.mjs @@ -39,13 +39,29 @@ async function fingerprint(dir) { return files; } -async function loadCanvas({ id, extensionDir, workspacePath }) { +async function loadCanvas({ id, extensionDir, workspacePath, triggerDuringJoin = false }) { let canvas; + let startupResults; const context = vm.createContext({ setTimeout }); const sdk = { createCanvas: (definition) => definition, joinSession: async (options) => { [canvas] = options.canvases; + if (triggerDuringJoin) { + const ctx = { instanceId: "startup-test", input: {} }; + const requests = [ + canvas.actions.find((entry) => entry.name === "get_board").handler(ctx), + canvas.open(ctx), + ]; + startupResults = Promise.all(requests.map(async (request) => { + try { + await request; + return { ok: true }; + } catch (error) { + return { ok: false, code: error.code, message: error.message }; + } + })); + } return { workspacePath, log() {}, send: async () => {} }; }, CanvasError: class CanvasError extends Error { @@ -76,11 +92,13 @@ async function loadCanvas({ id, extensionDir, workspacePath }) { }, { context }); }); await module.evaluate(); - return (name, input = {}) => { + const act = (name, input = {}) => { const action = canvas.actions.find((entry) => entry.name === name); assert.ok(action, `Missing canvas action ${name}`); return action.handler({ instanceId: "test-board", input }); }; + act.startupResults = startupResults; + return act; } async function writeJSON(file, value) { @@ -148,6 +166,19 @@ for (const id of ["open-pr-board", "external-plugin-review-board"]) { }); } +test("external-plugin-review-board returns retryable not_ready errors during startup", async (t) => { + const f = await fixture(t, "external-plugin-review-board"); + const act = await loadCanvas({ ...f, triggerDuringJoin: true }); + const results = await act.startupResults; + assert.equal(results.length, 2); + for (const result of results) { + assert.equal(result.ok, false); + assert.equal(result.code, "not_ready"); + assert.match(result.message, /still starting/i); + } + assert.equal((await act("get_board")).items.length, 0); +}); + test("both canvases keep independent boards in the same session workspace", async (t) => { const pr = await fixture(t, "open-pr-board"); const plugin = { ...await fixture(t, "external-plugin-review-board"), workspacePath: pr.workspacePath };