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.
104 lines
4.5 KiB
JavaScript
104 lines
4.5 KiB
JavaScript
'use strict';
|
|
|
|
const { test, describe, mock } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { gitOrThrow } = require('./helpers/git-fixture.cjs');
|
|
const { createTempDir, cleanup } = require('./helpers.cjs');
|
|
|
|
const { computeMigrationPlan, applyMigration } = require('../msd-core/bin/lib/roadmap-upgrade.cjs');
|
|
|
|
/**
|
|
* Build a git project whose `.planning/` is GITIGNORED (commit_docs:false) —
|
|
* the condition under which the old `git reset --hard` + `git clean -fd`
|
|
* rollback restored nothing yet still reported "rolled back". #1542.
|
|
*/
|
|
function makeGitignoredPlanningProject() {
|
|
const dir = createTempDir('m3-rollback-');
|
|
fs.writeFileSync(path.join(dir, '.gitignore'), '.planning/\n');
|
|
fs.writeFileSync(path.join(dir, 'README.md'), '# tracked\n');
|
|
const git = (argv) => gitOrThrow(argv, { cwd: dir });
|
|
git(['init']);
|
|
git(['config', 'user.email', 't@t.t']);
|
|
git(['config', 'user.name', 't']);
|
|
git(['config', 'commit.gpgsign', 'false']);
|
|
git(['add', '-A']);
|
|
git(['commit', '-m', 'initial']);
|
|
|
|
// .planning created AFTER the commit → untracked + gitignored.
|
|
const planning = path.join(dir, '.planning');
|
|
fs.mkdirSync(path.join(planning, 'phases', '01-foo'), { recursive: true });
|
|
fs.mkdirSync(path.join(planning, 'phases', '02-bar'), { recursive: true });
|
|
fs.writeFileSync(path.join(planning, 'phases', '01-foo', 'PLAN.md'), 'foo plan\n');
|
|
fs.writeFileSync(path.join(planning, 'phases', '02-bar', 'PLAN.md'), 'bar plan\n');
|
|
fs.writeFileSync(
|
|
path.join(planning, 'ROADMAP.md'),
|
|
['## v1.0: First Milestone', '', '### Phase 1: Foo', '', '### Phase 2: Bar', ''].join('\n'),
|
|
);
|
|
return dir;
|
|
}
|
|
|
|
function snapshotPlanning(dir) {
|
|
const planning = path.join(dir, '.planning');
|
|
return {
|
|
phases: fs.readdirSync(path.join(planning, 'phases')).sort(),
|
|
roadmap: fs.readFileSync(path.join(planning, 'ROADMAP.md'), 'utf8'),
|
|
hasConfig: fs.existsSync(path.join(planning, 'config.json')),
|
|
};
|
|
}
|
|
|
|
describe('roadmap upgrade rollback (#1542)', () => {
|
|
test('a mid-migration failure restores .planning even when it is gitignored', (t) => {
|
|
const dir = makeGitignoredPlanningProject();
|
|
t.after(() => cleanup(dir));
|
|
|
|
const plan = computeMigrationPlan(dir);
|
|
assert.equal(plan.alreadyMigrated, false);
|
|
assert.ok(plan.phases.length >= 1, 'fixture must produce phase renames');
|
|
assert.ok(plan.roadmapEdits.length >= 1, 'fixture must produce roadmap edits');
|
|
|
|
const before = snapshotPlanning(dir);
|
|
assert.equal(before.hasConfig, false, 'precondition: no config.json yet');
|
|
|
|
// Inject a failure on the LAST mutation step (the config.json write) so the
|
|
// phase renames AND the ROADMAP rewrite have already happened when rollback
|
|
// fires — exactly the half-migrated state the old git rollback could not undo.
|
|
const realWrite = fs.writeFileSync;
|
|
const writeMock = mock.method(fs, 'writeFileSync', function (target, data, opts) {
|
|
if (String(target).endsWith('config.json')) {
|
|
const err = new Error('EIO: simulated write failure');
|
|
err.code = 'EIO';
|
|
throw err;
|
|
}
|
|
return realWrite.call(fs, target, data, opts);
|
|
});
|
|
t.after(() => writeMock.mock.restore());
|
|
|
|
assert.throws(() => applyMigration(dir, plan, { dryRun: false }), /Migration failed/);
|
|
|
|
// The rollback must have actually restored the workspace — not just claimed to.
|
|
const after = snapshotPlanning(dir);
|
|
assert.deepEqual(after.phases, before.phases, 'phase dirs must be restored to their original names');
|
|
assert.equal(after.roadmap, before.roadmap, 'ROADMAP.md must be restored to its original content');
|
|
assert.equal(after.hasConfig, false, 'config.json created during migration must be removed on rollback');
|
|
});
|
|
|
|
test('a successful migration still applies (renames + roadmap rewrite + config), no rollback', (t) => {
|
|
const dir = makeGitignoredPlanningProject();
|
|
t.after(() => cleanup(dir));
|
|
|
|
const plan = computeMigrationPlan(dir);
|
|
const before = snapshotPlanning(dir);
|
|
|
|
const result = applyMigration(dir, plan, { dryRun: false });
|
|
|
|
assert.equal(result.applied, true);
|
|
const after = snapshotPlanning(dir);
|
|
assert.notDeepEqual(after.phases, before.phases, 'phase dirs renamed on success');
|
|
assert.equal(after.hasConfig, true, 'config.json written on success');
|
|
const config = JSON.parse(fs.readFileSync(path.join(dir, '.planning', 'config.json'), 'utf8'));
|
|
assert.equal(config.phase_id_convention, 'milestone-prefixed');
|
|
});
|
|
});
|