d994abee5d
qa-real-install.mjs builds the plugin, copies it to a node_modules-free cache, and exercises every hook + boots both MCP servers (F3 gate). publish-claude-invariant test pins the no-accidental-publish guardrails (workflow_dispatch-only, identity guard, missing-secret hard-fail) and is wired into test:claude. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
120 lines
4.5 KiB
JavaScript
120 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
// F3 real-install QA: prove the built plugin works from a COPIED cache tree
|
|
// (no node_modules, no `../` traversal) — the failure mode CI cannot catch,
|
|
// because CI runs from the monorepo where relative paths resolve.
|
|
//
|
|
// Steps: build the plugin -> copy plugin/ to a temp dir (the "cache") -> with
|
|
// CLAUDE_PLUGIN_ROOT pointed at the copy, fire each hook and boot both MCP
|
|
// servers, asserting each succeeds. Evidence is written under .omo/evidence/.
|
|
//
|
|
// node scripts/qa-real-install.mjs build, copy, exercise
|
|
// node scripts/qa-real-install.mjs --no-build use the already-built plugin
|
|
|
|
import { spawnSync } from "node:child_process";
|
|
import { cpSync, mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const PLUGIN_ROOT = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
const PACKAGE_ROOT = dirname(PLUGIN_ROOT);
|
|
const REPO_ROOT = dirname(dirname(PACKAGE_ROOT));
|
|
const EVIDENCE = join(REPO_ROOT, ".omo", "evidence", "task-30-install-qa");
|
|
|
|
const failures = [];
|
|
function check(name, ok, detail = "") {
|
|
console.log(`${ok ? "PASS" : "FAIL"} ${name}${detail ? ` — ${detail}` : ""}`);
|
|
if (!ok) failures.push(name);
|
|
}
|
|
|
|
function hook(cli, sub, payload, env = {}) {
|
|
const res = spawnSync("node", [cli, "hook", sub], {
|
|
input: JSON.stringify(payload),
|
|
encoding: "utf8",
|
|
env: { ...process.env, ...env },
|
|
});
|
|
return { status: res.status, out: (res.stdout ?? "") + (res.stderr ?? "") };
|
|
}
|
|
|
|
function mcpInitialize(cli, env) {
|
|
const req = `${JSON.stringify({
|
|
jsonrpc: "2.0",
|
|
id: 1,
|
|
method: "initialize",
|
|
params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "qa", version: "0" } },
|
|
})}\n`;
|
|
const res = spawnSync("node", [cli, "mcp"], { input: req, encoding: "utf8", timeout: 15000, env: { ...process.env, ...env } });
|
|
return (res.stdout ?? "") + (res.stderr ?? "");
|
|
}
|
|
|
|
if (!process.argv.includes("--no-build")) {
|
|
const build = spawnSync("npm", ["run", "build"], { cwd: PLUGIN_ROOT, stdio: "inherit" });
|
|
if (build.status !== 0) {
|
|
console.error("plugin build failed");
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
mkdirSync(EVIDENCE, { recursive: true });
|
|
const cache = join(mkdtempSync(join(tmpdir(), "omo-cache-")), "omo");
|
|
cpSync(PLUGIN_ROOT, cache, { recursive: true });
|
|
const work = mkdtempSync(join(tmpdir(), "omo-work-"));
|
|
writeFileSync(join(work, "CLAUDE.md"), "QA-RULE-MARKER project guidance\n");
|
|
writeFileSync(join(work, "x.ts"), "export const a = 1;\n");
|
|
const env = { CLAUDE_PLUGIN_ROOT: cache, PLUGIN_DATA: join(work, "pd") };
|
|
const C = (name) => join(cache, "components", name, "dist", "cli.js");
|
|
|
|
const ups = hook(C("rules"), "user-prompt-submit", {
|
|
hook_event_name: "UserPromptSubmit",
|
|
session_id: "qa",
|
|
transcript_path: null,
|
|
cwd: work,
|
|
permission_mode: "default",
|
|
prompt: "hello",
|
|
}, env);
|
|
check("rules injects from cache (no turn_id/model)", ups.status === 0 && ups.out.includes("hookSpecificOutput"));
|
|
|
|
const ulw = hook(C("ultrawork"), "user-prompt-submit", {
|
|
hook_event_name: "UserPromptSubmit",
|
|
session_id: "qa",
|
|
transcript_path: null,
|
|
cwd: work,
|
|
prompt: "ulw: go",
|
|
}, env);
|
|
check("ultrawork directive injects on `ulw:`", /ULTRAWORK/i.test(ulw.out));
|
|
|
|
for (const comp of ["comment-checker", "lsp", "rules"]) {
|
|
const ptu = hook(C(comp), "post-tool-use", {
|
|
hook_event_name: "PostToolUse",
|
|
session_id: "qa",
|
|
transcript_path: null,
|
|
cwd: work,
|
|
permission_mode: "default",
|
|
tool_name: "Write",
|
|
tool_use_id: "t",
|
|
tool_input: { file_path: join(work, "x.ts"), content: "export const a = 1;\n" },
|
|
tool_response: "ok",
|
|
}, env);
|
|
check(`${comp} PostToolUse runs from cache`, ptu.status === 0);
|
|
}
|
|
|
|
const stop = hook(C("start-work-continuation"), "stop", {
|
|
hook_event_name: "Stop",
|
|
session_id: "qa",
|
|
transcript_path: null,
|
|
cwd: work,
|
|
permission_mode: "default",
|
|
stop_hook_active: false,
|
|
}, env);
|
|
check("start-work-continuation Stop no-ops without boulder", stop.status === 0);
|
|
|
|
for (const server of ["ast-grep", "lsp"]) {
|
|
const handshake = mcpInitialize(join(cache, "mcp", server, "cli.js"), env);
|
|
writeFileSync(join(EVIDENCE, `mcp-${server}-handshake.txt`), handshake);
|
|
check(`${server} MCP server boots from cache`, handshake.includes('"serverInfo"'));
|
|
}
|
|
|
|
writeFileSync(join(EVIDENCE, "summary.txt"), `failures: ${failures.length ? failures.join(", ") : "none"}\ncache: ${cache}\n`);
|
|
console.log(failures.length === 0 ? "\nF3 real-install QA: ALL PASS" : `\nF3 real-install QA: ${failures.length} FAILURE(S)`);
|
|
process.exit(failures.length === 0 ? 0 : 1);
|