563 lines
17 KiB
TypeScript
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);
|
|
});
|