ndomo 0.1.0 → 0.2.1
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/.env.example +4 -0
- package/README.es.md +29 -23
- package/README.md +64 -24
- package/bun.lock +447 -0
- package/docs/configuration.md +4 -4
- package/docs/installation.md +53 -34
- package/docs/installer.md +164 -0
- package/docs/integrations.md +1 -1
- package/docs/web-ui.md +124 -0
- package/package.json +43 -4
- package/scripts/install.sh +28 -0
- package/scripts/smoke-install.sh +47 -0
- package/scripts/smoke-web.sh +335 -0
- package/src/cli/__tests__/install.test.ts +733 -0
- package/src/cli/index.ts +8 -0
- package/src/cli/install.ts +1273 -0
- package/src/config/__tests__/schema.test.ts +223 -0
- package/src/config/schema.ts +129 -16
- package/src/http/__tests__/auth.test.ts +10 -10
- package/src/http/__tests__/spa.test.ts +296 -0
- package/src/http/auth.ts +8 -1
- package/src/http/server.ts +71 -2
- package/.bun-version +0 -1
- package/.dockerignore +0 -79
- package/.editorconfig +0 -18
- package/.github/CODEOWNERS +0 -8
- package/.github/ISSUE_TEMPLATE/bug_report.yml +0 -62
- package/.github/ISSUE_TEMPLATE/config.yml +0 -2
- package/.github/ISSUE_TEMPLATE/feature_request.yml +0 -34
- package/.github/dependabot.yml +0 -36
- package/.github/pull_request_template.md +0 -24
- package/.github/release.yml +0 -30
- package/.github/workflows/gitleaks.yml +0 -28
- package/.github/workflows/release-please.yml +0 -27
- package/.github/workflows/smoke.yml +0 -29
- package/.husky/commit-msg +0 -1
- package/CHANGELOG.md +0 -114
- package/Dockerfile +0 -32
- package/bin/ndomo-analyses.ts +0 -4
- package/bin/ndomo-status.ts +0 -4
- package/biome.json +0 -57
- package/commitlint.config.js +0 -3
- package/opencode.json +0 -5
- package/release-please-config.json +0 -11
- package/scripts/dev-bust-cache.sh +0 -164
- package/scripts/smoke-e2e.ts +0 -704
- package/scripts/smoke-hot.ts +0 -417
- package/scripts/smoke-v4.ts +0 -256
- package/scripts/smoke-v5.ts +0 -397
- package/scripts/uninstall.sh +0 -224
- package/src/index.ts +0 -37
- package/src/lib.ts +0 -65
- package/src/mem/scoped.ts +0 -65
- package/src/orchestrator/background.test.ts +0 -268
- package/src/orchestrator/background.ts +0 -293
- package/src/orchestrator/memory-hook.ts +0 -182
- package/src/orchestrator/reconciler.ts +0 -123
- package/src/orchestrator/scheduler.test.ts +0 -300
- package/src/orchestrator/scheduler.ts +0 -243
- package/src/plugin.test.ts +0 -2574
- package/src/plugin.ts +0 -1690
- package/src/worktrees/manager.ts +0 -236
- package/src/worktrees/state.ts +0 -87
- package/tests/integration/ranger-flow.test.ts +0 -257
- package/tsconfig.json +0 -31
package/scripts/smoke-e2e.ts
DELETED
|
@@ -1,704 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ndomo v5 E2E stress test — full plan lifecycle post-hotfix.
|
|
3
|
-
*
|
|
4
|
-
* Verifies FTS5 correctness (the critical v4→v5 bug), archive lifecycle,
|
|
5
|
-
* migration idempotency, and edge cases.
|
|
6
|
-
*
|
|
7
|
-
* Stack: TS + Bun + bun:sqlite. NO code modifications — read + execute only.
|
|
8
|
-
*
|
|
9
|
-
* Usage: bun run scripts/smoke-e2e.ts
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { Database } from "bun:sqlite";
|
|
13
|
-
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
14
|
-
import { tmpdir } from "node:os";
|
|
15
|
-
import { join } from "node:path";
|
|
16
|
-
import { runMigrations } from "../src/db/migrations.ts";
|
|
17
|
-
import { archivePlan } from "../src/db/plan-archive.ts";
|
|
18
|
-
import {
|
|
19
|
-
addPlanTag,
|
|
20
|
-
approvePlan,
|
|
21
|
-
createPlan,
|
|
22
|
-
findPlansByCategory,
|
|
23
|
-
findPlansByTag,
|
|
24
|
-
getPlan,
|
|
25
|
-
getPlanProgress,
|
|
26
|
-
listPlans,
|
|
27
|
-
searchPlans,
|
|
28
|
-
updatePlanStatus,
|
|
29
|
-
} from "../src/db/plans.ts";
|
|
30
|
-
import { checkpointSession, startSession } from "../src/db/sessions.ts";
|
|
31
|
-
import {
|
|
32
|
-
createTasksBatch,
|
|
33
|
-
nextTaskForAgent,
|
|
34
|
-
searchTasks,
|
|
35
|
-
updateTaskStatus,
|
|
36
|
-
} from "../src/db/tasks.ts";
|
|
37
|
-
import type { Plan, PlanCategory } from "../src/db/types.ts";
|
|
38
|
-
|
|
39
|
-
// ── Test harness ─────────────────────────────────────────────────────────────
|
|
40
|
-
|
|
41
|
-
let pass = 0;
|
|
42
|
-
let fail = 0;
|
|
43
|
-
const failures: Array<{ test: string; msg: string }> = [];
|
|
44
|
-
|
|
45
|
-
function assert(cond: boolean, msg: string): void {
|
|
46
|
-
if (cond) {
|
|
47
|
-
pass++;
|
|
48
|
-
console.log(` ✓ ${msg}`);
|
|
49
|
-
} else {
|
|
50
|
-
fail++;
|
|
51
|
-
failures.push({ test: "current", msg });
|
|
52
|
-
console.error(` ✗ FAIL: ${msg}`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function assertThrows(fn: () => void, expectedMsg: string, msg: string): void {
|
|
57
|
-
try {
|
|
58
|
-
fn();
|
|
59
|
-
fail++;
|
|
60
|
-
failures.push({ test: "current", msg: `${msg} (expected error, got none)` });
|
|
61
|
-
console.error(` ✗ FAIL: ${msg} (expected error, got none)`);
|
|
62
|
-
} catch (err) {
|
|
63
|
-
const errMsg = err instanceof Error ? err.message : String(err);
|
|
64
|
-
if (errMsg.includes(expectedMsg)) {
|
|
65
|
-
pass++;
|
|
66
|
-
console.log(` ✓ ${msg}`);
|
|
67
|
-
} else {
|
|
68
|
-
fail++;
|
|
69
|
-
failures.push({
|
|
70
|
-
test: "current",
|
|
71
|
-
msg: `${msg} (expected '${expectedMsg}', got '${errMsg}')`,
|
|
72
|
-
});
|
|
73
|
-
console.error(` ✗ FAIL: ${msg} (expected '${expectedMsg}', got '${errMsg}')`);
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function section(name: string): void {
|
|
79
|
-
console.log(`\n${"─".repeat(60)}`);
|
|
80
|
-
console.log(` ${name}`);
|
|
81
|
-
console.log(`${"─".repeat(60)}`);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// ── Defaults ─────────────────────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
const PLAN_DEFAULTS = {
|
|
87
|
-
status: "draft" as const,
|
|
88
|
-
priority: 3,
|
|
89
|
-
overview: "smoke test plan overview for validation",
|
|
90
|
-
complexity: 3,
|
|
91
|
-
createdBy: "smoke-e2e",
|
|
92
|
-
updatedBy: "smoke-e2e",
|
|
93
|
-
metadata: {},
|
|
94
|
-
approvedAt: null,
|
|
95
|
-
completedAt: null,
|
|
96
|
-
sessionId: null,
|
|
97
|
-
approach: "Test approach for E2E smoke validation",
|
|
98
|
-
sourceSessionId: null,
|
|
99
|
-
sourceMessageId: null,
|
|
100
|
-
category: null as Plan["category"],
|
|
101
|
-
archivedAt: null,
|
|
102
|
-
};
|
|
103
|
-
|
|
104
|
-
const TASK_DEFAULTS = {
|
|
105
|
-
agent: "smith",
|
|
106
|
-
files: [] as string[],
|
|
107
|
-
complexity: 3,
|
|
108
|
-
createdBy: "smoke-e2e",
|
|
109
|
-
updatedBy: "smoke-e2e",
|
|
110
|
-
sourceSessionId: null,
|
|
111
|
-
sourceMessageId: null,
|
|
112
|
-
reviewedBy: null,
|
|
113
|
-
tokensUsed: null,
|
|
114
|
-
durationMs: null,
|
|
115
|
-
artifacts: [] as string[],
|
|
116
|
-
dependencies: [] as string[],
|
|
117
|
-
metadata: {},
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
function mkPlan(
|
|
121
|
-
id: string,
|
|
122
|
-
slug: string,
|
|
123
|
-
title: string,
|
|
124
|
-
overrides?: Partial<typeof PLAN_DEFAULTS>,
|
|
125
|
-
) {
|
|
126
|
-
return createPlan(db, { ...PLAN_DEFAULTS, id, slug, title, ...overrides });
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
130
|
-
|
|
131
|
-
const db = new Database(":memory:");
|
|
132
|
-
db.exec("PRAGMA journal_mode = WAL");
|
|
133
|
-
db.exec("PRAGMA foreign_keys = ON");
|
|
134
|
-
|
|
135
|
-
const testMemDir = join(tmpdir(), `ndomo-smoke-e2e-${Date.now()}`);
|
|
136
|
-
mkdirSync(testMemDir, { recursive: true });
|
|
137
|
-
|
|
138
|
-
console.log("ndomo v5 E2E stress test\n");
|
|
139
|
-
|
|
140
|
-
// ── Run migrations v1→v5 ────────────────────────────────────────────────────
|
|
141
|
-
console.log("Running migrations v1→v5...");
|
|
142
|
-
runMigrations(db);
|
|
143
|
-
const ver = db.query("SELECT MAX(version) as v FROM schema_version").get() as { v: number };
|
|
144
|
-
assert(ver.v === 5, `schema_version = 5 (got ${ver.v})`);
|
|
145
|
-
|
|
146
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
147
|
-
// TEST 1: FTS5 positivo (CRÍTICO — antes broken por content='')
|
|
148
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
149
|
-
section("Test 1: FTS5 positivo — searchPlans con keywords diferentes");
|
|
150
|
-
|
|
151
|
-
mkPlan("fts-auth", "fts-auth", "Authentication bug in login flow");
|
|
152
|
-
mkPlan("fts-authz", "fts-authz", "Authorization refactor for role-based access");
|
|
153
|
-
mkPlan("fts-perf", "fts-perf", "Performance optimization for database queries");
|
|
154
|
-
|
|
155
|
-
const r1 = searchPlans(db, "authentication");
|
|
156
|
-
assert(r1.length === 1, `searchPlans("authentication") → 1 result (got ${r1.length})`);
|
|
157
|
-
assert(r1[0]?.id === "fts-auth", `searchPlans("authentication") → fts-auth (got ${r1[0]?.id})`);
|
|
158
|
-
|
|
159
|
-
const r2 = searchPlans(db, "access");
|
|
160
|
-
assert(r2.length === 1, `searchPlans("access") → 1 result (got ${r2.length})`);
|
|
161
|
-
assert(r2[0]?.id === "fts-authz", `searchPlans("access") → fts-authz (got ${r2[0]?.id})`);
|
|
162
|
-
|
|
163
|
-
const r3 = searchPlans(db, "optimization");
|
|
164
|
-
assert(r3.length === 1, `searchPlans("optimization") → 1 result (got ${r3.length})`);
|
|
165
|
-
assert(r3[0]?.id === "fts-perf", `searchPlans("optimization") → fts-perf (got ${r3[0]?.id})`);
|
|
166
|
-
|
|
167
|
-
// BUG-1 FIX: FTS5 MATCH no longer throws on hyphens — escapeFtsQuery wraps input in "..."
|
|
168
|
-
// searchPlans("nonexistent-xyz") should return 0 results, NOT throw SQLiteError
|
|
169
|
-
const rHyphen = searchPlans(db, "nonexistent-xyz");
|
|
170
|
-
assert(
|
|
171
|
-
rHyphen.length === 0,
|
|
172
|
-
`searchPlans("nonexistent-xyz") → [] (no throw, hyphen safe) (got ${rHyphen.length})`,
|
|
173
|
-
);
|
|
174
|
-
|
|
175
|
-
// Use safe search term (no hyphens) for the actual empty-results assertion
|
|
176
|
-
const rEmpty = searchPlans(db, "nonexistentxyz12345");
|
|
177
|
-
assert(rEmpty.length === 0, `searchPlans("nonexistentxyz12345") → [] (got ${rEmpty.length})`);
|
|
178
|
-
|
|
179
|
-
// Multi-match: "user" appears in both titles
|
|
180
|
-
mkPlan("fts-login", "fts-login", "user login authentication flow");
|
|
181
|
-
mkPlan("fts-logout", "fts-logout", "user logout cleanup handler");
|
|
182
|
-
const rMulti = searchPlans(db, "user");
|
|
183
|
-
assert(rMulti.length === 2, `searchPlans("user") → 2 results (got ${rMulti.length})`);
|
|
184
|
-
const multiIds = rMulti.map((p) => p.id).sort();
|
|
185
|
-
assert(
|
|
186
|
-
multiIds.includes("fts-login") && multiIds.includes("fts-logout"),
|
|
187
|
-
`searchPlans("user") includes fts-login and fts-logout (got [${multiIds.join(", ")}])`,
|
|
188
|
-
);
|
|
189
|
-
|
|
190
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
191
|
-
// TEST 2: FTS5 con acentos (remove_diacritics)
|
|
192
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
193
|
-
section("Test 2: FTS5 con acentos — remove_diacritics");
|
|
194
|
-
|
|
195
|
-
mkPlan("fts-acc", "fts-acc", "Plan de Acción correctiva");
|
|
196
|
-
|
|
197
|
-
const rAcc1 = searchPlans(db, "accion");
|
|
198
|
-
assert(rAcc1.length === 1, `searchPlans("accion") finds "Acción" (got ${rAcc1.length})`);
|
|
199
|
-
assert(rAcc1[0]?.id === "fts-acc", `searchPlans("accion") → fts-acc (got ${rAcc1[0]?.id})`);
|
|
200
|
-
|
|
201
|
-
const rAcc2 = searchPlans(db, "acción");
|
|
202
|
-
assert(rAcc2.length === 1, `searchPlans("acción") finds "Acción" (got ${rAcc2.length})`);
|
|
203
|
-
assert(rAcc2[0]?.id === "fts-acc", `searchPlans("acción") → fts-acc (got ${rAcc2[0]?.id})`);
|
|
204
|
-
|
|
205
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
206
|
-
// TEST 2b: BUG-1 fix — escapeFtsQuery validation
|
|
207
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
208
|
-
section("Test 2b: BUG-1 fix — hyphens, quotes, diacritics via escapeFtsQuery");
|
|
209
|
-
|
|
210
|
-
// Hyphenated phrase: "auth-bug" treated as literal phrase, not column qualifier
|
|
211
|
-
mkPlan("fts-hyphen", "fts-hyphen", "Fix auth-bug in login module");
|
|
212
|
-
const rHyphenSearch = searchPlans(db, "auth-bug");
|
|
213
|
-
assert(
|
|
214
|
-
rHyphenSearch.length === 1,
|
|
215
|
-
`searchPlans("auth-bug") → 1 result (got ${rHyphenSearch.length})`,
|
|
216
|
-
);
|
|
217
|
-
assert(
|
|
218
|
-
rHyphenSearch[0]?.id === "fts-hyphen",
|
|
219
|
-
`searchPlans("auth-bug") → fts-hyphen (got ${rHyphenSearch[0]?.id})`,
|
|
220
|
-
);
|
|
221
|
-
|
|
222
|
-
// Internal quotes: must be escaped, not throw
|
|
223
|
-
mkPlan("fts-quotes", "fts-quotes", 'Plan with "quotes" in title');
|
|
224
|
-
const rQuotes = searchPlans(db, 'term with "quotes"');
|
|
225
|
-
assert(
|
|
226
|
-
rQuotes.length === 0,
|
|
227
|
-
`searchPlans('term with "quotes"') → 0 results, no throw (got ${rQuotes.length})`,
|
|
228
|
-
);
|
|
229
|
-
|
|
230
|
-
// Diacritics with explicit "Accion" plan (confirm accent-insensitive search)
|
|
231
|
-
mkPlan("fts-accion", "fts-accion", "Accion correctiva urgente");
|
|
232
|
-
const rAccion = searchPlans(db, "acción");
|
|
233
|
-
assert(
|
|
234
|
-
rAccion.some((p) => p.id === "fts-accion"),
|
|
235
|
-
`searchPlans("acción") finds "Accion" plan (fts-accion)`,
|
|
236
|
-
);
|
|
237
|
-
|
|
238
|
-
// searchTasks with hyphens: also uses escapeFtsQuery
|
|
239
|
-
createTasksBatch(db, "fts-hyphen", [
|
|
240
|
-
{ ...TASK_DEFAULTS, description: "Fix auth-bug in API endpoint", orderIndex: 0 },
|
|
241
|
-
]);
|
|
242
|
-
const rTaskHyphen = searchTasks(db, "auth-bug");
|
|
243
|
-
assert(rTaskHyphen.length >= 1, `searchTasks("auth-bug") → ≥1 result (got ${rTaskHyphen.length})`);
|
|
244
|
-
|
|
245
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
246
|
-
// TEST 3: FTS5 stopwords
|
|
247
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
248
|
-
section("Test 3: FTS5 stopwords — 'el' es stopword español");
|
|
249
|
-
|
|
250
|
-
mkPlan("fts-stop", "fts-stop", "El plan para hacer algo importante");
|
|
251
|
-
|
|
252
|
-
// "el" is a Spanish stopword in the fts5_stopwords table, BUT the FTS5 tokenizer
|
|
253
|
-
// (unicode61 remove_diacritics 1) does NOT reference that custom stopwords table.
|
|
254
|
-
// unicode61 default stopword list does not include Spanish words.
|
|
255
|
-
// Spec says "[] o muy pocos" — we document this as a finding and accept ≤1.
|
|
256
|
-
const rStop = searchPlans(db, "el");
|
|
257
|
-
if (rStop.length > 0) {
|
|
258
|
-
console.log(
|
|
259
|
-
" ⚠ FINDING: searchPlans('el') returned results — fts5_stopwords table not wired to FTS5 tokenizer",
|
|
260
|
-
);
|
|
261
|
-
console.log(
|
|
262
|
-
" → unicode61 remove_diacritics 1 does NOT use custom stopwords. Spanish stopwords are decorative.",
|
|
263
|
-
);
|
|
264
|
-
}
|
|
265
|
-
assert(
|
|
266
|
-
rStop.length <= 1,
|
|
267
|
-
`searchPlans("el") → [] o muy pocos (got ${rStop.length}, spec allows ≤1)`,
|
|
268
|
-
);
|
|
269
|
-
|
|
270
|
-
// "plan" is NOT a stopword → should find it
|
|
271
|
-
const rPlan = searchPlans(db, "plan");
|
|
272
|
-
assert(rPlan.length >= 1, `searchPlans("plan") finds the plan (got ${rPlan.length})`);
|
|
273
|
-
assert(
|
|
274
|
-
rPlan.some((p) => p.id === "fts-stop"),
|
|
275
|
-
`searchPlans("plan") includes fts-stop`,
|
|
276
|
-
);
|
|
277
|
-
|
|
278
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
279
|
-
// TEST 4: Plan lifecycle completo
|
|
280
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
281
|
-
section(
|
|
282
|
-
"Test 4: Plan lifecycle completo — create → approve → tasks → session → complete → archive",
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
// 4.1 Create plan (status=draft)
|
|
286
|
-
const lifecyclePlan = mkPlan("lc-plan", "lifecycle-test", "Full Lifecycle Test Plan");
|
|
287
|
-
assert(
|
|
288
|
-
lifecyclePlan.status === "draft",
|
|
289
|
-
`plan created with status=draft (got ${lifecyclePlan.status})`,
|
|
290
|
-
);
|
|
291
|
-
|
|
292
|
-
// 4.2 Approve plan
|
|
293
|
-
const approvedPlan = approvePlan(db, "lc-plan", { updatedBy: "smoke-e2e" });
|
|
294
|
-
assert(approvedPlan !== null, "approvePlan returned plan");
|
|
295
|
-
assert(approvedPlan?.status === "approved", `plan approved (got ${approvedPlan?.status})`);
|
|
296
|
-
assert(approvedPlan?.approvedAt !== null, "approved_at is set");
|
|
297
|
-
|
|
298
|
-
// 4.3 Create 3 tasks
|
|
299
|
-
const lcTasks = createTasksBatch(db, "lc-plan", [
|
|
300
|
-
{ ...TASK_DEFAULTS, description: "Implement authentication module", orderIndex: 0 },
|
|
301
|
-
{ ...TASK_DEFAULTS, description: "Write unit tests", orderIndex: 1 },
|
|
302
|
-
{ ...TASK_DEFAULTS, description: "Update documentation", orderIndex: 2 },
|
|
303
|
-
]);
|
|
304
|
-
assert(lcTasks.length === 3, `created 3 tasks (got ${lcTasks.length})`);
|
|
305
|
-
|
|
306
|
-
// 4.4 Start session
|
|
307
|
-
const lcSession = startSession(db, {
|
|
308
|
-
id: "lc-session-1",
|
|
309
|
-
goal: "Complete lifecycle test plan",
|
|
310
|
-
planId: "lc-plan",
|
|
311
|
-
});
|
|
312
|
-
assert(lcSession.planId === "lc-plan", `session linked to plan (got ${lcSession.planId})`);
|
|
313
|
-
|
|
314
|
-
// 4.5 Task 1: pending → running
|
|
315
|
-
assert(lcTasks[0] !== undefined, "task 0 exists");
|
|
316
|
-
assert(lcTasks[1] !== undefined, "task 1 exists");
|
|
317
|
-
assert(lcTasks[2] !== undefined, "task 2 exists");
|
|
318
|
-
const t1 = lcTasks[0] as NonNullable<(typeof lcTasks)[number]>;
|
|
319
|
-
updateTaskStatus(db, t1.id, "running");
|
|
320
|
-
const t1Running = db.query("SELECT status FROM plan_tasks WHERE id = ?").get(t1.id) as {
|
|
321
|
-
status: string;
|
|
322
|
-
} | null;
|
|
323
|
-
assert(t1Running?.status === "running", `task 1 → running (got ${t1Running?.status})`);
|
|
324
|
-
|
|
325
|
-
// 4.6 Task 1: running → done
|
|
326
|
-
updateTaskStatus(db, t1.id, "done", { result: "Authentication module implemented" });
|
|
327
|
-
const t1Done = db.query("SELECT status FROM plan_tasks WHERE id = ?").get(t1.id) as {
|
|
328
|
-
status: string;
|
|
329
|
-
} | null;
|
|
330
|
-
assert(t1Done?.status === "done", `task 1 → done (got ${t1Done?.status})`);
|
|
331
|
-
|
|
332
|
-
// 4.7 Task 2: pending → done
|
|
333
|
-
const t2 = lcTasks[1] as NonNullable<(typeof lcTasks)[number]>;
|
|
334
|
-
updateTaskStatus(db, t2.id, "done", { result: "All tests passing" });
|
|
335
|
-
|
|
336
|
-
// 4.8 Task 3: pending → done
|
|
337
|
-
const t3 = lcTasks[2] as NonNullable<(typeof lcTasks)[number]>;
|
|
338
|
-
updateTaskStatus(db, t3.id, "done", { result: "Docs updated" });
|
|
339
|
-
|
|
340
|
-
// 4.9 Session checkpoint
|
|
341
|
-
const checkpoint = checkpointSession(
|
|
342
|
-
db,
|
|
343
|
-
"lc-session-1",
|
|
344
|
-
{ tasksCompleted: 3, totalTasks: 3 },
|
|
345
|
-
"All tasks completed successfully",
|
|
346
|
-
);
|
|
347
|
-
assert(checkpoint !== null, "session checkpoint succeeded");
|
|
348
|
-
|
|
349
|
-
// 4.10 Plan → completed
|
|
350
|
-
const completedPlan = updatePlanStatus(db, "lc-plan", "completed", { updatedBy: "smoke-e2e" });
|
|
351
|
-
assert(completedPlan?.status === "completed", `plan → completed (got ${completedPlan?.status})`);
|
|
352
|
-
|
|
353
|
-
// 4.11 Archive the plan
|
|
354
|
-
const archiveResult = archivePlan(db, "lc-plan", { memDir: testMemDir });
|
|
355
|
-
assert(archiveResult.planId === "lc-plan", "archivePlan planId=lc-plan");
|
|
356
|
-
assert(
|
|
357
|
-
archiveResult.tasksCount === 3,
|
|
358
|
-
`archivePlan tasksCount=3 (got ${archiveResult.tasksCount})`,
|
|
359
|
-
);
|
|
360
|
-
assert(
|
|
361
|
-
archiveResult.sessionsCount === 1,
|
|
362
|
-
`archivePlan sessionsCount=1 (got ${archiveResult.sessionsCount})`,
|
|
363
|
-
);
|
|
364
|
-
|
|
365
|
-
// 4.12 Verify archived_at set on plan, tasks, sessions
|
|
366
|
-
const archivedPlan = getPlan(db, "lc-plan");
|
|
367
|
-
assert(archivedPlan?.archivedAt !== null, "plan.archived_at is set");
|
|
368
|
-
|
|
369
|
-
const archivedTasks = db
|
|
370
|
-
.query(
|
|
371
|
-
"SELECT COUNT(*) as c FROM plan_tasks WHERE plan_id = 'lc-plan' AND archived_at IS NOT NULL",
|
|
372
|
-
)
|
|
373
|
-
.get() as { c: number };
|
|
374
|
-
assert(archivedTasks.c === 3, `3 tasks have archived_at set (got ${archivedTasks.c})`);
|
|
375
|
-
|
|
376
|
-
const archivedSession = db
|
|
377
|
-
.query("SELECT archived_at FROM sessions WHERE id = 'lc-session-1'")
|
|
378
|
-
.get() as { archived_at: number | null } | null;
|
|
379
|
-
assert(archivedSession?.archived_at !== null, "session.archived_at is set");
|
|
380
|
-
|
|
381
|
-
// 4.13 listPlans() default excludes archived
|
|
382
|
-
const activeOnly = listPlans(db);
|
|
383
|
-
assert(!activeOnly.some((p) => p.id === "lc-plan"), "listPlans() excludes archived plan");
|
|
384
|
-
|
|
385
|
-
// 4.14 listPlans({ includeArchived: true }) includes archived
|
|
386
|
-
const withArchived = listPlans(db, { includeArchived: true });
|
|
387
|
-
assert(
|
|
388
|
-
withArchived.some((p) => p.id === "lc-plan"),
|
|
389
|
-
"listPlans({ includeArchived: true }) includes archived plan",
|
|
390
|
-
);
|
|
391
|
-
|
|
392
|
-
// 4.15 Memory file exists and is valid
|
|
393
|
-
assert(existsSync(archiveResult.filePath), `memory file exists: ${archiveResult.filePath}`);
|
|
394
|
-
const mdContent = readFileSync(archiveResult.filePath, "utf-8");
|
|
395
|
-
assert(mdContent.length > 500, `markdown > 500 bytes (got ${mdContent.length})`);
|
|
396
|
-
assert(mdContent.includes("# Plan:"), "markdown contains '# Plan:'");
|
|
397
|
-
assert(mdContent.includes("## Tasks"), "markdown contains '## Tasks'");
|
|
398
|
-
assert(mdContent.includes("## Sessions"), "markdown contains '## Sessions'");
|
|
399
|
-
assert(mdContent.includes("## Metadata"), "markdown contains '## Metadata'");
|
|
400
|
-
|
|
401
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
402
|
-
// TEST 5: Auto-archive no rompe flow
|
|
403
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
404
|
-
section("Test 5: Auto-archive no rompe flow");
|
|
405
|
-
|
|
406
|
-
mkPlan("aa-plan", "test-no-archive", "Auto Archive Test Plan");
|
|
407
|
-
const aaResult = archivePlan(db, "aa-plan", { memDir: testMemDir });
|
|
408
|
-
assert(
|
|
409
|
-
typeof aaResult.filePath === "string" && aaResult.filePath.length > 0,
|
|
410
|
-
`archived.filePath is set: ${aaResult.filePath}`,
|
|
411
|
-
);
|
|
412
|
-
assert(existsSync(aaResult.filePath), "archived file exists on disk");
|
|
413
|
-
|
|
414
|
-
// Verify planUpdateStatus returns updated plan for already-completed scenario
|
|
415
|
-
mkPlan("aa-plan2", "test-no-archive-2", "Auto Archive Test Plan 2");
|
|
416
|
-
updatePlanStatus(db, "aa-plan2", "completed", { updatedBy: "smoke-e2e" });
|
|
417
|
-
const aa2Plan = getPlan(db, "aa-plan2");
|
|
418
|
-
assert(
|
|
419
|
-
aa2Plan?.status === "completed",
|
|
420
|
-
`planUpdateStatus returned completed plan (got ${aa2Plan?.status})`,
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
424
|
-
// TEST 6: Idempotencia del archive
|
|
425
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
426
|
-
section("Test 6: Idempotencia del archive");
|
|
427
|
-
|
|
428
|
-
// 6.1 Re-archive already archived plan → throws "already archived"
|
|
429
|
-
assertThrows(
|
|
430
|
-
() => archivePlan(db, "lc-plan", { memDir: testMemDir }),
|
|
431
|
-
"already archived",
|
|
432
|
-
"archivePlan on already-archived plan throws 'already archived'",
|
|
433
|
-
);
|
|
434
|
-
|
|
435
|
-
// 6.2 updatePlanStatus on archived plan → should work (not restrictive)
|
|
436
|
-
const updatedArchived = updatePlanStatus(db, "lc-plan", "failed", { updatedBy: "smoke-e2e" });
|
|
437
|
-
assert(updatedArchived !== null, "updatePlanStatus on archived plan returns plan");
|
|
438
|
-
assert(
|
|
439
|
-
updatedArchived?.status === "failed",
|
|
440
|
-
`archived plan status → failed (got ${updatedArchived?.status})`,
|
|
441
|
-
);
|
|
442
|
-
|
|
443
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
444
|
-
// TEST 7: nextTaskForAgent con archived
|
|
445
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
446
|
-
section("Test 7: nextTaskForAgent con archived filter");
|
|
447
|
-
|
|
448
|
-
mkPlan("agent-plan", "agent-plan-test", "Agent Plan for nextTask Test");
|
|
449
|
-
createTasksBatch(db, "agent-plan", [
|
|
450
|
-
{ ...TASK_DEFAULTS, description: "Agent task 1", orderIndex: 0, agent: "smith" },
|
|
451
|
-
{ ...TASK_DEFAULTS, description: "Agent task 2", orderIndex: 1, agent: "smith" },
|
|
452
|
-
]);
|
|
453
|
-
|
|
454
|
-
// Archive the plan (auto-archives tasks)
|
|
455
|
-
archivePlan(db, "agent-plan", { memDir: testMemDir });
|
|
456
|
-
|
|
457
|
-
// Default: should NOT return tasks from archived plan
|
|
458
|
-
const archivedAgentTask = nextTaskForAgent(db, "smith", { planId: "agent-plan" });
|
|
459
|
-
assert(
|
|
460
|
-
archivedAgentTask === null,
|
|
461
|
-
`nextTaskForAgent default skips archived plan tasks (got ${archivedAgentTask?.id ?? "null"})`,
|
|
462
|
-
);
|
|
463
|
-
|
|
464
|
-
// With includeArchived: true → should return pending task
|
|
465
|
-
const includedTask = nextTaskForAgent(db, "smith", { planId: "agent-plan", includeArchived: true });
|
|
466
|
-
assert(
|
|
467
|
-
includedTask !== null && includedTask.planId === "agent-plan",
|
|
468
|
-
`nextTaskForAgent with includeArchived=true returns task (got ${includedTask?.id ?? "null"})`,
|
|
469
|
-
);
|
|
470
|
-
assert(
|
|
471
|
-
includedTask?.status === "pending",
|
|
472
|
-
`returned task status=pending (got ${includedTask?.status})`,
|
|
473
|
-
);
|
|
474
|
-
|
|
475
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
476
|
-
// TEST 8: Migration upgrade path
|
|
477
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
478
|
-
section("Test 8: Migration upgrade path — archived_at columns exist");
|
|
479
|
-
|
|
480
|
-
const db8 = new Database(":memory:");
|
|
481
|
-
db8.exec("PRAGMA journal_mode = WAL");
|
|
482
|
-
db8.exec("PRAGMA foreign_keys = ON");
|
|
483
|
-
runMigrations(db8);
|
|
484
|
-
|
|
485
|
-
// Verify archived_at exists on all 3 tables
|
|
486
|
-
for (const table of ["plans", "plan_tasks", "sessions"]) {
|
|
487
|
-
const cols = db8.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
488
|
-
assert(
|
|
489
|
-
cols.some((c) => c.name === "archived_at"),
|
|
490
|
-
`${table}.archived_at column exists`,
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
// Verify indexes exist
|
|
495
|
-
const indexes = db8
|
|
496
|
-
.query("SELECT name FROM sqlite_master WHERE type = 'index' AND name LIKE 'idx_%archived%'")
|
|
497
|
-
.all() as Array<{ name: string }>;
|
|
498
|
-
assert(indexes.length === 3, `3 archived indexes exist (got ${indexes.length})`);
|
|
499
|
-
|
|
500
|
-
db8.close();
|
|
501
|
-
|
|
502
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
503
|
-
// TEST 9: Re-run de migrations (idempotencia)
|
|
504
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
505
|
-
section("Test 9: Re-run de migrations — idempotencia");
|
|
506
|
-
|
|
507
|
-
const db9 = new Database(":memory:");
|
|
508
|
-
db9.exec("PRAGMA journal_mode = WAL");
|
|
509
|
-
db9.exec("PRAGMA foreign_keys = ON");
|
|
510
|
-
|
|
511
|
-
// First run
|
|
512
|
-
runMigrations(db9);
|
|
513
|
-
const ver9a = db9.query("SELECT MAX(version) as v FROM schema_version").get() as { v: number };
|
|
514
|
-
assert(ver9a.v === 5, `first run: schema_version = 5 (got ${ver9a.v})`);
|
|
515
|
-
|
|
516
|
-
// Second run — should not fail
|
|
517
|
-
let rerunFailed = false;
|
|
518
|
-
try {
|
|
519
|
-
runMigrations(db9);
|
|
520
|
-
} catch (err) {
|
|
521
|
-
rerunFailed = true;
|
|
522
|
-
console.error(` ✗ runMigrations() re-run threw: ${err}`);
|
|
523
|
-
}
|
|
524
|
-
assert(!rerunFailed, "runMigrations() re-run did not throw");
|
|
525
|
-
|
|
526
|
-
// Verify no duplicate schema_version rows
|
|
527
|
-
const versionRows = db9
|
|
528
|
-
.query("SELECT version, COUNT(*) as c FROM schema_version GROUP BY version HAVING c > 1")
|
|
529
|
-
.all();
|
|
530
|
-
assert(versionRows.length === 0, "no duplicate schema_version rows");
|
|
531
|
-
|
|
532
|
-
// Verify no duplicate indexes
|
|
533
|
-
const idxCount = db9
|
|
534
|
-
.query(
|
|
535
|
-
"SELECT name, COUNT(*) as c FROM sqlite_master WHERE type='index' GROUP BY name HAVING c > 1",
|
|
536
|
-
)
|
|
537
|
-
.all();
|
|
538
|
-
assert(idxCount.length === 0, "no duplicate indexes");
|
|
539
|
-
|
|
540
|
-
// Verify FTS table still works after re-run (use createPlan with db9 directly)
|
|
541
|
-
createPlan(db9, {
|
|
542
|
-
...PLAN_DEFAULTS,
|
|
543
|
-
id: "rerun-test",
|
|
544
|
-
slug: "rerun-test",
|
|
545
|
-
title: "Rerun Test Plan for FTS validation",
|
|
546
|
-
});
|
|
547
|
-
const rerunSearch = searchPlans(db9, "rerun");
|
|
548
|
-
assert(
|
|
549
|
-
rerunSearch.length === 1,
|
|
550
|
-
`post-rerun: searchPlans("rerun") → 1 (got ${rerunSearch.length})`,
|
|
551
|
-
);
|
|
552
|
-
|
|
553
|
-
db9.close();
|
|
554
|
-
|
|
555
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
556
|
-
// TEST 10: FTS5 cleanup post-archive
|
|
557
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
558
|
-
section("Test 10: FTS5 cleanup post-archive — archived plans excluded from search");
|
|
559
|
-
|
|
560
|
-
mkPlan("fts-archive", "fts-archive-marker", "ZZZmarkerxyz unique title");
|
|
561
|
-
|
|
562
|
-
// Verify it's findable before archive
|
|
563
|
-
const preArchive = searchPlans(db, "ZZZmarkerxyz");
|
|
564
|
-
assert(
|
|
565
|
-
preArchive.length === 1,
|
|
566
|
-
`pre-archive: searchPlans("ZZZmarkerxyz") → 1 (got ${preArchive.length})`,
|
|
567
|
-
);
|
|
568
|
-
|
|
569
|
-
// Archive it
|
|
570
|
-
archivePlan(db, "fts-archive", { memDir: testMemDir });
|
|
571
|
-
|
|
572
|
-
// listPlans() default should NOT include it
|
|
573
|
-
const postArchiveList = listPlans(db);
|
|
574
|
-
assert(
|
|
575
|
-
!postArchiveList.some((p) => p.id === "fts-archive"),
|
|
576
|
-
"post-archive: listPlans() excludes archived plan",
|
|
577
|
-
);
|
|
578
|
-
|
|
579
|
-
// searchPlans() default should NOT find it (archived filter in JOIN)
|
|
580
|
-
const postArchiveSearch = searchPlans(db, "ZZZmarkerxyz");
|
|
581
|
-
assert(
|
|
582
|
-
postArchiveSearch.length === 0,
|
|
583
|
-
`post-archive: searchPlans("ZZZmarkerxyz") default → 0 (got ${postArchiveSearch.length})`,
|
|
584
|
-
);
|
|
585
|
-
|
|
586
|
-
// searchPlans with includeArchived: true SHOULD find it
|
|
587
|
-
const postArchiveSearchAll = searchPlans(db, "ZZZmarkerxyz", 20, { includeArchived: true });
|
|
588
|
-
assert(
|
|
589
|
-
postArchiveSearchAll.length === 1,
|
|
590
|
-
`post-archive: searchPlans("ZZZmarkerxyz", includeArchived=true) → 1 (got ${postArchiveSearchAll.length})`,
|
|
591
|
-
);
|
|
592
|
-
assert(
|
|
593
|
-
postArchiveSearchAll[0]?.id === "fts-archive",
|
|
594
|
-
`includeArchived search returns fts-archive (got ${postArchiveSearchAll[0]?.id})`,
|
|
595
|
-
);
|
|
596
|
-
|
|
597
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
598
|
-
// BONUS: findPlansByTag with archived filter
|
|
599
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
600
|
-
section("Bonus: findPlansByTag with archived filter");
|
|
601
|
-
|
|
602
|
-
mkPlan("tag-plan", "tag-plan-test", "Tag Test Plan");
|
|
603
|
-
addPlanTag(db, "tag-plan", "urgent", "smoke-e2e");
|
|
604
|
-
|
|
605
|
-
// Verify tag search works
|
|
606
|
-
const tagResults = findPlansByTag(db, "urgent");
|
|
607
|
-
assert(
|
|
608
|
-
tagResults.some((p) => p.id === "tag-plan"),
|
|
609
|
-
"findPlansByTag finds tagged plan",
|
|
610
|
-
);
|
|
611
|
-
|
|
612
|
-
// Archive and verify filter
|
|
613
|
-
archivePlan(db, "tag-plan", { memDir: testMemDir });
|
|
614
|
-
|
|
615
|
-
const tagAfterArchive = findPlansByTag(db, "urgent");
|
|
616
|
-
assert(
|
|
617
|
-
!tagAfterArchive.some((p) => p.id === "tag-plan"),
|
|
618
|
-
"findPlansByTag excludes archived plan by default",
|
|
619
|
-
);
|
|
620
|
-
|
|
621
|
-
const tagWithArchived = findPlansByTag(db, "urgent", 20, { includeArchived: true });
|
|
622
|
-
assert(
|
|
623
|
-
tagWithArchived.some((p) => p.id === "tag-plan"),
|
|
624
|
-
"findPlansByTag with includeArchived=true includes archived plan",
|
|
625
|
-
);
|
|
626
|
-
|
|
627
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
628
|
-
// BONUS: findPlansByCategory with archived filter
|
|
629
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
630
|
-
section("Bonus: findPlansByCategory with archived filter");
|
|
631
|
-
|
|
632
|
-
mkPlan("cat-plan", "cat-plan-test", "Category Test Plan", { category: "bugfix" as PlanCategory });
|
|
633
|
-
archivePlan(db, "cat-plan", { memDir: testMemDir });
|
|
634
|
-
|
|
635
|
-
const catResults = findPlansByCategory(db, "bugfix");
|
|
636
|
-
assert(
|
|
637
|
-
!catResults.some((p) => p.id === "cat-plan"),
|
|
638
|
-
"findPlansByCategory excludes archived plan by default",
|
|
639
|
-
);
|
|
640
|
-
|
|
641
|
-
const catWithArchived = findPlansByCategory(db, "bugfix", 20, { includeArchived: true });
|
|
642
|
-
assert(
|
|
643
|
-
catWithArchived.some((p) => p.id === "cat-plan"),
|
|
644
|
-
"findPlansByCategory with includeArchived=true includes archived plan",
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
648
|
-
// BONUS: plan_progress view with archived filter
|
|
649
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
650
|
-
section("Bonus: plan_progress view — archived tasks excluded from counts");
|
|
651
|
-
|
|
652
|
-
mkPlan("prog-plan", "prog-plan-test", "Progress Test Plan");
|
|
653
|
-
createTasksBatch(db, "prog-plan", [
|
|
654
|
-
{ ...TASK_DEFAULTS, description: "Progress task 1", orderIndex: 0 },
|
|
655
|
-
{ ...TASK_DEFAULTS, description: "Progress task 2", orderIndex: 1 },
|
|
656
|
-
]);
|
|
657
|
-
|
|
658
|
-
const progBefore = getPlanProgress(db, "prog-plan");
|
|
659
|
-
assert(progBefore.length === 1, "plan_progress returns 1 row");
|
|
660
|
-
assert(
|
|
661
|
-
progBefore[0]?.totalTasks === 2,
|
|
662
|
-
`before archive: totalTasks=2 (got ${progBefore[0]?.totalTasks})`,
|
|
663
|
-
);
|
|
664
|
-
|
|
665
|
-
archivePlan(db, "prog-plan", { memDir: testMemDir });
|
|
666
|
-
|
|
667
|
-
const progAfter = getPlanProgress(db, "prog-plan");
|
|
668
|
-
assert(progAfter.length === 1, "plan_progress still returns 1 row after archive");
|
|
669
|
-
assert(
|
|
670
|
-
progAfter[0]?.totalTasks === 0,
|
|
671
|
-
`after archive: totalTasks=0 (archived excluded, got ${progAfter[0]?.totalTasks})`,
|
|
672
|
-
);
|
|
673
|
-
|
|
674
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
675
|
-
// Cleanup
|
|
676
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
677
|
-
rmSync(testMemDir, { recursive: true, force: true });
|
|
678
|
-
db.close();
|
|
679
|
-
|
|
680
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
681
|
-
// Reporte (caveman)
|
|
682
|
-
// ═════════════════════════════════════════════════════════════════════════════
|
|
683
|
-
console.log(`\n${"═".repeat(60)}`);
|
|
684
|
-
console.log(" REPORTE E2E SMOKE TEST");
|
|
685
|
-
console.log(`${"═".repeat(60)}`);
|
|
686
|
-
console.log(`Total asserts: ${pass} pass / ${fail} fail`);
|
|
687
|
-
console.log(`Total tests: ${pass + fail}`);
|
|
688
|
-
|
|
689
|
-
if (fail > 0) {
|
|
690
|
-
console.log("\nFALLOS:");
|
|
691
|
-
for (const f of failures) {
|
|
692
|
-
console.log(` ✗ ${f.test}: ${f.msg}`);
|
|
693
|
-
}
|
|
694
|
-
console.log("\nANÁLISIS DE CAUSA RAÍZ:");
|
|
695
|
-
console.log(` Revisar los ${fail} fallos arriba. Cada uno indica:`);
|
|
696
|
-
console.log(" - file:line → scripts/smoke-e2e.ts");
|
|
697
|
-
console.log(" - error exacto → ver mensaje de fallo");
|
|
698
|
-
console.log(" - causa raíz → probablemente FTS5 config, archive filter, o migration issue");
|
|
699
|
-
} else {
|
|
700
|
-
console.log("\n✅ TODOS LOS TESTS PASARON - v5 hotfix validado end-to-end");
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
console.log(`${"═".repeat(60)}`);
|
|
704
|
-
process.exit(fail > 0 ? 1 : 0);
|