import test from 'node:test'; import assert from 'node:assert/strict'; import { createDefaultChatTypeExecutionPolicy, migrateLegacyChatTypeContexts, sanitizePersistedChatTypes, synchronizeBuiltinCodexChatTypes, resolveAppConfigByOrigin, resolveCanonicalChatTypesFromConfig, resolveCanonicalChatContextSettingsFromConfig, stripChatContextSettingsFromScopedAppConfigs, stripSharedContextDataFromScopedAppConfigs, } from './app-config-service.js'; test('sanitizePersistedChatTypes keeps saved chat type edits as-is', () => { const merged = sanitizePersistedChatTypes([ { id: 'general-request', name: '일반 요청', sortOrder: 3, description: '사용자가 수정한 일반 요청 문맥', permissions: ['guest', 'token-user'], enabled: true, updatedAt: '2026-04-24T09:00:00.000Z', }, ]); const generalRequest = merged.find((item) => item.id === 'general-request'); assert.ok(generalRequest); assert.equal(generalRequest.description, '사용자가 수정한 일반 요청 문맥'); assert.deepEqual(generalRequest.permissions, ['guest', 'token-user']); assert.equal(generalRequest.sortOrder, 1); }); test('sanitizePersistedChatTypes keeps saved layout editor execution entries', () => { const merged = sanitizePersistedChatTypes([ { id: 'layout-editor-execution', name: 'Layout editor 실행', sortOrder: 2, description: '호출 가능한 API 요청만 처리합니다.', permissions: ['token-user'], enabled: true, updatedAt: '2026-04-27T00:00:00.000Z', }, ]); const layoutEditorExecution = merged.find((item) => item.id === 'layout-editor-execution'); assert.ok(layoutEditorExecution); assert.equal(layoutEditorExecution.description, '호출 가능한 API 요청만 처리합니다.'); assert.equal(layoutEditorExecution.sortOrder, 1); }); test('sanitizePersistedChatTypes keeps saved guided layout editor entries', () => { const merged = sanitizePersistedChatTypes([ { id: 'layout-editor-guided-execution', name: 'Layout editor 단계별 실행', sortOrder: 4, description: '사용자가 정리한 단계별 Layout 실행 문맥', permissions: ['token-user'], enabled: true, updatedAt: '2026-05-01T09:00:00.000Z', }, ]); const guidedLayoutEditorExecution = merged.find((item) => item.id === 'layout-editor-guided-execution'); assert.ok(guidedLayoutEditorExecution); assert.equal(guidedLayoutEditorExecution.description, '사용자가 정리한 단계별 Layout 실행 문맥'); assert.equal(guidedLayoutEditorExecution.sortOrder, 1); }); test('sanitizePersistedChatTypes returns empty list when nothing is saved', () => { const merged = sanitizePersistedChatTypes([]); assert.deepEqual(merged, []); }); test('sanitizePersistedChatTypes keeps saved chat type list without backfilling removed entries', () => { const merged = sanitizePersistedChatTypes([ { id: 'general-request', name: '일반 요청', sortOrder: 2, description: '사용자가 수정한 일반 요청 문맥', permissions: ['token-user'], enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, { id: 'custom-support-flow', name: '운영 문의 전용', sortOrder: 1, description: 'custom', permissions: ['token-user'], enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, ]); assert.ok(!merged.some((item) => item.id === 'layout-editor-guided-execution')); assert.ok(!merged.some((item) => item.id === 'layout-editor-execution')); assert.ok(merged.some((item) => item.id === 'custom-support-flow')); }); test('sanitizePersistedChatTypes keeps all saved chat types without special filtering', () => { const stripped = sanitizePersistedChatTypes([ { id: 'general-request', name: '일반 요청', sortOrder: 2, description: 'builtin', permissions: ['token-user'], enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, { id: 'plan-checklist-execution', name: 'Plan 체크리스트 실행', sortOrder: 3, description: 'custom-seeded', permissions: ['token-user'], enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, { id: 'custom-support-flow', name: '운영 문의 전용', sortOrder: 1, description: 'custom', permissions: ['token-user'], enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, ]); assert.deepEqual(stripped.map((item) => item.id), ['custom-support-flow', 'general-request', 'plan-checklist-execution']); }); test('synchronizeBuiltinCodexChatTypes upgrades legacy codex summary execution policy', () => { const synced = synchronizeBuiltinCodexChatTypes([ { id: 'codex-summary', name: 'Codex 종합', sortOrder: 13, description: '## 처리 범위\n- Codex 봇들의 대화와 의견 수렴 과정을 거친 뒤 최종 결과물을 정리해 제공하는 채팅에 사용합니다.\n- Plan 자동화 문맥을 기본값으로 섞지 않고, 현재 채팅방 공통 문맥과 사용자 요청만 기준으로 결과를 정리합니다.\n\n## 응답 방식\n- 중간 대화에서 나온 핵심 의견, 판단 근거, 남은 쟁점을 압축해 정리합니다.\n- 최종 답변에는 필요한 산출물, 검증 결과, 적용 결론을 함께 제공합니다.', executionPolicy: createDefaultChatTypeExecutionPolicy(), permissions: ['token-user'], enabled: true, updatedAt: '2026-05-17T00:00:00.000Z', }, ]); const codexSummary = synced.find((item) => item.id === 'codex-summary'); const codexDispatcher = synced.find((item) => item.id === 'codex-dispatcher-workers'); const codexLiveDefault = synced.find((item) => item.id === 'codex-live-default'); assert.ok(codexSummary); assert.equal(codexSummary.executionPolicy.mode, 'summary-free-talking'); assert.match(codexSummary.description, /회의 기록자 1명/); assert.ok(codexDispatcher); assert.equal(codexDispatcher.executionPolicy.mode, 'dispatcher-workers'); assert.ok(codexLiveDefault); assert.equal(codexLiveDefault.executionPolicy.mode, 'default'); }); test('migrateLegacyChatTypeContexts moves legacy plan checklist chat type into default context settings', () => { const migrated = migrateLegacyChatTypeContexts( { defaultContexts: [], chatTypeDefaults: [ { chatTypeId: 'plan-checklist-execution', defaultContextIds: ['legacy-linked-context'], updatedAt: '2026-05-08T00:00:00.000Z', }, ], roomContexts: [], }, [ { id: 'plan-checklist-execution', name: 'Plan 체크리스트 실행', sortOrder: 1, description: 'legacy plan context', executionPolicy: createDefaultChatTypeExecutionPolicy(), permissions: ['token-user'], enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, ], ); assert.equal(migrated.defaultContexts.some((item) => item.id === 'chat-default-plan-checklist-execution'), true); assert.equal( migrated.defaultContexts.find((item) => item.id === 'chat-default-plan-checklist-execution')?.content, 'legacy plan context', ); assert.equal(migrated.chatTypeDefaults.some((item) => item.chatTypeId === 'plan-checklist-execution'), false); }); test('resolveAppConfigByOrigin prefers scoped app config over legacy global config', () => { const resolved = resolveAppConfigByOrigin( { chat: { maxContextMessages: 12, receiveRoomNotifications: true, }, automation: { notifyOnAutomationStart: true, }, scopedAppConfigs: { 'https://rel.sm-home.cloud': { config: { chat: { receiveRoomNotifications: false, }, }, }, }, }, 'https://rel.sm-home.cloud', ) as { chat?: { maxContextMessages?: number; receiveRoomNotifications?: boolean }; automation?: { notifyOnAutomationStart?: boolean }; }; assert.equal(resolved.chat?.maxContextMessages, 12); assert.equal(resolved.chat?.receiveRoomNotifications, false); assert.equal(resolved.automation?.notifyOnAutomationStart, true); }); test('resolveAppConfigByOrigin falls back to legacy global config when scoped config is missing', () => { const resolved = resolveAppConfigByOrigin( { chat: { receiveRoomNotifications: true, }, }, 'https://preview.sm-home.cloud', ) as { chat?: { receiveRoomNotifications?: boolean }; }; assert.equal(resolved.chat?.receiveRoomNotifications, true); }); test('resolveCanonicalChatContextSettingsFromConfig prefers global chat context settings over stale scoped entries', () => { const resolved = resolveCanonicalChatContextSettingsFromConfig( { chatContextSettings: { defaultContexts: [ { id: 'global-a', title: '전역 A', content: 'global', enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, { id: 'global-b', title: '전역 B', content: 'global', enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, ], }, scopedAppConfigs: { 'https://preview.sm-home.cloud': { config: { chatContextSettings: { defaultContexts: [ { id: 'scoped-a', title: '스코프 A', content: 'scoped', enabled: true, updatedAt: '2026-05-01T00:00:00.000Z', }, ], }, }, }, }, }, 'https://preview.sm-home.cloud', ); assert.deepEqual( resolved.defaultContexts.map((item) => item.id), ['global-a', 'global-b'], ); }); test('resolveCanonicalChatContextSettingsFromConfig keeps saved default context sort order and renumbers gaps', () => { const resolved = resolveCanonicalChatContextSettingsFromConfig({ chatContextSettings: { defaultContexts: [ { id: 'context-b', title: 'B 문맥', sortOrder: 3, content: 'b', enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, { id: 'context-a', title: 'A 문맥', sortOrder: 1, content: 'a', enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, ], }, }); assert.deepEqual( resolved.defaultContexts.map((item) => ({ id: item.id, sortOrder: item.sortOrder })), [ { id: 'context-a', sortOrder: 1 }, { id: 'context-b', sortOrder: 2 }, ], ); }); test('resolveCanonicalChatContextSettingsFromConfig appends unsorted default contexts after sorted entries', () => { const resolved = resolveCanonicalChatContextSettingsFromConfig({ chatContextSettings: { defaultContexts: [ { id: 'context-b', title: 'B 문맥', content: 'b', enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, { id: 'context-a', title: 'A 문맥', sortOrder: 1, content: 'a', enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, ], }, }); assert.deepEqual( resolved.defaultContexts.map((item) => ({ id: item.id, sortOrder: item.sortOrder })), [ { id: 'context-a', sortOrder: 1 }, { id: 'context-b', sortOrder: 2 }, ], ); }); test('resolveCanonicalChatContextSettingsFromConfig falls back to scoped settings only when global settings are empty', () => { const resolved = resolveCanonicalChatContextSettingsFromConfig( { scopedAppConfigs: { 'https://preview.sm-home.cloud': { config: { chatContextSettings: { defaultContexts: [ { id: 'scoped-a', title: '스코프 A', content: 'scoped', enabled: true, updatedAt: '2026-05-01T00:00:00.000Z', }, ], }, }, }, }, }, 'https://preview.sm-home.cloud', ); assert.deepEqual( resolved.defaultContexts.map((item) => item.id), ['scoped-a'], ); }); test('resolveCanonicalChatTypesFromConfig merges global chat types with stale scoped entries', () => { const resolved = resolveCanonicalChatTypesFromConfig( { chatTypes: [ { id: 'verification-test-generation', name: '검증 밑 테스트 생성', description: 'global', permissions: ['token-user'], enabled: true, updatedAt: '2026-05-08T08:15:18.440Z', }, ], scopedAppConfigs: { 'https://preview.sm-home.cloud': { config: { chatTypes: [ { id: 'general-request', name: '일반 요청', description: 'scoped', permissions: ['token-user'], enabled: true, updatedAt: '2026-05-01T00:00:00.000Z', }, ], }, }, }, }, 'https://preview.sm-home.cloud', ); assert.ok(resolved); assert.equal(resolved.some((item) => item.id === 'verification-test-generation'), true); assert.equal(resolved.some((item) => item.id === 'general-request'), true); }); test('stripChatContextSettingsFromScopedAppConfigs removes stale scoped chat context settings only', () => { const stripped = stripChatContextSettingsFromScopedAppConfigs({ scopedAppConfigs: { 'https://preview.sm-home.cloud': { config: { chatContextSettings: { defaultContexts: [{ id: 'legacy', title: 'legacy', content: 'legacy', enabled: true }], }, chat: { receiveRoomNotifications: false, }, }, updatedAt: '2026-05-08T00:00:00.000Z', }, }, }); assert.equal(stripped.changed, true); assert.deepEqual(stripped.scopedConfigs, { 'https://preview.sm-home.cloud': { config: { chat: { receiveRoomNotifications: false, }, }, updatedAt: '2026-05-08T00:00:00.000Z', }, }); }); test('stripSharedContextDataFromScopedAppConfigs removes scoped chat-type/context data and backs up non-shared origins', () => { const stripped = stripSharedContextDataFromScopedAppConfigs( { scopedAppConfigs: { 'https://preview.sm-home.cloud': { config: { chatTypes: [ { id: 'general-request', name: '일반 요청', description: 'preview-shared', permissions: ['token-user'], enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, ], chatContextSettings: { defaultContexts: [ { id: 'preview-context', title: 'preview', content: 'shared', enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, ], }, chat: { receiveRoomNotifications: false, }, }, updatedAt: '2026-05-08T00:00:00.000Z', appDomain: 'preview.sm-home.cloud', }, 'https://test.sm-home.cloud': { config: { chatTypes: [ { id: 'chat-type-test-temp', name: '임시 유형', description: 'test-only', permissions: ['token-user'], enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, ], chatContextSettings: { defaultContexts: [ { id: 'test-context', title: 'test', content: 'legacy', enabled: true, updatedAt: '2026-05-08T00:00:00.000Z', }, ], }, automation: { notifyOnAutomationStart: true, }, }, updatedAt: '2026-05-08T00:00:00.000Z', appDomain: 'test.sm-home.cloud', }, }, }, 'https://preview.sm-home.cloud', ); assert.equal(stripped.changed, true); assert.deepEqual(stripped.scopedConfigs, { 'https://preview.sm-home.cloud': { config: { chat: { receiveRoomNotifications: false, }, }, updatedAt: '2026-05-08T00:00:00.000Z', appDomain: 'preview.sm-home.cloud', }, 'https://test.sm-home.cloud': { config: { automation: { notifyOnAutomationStart: true, }, }, updatedAt: '2026-05-08T00:00:00.000Z', appDomain: 'test.sm-home.cloud', }, }); assert.equal( Array.isArray(stripped.backups['https://test.sm-home.cloud']?.chatTypes), true, ); assert.equal( stripped.backups['https://test.sm-home.cloud']?.chatContextSettings?.defaultContexts[0]?.id, 'test-context', ); assert.equal(stripped.backups['https://preview.sm-home.cloud'], undefined); });