mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 20:25:56 +00:00
9e7a5ffdeb
GHSA-vfj7-8cjw-p6xm affects every braces release and has no fix, so the required frontend audit failed with seven high findings on any pull request that changes the dependency graph. Tailwind CSS 3 (chokidar, fast-glob, micromatch) and jscpd 4 (@jscpd/finder, fast-glob) were the only paths in, both dev-only, and no override can remove braces from fast-glob or micromatch. Tailwind CSS 4.3.3 through @tailwindcss/vite and jscpd 5.4.0 leave none; npm audit over the full graph reports zero. The stylesheet keeps the v3 rendering. It pins the v3 palette (inlined, so opacity modifiers compile to static colors), font stacks and line heights, keeps v3's cascade order by placing former component and unlayered rules around the generated utilities, and restores v3 hover on touch devices, gray-200 default borders, the 3px blue ring, placeholder color, button cursor, table-cell padding and the space-* and divide-* child selectors. The 900/950 shades Pulse overrode with 25% alpha are opaque now and their callers say /25, because v4 multiplies an opacity modifier into a translucent color instead of replacing it. Opacity modifiers on var() colors that v3 never generated are removed. Utility order changed from lexical to numeric, so a few caller overrides that silently lost in v3 would start to win. Touch targets keep their authored 44px phone size (the Machines row stays 32px); cosmetic overrides v3 never applied are removed, and overrides it did apply use the important suffix. Contract-Neutral: Tailwind CSS 4 migration for GHSA-vfj7-8cjw-p6xm: utility class renames and stylesheet compatibility, no subsystem contract change
229 lines
7.3 KiB
JavaScript
229 lines
7.3 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"])
|
|
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();
|
|
}
|