Files
msd-core/tests/bug-416-archive-dir-null.test.cjs
Tom Boucher 79002a00cb chore(#518): rename npm package + bin to @opengsd/gsd-core (#519)
* chore: rename npm package + bin to @opengsd/gsd-core (functional)

- package.json: name @opengsd/get-shit-done-redux → @opengsd/gsd-core,
  bin key get-shit-done-redux → gsd-core, repository/homepage/bugs URLs
- package-lock.json: regenerated (npm install --package-lock-only)
- tests/**, scripts/**, bin/**, .github/**, agents/**, commands/**,
  get-shit-done/bin/**, get-shit-done/workflows/**:
  applied the 4-rule replacement (scoped npm ref, GitHub repo path,
  bin/clone invocations) per #505 single-source refactor

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: sweep live references to @opengsd/gsd-core

Update all live documentation (README.md + translations, docs/**,
CONTRIBUTING.md, VERSIONING.md, SECURITY.md, CONTEXT.md,
docs/CANARY.md) to reflect the renamed package and repository.

Rules applied:
- @opengsd/get-shit-done-redux → @opengsd/gsd-core (scoped npm name)
- open-gsd/get-shit-done-redux → open-gsd/gsd-core (GitHub repo)
- GSD-redux/get-shit-done-redux → open-gsd/gsd-core (stale badge org)
- bare bin/clone refs → gsd-core

CHANGELOG.md, docs/adr/**, docs/RELEASE-*.md, docs/research/**,
and .changeset/** are preserved byte-identical.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: add negative lookbehind to slash-command regex in bug-2954 test

The extractSlashReferences regex matched /gsd-core inside npm package
URLs (@opengsd/gsd-core), producing a false /gsd:core command reference.
Adding a negative lookbehind (?<![a-z]) excludes matches preceded by a
letter, so only standalone /gsd-<cmd> and /gsd:<cmd> tokens are found.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(#518): add changeset for package rename

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(#518): update package-identity expectations to the renamed coordinates

The rebase regenerated the seam to @opengsd/gsd-core (bin gsd-core, repo
open-gsd/gsd-core). The #498 seam tests assert deriveIdentity against the REAL
package.json, so their expected literals must follow the rename. The drift-lint
unit test is left as-is — its SEAM is a self-consistent fixture and its
stale-literal detection cases would shift if altered; the live-repo scan in it
already passes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-30 17:25:02 -04:00

213 lines
9.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
/**
* Regression tests for issue #416 (open-gsd/gsd-core).
*
* Bug: getActiveMilestoneArchiveDir falls back to the newest archive directory
* when STATE.md names a milestone that has no matching archive yet, producing
* W007 false positives for phases from a prior (completed) milestone.
*
* Fix: when STATE.md is present and parseable and names a milestone, but no
* milestones/<vX.Y>-phases/ directory matches, return null. The version-sort
* fallback to the newest archive fires only when STATE.md is absent or
* unparseable.
*
* Knuth invariant: the resolver answers one question —
* "what archive directory holds the active milestone's phases?"
* Answer space: <dir> | null.
*/
process.env.GSD_TEST_MODE = '1';
const { describe, test, before, after } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const os = require('node:os');
const { runGsdTools } = require('./helpers.cjs');
// ── helpers ──────────────────────────────────────────────────────────────────
function mkplanning(base) {
const planningDir = path.join(base, '.planning');
const phasesDir = path.join(planningDir, 'phases');
fs.mkdirSync(phasesDir, { recursive: true });
return planningDir;
}
function writeMinimalRoadmap(planningDir, phases) {
// phases: array of { num, name, checked }
const checkboxes = phases.map(({ num, name, checked }) =>
`- [${checked ? 'x' : ' '}] **Phase ${num}:** ${name}`,
).join('\n');
const headings = phases.map(({ num, name }) =>
`### Phase ${num}: ${name}\n**Goal:** Completed.\n`,
).join('\n');
fs.writeFileSync(
path.join(planningDir, 'ROADMAP.md'),
`# Roadmap\n\n${checkboxes}\n\n${headings}`,
);
}
function writeStateMdMilestone(planningDir, milestone) {
fs.writeFileSync(
path.join(planningDir, 'STATE.md'),
`# State\n\n**milestone:** ${milestone}\n**Current Phase:** 23\n**Status:** In progress\n`,
);
}
function writeProjectMd(planningDir) {
fs.writeFileSync(
path.join(planningDir, 'PROJECT.md'),
'# Project\n\n## What This Is\nTest.\n\n## Core Value\nTest.\n\n## Requirements\nTest.\n',
);
}
function writeConfigJson(planningDir) {
fs.writeFileSync(
path.join(planningDir, 'config.json'),
JSON.stringify({ model_profile: 'balanced' }),
);
}
function mkArchivePhases(planningDir, version, phaseNums) {
// Creates .planning/milestones/<version>-phases/<NN>-phase-name/ dirs
const archiveDir = path.join(planningDir, 'milestones', `${version}-phases`);
for (const num of phaseNums) {
const padded = String(num).padStart(2, '0');
fs.mkdirSync(path.join(archiveDir, `${padded}-phase-${num}`), { recursive: true });
}
return archiveDir;
}
// ─────────────────────────────────────────────────────────────────────────────
// Case 1: STATE.md milestone: v6.0, only v5.0-phases/ on disk
// → resolver returns null, verifier emits zero W007 for phases 17–22
// ─────────────────────────────────────────────────────────────────────────────
describe('bug #416 case 1: STATE.md v6.0 with only v5.0-phases/ on disk → null, no W007', () => {
let tmpDir;
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-416-c1-'));
const planningDir = mkplanning(tmpDir);
writeProjectMd(planningDir);
writeConfigJson(planningDir);
// Active milestone is v6.0 — no archive for it yet (phases live in flat phases/)
writeStateMdMilestone(planningDir, 'v6.0');
// v5.0 was the prior completed milestone; its archive exists on disk
mkArchivePhases(planningDir, 'v5.0', [17, 18, 19, 20, 21, 22]);
// ROADMAP reflects only v6.0 phases (v5.0 phases are in a prior milestone,
// not in the current roadmap section)
writeMinimalRoadmap(planningDir, [
{ num: 23, name: 'New Foundation', checked: false },
]);
});
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
test('validate health emits zero W007 warnings (no prior-milestone phases surfaced)', () => {
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
assert.strictEqual(result.success, true, `validate health failed: ${result.error}`);
const data = JSON.parse(result.output);
const w007 = (data.warnings ?? []).filter((w) => w.code === 'W007');
assert.strictEqual(
w007.length,
0,
`Expected zero W007 — phases 17–22 from v5.0-phases/ must not appear as "active".\nGot: ${JSON.stringify(w007, null, 2)}`,
);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Case 2: No STATE.md, multiple archives on disk → version-sort fallback
// returns the highest-versioned archive (existing behavior preserved)
// ─────────────────────────────────────────────────────────────────────────────
describe('bug #416 case 2: no STATE.md + multiple archives → version-sort fallback to newest', () => {
let tmpDir;
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-416-c2-'));
const planningDir = mkplanning(tmpDir);
writeProjectMd(planningDir);
writeConfigJson(planningDir);
// No STATE.md — resolver must use the version-sort fallback
// Two archives: v4.0 and v5.0; v5.0 is newer
mkArchivePhases(planningDir, 'v4.0', [10, 11, 12]);
mkArchivePhases(planningDir, 'v5.0', [17, 18, 19]);
// ROADMAP lists v5.0 phases so W007 does not fire
writeMinimalRoadmap(planningDir, [
{ num: 17, name: 'Alpha', checked: true },
{ num: 18, name: 'Beta', checked: true },
{ num: 19, name: 'Gamma', checked: true },
]);
});
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
test('validate health succeeds and does not emit W007 for v5.0 archive phases', () => {
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
assert.strictEqual(result.success, true, `validate health failed: ${result.error}`);
const data = JSON.parse(result.output);
const w007 = (data.warnings ?? []).filter((w) => w.code === 'W007');
// v5.0 phases (17–19) are in the archive returned by the fallback and
// in the ROADMAP, so no W007 should fire.
assert.strictEqual(
w007.length,
0,
`Expected zero W007 for v5.0 archive phases present in ROADMAP.\nGot: ${JSON.stringify(w007, null, 2)}`,
);
});
});
// ─────────────────────────────────────────────────────────────────────────────
// Case 3: STATE.md milestone: v5.0, matching v5.0-phases/ exists → returns it
// (regression guard — happy path must not break)
// ─────────────────────────────────────────────────────────────────────────────
describe('bug #416 case 3: STATE.md v5.0 with matching v5.0-phases/ → returns archive dir', () => {
let tmpDir;
before(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-416-c3-'));
const planningDir = mkplanning(tmpDir);
writeProjectMd(planningDir);
writeConfigJson(planningDir);
// STATE.md names v5.0 and a matching archive exists
writeStateMdMilestone(planningDir, 'v5.0');
mkArchivePhases(planningDir, 'v5.0', [17, 18, 19, 20, 21, 22]);
// ROADMAP lists v5.0 phases so W007 does not fire
writeMinimalRoadmap(planningDir, [
{ num: 17, name: 'Alpha', checked: true },
{ num: 18, name: 'Beta', checked: true },
{ num: 19, name: 'Gamma', checked: true },
{ num: 20, name: 'Delta', checked: true },
{ num: 21, name: 'Epsilon', checked: true },
{ num: 22, name: 'Zeta', checked: true },
]);
});
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
test('validate health emits zero W007 — archive phases are in ROADMAP and active', () => {
const result = runGsdTools(['validate', 'health', '--json'], tmpDir);
assert.strictEqual(result.success, true, `validate health failed: ${result.error}`);
const data = JSON.parse(result.output);
const w007 = (data.warnings ?? []).filter((w) => w.code === 'W007');
assert.strictEqual(
w007.length,
0,
`Expected zero W007 for matching v5.0 archive with v5.0 in STATE.md.\nGot: ${JSON.stringify(w007, null, 2)}`,
);
});
});