@cleocode/core 2026.3.57 → 2026.3.59
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/dist/agents/agent-registry.d.ts +206 -0
- package/dist/agents/agent-registry.d.ts.map +1 -0
- package/dist/agents/agent-schema.d.ts.map +1 -1
- package/dist/agents/execution-learning.d.ts +223 -0
- package/dist/agents/execution-learning.d.ts.map +1 -0
- package/dist/agents/health-monitor.d.ts +161 -0
- package/dist/agents/health-monitor.d.ts.map +1 -0
- package/dist/agents/index.d.ts +4 -1
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/retry.d.ts +57 -4
- package/dist/agents/retry.d.ts.map +1 -1
- package/dist/backfill/index.d.ts +83 -0
- package/dist/backfill/index.d.ts.map +1 -0
- package/dist/bootstrap.d.ts +1 -1
- package/dist/config.d.ts +47 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6985 -5068
- package/dist/index.js.map +4 -4
- package/dist/intelligence/adaptive-validation.d.ts +151 -0
- package/dist/intelligence/adaptive-validation.d.ts.map +1 -0
- package/dist/intelligence/impact.d.ts +34 -1
- package/dist/intelligence/impact.d.ts.map +1 -1
- package/dist/intelligence/index.d.ts +7 -2
- package/dist/intelligence/index.d.ts.map +1 -1
- package/dist/intelligence/types.d.ts +60 -0
- package/dist/intelligence/types.d.ts.map +1 -1
- package/dist/internal.d.ts +8 -4
- package/dist/internal.d.ts.map +1 -1
- package/dist/lib/index.d.ts +10 -0
- package/dist/lib/index.d.ts.map +1 -0
- package/dist/lib/retry.d.ts +128 -0
- package/dist/lib/retry.d.ts.map +1 -0
- package/dist/nexus/sharing/index.d.ts +48 -2
- package/dist/nexus/sharing/index.d.ts.map +1 -1
- package/dist/sessions/session-enforcement.d.ts.map +1 -1
- package/dist/stats/index.d.ts +1 -0
- package/dist/stats/index.d.ts.map +1 -1
- package/dist/stats/workflow-telemetry.d.ts +89 -0
- package/dist/stats/workflow-telemetry.d.ts.map +1 -0
- package/dist/store/brain-schema.d.ts.map +1 -1
- package/dist/store/converters.d.ts.map +1 -1
- package/dist/store/cross-db-cleanup.d.ts +93 -0
- package/dist/store/cross-db-cleanup.d.ts.map +1 -0
- package/dist/store/db-helpers.d.ts.map +1 -1
- package/dist/store/migration-sqlite.d.ts.map +1 -1
- package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
- package/dist/store/sqlite.d.ts.map +1 -1
- package/dist/store/task-store.d.ts.map +1 -1
- package/dist/store/tasks-schema.d.ts +18 -3
- package/dist/store/tasks-schema.d.ts.map +1 -1
- package/dist/store/validation-schemas.d.ts +32 -0
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/dist/tasks/add.d.ts +10 -1
- package/dist/tasks/add.d.ts.map +1 -1
- package/dist/tasks/complete.d.ts.map +1 -1
- package/dist/tasks/enforcement.d.ts +22 -0
- package/dist/tasks/enforcement.d.ts.map +1 -0
- package/dist/tasks/epic-enforcement.d.ts +199 -0
- package/dist/tasks/epic-enforcement.d.ts.map +1 -0
- package/dist/tasks/index.d.ts +1 -1
- package/dist/tasks/index.d.ts.map +1 -1
- package/dist/tasks/pipeline-stage.d.ts +181 -0
- package/dist/tasks/pipeline-stage.d.ts.map +1 -0
- package/dist/tasks/update.d.ts +2 -0
- package/dist/tasks/update.d.ts.map +1 -1
- package/migrations/drizzle-brain/20260321000001_t033-brain-indexes/migration.sql +12 -0
- package/migrations/drizzle-brain/20260321000001_t033-brain-indexes/snapshot.json +1232 -0
- package/migrations/drizzle-tasks/20260321000000_t033-connection-health/migration.sql +518 -0
- package/migrations/drizzle-tasks/20260321000000_t033-connection-health/snapshot.json +4312 -0
- package/migrations/drizzle-tasks/20260321000002_t060-pipeline-stage-binding/migration.sql +82 -0
- package/migrations/drizzle-tasks/20260321000002_t060-pipeline-stage-binding/snapshot.json +9 -0
- package/package.json +5 -5
- package/schemas/config.schema.json +37 -1547
- package/src/__tests__/sharing.test.ts +24 -0
- package/src/agents/__tests__/agent-registry.test.ts +351 -0
- package/src/agents/__tests__/execution-learning.test.ts +684 -0
- package/src/agents/__tests__/health-monitor.test.ts +332 -0
- package/src/agents/__tests__/registry.test.ts +30 -2
- package/src/agents/agent-registry.ts +394 -0
- package/src/agents/agent-schema.ts +5 -0
- package/src/agents/execution-learning.ts +675 -0
- package/src/agents/health-monitor.ts +279 -0
- package/src/agents/index.ts +37 -1
- package/src/agents/retry.ts +57 -4
- package/src/backfill/index.ts +309 -0
- package/src/bootstrap.ts +1 -1
- package/src/config.ts +126 -0
- package/src/index.ts +8 -1
- package/src/intelligence/__tests__/adaptive-validation.test.ts +694 -0
- package/src/intelligence/__tests__/impact.test.ts +165 -1
- package/src/intelligence/adaptive-validation.ts +764 -0
- package/src/intelligence/impact.ts +203 -0
- package/src/intelligence/index.ts +19 -0
- package/src/intelligence/types.ts +76 -0
- package/src/internal.ts +39 -0
- package/src/lib/__tests__/retry.test.ts +321 -0
- package/src/lib/index.ts +16 -0
- package/src/lib/retry.ts +224 -0
- package/src/lifecycle/__tests__/chain-store.test.ts +7 -0
- package/src/lifecycle/__tests__/tessera-engine.test.ts +52 -0
- package/src/nexus/sharing/index.ts +142 -2
- package/src/sessions/__tests__/session-edge-cases.test.ts +24 -1
- package/src/sessions/session-enforcement.ts +13 -2
- package/src/stats/index.ts +7 -0
- package/src/stats/workflow-telemetry.ts +502 -0
- package/src/store/__tests__/migration-safety.test.ts +3 -0
- package/src/store/__tests__/session-store.test.ts +132 -1
- package/src/store/__tests__/task-store.test.ts +22 -1
- package/src/store/__tests__/test-db-helper.ts +29 -2
- package/src/store/brain-schema.ts +4 -1
- package/src/store/converters.ts +2 -0
- package/src/store/cross-db-cleanup.ts +192 -0
- package/src/store/db-helpers.ts +2 -0
- package/src/store/migration-sqlite.ts +6 -0
- package/src/store/sqlite-data-accessor.ts +20 -28
- package/src/store/sqlite.ts +14 -2
- package/src/store/task-store.ts +6 -0
- package/src/store/tasks-schema.ts +59 -20
- package/src/tasks/__tests__/add.test.ts +16 -0
- package/src/tasks/__tests__/complete-unblocks.test.ts +10 -1
- package/src/tasks/__tests__/complete.test.ts +11 -2
- package/src/tasks/__tests__/epic-enforcement.test.ts +909 -0
- package/src/tasks/__tests__/minimal-test.test.ts +28 -0
- package/src/tasks/__tests__/pipeline-stage.test.ts +403 -0
- package/src/tasks/__tests__/update.test.ts +40 -6
- package/src/tasks/add.ts +128 -2
- package/src/tasks/complete.ts +29 -17
- package/src/tasks/enforcement.ts +127 -0
- package/src/tasks/epic-enforcement.ts +364 -0
- package/src/tasks/index.ts +1 -0
- package/src/tasks/pipeline-stage.ts +293 -0
- package/src/tasks/update.ts +62 -0
- package/templates/config.template.json +34 -111
- package/templates/global-config.template.json +24 -40
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
5
|
+
|
|
6
|
+
describe('minimal repro', () => {
|
|
7
|
+
let tempDir: string;
|
|
8
|
+
let cleoDir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
tempDir = mkdtempSync(join(tmpdir(), 'cleo-test-'));
|
|
12
|
+
cleoDir = join(tempDir, '.cleo');
|
|
13
|
+
mkdirSync(cleoDir, { recursive: true });
|
|
14
|
+
writeFileSync(join(cleoDir, 'config.json'), JSON.stringify({ test: true }));
|
|
15
|
+
process.env['CLEO_DIR'] = cleoDir;
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
delete process.env['CLEO_DIR'];
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('config exists in test', () => {
|
|
23
|
+
const contents = readdirSync(cleoDir);
|
|
24
|
+
console.log('contents:', contents);
|
|
25
|
+
console.log('CLEO_DIR:', process.env['CLEO_DIR']);
|
|
26
|
+
expect(existsSync(join(cleoDir, 'config.json'))).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for pipeline stage binding (T060).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Stage validation
|
|
6
|
+
* - Auto-assignment on task creation (standalone, epic, under-parent)
|
|
7
|
+
* - Forward-only transition enforcement on update
|
|
8
|
+
* - Stage persistence through rowToTask / taskToRow round-trip
|
|
9
|
+
*
|
|
10
|
+
* @task T060
|
|
11
|
+
* @epic T056
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { writeFile } from 'node:fs/promises';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
17
|
+
import { createTestDb, type TestDbEnv } from '../../store/__tests__/test-db-helper.js';
|
|
18
|
+
import type { DataAccessor } from '../../store/data-accessor.js';
|
|
19
|
+
import { addTask } from '../add.js';
|
|
20
|
+
|
|
21
|
+
/** Config that disables session, acceptance, and lifecycle enforcement for test isolation. */
|
|
22
|
+
const NO_SESSION_CONFIG = JSON.stringify({
|
|
23
|
+
lifecycle: { mode: 'off' },
|
|
24
|
+
enforcement: {
|
|
25
|
+
session: { requiredForMutate: false },
|
|
26
|
+
acceptance: { mode: 'off' },
|
|
27
|
+
},
|
|
28
|
+
verification: { enabled: false },
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
getPipelineStageOrder,
|
|
33
|
+
isPipelineTransitionForward,
|
|
34
|
+
isValidPipelineStage,
|
|
35
|
+
resolveDefaultPipelineStage,
|
|
36
|
+
TASK_PIPELINE_STAGES,
|
|
37
|
+
validatePipelineStage,
|
|
38
|
+
validatePipelineTransition,
|
|
39
|
+
} from '../pipeline-stage.js';
|
|
40
|
+
import { updateTask } from '../update.js';
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Unit tests — pure functions
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
describe('isValidPipelineStage', () => {
|
|
47
|
+
it('returns true for all canonical stages', () => {
|
|
48
|
+
for (const stage of TASK_PIPELINE_STAGES) {
|
|
49
|
+
expect(isValidPipelineStage(stage)).toBe(true);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('returns false for unknown strings', () => {
|
|
54
|
+
expect(isValidPipelineStage('unknown')).toBe(false);
|
|
55
|
+
expect(isValidPipelineStage('')).toBe(false);
|
|
56
|
+
expect(isValidPipelineStage('RESEARCH')).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('validatePipelineStage', () => {
|
|
61
|
+
it('does not throw for valid stages', () => {
|
|
62
|
+
expect(() => validatePipelineStage('research')).not.toThrow();
|
|
63
|
+
expect(() => validatePipelineStage('implementation')).not.toThrow();
|
|
64
|
+
expect(() => validatePipelineStage('contribution')).not.toThrow();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('throws CleoError for invalid stage', () => {
|
|
68
|
+
expect(() => validatePipelineStage('invalid')).toThrow('Invalid pipeline stage');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
describe('getPipelineStageOrder', () => {
|
|
73
|
+
it('returns expected order values', () => {
|
|
74
|
+
expect(getPipelineStageOrder('research')).toBe(1);
|
|
75
|
+
expect(getPipelineStageOrder('implementation')).toBe(6);
|
|
76
|
+
expect(getPipelineStageOrder('release')).toBe(9);
|
|
77
|
+
expect(getPipelineStageOrder('contribution')).toBe(10);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('returns -1 for unknown stage', () => {
|
|
81
|
+
expect(getPipelineStageOrder('unknown')).toBe(-1);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('isPipelineTransitionForward', () => {
|
|
86
|
+
it('allows same stage (no-op)', () => {
|
|
87
|
+
expect(isPipelineTransitionForward('research', 'research')).toBe(true);
|
|
88
|
+
expect(isPipelineTransitionForward('implementation', 'implementation')).toBe(true);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('allows forward transitions', () => {
|
|
92
|
+
expect(isPipelineTransitionForward('research', 'consensus')).toBe(true);
|
|
93
|
+
expect(isPipelineTransitionForward('research', 'implementation')).toBe(true);
|
|
94
|
+
expect(isPipelineTransitionForward('implementation', 'release')).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects backward transitions', () => {
|
|
98
|
+
expect(isPipelineTransitionForward('implementation', 'research')).toBe(false);
|
|
99
|
+
expect(isPipelineTransitionForward('testing', 'specification')).toBe(false);
|
|
100
|
+
expect(isPipelineTransitionForward('release', 'implementation')).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('allows transitions with unknown stages (defensive)', () => {
|
|
104
|
+
expect(isPipelineTransitionForward('unknown', 'research')).toBe(true);
|
|
105
|
+
expect(isPipelineTransitionForward('research', 'unknown')).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('validatePipelineTransition', () => {
|
|
110
|
+
it('allows forward transitions', () => {
|
|
111
|
+
expect(() => validatePipelineTransition('research', 'implementation')).not.toThrow();
|
|
112
|
+
expect(() => validatePipelineTransition('implementation', 'testing')).not.toThrow();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it('allows same-stage (no-op)', () => {
|
|
116
|
+
expect(() => validatePipelineTransition('implementation', 'implementation')).not.toThrow();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('allows any transition from null/undefined current stage', () => {
|
|
120
|
+
expect(() => validatePipelineTransition(null, 'implementation')).not.toThrow();
|
|
121
|
+
expect(() => validatePipelineTransition(undefined, 'research')).not.toThrow();
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('throws for backward transitions', () => {
|
|
125
|
+
expect(() => validatePipelineTransition('implementation', 'research')).toThrow(
|
|
126
|
+
'cannot move backward',
|
|
127
|
+
);
|
|
128
|
+
expect(() => validatePipelineTransition('testing', 'specification')).toThrow(
|
|
129
|
+
'cannot move backward',
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('throws for invalid new stage', () => {
|
|
134
|
+
expect(() => validatePipelineTransition(null, 'invalid')).toThrow('Invalid pipeline stage');
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('resolveDefaultPipelineStage', () => {
|
|
139
|
+
it('returns explicit stage when provided and valid', () => {
|
|
140
|
+
expect(resolveDefaultPipelineStage({ explicitStage: 'testing' })).toBe('testing');
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('inherits from parent pipeline stage', () => {
|
|
144
|
+
const result = resolveDefaultPipelineStage({
|
|
145
|
+
parentTask: { pipelineStage: 'specification', type: 'epic' },
|
|
146
|
+
});
|
|
147
|
+
expect(result).toBe('specification');
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('defaults to research for epics', () => {
|
|
151
|
+
expect(resolveDefaultPipelineStage({ taskType: 'epic' })).toBe('research');
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('defaults to implementation for standalone tasks', () => {
|
|
155
|
+
expect(resolveDefaultPipelineStage({ taskType: 'task' })).toBe('implementation');
|
|
156
|
+
expect(resolveDefaultPipelineStage({})).toBe('implementation');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('explicit stage takes priority over parent inheritance', () => {
|
|
160
|
+
const result = resolveDefaultPipelineStage({
|
|
161
|
+
explicitStage: 'testing',
|
|
162
|
+
parentTask: { pipelineStage: 'research', type: 'epic' },
|
|
163
|
+
});
|
|
164
|
+
expect(result).toBe('testing');
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it('parent inheritance takes priority over type default', () => {
|
|
168
|
+
const result = resolveDefaultPipelineStage({
|
|
169
|
+
taskType: 'task', // would default to implementation
|
|
170
|
+
parentTask: { pipelineStage: 'specification', type: 'epic' }, // but parent overrides
|
|
171
|
+
});
|
|
172
|
+
expect(result).toBe('specification');
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it('ignores parent with invalid pipelineStage', () => {
|
|
176
|
+
const result = resolveDefaultPipelineStage({
|
|
177
|
+
taskType: 'task',
|
|
178
|
+
parentTask: { pipelineStage: 'invalid_stage', type: 'epic' },
|
|
179
|
+
});
|
|
180
|
+
// Falls through to task default
|
|
181
|
+
expect(result).toBe('implementation');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Integration tests — addTask + updateTask with real SQLite
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
describe('addTask pipeline stage auto-assignment', () => {
|
|
190
|
+
let env: TestDbEnv;
|
|
191
|
+
let accessor: DataAccessor;
|
|
192
|
+
|
|
193
|
+
beforeEach(async () => {
|
|
194
|
+
env = await createTestDb();
|
|
195
|
+
accessor = env.accessor;
|
|
196
|
+
// Disable session enforcement so tests run without an active session
|
|
197
|
+
await writeFile(join(env.cleoDir, 'config.json'), NO_SESSION_CONFIG);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
afterEach(async () => {
|
|
201
|
+
await env.cleanup();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('assigns implementation to a standalone task by default', async () => {
|
|
205
|
+
const result = await addTask(
|
|
206
|
+
{ title: 'Standalone task', description: 'A standalone task without explicit stage' },
|
|
207
|
+
env.tempDir,
|
|
208
|
+
accessor,
|
|
209
|
+
);
|
|
210
|
+
expect(result.task.pipelineStage).toBe('implementation');
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('assigns research to an epic by default', async () => {
|
|
214
|
+
const result = await addTask(
|
|
215
|
+
{
|
|
216
|
+
title: 'My epic',
|
|
217
|
+
description: 'An epic with default stage',
|
|
218
|
+
type: 'epic',
|
|
219
|
+
},
|
|
220
|
+
env.tempDir,
|
|
221
|
+
accessor,
|
|
222
|
+
);
|
|
223
|
+
expect(result.task.pipelineStage).toBe('research');
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it('respects explicit pipelineStage on creation', async () => {
|
|
227
|
+
const result = await addTask(
|
|
228
|
+
{
|
|
229
|
+
title: 'Testing task',
|
|
230
|
+
description: 'Task at testing stage',
|
|
231
|
+
pipelineStage: 'testing',
|
|
232
|
+
},
|
|
233
|
+
env.tempDir,
|
|
234
|
+
accessor,
|
|
235
|
+
);
|
|
236
|
+
expect(result.task.pipelineStage).toBe('testing');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('inherits parent pipeline stage when creating child task', async () => {
|
|
240
|
+
// Create epic at specification stage
|
|
241
|
+
const epicResult = await addTask(
|
|
242
|
+
{
|
|
243
|
+
title: 'Parent epic',
|
|
244
|
+
description: 'Epic at specification stage',
|
|
245
|
+
type: 'epic',
|
|
246
|
+
pipelineStage: 'specification',
|
|
247
|
+
},
|
|
248
|
+
env.tempDir,
|
|
249
|
+
accessor,
|
|
250
|
+
);
|
|
251
|
+
const epicId = epicResult.task.id;
|
|
252
|
+
|
|
253
|
+
// Create child task under that epic — should inherit specification
|
|
254
|
+
const childResult = await addTask(
|
|
255
|
+
{
|
|
256
|
+
title: 'Child task',
|
|
257
|
+
description: 'Child task inheriting parent stage',
|
|
258
|
+
parentId: epicId,
|
|
259
|
+
},
|
|
260
|
+
env.tempDir,
|
|
261
|
+
accessor,
|
|
262
|
+
);
|
|
263
|
+
expect(childResult.task.pipelineStage).toBe('specification');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('rejects invalid pipelineStage on creation', async () => {
|
|
267
|
+
await expect(
|
|
268
|
+
addTask(
|
|
269
|
+
{
|
|
270
|
+
title: 'Bad stage',
|
|
271
|
+
description: 'Task with invalid stage',
|
|
272
|
+
pipelineStage: 'not_a_real_stage',
|
|
273
|
+
},
|
|
274
|
+
env.tempDir,
|
|
275
|
+
accessor,
|
|
276
|
+
),
|
|
277
|
+
).rejects.toThrow('Invalid pipeline stage');
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('persists pipelineStage through round-trip', async () => {
|
|
281
|
+
await addTask(
|
|
282
|
+
{
|
|
283
|
+
title: 'Persist test',
|
|
284
|
+
description: 'Test that stage persists to DB',
|
|
285
|
+
pipelineStage: 'validation',
|
|
286
|
+
},
|
|
287
|
+
env.tempDir,
|
|
288
|
+
accessor,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
// Load back from DB
|
|
292
|
+
const loaded = await accessor.loadSingleTask('T001');
|
|
293
|
+
expect(loaded?.pipelineStage).toBe('validation');
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('updateTask pipeline stage transitions', () => {
|
|
298
|
+
let env: TestDbEnv;
|
|
299
|
+
let accessor: DataAccessor;
|
|
300
|
+
|
|
301
|
+
beforeEach(async () => {
|
|
302
|
+
env = await createTestDb();
|
|
303
|
+
accessor = env.accessor;
|
|
304
|
+
// Disable session enforcement so tests run without an active session
|
|
305
|
+
await writeFile(join(env.cleoDir, 'config.json'), NO_SESSION_CONFIG);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
afterEach(async () => {
|
|
309
|
+
await env.cleanup();
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('allows forward stage transition', async () => {
|
|
313
|
+
// Create at implementation stage
|
|
314
|
+
await addTask(
|
|
315
|
+
{
|
|
316
|
+
title: 'Transition test',
|
|
317
|
+
description: 'Task for transition testing',
|
|
318
|
+
pipelineStage: 'implementation',
|
|
319
|
+
},
|
|
320
|
+
env.tempDir,
|
|
321
|
+
accessor,
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
// Move forward to validation
|
|
325
|
+
const result = await updateTask(
|
|
326
|
+
{ taskId: 'T001', pipelineStage: 'validation' },
|
|
327
|
+
env.tempDir,
|
|
328
|
+
accessor,
|
|
329
|
+
);
|
|
330
|
+
expect(result.task.pipelineStage).toBe('validation');
|
|
331
|
+
expect(result.changes).toContain('pipelineStage');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it('allows same-stage transition (no-op)', async () => {
|
|
335
|
+
await addTask(
|
|
336
|
+
{
|
|
337
|
+
title: 'Same stage test',
|
|
338
|
+
description: 'Task for same-stage transition test',
|
|
339
|
+
pipelineStage: 'implementation',
|
|
340
|
+
},
|
|
341
|
+
env.tempDir,
|
|
342
|
+
accessor,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
const result = await updateTask(
|
|
346
|
+
{ taskId: 'T001', pipelineStage: 'implementation' },
|
|
347
|
+
env.tempDir,
|
|
348
|
+
accessor,
|
|
349
|
+
);
|
|
350
|
+
expect(result.task.pipelineStage).toBe('implementation');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
it('rejects backward stage transition', async () => {
|
|
354
|
+
// Create at testing stage
|
|
355
|
+
await addTask(
|
|
356
|
+
{
|
|
357
|
+
title: 'Backward test',
|
|
358
|
+
description: 'Task that should not go backward',
|
|
359
|
+
pipelineStage: 'testing',
|
|
360
|
+
},
|
|
361
|
+
env.tempDir,
|
|
362
|
+
accessor,
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
// Attempt to move backward to implementation
|
|
366
|
+
await expect(
|
|
367
|
+
updateTask({ taskId: 'T001', pipelineStage: 'implementation' }, env.tempDir, accessor),
|
|
368
|
+
).rejects.toThrow('cannot move backward');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('rejects invalid stage on update', async () => {
|
|
372
|
+
await addTask(
|
|
373
|
+
{
|
|
374
|
+
title: 'Invalid update',
|
|
375
|
+
description: 'Task with upcoming invalid update',
|
|
376
|
+
pipelineStage: 'implementation',
|
|
377
|
+
},
|
|
378
|
+
env.tempDir,
|
|
379
|
+
accessor,
|
|
380
|
+
);
|
|
381
|
+
|
|
382
|
+
await expect(
|
|
383
|
+
updateTask({ taskId: 'T001', pipelineStage: 'not_real' }, env.tempDir, accessor),
|
|
384
|
+
).rejects.toThrow('Invalid pipeline stage');
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
it('persists updated stage to DB', async () => {
|
|
388
|
+
await addTask(
|
|
389
|
+
{
|
|
390
|
+
title: 'Persist update',
|
|
391
|
+
description: 'Task for update persistence test',
|
|
392
|
+
pipelineStage: 'implementation',
|
|
393
|
+
},
|
|
394
|
+
env.tempDir,
|
|
395
|
+
accessor,
|
|
396
|
+
);
|
|
397
|
+
|
|
398
|
+
await updateTask({ taskId: 'T001', pipelineStage: 'validation' }, env.tempDir, accessor);
|
|
399
|
+
|
|
400
|
+
const loaded = await accessor.loadSingleTask('T001');
|
|
401
|
+
expect(loaded?.pipelineStage).toBe('validation');
|
|
402
|
+
});
|
|
403
|
+
});
|
|
@@ -9,6 +9,7 @@ import { join } from 'node:path';
|
|
|
9
9
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
10
10
|
import { createTestDb, seedTasks, type TestDbEnv } from '../../store/__tests__/test-db-helper.js';
|
|
11
11
|
import type { DataAccessor } from '../../store/data-accessor.js';
|
|
12
|
+
import { resetDbState } from '../../store/sqlite.js';
|
|
12
13
|
import { updateTask } from '../update.js';
|
|
13
14
|
|
|
14
15
|
describe('updateTask', () => {
|
|
@@ -18,13 +19,21 @@ describe('updateTask', () => {
|
|
|
18
19
|
beforeEach(async () => {
|
|
19
20
|
env = await createTestDb();
|
|
20
21
|
accessor = env.accessor;
|
|
22
|
+
// Pin CLEO_DIR so concurrent workers cannot contaminate path resolution
|
|
23
|
+
process.env['CLEO_DIR'] = env.cleoDir;
|
|
21
24
|
await writeFile(
|
|
22
25
|
join(env.cleoDir, 'config.json'),
|
|
23
|
-
JSON.stringify({
|
|
26
|
+
JSON.stringify({
|
|
27
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
28
|
+
lifecycle: { mode: 'off' },
|
|
29
|
+
verification: { enabled: false },
|
|
30
|
+
}),
|
|
24
31
|
);
|
|
25
32
|
});
|
|
26
33
|
|
|
27
34
|
afterEach(async () => {
|
|
35
|
+
delete process.env['CLEO_DIR'];
|
|
36
|
+
resetDbState();
|
|
28
37
|
await env.cleanup();
|
|
29
38
|
});
|
|
30
39
|
|
|
@@ -221,7 +230,12 @@ describe('updateTask', () => {
|
|
|
221
230
|
]);
|
|
222
231
|
await writeFile(
|
|
223
232
|
join(env.cleoDir, 'config.json'),
|
|
224
|
-
JSON.stringify({
|
|
233
|
+
JSON.stringify({
|
|
234
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
235
|
+
lifecycle: { mode: 'off' },
|
|
236
|
+
verification: { enabled: false },
|
|
237
|
+
hierarchy: { maxDepth: 3, maxSiblings: 20 },
|
|
238
|
+
}),
|
|
225
239
|
);
|
|
226
240
|
|
|
227
241
|
const result = await updateTask({ taskId: 'T002', parentId: 'T001' }, env.tempDir, accessor);
|
|
@@ -251,7 +265,12 @@ describe('updateTask', () => {
|
|
|
251
265
|
]);
|
|
252
266
|
await writeFile(
|
|
253
267
|
join(env.cleoDir, 'config.json'),
|
|
254
|
-
JSON.stringify({
|
|
268
|
+
JSON.stringify({
|
|
269
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
270
|
+
lifecycle: { mode: 'off' },
|
|
271
|
+
verification: { enabled: false },
|
|
272
|
+
hierarchy: { maxDepth: 3, maxSiblings: 20 },
|
|
273
|
+
}),
|
|
255
274
|
);
|
|
256
275
|
|
|
257
276
|
const result = await updateTask({ taskId: 'T002', parentId: null }, env.tempDir, accessor);
|
|
@@ -281,7 +300,12 @@ describe('updateTask', () => {
|
|
|
281
300
|
]);
|
|
282
301
|
await writeFile(
|
|
283
302
|
join(env.cleoDir, 'config.json'),
|
|
284
|
-
JSON.stringify({
|
|
303
|
+
JSON.stringify({
|
|
304
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
305
|
+
lifecycle: { mode: 'off' },
|
|
306
|
+
verification: { enabled: false },
|
|
307
|
+
hierarchy: { maxDepth: 3, maxSiblings: 20 },
|
|
308
|
+
}),
|
|
285
309
|
);
|
|
286
310
|
|
|
287
311
|
const result = await updateTask({ taskId: 'T002', parentId: '' }, env.tempDir, accessor);
|
|
@@ -311,7 +335,12 @@ describe('updateTask', () => {
|
|
|
311
335
|
]);
|
|
312
336
|
await writeFile(
|
|
313
337
|
join(env.cleoDir, 'config.json'),
|
|
314
|
-
JSON.stringify({
|
|
338
|
+
JSON.stringify({
|
|
339
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
340
|
+
lifecycle: { mode: 'off' },
|
|
341
|
+
verification: { enabled: false },
|
|
342
|
+
hierarchy: { maxDepth: 3, maxSiblings: 20 },
|
|
343
|
+
}),
|
|
315
344
|
);
|
|
316
345
|
|
|
317
346
|
await expect(
|
|
@@ -340,7 +369,12 @@ describe('updateTask', () => {
|
|
|
340
369
|
]);
|
|
341
370
|
await writeFile(
|
|
342
371
|
join(env.cleoDir, 'config.json'),
|
|
343
|
-
JSON.stringify({
|
|
372
|
+
JSON.stringify({
|
|
373
|
+
enforcement: { session: { requiredForMutate: false } },
|
|
374
|
+
lifecycle: { mode: 'off' },
|
|
375
|
+
verification: { enabled: false },
|
|
376
|
+
hierarchy: { maxDepth: 3, maxSiblings: 20 },
|
|
377
|
+
}),
|
|
344
378
|
);
|
|
345
379
|
|
|
346
380
|
const result = await updateTask(
|