ultra-builder-pro-cli 0.14.0 → 0.16.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 +77 -1
- package/README.md +46 -24
- package/adapters/_shared/codex-assets.cjs +6 -2
- package/adapters/_shared/file-ops.cjs +6 -2
- package/adapters/_shared/handbook.cjs +2 -1
- package/adapters/claude.js +3 -2
- package/adapters/codex.js +1 -0
- package/adapters/kimi.js +3 -2
- package/adapters/opencode.js +3 -2
- package/agents/review-coordinator.md +9 -6
- package/commands/ultra-change.md +1 -1
- package/commands/ultra-plan.md +1 -1
- package/commands/ultra-research.md +1 -1
- package/commands/ultra-think.md +2 -2
- package/docs/AGENT-CONTEXT.md +43 -19
- package/docs/ARCHITECTURE.md +54 -13
- package/docs/PLAN.zh-CN.md +4 -2
- package/docs/ROADMAP.md +10 -4
- package/docs/RUNTIME-COMPAT-MATRIX.md +4 -4
- package/docs/STATE-DB-ACCESS-POLICY.md +33 -29
- package/docs/USER-HANDBOOK-CONTRACT.md +8 -2
- package/docs/WORKFLOW-LIFECYCLE.md +342 -0
- package/hooks/README.md +6 -1
- package/hooks/health_check.py +12 -3
- package/hooks/subagent_tracker.py +52 -77
- 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 +598 -85
- package/mcp-server/lib/context-spine.cjs +305 -25
- package/mcp-server/lib/decision-dialogue.cjs +713 -0
- package/mcp-server/lib/doctor.cjs +42 -3
- package/mcp-server/lib/init-project.cjs +210 -16
- package/mcp-server/lib/plan-store.cjs +4 -1
- package/mcp-server/lib/pricing.cjs +7 -10
- package/mcp-server/lib/project-breadcrumb.cjs +15 -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 +168 -8
- package/mcp-server/lib/state-ops.cjs +160 -9
- package/mcp-server/lib/workflow-state.cjs +2305 -0
- package/mcp-server/server.cjs +215 -15
- package/orchestrator/daemon.cjs +1 -2
- package/orchestrator/dispatch-rules.cjs +5 -31
- package/orchestrator/planner/plan-builder.cjs +21 -12
- package/package.json +13 -3
- package/skills/ultra-change/SKILL.md +94 -80
- package/skills/ultra-change/references/change-contract.md +36 -0
- package/skills/ultra-deliver/SKILL.md +76 -79
- package/skills/ultra-deliver/references/baseline-reconciliation.md +20 -0
- package/skills/ultra-dev/SKILL.md +62 -59
- package/skills/ultra-doctor/SKILL.md +61 -82
- package/skills/ultra-init/SKILL.md +101 -103
- package/skills/ultra-init/references/brownfield-adoption.md +2 -2
- package/skills/ultra-plan/SKILL.md +99 -85
- package/skills/ultra-plan/references/semantic-preflight.md +18 -0
- package/skills/ultra-research/SKILL.md +102 -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 +63 -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 -41
- package/skills/ultra-test/SKILL.md +54 -55
- package/skills/ultra-think/SKILL.md +26 -12
- package/skills/ultra-think/references/decision-dialogue.md +130 -0
- package/skills/ultra-verify/SKILL.md +1 -1
- package/skills/ultra-verify/references/{confidence-system.md → evidence-status.md} +3 -3
- package/spec/cli-protocol.md +17 -3
- package/spec/mcp-tools.yaml +683 -64
- 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 +140 -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 +228 -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 -149
- 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 -138
- package/adapters/tests/codex-hook.test.cjs +0 -142
- package/adapters/tests/codex.test.cjs +0 -430
- package/adapters/tests/kimi-hook.test.cjs +0 -149
- package/adapters/tests/kimi.test.cjs +0 -259
- package/adapters/tests/opencode.test.cjs +0 -357
- 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/plan-store.test.cjs +0 -132
- package/mcp-server/lib/prd-parser.test.cjs +0 -82
- package/mcp-server/lib/project-breadcrumb.test.cjs +0 -112
- 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 -169
- 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 -1467
- 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/plan-builder.test.cjs +0 -165
- 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,93 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const test = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const os = require('node:os');
|
|
7
|
-
const path = require('node:path');
|
|
8
|
-
|
|
9
|
-
const { compileRoleContext, deriveNextAction, readBreadcrumb } = require('./context-spine.cjs');
|
|
10
|
-
const { initStateDb } = require('./state-db.cjs');
|
|
11
|
-
const { createChange } = require('./change-workflow.cjs');
|
|
12
|
-
const { createTask } = require('./state-ops.cjs');
|
|
13
|
-
|
|
14
|
-
test('breadcrumb routes a project without a converged baseline back to adoption', () => {
|
|
15
|
-
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-spine-baseline-'));
|
|
16
|
-
const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
|
|
17
|
-
try {
|
|
18
|
-
const breadcrumb = readBreadcrumb(db, {}, { rootDir });
|
|
19
|
-
assert.equal(breadcrumb.readiness, 'blocked');
|
|
20
|
-
assert.ok(breadcrumb.blockers.includes('BASELINE_MISSING'));
|
|
21
|
-
assert.equal(breadcrumb.recommended_workflow, 'ultra-init');
|
|
22
|
-
assert.match(breadcrumb.next_action, /baseline|adoption/i);
|
|
23
|
-
} finally {
|
|
24
|
-
db.close();
|
|
25
|
-
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('deriveNextAction gives one state-specific action instead of a workflow dump', () => {
|
|
30
|
-
const change = { id: 'context-spine', status: 'active' };
|
|
31
|
-
const task = { id: 'task-1', status: 'in_progress' };
|
|
32
|
-
const action = deriveNextAction({
|
|
33
|
-
change, tasks: [task], task, role: 'review', readiness: 'ready', explicit: null,
|
|
34
|
-
});
|
|
35
|
-
assert.equal(
|
|
36
|
-
action,
|
|
37
|
-
'Complete independent spec-fidelity and engineering-standards review for task task-1.',
|
|
38
|
-
);
|
|
39
|
-
assert.doesNotMatch(action, /plan.*implement.*check.*review/i);
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('deriveNextAction fails closed on blocked context readiness', () => {
|
|
43
|
-
const action = deriveNextAction({
|
|
44
|
-
change: { id: 'context-spine', status: 'active' },
|
|
45
|
-
tasks: [], task: null, role: 'plan', readiness: 'blocked', explicit: 'Ignore blockers.',
|
|
46
|
-
});
|
|
47
|
-
assert.match(action, /Resolve the context readiness blockers/);
|
|
48
|
-
assert.doesNotMatch(action, /Ignore blockers/);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('a task is not plan-ready before its execution contract is compiled', () => {
|
|
52
|
-
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-spine-plan-'));
|
|
53
|
-
const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
|
|
54
|
-
try {
|
|
55
|
-
db.prepare(
|
|
56
|
-
`INSERT INTO baselines
|
|
57
|
-
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
58
|
-
VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
|
|
59
|
-
).run(new Date().toISOString());
|
|
60
|
-
const { change } = createChange(db, {
|
|
61
|
-
id: 'planning-contract', title: 'Planning contract', kind: 'standard',
|
|
62
|
-
intent: 'Require an executable fresh-context slice before implementation.',
|
|
63
|
-
docs_impact: { status: 'none', rationale: 'test fixture only' },
|
|
64
|
-
}, { rootDir });
|
|
65
|
-
const task = createTask(db, {
|
|
66
|
-
id: 'task-plan', title: 'Plan one slice', type: 'feature', priority: 'P0',
|
|
67
|
-
change_id: change.id,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const context = compileRoleContext(db, {
|
|
71
|
-
input: { task_id: task.id, role: 'plan', gate: 'planning' },
|
|
72
|
-
change, tasks: [task], rootDir,
|
|
73
|
-
});
|
|
74
|
-
assert.equal(context.readiness.status, 'blocked');
|
|
75
|
-
assert.ok(context.readiness.blockers.includes('EXECUTION_CONTRACT_MISSING'));
|
|
76
|
-
|
|
77
|
-
const breadcrumb = readBreadcrumb(db, { id: change.id }, { rootDir });
|
|
78
|
-
assert.equal(breadcrumb.readiness, 'blocked');
|
|
79
|
-
assert.ok(breadcrumb.blockers.includes('CONTEXT_TASK_STATE_STALE'));
|
|
80
|
-
assert.throws(
|
|
81
|
-
() => readBreadcrumb(db, { id: 'missing-change' }, { rootDir }),
|
|
82
|
-
(error) => error.code === 'CHANGE_NOT_FOUND',
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
db.prepare("UPDATE context_snapshots SET context_json = '{}', readiness = 'ready'").run();
|
|
86
|
-
const migratedLegacy = readBreadcrumb(db, { id: change.id }, { rootDir });
|
|
87
|
-
assert.equal(migratedLegacy.readiness, 'blocked');
|
|
88
|
-
assert.ok(migratedLegacy.blockers.includes('CONTEXT_SNAPSHOT_UPGRADE_REQUIRED'));
|
|
89
|
-
} finally {
|
|
90
|
-
db.close();
|
|
91
|
-
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
92
|
-
}
|
|
93
|
-
});
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { test } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const os = require('node:os');
|
|
7
|
-
const path = require('node:path');
|
|
8
|
-
|
|
9
|
-
const { initStateDb, closeStateDb } = require('./state-db.cjs');
|
|
10
|
-
const ops = require('./state-ops.cjs');
|
|
11
|
-
const runtime = require('./runtime-state.cjs');
|
|
12
|
-
const doctor = require('./doctor.cjs');
|
|
13
|
-
const baselines = require('./baseline-workflow.cjs');
|
|
14
|
-
const changes = require('./change-workflow.cjs');
|
|
15
|
-
const archiveJournal = require('./archive-journal.cjs');
|
|
16
|
-
|
|
17
|
-
function fixture({ baseline = true } = {}) {
|
|
18
|
-
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-doctor-'));
|
|
19
|
-
const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
|
|
20
|
-
if (baseline) {
|
|
21
|
-
db.prepare(
|
|
22
|
-
`INSERT INTO baselines
|
|
23
|
-
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
24
|
-
VALUES ('test-baseline', 'fixture', 'greenfield', 'ready', 'test', 'accepted fixture', ?)`,
|
|
25
|
-
).run(new Date().toISOString());
|
|
26
|
-
}
|
|
27
|
-
return { rootDir, db };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
function cleanup({ rootDir, db }) {
|
|
31
|
-
closeStateDb(db);
|
|
32
|
-
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
test('doctor reports structured health for an initialized Ultra project', async () => {
|
|
36
|
-
const fx = fixture();
|
|
37
|
-
try {
|
|
38
|
-
const report = await doctor.runDoctor(fx.db, { rootDir: fx.rootDir });
|
|
39
|
-
assert.equal(report.status, 'healthy');
|
|
40
|
-
assert.equal(report.repair_performed, false);
|
|
41
|
-
assert.equal(report.checks.state_db.status, 'pass');
|
|
42
|
-
assert.equal(report.checks.external_providers.ownership, 'external');
|
|
43
|
-
} finally {
|
|
44
|
-
cleanup(fx);
|
|
45
|
-
}
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
test('doctor reports incomplete brownfield adoption as advisory rather than authority failure', async () => {
|
|
49
|
-
const fx = fixture({ baseline: false });
|
|
50
|
-
try {
|
|
51
|
-
baselines.startBaseline(fx.db, {
|
|
52
|
-
id: 'adoption', project_name: 'legacy', mode: 'brownfield', scope: ['.'],
|
|
53
|
-
}, { rootDir: fx.rootDir, emitEvent: false });
|
|
54
|
-
const report = await doctor.runDoctor(fx.db, { rootDir: fx.rootDir });
|
|
55
|
-
assert.equal(report.status, 'healthy');
|
|
56
|
-
assert.equal(report.checks.baseline.status, 'warning');
|
|
57
|
-
assert.equal(report.checks.baseline.mode, 'brownfield');
|
|
58
|
-
assert.deepEqual(report.checks.baseline.gaps, {
|
|
59
|
-
total: 0, open: 0, blocking: 0, by_category: {},
|
|
60
|
-
});
|
|
61
|
-
assert.ok(report.checks.baseline.blockers.includes('BASELINE_NOT_READY:adopting'));
|
|
62
|
-
} finally {
|
|
63
|
-
cleanup(fx);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('doctor repair is backup-first and drains projection/spec-event recovery work', async () => {
|
|
68
|
-
const fx = fixture();
|
|
69
|
-
try {
|
|
70
|
-
ops.createTask(fx.db, {
|
|
71
|
-
id: 'stale-target', title: 'Refresh context', type: 'feature', priority: 'P1',
|
|
72
|
-
trace_to: '.ultra/specs/product.md#continuous-change',
|
|
73
|
-
});
|
|
74
|
-
ops.appendEvent(fx.db, {
|
|
75
|
-
type: 'spec_changed',
|
|
76
|
-
payload: { sections: ['.ultra/specs/product.md#continuous-change'] },
|
|
77
|
-
});
|
|
78
|
-
runtime.enqueueProjection(fx.db, { tool_name: 'task.append_event', event_cursor: 2 });
|
|
79
|
-
|
|
80
|
-
const report = await doctor.runDoctor(fx.db, { rootDir: fx.rootDir, repair: true });
|
|
81
|
-
assert.equal(report.repair_performed, true);
|
|
82
|
-
assert.ok(fs.existsSync(report.backup_path));
|
|
83
|
-
assert.equal(ops.readTask(fx.db, 'stale-target').stale, true);
|
|
84
|
-
assert.equal(runtime.readConsumerCursor(fx.db, 'spec-staleness'), 2);
|
|
85
|
-
assert.equal(runtime.listProjectionJobs(fx.db, { status: 'pending' }).length, 0);
|
|
86
|
-
assert.equal(report.status, 'healthy');
|
|
87
|
-
} finally {
|
|
88
|
-
cleanup(fx);
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('doctor reports running projection work and repairs only stale interrupted jobs', async () => {
|
|
93
|
-
const fx = fixture();
|
|
94
|
-
try {
|
|
95
|
-
ops.appendEvent(fx.db, { type: 'spec_changed', payload: { sections: [] } });
|
|
96
|
-
const job = runtime.enqueueProjection(fx.db, { tool_name: 'task.append_event', event_cursor: 1 });
|
|
97
|
-
fx.db.prepare("UPDATE projection_jobs SET status = 'running', updated_at = ? WHERE id = ?")
|
|
98
|
-
.run('2000-01-01T00:00:00.000Z', job.id);
|
|
99
|
-
|
|
100
|
-
const before = await doctor.runDoctor(fx.db, { rootDir: fx.rootDir });
|
|
101
|
-
assert.equal(before.status, 'degraded');
|
|
102
|
-
assert.equal(before.checks.projections.running, 1);
|
|
103
|
-
|
|
104
|
-
const after = await doctor.runDoctor(fx.db, {
|
|
105
|
-
rootDir: fx.rootDir,
|
|
106
|
-
repair: true,
|
|
107
|
-
projectionStaleAfterMs: 0,
|
|
108
|
-
});
|
|
109
|
-
assert.equal(after.repair.interrupted.requeued, 1);
|
|
110
|
-
assert.equal(after.checks.projections.running, 0);
|
|
111
|
-
assert.equal(after.status, 'healthy');
|
|
112
|
-
} finally {
|
|
113
|
-
cleanup(fx);
|
|
114
|
-
}
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('doctor repair resumes an archive journal left after a process crash', async () => {
|
|
118
|
-
const fx = fixture();
|
|
119
|
-
try {
|
|
120
|
-
const activeRoot = path.join(fx.rootDir, '.ultra', 'changes', 'active', 'doctor-archive');
|
|
121
|
-
fs.mkdirSync(activeRoot, { recursive: true });
|
|
122
|
-
fs.writeFileSync(path.join(activeRoot, 'intent.md'), '# Authorized archive\n');
|
|
123
|
-
fx.db.prepare(
|
|
124
|
-
`INSERT INTO changes
|
|
125
|
-
(id, title, kind, status, intent, docs_impact_json, provider_refs_json, artifact_root)
|
|
126
|
-
VALUES ('doctor-archive', 'Recover archive', 'quick', 'ready', 'Recover it',
|
|
127
|
-
'{"status":"none","files":[],"rationale":"recovery"}', '{}',
|
|
128
|
-
'.ultra/changes/active/doctor-archive')`,
|
|
129
|
-
).run();
|
|
130
|
-
const prepared = archiveJournal.prepareArchiveMove({
|
|
131
|
-
rootDir: fx.rootDir, change: changes.readChange(fx.db, 'doctor-archive'),
|
|
132
|
-
summary: 'Resume the authorized archive.', baselineUpdates: [],
|
|
133
|
-
noBaselineChangeReason: 'No baseline content changed.',
|
|
134
|
-
});
|
|
135
|
-
assert.equal(fs.existsSync(prepared.source), false);
|
|
136
|
-
const before = doctor.inspectSystem(fx.db, { rootDir: fx.rootDir });
|
|
137
|
-
assert.equal(before.status, 'degraded');
|
|
138
|
-
assert.equal(before.checks.archive_recovery.pending, 1);
|
|
139
|
-
|
|
140
|
-
const after = await doctor.runDoctor(fx.db, { rootDir: fx.rootDir, repair: true });
|
|
141
|
-
assert.equal(after.repair.archives.resumed, 1);
|
|
142
|
-
assert.equal(changes.readChange(fx.db, 'doctor-archive').status, 'archived');
|
|
143
|
-
assert.equal(after.checks.archive_recovery.pending, 0);
|
|
144
|
-
assert.equal(fs.existsSync(path.join(prepared.destination, archiveJournal.INTENT_FILE)), false);
|
|
145
|
-
} finally {
|
|
146
|
-
cleanup(fx);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const test = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const os = require('node:os');
|
|
7
|
-
const path = require('node:path');
|
|
8
|
-
|
|
9
|
-
const {
|
|
10
|
-
savePlanArtifact,
|
|
11
|
-
loadPlanArtifact,
|
|
12
|
-
selectSection,
|
|
13
|
-
renderPlanMd,
|
|
14
|
-
DEFAULT_ARTIFACT_RELPATH,
|
|
15
|
-
} = require('./plan-store.cjs');
|
|
16
|
-
|
|
17
|
-
function tmpRoot() {
|
|
18
|
-
return fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-plan-'));
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const SAMPLE_PLAN = Object.freeze({
|
|
22
|
-
waves: [
|
|
23
|
-
{ id: 1, tasks: ['a', 'b'], parallel: true },
|
|
24
|
-
{ id: 2, tasks: ['c'], parallel: false },
|
|
25
|
-
],
|
|
26
|
-
ownership_forecast: { a: ['src/a.ts'], b: ['src/b.ts'], c: ['src/c.ts'] },
|
|
27
|
-
conflict_surface: [],
|
|
28
|
-
estimated_cost_usd: 0.12,
|
|
29
|
-
estimated_duration_min: 35,
|
|
30
|
-
cycles: [],
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test('savePlanArtifact: json format writes a file we can JSON.parse back', () => {
|
|
34
|
-
const root = tmpRoot();
|
|
35
|
-
try {
|
|
36
|
-
const outPath = path.join(root, '.ultra', 'execution-plan.json');
|
|
37
|
-
const { plan_path } = savePlanArtifact(SAMPLE_PLAN, outPath, 'json');
|
|
38
|
-
assert.equal(plan_path, path.resolve(outPath));
|
|
39
|
-
assert.ok(fs.existsSync(plan_path));
|
|
40
|
-
const parsed = JSON.parse(fs.readFileSync(plan_path, 'utf8'));
|
|
41
|
-
assert.deepEqual(parsed.waves, SAMPLE_PLAN.waves);
|
|
42
|
-
} finally {
|
|
43
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test('savePlanArtifact: md format writes a human-readable report', () => {
|
|
48
|
-
const root = tmpRoot();
|
|
49
|
-
try {
|
|
50
|
-
const outPath = path.join(root, '.ultra', 'execution-plan.md');
|
|
51
|
-
savePlanArtifact(SAMPLE_PLAN, outPath, 'md');
|
|
52
|
-
const text = fs.readFileSync(outPath, 'utf8');
|
|
53
|
-
assert.match(text, /# Execution Plan/);
|
|
54
|
-
assert.match(text, /Wave 1/);
|
|
55
|
-
assert.match(text, /parallel/);
|
|
56
|
-
} finally {
|
|
57
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('savePlanArtifact: missing out_path → WRITE_FAILED', () => {
|
|
62
|
-
assert.throws(
|
|
63
|
-
() => savePlanArtifact(SAMPLE_PLAN, '', 'json'),
|
|
64
|
-
(err) => err.code === 'WRITE_FAILED',
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('loadPlanArtifact: no file → null', () => {
|
|
69
|
-
const root = tmpRoot();
|
|
70
|
-
try {
|
|
71
|
-
assert.equal(loadPlanArtifact(root), null);
|
|
72
|
-
} finally {
|
|
73
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
74
|
-
}
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
test('loadPlanArtifact: round-trip via DEFAULT_ARTIFACT_RELPATH', () => {
|
|
78
|
-
const root = tmpRoot();
|
|
79
|
-
try {
|
|
80
|
-
const target = path.join(root, DEFAULT_ARTIFACT_RELPATH);
|
|
81
|
-
savePlanArtifact(SAMPLE_PLAN, target, 'json');
|
|
82
|
-
const loaded = loadPlanArtifact(root);
|
|
83
|
-
assert.deepEqual(loaded.waves, SAMPLE_PLAN.waves);
|
|
84
|
-
assert.equal(loaded.estimated_cost_usd, 0.12);
|
|
85
|
-
} finally {
|
|
86
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
test('loadPlanArtifact: corrupt file → null (no throw)', () => {
|
|
91
|
-
const root = tmpRoot();
|
|
92
|
-
try {
|
|
93
|
-
const target = path.join(root, DEFAULT_ARTIFACT_RELPATH);
|
|
94
|
-
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
95
|
-
fs.writeFileSync(target, '{not json');
|
|
96
|
-
assert.equal(loadPlanArtifact(root), null);
|
|
97
|
-
} finally {
|
|
98
|
-
fs.rmSync(root, { recursive: true, force: true });
|
|
99
|
-
}
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('selectSection: tasks returns ownership_forecast only', () => {
|
|
103
|
-
const r = selectSection(SAMPLE_PLAN, 'tasks');
|
|
104
|
-
assert.deepEqual(Object.keys(r), ['ownership_forecast']);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('selectSection: topo returns waves only', () => {
|
|
108
|
-
const r = selectSection(SAMPLE_PLAN, 'topo');
|
|
109
|
-
assert.deepEqual(Object.keys(r), ['waves']);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
test('selectSection: conflicts returns conflict_surface only', () => {
|
|
113
|
-
const r = selectSection(SAMPLE_PLAN, 'conflicts');
|
|
114
|
-
assert.deepEqual(Object.keys(r), ['conflict_surface']);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('selectSection: all (default) returns whole plan', () => {
|
|
118
|
-
assert.deepEqual(selectSection(SAMPLE_PLAN, 'all'), SAMPLE_PLAN);
|
|
119
|
-
assert.deepEqual(selectSection(SAMPLE_PLAN), SAMPLE_PLAN);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('renderPlanMd: reports cycles + conflicts counts when present', () => {
|
|
123
|
-
const planWithConflicts = {
|
|
124
|
-
...SAMPLE_PLAN,
|
|
125
|
-
conflict_surface: [{ files: ['x.ts'], tasks: ['a', 'b'], recommend: 'sequentialize' }],
|
|
126
|
-
cycles: [['z', 'y']],
|
|
127
|
-
};
|
|
128
|
-
const md = renderPlanMd(planWithConflicts);
|
|
129
|
-
assert.match(md, /Conflicts: 1/);
|
|
130
|
-
assert.match(md, /Cycles: 1/);
|
|
131
|
-
assert.match(md, /## Conflict Surface/);
|
|
132
|
-
});
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const test = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
|
|
6
|
-
const { parsePrd, PrdParseError, TASK_LIST_SCHEMA } = require('./prd-parser.cjs');
|
|
7
|
-
|
|
8
|
-
const HAPPY_TASKS = [
|
|
9
|
-
{
|
|
10
|
-
id: 'task-1',
|
|
11
|
-
title: 'Design auth schema',
|
|
12
|
-
type: 'architecture',
|
|
13
|
-
priority: 'P1',
|
|
14
|
-
complexity: 5,
|
|
15
|
-
deps: [],
|
|
16
|
-
files_modified: ['src/auth/schema.ts'],
|
|
17
|
-
},
|
|
18
|
-
{
|
|
19
|
-
id: 'task-2',
|
|
20
|
-
title: 'Implement login endpoint',
|
|
21
|
-
type: 'feature',
|
|
22
|
-
priority: 'P1',
|
|
23
|
-
complexity: 6,
|
|
24
|
-
deps: ['task-1'],
|
|
25
|
-
files_modified: ['src/auth/login.ts'],
|
|
26
|
-
},
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
test('parsePrd validates and normalizes host-derived tasks without a provider client', () => {
|
|
30
|
-
const result = parsePrd(HAPPY_TASKS, { tag: 'feat-auth', changeId: 'auth-change' });
|
|
31
|
-
assert.equal(result.tasks.length, 2);
|
|
32
|
-
assert.equal(result.tasks[0].tag, 'feat-auth');
|
|
33
|
-
assert.equal(result.tasks[0].change_id, 'auth-change');
|
|
34
|
-
assert.deepEqual(result.tasks[1].deps, ['task-1']);
|
|
35
|
-
assert.equal(Object.hasOwn(result, 'model'), false);
|
|
36
|
-
assert.equal(Object.hasOwn(result, 'provider'), false);
|
|
37
|
-
assert.equal(Object.hasOwn(result, 'usage'), false);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test('parsePrd requires at least one host-derived task', () => {
|
|
41
|
-
for (const value of [undefined, null, [], 'not-an-array']) {
|
|
42
|
-
assert.throws(
|
|
43
|
-
() => parsePrd(value),
|
|
44
|
-
(err) => err instanceof PrdParseError && err.code === 'NO_INPUT',
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test('parsePrd rejects an invalid task shape', () => {
|
|
50
|
-
assert.throws(
|
|
51
|
-
() => parsePrd([{
|
|
52
|
-
id: 'task-1', title: 'Missing priority', type: 'feature',
|
|
53
|
-
}]),
|
|
54
|
-
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /priority/.test(err.message),
|
|
55
|
-
);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test('parsePrd rejects duplicate task ids', () => {
|
|
59
|
-
assert.throws(
|
|
60
|
-
() => parsePrd([
|
|
61
|
-
{ id: 'task-1', title: 'First copy', type: 'feature', priority: 'P2' },
|
|
62
|
-
{ id: 'task-1', title: 'Second copy', type: 'feature', priority: 'P2' },
|
|
63
|
-
]),
|
|
64
|
-
(err) => err instanceof PrdParseError && err.code === 'PARSE_FAILED' && /duplicate/i.test(err.message),
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test('parsePrd normalizes optional fields', () => {
|
|
69
|
-
const result = parsePrd([{
|
|
70
|
-
id: 'task-1', title: 'Minimal task', type: 'feature', priority: 'P3',
|
|
71
|
-
}]);
|
|
72
|
-
assert.equal(result.tasks[0].complexity, null);
|
|
73
|
-
assert.deepEqual(result.tasks[0].deps, []);
|
|
74
|
-
assert.deepEqual(result.tasks[0].files_modified, []);
|
|
75
|
-
assert.equal(result.tasks[0].tag, null);
|
|
76
|
-
assert.equal(result.tasks[0].change_id, null);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test('TASK_LIST_SCHEMA remains available to MCP contract consumers', () => {
|
|
80
|
-
assert.equal(TASK_LIST_SCHEMA.required[0], 'tasks');
|
|
81
|
-
assert.equal(TASK_LIST_SCHEMA.properties.tasks.minItems, 1);
|
|
82
|
-
});
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const test = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const os = require('node:os');
|
|
7
|
-
const path = require('node:path');
|
|
8
|
-
const Database = require('better-sqlite3');
|
|
9
|
-
|
|
10
|
-
const { initStateDb, closeStateDb } = require('./state-db.cjs');
|
|
11
|
-
const { createChange } = require('./change-workflow.cjs');
|
|
12
|
-
const { createTask } = require('./state-ops.cjs');
|
|
13
|
-
const { compileRoleContext, readBreadcrumb } = require('./context-spine.cjs');
|
|
14
|
-
const {
|
|
15
|
-
readProjectBreadcrumb,
|
|
16
|
-
renderProjectBreadcrumb,
|
|
17
|
-
} = require('./project-breadcrumb.cjs');
|
|
18
|
-
|
|
19
|
-
function tmpProject(prefix) {
|
|
20
|
-
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
21
|
-
fs.mkdirSync(path.join(rootDir, '.ultra'), { recursive: true });
|
|
22
|
-
return rootDir;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
test('project reader returns the exact canonical breadcrumb from state.db', () => {
|
|
26
|
-
const rootDir = tmpProject('ubp-project-breadcrumb-');
|
|
27
|
-
const state = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
|
|
28
|
-
try {
|
|
29
|
-
state.db.prepare(
|
|
30
|
-
`INSERT INTO baselines
|
|
31
|
-
(id, project_name, mode, status, approved_by, approval_note, converged_at)
|
|
32
|
-
VALUES ('baseline-1', 'fixture', 'greenfield', 'ready', 'test', 'fixture', ?)`,
|
|
33
|
-
).run(new Date().toISOString());
|
|
34
|
-
const { change } = createChange(state.db, {
|
|
35
|
-
id: 'db-change', title: 'Database change', kind: 'quick',
|
|
36
|
-
intent: 'Prove every host consumes one breadcrumb.',
|
|
37
|
-
docs_impact: { status: 'none', rationale: 'test fixture' },
|
|
38
|
-
}, { rootDir });
|
|
39
|
-
const task = createTask(state.db, {
|
|
40
|
-
id: 'db-task', title: 'Use one breadcrumb', type: 'bugfix', priority: 'P0',
|
|
41
|
-
change_id: change.id,
|
|
42
|
-
});
|
|
43
|
-
compileRoleContext(state.db, {
|
|
44
|
-
input: {
|
|
45
|
-
task_id: task.id,
|
|
46
|
-
role: 'implement',
|
|
47
|
-
gate: 'implementation',
|
|
48
|
-
execution_contract: {
|
|
49
|
-
slice_kind: 'tracer_bullet',
|
|
50
|
-
public_seam: 'all host injections',
|
|
51
|
-
verification_command: 'npm test',
|
|
52
|
-
},
|
|
53
|
-
next_action: 'Run the cross-host breadcrumb regression test.',
|
|
54
|
-
},
|
|
55
|
-
change,
|
|
56
|
-
tasks: [task],
|
|
57
|
-
rootDir,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const expected = readBreadcrumb(state.db, {}, { rootDir });
|
|
61
|
-
const actual = readProjectBreadcrumb(rootDir);
|
|
62
|
-
assert.deepEqual(actual, expected);
|
|
63
|
-
const rendered = renderProjectBreadcrumb(rootDir, actual);
|
|
64
|
-
assert.match(rendered, /Change: db-change/);
|
|
65
|
-
assert.match(rendered, /Task: db-task/);
|
|
66
|
-
assert.ok(rendered.includes(actual.next_action));
|
|
67
|
-
assert.match(rendered, /Authority: \.ultra\/state\.db/);
|
|
68
|
-
} finally {
|
|
69
|
-
closeStateDb(state.db);
|
|
70
|
-
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
test('project reader routes a missing authority to ultra-init', () => {
|
|
75
|
-
const rootDir = tmpProject('ubp-project-missing-db-');
|
|
76
|
-
try {
|
|
77
|
-
const breadcrumb = readProjectBreadcrumb(rootDir);
|
|
78
|
-
assert.equal(breadcrumb.readiness, 'blocked');
|
|
79
|
-
assert.deepEqual(breadcrumb.blockers, ['STATE_DB_MISSING']);
|
|
80
|
-
assert.equal(breadcrumb.recommended_workflow, 'ultra-init');
|
|
81
|
-
} finally {
|
|
82
|
-
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
83
|
-
}
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('project reader routes an old schema to ultra-init without consulting projections', () => {
|
|
87
|
-
const rootDir = tmpProject('ubp-project-old-schema-');
|
|
88
|
-
const db = new Database(path.join(rootDir, '.ultra', 'state.db'));
|
|
89
|
-
try {
|
|
90
|
-
db.exec(`
|
|
91
|
-
CREATE TABLE schema_version (
|
|
92
|
-
version TEXT PRIMARY KEY,
|
|
93
|
-
applied_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
94
|
-
);
|
|
95
|
-
INSERT INTO schema_version(version) VALUES ('10.0');
|
|
96
|
-
`);
|
|
97
|
-
} finally {
|
|
98
|
-
db.close();
|
|
99
|
-
}
|
|
100
|
-
fs.writeFileSync(path.join(rootDir, '.ultra', 'workflow-state.json'), JSON.stringify({
|
|
101
|
-
command: 'ultra-dev', task_id: 'projection-task', status: 'active',
|
|
102
|
-
}));
|
|
103
|
-
try {
|
|
104
|
-
const breadcrumb = readProjectBreadcrumb(rootDir);
|
|
105
|
-
assert.equal(breadcrumb.readiness, 'blocked');
|
|
106
|
-
assert.ok(breadcrumb.blockers.includes('STATE_SCHEMA_MIGRATION_REQUIRED:10.0'));
|
|
107
|
-
assert.equal(breadcrumb.recommended_workflow, 'ultra-init');
|
|
108
|
-
assert.doesNotMatch(JSON.stringify(breadcrumb), /projection-task|ultra-dev/);
|
|
109
|
-
} finally {
|
|
110
|
-
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
111
|
-
}
|
|
112
|
-
});
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
const { test } = require('node:test');
|
|
4
|
-
const assert = require('node:assert/strict');
|
|
5
|
-
const fs = require('node:fs');
|
|
6
|
-
const os = require('node:os');
|
|
7
|
-
const path = require('node:path');
|
|
8
|
-
|
|
9
|
-
const { initStateDb, closeStateDb } = require('./state-db.cjs');
|
|
10
|
-
const runtime = require('./runtime-state.cjs');
|
|
11
|
-
|
|
12
|
-
function fixture() {
|
|
13
|
-
const rootDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ubp-runtime-state-'));
|
|
14
|
-
const { db } = initStateDb(path.join(rootDir, '.ultra', 'state.db'));
|
|
15
|
-
return { rootDir, db };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function cleanup({ rootDir, db }) {
|
|
19
|
-
closeStateDb(db);
|
|
20
|
-
fs.rmSync(rootDir, { recursive: true, force: true });
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
test('projection jobs complete and retain their event cursor', () => {
|
|
24
|
-
const fx = fixture();
|
|
25
|
-
try {
|
|
26
|
-
const job = runtime.enqueueProjection(fx.db, { tool_name: 'task.create', event_cursor: 7 });
|
|
27
|
-
const result = runtime.processProjectionJobs(fx.db, {
|
|
28
|
-
rootDir: fx.rootDir,
|
|
29
|
-
project: () => ({ tasks_json: { count: 0 }, contexts: [] }),
|
|
30
|
-
});
|
|
31
|
-
assert.equal(result.processed, 1);
|
|
32
|
-
assert.equal(result.jobs[0].status, 'completed');
|
|
33
|
-
const stored = runtime.readProjectionJob(fx.db, job.id);
|
|
34
|
-
assert.equal(stored.status, 'completed');
|
|
35
|
-
assert.equal(stored.event_cursor, 7);
|
|
36
|
-
} finally {
|
|
37
|
-
cleanup(fx);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test('projection failures are explicit, retryable incidents rather than swallowed warnings', () => {
|
|
42
|
-
const fx = fixture();
|
|
43
|
-
try {
|
|
44
|
-
runtime.enqueueProjection(fx.db, { tool_name: 'task.update', event_cursor: 4, max_attempts: 1 });
|
|
45
|
-
// Test Double rationale: deterministic projector failure is required; filesystem permission failures vary by host.
|
|
46
|
-
const result = runtime.processProjectionJobs(fx.db, {
|
|
47
|
-
rootDir: fx.rootDir,
|
|
48
|
-
project: () => { throw new Error('projection disk unavailable'); },
|
|
49
|
-
});
|
|
50
|
-
assert.equal(result.jobs[0].status, 'failed');
|
|
51
|
-
assert.match(result.jobs[0].error, /projection disk unavailable/);
|
|
52
|
-
const incidents = runtime.listIncidents(fx.db, { status: 'open' });
|
|
53
|
-
assert.equal(incidents.length, 1);
|
|
54
|
-
assert.equal(incidents[0].code, 'PROJECTION_FAILED');
|
|
55
|
-
assert.equal(incidents[0].retryable, true);
|
|
56
|
-
} finally {
|
|
57
|
-
cleanup(fx);
|
|
58
|
-
}
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test('event consumer cursors are durable and monotonic', () => {
|
|
62
|
-
const fx = fixture();
|
|
63
|
-
try {
|
|
64
|
-
assert.equal(runtime.readConsumerCursor(fx.db, 'spec-staleness'), 0);
|
|
65
|
-
runtime.writeConsumerCursor(fx.db, 'spec-staleness', 12);
|
|
66
|
-
runtime.writeConsumerCursor(fx.db, 'spec-staleness', 9);
|
|
67
|
-
assert.equal(runtime.readConsumerCursor(fx.db, 'spec-staleness'), 12);
|
|
68
|
-
} finally {
|
|
69
|
-
cleanup(fx);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('interrupted running projections are requeued only after the stale cutoff', () => {
|
|
74
|
-
const fx = fixture();
|
|
75
|
-
try {
|
|
76
|
-
const stale = runtime.enqueueProjection(fx.db, { tool_name: 'task.update', event_cursor: 3 });
|
|
77
|
-
const fresh = runtime.enqueueProjection(fx.db, { tool_name: 'task.create', event_cursor: 4 });
|
|
78
|
-
fx.db.prepare("UPDATE projection_jobs SET status = 'running', updated_at = ? WHERE id = ?")
|
|
79
|
-
.run('2000-01-01T00:00:00.000Z', stale.id);
|
|
80
|
-
fx.db.prepare("UPDATE projection_jobs SET status = 'running', updated_at = ? WHERE id = ?")
|
|
81
|
-
.run('2999-01-01T00:00:00.000Z', fresh.id);
|
|
82
|
-
|
|
83
|
-
const result = runtime.requeueInterruptedProjections(fx.db, {
|
|
84
|
-
staleBefore: '2026-01-01T00:00:00.000Z',
|
|
85
|
-
});
|
|
86
|
-
assert.equal(result.requeued, 1);
|
|
87
|
-
assert.equal(runtime.readProjectionJob(fx.db, stale.id).status, 'pending');
|
|
88
|
-
assert.equal(runtime.readProjectionJob(fx.db, fresh.id).status, 'running');
|
|
89
|
-
} finally {
|
|
90
|
-
cleanup(fx);
|
|
91
|
-
}
|
|
92
|
-
});
|