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.
- package/.bun-version +1 -0
- package/.dockerignore +79 -0
- package/.editorconfig +18 -0
- package/.env.example +19 -0
- package/.github/CODEOWNERS +8 -0
- package/.github/ISSUE_TEMPLATE/bug_report.yml +62 -0
- package/.github/ISSUE_TEMPLATE/config.yml +2 -0
- package/.github/ISSUE_TEMPLATE/feature_request.yml +34 -0
- package/.github/dependabot.yml +36 -0
- package/.github/pull_request_template.md +24 -0
- package/.github/release.yml +30 -0
- package/.github/workflows/gitleaks.yml +28 -0
- package/.github/workflows/release-please.yml +27 -0
- package/.github/workflows/smoke.yml +29 -0
- package/.husky/commit-msg +1 -0
- package/CHANGELOG.md +114 -0
- package/Dockerfile +32 -0
- package/README.es.md +174 -0
- package/README.md +187 -0
- package/agents/chronicler.md +98 -0
- package/agents/ci-smith.md +136 -0
- package/agents/craftsman.md +341 -0
- package/agents/deploy-smith.md +138 -0
- package/agents/foreman.md +377 -0
- package/agents/go-smith.md +164 -0
- package/agents/guild.md +188 -0
- package/agents/inspector.md +83 -0
- package/agents/js-smith.md +127 -0
- package/agents/ops-scout.md +173 -0
- package/agents/painter.md +200 -0
- package/agents/python-smith.md +120 -0
- package/agents/ranger.md +307 -0
- package/agents/release-smith.md +165 -0
- package/agents/rust-smith.md +159 -0
- package/agents/sage.md +178 -0
- package/agents/scout.md +144 -0
- package/agents/scribe.md +156 -0
- package/agents/smith.md +201 -0
- package/agents/vue-smith.md +155 -0
- package/agents/warden.md +216 -0
- package/agents/zig-smith.md +156 -0
- package/bin/ndomo-analyses.ts +4 -0
- package/bin/ndomo-status.ts +4 -0
- package/biome.json +57 -0
- package/bun.lock +514 -0
- package/commitlint.config.js +3 -0
- package/config/ndomo.config.json +258 -0
- package/config/ndomo.schema.json +166 -0
- package/docs/agents.md +375 -0
- package/docs/bugs/plan-create-orphan-fk.md +131 -0
- package/docs/bugs/task_create_batch-order-index-collision.md +158 -0
- package/docs/configuration.md +276 -0
- package/docs/database.md +364 -0
- package/docs/features/feature-flexible-builder-v1.md +724 -0
- package/docs/features/feature-flexible-builder-v2.md +882 -0
- package/docs/features/feature-flexible-builder.md +974 -0
- package/docs/http-server.md +244 -0
- package/docs/installation.md +259 -0
- package/docs/integrations.md +129 -0
- package/docs/operations/anti-pattern-sub-agent-verify-2026-06-21.md +32 -0
- package/docs/operations/audit-v1.md +417 -0
- package/docs/operations/audit-v2.md +197 -0
- package/docs/operations/audit-v3.md +306 -0
- package/docs/operations/db-optimize-foundations.md +123 -0
- package/docs/operations/verify-gate-architecture.md +82 -0
- package/docs/workflows.md +448 -0
- package/opencode.json +5 -0
- package/package.json +65 -0
- package/release-please-config.json +11 -0
- package/scripts/dev-bust-cache.sh +164 -0
- package/scripts/install.sh +688 -0
- package/scripts/smoke-e2e.ts +704 -0
- package/scripts/smoke-hot.ts +417 -0
- package/scripts/smoke-http.sh +228 -0
- package/scripts/smoke-v4.ts +256 -0
- package/scripts/smoke-v5.ts +397 -0
- package/scripts/smoke.sh +9 -0
- package/scripts/uninstall.sh +224 -0
- package/skills/api-security-best-practices/SKILL.md +915 -0
- package/skills/bash-scripting/SKILL.md +201 -0
- package/skills/bun/SKILL.md +313 -0
- package/skills/cavecrew/SKILL.md +82 -0
- package/skills/caveman/SKILL.md +74 -0
- package/skills/caveman-review/README.md +33 -0
- package/skills/caveman-review/SKILL.md +55 -0
- package/skills/find-skills/SKILL.md +142 -0
- package/skills/frontend-design/LICENSE.txt +177 -0
- package/skills/frontend-design/SKILL.md +55 -0
- package/skills/golang-patterns/SKILL.md +674 -0
- package/skills/golang-security/SKILL.md +185 -0
- package/skills/golang-security/evals/evals.json +595 -0
- package/skills/golang-security/references/architecture.md +268 -0
- package/skills/golang-security/references/checklist.md +80 -0
- package/skills/golang-security/references/cookies.md +200 -0
- package/skills/golang-security/references/cryptography.md +424 -0
- package/skills/golang-security/references/filesystem.md +285 -0
- package/skills/golang-security/references/injection.md +315 -0
- package/skills/golang-security/references/logging.md +163 -0
- package/skills/golang-security/references/memory-safety.md +241 -0
- package/skills/golang-security/references/network.md +253 -0
- package/skills/golang-security/references/secrets.md +189 -0
- package/skills/golang-security/references/third-party.md +159 -0
- package/skills/golang-security/references/threat-modeling.md +189 -0
- package/skills/golang-testing/SKILL.md +720 -0
- package/skills/grill-me/SKILL.md +7 -0
- package/skills/javascript-testing-patterns/SKILL.md +537 -0
- package/skills/javascript-testing-patterns/references/advanced-testing-patterns.md +513 -0
- package/skills/modern-javascript-patterns/SKILL.md +43 -0
- package/skills/modern-javascript-patterns/references/advanced-patterns.md +487 -0
- package/skills/modern-javascript-patterns/references/details.md +457 -0
- package/skills/python-anti-patterns/SKILL.md +349 -0
- package/skills/python-design-patterns/SKILL.md +85 -0
- package/skills/python-design-patterns/references/details.md +353 -0
- package/skills/python-error-handling/SKILL.md +193 -0
- package/skills/python-error-handling/references/details.md +171 -0
- package/skills/python-testing-patterns/SKILL.md +278 -0
- package/skills/python-testing-patterns/references/advanced-patterns.md +411 -0
- package/skills/python-testing-patterns/references/details.md +349 -0
- package/skills/rust-patterns/SKILL.md +500 -0
- package/skills/rust-testing/SKILL.md +501 -0
- package/skills/security-review/SKILL.md +504 -0
- package/skills/security-review/cloud-infrastructure-security.md +361 -0
- package/skills/vue-best-practices/SKILL.md +154 -0
- package/skills/vue-best-practices/references/animation-class-based-technique.md +254 -0
- package/skills/vue-best-practices/references/animation-state-driven-technique.md +291 -0
- package/skills/vue-best-practices/references/component-async.md +97 -0
- package/skills/vue-best-practices/references/component-data-flow.md +307 -0
- package/skills/vue-best-practices/references/component-fallthrough-attrs.md +174 -0
- package/skills/vue-best-practices/references/component-keep-alive.md +137 -0
- package/skills/vue-best-practices/references/component-slots.md +216 -0
- package/skills/vue-best-practices/references/component-suspense.md +228 -0
- package/skills/vue-best-practices/references/component-teleport.md +108 -0
- package/skills/vue-best-practices/references/component-transition-group.md +128 -0
- package/skills/vue-best-practices/references/component-transition.md +125 -0
- package/skills/vue-best-practices/references/composables.md +290 -0
- package/skills/vue-best-practices/references/directives.md +162 -0
- package/skills/vue-best-practices/references/perf-avoid-component-abstraction-in-lists.md +159 -0
- package/skills/vue-best-practices/references/perf-v-once-v-memo-directives.md +182 -0
- package/skills/vue-best-practices/references/perf-virtualize-large-lists.md +187 -0
- package/skills/vue-best-practices/references/plugins.md +166 -0
- package/skills/vue-best-practices/references/reactivity.md +344 -0
- package/skills/vue-best-practices/references/render-functions.md +201 -0
- package/skills/vue-best-practices/references/sfc.md +310 -0
- package/skills/vue-best-practices/references/state-management.md +135 -0
- package/skills/vue-best-practices/references/updated-hook-performance.md +187 -0
- package/skills/vue-pinia-best-practices/SKILL.md +21 -0
- package/skills/vue-pinia-best-practices/reference/pinia-no-active-pinia-error.md +248 -0
- package/skills/vue-pinia-best-practices/reference/pinia-setup-store-return-all-state.md +227 -0
- package/skills/vue-pinia-best-practices/reference/pinia-store-destructuring-breaks-reactivity.md +193 -0
- package/skills/vue-pinia-best-practices/reference/state-url-for-ephemeral-filters.md +238 -0
- package/skills/vue-pinia-best-practices/reference/state-use-pinia-for-large-apps.md +262 -0
- package/skills/vue-pinia-best-practices/reference/store-method-binding-parentheses.md +191 -0
- package/skills/zig-0.16/SKILL.md +840 -0
- package/skills/zig-0.16/scripts/check-zig-version.sh +21 -0
- package/src/cli/analyses.ts +280 -0
- package/src/cli/index.ts +108 -0
- package/src/cli/serve.ts +192 -0
- package/src/cli/smoke.ts +131 -0
- package/src/cli/status.test.ts +204 -0
- package/src/cli/status.ts +263 -0
- package/src/cli/vacuum.test.ts +82 -0
- package/src/cli/vacuum.ts +96 -0
- package/src/config/schema.test.ts +88 -0
- package/src/config/schema.ts +64 -0
- package/src/db/analyses-migration.test.ts +210 -0
- package/src/db/analyses.test.ts +466 -0
- package/src/db/analyses.ts +375 -0
- package/src/db/auto-checkpoint.ts +131 -0
- package/src/db/client.test.ts +129 -0
- package/src/db/client.ts +55 -0
- package/src/db/fts-escape.ts +20 -0
- package/src/db/incidents.test.ts +201 -0
- package/src/db/incidents.ts +93 -0
- package/src/db/index.ts +86 -0
- package/src/db/migrations-v13.test.ts +141 -0
- package/src/db/migrations-v8.test.ts +301 -0
- package/src/db/migrations.ts +147 -0
- package/src/db/plan-archive.test.ts +180 -0
- package/src/db/plan-archive.ts +274 -0
- package/src/db/plan-create.test.ts +276 -0
- package/src/db/plan-create.ts +78 -0
- package/src/db/plan-files.test.ts +289 -0
- package/src/db/plan-update-status.ts +287 -0
- package/src/db/plans.test.ts +490 -0
- package/src/db/plans.ts +534 -0
- package/src/db/resolve-project-dir.test.ts +143 -0
- package/src/db/resolve-project-dir.ts +75 -0
- package/src/db/rollbacks.test.ts +150 -0
- package/src/db/rollbacks.ts +67 -0
- package/src/db/schema.ts +907 -0
- package/src/db/sessions.test.ts +80 -0
- package/src/db/sessions.ts +135 -0
- package/src/db/shutdown.test.ts +147 -0
- package/src/db/shutdown.ts +45 -0
- package/src/db/tasks.test.ts +921 -0
- package/src/db/tasks.ts +747 -0
- package/src/db/types.ts +619 -0
- package/src/http/__tests__/auth.test.ts +196 -0
- package/src/http/__tests__/routes.test.ts +465 -0
- package/src/http/__tests__/sse.test.ts +317 -0
- package/src/http/auth.ts +72 -0
- package/src/http/middleware/cors.ts +53 -0
- package/src/http/middleware/security-headers.ts +21 -0
- package/src/http/routes/events.ts +112 -0
- package/src/http/routes/health.ts +51 -0
- package/src/http/routes/plans.ts +66 -0
- package/src/http/routes/sessions.ts +50 -0
- package/src/http/routes/tasks.ts +60 -0
- package/src/http/server.ts +95 -0
- package/src/http/sse.ts +116 -0
- package/src/index.ts +37 -0
- package/src/lib.ts +65 -0
- package/src/mem/scoped.ts +65 -0
- package/src/orchestrator/background.test.ts +268 -0
- package/src/orchestrator/background.ts +293 -0
- package/src/orchestrator/memory-hook.ts +182 -0
- package/src/orchestrator/reconciler.ts +123 -0
- package/src/orchestrator/scheduler.test.ts +300 -0
- package/src/orchestrator/scheduler.ts +243 -0
- package/src/plugin.test.ts +2574 -0
- package/src/plugin.ts +1690 -0
- package/src/sdk/client.ts +66 -0
- package/src/worktrees/manager.ts +236 -0
- package/src/worktrees/state.ts +87 -0
- package/tests/integration/ranger-flow.test.ts +257 -0
- package/tools/analysis_archive.ts +28 -0
- package/tools/analysis_create.ts +55 -0
- package/tools/analysis_get.ts +33 -0
- package/tools/analysis_link_plan.ts +44 -0
- package/tools/analysis_list.ts +48 -0
- package/tools/analysis_search.ts +36 -0
- package/tools/analysis_update.ts +44 -0
- package/tools/plan_approve.ts +31 -0
- package/tools/plan_create.ts +58 -0
- package/tools/plan_get.ts +40 -0
- package/tools/plan_list.ts +37 -0
- package/tools/plan_search.ts +34 -0
- package/tools/plan_update_status.ts +71 -0
- package/tools/session_checkpoint.ts +31 -0
- package/tools/session_end.ts +26 -0
- package/tools/session_start.ts +43 -0
- package/tools/task_create_batch.ts +70 -0
- package/tools/task_list.ts +35 -0
- package/tools/task_next_for_agent.ts +30 -0
- package/tools/task_search.ts +34 -0
- package/tools/task_update_status.ts +37 -0
- 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
|