fix(gateway): harden build workspace boundaries

This commit is contained in:
2026-07-31 17:58:12 +00:00
parent a112dced68
commit 2596c47fc5
4 changed files with 176 additions and 8 deletions
+43
View File
@@ -0,0 +1,43 @@
import path from 'node:path';
import { describe, expect, it } from 'vitest';
import { MAX_BUILD_OUTPUT_CHARS, PnpmBuildRunner } from '../src/orchestrator/buildRunner.js';
describe('PnpmBuildRunner', () => {
it('returns a failed result when a command cannot be spawned', async () => {
const runner = new PnpmBuildRunner();
const result = await runner.run([
{
command: path.join(process.cwd(), 'missing-build-command'),
args: [],
cwd: process.cwd(),
},
]);
expect(result.ok).toBe(false);
expect(result.exitCode).toBeNull();
expect(result.output).toContain('ENOENT');
});
it('retains only a bounded tail across command output', async () => {
const runner = new PnpmBuildRunner();
const result = await runner.run([
{
command: process.execPath,
args: ['-e', `process.stdout.write('a'.repeat(${MAX_BUILD_OUTPUT_CHARS}));`],
cwd: process.cwd(),
},
{
command: process.execPath,
args: ['-e', "process.stdout.write('tail-marker');"],
cwd: process.cwd(),
},
]);
expect(result.ok).toBe(true);
expect(result.output.length).toBe(MAX_BUILD_OUTPUT_CHARS);
expect(result.output.endsWith('tail-marker')).toBe(true);
});
});
@@ -85,4 +85,65 @@ describe('GitWorkspaceManager source resolution', () => {
await expect(manager.resolveCommit('BRANCH', '--upload-pack=bad')).rejects.toThrow('Invalid git ref');
await expect(manager.resolveCommit('COMMIT', 'HEAD..main')).rejects.toThrow('Invalid git ref');
});
it('reuses only a clean registered worktree at the requested commit', async () => {
const fixture = createRepositoryFixture();
const manager = new GitWorkspaceManager({
repoRoot: fixture.checkout,
worktreeRoot: fixture.worktrees,
});
const created = await manager.prepare(fixture.firstCommit);
expect(created.created).toBe(true);
const reused = await manager.prepare(fixture.firstCommit);
expect(reused).toMatchObject({ root: created.root, created: false });
fs.writeFileSync(path.join(created.root, 'untracked.txt'), 'dirty\n');
await expect(manager.prepare(fixture.firstCommit)).rejects.toThrow('uncommitted changes');
});
it('rejects an unregistered directory that occupies a commit workspace path', async () => {
const fixture = createRepositoryFixture();
const manager = new GitWorkspaceManager({
repoRoot: fixture.checkout,
worktreeRoot: fixture.worktrees,
});
const occupied = path.join(fixture.worktrees, fixture.firstCommit);
fs.mkdirSync(occupied, { recursive: true });
await expect(manager.prepare(fixture.firstCommit)).rejects.toThrow('not registered as a git worktree');
});
it('removes only registered direct commit workspaces and never the root or sibling prefixes', async () => {
const fixture = createRepositoryFixture();
const manager = new GitWorkspaceManager({
repoRoot: fixture.checkout,
worktreeRoot: fixture.worktrees,
});
const workspace = await manager.prepare(fixture.firstCommit);
const siblingPrefix = `${fixture.worktrees}-outside`;
fs.mkdirSync(siblingPrefix, { recursive: true });
await expect(manager.remove(fixture.worktrees)).rejects.toThrow('must be a child');
await expect(manager.remove(path.join(siblingPrefix, fixture.firstCommit))).rejects.toThrow('must be a child');
expect(fs.existsSync(siblingPrefix)).toBe(true);
await expect(manager.remove(workspace.root)).resolves.toBe(true);
expect(fs.existsSync(workspace.root)).toBe(false);
});
it('rejects deletion of unregistered and nested paths under the worktree root', async () => {
const fixture = createRepositoryFixture();
const manager = new GitWorkspaceManager({
repoRoot: fixture.checkout,
worktreeRoot: fixture.worktrees,
});
const unregistered = path.join(fixture.worktrees, fixture.firstCommit);
fs.mkdirSync(unregistered, { recursive: true });
await expect(manager.remove(unregistered)).rejects.toThrow('not registered as a git worktree');
await expect(manager.remove(path.join(unregistered, 'nested'))).rejects.toThrow(
'not a managed commit workspace'
);
expect(fs.existsSync(unregistered)).toBe(true);
});
});