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,490 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for plan status transitions and FK-scoped ensureSession.
|
|
3
|
+
*
|
|
4
|
+
* Validates Fix #1 (scoped ensureSession) and Fix #8 (auto-link session):
|
|
5
|
+
* - Terminal statuses (completed/failed/abandoned) do NOT auto-create sessions
|
|
6
|
+
* - Active statuses (executing/approved) DO auto-create sessions
|
|
7
|
+
*
|
|
8
|
+
* Uses in-memory SQLite via bun:sqlite. Each test gets a fresh DB
|
|
9
|
+
* with the full schema applied by runMigrations.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { Database } from "bun:sqlite";
|
|
13
|
+
import { beforeEach, describe, expect, test } from "bun:test";
|
|
14
|
+
import { runMigrations } from "./migrations.ts";
|
|
15
|
+
import {
|
|
16
|
+
approvePlan,
|
|
17
|
+
createPlan,
|
|
18
|
+
deletePlan,
|
|
19
|
+
getPlan,
|
|
20
|
+
getPlanBySlug,
|
|
21
|
+
getPlanProgress,
|
|
22
|
+
listPlans,
|
|
23
|
+
updatePlanStatus,
|
|
24
|
+
} from "./plans.ts";
|
|
25
|
+
import { getSession } from "./sessions.ts";
|
|
26
|
+
import { createTasksBatch } from "./tasks.ts";
|
|
27
|
+
import type { Plan } from "./types.ts";
|
|
28
|
+
|
|
29
|
+
let db: Database;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
db = new Database(":memory:");
|
|
33
|
+
db.exec("PRAGMA foreign_keys = ON");
|
|
34
|
+
runMigrations(db);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Helper — create a plan with sensible defaults, override only what the test needs.
|
|
39
|
+
*/
|
|
40
|
+
function makePlan(overrides: Partial<Parameters<typeof createPlan>[1]> = {}): Plan {
|
|
41
|
+
return createPlan(db, {
|
|
42
|
+
id: crypto.randomUUID(),
|
|
43
|
+
slug: "test-plan",
|
|
44
|
+
title: "Test",
|
|
45
|
+
status: "draft",
|
|
46
|
+
priority: 2,
|
|
47
|
+
approvedAt: null,
|
|
48
|
+
completedAt: null,
|
|
49
|
+
sessionId: null,
|
|
50
|
+
overview: "test",
|
|
51
|
+
approach: null,
|
|
52
|
+
complexity: 3,
|
|
53
|
+
createdBy: "test",
|
|
54
|
+
updatedBy: "test",
|
|
55
|
+
sourceSessionId: null,
|
|
56
|
+
sourceMessageId: null,
|
|
57
|
+
category: null,
|
|
58
|
+
metadata: {},
|
|
59
|
+
archivedAt: null,
|
|
60
|
+
...overrides,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
describe("updatePlanStatus", () => {
|
|
65
|
+
test("status=completed succeeds without sessionId", () => {
|
|
66
|
+
const plan = makePlan();
|
|
67
|
+
const updated = updatePlanStatus(db, plan.id, "completed");
|
|
68
|
+
|
|
69
|
+
expect(updated).not.toBeNull();
|
|
70
|
+
expect(updated?.status).toBe("completed");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("status=completed + sessionId does NOT auto-create session (scope fix)", () => {
|
|
74
|
+
const plan = makePlan();
|
|
75
|
+
const sessionId = `ses_${crypto.randomUUID()}`;
|
|
76
|
+
|
|
77
|
+
updatePlanStatus(db, plan.id, "completed", { sessionId });
|
|
78
|
+
|
|
79
|
+
// Terminal status — no session row should be created
|
|
80
|
+
const session = getSession(db, sessionId);
|
|
81
|
+
expect(session).toBeNull();
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("status=executing + sessionId auto-creates session", () => {
|
|
85
|
+
const plan = makePlan();
|
|
86
|
+
const sessionId = `ses_${crypto.randomUUID()}`;
|
|
87
|
+
|
|
88
|
+
updatePlanStatus(db, plan.id, "executing", { sessionId });
|
|
89
|
+
|
|
90
|
+
const session = getSession(db, sessionId);
|
|
91
|
+
expect(session).not.toBeNull();
|
|
92
|
+
expect(session?.id).toBe(sessionId);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("status=approved + sessionId auto-creates session", () => {
|
|
96
|
+
const plan = makePlan();
|
|
97
|
+
const sessionId = `ses_${crypto.randomUUID()}`;
|
|
98
|
+
|
|
99
|
+
updatePlanStatus(db, plan.id, "approved", { sessionId });
|
|
100
|
+
|
|
101
|
+
const session = getSession(db, sessionId);
|
|
102
|
+
expect(session).not.toBeNull();
|
|
103
|
+
expect(session?.id).toBe(sessionId);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("status=failed + sessionId does NOT auto-create session (scope fix)", () => {
|
|
107
|
+
const plan = makePlan();
|
|
108
|
+
const sessionId = `ses_${crypto.randomUUID()}`;
|
|
109
|
+
|
|
110
|
+
updatePlanStatus(db, plan.id, "failed", { sessionId });
|
|
111
|
+
|
|
112
|
+
// Terminal status — no session row should be created
|
|
113
|
+
const session = getSession(db, sessionId);
|
|
114
|
+
expect(session).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
test("status=abandoned + sessionId does NOT auto-create session (scope fix)", () => {
|
|
118
|
+
const plan = makePlan();
|
|
119
|
+
const sessionId = `ses_${crypto.randomUUID()}`;
|
|
120
|
+
|
|
121
|
+
updatePlanStatus(db, plan.id, "abandoned", { sessionId });
|
|
122
|
+
|
|
123
|
+
// Terminal status — no session row should be created
|
|
124
|
+
const session = getSession(db, sessionId);
|
|
125
|
+
expect(session).toBeNull();
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe("approvePlan", () => {
|
|
130
|
+
test("auto-creates session when sessionId provided and missing", () => {
|
|
131
|
+
const plan = makePlan();
|
|
132
|
+
const sessionId = `ses_${crypto.randomUUID()}`;
|
|
133
|
+
|
|
134
|
+
const approved = approvePlan(db, plan.id, { sessionId });
|
|
135
|
+
|
|
136
|
+
expect(approved).not.toBeNull();
|
|
137
|
+
expect(approved?.status).toBe("approved");
|
|
138
|
+
expect(approved?.sessionId).toBe(sessionId);
|
|
139
|
+
|
|
140
|
+
const session = getSession(db, sessionId);
|
|
141
|
+
expect(session).not.toBeNull();
|
|
142
|
+
expect(session?.id).toBe(sessionId);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("succeeds without sessionId and does NOT create session", () => {
|
|
146
|
+
const plan = makePlan();
|
|
147
|
+
|
|
148
|
+
const approved = approvePlan(db, plan.id, { updatedBy: "test" });
|
|
149
|
+
|
|
150
|
+
expect(approved).not.toBeNull();
|
|
151
|
+
expect(approved?.status).toBe("approved");
|
|
152
|
+
expect(approved?.sessionId).toBeNull();
|
|
153
|
+
|
|
154
|
+
// No session row should exist — no sessionId was provided
|
|
155
|
+
const rows = db.query("SELECT * FROM sessions").all();
|
|
156
|
+
expect(rows).toHaveLength(0);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
describe("deletePlan", () => {
|
|
161
|
+
test("success: deletes approved plan with all done tasks", () => {
|
|
162
|
+
const plan = makePlan({ status: "approved" });
|
|
163
|
+
// Create tasks — all done
|
|
164
|
+
const tasks = createTasksBatch(db, plan.id, [
|
|
165
|
+
{
|
|
166
|
+
orderIndex: 0,
|
|
167
|
+
description: "task1",
|
|
168
|
+
agent: "test",
|
|
169
|
+
files: [],
|
|
170
|
+
complexity: 1,
|
|
171
|
+
dependencies: [],
|
|
172
|
+
createdBy: "test",
|
|
173
|
+
updatedBy: "test",
|
|
174
|
+
sourceSessionId: null,
|
|
175
|
+
sourceMessageId: null,
|
|
176
|
+
reviewedBy: null,
|
|
177
|
+
tokensUsed: null,
|
|
178
|
+
durationMs: null,
|
|
179
|
+
artifacts: [],
|
|
180
|
+
metadata: {},
|
|
181
|
+
},
|
|
182
|
+
]);
|
|
183
|
+
// Mark task as done
|
|
184
|
+
db.query("UPDATE plan_tasks SET status = 'done' WHERE id = ?").run(tasks[0]?.id as string);
|
|
185
|
+
|
|
186
|
+
const result = deletePlan(db, plan.id, { confirm: true });
|
|
187
|
+
|
|
188
|
+
expect(result.planId).toBe(plan.id);
|
|
189
|
+
expect(result.slug).toBe("test-plan");
|
|
190
|
+
expect(result.tasksDeleted).toBe(1);
|
|
191
|
+
expect(result.sessionsUnlinked).toBe(0);
|
|
192
|
+
|
|
193
|
+
// Verify plan is gone
|
|
194
|
+
expect(getPlan(db, plan.id)).toBeNull();
|
|
195
|
+
// Verify tasks are gone (CASCADE)
|
|
196
|
+
const taskRows = db.query("SELECT * FROM plan_tasks WHERE plan_id = ?").all(plan.id);
|
|
197
|
+
expect(taskRows).toHaveLength(0);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("rejects draft plan", () => {
|
|
201
|
+
const plan = makePlan({ status: "draft" });
|
|
202
|
+
|
|
203
|
+
expect(() => deletePlan(db, plan.id, { confirm: true })).toThrow("cannot delete a draft plan");
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("rejects plan with pending/running tasks", () => {
|
|
207
|
+
const plan = makePlan({ status: "approved" });
|
|
208
|
+
createTasksBatch(db, plan.id, [
|
|
209
|
+
{
|
|
210
|
+
orderIndex: 0,
|
|
211
|
+
description: "active-task",
|
|
212
|
+
agent: "test",
|
|
213
|
+
files: [],
|
|
214
|
+
complexity: 1,
|
|
215
|
+
dependencies: [],
|
|
216
|
+
createdBy: "test",
|
|
217
|
+
updatedBy: "test",
|
|
218
|
+
sourceSessionId: null,
|
|
219
|
+
sourceMessageId: null,
|
|
220
|
+
reviewedBy: null,
|
|
221
|
+
tokensUsed: null,
|
|
222
|
+
durationMs: null,
|
|
223
|
+
artifacts: [],
|
|
224
|
+
metadata: {},
|
|
225
|
+
},
|
|
226
|
+
]);
|
|
227
|
+
|
|
228
|
+
expect(() => deletePlan(db, plan.id, { confirm: true })).toThrow("active task(s)");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("rejects without confirm: true", () => {
|
|
232
|
+
const plan = makePlan({ status: "approved" });
|
|
233
|
+
|
|
234
|
+
expect(() => deletePlan(db, plan.id, { confirm: false })).toThrow("confirm: true");
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe("createPlan", () => {
|
|
239
|
+
test("sessionId=null succeeds", () => {
|
|
240
|
+
const plan = makePlan({ sessionId: null });
|
|
241
|
+
|
|
242
|
+
expect(plan).not.toBeNull();
|
|
243
|
+
expect(plan.sessionId).toBeNull();
|
|
244
|
+
|
|
245
|
+
const fetched = getPlan(db, plan.id);
|
|
246
|
+
expect(fetched).not.toBeNull();
|
|
247
|
+
expect(fetched?.sessionId).toBeNull();
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ─── Critical 1: updatePlanStatus executed_by via wrapper-style call ─────────
|
|
252
|
+
|
|
253
|
+
describe("updatePlanStatus — executed_by on 'executing' (wrapper-style)", () => {
|
|
254
|
+
test("executing → sets executed_by_agent from opts", () => {
|
|
255
|
+
const plan = makePlan();
|
|
256
|
+
updatePlanStatus(db, plan.id, "executing", {
|
|
257
|
+
updatedBy: "js-smith",
|
|
258
|
+
sessionId: "ses_exec_1",
|
|
259
|
+
executedByAgent: "js-smith",
|
|
260
|
+
executedBySession: "ses_exec_1",
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
const fetched = getPlan(db, plan.id);
|
|
264
|
+
expect(fetched?.executedByAgent).toBe("js-smith");
|
|
265
|
+
expect(fetched?.executedBySession).toBe("ses_exec_1");
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("executing → completed → executed_by_agent does NOT change (write-once)", () => {
|
|
269
|
+
const plan = makePlan();
|
|
270
|
+
updatePlanStatus(db, plan.id, "executing", {
|
|
271
|
+
updatedBy: "js-smith",
|
|
272
|
+
sessionId: "ses_exec_1",
|
|
273
|
+
executedByAgent: "js-smith",
|
|
274
|
+
executedBySession: "ses_exec_1",
|
|
275
|
+
});
|
|
276
|
+
updatePlanStatus(db, plan.id, "completed", { updatedBy: "js-smith" });
|
|
277
|
+
|
|
278
|
+
const fetched = getPlan(db, plan.id);
|
|
279
|
+
expect(fetched?.executedByAgent).toBe("js-smith");
|
|
280
|
+
expect(fetched?.executedBySession).toBe("ses_exec_1");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("approved → does NOT set executed_by_agent", () => {
|
|
284
|
+
const plan = makePlan();
|
|
285
|
+
updatePlanStatus(db, plan.id, "approved", {
|
|
286
|
+
updatedBy: "js-smith",
|
|
287
|
+
sessionId: "ses_appr",
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const fetched = getPlan(db, plan.id);
|
|
291
|
+
expect(fetched?.executedByAgent).toBeNull();
|
|
292
|
+
expect(fetched?.executedBySession).toBeNull();
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
describe("plan_progress view", () => {
|
|
297
|
+
test("excludes archived plans from progress", () => {
|
|
298
|
+
// Create two plans
|
|
299
|
+
const active = makePlan({ slug: "active-plan", title: "Active" });
|
|
300
|
+
const archived = makePlan({ slug: "archived-plan", title: "Archived" });
|
|
301
|
+
|
|
302
|
+
// Add tasks to both
|
|
303
|
+
createTasksBatch(db, active.id, [
|
|
304
|
+
{
|
|
305
|
+
orderIndex: 0,
|
|
306
|
+
description: "task1",
|
|
307
|
+
agent: "test",
|
|
308
|
+
files: [],
|
|
309
|
+
complexity: 1,
|
|
310
|
+
dependencies: [],
|
|
311
|
+
createdBy: "test",
|
|
312
|
+
updatedBy: "test",
|
|
313
|
+
sourceSessionId: null,
|
|
314
|
+
sourceMessageId: null,
|
|
315
|
+
reviewedBy: null,
|
|
316
|
+
tokensUsed: null,
|
|
317
|
+
durationMs: null,
|
|
318
|
+
artifacts: [],
|
|
319
|
+
metadata: {},
|
|
320
|
+
},
|
|
321
|
+
]);
|
|
322
|
+
createTasksBatch(db, archived.id, [
|
|
323
|
+
{
|
|
324
|
+
orderIndex: 0,
|
|
325
|
+
description: "task2",
|
|
326
|
+
agent: "test",
|
|
327
|
+
files: [],
|
|
328
|
+
complexity: 1,
|
|
329
|
+
dependencies: [],
|
|
330
|
+
createdBy: "test",
|
|
331
|
+
updatedBy: "test",
|
|
332
|
+
sourceSessionId: null,
|
|
333
|
+
sourceMessageId: null,
|
|
334
|
+
reviewedBy: null,
|
|
335
|
+
tokensUsed: null,
|
|
336
|
+
durationMs: null,
|
|
337
|
+
artifacts: [],
|
|
338
|
+
metadata: {},
|
|
339
|
+
},
|
|
340
|
+
]);
|
|
341
|
+
|
|
342
|
+
// Archive the second plan
|
|
343
|
+
db.query("UPDATE plans SET archived_at = ? WHERE id = ?").run(Date.now(), archived.id);
|
|
344
|
+
db.query("UPDATE plan_tasks SET archived_at = ? WHERE plan_id = ?").run(
|
|
345
|
+
Date.now(),
|
|
346
|
+
archived.id,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const progress = getPlanProgress(db);
|
|
350
|
+
expect(progress).toHaveLength(1);
|
|
351
|
+
expect(progress[0]?.planId).toBe(active.id);
|
|
352
|
+
expect(progress[0]?.slug).toBe("active-plan");
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
// ─── Issue 2: plan_files JOIN ──────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
describe("getPlan — plan_files JOIN", () => {
|
|
359
|
+
test("returns files array when plan has files", () => {
|
|
360
|
+
const plan = makePlan();
|
|
361
|
+
|
|
362
|
+
// Insert files
|
|
363
|
+
db.query("INSERT INTO plan_files (plan_id, file_path, role) VALUES (?, ?, ?)").run(
|
|
364
|
+
plan.id,
|
|
365
|
+
"src/index.ts",
|
|
366
|
+
"input",
|
|
367
|
+
);
|
|
368
|
+
db.query("INSERT INTO plan_files (plan_id, file_path, role) VALUES (?, ?, ?)").run(
|
|
369
|
+
plan.id,
|
|
370
|
+
"README.md",
|
|
371
|
+
"reference",
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
const fetched = getPlan(db, plan.id);
|
|
375
|
+
expect(fetched).not.toBeNull();
|
|
376
|
+
expect(fetched?.files).toHaveLength(2);
|
|
377
|
+
expect(fetched?.files?.[0]).toEqual({ filePath: "README.md", role: "reference" });
|
|
378
|
+
expect(fetched?.files?.[1]).toEqual({ filePath: "src/index.ts", role: "input" });
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("returns empty files array when plan has no files", () => {
|
|
382
|
+
const plan = makePlan();
|
|
383
|
+
const fetched = getPlan(db, plan.id);
|
|
384
|
+
expect(fetched).not.toBeNull();
|
|
385
|
+
expect(fetched?.files).toEqual([]);
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe("getPlanBySlug — plan_files JOIN", () => {
|
|
390
|
+
test("returns files array when plan has files", () => {
|
|
391
|
+
const plan = makePlan({ slug: "slug-test" });
|
|
392
|
+
|
|
393
|
+
db.query("INSERT INTO plan_files (plan_id, file_path, role) VALUES (?, ?, ?)").run(
|
|
394
|
+
plan.id,
|
|
395
|
+
"src/utils.ts",
|
|
396
|
+
"modified",
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
const fetched = getPlanBySlug(db, "slug-test");
|
|
400
|
+
expect(fetched).not.toBeNull();
|
|
401
|
+
expect(fetched?.files).toHaveLength(1);
|
|
402
|
+
expect(fetched?.files?.[0]).toEqual({ filePath: "src/utils.ts", role: "modified" });
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("returns empty files array when plan has no files", () => {
|
|
406
|
+
makePlan({ slug: "no-files" });
|
|
407
|
+
const fetched = getPlanBySlug(db, "no-files");
|
|
408
|
+
expect(fetched).not.toBeNull();
|
|
409
|
+
expect(fetched?.files).toEqual([]);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe("listPlans — plan_files JOIN", () => {
|
|
414
|
+
test("returns files for each plan", () => {
|
|
415
|
+
const plan1 = makePlan({ slug: "plan-1" });
|
|
416
|
+
const plan2 = makePlan({ slug: "plan-2" });
|
|
417
|
+
|
|
418
|
+
db.query("INSERT INTO plan_files (plan_id, file_path, role) VALUES (?, ?, ?)").run(
|
|
419
|
+
plan1.id,
|
|
420
|
+
"src/a.ts",
|
|
421
|
+
"input",
|
|
422
|
+
);
|
|
423
|
+
db.query("INSERT INTO plan_files (plan_id, file_path, role) VALUES (?, ?, ?)").run(
|
|
424
|
+
plan2.id,
|
|
425
|
+
"src/b.ts",
|
|
426
|
+
"output",
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const plans = listPlans(db);
|
|
430
|
+
expect(plans).toHaveLength(2);
|
|
431
|
+
|
|
432
|
+
const p1 = plans.find((p) => p.id === plan1.id);
|
|
433
|
+
const p2 = plans.find((p) => p.id === plan2.id);
|
|
434
|
+
|
|
435
|
+
expect(p1?.files).toHaveLength(1);
|
|
436
|
+
expect(p1?.files?.[0]).toEqual({ filePath: "src/a.ts", role: "input" });
|
|
437
|
+
expect(p2?.files).toHaveLength(1);
|
|
438
|
+
expect(p2?.files?.[0]).toEqual({ filePath: "src/b.ts", role: "output" });
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
test("returns empty files for plans without files", () => {
|
|
442
|
+
makePlan({ slug: "empty-1" });
|
|
443
|
+
makePlan({ slug: "empty-2" });
|
|
444
|
+
|
|
445
|
+
const plans = listPlans(db);
|
|
446
|
+
expect(plans).toHaveLength(2);
|
|
447
|
+
expect(plans[0]?.files).toEqual([]);
|
|
448
|
+
expect(plans[1]?.files).toEqual([]);
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// ─── M5: original_plan_data snapshot completeness ───────────────────────────
|
|
453
|
+
|
|
454
|
+
describe("createPlan — original_plan_data snapshot (M5)", () => {
|
|
455
|
+
test("snapshot includes files array", () => {
|
|
456
|
+
const plan = makePlan({ files: [{ filePath: "src/index.ts", role: "input" }] });
|
|
457
|
+
const snapshot = JSON.parse(plan.originalPlanData ?? "{}");
|
|
458
|
+
|
|
459
|
+
expect(snapshot.files).toEqual([{ filePath: "src/index.ts", role: "input" }]);
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
test("snapshot includes metadata object", () => {
|
|
463
|
+
const plan = makePlan({
|
|
464
|
+
metadata: { category: "feature", externalRefs: { jiraTicket: "ND-42" } },
|
|
465
|
+
});
|
|
466
|
+
const snapshot = JSON.parse(plan.originalPlanData ?? "{}");
|
|
467
|
+
|
|
468
|
+
expect(snapshot.metadata).toEqual({
|
|
469
|
+
category: "feature",
|
|
470
|
+
externalRefs: { jiraTicket: "ND-42" },
|
|
471
|
+
});
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test("snapshot includes approach, priority, complexity", () => {
|
|
475
|
+
const plan = makePlan({ approach: "top-down", priority: 1, complexity: 5 });
|
|
476
|
+
const snapshot = JSON.parse(plan.originalPlanData ?? "{}");
|
|
477
|
+
|
|
478
|
+
expect(snapshot.approach).toBe("top-down");
|
|
479
|
+
expect(snapshot.priority).toBe(1);
|
|
480
|
+
expect(snapshot.complexity).toBe(5);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
test("snapshot includes empty arrays/objects when fields omitted", () => {
|
|
484
|
+
const plan = makePlan();
|
|
485
|
+
const snapshot = JSON.parse(plan.originalPlanData ?? "{}");
|
|
486
|
+
|
|
487
|
+
expect(snapshot.files).toEqual([]);
|
|
488
|
+
expect(snapshot.metadata).toEqual({});
|
|
489
|
+
});
|
|
490
|
+
});
|