Files
ai-code-app/etc/servers/work-server/src/services/app-config-service.test.ts

563 lines
17 KiB
TypeScript

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);
});