refactor(#1559): audit installer compatibility exports (#1565)

This commit is contained in:
Tom Boucher
2026-06-22 00:38:55 -04:00
committed by GitHub
parent 0224f5bcf3
commit a570cd049c
5 changed files with 69 additions and 56 deletions

View File

@@ -12048,7 +12048,10 @@ module.exports = {
// #1191 — exported so tests exercise the REAL readSettings, not a replica
readSettings,
stripJsonComments,
...runtimeArtifactConversion,
// Compatibility relays retained after auditing the former broad
// runtimeArtifactConversion spread (#1559).
processAttribution,
applyRuntimeContentRewritesForCommandsInPlace,
};
// Main logic — only run when not loaded as a module for testing

View File

@@ -56,10 +56,14 @@ function extractFrontmatter(content: string): Frontmatter {
const frontmatter: Frontmatter = {};
// Match frontmatter only at byte 0 — a `---` block later in the document
// body (YAML examples, horizontal rules) must never be treated as frontmatter.
const match = content.match(/^---\r?\n([\s\S]+?)\r?\n---/);
if (!match) return frontmatter;
const headerEnd = content.startsWith('---\r\n') ? 5 : content.startsWith('---\n') ? 4 : -1;
if (headerEnd === -1) return frontmatter;
const yaml = match[1];
const closingLineStart = content.indexOf('\n---', headerEnd);
if (closingLineStart === -1) return frontmatter;
const yamlEnd = content[closingLineStart - 1] === '\r' ? closingLineStart - 1 : closingLineStart;
const yaml = content.slice(headerEnd, yamlEnd);
const lines = yaml.split(/\r?\n/);
// Stack to track nested objects: [{obj, key, indent}]

View File

@@ -101,10 +101,8 @@ describe('processAttribution (relocated to runtime-artifact-conversion)', () =>
});
test('bin/install.js re-exports the SAME processAttribution reference (no drift)', () => {
// processAttribution flows into install.js's exports via the
// ...runtimeArtifactConversion spread, so the installer's processAttribution
// must be the conversion module's single implementation (the local copy is
// deleted; install.js binds it for its internal callers).
// processAttribution remains an explicit installer compatibility relay, so
// the export must keep pointing at the conversion module's implementation.
assert.strictEqual(installer.processAttribution, conversion.processAttribution);
});
});

View File

@@ -0,0 +1,45 @@
'use strict';
const { describe, test, before } = require('node:test');
const assert = require('node:assert/strict');
let installer;
let conversion;
before(() => {
process.env['GSD_TEST_MODE'] = '1';
installer = require('../bin/install.js');
conversion = require('../gsd-core/bin/lib/runtime-artifact-conversion.cjs');
});
describe('bin/install.js compatibility export audit (#1559)', () => {
test('retains audited compatibility relays for shared rewrite helpers', () => {
assert.strictEqual(installer.processAttribution, conversion.processAttribution);
assert.strictEqual(
installer.applyRuntimeContentRewritesForCommandsInPlace,
conversion.applyRuntimeContentRewritesForCommandsInPlace,
);
});
test('does not leak unaudited conversion-module helpers through the installer', () => {
for (const name of [
'yamlQuote',
'toSingleLine',
'extractFrontmatterAndBody',
'extractFrontmatterField',
'convertClaudeToCursorMarkdown',
'convertClaudeToCodexMarkdown',
'transformContentToHyphen',
'claudeToGeminiTools',
'convertGeminiToolName',
'rewriteStagedSkillBodies',
'rewriteStagedCommandBodies',
'_computePathPrefix',
'_stampNonClaudeRuntimeDefaults',
'NON_CLAUDE_RUNTIMES',
]) {
assert.ok(name in conversion, `${name} remains available from the conversion module`);
assert.equal(installer[name], undefined, `${name} is not an installer compatibility export`);
}
});
});

View File

@@ -116,20 +116,11 @@ test('extractFrontmatter is total over 500 deterministic random inputs (seed=123
}
});
test('extractFrontmatter scales sub-quadratically (complexity ratio guard)', () => {
// Rationale: an absolute wall-clock bound (e.g. < 2000 ms) is flaky —
// it fails on slow CI machines and passes on a fast local box even when
// a quadratic regression has been introduced. A *ratio* test is
// self-calibrating: we measure how much longer the parser takes on a
// 10x-larger input (by line count). For an O(n) parser the ratio should
// be near 10; for an O(n^2) parser it would be near 100. We tolerate
// up to 60x to give ample room for JIT, GC, constant-factor differences,
// and measurement noise — yet a true quadratic regression (ratio ~100)
// will still be caught.
//
// Input shape: pure key:value lines so the line count directly controls
// the amount of work the parser does per call. No randomness needed here
// — the property being tested is complexity, not totality.
test('extractFrontmatter handles large frontmatter blocks without body bleed', () => {
// Deterministic large-input coverage replaces the former wall-clock ratio
// guard. Timing assertions are host-sensitive; this pins the parser contract
// instead: parse every frontmatter line once and stop at the first closing
// delimiter before the body.
/** Build a frontmatter string with exactly `lineCount` key:value lines. */
function buildScaleInput(lineCount) {
@@ -140,39 +131,11 @@ test('extractFrontmatter scales sub-quadratically (complexity ratio guard)', ()
return s + '---\nBody.\n';
}
const SMALL_LINES = 20;
const LARGE_LINES = 200; // 10x more lines than SMALL_LINES
const SIZE_RATIO = LARGE_LINES / SMALL_LINES; // 10
const REPS = 3000; // enough iterations for hrtime to produce stable ns totals
const MAX_RATIO = SIZE_RATIO * 6; // 60 — well above O(n) (10) but well below O(n^2) (100)
const smallInput = buildScaleInput(SMALL_LINES);
const largeInput = buildScaleInput(LARGE_LINES);
// Warmup: let V8 JIT-compile the hot path before we measure.
for (let i = 0; i < 300; i++) {
extractFrontmatter(smallInput);
extractFrontmatter(largeInput);
for (const lineCount of [20, 200, 2000]) {
const result = extractFrontmatter(buildScaleInput(lineCount) + 'body_key: not-frontmatter\n');
assert.equal(Object.keys(result).length, lineCount);
assert.equal(result.key0, 'value0');
assert.equal(result[`key${lineCount - 1}`], `value${lineCount - 1}`);
assert.equal(result.body_key, undefined);
}
const t1 = process.hrtime.bigint();
for (let i = 0; i < REPS; i++) extractFrontmatter(smallInput);
const dSmall = Number(process.hrtime.bigint() - t1);
const t2 = process.hrtime.bigint();
for (let i = 0; i < REPS; i++) extractFrontmatter(largeInput);
const dLarge = Number(process.hrtime.bigint() - t2);
// Guard against a degenerate measurement (< 1 µs total) that would
// make the ratio meaningless. If the machine is this fast, the parser
// is trivially fine and we skip the ratio check.
if (dSmall < 1000 /* 1 µs */) return;
const ratio = dLarge / dSmall;
assert.ok(
ratio < MAX_RATIO,
`complexity ratio ${ratio.toFixed(1)} exceeds ${MAX_RATIO} ` +
`(${LARGE_LINES}-line input took ${(ratio).toFixed(1)}x longer than ${SMALL_LINES}-line input; ` +
`expected ≤ ${MAX_RATIO}x for sub-quadratic behaviour — possible O(n²) regression)`,
);
});