feat(coding): compose plugin host lifecycle
This commit is contained in:
99
tests/unit/coding-plugin-routes.test.ts
Normal file
99
tests/unit/coding-plugin-routes.test.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
// @vitest-environment node
|
||||
|
||||
import { EventEmitter } from 'node:events';
|
||||
import type { IncomingMessage, ServerResponse } from 'node:http';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { HostApiContext } from '../../electron/api/context';
|
||||
import { handleCodingPluginRoutes } from '../../electron/api/routes/coding-plugins';
|
||||
|
||||
function request(method: string, body?: unknown): IncomingMessage {
|
||||
const req = new EventEmitter();
|
||||
const raw = body === undefined ? undefined : JSON.stringify(body);
|
||||
Object.assign(req, {
|
||||
method,
|
||||
headers: raw === undefined ? {} : { 'content-length': String(Buffer.byteLength(raw)) },
|
||||
[Symbol.asyncIterator]: async function* () {
|
||||
if (raw !== undefined) yield Buffer.from(raw);
|
||||
},
|
||||
});
|
||||
return req as IncomingMessage;
|
||||
}
|
||||
|
||||
function response() {
|
||||
const chunks: string[] = [];
|
||||
const res = new EventEmitter();
|
||||
Object.assign(res, {
|
||||
statusCode: 0,
|
||||
setHeader: vi.fn(),
|
||||
end: vi.fn((chunk?: string) => { if (chunk) chunks.push(chunk); }),
|
||||
});
|
||||
return {
|
||||
res: res as unknown as ServerResponse,
|
||||
get status() { return (res as { statusCode: number }).statusCode; },
|
||||
json: () => JSON.parse(chunks.join('')) as Record<string, unknown>,
|
||||
};
|
||||
}
|
||||
|
||||
async function invoke(ctx: HostApiContext, method: string, target: string, body?: unknown) {
|
||||
const output = response();
|
||||
const handled = await handleCodingPluginRoutes(
|
||||
request(method, body),
|
||||
output.res,
|
||||
new URL(`http://localhost${target}`),
|
||||
ctx,
|
||||
);
|
||||
return { handled, status: output.status, payload: output.json() };
|
||||
}
|
||||
|
||||
describe('coding plugin Host routes', () => {
|
||||
it('uses only the local project handle for GET and preserves the bounded projection', async () => {
|
||||
const projection = {
|
||||
schemaVersion: 1 as const,
|
||||
project: { localProjectId: 'local-a', durableProjectId: 'durable-a' },
|
||||
policyStatus: 'current' as const,
|
||||
items: [],
|
||||
};
|
||||
const list = vi.fn().mockResolvedValue(projection);
|
||||
const ctx = { codingProducts: { plugins: { list } } } as unknown as HostApiContext;
|
||||
|
||||
const result = await invoke(ctx, 'GET', '/api/coding/plugins?projectId=local-a');
|
||||
|
||||
expect(result).toMatchObject({ handled: true, status: 200, payload: projection });
|
||||
expect(list).toHaveBeenCalledWith('local-a');
|
||||
expect(JSON.stringify(result.payload)).not.toMatch(/projectPath|owner|token|entitlement_scope/u);
|
||||
});
|
||||
|
||||
it('accepts only the exact PUT body and never forwards authority fields', async () => {
|
||||
const setEnabled = vi.fn().mockResolvedValue({
|
||||
schemaVersion: 1,
|
||||
project: { localProjectId: 'local-a', durableProjectId: null },
|
||||
policyStatus: 'unavailable',
|
||||
items: [],
|
||||
});
|
||||
const ctx = { codingProducts: { plugins: { setEnabled } } } as unknown as HostApiContext;
|
||||
|
||||
const accepted = await invoke(ctx, 'PUT', '/api/coding/plugins/makelore.data-service', {
|
||||
projectId: 'local-a', enabled: true,
|
||||
});
|
||||
const rejected = await invoke(ctx, 'PUT', '/api/coding/plugins/makelore.data-service', {
|
||||
projectId: 'local-a', enabled: true, projectPath: 'C:\\untrusted',
|
||||
});
|
||||
|
||||
expect(accepted.status).toBe(200);
|
||||
expect(setEnabled).toHaveBeenCalledOnce();
|
||||
expect(setEnabled).toHaveBeenCalledWith('local-a', 'makelore.data-service', true);
|
||||
expect(rejected).toMatchObject({ status: 400, payload: { success: false, code: 'plugin_request_invalid' } });
|
||||
});
|
||||
|
||||
it('rejects duplicate or unexpected query authority before calling Main services', async () => {
|
||||
const list = vi.fn();
|
||||
const ctx = { codingProducts: { plugins: { list } } } as unknown as HostApiContext;
|
||||
|
||||
const duplicate = await invoke(ctx, 'GET', '/api/coding/plugins?projectId=local-a&projectId=local-b');
|
||||
const authority = await invoke(ctx, 'GET', '/api/coding/plugins?projectId=local-a&durableProjectId=forged');
|
||||
|
||||
expect(duplicate.status).toBe(400);
|
||||
expect(authority.status).toBe(400);
|
||||
expect(list).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user