mirror of
https://github.com/glittercowboy/get-shit-done
synced 2026-04-25 17:25:23 +02:00
* fix(detect-custom-files): exclude skills and command dirs not wiped by installer (closes #2505) GSD_MANAGED_DIRS included 'skills' and 'command' directories, but the installer never wipes those paths. Users with third-party skills installed (40+ files, none in GSD's manifest) had every skill flagged as a "custom file" requiring backup, producing noisy false-positive reports on every /gsd-update run. Removes 'skills' and 'command' from both gsd-tools.cjs and the SDK's detect-custom-files.ts. Adds two regression tests confirming neither directory is scanned. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(settings): warn that model profiles are no-ops on non-Claude runtimes (closes #2506) settings.md presented Quality/Balanced/Budget model profiles without any indication that these tiers map to Claude models (Opus/Sonnet/Haiku) and have no effect on non-Claude runtimes (Codex, Gemini CLI, OpenRouter). Users on Codex saw the profile chooser as if it would meaningfully select models, but all agents silently used the runtime default regardless. Adds a non-Claude runtime note before the profile question (shown in TEXT_MODE, the path all non-Claude runtimes take) explaining the profiles are no-ops and directing users to either choose Inherit or configure model_overrides manually. Also updates the Inherit option description to explicitly name the runtimes where it is the correct choice. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
283 lines
10 KiB
JavaScript
283 lines
10 KiB
JavaScript
/**
|
|
* GSD Tools Tests — update workflow custom file backup detection (#1997)
|
|
*
|
|
* The update workflow must detect user-added files inside GSD-managed
|
|
* directories (get-shit-done/, agents/, commands/gsd/, hooks/) before the
|
|
* installer wipes those directories.
|
|
*
|
|
* This tests the `detect-custom-files` subcommand of gsd-tools.cjs, which is
|
|
* the correct fix for the bash path-stripping failure described in #1997.
|
|
*
|
|
* The bash pattern `${filepath#$RUNTIME_DIR/}` is unreliable because
|
|
* $RUNTIME_DIR may not be set and the stripped relative path may not match
|
|
* manifest key format. Moving the logic into gsd-tools.cjs eliminates the
|
|
* shell variable expansion failure entirely.
|
|
*
|
|
* Closes: #1997
|
|
*/
|
|
|
|
const { describe, test, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
const { runGsdTools, createTempDir, cleanup } = require('./helpers.cjs');
|
|
|
|
function sha256(content) {
|
|
return crypto.createHash('sha256').update(content).digest('hex');
|
|
}
|
|
|
|
/**
|
|
* Write a fake gsd-file-manifest.json into configDir with the given file entries.
|
|
*/
|
|
function writeManifest(configDir, files) {
|
|
const manifest = {
|
|
version: '1.32.0',
|
|
timestamp: new Date().toISOString(),
|
|
files: {}
|
|
};
|
|
for (const [relPath, content] of Object.entries(files)) {
|
|
const fullPath = path.join(configDir, relPath);
|
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
fs.writeFileSync(fullPath, content);
|
|
manifest.files[relPath] = sha256(content);
|
|
}
|
|
fs.writeFileSync(
|
|
path.join(configDir, 'gsd-file-manifest.json'),
|
|
JSON.stringify(manifest, null, 2)
|
|
);
|
|
}
|
|
|
|
describe('detect-custom-files — update workflow backup detection (#1997)', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir('gsd-custom-detect-');
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('detects a custom file added inside get-shit-done/workflows/', () => {
|
|
writeManifest(tmpDir, {
|
|
'get-shit-done/workflows/execute-phase.md': '# Execute Phase\n',
|
|
'get-shit-done/workflows/plan-phase.md': '# Plan Phase\n',
|
|
});
|
|
|
|
// Add a custom file NOT in the manifest
|
|
const customFile = path.join(tmpDir, 'get-shit-done/workflows/my-custom-workflow.md');
|
|
fs.writeFileSync(customFile, '# My Custom Workflow\n');
|
|
|
|
const result = runGsdTools(
|
|
['detect-custom-files', '--config-dir', tmpDir],
|
|
tmpDir
|
|
);
|
|
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const json = JSON.parse(result.output);
|
|
assert.ok(Array.isArray(json.custom_files), 'should return custom_files array');
|
|
assert.ok(json.custom_files.length > 0, 'should detect at least one custom file');
|
|
assert.ok(
|
|
json.custom_files.includes('get-shit-done/workflows/my-custom-workflow.md'),
|
|
`custom file should be listed; got: ${JSON.stringify(json.custom_files)}`
|
|
);
|
|
});
|
|
|
|
test('detects custom files added inside agents/', () => {
|
|
writeManifest(tmpDir, {
|
|
'agents/gsd-executor.md': '# GSD Executor\n',
|
|
});
|
|
|
|
// Add a user's custom agent (not prefixed with gsd-)
|
|
const customAgent = path.join(tmpDir, 'agents/my-custom-agent.md');
|
|
fs.mkdirSync(path.dirname(customAgent), { recursive: true });
|
|
fs.writeFileSync(customAgent, '# My Custom Agent\n');
|
|
|
|
const result = runGsdTools(
|
|
['detect-custom-files', '--config-dir', tmpDir],
|
|
tmpDir
|
|
);
|
|
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const json = JSON.parse(result.output);
|
|
assert.ok(json.custom_files.includes('agents/my-custom-agent.md'),
|
|
`custom agent should be detected; got: ${JSON.stringify(json.custom_files)}`);
|
|
});
|
|
|
|
test('reports zero custom files when all files are in manifest', () => {
|
|
writeManifest(tmpDir, {
|
|
'get-shit-done/workflows/execute-phase.md': '# Execute Phase\n',
|
|
'get-shit-done/references/gates.md': '# Gates\n',
|
|
'agents/gsd-executor.md': '# Executor\n',
|
|
});
|
|
// No extra files added
|
|
|
|
const result = runGsdTools(
|
|
['detect-custom-files', '--config-dir', tmpDir],
|
|
tmpDir
|
|
);
|
|
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const json = JSON.parse(result.output);
|
|
assert.ok(Array.isArray(json.custom_files), 'should return custom_files array');
|
|
assert.strictEqual(json.custom_files.length, 0, 'no custom files should be detected');
|
|
assert.strictEqual(json.custom_count, 0, 'custom_count should be 0');
|
|
});
|
|
|
|
test('returns custom_count equal to custom_files length', () => {
|
|
writeManifest(tmpDir, {
|
|
'get-shit-done/workflows/execute-phase.md': '# Execute Phase\n',
|
|
});
|
|
|
|
// Add two custom files
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'get-shit-done/workflows/custom-a.md'),
|
|
'# Custom A\n'
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'get-shit-done/workflows/custom-b.md'),
|
|
'# Custom B\n'
|
|
);
|
|
|
|
const result = runGsdTools(
|
|
['detect-custom-files', '--config-dir', tmpDir],
|
|
tmpDir
|
|
);
|
|
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const json = JSON.parse(result.output);
|
|
assert.strictEqual(json.custom_count, json.custom_files.length,
|
|
'custom_count should equal custom_files.length');
|
|
assert.strictEqual(json.custom_count, 2, 'should detect exactly 2 custom files');
|
|
});
|
|
|
|
test('does not flag manifest files as custom even if content was modified', () => {
|
|
writeManifest(tmpDir, {
|
|
'get-shit-done/workflows/execute-phase.md': '# Execute Phase\nOriginal\n',
|
|
});
|
|
|
|
// Modify the content of an existing manifest file
|
|
fs.writeFileSync(
|
|
path.join(tmpDir, 'get-shit-done/workflows/execute-phase.md'),
|
|
'# Execute Phase\nModified by user\n'
|
|
);
|
|
|
|
const result = runGsdTools(
|
|
['detect-custom-files', '--config-dir', tmpDir],
|
|
tmpDir
|
|
);
|
|
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const json = JSON.parse(result.output);
|
|
// Modified manifest files are handled by saveLocalPatches (in install.js).
|
|
// detect-custom-files only finds files NOT in the manifest at all.
|
|
assert.ok(
|
|
!json.custom_files.includes('get-shit-done/workflows/execute-phase.md'),
|
|
'modified manifest files should NOT be listed as custom (that is saveLocalPatches territory)'
|
|
);
|
|
});
|
|
|
|
test('handles missing manifest gracefully — treats all GSD-dir files as custom', () => {
|
|
// No manifest. Add a file in a GSD-managed dir.
|
|
const workflowDir = path.join(tmpDir, 'get-shit-done/workflows');
|
|
fs.mkdirSync(workflowDir, { recursive: true });
|
|
fs.writeFileSync(path.join(workflowDir, 'my-workflow.md'), '# My Workflow\n');
|
|
|
|
const result = runGsdTools(
|
|
['detect-custom-files', '--config-dir', tmpDir],
|
|
tmpDir
|
|
);
|
|
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const json = JSON.parse(result.output);
|
|
// Without a manifest, we cannot determine what is custom vs GSD-owned.
|
|
// The command should return an empty list (no manifest = skip detection,
|
|
// which is safe since saveLocalPatches also does nothing without a manifest).
|
|
assert.ok(Array.isArray(json.custom_files), 'should return custom_files array');
|
|
assert.ok(typeof json.custom_count === 'number', 'should return numeric custom_count');
|
|
});
|
|
|
|
test('detects custom files inside get-shit-done/references/', () => {
|
|
writeManifest(tmpDir, {
|
|
'get-shit-done/references/gates.md': '# Gates\n',
|
|
});
|
|
|
|
const customRef = path.join(tmpDir, 'get-shit-done/references/my-domain-probes.md');
|
|
fs.writeFileSync(customRef, '# My Domain Probes\n');
|
|
|
|
const result = runGsdTools(
|
|
['detect-custom-files', '--config-dir', tmpDir],
|
|
tmpDir
|
|
);
|
|
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const json = JSON.parse(result.output);
|
|
assert.ok(
|
|
json.custom_files.includes('get-shit-done/references/my-domain-probes.md'),
|
|
`should detect custom reference; got: ${JSON.stringify(json.custom_files)}`
|
|
);
|
|
});
|
|
|
|
// #2505 — installer does NOT wipe skills/ or command/; scanning them produces
|
|
// false-positive "custom file" reports for every skill the user has installed
|
|
// from other packages.
|
|
test('does not scan skills/ directory (installer does not wipe it)', () => {
|
|
writeManifest(tmpDir, {
|
|
'get-shit-done/workflows/execute-phase.md': '# Execute Phase\n',
|
|
});
|
|
|
|
// Simulate user having third-party skills installed — none in manifest
|
|
const skillsDir = path.join(tmpDir, 'skills');
|
|
fs.mkdirSync(skillsDir, { recursive: true });
|
|
fs.writeFileSync(path.join(skillsDir, 'my-custom-skill.md'), '# My Skill\n');
|
|
fs.writeFileSync(path.join(skillsDir, 'another-plugin-skill.md'), '# Another\n');
|
|
|
|
const result = runGsdTools(
|
|
['detect-custom-files', '--config-dir', tmpDir],
|
|
tmpDir
|
|
);
|
|
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const json = JSON.parse(result.output);
|
|
const skillFiles = json.custom_files.filter(f => f.startsWith('skills/'));
|
|
assert.strictEqual(
|
|
skillFiles.length, 0,
|
|
`skills/ should not be scanned; got false positives: ${JSON.stringify(skillFiles)}`
|
|
);
|
|
});
|
|
|
|
test('does not scan command/ directory (installer does not wipe it)', () => {
|
|
writeManifest(tmpDir, {
|
|
'get-shit-done/workflows/execute-phase.md': '# Execute Phase\n',
|
|
});
|
|
|
|
// Simulate files in command/ dir not wiped by installer
|
|
const commandDir = path.join(tmpDir, 'command');
|
|
fs.mkdirSync(commandDir, { recursive: true });
|
|
fs.writeFileSync(path.join(commandDir, 'user-command.md'), '# User Command\n');
|
|
|
|
const result = runGsdTools(
|
|
['detect-custom-files', '--config-dir', tmpDir],
|
|
tmpDir
|
|
);
|
|
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const json = JSON.parse(result.output);
|
|
const commandFiles = json.custom_files.filter(f => f.startsWith('command/'));
|
|
assert.strictEqual(
|
|
commandFiles.length, 0,
|
|
`command/ should not be scanned; got false positives: ${JSON.stringify(commandFiles)}`
|
|
);
|
|
});
|
|
});
|