kodelyth-ecc 1.5.10 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/publish.yml +19 -0
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +447 -0
- package/CLAUDE.md +48 -16
- package/README.md +265 -81
- package/VERSION +1 -1
- package/actions/ecc-review/README.md +243 -0
- package/actions/ecc-review/action.yml +158 -0
- package/actions/ecc-review/post-comment.js +171 -0
- package/actions/ecc-review/run-review.js +285 -0
- package/agents/backdoor-hunter.md +260 -0
- package/agents/chaos-engineer.md +251 -0
- package/agents/code-stealer-detector.md +228 -0
- package/agents/jailbreak-tester.md +222 -0
- package/agents/license-violation-finder.md +212 -0
- package/agents/prompt-injection-hunter.md +126 -0
- package/agents/secret-hunter.md +209 -0
- package/agents/supply-chain-auditor.md +195 -0
- package/bin/kodelyth-ecc.js +875 -1
- package/bundles/enterprise.md +172 -0
- package/bundles/indie-hacker.md +106 -0
- package/bundles/red-team.md +138 -0
- package/cat +0 -0
- package/commands/dashboard.md +67 -0
- package/commands/devil-mode.md +121 -0
- package/commands/memory-evolve.md +71 -0
- package/commands/replay.md +61 -0
- package/commands/route-model.md +48 -0
- package/commands/swarm.md +68 -0
- package/commands/verify-supply-chain.md +59 -0
- package/docs/dashboard.md +211 -0
- package/docs/evolve.md +303 -0
- package/docs/mcp-clients.md +167 -0
- package/docs/mcp.md +178 -0
- package/docs/replay.md +244 -0
- package/docs/supply-chain.md +207 -0
- package/docs/swarm.md +243 -0
- package/hooks/hooks.json +52 -0
- package/hooks/memory/auto-recall.js +29 -1
- package/hooks/safety/README.md +124 -0
- package/hooks/safety/lib/patterns.js +179 -0
- package/hooks/safety/prompt-injection-guard.js +179 -0
- package/hooks/safety/token-budget.js +229 -0
- package/install.ps1 +199 -2
- package/install.sh +285 -3
- package/package.json +30 -4
- package/rules/common/agent-intent-routing.md +182 -0
- package/rules/common/cost-aware-model-routing.md +152 -0
- package/scripts/dashboard/data.js +353 -0
- package/scripts/dashboard/server.js +324 -0
- package/scripts/dashboard/static/index.html +645 -0
- package/scripts/evolve/analyze.js +303 -0
- package/scripts/evolve/proposals.js +162 -0
- package/scripts/evolve/stats.js +219 -0
- package/scripts/mcp/catalog.js +244 -0
- package/scripts/mcp/client.js +181 -0
- package/scripts/mcp/prompts.js +133 -0
- package/scripts/mcp/resources.js +94 -0
- package/scripts/mcp/server.js +153 -0
- package/scripts/mcp/tools.js +465 -0
- package/scripts/replay/bundle.js +191 -0
- package/scripts/replay/replay.js +107 -0
- package/scripts/router/classify.js +232 -0
- package/scripts/supply-chain/manifest.js +154 -0
- package/scripts/supply-chain/sbom.js +202 -0
- package/scripts/supply-chain/verify.js +102 -0
- package/scripts/swarm/build-plan.js +193 -0
- package/skills/cost-aware-model-routing/SKILL.md +153 -0
- package/skills/kodelyth-quickstart/SKILL.md +7 -0
- package/skills/observability-dashboard/SKILL.md +119 -0
- package/skills/self-evolving-memory/SKILL.md +175 -0
- package/skills/session-replay/SKILL.md +199 -0
- package/skills/supply-chain-verification/SKILL.md +201 -0
- package/skills/swarm-orchestrator/SKILL.md +177 -0
- package/social/card-install.svg +1 -1
- package/social/facebook-group/POST.md +121 -0
- package/social/facebook-group/fb-1-3am-debug.png +0 -0
- package/social/facebook-group/fb-1-3am-debug.svg +97 -0
- package/social/facebook-group/fb-2-cpu-upgrade.png +0 -0
- package/social/facebook-group/fb-2-cpu-upgrade.svg +132 -0
- package/social/facebook-group/fb-3-before-after.png +0 -0
- package/social/facebook-group/fb-3-before-after.svg +94 -0
- package/social/facebook-v150.svg +5 -5
- package/social/github-social-preview.svg +119 -100
- package/social/readme-hero.svg +11 -11
- package/social/section-agents.svg +57 -0
- package/social/section-author.svg +54 -0
- package/social/section-dashboard.svg +59 -0
- package/social/section-devil.svg +54 -0
- package/social/section-hooks.svg +51 -0
- package/social/section-install.svg +42 -0
- package/social/section-learning.svg +52 -0
- package/social/section-mcp.svg +46 -0
- package/social/section-memory.svg +57 -0
- package/social/section-parallel.svg +72 -0
- package/social/section-routing.svg +51 -0
- package/social/x-card-agents-grid.svg +6 -6
- package/tests/dashboard/data.test.js +235 -0
- package/tests/dashboard/server.test.js +240 -0
- package/tests/evolve/analyze.test.js +169 -0
- package/tests/evolve/proposals.test.js +173 -0
- package/tests/evolve/stats.test.js +159 -0
- package/tests/mcp/catalog.test.js +98 -0
- package/tests/mcp/client.test.js +109 -0
- package/tests/mcp/resources-prompts.test.js +70 -0
- package/tests/mcp/tools.test.js +159 -0
- package/tests/replay/bundle.test.js +181 -0
- package/tests/replay/replay.test.js +131 -0
- package/tests/router/classify.test.js +164 -0
- package/tests/safety/patterns.test.js +94 -0
- package/tests/safety/prompt-injection-guard.test.js +111 -0
- package/tests/safety/token-budget.test.js +119 -0
- package/tests/supply-chain/manifest.test.js +147 -0
- package/tests/supply-chain/sbom.test.js +170 -0
- package/tests/supply-chain/verify.test.js +146 -0
- package/tests/swarm/build-plan.test.js +188 -0
- package/wiki/Agent-Reference.md +58 -7
- package/wiki/FAQ.md +204 -7
- package/wiki/Home.md +104 -28
- package/wiki/Hook-Reference.md +1 -1
- package/wiki/Installation-Guide.md +109 -6
- package/wiki/Platform-Support.md +239 -25
- package/wiki/Skill-Reference.md +101 -6
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<svg viewBox="0 0 900 185" xmlns="http://www.w3.org/2000/svg" font-family="'Segoe UI', system-ui, -apple-system, sans-serif">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#0d1117"/>
|
|
5
|
+
<stop offset="100%" style="stop-color:#161b22"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<marker id="arr" markerWidth="7" markerHeight="7" refX="5" refY="3" orient="auto">
|
|
8
|
+
<path d="M0,0 L5,3 L0,6 Z" fill="#a78bfa" opacity="0.8"/>
|
|
9
|
+
</marker>
|
|
10
|
+
<marker id="arrg" markerWidth="7" markerHeight="7" refX="5" refY="3" orient="auto">
|
|
11
|
+
<path d="M0,0 L5,3 L0,6 Z" fill="#34d399" opacity="0.8"/>
|
|
12
|
+
</marker>
|
|
13
|
+
</defs>
|
|
14
|
+
<rect width="900" height="185" fill="url(#bg)" rx="12"/>
|
|
15
|
+
<rect width="900" height="185" fill="none" stroke="#21262d" stroke-width="1" rx="12"/>
|
|
16
|
+
|
|
17
|
+
<!-- Label — widened to fit text -->
|
|
18
|
+
<rect x="32" y="20" width="248" height="22" rx="11" fill="#f59e0b" fill-opacity="0.12" stroke="#f59e0b" stroke-opacity="0.4" stroke-width="1"/>
|
|
19
|
+
<text x="156" y="35" font-size="10" fill="#fbbf24" text-anchor="middle" font-weight="700" letter-spacing="1">PARALLEL MULTI-AGENT COMMANDS</text>
|
|
20
|
+
|
|
21
|
+
<text x="32" y="72" font-size="21" font-weight="800" fill="#ffffff">8 specialists fire simultaneously — not one by one</text>
|
|
22
|
+
<text x="32" y="94" font-size="12" fill="#6e7681">60 min sequential review → 15 min parallel · all results aggregated into one structured report</text>
|
|
23
|
+
|
|
24
|
+
<!-- Command trigger box -->
|
|
25
|
+
<rect x="32" y="112" width="120" height="56" rx="7" fill="#161b22" stroke="#a78bfa" stroke-opacity="0.5" stroke-width="1"/>
|
|
26
|
+
<text x="92" y="132" font-size="10" fill="#a78bfa" text-anchor="middle" font-weight="700">/team-review</text>
|
|
27
|
+
<text x="92" y="148" font-size="8" fill="#6e7681" text-anchor="middle">one command</text>
|
|
28
|
+
<text x="92" y="160" font-size="8" fill="#6e7681" text-anchor="middle">fires 4 agents</text>
|
|
29
|
+
|
|
30
|
+
<!-- Fan-out lines to 4 agents -->
|
|
31
|
+
<line x1="155" y1="122" x2="196" y2="115" stroke="#a78bfa" stroke-width="1" marker-end="url(#arr)" opacity="0.6"/>
|
|
32
|
+
<line x1="155" y1="131" x2="196" y2="131" stroke="#a78bfa" stroke-width="1" marker-end="url(#arr)" opacity="0.6"/>
|
|
33
|
+
<line x1="155" y1="140" x2="196" y2="147" stroke="#a78bfa" stroke-width="1" marker-end="url(#arr)" opacity="0.6"/>
|
|
34
|
+
<line x1="155" y1="149" x2="196" y2="163" stroke="#a78bfa" stroke-width="1" marker-end="url(#arr)" opacity="0.6"/>
|
|
35
|
+
|
|
36
|
+
<!-- 4 agent pills — 130px wide each, text fits at font-size 9 -->
|
|
37
|
+
<rect x="199" y="106" width="130" height="20" rx="5" fill="#a78bfa" fill-opacity="0.12" stroke="#a78bfa" stroke-opacity="0.4" stroke-width="1"/>
|
|
38
|
+
<text x="264" y="119" font-size="9" fill="#a78bfa" text-anchor="middle" font-weight="600">code-reviewer</text>
|
|
39
|
+
|
|
40
|
+
<rect x="199" y="122" width="130" height="20" rx="5" fill="#ef4444" fill-opacity="0.1" stroke="#ef4444" stroke-opacity="0.35" stroke-width="1"/>
|
|
41
|
+
<text x="264" y="135" font-size="9" fill="#f87171" text-anchor="middle" font-weight="600">security-reviewer</text>
|
|
42
|
+
|
|
43
|
+
<rect x="199" y="138" width="130" height="20" rx="5" fill="#f59e0b" fill-opacity="0.1" stroke="#f59e0b" stroke-opacity="0.35" stroke-width="1"/>
|
|
44
|
+
<text x="264" y="151" font-size="9" fill="#fbbf24" text-anchor="middle" font-weight="600">performance-optimizer</text>
|
|
45
|
+
|
|
46
|
+
<rect x="199" y="154" width="130" height="20" rx="5" fill="#2563eb" fill-opacity="0.1" stroke="#2563eb" stroke-opacity="0.35" stroke-width="1"/>
|
|
47
|
+
<text x="264" y="167" font-size="9" fill="#60a5fa" text-anchor="middle" font-weight="600">api-guardian</text>
|
|
48
|
+
|
|
49
|
+
<!-- Fan-in lines to result -->
|
|
50
|
+
<line x1="332" y1="115" x2="370" y2="132" stroke="#34d399" stroke-width="1" marker-end="url(#arrg)" opacity="0.6"/>
|
|
51
|
+
<line x1="332" y1="131" x2="370" y2="136" stroke="#34d399" stroke-width="1" marker-end="url(#arrg)" opacity="0.6"/>
|
|
52
|
+
<line x1="332" y1="147" x2="370" y2="140" stroke="#34d399" stroke-width="1" marker-end="url(#arrg)" opacity="0.6"/>
|
|
53
|
+
<line x1="332" y1="163" x2="370" y2="144" stroke="#34d399" stroke-width="1" marker-end="url(#arrg)" opacity="0.6"/>
|
|
54
|
+
|
|
55
|
+
<!-- Result box -->
|
|
56
|
+
<rect x="373" y="112" width="148" height="56" rx="7" fill="#059669" fill-opacity="0.1" stroke="#059669" stroke-opacity="0.4" stroke-width="1"/>
|
|
57
|
+
<text x="447" y="131" font-size="10" fill="#34d399" text-anchor="middle" font-weight="700">Team Review Report</text>
|
|
58
|
+
<text x="447" y="147" font-size="8" fill="#34d399" text-anchor="middle" opacity="0.8">CRITICAL · HIGH · MEDIUM</text>
|
|
59
|
+
<text x="447" y="160" font-size="8" fill="#34d399" text-anchor="middle" opacity="0.75">1 critical found → fix first</text>
|
|
60
|
+
|
|
61
|
+
<!-- All 8 parallel commands — two columns -->
|
|
62
|
+
<text x="546" y="112" font-size="9" fill="#6e7681" font-weight="600">All 8 parallel commands:</text>
|
|
63
|
+
<text x="546" y="127" font-size="9" fill="#a78bfa">/team-review</text>
|
|
64
|
+
<text x="546" y="141" font-size="9" fill="#a78bfa">/project-launch</text>
|
|
65
|
+
<text x="546" y="155" font-size="9" fill="#ef4444">/devil-mode</text>
|
|
66
|
+
<text x="546" y="169" font-size="9" fill="#f59e0b">/security-audit</text>
|
|
67
|
+
|
|
68
|
+
<text x="700" y="127" font-size="9" fill="#38bdf8">/debug-blitz</text>
|
|
69
|
+
<text x="700" y="141" font-size="9" fill="#38bdf8">/refactor-sprint</text>
|
|
70
|
+
<text x="700" y="155" font-size="9" fill="#34d399">/pre-release</text>
|
|
71
|
+
<text x="700" y="169" font-size="9" fill="#34d399">/onboard</text>
|
|
72
|
+
</svg>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<svg viewBox="0 0 900 170" xmlns="http://www.w3.org/2000/svg" font-family="'Segoe UI', system-ui, -apple-system, sans-serif">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" style="stop-color:#0d1117"/>
|
|
5
|
+
<stop offset="100%" style="stop-color:#161b22"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="acc" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
8
|
+
<stop offset="0%" style="stop-color:#a78bfa"/>
|
|
9
|
+
<stop offset="100%" style="stop-color:#38bdf8"/>
|
|
10
|
+
</linearGradient>
|
|
11
|
+
<marker id="arr" markerWidth="8" markerHeight="8" refX="6" refY="3" orient="auto">
|
|
12
|
+
<path d="M0,0 L6,3 L0,6 Z" fill="#a78bfa" opacity="0.7"/>
|
|
13
|
+
</marker>
|
|
14
|
+
</defs>
|
|
15
|
+
<rect width="900" height="170" fill="url(#bg)" rx="12"/>
|
|
16
|
+
<rect width="900" height="170" fill="none" stroke="#21262d" stroke-width="1" rx="12"/>
|
|
17
|
+
|
|
18
|
+
<!-- Section label -->
|
|
19
|
+
<rect x="32" y="20" width="180" height="22" rx="11" fill="#a78bfa" fill-opacity="0.12" stroke="#a78bfa" stroke-opacity="0.35" stroke-width="1"/>
|
|
20
|
+
<text x="122" y="35" font-size="10" fill="#a78bfa" text-anchor="middle" font-weight="700" letter-spacing="1">INTENT ROUTING SYSTEM</text>
|
|
21
|
+
|
|
22
|
+
<!-- Title -->
|
|
23
|
+
<text x="32" y="72" font-size="22" font-weight="800" fill="#ffffff">Plain language → right specialist — automatically</text>
|
|
24
|
+
<text x="32" y="96" font-size="12" fill="#6e7681">The 10-tier routing rule fires on every session. No agent names needed. The AI announces who's taking over.</text>
|
|
25
|
+
|
|
26
|
+
<!-- Flow diagram -->
|
|
27
|
+
<rect x="32" y="118" width="120" height="40" rx="6" fill="#21262d" stroke="#30363d" stroke-width="1"/>
|
|
28
|
+
<text x="92" y="138" font-size="10" fill="#c9d1d9" text-anchor="middle">"I've been stuck for</text>
|
|
29
|
+
<text x="92" y="152" font-size="10" fill="#c9d1d9" text-anchor="middle">hours on this bug"</text>
|
|
30
|
+
|
|
31
|
+
<line x1="155" y1="138" x2="195" y2="138" stroke="#a78bfa" stroke-width="1.5" marker-end="url(#arr)" opacity="0.7"/>
|
|
32
|
+
|
|
33
|
+
<rect x="198" y="118" width="108" height="40" rx="6" fill="#a78bfa" fill-opacity="0.15" stroke="#a78bfa" stroke-opacity="0.5" stroke-width="1"/>
|
|
34
|
+
<text x="252" y="136" font-size="10" fill="#a78bfa" text-anchor="middle" font-weight="700">10-tier router</text>
|
|
35
|
+
<text x="252" y="150" font-size="9" fill="#a78bfa" text-anchor="middle" opacity="0.7">frustration + error signal</text>
|
|
36
|
+
|
|
37
|
+
<line x1="309" y1="138" x2="349" y2="138" stroke="#a78bfa" stroke-width="1.5" marker-end="url(#arr)" opacity="0.7"/>
|
|
38
|
+
|
|
39
|
+
<rect x="352" y="118" width="130" height="40" rx="6" fill="#38bdf8" fill-opacity="0.12" stroke="#38bdf8" stroke-opacity="0.4" stroke-width="1"/>
|
|
40
|
+
<text x="417" y="136" font-size="10" fill="#38bdf8" text-anchor="middle" font-weight="700">→ debug-detective</text>
|
|
41
|
+
<text x="417" y="150" font-size="9" fill="#38bdf8" text-anchor="middle" opacity="0.7">root-cause analyst</text>
|
|
42
|
+
|
|
43
|
+
<line x1="485" y1="138" x2="525" y2="138" stroke="#6e7681" stroke-width="1" stroke-dasharray="4,3" opacity="0.5"/>
|
|
44
|
+
|
|
45
|
+
<rect x="528" y="118" width="155" height="40" rx="6" fill="#059669" fill-opacity="0.1" stroke="#059669" stroke-opacity="0.4" stroke-width="1"/>
|
|
46
|
+
<text x="605" y="135" font-size="9.5" fill="#34d399" text-anchor="middle">"Tip: next time type</text>
|
|
47
|
+
<text x="605" y="149" font-size="9.5" fill="#34d399" text-anchor="middle">'use debug-detective'"</text>
|
|
48
|
+
|
|
49
|
+
<!-- Right label -->
|
|
50
|
+
<text x="868" y="96" font-size="10" fill="#6e7681" text-anchor="end">70 agents · 10 tiers · 50+ patterns</text>
|
|
51
|
+
</svg>
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
<circle cx="100" cy="100" r="200" fill="#a78bfa" opacity="0.03"/>
|
|
20
20
|
|
|
21
21
|
<!-- Header -->
|
|
22
|
-
<text x="80" y="72" font-size="48" font-weight="900" letter-spacing="-2" fill="#ffffff">
|
|
22
|
+
<text x="80" y="72" font-size="48" font-weight="900" letter-spacing="-2" fill="#ffffff">70 agents. One install.</text>
|
|
23
23
|
<text x="80" y="110" font-size="18" fill="#6e7681">Semantic routing picks the right one — you never need to know their names.</text>
|
|
24
24
|
|
|
25
25
|
<!-- Left column — agent cards -->
|
|
@@ -91,18 +91,18 @@
|
|
|
91
91
|
<text x="460" y="608" font-size="12" fill="#484f58" text-anchor="middle">Works with Claude Code · Windsurf · Cursor · Codex · Antigravity · OpenCode</text>
|
|
92
92
|
|
|
93
93
|
<!-- Stats -->
|
|
94
|
-
<text x="920" y="200" font-size="72" font-weight="900" fill="#ffffff" text-anchor="middle">
|
|
94
|
+
<text x="920" y="200" font-size="72" font-weight="900" fill="#ffffff" text-anchor="middle">70</text>
|
|
95
95
|
<text x="920" y="228" font-size="10" fill="#484f58" text-anchor="middle" letter-spacing="3">AGENTS</text>
|
|
96
96
|
<line x1="860" y1="248" x2="980" y2="248" stroke="#21262d" stroke-width="1"/>
|
|
97
|
-
<text x="920" y="280" font-size="32" font-weight="800" fill="#ffffff" text-anchor="middle">
|
|
97
|
+
<text x="920" y="280" font-size="32" font-weight="800" fill="#ffffff" text-anchor="middle">194</text>
|
|
98
98
|
<text x="920" y="302" font-size="10" fill="#484f58" text-anchor="middle" letter-spacing="2">SKILLS</text>
|
|
99
99
|
<line x1="860" y1="320" x2="980" y2="320" stroke="#21262d" stroke-width="1"/>
|
|
100
|
-
<text x="920" y="352" font-size="32" font-weight="800" fill="#ffffff" text-anchor="middle">
|
|
100
|
+
<text x="920" y="352" font-size="32" font-weight="800" fill="#ffffff" text-anchor="middle">22+</text>
|
|
101
101
|
<text x="920" y="374" font-size="10" fill="#484f58" text-anchor="middle" letter-spacing="2">HOOKS</text>
|
|
102
102
|
<line x1="860" y1="392" x2="980" y2="392" stroke="#21262d" stroke-width="1"/>
|
|
103
|
-
<text x="920" y="424" font-size="32" font-weight="800" fill="#ffffff" text-anchor="middle">
|
|
103
|
+
<text x="920" y="424" font-size="32" font-weight="800" fill="#ffffff" text-anchor="middle">12</text>
|
|
104
104
|
<text x="920" y="446" font-size="10" fill="#484f58" text-anchor="middle" letter-spacing="2">PLATFORMS</text>
|
|
105
105
|
<line x1="860" y1="464" x2="980" y2="464" stroke="#21262d" stroke-width="1"/>
|
|
106
|
-
<text x="920" y="496" font-size="20" font-weight="800" fill="#ffffff" text-anchor="middle">v1.
|
|
106
|
+
<text x="920" y="496" font-size="20" font-weight="800" fill="#ffffff" text-anchor="middle">v1.7.0</text>
|
|
107
107
|
<text x="920" y="516" font-size="10" fill="#484f58" text-anchor="middle" letter-spacing="2">LATEST</text>
|
|
108
108
|
</svg>
|
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
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/token-budget returns aggregate shape', async () => {
|
|
154
|
+
const server = createServer();
|
|
155
|
+
const port = await listenEphemeral(server);
|
|
156
|
+
try {
|
|
157
|
+
const r = await get(port, '/api/token-budget');
|
|
158
|
+
assert.equal(r.status, 200);
|
|
159
|
+
const body = JSON.parse(r.body);
|
|
160
|
+
assert.ok(Array.isArray(body.sessions));
|
|
161
|
+
assert.ok(typeof body.total_tokens === 'number');
|
|
162
|
+
} finally { await shutdown(server); }
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test('GET /api/missing-route returns 404', async () => {
|
|
166
|
+
const server = createServer();
|
|
167
|
+
const port = await listenEphemeral(server);
|
|
168
|
+
try {
|
|
169
|
+
const r = await get(port, '/api/totally-made-up');
|
|
170
|
+
assert.equal(r.status, 404);
|
|
171
|
+
} finally { await shutdown(server); }
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
test('POST is rejected with 405', async () => {
|
|
175
|
+
const server = createServer();
|
|
176
|
+
const port = await listenEphemeral(server);
|
|
177
|
+
try {
|
|
178
|
+
const r = await get(port, '/api/health', { method: 'POST' });
|
|
179
|
+
assert.equal(r.status, 405);
|
|
180
|
+
assert.equal(r.headers.allow, 'GET');
|
|
181
|
+
} finally { await shutdown(server); }
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test('static root serves index.html', async () => {
|
|
185
|
+
const server = createServer();
|
|
186
|
+
const port = await listenEphemeral(server);
|
|
187
|
+
try {
|
|
188
|
+
const r = await get(port, '/');
|
|
189
|
+
assert.equal(r.status, 200);
|
|
190
|
+
assert.match(r.headers['content-type'], /^text\/html/);
|
|
191
|
+
assert.match(r.body, /Kodelyth ECC/);
|
|
192
|
+
} finally { await shutdown(server); }
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
test('hardened headers on every response', async () => {
|
|
196
|
+
const server = createServer();
|
|
197
|
+
const port = await listenEphemeral(server);
|
|
198
|
+
try {
|
|
199
|
+
const r = await get(port, '/api/health');
|
|
200
|
+
assert.equal(r.headers['x-frame-options'], 'DENY');
|
|
201
|
+
assert.equal(r.headers['x-content-type-options'], 'nosniff');
|
|
202
|
+
assert.equal(r.headers['referrer-policy'], 'no-referrer');
|
|
203
|
+
assert.equal(r.headers['cache-control'], 'no-store');
|
|
204
|
+
} finally { await shutdown(server); }
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('start() refuses non-localhost without override', async () => {
|
|
208
|
+
await assert.rejects(
|
|
209
|
+
() => dashboard.start({ port: 0, host: '0.0.0.0', openBrowser: false, log: () => {} }),
|
|
210
|
+
/refusing to bind/,
|
|
211
|
+
);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('start() allows non-localhost when override env var is set', async () => {
|
|
215
|
+
const oldEnv = process.env.KODELYTH_DASHBOARD_ALLOW_REMOTE;
|
|
216
|
+
process.env.KODELYTH_DASHBOARD_ALLOW_REMOTE = '1';
|
|
217
|
+
try {
|
|
218
|
+
const { server } = await dashboard.start({ port: 0, host: '127.0.0.1', openBrowser: false, log: () => {} });
|
|
219
|
+
await shutdown(server);
|
|
220
|
+
} finally {
|
|
221
|
+
if (oldEnv === undefined) delete process.env.KODELYTH_DASHBOARD_ALLOW_REMOTE;
|
|
222
|
+
else process.env.KODELYTH_DASHBOARD_ALLOW_REMOTE = oldEnv;
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test('resolveStatic rejects path traversal', () => {
|
|
227
|
+
assert.equal(_internals.resolveStatic('/../etc/passwd'), null);
|
|
228
|
+
assert.equal(_internals.resolveStatic('/..%2F..%2Fpackage.json'), null);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test('resolveStatic resolves root to index.html', () => {
|
|
232
|
+
const p = _internals.resolveStatic('/');
|
|
233
|
+
assert.match(p || '', /index\.html$/);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
test('chooseOpenCommand returns an OS-appropriate string', () => {
|
|
237
|
+
const cmd = _internals.chooseOpenCommand();
|
|
238
|
+
assert.ok(typeof cmd === 'string');
|
|
239
|
+
assert.ok(cmd.length > 0);
|
|
240
|
+
});
|