skyloom 1.12.0 → 1.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/.github/workflows/ci.yml +36 -36
  2. package/README.md +142 -46
  3. package/config/default.yaml +43 -47
  4. package/config/models.yaml +155 -155
  5. package/config/providers.yaml +39 -39
  6. package/config/skills/api_integrator/SKILL.md +15 -15
  7. package/config/skills/arch_designer/SKILL.md +13 -13
  8. package/config/skills/ci_cd_manager/SKILL.md +14 -14
  9. package/config/skills/code_analysis/SKILL.md +13 -13
  10. package/config/skills/code_generator/SKILL.md +12 -12
  11. package/config/skills/code_reviewer/SKILL.md +13 -13
  12. package/config/skills/content_writer/SKILL.md +14 -14
  13. package/config/skills/data_transformer/SKILL.md +15 -15
  14. package/config/skills/document_analysis/SKILL.md +13 -13
  15. package/config/skills/emotional_companion/SKILL.md +15 -15
  16. package/config/skills/performance_checker/SKILL.md +14 -14
  17. package/config/skills/security_auditor/SKILL.md +14 -14
  18. package/config/skills/self_evolve/SKILL.md +13 -13
  19. package/config/skills/sys_operator/SKILL.md +15 -15
  20. package/config/skills/task_planner/SKILL.md +14 -14
  21. package/config/skills/web_research/SKILL.md +14 -14
  22. package/config/skills/workflow_designer/SKILL.md +13 -13
  23. package/dist/agents/dew.js +52 -52
  24. package/dist/agents/fair.js +84 -84
  25. package/dist/agents/fog.js +30 -30
  26. package/dist/agents/frost.js +32 -32
  27. package/dist/agents/rain.js +32 -32
  28. package/dist/agents/snow.js +68 -68
  29. package/dist/cli/main.js +103 -51
  30. package/dist/cli/main.js.map +1 -1
  31. package/dist/cli/tui.d.ts.map +1 -1
  32. package/dist/cli/tui.js +8 -1
  33. package/dist/cli/tui.js.map +1 -1
  34. package/dist/core/agent/task.d.ts +58 -0
  35. package/dist/core/agent/task.d.ts.map +1 -0
  36. package/dist/core/agent/task.js +83 -0
  37. package/dist/core/agent/task.js.map +1 -0
  38. package/dist/core/agent.d.ts +2 -45
  39. package/dist/core/agent.d.ts.map +1 -1
  40. package/dist/core/agent.js +61 -145
  41. package/dist/core/agent.js.map +1 -1
  42. package/dist/core/agent_helpers.d.ts +10 -0
  43. package/dist/core/agent_helpers.d.ts.map +1 -1
  44. package/dist/core/agent_helpers.js +39 -0
  45. package/dist/core/agent_helpers.js.map +1 -1
  46. package/dist/core/catalog.d.ts +71 -0
  47. package/dist/core/catalog.d.ts.map +1 -0
  48. package/dist/core/catalog.js +176 -0
  49. package/dist/core/catalog.js.map +1 -0
  50. package/dist/core/config.d.ts +8 -0
  51. package/dist/core/config.d.ts.map +1 -1
  52. package/dist/core/config.js +12 -4
  53. package/dist/core/config.js.map +1 -1
  54. package/dist/core/factory.js +16 -16
  55. package/dist/core/llm.d.ts +7 -0
  56. package/dist/core/llm.d.ts.map +1 -1
  57. package/dist/core/llm.js +139 -7
  58. package/dist/core/llm.js.map +1 -1
  59. package/dist/core/longdoc.js +5 -5
  60. package/dist/core/memory.d.ts.map +1 -1
  61. package/dist/core/memory.js +69 -62
  62. package/dist/core/memory.js.map +1 -1
  63. package/dist/core/theme.d.ts +46 -0
  64. package/dist/core/theme.d.ts.map +1 -0
  65. package/dist/core/theme.js +42 -0
  66. package/dist/core/theme.js.map +1 -0
  67. package/dist/web/server.js +542 -519
  68. package/dist/web/server.js.map +1 -1
  69. package/docs/AESTHETIC_DESIGN.md +144 -0
  70. package/docs/OPTIMIZATION_PLAN.md +178 -0
  71. package/package.json +60 -60
  72. package/scripts/install.js +48 -48
  73. package/scripts/link.js +10 -10
  74. package/setup.bat +79 -79
  75. package/skill-test-ty2fOA/test.md +10 -10
  76. package/src/agents/dew.ts +70 -70
  77. package/src/agents/fair.ts +102 -102
  78. package/src/agents/fog.ts +48 -48
  79. package/src/agents/frost.ts +50 -50
  80. package/src/agents/rain.ts +50 -50
  81. package/src/agents/snow.ts +239 -239
  82. package/src/cli/main.ts +425 -372
  83. package/src/cli/mode.ts +58 -58
  84. package/src/cli/tui.ts +272 -269
  85. package/src/core/agent/task.ts +100 -0
  86. package/src/core/agent.ts +1446 -1549
  87. package/src/core/agent_helpers.ts +496 -461
  88. package/src/core/arbitrate.ts +162 -162
  89. package/src/core/catalog.ts +178 -0
  90. package/src/core/checkpoint.ts +94 -94
  91. package/src/core/config.ts +20 -4
  92. package/src/core/estimate.ts +104 -104
  93. package/src/core/evolve.ts +191 -191
  94. package/src/core/factory.ts +627 -627
  95. package/src/core/filter.ts +103 -103
  96. package/src/core/graph.ts +156 -156
  97. package/src/core/icons.ts +53 -53
  98. package/src/core/index.ts +37 -37
  99. package/src/core/learn.ts +146 -146
  100. package/src/core/llm.ts +108 -5
  101. package/src/core/longdoc.ts +155 -155
  102. package/src/core/mcp_server.ts +176 -176
  103. package/src/core/memory.ts +1178 -1171
  104. package/src/core/profile.ts +255 -255
  105. package/src/core/router.ts +124 -124
  106. package/src/core/sandbox.ts +142 -142
  107. package/src/core/security.ts +243 -243
  108. package/src/core/skill.ts +342 -342
  109. package/src/core/theme.ts +65 -0
  110. package/src/core/tool_router.ts +193 -193
  111. package/src/core/vector.ts +152 -152
  112. package/src/core/workspace.ts +150 -150
  113. package/src/plugins/loader.ts +66 -66
  114. package/src/skills/loader.ts +46 -46
  115. package/src/sql.js.d.ts +29 -29
  116. package/src/tools/builtin.ts +380 -380
  117. package/src/tools/computer.ts +269 -269
  118. package/src/tools/delegate.ts +49 -49
  119. package/src/web/server.ts +660 -634
  120. package/src/web/tts.ts +93 -93
  121. package/tests/agent_helpers.test.ts +48 -0
  122. package/tests/bus.test.ts +121 -121
  123. package/tests/catalog.test.ts +86 -0
  124. package/tests/config.test.ts +41 -0
  125. package/tests/icons.test.ts +45 -45
  126. package/tests/memory.test.ts +147 -0
  127. package/tests/router.test.ts +86 -86
  128. package/tests/schemas.test.ts +51 -51
  129. package/tests/semantic.test.ts +83 -83
  130. package/tests/setup.ts +10 -10
  131. package/tests/skill.test.ts +172 -172
  132. package/tests/task.test.ts +60 -0
  133. package/tests/tool.test.ts +108 -108
  134. package/tests/tool_router.test.ts +71 -71
  135. package/vitest.config.ts +17 -17
package/src/web/tts.ts CHANGED
@@ -1,93 +1,93 @@
1
- /**
2
- * Text-to-Speech integration using Volcano Engine (Doubao) TTS API.
3
- * Uses POST to https://openspeech.bytedance.com/api/v3/tts/unidirectional.
4
- */
5
-
6
- const HTTP_ENDPOINT = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional';
7
-
8
- export interface VoiceOption {
9
- key: string;
10
- name: string;
11
- desc: string;
12
- voiceType: string;
13
- }
14
-
15
- export const VOICE_CATALOG: VoiceOption[] = [
16
- { key: 'xiaohe', name: '小河', desc: '温柔自然女声', voiceType: 'zh_female_xiaohe_uranus_bigtts' },
17
- { key: 'qingxinnvsheng', name: '清新女声', desc: '清澈自然女声', voiceType: 'zh_female_qingxinnvsheng_uranus_bigtts' },
18
- { key: 'cancan', name: '灿灿', desc: '活力甜美少女音', voiceType: 'zh_female_cancan_uranus_bigtts' },
19
- { key: 'sajiaoxuemei', name: '撒娇雪梅', desc: '甜美撒娇少女音', voiceType: 'zh_female_sajiaoxuemei_uranus_bigtts' },
20
- { key: 'meilinvyou', name: '魅力女游', desc: '温柔魅力女声', voiceType: 'zh_female_meilinvyou_uranus_bigtts' },
21
- { key: 'xiaoshan', name: '小杉', desc: '温暖磁性男声', voiceType: 'zh_male_xiaoshan_uranus_bigtts' },
22
- ];
23
-
24
- export interface TTSOptions {
25
- text: string;
26
- voice?: string;
27
- speed?: number;
28
- pitch?: number;
29
- apiKey?: string;
30
- }
31
-
32
- export interface TTSResult {
33
- success: boolean;
34
- audioBase64?: string;
35
- format?: string;
36
- error?: string;
37
- }
38
-
39
- /**
40
- * Convert text to speech using Volcano Engine TTS API.
41
- */
42
- export async function textToSpeech(options: TTSOptions): Promise<TTSResult> {
43
- const { text, voice = 'zh_female_xiaohe_uranus_bigtts', speed = 1.0, pitch = 1.0 } = options;
44
-
45
- if (!text || !text.trim()) {
46
- return { success: false, error: 'Text is required' };
47
- }
48
-
49
- const apiKey = options.apiKey || process.env.VOLC_ACCESS_TOKEN;
50
- if (!apiKey) {
51
- return { success: false, error: 'API key not configured. Set VOLC_ACCESS_TOKEN or pass apiKey.' };
52
- }
53
-
54
- const payload = {
55
- app: { appid: process.env.VOLC_APP_ID || '' },
56
- user: { uid: 'skyloom' },
57
- request: {
58
- reqid: Math.random().toString(36).slice(2, 14),
59
- text,
60
- text_type: 'plain',
61
- operation: 'query',
62
- frontend_type: 'unitTson',
63
- voice: { voice_type: voice, speed_rate: speed, pitch_rate: pitch },
64
- audio: { audio_type: 'mp3' },
65
- },
66
- };
67
-
68
- try {
69
- const response = await fetch(HTTP_ENDPOINT, {
70
- method: 'POST',
71
- headers: {
72
- 'Content-Type': 'application/json',
73
- 'Authorization': `Bearer ${apiKey}`,
74
- },
75
- body: JSON.stringify(payload),
76
- });
77
-
78
- const data: any = await response.json();
79
- if (data.code !== 3000) {
80
- return { success: false, error: `API error: ${data.code} - ${data.message || 'unknown'}` };
81
- }
82
- return { success: true, audioBase64: data.data, format: 'mp3' };
83
- } catch (e: any) {
84
- return { success: false, error: `TTS request failed: ${e.message || e}` };
85
- }
86
- }
87
-
88
- /**
89
- * List available TTS voices.
90
- */
91
- export function listVoices(): VoiceOption[] {
92
- return [...VOICE_CATALOG];
93
- }
1
+ /**
2
+ * Text-to-Speech integration using Volcano Engine (Doubao) TTS API.
3
+ * Uses POST to https://openspeech.bytedance.com/api/v3/tts/unidirectional.
4
+ */
5
+
6
+ const HTTP_ENDPOINT = 'https://openspeech.bytedance.com/api/v3/tts/unidirectional';
7
+
8
+ export interface VoiceOption {
9
+ key: string;
10
+ name: string;
11
+ desc: string;
12
+ voiceType: string;
13
+ }
14
+
15
+ export const VOICE_CATALOG: VoiceOption[] = [
16
+ { key: 'xiaohe', name: '小河', desc: '温柔自然女声', voiceType: 'zh_female_xiaohe_uranus_bigtts' },
17
+ { key: 'qingxinnvsheng', name: '清新女声', desc: '清澈自然女声', voiceType: 'zh_female_qingxinnvsheng_uranus_bigtts' },
18
+ { key: 'cancan', name: '灿灿', desc: '活力甜美少女音', voiceType: 'zh_female_cancan_uranus_bigtts' },
19
+ { key: 'sajiaoxuemei', name: '撒娇雪梅', desc: '甜美撒娇少女音', voiceType: 'zh_female_sajiaoxuemei_uranus_bigtts' },
20
+ { key: 'meilinvyou', name: '魅力女游', desc: '温柔魅力女声', voiceType: 'zh_female_meilinvyou_uranus_bigtts' },
21
+ { key: 'xiaoshan', name: '小杉', desc: '温暖磁性男声', voiceType: 'zh_male_xiaoshan_uranus_bigtts' },
22
+ ];
23
+
24
+ export interface TTSOptions {
25
+ text: string;
26
+ voice?: string;
27
+ speed?: number;
28
+ pitch?: number;
29
+ apiKey?: string;
30
+ }
31
+
32
+ export interface TTSResult {
33
+ success: boolean;
34
+ audioBase64?: string;
35
+ format?: string;
36
+ error?: string;
37
+ }
38
+
39
+ /**
40
+ * Convert text to speech using Volcano Engine TTS API.
41
+ */
42
+ export async function textToSpeech(options: TTSOptions): Promise<TTSResult> {
43
+ const { text, voice = 'zh_female_xiaohe_uranus_bigtts', speed = 1.0, pitch = 1.0 } = options;
44
+
45
+ if (!text || !text.trim()) {
46
+ return { success: false, error: 'Text is required' };
47
+ }
48
+
49
+ const apiKey = options.apiKey || process.env.VOLC_ACCESS_TOKEN;
50
+ if (!apiKey) {
51
+ return { success: false, error: 'API key not configured. Set VOLC_ACCESS_TOKEN or pass apiKey.' };
52
+ }
53
+
54
+ const payload = {
55
+ app: { appid: process.env.VOLC_APP_ID || '' },
56
+ user: { uid: 'skyloom' },
57
+ request: {
58
+ reqid: Math.random().toString(36).slice(2, 14),
59
+ text,
60
+ text_type: 'plain',
61
+ operation: 'query',
62
+ frontend_type: 'unitTson',
63
+ voice: { voice_type: voice, speed_rate: speed, pitch_rate: pitch },
64
+ audio: { audio_type: 'mp3' },
65
+ },
66
+ };
67
+
68
+ try {
69
+ const response = await fetch(HTTP_ENDPOINT, {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/json',
73
+ 'Authorization': `Bearer ${apiKey}`,
74
+ },
75
+ body: JSON.stringify(payload),
76
+ });
77
+
78
+ const data: any = await response.json();
79
+ if (data.code !== 3000) {
80
+ return { success: false, error: `API error: ${data.code} - ${data.message || 'unknown'}` };
81
+ }
82
+ return { success: true, audioBase64: data.data, format: 'mp3' };
83
+ } catch (e: any) {
84
+ return { success: false, error: `TTS request failed: ${e.message || e}` };
85
+ }
86
+ }
87
+
88
+ /**
89
+ * List available TTS voices.
90
+ */
91
+ export function listVoices(): VoiceOption[] {
92
+ return [...VOICE_CATALOG];
93
+ }
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { parseExtractedFacts, synthesizeDelegationSummary } from "../src/core/agent_helpers";
3
+
4
+ describe("parseExtractedFacts", () => {
5
+ it("parses a raw JSON array", () => {
6
+ const out = parseExtractedFacts('[{"key":"lang","value":"ts"}]');
7
+ expect(out).toEqual([{ key: "lang", value: "ts" }]);
8
+ });
9
+
10
+ it("parses JSON inside a markdown fence", () => {
11
+ const out = parseExtractedFacts('```json\n[{"key":"a","value":1}]\n```');
12
+ expect(out).toEqual([{ key: "a", value: 1 }]);
13
+ });
14
+
15
+ it("extracts a JSON array embedded in prose", () => {
16
+ const out = parseExtractedFacts('Sure! Here are the facts: [{"key":"x","value":true}] done.');
17
+ expect(out).toEqual([{ key: "x", value: true }]);
18
+ });
19
+
20
+ it("returns [] for empty or non-JSON input", () => {
21
+ expect(parseExtractedFacts("")).toEqual([]);
22
+ expect(parseExtractedFacts("no json here")).toEqual([]);
23
+ expect(parseExtractedFacts(" ")).toEqual([]);
24
+ });
25
+
26
+ it("returns [] when JSON is an object, not an array", () => {
27
+ expect(parseExtractedFacts('{"key":"x"}')).toEqual([]);
28
+ });
29
+
30
+ it("filters out non-object array members", () => {
31
+ const out = parseExtractedFacts('[{"key":"a","value":1}, "junk", 42, null]');
32
+ // null is typeof 'object' in JS, so it survives the filter — assert the real members
33
+ expect(out).toContainEqual({ key: "a", value: 1 });
34
+ expect(out).not.toContain("junk");
35
+ expect(out).not.toContain(42);
36
+ });
37
+ });
38
+
39
+ describe("synthesizeDelegationSummary", () => {
40
+ it("summarizes successes and failures", () => {
41
+ expect(synthesizeDelegationSummary([["fog", true], ["rain", false]])).toBe(
42
+ "[Delegated: fog | Failed: rain]"
43
+ );
44
+ });
45
+ it("returns empty string when no delegations", () => {
46
+ expect(synthesizeDelegationSummary([])).toBe("");
47
+ });
48
+ });
package/tests/bus.test.ts CHANGED
@@ -1,121 +1,121 @@
1
- /**
2
- * Tests for the event bus.
3
- */
4
- import { describe, it, expect, vi } from 'vitest';
5
- import { MessageBus, Event, EventType } from '../src/core/bus';
6
-
7
- describe('MessageBus', () => {
8
- it('subscribe and publish broadcasts to subscribers', async () => {
9
- const bus = new MessageBus();
10
- const received: Event[] = [];
11
-
12
- async function handler(event: Event) {
13
- received.push(event);
14
- }
15
-
16
- bus.subscribe('test_agent', handler);
17
- const event = new Event(EventType.SYSTEM_EVENT, 'system', null, { msg: 'hello' });
18
- await bus.publish(event);
19
-
20
- expect(received).toHaveLength(1);
21
- expect(received[0].type).toBe(EventType.SYSTEM_EVENT);
22
- expect(received[0].data).toEqual({ msg: 'hello' });
23
- });
24
-
25
- it('direct message goes to target agent only', async () => {
26
- const bus = new MessageBus();
27
- const received: Event[] = [];
28
-
29
- async function handler(event: Event) {
30
- received.push(event);
31
- }
32
-
33
- bus.subscribe('target_agent', handler);
34
- const event = new Event(EventType.TASK_ASSIGNED, 'snow', 'target_agent', { task: 'test' });
35
- await bus.publish(event);
36
-
37
- expect(received).toHaveLength(1);
38
- });
39
-
40
- it('unsubscribe removes handler', async () => {
41
- const bus = new MessageBus();
42
- const received: Event[] = [];
43
-
44
- async function handler(event: Event) {
45
- received.push(event);
46
- }
47
-
48
- bus.subscribe('agent', handler);
49
- bus.unsubscribe('agent');
50
- await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
51
- expect(received).toHaveLength(0);
52
- });
53
-
54
- it('state listener receives state change events', async () => {
55
- const bus = new MessageBus();
56
- const received: Event[] = [];
57
-
58
- async function listener(event: Event) {
59
- received.push(event);
60
- }
61
-
62
- bus.onStateChange(listener);
63
- await bus.notifyStateChange(
64
- new Event(EventType.STATE_CHANGE, 'fog', null, { old_state: 'idle', new_state: 'thinking' })
65
- );
66
-
67
- expect(received).toHaveLength(1);
68
- expect(received[0].type).toBe(EventType.STATE_CHANGE);
69
- expect(received[0].source).toBe('fog');
70
- });
71
-
72
- it('state listener ignores non-state events', async () => {
73
- const bus = new MessageBus();
74
- const received: Event[] = [];
75
-
76
- async function listener(event: Event) {
77
- received.push(event);
78
- }
79
-
80
- bus.onStateChange(listener);
81
- await bus.notifyStateChange(new Event(EventType.TASK_ASSIGNED, 'snow'));
82
- expect(received).toHaveLength(0);
83
- });
84
-
85
- it('history tracks published events', async () => {
86
- const bus = new MessageBus();
87
- for (let i = 0; i < 5; i++) {
88
- await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
89
- }
90
- expect(bus.getHistory()).toHaveLength(5);
91
- expect(bus.getHistory(undefined, undefined, 2)).toHaveLength(2);
92
- });
93
-
94
- it('broadcast skips source agent', async () => {
95
- const bus = new MessageBus();
96
- const called: string[] = [];
97
-
98
- async function fogHandler(_event: Event) { called.push('fog'); }
99
- async function rainHandler(_event: Event) { called.push('rain'); }
100
-
101
- bus.subscribe('fog', fogHandler);
102
- bus.subscribe('rain', rainHandler);
103
- await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
104
-
105
- expect(called).not.toContain('fog');
106
- expect(called).toContain('rain');
107
- });
108
-
109
- it('handler exception does not block other handlers', async () => {
110
- const bus = new MessageBus();
111
- const good: Event[] = [];
112
-
113
- async function failing(_event: Event) { throw new Error('failed'); }
114
- async function ok(event: Event) { good.push(event); }
115
-
116
- bus.subscribe('rain', failing);
117
- bus.subscribe('dew', ok);
118
- await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
119
- expect(good).toHaveLength(1);
120
- });
121
- });
1
+ /**
2
+ * Tests for the event bus.
3
+ */
4
+ import { describe, it, expect, vi } from 'vitest';
5
+ import { MessageBus, Event, EventType } from '../src/core/bus';
6
+
7
+ describe('MessageBus', () => {
8
+ it('subscribe and publish broadcasts to subscribers', async () => {
9
+ const bus = new MessageBus();
10
+ const received: Event[] = [];
11
+
12
+ async function handler(event: Event) {
13
+ received.push(event);
14
+ }
15
+
16
+ bus.subscribe('test_agent', handler);
17
+ const event = new Event(EventType.SYSTEM_EVENT, 'system', null, { msg: 'hello' });
18
+ await bus.publish(event);
19
+
20
+ expect(received).toHaveLength(1);
21
+ expect(received[0].type).toBe(EventType.SYSTEM_EVENT);
22
+ expect(received[0].data).toEqual({ msg: 'hello' });
23
+ });
24
+
25
+ it('direct message goes to target agent only', async () => {
26
+ const bus = new MessageBus();
27
+ const received: Event[] = [];
28
+
29
+ async function handler(event: Event) {
30
+ received.push(event);
31
+ }
32
+
33
+ bus.subscribe('target_agent', handler);
34
+ const event = new Event(EventType.TASK_ASSIGNED, 'snow', 'target_agent', { task: 'test' });
35
+ await bus.publish(event);
36
+
37
+ expect(received).toHaveLength(1);
38
+ });
39
+
40
+ it('unsubscribe removes handler', async () => {
41
+ const bus = new MessageBus();
42
+ const received: Event[] = [];
43
+
44
+ async function handler(event: Event) {
45
+ received.push(event);
46
+ }
47
+
48
+ bus.subscribe('agent', handler);
49
+ bus.unsubscribe('agent');
50
+ await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
51
+ expect(received).toHaveLength(0);
52
+ });
53
+
54
+ it('state listener receives state change events', async () => {
55
+ const bus = new MessageBus();
56
+ const received: Event[] = [];
57
+
58
+ async function listener(event: Event) {
59
+ received.push(event);
60
+ }
61
+
62
+ bus.onStateChange(listener);
63
+ await bus.notifyStateChange(
64
+ new Event(EventType.STATE_CHANGE, 'fog', null, { old_state: 'idle', new_state: 'thinking' })
65
+ );
66
+
67
+ expect(received).toHaveLength(1);
68
+ expect(received[0].type).toBe(EventType.STATE_CHANGE);
69
+ expect(received[0].source).toBe('fog');
70
+ });
71
+
72
+ it('state listener ignores non-state events', async () => {
73
+ const bus = new MessageBus();
74
+ const received: Event[] = [];
75
+
76
+ async function listener(event: Event) {
77
+ received.push(event);
78
+ }
79
+
80
+ bus.onStateChange(listener);
81
+ await bus.notifyStateChange(new Event(EventType.TASK_ASSIGNED, 'snow'));
82
+ expect(received).toHaveLength(0);
83
+ });
84
+
85
+ it('history tracks published events', async () => {
86
+ const bus = new MessageBus();
87
+ for (let i = 0; i < 5; i++) {
88
+ await bus.publish(new Event(EventType.SYSTEM_EVENT, 'system'));
89
+ }
90
+ expect(bus.getHistory()).toHaveLength(5);
91
+ expect(bus.getHistory(undefined, undefined, 2)).toHaveLength(2);
92
+ });
93
+
94
+ it('broadcast skips source agent', async () => {
95
+ const bus = new MessageBus();
96
+ const called: string[] = [];
97
+
98
+ async function fogHandler(_event: Event) { called.push('fog'); }
99
+ async function rainHandler(_event: Event) { called.push('rain'); }
100
+
101
+ bus.subscribe('fog', fogHandler);
102
+ bus.subscribe('rain', rainHandler);
103
+ await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
104
+
105
+ expect(called).not.toContain('fog');
106
+ expect(called).toContain('rain');
107
+ });
108
+
109
+ it('handler exception does not block other handlers', async () => {
110
+ const bus = new MessageBus();
111
+ const good: Event[] = [];
112
+
113
+ async function failing(_event: Event) { throw new Error('failed'); }
114
+ async function ok(event: Event) { good.push(event); }
115
+
116
+ bus.subscribe('rain', failing);
117
+ bus.subscribe('dew', ok);
118
+ await bus.publish(new Event(EventType.SYSTEM_EVENT, 'fog'));
119
+ expect(good).toHaveLength(1);
120
+ });
121
+ });
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import {
3
+ loadCatalog,
4
+ listProviders,
5
+ modelsFor,
6
+ allModels,
7
+ getModelInfo,
8
+ isKnownModel,
9
+ providerLabel,
10
+ validateModel,
11
+ resetCatalogCache,
12
+ PROVIDER_META,
13
+ } from "../src/core/catalog";
14
+
15
+ describe("catalog", () => {
16
+ beforeEach(() => resetCatalogCache());
17
+
18
+ it("loads providers from models.yaml", () => {
19
+ const providers = listProviders();
20
+ expect(providers.length).toBeGreaterThan(0);
21
+ expect(providers).toContain("openai");
22
+ expect(providers).toContain("deepseek");
23
+ });
24
+
25
+ it("orders providers by wizard order", () => {
26
+ const providers = listProviders();
27
+ const orders = providers.map((p) => PROVIDER_META[p]?.order ?? 99);
28
+ const sorted = [...orders].sort((a, b) => a - b);
29
+ expect(orders).toEqual(sorted);
30
+ });
31
+
32
+ it("parses real model fields (context + cost)", () => {
33
+ const gpt4o = getModelInfo("gpt-4o");
34
+ expect(gpt4o).not.toBeNull();
35
+ expect(gpt4o!.provider).toBe("openai");
36
+ expect(gpt4o!.context).toBeGreaterThan(0);
37
+ expect(gpt4o!.costIn).toBeGreaterThan(0);
38
+ });
39
+
40
+ it("contains all real, API-verified deepseek models", () => {
41
+ // Verified live against https://api.deepseek.com/v1/models
42
+ expect(isKnownModel("deepseek-chat")).toBe(true);
43
+ expect(isKnownModel("deepseek-reasoner")).toBe(true);
44
+ expect(isKnownModel("deepseek-v4-flash")).toBe(true);
45
+ expect(isKnownModel("deepseek-v4-pro")).toBe(true);
46
+ });
47
+
48
+ it("still rejects genuinely unknown model ids", () => {
49
+ expect(isKnownModel("deepseek-v9-ultra")).toBe(false);
50
+ expect(isKnownModel("gpt-5-imaginary")).toBe(false);
51
+ });
52
+
53
+ it("marks ollama / zero-cost models as local", () => {
54
+ const local = allModels().filter((m) => m.local);
55
+ expect(local.every((m) => m.costIn === 0 && m.costOut === 0)).toBe(true);
56
+ });
57
+
58
+ it("tolerates provider/ prefix when resolving", () => {
59
+ // openrouter lists "openai/gpt-4.1"; bare "gpt-4.1" should also resolve
60
+ const direct = getModelInfo("gpt-4.1");
61
+ expect(direct).not.toBeNull();
62
+ });
63
+
64
+ it("validateModel passes for a real model and fails with suggestions otherwise", () => {
65
+ expect(validateModel("gpt-4o").ok).toBe(true);
66
+ const bad = validateModel("totally-made-up-model");
67
+ expect(bad.ok).toBe(false);
68
+ expect(bad.suggestions.length).toBeGreaterThan(0);
69
+ expect(bad.suggestions.every((s) => isKnownModel(s))).toBe(true);
70
+ });
71
+
72
+ it("provides display labels", () => {
73
+ expect(providerLabel("openai")).toBe("OpenAI");
74
+ expect(providerLabel("unknown-xyz")).toBe("unknown-xyz");
75
+ });
76
+
77
+ it("modelsFor returns empty for unknown provider", () => {
78
+ expect(modelsFor("nope")).toEqual([]);
79
+ });
80
+
81
+ it("caches the catalog across calls", () => {
82
+ const a = loadCatalog();
83
+ const b = loadCatalog();
84
+ expect(a).toBe(b);
85
+ });
86
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { mergeConfigs } from "../src/core/config";
3
+
4
+ describe("mergeConfigs", () => {
5
+ it("preserves top-level default_model / default_provider (regression)", () => {
6
+ // Previously these were dropped, so the wizard's chosen model never applied.
7
+ const def: any = { agents: { fog: { temperature: 0.7 } }, llm: { default_model: "gpt-4o" } };
8
+ const user: any = { default_model: "deepseek-v4-flash", default_provider: "deepseek" };
9
+ const merged: any = mergeConfigs(def, user);
10
+ expect(merged.default_model).toBe("deepseek-v4-flash");
11
+ expect(merged.default_provider).toBe("deepseek");
12
+ });
13
+
14
+ it("deep-merges the llm block with the user winning", () => {
15
+ const def: any = { agents: {}, llm: { default_model: "gpt-4o", temperature: 0.7 } };
16
+ const user: any = { llm: { default_model: "deepseek-chat" } };
17
+ const merged: any = mergeConfigs(def, user);
18
+ expect(merged.llm.default_model).toBe("deepseek-chat");
19
+ expect(merged.llm.temperature).toBe(0.7); // preserved from default
20
+ });
21
+
22
+ it("merges agents (user overrides, defaults retained)", () => {
23
+ const def: any = { agents: { fog: { temperature: 0.7 }, rain: { temperature: 0.5 } } };
24
+ const user: any = { agents: { fog: { model: "deepseek-v4-pro", temperature: 0.2 } } };
25
+ const merged: any = mergeConfigs(def, user);
26
+ expect(merged.agents.fog.model).toBe("deepseek-v4-pro");
27
+ expect(merged.agents.rain.temperature).toBe(0.5);
28
+ });
29
+
30
+ it("returns the default config unchanged when no user config", () => {
31
+ const def: any = { agents: { fog: {} }, default_model: "gpt-4o" };
32
+ expect(mergeConfigs(def, null)).toBe(def);
33
+ });
34
+
35
+ it("preserves passthrough top-level blocks (memory, workspace)", () => {
36
+ const def: any = { agents: {}, memory: { short_term_limit: 100 }, workspace: { path: "auto" } };
37
+ const merged: any = mergeConfigs(def, { agents: {} } as any);
38
+ expect(merged.memory.short_term_limit).toBe(100);
39
+ expect(merged.workspace.path).toBe("auto");
40
+ });
41
+ });