Files
pulse/scripts/tests/check-frontend-serialization.test.mjs
pulse-triage[bot] 1691d91bf8 Run serialization graph regression controls in the pre-install CI check
Keep the negative, partial-repair and nested-resolution controls beside the actual locked-graph check so future edits cannot silently disable the intended safeguard.

Contract-Neutral: Run the existing serialization regression suite in its pre-install CI check; no additional contract or user-visible runtime change.
Change-source: pulse-maintainer
2026-10-14 01:54:51 +01:00

135 lines
6.1 KiB
JavaScript

import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { test } from 'node:test';
import { serializationGraphErrors } from '../check-frontend-serialization.mjs';
function graph(solid = '1.9.16', range = '~1.6.8', seroval = '1.6.8', plugins = '1.6.8') {
return {
lockfileVersion: 3,
packages: {
'node_modules/solid-js': { version: solid, dependencies: { seroval: range, 'seroval-plugins': range } },
'node_modules/seroval': { version: seroval },
'node_modules/seroval-plugins': { version: plugins },
},
};
}
test('current complete lock preserves the qualified security override', () => {
const lock = JSON.parse(readFileSync(new URL('../../frontend-modern/package-lock.json', import.meta.url), 'utf8'));
assert.deepEqual(serializationGraphErrors(lock), []);
});
test('the exact old qualified tuple is not mistaken for a broken graph', () => {
assert.deepEqual(serializationGraphErrors(graph('1.9.15', '~1.5.4', '1.6.3', '1.5.5')), []);
});
test('PR 2759 shape rejects both stale serializer dependencies despite npm overrides', () => {
const errors = serializationGraphErrors(graph('1.9.16', '~1.6.8', '1.6.3', '1.5.5'));
assert.equal(errors.length, 2);
assert.match(errors[0], /requires seroval ~1\.6\.8, but resolves 1\.6\.3/);
assert.match(errors[1], /requires seroval-plugins ~1\.6\.8, but resolves 1\.5\.5/);
});
test('correcting only seroval still rejects the stale plugin', () => {
const errors = serializationGraphErrors(graph('1.9.16', '~1.6.8', '1.6.8', '1.5.5'));
assert.equal(errors.length, 1);
assert.match(errors[0], /requires seroval-plugins/);
});
test('correcting only the plugin still rejects stale seroval', () => {
const errors = serializationGraphErrors(graph('1.9.16', '~1.6.8', '1.6.3', '1.6.8'));
assert.equal(errors.length, 1);
assert.match(errors[0], /requires seroval /);
});
test('the coupled update and later matching patches are not permanently pinned', () => {
assert.deepEqual(serializationGraphErrors(graph()), []);
assert.deepEqual(serializationGraphErrors(graph('1.9.17', '~1.6.8', '1.6.9', '1.6.10')), []);
});
test('major and minor mismatches do not satisfy a tilde requirement', () => {
for (const version of ['2.6.8', '1.7.8', '1.5.99']) {
assert.equal(serializationGraphErrors(graph('1.9.16', '~1.6.8', version)).length, 1);
}
});
test('a new Solid cannot borrow the old consumer compatibility exception', () => {
const errors = serializationGraphErrors(graph('1.9.17', '~1.5.4', '1.6.3', '1.5.5'));
assert.equal(errors.length, 1);
});
test('a different serializer cannot borrow the exact old override', () => {
assert.equal(serializationGraphErrors(graph('1.9.15', '~1.5.4', '1.6.4', '1.5.5')).length, 1);
});
test('nested resolution wins over a correct root serializer', () => {
const lock = graph();
lock.packages['node_modules/solid-js/node_modules/seroval'] = { version: '1.6.3' };
const errors = serializationGraphErrors(lock);
assert.equal(errors.length, 1);
assert.match(errors[0], /resolves 1\.6\.3/);
});
test('a nested Solid resolves its nearest dependency instead of the root', () => {
const lock = graph();
lock.packages['node_modules/fixture/node_modules/solid-js'] = structuredClone(lock.packages['node_modules/solid-js']);
lock.packages['node_modules/fixture/node_modules/seroval-plugins'] = { version: '1.5.5' };
assert.equal(serializationGraphErrors(lock).length, 1);
lock.packages['node_modules/fixture/node_modules/seroval-plugins'] = { version: '1.6.8' };
assert.deepEqual(serializationGraphErrors(lock), []);
});
test('the old root-only override does not qualify a new nested resolution', () => {
const lock = graph('1.9.15', '~1.5.4', '1.6.3', '1.5.5');
lock.packages['node_modules/solid-js/node_modules/seroval'] = { version: '1.6.3' };
assert.equal(serializationGraphErrors(lock).length, 1);
});
test('missing dependency and consumer entries fail closed', () => {
for (const name of ['seroval', 'seroval-plugins', 'solid-js']) {
const lock = graph();
delete lock.packages[`node_modules/${name}`];
assert.equal(serializationGraphErrors(lock).length, 1);
}
});
test('incomplete lock formats fail closed', () => {
for (const lock of [null, {}, { lockfileVersion: 2, packages: {} }, { lockfileVersion: 3, packages: [] }]) {
assert.equal(serializationGraphErrors(lock).length, 1);
}
});
test('unknown ranges require explicit review rather than a made-up semver verdict', () => {
for (const range of ['^1.6.8', '~1.6', '*', '~1.6.8-beta.1']) {
const errors = serializationGraphErrors(graph('1.9.16', range));
assert.equal(errors.length, 2);
}
const lock = graph();
delete lock.packages['node_modules/solid-js'].dependencies.seroval;
assert.equal(serializationGraphErrors(lock).length, 1);
});
test('prereleases and malformed locked versions cannot claim compatibility', () => {
for (const version of ['1.6.8-beta.1', '1.6', '01.6.8', 'not-a-version', undefined]) {
const lock = graph();
lock.packages['node_modules/seroval'].version = version;
assert.equal(serializationGraphErrors(lock).length, 1);
}
const lock = graph();
lock.packages['node_modules/solid-js'] = null;
assert.equal(serializationGraphErrors(lock).length, 1);
});
test('CI fails the graph check before installation without relaxing the audit', () => {
const workflow = readFileSync(new URL('../../.github/workflows/build-and-test.yml', import.meta.url), 'utf8');
const frontend = workflow.slice(workflow.indexOf('\n frontend:'));
const check = frontend.indexOf('- name: Check Solid serialization dependency edges');
const install = frontend.indexOf('- name: Install frontend dependencies');
assert.ok(check >= 0 && check < install);
assert.match(frontend.slice(check, install), /node --test scripts\/tests\/check-frontend-serialization\.test\.mjs/);
assert.match(frontend.slice(check, install), /node scripts\/check-frontend-serialization\.mjs/);
assert.doesNotMatch(frontend.slice(check, install), /continue-on-error/);
assert.match(frontend, /NPM_AUDIT_REQUIRE_RESULT: \$\{\{ needs\.changes\.outputs\.frontend_deps \}\}/);
assert.match(frontend, /npm-audit-retry\.sh" all/);
});