mirror of
https://github.com/github/awesome-copilot.git
synced 2026-10-12 23:04:14 +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.
|
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.
|
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
|
Infinite sessions must be enabled to provide the workspace. Keeping writes outside the extension
|
||||||
folder avoids invalidating its safety fingerprint.
|
folder avoids invalidating its safety fingerprint. State previously stored in the extension folder is
|
||||||
|
not imported; boards start fresh.
|
||||||
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.
|
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -47,4 +45,4 @@ present, without modifying it. Existing session state always takes precedence.
|
|||||||
## Regression tests
|
## Regression tests
|
||||||
|
|
||||||
Run `node --experimental-vm-modules --test eng/board-state-storage.test.mjs` from the repository root
|
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(
|
store = await new BoardStore(
|
||||||
path.join(session.workspacePath, "extensions", CANVAS_ID, "state", "board.json"),
|
path.join(session.workspacePath, "extensions", CANVAS_ID, "state", "board.json"),
|
||||||
REPO,
|
REPO,
|
||||||
).load({ legacyFile: path.join(EXT_DIR, "state", "board.json") });
|
).load();
|
||||||
|
|||||||
@@ -41,25 +41,14 @@ export class BoardStore {
|
|||||||
this.writeChain = Promise.resolve();
|
this.writeChain = Promise.resolve();
|
||||||
}
|
}
|
||||||
|
|
||||||
async load({ legacyFile } = {}) {
|
async load() {
|
||||||
let raw;
|
|
||||||
let imported = false;
|
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
if (error.code !== "ENOENT") throw 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;
|
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
|
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.
|
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.
|
Keeping writes outside the extension folder avoids invalidating its safety fingerprint.
|
||||||
|
State previously stored in the extension folder is not imported; boards start fresh.
|
||||||
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.
|
|
||||||
Use **Reset board** (or the `reset_board` action) to clear reviews and history without
|
Use **Reset board** (or the `reset_board` action) to clear reviews and history without
|
||||||
touching GitHub.
|
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
|
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.
|
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
|
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.
|
// The repo is validated against REPO_PATTERN above, so it cannot escape the state directory.
|
||||||
const filename = `${repo.replace("/", "__")}.json`;
|
const filename = `${repo.replace("/", "__")}.json`;
|
||||||
const file = path.join(session.workspacePath, "extensions", CANVAS_ID, "state", filename);
|
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();
|
||||||
store = await new BoardStore({ file, repo }).load({ legacyFile });
|
|
||||||
stores.set(repo, store);
|
stores.set(repo, store);
|
||||||
}
|
}
|
||||||
return store;
|
return store;
|
||||||
|
|||||||
@@ -68,30 +68,19 @@ export class BoardStore {
|
|||||||
return () => this.listeners.delete(listener);
|
return () => this.listeners.delete(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
async load({ legacyFile } = {}) {
|
async load() {
|
||||||
let raw;
|
|
||||||
let imported = false;
|
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
if (error.code !== "ENOENT") throw 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;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -116,22 +116,17 @@ for (const id of ["open-pr-board", "external-plugin-review-board"]) {
|
|||||||
assert.deepEqual(await fingerprint(f.extensionDir), before);
|
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 f = await fixture(t, id);
|
||||||
const legacy = {
|
await writeJSON(f.legacyFile, {
|
||||||
version: 1, repo, items: { "1": { ...item, manualColumn: column, reviewStatus: "reviewed" } },
|
version: 1, repo, items: { "1": { ...item, manualColumn: column, reviewStatus: "reviewed" } },
|
||||||
history: [{ number: 1, comment: "Preserved history" }],
|
history: [{ number: 1, comment: "Old history" }],
|
||||||
};
|
});
|
||||||
await writeJSON(f.legacyFile, legacy);
|
|
||||||
const before = await fingerprint(f.extensionDir);
|
const before = await fingerprint(f.extensionDir);
|
||||||
const act = await loadCanvas(f);
|
const act = await loadCanvas(f);
|
||||||
assert.equal((await act("get_board")).items[0].manualOverride, column);
|
assert.equal((await act("get_board")).items.length, 0);
|
||||||
const imported = JSON.parse(await readFile(f.stateFile, "utf8"));
|
await act("refresh");
|
||||||
assert.deepEqual(imported.items, legacy.items);
|
assert.equal((await act("get_board")).items[0].manualOverride, null);
|
||||||
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.deepEqual(await fingerprint(f.extensionDir), before);
|
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);
|
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);
|
const f = await fixture(t, id);
|
||||||
await writeJSON(f.legacyFile, { repo, items: {}, history: [] });
|
|
||||||
await mkdir(path.dirname(f.stateFile), { recursive: true });
|
await mkdir(path.dirname(f.stateFile), { recursive: true });
|
||||||
await writeFile(f.stateFile, "not json", "utf8");
|
await writeFile(f.stateFile, "not json", "utf8");
|
||||||
await assert.rejects(async () => {
|
await assert.rejects(async () => {
|
||||||
@@ -152,23 +146,6 @@ for (const id of ["open-pr-board", "external-plugin-review-board"]) {
|
|||||||
await act("get_board");
|
await act("get_board");
|
||||||
}, /JSON|Unexpected token/);
|
}, /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) => {
|
test("both canvases keep independent boards in the same session workspace", async (t) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user