ultra-builder-pro-cli 0.13.0 → 0.15.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/.ultra-template/docs/research/README.md +11 -7
- package/.ultra-template/specs/architecture.md +36 -36
- package/.ultra-template/specs/discovery.md +46 -33
- package/.ultra-template/specs/product.md +38 -29
- package/.ultra-template/specs/research-distillate.md +48 -0
- package/.ultra-template/tasks/contexts/TEMPLATE.md +11 -28
- package/CHANGELOG.md +86 -1
- package/README.md +40 -26
- package/adapters/_shared/codex-assets.cjs +29 -165
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/runtime-assets.cjs +1 -0
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +33 -122
- package/agents/review-coordinator.md +9 -6
- package/docs/AGENT-CONTEXT.md +21 -11
- package/docs/ARCHITECTURE.md +41 -13
- package/docs/PLAN.zh-CN.md +3 -2
- package/docs/ROADMAP.md +9 -5
- package/docs/RUNTIME-COMPAT-MATRIX.md +6 -6
- package/docs/STATE-DB-ACCESS-POLICY.md +27 -25
- package/docs/WORKFLOW-LIFECYCLE.md +266 -0
- package/hooks/README.md +6 -1
- package/hooks/active_task_context.py +22 -50
- package/hooks/context_spine.py +61 -332
- package/hooks/health_check.py +29 -18
- package/hooks/pre_stop_check.py +28 -33
- package/hooks/subagent_tracker.py +54 -85
- package/hooks/workflow_checkpoint.py +39 -47
- package/hooks/workflow_context.py +4 -9
- package/hooks/workflow_resume.py +48 -128
- package/mcp-server/breadcrumb.cjs +22 -0
- package/mcp-server/hook-event.cjs +34 -0
- package/mcp-server/lib/archive-journal.cjs +17 -5
- package/mcp-server/lib/baseline-workflow.cjs +158 -18
- package/mcp-server/lib/change-workflow.cjs +550 -85
- package/mcp-server/lib/context-spine.cjs +242 -24
- package/mcp-server/lib/doctor.cjs +33 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/prd-parser.cjs +14 -41
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +145 -0
- package/mcp-server/lib/projector.cjs +57 -3
- package/mcp-server/lib/spec-learning.cjs +79 -2
- package/mcp-server/lib/state-db.cjs +145 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/task-expander.cjs +9 -38
- package/mcp-server/lib/workflow-state.cjs +2292 -0
- package/mcp-server/server.cjs +182 -50
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +14 -6
- package/skills/cc-collab/SKILL.md +2 -2
- package/skills/codex-collab/SKILL.md +2 -3
- package/skills/ultra-change/SKILL.md +53 -68
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +69 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +54 -59
- package/skills/ultra-doctor/SKILL.md +40 -71
- package/skills/ultra-init/SKILL.md +96 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +79 -75
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +87 -60
- package/skills/ultra-research/references/00-problem-validation.md +36 -0
- package/skills/ultra-research/references/01-opportunity-discovery.md +33 -0
- package/skills/ultra-research/references/02-market-assessment.md +30 -0
- package/skills/ultra-research/references/03-competitive-landscape.md +31 -0
- package/skills/ultra-research/references/04-product-strategy.md +32 -0
- package/skills/ultra-research/references/05-assumptions-validation.md +31 -0
- package/skills/ultra-research/references/10-user-personas.md +29 -0
- package/skills/ultra-research/references/11-user-scenarios.md +28 -0
- package/skills/ultra-research/references/20-user-stories.md +30 -0
- package/skills/ultra-research/references/21-features-scope.md +29 -0
- package/skills/ultra-research/references/22-success-metrics.md +28 -0
- package/skills/ultra-research/references/30-architecture-context.md +30 -0
- package/skills/ultra-research/references/31-solution-strategy.md +28 -0
- package/skills/ultra-research/references/32-building-blocks.md +28 -0
- package/skills/ultra-research/references/40-deployment.md +29 -0
- package/skills/ultra-research/references/41-quality-risks.md +28 -0
- package/skills/ultra-research/references/99-synthesis.md +35 -0
- package/skills/ultra-review/SKILL.md +57 -73
- package/skills/ultra-review/references/review-modes.md +27 -0
- package/skills/ultra-review/references/unified-schema.md +25 -0
- package/skills/ultra-review/scripts/review_wait.py +40 -3
- package/skills/ultra-status/SKILL.md +41 -45
- package/skills/ultra-test/SKILL.md +48 -55
- package/skills/ultra-verify/SKILL.md +22 -31
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/skills/ultra-verify/scripts/verify_wait.py +54 -54
- package/spec/cli-protocol.md +7 -2
- package/spec/mcp-tools.yaml +422 -73
- package/spec/schemas/baseline-reconciliation.v1.schema.json +73 -0
- package/spec/schemas/delivery-report.v1.schema.json +111 -0
- package/spec/schemas/mcp-tools.schema.json +1 -1
- package/spec/schemas/state-db.sql +82 -1
- package/spec/schemas/tasks.v4.5.schema.json +8 -1
- package/spec/schemas/test-report.v1.schema.json +194 -0
- package/ultra-tools/cli.cjs +1 -1
- package/ultra-tools/commands/migrate.cjs +3 -3
- package/ultra-tools/commands/status.cjs +213 -12
- package/.ultra-template/delivery-report.json +0 -9
- package/.ultra-template/test-report.json +0 -8
- package/adapters/_shared/tests/file-ops.test.cjs +0 -54
- package/adapters/_shared/tests/frontmatter.test.cjs +0 -47
- package/adapters/_shared/tests/handbook.test.cjs +0 -101
- package/adapters/_shared/tests/path-rewrite.test.cjs +0 -60
- package/adapters/_shared/tests/provenance.test.cjs +0 -133
- package/adapters/_shared/tests/resolve-target.test.cjs +0 -75
- package/adapters/_shared/tests/runtime-assets.test.cjs +0 -131
- package/adapters/_shared/tests/settings-merge.test.cjs +0 -85
- package/adapters/_shared/tests/validate.test.cjs +0 -30
- package/adapters/tests/claude.test.cjs +0 -137
- package/adapters/tests/codex-hook.test.cjs +0 -115
- package/adapters/tests/codex.test.cjs +0 -423
- package/adapters/tests/kimi-hook.test.cjs +0 -122
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -371
- package/mcp-server/lib/archive-journal.test.cjs +0 -35
- package/mcp-server/lib/baseline-workflow.test.cjs +0 -477
- package/mcp-server/lib/change-workflow.test.cjs +0 -971
- package/mcp-server/lib/context-spine.test.cjs +0 -93
- package/mcp-server/lib/doctor.test.cjs +0 -148
- package/mcp-server/lib/llm-client.cjs +0 -157
- package/mcp-server/lib/llm-client.test.cjs +0 -156
- package/mcp-server/lib/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -171
- package/mcp-server/lib/runtime-state.test.cjs +0 -92
- package/mcp-server/lib/spec-learning.test.cjs +0 -67
- package/mcp-server/lib/state-db.test.cjs +0 -314
- package/mcp-server/lib/task-expander.test.cjs +0 -324
- package/mcp-server/lib/topo.test.cjs +0 -158
- package/mcp-server/tests/circuit-breaker.test.cjs +0 -125
- package/mcp-server/tests/commit-hash-flow.test.cjs +0 -117
- package/mcp-server/tests/concurrency-update.test.cjs +0 -96
- package/mcp-server/tests/concurrency.test.cjs +0 -112
- package/mcp-server/tests/db-ops.test.cjs +0 -121
- package/mcp-server/tests/init-project.test.cjs +0 -393
- package/mcp-server/tests/migrate.test.cjs +0 -232
- package/mcp-server/tests/projector.test.cjs +0 -162
- package/mcp-server/tests/server-contract.test.cjs +0 -1471
- package/mcp-server/tests/staleness.test.cjs +0 -190
- package/mcp-server/tests/state-db.test.cjs +0 -106
- package/mcp-server/tests/state-ops.test.cjs +0 -449
- package/mcp-server/tests/tagged-tasks.test.cjs +0 -163
- package/mcp-server/tests/telemetry.test.cjs +0 -219
- package/orchestrator/planner/expand-prompt.cjs +0 -62
- package/orchestrator/planner/expand-prompt.test.cjs +0 -52
- package/orchestrator/planner/plan-builder.test.cjs +0 -165
- package/orchestrator/planner/prd-prompt.cjs +0 -44
- package/orchestrator/planner/prd-prompt.test.cjs +0 -30
- package/orchestrator/tests/auto-merge.test.cjs +0 -216
- package/orchestrator/tests/daemon.test.cjs +0 -311
- package/orchestrator/tests/dispatch-rules.test.cjs +0 -124
- package/orchestrator/tests/parallel-orchestrator.test.cjs +0 -233
- package/orchestrator/tests/phase-8b-gate.test.cjs +0 -183
- package/orchestrator/tests/recovery.test.cjs +0 -190
- package/orchestrator/tests/session-runner.test.cjs +0 -314
- package/orchestrator/tests/skill-miner.test.cjs +0 -147
- package/orchestrator/tests/worktree-manager.test.cjs +0 -148
- package/skills/ultra-research/references/architecture.md +0 -38
- package/skills/ultra-research/references/problem-product.md +0 -39
- package/skills/ultra-research/references/quality-delivery.md +0 -34
- package/skills/ultra-research/references/synthesis.md +0 -38
- package/skills/ultra-research/references/users-scope.md +0 -40
- package/ultra-tools/legacy-memory.test.cjs +0 -80
- package/ultra-tools/status-command.test.cjs +0 -181
- package/ultra-tools/system-command.test.cjs +0 -309
|
@@ -1,233 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Phase 8B.2 — Parallel orchestrator: consumes execution-plan.json waves,
|
|
4
|
-
// spawns sessions via session-runner, respects parallel vs serial per wave,
|
|
5
|
-
// and emits wave/plan events.
|
|
6
|
-
//
|
|
7
|
-
// AC (PLAN §6 Phase 8B.2):
|
|
8
|
-
// - 10 independent-file tasks run in parallel
|
|
9
|
-
// - 2 tasks sharing a file serialize (plan-builder flips wave.parallel=false)
|
|
10
|
-
// - wave boundary strict: wave N+1 waits on wave N close
|
|
11
|
-
// - Phase 5.4 daemon tests stay green (no shared state)
|
|
12
|
-
|
|
13
|
-
const { test } = require('node:test');
|
|
14
|
-
const assert = require('node:assert/strict');
|
|
15
|
-
const fs = require('node:fs');
|
|
16
|
-
const os = require('node:os');
|
|
17
|
-
const path = require('node:path');
|
|
18
|
-
const { execFileSync } = require('node:child_process');
|
|
19
|
-
|
|
20
|
-
const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
|
|
21
|
-
const ops = require('../../mcp-server/lib/state-ops.cjs');
|
|
22
|
-
const { buildPlan } = require('../planner/plan-builder.cjs');
|
|
23
|
-
const parallelOrch = require('../parallel-orchestrator.cjs');
|
|
24
|
-
|
|
25
|
-
// Test-Double rationale: we don't invoke real LLM runtimes; a short-lived
|
|
26
|
-
// Node subprocess is enough to exercise spawn → exit → session.close flow.
|
|
27
|
-
const NODE = process.execPath;
|
|
28
|
-
function exitOk(delayMs = 20) { return ['-e', `setTimeout(() => process.exit(0), ${delayMs})`]; }
|
|
29
|
-
function exitFail() { return ['-e', 'process.exit(1)']; }
|
|
30
|
-
|
|
31
|
-
function mkRepo() {
|
|
32
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-parorch-'));
|
|
33
|
-
execFileSync('git', ['init', '-q'], { cwd: dir });
|
|
34
|
-
execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: dir });
|
|
35
|
-
execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: dir });
|
|
36
|
-
fs.writeFileSync(path.join(dir, 'seed.md'), '# seed\n');
|
|
37
|
-
execFileSync('git', ['add', '-A'], { cwd: dir });
|
|
38
|
-
execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: dir });
|
|
39
|
-
return dir;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function mkDb(repoRoot) {
|
|
43
|
-
const dbPath = path.join(repoRoot, '.ultra', 'state.db');
|
|
44
|
-
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
45
|
-
const { db } = initStateDb(dbPath);
|
|
46
|
-
return db;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function seedTask(db, id, files_modified = [], deps = []) {
|
|
50
|
-
ops.createTask(db, {
|
|
51
|
-
id, title: `task ${id}`, type: 'feature', priority: 'P2',
|
|
52
|
-
complexity: 2, files_modified, deps,
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function cleanup(repoRoot, db) {
|
|
57
|
-
try { closeStateDb(db); } catch (_) { /* best-effort */ }
|
|
58
|
-
try { fs.rmSync(repoRoot, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ─── happy path ───────────────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
test('runPlan: 1 wave 1 task → task.status=completed + plan_completed event', async () => {
|
|
64
|
-
const repo = mkRepo();
|
|
65
|
-
const db = mkDb(repo);
|
|
66
|
-
try {
|
|
67
|
-
seedTask(db, 't1');
|
|
68
|
-
const plan = buildPlan([{ id: 't1', deps: [], complexity: 2 }]);
|
|
69
|
-
await parallelOrch.runPlan({
|
|
70
|
-
db, repoRoot: repo, plan,
|
|
71
|
-
runtimes: ['claude'],
|
|
72
|
-
command: NODE, commandArgs: exitOk(),
|
|
73
|
-
});
|
|
74
|
-
const t = ops.readTask(db, 't1');
|
|
75
|
-
assert.equal(t.status, 'completed');
|
|
76
|
-
const { events } = ops.subscribeEventsSince(db, 0);
|
|
77
|
-
const types = events.map((e) => e.type);
|
|
78
|
-
assert.ok(types.includes('wave_started'));
|
|
79
|
-
assert.ok(types.includes('wave_completed'));
|
|
80
|
-
assert.ok(types.includes('plan_completed'));
|
|
81
|
-
} finally { cleanup(repo, db); }
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test('runPlan: 2-wave 6-task independent files → all parallel, all completed', async () => {
|
|
85
|
-
const repo = mkRepo();
|
|
86
|
-
const db = mkDb(repo);
|
|
87
|
-
try {
|
|
88
|
-
const tasks = [];
|
|
89
|
-
for (let i = 1; i <= 3; i++) {
|
|
90
|
-
seedTask(db, `a${i}`, [`dir/a${i}.js`]);
|
|
91
|
-
tasks.push({ id: `a${i}`, deps: [], complexity: 1, files_modified: [`dir/a${i}.js`] });
|
|
92
|
-
}
|
|
93
|
-
for (let i = 1; i <= 3; i++) {
|
|
94
|
-
seedTask(db, `b${i}`, [`dir/b${i}.js`], [`a${i}`]);
|
|
95
|
-
tasks.push({ id: `b${i}`, deps: [`a${i}`], complexity: 1, files_modified: [`dir/b${i}.js`] });
|
|
96
|
-
}
|
|
97
|
-
const plan = buildPlan(tasks);
|
|
98
|
-
assert.equal(plan.waves.length, 2, 'topo should yield 2 waves');
|
|
99
|
-
assert.equal(plan.waves[0].parallel, true);
|
|
100
|
-
assert.equal(plan.waves[1].parallel, true);
|
|
101
|
-
|
|
102
|
-
await parallelOrch.runPlan({
|
|
103
|
-
db, repoRoot: repo, plan,
|
|
104
|
-
runtimes: ['claude'],
|
|
105
|
-
command: NODE, commandArgs: exitOk(30),
|
|
106
|
-
});
|
|
107
|
-
for (const t of tasks) {
|
|
108
|
-
assert.equal(ops.readTask(db, t.id).status, 'completed', `${t.id} should complete`);
|
|
109
|
-
}
|
|
110
|
-
} finally { cleanup(repo, db); }
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('runPlan: 2 tasks sharing one file → plan-builder flips parallel=false → serial exec', async () => {
|
|
114
|
-
const repo = mkRepo();
|
|
115
|
-
const db = mkDb(repo);
|
|
116
|
-
try {
|
|
117
|
-
seedTask(db, 'c1', ['shared.js']);
|
|
118
|
-
seedTask(db, 'c2', ['shared.js']);
|
|
119
|
-
const plan = buildPlan([
|
|
120
|
-
{ id: 'c1', deps: [], complexity: 1, files_modified: ['shared.js'] },
|
|
121
|
-
{ id: 'c2', deps: [], complexity: 1, files_modified: ['shared.js'] },
|
|
122
|
-
]);
|
|
123
|
-
assert.equal(plan.waves[0].parallel, false, 'plan-builder should detect conflict');
|
|
124
|
-
|
|
125
|
-
await parallelOrch.runPlan({
|
|
126
|
-
db, repoRoot: repo, plan,
|
|
127
|
-
runtimes: ['claude'],
|
|
128
|
-
command: NODE, commandArgs: exitOk(30),
|
|
129
|
-
});
|
|
130
|
-
assert.equal(ops.readTask(db, 'c1').status, 'completed');
|
|
131
|
-
assert.equal(ops.readTask(db, 'c2').status, 'completed');
|
|
132
|
-
} finally { cleanup(repo, db); }
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
test('runPlan: wave boundary — wave 2 does not start until wave 1 closes', async () => {
|
|
136
|
-
const repo = mkRepo();
|
|
137
|
-
const db = mkDb(repo);
|
|
138
|
-
try {
|
|
139
|
-
seedTask(db, 'w1');
|
|
140
|
-
seedTask(db, 'w2', [], ['w1']);
|
|
141
|
-
const plan = buildPlan([
|
|
142
|
-
{ id: 'w1', deps: [] },
|
|
143
|
-
{ id: 'w2', deps: ['w1'] },
|
|
144
|
-
]);
|
|
145
|
-
assert.equal(plan.waves.length, 2);
|
|
146
|
-
|
|
147
|
-
await parallelOrch.runPlan({
|
|
148
|
-
db, repoRoot: repo, plan,
|
|
149
|
-
runtimes: ['claude'],
|
|
150
|
-
command: NODE, commandArgs: exitOk(40),
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
const { events } = ops.subscribeEventsSince(db, 0);
|
|
154
|
-
const spawns = events.filter((e) => e.type === 'session_spawned');
|
|
155
|
-
const closes = events.filter((e) => e.type === 'session_closed');
|
|
156
|
-
const w2SpawnIdx = events.findIndex((e) => e.type === 'session_spawned' && e.task_id === 'w2');
|
|
157
|
-
const w1CloseIdx = events.findIndex((e) => e.type === 'session_closed' && e.task_id === 'w1');
|
|
158
|
-
assert.ok(w1CloseIdx > -1 && w2SpawnIdx > -1, 'both events should exist');
|
|
159
|
-
assert.ok(w1CloseIdx < w2SpawnIdx, 'w1 must close before w2 spawns');
|
|
160
|
-
assert.equal(spawns.length, 2);
|
|
161
|
-
assert.equal(closes.length, 2);
|
|
162
|
-
} finally { cleanup(repo, db); }
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// ─── failure path ─────────────────────────────────────────────────────────
|
|
166
|
-
|
|
167
|
-
test('runPlan: task exits non-zero → session crashed, task stays pending', async () => {
|
|
168
|
-
const repo = mkRepo();
|
|
169
|
-
const db = mkDb(repo);
|
|
170
|
-
try {
|
|
171
|
-
seedTask(db, 'fail1');
|
|
172
|
-
const plan = buildPlan([{ id: 'fail1', deps: [] }]);
|
|
173
|
-
await parallelOrch.runPlan({
|
|
174
|
-
db, repoRoot: repo, plan,
|
|
175
|
-
runtimes: ['claude'],
|
|
176
|
-
command: NODE, commandArgs: exitFail(),
|
|
177
|
-
});
|
|
178
|
-
const t = ops.readTask(db, 'fail1');
|
|
179
|
-
assert.equal(t.status, 'pending', 'failed task stays pending for recovery');
|
|
180
|
-
const { events } = ops.subscribeEventsSince(db, 0);
|
|
181
|
-
const crashed = events.find((e) => e.type === 'session_crashed');
|
|
182
|
-
assert.ok(crashed, 'session_crashed event expected');
|
|
183
|
-
} finally { cleanup(repo, db); }
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
test('runPlan: one task fails in wave, others still complete (no short-circuit)', async () => {
|
|
187
|
-
const repo = mkRepo();
|
|
188
|
-
const db = mkDb(repo);
|
|
189
|
-
try {
|
|
190
|
-
seedTask(db, 'ok1', ['f1.js']);
|
|
191
|
-
seedTask(db, 'bad1', ['f2.js']);
|
|
192
|
-
seedTask(db, 'ok2', ['f3.js']);
|
|
193
|
-
const plan = buildPlan([
|
|
194
|
-
{ id: 'ok1', deps: [], files_modified: ['f1.js'] },
|
|
195
|
-
{ id: 'bad1', deps: [], files_modified: ['f2.js'] },
|
|
196
|
-
{ id: 'ok2', deps: [], files_modified: ['f3.js'] },
|
|
197
|
-
]);
|
|
198
|
-
await parallelOrch.runPlan({
|
|
199
|
-
db, repoRoot: repo, plan,
|
|
200
|
-
runtimes: ['claude'],
|
|
201
|
-
command: NODE,
|
|
202
|
-
commandArgsFor: (task) => (task.id === 'bad1' ? exitFail() : exitOk(20)),
|
|
203
|
-
});
|
|
204
|
-
assert.equal(ops.readTask(db, 'ok1').status, 'completed');
|
|
205
|
-
assert.equal(ops.readTask(db, 'ok2').status, 'completed');
|
|
206
|
-
assert.equal(ops.readTask(db, 'bad1').status, 'pending');
|
|
207
|
-
} finally { cleanup(repo, db); }
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
test('runPlan: emits wave_started/wave_completed per wave + one plan_completed', async () => {
|
|
211
|
-
const repo = mkRepo();
|
|
212
|
-
const db = mkDb(repo);
|
|
213
|
-
try {
|
|
214
|
-
seedTask(db, 'e1');
|
|
215
|
-
seedTask(db, 'e2', [], ['e1']);
|
|
216
|
-
const plan = buildPlan([
|
|
217
|
-
{ id: 'e1', deps: [] },
|
|
218
|
-
{ id: 'e2', deps: ['e1'] },
|
|
219
|
-
]);
|
|
220
|
-
await parallelOrch.runPlan({
|
|
221
|
-
db, repoRoot: repo, plan,
|
|
222
|
-
runtimes: ['claude'],
|
|
223
|
-
command: NODE, commandArgs: exitOk(20),
|
|
224
|
-
});
|
|
225
|
-
const { events } = ops.subscribeEventsSince(db, 0);
|
|
226
|
-
const waveStarts = events.filter((e) => e.type === 'wave_started');
|
|
227
|
-
const waveCloses = events.filter((e) => e.type === 'wave_completed');
|
|
228
|
-
const planCloses = events.filter((e) => e.type === 'plan_completed');
|
|
229
|
-
assert.equal(waveStarts.length, 2);
|
|
230
|
-
assert.equal(waveCloses.length, 2);
|
|
231
|
-
assert.equal(planCloses.length, 1);
|
|
232
|
-
} finally { cleanup(repo, db); }
|
|
233
|
-
});
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Phase 8B gate — end-to-end integration verifying the three PLAN gate clauses:
|
|
4
|
-
//
|
|
5
|
-
// 1. "10-task PRD run → auto-completion rate ≥ 80%"
|
|
6
|
-
// 2. "5-slice worktree stress → no .git/config.lock contention"
|
|
7
|
-
// 3. "merge-back conflicts correctly identified"
|
|
8
|
-
//
|
|
9
|
-
// All use the Test-Double command (`node -e '...'`) so there's no LLM cost;
|
|
10
|
-
// Phase 8A.1 LLM-backed parse_prd is out of scope here.
|
|
11
|
-
|
|
12
|
-
const { test } = require('node:test');
|
|
13
|
-
const assert = require('node:assert/strict');
|
|
14
|
-
const fs = require('node:fs');
|
|
15
|
-
const os = require('node:os');
|
|
16
|
-
const path = require('node:path');
|
|
17
|
-
const { execFileSync } = require('node:child_process');
|
|
18
|
-
|
|
19
|
-
const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
|
|
20
|
-
const ops = require('../../mcp-server/lib/state-ops.cjs');
|
|
21
|
-
const { buildPlan } = require('../planner/plan-builder.cjs');
|
|
22
|
-
const parallelOrch = require('../parallel-orchestrator.cjs');
|
|
23
|
-
const wtmgr = require('../worktree-manager.cjs');
|
|
24
|
-
|
|
25
|
-
const NODE = process.execPath;
|
|
26
|
-
function commitScript(filename, content) {
|
|
27
|
-
// Create a file (with parent dirs) in the session's CWD and commit it —
|
|
28
|
-
// simulates an agent's session work.
|
|
29
|
-
return [
|
|
30
|
-
'-e',
|
|
31
|
-
`
|
|
32
|
-
const fs = require('fs');
|
|
33
|
-
const path = require('path');
|
|
34
|
-
const { execFileSync } = require('child_process');
|
|
35
|
-
const f = ${JSON.stringify(filename)};
|
|
36
|
-
fs.mkdirSync(path.dirname(f) || '.', { recursive: true });
|
|
37
|
-
fs.writeFileSync(f, ${JSON.stringify(content)});
|
|
38
|
-
execFileSync('git', ['add', '-A']);
|
|
39
|
-
execFileSync('git', ['-c', 'user.email=agent@ubp.dev', '-c', 'user.name=agent',
|
|
40
|
-
'commit', '-q', '-m', 'agent change ' + f]);
|
|
41
|
-
process.exit(0);
|
|
42
|
-
`,
|
|
43
|
-
];
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function mkRepo() {
|
|
47
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-8b-gate-'));
|
|
48
|
-
execFileSync('git', ['init', '-q', '-b', 'main'], { cwd: dir });
|
|
49
|
-
execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: dir });
|
|
50
|
-
execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: dir });
|
|
51
|
-
fs.writeFileSync(path.join(dir, 'seed.md'), '# seed\n');
|
|
52
|
-
fs.writeFileSync(path.join(dir, '.gitignore'), '.ultra/\n');
|
|
53
|
-
execFileSync('git', ['add', '-A'], { cwd: dir });
|
|
54
|
-
execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: dir });
|
|
55
|
-
return dir;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function mkDb(repoRoot) {
|
|
59
|
-
const dbPath = path.join(repoRoot, '.ultra', 'state.db');
|
|
60
|
-
fs.mkdirSync(path.dirname(dbPath), { recursive: true });
|
|
61
|
-
const { db } = initStateDb(dbPath);
|
|
62
|
-
return db;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function cleanup(repoRoot, db) {
|
|
66
|
-
try { if (db) closeStateDb(db); } catch (_) { /* best-effort */ }
|
|
67
|
-
try { fs.rmSync(repoRoot, { recursive: true, force: true }); } catch (_) { /* best-effort */ }
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
// ─── Gate clause 1 — 10-task end-to-end, auto-completion ≥ 80% ────────────
|
|
71
|
-
|
|
72
|
-
test('gate 1: 10-task plan with independent files → completion ≥ 80%', async () => {
|
|
73
|
-
const repo = mkRepo();
|
|
74
|
-
const db = mkDb(repo);
|
|
75
|
-
try {
|
|
76
|
-
const tasks = [];
|
|
77
|
-
for (let i = 1; i <= 10; i++) {
|
|
78
|
-
const id = `T${String(i).padStart(2, '0')}`;
|
|
79
|
-
const file = `feat/${id}.txt`;
|
|
80
|
-
ops.createTask(db, {
|
|
81
|
-
id, title: `feature ${i}`,
|
|
82
|
-
type: 'feature', priority: 'P2',
|
|
83
|
-
complexity: 2, files_modified: [file],
|
|
84
|
-
});
|
|
85
|
-
tasks.push({ id, deps: [], complexity: 2, files_modified: [file] });
|
|
86
|
-
}
|
|
87
|
-
const plan = buildPlan(tasks);
|
|
88
|
-
// All files independent → single parallel wave.
|
|
89
|
-
assert.equal(plan.waves.length, 1);
|
|
90
|
-
assert.equal(plan.waves[0].parallel, true);
|
|
91
|
-
|
|
92
|
-
const { results } = await parallelOrch.runPlan({
|
|
93
|
-
db, repoRoot: repo, plan,
|
|
94
|
-
runtimes: ['claude'],
|
|
95
|
-
command: NODE,
|
|
96
|
-
commandArgsFor: (task) => commitScript(task.files_modified[0], `${task.id}\n`),
|
|
97
|
-
autoMerge: true,
|
|
98
|
-
mergeBaseBranch: 'main',
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
const completed = results.filter((r) => r.status === 'completed').length;
|
|
102
|
-
const completionRate = completed / tasks.length;
|
|
103
|
-
assert.ok(completionRate >= 0.8, `completion rate ${completionRate} below 0.8`);
|
|
104
|
-
|
|
105
|
-
// Independent files → all merges clean → main HEAD reflects each file.
|
|
106
|
-
const merged = results.filter((r) => r.merge && r.merge.merged).length;
|
|
107
|
-
assert.ok(merged >= 8, `expected ≥8 clean merges, got ${merged}`);
|
|
108
|
-
for (let i = 1; i <= 10; i++) {
|
|
109
|
-
const id = `T${String(i).padStart(2, '0')}`;
|
|
110
|
-
const file = path.join(repo, 'feat', `${id}.txt`);
|
|
111
|
-
if (fs.existsSync(file)) {
|
|
112
|
-
// if merged, the file must be present on main
|
|
113
|
-
const content = fs.readFileSync(file, 'utf8');
|
|
114
|
-
assert.equal(content, `${id}\n`);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
} finally { cleanup(repo, db); }
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// ─── Gate clause 2 — 5-slice stress, no .git/config.lock contention ───────
|
|
121
|
-
|
|
122
|
-
test('gate 2: 5 rapid worktree allocations → no git lock errors, all tracked', () => {
|
|
123
|
-
const repo = mkRepo();
|
|
124
|
-
try {
|
|
125
|
-
const sids = ['g2-1', 'g2-2', 'g2-3', 'g2-4', 'g2-5'];
|
|
126
|
-
// Back-to-back — Node single-thread execFileSync serializes git calls.
|
|
127
|
-
for (const sid of sids) wtmgr.allocate({ repoRoot: repo, sid });
|
|
128
|
-
const active = wtmgr.listActive(repo);
|
|
129
|
-
assert.equal(active.length, 5);
|
|
130
|
-
const seenSids = new Set(active.map((a) => a.sid));
|
|
131
|
-
for (const sid of sids) assert.ok(seenSids.has(sid), `missing slice ${sid}`);
|
|
132
|
-
// Cleanup and re-check — releaseAll must fully drain.
|
|
133
|
-
wtmgr.releaseAll(repo);
|
|
134
|
-
assert.equal(wtmgr.listActive(repo).length, 0);
|
|
135
|
-
} finally { cleanup(repo); }
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// ─── Gate clause 3 — merge-back conflict identification ───────────────────
|
|
139
|
-
|
|
140
|
-
test('gate 3: 2 parallel tasks on same file → 1 merged, 1 conflict event captured', async () => {
|
|
141
|
-
const repo = mkRepo();
|
|
142
|
-
const db = mkDb(repo);
|
|
143
|
-
try {
|
|
144
|
-
ops.createTask(db, {
|
|
145
|
-
id: 'X1', title: 'conflict A', type: 'feature', priority: 'P2',
|
|
146
|
-
files_modified: ['battle.txt'],
|
|
147
|
-
});
|
|
148
|
-
ops.createTask(db, {
|
|
149
|
-
id: 'X2', title: 'conflict B', type: 'feature', priority: 'P2',
|
|
150
|
-
files_modified: ['battle.txt'],
|
|
151
|
-
});
|
|
152
|
-
// Force concurrent execution so both worktrees fork from the SAME base
|
|
153
|
-
// commit. (plan-builder normally serializes these, which would cause X2
|
|
154
|
-
// to fork from X1's merged HEAD and fast-forward cleanly.)
|
|
155
|
-
const manualPlan = {
|
|
156
|
-
waves: [{
|
|
157
|
-
id: 1, tasks: ['X1', 'X2'], parallel: true,
|
|
158
|
-
reason: 'forced concurrent for conflict repro',
|
|
159
|
-
}],
|
|
160
|
-
ownership_forecast: { X1: ['battle.txt'], X2: ['battle.txt'] },
|
|
161
|
-
conflict_surface: [{ files: ['battle.txt'], tasks: ['X1', 'X2'], recommend: 'sequentialize' }],
|
|
162
|
-
estimated_cost_usd: 0,
|
|
163
|
-
estimated_duration_min: 0,
|
|
164
|
-
cycles: [],
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
await parallelOrch.runPlan({
|
|
168
|
-
db, repoRoot: repo, plan: manualPlan,
|
|
169
|
-
runtimes: ['claude'],
|
|
170
|
-
command: NODE,
|
|
171
|
-
commandArgsFor: (task) => commitScript('battle.txt', `${task.id}-version\n`),
|
|
172
|
-
autoMerge: true,
|
|
173
|
-
mergeBaseBranch: 'main',
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
const { events } = ops.subscribeEventsSince(db, 0);
|
|
177
|
-
const merged = events.filter((e) => e.type === 'merged_back');
|
|
178
|
-
const conflict = events.filter((e) => e.type === 'merge_conflict');
|
|
179
|
-
assert.equal(merged.length, 1, 'exactly one task should merge');
|
|
180
|
-
assert.equal(conflict.length, 1, 'exactly one task should conflict');
|
|
181
|
-
assert.ok(conflict[0].payload.conflict_paths.includes('battle.txt'));
|
|
182
|
-
} finally { cleanup(repo, db); }
|
|
183
|
-
});
|
|
@@ -1,190 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// Phase 5.1 — recoverOnBoot semantics:
|
|
4
|
-
// • running + fresh heartbeat → untouched
|
|
5
|
-
// • running + lease/heartbeat expired → orphan (handled by 4.5 reaper)
|
|
6
|
-
// • orphan + pid is dead → crashed + task_failure event
|
|
7
|
-
// • orphan + pid still alive → stays orphan (might recover)
|
|
8
|
-
//
|
|
9
|
-
// "pid dead" is determined via process.kill(pid, 0) throwing ESRCH.
|
|
10
|
-
|
|
11
|
-
const { test } = require('node:test');
|
|
12
|
-
const assert = require('node:assert/strict');
|
|
13
|
-
const fs = require('node:fs');
|
|
14
|
-
const os = require('node:os');
|
|
15
|
-
const path = require('node:path');
|
|
16
|
-
const { execFileSync } = require('node:child_process');
|
|
17
|
-
|
|
18
|
-
const { initStateDb, closeStateDb } = require('../../mcp-server/lib/state-db.cjs');
|
|
19
|
-
const ops = require('../../mcp-server/lib/state-ops.cjs');
|
|
20
|
-
const runner = require('../session-runner.cjs');
|
|
21
|
-
const recovery = require('../recovery.cjs');
|
|
22
|
-
|
|
23
|
-
const LONG_SLEEP_CMD = process.execPath;
|
|
24
|
-
const LONG_SLEEP_ARGS = ['-e', 'setInterval(() => {}, 60000);'];
|
|
25
|
-
|
|
26
|
-
function mkRepo() {
|
|
27
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-recovery-repo-'));
|
|
28
|
-
execFileSync('git', ['init', '-q'], { cwd: dir });
|
|
29
|
-
execFileSync('git', ['config', 'user.email', 'test@ubp.dev'], { cwd: dir });
|
|
30
|
-
execFileSync('git', ['config', 'user.name', 'ubp-test'], { cwd: dir });
|
|
31
|
-
fs.writeFileSync(path.join(dir, 'README.md'), '# test\n');
|
|
32
|
-
execFileSync('git', ['add', '-A'], { cwd: dir });
|
|
33
|
-
execFileSync('git', ['commit', '-q', '-m', 'seed'], { cwd: dir });
|
|
34
|
-
return dir;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function mkDb(repoRoot) {
|
|
38
|
-
const dbPath = path.join(repoRoot, '.ultra', 'state.db');
|
|
39
|
-
const { db } = initStateDb(dbPath);
|
|
40
|
-
return { db, dbPath };
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function seedTask(db, id) {
|
|
44
|
-
ops.createTask(db, { id, title: 'recovery target', type: 'feature', priority: 'P1' });
|
|
45
|
-
return id;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function cleanup(repoRoot, db) {
|
|
49
|
-
try { closeStateDb(db); } catch (_) { /* ignore */ }
|
|
50
|
-
try { fs.rmSync(repoRoot, { recursive: true, force: true }); } catch (_) { /* ignore */ }
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Create a session row bypassing real spawn — lets us control pid precisely.
|
|
54
|
-
function insertRawSession(db, { sid, task_id, pid, status = 'orphan', worktree_path, artifact_dir }) {
|
|
55
|
-
const row = ops.createSession(db, {
|
|
56
|
-
sid, task_id, runtime: 'claude', pid,
|
|
57
|
-
worktree_path: worktree_path || `/tmp/ubp-fake-wt/${sid}`,
|
|
58
|
-
artifact_dir: artifact_dir || `/tmp/ubp-fake-art/${sid}`,
|
|
59
|
-
});
|
|
60
|
-
if (status !== 'running') ops.updateSession(db, sid, { status });
|
|
61
|
-
return row;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
test('recoverOnBoot: orphan with dead pid → crashed + task_failure event', () => {
|
|
65
|
-
const repoRoot = mkRepo();
|
|
66
|
-
const { db } = mkDb(repoRoot);
|
|
67
|
-
try {
|
|
68
|
-
seedTask(db, 'rec-1');
|
|
69
|
-
// pid 0x7FFFFFFE is almost certainly not assigned; process.kill will ESRCH.
|
|
70
|
-
insertRawSession(db, { sid: 'sess-dead01', task_id: 'rec-1', pid: 0x7FFFFFFE, status: 'orphan' });
|
|
71
|
-
|
|
72
|
-
const { events: beforeEvents } = ops.subscribeEventsSince(db, { since_id: 0, limit: 500 });
|
|
73
|
-
const result = recovery.recoverOnBoot(db);
|
|
74
|
-
|
|
75
|
-
assert.equal(result.count, 1);
|
|
76
|
-
assert.equal(result.recovered[0].sid, 'sess-dead01');
|
|
77
|
-
assert.equal(result.recovered[0].task_id, 'rec-1');
|
|
78
|
-
assert.equal(result.recovered[0].pid_alive, false);
|
|
79
|
-
|
|
80
|
-
const row = ops.readSession(db, 'sess-dead01');
|
|
81
|
-
assert.equal(row.status, 'crashed');
|
|
82
|
-
|
|
83
|
-
const { events: afterEvents } = ops.subscribeEventsSince(db, { since_id: 0, limit: 500 });
|
|
84
|
-
const newEvents = afterEvents.slice(beforeEvents.length);
|
|
85
|
-
assert.ok(newEvents.some((e) => e.type === 'session_crashed' && e.session_id === 'sess-dead01'));
|
|
86
|
-
assert.ok(newEvents.some((e) => e.type === 'task_failure' && e.task_id === 'rec-1'));
|
|
87
|
-
} finally {
|
|
88
|
-
cleanup(repoRoot, db);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('recoverOnBoot: orphan with alive pid → stays orphan', () => {
|
|
93
|
-
const repoRoot = mkRepo();
|
|
94
|
-
const { db } = mkDb(repoRoot);
|
|
95
|
-
try {
|
|
96
|
-
seedTask(db, 'rec-2');
|
|
97
|
-
// Current process is definitely alive.
|
|
98
|
-
insertRawSession(db, { sid: 'sess-alive01', task_id: 'rec-2', pid: process.pid, status: 'orphan' });
|
|
99
|
-
|
|
100
|
-
const result = recovery.recoverOnBoot(db);
|
|
101
|
-
assert.equal(result.count, 0);
|
|
102
|
-
const row = ops.readSession(db, 'sess-alive01');
|
|
103
|
-
assert.equal(row.status, 'orphan');
|
|
104
|
-
} finally {
|
|
105
|
-
cleanup(repoRoot, db);
|
|
106
|
-
}
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
test('recoverOnBoot: running with fresh heartbeat → unchanged', () => {
|
|
110
|
-
const repoRoot = mkRepo();
|
|
111
|
-
const { db } = mkDb(repoRoot);
|
|
112
|
-
try {
|
|
113
|
-
seedTask(db, 'rec-3');
|
|
114
|
-
// Running session that will NOT be reaped — fresh heartbeat + future lease.
|
|
115
|
-
ops.createSession(db, {
|
|
116
|
-
sid: 'sess-healthy',
|
|
117
|
-
task_id: 'rec-3',
|
|
118
|
-
runtime: 'claude',
|
|
119
|
-
pid: process.pid,
|
|
120
|
-
worktree_path: '/tmp/ubp-fake-wt/sess-healthy',
|
|
121
|
-
artifact_dir: '/tmp/ubp-fake-art/sess-healthy',
|
|
122
|
-
});
|
|
123
|
-
ops.heartbeatSession(db, 'sess-healthy');
|
|
124
|
-
|
|
125
|
-
recovery.recoverOnBoot(db);
|
|
126
|
-
const row = ops.readSession(db, 'sess-healthy');
|
|
127
|
-
assert.equal(row.status, 'running');
|
|
128
|
-
} finally {
|
|
129
|
-
cleanup(repoRoot, db);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
test('recoverOnBoot: first reaps stale running → orphan, then upgrades dead ones to crashed', () => {
|
|
134
|
-
const repoRoot = mkRepo();
|
|
135
|
-
const { db } = mkDb(repoRoot);
|
|
136
|
-
try {
|
|
137
|
-
seedTask(db, 'rec-4');
|
|
138
|
-
// Running session with expired lease + dead pid.
|
|
139
|
-
ops.createSession(db, {
|
|
140
|
-
sid: 'sess-stale-dead',
|
|
141
|
-
task_id: 'rec-4',
|
|
142
|
-
runtime: 'claude',
|
|
143
|
-
pid: 0x7FFFFFFE,
|
|
144
|
-
worktree_path: '/tmp/ubp-fake-wt/sess-stale-dead',
|
|
145
|
-
artifact_dir: '/tmp/ubp-fake-art/sess-stale-dead',
|
|
146
|
-
});
|
|
147
|
-
ops.updateSession(db, 'sess-stale-dead', {
|
|
148
|
-
lease_expires_at: new Date(Date.now() - 60 * 1000).toISOString(),
|
|
149
|
-
heartbeat_at: new Date(Date.now() - 60 * 1000).toISOString(),
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
const result = recovery.recoverOnBoot(db, { graceSeconds: 30 });
|
|
153
|
-
// Should have reaped to orphan first, then crashed.
|
|
154
|
-
assert.equal(result.count, 1);
|
|
155
|
-
assert.equal(result.recovered[0].sid, 'sess-stale-dead');
|
|
156
|
-
const row = ops.readSession(db, 'sess-stale-dead');
|
|
157
|
-
assert.equal(row.status, 'crashed');
|
|
158
|
-
} finally {
|
|
159
|
-
cleanup(repoRoot, db);
|
|
160
|
-
}
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test('integration: kill -9 real child → recoverOnBoot marks crashed', async () => {
|
|
164
|
-
const repoRoot = mkRepo();
|
|
165
|
-
const { db } = mkDb(repoRoot);
|
|
166
|
-
let handle;
|
|
167
|
-
try {
|
|
168
|
-
seedTask(db, 'rec-int');
|
|
169
|
-
handle = runner.spawnSession({
|
|
170
|
-
db, repoRoot,
|
|
171
|
-
task_id: 'rec-int', runtime: 'claude',
|
|
172
|
-
command: LONG_SLEEP_CMD, args: LONG_SLEEP_ARGS,
|
|
173
|
-
});
|
|
174
|
-
// Force lease/heartbeat into the past so reaper catches it.
|
|
175
|
-
ops.updateSession(db, handle.sid, {
|
|
176
|
-
lease_expires_at: new Date(Date.now() - 60 * 1000).toISOString(),
|
|
177
|
-
heartbeat_at: new Date(Date.now() - 60 * 1000).toISOString(),
|
|
178
|
-
});
|
|
179
|
-
process.kill(handle.pid, 'SIGKILL');
|
|
180
|
-
// Let the kernel reap the zombie.
|
|
181
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
182
|
-
|
|
183
|
-
const result = recovery.recoverOnBoot(db, { graceSeconds: 30 });
|
|
184
|
-
assert.ok(result.recovered.some((r) => r.sid === handle.sid));
|
|
185
|
-
const row = ops.readSession(db, handle.sid);
|
|
186
|
-
assert.equal(row.status, 'crashed');
|
|
187
|
-
} finally {
|
|
188
|
-
cleanup(repoRoot, db);
|
|
189
|
-
}
|
|
190
|
-
});
|