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.
- package/CLAUDE.md +1 -1
- package/VERSION +1 -1
- package/bin/kodelyth-ecc.js +54 -5
- package/install.ps1 +30 -13
- package/install.sh +11 -4
- package/package.json +1 -1
- package/rules/common/agent-intent-routing.md +96 -195
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -47
- package/.github/ISSUE_TEMPLATE/config.yml +0 -8
- package/.github/ISSUE_TEMPLATE/feature_request.md +0 -41
- package/.github/ISSUE_TEMPLATE/good_first_issue.md +0 -36
- package/.github/ISSUE_TEMPLATE/new_agent.md +0 -52
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -53
- package/.github/workflows/ci.yml +0 -45
- package/.github/workflows/publish.yml +0 -60
- package/brand/concepts.svg +0 -109
- package/brand/convert.js +0 -161
- package/brand/favicon.svg +0 -7
- package/brand/fb-cover.svg +0 -34
- package/brand/fb-profile.svg +0 -21
- package/brand/kodelyth-dark.svg +0 -24
- package/brand/kodelyth-light.svg +0 -24
- package/brand/kodelyth-mark.svg +0 -20
- package/brand/package.json +0 -12
- package/docs/dashboard.md +0 -211
- package/docs/evolve.md +0 -303
- package/docs/mcp-clients.md +0 -167
- package/docs/mcp.md +0 -178
- package/docs/replay.md +0 -244
- package/docs/supply-chain.md +0 -207
- package/docs/swarm.md +0 -243
- package/skills/skill-comply/tests/test_grader.py +0 -137
- package/skills/skill-comply/tests/test_parser.py +0 -90
- package/social/card-agents.svg +0 -104
- package/social/card-install.svg +0 -58
- package/social/card-main.svg +0 -54
- package/social/facebook-v150.svg +0 -110
- package/social/fb-ad-main.svg +0 -128
- package/social/fb-post-features.svg +0 -118
- package/social/fb-post-launch.svg +0 -144
- package/social/fb-post-platforms.svg +0 -135
- package/social/github-social-preview.svg +0 -151
- package/social/hype-compound-learning.svg +0 -129
- package/social/hype-devil-mode.svg +0 -240
- package/social/hype-mcp-server.svg +0 -148
- package/social/hype-parallel-agents.svg +0 -162
- package/social/hype-stats-hero.svg +0 -138
- package/social/og-image.svg +0 -151
- package/social/readme-agents.svg +0 -138
- package/social/readme-hero.svg +0 -102
- package/social/section-agents.svg +0 -69
- package/social/section-author.svg +0 -66
- package/social/section-dashboard.svg +0 -71
- package/social/section-devil.svg +0 -66
- package/social/section-hooks.svg +0 -63
- package/social/section-install.svg +0 -54
- package/social/section-learning.svg +0 -64
- package/social/section-mcp.svg +0 -58
- package/social/section-memory.svg +0 -69
- package/social/section-parallel.svg +0 -84
- package/social/section-routing.svg +0 -63
- package/social/twitter-threads.md +0 -621
- package/social/x-card-agents-grid.svg +0 -120
- package/social/x-card-free.svg +0 -71
- package/social/x-card-hook.svg +0 -94
- package/tests/dashboard/data.test.js +0 -391
- package/tests/dashboard/server.test.js +0 -257
- package/tests/evolve/analyze.test.js +0 -169
- package/tests/evolve/proposals.test.js +0 -173
- package/tests/evolve/stats.test.js +0 -167
- package/tests/hooks/branch-name-check.test.js +0 -184
- package/tests/hooks/smart-suggest.test.js +0 -149
- package/tests/hooks/test-reminder.test.js +0 -181
- package/tests/mcp/catalog.test.js +0 -98
- package/tests/mcp/client.test.js +0 -113
- package/tests/mcp/resources-prompts.test.js +0 -70
- package/tests/mcp/tools.test.js +0 -159
- package/tests/memory/auto-recall.test.js +0 -132
- package/tests/memory/instincts.test.js +0 -258
- package/tests/memory/store.test.js +0 -203
- package/tests/replay/bundle.test.js +0 -181
- package/tests/replay/replay.test.js +0 -131
- package/tests/router/classify.test.js +0 -164
- package/tests/run-all.js +0 -55
- package/tests/safety/patterns.test.js +0 -94
- package/tests/safety/prompt-injection-guard.test.js +0 -111
- package/tests/safety/token-budget.test.js +0 -119
- package/tests/supply-chain/manifest.test.js +0 -147
- package/tests/supply-chain/sbom.test.js +0 -170
- package/tests/supply-chain/verify.test.js +0 -146
- 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
|
-
});
|