chore(cowork-service): type test doubles
Replace cowork-service test any casts with structural test doubles for IPC bridges, Fastify request payloads, and private IPC bridge probes. Verification: pnpm --filter @lysnrai/cowork-service build; pnpm --filter @lysnrai/cowork-service test; pnpm --filter @lysnrai/cowork-service exec eslint . --ext .ts,.tsx; pnpm lint.
This commit is contained in:
parent
9625999ad4
commit
6978ddb209
@ -1,6 +1,15 @@
|
||||
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
||||
import { IpcBridge, getIpcBridge, setIpcBridge } from './ipc-bridge.js';
|
||||
|
||||
type TestableIpcBridge = IpcBridge & {
|
||||
handleLine(line: string): void;
|
||||
sendResponse(id: number, result?: unknown, error?: { code: number; message: string }): void;
|
||||
};
|
||||
|
||||
function asTestableBridge(bridge: IpcBridge): TestableIpcBridge {
|
||||
return bridge as unknown as TestableIpcBridge;
|
||||
}
|
||||
|
||||
vi.mock('./config.js', () => ({
|
||||
config: {
|
||||
RUST_RUNTIME_BIN: 'echo',
|
||||
@ -88,7 +97,9 @@ describe('IpcBridge convenience methods', () => {
|
||||
it('getTaskStatus builds correct params', async () => {
|
||||
const bridge = new IpcBridge();
|
||||
const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
|
||||
jsonrpc: '2.0', id: 2, result: { id: 'task-1', status: 'running' },
|
||||
jsonrpc: '2.0',
|
||||
id: 2,
|
||||
result: { id: 'task-1', status: 'running' },
|
||||
});
|
||||
|
||||
const auth = { userId: 'u1' };
|
||||
@ -99,27 +110,39 @@ describe('IpcBridge convenience methods', () => {
|
||||
it('cancelTask builds correct params', async () => {
|
||||
const bridge = new IpcBridge();
|
||||
const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
|
||||
jsonrpc: '2.0', id: 3, result: { status: 'cancelled' },
|
||||
jsonrpc: '2.0',
|
||||
id: 3,
|
||||
result: { status: 'cancelled' },
|
||||
});
|
||||
|
||||
await bridge.cancelTask('task-1', { userId: 'u1' });
|
||||
expect(callSpy).toHaveBeenCalledWith('cancel_task', { taskId: 'task-1', auth: { userId: 'u1' } });
|
||||
expect(callSpy).toHaveBeenCalledWith('cancel_task', {
|
||||
taskId: 'task-1',
|
||||
auth: { userId: 'u1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('listTasks with status filter', async () => {
|
||||
const bridge = new IpcBridge();
|
||||
const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
|
||||
jsonrpc: '2.0', id: 4, result: { tasks: [] },
|
||||
jsonrpc: '2.0',
|
||||
id: 4,
|
||||
result: { tasks: [] },
|
||||
});
|
||||
|
||||
await bridge.listTasks({ userId: 'u1' }, 'pending');
|
||||
expect(callSpy).toHaveBeenCalledWith('list_tasks', { auth: { userId: 'u1' }, status: 'pending' });
|
||||
expect(callSpy).toHaveBeenCalledWith('list_tasks', {
|
||||
auth: { userId: 'u1' },
|
||||
status: 'pending',
|
||||
});
|
||||
});
|
||||
|
||||
it('listTasks without status filter', async () => {
|
||||
const bridge = new IpcBridge();
|
||||
const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
|
||||
jsonrpc: '2.0', id: 5, result: { tasks: [] },
|
||||
jsonrpc: '2.0',
|
||||
id: 5,
|
||||
result: { tasks: [] },
|
||||
});
|
||||
|
||||
await bridge.listTasks({ userId: 'u1' });
|
||||
@ -129,10 +152,15 @@ describe('IpcBridge convenience methods', () => {
|
||||
it('updateFlags builds correct params', async () => {
|
||||
const bridge = new IpcBridge();
|
||||
const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
|
||||
jsonrpc: '2.0', id: 6, result: { updated: 2 },
|
||||
jsonrpc: '2.0',
|
||||
id: 6,
|
||||
result: { updated: 2 },
|
||||
});
|
||||
|
||||
await bridge.updateFlags({ sandbox_enabled: true, plugins_enabled: false }, { userId: 'admin' });
|
||||
await bridge.updateFlags(
|
||||
{ sandbox_enabled: true, plugins_enabled: false },
|
||||
{ userId: 'admin' }
|
||||
);
|
||||
expect(callSpy).toHaveBeenCalledWith('update_flags', {
|
||||
flags: { sandbox_enabled: true, plugins_enabled: false },
|
||||
auth: { userId: 'admin' },
|
||||
@ -142,7 +170,9 @@ describe('IpcBridge convenience methods', () => {
|
||||
it('flushAudit builds correct params', async () => {
|
||||
const bridge = new IpcBridge();
|
||||
const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
|
||||
jsonrpc: '2.0', id: 7, result: { count: 0, entries: [] },
|
||||
jsonrpc: '2.0',
|
||||
id: 7,
|
||||
result: { count: 0, entries: [] },
|
||||
});
|
||||
|
||||
await bridge.flushAudit({ userId: 'admin' });
|
||||
@ -152,7 +182,9 @@ describe('IpcBridge convenience methods', () => {
|
||||
it('recordSpend builds correct params', async () => {
|
||||
const bridge = new IpcBridge();
|
||||
const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
|
||||
jsonrpc: '2.0', id: 8, result: { totalCostUsd: 0.015, allowed: true },
|
||||
jsonrpc: '2.0',
|
||||
id: 8,
|
||||
result: { totalCostUsd: 0.015, allowed: true },
|
||||
});
|
||||
|
||||
await bridge.recordSpend('gpt-4o', 1000, 500, 0.015, { userId: 'u1' }, 'task-1');
|
||||
@ -169,7 +201,9 @@ describe('IpcBridge convenience methods', () => {
|
||||
it('updateVerdict builds correct params', async () => {
|
||||
const bridge = new IpcBridge();
|
||||
const callSpy = vi.spyOn(bridge, 'call').mockResolvedValue({
|
||||
jsonrpc: '2.0', id: 9, result: { allowed: false },
|
||||
jsonrpc: '2.0',
|
||||
id: 9,
|
||||
result: { allowed: false },
|
||||
});
|
||||
|
||||
await bridge.updateVerdict({ verdict: 'block', spent_usd: 10 }, { userId: 'admin' });
|
||||
@ -191,7 +225,9 @@ describe('reverse IPC (incoming requests)', () => {
|
||||
|
||||
it('handleLine routes incoming requests to the registered handler', async () => {
|
||||
const bridge = new IpcBridge();
|
||||
const handler = vi.fn().mockResolvedValue({ response: 'hello', provider: 'test', model: 'test-model' });
|
||||
const handler = vi
|
||||
.fn()
|
||||
.mockResolvedValue({ response: 'hello', provider: 'test', model: 'test-model' });
|
||||
bridge.onIncomingRequest(handler);
|
||||
|
||||
// Simulate an incoming JSON-RPC request line from Rust
|
||||
@ -202,11 +238,11 @@ describe('reverse IPC (incoming requests)', () => {
|
||||
params: { messages: [{ role: 'user', content: 'hi' }], model: 'test-model' },
|
||||
});
|
||||
|
||||
// Access private handleLine via any cast (test-only)
|
||||
(bridge as any).handleLine(incomingLine);
|
||||
// Access private handleLine through the typed test bridge.
|
||||
asTestableBridge(bridge).handleLine(incomingLine);
|
||||
|
||||
// Wait for async handler
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
|
||||
expect(handler).toHaveBeenCalledWith('intercept_llm', {
|
||||
messages: [{ role: 'user', content: 'hi' }],
|
||||
@ -216,7 +252,7 @@ describe('reverse IPC (incoming requests)', () => {
|
||||
|
||||
it('handleLine sends error response when no handler is registered', () => {
|
||||
const bridge = new IpcBridge();
|
||||
const sendSpy = vi.spyOn(bridge as any, 'sendResponse');
|
||||
const sendSpy = vi.spyOn(asTestableBridge(bridge), 'sendResponse');
|
||||
|
||||
const incomingLine = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
@ -225,7 +261,7 @@ describe('reverse IPC (incoming requests)', () => {
|
||||
params: {},
|
||||
});
|
||||
|
||||
(bridge as any).handleLine(incomingLine);
|
||||
asTestableBridge(bridge).handleLine(incomingLine);
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(99, undefined, {
|
||||
code: -32601,
|
||||
@ -237,7 +273,7 @@ describe('reverse IPC (incoming requests)', () => {
|
||||
const bridge = new IpcBridge();
|
||||
const handler = vi.fn().mockRejectedValue(new Error('LLM router not initialized'));
|
||||
bridge.onIncomingRequest(handler);
|
||||
const sendSpy = vi.spyOn(bridge as any, 'sendResponse');
|
||||
const sendSpy = vi.spyOn(asTestableBridge(bridge), 'sendResponse');
|
||||
|
||||
const incomingLine = JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
@ -246,8 +282,8 @@ describe('reverse IPC (incoming requests)', () => {
|
||||
params: { messages: [] },
|
||||
});
|
||||
|
||||
(bridge as any).handleLine(incomingLine);
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
asTestableBridge(bridge).handleLine(incomingLine);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
|
||||
expect(sendSpy).toHaveBeenCalledWith(50, undefined, {
|
||||
code: -32000,
|
||||
@ -262,12 +298,12 @@ describe('reverse IPC (incoming requests)', () => {
|
||||
|
||||
// Response (no method field) — should not call handler
|
||||
const responseLine = JSON.stringify({ jsonrpc: '2.0', id: 1, result: { ok: true } });
|
||||
(bridge as any).handleLine(responseLine);
|
||||
asTestableBridge(bridge).handleLine(responseLine);
|
||||
expect(handler).not.toHaveBeenCalled();
|
||||
|
||||
// Request (has method field) — should call handler
|
||||
const requestLine = JSON.stringify({ jsonrpc: '2.0', id: 2, method: 'test', params: {} });
|
||||
(bridge as any).handleLine(requestLine);
|
||||
asTestableBridge(bridge).handleLine(requestLine);
|
||||
expect(handler).toHaveBeenCalledWith('test', {});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import Fastify from 'fastify';
|
||||
import { agentRuntimeRoutes } from './routes.js';
|
||||
import { setIpcBridge } from '../../lib/ipc-bridge.js';
|
||||
import { setIpcBridge, type IpcBridge } from '../../lib/ipc-bridge.js';
|
||||
|
||||
vi.mock('../../lib/config.js', () => ({
|
||||
config: {
|
||||
@ -30,7 +30,7 @@ beforeEach(async () => {
|
||||
setIpcBridge({
|
||||
isRunning: true,
|
||||
call,
|
||||
} as any);
|
||||
} as unknown as IpcBridge);
|
||||
|
||||
app = Fastify({ logger: false });
|
||||
app.decorateRequest('jwtPayload', null);
|
||||
|
||||
@ -18,8 +18,11 @@ vi.stubGlobal('fetch', mockFetch);
|
||||
function buildApp() {
|
||||
const app = Fastify({ logger: false });
|
||||
app.decorateRequest('jwtPayload', null);
|
||||
app.addHook('onRequest', async (req) => {
|
||||
(req as any).jwtPayload = { sub: 'user-1', role: 'user' };
|
||||
app.addHook('onRequest', async req => {
|
||||
(req as unknown as { jwtPayload: { sub: string; role: string } }).jwtPayload = {
|
||||
sub: 'user-1',
|
||||
role: 'user',
|
||||
};
|
||||
});
|
||||
app.register(marketplaceRoutes);
|
||||
return app;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import Fastify from 'fastify';
|
||||
import { pluginRoutes } from './routes.js';
|
||||
import { setIpcBridge } from '../../lib/ipc-bridge.js';
|
||||
import { setIpcBridge, type IpcBridge } from '../../lib/ipc-bridge.js';
|
||||
|
||||
vi.mock('../../lib/product-config.js', () => ({
|
||||
PRODUCT_ID: 'clawcowork',
|
||||
@ -21,7 +21,7 @@ beforeEach(async () => {
|
||||
setIpcBridge({
|
||||
isRunning: true,
|
||||
call,
|
||||
} as any);
|
||||
} as unknown as IpcBridge);
|
||||
|
||||
app = Fastify({ logger: false });
|
||||
app.decorateRequest('jwtPayload', null);
|
||||
@ -128,7 +128,7 @@ describe('plugin routes', () => {
|
||||
|
||||
// Spin up a fresh app with a preHandler that injects an admin jwtPayload.
|
||||
const adminApp = Fastify({ logger: false });
|
||||
(adminApp as any).decorateRequest('jwtPayload', null);
|
||||
adminApp.decorateRequest('jwtPayload', null);
|
||||
adminApp.addHook('preHandler', async req => {
|
||||
(req as unknown as { jwtPayload: unknown }).jwtPayload = {
|
||||
sub: 'demo-user',
|
||||
@ -157,10 +157,10 @@ describe('plugin routes', () => {
|
||||
});
|
||||
|
||||
it('returns 400 when IPC bridge is unavailable', async () => {
|
||||
setIpcBridge({ isRunning: false, call } as any);
|
||||
setIpcBridge({ isRunning: false, call } as unknown as IpcBridge);
|
||||
|
||||
const downApp = Fastify({ logger: false });
|
||||
(downApp as any).decorateRequest('jwtPayload', null);
|
||||
downApp.decorateRequest('jwtPayload', null);
|
||||
downApp.addHook('preHandler', async req => {
|
||||
(req as unknown as { jwtPayload: unknown }).jwtPayload = { role: 'admin' };
|
||||
});
|
||||
@ -176,7 +176,7 @@ describe('plugin routes', () => {
|
||||
call.mockResolvedValue({ error: { code: -32003, message: 'admin required' } });
|
||||
|
||||
const adminApp = Fastify({ logger: false });
|
||||
(adminApp as any).decorateRequest('jwtPayload', null);
|
||||
adminApp.decorateRequest('jwtPayload', null);
|
||||
adminApp.addHook('preHandler', async req => {
|
||||
(req as unknown as { jwtPayload: unknown }).jwtPayload = { role: 'admin' };
|
||||
});
|
||||
@ -191,7 +191,7 @@ describe('plugin routes', () => {
|
||||
call.mockResolvedValue({ error: { code: -32603, message: 'internal boom' } });
|
||||
|
||||
const adminApp = Fastify({ logger: false });
|
||||
(adminApp as any).decorateRequest('jwtPayload', null);
|
||||
adminApp.decorateRequest('jwtPayload', null);
|
||||
adminApp.addHook('preHandler', async req => {
|
||||
(req as unknown as { jwtPayload: unknown }).jwtPayload = { role: 'admin' };
|
||||
});
|
||||
@ -207,7 +207,7 @@ describe('plugin routes', () => {
|
||||
call.mockResolvedValue({ result: { loaded: 'not-a-number' } });
|
||||
|
||||
const adminApp = Fastify({ logger: false });
|
||||
(adminApp as any).decorateRequest('jwtPayload', null);
|
||||
adminApp.decorateRequest('jwtPayload', null);
|
||||
adminApp.addHook('preHandler', async req => {
|
||||
(req as unknown as { jwtPayload: unknown }).jwtPayload = { role: 'admin' };
|
||||
});
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import Fastify from 'fastify';
|
||||
import { scheduleRoutes } from './routes.js';
|
||||
import { setIpcBridge } from '../../lib/ipc-bridge.js';
|
||||
import { setIpcBridge, type IpcBridge } from '../../lib/ipc-bridge.js';
|
||||
|
||||
vi.mock('../../lib/product-config.js', () => ({
|
||||
PRODUCT_ID: 'clawcowork',
|
||||
@ -21,7 +21,7 @@ beforeEach(async () => {
|
||||
setIpcBridge({
|
||||
isRunning: true,
|
||||
call,
|
||||
} as any);
|
||||
} as unknown as IpcBridge);
|
||||
|
||||
app = Fastify({ logger: false });
|
||||
app.decorateRequest('jwtPayload', null);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import Fastify from 'fastify';
|
||||
import { sessionRoutes } from './routes.js';
|
||||
import { setIpcBridge } from '../../lib/ipc-bridge.js';
|
||||
import { setIpcBridge, type IpcBridge } from '../../lib/ipc-bridge.js';
|
||||
|
||||
vi.mock('../../lib/product-config.js', () => ({
|
||||
PRODUCT_ID: 'clawcowork',
|
||||
@ -21,7 +21,7 @@ beforeEach(async () => {
|
||||
setIpcBridge({
|
||||
isRunning: true,
|
||||
call,
|
||||
} as any);
|
||||
} as unknown as IpcBridge);
|
||||
|
||||
app = Fastify({ logger: false });
|
||||
app.decorateRequest('jwtPayload', null);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user