Merge remote-tracking branch 'origin/main' into capy/fixwave-C1-windows-setup-degrade

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

# Conflicts:
#	CHANGELOG.md
#	VERSION
#	agents-digest/gstack-AGENTS.md
#	package.json
This commit is contained in:
garrytan committed 2026-10-08 15:28:18 +00:00
commit 866d3f583e
10 files changed
+233 -34

No files matched your search

+33 -7
View File
@@ -1,10 +1,10 @@
import { afterEach, expect, test } from 'bun:test';
import { createHash } from 'node:crypto';
import { existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
import { existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join, resolve } from 'node:path';
import { spawnSync } from 'node:child_process';
import { prepareMethodology } from '../bin/gstack-autoplan-snapshot';
import { canonicalDestination, prepareMethodology } from '../bin/gstack-autoplan-snapshot';
const ROOT = resolve(import.meta.dir, '..');
const TOOL = join(ROOT, 'bin/gstack-autoplan-snapshot.ts');
@@ -41,7 +41,7 @@ test('actual raw R input initializes before scope and reaches a complete CEO dis
expect(missing.status).toBe(1);
expect(missing.stderr).toContain('Expected one Implementation plan section');
const initialized = invoke('init', f.source, f.source, f.restore);
expect(initialized.activePlan).toBe(f.source);
expect(initialized.activePlan).toBe(canonicalDestination(f.source));
expect(initialized.originalBytes).toBe(4607);
expect(initialized.originalSha256).toBe(hash(original));
expect(readFileSync(f.restore)).toEqual(original);
@@ -64,8 +64,8 @@ test('assigned active path preserves the separate original source and exact rest
if (emptyAssigned) writeFileSync(f.active, '');
const sourceMtime = statSync(f.source).mtimeMs;
const initialized = invoke('init', f.source, f.active, f.restore);
expect(initialized.activePlan).toBe(f.active);
expect(initialized.restorePath).toBe(f.restore);
expect(initialized.activePlan).toBe(canonicalDestination(f.active));
expect(initialized.restorePath).toBe(canonicalDestination(f.restore));
expect(readFileSync(f.source)).toEqual(original);
expect(statSync(f.source).mtimeMs).toBe(sourceMtime);
expect(readFileSync(f.restore)).toEqual(original);
@@ -79,8 +79,8 @@ test('the observed missing harness plans directory is initialized without a sepa
const active = join(f.dir, 'harness', 'plans', 'assigned.md');
const restore = join(f.dir, 'state', 'project', 'restore.md');
const initialized = invoke('init', f.source, active, restore);
expect(initialized.activePlan).toBe(active);
expect(initialized.restorePath).toBe(restore);
expect(initialized.activePlan).toBe(canonicalDestination(active));
expect(initialized.restorePath).toBe(canonicalDestination(restore));
expect(initialized.scope.dxRequired).toBe(true);
expect(readFileSync(f.source)).toEqual(original);
expect(readFileSync(restore)).toEqual(original);
@@ -241,3 +241,29 @@ catch (error) { console.error(error.message); process.exitCode = 1; }
expect(existsSync(active)).toBe(false);
expect(existsSync(restore)).toBe(false);
});
test.skipIf(process.platform === 'win32')('PAX-3638 Fix A: init through a symlinked plan directory reports the canonical destinations', () => {
const f = fixture();
const real = join(f.dir, 'real'), alias = join(f.dir, 'alias');
mkdirSync(real); symlinkSync(real, alias, 'dir');
const active = join(alias, 'nested', 'active.md'), restore = join(alias, 'restore.md');
const canonicalActive = join(realpathSync(real), 'nested', 'active.md');
expect(canonicalDestination(active)).toBe(canonicalActive);
expect(canonicalDestination(restore)).toBe(join(realpathSync(real), 'restore.md'));
const initialized = invoke('init', f.source, active, restore);
expect(initialized.activePlan).toBe(canonicalActive);
expect(initialized.restorePath).toBe(join(realpathSync(real), 'restore.md'));
expect(initialized.activePlan).not.toBe(active);
expect(readFileSync(canonicalActive, 'utf8')).toContain('## Implementation plan\n');
});
test('PAX-3638 Fix A: canonicalDestination is pure path math and leaves a leaf link or directory to initializePlan', () => {
const f = fixture();
const dirLeaf = join(f.dir, 'a-directory'); mkdirSync(dirLeaf);
expect(canonicalDestination(dirLeaf)).toBe(join(realpathSync(f.dir), 'a-directory'));
expect(existsSync(join(f.dir, 'missing', 'deeper'))).toBe(false);
expect(canonicalDestination(join(f.dir, 'missing', 'deeper', 'x.md'))).toBe(join(realpathSync(f.dir), 'missing', 'deeper', 'x.md'));
expect(existsSync(join(f.dir, 'missing'))).toBe(false);
// initializePlan still refuses a directory destination.
expect(cli('init', f.source, dirLeaf, f.restore).status).not.toBe(0);
});
+88
View File
@@ -1510,3 +1510,91 @@ describe('Autoplan ownership in a linked git worktree session', () => {
expect(output.hookSpecificOutput.permissionDecisionReason).toContain('code foreign_cwd');
});
});
describe('PAX-3638: symlinked plan directory and superseded failed inits', () => {
const skipWin = process.platform === 'win32';
/** The init Bash command spells active/restore through a symlinked directory; the journaled JSON stays canonical. */
function aliasFixture() {
const f = fixture();
const aliasDir = `${f.cwd}-alias`; dirs.push(aliasDir);
fs.symlinkSync(f.cwd, aliasDir, 'dir');
const aliasActive = path.join(aliasDir, 'active.md'), aliasRestore = path.join(aliasDir, 'restore.md');
const command = `bun "${ROOT}/bin/gstack-autoplan-snapshot.ts" init "${f.source}" "${aliasActive}" "${aliasRestore}"`;
(f.events[0] as any).input.command = command;
expect(f.init.activePlan).toBe(f.active); // canonical, unlike the argv above
expect(aliasActive).not.toBe(f.active);
const erroredInit = (id: string) => [
{ ...(structuredClone(f.events[0]!) as any), toolUseId: id },
{ ...(structuredClone(f.events[1]!) as any), toolUseId: id, isError: true, content: 'Error: Initialization source must be nonempty UTF-8 text' },
];
const ready = () => { f.message(); f.current(); f.reorder(); };
return { ...f, aliasDir, aliasActive, aliasRestore, command, erroredInit, ready };
}
test.skipIf(skipWin)('PAX-3638 Fix A: alias argv binds to init canonical paths', () => {
const f = aliasFixture(); f.ready();
expect(f.evaluate()).toEqual({ allow: true });
});
test.skipIf(skipWin)('PAX-3638 Fix B: errored first init followed by a correct init binds', () => {
const f = aliasFixture(); f.events.unshift(...f.erroredInit('bad-init') as any); f.ready();
expect(f.evaluate()).toEqual({ allow: true });
});
test.skipIf(skipWin)('PAX-3638 Fix C: errored init with no later success is init_failed and says re-run', () => {
const f = aliasFixture(); (f.events[1] as any).isError = true; f.ready();
const decision = f.evaluate() as any, reason: string = decision.reason;
expect(decision.allow).toBe(false); expect(reason).toContain('(code init_failed,');
expect(reason).toContain('re-run');
expect(reason).toContain('The most recent snapshot init call failed');
});
test.skipIf(skipWin)('PAX-3638 Fix B: a trailing errored init after a good bind is init_failed', () => {
const f = aliasFixture(); f.events.splice(2, 0, ...f.erroredInit('late-bad-init') as any); f.ready();
expect(f.evaluate()).toMatchObject({ allow: false, reason: expect.stringContaining('(code init_failed,') });
});
test.skipIf(skipWin)('PAX-3638 Fix B: a later good init supersedes a trailing errored one', () => {
const f = aliasFixture(); f.events.splice(2, 0, ...f.erroredInit('late-bad-init') as any);
f.events.splice(4, 0, { ...(structuredClone(f.events[0]!) as any), toolUseId: 'again' },
{ ...(structuredClone(f.events[1]!) as any), toolUseId: 'again', content: JSON.stringify({ ...f.init, reused: true }) });
f.ready();
expect(f.evaluate()).toEqual({ allow: true });
});
for (const field of ['activePlan', 'restorePath'] as const)
test.skipIf(skipWin)(`PAX-3638 Fix C: retargeted ${field} is init_mismatch without a re-run hint`, () => {
const f = aliasFixture(); (f.events[1] as any).content = JSON.stringify({ ...f.init, [field]: f.source }); f.ready();
const decision = f.evaluate() as any, reason: string = decision.reason;
expect(decision.allow).toBe(false); expect(reason).toContain('(code init_mismatch,');
expect(reason).toContain('do not match the canonical form of its own command arguments');
expect(reason).not.toContain('re-run');
});
test.skipIf(skipWin)('PAX-3638 Fix B: zero results for an init use still denies', () => {
const f = aliasFixture(); f.events.splice(1, 1); f.ready();
expect(f.evaluate()).toMatchObject({ allow: false, reason: expect.stringContaining('(code init_mismatch,') });
});
test.skipIf(skipWin)('PAX-3638 Fix A: an argv that cannot be canonicalized is init_mismatch, not a hook crash', () => {
const f = aliasFixture();
(f.events[0] as any).input.command = `bun "${ROOT}/bin/gstack-autoplan-snapshot.ts" init "${f.source}" "${f.source}/not-a-dir/active.md" "${f.aliasRestore}"`;
f.ready();
expect(f.evaluate()).toMatchObject({ allow: false, reason: expect.stringContaining('(code init_mismatch,') });
});
test.skipIf(skipWin)('PAX-3638 Fix A: a mistyped tool path is unbindable, and a later correct init still binds', () => {
const f = aliasFixture();
const bad = { ...(structuredClone(f.events[0]!) as any), toolUseId: 'typo' };
bad.input.command = bad.input.command.replace('gstack-autoplan-snapshot.ts', 'gstack-autoplan-snapshto.ts');
f.events.unshift(bad); f.ready();
expect(f.evaluate()).toEqual({ allow: true });
});
test.skipIf(skipWin)('PAX-3638 Fix A2: a pending Edit through the alias spelling is the same file', () => {
const f = aliasFixture(); f.message();
f.use('alias-edit', 'Edit', { file_path: f.aliasActive, old_string: 'Keep documented behavior.', new_string: 'Implement new behavior.' });
f.current(); f.reorder();
expect(f.evaluate()).toMatchObject({ allow: false, reason: expect.stringContaining('(code mutation_pending,') });
});
});