mirror of
https://github.com/github/awesome-copilot.git
synced 2026-10-12 14:54:16 +00:00
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
This commit is contained in:
co-authored by
Copilot App
parent
f4319559ab
commit
758f4382db
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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/<owner>__<repo>.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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user