mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
30e41c23f3
* fix(#2679): sync statusline-generator with committed .claude/helpers/statusline.cjs Closes #2679. Replaces the 946-line inline template string in generateStatuslineScript() with a read-committed-helper-and-substitute pattern. Single source of truth = the shipped .claude/helpers/statusline.cjs. ## Root cause of the drift The generator's inline template shipped as the pre-#2195 non-delegation build. Meanwhile the deployed helper got continuous v3.29.0 improvements (whole-row-clickable OSC 8, (domain) suffix, ellipsis on truncation, bright-white bold command styling, 300s cache TTL, windowsHide on every subprocess spawn). No mechanism kept them in sync — every `ruflo init` regressed a user's install to the older shape. ## Fix generateStatuslineScript() now: 1. Walks up from `__dirname_sg` looking for `@claude-flow/cli`'s package.json. Reads `.claude/helpers/statusline.cjs` relative to that. Falls back to `createRequire.resolve` for the installed-CLI case where walk-up misses (shipped tarball extracted outside a workspace). 2. Substitutes two known values: - `maxAgents: 15,` → `maxAgents: <options.runtime.maxAgents>,` - `let ver = "…";` → `let ver = <bakedVersion>;` (with non-downgrade guard so test environments that resolve to an older node_modules install can't clobber a fresh committed helper) Function body: ~1050 lines → ~60 lines. Every future edit to the helper now propagates to init output automatically. ## Also synced (was silently drifted) `v3/@claude-flow/cli/.claude/helpers/statusline.cjs` was a stale pre-v3.29.0 copy. Copied the root `.claude/helpers/statusline.cjs` (the current v3.29.0 shape) over the top. Re-signed helpers manifest. ## Test updates Three test assertions updated to match the current v3.29.0 helper shape: 1. `statusline-cost-display.test.ts` drift guard — restored to strict byte-comparison after normalizing the version line (the ONE legitimately environment-dependent line — vitest resolves to a different pnpm-installed CLI than production). 2. `funnel.test.ts` "re-sanitizes promo text" — was asserting the old `.slice(0, 100)` truncation; now asserts the v3.29.0 MAX_LEN + ellipsis path. 3. `funnel.test.ts` "styles the label as a link" — was asserting the old `UL_ON + safeTerminalLink(label, promo.url) + UL_OFF` label-only OSC 8 wrap; now asserts the v3.29.0 whole-row wrap via `wrapWholeRowInHyperlink` + the bright-white bold command styling. ## Verified - Local: 135/135 pass across funnel.test.ts + statusline-cost-display.test.ts. - TypeScript build clean. - Direct node call: generator produces exact byte-for-byte match with committed helper (modulo the substituted maxAgents + version). ## Follow-ups tracked None blocking. The `wrapWholeRowInHyperlink` name assertion pins one implementation detail — worth revisiting if we ever refactor that path. Related: v3.30.0 release (74bc81f8e/fea1d9e95). Co-Authored-By: RuFlo <ruv@ruv.net> * ci: update statusline-delegation guards for #2679 read-and-substitute pattern Two static-regression CI guards greppedthe GENERATOR source for strings that #2679 moved into the HELPER file (via the read-and-substitute refactor). Both now correctly grep the helper — with a paired invariant that the generator still reads it (so drift on either side is caught). ## Changes ### .github/workflows/v3-ci.yml — statusline generator delegation smoke (#2195) - \`grep 'hooks statusline --json'\` → grep the HELPER, not the generator - \`grep 'v3/docs/adr'\` → grep the HELPER - \`grep 'v3/implementation/adrs'\` → grep the HELPER - \`grep 'getLearningStats' | 'getV3Progress'\` (negative) → grep the HELPER - NEW paired assertion: generator must still reference \`statusline.cjs\` AND use \`readFileSync\` for helperContent (so the read-and-substitute pattern itself can't silently regress) ### scripts/audit-fix-invariants.mjs — #2196 + #1951 invariants - #2196 delegation invariant: moved from generator to helper file - #2679 NEW invariant: generator must reference \`statusline.cjs\` (proves the sync pattern is intact) - #1951 marketplaces/ruflo generator invariant: removed (candidate lives in the helper file; already covered by the two helper invariants above) ## Verified - \`node scripts/audit-fix-invariants.mjs\` → 37/37 invariants present, no missing - The delegation contract is now enforced by TWO checks: (1) the helper contains it, (2) the generator reads the helper — both must pass, catching drift on either side Related: #2679 (the refactor these guards now match), v3.30.0 release. Co-Authored-By: RuFlo <ruv@ruv.net> * ci: update statusline delegation smoke for #2679 sync pattern Third and final CI guard to update after #2679's read-and-substitute refactor. The smoke script's static-source contract greps the generator for content that now lives in the helper file. ## Change Split the Layer 1 static-source contract across TWO files: 1. **Generator** must still reference `statusline.cjs` AND use `readFileSync` for helperContent — proves the sync mechanism is intact. 2. **Helper** must carry the delegation contract: - `hooks statusline --json` present - No `getLearningStats` / `getV3Progress` (old local readers) - Both `v3/docs/adr` and `v3/implementation/adrs` present for ADR counting Splitting the invariant across the two files means neither can silently regress — a future refactor that breaks the generator's read OR strips the delegation from the helper will fail this smoke. ## Verified `node scripts/smoke-statusline-generator-delegation.mjs` → 20/20 pass. Includes all three layers: - [1/3] Static source contract (updated for #2679) - [2/3] Generated .cjs syntax check (unchanged — validates node --check on generator output) - [3/3] Runtime JSON range validation (unchanged — runs the .cjs and checks output fields) Related: PR #2680, issue #2679. Co-Authored-By: RuFlo <ruv@ruv.net>
239 lines
9.8 KiB
JavaScript
239 lines
9.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Regression guard for ruvnet/ruflo#2195.
|
|
*
|
|
* The statusline generator previously re-implemented all data readers
|
|
* locally with fragile file probes that returned wrong values:
|
|
* - DDD: 0/5 (AgentDB has 26k+ patterns → should be 5/5)
|
|
* - Intelligence: 1% (healthy system → should be 100%)
|
|
* - ADR count: 87/87 (missed v3/docs/adr/ → should be 128)
|
|
* - Vectors: 22 (read session-imports, not AgentDB total)
|
|
*
|
|
* The fix delegates to 'npx @claude-flow/cli@latest hooks statusline --json'
|
|
* as the single source of truth and counts ADRs in both directories.
|
|
*
|
|
* This smoke verifies:
|
|
* [1/3] Generator emits a .cjs that calls 'hooks statusline --json'
|
|
* [2/3] Generated .cjs runs without syntax errors (node --check)
|
|
* [3/3] Generated .cjs JSON output fields are in valid ranges
|
|
*/
|
|
|
|
import { execSync, execFileSync } from 'node:child_process';
|
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
import { resolve, join } from 'node:path';
|
|
import { tmpdir, homedir } from 'node:os';
|
|
|
|
const REPO_ROOT = resolve(process.cwd());
|
|
const GENERATOR_SRC = join(REPO_ROOT, 'v3/@claude-flow/cli/src/init/statusline-generator.ts');
|
|
const GENERATOR_DIST = join(REPO_ROOT, 'v3/@claude-flow/cli/dist/src/init/statusline-generator.js');
|
|
// #2679: generator now reads .claude/helpers/statusline.cjs as its single
|
|
// source of truth (read-and-substitute pattern) instead of inlining a
|
|
// 1000-line template. Static-content contract lives in the HELPER now;
|
|
// the generator just needs to still be reading it. This constant is the
|
|
// source-of-truth helper used by the static-source contract below.
|
|
const HELPER_SRC = join(REPO_ROOT, 'v3/@claude-flow/cli/.claude/helpers/statusline.cjs');
|
|
const CJS_PATH = join(tmpdir(), 'ruflo-smoke-statusline.cjs');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function pass(msg) { console.log(` ok: ${msg}`); passed++; }
|
|
function fail(msg) { console.error(` FAIL: ${msg}`); failed++; }
|
|
|
|
// ─── Layer 1: static source contract ────────────────────────────
|
|
console.log('\n[1/3] STATIC SOURCE CONTRACT');
|
|
|
|
// #2679: two paired checks — the HELPER carries the delegation content
|
|
// (since the generator now reads it verbatim), and the GENERATOR still
|
|
// reads the helper (so drift on either side is caught). Splitting the
|
|
// invariant across the two files means neither can silently regress.
|
|
if (!existsSync(GENERATOR_SRC)) {
|
|
fail(`generator source not found: ${GENERATOR_SRC}`);
|
|
} else {
|
|
const genSrc = readFileSync(GENERATOR_SRC, 'utf-8');
|
|
|
|
if (genSrc.includes('statusline.cjs')) {
|
|
pass('generator references statusline.cjs (#2679 read-and-substitute pattern)');
|
|
} else {
|
|
fail('generator does NOT reference statusline.cjs — #2679 sync pattern regressed; init would emit stale template');
|
|
}
|
|
|
|
if (/readFileSync[\s\S]{0,200}helperContent|helperContent[\s\S]{0,200}readFileSync/.test(genSrc)) {
|
|
pass('generator reads helper via readFileSync (#2679)');
|
|
} else {
|
|
fail('generator no longer reads helper via readFileSync — #2679 sync mechanism broken');
|
|
}
|
|
}
|
|
|
|
if (!existsSync(HELPER_SRC)) {
|
|
fail(`helper source not found: ${HELPER_SRC}`);
|
|
} else {
|
|
const src = readFileSync(HELPER_SRC, 'utf-8');
|
|
|
|
if (src.includes('hooks statusline --json')) {
|
|
pass('helper delegates to hooks statusline --json');
|
|
} else {
|
|
fail('helper does NOT contain delegation to hooks statusline --json (regression of #2195)');
|
|
}
|
|
|
|
if (!src.includes('getLearningStats')) {
|
|
pass('getLearningStats (old local reader) removed from helper');
|
|
} else {
|
|
fail('getLearningStats still present in helper — old fragile local reader NOT removed (#2195)');
|
|
}
|
|
|
|
if (!src.includes('getV3Progress')) {
|
|
pass('getV3Progress (old local reader) removed from helper');
|
|
} else {
|
|
fail('getV3Progress still present in helper — old fragile local reader NOT removed (#2195)');
|
|
}
|
|
|
|
if (src.includes("'v3', 'docs', 'adr'") || src.includes("v3/docs/adr")) {
|
|
pass('helper counts v3/docs/adr ADR directory');
|
|
} else {
|
|
fail('helper missing v3/docs/adr in ADR count — will undercount ADRs (#2195)');
|
|
}
|
|
|
|
if (src.includes("'v3', 'implementation', 'adrs'") || src.includes("v3/implementation/adrs")) {
|
|
pass('helper counts v3/implementation/adrs ADR directory');
|
|
} else {
|
|
fail('helper missing v3/implementation/adrs in ADR count');
|
|
}
|
|
}
|
|
|
|
// ─── Layer 2: generated .cjs syntax check ───────────────────────
|
|
console.log('\n[2/3] GENERATED .CJS SYNTAX CHECK');
|
|
|
|
if (!existsSync(GENERATOR_DIST)) {
|
|
console.log(' skip: dist not built (expected in static-scan-only environments)');
|
|
} else {
|
|
try {
|
|
// Generate the .cjs
|
|
const genScript = `
|
|
const { generateStatuslineScript } = require(${JSON.stringify(GENERATOR_DIST)});
|
|
const content = generateStatuslineScript({ runtime: { maxAgents: 15 }, statusline: { enabled: true } });
|
|
require('fs').writeFileSync(${JSON.stringify(CJS_PATH)}, content, 'utf-8');
|
|
process.stdout.write('Generated ' + content.length + ' chars\\n');
|
|
`;
|
|
execSync(`node -e "${genScript.replace(/"/g, '\\"').replace(/\n/g, ' ')}"`, {
|
|
encoding: 'utf-8',
|
|
timeout: 10000,
|
|
cwd: REPO_ROOT,
|
|
});
|
|
|
|
if (existsSync(CJS_PATH)) {
|
|
pass(`generator wrote ${CJS_PATH}`);
|
|
|
|
// Syntax check only (--check flag, no execution)
|
|
try {
|
|
execSync(`node --check "${CJS_PATH}"`, { encoding: 'utf-8', timeout: 5000 });
|
|
pass('generated .cjs passes Node.js syntax check');
|
|
} catch (err) {
|
|
fail(`generated .cjs has syntax errors: ${err.message.split('\n')[0]}`);
|
|
}
|
|
|
|
// Verify delegation pattern in generated .cjs
|
|
const cjsContent = readFileSync(CJS_PATH, 'utf-8');
|
|
if (cjsContent.includes('hooks statusline --json')) {
|
|
pass('generated .cjs contains delegation to hooks statusline --json');
|
|
} else {
|
|
fail('generated .cjs does NOT delegate to hooks statusline --json — generator output is wrong');
|
|
}
|
|
|
|
if (cjsContent.includes('v3/docs/adr')) {
|
|
pass('generated .cjs includes v3/docs/adr in ADR count');
|
|
} else {
|
|
fail('generated .cjs missing v3/docs/adr in ADR count');
|
|
}
|
|
|
|
} else {
|
|
fail('generator did not write CJS file');
|
|
}
|
|
} catch (err) {
|
|
fail(`generator script threw: ${err.message.split('\n')[0]}`);
|
|
}
|
|
}
|
|
|
|
// ─── Layer 3: runtime JSON range validation ──────────────────────
|
|
console.log('\n[3/3] RUNTIME JSON RANGE VALIDATION');
|
|
|
|
if (!existsSync(CJS_PATH)) {
|
|
console.log(' skip: .cjs not available (dist not built)');
|
|
} else {
|
|
try {
|
|
// Run the .cjs with --json, expect valid JSON output
|
|
// The CLI delegation requires npx which may not be available in CI without network.
|
|
// So we only check: valid JSON, numeric fields in range, no syntax/runtime error.
|
|
const raw = execSync(`node "${CJS_PATH}" --json`, {
|
|
encoding: 'utf-8',
|
|
timeout: 15000,
|
|
cwd: REPO_ROOT,
|
|
env: { ...process.env, PATH: process.env.PATH },
|
|
}).trim();
|
|
|
|
// Find first '{' in case there's preamble
|
|
const jsonStart = raw.indexOf('{');
|
|
if (jsonStart === -1) {
|
|
fail('--json output contains no JSON object');
|
|
} else {
|
|
try {
|
|
const data = JSON.parse(raw.slice(jsonStart));
|
|
pass('--json output is valid JSON');
|
|
|
|
// Field range checks (must be present and numeric)
|
|
const checks = [
|
|
['v3Progress.domainsCompleted', data.v3Progress?.domainsCompleted, 0, 10],
|
|
['v3Progress.totalDomains', data.v3Progress?.totalDomains, 1, 20],
|
|
['v3Progress.dddProgress', data.v3Progress?.dddProgress, 0, 100],
|
|
['system.intelligencePct', data.system?.intelligencePct, 0, 100],
|
|
['system.memoryMB', data.system?.memoryMB, 0, 100000],
|
|
['swarm.maxAgents', data.swarm?.maxAgents, 1, 1000],
|
|
['adrs.count', data.adrs?.count, 0, 10000],
|
|
];
|
|
|
|
for (const [name, value, min, max] of checks) {
|
|
if (typeof value !== 'number') {
|
|
fail(`${name} is not a number (got ${typeof value}: ${value})`);
|
|
} else if (value < min || value > max) {
|
|
fail(`${name} = ${value} out of range [${min}, ${max}]`);
|
|
} else {
|
|
pass(`${name} = ${value} (in range [${min}, ${max}])`);
|
|
}
|
|
}
|
|
|
|
// Verify ADR count includes both dirs (CI has the git checkout, both
|
|
// directories exist, so we should see count > 87 on a full checkout)
|
|
if (data.adrs?.count > 87) {
|
|
pass(`adrs.count ${data.adrs.count} > 87, confirming both ADR directories counted`);
|
|
} else if (data.adrs?.count === 0) {
|
|
pass('adrs.count = 0 (acceptable in minimal checkout without ADR dirs)');
|
|
} else {
|
|
// count in [1, 87] — acceptable in a sparse checkout but worth noting
|
|
pass(`adrs.count = ${data.adrs?.count} (single ADR directory only — acceptable in sparse checkout)`);
|
|
}
|
|
|
|
} catch (e) {
|
|
fail(`JSON parse error: ${e.message}`);
|
|
}
|
|
}
|
|
} catch (err) {
|
|
// If the CLI delegation timed out (network unavailable in CI), the .cjs
|
|
// falls back to buildLocalFallback() which returns all-zeros but valid JSON.
|
|
// That's acceptable — the important thing is no crash.
|
|
if (err.status === 0 || err.stdout) {
|
|
pass('--json ran without crash (fallback path ok)');
|
|
} else {
|
|
// A non-zero exit with no stdout means the script itself crashed.
|
|
fail(`--json crashed: ${err.message?.split('\n')[0] || 'unknown error'}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ─── Result ───────────────────────────────────────────────────────
|
|
console.log(`\n${'─'.repeat(60)}`);
|
|
console.log(`statusline-generator-delegation smoke: ${passed} passed, ${failed} failed`);
|
|
|
|
if (failed > 0) {
|
|
process.exit(1);
|
|
}
|