@eltonssouza/development-utility-kit 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/.claude/agents/analyst.md +198 -0
  2. package/.claude/agents/backend-developer.md +126 -0
  3. package/.claude/agents/brain-keeper.md +229 -0
  4. package/.claude/agents/code-reviewer.md +181 -0
  5. package/.claude/agents/database-engineer.md +94 -0
  6. package/.claude/agents/devops-engineer.md +141 -0
  7. package/.claude/agents/frontend-developer.md +97 -0
  8. package/.claude/agents/gate-keeper.md +118 -0
  9. package/.claude/agents/migrator.md +291 -0
  10. package/.claude/agents/mobile-developer.md +80 -0
  11. package/.claude/agents/n8n-specialist.md +94 -0
  12. package/.claude/agents/product-owner.md +115 -0
  13. package/.claude/agents/qa-engineer.md +232 -0
  14. package/.claude/agents/release-engineer.md +204 -0
  15. package/.claude/agents/scaffold.md +87 -0
  16. package/.claude/agents/security-engineer.md +199 -0
  17. package/.claude/agents/sprint-runner.md +44 -0
  18. package/.claude/agents/stack-resolver.md +84 -0
  19. package/.claude/agents/tech-lead.md +182 -0
  20. package/.claude/agents/update-template.md +54 -0
  21. package/.claude/agents/ux-designer.md +118 -0
  22. package/.claude/settings.json +44 -0
  23. package/.claude/skills/README.md +332 -0
  24. package/.claude/skills/active-project/SKILL.md +129 -0
  25. package/.claude/skills/api-integration-test/SKILL.md +64 -0
  26. package/.claude/skills/auto-test-guard/SKILL.md +237 -0
  27. package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
  28. package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
  29. package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
  30. package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
  31. package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
  32. package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
  33. package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
  34. package/.claude/skills/brain-keeper/SKILL.md +60 -0
  35. package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
  36. package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
  37. package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
  38. package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
  39. package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
  40. package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
  41. package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
  42. package/.claude/skills/brain-keeper/templates/README.md +51 -0
  43. package/.claude/skills/brain-keeper/templates/adr.md +40 -0
  44. package/.claude/skills/brain-keeper/templates/bug.md +35 -0
  45. package/.claude/skills/brain-keeper/templates/daily.md +38 -0
  46. package/.claude/skills/brain-keeper/templates/feature.md +62 -0
  47. package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
  48. package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
  49. package/.claude/skills/caveman/SKILL.md +187 -0
  50. package/.claude/skills/create-stack-pack/SKILL.md +281 -0
  51. package/.claude/skills/grill-me/SKILL.md +79 -0
  52. package/.claude/skills/honcho-memory/SKILL.md +207 -0
  53. package/.claude/skills/honcho-memory/docs/api-endpoints-verified.md +75 -0
  54. package/.claude/skills/honcho-memory/hooks/on-prompt-submit.js +221 -0
  55. package/.claude/skills/honcho-memory/hooks/on-stop.js +193 -0
  56. package/.claude/skills/honcho-memory/lib/honcho-client.js +363 -0
  57. package/.claude/skills/honcho-memory/lib/memory-injector.js +93 -0
  58. package/.claude/skills/honcho-memory/package.json +32 -0
  59. package/.claude/skills/honcho-memory/scripts/cli.js +370 -0
  60. package/.claude/skills/honcho-memory/scripts/setup.js +109 -0
  61. package/.claude/skills/honcho-memory/tests/t001-api-endpoints-verified.test.js +89 -0
  62. package/.claude/skills/honcho-memory/tests/t002-structure.test.js +97 -0
  63. package/.claude/skills/honcho-memory/tests/t003-honcho-client.test.js +162 -0
  64. package/.claude/skills/honcho-memory/tests/t004-soft-delete.test.js +259 -0
  65. package/.claude/skills/honcho-memory/tests/t005-memory-injector.test.js +175 -0
  66. package/.claude/skills/honcho-memory/tests/t006-on-prompt-submit.test.js +215 -0
  67. package/.claude/skills/honcho-memory/tests/t007-on-stop.test.js +165 -0
  68. package/.claude/skills/honcho-memory/tests/t008-cli.test.js +214 -0
  69. package/.claude/skills/honcho-memory/tests/t009-setup.test.js +232 -0
  70. package/.claude/skills/honcho-memory/tests/t010-skill-md.test.js +114 -0
  71. package/.claude/skills/honcho-memory/tests/t011-settings-hooks.test.js +105 -0
  72. package/.claude/skills/honcho-memory/tests/t012-docs-update.test.js +106 -0
  73. package/.claude/skills/honcho-memory/tests/t013-smoke-e2e.test.js +90 -0
  74. package/.claude/skills/pair-debug/SKILL.md +288 -0
  75. package/.claude/skills/prd-ready-check/SKILL.md +58 -0
  76. package/.claude/skills/project-manager/SKILL.md +167 -0
  77. package/.claude/skills/quality-standards/SKILL.md +201 -0
  78. package/.claude/skills/quick-feature/SKILL.md +264 -0
  79. package/.claude/skills/run-sprint/SKILL.md +342 -0
  80. package/.claude/skills/scaffold/SKILL.md +58 -0
  81. package/.claude/skills/stack-discovery/SKILL.md +159 -0
  82. package/.claude/skills/test-coverage-auditor/SKILL.md +59 -0
  83. package/.claude/skills/to-issues/SKILL.md +163 -0
  84. package/.claude/skills/to-prd/SKILL.md +130 -0
  85. package/.claude/skills/update-template/SKILL.md +254 -0
  86. package/.claude/stacks/CODEOWNERS +30 -0
  87. package/.claude/stacks/README.md +88 -0
  88. package/.claude/stacks/_template.md +116 -0
  89. package/.claude/stacks/java/spring-boot-3.md +376 -0
  90. package/.claude/stacks/java/spring-boot-4.md +438 -0
  91. package/.claude/stacks/typescript/angular-18.md +420 -0
  92. package/.claude/stacks/typescript/angular-19.md +397 -0
  93. package/.claude/stacks/typescript/angular-21.md +494 -0
  94. package/CLAUDE.md +453 -0
  95. package/README.md +391 -0
  96. package/bin/cli.js +773 -0
  97. package/bin/lib/backup.js +62 -0
  98. package/bin/lib/detect-stack.js +476 -0
  99. package/bin/lib/help.js +233 -0
  100. package/bin/lib/identity.js +108 -0
  101. package/bin/lib/local-dir.js +69 -0
  102. package/bin/lib/manifest.js +236 -0
  103. package/bin/lib/sync-all.js +394 -0
  104. package/bin/lib/version-check.js +398 -0
  105. package/dashboard/db.js +199 -0
  106. package/dashboard/package.json +22 -0
  107. package/dashboard/public/app.js +709 -0
  108. package/dashboard/public/content/docs/agents-reference.en.md +911 -0
  109. package/dashboard/public/content/docs/architecture-overview.en.md +260 -0
  110. package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
  111. package/dashboard/public/content/docs/git-flow.en.md +525 -0
  112. package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
  113. package/dashboard/public/content/docs/hooks-reference.en.md +420 -0
  114. package/dashboard/public/content/docs/pipeline.en.md +400 -0
  115. package/dashboard/public/content/docs/quality-gate.en.md +315 -0
  116. package/dashboard/public/content/docs/skills-reference.en.md +500 -0
  117. package/dashboard/public/content/docs/stack-rules.en.md +362 -0
  118. package/dashboard/public/content/docs/troubleshooting.en.md +637 -0
  119. package/dashboard/public/content/manifest.json +102 -0
  120. package/dashboard/public/content/manual/backend.en.md +1138 -0
  121. package/dashboard/public/content/manual/existing-project.en.md +831 -0
  122. package/dashboard/public/content/manual/frontend.en.md +1065 -0
  123. package/dashboard/public/content/manual/fullstack.en.md +1508 -0
  124. package/dashboard/public/content/manual/mobile.en.md +866 -0
  125. package/dashboard/public/index.html +108 -0
  126. package/dashboard/public/style.css +610 -0
  127. package/dashboard/public/vendor/marked.min.js +69 -0
  128. package/dashboard/rtk.js +143 -0
  129. package/dashboard/server-app.js +403 -0
  130. package/dashboard/server.js +104 -0
  131. package/dashboard/test/sprint1.test.js +406 -0
  132. package/dashboard/test/sprint2.test.js +571 -0
  133. package/dashboard/test/sprint3.test.js +560 -0
  134. package/package.json +33 -0
  135. package/scripts/hooks/subagent-telemetry.sh +14 -0
  136. package/scripts/hooks/telemetry-writer.js +250 -0
  137. package/scripts/latest-versions.json +56 -0
@@ -0,0 +1,406 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Sprint 1 failing tests — written before implementation (TDD RED phase).
5
+ * Uses Node.js built-in test runner (node:test) + supertest for HTTP assertions.
6
+ * Run: node --test dashboard/test/sprint1.test.js
7
+ */
8
+
9
+ const { test, describe, before, after } = require('node:test');
10
+ const assert = require('node:assert/strict');
11
+ const fs = require('node:fs');
12
+ const path = require('node:path');
13
+
14
+ const REPO_ROOT = path.resolve(__dirname, '..', '..');
15
+ const PUBLIC_DIR = path.join(REPO_ROOT, 'dashboard', 'public');
16
+
17
+ // ── T-001 / T-005: HTML structure + CSS classes ───────────────────────────────
18
+
19
+ describe('T-001: index.html tab structure', () => {
20
+ let html;
21
+ before(() => {
22
+ html = fs.readFileSync(path.join(PUBLIC_DIR, 'index.html'), 'utf8');
23
+ });
24
+
25
+ test('contains exactly 3 data-tab attributes', () => {
26
+ const matches = html.match(/data-tab=/g) || [];
27
+ assert.equal(matches.length, 3, 'Expected 3 data-tab attributes');
28
+ });
29
+
30
+ test('has data-tab="dashboard"', () => {
31
+ assert.ok(html.includes('data-tab="dashboard"'), 'Missing data-tab="dashboard"');
32
+ });
33
+
34
+ test('has data-tab="manual"', () => {
35
+ assert.ok(html.includes('data-tab="manual"'), 'Missing data-tab="manual"');
36
+ });
37
+
38
+ test('has data-tab="docs"', () => {
39
+ assert.ok(html.includes('data-tab="docs"'), 'Missing data-tab="docs"');
40
+ });
41
+
42
+ test('has .tab-nav element', () => {
43
+ assert.ok(html.includes('class="tab-nav"') || html.includes("class='tab-nav'"), 'Missing tab-nav class');
44
+ });
45
+
46
+ test('has lang selector with PT and EN options', () => {
47
+ assert.ok(html.includes('lang-selector') || html.includes('kud_lang'), 'Missing lang selector');
48
+ });
49
+
50
+ test('has marked CDN script tag', () => {
51
+ assert.ok(html.includes('cdn.jsdelivr.net/npm/marked'), 'Missing marked CDN script');
52
+ });
53
+
54
+ test('has onerror fallback for marked CDN', () => {
55
+ assert.ok(html.includes('onerror'), 'Missing onerror fallback on marked script tag');
56
+ });
57
+
58
+ test('marked CDN script references vendor/marked.min.js as fallback', () => {
59
+ assert.ok(html.includes('vendor/marked.min.js'), 'Missing vendor/marked.min.js reference');
60
+ });
61
+
62
+ test('has panel for #dashboard', () => {
63
+ assert.ok(
64
+ html.includes('id="panel-dashboard"') || html.includes('id="tab-dashboard"'),
65
+ 'Missing dashboard panel'
66
+ );
67
+ });
68
+
69
+ test('has panel for #manual', () => {
70
+ assert.ok(
71
+ html.includes('id="panel-manual"') || html.includes('id="tab-manual"'),
72
+ 'Missing manual panel'
73
+ );
74
+ });
75
+
76
+ test('has panel for #docs', () => {
77
+ assert.ok(
78
+ html.includes('id="panel-docs"') || html.includes('id="tab-docs"'),
79
+ 'Missing docs panel'
80
+ );
81
+ });
82
+
83
+ test('existing 3 cards preserved', () => {
84
+ assert.ok(html.includes('id="projects-card"'), 'Missing projects-card');
85
+ assert.ok(html.includes('id="models-card"'), 'Missing models-card');
86
+ assert.ok(html.includes('id="rtk-card"'), 'Missing rtk-card');
87
+ });
88
+ });
89
+
90
+ describe('T-001: vendor/marked.min.js exists', () => {
91
+ test('file exists with size > 10000 bytes', () => {
92
+ const vendorPath = path.join(PUBLIC_DIR, 'vendor', 'marked.min.js');
93
+ assert.ok(fs.existsSync(vendorPath), 'vendor/marked.min.js does not exist');
94
+ const stat = fs.statSync(vendorPath);
95
+ assert.ok(stat.size > 10000, `vendor/marked.min.js too small: ${stat.size} bytes`);
96
+ });
97
+ });
98
+
99
+ // ── T-002: app.js tab engine + language + polling ─────────────────────────────
100
+
101
+ describe('T-002: app.js tab engine', () => {
102
+ let js;
103
+ before(() => {
104
+ js = fs.readFileSync(path.join(PUBLIC_DIR, 'app.js'), 'utf8');
105
+ });
106
+
107
+ test('contains activateTab function', () => {
108
+ assert.ok(js.includes('activateTab'), 'Missing activateTab function');
109
+ });
110
+
111
+ test('references tab-nav (at least 2 occurrences of activateTab or tab-nav)', () => {
112
+ const count = (js.match(/activateTab|tab-nav/g) || []).length;
113
+ assert.ok(count >= 2, `Expected >= 2 occurrences of activateTab|tab-nav, got ${count}`);
114
+ });
115
+
116
+ test('contains kud_lang localStorage key (at least 2 occurrences)', () => {
117
+ const count = (js.match(/kud_lang/g) || []).length;
118
+ assert.ok(count >= 2, `Expected >= 2 occurrences of kud_lang, got ${count}`);
119
+ });
120
+
121
+ test('contains hashchange listener', () => {
122
+ assert.ok(js.includes('hashchange'), 'Missing hashchange listener');
123
+ });
124
+
125
+ test('contains DOMContentLoaded handler', () => {
126
+ assert.ok(js.includes('DOMContentLoaded'), 'Missing DOMContentLoaded handler');
127
+ });
128
+
129
+ test('contains translation-banner reference', () => {
130
+ const count = (js.match(/translation-banner/g) || []).length;
131
+ assert.ok(count >= 1, `Expected >= 1 occurrence of translation-banner, got ${count}`);
132
+ });
133
+
134
+ test('contains buildToc or generateToc function', () => {
135
+ const count = (js.match(/buildToc|generateToc/g) || []).length;
136
+ assert.ok(count >= 1, `Expected >= 1 occurrence of buildToc|generateToc, got ${count}`);
137
+ });
138
+
139
+ test('polling pauses when dashboard tab is not active', () => {
140
+ // Must reference pausing/clearing interval or conditional check
141
+ assert.ok(
142
+ js.includes('clearInterval') || js.includes('pause') || js.includes('activeTab'),
143
+ 'Missing polling pause logic'
144
+ );
145
+ });
146
+
147
+ test('contains localStorage.setItem for kud_lang', () => {
148
+ assert.ok(js.includes("localStorage.setItem"), 'Missing localStorage.setItem');
149
+ });
150
+
151
+ test('contains localStorage.getItem for kud_lang', () => {
152
+ assert.ok(js.includes("localStorage.getItem"), 'Missing localStorage.getItem');
153
+ });
154
+ });
155
+
156
+ // ── T-003: /api/docs/* endpoints ──────────────────────────────────────────────
157
+
158
+ describe('T-003: /api/docs/* server endpoints', () => {
159
+ let request;
160
+ let serverModule;
161
+
162
+ before(async () => {
163
+ // We test the Express app directly without starting a real server
164
+ // by requiring the app factory (we need to refactor server.js to export app,
165
+ // or we test via supertest with a dynamic port)
166
+ const supertest = require('supertest');
167
+
168
+ // Create a minimal test server that only has the docs routes
169
+ // by loading the actual server module — but server.js auto-starts.
170
+ // So we test via HTTP against a running server, or we extract the app.
171
+ // For TDD: these tests will FAIL until server.js exports `app` OR
172
+ // we test with a live server.
173
+ // Strategy: require server-app module that exports Express app without listening.
174
+ const appPath = path.join(REPO_ROOT, 'dashboard', 'server-app.js');
175
+ assert.ok(fs.existsSync(appPath), 'server-app.js must exist (extracted Express app for testing)');
176
+ const { app } = require(appPath);
177
+ request = supertest(app);
178
+ });
179
+
180
+ test('GET /api/docs/skills returns 200 with JSON array', async () => {
181
+ const res = await request.get('/api/docs/skills');
182
+ assert.equal(res.status, 200);
183
+ assert.ok(Array.isArray(res.body), 'Response body must be an array');
184
+ assert.ok(res.body.length >= 5, `Expected >= 5 skills, got ${res.body.length}`);
185
+ });
186
+
187
+ test('GET /api/docs/skills entries have required fields', async () => {
188
+ const res = await request.get('/api/docs/skills');
189
+ assert.equal(res.status, 200);
190
+ for (const entry of res.body) {
191
+ assert.ok(entry.name, 'Missing name field');
192
+ assert.ok(entry.path, 'Missing path field');
193
+ }
194
+ });
195
+
196
+ test('GET /api/docs/agents returns 200 with JSON array', async () => {
197
+ const res = await request.get('/api/docs/agents');
198
+ assert.equal(res.status, 200);
199
+ assert.ok(Array.isArray(res.body), 'Response body must be an array');
200
+ assert.ok(res.body.length >= 5, `Expected >= 5 agents, got ${res.body.length}`);
201
+ });
202
+
203
+ test('GET /api/docs/agents entries have required fields', async () => {
204
+ const res = await request.get('/api/docs/agents');
205
+ assert.equal(res.status, 200);
206
+ for (const entry of res.body) {
207
+ assert.ok(entry.name, 'Missing name field');
208
+ assert.ok(entry.path, 'Missing path field');
209
+ }
210
+ });
211
+
212
+ test('GET /api/docs/adrs returns 200 with JSON array sorted by number', async () => {
213
+ const res = await request.get('/api/docs/adrs');
214
+ assert.equal(res.status, 200);
215
+ assert.ok(Array.isArray(res.body), 'Response body must be an array');
216
+ assert.ok(res.body.length > 0, 'ADRs array must not be empty');
217
+ const nums = res.body.map((x) => x.number);
218
+ for (let i = 1; i < nums.length; i++) {
219
+ assert.ok(nums[i] >= nums[i - 1], `ADRs not sorted: ${nums[i - 1]} > ${nums[i]}`);
220
+ }
221
+ });
222
+
223
+ test('GET /api/docs/adrs entries have required fields', async () => {
224
+ const res = await request.get('/api/docs/adrs');
225
+ assert.equal(res.status, 200);
226
+ for (const entry of res.body) {
227
+ assert.ok(typeof entry.number === 'number', 'Missing/invalid number field');
228
+ assert.ok(entry.slug, 'Missing slug field');
229
+ assert.ok(entry.path, 'Missing path field');
230
+ }
231
+ });
232
+
233
+ test('GET /api/docs/file with valid whitelisted path returns 200', async () => {
234
+ const res = await request.get('/api/docs/file?path=.claude/agents/analyst.md');
235
+ assert.equal(res.status, 200);
236
+ assert.ok(res.text.length > 0, 'Response body must not be empty');
237
+ });
238
+
239
+ test('GET /api/docs/file with path traversal .. returns 403', async () => {
240
+ const res = await request.get('/api/docs/file?path=../../package.json');
241
+ assert.equal(res.status, 403);
242
+ });
243
+
244
+ test('GET /api/docs/file with absolute path /etc/passwd returns 403', async () => {
245
+ const res = await request.get('/api/docs/file?path=/etc/passwd');
246
+ assert.equal(res.status, 403);
247
+ });
248
+
249
+ test('GET /api/docs/file with Windows drive letter returns 403', async () => {
250
+ const res = await request.get('/api/docs/file?path=C:\\Windows\\system32\\drivers\\etc\\hosts');
251
+ assert.equal(res.status, 403);
252
+ });
253
+
254
+ test('GET /api/docs/file with path outside whitelist returns 403', async () => {
255
+ const res = await request.get('/api/docs/file?path=bin/cli.js');
256
+ assert.equal(res.status, 403);
257
+ });
258
+
259
+ test('GET /api/docs/file with manifest.json (in whitelist) returns 200', async () => {
260
+ const res = await request.get('/api/docs/file?path=dashboard/public/content/manifest.json');
261
+ assert.equal(res.status, 200);
262
+ });
263
+
264
+ test('GET /api/docs/file with SKILL.md (in whitelist) returns 200', async () => {
265
+ const res = await request.get('/api/docs/file?path=.claude/skills/caveman/SKILL.md');
266
+ assert.equal(res.status, 200);
267
+ });
268
+ });
269
+
270
+ // ── T-004: manifest.json + content directories ────────────────────────────────
271
+
272
+ describe('T-004: manifest.json and content directories', () => {
273
+ const contentDir = path.join(PUBLIC_DIR, 'content');
274
+ const manifestPath = path.join(contentDir, 'manifest.json');
275
+
276
+ test('manifest.json exists', () => {
277
+ assert.ok(fs.existsSync(manifestPath), 'manifest.json does not exist');
278
+ });
279
+
280
+ test('manifest.json is valid JSON', () => {
281
+ const raw = fs.readFileSync(manifestPath, 'utf8');
282
+ assert.doesNotThrow(() => JSON.parse(raw), 'manifest.json is not valid JSON');
283
+ });
284
+
285
+ test('manifest.json has 5 manual entries', () => {
286
+ const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
287
+ assert.equal(m.manual.length, 5, `Expected 5 manual entries, got ${m.manual.length}`);
288
+ });
289
+
290
+ test('manifest.json has 5 docs entries', () => {
291
+ const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
292
+ assert.equal(m.docs.length, 5, `Expected 5 docs entries, got ${m.docs.length}`);
293
+ });
294
+
295
+ test('manual entries have required fields', () => {
296
+ const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
297
+ for (const entry of m.manual) {
298
+ assert.ok(entry.slug || entry.id, 'Manual entry missing slug/id');
299
+ assert.ok(entry.title_pt, 'Manual entry missing title_pt');
300
+ assert.ok(entry.title_en, 'Manual entry missing title_en');
301
+ }
302
+ });
303
+
304
+ test('docs entries have required fields', () => {
305
+ const m = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
306
+ for (const entry of m.docs) {
307
+ assert.ok(entry.slug || entry.id, 'Docs entry missing slug/id');
308
+ assert.ok(entry.title_pt, 'Docs entry missing title_pt');
309
+ assert.ok(entry.title_en, 'Docs entry missing title_en');
310
+ }
311
+ });
312
+
313
+ test('manual/ directory exists', () => {
314
+ assert.ok(fs.existsSync(path.join(contentDir, 'manual')), 'manual/ directory does not exist');
315
+ });
316
+
317
+ test('docs/ directory exists', () => {
318
+ assert.ok(fs.existsSync(path.join(contentDir, 'docs')), 'docs/ directory does not exist');
319
+ });
320
+
321
+ test('at least 20 .md files in content/', () => {
322
+ function countMd(dir) {
323
+ let count = 0;
324
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
325
+ const full = path.join(dir, entry.name);
326
+ if (entry.isDirectory()) count += countMd(full);
327
+ else if (entry.name.endsWith('.md')) count++;
328
+ }
329
+ return count;
330
+ }
331
+ const count = countMd(contentDir);
332
+ assert.ok(count >= 20, `Expected >= 20 .md files in content/, got ${count}`);
333
+ });
334
+ });
335
+
336
+ // ── T-005: CSS classes ────────────────────────────────────────────────────────
337
+
338
+ describe('T-005: style.css new UI classes', () => {
339
+ let css;
340
+ before(() => {
341
+ css = fs.readFileSync(path.join(PUBLIC_DIR, 'style.css'), 'utf8');
342
+ });
343
+
344
+ test('contains .tab-nav', () => {
345
+ assert.ok(css.includes('.tab-nav'), 'Missing .tab-nav in style.css');
346
+ });
347
+
348
+ test('contains .tab-panel', () => {
349
+ assert.ok(css.includes('.tab-panel'), 'Missing .tab-panel in style.css');
350
+ });
351
+
352
+ test('contains .lang-selector', () => {
353
+ assert.ok(css.includes('.lang-selector'), 'Missing .lang-selector in style.css');
354
+ });
355
+
356
+ test('contains .accordion', () => {
357
+ assert.ok(css.includes('.accordion'), 'Missing .accordion in style.css');
358
+ });
359
+
360
+ test('contains .toc-sidebar', () => {
361
+ assert.ok(css.includes('.toc-sidebar'), 'Missing .toc-sidebar in style.css');
362
+ });
363
+
364
+ test('contains .translation-banner', () => {
365
+ assert.ok(css.includes('.translation-banner'), 'Missing .translation-banner in style.css');
366
+ });
367
+
368
+ test('all 5+ required selectors present (combined check)', () => {
369
+ const required = ['.tab-nav', '.accordion', '.toc-sidebar', '.translation-banner', '.lang-selector'];
370
+ const found = required.filter((sel) => css.includes(sel));
371
+ assert.ok(found.length >= 5, `Expected >= 5 selectors, found: ${found.join(', ')}`);
372
+ });
373
+
374
+ test('contains responsive rule for <= 768px hiding toc-sidebar', () => {
375
+ assert.ok(
376
+ css.includes('768px') && (css.includes('toc-sidebar') || css.includes('accordion')),
377
+ 'Missing responsive 768px rule for toc-sidebar or accordion'
378
+ );
379
+ });
380
+ });
381
+
382
+ // ── T-006: Non-regression ─────────────────────────────────────────────────────
383
+
384
+ describe('T-006: non-regression — /api/stats still works', () => {
385
+ let request;
386
+
387
+ before(async () => {
388
+ const supertest = require('supertest');
389
+ const appPath = path.join(REPO_ROOT, 'dashboard', 'server-app.js');
390
+ assert.ok(fs.existsSync(appPath), 'server-app.js must exist');
391
+ const { app } = require(appPath);
392
+ request = supertest(app);
393
+ });
394
+
395
+ test('GET /api/stats returns 200', async () => {
396
+ const res = await request.get('/api/stats');
397
+ assert.equal(res.status, 200);
398
+ });
399
+
400
+ test('GET /api/stats returns projects and models fields', async () => {
401
+ const res = await request.get('/api/stats');
402
+ assert.equal(res.status, 200);
403
+ assert.ok(Object.prototype.hasOwnProperty.call(res.body, 'projects'), 'Missing projects field');
404
+ assert.ok(Object.prototype.hasOwnProperty.call(res.body, 'models'), 'Missing models field');
405
+ });
406
+ });