mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-04 19:28:46 +01:00
merge: preserve current canonical source and Claude fixture review
Keep the qualified four-target selection and both Linux provider-pack assembly paths while carrying the current shared source. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
commit
f29dad6c94
449 files changed
+22989
-15619
No files matched your search
@@ -16,6 +16,7 @@ const playwrightConfig = path.join(repoRoot, "tests", "e2e", "playwright.config.
|
||||
const prCallerWorkflow = path.join(repoRoot, ".github", "workflows", "pr.yml");
|
||||
const trustedPrWorkflowPath = ".github/workflows/pr-trusted.yml";
|
||||
const trustedPrWorkflow = path.join(repoRoot, trustedPrWorkflowPath);
|
||||
const routingWorkflow = path.join(repoRoot, ".github", "workflows", "ci-routing.yml");
|
||||
|
||||
const SHARD_COUNT = 8;
|
||||
|
||||
@@ -54,11 +55,11 @@ function readWorkflowJobs(workflow) {
|
||||
}
|
||||
|
||||
function runStackScope(stack, prBaseRef) {
|
||||
const workflow = readFileSync(trustedPrWorkflow, "utf8");
|
||||
const workflow = readFileSync(routingWorkflow, "utf8");
|
||||
const match = workflow.match(
|
||||
/ - name: Select stacked PR CI scope[\s\S]*? run: \|\n([\s\S]*?)\n\n policy:/,
|
||||
/ - name: Select stacked PR CI scope[\s\S]*? run: \|\n([\s\S]*?)\s*$/,
|
||||
);
|
||||
assert.ok(match, "trusted workflow must define the stacked PR scope script");
|
||||
assert.ok(match, "routing workflow must define the stacked PR scope script");
|
||||
const script = match[1]
|
||||
.split("\n")
|
||||
.map((line) => line.replace(/^ {10}/, ""))
|
||||
@@ -207,10 +208,22 @@ test("the trusted PR workflow keeps a stable aggregate check named e2e over the
|
||||
}
|
||||
});
|
||||
|
||||
test("runner selection has reserved AWS capacity and executes metadata checks only", () => {
|
||||
const workflow = readFileSync(routingWorkflow, "utf8");
|
||||
const jobs = readWorkflowJobs(workflow.split(/^jobs:\n/m)[1]);
|
||||
assert.deepEqual([...jobs.keys()], ["gate"]);
|
||||
assert.match(jobs.get("gate"), /^ {4}runs-on: \$\{\{ vars\.AWS_CI_ENABLED[\s\S]*paperclip-ci-routing-x64/m);
|
||||
assert.doesNotMatch(jobs.get("gate"), /uses:|actions\/checkout|secrets\./);
|
||||
assert.match(workflow, /^ {6}runner:\n {8}value: \$\{\{ jobs\.gate\.outputs\.runner \}\}$/m);
|
||||
assert.match(workflow, /^ {6}full_ci:\n {8}value: \$\{\{ jobs\.gate\.outputs\.full_ci \}\}$/m);
|
||||
assert.doesNotMatch(workflow, /^ {2}(?:actions|contents|pull-requests): write$/m);
|
||||
});
|
||||
|
||||
test("the trusted PR workflow limits full CI to merge-relevant stack layers", () => {
|
||||
const workflow = readFileSync(trustedPrWorkflow, "utf8");
|
||||
const jobs = readWorkflowJobs(workflow);
|
||||
const gate = jobs.get("gate");
|
||||
assert.match(jobs.get("gate"), /uses: paperclipai\/paperclip\/\.github\/workflows\/ci-routing\.yml@master/);
|
||||
const gate = readWorkflowJobs(readFileSync(routingWorkflow, "utf8")).get("gate");
|
||||
|
||||
assert.match(gate, /^ {6}full_ci: \$\{\{ steps\.scope\.outputs\.full_ci \}\}$/m);
|
||||
assert.match(gate, /STACK_JSON: \$\{\{ toJSON\(github\.event\.pull_request\.stack\) \}\}/);
|
||||
|
||||
@@ -150,7 +150,6 @@ test('verification never elevates PR-controlled provisioning or cleanup on the h
|
||||
|
||||
test('the image and Grok downloads start before staging and join without weakening the npm checks', () => {
|
||||
const source = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
|
||||
// The optional Mac consumer owns separate scratch and no Linux prefetches.
|
||||
const linuxSource = source.slice(source.indexOf("assert.equal(process.platform, 'linux'"));
|
||||
const position = fragment => { const index = linuxSource.indexOf(fragment); assert.notEqual(index, -1, `verifier must contain ${fragment}`); return index; };
|
||||
// Exactly the pinned consumer image is pulled, for the platform the probes run on.
|
||||
@@ -349,8 +348,6 @@ test('the installed Codex probe exercises the public export and rejects incomple
|
||||
writeFileSync(legacyCommand, '#!/bin/sh\nexit 0\n', { mode: 0o755 });
|
||||
writeFileSync(index, `export const resolvePinnedCodexCommand = () => ${JSON.stringify(legacyCommand)};`);
|
||||
result = run(); assert.notEqual(result.status, 0); assert.match(result.stderr, /Selected Codex platform version must match the qualified pin/);
|
||||
// Native Codex uses protocol compatibility, not the ACPX version allowlist.
|
||||
// Keep exact official-npm host provenance and the selected resource boundary.
|
||||
mkdirSync(join(legacy, 'vendor'));
|
||||
const legacyNative = join(legacy, 'vendor', triple, 'codex', process.platform === 'win32' ? 'codex.exe' : 'codex');
|
||||
mkdirSync(join(legacy, 'vendor', triple, 'codex'), { recursive: true }); writeFileSync(legacyNative, nativeBytes, { mode: 0o755 });
|
||||
|
||||
@@ -845,9 +845,6 @@ const waitForFixture = async (predicate, label) => {
|
||||
assert.fail(label);
|
||||
};
|
||||
|
||||
// Execute the workflow's actual coordinator with two tiny local Node commands.
|
||||
// Both must cross the start barrier before either can finish; no pnpm, Cargo,
|
||||
// provider, registry, or Docker build runs in these fixtures.
|
||||
async function withNativePreparationFixture(mode, check) {
|
||||
const root = realpathSync(mkdtempSync(path.join(tmpdir(), 'native-preparation-')));
|
||||
let controller;
|
||||
@@ -1014,7 +1011,6 @@ test("ARM qualification binds separate product and reusable-workflow revisions e
|
||||
TOOLING_WORKFLOW_REF: "paperclipai/paperclip/.github/workflows/release-smoke.yml@refs/heads/qualification",
|
||||
TOOLING_WORKFLOW_PATH: ".github/workflows/release-smoke.yml",
|
||||
FIXTURE_PRODUCT_HEAD: productHead, FIXTURE_TOOLING_HEAD: toolingHead, FIXTURE_PREPARED: prepared,
|
||||
// Reusable calls retain the caller context; it must not select tooling.
|
||||
GITHUB_REPOSITORY: "different/caller", GITHUB_SHA: "d".repeat(40),
|
||||
} });
|
||||
assert.equal(result.status, expectedStatus, result.stderr);
|
||||
|
||||
@@ -50,9 +50,6 @@ export function resolveNativeRunnerDevBinary({
|
||||
const flat = path.join(packageRoot, "dist", "bin", executable);
|
||||
const targetStat = statSync(target, { throwIfNoEntry: false });
|
||||
const flatStat = statSync(flat, { throwIfNoEntry: false });
|
||||
// Release targets remain authoritative unless a newer source build matches
|
||||
// this host. In particular, a build-host flat artifact cannot mask a remotely
|
||||
// staged Mac target or force a local Cargo build.
|
||||
if (flatStat?.isFile() && (!targetStat?.isFile() || flatStat.mtimeMs > targetStat.mtimeMs)
|
||||
&& runnerBinaryTarget(readFileSync(flat)) === `${platform}-${architecture}`) {
|
||||
return { binary: flat, sourceBuild: true };
|
||||
|
||||
@@ -172,8 +172,6 @@ const env: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
PAPERCLIP_UI_DEV_MIDDLEWARE: explicitUiDevMiddleware ?? (serveBuiltUiForManagedRuntime ? "false" : "true"),
|
||||
};
|
||||
// An automatic source-build override must not hide subsequent Rust edits from
|
||||
// the watcher. Keep the operator's original choice separate from server env.
|
||||
const configuredRunnerBinary = env.PAPERCLIP_RUNNER_BINARY;
|
||||
|
||||
if (mode === "dev") {
|
||||
|
||||
@@ -11,8 +11,6 @@ export const GROK_PUBLIC_INSTALL_LIFECYCLE = [
|
||||
'npm', 'rebuild', '--offline', '--ignore-scripts=false', '--dangerously-allow-all-scripts',
|
||||
];
|
||||
|
||||
// setup-node provides its matching SDK alongside the executing Node binary.
|
||||
// Deferred lifecycle scripts may read that SDK, but may never download headers.
|
||||
export function macNodeHeadersDirectory(nodeExecutable, nodeVersion) {
|
||||
assert.match(nodeVersion, /^\d+\.\d+\.\d+$/, 'Executing Node must have an exact release version');
|
||||
const executable = realpathSync(nodeExecutable);
|
||||
@@ -213,7 +211,6 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
|
||||
`;
|
||||
}
|
||||
|
||||
/** Installed bytes and consumer resolution must survive host protocol startup. */
|
||||
export function assertInstalledNativeConsumerContinuity(before, after) {
|
||||
for (const field of ['lockSha256', 'runnerSha256', 'codexCommandSha256', 'codexNativeExecutableSha256']) {
|
||||
if (!/^[a-f0-9]{64}$/.test(before[field]) || before[field] !== after[field]) {
|
||||
@@ -222,7 +219,6 @@ export function assertInstalledNativeConsumerContinuity(before, after) {
|
||||
}
|
||||
}
|
||||
|
||||
/** The installed consumer must select its packaged host daemon, never PATH. */
|
||||
export function installedDaemonProbeSource(outputRoot, consumerRoot, sourceRevision) {
|
||||
return `
|
||||
import assert from 'node:assert/strict';
|
||||
@@ -267,8 +263,6 @@ export function installedDaemonProbeSource(outputRoot, consumerRoot, sourceRevis
|
||||
`;
|
||||
}
|
||||
|
||||
// Hosted, credential-free Mac consumers defer only npm lifecycle execution.
|
||||
// Offline npm flags are not an OS network boundary; sandbox-exec supplies it.
|
||||
export function macPublicInstallLifecycleProfile(ownedRoot) {
|
||||
if (!ownedRoot.startsWith('/') || ownedRoot.includes('\0')) throw new Error('Mac lifecycle requires an absolute owned root');
|
||||
return `(version 1)\n(allow default)\n(deny network*)\n(deny file-write* (require-all (require-not (subpath ${JSON.stringify(ownedRoot)})) (require-not (literal "/dev/null"))))\n`;
|
||||
|
||||
@@ -32,8 +32,6 @@ export async function verifyPiPackagedDaemon(server, daemon, target, runnerBinar
|
||||
assert.equal(`sha256:${createHash('sha256').update(bytes).digest('hex')}`, artifact.sha256);
|
||||
assert.equal(runnerBinaryTarget(bytes), target);
|
||||
} else {
|
||||
// Existing host-only source packages have no multi-target release manifest.
|
||||
// They must still use their exact normal flat daemon, never an override.
|
||||
assert.equal(await realpath(daemon), expectedPath);
|
||||
assert.equal(runnerBinaryTarget(await readFile(expectedPath)), target);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prepare the disposable GitHub ARM qualification host using stock distro files.
|
||||
# Extraction skips package maintainer scripts and their global policy changes.
|
||||
set -euo pipefail
|
||||
|
||||
if [[ "${GITHUB_ACTIONS:-}" != true || "${RUNNER_OS:-}" != Linux ]]; then
|
||||
@@ -87,7 +85,6 @@ test "$(od -An -tx1 -N4 "$stock_binary" | tr -d ' \n')" = 7f454c46
|
||||
readelf -h "$stock_binary" | tee "$prerequisite_report_dir/bwrap-elf-header.txt"
|
||||
grep -E 'Machine:.*AArch64' "$prerequisite_report_dir/bwrap-elf-header.txt"
|
||||
|
||||
# Reject every target symlink, including dangling links, before privileged copy.
|
||||
test ! -L /usr/bin/bwrap
|
||||
if test -e /usr/bin/bwrap; then
|
||||
test -f /usr/bin/bwrap
|
||||
|
||||
@@ -284,9 +284,6 @@ release_info ""
|
||||
release_info "==> Step 2/7: Building workspace artifacts..."
|
||||
cd "$REPO_ROOT"
|
||||
pnpm build
|
||||
# Hosted producers supply data-only, hash/source/architecture-bound daemon
|
||||
# receipts. Assemble after the build (which stages its host binary), before
|
||||
# public packages are packed. The server vendors this same compiled runner.
|
||||
if [ -n "${PAPERCLIP_RELEASE_RUNNER_ARTIFACTS_DIR:-}" ]; then
|
||||
pack_args=()
|
||||
if [ -n "${PAPERCLIP_RELEASE_PROVIDER_PACK_ARTIFACTS_DIR:-}" ]; then
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/usr/bin/env node
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
||||
import { createRequire } from "node:module";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptPath = fileURLToPath(import.meta.url);
|
||||
const defaultRoot = path.resolve(path.dirname(scriptPath), "..");
|
||||
const storyRoot = "ui/storybook/stories";
|
||||
const catalogPath = "maps/storybook-map/catalog.json";
|
||||
const indexPath = "maps/storybook-map/INDEX.md";
|
||||
const statuses = ["current", "proposed", "historical", "mixed", "unreviewed"];
|
||||
const implementations = ["production", "prototype", "mixed", "guide"];
|
||||
const boundaries = ["fixtures", "fixture-server", "static"];
|
||||
|
||||
function files(root, dir) {
|
||||
return readdirSync(path.join(root, dir), { withFileTypes: true }).flatMap(entry => {
|
||||
const name = `${dir}/${entry.name}`;
|
||||
return entry.isDirectory() ? files(root, name) : [name];
|
||||
}).sort();
|
||||
}
|
||||
|
||||
export function fingerprint(root, sources) {
|
||||
const hash = createHash("sha256");
|
||||
for (const file of [...new Set(sources)].sort()) {
|
||||
hash.update(file).update("\0").update(readFileSync(path.join(root, file))).update("\0");
|
||||
}
|
||||
return hash.digest("hex");
|
||||
}
|
||||
|
||||
export function capabilities(root) {
|
||||
const result = new Map();
|
||||
for (const file of files(root, "maps/feature-map").filter(file => file.endsWith(".md") && !file.endsWith("README.md"))) {
|
||||
const text = readFileSync(path.join(root, file), "utf8");
|
||||
const area = path.basename(file, ".md");
|
||||
const section = text.split(/^## Sub-features\s*$/m)[1]?.split(/^## /m)[0] ?? "";
|
||||
for (const match of section.matchAll(/^- `([^`]+)`:/gm)) result.set(`${area}/${match[1]}`, file);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export function featureNavigation(root) {
|
||||
const navigation = new Map();
|
||||
let section;
|
||||
for (const line of readFileSync(path.join(root, "maps/feature-map/README.md"), "utf8").split("\n")) {
|
||||
if (line.startsWith("### ")) section = line.slice(4);
|
||||
const match = line.match(/^\| \[([^\]]+)\]\(\.\/([^.)]+)\.md\)/);
|
||||
if (match && section) navigation.set(match[2], `${section}/${match[1]}`);
|
||||
}
|
||||
return navigation;
|
||||
}
|
||||
|
||||
export function scanStories(root = defaultRoot) {
|
||||
const require = createRequire(path.join(root, "ui/package.json"));
|
||||
const { loadCsf } = require("storybook/internal/csf-tools");
|
||||
return files(root, storyRoot).filter(file => /\.stories\.(ts|tsx|mdx)$/.test(file)).map(file => {
|
||||
const csf = loadCsf(readFileSync(path.join(root, file), "utf8"), { fileName: file, makeTitle: title => title }).parse();
|
||||
return { file, title: csf.meta.title, metaId: csf.meta.id, ids: csf.stories.map(story => story.id), plays: csf.stories.filter(story => story.__stats?.play).length };
|
||||
});
|
||||
}
|
||||
|
||||
export function validateCatalog(root, catalog, stories) {
|
||||
const errors = [];
|
||||
const refs = capabilities(root);
|
||||
const navigation = featureNavigation(root);
|
||||
const byFile = new Map(stories.map(story => [story.file, story]));
|
||||
const seen = new Set();
|
||||
const local = file => typeof file === "string" && !path.isAbsolute(file) && !file.split("/").includes("..") && existsSync(path.join(root, file));
|
||||
if (catalog.version !== 1 || !Array.isArray(catalog.entries)) return ["Unsupported catalog version or missing entries"];
|
||||
function review(label, sources, hash) {
|
||||
if (!Array.isArray(sources) || !sources.length || sources.some(file => !local(file))) {
|
||||
errors.push(`${label}: missing/invalid review sources`);
|
||||
} else if (fingerprint(root, sources) !== hash) errors.push(`${label}: source changed; review before updating its fingerprint`);
|
||||
}
|
||||
review("Shared Storybook setup", catalog.sharedReview?.sources, catalog.sharedReview?.fingerprint);
|
||||
for (const entry of catalog.entries) {
|
||||
const label = entry.file;
|
||||
if (seen.has(label)) errors.push(`${label}: duplicate catalog entry`);
|
||||
seen.add(label);
|
||||
if (!byFile.has(label)) errors.push(`${label}: missing story file`);
|
||||
if (!statuses.includes(entry.status)) errors.push(`${label}: invalid product status`);
|
||||
if (!implementations.includes(entry.implementation)) errors.push(`${label}: invalid implementation`);
|
||||
if (!boundaries.includes(entry.boundary)) errors.push(`${label}: invalid data boundary`);
|
||||
if (!entry.purpose?.trim() || !entry.notes?.trim()) errors.push(`${label}: purpose and review notes are required`);
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(entry.reviewedAt ?? "")) errors.push(`${label}: missing review date`);
|
||||
if (!Array.isArray(entry.capabilities)) { errors.push(`${label}: missing capabilities`); continue; }
|
||||
for (const ref of entry.capabilities) if (!refs.has(ref)) errors.push(`${label}: unknown feature capability ${ref}`);
|
||||
if (!entry.capabilities.length && !entry.exception?.trim()) errors.push(`${label}: unmapped story needs an explicit reason`);
|
||||
if (entry.capabilities.length !== new Set(entry.capabilities).size) errors.push(`${label}: duplicate capability references`);
|
||||
const area = entry.capabilities[0]?.split("/")[0] ?? (entry.implementation === "guide" ? "overview" : "foundations");
|
||||
const prefix = navigation.get(area) ?? ({ overview: "Overview", foundations: "Foundations" })[area];
|
||||
const story = byFile.get(label);
|
||||
if (!label.startsWith(`${storyRoot}/${area}/`)) errors.push(`${label}: story must live in its primary feature folder ${area}`);
|
||||
if (!prefix || !entry.sidebarTitle?.startsWith(`${prefix}/`) || story?.title !== entry.sidebarTitle) errors.push(`${label}: sidebar title must match its feature-map home`);
|
||||
if (!entry.stableId || story?.metaId !== entry.stableId) errors.push(`${label}: explicit meta ID must preserve the catalog stableId`);
|
||||
if (entry.replacement && !byFile.has(entry.replacement)) errors.push(`${label}: replacement story is missing`);
|
||||
if (!entry.sources?.includes(label)) errors.push(`${label}: review must include its story source`);
|
||||
review(label, entry.sources, entry.fingerprint);
|
||||
}
|
||||
for (const story of stories) if (!seen.has(story.file)) errors.push(`${story.file}: story missing from catalog`);
|
||||
const ids = stories.flatMap(story => story.ids);
|
||||
if (ids.length !== new Set(ids).size) errors.push("Duplicate story IDs");
|
||||
return errors;
|
||||
}
|
||||
|
||||
function cell(value) { return String(value).replaceAll("|", "\\|").replaceAll("\n", " "); }
|
||||
|
||||
export function renderIndex(catalog, stories, refs) {
|
||||
const byFile = new Map(stories.map(story => [story.file, story]));
|
||||
const groups = new Map();
|
||||
for (const entry of catalog.entries) {
|
||||
const area = entry.capabilities[0]?.split("/")[0] ?? "foundations-and-guides";
|
||||
if (!groups.has(area)) groups.set(area, []);
|
||||
groups.get(area).push(entry);
|
||||
}
|
||||
const lines = ["# Storybook by feature", "", "<!-- Generated by node scripts/storybook-catalog.mjs --write. Edit catalog.json. -->", "",
|
||||
"[Contract and maintenance](README.md) · [Review findings](REVIEW.md)", "",
|
||||
`${stories.length} story files; ${stories.reduce((n, story) => n + story.ids.length, 0)} indexed stories. Counts describe inventory, not passed tests or complete feature coverage.`, "",
|
||||
"Status describes product alignment; implementation describes the rendered subject; boundary describes simulated data. Sources and limitations are in catalog.json. Related capabilities cross-list one canonical story file.", ""];
|
||||
for (const [area, entries] of [...groups].sort(([a], [b]) => a.localeCompare(b))) {
|
||||
lines.push(`## ${area}`, "", ...(area !== "foundations-and-guides" ? [`[Feature recipe](../feature-map/${area}.md)`, ""] : []),
|
||||
"| Story suite | Capabilities | Product / implementation / data | Stories / play functions | Review note |",
|
||||
"| --- | --- | --- | --- | --- |");
|
||||
for (const entry of entries.sort((a, b) => a.file.localeCompare(b.file))) {
|
||||
const story = byFile.get(entry.file);
|
||||
lines.push(`| [${cell(story.title)}](../../${entry.file}) | ${entry.capabilities.map(ref => `\`${ref}\``).join(", ") || cell(entry.exception)} | ${entry.status} / ${entry.implementation} / ${entry.boundary} | ${story.ids.length} / ${story.plays} | ${cell(entry.notes)} |`);
|
||||
}
|
||||
lines.push("");
|
||||
}
|
||||
lines.push("## Capability lookup", "", "References are navigation links, not proof of parity. An absent link means no cataloged story, not a missing product feature. CLI/operator capabilities may need no Storybook example.", "");
|
||||
for (const ref of refs.keys()) {
|
||||
const entries = catalog.entries.filter(entry => entry.capabilities.includes(ref));
|
||||
lines.push(`- \`${ref}\`: ${entries.length ? entries.map(entry => `[${cell(byFile.get(entry.file).title)}](../../${entry.file}) (${entry.status})`).join(", ") : "No cataloged story."}`);
|
||||
}
|
||||
return lines.join("\n") + "\n";
|
||||
}
|
||||
|
||||
if (process.argv[1] && path.resolve(process.argv[1]) === scriptPath) {
|
||||
const args = process.argv.slice(2);
|
||||
if (args.some(arg => arg !== "--write")) throw new Error("Usage: node scripts/storybook-catalog.mjs [--write]");
|
||||
const catalog = JSON.parse(readFileSync(path.join(defaultRoot, catalogPath), "utf8"));
|
||||
const stories = scanStories();
|
||||
const errors = validateCatalog(defaultRoot, catalog, stories);
|
||||
if (!errors.length) {
|
||||
const generated = renderIndex(catalog, stories, capabilities(defaultRoot));
|
||||
if (args.includes("--write")) writeFileSync(path.join(defaultRoot, indexPath), generated);
|
||||
else if (!existsSync(path.join(defaultRoot, indexPath)) || readFileSync(path.join(defaultRoot, indexPath), "utf8") !== generated) errors.push("Generated INDEX.md is stale; run with --write after reviewing catalog changes");
|
||||
}
|
||||
if (errors.length) { errors.forEach(error => console.error(error)); process.exitCode = 1; }
|
||||
else console.log(`Storybook catalog aligned: ${stories.length} files, ${stories.reduce((n, story) => n + story.ids.length, 0)} stories. Structure and review fingerprints only; no runtime proof.`);
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, symlinkSync } from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { test } from "node:test";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { capabilities, fingerprint, renderIndex, scanStories, validateCatalog } from "./storybook-catalog.mjs";
|
||||
|
||||
function fixture(t) {
|
||||
const root = mkdtempSync(path.join(os.tmpdir(), "storybook-catalog-"));
|
||||
t.after(() => rmSync(root, { recursive: true, force: true }));
|
||||
function write(file, text) {
|
||||
mkdirSync(path.dirname(path.join(root, file)), { recursive: true });
|
||||
writeFileSync(path.join(root, file), text);
|
||||
}
|
||||
write("maps/feature-map/README.md", "# Features\n\n### Tasks and conversations\n\n| [Tasks](./tasks.md) | Tasks |\n");
|
||||
write("maps/feature-map/tasks.md", "# Tasks\n\n## Sub-features\n\n- `create`: create\n- `discussion`: discuss\n\n## How to get to it\n");
|
||||
write("ui/storybook/.storybook/preview.tsx", "// shared fixtures\n");
|
||||
const file = "ui/storybook/stories/tasks/task.stories.tsx";
|
||||
write(file, "// a story\n");
|
||||
write("ui/src/Task.tsx", "// production subject\n");
|
||||
const sharedSources = ["ui/storybook/.storybook/preview.tsx"];
|
||||
const sources = [file, "ui/src/Task.tsx"];
|
||||
const catalog = {
|
||||
version: 1, sharedReview: { sources: sharedSources, fingerprint: fingerprint(root, sharedSources) },
|
||||
entries: [{ file, stableId: "tasks-create", sidebarTitle: "Tasks and conversations/Tasks/Create", capabilities: ["tasks/create", "tasks/discussion"], purpose: "Create a task", status: "current",
|
||||
implementation: "production", boundary: "fixtures", notes: "Production subject; callbacks simulated.",
|
||||
reviewedAt: "2026-10-12", sources, fingerprint: fingerprint(root, sources) }],
|
||||
};
|
||||
const stories = [{ file, title: "Tasks and conversations/Tasks/Create", metaId: "tasks-create", ids: ["tasks-create--empty"], plays: 0 }];
|
||||
return { root, write, file, catalog, stories };
|
||||
}
|
||||
|
||||
test("complete inventory accepts production subjects with simulated data", t => {
|
||||
const f = fixture(t);
|
||||
assert.deepEqual(validateCatalog(f.root, f.catalog, f.stories), []);
|
||||
});
|
||||
|
||||
test("adding, removing or duplicating story files requires reconciliation", t => {
|
||||
const f = fixture(t);
|
||||
assert.match(validateCatalog(f.root, { ...f.catalog, entries: [] }, f.stories).join("\n"), /missing from catalog/);
|
||||
assert.match(validateCatalog(f.root, f.catalog, []).join("\n"), /missing story file/);
|
||||
f.catalog.entries.push(f.catalog.entries[0]);
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /duplicate catalog entry/);
|
||||
});
|
||||
|
||||
test("capability renames and unjustified unmapped stories fail", t => {
|
||||
const f = fixture(t);
|
||||
f.catalog.entries[0].capabilities = ["tasks/old-name"];
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /unknown feature capability/);
|
||||
f.catalog.entries[0].capabilities = [];
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /explicit reason/);
|
||||
f.catalog.entries[0].exception = "Shared foundation";
|
||||
const file = "ui/storybook/stories/foundations/task.stories.tsx";
|
||||
f.write(file, "export default {};");
|
||||
const entry = f.catalog.entries[0];
|
||||
entry.file = file;
|
||||
entry.sources = [file, "ui/src/Task.tsx"];
|
||||
entry.fingerprint = fingerprint(f.root, entry.sources);
|
||||
entry.sidebarTitle = "Foundations/Task";
|
||||
f.stories[0].file = file;
|
||||
f.stories[0].title = entry.sidebarTitle;
|
||||
assert.deepEqual(validateCatalog(f.root, f.catalog, f.stories), []);
|
||||
});
|
||||
|
||||
test("feature organization rejects wrong folders, sidebar homes and unstable meta IDs", t => {
|
||||
const f = fixture(t);
|
||||
f.catalog.entries[0].capabilities = ["tasks/discussion"];
|
||||
assert.deepEqual(validateCatalog(f.root, f.catalog, f.stories), []);
|
||||
f.stories[0].metaId = "renamed-task";
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /explicit meta ID/);
|
||||
f.stories[0].metaId = "tasks-create";
|
||||
f.stories[0].title = "Unrelated/Task";
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /sidebar title/);
|
||||
f.stories[0].title = f.catalog.entries[0].sidebarTitle;
|
||||
const originalFile = f.catalog.entries[0].file;
|
||||
f.catalog.entries[0].file = "ui/storybook/stories/foundations/task.stories.tsx";
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /primary feature folder tasks/);
|
||||
f.catalog.entries[0].file = originalFile;
|
||||
f.write("maps/feature-map/README.md", "### Conversations\n| [Tasks](./tasks.md) | Tasks |\n");
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /sidebar title/);
|
||||
});
|
||||
|
||||
test("production and shared fixture drift invalidate review fingerprints", t => {
|
||||
const f = fixture(t);
|
||||
f.write("ui/src/Task.tsx", "// changed contract\n");
|
||||
f.write("ui/storybook/.storybook/preview.tsx", "// changed shared mocks\n");
|
||||
const errors = validateCatalog(f.root, f.catalog, f.stories);
|
||||
assert.equal(errors.filter(error => error.includes("source changed")).length, 2);
|
||||
assert.equal(f.catalog.entries[0].fingerprint.length, 64);
|
||||
});
|
||||
|
||||
test("missing and escaping source references cannot certify a review", t => {
|
||||
const f = fixture(t);
|
||||
f.catalog.entries[0].sources.push("../outside.ts");
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /invalid review sources/);
|
||||
});
|
||||
|
||||
test("cross-capability lookup retains one canonical file and honest status", t => {
|
||||
const f = fixture(t);
|
||||
f.catalog.entries[0].status = "historical";
|
||||
const index = renderIndex(f.catalog, f.stories, capabilities(f.root));
|
||||
assert.match(index, /tasks\/create.*\(historical\)/);
|
||||
assert.match(index, /tasks\/discussion.*\(historical\)/);
|
||||
assert.match(index, /1 story files; 1 indexed stories/);
|
||||
assert.match(index, /historical \/ production \/ fixtures/);
|
||||
});
|
||||
|
||||
test("duplicate story IDs cannot silently collapse distinct examples", t => {
|
||||
const f = fixture(t);
|
||||
f.stories[0].ids.push(f.stories[0].ids[0]);
|
||||
assert.match(validateCatalog(f.root, f.catalog, f.stories).join("\n"), /Duplicate story IDs/);
|
||||
});
|
||||
|
||||
test("Storybook parser discovers nested files, filters helpers, preserves explicit IDs and counts play functions", t => {
|
||||
const f = fixture(t);
|
||||
f.write("ui/package.json", '{"type":"module"}');
|
||||
symlinkSync(fileURLToPath(new URL("../ui/node_modules", import.meta.url)), path.join(f.root, "ui/node_modules"), "junction");
|
||||
f.write(f.file, `
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
const meta = { title: "Tasks/Create", id: "stable-task", excludeStories: ["empty"] } satisfies Meta;
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
export const empty = { value: "fixture" };
|
||||
export const Empty: Story = {};
|
||||
export const Filled: Story = { play: async () => {} };
|
||||
`);
|
||||
f.write("ui/storybook/stories/nested/other.stories.ts", `
|
||||
export default { title: "Other/Subject", includeStories: ["Included"] };
|
||||
export const helper = {};
|
||||
export const Included = {};
|
||||
`);
|
||||
f.write("ui/storybook/stories/nested/fixture.ts", "export const fixture = {};");
|
||||
const stories = scanStories(f.root);
|
||||
assert.equal(stories.length, 2);
|
||||
assert.deepEqual(stories.find(story => story.file === f.file), {
|
||||
file: f.file, title: "Tasks/Create", metaId: "stable-task", ids: ["stable-task--empty", "stable-task--filled"], plays: 1,
|
||||
});
|
||||
assert.deepEqual(stories.find(story => story.file.endsWith("other.stories.ts")), {
|
||||
file: "ui/storybook/stories/nested/other.stories.ts", title: "Other/Subject", metaId: undefined, ids: ["other-subject--included"], plays: 0,
|
||||
});
|
||||
});
|
||||
@@ -36,8 +36,6 @@ async function verifyMacConsumer(packageDirectory, sourceRevision, receiptPath)
|
||||
for (const directory of [consumer, home, tools, join(root, 'tmp'), join(root, 'cache')]) mkdirSync(directory);
|
||||
const npmCli = realpathSync(execFileSync('/usr/bin/which', ['npm'], { encoding: 'utf8' }).trim());
|
||||
symlinkSync(process.execPath, join(tools, 'node'));
|
||||
// npm subprocesses get a Node-only owned tool directory; no ambient harness
|
||||
// command can hide missing installed dependencies.
|
||||
const env = { PATH: `${tools}:/usr/bin:/bin:/usr/sbin:/sbin`, HOME: home, TMPDIR: join(root, 'tmp'),
|
||||
npm_config_cache: join(root, 'cache'), npm_config_audit: 'false', npm_config_fund: 'false' };
|
||||
const run = (command, args, cwd = consumer, runEnv = env) => execFileSync(command, args, { cwd, env: runEnv,
|
||||
@@ -148,13 +146,8 @@ async function verifyLinuxArmConsumer(packageDirectory, sourceRevision, receiptP
|
||||
const daemon = probe('daemon-probe.mjs', installedDaemonProbeSource(installed, '/consumer', sourceRevision));
|
||||
assert.equal(daemon.target, 'linux-arm64');
|
||||
const codex = probe('codex-probe.mjs', installedCodexProbeSource(installed + '/index.js', '/consumer', undefined, { nativeCodex: true }));
|
||||
// Reuse the existing zero-turn hook as read-only verifier source. All
|
||||
// production imports still resolve from the installed consumer graph.
|
||||
cpSync(join(repo, 'tests/runner-e2e/installed-cli.ts'), join(assets, 'installed-cli.ts'));
|
||||
writeFileSync(join(assets, 'types.js'), stripTypeScriptTypes(readFileSync(join(repo, 'tests/runner-e2e/types.ts'), 'utf8')));
|
||||
// The restricted install container deliberately forbids nested namespaces.
|
||||
// Keep its lifecycle/no-network proof, then test the exact same bind-mounted
|
||||
// graph on the native host using Codex's normal read-only namespace policy.
|
||||
const home = join(root, 'host-home'), hostTmp = join(root, 'host-tmp');
|
||||
for (const directory of [home, hostTmp]) mkdirSync(directory, { mode: 0o700 });
|
||||
const hostEnv = { PATH: `${dirname(process.execPath)}:/usr/bin:/bin:/usr/sbin:/sbin`, HOME: home, TMPDIR: hostTmp, LC_ALL: 'C', TZ: 'UTC' };
|
||||
@@ -332,9 +325,6 @@ try {
|
||||
timing.stage_pack_ms = Math.round(performance.now() - packStarted);
|
||||
Object.assign(timing, packTimings(packs, concurrency));
|
||||
|
||||
// Preserve the exact Linux-produced public tarballs for independent Mac
|
||||
// consumers even if a later Linux runtime probe fails. This is a packaging
|
||||
// receipt, never an installation or provider-admission pass.
|
||||
if (process.env.PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR) {
|
||||
const output = resolve(process.env.PAPERCLIP_RUNNER_PUBLIC_PACKS_DIR);
|
||||
assert.equal(JSON.parse(readFileSync(join(repo, 'server/dist/build-info.json'))).commit, sourceRevision);
|
||||
|
||||
Reference in new issue
Block a user