kodelyth-ecc 1.8.0 → 1.8.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 (91) hide show
  1. package/CLAUDE.md +1 -1
  2. package/VERSION +1 -1
  3. package/bin/kodelyth-ecc.js +54 -5
  4. package/install.ps1 +30 -13
  5. package/install.sh +11 -4
  6. package/package.json +1 -1
  7. package/rules/common/agent-intent-routing.md +96 -195
  8. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -47
  9. package/.github/ISSUE_TEMPLATE/config.yml +0 -8
  10. package/.github/ISSUE_TEMPLATE/feature_request.md +0 -41
  11. package/.github/ISSUE_TEMPLATE/good_first_issue.md +0 -36
  12. package/.github/ISSUE_TEMPLATE/new_agent.md +0 -52
  13. package/.github/PULL_REQUEST_TEMPLATE.md +0 -53
  14. package/.github/workflows/ci.yml +0 -45
  15. package/.github/workflows/publish.yml +0 -60
  16. package/brand/concepts.svg +0 -109
  17. package/brand/convert.js +0 -161
  18. package/brand/favicon.svg +0 -7
  19. package/brand/fb-cover.svg +0 -34
  20. package/brand/fb-profile.svg +0 -21
  21. package/brand/kodelyth-dark.svg +0 -24
  22. package/brand/kodelyth-light.svg +0 -24
  23. package/brand/kodelyth-mark.svg +0 -20
  24. package/brand/package.json +0 -12
  25. package/docs/dashboard.md +0 -211
  26. package/docs/evolve.md +0 -303
  27. package/docs/mcp-clients.md +0 -167
  28. package/docs/mcp.md +0 -178
  29. package/docs/replay.md +0 -244
  30. package/docs/supply-chain.md +0 -207
  31. package/docs/swarm.md +0 -243
  32. package/skills/skill-comply/tests/test_grader.py +0 -137
  33. package/skills/skill-comply/tests/test_parser.py +0 -90
  34. package/social/card-agents.svg +0 -104
  35. package/social/card-install.svg +0 -58
  36. package/social/card-main.svg +0 -54
  37. package/social/facebook-v150.svg +0 -110
  38. package/social/fb-ad-main.svg +0 -128
  39. package/social/fb-post-features.svg +0 -118
  40. package/social/fb-post-launch.svg +0 -144
  41. package/social/fb-post-platforms.svg +0 -135
  42. package/social/github-social-preview.svg +0 -151
  43. package/social/hype-compound-learning.svg +0 -129
  44. package/social/hype-devil-mode.svg +0 -240
  45. package/social/hype-mcp-server.svg +0 -148
  46. package/social/hype-parallel-agents.svg +0 -162
  47. package/social/hype-stats-hero.svg +0 -138
  48. package/social/og-image.svg +0 -151
  49. package/social/readme-agents.svg +0 -138
  50. package/social/readme-hero.svg +0 -102
  51. package/social/section-agents.svg +0 -69
  52. package/social/section-author.svg +0 -66
  53. package/social/section-dashboard.svg +0 -71
  54. package/social/section-devil.svg +0 -66
  55. package/social/section-hooks.svg +0 -63
  56. package/social/section-install.svg +0 -54
  57. package/social/section-learning.svg +0 -64
  58. package/social/section-mcp.svg +0 -58
  59. package/social/section-memory.svg +0 -69
  60. package/social/section-parallel.svg +0 -84
  61. package/social/section-routing.svg +0 -63
  62. package/social/twitter-threads.md +0 -621
  63. package/social/x-card-agents-grid.svg +0 -120
  64. package/social/x-card-free.svg +0 -71
  65. package/social/x-card-hook.svg +0 -94
  66. package/tests/dashboard/data.test.js +0 -391
  67. package/tests/dashboard/server.test.js +0 -257
  68. package/tests/evolve/analyze.test.js +0 -169
  69. package/tests/evolve/proposals.test.js +0 -173
  70. package/tests/evolve/stats.test.js +0 -167
  71. package/tests/hooks/branch-name-check.test.js +0 -184
  72. package/tests/hooks/smart-suggest.test.js +0 -149
  73. package/tests/hooks/test-reminder.test.js +0 -181
  74. package/tests/mcp/catalog.test.js +0 -98
  75. package/tests/mcp/client.test.js +0 -113
  76. package/tests/mcp/resources-prompts.test.js +0 -70
  77. package/tests/mcp/tools.test.js +0 -159
  78. package/tests/memory/auto-recall.test.js +0 -132
  79. package/tests/memory/instincts.test.js +0 -258
  80. package/tests/memory/store.test.js +0 -203
  81. package/tests/replay/bundle.test.js +0 -181
  82. package/tests/replay/replay.test.js +0 -131
  83. package/tests/router/classify.test.js +0 -164
  84. package/tests/run-all.js +0 -55
  85. package/tests/safety/patterns.test.js +0 -94
  86. package/tests/safety/prompt-injection-guard.test.js +0 -111
  87. package/tests/safety/token-budget.test.js +0 -119
  88. package/tests/supply-chain/manifest.test.js +0 -147
  89. package/tests/supply-chain/sbom.test.js +0 -170
  90. package/tests/supply-chain/verify.test.js +0 -146
  91. package/tests/swarm/build-plan.test.js +0 -188
@@ -1,391 +0,0 @@
1
- // Tests for scripts/dashboard/data.js
2
- 'use strict';
3
-
4
- const test = require('node:test');
5
- const assert = require('node:assert/strict');
6
- const fs = require('node:fs');
7
- const os = require('node:os');
8
- const path = require('node:path');
9
-
10
- // IMPORTANT: the memory store freezes KODELYTH_MEMORY_DIR at require time.
11
- // Configure a TMP dir before requiring anything that loads it transitively.
12
- const TMP_MEM = fs.mkdtempSync(path.join(os.tmpdir(), 'kodelyth-dash-mem-suite-'));
13
- process.env.KODELYTH_MEMORY_DIR = TMP_MEM;
14
-
15
- const data = require('../../scripts/dashboard/data.js');
16
- const memoryStore = require('../../scripts/memory/store.js');
17
-
18
- function tmpDir(prefix) { return fs.mkdtempSync(path.join(os.tmpdir(), prefix)); }
19
- function cleanup(dir) { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* */ } }
20
-
21
- // ── overview ────────────────────────────────────────────────────────────────
22
-
23
- test('overview: returns shape with counts + storage paths even on cold dirs', () => {
24
- const memDir = tmpDir('kodelyth-dash-mem-');
25
- const evolveDir = tmpDir('kodelyth-dash-evol-');
26
- const coordDir = tmpDir('kodelyth-dash-coord-');
27
- try {
28
- const o = data.overview({ memoryDir: memDir, evolveDir, coordRoot: coordDir });
29
- assert.equal(o.storage.memory_dir, memDir);
30
- assert.equal(o.storage.evolve_dir, evolveDir);
31
- assert.equal(o.storage.coord_root, coordDir);
32
- assert.ok(typeof o.catalog.agents === 'number');
33
- assert.ok(typeof o.catalog.skills === 'number');
34
- assert.equal(o.evolve.proposals.pending, 0);
35
- assert.equal(o.evolve.proposals.total, 0);
36
- assert.equal(o.swarm.sessions, 0);
37
- assert.match(o.generated_at, /^\d{4}-\d{2}-\d{2}T/);
38
- } finally { cleanup(memDir); cleanup(evolveDir); cleanup(coordDir); }
39
- });
40
-
41
- test('overview: catalog counts are non-zero for the real package root', () => {
42
- const o = data.overview();
43
- assert.ok(o.catalog.skills > 0, 'skills count > 0 for real install');
44
- assert.ok(o.catalog.commands > 0);
45
- });
46
-
47
- // ── memory ──────────────────────────────────────────────────────────────────
48
- // All memory tests share TMP_MEM (the store freezes its dir at require time).
49
- // Run order matters: empty assertion → seed → readback.
50
-
51
- test('memory: empty store yields empty recent list', () => {
52
- // Runs first, before any captures.
53
- assert.deepEqual(data.recentMemories({ limit: 5 }), []);
54
- assert.equal(data.memoryStats().total, 0);
55
- });
56
-
57
- test('memory: empty query returns []', () => {
58
- assert.deepEqual(data.memorySearch({ query: '' }), []);
59
- assert.deepEqual(data.memorySearch({}), []);
60
- });
61
-
62
- test('memory: recent + search after seeding two captures', async () => {
63
- const m1 = memoryStore.capture({ problem: 'unique-token-tailwind-v4-arbitrary one', approach: 'A1', tags: ['css'], language: 'js' });
64
- await new Promise(r => setTimeout(r, 15));
65
- const m2 = memoryStore.capture({ problem: 'second-distinct-problem-here', approach: 'A2', tags: ['ts'], language: 'ts' });
66
-
67
- const recent = data.recentMemories({ limit: 5 });
68
- assert.ok(recent.length >= 2, 'has at least the two captures');
69
- // Newest first.
70
- const newestId = recent[0].id;
71
- assert.equal(newestId, m2.id);
72
- assert.ok(recent.some(r => r.id === m1.id));
73
- assert.match(recent[0].approach, /^A2/);
74
-
75
- const stats = data.memoryStats();
76
- assert.ok(stats.total >= 2);
77
- assert.ok(typeof stats.byLanguage === 'object');
78
-
79
- const hits = data.memorySearch({ query: 'unique-token-tailwind-v4-arbitrary', limit: 5 });
80
- assert.ok(hits.length >= 1, 'BM25 finds seeded memory');
81
- assert.match(hits[0].problem, /unique-token-tailwind-v4-arbitrary/);
82
- assert.ok(typeof hits[0].score === 'number');
83
- });
84
-
85
- // ── evolve ──────────────────────────────────────────────────────────────────
86
-
87
- test('evolveSnapshot: empty dir → zero counts + empty arrays', () => {
88
- const evolveDir = tmpDir('kodelyth-dash-evol-');
89
- try {
90
- const e = data.evolveSnapshot({ evolveDir });
91
- assert.equal(e.reuse.total_memories_tracked, 0);
92
- assert.equal(e.reuse.total_surfaces, 0);
93
- assert.equal(e.miss.total_misses, 0);
94
- assert.deepEqual(e.proposals, []);
95
- } finally { cleanup(evolveDir); }
96
- });
97
-
98
- test('evolveSnapshot: surfaces a recorded reuse + proposal', () => {
99
- const evolveDir = tmpDir('kodelyth-dash-evol-');
100
- try {
101
- const stats = require('../../scripts/evolve/stats.js');
102
- const proposals = require('../../scripts/evolve/proposals.js');
103
- stats.recordSurface({ memoryId: 'mem-x', sessionId: 's1' }, evolveDir);
104
- proposals.appendProposal({
105
- id: 'skill-test',
106
- type: 'skill-upgrade',
107
- proposal: { kind: 'create-skill', target_path: 'skills/x/SKILL.md', diff: '# x', rationale: 'why' },
108
- }, evolveDir);
109
-
110
- const e = data.evolveSnapshot({ evolveDir });
111
- assert.equal(e.reuse.total_memories_tracked, 1);
112
- assert.equal(e.reuse.total_surfaces, 1);
113
- assert.equal(e.proposals.length, 1);
114
- assert.equal(e.proposals[0].id, 'skill-test');
115
- assert.equal(e.proposals[0].status, 'pending');
116
- assert.equal(e.proposals[0].target_path, 'skills/x/SKILL.md');
117
- } finally { cleanup(evolveDir); }
118
- });
119
-
120
- // ── catalog ─────────────────────────────────────────────────────────────────
121
-
122
- test('catalogList: rejects unknown kind by returning []', () => {
123
- assert.deepEqual(data.catalogList({ kind: 'junk' }), []);
124
- });
125
-
126
- test('catalogList: returns skills for kind=skills', () => {
127
- const items = data.catalogList({ kind: 'skills', limit: 5 });
128
- assert.ok(items.length >= 1);
129
- assert.ok(typeof items[0].name === 'string');
130
- });
131
-
132
- test('catalogList: query filters by substring', () => {
133
- const items = data.catalogList({ kind: 'skills', query: 'self-evolving', limit: 50 });
134
- // Phase 3.4 just shipped this skill — must be findable.
135
- assert.ok(items.some(it => it.name.includes('self-evolving')), 'finds self-evolving-memory skill');
136
- });
137
-
138
- test('catalogList: limit caps results', () => {
139
- const items = data.catalogList({ kind: 'skills', limit: 3 });
140
- assert.ok(items.length <= 3);
141
- });
142
-
143
- test('catalogCounts: matches stats() shape', () => {
144
- const c = data.catalogCounts();
145
- for (const k of ['agents', 'skills', 'commands', 'rules', 'bundles']) {
146
- assert.ok(typeof c[k] === 'number');
147
- }
148
- });
149
-
150
- // ── sessions ────────────────────────────────────────────────────────────────
151
-
152
- test('sessionsList: missing coord dir returns []', () => {
153
- assert.deepEqual(data.sessionsList({ coordRoot: '/tmp/__never_exists_xyz_kodelyth' }), []);
154
- });
155
-
156
- test('sessionsList: returns sessions with worker counts', () => {
157
- const root = tmpDir('kodelyth-dash-coord-');
158
- try {
159
- fs.mkdirSync(path.join(root, 'sess-A', 'worker-1'), { recursive: true });
160
- fs.mkdirSync(path.join(root, 'sess-A', 'worker-2'), { recursive: true });
161
- fs.mkdirSync(path.join(root, 'sess-B', 'worker-1'), { recursive: true });
162
- const list = data.sessionsList({ coordRoot: root });
163
- assert.equal(list.length, 2);
164
- const a = list.find(s => s.session === 'sess-A');
165
- assert.equal(a.worker_count, 2);
166
- assert.deepEqual(a.workers.sort(), ['worker-1', 'worker-2']);
167
- } finally { cleanup(root); }
168
- });
169
-
170
- test('sessionDetail: returns null for missing session', () => {
171
- assert.equal(data.sessionDetail({ session: 'nope', coordRoot: '/tmp/__never_exists_xyz_kodelyth' }), null);
172
- });
173
-
174
- test('sessionDetail: reads task/handoff/status excerpts', () => {
175
- const root = tmpDir('kodelyth-dash-coord-');
176
- try {
177
- const wdir = path.join(root, 'sess-X', 'worker-1');
178
- fs.mkdirSync(wdir, { recursive: true });
179
- fs.writeFileSync(path.join(wdir, 'task.md'), '# task\nbody\n');
180
- fs.writeFileSync(path.join(wdir, 'handoff.md'), '# handoff\nhello\n');
181
- const d = data.sessionDetail({ session: 'sess-X', coordRoot: root });
182
- assert.ok(d);
183
- assert.equal(d.workers.length, 1);
184
- const w = d.workers[0];
185
- assert.equal(w.has_task, true);
186
- assert.equal(w.has_handoff, true);
187
- assert.equal(w.has_status, false);
188
- assert.match(w.task_excerpt, /^# task/);
189
- assert.match(w.handoff_excerpt, /hello/);
190
- } finally { cleanup(root); }
191
- });
192
-
193
- test('safeReadExcerpt: truncates large files', () => {
194
- const tmp = tmpDir('kodelyth-dash-trunc-');
195
- const f = path.join(tmp, 'big.md');
196
- try {
197
- fs.writeFileSync(f, 'x'.repeat(5000));
198
- const ex = data._internals.safeReadExcerpt(f, 100);
199
- assert.ok(ex.endsWith('…(truncated)'));
200
- assert.ok(ex.length < 5000);
201
- } finally { cleanup(tmp); }
202
- });
203
-
204
- // ── token budget ────────────────────────────────────────────────────────────
205
-
206
- test('tokenBudgetSnapshot: missing dir → empty result', () => {
207
- const r = data.tokenBudgetSnapshot({ budgetDir: '/tmp/__never_exists_kodelyth_budget' });
208
- assert.deepEqual(r, { sessions: [], total_tokens: 0 });
209
- });
210
-
211
- test('tokenBudgetSnapshot: aggregates JSON budget files', () => {
212
- const dir = tmpDir('kodelyth-dash-budget-');
213
- try {
214
- fs.writeFileSync(path.join(dir, 'budget-s1.json'),
215
- JSON.stringify({ session_id: 's1', tokens: 1234, budget: 10000, mode: 'warn', last_event: '2026-05-10T10:00:00Z' }));
216
- fs.writeFileSync(path.join(dir, 'budget-s2.json'),
217
- JSON.stringify({ session_id: 's2', tokens: 5678, budget: 10000, mode: 'warn', last_event: '2026-05-10T11:00:00Z' }));
218
- const r = data.tokenBudgetSnapshot({ budgetDir: dir });
219
- assert.equal(r.sessions.length, 2);
220
- assert.equal(r.total_tokens, 1234 + 5678);
221
- assert.equal(r.sessions[0].session_id, 's2', 'sorted by last_event desc');
222
- } finally { cleanup(dir); }
223
- });
224
-
225
- test('tokenBudgetSnapshot: silently skips malformed budget files', () => {
226
- const dir = tmpDir('kodelyth-dash-budget-');
227
- try {
228
- fs.writeFileSync(path.join(dir, 'budget-broken.json'), 'not-json{{{');
229
- fs.writeFileSync(path.join(dir, 'budget-good.json'),
230
- JSON.stringify({ session_id: 'good', tokens: 100 }));
231
- const r = data.tokenBudgetSnapshot({ budgetDir: dir });
232
- assert.equal(r.sessions.length, 1, 'only good file counted');
233
- assert.equal(r.total_tokens, 100);
234
- } finally { cleanup(dir); }
235
- });
236
-
237
- // ── ide sessions (claude code / windsurf / antigravity) ─────────────────────
238
-
239
- function emptyIdeOpts(label) {
240
- // Build a fully-empty IDE options bag so each call only sees what the test feeds it.
241
- const cc = tmpDir(`kodelyth-cc-${label}-`); cleanup(cc);
242
- const ws = tmpDir(`kodelyth-ws-${label}-`); cleanup(ws);
243
- const wn = tmpDir(`kodelyth-wn-${label}-`); cleanup(wn);
244
- const cu = tmpDir(`kodelyth-cu-${label}-`); cleanup(cu);
245
- const ag = tmpDir(`kodelyth-ag-${label}-`); cleanup(ag);
246
- const cwd = tmpDir(`kodelyth-cwd-${label}-`);
247
- return {
248
- claudeProjectsDir: cc, windsurfDir: ws, windsurfNextDir: wn,
249
- cursorDir: cu, antigravityDir: ag, extraDirs: [], cwd,
250
- _cleanup: () => cleanup(cwd),
251
- };
252
- }
253
-
254
- test('ideSessionsList: missing dirs return []', () => {
255
- const o = emptyIdeOpts('missing');
256
- try {
257
- const list = data.ideSessionsList(o);
258
- assert.deepEqual(list, []);
259
- } finally { o._cleanup(); }
260
- });
261
-
262
- test('ideSessionsList: surfaces claude-code sessions sorted by mtime desc', async () => {
263
- const o = emptyIdeOpts('cc-sort');
264
- o.claudeProjectsDir = tmpDir('kodelyth-cc-');
265
- try {
266
- const proj = path.join(o.claudeProjectsDir, '-Users-alice-myproject');
267
- fs.mkdirSync(proj, { recursive: true });
268
- fs.writeFileSync(path.join(proj, 'aaa-old.jsonl'), '{"x":1}\n');
269
- await new Promise(r => setTimeout(r, 20));
270
- fs.writeFileSync(path.join(proj, 'bbb-new.jsonl'), '{"x":2}\n');
271
-
272
- const list = data.ideSessionsList({ ...o, limit: 10 });
273
- assert.equal(list.length, 2);
274
- assert.equal(list[0].platform, 'claude-code');
275
- assert.equal(list[0].session_id, 'bbb-new', 'newest first');
276
- assert.equal(list[1].session_id, 'aaa-old');
277
- assert.match(list[0].project, /alice/, 'project path is decoded best-effort');
278
- } finally { cleanup(o.claudeProjectsDir); o._cleanup(); }
279
- });
280
-
281
- test('ideSessionsList: detects antigravity .agent/ in cwd', () => {
282
- const o = emptyIdeOpts('ag-cwd');
283
- try {
284
- fs.mkdirSync(path.join(o.cwd, '.agent'), { recursive: true });
285
- const list = data.ideSessionsList(o);
286
- assert.equal(list.length, 1);
287
- assert.equal(list[0].platform, 'antigravity');
288
- assert.equal(list[0].project, o.cwd);
289
- } finally { o._cleanup(); }
290
- });
291
-
292
- test('ideSessionsList: detects windsurf-next state files', () => {
293
- const o = emptyIdeOpts('wn');
294
- o.windsurfNextDir = tmpDir('kodelyth-wn-pop-');
295
- try {
296
- fs.writeFileSync(path.join(o.windsurfNextDir, 'state.json'), '{}');
297
- const list = data.ideSessionsList(o);
298
- assert.equal(list.length, 1);
299
- assert.equal(list[0].platform, 'windsurf-next');
300
- assert.equal(list[0].session_id, 'state.json');
301
- } finally { cleanup(o.windsurfNextDir); o._cleanup(); }
302
- });
303
-
304
- test('ideSessionsList: detects cursor workspace dirs', () => {
305
- const o = emptyIdeOpts('cu');
306
- o.cursorDir = tmpDir('kodelyth-cu-pop-');
307
- try {
308
- fs.mkdirSync(path.join(o.cursorDir, 'abc123hash'), { recursive: true });
309
- const list = data.ideSessionsList(o);
310
- assert.equal(list.length, 1);
311
- assert.equal(list[0].platform, 'cursor');
312
- assert.equal(list[0].session_id, 'abc123hash');
313
- } finally { cleanup(o.cursorDir); o._cleanup(); }
314
- });
315
-
316
- test('ideSessionsList: KODELYTH_EXTRA_IDE_WATCH paths surface as custom platform', () => {
317
- const o = emptyIdeOpts('extra');
318
- const extra = tmpDir('kodelyth-extra-');
319
- try {
320
- fs.writeFileSync(path.join(extra, 'log.txt'), 'hi');
321
- o.extraDirs = [extra];
322
- const list = data.ideSessionsList(o);
323
- const customs = list.filter(s => s.platform === 'custom');
324
- assert.equal(customs.length, 1);
325
- assert.equal(customs[0].path, extra);
326
- } finally { cleanup(extra); o._cleanup(); }
327
- });
328
-
329
- test('extraIdeWatchDirs: splits comma/semicolon/colon separated list', () => {
330
- const oldEnv = process.env.KODELYTH_EXTRA_IDE_WATCH;
331
- try {
332
- process.env.KODELYTH_EXTRA_IDE_WATCH = '/a/b , /c/d ; /e/f : /g/h';
333
- const dirs = data._internals.extraIdeWatchDirs();
334
- assert.deepEqual(dirs, ['/a/b', '/c/d', '/e/f', '/g/h']);
335
- } finally {
336
- if (oldEnv === undefined) delete process.env.KODELYTH_EXTRA_IDE_WATCH;
337
- else process.env.KODELYTH_EXTRA_IDE_WATCH = oldEnv;
338
- }
339
- });
340
-
341
- test('defaultCursorWorkspaceDir: returns an OS-appropriate path', () => {
342
- const oldEnv = process.env.KODELYTH_CURSOR_DIR;
343
- delete process.env.KODELYTH_CURSOR_DIR;
344
- try {
345
- const p = data._internals.defaultCursorWorkspaceDir();
346
- assert.match(p, /Cursor/);
347
- } finally {
348
- if (oldEnv !== undefined) process.env.KODELYTH_CURSOR_DIR = oldEnv;
349
- }
350
- });
351
-
352
- test('getMaxIdeMtime: returns 0 when no IDE dirs exist', () => {
353
- const o = emptyIdeOpts('mtime-zero');
354
- try {
355
- const m = data._internals.getMaxIdeMtime(o);
356
- assert.equal(m, 0);
357
- } finally { o._cleanup(); }
358
- });
359
-
360
- test('getMaxIdeMtime: reflects newest mtime across watched IDE files', async () => {
361
- const o = emptyIdeOpts('mtime-new');
362
- o.claudeProjectsDir = tmpDir('kodelyth-cc-mtime-');
363
- try {
364
- const proj = path.join(o.claudeProjectsDir, '-tmp-x');
365
- fs.mkdirSync(proj, { recursive: true });
366
- fs.writeFileSync(path.join(proj, 'first.jsonl'), 'x');
367
- await new Promise(r => setTimeout(r, 20));
368
- fs.writeFileSync(path.join(proj, 'second.jsonl'), 'y');
369
-
370
- const m = data._internals.getMaxIdeMtime(o);
371
- const newestStat = fs.statSync(path.join(proj, 'second.jsonl'));
372
- assert.equal(m, newestStat.mtimeMs);
373
- } finally { cleanup(o.claudeProjectsDir); o._cleanup(); }
374
- });
375
-
376
- test('getMaxIdeMtime: picks up windsurf-next + cursor + extra dirs', () => {
377
- const o = emptyIdeOpts('mtime-all');
378
- o.windsurfNextDir = tmpDir('kodelyth-wn-mt-');
379
- o.cursorDir = tmpDir('kodelyth-cu-mt-');
380
- const extra = tmpDir('kodelyth-extra-mt-');
381
- o.extraDirs = [extra];
382
- try {
383
- fs.writeFileSync(path.join(o.windsurfNextDir, 's.json'), 'a');
384
- fs.mkdirSync(path.join(o.cursorDir, 'workspace1'), { recursive: true });
385
- fs.writeFileSync(path.join(extra, 'note.txt'), 'b');
386
- const m = data._internals.getMaxIdeMtime(o);
387
- assert.ok(m > 0, 'should return a positive mtime');
388
- } finally {
389
- cleanup(o.windsurfNextDir); cleanup(o.cursorDir); cleanup(extra); o._cleanup();
390
- }
391
- });
@@ -1,257 +0,0 @@
1
- // Smoke tests for scripts/dashboard/server.js
2
- //
3
- // We boot the real HTTP server on an ephemeral localhost port, exercise
4
- // every route via the built-in `http` module (no external HTTP client),
5
- // and assert response shapes + security posture.
6
- 'use strict';
7
-
8
- const test = require('node:test');
9
- const assert = require('node:assert/strict');
10
- const http = require('node:http');
11
-
12
- const { createServer, _internals } = require('../../scripts/dashboard/server.js');
13
- const dashboard = require('../../scripts/dashboard/server.js');
14
-
15
- // ── helpers ──────────────────────────────────────────────────────────────────
16
-
17
- function listenEphemeral(server) {
18
- return new Promise((resolve, reject) => {
19
- server.once('error', reject);
20
- server.listen(0, '127.0.0.1', () => resolve(server.address().port));
21
- });
22
- }
23
-
24
- function get(port, path, { method = 'GET', headers = {} } = {}) {
25
- return new Promise((resolve, reject) => {
26
- const req = http.request({ host: '127.0.0.1', port, path, method, headers }, res => {
27
- const chunks = [];
28
- res.on('data', c => chunks.push(c));
29
- res.on('end', () => resolve({
30
- status: res.statusCode,
31
- headers: res.headers,
32
- body: Buffer.concat(chunks).toString('utf8'),
33
- }));
34
- });
35
- req.on('error', reject);
36
- req.end();
37
- });
38
- }
39
-
40
- function shutdown(server) {
41
- return new Promise(resolve => server.close(resolve));
42
- }
43
-
44
- // ── tests ───────────────────────────────────────────────────────────────────
45
-
46
- test('GET /api/health returns ok+time', async () => {
47
- const server = createServer();
48
- const port = await listenEphemeral(server);
49
- try {
50
- const r = await get(port, '/api/health');
51
- assert.equal(r.status, 200);
52
- const body = JSON.parse(r.body);
53
- assert.equal(body.ok, true);
54
- assert.match(body.time, /^\d{4}-\d{2}-\d{2}T/);
55
- } finally { await shutdown(server); }
56
- });
57
-
58
- test('GET /api/overview returns shape', async () => {
59
- const server = createServer();
60
- const port = await listenEphemeral(server);
61
- try {
62
- const r = await get(port, '/api/overview');
63
- assert.equal(r.status, 200);
64
- const body = JSON.parse(r.body);
65
- assert.ok(body.catalog);
66
- assert.ok(body.memory);
67
- assert.ok(body.evolve);
68
- assert.ok(body.swarm);
69
- assert.ok(body.storage);
70
- } finally { await shutdown(server); }
71
- });
72
-
73
- test('GET /api/memory returns stats + recent', async () => {
74
- const server = createServer();
75
- const port = await listenEphemeral(server);
76
- try {
77
- const r = await get(port, '/api/memory?limit=5');
78
- assert.equal(r.status, 200);
79
- const body = JSON.parse(r.body);
80
- assert.ok(body.stats);
81
- assert.ok(Array.isArray(body.recent));
82
- } finally { await shutdown(server); }
83
- });
84
-
85
- test('GET /api/memory/search rejects missing q', async () => {
86
- const server = createServer();
87
- const port = await listenEphemeral(server);
88
- try {
89
- const r = await get(port, '/api/memory/search');
90
- assert.equal(r.status, 400);
91
- const body = JSON.parse(r.body);
92
- assert.match(body.error, /q/);
93
- } finally { await shutdown(server); }
94
- });
95
-
96
- test('GET /api/evolve returns reuse + miss + proposals', async () => {
97
- const server = createServer();
98
- const port = await listenEphemeral(server);
99
- try {
100
- const r = await get(port, '/api/evolve');
101
- assert.equal(r.status, 200);
102
- const body = JSON.parse(r.body);
103
- assert.ok(body.reuse);
104
- assert.ok(body.miss);
105
- assert.ok(Array.isArray(body.proposals));
106
- } finally { await shutdown(server); }
107
- });
108
-
109
- test('GET /api/catalog?kind=skills returns items', async () => {
110
- const server = createServer();
111
- const port = await listenEphemeral(server);
112
- try {
113
- const r = await get(port, '/api/catalog?kind=skills&limit=5');
114
- assert.equal(r.status, 200);
115
- const body = JSON.parse(r.body);
116
- assert.equal(body.kind, 'skills');
117
- assert.ok(body.counts);
118
- assert.ok(Array.isArray(body.items));
119
- } finally { await shutdown(server); }
120
- });
121
-
122
- test('GET /api/catalog rejects bad kind with 400', async () => {
123
- const server = createServer();
124
- const port = await listenEphemeral(server);
125
- try {
126
- const r = await get(port, '/api/catalog?kind=junk');
127
- assert.equal(r.status, 400);
128
- const body = JSON.parse(r.body);
129
- assert.match(body.error, /kind/);
130
- } finally { await shutdown(server); }
131
- });
132
-
133
- test('GET /api/sessions returns array', async () => {
134
- const server = createServer();
135
- const port = await listenEphemeral(server);
136
- try {
137
- const r = await get(port, '/api/sessions');
138
- assert.equal(r.status, 200);
139
- const body = JSON.parse(r.body);
140
- assert.ok(Array.isArray(body.sessions));
141
- } finally { await shutdown(server); }
142
- });
143
-
144
- test('GET /api/sessions/:nonexistent returns 404', async () => {
145
- const server = createServer();
146
- const port = await listenEphemeral(server);
147
- try {
148
- const r = await get(port, '/api/sessions/this-session-never-exists-xyz');
149
- assert.equal(r.status, 404);
150
- } finally { await shutdown(server); }
151
- });
152
-
153
- test('GET /api/ide-sessions returns array', async () => {
154
- const server = createServer();
155
- const port = await listenEphemeral(server);
156
- try {
157
- const r = await get(port, '/api/ide-sessions?limit=3');
158
- assert.equal(r.status, 200);
159
- const body = JSON.parse(r.body);
160
- assert.ok(Array.isArray(body.sessions));
161
- // Each entry, if any, has the expected shape.
162
- for (const s of body.sessions) {
163
- assert.ok(['claude-code', 'windsurf', 'antigravity'].includes(s.platform));
164
- assert.ok(typeof s.session_id === 'string');
165
- assert.ok(typeof s.modified === 'string');
166
- }
167
- } finally { await shutdown(server); }
168
- });
169
-
170
- test('GET /api/token-budget returns aggregate shape', async () => {
171
- const server = createServer();
172
- const port = await listenEphemeral(server);
173
- try {
174
- const r = await get(port, '/api/token-budget');
175
- assert.equal(r.status, 200);
176
- const body = JSON.parse(r.body);
177
- assert.ok(Array.isArray(body.sessions));
178
- assert.ok(typeof body.total_tokens === 'number');
179
- } finally { await shutdown(server); }
180
- });
181
-
182
- test('GET /api/missing-route returns 404', async () => {
183
- const server = createServer();
184
- const port = await listenEphemeral(server);
185
- try {
186
- const r = await get(port, '/api/totally-made-up');
187
- assert.equal(r.status, 404);
188
- } finally { await shutdown(server); }
189
- });
190
-
191
- test('POST is rejected with 405', async () => {
192
- const server = createServer();
193
- const port = await listenEphemeral(server);
194
- try {
195
- const r = await get(port, '/api/health', { method: 'POST' });
196
- assert.equal(r.status, 405);
197
- assert.equal(r.headers.allow, 'GET');
198
- } finally { await shutdown(server); }
199
- });
200
-
201
- test('static root serves index.html', async () => {
202
- const server = createServer();
203
- const port = await listenEphemeral(server);
204
- try {
205
- const r = await get(port, '/');
206
- assert.equal(r.status, 200);
207
- assert.match(r.headers['content-type'], /^text\/html/);
208
- assert.match(r.body, /Kodelyth ECC/);
209
- } finally { await shutdown(server); }
210
- });
211
-
212
- test('hardened headers on every response', async () => {
213
- const server = createServer();
214
- const port = await listenEphemeral(server);
215
- try {
216
- const r = await get(port, '/api/health');
217
- assert.equal(r.headers['x-frame-options'], 'DENY');
218
- assert.equal(r.headers['x-content-type-options'], 'nosniff');
219
- assert.equal(r.headers['referrer-policy'], 'no-referrer');
220
- assert.equal(r.headers['cache-control'], 'no-store');
221
- } finally { await shutdown(server); }
222
- });
223
-
224
- test('start() refuses non-localhost without override', async () => {
225
- await assert.rejects(
226
- () => dashboard.start({ port: 0, host: '0.0.0.0', openBrowser: false, log: () => {} }),
227
- /refusing to bind/,
228
- );
229
- });
230
-
231
- test('start() allows non-localhost when override env var is set', async () => {
232
- const oldEnv = process.env.KODELYTH_DASHBOARD_ALLOW_REMOTE;
233
- process.env.KODELYTH_DASHBOARD_ALLOW_REMOTE = '1';
234
- try {
235
- const { server } = await dashboard.start({ port: 0, host: '127.0.0.1', openBrowser: false, log: () => {} });
236
- await shutdown(server);
237
- } finally {
238
- if (oldEnv === undefined) delete process.env.KODELYTH_DASHBOARD_ALLOW_REMOTE;
239
- else process.env.KODELYTH_DASHBOARD_ALLOW_REMOTE = oldEnv;
240
- }
241
- });
242
-
243
- test('resolveStatic rejects path traversal', () => {
244
- assert.equal(_internals.resolveStatic('/../etc/passwd'), null);
245
- assert.equal(_internals.resolveStatic('/..%2F..%2Fpackage.json'), null);
246
- });
247
-
248
- test('resolveStatic resolves root to index.html', () => {
249
- const p = _internals.resolveStatic('/');
250
- assert.match(p || '', /index\.html$/);
251
- });
252
-
253
- test('chooseOpenCommand returns an OS-appropriate string', () => {
254
- const cmd = _internals.chooseOpenCommand();
255
- assert.ok(typeof cmd === 'string');
256
- assert.ok(cmd.length > 0);
257
- });