import { describe, it, beforeEach } from 'node:test'; import assert from 'node:assert/strict'; import { registerBrowserSocketHandlers } from './socketHandlers.js'; // --- Test-hjelpere --- class FakeSocket { constructor(user = null) { this.request = { session: { user } }; this.handlers = new Map(); this.emitted = []; } on(eventName, handler) { this.handlers.set(eventName, handler); } emit(eventName, payload) { this.emitted.push({ eventName, payload }); } async trigger(eventName, payload) { const handler = this.handlers.get(eventName); if (handler) { await handler(payload); } } findEmitted(eventName) { return this.emitted.find(e => e.eventName === eventName); } } function createFakeCallAgent() { const fakeCallAgent = async (tool, args) => { fakeCallAgent.calls.push({ tool, args }); if (fakeCallAgent.nextError) { throw fakeCallAgent.nextError; } return fakeCallAgent.nextResult; }; fakeCallAgent.calls = []; fakeCallAgent.nextResult = null; fakeCallAgent.nextError = null; fakeCallAgent.setNextResult = (result) => { fakeCallAgent.nextResult = result; fakeCallAgent.nextError = null; }; fakeCallAgent.setNextError = (error) => { fakeCallAgent.nextError = error; fakeCallAgent.nextResult = null; }; return fakeCallAgent; } // --- Tester --- describe('registerBrowserSocketHandlers', () => { let fakeSocket; let fakeCallAgent; beforeEach(() => { fakeCallAgent = createFakeCallAgent(); }); describe('for unauthenticated user', () => { beforeEach(() => { fakeSocket = new FakeSocket(null); // Ingen bruker i session registerBrowserSocketHandlers(fakeSocket, { callAgentFn: fakeCallAgent }); }); it('git:getStatus is rejected and does not call agent', async () => { await fakeSocket.trigger('git:getStatus'); const errorEvent = fakeSocket.findEmitted('git:status:error'); assert.deepStrictEqual(errorEvent?.payload, { message: 'Du må være logget inn.' }); assert.strictEqual(fakeCallAgent.calls.length, 0); assert.strictEqual(fakeSocket.emitted.length, 1); }); it('git:getBranch is rejected and does not call agent', async () => { await fakeSocket.trigger('git:getBranch'); const errorEvent = fakeSocket.findEmitted('git:branch:error'); assert.deepStrictEqual(errorEvent?.payload, { message: 'Du må være logget inn.' }); assert.strictEqual(fakeCallAgent.calls.length, 0); assert.strictEqual(fakeSocket.emitted.length, 1); }); }); describe('for authenticated user', () => { beforeEach(() => { fakeSocket = new FakeSocket({ email: 'test@example.com' }); registerBrowserSocketHandlers(fakeSocket, { callAgentFn: fakeCallAgent }); }); it('git:getStatus calls agent correctly and returns normalized data', async () => { const mcpResult = { status: 'dirty', details: 'M file.txt', secret: 'should-be-stripped' }; fakeCallAgent.setNextResult(mcpResult); await fakeSocket.trigger('git:getStatus'); assert.strictEqual(fakeCallAgent.calls.length, 1); assert.deepStrictEqual(fakeCallAgent.calls[0], { tool: 'get_git_status', args: {} }); const resultEvent = fakeSocket.findEmitted('git:status:result'); assert.ok(resultEvent, 'Expected a result event'); assert.deepStrictEqual(resultEvent.payload.data, { status: 'dirty', details: 'M file.txt' }); }); it('git:getBranch calls agent correctly and returns normalized data', async () => { const mcpResult = { current_branch: 'main', url: 'https://internal.example.invalid' }; fakeCallAgent.setNextResult(mcpResult); await fakeSocket.trigger('git:getBranch'); assert.strictEqual(fakeCallAgent.calls.length, 1); assert.deepStrictEqual(fakeCallAgent.calls[0], { tool: 'get_git_branch', args: {} }); const resultEvent = fakeSocket.findEmitted('git:branch:result'); assert.ok(resultEvent, 'Expected a result event'); assert.deepStrictEqual(resultEvent.payload.data, { current_branch: 'main' }); }); it('emits static error if git:getStatus agent response is malformed', async () => { const malformedResults = [null, {}, [], 'a string', { status: 123, details: 'a' }]; for (const mcpResult of malformedResults) { fakeCallAgent.setNextResult(mcpResult); await fakeSocket.trigger('git:getStatus'); } assert.strictEqual(fakeSocket.emitted.length, malformedResults.length); assert.ok(fakeSocket.emitted.every(e => e.eventName === 'git:status:error')); assert.ok(fakeSocket.emitted.every(e => e.payload.message === 'Kunne ikke hente Git-status.')); }); it('emits static error if git:getBranch agent response is malformed', async () => { const malformedResults = [null, {}, [], 'a string', { current_branch: 123 }]; for (const mcpResult of malformedResults) { fakeCallAgent.setNextResult(mcpResult); await fakeSocket.trigger('git:getBranch'); } assert.strictEqual(fakeSocket.emitted.length, malformedResults.length); assert.ok(fakeSocket.emitted.every(e => e.eventName === 'git:branch:error')); assert.ok(fakeSocket.emitted.every(e => e.payload.message === 'Kunne ikke hente aktiv branch.')); }); it('masks agent error for git:getStatus and returns static message', async () => { const internalError = new Error('Contains test-secret-must-not-leak'); fakeCallAgent.setNextError(internalError); await fakeSocket.trigger('git:getStatus'); const errorEvent = fakeSocket.findEmitted('git:status:error'); assert.ok(errorEvent, 'Expected an error event'); assert.deepStrictEqual(errorEvent.payload, { message: 'Kunne ikke hente Git-status.' }); assert.strictEqual(fakeSocket.findEmitted('git:status:result'), undefined); }); it('masks agent error for git:getBranch and returns static message', async () => { const internalError = new Error('Another secret for https://internal.example.invalid'); fakeCallAgent.setNextError(internalError); await fakeSocket.trigger('git:getBranch'); const errorEvent = fakeSocket.findEmitted('git:branch:error'); assert.ok(errorEvent, 'Expected an error event'); assert.deepStrictEqual(errorEvent.payload, { message: 'Kunne ikke hente aktiv branch.' }); assert.strictEqual(fakeSocket.findEmitted('git:branch:result'), undefined); }); }); describe('legacy mcp-call handler', () => { beforeEach(() => { fakeSocket = new FakeSocket({ email: 'test@example.com' }); registerBrowserSocketHandlers(fakeSocket, { callAgentFn: fakeCallAgent }); }); it('rejects calls and does not call agent', async () => { const payload = { id: 'test-id', tool: 'commit_and_push_files', args: { message: 'evil' } }; await fakeSocket.trigger('mcp-call', payload); assert.strictEqual(fakeCallAgent.calls.length, 0); const resultEvent = fakeSocket.findEmitted('mcp-result'); assert.ok(resultEvent, 'Expected a result event'); assert.deepStrictEqual(resultEvent.payload, { id: 'test-id', error: 'Denne funksjonen er ikke tilgjengelig.' }); }); it('handles malformed payloads gracefully and uses null id', async () => { const malformed = [null, 'a string', { id: 'a'.repeat(129) }]; for (const payload of malformed) { await fakeSocket.trigger('mcp-call', payload); } assert.strictEqual(fakeCallAgent.calls.length, 0); assert.strictEqual(fakeSocket.emitted.length, malformed.length); assert.ok(fakeSocket.emitted.every(e => e.eventName === 'mcp-result')); assert.ok(fakeSocket.emitted.every(e => e.payload.id === null)); }); }); });