import test from 'node:test'; import assert from 'node:assert/strict'; import { buildChatConversationRequestPatchFromMessage, isVisibleConversationMessage, mergeChatConversationRequestStatus, resolveNextConversationChatTypeId, shouldClearConversationJobState, selectChatConversationResponseCandidate, } from './chat-room-service.js'; test('mergeChatConversationRequestStatus keeps terminal states from being downgraded', () => { assert.equal(mergeChatConversationRequestStatus('completed', 'accepted'), 'completed'); assert.equal(mergeChatConversationRequestStatus('started', 'accepted'), 'started'); assert.equal(mergeChatConversationRequestStatus('queued', 'accepted'), 'queued'); assert.equal(mergeChatConversationRequestStatus('accepted', 'completed'), 'completed'); }); test('resolveNextConversationChatTypeId prefers the requested chat type over the stored one', () => { assert.equal(resolveNextConversationChatTypeId('general-request', 'api-request-template'), 'api-request-template'); assert.equal(resolveNextConversationChatTypeId('general-request', ' api-request-template '), 'api-request-template'); }); test('resolveNextConversationChatTypeId falls back to the stored chat type when the request is empty', () => { assert.equal(resolveNextConversationChatTypeId('general-request', null), 'general-request'); assert.equal(resolveNextConversationChatTypeId('general-request', ' '), 'general-request'); assert.equal(resolveNextConversationChatTypeId(null, null), null); }); test('buildChatConversationRequestPatchFromMessage ignores system progress messages', () => { assert.equal( buildChatConversationRequestPatchFromMessage({ id: 10, author: 'system', text: '요청 실행 중입니다.', clientRequestId: 'chat-req-1', }), null, ); }); test('isVisibleConversationMessage hides internal system messages and keeps activity logs', () => { assert.equal( isVisibleConversationMessage({ id: 1, author: 'system', text: '응답을 준비하고 있습니다.', timestamp: '2026-04-22 10:00:00', }), false, ); assert.equal( isVisibleConversationMessage({ id: 2, author: 'system', text: '[[activity-log]]\n작업을 시작했습니다.', timestamp: '2026-04-22 10:00:01', }), true, ); }); test('buildChatConversationRequestPatchFromMessage builds user and codex request patches', () => { assert.deepEqual( buildChatConversationRequestPatchFromMessage({ id: 11, author: 'user', text: '질문', clientRequestId: 'chat-req-2', }), { requestId: 'chat-req-2', status: 'accepted', userMessageId: 11, userText: '질문', }, ); assert.deepEqual( buildChatConversationRequestPatchFromMessage({ id: 12, author: 'codex', text: '답변', clientRequestId: 'chat-req-2', }), { requestId: 'chat-req-2', status: 'started', responseMessageId: 12, responseText: '답변', }, ); }); test('selectChatConversationResponseCandidate falls back to codex replies in the request window', () => { const candidate = selectChatConversationResponseCandidate( { requestId: 'chat-req-3', createdAt: '2026-04-18T14:00:00.000Z', responseMessageId: null, }, { createdAt: '2026-04-18T14:10:00.000Z', }, [ { id: 1, messageId: 1001, author: 'codex', text: '이전 답변', clientRequestId: null, createdAt: '2026-04-18T13:59:00.000Z', }, { id: 2, messageId: 1002, author: 'codex', text: '현재 요청 답변', clientRequestId: null, createdAt: '2026-04-18T14:05:00.000Z', }, { id: 3, messageId: 1003, author: 'codex', text: '다음 요청 답변', clientRequestId: null, createdAt: '2026-04-18T14:11:00.000Z', }, ], ); assert.deepEqual(candidate, { id: 2, messageId: 1002, author: 'codex', text: '현재 요청 답변', clientRequestId: null, createdAt: '2026-04-18T14:05:00.000Z', }); }); test('shouldClearConversationJobState clears stale job state when terminal request already has a response', () => { assert.equal( shouldClearConversationJobState({ currentRequestId: 'chat-req-4', currentJobStatus: 'started', request: { requestId: 'chat-req-4', status: 'completed', responseMessageId: 101, responseText: '답변', terminalAt: '2026-04-19T01:00:00.000Z', }, }), true, ); }); test('shouldClearConversationJobState clears orphaned job state without a current request id', () => { assert.equal( shouldClearConversationJobState({ currentRequestId: null, currentJobStatus: 'started', request: null, }), true, ); }); test('shouldClearConversationJobState keeps active job state when request is still running without a response', () => { assert.equal( shouldClearConversationJobState({ currentRequestId: 'chat-req-5', currentJobStatus: 'started', request: { requestId: 'chat-req-5', status: 'started', responseMessageId: null, responseText: '', terminalAt: null, }, }), false, ); }); test('shouldClearConversationJobState does not clear placeholder-only started responses', () => { assert.equal( shouldClearConversationJobState({ currentRequestId: 'chat-req-6', currentJobStatus: 'started', request: { requestId: 'chat-req-6', status: 'started', responseMessageId: 301, responseText: '응답을 준비하고 있습니다...', terminalAt: null, }, }), false, ); }); test('shouldClearConversationJobState clears stale placeholder-only started responses when runtime is gone', () => { assert.equal( shouldClearConversationJobState({ currentRequestId: 'chat-req-7', currentJobStatus: 'started', currentStatusUpdatedAt: '2026-04-19T08:08:35.813Z', runtimeActive: false, nowMs: Date.parse('2026-04-19T08:11:36.000Z'), request: { requestId: 'chat-req-7', status: 'started', responseMessageId: 302, responseText: '응답을 준비하고 있습니다...', terminalAt: null, updatedAt: '2026-04-19T08:08:56.086Z', }, }), true, ); }); test('shouldClearConversationJobState keeps placeholder-only started responses while runtime is active', () => { assert.equal( shouldClearConversationJobState({ currentRequestId: 'chat-req-8', currentJobStatus: 'started', currentStatusUpdatedAt: '2026-04-19T08:08:35.813Z', runtimeActive: true, nowMs: Date.parse('2026-04-19T08:11:36.000Z'), request: { requestId: 'chat-req-8', status: 'started', responseMessageId: 303, responseText: '응답을 준비하고 있습니다...', terminalAt: null, updatedAt: '2026-04-19T08:08:56.086Z', }, }), false, ); });