Files
pulse/scripts/check-assistant-identity.mjs
pulse-triage[bot] 2d36bb7d4d test(assistant): add portable identity browser regression
Release-line could reproduce tool identity and shared evidence regressions but could not replay browser evidence tied to private capture paths. Supply a synthetic standalone real reducer/transcript journey with blocked backend and external requests, desktop and narrow assertions, and content-hashed receipts. This verifies renderer behaviour without claiming provider, production or release admission qualification.

Change-source: pulse-maintainer
2026-09-06 12:29:50 +01:00

233 lines
7.4 KiB
JavaScript

// Run with pulse-heavy-run -- node scripts/check-assistant-identity.mjs
import { chromium, expect } from "../node_modules/@playwright/test/index.mjs";
import { createServer } from "../frontend-modern/node_modules/vite/dist/node/index.js";
import solid from "../frontend-modern/node_modules/vite-plugin-solid/dist/esm/index.mjs";
import { resolve } from "node:path";
import { mkdir, readFile, writeFile, readdir, rm } from "node:fs/promises";
import { createHash } from "node:crypto";
const root = resolve(import.meta.dirname, "../frontend-modern");
const output = resolve(
process.env.ASSISTANT_IDENTITY_OUTPUT || `${root}/../tmp/assistant-identity`,
);
await mkdir(output, { recursive: true });
await rm(`${output}/receipt.json`, { force: true });
process.chdir(root);
const server = await createServer({
configFile: false,
root,
optimizeDeps: { esbuildOptions: { target: "esnext" } },
plugins: [solid()],
resolve: { alias: { "@": `${root}/src` } },
server: { host: "127.0.0.1", port: 0 },
});
let browser;
const results = [];
try {
await server.listen();
const base = server.resolvedUrls.local[0];
browser = await chromium.launch({ headless: true });
for (const width of [1440, 900, 390]) {
const context = await browser.newContext({
viewport: { width, height: 1000 },
serviceWorkers: "block",
});
const page = await context.newPage();
const errors = [],
blocked = [];
page.on("pageerror", (e) => errors.push(e.message));
await context.route("**/*", (route) => {
const url = new URL(route.request().url());
if (
url.origin === new URL(base).origin &&
!url.pathname.startsWith("/api/")
)
return route.continue();
blocked.push(url.pathname);
return route.abort();
});
await page.goto(`${base}qualification/assistant-identity/`);
await page.waitForFunction(() => !!window.identityFixture);
const call = (method, arg) =>
page.evaluate(
([method, arg]) => window.identityFixture[method](arg),
[method, arg],
);
const fire = (type, data) => call("fire", { type, data });
await call("start");
for (const id of ["a", "b"])
await fire("tool_start", {
id,
name: "pulse_query",
input: JSON.stringify({ action: "search", query: `fixture-${id}` }),
});
expect((await call("snapshot")).pendingTools.map((t) => t.id)).toEqual([
"a",
"b",
]);
await fire("tool_progress", {
id: "a",
name: "pulse_query",
message: "Reading A",
});
await fire("tool_end", {
id: "a",
name: "pulse_query",
output: "identity evidence A",
success: true,
});
expect((await call("snapshot")).pendingTools.map((t) => t.id)).toEqual([
"b",
]);
await fire("tool_end", {
id: "b",
name: "pulse_query",
output: "identity evidence B",
success: false,
});
expect((await call("snapshot")).toolCalls.map((t) => t.output)).toEqual([
"identity evidence A",
"identity evidence B",
]);
for (const id of ["c", "d"]) {
await fire("tool_start", {
id,
name: "pulse_control",
input: JSON.stringify({ resource_id: id }),
});
await fire("approval_needed", {
tool_id: id,
tool_name: "pulse_control",
approval_id: `approval-${id}`,
command: `synthetic-${id}`,
});
}
await expect(
page.getByText("Approval Required", { exact: true }),
).toHaveCount(2);
await fire("tool_end", {
id: "c",
name: "pulse_control",
output: "synthetic c completed",
success: true,
});
await expect(
page.getByText("Approval Required", { exact: true }),
).toHaveCount(1);
expect(
(await call("snapshot")).pendingApprovals.map((a) => a.toolId),
).toEqual(["d"]);
await expect(page.getByText("synthetic-d", { exact: true })).toBeVisible();
await page.screenshot({
path: `${output}/approvals-${width}.png`,
fullPage: true,
});
await fire("done", {});
while (await page.locator('[aria-expanded="false"]').count())
await page.locator('[aria-expanded="false"]').first().click();
await expect(
page.getByText("identity evidence A", { exact: true }).last(),
).toBeVisible();
await expect(
page.getByText("identity evidence B", { exact: true }).last(),
).toBeVisible();
for (let stage = 0; stage < 4; stage++) {
await call("shared", stage);
// Allow Solid's render effects to run between immutable snapshots.
await page.evaluate(() => new Promise(requestAnimationFrame));
}
expect(await call("evidence")).toEqual([
{
name: "pulse_query",
input: "client",
output: "client evidence",
success: true,
},
{
name: "pulse_alerts",
input: "alerts",
output: "alert evidence",
success: true,
},
]);
const expand = page.locator('[role="button"][aria-expanded="false"]');
while (await expand.count()) await expand.first().click();
await expect(
page.getByText("client evidence", { exact: true }).last(),
).toBeVisible();
await expect(
page.getByText("alert evidence", { exact: true }).last(),
).toBeVisible();
await expect(
page.getByText("client", { exact: true }).last(),
).toBeVisible();
await expect(
page.getByText("alerts", { exact: true }).last(),
).toBeVisible();
expect(
await page.evaluate(
() => document.documentElement.scrollWidth <= innerWidth,
),
).toBe(true);
await page.screenshot({
path: `${output}/expanded-${width}.png`,
fullPage: true,
});
expect(errors).toEqual([]);
expect(blocked).toEqual([]);
results.push({ width, passed: true });
await context.close();
}
const hashes = {};
for (const file of await readdir(output)) {
if (file.endsWith(".png"))
hashes[`artifacts/${file}`] = createHash("sha256")
.update(await readFile(`${output}/${file}`))
.digest("hex");
}
hashes["../package-lock.json"] = createHash("sha256")
.update(await readFile(`${root}/../package-lock.json`))
.digest("hex");
async function hashTree(dir) {
for (const entry of await readdir(dir, { withFileTypes: true })) {
const path = `${dir}/${entry.name}`;
if (entry.isDirectory()) await hashTree(path);
else
hashes[path.slice(root.length + 1)] = createHash("sha256")
.update(await readFile(path))
.digest("hex");
}
}
await hashTree(`${root}/src`);
await hashTree(`${root}/qualification/assistant-identity`);
for (const path of [
"package-lock.json",
"tailwind.config.js",
"postcss.config.js",
])
hashes[path] = createHash("sha256")
.update(await readFile(`${root}/${path}`))
.digest("hex");
hashes["../scripts/check-assistant-identity.mjs"] = createHash("sha256")
.update(await readFile(import.meta.filename))
.digest("hex");
await writeFile(
`${output}/receipt.json`,
JSON.stringify(
{
recordedAt: new Date().toISOString(),
scope:
"Synthetic reducer and renderer only; no provider, backend or real actions qualified",
browser: browser.version(),
results,
hashes,
},
null,
2,
),
);
console.log(JSON.stringify({ output, results }));
} finally {
await browser?.close();
await server.close();
}