mirror of
https://github.com/github/awesome-copilot.git
synced 2026-10-12 14:54:16 +00:00
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 545df520-6557-4211-a110-53c60f4b9f3a
196 lines
8.0 KiB
JavaScript
196 lines
8.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { cp, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import vm from "node:vm";
|
|
import { test } from "node:test";
|
|
|
|
const extensionsDir = fileURLToPath(new URL("../.github/extensions/", import.meta.url));
|
|
const repo = "github/awesome-copilot";
|
|
const item = {
|
|
number: 1, title: "Example", author: "contributor", labels: [],
|
|
url: "https://github.com/github/awesome-copilot/issues/1",
|
|
};
|
|
|
|
async function fixture(t, id) {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "board-state-storage-"));
|
|
t.after(() => rm(root, { recursive: true, force: true }));
|
|
const extensionDir = path.join(root, id);
|
|
await cp(path.join(extensionsDir, id), extensionDir, { recursive: true });
|
|
const workspacePath = path.join(root, "session");
|
|
const filename = id === "open-pr-board" ? "github__awesome-copilot.json" : "board.json";
|
|
return {
|
|
id, root, extensionDir, workspacePath,
|
|
legacyFile: path.join(extensionDir, "state", filename),
|
|
stateFile: path.join(workspacePath, "extensions", id, "state", filename),
|
|
};
|
|
}
|
|
|
|
async function fingerprint(dir) {
|
|
const files = [];
|
|
for (const entry of (await readdir(dir, { withFileTypes: true })).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
const file = path.join(dir, entry.name);
|
|
files.push([entry.name, entry.isDirectory()
|
|
? await fingerprint(file)
|
|
: createHash("sha256").update(await readFile(file)).digest("hex")]);
|
|
}
|
|
return files;
|
|
}
|
|
|
|
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 {
|
|
constructor(code, message) { super(message); this.code = code; }
|
|
},
|
|
};
|
|
const filename = path.join(extensionDir, "extension.mjs");
|
|
const module = new vm.SourceTextModule(await readFile(filename, "utf8"), {
|
|
context,
|
|
initializeImportMeta(meta) { meta.url = pathToFileURL(filename).href; },
|
|
});
|
|
await module.link(async (specifier) => {
|
|
let exports;
|
|
if (specifier === "@github/copilot-sdk/extension") {
|
|
exports = sdk;
|
|
} else if (specifier === "./lib/github.mjs") {
|
|
exports = {
|
|
listOpenPullRequests: async () => [item],
|
|
listReadyIssues: async () => [item],
|
|
};
|
|
} else {
|
|
exports = await import(specifier.startsWith(".")
|
|
? pathToFileURL(path.join(extensionsDir, id, specifier)).href
|
|
: specifier);
|
|
}
|
|
return new vm.SyntheticModule(Object.keys(exports), function () {
|
|
for (const [name, value] of Object.entries(exports)) this.setExport(name, value);
|
|
}, { context });
|
|
});
|
|
await module.evaluate();
|
|
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) {
|
|
await mkdir(path.dirname(file), { recursive: true });
|
|
await writeFile(file, JSON.stringify(value), "utf8");
|
|
}
|
|
|
|
for (const id of ["open-pr-board", "external-plugin-review-board"]) {
|
|
const column = id === "open-pr-board" ? "needs-insight" : "needs-review";
|
|
|
|
test(`${id} writes only in its session namespace and survives reloads`, async (t) => {
|
|
const f = await fixture(t, id);
|
|
const before = await fingerprint(f.extensionDir);
|
|
const act = await loadCanvas(f);
|
|
await act("refresh");
|
|
await act("move_item", { number: 1, column });
|
|
assert.equal(JSON.parse(await readFile(f.stateFile, "utf8")).items["1"].manualColumn, column);
|
|
assert.deepEqual(await fingerprint(f.extensionDir), before);
|
|
|
|
const reloaded = await loadCanvas(f);
|
|
assert.equal((await reloaded("get_board")).items[0].manualOverride, column);
|
|
const otherSession = await loadCanvas({ ...f, workspacePath: path.join(f.root, "other-session") });
|
|
assert.equal((await otherSession("get_board")).items.length, 0);
|
|
|
|
if (id === "open-pr-board") {
|
|
await act("refresh", { repo: "example/other" });
|
|
const otherFile = path.join(path.dirname(f.stateFile), "example__other.json");
|
|
assert.equal(JSON.parse(await readFile(otherFile, "utf8")).repo, "example/other");
|
|
await act("reset_board");
|
|
const afterReset = await loadCanvas(f);
|
|
assert.equal((await afterReset("get_board")).items[0].manualOverride, null);
|
|
}
|
|
assert.deepEqual(await fingerprint(f.extensionDir), before);
|
|
});
|
|
|
|
test(`${id} starts fresh and ignores state stored in the extension folder`, async (t) => {
|
|
const f = await fixture(t, id);
|
|
await writeJSON(f.legacyFile, {
|
|
version: 1, repo, items: { "1": { ...item, manualColumn: column, reviewStatus: "reviewed" } },
|
|
history: [{ number: 1, comment: "Old history" }],
|
|
});
|
|
const before = await fingerprint(f.extensionDir);
|
|
const act = await loadCanvas(f);
|
|
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);
|
|
});
|
|
|
|
test(`${id} rejects a missing session workspace instead of writing to the extension`, async (t) => {
|
|
const f = await fixture(t, id);
|
|
const before = await fingerprint(f.extensionDir);
|
|
await assert.rejects(loadCanvas({ ...f, workspacePath: undefined }), /requires a session workspace/);
|
|
assert.deepEqual(await fingerprint(f.extensionDir), before);
|
|
});
|
|
|
|
test(`${id} surfaces corrupt session state`, async (t) => {
|
|
const f = await fixture(t, id);
|
|
await mkdir(path.dirname(f.stateFile), { recursive: true });
|
|
await writeFile(f.stateFile, "not json", "utf8");
|
|
await assert.rejects(async () => {
|
|
const act = await loadCanvas(f);
|
|
await act("get_board");
|
|
}, /JSON|Unexpected token/);
|
|
});
|
|
}
|
|
|
|
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 };
|
|
const prBoard = await loadCanvas(pr);
|
|
const pluginBoard = await loadCanvas(plugin);
|
|
await prBoard("refresh");
|
|
await pluginBoard("refresh");
|
|
await prBoard("move_item", { number: 1, column: "needs-insight" });
|
|
await pluginBoard("move_item", { number: 1, column: "accept" });
|
|
const reloadedPR = await loadCanvas(pr);
|
|
const reloadedPlugin = await loadCanvas(plugin);
|
|
assert.equal((await reloadedPR("get_board")).items[0].manualOverride, "needs-insight");
|
|
assert.equal((await reloadedPlugin("get_board")).items[0].manualOverride, "accept");
|
|
});
|