@dot-ai/core 0.5.2

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 (96) hide show
  1. package/.ai/memory/2026-03-04.md +2 -0
  2. package/.ai/tasks.json +7 -0
  3. package/LICENSE +21 -0
  4. package/dist/__tests__/config.test.d.ts +2 -0
  5. package/dist/__tests__/config.test.d.ts.map +1 -0
  6. package/dist/__tests__/config.test.js +128 -0
  7. package/dist/__tests__/config.test.js.map +1 -0
  8. package/dist/__tests__/e2e.test.d.ts +2 -0
  9. package/dist/__tests__/e2e.test.d.ts.map +1 -0
  10. package/dist/__tests__/e2e.test.js +211 -0
  11. package/dist/__tests__/e2e.test.js.map +1 -0
  12. package/dist/__tests__/engine.test.d.ts +2 -0
  13. package/dist/__tests__/engine.test.d.ts.map +1 -0
  14. package/dist/__tests__/engine.test.js +271 -0
  15. package/dist/__tests__/engine.test.js.map +1 -0
  16. package/dist/__tests__/format.test.d.ts +2 -0
  17. package/dist/__tests__/format.test.d.ts.map +1 -0
  18. package/dist/__tests__/format.test.js +200 -0
  19. package/dist/__tests__/format.test.js.map +1 -0
  20. package/dist/__tests__/labels.test.d.ts +2 -0
  21. package/dist/__tests__/labels.test.d.ts.map +1 -0
  22. package/dist/__tests__/labels.test.js +82 -0
  23. package/dist/__tests__/labels.test.js.map +1 -0
  24. package/dist/__tests__/loader.test.d.ts +2 -0
  25. package/dist/__tests__/loader.test.d.ts.map +1 -0
  26. package/dist/__tests__/loader.test.js +161 -0
  27. package/dist/__tests__/loader.test.js.map +1 -0
  28. package/dist/__tests__/logger.test.d.ts +2 -0
  29. package/dist/__tests__/logger.test.d.ts.map +1 -0
  30. package/dist/__tests__/logger.test.js +95 -0
  31. package/dist/__tests__/logger.test.js.map +1 -0
  32. package/dist/__tests__/nodes.test.d.ts +2 -0
  33. package/dist/__tests__/nodes.test.d.ts.map +1 -0
  34. package/dist/__tests__/nodes.test.js +83 -0
  35. package/dist/__tests__/nodes.test.js.map +1 -0
  36. package/dist/config.d.ts +29 -0
  37. package/dist/config.d.ts.map +1 -0
  38. package/dist/config.js +141 -0
  39. package/dist/config.js.map +1 -0
  40. package/dist/contracts.d.ts +56 -0
  41. package/dist/contracts.d.ts.map +1 -0
  42. package/dist/contracts.js +2 -0
  43. package/dist/contracts.js.map +1 -0
  44. package/dist/engine.d.ts +38 -0
  45. package/dist/engine.d.ts.map +1 -0
  46. package/dist/engine.js +88 -0
  47. package/dist/engine.js.map +1 -0
  48. package/dist/format.d.ts +18 -0
  49. package/dist/format.d.ts.map +1 -0
  50. package/dist/format.js +89 -0
  51. package/dist/format.js.map +1 -0
  52. package/dist/index.d.ts +21 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +22 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/labels.d.ts +13 -0
  57. package/dist/labels.d.ts.map +1 -0
  58. package/dist/labels.js +36 -0
  59. package/dist/labels.js.map +1 -0
  60. package/dist/loader.d.ts +26 -0
  61. package/dist/loader.d.ts.map +1 -0
  62. package/dist/loader.js +120 -0
  63. package/dist/loader.js.map +1 -0
  64. package/dist/logger.d.ts +29 -0
  65. package/dist/logger.d.ts.map +1 -0
  66. package/dist/logger.js +29 -0
  67. package/dist/logger.js.map +1 -0
  68. package/dist/nodes.d.ts +15 -0
  69. package/dist/nodes.d.ts.map +1 -0
  70. package/dist/nodes.js +46 -0
  71. package/dist/nodes.js.map +1 -0
  72. package/dist/types.d.ts +111 -0
  73. package/dist/types.d.ts.map +1 -0
  74. package/dist/types.js +2 -0
  75. package/dist/types.js.map +1 -0
  76. package/package.json +23 -0
  77. package/src/__tests__/config.test.ts +166 -0
  78. package/src/__tests__/e2e.test.ts +257 -0
  79. package/src/__tests__/engine.test.ts +305 -0
  80. package/src/__tests__/format.test.ts +247 -0
  81. package/src/__tests__/labels.test.ts +96 -0
  82. package/src/__tests__/loader.test.ts +191 -0
  83. package/src/__tests__/logger.test.ts +113 -0
  84. package/src/__tests__/nodes.test.ts +103 -0
  85. package/src/config.ts +178 -0
  86. package/src/contracts.ts +71 -0
  87. package/src/engine.ts +145 -0
  88. package/src/format.ts +113 -0
  89. package/src/index.ts +63 -0
  90. package/src/labels.ts +40 -0
  91. package/src/loader.ts +152 -0
  92. package/src/logger.ts +49 -0
  93. package/src/nodes.ts +46 -0
  94. package/src/types.ts +123 -0
  95. package/tsconfig.json +23 -0
  96. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,305 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { boot, enrich, learn } from '../engine.js';
3
+ import type { Providers } from '../engine.js';
4
+
5
+ function createMockProviders(overrides?: Partial<Providers>): Providers {
6
+ return {
7
+ memory: {
8
+ search: vi.fn().mockResolvedValue([]),
9
+ store: vi.fn().mockResolvedValue(undefined),
10
+ },
11
+ skills: {
12
+ list: vi.fn().mockResolvedValue([]),
13
+ match: vi.fn().mockResolvedValue([]),
14
+ load: vi.fn().mockResolvedValue(null),
15
+ },
16
+ identity: {
17
+ load: vi.fn().mockResolvedValue([]),
18
+ },
19
+ routing: {
20
+ route: vi.fn().mockResolvedValue({ model: 'sonnet', reason: 'default' }),
21
+ },
22
+ tasks: {
23
+ list: vi.fn().mockResolvedValue([]),
24
+ get: vi.fn().mockResolvedValue(null),
25
+ create: vi.fn(),
26
+ update: vi.fn(),
27
+ },
28
+ tools: {
29
+ list: vi.fn().mockResolvedValue([]),
30
+ match: vi.fn().mockResolvedValue([]),
31
+ load: vi.fn().mockResolvedValue(null),
32
+ },
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ describe('boot', () => {
38
+ it('calls identity.load(), skills.list(), tools.list()', async () => {
39
+ const providers = createMockProviders();
40
+ await boot(providers);
41
+ expect(providers.identity.load).toHaveBeenCalledOnce();
42
+ expect(providers.skills.list).toHaveBeenCalledOnce();
43
+ expect(providers.tools.list).toHaveBeenCalledOnce();
44
+ });
45
+
46
+ it('returns a BootCache with identities, vocabulary, skills', async () => {
47
+ const providers = createMockProviders();
48
+ const cache = await boot(providers);
49
+ expect(cache).toHaveProperty('identities');
50
+ expect(cache).toHaveProperty('vocabulary');
51
+ expect(cache).toHaveProperty('skills');
52
+ });
53
+
54
+ it('populates identities from identity provider', async () => {
55
+ const mockIdentities = [
56
+ { type: 'agents', content: 'I am Kiwi', source: 'file', priority: 1 },
57
+ ];
58
+ const providers = createMockProviders({
59
+ identity: { load: vi.fn().mockResolvedValue(mockIdentities) },
60
+ });
61
+ const cache = await boot(providers);
62
+ expect(cache.identities).toEqual(mockIdentities);
63
+ });
64
+
65
+ it('populates skills from skills provider', async () => {
66
+ const mockSkills = [
67
+ { name: 'dot-ai-tasks', description: 'Task management', labels: ['tasks', 'cockpit'] },
68
+ ];
69
+ const providers = createMockProviders({
70
+ skills: {
71
+ list: vi.fn().mockResolvedValue(mockSkills),
72
+ match: vi.fn().mockResolvedValue([]),
73
+ load: vi.fn().mockResolvedValue(null),
74
+ },
75
+ });
76
+ const cache = await boot(providers);
77
+ expect(cache.skills).toEqual(mockSkills);
78
+ });
79
+
80
+ it('builds vocabulary from skill and tool labels', async () => {
81
+ const providers = createMockProviders({
82
+ skills: {
83
+ list: vi.fn().mockResolvedValue([
84
+ { name: 'skill-a', description: 'A', labels: ['memory', 'routing'] },
85
+ ]),
86
+ match: vi.fn().mockResolvedValue([]),
87
+ load: vi.fn().mockResolvedValue(null),
88
+ },
89
+ tools: {
90
+ list: vi.fn().mockResolvedValue([
91
+ { name: 'tool-b', description: 'B', labels: ['ui', 'ux'], config: {}, source: 'file' },
92
+ ]),
93
+ match: vi.fn().mockResolvedValue([]),
94
+ load: vi.fn().mockResolvedValue(null),
95
+ },
96
+ });
97
+ const cache = await boot(providers);
98
+ expect(cache.vocabulary).toContain('memory');
99
+ expect(cache.vocabulary).toContain('routing');
100
+ expect(cache.vocabulary).toContain('ui');
101
+ expect(cache.vocabulary).toContain('ux');
102
+ });
103
+
104
+ it('calls identity, skills, and tools providers in parallel (all called before any awaited)', async () => {
105
+ const callOrder: string[] = [];
106
+ const providers = createMockProviders({
107
+ identity: {
108
+ load: vi.fn().mockImplementation(async () => {
109
+ callOrder.push('identity');
110
+ return [];
111
+ }),
112
+ },
113
+ skills: {
114
+ list: vi.fn().mockImplementation(async () => {
115
+ callOrder.push('skills');
116
+ return [];
117
+ }),
118
+ match: vi.fn().mockResolvedValue([]),
119
+ load: vi.fn().mockResolvedValue(null),
120
+ },
121
+ tools: {
122
+ list: vi.fn().mockImplementation(async () => {
123
+ callOrder.push('tools');
124
+ return [];
125
+ }),
126
+ match: vi.fn().mockResolvedValue([]),
127
+ load: vi.fn().mockResolvedValue(null),
128
+ },
129
+ });
130
+ await boot(providers);
131
+ // All three should have been called (order may vary in parallel)
132
+ expect(callOrder).toContain('identity');
133
+ expect(callOrder).toContain('skills');
134
+ expect(callOrder).toContain('tools');
135
+ });
136
+ });
137
+
138
+ describe('enrich', () => {
139
+ const baseCache = {
140
+ identities: [{ type: 'agents', content: 'I am Kiwi', source: 'file', priority: 1 }],
141
+ vocabulary: ['memory', 'routing', 'tasks'],
142
+ skills: [],
143
+ };
144
+
145
+ it('returns an EnrichedContext with all required fields', async () => {
146
+ const providers = createMockProviders();
147
+ const result = await enrich('fix the memory issue', providers, baseCache);
148
+ expect(result).toHaveProperty('prompt');
149
+ expect(result).toHaveProperty('labels');
150
+ expect(result).toHaveProperty('identities');
151
+ expect(result).toHaveProperty('memories');
152
+ expect(result).toHaveProperty('skills');
153
+ expect(result).toHaveProperty('tools');
154
+ expect(result).toHaveProperty('routing');
155
+ });
156
+
157
+ it('includes the original prompt in the result', async () => {
158
+ const providers = createMockProviders();
159
+ const result = await enrich('fix the memory issue', providers, baseCache);
160
+ expect(result.prompt).toBe('fix the memory issue');
161
+ });
162
+
163
+ it('extracts labels from prompt against vocabulary', async () => {
164
+ const providers = createMockProviders();
165
+ const result = await enrich('fix the memory issue', providers, baseCache);
166
+ const labelNames = result.labels.map((l) => l.name);
167
+ expect(labelNames).toContain('memory');
168
+ expect(labelNames).not.toContain('routing');
169
+ });
170
+
171
+ it('includes identities from cache', async () => {
172
+ const providers = createMockProviders();
173
+ const result = await enrich('hello', providers, baseCache);
174
+ expect(result.identities).toEqual(baseCache.identities);
175
+ });
176
+
177
+ it('calls memory.search with prompt and extracted label names', async () => {
178
+ const providers = createMockProviders();
179
+ await enrich('fix the memory issue', providers, baseCache);
180
+ expect(providers.memory.search).toHaveBeenCalledWith('fix the memory issue', ['memory']);
181
+ });
182
+
183
+ it('calls skills.match with extracted labels', async () => {
184
+ const providers = createMockProviders();
185
+ await enrich('fix the routing logic', providers, baseCache);
186
+ expect(providers.skills.match).toHaveBeenCalledWith(
187
+ expect.arrayContaining([expect.objectContaining({ name: 'routing' })]),
188
+ );
189
+ });
190
+
191
+ it('calls tools.match with extracted labels', async () => {
192
+ const providers = createMockProviders();
193
+ await enrich('tasks need fixing', providers, baseCache);
194
+ expect(providers.tools.match).toHaveBeenCalledWith(
195
+ expect.arrayContaining([expect.objectContaining({ name: 'tasks' })]),
196
+ );
197
+ });
198
+
199
+ it('calls routing.route with extracted labels', async () => {
200
+ const providers = createMockProviders();
201
+ await enrich('fix the memory issue', providers, baseCache);
202
+ expect(providers.routing.route).toHaveBeenCalledWith(
203
+ expect.arrayContaining([expect.objectContaining({ name: 'memory' })]),
204
+ );
205
+ });
206
+
207
+ it('populates memories from memory.search result', async () => {
208
+ const mockMemories = [
209
+ { content: 'Memory fact', type: 'fact', source: 'file' },
210
+ ];
211
+ const providers = createMockProviders({
212
+ memory: {
213
+ search: vi.fn().mockResolvedValue(mockMemories),
214
+ store: vi.fn().mockResolvedValue(undefined),
215
+ },
216
+ });
217
+ const result = await enrich('fix the memory issue', providers, baseCache);
218
+ expect(result.memories).toEqual(mockMemories);
219
+ });
220
+
221
+ it('populates skills from skills.match result', async () => {
222
+ const mockSkills = [
223
+ { name: 'dot-ai-tasks', description: 'Tasks', labels: ['tasks'] },
224
+ ];
225
+ const providers = createMockProviders({
226
+ skills: {
227
+ list: vi.fn().mockResolvedValue([]),
228
+ match: vi.fn().mockResolvedValue(mockSkills),
229
+ load: vi.fn().mockResolvedValue(null),
230
+ },
231
+ });
232
+ const result = await enrich('hello', providers, baseCache);
233
+ expect(result.skills).toEqual(mockSkills);
234
+ });
235
+
236
+ it('populates routing from routing.route result', async () => {
237
+ const mockRouting = { model: 'opus', reason: 'complex task' };
238
+ const providers = createMockProviders({
239
+ routing: { route: vi.fn().mockResolvedValue(mockRouting) },
240
+ });
241
+ const result = await enrich('hello', providers, baseCache);
242
+ expect(result.routing).toEqual(mockRouting);
243
+ });
244
+
245
+ it('calls memory, skills, tools, routing in parallel', async () => {
246
+ const callOrder: string[] = [];
247
+ const providers = createMockProviders({
248
+ memory: {
249
+ search: vi.fn().mockImplementation(async () => { callOrder.push('memory'); return []; }),
250
+ store: vi.fn().mockResolvedValue(undefined),
251
+ },
252
+ skills: {
253
+ list: vi.fn().mockResolvedValue([]),
254
+ match: vi.fn().mockImplementation(async () => { callOrder.push('skills'); return []; }),
255
+ load: vi.fn().mockResolvedValue(null),
256
+ },
257
+ tools: {
258
+ list: vi.fn().mockResolvedValue([]),
259
+ match: vi.fn().mockImplementation(async () => { callOrder.push('tools'); return []; }),
260
+ load: vi.fn().mockResolvedValue(null),
261
+ },
262
+ routing: {
263
+ route: vi.fn().mockImplementation(async () => { callOrder.push('routing'); return { model: 'sonnet', reason: 'default' }; }),
264
+ },
265
+ });
266
+ await enrich('hello world', providers, baseCache);
267
+ expect(callOrder).toContain('memory');
268
+ expect(callOrder).toContain('skills');
269
+ expect(callOrder).toContain('tools');
270
+ expect(callOrder).toContain('routing');
271
+ });
272
+ });
273
+
274
+ describe('learn', () => {
275
+ it('calls memory.store with the response', async () => {
276
+ const providers = createMockProviders();
277
+ await learn('This is what I learned today', providers);
278
+ expect(providers.memory.store).toHaveBeenCalledOnce();
279
+ expect(providers.memory.store).toHaveBeenCalledWith(
280
+ expect.objectContaining({ content: 'This is what I learned today', type: 'log' }),
281
+ );
282
+ });
283
+
284
+ it('stores entry with today\'s date', async () => {
285
+ const providers = createMockProviders();
286
+ const today = new Date().toISOString().slice(0, 10);
287
+ await learn('response', providers);
288
+ expect(providers.memory.store).toHaveBeenCalledWith(
289
+ expect.objectContaining({ date: today }),
290
+ );
291
+ });
292
+
293
+ it('does not call any other provider methods', async () => {
294
+ const providers = createMockProviders();
295
+ await learn('response', providers);
296
+ expect(providers.skills.list).not.toHaveBeenCalled();
297
+ expect(providers.identity.load).not.toHaveBeenCalled();
298
+ expect(providers.routing.route).not.toHaveBeenCalled();
299
+ });
300
+
301
+ it('resolves without error on successful store', async () => {
302
+ const providers = createMockProviders();
303
+ await expect(learn('response', providers)).resolves.toBeUndefined();
304
+ });
305
+ });
@@ -0,0 +1,247 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { formatContext } from '../format.js';
3
+ import type { EnrichedContext, Identity, MemoryEntry, Skill, Tool } from '../types.js';
4
+
5
+ function makeContext(overrides?: Partial<EnrichedContext>): EnrichedContext {
6
+ return {
7
+ prompt: 'test prompt',
8
+ labels: [],
9
+ identities: [],
10
+ memories: [],
11
+ skills: [],
12
+ tools: [],
13
+ routing: { model: 'default', reason: '' },
14
+ ...overrides,
15
+ };
16
+ }
17
+
18
+ function makeIdentity(overrides?: Partial<Identity>): Identity {
19
+ return {
20
+ type: 'agents',
21
+ content: 'You are Kiwi.',
22
+ source: 'file',
23
+ priority: 10,
24
+ ...overrides,
25
+ };
26
+ }
27
+
28
+ function makeMemory(overrides?: Partial<MemoryEntry>): MemoryEntry {
29
+ return {
30
+ content: 'User prefers TypeScript',
31
+ type: 'fact',
32
+ source: 'file',
33
+ ...overrides,
34
+ };
35
+ }
36
+
37
+ function makeSkill(overrides?: Partial<Skill>): Skill {
38
+ return {
39
+ name: 'my-skill',
40
+ description: 'A skill',
41
+ labels: [],
42
+ content: 'Skill content here.',
43
+ ...overrides,
44
+ };
45
+ }
46
+
47
+ function makeTool(overrides?: Partial<Tool>): Tool {
48
+ return {
49
+ name: 'bash',
50
+ description: 'Run shell commands',
51
+ labels: [],
52
+ config: {},
53
+ source: 'file',
54
+ ...overrides,
55
+ };
56
+ }
57
+
58
+ describe('formatContext', () => {
59
+ it('includes all sections when identities, memories, skills, and tools are present', () => {
60
+ const ctx = makeContext({
61
+ identities: [makeIdentity({ content: 'You are Kiwi.' })],
62
+ memories: [makeMemory({ content: 'User prefers TypeScript' })],
63
+ skills: [makeSkill({ name: 'dot-ai', content: 'dot-ai skill content' })],
64
+ tools: [makeTool({ name: 'bash', description: 'Run shell commands' })],
65
+ });
66
+
67
+ const result = formatContext(ctx);
68
+
69
+ expect(result).toContain('You are Kiwi.');
70
+ expect(result).toContain('## Relevant Memory');
71
+ expect(result).toContain('User prefers TypeScript');
72
+ expect(result).toContain('## Active Skills');
73
+ expect(result).toContain('dot-ai skill content');
74
+ expect(result).toContain('## Available Tools');
75
+ expect(result).toContain('bash');
76
+ });
77
+
78
+ it('skips identities when skipIdentities: true', () => {
79
+ const ctx = makeContext({
80
+ identities: [makeIdentity({ content: 'You are Kiwi.' })],
81
+ memories: [makeMemory({ content: 'Some memory' })],
82
+ });
83
+
84
+ const result = formatContext(ctx, { skipIdentities: true });
85
+
86
+ expect(result).not.toContain('You are Kiwi.');
87
+ expect(result).toContain('## Relevant Memory');
88
+ });
89
+
90
+ it('includes identities when skipIdentities: false', () => {
91
+ const ctx = makeContext({
92
+ identities: [makeIdentity({ content: 'You are Kiwi.' })],
93
+ });
94
+
95
+ const result = formatContext(ctx, { skipIdentities: false });
96
+
97
+ expect(result).toContain('You are Kiwi.');
98
+ });
99
+
100
+ it('includes identities when skipIdentities is undefined (backward compat)', () => {
101
+ const ctx = makeContext({
102
+ identities: [makeIdentity({ content: 'You are Kiwi.' })],
103
+ });
104
+
105
+ const result = formatContext(ctx);
106
+
107
+ expect(result).toContain('You are Kiwi.');
108
+ });
109
+
110
+ it('sorts identities by priority descending', () => {
111
+ const ctx = makeContext({
112
+ identities: [
113
+ makeIdentity({ content: 'Low priority identity.', priority: 1 }),
114
+ makeIdentity({ content: 'High priority identity.', priority: 100 }),
115
+ ],
116
+ });
117
+
118
+ const result = formatContext(ctx);
119
+ const highPos = result.indexOf('High priority identity.');
120
+ const lowPos = result.indexOf('Low priority identity.');
121
+
122
+ expect(highPos).toBeLessThan(lowPos);
123
+ });
124
+
125
+ it('truncates skill content at maxSkillLength with [...truncated] marker', () => {
126
+ const longContent = 'A'.repeat(200);
127
+ const ctx = makeContext({
128
+ skills: [makeSkill({ content: longContent })],
129
+ });
130
+
131
+ const result = formatContext(ctx, { maxSkillLength: 50 });
132
+
133
+ expect(result).toContain('A'.repeat(50));
134
+ expect(result).toContain('[...truncated]');
135
+ expect(result).not.toContain('A'.repeat(51));
136
+ });
137
+
138
+ it('does not truncate skill content shorter than maxSkillLength', () => {
139
+ const ctx = makeContext({
140
+ skills: [makeSkill({ content: 'Short content.' })],
141
+ });
142
+
143
+ const result = formatContext(ctx, { maxSkillLength: 200 });
144
+
145
+ expect(result).toContain('Short content.');
146
+ expect(result).not.toContain('[...truncated]');
147
+ });
148
+
149
+ it('limits number of skills to maxSkills', () => {
150
+ const ctx = makeContext({
151
+ skills: [
152
+ makeSkill({ name: 'skill-a', content: 'Content A' }),
153
+ makeSkill({ name: 'skill-b', content: 'Content B' }),
154
+ makeSkill({ name: 'skill-c', content: 'Content C' }),
155
+ ],
156
+ });
157
+
158
+ const result = formatContext(ctx, { maxSkills: 2 });
159
+
160
+ expect(result).toContain('skill-a');
161
+ expect(result).toContain('Content A');
162
+ expect(result).toContain('skill-b');
163
+ expect(result).toContain('Content B');
164
+ expect(result).not.toContain('skill-c');
165
+ expect(result).not.toContain('Content C');
166
+ });
167
+
168
+ it('combines skipIdentities + maxSkillLength + maxSkills', () => {
169
+ const ctx = makeContext({
170
+ identities: [makeIdentity({ content: 'You are Kiwi.' })],
171
+ skills: [
172
+ makeSkill({ name: 'skill-a', content: 'A'.repeat(100) }),
173
+ makeSkill({ name: 'skill-b', content: 'B'.repeat(100) }),
174
+ makeSkill({ name: 'skill-c', content: 'C'.repeat(100) }),
175
+ ],
176
+ });
177
+
178
+ const result = formatContext(ctx, {
179
+ skipIdentities: true,
180
+ maxSkillLength: 20,
181
+ maxSkills: 2,
182
+ });
183
+
184
+ expect(result).not.toContain('You are Kiwi.');
185
+ expect(result).toContain('skill-a');
186
+ expect(result).toContain('skill-b');
187
+ expect(result).not.toContain('skill-c');
188
+ expect(result).toContain('[...truncated]');
189
+ expect(result).not.toContain('A'.repeat(21));
190
+ });
191
+
192
+ it('returns empty string when context has all empty arrays', () => {
193
+ const ctx = makeContext();
194
+
195
+ const result = formatContext(ctx);
196
+
197
+ expect(result).toBe('');
198
+ });
199
+
200
+ it('omits routing section when model is "default"', () => {
201
+ const ctx = makeContext({
202
+ routing: { model: 'default', reason: 'no routing needed' },
203
+ });
204
+
205
+ const result = formatContext(ctx);
206
+
207
+ expect(result).not.toContain('## Model Routing');
208
+ });
209
+
210
+ it('includes routing section when model is not "default"', () => {
211
+ const ctx = makeContext({
212
+ routing: { model: 'opus', reason: 'complex task' },
213
+ });
214
+
215
+ const result = formatContext(ctx);
216
+
217
+ expect(result).toContain('## Model Routing');
218
+ expect(result).toContain('opus');
219
+ expect(result).toContain('complex task');
220
+ });
221
+
222
+ it('separates multiple sections with ---', () => {
223
+ const ctx = makeContext({
224
+ identities: [makeIdentity({ content: 'Identity content.' })],
225
+ memories: [makeMemory({ content: 'A memory.' })],
226
+ });
227
+
228
+ const result = formatContext(ctx);
229
+
230
+ expect(result).toContain('---');
231
+ });
232
+
233
+ it('skips skills without content', () => {
234
+ const ctx = makeContext({
235
+ skills: [
236
+ makeSkill({ name: 'no-content', content: undefined }),
237
+ makeSkill({ name: 'has-content', content: 'Real content' }),
238
+ ],
239
+ });
240
+
241
+ const result = formatContext(ctx);
242
+
243
+ expect(result).toContain('has-content');
244
+ expect(result).toContain('Real content');
245
+ expect(result).not.toContain('no-content');
246
+ });
247
+ });
@@ -0,0 +1,96 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { extractLabels, buildVocabulary } from '../labels.js';
3
+
4
+ describe('extractLabels', () => {
5
+ it('returns matched labels from vocabulary', () => {
6
+ const labels = extractLabels('I need to fix the memory issue', ['memory', 'routing', 'skills']);
7
+ expect(labels).toHaveLength(1);
8
+ expect(labels[0]).toEqual({ name: 'memory', source: 'extract' });
9
+ });
10
+
11
+ it('returns multiple matches when several vocabulary words are present', () => {
12
+ const labels = extractLabels('routing and memory both matter', ['memory', 'routing', 'skills']);
13
+ expect(labels).toHaveLength(2);
14
+ const names = labels.map((l) => l.name);
15
+ expect(names).toContain('memory');
16
+ expect(names).toContain('routing');
17
+ });
18
+
19
+ it('returns empty array when no vocabulary words match', () => {
20
+ const labels = extractLabels('hello world', ['memory', 'routing', 'skills']);
21
+ expect(labels).toHaveLength(0);
22
+ });
23
+
24
+ it('is case insensitive', () => {
25
+ const labels = extractLabels('MEMORY is important', ['memory']);
26
+ expect(labels).toHaveLength(1);
27
+ expect(labels[0].name).toBe('memory');
28
+ });
29
+
30
+ it('is case insensitive for vocabulary words too', () => {
31
+ const labels = extractLabels('memory is important', ['Memory']);
32
+ expect(labels).toHaveLength(1);
33
+ expect(labels[0].name).toBe('Memory');
34
+ });
35
+
36
+ it('sets source to "extract" for all labels', () => {
37
+ const labels = extractLabels('use routing for memory', ['routing', 'memory']);
38
+ for (const label of labels) {
39
+ expect(label.source).toBe('extract');
40
+ }
41
+ });
42
+
43
+ it('returns empty array when vocabulary is empty', () => {
44
+ const labels = extractLabels('any prompt at all', []);
45
+ expect(labels).toHaveLength(0);
46
+ });
47
+
48
+ it('does not match partial words within a prompt', () => {
49
+ const labels = extractLabels('my skillset is broad', ['skill']);
50
+ expect(labels).toHaveLength(0);
51
+ });
52
+
53
+ it('matches whole words at word boundaries', () => {
54
+ const labels = extractLabels('my skill is broad', ['skill']);
55
+ expect(labels).toHaveLength(1);
56
+ expect(labels[0].name).toBe('skill');
57
+ });
58
+ });
59
+
60
+ describe('buildVocabulary', () => {
61
+ it('merges skill labels and tool labels into a flat array', () => {
62
+ const vocab = buildVocabulary([['memory', 'routing']], [['ui', 'ux']]);
63
+ expect(vocab).toContain('memory');
64
+ expect(vocab).toContain('routing');
65
+ expect(vocab).toContain('ui');
66
+ expect(vocab).toContain('ux');
67
+ });
68
+
69
+ it('deduplicates labels appearing in multiple skills', () => {
70
+ const vocab = buildVocabulary([['memory', 'routing'], ['memory', 'skills']], []);
71
+ const memoryCount = vocab.filter((v) => v === 'memory').length;
72
+ expect(memoryCount).toBe(1);
73
+ });
74
+
75
+ it('deduplicates labels appearing across skills and tools', () => {
76
+ const vocab = buildVocabulary([['shared']], [['shared']]);
77
+ const count = vocab.filter((v) => v === 'shared').length;
78
+ expect(count).toBe(1);
79
+ });
80
+
81
+ it('returns empty array when both inputs are empty', () => {
82
+ const vocab = buildVocabulary([], []);
83
+ expect(vocab).toHaveLength(0);
84
+ });
85
+
86
+ it('handles empty skill label arrays', () => {
87
+ const vocab = buildVocabulary([[], []], [['tool-label']]);
88
+ expect(vocab).toEqual(['tool-label']);
89
+ });
90
+
91
+ it('returns unique values only', () => {
92
+ const vocab = buildVocabulary([['a', 'b', 'a']], [['b', 'c']]);
93
+ expect(vocab.length).toBe(3);
94
+ expect(new Set(vocab).size).toBe(3);
95
+ });
96
+ });