* 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>
181 lines
7.2 KiB
JavaScript
181 lines
7.2 KiB
JavaScript
// allow-test-rule: integration-test-input
|
||
// The script under test (scripts/release-tarball-smoke.cjs) is the system
|
||
// under test. We exercise it via its exported pure function, not by reading
|
||
// source text. The tarball fixture is produced by npm pack in before().
|
||
|
||
'use strict';
|
||
|
||
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 { cleanup, createTempDir, runNpm, isolatedNpmEnv } = require('./helpers.cjs');
|
||
const { SMOKE, runSmoke } = require('../scripts/release-tarball-smoke.cjs');
|
||
|
||
const smokeMsg = (label, result) =>
|
||
`${label}: code=${result.code} details=${JSON.stringify(result.details)}`;
|
||
|
||
const PKG_PATH = path.join(__dirname, '..', 'package.json');
|
||
const pkg = JSON.parse(fs.readFileSync(PKG_PATH, 'utf-8'));
|
||
|
||
describe('release-tarball-smoke', () => {
|
||
// Shared fixture state: pack the tarball once, install it once, reuse for all tests.
|
||
let packDir;
|
||
let installPrefix;
|
||
let tarballPath;
|
||
// fixtureDir for lifecycle / init tests; created once in before(), cleaned in after().
|
||
let fixtureDir;
|
||
|
||
before(async () => {
|
||
// Pack once into a temp dir.
|
||
packDir = createTempDir('gsd-smoke-pack-');
|
||
installPrefix = createTempDir('gsd-smoke-prefix-');
|
||
fixtureDir = createTempDir('gsd-smoke-fixture-');
|
||
|
||
// npm pack + npm install -g on a large tarball (1499 files, ~10 MB) can take
|
||
// 3–6 minutes on slow Docker hosts (cold disk, constrained CPU). The runNpm
|
||
// default timeout of 180 s is sufficient on fast machines but insufficient on
|
||
// cartographer-class hosts. 600 s (10 min) gives a safe ceiling without
|
||
// masking genuine hangs.
|
||
const SLOW_HOST_TIMEOUT = 600_000;
|
||
|
||
const packOutput = runNpm(
|
||
['pack', '--pack-destination', packDir],
|
||
{ cwd: path.join(__dirname, '..'), timeout: SLOW_HOST_TIMEOUT },
|
||
);
|
||
|
||
// npm pack prints the filename as the last line of stdout.
|
||
const lines = packOutput.split(/\r?\n/).filter(Boolean);
|
||
const tgzName = lines[lines.length - 1];
|
||
tarballPath = path.join(packDir, tgzName);
|
||
if (!fs.existsSync(tarballPath)) {
|
||
const found = fs.readdirSync(packDir).find((f) => f.endsWith('.tgz'));
|
||
if (!found) throw new Error(`npm pack produced no .tgz in ${packDir}; output: ${packOutput}`);
|
||
tarballPath = path.join(packDir, found);
|
||
}
|
||
|
||
// Install once into installPrefix. All tests share this install.
|
||
runNpm(['install', '-g', '--prefix', installPrefix, tarballPath], { timeout: SLOW_HOST_TIMEOUT });
|
||
});
|
||
|
||
after(() => {
|
||
cleanup(packDir);
|
||
cleanup(installPrefix);
|
||
cleanup(fixtureDir);
|
||
});
|
||
|
||
// ── Test A — happy path ────────────────────────────────────────────────────
|
||
test('A: happy path — installed version matches package.json', () => {
|
||
const result = runSmoke({
|
||
tarballPath,
|
||
installPrefix,
|
||
expectedVersion: pkg.version,
|
||
fixtureDir,
|
||
npmEnv: isolatedNpmEnv(),
|
||
});
|
||
|
||
assert.equal(result.code, SMOKE.OK, smokeMsg('A', result));
|
||
assert.equal(result.details.version, pkg.version, smokeMsg('A', result));
|
||
});
|
||
|
||
// ── Test B — version mismatch detected ────────────────────────────────────
|
||
test('B: version mismatch detected — returns VERSION_MISMATCH', () => {
|
||
const result = runSmoke({
|
||
tarballPath,
|
||
installPrefix,
|
||
expectedVersion: '99.99.99',
|
||
fixtureDir,
|
||
npmEnv: isolatedNpmEnv(),
|
||
});
|
||
|
||
assert.equal(result.code, SMOKE.VERSION_MISMATCH, smokeMsg('B', result));
|
||
});
|
||
|
||
// ── Test C — happy lifecycle ───────────────────────────────────────────────
|
||
// Verifies that the installed package has all expected command .md files and
|
||
// that each command resolves a workflow .md file that also exists.
|
||
// Also verifies that `gsd-core --local --claude` (init) succeeds in
|
||
// the fixtureDir and creates the expected .claude/ directories.
|
||
test('C: happy lifecycle — command + workflow files resolve OK', () => {
|
||
const result = runSmoke({
|
||
tarballPath,
|
||
installPrefix,
|
||
expectedVersion: pkg.version,
|
||
fixtureDir,
|
||
lifecycleCommands: ['init', 'discuss-phase', 'plan-phase'],
|
||
npmEnv: isolatedNpmEnv(),
|
||
});
|
||
|
||
assert.equal(result.code, SMOKE.OK, smokeMsg('C', result));
|
||
|
||
// Each non-init command must be in lifecycleResolved with both paths populated
|
||
const resolved = result.details.lifecycleResolved;
|
||
assert.ok(Array.isArray(resolved));
|
||
|
||
for (const entry of resolved) {
|
||
assert.ok(
|
||
typeof entry.commandPath === 'string' && entry.commandPath.length > 0,
|
||
`expected commandPath for ${entry.command}`,
|
||
);
|
||
assert.ok(
|
||
fs.existsSync(entry.commandPath) && fs.statSync(entry.commandPath).isFile(),
|
||
`commandPath must be an existing file: ${entry.commandPath}`,
|
||
);
|
||
assert.ok(
|
||
typeof entry.workflowPath === 'string' && entry.workflowPath.length > 0,
|
||
`expected workflowPath for ${entry.command}`,
|
||
);
|
||
assert.ok(
|
||
fs.existsSync(entry.workflowPath) && fs.statSync(entry.workflowPath).isFile(),
|
||
`workflowPath must be an existing file: ${entry.workflowPath}`,
|
||
);
|
||
}
|
||
});
|
||
|
||
// ── Test D — missing command detected ─────────────────────────────────────
|
||
// Passes a nonexistent command name; expects the smoke to detect the missing
|
||
// command .md file and return COMMAND_FILE_MISSING with the right details.
|
||
test('D: missing command detected — returns COMMAND_FILE_MISSING', () => {
|
||
const result = runSmoke({
|
||
tarballPath,
|
||
installPrefix,
|
||
expectedVersion: pkg.version,
|
||
fixtureDir,
|
||
lifecycleCommands: ['init', 'nonexistent-phase-xyz'],
|
||
npmEnv: isolatedNpmEnv(),
|
||
});
|
||
|
||
assert.equal(result.code, SMOKE.COMMAND_FILE_MISSING, smokeMsg('D', result));
|
||
assert.equal(result.details.command, 'nonexistent-phase-xyz', smokeMsg('D', result));
|
||
assert.ok(typeof result.details.path === 'string' && result.details.path.length > 0, smokeMsg('D', result));
|
||
});
|
||
|
||
// ── Test E — workflow-body checks run (informational) ─────────────────────
|
||
// Asserts that the workflow-body scanning machinery ran (structural assertion).
|
||
// Does NOT assert colonLeakCount is zero — when those issues are fixed, this
|
||
// test continues to pass unchanged.
|
||
test('E: workflow-body checks run — scan counts are present integers', () => {
|
||
const result = runSmoke({
|
||
tarballPath,
|
||
installPrefix,
|
||
expectedVersion: pkg.version,
|
||
fixtureDir,
|
||
lifecycleCommands: [],
|
||
npmEnv: isolatedNpmEnv(),
|
||
});
|
||
|
||
// Structural: the scan ran and populated the counters
|
||
assert.ok(
|
||
Number.isInteger(result.details.workflowsScanned) && result.details.workflowsScanned >= 1,
|
||
smokeMsg('E', result),
|
||
);
|
||
assert.ok(
|
||
Number.isInteger(result.details.colonLeakCount),
|
||
smokeMsg('E', result),
|
||
);
|
||
});
|
||
});
|