@@ -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
|
||||
|
||||
@@ -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}]
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
45
tests/enh-1559-installer-export-audit.test.cjs
Normal file
45
tests/enh-1559-installer-export-audit.test.cjs
Normal 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`);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -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)`,
|
||||
);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user