Three parallel diagnostic agents (G: sdk/install path-sep, H: hook scripts, I: worktree/workspace) characterised the remaining windows-22 failures on23b52f1a. Patches by class: CRLF in test parsers / file-content reads - bug-2136-sh-hook-version: shebang check split('\n') → split(/\r?\n/) - bug-3542-executor-git-stash-prohibition: strip \r from read content (git rewrites stashed text with CRLF on win autocrlf=true checkout) - workspace: BOM-strip + explicit \r strip in parseCommandFile (BOM at byte 0 defeats /^---/ anchor → fmMatch null → fm.name undefined) Windows path-separator / 8.3-shortname normalization - bug-3491-nested-git-worktree: use fs.realpathSync.native to expand %TEMP% RUNNER~1 → runneradmin; normalize sep before path-equality - prune-orphaned-worktrees: normalize \\→/ before substring includes (git emits forward-slash in --porcelain on Windows even when path.join produced backslashes) - bug-3017-codex-hook-absolute-node: accept POSIX path OR path with drive-letter prefix in hookPath equality assertion - bug-3126-global-skills-base-runtime-path: use path.join for expected /xdg/<runtime> values (production calls path.join → \xdg\… on win32) Test under-specified platform / forgot win32 env - bug-2979-hook-absolute-node: pass {platform:'linux'} to buildHookCommand + rewriteLegacyManagedNodeHookCommands so the POSIX-branch tests don't pick up the #3393 GitBash code path - bug-3288-model-catalog + bug-3571-config-manifest: also set USERPROFILE alongside HOME so os.homedir() on win32 redirects to the test fixture instead of the runner's real ~ External-cmd resolution - bug-2647-outer-tarball-sdk-dist: use npm.cmd + {shell:true} on win32 so execFileSync resolves PATHEXT (literal `npm` is ENOENT) ESM loader: tests/runtime-bridge-sync-smoke.test.cjs already migrated to pathToFileURL in23b52f1a(cluster E). Explicit per-test/describe skip on win32 (with required string reasons to satisfy no-unconditional-win32-skip guard) - feat-3347-graphify-auto-update-hook: 3 describes — harness spawns bash/kill/sleep + the hook itself is bash - feat-3595-fs-fault-injection: move \t and \n filenames into the POSIX-only branch (NTFS forbids 0x00–0x1F in filenames) - bug-2775/2829/3033/3231/3359: POSIX shim under ~/.local/bin with chmod 0o755 — not how Windows install works - bug-3211: single subtest where cp.execSync reassignment isn't picked up on win32 (POSIX coverage via the mock; live windows behavior covered by 3211-D which keeps running) - install-path-detection: parses sh-style export PATH= rc files; Windows has no rc files (registry Path) - worktree-safety-policy: single test using POSIX /repo/wt fixture paths that can't be expressed under win32 path.resolve Validated: plex2 (ubuntu docker) 11224/0 pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
277 lines
9.7 KiB
JavaScript
277 lines
9.7 KiB
JavaScript
'use strict';
|
|
|
|
const { describe, test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
|
|
const {
|
|
resolveWorktreeContext,
|
|
parseWorktreePorcelain,
|
|
planWorktreePrune,
|
|
executeWorktreePrunePlan,
|
|
listLinkedWorktreePaths,
|
|
inspectWorktreeHealth,
|
|
snapshotWorktreeInventory,
|
|
} = require('../get-shit-done/bin/lib/worktree-safety.cjs');
|
|
|
|
const isWindows = process.platform === 'win32';
|
|
|
|
describe('worktree-safety policy module', () => {
|
|
test('resolveWorktreeContext prefers current directory when .planning exists', () => {
|
|
const context = resolveWorktreeContext('/repo/wt', {
|
|
existsSync: () => true,
|
|
execGit: () => ({ exitCode: 1, stdout: '', stderr: '' }),
|
|
});
|
|
assert.strictEqual(context.effectiveRoot, '/repo/wt');
|
|
assert.strictEqual(context.reason, 'has_local_planning');
|
|
assert.strictEqual(context.mode, 'current_directory');
|
|
});
|
|
|
|
test('resolveWorktreeContext maps linked worktree to common-dir parent',
|
|
{ skip: isWindows ? 'POSIX-rooted fixture paths cannot be expressed on Windows path.resolve; resolveWorktreeContext uses platform-native path module and would prepend a drive letter to "/repo" inputs. Behaviour is covered indirectly by real-fs worktree tests.' : false },
|
|
() => {
|
|
const context = resolveWorktreeContext('/repo/wt', {
|
|
existsSync: () => false,
|
|
execGit: (args) => {
|
|
if (args[1] === '--git-dir') return { exitCode: 0, stdout: '.git/worktrees/wt', stderr: '' };
|
|
if (args[1] === '--git-common-dir') return { exitCode: 0, stdout: '../.git', stderr: '' };
|
|
return { exitCode: 1, stdout: '', stderr: '' };
|
|
},
|
|
});
|
|
assert.strictEqual(context.effectiveRoot, '/repo');
|
|
assert.strictEqual(context.reason, 'linked_worktree');
|
|
assert.strictEqual(context.mode, 'linked_worktree_root');
|
|
});
|
|
|
|
test('resolveWorktreeContext falls back when git metadata is unavailable', () => {
|
|
const context = resolveWorktreeContext('/repo/wt', {
|
|
existsSync: () => false,
|
|
execGit: () => ({ exitCode: 1, stdout: '', stderr: '' }),
|
|
});
|
|
assert.strictEqual(context.effectiveRoot, '/repo/wt');
|
|
assert.strictEqual(context.reason, 'not_git_repo');
|
|
});
|
|
|
|
test('resolveWorktreeContext keeps cwd for main worktree checkout', () => {
|
|
const context = resolveWorktreeContext('/repo/main', {
|
|
existsSync: () => false,
|
|
execGit: (args) => {
|
|
if (args[1] === '--git-dir') return { exitCode: 0, stdout: '.git', stderr: '' };
|
|
if (args[1] === '--git-common-dir') return { exitCode: 0, stdout: '.git', stderr: '' };
|
|
return { exitCode: 1, stdout: '', stderr: '' };
|
|
},
|
|
});
|
|
assert.strictEqual(context.effectiveRoot, '/repo/main');
|
|
assert.strictEqual(context.reason, 'main_worktree');
|
|
assert.strictEqual(context.mode, 'current_directory');
|
|
});
|
|
|
|
test('parseWorktreePorcelain skips detached HEAD entries', () => {
|
|
const porcelain = [
|
|
'worktree /repo/main',
|
|
'HEAD deadbeef',
|
|
'branch refs/heads/main',
|
|
'',
|
|
'worktree /repo/wt-detached',
|
|
'HEAD cafe1234',
|
|
'detached',
|
|
'',
|
|
'worktree /repo/wt-feature',
|
|
'HEAD f00dbabe',
|
|
'branch refs/heads/feature-x',
|
|
'',
|
|
].join('\n');
|
|
const parsed = parseWorktreePorcelain(porcelain);
|
|
assert.deepStrictEqual(parsed, [
|
|
{ path: '/repo/main', branch: 'main' },
|
|
{ path: '/repo/wt-feature', branch: 'feature-x' },
|
|
]);
|
|
});
|
|
|
|
test('planWorktreePrune is non-destructive by default', () => {
|
|
const plan = planWorktreePrune('/repo/main', {}, {
|
|
execGit: () => ({ exitCode: 0, stdout: 'worktree /repo/main\nbranch refs/heads/main\n', stderr: '' }),
|
|
parseWorktreePorcelain: () => [{ path: '/repo/main', branch: 'main' }],
|
|
});
|
|
assert.strictEqual(plan.action, 'metadata_prune_only');
|
|
assert.strictEqual(plan.reason, 'worktrees_present');
|
|
assert.strictEqual(plan.destructiveModeRequested, false);
|
|
});
|
|
|
|
test('planWorktreePrune keeps metadata-prune action when destructive mode is requested (scaffold)', () => {
|
|
const plan = planWorktreePrune('/repo/main', { allowDestructive: true }, {
|
|
execGit: () => ({ exitCode: 0, stdout: '', stderr: '' }),
|
|
parseWorktreePorcelain: () => [],
|
|
});
|
|
assert.strictEqual(plan.action, 'metadata_prune_only');
|
|
assert.strictEqual(plan.reason, 'no_worktrees');
|
|
assert.strictEqual(plan.destructiveModeRequested, true);
|
|
});
|
|
|
|
test('planWorktreePrune skips when git worktree list fails', () => {
|
|
const plan = planWorktreePrune('/repo/main', {}, {
|
|
execGit: () => ({ exitCode: 2, stdout: '', stderr: 'fatal' }),
|
|
});
|
|
assert.strictEqual(plan.action, 'skip');
|
|
assert.strictEqual(plan.reason, 'git_list_failed');
|
|
});
|
|
|
|
test('planWorktreePrune still metadata-prunes when porcelain parser throws', () => {
|
|
const plan = planWorktreePrune('/repo/main', {}, {
|
|
execGit: () => ({ exitCode: 0, stdout: 'not-porcelain', stderr: '' }),
|
|
parseWorktreePorcelain: () => {
|
|
throw new Error('parse failed');
|
|
},
|
|
});
|
|
assert.strictEqual(plan.action, 'metadata_prune_only');
|
|
assert.strictEqual(plan.reason, 'no_worktrees');
|
|
});
|
|
|
|
test('executeWorktreePrunePlan runs git worktree prune for metadata plan', () => {
|
|
const calls = [];
|
|
const result = executeWorktreePrunePlan(
|
|
{ repoRoot: '/repo/main', action: 'metadata_prune_only', reason: 'worktrees_present' },
|
|
{
|
|
execGit: (args, opts) => {
|
|
calls.push({ cwd: opts.cwd, args });
|
|
return { exitCode: 0, stdout: '', stderr: '' };
|
|
},
|
|
}
|
|
);
|
|
assert.strictEqual(result.ok, true);
|
|
assert.deepStrictEqual(calls, [{ cwd: '/repo/main', args: ['worktree', 'prune'] }]);
|
|
});
|
|
|
|
test('executeWorktreePrunePlan returns skip for missing plan', () => {
|
|
const result = executeWorktreePrunePlan(null, {
|
|
execGit: () => ({ exitCode: 0, stdout: '', stderr: '' }),
|
|
});
|
|
assert.strictEqual(result.ok, false);
|
|
assert.strictEqual(result.action, 'skip');
|
|
assert.strictEqual(result.reason, 'missing_plan');
|
|
});
|
|
|
|
test('executeWorktreePrunePlan returns skip plan unchanged without git call', () => {
|
|
let called = false;
|
|
const result = executeWorktreePrunePlan(
|
|
{ repoRoot: '/repo/main', action: 'skip', reason: 'git_list_failed' },
|
|
{
|
|
execGit: () => {
|
|
called = true;
|
|
return { exitCode: 0, stdout: '', stderr: '' };
|
|
},
|
|
}
|
|
);
|
|
assert.strictEqual(result.ok, false);
|
|
assert.strictEqual(result.action, 'skip');
|
|
assert.strictEqual(result.reason, 'git_list_failed');
|
|
assert.strictEqual(called, false);
|
|
});
|
|
|
|
test('executeWorktreePrunePlan rejects unsupported actions', () => {
|
|
const result = executeWorktreePrunePlan(
|
|
{ repoRoot: '/repo/main', action: 'remove_missing_paths', reason: 'explicit' },
|
|
{
|
|
execGit: () => ({ exitCode: 0, stdout: '', stderr: '' }),
|
|
}
|
|
);
|
|
assert.strictEqual(result.ok, false);
|
|
assert.strictEqual(result.action, 'remove_missing_paths');
|
|
assert.strictEqual(result.reason, 'unsupported_action');
|
|
});
|
|
|
|
test('listLinkedWorktreePaths parses porcelain and skips first/main path', () => {
|
|
const listed = listLinkedWorktreePaths('/repo/main', {
|
|
execGit: () => ({
|
|
exitCode: 0,
|
|
stdout: [
|
|
'worktree /repo/main',
|
|
'HEAD aaa',
|
|
'branch refs/heads/main',
|
|
'',
|
|
'worktree /repo/wt-a',
|
|
'HEAD bbb',
|
|
'branch refs/heads/feat-a',
|
|
'',
|
|
'worktree /repo/wt-b',
|
|
'HEAD ccc',
|
|
'detached',
|
|
'',
|
|
].join('\n'),
|
|
stderr: '',
|
|
}),
|
|
});
|
|
assert.strictEqual(listed.ok, true);
|
|
assert.deepStrictEqual(listed.paths, ['/repo/wt-a', '/repo/wt-b']);
|
|
});
|
|
|
|
test('inspectWorktreeHealth reports orphan and stale findings', () => {
|
|
const health = inspectWorktreeHealth(
|
|
'/repo/main',
|
|
{ staleAfterMs: 60 * 60 * 1000, nowMs: 2 * 60 * 60 * 1000 },
|
|
{
|
|
execGit: () => ({
|
|
exitCode: 0,
|
|
stdout: [
|
|
'worktree /repo/main',
|
|
'HEAD aaa',
|
|
'branch refs/heads/main',
|
|
'',
|
|
'worktree /repo/wt-orphan',
|
|
'HEAD bbb',
|
|
'branch refs/heads/feat-a',
|
|
'',
|
|
'worktree /repo/wt-stale',
|
|
'HEAD ccc',
|
|
'branch refs/heads/feat-b',
|
|
'',
|
|
].join('\n'),
|
|
stderr: '',
|
|
}),
|
|
existsSync: p => p !== '/repo/wt-orphan',
|
|
statSync: () => ({ mtimeMs: 0 }),
|
|
}
|
|
);
|
|
|
|
assert.strictEqual(health.ok, true);
|
|
assert.deepStrictEqual(health.findings, [
|
|
{ kind: 'orphan', path: '/repo/wt-orphan' },
|
|
{ kind: 'stale', path: '/repo/wt-stale', ageMinutes: 120 },
|
|
]);
|
|
});
|
|
|
|
test('snapshotWorktreeInventory returns typed linked-worktree entries', () => {
|
|
const inventory = snapshotWorktreeInventory(
|
|
'/repo/main',
|
|
{ staleAfterMs: 60 * 60 * 1000, nowMs: 2 * 60 * 60 * 1000 },
|
|
{
|
|
execGit: () => ({
|
|
exitCode: 0,
|
|
stdout: [
|
|
'worktree /repo/main',
|
|
'HEAD aaa',
|
|
'branch refs/heads/main',
|
|
'',
|
|
'worktree /repo/wt-a',
|
|
'HEAD bbb',
|
|
'branch refs/heads/feat-a',
|
|
'',
|
|
'worktree /repo/wt-b',
|
|
'HEAD ccc',
|
|
'branch refs/heads/feat-b',
|
|
'',
|
|
].join('\n'),
|
|
stderr: '',
|
|
}),
|
|
existsSync: p => p !== '/repo/wt-b',
|
|
statSync: () => ({ mtimeMs: 0 }),
|
|
}
|
|
);
|
|
|
|
assert.strictEqual(inventory.ok, true);
|
|
assert.deepStrictEqual(inventory.entries, [
|
|
{ path: '/repo/wt-a', exists: true, isStale: true, ageMinutes: 120 },
|
|
{ path: '/repo/wt-b', exists: false, isStale: false, ageMinutes: null },
|
|
]);
|
|
});
|
|
});
|