Co-Authored-By: Claude Opus 4.8 <[email protected]> Claude-Session: https://claude.ai/code/session_01ESFAnh2t9HmLwng8oW95St
117 lines
4.8 KiB
TypeScript
117 lines
4.8 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import type { ChatRuntimeMode } from '../chat/chat-runtime.js';
|
|
import { ConversationsController } from './conversations.controller.js';
|
|
|
|
/**
|
|
* Task 5 harness fence for the conversations REST write path.
|
|
*
|
|
* Under `pi-rpc` the durable/harness conversation path (Task 15) owns message persistence, so the
|
|
* legacy direct-repository write via `POST /api/conversations/:id/messages` must be refused with a
|
|
* fixed typed `runtime_unsupported` BEFORE the repository is touched — never a duplicate write.
|
|
* Under `legacy` the endpoint keeps its current behaviour and writes through `brain.conversations`.
|
|
*
|
|
* Item 3 (single runtime-mode source of truth): the mode is the router's ONE init-time resolution,
|
|
* injected into the controller and read as `router.runtimeMode`. It is NOT re-derived from
|
|
* `process.env` at request time. The two "env is flipped after construction" tests below are the
|
|
* load-bearing guard: they pass only because the controller reads the fixed injected mode, and turn
|
|
* RED the instant the fence is reverted to `resolveChatRuntimeMode(process.env)`.
|
|
*/
|
|
const CONVERSATION_ID = '22222222-2222-4222-8222-222222222222';
|
|
const USER = { id: 'user-1' };
|
|
|
|
function sendMessageDto() {
|
|
return {
|
|
role: 'user' as const,
|
|
content: 'hello from the legacy REST write path',
|
|
metadata: undefined,
|
|
};
|
|
}
|
|
|
|
function brainWithMessageSpy() {
|
|
const addMessage = vi.fn().mockResolvedValue({
|
|
id: 'message-1',
|
|
conversationId: CONVERSATION_ID,
|
|
role: 'user',
|
|
content: 'hello from the legacy REST write path',
|
|
});
|
|
return {
|
|
brain: { conversations: { addMessage } } as never,
|
|
addMessage,
|
|
};
|
|
}
|
|
|
|
/** The controller only needs the router's immutable `runtimeMode`; supply exactly that. */
|
|
function routerFixedTo(mode: ChatRuntimeMode) {
|
|
return { runtimeMode: mode };
|
|
}
|
|
|
|
let priorMode: string | undefined;
|
|
|
|
describe('conversations REST write path — Task 5 harness fence', () => {
|
|
beforeEach(() => {
|
|
priorMode = process.env['CHAT_HARNESS_RUNTIME'];
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (priorMode === undefined) delete process.env['CHAT_HARNESS_RUNTIME'];
|
|
else process.env['CHAT_HARNESS_RUNTIME'] = priorMode;
|
|
});
|
|
|
|
it('refuses the legacy repository write when the router resolved pi-rpc, before any write', async () => {
|
|
const { brain, addMessage } = brainWithMessageSpy();
|
|
const controller = new ConversationsController(brain, routerFixedTo('pi-rpc'));
|
|
|
|
await expect(
|
|
controller.addMessage(CONVERSATION_ID, sendMessageDto(), USER),
|
|
).rejects.toMatchObject({ code: 'runtime_unsupported' });
|
|
|
|
// Load-bearing: the durable/harness path owns pi-rpc persistence — the legacy repo must not be
|
|
// written, so no duplicate message can be produced.
|
|
expect(addMessage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('writes through the repository when the router resolved legacy (GREEN control)', async () => {
|
|
const { brain, addMessage } = brainWithMessageSpy();
|
|
const controller = new ConversationsController(brain, routerFixedTo('legacy'));
|
|
|
|
const result = await controller.addMessage(CONVERSATION_ID, sendMessageDto(), USER);
|
|
|
|
expect(addMessage).toHaveBeenCalledWith(
|
|
{
|
|
conversationId: CONVERSATION_ID,
|
|
role: 'user',
|
|
content: 'hello from the legacy REST write path',
|
|
metadata: undefined,
|
|
},
|
|
USER.id,
|
|
);
|
|
expect(result).toMatchObject({ id: 'message-1', conversationId: CONVERSATION_ID });
|
|
});
|
|
|
|
it('keeps refusing under a pi-rpc router even when CHAT_HARNESS_RUNTIME is flipped to legacy after startup', async () => {
|
|
// The runtime mode is fixed at module init. A later env mutation must not reopen the fence:
|
|
// a request-time `resolveChatRuntimeMode(process.env)` read would see `legacy` and wrongly write.
|
|
process.env['CHAT_HARNESS_RUNTIME'] = 'legacy';
|
|
const { brain, addMessage } = brainWithMessageSpy();
|
|
const controller = new ConversationsController(brain, routerFixedTo('pi-rpc'));
|
|
|
|
await expect(
|
|
controller.addMessage(CONVERSATION_ID, sendMessageDto(), USER),
|
|
).rejects.toMatchObject({ code: 'runtime_unsupported' });
|
|
|
|
expect(addMessage).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('keeps writing under a legacy router even when CHAT_HARNESS_RUNTIME is flipped to pi-rpc after startup', async () => {
|
|
// Symmetric guard: a legacy-resolved router must keep writing regardless of the live env, so a
|
|
// request-time env read of `pi-rpc` cannot spuriously refuse a legitimate legacy write.
|
|
process.env['CHAT_HARNESS_RUNTIME'] = 'pi-rpc';
|
|
const { brain, addMessage } = brainWithMessageSpy();
|
|
const controller = new ConversationsController(brain, routerFixedTo('legacy'));
|
|
|
|
await controller.addMessage(CONVERSATION_ID, sendMessageDto(), USER);
|
|
|
|
expect(addMessage).toHaveBeenCalledTimes(1);
|
|
});
|
|
});
|