* chore(npm): rebrand packages to @opengsd scope Rename: - get-shit-done-redux → @opengsd/get-shit-done-redux - @gsd-redux/sdk → @opengsd/gsd-sdk Add publishConfig.access=public for first-time scoped publish. CLI binary names (get-shit-done-redux, gsd-sdk, gsd-tools) unchanged. Sweeps install commands, npx invocations, CI publish/version-check workflows, tests, docs, READMEs (all translations), and the PACKAGE_NAME constant in check-latest-version. Bumps qs 6.15.1 → 6.15.2 to clear a moderate advisory surfaced by the audit-clean test (GHSA-q8mj-m7cp-5q26). Closes #126 * chore: pin 2.0.0 release + remove canary workflow - Bump both packages 1.50.0-canary.0 → 2.0.0 for first @opengsd publish - Remove .github/workflows/canary.yml and canary dist-tag handling in release.yml / release-sdk.yml - Drop canary section from VERSIONING.md Refs #126 * chore: address review findings + harden tarball-smoke timeout - .changeset/opengsd-org-rename.md: match project's custom parse.cjs frontmatter (type: Changed / pr: 127); the scoped @changesets/cli keys were silently rejected. - CONTEXT.md: drop two canary-stream policy lines and a dangling DEFECT.CANARY-VERSION-LEAK.cross-ref now that canary.yml is gone. - tests/release-tarball-smoke.install.test.cjs: pass timeout: 600_000 for npm pack + global install; the 3-minute runNpm default was timing out on slower Docker hosts (cartographer). Refs #126 * fix(sdk): add missing type/runtime devDependencies for build prepublishOnly invokes tsc which couldn't resolve @types/node, @types/ws, or synckit. They had been hoisted from root but were not declared in sdk/'s own package.json — first publish from a clean SDK tree failed. Refs #126 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(ci): use npm pack stdout instead of glob to find tarball `npm pack --silent` for a scoped package (@opengsd/get-shit-done-redux) produces `opengsd-get-shit-done-redux-*.tgz`, not `get-shit-done-redux-*.tgz`. Capture the filename from stdout instead of a hardcoded glob so the step works regardless of package name format. Fixes smoke (ubuntu-latest, 22, false) CI failure. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * ci: treat workflow-file changes as test-skip eligible `.github/workflows/install-smoke.yml` (and other workflow files) were in neither `test.yml` paths nor `test-skip.yml` paths-ignore, so neither workflow ran on a workflow-only commit — leaving the required test-skip check perpetually missing. Refs #126 * chore: reset version to 1.0.0 for first @opengsd publish Nothing has been published yet under the @opengsd scope, so the inaugural release uses 1.0.0 rather than 2.0.0. The "major bump" in the changeset reflects the breaking install-command change for users migrating from the prior unscoped `get-shit-done-redux`, not a numeric continuation from a 1.x line under the new identity. Refs #126 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
195 lines
8.0 KiB
JavaScript
195 lines
8.0 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 } = require('./helpers.cjs');
|
||
const { SMOKE, runSmoke } = require('../scripts/release-tarball-smoke.cjs');
|
||
|
||
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,
|
||
});
|
||
|
||
assert.equal(result.code, SMOKE.OK);
|
||
assert.equal(result.details.version, pkg.version);
|
||
});
|
||
|
||
// ── Test B — version mismatch detected ────────────────────────────────────
|
||
test('B: version mismatch detected — returns VERSION_MISMATCH', () => {
|
||
const result = runSmoke({
|
||
tarballPath,
|
||
installPrefix,
|
||
expectedVersion: '99.99.99',
|
||
fixtureDir,
|
||
});
|
||
|
||
assert.equal(result.code, SMOKE.VERSION_MISMATCH);
|
||
});
|
||
|
||
// ── 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 `get-shit-done-redux --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'],
|
||
});
|
||
|
||
assert.equal(result.code, SMOKE.OK);
|
||
|
||
// 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'],
|
||
});
|
||
|
||
assert.equal(result.code, SMOKE.COMMAND_FILE_MISSING);
|
||
assert.equal(result.details.command, 'nonexistent-phase-xyz');
|
||
assert.ok(typeof result.details.path === 'string' && result.details.path.length > 0);
|
||
});
|
||
|
||
// ── Test E — SDK binary callable ──────────────────────────────────────────
|
||
// Verifies that `gsd-sdk query state.json` exits 0 and returns parseable JSON.
|
||
// This is the primary guard for SDK_BINARY_NOT_CALLABLE regressions.
|
||
test('E: sdk binary callable — gsd-sdk query produces parseable JSON', () => {
|
||
const result = runSmoke({
|
||
tarballPath,
|
||
installPrefix,
|
||
expectedVersion: pkg.version,
|
||
fixtureDir,
|
||
lifecycleCommands: [], // skip lifecycle checks; isolate SDK check
|
||
});
|
||
|
||
// If the binary can't be called, code would be SDK_BINARY_NOT_CALLABLE
|
||
assert.notEqual(result.code, SMOKE.SDK_BINARY_NOT_CALLABLE);
|
||
assert.equal(result.details.sdkQueryParsed, true);
|
||
});
|
||
|
||
// ── Test F — workflow-body checks run (informational) ─────────────────────
|
||
// Asserts that the workflow-body scanning machinery ran (structural assertion).
|
||
// Does NOT assert colonLeakCount or missingFallbackCount are zero — they will
|
||
// be non-zero against current main per #3668 and the /gsd: leak backlog.
|
||
// When those issues are fixed, this test continues to pass unchanged.
|
||
test('F: workflow-body checks run — scan counts are present integers', () => {
|
||
const result = runSmoke({
|
||
tarballPath,
|
||
installPrefix,
|
||
expectedVersion: pkg.version,
|
||
fixtureDir,
|
||
lifecycleCommands: [],
|
||
});
|
||
|
||
// Structural: the scan ran and populated the counters
|
||
assert.ok(
|
||
Number.isInteger(result.details.workflowsScanned) && result.details.workflowsScanned >= 1,
|
||
`expected workflowsScanned >= 1, got ${result.details.workflowsScanned}`,
|
||
);
|
||
assert.ok(
|
||
Number.isInteger(result.details.colonLeakCount),
|
||
`expected colonLeakCount to be an integer, got ${result.details.colonLeakCount}`,
|
||
);
|
||
assert.ok(
|
||
Number.isInteger(result.details.missingFallbackCount),
|
||
`expected missingFallbackCount to be an integer, got ${result.details.missingFallbackCount}`,
|
||
);
|
||
});
|
||
});
|