mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
2d36bb7d4d
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
233 lines
7.4 KiB
JavaScript
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();
|
|
}
|