Files

147 lines
6.4 KiB
JavaScript
Raw Permalink Normal View History

import { describe, expect, it, vi } from 'vitest';
import { createPermissionAutoAcceptRuntime } from './runtime.js';
const createRuntime = ({ stored, fetchImpl, retryDelaysMs = [0] } = {}) => {
let settings = stored ?? { permissionAutoAccept: { sessions: {} } };
let eventHandler;
let statusHandler;
const runtime = createPermissionAutoAcceptRuntime({
globalEventHub: {
subscribeEvent(handler) { eventHandler = handler; return () => {}; },
subscribeStatus(handler) { statusHandler = handler; return () => {}; },
},
buildOpenCodeUrl: (path) => `http://opencode.test${path}`,
getOpenCodeAuthHeaders: () => ({}),
readSettingsFromDiskMigrated: async () => settings,
persistSettings: async (changes) => { settings = { ...settings, ...changes }; },
fetchImpl: fetchImpl ?? vi.fn(async () => new Response('[]')),
retryDelaysMs,
});
runtime.start();
return {
runtime,
getSettings: () => settings,
emit: (payload, directory = '/project') => eventHandler({ payload, directory }),
connect: () => statusHandler({ type: 'connect' }),
};
};
const flush = async () => {
for (let index = 0; index < 20; index += 1) await Promise.resolve();
};
describe('permission auto-accept runtime', () => {
it('persists explicit session policies across runtime restarts', async () => {
const first = createRuntime();
await first.runtime.setSessionPolicy('root', true);
const second = createRuntime({ stored: first.getSettings() });
await expect(second.runtime.load()).resolves.toEqual({
sessions: { root: true },
revision: 1,
});
});
it('increments the authoritative policy revision', async () => {
const { runtime, getSettings } = createRuntime();
await expect(runtime.setSessionPolicy('root', true)).resolves.toMatchObject({ revision: 1 });
await expect(runtime.setSessionPolicy('child', false)).resolves.toMatchObject({ revision: 2 });
expect(getSettings().permissionAutoAccept.revision).toBe(2);
});
it('uses nearest explicit ancestor policy for subagents', async () => {
const { runtime, emit } = createRuntime({
stored: { permissionAutoAccept: { sessions: { root: true, child: false } } },
});
emit({ type: 'session.created', properties: { info: { id: 'child', parentID: 'root' } } });
emit({ type: 'session.created', properties: { info: { id: 'grandchild', parentID: 'child' } } });
await expect(runtime.isSessionAutoAccepting('grandchild', '/project')).resolves.toBe(false);
await runtime.setSessionPolicy('child', true);
await expect(runtime.isSessionAutoAccepting('grandchild', '/project')).resolves.toBe(true);
});
it('fetches missing subagent lineage before replying', async () => {
const fetchImpl = vi.fn(async (url, init = {}) => {
const path = new URL(url).pathname;
if (path === '/permission') return new Response('[]');
if (path === '/session/child') return Response.json({ id: 'child', parentID: 'root', directory: '/project' });
if (init.method === 'POST') return Response.json({});
return new Response('', { status: 404 });
});
const { runtime } = createRuntime({
stored: { permissionAutoAccept: { sessions: { root: true } } },
fetchImpl,
});
await expect(runtime.processPermission({ id: 'perm', sessionID: 'child' }, '/project')).resolves.toBe(true);
expect(fetchImpl.mock.calls.some(([url, init]) => new URL(url).pathname === '/permission/perm/reply' && init.method === 'POST')).toBe(true);
});
it('retries a transient reply failure and deduplicates concurrent events', async () => {
let replyAttempts = 0;
const fetchImpl = vi.fn(async (url, init = {}) => {
const path = new URL(url).pathname;
if (path === '/permission') return new Response('[]');
if (path === '/permission/perm/reply' && init.method === 'POST') {
replyAttempts += 1;
return replyAttempts === 1 ? new Response('', { status: 503 }) : Response.json({});
}
return Response.json({ id: 'root' });
});
const { runtime } = createRuntime({
stored: { permissionAutoAccept: { sessions: { root: true } } },
fetchImpl,
retryDelaysMs: [0, 0],
});
const permission = { id: 'perm', sessionID: 'root' };
const first = runtime.processPermission(permission, '/project');
const second = runtime.processPermission(permission, '/project');
await expect(Promise.all([first, second])).resolves.toEqual([true, true]);
expect(replyAttempts).toBe(2);
});
it('reconciles pending permissions after reconnect', async () => {
const fetchImpl = vi.fn(async (url, init = {}) => {
const path = new URL(url).pathname;
if (path === '/permission') return Response.json([{ id: 'pending', sessionID: 'root' }]);
if (path === '/permission/pending/reply' && init.method === 'POST') return Response.json({});
return Response.json({ id: 'root' });
});
const { connect } = createRuntime({
stored: { permissionAutoAccept: { sessions: { root: true } } },
fetchImpl,
});
connect();
await flush();
expect(fetchImpl.mock.calls.some(([url]) => new URL(url).pathname === '/permission/pending/reply')).toBe(true);
});
it('accepts existing pending permissions when a session policy is enabled', async () => {
const fetchImpl = vi.fn(async (url, init = {}) => {
const parsed = new URL(url);
const path = parsed.pathname;
if (path === '/permission') {
return parsed.searchParams.get('directory') === '/project'
? Response.json([
{ id: 'root-pending', sessionID: 'root' },
{ id: 'other-pending', sessionID: 'other' },
])
: Response.json([]);
}
if (path === '/permission/root-pending/reply' && init.method === 'POST') return Response.json({});
if (path === '/session/other') return Response.json({ id: 'other' });
return new Response('', { status: 404 });
});
const { runtime } = createRuntime({ fetchImpl });
await runtime.setSessionPolicy('root', true, '/project');
const replyPaths = fetchImpl.mock.calls
.filter(([, init]) => init?.method === 'POST')
.map(([url]) => new URL(url).pathname);
expect(replyPaths).toEqual(['/permission/root-pending/reply']);
expect(fetchImpl.mock.calls.some(([url]) => new URL(url).searchParams.get('directory') === '/project')).toBe(true);
expect(await runtime.load()).toEqual({ sessions: { root: true }, revision: 1 });
});
});