93 lines
3.2 KiB
TypeScript
93 lines
3.2 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { createWorkerExecutionContext } from '../src/execution-policy.js';
|
|
|
|
describe('worker execution policy', () => {
|
|
let dataDir: string;
|
|
|
|
beforeEach(() => {
|
|
dataDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-worker-policy-'));
|
|
vi.stubEnv('WAGGLE_DATA_DIR', dataDir);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('derives a canonical tenant workspace from WAGGLE_DATA_DIR and teamId', () => {
|
|
const context = createWorkerExecutionContext('team-123');
|
|
const expected = fs.realpathSync.native(path.join(dataDir, 'teams', 'team-123', 'files'));
|
|
|
|
expect(context.workspaceDir).toBe(expected);
|
|
expect(fs.statSync(context.workspaceDir).isDirectory()).toBe(true);
|
|
});
|
|
|
|
it('rejects a team directory link that aliases another tenant', () => {
|
|
const teamsDir = path.join(dataDir, 'teams');
|
|
const otherTeamDir = path.join(teamsDir, 'team-other');
|
|
fs.mkdirSync(path.join(otherTeamDir, 'files'), { recursive: true });
|
|
fs.symlinkSync(
|
|
otherTeamDir,
|
|
path.join(teamsDir, 'team-requested'),
|
|
process.platform === 'win32' ? 'junction' : 'dir',
|
|
);
|
|
|
|
expect(() => createWorkerExecutionContext('team-requested')).toThrow('link or junction');
|
|
});
|
|
|
|
it('rejects a files directory link that widens the tenant root', () => {
|
|
const teamDir = path.join(dataDir, 'teams', 'team-123');
|
|
fs.mkdirSync(teamDir, { recursive: true });
|
|
fs.symlinkSync(
|
|
teamDir,
|
|
path.join(teamDir, 'files'),
|
|
process.platform === 'win32' ? 'junction' : 'dir',
|
|
);
|
|
|
|
expect(() => createWorkerExecutionContext('team-123')).toThrow('link or junction');
|
|
});
|
|
|
|
it('exposes exactly the read-only system tool pool', () => {
|
|
const context = createWorkerExecutionContext('team-123');
|
|
|
|
expect(context.tools.map(tool => tool.name)).toEqual([
|
|
'read_file',
|
|
'search_files',
|
|
'search_content',
|
|
'web_search',
|
|
'web_fetch',
|
|
]);
|
|
});
|
|
|
|
it('truthfully instructs the model to propose changes instead of mutating', () => {
|
|
const context = createWorkerExecutionContext('team-123');
|
|
|
|
expect(context.systemPrompt).toContain('non-interactive read-only worker');
|
|
expect(context.systemPrompt).toContain('cannot run shell commands, execute code, or write, edit, or delete files');
|
|
expect(context.systemPrompt).toContain('proposed patch');
|
|
});
|
|
|
|
it('fails closed without WAGGLE_DATA_DIR', () => {
|
|
vi.stubEnv('WAGGLE_DATA_DIR', ' ');
|
|
|
|
expect(() => createWorkerExecutionContext('team-123')).toThrow('WAGGLE_DATA_DIR');
|
|
});
|
|
|
|
it.each([
|
|
['empty', ''],
|
|
['current directory', '.'],
|
|
['parent directory', '..'],
|
|
['forward-slash traversal', '../outside'],
|
|
['backslash traversal', '..\\outside'],
|
|
['absolute Windows path', 'C:\\outside'],
|
|
['overlong identifier', 'a'.repeat(129)],
|
|
['missing runtime identifier', undefined as unknown as string],
|
|
['null runtime identifier', null as unknown as string],
|
|
])('rejects an unsafe %s teamId', (_label, teamId) => {
|
|
expect(() => createWorkerExecutionContext(teamId)).toThrow('Invalid teamId');
|
|
});
|
|
});
|