From 758f4382db2cf112e79c4cc5017ba84f57e6878b Mon Sep 17 00:00:00 2001 From: Aaron Powell Date: Mon, 12 Oct 2026 13:37:10 +1100 Subject: [PATCH] fix(canvas): start board state fresh in the session workspace Drop the legacy extension-folder state import; boards now start empty in session.workspacePath. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 545df520-6557-4211-a110-53c60f4b9f3a --- .../external-plugin-review-board/README.md | 8 ++-- .../extension.mjs | 2 +- .../lib/state.mjs | 19 ++------- .github/extensions/open-pr-board/README.md | 7 +--- .../extensions/open-pr-board/extension.mjs | 3 +- .../extensions/open-pr-board/lib/state.mjs | 29 +++++--------- eng/board-state-storage.test.mjs | 39 ++++--------------- 7 files changed, 28 insertions(+), 79 deletions(-) diff --git a/.github/extensions/external-plugin-review-board/README.md b/.github/extensions/external-plugin-review-board/README.md index 3524265a0..478324ec8 100644 --- a/.github/extensions/external-plugin-review-board/README.md +++ b/.github/extensions/external-plugin-review-board/README.md @@ -30,10 +30,8 @@ This is Copilot's session-state workspace, not the repository checkout. It store metadata, AI reviews, manual bucket overrides, and a history of decisions made from the board. State survives extension reloads and resuming the same session; a new session has its own board. Infinite sessions must be enabled to provide the workspace. Keeping writes outside the extension -folder avoids invalidating its safety fingerprint. - -If no session state exists, the board imports the legacy `state/board.json` next to this file when -present, without modifying it. Existing session state always takes precedence. +folder avoids invalidating its safety fingerprint. State previously stored in the extension folder is +not imported; boards start fresh. ## Requirements @@ -47,4 +45,4 @@ present, without modifying it. Existing session state always takes precedence. ## Regression tests Run `node --experimental-vm-modules --test eng/board-state-storage.test.mjs` from the repository root -to check both canvases' session-scoped persistence and legacy imports without accessing GitHub. +to check both canvases' session-scoped persistence without accessing GitHub. diff --git a/.github/extensions/external-plugin-review-board/extension.mjs b/.github/extensions/external-plugin-review-board/extension.mjs index 89b0ed4e6..ad1006c35 100644 --- a/.github/extensions/external-plugin-review-board/extension.mjs +++ b/.github/extensions/external-plugin-review-board/extension.mjs @@ -358,4 +358,4 @@ if (!session.workspacePath) { store = await new BoardStore( path.join(session.workspacePath, "extensions", CANVAS_ID, "state", "board.json"), REPO, -).load({ legacyFile: path.join(EXT_DIR, "state", "board.json") }); +).load(); diff --git a/.github/extensions/external-plugin-review-board/lib/state.mjs b/.github/extensions/external-plugin-review-board/lib/state.mjs index ca1e2e21d..f42a99521 100644 --- a/.github/extensions/external-plugin-review-board/lib/state.mjs +++ b/.github/extensions/external-plugin-review-board/lib/state.mjs @@ -41,25 +41,14 @@ export class BoardStore { this.writeChain = Promise.resolve(); } - async load({ legacyFile } = {}) { - let raw; - let imported = false; + async load() { try { - raw = await readFile(this.filePath, "utf8"); + const raw = await readFile(this.filePath, "utf8"); + const parsed = JSON.parse(raw); + this.state = { ...emptyState(this.repo), ...parsed, repo: parsed.repo ?? this.repo }; } catch (error) { if (error.code !== "ENOENT") throw error; - if (!legacyFile) return this; - try { - raw = await readFile(legacyFile, "utf8"); - imported = true; - } catch (error) { - if (error.code !== "ENOENT") throw error; - return this; - } } - const parsed = JSON.parse(raw); - this.state = { ...emptyState(this.repo), ...parsed, repo: parsed.repo ?? this.repo }; - if (imported) await this.commit(); return this; } diff --git a/.github/extensions/open-pr-board/README.md b/.github/extensions/open-pr-board/README.md index 8299398d5..225d2874e 100644 --- a/.github/extensions/open-pr-board/README.md +++ b/.github/extensions/open-pr-board/README.md @@ -52,10 +52,7 @@ file per repository. This is Copilot's session-state workspace, not the reposito checkout. State survives extension reloads and resuming the same session; a new session has its own board. Infinite sessions must be enabled to provide the workspace. Keeping writes outside the extension folder avoids invalidating its safety fingerprint. - -If no session state exists, the board imports the legacy -`.github/extensions/open-pr-board/state/__.json` file when present, -without modifying it. Existing session state always takes precedence. +State previously stored in the extension folder is not imported; boards start fresh. Use **Reset board** (or the `reset_board` action) to clear reviews and history without touching GitHub. @@ -88,4 +85,4 @@ Results arriving after a manual move, reset, or replacement review are rejected. Run `node --test eng/open-pr-board.test.mjs` from the repository root. GitHub writes are tested with injected in-process fakes, never the authenticated CLI. Run `node --experimental-vm-modules --test eng/board-state-storage.test.mjs` to check -both canvases' session-scoped persistence and legacy imports without accessing GitHub. +both canvases' session-scoped persistence without accessing GitHub. diff --git a/.github/extensions/open-pr-board/extension.mjs b/.github/extensions/open-pr-board/extension.mjs index 11f7f02fb..b51eb98e5 100644 --- a/.github/extensions/open-pr-board/extension.mjs +++ b/.github/extensions/open-pr-board/extension.mjs @@ -43,8 +43,7 @@ async function getStore(repo) { // The repo is validated against REPO_PATTERN above, so it cannot escape the state directory. const filename = `${repo.replace("/", "__")}.json`; const file = path.join(session.workspacePath, "extensions", CANVAS_ID, "state", filename); - const legacyFile = path.join(EXT_DIR, "state", filename); - store = await new BoardStore({ file, repo }).load({ legacyFile }); + store = await new BoardStore({ file, repo }).load(); stores.set(repo, store); } return store; diff --git a/.github/extensions/open-pr-board/lib/state.mjs b/.github/extensions/open-pr-board/lib/state.mjs index 12ea86b07..7a0f3f498 100644 --- a/.github/extensions/open-pr-board/lib/state.mjs +++ b/.github/extensions/open-pr-board/lib/state.mjs @@ -68,30 +68,19 @@ export class BoardStore { return () => this.listeners.delete(listener); } - async load({ legacyFile } = {}) { - let raw; - let imported = false; + async load() { try { - raw = await readFile(this.file, "utf8"); + const raw = await readFile(this.file, "utf8"); + const parsed = JSON.parse(raw); + this.state = { + repo: this.repo, + updatedAt: parsed.updatedAt ?? null, + items: parsed.items && typeof parsed.items === "object" ? parsed.items : {}, + history: Array.isArray(parsed.history) ? parsed.history : [], + }; } catch (error) { if (error.code !== "ENOENT") throw error; - if (!legacyFile) return this; - try { - raw = await readFile(legacyFile, "utf8"); - imported = true; - } catch (error) { - if (error.code !== "ENOENT") throw error; - return this; - } } - const parsed = JSON.parse(raw); - this.state = { - repo: this.repo, - updatedAt: parsed.updatedAt ?? null, - items: parsed.items && typeof parsed.items === "object" ? parsed.items : {}, - history: Array.isArray(parsed.history) ? parsed.history : [], - }; - if (imported) await this.persist(); return this; } diff --git a/eng/board-state-storage.test.mjs b/eng/board-state-storage.test.mjs index e5f2cac50..29f0d8ef0 100644 --- a/eng/board-state-storage.test.mjs +++ b/eng/board-state-storage.test.mjs @@ -116,22 +116,17 @@ for (const id of ["open-pr-board", "external-plugin-review-board"]) { assert.deepEqual(await fingerprint(f.extensionDir), before); }); - test(`${id} imports legacy state without changing it and prefers session state thereafter`, async (t) => { + test(`${id} starts fresh and ignores state stored in the extension folder`, async (t) => { const f = await fixture(t, id); - const legacy = { + await writeJSON(f.legacyFile, { version: 1, repo, items: { "1": { ...item, manualColumn: column, reviewStatus: "reviewed" } }, - history: [{ number: 1, comment: "Preserved history" }], - }; - await writeJSON(f.legacyFile, legacy); + history: [{ number: 1, comment: "Old history" }], + }); const before = await fingerprint(f.extensionDir); const act = await loadCanvas(f); - assert.equal((await act("get_board")).items[0].manualOverride, column); - const imported = JSON.parse(await readFile(f.stateFile, "utf8")); - assert.deepEqual(imported.items, legacy.items); - assert.deepEqual(imported.history, legacy.history); - await act("move_item", { number: 1, column: "unreviewed" }); - const reloaded = await loadCanvas(f); - assert.equal((await reloaded("get_board")).items[0].manualOverride, null); + assert.equal((await act("get_board")).items.length, 0); + await act("refresh"); + assert.equal((await act("get_board")).items[0].manualOverride, null); assert.deepEqual(await fingerprint(f.extensionDir), before); }); @@ -142,9 +137,8 @@ for (const id of ["open-pr-board", "external-plugin-review-board"]) { assert.deepEqual(await fingerprint(f.extensionDir), before); }); - test(`${id} surfaces corrupt session state instead of falling back to legacy data`, async (t) => { + test(`${id} surfaces corrupt session state`, async (t) => { const f = await fixture(t, id); - await writeJSON(f.legacyFile, { repo, items: {}, history: [] }); await mkdir(path.dirname(f.stateFile), { recursive: true }); await writeFile(f.stateFile, "not json", "utf8"); await assert.rejects(async () => { @@ -152,23 +146,6 @@ for (const id of ["open-pr-board", "external-plugin-review-board"]) { await act("get_board"); }, /JSON|Unexpected token/); }); - - test(`${id} surfaces legacy parse and migration write failures`, async (t) => { - const f = await fixture(t, id); - await mkdir(path.dirname(f.legacyFile), { recursive: true }); - await writeFile(f.legacyFile, "not json", "utf8"); - await assert.rejects(async () => { - const act = await loadCanvas(f); - await act("get_board"); - }, /JSON|Unexpected token/); - - await writeJSON(f.legacyFile, { repo, items: {}, history: [] }); - await writeFile(f.workspacePath, "not a directory", "utf8"); - await assert.rejects(async () => { - const act = await loadCanvas(f); - await act("get_board"); - }, /ENOTDIR|ENOENT|EEXIST/); - }); } test("both canvases keep independent boards in the same session workspace", async (t) => {