* feat(shell-projection): add exec* dispatch and platform* file I/O seam (Phase 1, #3465) Extends shell-command-projection.cjs with two new sections: Subprocess dispatch: - execGit(args, opts) → { exitCode, stdout, stderr } - execNpm(args, opts) → same; owns shell:true on Windows (npm.cmd) - execTool(program, args, opts) → same; ENOENT → exitCode 127, no throw - probeTty(opts) → string | null; returns null on Windows and non-tty Platform file I/O: - normalizeContent(filePath, content) → { content, encoding }; pure function; .md → full normalizeMd pass; other → CRLF→LF + trailing newline - platformWriteSync(filePath, content, opts) → ensureDir + normalizeContent + atomic write - platformReadSync(filePath, opts) → null on ENOENT; throws when required:true - platformEnsureDir(dirPath) → mkdirSync recursive, idempotent Absorbs normalizeMd fence-tracking logic from core.cjs (no circular dep). Existing rendering exports untouched. core.cjs compat exports unchanged until Phase 4. Updates CONTEXT.md with Shell Command Projection Module canonical definition. 31 new behavioral tests; full suite green. Closes #3465 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(context): record shell-projection expansion session learnings * chore(changeset): add entry for shell-projection I/O seam (#3465) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(adr): add ADR-0010 skill-surface budget module and ADR-0011 review default reviewers (Phase 1, #3465) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs(context): record phase 1 rebase and PR session learnings (#3465) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test(shell-projection): drop substring match on stderr — assert typed shape only The lint-no-source-grep rule prohibits substring matching on stderr (or any test-output text). The exitCode === 127 assertion already proves the ENOENT path; the stderr content was implementation-detail of the OS. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(pr3470): address CodeRabbit findings and minimal-core drift * docs(adr0010): align profile interface with phase-1 scope * docs(adr): index newly added 0010/0011 ADR drafts in README enh-3271 invariant requires every file in docs/adr/ to be linked from the README. Adds entries for the three ADR drafts committed earlier in this PR. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
88 lines
3.3 KiB
JavaScript
88 lines
3.3 KiB
JavaScript
'use strict';
|
|
/**
|
|
* Back-compat regression: --minimal still produces the same file set as before
|
|
* the profile model was introduced (modulo the phase-inclusion fix).
|
|
*
|
|
* Also verifies --profile=core is equivalent to --minimal.
|
|
*/
|
|
|
|
const { test, describe } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const os = require('os');
|
|
const { spawnSync } = require('child_process');
|
|
|
|
const {
|
|
MINIMAL_SKILL_ALLOWLIST,
|
|
PROFILES,
|
|
} = require('../get-shit-done/bin/lib/install-profiles.cjs');
|
|
|
|
const INSTALL_SCRIPT = path.join(__dirname, '..', 'bin', 'install.js');
|
|
const MANIFEST_NAME = 'gsd-file-manifest.json';
|
|
|
|
describe('install-minimal-backcompat: PROFILES.core matches MINIMAL_SKILL_ALLOWLIST', () => {
|
|
test('PROFILES.core contains the same 7 skills as MINIMAL_SKILL_ALLOWLIST', () => {
|
|
assert.deepStrictEqual(
|
|
[...PROFILES.core].sort(),
|
|
[...MINIMAL_SKILL_ALLOWLIST].sort(),
|
|
'PROFILES.core must equal MINIMAL_SKILL_ALLOWLIST for back-compat',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('install-minimal-backcompat: --minimal and --profile=core produce the same manifest skill count', () => {
|
|
function installAndGetManifest(extraArgs) {
|
|
const targetDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-backcompat-'));
|
|
try {
|
|
spawnSync(
|
|
process.execPath,
|
|
[INSTALL_SCRIPT, '--claude', '--global', '--config-dir', targetDir, ...extraArgs],
|
|
{ encoding: 'utf8' },
|
|
);
|
|
const manifestPath = path.join(targetDir, MANIFEST_NAME);
|
|
if (!fs.existsSync(manifestPath)) return { mode: null, skillCount: 0, profileMarker: null };
|
|
const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
const skillCount = new Set(
|
|
Object.keys(m.files || {})
|
|
.filter((k) => k.startsWith('skills/'))
|
|
.map((k) => k.split('/')[1]),
|
|
).size;
|
|
// Read marker
|
|
const markerPath = path.join(targetDir, '.gsd-profile');
|
|
const profileMarker = fs.existsSync(markerPath)
|
|
? fs.readFileSync(markerPath, 'utf8').trim()
|
|
: null;
|
|
return { mode: m.mode, skillCount, profileMarker };
|
|
} finally {
|
|
fs.rmSync(targetDir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
test('--minimal produces mode "minimal" with exactly 7 skills', () => {
|
|
const r = installAndGetManifest(['--minimal']);
|
|
assert.strictEqual(r.mode, 'minimal');
|
|
assert.strictEqual(r.skillCount, 7);
|
|
});
|
|
|
|
test('--minimal writes .gsd-profile marker with "core"', () => {
|
|
const r = installAndGetManifest(['--minimal']);
|
|
assert.strictEqual(r.profileMarker, 'core', '--minimal should write profile marker "core"');
|
|
});
|
|
|
|
test('default (no flags) writes .gsd-profile marker with "full"', () => {
|
|
const r = installAndGetManifest([]);
|
|
assert.strictEqual(r.profileMarker, 'full', 'default install should write profile marker "full"');
|
|
});
|
|
|
|
test('--profile=core writes .gsd-profile marker with "core"', () => {
|
|
const r = installAndGetManifest(['--profile=core']);
|
|
assert.strictEqual(r.profileMarker, 'core', '--profile=core should write profile marker "core"');
|
|
});
|
|
|
|
test('--profile=standard writes .gsd-profile marker with "standard"', () => {
|
|
const r = installAndGetManifest(['--profile=standard']);
|
|
assert.strictEqual(r.profileMarker, 'standard', '--profile=standard should write profile marker "standard"');
|
|
});
|
|
});
|