ndomo 0.1.0 → 0.2.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 (65) hide show
  1. package/.env.example +4 -0
  2. package/README.es.md +29 -23
  3. package/README.md +64 -24
  4. package/bun.lock +447 -0
  5. package/docs/configuration.md +4 -4
  6. package/docs/installation.md +53 -34
  7. package/docs/installer.md +164 -0
  8. package/docs/integrations.md +1 -1
  9. package/docs/web-ui.md +124 -0
  10. package/package.json +43 -4
  11. package/scripts/install.sh +28 -0
  12. package/scripts/smoke-install.sh +47 -0
  13. package/scripts/smoke-web.sh +335 -0
  14. package/src/cli/__tests__/install.test.ts +733 -0
  15. package/src/cli/index.ts +8 -0
  16. package/src/cli/install.ts +1292 -0
  17. package/src/config/__tests__/schema.test.ts +223 -0
  18. package/src/config/schema.ts +129 -16
  19. package/src/http/__tests__/auth.test.ts +10 -10
  20. package/src/http/__tests__/spa.test.ts +296 -0
  21. package/src/http/auth.ts +8 -1
  22. package/src/http/server.ts +71 -2
  23. package/.bun-version +0 -1
  24. package/.dockerignore +0 -79
  25. package/.editorconfig +0 -18
  26. package/.github/CODEOWNERS +0 -8
  27. package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
  28. package/.github/ISSUE_TEMPLATE/config.yml +0 -2
  29. package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -34
  30. package/.github/dependabot.yml +0 -36
  31. package/.github/pull_request_template.md +0 -24
  32. package/.github/release.yml +0 -30
  33. package/.github/workflows/gitleaks.yml +0 -28
  34. package/.github/workflows/release-please.yml +0 -27
  35. package/.github/workflows/smoke.yml +0 -29
  36. package/.husky/commit-msg +0 -1
  37. package/CHANGELOG.md +0 -114
  38. package/Dockerfile +0 -32
  39. package/bin/ndomo-analyses.ts +0 -4
  40. package/bin/ndomo-status.ts +0 -4
  41. package/biome.json +0 -57
  42. package/commitlint.config.js +0 -3
  43. package/opencode.json +0 -5
  44. package/release-please-config.json +0 -11
  45. package/scripts/dev-bust-cache.sh +0 -164
  46. package/scripts/smoke-e2e.ts +0 -704
  47. package/scripts/smoke-hot.ts +0 -417
  48. package/scripts/smoke-v4.ts +0 -256
  49. package/scripts/smoke-v5.ts +0 -397
  50. package/scripts/uninstall.sh +0 -224
  51. package/src/index.ts +0 -37
  52. package/src/lib.ts +0 -65
  53. package/src/mem/scoped.ts +0 -65
  54. package/src/orchestrator/background.test.ts +0 -268
  55. package/src/orchestrator/background.ts +0 -293
  56. package/src/orchestrator/memory-hook.ts +0 -182
  57. package/src/orchestrator/reconciler.ts +0 -123
  58. package/src/orchestrator/scheduler.test.ts +0 -300
  59. package/src/orchestrator/scheduler.ts +0 -243
  60. package/src/plugin.test.ts +0 -2574
  61. package/src/plugin.ts +0 -1690
  62. package/src/worktrees/manager.ts +0 -236
  63. package/src/worktrees/state.ts +0 -87
  64. package/tests/integration/ranger-flow.test.ts +0 -257
  65. package/tsconfig.json +0 -31
@@ -1,417 +0,0 @@
1
- /**
2
- * ndomo smoke-hot — end-to-end DB feature test (v1-v5).
3
- *
4
- * Runs 7 numbered tests on a fresh DB. Exits 0 on all pass, 1 on any fail.
5
- * Uses HOME from env, creates $HOME/.ndomo/ for mem/plans archive.
6
- * NDOMO_MEM_DIR env var overrides mem dir (default ~/.ndomo/mem/plans).
7
- *
8
- * Usage:
9
- * TESTHOME=$(mktemp -d) \
10
- * HOME=$TESTHOME \
11
- * NDOMO_MEM_DIR=$TESTHOME/.ndomo/mem/plans \
12
- * bun scripts/smoke-hot.ts
13
- *
14
- * Cleanup:
15
- * rm -rf $TESTHOME
16
- */
17
-
18
- import { existsSync, mkdirSync, readdirSync, readFileSync } from "node:fs";
19
- import { join } from "node:path";
20
- import { openDb } from "../src/db/client.ts";
21
- import { runMigrations } from "../src/db/migrations.ts";
22
- import { archivePlan } from "../src/db/plan-archive.ts";
23
- import { approvePlan, createPlan, searchPlans, updatePlanStatus } from "../src/db/plans.ts";
24
- import { checkpointSession, getSession, startSession } from "../src/db/sessions.ts";
25
- import { createTasksBatch, updateTaskStatus } from "../src/db/tasks.ts";
26
-
27
- // ─── Setup ─────────────────────────────────────────────────────────────────
28
-
29
- const home = process.env.HOME;
30
- if (!home) {
31
- console.error("HOME not set");
32
- process.exit(1);
33
- }
34
-
35
- const testDir = join(home, ".ndomo-test");
36
- mkdirSync(testDir, { recursive: true });
37
-
38
- // Ensure $HOME/.ndomo/ exists (spec requirement)
39
- mkdirSync(join(home, ".ndomo"), { recursive: true });
40
-
41
- const db = openDb(testDir);
42
- runMigrations(db);
43
-
44
- let testN = 0;
45
-
46
- function fail(n: number, msg: string, err?: unknown): never {
47
- console.error(`[${n}/7] ${msg}... FAILED`);
48
- if (err !== undefined) {
49
- console.error(err instanceof Error ? err.message : String(err));
50
- }
51
- console.error("SMOKE FAILED");
52
- db.close();
53
- process.exit(1);
54
- }
55
-
56
- // ─── Test 1: Migrations in fresh DB ────────────────────────────────────────
57
-
58
- testN++;
59
- try {
60
- const versions = db.query("SELECT version FROM schema_version ORDER BY version").all() as {
61
- version: number;
62
- }[];
63
-
64
- if (versions.length !== 5) {
65
- fail(testN, `schema_version has ${versions.length} entries, expected 5`, versions);
66
- }
67
- console.log(`[${testN}/7] schema_version has 5 entries (v1..v5)... OK`);
68
- console.log(JSON.stringify(versions, null, 2));
69
-
70
- const tables = db
71
- .query("SELECT name FROM sqlite_master WHERE type IN ('table','view') ORDER BY name")
72
- .all() as { name: string }[];
73
- const tableNames = tables.map((r) => r.name);
74
-
75
- const expected = ["plans", "plan_tasks", "sessions", "plan_tags", "task_tags", "plan_progress"];
76
- for (const name of expected) {
77
- if (!tableNames.includes(name)) {
78
- fail(testN, `missing table/view: ${name}`, tableNames);
79
- }
80
- }
81
- console.log(`[${testN}/7] Required tables/views exist... OK`);
82
- console.log(JSON.stringify(tableNames, null, 2));
83
- } catch (err) {
84
- fail(testN, "Migrations in fresh DB", err);
85
- }
86
-
87
- // ─── Test 2: createPlan (draft) ────────────────────────────────────────────
88
-
89
- testN++;
90
- try {
91
- const plan = createPlan(db, {
92
- id: "hp1",
93
- slug: "hot-test",
94
- title: "Hot Test",
95
- status: "draft",
96
- priority: 3,
97
- overview: "smoke hot",
98
- approvedAt: null,
99
- completedAt: null,
100
- sessionId: null,
101
- approach: null,
102
- complexity: 3,
103
- createdBy: "smoke",
104
- updatedBy: "smoke",
105
- sourceSessionId: null,
106
- sourceMessageId: null,
107
- category: null,
108
- metadata: {},
109
- archivedAt: null,
110
- });
111
-
112
- if (plan.status !== "draft") {
113
- fail(testN, `expected status "draft", got "${plan.status}"`, plan);
114
- }
115
- if (plan.id !== "hp1") {
116
- fail(testN, `expected id "hp1", got "${plan.id}"`, plan);
117
- }
118
- console.log(`[${testN}/7] createPlan (draft) id=hp1 status=draft... OK`);
119
- console.log(JSON.stringify({ id: plan.id, status: plan.status }, null, 2));
120
- } catch (err) {
121
- fail(testN, "createPlan (draft)", err);
122
- }
123
-
124
- // ─── Test 3: approvePlan + createTasksBatch ────────────────────────────────
125
-
126
- testN++;
127
- try {
128
- const approved = approvePlan(db, "hp1");
129
- if (!approved) {
130
- fail(testN, "approvePlan returned null");
131
- }
132
- if (approved.status !== "approved") {
133
- fail(testN, `expected status "approved", got "${approved.status}"`, approved);
134
- }
135
- if (approved.approvedAt === null) {
136
- fail(testN, "approvedAt should not be null after approvePlan", approved);
137
- }
138
- console.log(`[${testN}/7] approvePlan status=approved approvedAt=set... OK`);
139
-
140
- const tasks = createTasksBatch(db, "hp1", [
141
- {
142
- orderIndex: 0,
143
- description: "task 1",
144
- agent: "smith",
145
- files: [],
146
- dependencies: [],
147
- complexity: 2,
148
- createdBy: "smoke",
149
- updatedBy: "smoke",
150
- sourceSessionId: null,
151
- sourceMessageId: null,
152
- reviewedBy: null,
153
- tokensUsed: null,
154
- durationMs: null,
155
- artifacts: [],
156
- metadata: {},
157
- },
158
- {
159
- orderIndex: 1,
160
- description: "task 2",
161
- agent: "js-smith",
162
- files: [],
163
- dependencies: [],
164
- complexity: 3,
165
- createdBy: "smoke",
166
- updatedBy: "smoke",
167
- sourceSessionId: null,
168
- sourceMessageId: null,
169
- reviewedBy: null,
170
- tokensUsed: null,
171
- durationMs: null,
172
- artifacts: [],
173
- metadata: {},
174
- },
175
- ]);
176
-
177
- if (tasks.length !== 2) {
178
- fail(testN, `expected 2 tasks, got ${tasks.length}`, tasks);
179
- }
180
- if (tasks[0]?.orderIndex !== 0) {
181
- fail(testN, `task[0].orderIndex expected 0, got ${tasks[0]?.orderIndex}`, tasks[0]);
182
- }
183
- if (tasks[1]?.orderIndex !== 1) {
184
- fail(testN, `task[1].orderIndex expected 1, got ${tasks[1]?.orderIndex}`, tasks[1]);
185
- }
186
- console.log(`[${testN}/7] createTasksBatch 2 tasks orderIndex 0,1... OK`);
187
- console.log(
188
- JSON.stringify(
189
- tasks.map((t) => ({ id: t.id, orderIndex: t.orderIndex, description: t.description })),
190
- null,
191
- 2,
192
- ),
193
- );
194
- } catch (err) {
195
- fail(testN, "approvePlan + createTasksBatch", err);
196
- }
197
-
198
- // ─── Test 4: session + checkpoint + task updates ───────────────────────────
199
-
200
- testN++;
201
- try {
202
- const sessionStarted = startSession(db, {
203
- id: "hs1",
204
- planId: "hp1",
205
- goal: "smoke",
206
- metadata: {},
207
- });
208
- if (sessionStarted.id !== "hs1") {
209
- fail(testN, `startSession id mismatch: got "${sessionStarted.id}"`);
210
- }
211
- console.log(`[${testN}/7] startSession id=hs1... OK`);
212
-
213
- const sessionCheckpointed = checkpointSession(
214
- db,
215
- "hs1",
216
- { phase: "testing" },
217
- "chose smoke path",
218
- );
219
- if (!sessionCheckpointed) {
220
- fail(testN, "checkpointSession returned null");
221
- }
222
- if (sessionCheckpointed.state.phase !== "testing") {
223
- fail(
224
- testN,
225
- `expected state.phase "testing", got "${String(sessionCheckpointed.state.phase)}"`,
226
- sessionCheckpointed.state,
227
- );
228
- }
229
- if (sessionCheckpointed.keyDecisions !== "chose smoke path") {
230
- fail(
231
- testN,
232
- `expected keyDecisions "chose smoke path", got "${sessionCheckpointed.keyDecisions}"`,
233
- );
234
- }
235
- console.log(`[${testN}/7] checkpointSession phase=testing keyDecisions=set... OK`);
236
-
237
- // Get tasks for this plan via direct query (avoids import cycle)
238
- const tasks = (
239
- db
240
- .query("SELECT id, order_index FROM plan_tasks WHERE plan_id = ? ORDER BY order_index")
241
- .all("hp1") as { id: string; order_index: number }[]
242
- ).map((r) => ({ id: r.id, orderIndex: r.order_index }));
243
-
244
- if (tasks.length < 2) {
245
- fail(testN, `expected at least 2 tasks, got ${tasks.length}`, tasks);
246
- }
247
- const t0 = tasks[0] as { id: string };
248
- const t1 = tasks[1] as { id: string };
249
-
250
- // Set task 0 to running
251
- const runningTask = updateTaskStatus(db, t0.id, "running");
252
- if (!runningTask || runningTask.status !== "running") {
253
- fail(testN, `task[0] status expected "running", got "${runningTask?.status}"`);
254
- }
255
- if (runningTask.startedAt === null) {
256
- fail(testN, "task[0].startedAt should be set after running status", runningTask);
257
- }
258
- console.log(`[${testN}/7] task[0] status=running startedAt=set... OK`);
259
-
260
- // Set task 0 to done with result
261
- const doneTask0 = updateTaskStatus(db, t0.id, "done", {
262
- result: "completed successfully",
263
- });
264
- if (!doneTask0 || doneTask0.status !== "done") {
265
- fail(testN, `task[0] status expected "done", got "${doneTask0?.status}"`);
266
- }
267
- if (doneTask0.completedAt === null) {
268
- fail(testN, "task[0].completedAt should be set after done status", doneTask0);
269
- }
270
- console.log(`[${testN}/7] task[0] status=done completedAt=set... OK`);
271
-
272
- // Set task 1 to done with result
273
- const doneTask1 = updateTaskStatus(db, t1.id, "done", { result: "ok" });
274
- if (!doneTask1 || doneTask1.status !== "done") {
275
- fail(testN, `task[1] status expected "done", got "${doneTask1?.status}"`);
276
- }
277
- console.log(`[${testN}/7] task[1] status=done... OK`);
278
-
279
- // Verify session state persisted
280
- const sessionReloaded = getSession(db, "hs1");
281
- if (!sessionReloaded) {
282
- fail(testN, "getSession returned null after updates");
283
- }
284
- if (sessionReloaded.state.phase !== "testing") {
285
- fail(testN, "session.state.phase should persist after checkpoint", sessionReloaded.state);
286
- }
287
- console.log(`[${testN}/7] session state persisted phase=testing... OK`);
288
- } catch (err) {
289
- fail(testN, "session + checkpoint + task updates", err);
290
- }
291
-
292
- // ─── Test 5: updatePlanStatus(completed) + auto-archive ────────────────────
293
-
294
- testN++;
295
- try {
296
- const updated = updatePlanStatus(db, "hp1", "completed");
297
- if (!updated || updated.status !== "completed") {
298
- fail(testN, `updatePlanStatus expected "completed", got "${updated?.status}"`);
299
- }
300
- console.log(`[${testN}/7] updatePlanStatus(completed)... OK`);
301
-
302
- // Replicate auto-archive logic from plugin.ts
303
- // getMemDir equivalent: NDOMO_MEM_DIR env var, else ~/.ndomo/mem/plans
304
- const localMemDir = process.env.NDOMO_MEM_DIR ?? join(home, ".ndomo", "mem", "plans");
305
- mkdirSync(localMemDir, { recursive: true });
306
-
307
- const archiveResult = archivePlan(db, "hp1", { memDir: localMemDir });
308
-
309
- if (!existsSync(archiveResult.filePath)) {
310
- fail(testN, `archive file not found at ${archiveResult.filePath}`, archiveResult);
311
- }
312
- console.log(`[${testN}/7] archive file exists... OK`);
313
- console.log(
314
- JSON.stringify({ filePath: archiveResult.filePath, byteSize: archiveResult.byteSize }, null, 2),
315
- );
316
-
317
- const mdContent = readFileSync(archiveResult.filePath, "utf-8");
318
- if (!mdContent.includes("Hot Test")) {
319
- fail(testN, "archive markdown missing 'Hot Test'", { preview: mdContent.slice(0, 200) });
320
- }
321
- if (!mdContent.includes("## Tasks")) {
322
- fail(testN, "archive markdown missing '## Tasks' section", {
323
- preview: mdContent.slice(0, 500),
324
- });
325
- }
326
- console.log(`[${testN}/7] markdown includes "Hot Test" and "## Tasks"... OK`);
327
- } catch (err) {
328
- fail(testN, "updatePlanStatus + auto-archive", err);
329
- }
330
-
331
- // ─── Test 6: searchPlans filters archived by default ───────────────────────
332
-
333
- testN++;
334
- try {
335
- const defaultResults = searchPlans(db, "hot test");
336
- if (defaultResults.length !== 0) {
337
- fail(
338
- testN,
339
- `searchPlans() expected 0 results, got ${defaultResults.length}`,
340
- defaultResults.map((p) => ({ id: p.id, title: p.title })),
341
- );
342
- }
343
- console.log(`[${testN}/7] searchPlans() returns 0 (archived excluded)... OK`);
344
-
345
- const archivedResults = searchPlans(db, "hot test", 20, {
346
- includeArchived: true,
347
- });
348
- if (archivedResults.length !== 1) {
349
- fail(
350
- testN,
351
- `searchPlans(includeArchived:true) expected 1 result, got ${archivedResults.length}`,
352
- archivedResults.map((p) => ({ id: p.id, title: p.title })),
353
- );
354
- }
355
- if (archivedResults[0]?.id !== "hp1") {
356
- fail(testN, `expected hp1, got ${archivedResults[0]?.id}`, archivedResults[0]);
357
- }
358
- console.log(`[${testN}/7] searchPlans(includeArchived:true) returns hp1... OK`);
359
- console.log(
360
- JSON.stringify(
361
- archivedResults.map((p) => ({ id: p.id, title: p.title })),
362
- null,
363
- 2,
364
- ),
365
- );
366
- } catch (err) {
367
- fail(testN, "searchPlans archive filter", err);
368
- }
369
-
370
- // ─── Test 7: verify archive markdown format ────────────────────────────────
371
-
372
- testN++;
373
- try {
374
- const localMemDir = process.env.NDOMO_MEM_DIR ?? join(home, ".ndomo", "mem", "plans");
375
- // Find the md file for hp1 (hot-test-2026-*.md)
376
- const files = readdirSync(localMemDir).filter(
377
- (f) => f.startsWith("hot-test-") && f.endsWith(".md"),
378
- );
379
- if (files.length === 0) {
380
- fail(testN, "no archive markdown file found in memDir", localMemDir);
381
- }
382
- const firstFile = files[0] as string;
383
- const mdPath = join(localMemDir, firstFile);
384
- const mdContent = readFileSync(mdPath, "utf-8");
385
- const lines = mdContent.split("\n");
386
-
387
- // First line should be # Plan: Hot Test
388
- const firstLine = lines[0] ?? "";
389
- if (!firstLine.includes("Hot Test")) {
390
- fail(testN, `first line should contain "Hot Test", got: "${firstLine}"`);
391
- }
392
- console.log(`[${testN}/7] first line contains "Hot Test"... OK`);
393
-
394
- // Section ## Tasks with 2 [x] checkboxes
395
- const taskCheckboxMatches = mdContent.match(/\[x\]/g);
396
- if (!taskCheckboxMatches || taskCheckboxMatches.length < 2) {
397
- fail(testN, `expected 2 [x] checkboxes, found ${taskCheckboxMatches?.length ?? 0}`);
398
- }
399
- console.log(`[${testN}/7] has ## Tasks with 2 [x] checkboxes... OK`);
400
-
401
- // Section ## Metadata with JSON block
402
- if (!mdContent.includes("## Metadata")) {
403
- fail(testN, "missing ## Metadata section", { preview: mdContent.slice(-500) });
404
- }
405
- if (!mdContent.includes("```json")) {
406
- fail(testN, "missing JSON code block in Metadata", { preview: mdContent.slice(-500) });
407
- }
408
- console.log(`[${testN}/7] has ## Metadata with JSON block... OK`);
409
- } catch (err) {
410
- fail(testN, "archive markdown format check", err);
411
- }
412
-
413
- // ─── Done ──────────────────────────────────────────────────────────────────
414
-
415
- db.close();
416
- console.log("SMOKE OK");
417
- process.exit(0);
@@ -1,256 +0,0 @@
1
- /**
2
- * ndomo v4 migration smoke test.
3
- *
4
- * Verifies all 6 fixes work correctly:
5
- * 1. priority CHECK trigger (1-4)
6
- * 2. slug format validation trigger (kebab-case)
7
- * 3. plan_progress view
8
- * 4. FTS5 diacritics normalization
9
- * 5. result/error truncation (code-level)
10
- * 6. metadata DEFAULT '{}' trigger
11
- *
12
- * Usage: bun run scripts/smoke-v4.ts
13
- */
14
-
15
- import { Database } from "bun:sqlite";
16
- import { runMigrations } from "../src/db/migrations.ts";
17
- import { createPlan, getPlanProgress } from "../src/db/plans.ts";
18
- import { createTasksBatch, getTask, updateTaskStatus } from "../src/db/tasks.ts";
19
- import type { Plan } from "../src/db/types.ts";
20
-
21
- let pass = 0;
22
- let fail = 0;
23
-
24
- function assert(cond: boolean, msg: string): void {
25
- if (cond) {
26
- pass++;
27
- console.log(` ✓ ${msg}`);
28
- } else {
29
- fail++;
30
- console.error(` ✗ ${msg}`);
31
- }
32
- }
33
-
34
- function assertThrows(fn: () => void, msg: string): void {
35
- try {
36
- fn();
37
- fail++;
38
- console.error(` ✗ ${msg} (expected error, got none)`);
39
- } catch {
40
- pass++;
41
- console.log(` ✓ ${msg}`);
42
- }
43
- }
44
-
45
- const PLAN_DEFAULTS = {
46
- title: "Test",
47
- status: "draft" as const,
48
- priority: 3,
49
- overview: "test",
50
- complexity: 3,
51
- createdBy: "smoke",
52
- updatedBy: "smoke",
53
- metadata: {},
54
- approvedAt: null,
55
- completedAt: null,
56
- sessionId: null,
57
- approach: null,
58
- sourceSessionId: null,
59
- sourceMessageId: null,
60
- category: null,
61
- archivedAt: null,
62
- };
63
-
64
- function mkPlan(
65
- overrides: Pick<Plan, "id" | "slug"> &
66
- Partial<Omit<Plan, "id" | "slug" | "createdAt" | "updatedAt">>,
67
- ): Omit<Plan, "createdAt" | "updatedAt"> {
68
- return { ...PLAN_DEFAULTS, ...overrides };
69
- }
70
-
71
- const TASK_DEFAULTS = {
72
- agent: "smoke",
73
- files: [] as string[],
74
- complexity: 3,
75
- createdBy: "smoke",
76
- updatedBy: "smoke",
77
- sourceSessionId: null,
78
- sourceMessageId: null,
79
- reviewedBy: null,
80
- tokensUsed: null,
81
- durationMs: null,
82
- artifacts: [] as string[],
83
- dependencies: [] as string[],
84
- metadata: {},
85
- };
86
-
87
- // Use in-memory DB for isolated smoke test
88
- const db = new Database(":memory:");
89
- db.exec("PRAGMA journal_mode = WAL");
90
- db.exec("PRAGMA foreign_keys = ON");
91
-
92
- console.log("ndomo v4 smoke test\n");
93
-
94
- // ── Run migrations ──────────────────────────────────────────────────────────
95
- console.log("Running migrations v1→v4...");
96
- runMigrations(db);
97
- const ver = db.query("SELECT MAX(version) as v FROM schema_version").get() as { v: number };
98
- assert(ver.v >= 4, `schema_version >= 4 (got ${ver.v})`);
99
-
100
- // ── Fix 1: priority CHECK ───────────────────────────────────────────────────
101
- console.log("\nFix 1: priority CHECK trigger");
102
- createPlan(db, mkPlan({ id: "p1", slug: "test-plan", priority: 1 }));
103
- assert(true, "priority=1 accepted");
104
-
105
- createPlan(db, mkPlan({ id: "p2", slug: "test-plan-2", priority: 4 }));
106
- assert(true, "priority=4 accepted");
107
-
108
- assertThrows(
109
- () => createPlan(db, mkPlan({ id: "p-bad", slug: "bad-priority", priority: 5 })),
110
- "priority=5 rejected",
111
- );
112
-
113
- assertThrows(
114
- () => createPlan(db, mkPlan({ id: "p-bad2", slug: "bad-priority-2", priority: 0 })),
115
- "priority=0 rejected",
116
- );
117
-
118
- // ── Fix 2: slug validation ──────────────────────────────────────────────────
119
- console.log("\nFix 2: slug format validation");
120
- assertThrows(
121
- () => createPlan(db, mkPlan({ id: "s-bad1", slug: "Foo Bar" })),
122
- "slug 'Foo Bar' rejected (space)",
123
- );
124
- assertThrows(
125
- () => createPlan(db, mkPlan({ id: "s-bad2", slug: "foo_bar" })),
126
- "slug 'foo_bar' rejected (underscore)",
127
- );
128
- assertThrows(
129
- () => createPlan(db, mkPlan({ id: "s-bad3", slug: "foo--bar" })),
130
- "slug 'foo--bar' rejected (double dash)",
131
- );
132
- assertThrows(
133
- () => createPlan(db, mkPlan({ id: "s-bad4", slug: "foo-" })),
134
- "slug 'foo-' rejected (trailing dash)",
135
- );
136
-
137
- // Valid slugs
138
- createPlan(db, mkPlan({ id: "s-ok1", slug: "foo-bar" }));
139
- assert(true, "slug 'foo-bar' accepted");
140
- createPlan(db, mkPlan({ id: "s-ok2", slug: "foo123" }));
141
- assert(true, "slug 'foo123' accepted");
142
- createPlan(db, mkPlan({ id: "s-ok3", slug: "foo" }));
143
- assert(true, "slug 'foo' accepted");
144
-
145
- // ── Fix 3: plan_progress view ───────────────────────────────────────────────
146
- console.log("\nFix 3: plan_progress view");
147
-
148
- createTasksBatch(db, "p1", [
149
- { ...TASK_DEFAULTS, description: "task a", orderIndex: 0 },
150
- { ...TASK_DEFAULTS, description: "task b", orderIndex: 1 },
151
- { ...TASK_DEFAULTS, description: "task c", orderIndex: 2 },
152
- ]);
153
-
154
- const tasks = db
155
- .query("SELECT id FROM plan_tasks WHERE plan_id = 'p1' ORDER BY order_index")
156
- .all() as Array<{ id: string }>;
157
- const t0 = tasks[0]?.id;
158
- const t1 = tasks[1]?.id;
159
- const t2 = tasks[2]?.id;
160
- if (t0) updateTaskStatus(db, t0, "done", { result: "ok" });
161
- if (t1) updateTaskStatus(db, t1, "failed", { error: "oops" });
162
- // t2 remains pending
163
-
164
- const progress = getPlanProgress(db, "p1");
165
- assert(progress.length === 1, "plan_progress returns 1 row for p1");
166
- const p = progress[0];
167
- if (p) {
168
- assert(p.totalTasks === 3, "total_tasks = 3");
169
- assert(p.done === 1, "done = 1");
170
- assert(p.failed === 1, "failed = 1");
171
- assert(p.pending === 1, "pending = 1");
172
- assert(p.progressPct === 33, "progress_pct = 33 (1/3 * 100 rounded)");
173
- }
174
-
175
- // All plans
176
- const allProgress = getPlanProgress(db);
177
- assert(allProgress.length >= 3, `plan_progress returns ${allProgress.length} rows (all plans)`);
178
-
179
- // ── Fix 4: FTS5 diacritics ──────────────────────────────────────────────────
180
- console.log("\nFix 4: FTS5 diacritics normalization");
181
-
182
- createPlan(
183
- db,
184
- mkPlan({
185
- id: "fts1",
186
- slug: "fts-test",
187
- title: "Implementación de acción correctiva",
188
- overview: "Revisión del módulo de conexión",
189
- }),
190
- );
191
-
192
- const ftsResults = db
193
- .query("SELECT id FROM plans_fts_v2 WHERE plans_fts_v2 MATCH ?")
194
- .all("accion") as Array<{ id: string }>;
195
- assert(ftsResults.length === 1, "FTS5: search 'accion' finds 'acción' (diacritics normalized)");
196
-
197
- const ftsResults2 = db
198
- .query("SELECT id FROM plans_fts_v2 WHERE plans_fts_v2 MATCH ?")
199
- .all("implementacion") as Array<{ id: string }>;
200
- assert(ftsResults2.length === 1, "FTS5: search 'implementacion' finds 'Implementación'");
201
-
202
- // Task FTS
203
- if (t0) updateTaskStatus(db, t0, "done", { result: "Corrección aplicada exitosamente" });
204
- const taskFts = db
205
- .query("SELECT id FROM tasks_fts WHERE tasks_fts MATCH ?")
206
- .all("correccion") as Array<{ id: string }>;
207
- assert(taskFts.length === 1, "tasks_fts: search 'correccion' finds 'Corrección'");
208
-
209
- // ── Fix 5: result/error truncation ───────────────────────────────────────────
210
- console.log("\nFix 5: result/error truncation");
211
-
212
- const bigResult = "x".repeat(20 * 1024); // 20 KB
213
- if (t2) updateTaskStatus(db, t2, "done", { result: bigResult });
214
- const truncated = t2 ? getTask(db, t2) : null;
215
- assert(truncated !== null, "task exists after update");
216
- if (truncated?.result) {
217
- assert(
218
- truncated.result.length <= 16 * 1024,
219
- `result truncated: ${truncated.result.length} bytes <= 16384`,
220
- );
221
- assert(truncated.result.endsWith("…[truncated]"), "result ends with truncation marker");
222
- }
223
-
224
- // ── Fix 6: metadata DEFAULT trigger ─────────────────────────────────────────
225
- console.log("\nFix 6: metadata DEFAULT '{}' trigger");
226
-
227
- db.query(
228
- `INSERT INTO plan_tasks (id, plan_id, order_index, description, agent, files, complexity, status, dependencies, metadata, created_by, updated_by)
229
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
230
- ).run(
231
- "meta-test",
232
- "p1",
233
- 99,
234
- "meta test",
235
- "smoke",
236
- "[]",
237
- 3,
238
- "pending",
239
- "[]",
240
- null,
241
- "smoke",
242
- "smoke",
243
- );
244
- const metaTask = db.query("SELECT metadata FROM plan_tasks WHERE id = 'meta-test'").get() as {
245
- metadata: string;
246
- } | null;
247
- assert(metaTask !== null, "direct insert with NULL metadata succeeded");
248
- if (metaTask) {
249
- assert(metaTask.metadata === "{}", `metadata defaulted to '{}': got '${metaTask.metadata}'`);
250
- }
251
-
252
- // ── Summary ─────────────────────────────────────────────────────────────────
253
- console.log(`\n${"─".repeat(50)}`);
254
- console.log(`Results: ${pass} passed, ${fail} failed`);
255
- db.close();
256
- process.exit(fail > 0 ? 1 : 0);