ndomo 0.1.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 (247) hide show
  1. package/.bun-version +1 -0
  2. package/.dockerignore +79 -0
  3. package/.editorconfig +18 -0
  4. package/.env.example +19 -0
  5. package/.github/CODEOWNERS +8 -0
  6. package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
  7. package/.github/ISSUE_TEMPLATE/config.yml +2 -0
  8. package/.github/ISSUE_TEMPLATE/feature_request.yml +34 -0
  9. package/.github/dependabot.yml +36 -0
  10. package/.github/pull_request_template.md +24 -0
  11. package/.github/release.yml +30 -0
  12. package/.github/workflows/gitleaks.yml +28 -0
  13. package/.github/workflows/release-please.yml +27 -0
  14. package/.github/workflows/smoke.yml +29 -0
  15. package/.husky/commit-msg +1 -0
  16. package/CHANGELOG.md +114 -0
  17. package/Dockerfile +32 -0
  18. package/README.es.md +174 -0
  19. package/README.md +187 -0
  20. package/agents/chronicler.md +98 -0
  21. package/agents/ci-smith.md +136 -0
  22. package/agents/craftsman.md +341 -0
  23. package/agents/deploy-smith.md +138 -0
  24. package/agents/foreman.md +377 -0
  25. package/agents/go-smith.md +164 -0
  26. package/agents/guild.md +188 -0
  27. package/agents/inspector.md +83 -0
  28. package/agents/js-smith.md +127 -0
  29. package/agents/ops-scout.md +173 -0
  30. package/agents/painter.md +200 -0
  31. package/agents/python-smith.md +120 -0
  32. package/agents/ranger.md +307 -0
  33. package/agents/release-smith.md +165 -0
  34. package/agents/rust-smith.md +159 -0
  35. package/agents/sage.md +178 -0
  36. package/agents/scout.md +144 -0
  37. package/agents/scribe.md +156 -0
  38. package/agents/smith.md +201 -0
  39. package/agents/vue-smith.md +155 -0
  40. package/agents/warden.md +216 -0
  41. package/agents/zig-smith.md +156 -0
  42. package/bin/ndomo-analyses.ts +4 -0
  43. package/bin/ndomo-status.ts +4 -0
  44. package/biome.json +57 -0
  45. package/bun.lock +514 -0
  46. package/commitlint.config.js +3 -0
  47. package/config/ndomo.config.json +258 -0
  48. package/config/ndomo.schema.json +166 -0
  49. package/docs/agents.md +375 -0
  50. package/docs/bugs/plan-create-orphan-fk.md +131 -0
  51. package/docs/bugs/task_create_batch-order-index-collision.md +158 -0
  52. package/docs/configuration.md +276 -0
  53. package/docs/database.md +364 -0
  54. package/docs/features/feature-flexible-builder-v1.md +724 -0
  55. package/docs/features/feature-flexible-builder-v2.md +882 -0
  56. package/docs/features/feature-flexible-builder.md +974 -0
  57. package/docs/http-server.md +244 -0
  58. package/docs/installation.md +259 -0
  59. package/docs/integrations.md +129 -0
  60. package/docs/operations/anti-pattern-sub-agent-verify-2026-06-21.md +32 -0
  61. package/docs/operations/audit-v1.md +417 -0
  62. package/docs/operations/audit-v2.md +197 -0
  63. package/docs/operations/audit-v3.md +306 -0
  64. package/docs/operations/db-optimize-foundations.md +123 -0
  65. package/docs/operations/verify-gate-architecture.md +82 -0
  66. package/docs/workflows.md +448 -0
  67. package/opencode.json +5 -0
  68. package/package.json +65 -0
  69. package/release-please-config.json +11 -0
  70. package/scripts/dev-bust-cache.sh +164 -0
  71. package/scripts/install.sh +688 -0
  72. package/scripts/smoke-e2e.ts +704 -0
  73. package/scripts/smoke-hot.ts +417 -0
  74. package/scripts/smoke-http.sh +228 -0
  75. package/scripts/smoke-v4.ts +256 -0
  76. package/scripts/smoke-v5.ts +397 -0
  77. package/scripts/smoke.sh +9 -0
  78. package/scripts/uninstall.sh +224 -0
  79. package/skills/api-security-best-practices/SKILL.md +915 -0
  80. package/skills/bash-scripting/SKILL.md +201 -0
  81. package/skills/bun/SKILL.md +313 -0
  82. package/skills/cavecrew/SKILL.md +82 -0
  83. package/skills/caveman/SKILL.md +74 -0
  84. package/skills/caveman-review/README.md +33 -0
  85. package/skills/caveman-review/SKILL.md +55 -0
  86. package/skills/find-skills/SKILL.md +142 -0
  87. package/skills/frontend-design/LICENSE.txt +177 -0
  88. package/skills/frontend-design/SKILL.md +55 -0
  89. package/skills/golang-patterns/SKILL.md +674 -0
  90. package/skills/golang-security/SKILL.md +185 -0
  91. package/skills/golang-security/evals/evals.json +595 -0
  92. package/skills/golang-security/references/architecture.md +268 -0
  93. package/skills/golang-security/references/checklist.md +80 -0
  94. package/skills/golang-security/references/cookies.md +200 -0
  95. package/skills/golang-security/references/cryptography.md +424 -0
  96. package/skills/golang-security/references/filesystem.md +285 -0
  97. package/skills/golang-security/references/injection.md +315 -0
  98. package/skills/golang-security/references/logging.md +163 -0
  99. package/skills/golang-security/references/memory-safety.md +241 -0
  100. package/skills/golang-security/references/network.md +253 -0
  101. package/skills/golang-security/references/secrets.md +189 -0
  102. package/skills/golang-security/references/third-party.md +159 -0
  103. package/skills/golang-security/references/threat-modeling.md +189 -0
  104. package/skills/golang-testing/SKILL.md +720 -0
  105. package/skills/grill-me/SKILL.md +7 -0
  106. package/skills/javascript-testing-patterns/SKILL.md +537 -0
  107. package/skills/javascript-testing-patterns/references/advanced-testing-patterns.md +513 -0
  108. package/skills/modern-javascript-patterns/SKILL.md +43 -0
  109. package/skills/modern-javascript-patterns/references/advanced-patterns.md +487 -0
  110. package/skills/modern-javascript-patterns/references/details.md +457 -0
  111. package/skills/python-anti-patterns/SKILL.md +349 -0
  112. package/skills/python-design-patterns/SKILL.md +85 -0
  113. package/skills/python-design-patterns/references/details.md +353 -0
  114. package/skills/python-error-handling/SKILL.md +193 -0
  115. package/skills/python-error-handling/references/details.md +171 -0
  116. package/skills/python-testing-patterns/SKILL.md +278 -0
  117. package/skills/python-testing-patterns/references/advanced-patterns.md +411 -0
  118. package/skills/python-testing-patterns/references/details.md +349 -0
  119. package/skills/rust-patterns/SKILL.md +500 -0
  120. package/skills/rust-testing/SKILL.md +501 -0
  121. package/skills/security-review/SKILL.md +504 -0
  122. package/skills/security-review/cloud-infrastructure-security.md +361 -0
  123. package/skills/vue-best-practices/SKILL.md +154 -0
  124. package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
  125. package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
  126. package/skills/vue-best-practices/references/component-async.md +97 -0
  127. package/skills/vue-best-practices/references/component-data-flow.md +307 -0
  128. package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
  129. package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
  130. package/skills/vue-best-practices/references/component-slots.md +216 -0
  131. package/skills/vue-best-practices/references/component-suspense.md +228 -0
  132. package/skills/vue-best-practices/references/component-teleport.md +108 -0
  133. package/skills/vue-best-practices/references/component-transition-group.md +128 -0
  134. package/skills/vue-best-practices/references/component-transition.md +125 -0
  135. package/skills/vue-best-practices/references/composables.md +290 -0
  136. package/skills/vue-best-practices/references/directives.md +162 -0
  137. package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
  138. package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
  139. package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
  140. package/skills/vue-best-practices/references/plugins.md +166 -0
  141. package/skills/vue-best-practices/references/reactivity.md +344 -0
  142. package/skills/vue-best-practices/references/render-functions.md +201 -0
  143. package/skills/vue-best-practices/references/sfc.md +310 -0
  144. package/skills/vue-best-practices/references/state-management.md +135 -0
  145. package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
  146. package/skills/vue-pinia-best-practices/SKILL.md +21 -0
  147. package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
  148. package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
  149. package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
  150. package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
  151. package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
  152. package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
  153. package/skills/zig-0.16/SKILL.md +840 -0
  154. package/skills/zig-0.16/scripts/check-zig-version.sh +21 -0
  155. package/src/cli/analyses.ts +280 -0
  156. package/src/cli/index.ts +108 -0
  157. package/src/cli/serve.ts +192 -0
  158. package/src/cli/smoke.ts +131 -0
  159. package/src/cli/status.test.ts +204 -0
  160. package/src/cli/status.ts +263 -0
  161. package/src/cli/vacuum.test.ts +82 -0
  162. package/src/cli/vacuum.ts +96 -0
  163. package/src/config/schema.test.ts +88 -0
  164. package/src/config/schema.ts +64 -0
  165. package/src/db/analyses-migration.test.ts +210 -0
  166. package/src/db/analyses.test.ts +466 -0
  167. package/src/db/analyses.ts +375 -0
  168. package/src/db/auto-checkpoint.ts +131 -0
  169. package/src/db/client.test.ts +129 -0
  170. package/src/db/client.ts +55 -0
  171. package/src/db/fts-escape.ts +20 -0
  172. package/src/db/incidents.test.ts +201 -0
  173. package/src/db/incidents.ts +93 -0
  174. package/src/db/index.ts +86 -0
  175. package/src/db/migrations-v13.test.ts +141 -0
  176. package/src/db/migrations-v8.test.ts +301 -0
  177. package/src/db/migrations.ts +147 -0
  178. package/src/db/plan-archive.test.ts +180 -0
  179. package/src/db/plan-archive.ts +274 -0
  180. package/src/db/plan-create.test.ts +276 -0
  181. package/src/db/plan-create.ts +78 -0
  182. package/src/db/plan-files.test.ts +289 -0
  183. package/src/db/plan-update-status.ts +287 -0
  184. package/src/db/plans.test.ts +490 -0
  185. package/src/db/plans.ts +534 -0
  186. package/src/db/resolve-project-dir.test.ts +143 -0
  187. package/src/db/resolve-project-dir.ts +75 -0
  188. package/src/db/rollbacks.test.ts +150 -0
  189. package/src/db/rollbacks.ts +67 -0
  190. package/src/db/schema.ts +907 -0
  191. package/src/db/sessions.test.ts +80 -0
  192. package/src/db/sessions.ts +135 -0
  193. package/src/db/shutdown.test.ts +147 -0
  194. package/src/db/shutdown.ts +45 -0
  195. package/src/db/tasks.test.ts +921 -0
  196. package/src/db/tasks.ts +747 -0
  197. package/src/db/types.ts +619 -0
  198. package/src/http/__tests__/auth.test.ts +196 -0
  199. package/src/http/__tests__/routes.test.ts +465 -0
  200. package/src/http/__tests__/sse.test.ts +317 -0
  201. package/src/http/auth.ts +72 -0
  202. package/src/http/middleware/cors.ts +53 -0
  203. package/src/http/middleware/security-headers.ts +21 -0
  204. package/src/http/routes/events.ts +112 -0
  205. package/src/http/routes/health.ts +51 -0
  206. package/src/http/routes/plans.ts +66 -0
  207. package/src/http/routes/sessions.ts +50 -0
  208. package/src/http/routes/tasks.ts +60 -0
  209. package/src/http/server.ts +95 -0
  210. package/src/http/sse.ts +116 -0
  211. package/src/index.ts +37 -0
  212. package/src/lib.ts +65 -0
  213. package/src/mem/scoped.ts +65 -0
  214. package/src/orchestrator/background.test.ts +268 -0
  215. package/src/orchestrator/background.ts +293 -0
  216. package/src/orchestrator/memory-hook.ts +182 -0
  217. package/src/orchestrator/reconciler.ts +123 -0
  218. package/src/orchestrator/scheduler.test.ts +300 -0
  219. package/src/orchestrator/scheduler.ts +243 -0
  220. package/src/plugin.test.ts +2574 -0
  221. package/src/plugin.ts +1690 -0
  222. package/src/sdk/client.ts +66 -0
  223. package/src/worktrees/manager.ts +236 -0
  224. package/src/worktrees/state.ts +87 -0
  225. package/tests/integration/ranger-flow.test.ts +257 -0
  226. package/tools/analysis_archive.ts +28 -0
  227. package/tools/analysis_create.ts +55 -0
  228. package/tools/analysis_get.ts +33 -0
  229. package/tools/analysis_link_plan.ts +44 -0
  230. package/tools/analysis_list.ts +48 -0
  231. package/tools/analysis_search.ts +36 -0
  232. package/tools/analysis_update.ts +44 -0
  233. package/tools/plan_approve.ts +31 -0
  234. package/tools/plan_create.ts +58 -0
  235. package/tools/plan_get.ts +40 -0
  236. package/tools/plan_list.ts +37 -0
  237. package/tools/plan_search.ts +34 -0
  238. package/tools/plan_update_status.ts +71 -0
  239. package/tools/session_checkpoint.ts +31 -0
  240. package/tools/session_end.ts +26 -0
  241. package/tools/session_start.ts +43 -0
  242. package/tools/task_create_batch.ts +70 -0
  243. package/tools/task_list.ts +35 -0
  244. package/tools/task_next_for_agent.ts +30 -0
  245. package/tools/task_search.ts +34 -0
  246. package/tools/task_update_status.ts +37 -0
  247. package/tsconfig.json +31 -0
@@ -0,0 +1,417 @@
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);
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env bash
2
+ # ndomo Phase 1 HTTP server smoke test.
3
+ #
4
+ # Boots the Elysia HTTP server, runs a battery of curl assertions against
5
+ # REST + SSE endpoints, verifies security headers, then kills the server.
6
+ #
7
+ # Usage:
8
+ # bash scripts/smoke-http.sh
9
+ #
10
+ # Env overrides:
11
+ # SMOKE_PORT TCP port for the server (default: 4097)
12
+ # SMOKE_PASSWORD HTTP Basic password (default: smoke-test-password)
13
+ # SMOKE_TIMEOUT Health-check wait in seconds (default: 10)
14
+ #
15
+ # Exit 0 on all assertions pass, exit 1 on any failure.
16
+ set -euo pipefail
17
+
18
+ # ─── Banner ──────────────────────────────────────────────────────────────────
19
+ echo "┌──────────────────────────────────────┐"
20
+ echo "│ Phase 1 HTTP server smoke test │"
21
+ echo "└──────────────────────────────────────┘"
22
+
23
+ # ─── Resolve project root (this script's parent dir) ─────────────────────────
24
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25
+ PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
26
+ cd "${PROJECT_ROOT}"
27
+
28
+ # ─── Pre-flight checks ───────────────────────────────────────────────────────
29
+ command -v bun >/dev/null 2>&1 || {
30
+ echo "[FAIL] bun not found in PATH — install from https://bun.sh" >&2
31
+ exit 1
32
+ }
33
+ command -v curl >/dev/null 2>&1 || {
34
+ echo "[FAIL] curl not found in PATH" >&2
35
+ exit 1
36
+ }
37
+
38
+ # ─── Config ──────────────────────────────────────────────────────────────────
39
+ SMOKE_PORT="${SMOKE_PORT:-4097}"
40
+ SMOKE_PASSWORD="${SMOKE_PASSWORD:-smoke-test-password}"
41
+ SMOKE_TIMEOUT="${SMOKE_TIMEOUT:-10}"
42
+
43
+ # Validate port range
44
+ if ! [[ "${SMOKE_PORT}" =~ ^[0-9]+$ ]] || [ "${SMOKE_PORT}" -lt 1 ] || [ "${SMOKE_PORT}" -gt 65535 ]; then
45
+ echo "[FAIL] invalid SMOKE_PORT: ${SMOKE_PORT}" >&2
46
+ exit 1
47
+ fi
48
+
49
+ # ─── Port fallback helper ────────────────────────────────────────────────────
50
+ # If SMOKE_PORT is in use, fall back to the next free port up to +10.
51
+ find_free_port() {
52
+ local start="$1"
53
+ for offset in $(seq 0 10); do
54
+ local candidate=$((start + offset))
55
+ if ! ss -lnt 2>/dev/null | awk '{print $4}' | grep -qE "(^|:)${candidate}$"; then
56
+ echo "${candidate}"
57
+ return 0
58
+ fi
59
+ done
60
+ return 1
61
+ }
62
+
63
+ PORT="$(find_free_port "${SMOKE_PORT}")" || {
64
+ echo "[FAIL] no free port in range ${SMOKE_PORT}-$((SMOKE_PORT + 10))" >&2
65
+ exit 1
66
+ }
67
+ if [ "${PORT}" != "${SMOKE_PORT}" ]; then
68
+ echo "[info] SMOKE_PORT ${SMOKE_PORT} busy — falling back to ${PORT}"
69
+ fi
70
+
71
+ # ─── Bootstrap .ndomo/state.db if missing ────────────────────────────────────
72
+ NDOMO_DB="${PROJECT_ROOT}/.ndomo/state.db"
73
+ if [ ! -f "${NDOMO_DB}" ]; then
74
+ echo "[setup] bootstrapping .ndomo/state.db..."
75
+ bun -e '
76
+ import { Database } from "bun:sqlite";
77
+ import { mkdirSync } from "node:fs";
78
+ import { join } from "node:path";
79
+ const dir = join(process.cwd(), ".ndomo");
80
+ mkdirSync(dir, { recursive: true });
81
+ const db = new Database(join(dir, "state.db"), { create: true });
82
+ db.exec("PRAGMA foreign_keys = ON");
83
+ db.exec("PRAGMA auto_vacuum = INCREMENTAL");
84
+ db.exec("PRAGMA journal_mode = WAL");
85
+ db.exec("PRAGMA synchronous = NORMAL");
86
+ const { runMigrations } = await import("./src/db/migrations.ts");
87
+ runMigrations(db);
88
+ db.close();
89
+ console.log("[setup] db initialized");
90
+ ' || {
91
+ echo "[FAIL] failed to bootstrap .ndomo/state.db" >&2
92
+ exit 1
93
+ }
94
+ fi
95
+
96
+ # ─── Export server env ───────────────────────────────────────────────────────
97
+ export NDOMO_HTTP_ENABLED=true
98
+ export NDOMO_HTTP_PORT="${PORT}"
99
+ export NDOMO_HTTP_AUTH_REQUIRED=true
100
+ export NDOMO_HTTP_CORS_ORIGINS='*'
101
+ export OPENCODE_SERVER_PASSWORD="${SMOKE_PASSWORD}"
102
+ export OPENCODE_SERVER_URL="${OPENCODE_SERVER_URL:-http://localhost:4096}"
103
+
104
+ # ─── Start server in background ─────────────────────────────────────────────
105
+ LOG="$(mktemp -t smoke-http-XXXXXX.log)"
106
+ echo "[setup] starting server on port ${PORT} (log: ${LOG})"
107
+
108
+ # Use --cors to ensure wildcard; --force is NOT needed because env enables it.
109
+ bun run src/cli/serve.ts --port "${PORT}" --cors '*' >"${LOG}" 2>&1 &
110
+ SERVER_PID=$!
111
+
112
+ # Register cleanup trap — runs even on assertion failure
113
+ cleanup() {
114
+ local exit_code=$?
115
+ if kill -0 "${SERVER_PID}" 2>/dev/null; then
116
+ echo "[cleanup] killing server pid ${SERVER_PID}"
117
+ kill "${SERVER_PID}" 2>/dev/null || true
118
+ # Wait up to 3s for graceful shutdown
119
+ for _ in 1 2 3 4 5 6; do
120
+ kill -0 "${SERVER_PID}" 2>/dev/null || break
121
+ sleep 0.5
122
+ done
123
+ # Hard kill if still alive
124
+ kill -9 "${SERVER_PID}" 2>/dev/null || true
125
+ wait "${SERVER_PID}" 2>/dev/null || true
126
+ fi
127
+ rm -f "${LOG}"
128
+ exit "${exit_code}"
129
+ }
130
+ trap cleanup EXIT INT TERM
131
+
132
+ # ─── Wait for /health (up to SMOKE_TIMEOUT seconds, 200ms backoff) ────────────
133
+ echo "[wait] polling /health (timeout ${SMOKE_TIMEOUT}s)..."
134
+ ready=false
135
+ deadline=$(( $(date +%s) + SMOKE_TIMEOUT ))
136
+ while [ "$(date +%s)" -lt "${deadline}" ]; do
137
+ if ! kill -0 "${SERVER_PID}" 2>/dev/null; then
138
+ echo "[FAIL] server died before becoming ready. Log:"
139
+ cat "${LOG}" >&2
140
+ exit 1
141
+ fi
142
+ if curl -fsS -o /dev/null "localhost:${PORT}/health" 2>/dev/null; then
143
+ ready=true
144
+ break
145
+ fi
146
+ sleep 0.2
147
+ done
148
+ if [ "${ready}" != "true" ]; then
149
+ echo "[FAIL] /health did not respond within ${SMOKE_TIMEOUT}s. Log:"
150
+ cat "${LOG}" >&2
151
+ exit 1
152
+ fi
153
+ echo "[ready] server up on port ${PORT}"
154
+
155
+ # ─── Assertion harness ───────────────────────────────────────────────────────
156
+ PASS=0
157
+ FAIL=0
158
+ FAILED_NAMES=()
159
+
160
+ assert() {
161
+ local name="$1"
162
+ local cmd="$2"
163
+ if eval "${cmd}" >/dev/null 2>&1; then
164
+ echo "[PASS] ${name}"
165
+ PASS=$((PASS + 1))
166
+ else
167
+ echo "[FAIL] ${name}"
168
+ FAIL=$((FAIL + 1))
169
+ FAILED_NAMES+=("${name}")
170
+ fi
171
+ }
172
+
173
+ assert "health 200 + status=ok" \
174
+ "[ \"\$(curl -fsS -o /tmp/smoke-health.json -w '%{http_code}' localhost:${PORT}/health)\" = '200' ] && [ \"\$(grep -o '\"status\":\"[^\"]*\"' /tmp/smoke-health.json | head -1 | cut -d'\"' -f4)\" = 'ok' ]"
175
+
176
+ assert "auth required: no creds → 401 + WWW-Authenticate" \
177
+ "[ \"\$(curl -s -o /dev/null -w '%{http_code}' localhost:${PORT}/api/plans)\" = '401' ] && [ -n \"\$(curl -s -i localhost:${PORT}/api/plans | grep -i '^www-authenticate:')\" ]"
178
+
179
+ assert "auth OK: /api/plans → 200 + JSON array" \
180
+ "[ \"\$(curl -fsS -o /tmp/smoke-plans.json -w '%{http_code}' -u \"user:${SMOKE_PASSWORD}\" localhost:${PORT}/api/plans)\" = '200' ] && head -c 1 /tmp/smoke-plans.json | grep -q '\\['"
181
+
182
+ assert "auth OK: /api/sessions/active → 200" \
183
+ "[ \"\$(curl -fsS -o /tmp/smoke-sessions.json -w '%{http_code}' -u \"user:${SMOKE_PASSWORD}\" localhost:${PORT}/api/sessions/active)\" = '200' ]"
184
+
185
+ assert "CORS preflight OPTIONS /api/plans → 204 + ACAO" \
186
+ "[ \"\$(curl -s -o /dev/null -w '%{http_code}' -X OPTIONS -H 'Origin: http://localhost' -H 'Access-Control-Request-Method: GET' localhost:${PORT}/api/plans)\" = '204' ] && [ -n \"\$(curl -s -i -X OPTIONS -H 'Origin: http://localhost' -H 'Access-Control-Request-Method: GET' localhost:${PORT}/api/plans | grep -i '^access-control-allow-origin:')\" ]"
187
+
188
+ # ─── Security headers (≥ 3 of the canonical set must be present) ────────────
189
+ HDRS_RAW="$(curl -s -i -u "user:${SMOKE_PASSWORD}" localhost:${PORT}/api/sessions/active || true)"
190
+ SEC_HITS=0
191
+ for h in "Strict-Transport-Security" "X-Content-Type-Options" "X-Frame-Options" "Content-Security-Policy" "Referrer-Policy"; do
192
+ if printf "%s" "${HDRS_RAW}" | grep -qi "^${h}:"; then
193
+ SEC_HITS=$((SEC_HITS + 1))
194
+ fi
195
+ done
196
+ assert "security headers ≥ 3 present (got ${SEC_HITS})" \
197
+ "[ ${SEC_HITS} -ge 3 ]"
198
+
199
+ # ─── SSE endpoint behavior ───────────────────────────────────────────────────
200
+ # Two valid outcomes:
201
+ # A) OpenCode SDK up → 200 + Content-Type: text/event-stream (real SSE stream)
202
+ # B) OpenCode SDK down → 503 + JSON body { error: "sdk_unavailable" } (graceful fallback)
203
+ # The test passes if EITHER outcome occurs. Both indicate the SSE route is
204
+ # correctly wired (events.ts returns 503 when sdkClient is null).
205
+ SSE_HDRS="$(curl -s -i -N --max-time 2 -u "user:${SMOKE_PASSWORD}" "localhost:${PORT}/api/events" 2>/dev/null || true)"
206
+ SSE_BODY="$(printf '%s' "${SSE_HDRS}" | awk 'BEGIN{p=0} /^\r?$/{p=1; next} p{print}')"
207
+ SSE_STATUS="$(printf '%s' "${SSE_HDRS}" | head -1 | awk '{print $2}')"
208
+ if printf '%s' "${SSE_HDRS}" | grep -qi '^content-type:[[:space:]]*text/event-stream'; then
209
+ assert "SSE: 200 + text/event-stream (SDK up)" "true"
210
+ elif [ "${SSE_STATUS}" = "503" ] && printf '%s' "${SSE_BODY}" | grep -q '"sdk_unavailable"'; then
211
+ assert "SSE: 503 + sdk_unavailable (SDK down — graceful fallback)" "true"
212
+ else
213
+ assert "SSE: 200/503 with correct Content-Type or sdk_unavailable body" "false"
214
+ fi
215
+
216
+ # ─── Report ──────────────────────────────────────────────────────────────────
217
+ echo ""
218
+ echo "────────────────────────────────────────"
219
+ echo "PASS: ${PASS}/${PASS}+${FAIL}"
220
+ if [ "${FAIL}" -gt 0 ]; then
221
+ echo "FAILED assertions:"
222
+ for name in "${FAILED_NAMES[@]}"; do
223
+ echo " - ${name}"
224
+ done
225
+ exit 1
226
+ fi
227
+ echo "PASS: ${PASS}/${PASS}"
228
+ exit 0