Files
msd-core/tests/installer-migration-authoring.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02:00

251 lines
7.1 KiB
JavaScript

'use strict';
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const {
discoverInstallerMigrations,
planInstallerMigrations,
} = require('../msd-core/bin/lib/installer-migrations.cjs');
const { cleanup, createTempDir } = require('./helpers.cjs');
function writeMigration(dir, fileName, source) {
fs.writeFileSync(path.join(dir, fileName), source, 'utf8');
}
function completeMigrationRecord(overrides = {}) {
return {
id: '2026-05-11-authoring-guard-test',
title: 'Authoring guard test migration',
description: 'Exercise installer migration authoring guardrails.',
introducedIn: '1.50.0',
scopes: ['global', 'local'],
destructive: true,
plan: () => [],
...overrides,
};
}
test('rejects discovered migration records missing required authoring metadata', (t) => {
const migrationsDir = createTempDir('msd-migration-authoring-');
t.after(() => cleanup(migrationsDir));
writeMigration(
migrationsDir,
'001-missing-title.cjs',
`'use strict';
module.exports = {
id: '2026-05-11-missing-title',
description: 'Incomplete migration record.',
introducedIn: '1.50.0',
scopes: ['global', 'local'],
destructive: false,
plan: () => [],
};
`
);
assert.throws(
() => discoverInstallerMigrations({ migrationsDir }),
/migration record must include a non-empty title: .*001-missing-title\.cjs/
);
});
test('rejects direct migration records missing required authoring metadata during planning', (t) => {
const configDir = createTempDir('msd-migration-authoring-plan-');
t.after(() => cleanup(configDir));
fs.writeFileSync(
path.join(configDir, 'msd-file-manifest.json'),
JSON.stringify({ version: '1.50.0', timestamp: '2026-05-11T00:00:00.000Z', mode: 'full', files: {} }),
'utf8'
);
assert.throws(
() => planInstallerMigrations({
configDir,
migrations: [
{
id: '2026-05-11-incomplete-direct-record',
title: 'Incomplete direct migration record',
introducedIn: '1.50.0',
scopes: ['global', 'local'],
destructive: false,
plan: () => [],
},
],
}),
/migration record must include a non-empty description: 2026-05-11-incomplete-direct-record/
);
});
test('rejects migration records without explicit install scopes', (t) => {
const configDir = createTempDir('msd-migration-authoring-scope-');
t.after(() => cleanup(configDir));
fs.writeFileSync(
path.join(configDir, 'msd-file-manifest.json'),
JSON.stringify({ version: '1.50.0', timestamp: '2026-05-11T00:00:00.000Z', mode: 'full', files: {} }),
'utf8'
);
assert.throws(
() => planInstallerMigrations({
configDir,
migrations: [
{
id: '2026-05-11-missing-scopes',
title: 'Missing scopes',
description: 'Missing explicit install scope.',
introducedIn: '1.50.0',
destructive: false,
plan: () => [],
},
],
}),
/migration record scopes must be a non-empty string array: 2026-05-11-missing-scopes/
);
});
test('rejects destructive migration actions without ownership evidence', (t) => {
const configDir = createTempDir('msd-migration-authoring-action-');
t.after(() => cleanup(configDir));
fs.writeFileSync(
path.join(configDir, 'msd-file-manifest.json'),
JSON.stringify({
version: '1.50.0',
timestamp: '2026-05-11T00:00:00.000Z',
mode: 'full',
files: {},
}),
'utf8'
);
assert.throws(
() => planInstallerMigrations({
configDir,
migrations: [
completeMigrationRecord({
plan: () => [
{
type: 'remove-managed',
relPath: 'hooks/old-hook.js',
reason: 'retired hook',
},
],
}),
],
scope: 'global',
}),
/migration action remove-managed must include ownershipEvidence: 2026-05-11-authoring-guard-test hooks\/old-hook\.js/
);
});
test('rejects remove-empty-dir actions without ownership evidence (same bar as remove-managed)', (t) => {
const configDir = createTempDir('msd-migration-authoring-dir-action-');
t.after(() => cleanup(configDir));
fs.writeFileSync(
path.join(configDir, 'msd-file-manifest.json'),
JSON.stringify({
version: '1.50.0',
timestamp: '2026-05-11T00:00:00.000Z',
mode: 'full',
files: {},
}),
'utf8'
);
assert.throws(
() => planInstallerMigrations({
configDir,
migrations: [
completeMigrationRecord({
plan: () => [
{
type: 'remove-empty-dir',
relPath: 'hooks',
reason: 'retired reserved directory',
},
],
}),
],
scope: 'global',
}),
/migration action remove-empty-dir must include ownershipEvidence: 2026-05-11-authoring-guard-test hooks/
);
});
test('rejects migration actions with absolute or traversal relPaths', (t) => {
const configDir = createTempDir('msd-migration-authoring-relpath-');
t.after(() => cleanup(configDir));
fs.writeFileSync(
path.join(configDir, 'msd-file-manifest.json'),
JSON.stringify({ version: '1.50.0', timestamp: '2026-05-11T00:00:00.000Z', mode: 'full', files: {} }),
'utf8'
);
for (const relPath of ['/tmp/outside.js', 'hooks/../outside.js', 'hooks/..', '.']) {
assert.throws(
() => planInstallerMigrations({
configDir,
migrations: [
completeMigrationRecord({
plan: () => [
{
type: 'remove-managed',
relPath,
reason: 'bad path',
ownershipEvidence: 'test fixture manifest-managed hook',
},
],
}),
],
scope: 'global',
}),
/relPath must stay inside configDir/
);
}
});
test('rejects runtime config rewrites without a runtime contract citation', (t) => {
const configDir = createTempDir('msd-migration-authoring-runtime-');
t.after(() => cleanup(configDir));
fs.writeFileSync(
path.join(configDir, 'msd-file-manifest.json'),
JSON.stringify({ version: '1.50.0', timestamp: '2026-05-11T00:00:00.000Z', mode: 'full', files: {} }),
'utf8'
);
fs.writeFileSync(path.join(configDir, 'hooks.json'), '{"hooks":[]}\n', 'utf8');
assert.throws(
() => planInstallerMigrations({
configDir,
migrations: [
completeMigrationRecord({
runtimes: ['codex'],
scopes: ['global', 'local'],
plan: () => [
{
type: 'rewrite-json',
relPath: 'hooks.json',
value: {},
deleteIfEmpty: true,
reason: 'retired generated Codex hook registration',
ownershipEvidence: 'matches generated MSD hook command path',
},
],
}),
],
runtime: 'codex',
scope: 'global',
}),
/migration action rewrite-json requires migration runtimeContract: 2026-05-11-authoring-guard-test hooks\.json/
);
});