@ktpartners/dgs-platform 2.7.5 → 2.8.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/CHANGELOG.md +16 -0
- package/agents/dgs-executor.md +0 -52
- package/deliver-great-systems/bin/dgs-tools.cjs +66 -10
- package/deliver-great-systems/bin/lib/commands.cjs +1 -8
- package/deliver-great-systems/bin/lib/config.cjs +9 -90
- package/deliver-great-systems/bin/lib/context.cjs +2 -2
- package/deliver-great-systems/bin/lib/context.test.cjs +100 -100
- package/deliver-great-systems/bin/lib/core.cjs +17 -57
- package/deliver-great-systems/bin/lib/core.test.cjs +166 -170
- package/deliver-great-systems/bin/lib/docs.cjs +3 -3
- package/deliver-great-systems/bin/lib/docs.test.cjs +14 -7
- package/deliver-great-systems/bin/lib/execution.cjs +2 -2
- package/deliver-great-systems/bin/lib/execution.test.cjs +65 -67
- package/deliver-great-systems/bin/lib/ideas.cjs +4 -4
- package/deliver-great-systems/bin/lib/ideas.test.cjs +45 -44
- package/deliver-great-systems/bin/lib/init.cjs +9 -4
- package/deliver-great-systems/bin/lib/init.test.cjs +242 -175
- package/deliver-great-systems/bin/lib/jobs.cjs +1 -1
- package/deliver-great-systems/bin/lib/jobs.test.cjs +203 -202
- package/deliver-great-systems/bin/lib/migration.cjs +256 -281
- package/deliver-great-systems/bin/lib/migration.test.cjs +385 -440
- package/deliver-great-systems/bin/lib/milestone.cjs +1 -1
- package/deliver-great-systems/bin/lib/overlap.cjs +4 -4
- package/deliver-great-systems/bin/lib/overlap.test.cjs +45 -44
- package/deliver-great-systems/bin/lib/path-audit.test.cjs +16 -22
- package/deliver-great-systems/bin/lib/paths.cjs +60 -59
- package/deliver-great-systems/bin/lib/paths.test.cjs +192 -225
- package/deliver-great-systems/bin/lib/phase.cjs +5 -4
- package/deliver-great-systems/bin/lib/projects.cjs +8 -8
- package/deliver-great-systems/bin/lib/projects.test.cjs +75 -74
- package/deliver-great-systems/bin/lib/repos.cjs +94 -230
- package/deliver-great-systems/bin/lib/repos.test.cjs +84 -75
- package/deliver-great-systems/bin/lib/search.cjs +4 -4
- package/deliver-great-systems/bin/lib/specs.cjs +2 -2
- package/deliver-great-systems/bin/lib/sync.cjs +1 -1
- package/deliver-great-systems/bin/lib/template.cjs +3 -3
- package/deliver-great-systems/bin/lib/test-helpers.cjs +59 -162
- package/deliver-great-systems/bin/lib/verify.cjs +3 -3
- package/deliver-great-systems/references/planning-config.md +7 -8
- package/deliver-great-systems/workflows/add-tests.md +1 -1
- package/deliver-great-systems/workflows/approve-spec.md +1 -11
- package/deliver-great-systems/workflows/complete-milestone.md +2 -2
- package/deliver-great-systems/workflows/consolidate-ideas.md +1 -1
- package/deliver-great-systems/workflows/create-milestone-job.md +2 -2
- package/deliver-great-systems/workflows/discuss-phase.md +2 -2
- package/deliver-great-systems/workflows/execute-phase.md +63 -4
- package/deliver-great-systems/workflows/execute-plan.md +0 -51
- package/deliver-great-systems/workflows/find-related-ideas.md +1 -1
- package/deliver-great-systems/workflows/help.md +25 -58
- package/deliver-great-systems/workflows/init-product.md +14 -451
- package/deliver-great-systems/workflows/map-codebase.md +109 -0
- package/deliver-great-systems/workflows/new-project.md +0 -1
- package/deliver-great-systems/workflows/quick.md +2 -2
- package/deliver-great-systems/workflows/run-job.md +56 -0
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for core.cjs
|
|
2
|
+
* Tests for core.cjs path resolution functions
|
|
3
3
|
*
|
|
4
4
|
* Uses Node.js built-in test runner (node:test) and assert (node:assert).
|
|
5
5
|
* Each test creates an isolated temp directory fixture and cleans up after.
|
|
@@ -26,17 +26,17 @@ const {
|
|
|
26
26
|
getProjectDir,
|
|
27
27
|
} = require('./core.cjs');
|
|
28
28
|
|
|
29
|
-
// ───
|
|
29
|
+
// ─── Root layout (no v2 markers) Tests ───────────────────────────────────────
|
|
30
30
|
|
|
31
|
-
describe('
|
|
31
|
+
describe('root layout (no v2 markers)', () => {
|
|
32
32
|
let fixture;
|
|
33
33
|
|
|
34
34
|
beforeEach(() => {
|
|
35
35
|
fixture = createFixture({
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
36
|
+
'config.json': JSON.stringify({}),
|
|
37
|
+
'STATE.md': '# State',
|
|
38
|
+
'ROADMAP.md': '# Roadmap',
|
|
39
|
+
'phases/': null,
|
|
40
40
|
});
|
|
41
41
|
});
|
|
42
42
|
|
|
@@ -44,24 +44,24 @@ describe('v1 mode (no v2 markers)', () => {
|
|
|
44
44
|
fixture.cleanup();
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
it('getProjectRoot returns .
|
|
47
|
+
it('getProjectRoot returns .', () => {
|
|
48
48
|
const result = getProjectRoot(fixture.cwd);
|
|
49
|
-
assert.equal(result, '.
|
|
49
|
+
assert.equal(result, '.');
|
|
50
50
|
});
|
|
51
51
|
|
|
52
|
-
it('resolveProjectPath with relative path returns
|
|
52
|
+
it('resolveProjectPath with relative path returns relative path', () => {
|
|
53
53
|
const result = resolveProjectPath(fixture.cwd, 'STATE.md');
|
|
54
|
-
assert.equal(result,
|
|
54
|
+
assert.equal(result, 'STATE.md');
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it('resolveProjectPath with nested path returns correct path', () => {
|
|
58
58
|
const result = resolveProjectPath(fixture.cwd, 'phases/01-foo');
|
|
59
|
-
assert.equal(result, path.join('
|
|
59
|
+
assert.equal(result, path.join('phases', '01-foo'));
|
|
60
60
|
});
|
|
61
61
|
|
|
62
|
-
it('resolveProjectPath without second arg returns .
|
|
62
|
+
it('resolveProjectPath without second arg returns .', () => {
|
|
63
63
|
const result = resolveProjectPath(fixture.cwd);
|
|
64
|
-
assert.equal(result, '.
|
|
64
|
+
assert.equal(result, '.');
|
|
65
65
|
});
|
|
66
66
|
|
|
67
67
|
it('isV2Install returns false', () => {
|
|
@@ -77,12 +77,13 @@ describe('v2 mode with current_project', () => {
|
|
|
77
77
|
|
|
78
78
|
beforeEach(() => {
|
|
79
79
|
fixture = createFixture({
|
|
80
|
-
'
|
|
81
|
-
'.
|
|
82
|
-
'.
|
|
83
|
-
'.
|
|
84
|
-
'
|
|
85
|
-
'
|
|
80
|
+
'config.json': JSON.stringify({}),
|
|
81
|
+
'config.local.json': JSON.stringify({ current_project: 'auth-overhaul' }),
|
|
82
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
83
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
84
|
+
'projects/auth-overhaul/STATE.md': '# State',
|
|
85
|
+
'projects/auth-overhaul/ROADMAP.md': '# Roadmap',
|
|
86
|
+
'projects/auth-overhaul/phases/': null,
|
|
86
87
|
});
|
|
87
88
|
});
|
|
88
89
|
|
|
@@ -90,19 +91,19 @@ describe('v2 mode with current_project', () => {
|
|
|
90
91
|
fixture.cleanup();
|
|
91
92
|
});
|
|
92
93
|
|
|
93
|
-
it('getProjectRoot returns
|
|
94
|
+
it('getProjectRoot returns projects/<project>', () => {
|
|
94
95
|
const result = getProjectRoot(fixture.cwd);
|
|
95
|
-
assert.equal(result, path.join('
|
|
96
|
+
assert.equal(result, path.join('projects', 'auth-overhaul'));
|
|
96
97
|
});
|
|
97
98
|
|
|
98
99
|
it('resolveProjectPath with STATE.md returns project-qualified path', () => {
|
|
99
100
|
const result = resolveProjectPath(fixture.cwd, 'STATE.md');
|
|
100
|
-
assert.equal(result, path.join('
|
|
101
|
+
assert.equal(result, path.join('projects', 'auth-overhaul', 'STATE.md'));
|
|
101
102
|
});
|
|
102
103
|
|
|
103
104
|
it('resolveProjectPath with nested path returns project-qualified path', () => {
|
|
104
105
|
const result = resolveProjectPath(fixture.cwd, 'phases/01-foo');
|
|
105
|
-
assert.equal(result, path.join('
|
|
106
|
+
assert.equal(result, path.join('projects', 'auth-overhaul', 'phases', '01-foo'));
|
|
106
107
|
});
|
|
107
108
|
});
|
|
108
109
|
|
|
@@ -111,8 +112,8 @@ describe('v2 mode with current_project', () => {
|
|
|
111
112
|
describe('v2 mode without current_project (guard trigger)', () => {
|
|
112
113
|
it('throws NO_CURRENT_PROJECT_V2 when PROJECTS.md exists with valid header', () => {
|
|
113
114
|
const fixture = createFixture({
|
|
114
|
-
'
|
|
115
|
-
'
|
|
115
|
+
'config.json': JSON.stringify({}),
|
|
116
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
116
117
|
});
|
|
117
118
|
|
|
118
119
|
try {
|
|
@@ -127,8 +128,8 @@ describe('v2 mode without current_project (guard trigger)', () => {
|
|
|
127
128
|
|
|
128
129
|
it('throws NO_CURRENT_PROJECT_V2 when REPOS.md exists with valid header', () => {
|
|
129
130
|
const fixture = createFixture({
|
|
130
|
-
'
|
|
131
|
-
'
|
|
131
|
+
'config.json': JSON.stringify({}),
|
|
132
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
132
133
|
});
|
|
133
134
|
|
|
134
135
|
try {
|
|
@@ -147,8 +148,8 @@ describe('v2 mode without current_project (guard trigger)', () => {
|
|
|
147
148
|
describe('strict v2 marker validation', () => {
|
|
148
149
|
it('isV2Install returns false when PROJECTS.md has non-DGS content', () => {
|
|
149
150
|
const fixture = createFixture({
|
|
150
|
-
'
|
|
151
|
-
'
|
|
151
|
+
'config.json': JSON.stringify({}),
|
|
152
|
+
'PROJECTS.md': 'Shopping List\n- apples\n- bananas',
|
|
152
153
|
});
|
|
153
154
|
|
|
154
155
|
try {
|
|
@@ -160,8 +161,8 @@ describe('strict v2 marker validation', () => {
|
|
|
160
161
|
|
|
161
162
|
it('isV2Install returns true when PROJECTS.md starts with # Projects', () => {
|
|
162
163
|
const fixture = createFixture({
|
|
163
|
-
'
|
|
164
|
-
'
|
|
164
|
+
'config.json': JSON.stringify({}),
|
|
165
|
+
'PROJECTS.md': '# Projects\n\n## Active\n',
|
|
165
166
|
});
|
|
166
167
|
|
|
167
168
|
try {
|
|
@@ -173,8 +174,8 @@ describe('strict v2 marker validation', () => {
|
|
|
173
174
|
|
|
174
175
|
it('isV2Install returns true when REPOS.md starts with # Repos', () => {
|
|
175
176
|
const fixture = createFixture({
|
|
176
|
-
'
|
|
177
|
-
'
|
|
177
|
+
'config.json': JSON.stringify({}),
|
|
178
|
+
'REPOS.md': '# Repos\n\n| Name | Path |\n',
|
|
178
179
|
});
|
|
179
180
|
|
|
180
181
|
try {
|
|
@@ -186,7 +187,7 @@ describe('strict v2 marker validation', () => {
|
|
|
186
187
|
|
|
187
188
|
it('isV2Install returns false when neither file exists', () => {
|
|
188
189
|
const fixture = createFixture({
|
|
189
|
-
'
|
|
190
|
+
'config.json': JSON.stringify({}),
|
|
190
191
|
});
|
|
191
192
|
|
|
192
193
|
try {
|
|
@@ -202,8 +203,9 @@ describe('strict v2 marker validation', () => {
|
|
|
202
203
|
describe('project directory validation', () => {
|
|
203
204
|
it('throws PROJECT_NOT_FOUND when current_project dir does not exist', () => {
|
|
204
205
|
const fixture = createFixture({
|
|
205
|
-
'
|
|
206
|
-
'.
|
|
206
|
+
'config.json': JSON.stringify({}),
|
|
207
|
+
'config.local.json': JSON.stringify({ current_project: 'ghost-project' }),
|
|
208
|
+
'PROJECTS.md': '# Projects\n',
|
|
207
209
|
});
|
|
208
210
|
|
|
209
211
|
try {
|
|
@@ -222,9 +224,10 @@ describe('project directory validation', () => {
|
|
|
222
224
|
describe('completed project guards', () => {
|
|
223
225
|
it('getProjectRoot throws PROJECT_COMPLETED when project STATUS is completed', () => {
|
|
224
226
|
const fixture = createFixture({
|
|
225
|
-
'
|
|
226
|
-
'.
|
|
227
|
-
'.
|
|
227
|
+
'config.json': JSON.stringify({}),
|
|
228
|
+
'config.local.json': JSON.stringify({ current_project: 'done-proj' }),
|
|
229
|
+
'PROJECTS.md': '# Projects\n',
|
|
230
|
+
'projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\nCompleted: 2026-02-20\n',
|
|
228
231
|
});
|
|
229
232
|
|
|
230
233
|
try {
|
|
@@ -239,9 +242,10 @@ describe('completed project guards', () => {
|
|
|
239
242
|
|
|
240
243
|
it('getProjectRoot throws PROJECT_COMPLETED for case-insensitive status (Completed)', () => {
|
|
241
244
|
const fixture = createFixture({
|
|
242
|
-
'
|
|
243
|
-
'.
|
|
244
|
-
'.
|
|
245
|
+
'config.json': JSON.stringify({}),
|
|
246
|
+
'config.local.json': JSON.stringify({ current_project: 'done-proj' }),
|
|
247
|
+
'PROJECTS.md': '# Projects\n',
|
|
248
|
+
'projects/done-proj/STATE.md': '# Project State\n\nStatus: Completed\n',
|
|
245
249
|
});
|
|
246
250
|
|
|
247
251
|
try {
|
|
@@ -256,14 +260,15 @@ describe('completed project guards', () => {
|
|
|
256
260
|
|
|
257
261
|
it('getProjectRoot does NOT throw for active project', () => {
|
|
258
262
|
const fixture = createFixture({
|
|
259
|
-
'
|
|
260
|
-
'.
|
|
261
|
-
'.
|
|
263
|
+
'config.json': JSON.stringify({}),
|
|
264
|
+
'config.local.json': JSON.stringify({ current_project: 'active-proj' }),
|
|
265
|
+
'PROJECTS.md': '# Projects\n',
|
|
266
|
+
'projects/active-proj/STATE.md': '# Project State\n\nStatus: Active\n',
|
|
262
267
|
});
|
|
263
268
|
|
|
264
269
|
try {
|
|
265
270
|
const result = getProjectRoot(fixture.cwd);
|
|
266
|
-
assert.equal(result, path.join('
|
|
271
|
+
assert.equal(result, path.join('projects', 'active-proj'));
|
|
267
272
|
} finally {
|
|
268
273
|
fixture.cleanup();
|
|
269
274
|
}
|
|
@@ -271,14 +276,15 @@ describe('completed project guards', () => {
|
|
|
271
276
|
|
|
272
277
|
it('getProjectRoot does NOT throw when STATE.md is missing', () => {
|
|
273
278
|
const fixture = createFixture({
|
|
274
|
-
'
|
|
275
|
-
'.
|
|
276
|
-
'.
|
|
279
|
+
'config.json': JSON.stringify({}),
|
|
280
|
+
'config.local.json': JSON.stringify({ current_project: 'no-state-proj' }),
|
|
281
|
+
'PROJECTS.md': '# Projects\n',
|
|
282
|
+
'projects/no-state-proj/': null,
|
|
277
283
|
});
|
|
278
284
|
|
|
279
285
|
try {
|
|
280
286
|
const result = getProjectRoot(fixture.cwd);
|
|
281
|
-
assert.equal(result, path.join('
|
|
287
|
+
assert.equal(result, path.join('projects', 'no-state-proj'));
|
|
282
288
|
} finally {
|
|
283
289
|
fixture.cleanup();
|
|
284
290
|
}
|
|
@@ -286,9 +292,10 @@ describe('completed project guards', () => {
|
|
|
286
292
|
|
|
287
293
|
it('requireProjectRoot propagates PROJECT_COMPLETED', () => {
|
|
288
294
|
const fixture = createFixture({
|
|
289
|
-
'
|
|
290
|
-
'.
|
|
291
|
-
'.
|
|
295
|
+
'config.json': JSON.stringify({}),
|
|
296
|
+
'config.local.json': JSON.stringify({ current_project: 'done-proj' }),
|
|
297
|
+
'PROJECTS.md': '# Projects\n',
|
|
298
|
+
'projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\n',
|
|
292
299
|
});
|
|
293
300
|
|
|
294
301
|
try {
|
|
@@ -303,9 +310,10 @@ describe('completed project guards', () => {
|
|
|
303
310
|
|
|
304
311
|
it('resolveProjectPath throws PROJECT_COMPLETED for completed project', () => {
|
|
305
312
|
const fixture = createFixture({
|
|
306
|
-
'
|
|
307
|
-
'.
|
|
308
|
-
'.
|
|
313
|
+
'config.json': JSON.stringify({}),
|
|
314
|
+
'config.local.json': JSON.stringify({ current_project: 'done-proj' }),
|
|
315
|
+
'PROJECTS.md': '# Projects\n',
|
|
316
|
+
'projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\n',
|
|
309
317
|
});
|
|
310
318
|
|
|
311
319
|
try {
|
|
@@ -320,8 +328,8 @@ describe('completed project guards', () => {
|
|
|
320
328
|
|
|
321
329
|
it('isProjectCompleted returns true for completed project', () => {
|
|
322
330
|
const fixture = createFixture({
|
|
323
|
-
'
|
|
324
|
-
'
|
|
331
|
+
'config.json': JSON.stringify({}),
|
|
332
|
+
'projects/done-proj/STATE.md': '# Project State\n\nStatus: completed\n',
|
|
325
333
|
});
|
|
326
334
|
|
|
327
335
|
try {
|
|
@@ -333,8 +341,8 @@ describe('completed project guards', () => {
|
|
|
333
341
|
|
|
334
342
|
it('isProjectCompleted returns false for active project', () => {
|
|
335
343
|
const fixture = createFixture({
|
|
336
|
-
'
|
|
337
|
-
'
|
|
344
|
+
'config.json': JSON.stringify({}),
|
|
345
|
+
'projects/active-proj/STATE.md': '# Project State\n\nStatus: Active\n',
|
|
338
346
|
});
|
|
339
347
|
|
|
340
348
|
try {
|
|
@@ -346,8 +354,8 @@ describe('completed project guards', () => {
|
|
|
346
354
|
|
|
347
355
|
it('isProjectCompleted returns false when STATE.md missing', () => {
|
|
348
356
|
const fixture = createFixture({
|
|
349
|
-
'
|
|
350
|
-
'
|
|
357
|
+
'config.json': JSON.stringify({}),
|
|
358
|
+
'projects/no-state-proj/': null,
|
|
351
359
|
});
|
|
352
360
|
|
|
353
361
|
try {
|
|
@@ -361,11 +369,11 @@ describe('completed project guards', () => {
|
|
|
361
369
|
// ─── getProjectFolders Tests ──────────────────────────────────────────────────
|
|
362
370
|
|
|
363
371
|
describe('getProjectFolders', () => {
|
|
364
|
-
it('returns empty array
|
|
372
|
+
it('returns empty array when no v2 markers exist', () => {
|
|
365
373
|
const fixture = createFixture({
|
|
366
|
-
'
|
|
367
|
-
'
|
|
368
|
-
'
|
|
374
|
+
'config.json': JSON.stringify({}),
|
|
375
|
+
'STATE.md': '# State',
|
|
376
|
+
'phases/': null,
|
|
369
377
|
});
|
|
370
378
|
|
|
371
379
|
try {
|
|
@@ -378,14 +386,14 @@ describe('getProjectFolders', () => {
|
|
|
378
386
|
|
|
379
387
|
it('returns project folder names from projects/ directory', () => {
|
|
380
388
|
const fixture = createFixture({
|
|
381
|
-
'
|
|
382
|
-
'
|
|
383
|
-
'
|
|
384
|
-
'
|
|
385
|
-
'
|
|
386
|
-
'
|
|
387
|
-
'
|
|
388
|
-
'
|
|
389
|
+
'config.json': JSON.stringify({}),
|
|
390
|
+
'PROJECTS.md': '# Projects\n',
|
|
391
|
+
'projects/auth-overhaul/STATE.md': '# State',
|
|
392
|
+
'projects/dashboard-v2/STATE.md': '# State',
|
|
393
|
+
'phases/': null,
|
|
394
|
+
'codebase/': null,
|
|
395
|
+
'archive/': null,
|
|
396
|
+
'todos/': null,
|
|
389
397
|
});
|
|
390
398
|
|
|
391
399
|
try {
|
|
@@ -398,10 +406,10 @@ describe('getProjectFolders', () => {
|
|
|
398
406
|
|
|
399
407
|
it('only includes directories containing STATE.md', () => {
|
|
400
408
|
const fixture = createFixture({
|
|
401
|
-
'
|
|
402
|
-
'
|
|
403
|
-
'
|
|
404
|
-
'
|
|
409
|
+
'config.json': JSON.stringify({}),
|
|
410
|
+
'PROJECTS.md': '# Projects\n',
|
|
411
|
+
'projects/valid-project/STATE.md': '# State',
|
|
412
|
+
'projects/empty-folder/': null,
|
|
405
413
|
});
|
|
406
414
|
|
|
407
415
|
try {
|
|
@@ -414,9 +422,9 @@ describe('getProjectFolders', () => {
|
|
|
414
422
|
|
|
415
423
|
it('excludes dot-directories', () => {
|
|
416
424
|
const fixture = createFixture({
|
|
417
|
-
'
|
|
418
|
-
'
|
|
419
|
-
'
|
|
425
|
+
'config.json': JSON.stringify({}),
|
|
426
|
+
'projects/.hidden/STATE.md': '# State',
|
|
427
|
+
'projects/real-project/STATE.md': '# State',
|
|
420
428
|
});
|
|
421
429
|
|
|
422
430
|
try {
|
|
@@ -427,21 +435,11 @@ describe('getProjectFolders', () => {
|
|
|
427
435
|
}
|
|
428
436
|
});
|
|
429
437
|
|
|
430
|
-
it('returns empty array when .planning does not exist', () => {
|
|
431
|
-
const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'dgs-test-'));
|
|
432
|
-
try {
|
|
433
|
-
const result = getProjectFolders(cwd);
|
|
434
|
-
assert.deepEqual(result, []);
|
|
435
|
-
} finally {
|
|
436
|
-
fs.rmSync(cwd, { recursive: true, force: true });
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
|
|
440
438
|
it('returns empty array when projects/ directory does not exist', () => {
|
|
441
439
|
const fixture = createFixture({
|
|
442
|
-
'
|
|
443
|
-
'
|
|
444
|
-
'
|
|
440
|
+
'config.json': JSON.stringify({}),
|
|
441
|
+
'PROJECTS.md': '# Projects\n',
|
|
442
|
+
'phases/': null,
|
|
445
443
|
});
|
|
446
444
|
|
|
447
445
|
try {
|
|
@@ -476,37 +474,39 @@ describe('getV2Hint', () => {
|
|
|
476
474
|
describe('edge cases', () => {
|
|
477
475
|
it('resolveProjectPath with empty string returns project root', () => {
|
|
478
476
|
const fixture = createFixture({
|
|
479
|
-
'
|
|
477
|
+
'config.json': JSON.stringify({}),
|
|
480
478
|
});
|
|
481
479
|
|
|
482
480
|
try {
|
|
483
481
|
const result = resolveProjectPath(fixture.cwd, '');
|
|
484
|
-
assert.equal(result, '.
|
|
482
|
+
assert.equal(result, '.');
|
|
485
483
|
} finally {
|
|
486
484
|
fixture.cleanup();
|
|
487
485
|
}
|
|
488
486
|
});
|
|
489
487
|
|
|
490
|
-
it('getProjectRoot
|
|
491
|
-
const
|
|
488
|
+
it('getProjectRoot returns . when no markers exist', () => {
|
|
489
|
+
const fixture = createFixture({
|
|
490
|
+
'config.json': JSON.stringify({}),
|
|
491
|
+
});
|
|
492
|
+
|
|
492
493
|
try {
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
assert.equal(result, '.planning');
|
|
494
|
+
const result = getProjectRoot(fixture.cwd);
|
|
495
|
+
assert.equal(result, '.');
|
|
496
496
|
} finally {
|
|
497
|
-
|
|
497
|
+
fixture.cleanup();
|
|
498
498
|
}
|
|
499
499
|
});
|
|
500
500
|
|
|
501
501
|
it('treats empty string current_project as unset', () => {
|
|
502
502
|
const fixture = createFixture({
|
|
503
|
-
'
|
|
503
|
+
'config.json': JSON.stringify({}),
|
|
504
|
+
'config.local.json': JSON.stringify({ current_project: '' }),
|
|
504
505
|
});
|
|
505
506
|
|
|
506
507
|
try {
|
|
507
|
-
// Should return .planning (v1 fallback), not .planning/
|
|
508
508
|
const result = getProjectRoot(fixture.cwd);
|
|
509
|
-
assert.equal(result, '.
|
|
509
|
+
assert.equal(result, '.');
|
|
510
510
|
} finally {
|
|
511
511
|
fixture.cleanup();
|
|
512
512
|
}
|
|
@@ -514,8 +514,9 @@ describe('edge cases', () => {
|
|
|
514
514
|
|
|
515
515
|
it('rejects current_project with path traversal (..)', () => {
|
|
516
516
|
const fixture = createFixture({
|
|
517
|
-
'
|
|
518
|
-
'.
|
|
517
|
+
'config.json': JSON.stringify({}),
|
|
518
|
+
'config.local.json': JSON.stringify({ current_project: '../etc' }),
|
|
519
|
+
'PROJECTS.md': '# Projects\n',
|
|
519
520
|
});
|
|
520
521
|
|
|
521
522
|
try {
|
|
@@ -530,8 +531,9 @@ describe('edge cases', () => {
|
|
|
530
531
|
|
|
531
532
|
it('rejects current_project with forward slash', () => {
|
|
532
533
|
const fixture = createFixture({
|
|
533
|
-
'
|
|
534
|
-
'.
|
|
534
|
+
'config.json': JSON.stringify({}),
|
|
535
|
+
'config.local.json': JSON.stringify({ current_project: 'foo/bar' }),
|
|
536
|
+
'PROJECTS.md': '# Projects\n',
|
|
535
537
|
});
|
|
536
538
|
|
|
537
539
|
try {
|
|
@@ -546,8 +548,9 @@ describe('edge cases', () => {
|
|
|
546
548
|
|
|
547
549
|
it('rejects current_project with backslash', () => {
|
|
548
550
|
const fixture = createFixture({
|
|
549
|
-
'
|
|
550
|
-
'.
|
|
551
|
+
'config.json': JSON.stringify({}),
|
|
552
|
+
'config.local.json': JSON.stringify({ current_project: 'foo\\bar' }),
|
|
553
|
+
'PROJECTS.md': '# Projects\n',
|
|
551
554
|
});
|
|
552
555
|
|
|
553
556
|
try {
|
|
@@ -564,12 +567,18 @@ describe('edge cases', () => {
|
|
|
564
567
|
// ─── requireProjectRoot Tests ─────────────────────────────────────────────────
|
|
565
568
|
|
|
566
569
|
describe('requireProjectRoot', () => {
|
|
567
|
-
it('returns
|
|
568
|
-
const fixture =
|
|
570
|
+
it('returns projects/<project> when v2 install has current_project set and directory exists', () => {
|
|
571
|
+
const fixture = createFixture({
|
|
572
|
+
'config.json': JSON.stringify({}),
|
|
573
|
+
'config.local.json': JSON.stringify({ current_project: 'my-app' }),
|
|
574
|
+
'PROJECTS.md': '# Projects\n',
|
|
575
|
+
'REPOS.md': '# Repos\n',
|
|
576
|
+
'projects/my-app/STATE.md': '# State',
|
|
577
|
+
});
|
|
569
578
|
|
|
570
579
|
try {
|
|
571
580
|
const result = requireProjectRoot(fixture.cwd);
|
|
572
|
-
assert.equal(result, path.join('
|
|
581
|
+
assert.equal(result, path.join('projects', 'my-app'));
|
|
573
582
|
} finally {
|
|
574
583
|
fixture.cleanup();
|
|
575
584
|
}
|
|
@@ -577,8 +586,8 @@ describe('requireProjectRoot', () => {
|
|
|
577
586
|
|
|
578
587
|
it('throws NO_CURRENT_PROJECT_V2 when v2 install has no current_project', () => {
|
|
579
588
|
const fixture = createFixture({
|
|
580
|
-
'
|
|
581
|
-
'
|
|
589
|
+
'config.json': JSON.stringify({}),
|
|
590
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
582
591
|
});
|
|
583
592
|
|
|
584
593
|
try {
|
|
@@ -593,8 +602,9 @@ describe('requireProjectRoot', () => {
|
|
|
593
602
|
|
|
594
603
|
it('throws NO_CURRENT_PROJECT_V2 when v2 install has empty string current_project', () => {
|
|
595
604
|
const fixture = createFixture({
|
|
596
|
-
'
|
|
597
|
-
'.
|
|
605
|
+
'config.json': JSON.stringify({}),
|
|
606
|
+
'config.local.json': JSON.stringify({ current_project: '' }),
|
|
607
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
598
608
|
});
|
|
599
609
|
|
|
600
610
|
try {
|
|
@@ -609,8 +619,9 @@ describe('requireProjectRoot', () => {
|
|
|
609
619
|
|
|
610
620
|
it('throws NO_CURRENT_PROJECT_V2 when v2 install has whitespace-only current_project', () => {
|
|
611
621
|
const fixture = createFixture({
|
|
612
|
-
'
|
|
613
|
-
'.
|
|
622
|
+
'config.json': JSON.stringify({}),
|
|
623
|
+
'config.local.json': JSON.stringify({ current_project: ' ' }),
|
|
624
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
614
625
|
});
|
|
615
626
|
|
|
616
627
|
try {
|
|
@@ -624,13 +635,12 @@ describe('requireProjectRoot', () => {
|
|
|
624
635
|
});
|
|
625
636
|
|
|
626
637
|
it('throws PROJECT_NOT_FOUND when v2 install has current_project pointing to nonexistent directory', () => {
|
|
627
|
-
const fixture =
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
);
|
|
638
|
+
const fixture = createFixture({
|
|
639
|
+
'config.json': JSON.stringify({}),
|
|
640
|
+
'config.local.json': JSON.stringify({ current_project: 'ghost-project' }),
|
|
641
|
+
'PROJECTS.md': '# Projects\n',
|
|
642
|
+
'REPOS.md': '# Repos\n',
|
|
643
|
+
});
|
|
634
644
|
|
|
635
645
|
try {
|
|
636
646
|
assert.throws(
|
|
@@ -643,12 +653,12 @@ describe('requireProjectRoot', () => {
|
|
|
643
653
|
});
|
|
644
654
|
|
|
645
655
|
it('throws INVALID_PROJECT_NAME when current_project contains ..', () => {
|
|
646
|
-
const fixture =
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
);
|
|
656
|
+
const fixture = createFixture({
|
|
657
|
+
'config.json': JSON.stringify({}),
|
|
658
|
+
'config.local.json': JSON.stringify({ current_project: '../etc' }),
|
|
659
|
+
'PROJECTS.md': '# Projects\n',
|
|
660
|
+
'REPOS.md': '# Repos\n',
|
|
661
|
+
});
|
|
652
662
|
|
|
653
663
|
try {
|
|
654
664
|
assert.throws(
|
|
@@ -660,21 +670,10 @@ describe('requireProjectRoot', () => {
|
|
|
660
670
|
}
|
|
661
671
|
});
|
|
662
672
|
|
|
663
|
-
it('returns .planning for v1 install (backward compatible)', () => {
|
|
664
|
-
const fixture = createTempProject({ version: 1 });
|
|
665
|
-
|
|
666
|
-
try {
|
|
667
|
-
const result = requireProjectRoot(fixture.cwd);
|
|
668
|
-
assert.equal(result, '.planning');
|
|
669
|
-
} finally {
|
|
670
|
-
fixture.cleanup();
|
|
671
|
-
}
|
|
672
|
-
});
|
|
673
|
-
|
|
674
673
|
it('resolveProjectPath throws NO_CURRENT_PROJECT_V2 on v2 install without current_project (guard propagates)', () => {
|
|
675
674
|
const fixture = createFixture({
|
|
676
|
-
'
|
|
677
|
-
'
|
|
675
|
+
'config.json': JSON.stringify({}),
|
|
676
|
+
'PROJECTS.md': '# Projects\n\n| Project | Status |\n',
|
|
678
677
|
});
|
|
679
678
|
|
|
680
679
|
try {
|
|
@@ -699,19 +698,18 @@ describe('root layout', () => {
|
|
|
699
698
|
});
|
|
700
699
|
|
|
701
700
|
it('loadConfig reads from root-layout config.json', () => {
|
|
702
|
-
fixture = createTempProject({
|
|
701
|
+
fixture = createTempProject({ withConfig: { model_profile: 'quality' } });
|
|
703
702
|
const config = loadConfig(fixture.cwd);
|
|
704
703
|
assert.equal(config.model_profile, 'quality');
|
|
705
704
|
});
|
|
706
705
|
|
|
707
706
|
it('loadConfig returns defaults when root-layout config.json has no model_profile', () => {
|
|
708
|
-
fixture = createTempProject(
|
|
707
|
+
fixture = createTempProject();
|
|
709
708
|
const config = loadConfig(fixture.cwd);
|
|
710
709
|
assert.equal(config.model_profile, 'balanced');
|
|
711
710
|
});
|
|
712
711
|
|
|
713
|
-
it('getProjectRoot returns . for
|
|
714
|
-
// Create a root-layout v1 fixture (no v2 markers with valid headers)
|
|
712
|
+
it('getProjectRoot returns . for root-layout without v2 markers', () => {
|
|
715
713
|
fixture = createFixture({
|
|
716
714
|
'config.local.json': JSON.stringify({ planningRoot: '.' }),
|
|
717
715
|
'config.json': JSON.stringify({}),
|
|
@@ -725,12 +723,12 @@ describe('root layout', () => {
|
|
|
725
723
|
});
|
|
726
724
|
|
|
727
725
|
it('isV2Install detects v2 markers in root-layout', () => {
|
|
728
|
-
fixture = createTempProject(
|
|
726
|
+
fixture = createTempProject();
|
|
729
727
|
const result = isV2Install(fixture.cwd);
|
|
730
728
|
assert.equal(result, true);
|
|
731
729
|
});
|
|
732
730
|
|
|
733
|
-
it('isV2Install returns false for root-layout
|
|
731
|
+
it('isV2Install returns false for root-layout without v2 markers', () => {
|
|
734
732
|
fixture = createFixture({
|
|
735
733
|
'config.local.json': JSON.stringify({ planningRoot: '.' }),
|
|
736
734
|
'config.json': JSON.stringify({}),
|
|
@@ -755,7 +753,6 @@ describe('root layout', () => {
|
|
|
755
753
|
});
|
|
756
754
|
|
|
757
755
|
it('loadConfig reads config.json in root-layout auto-detect', () => {
|
|
758
|
-
// Root layout with config.json + PROJECT.md auto-detect (no config.local.json needed)
|
|
759
756
|
fixture = createFixture({
|
|
760
757
|
'PROJECT.md': '# Project\n',
|
|
761
758
|
'config.json': JSON.stringify({ model_profile: 'budget' }),
|
|
@@ -769,13 +766,19 @@ describe('root layout', () => {
|
|
|
769
766
|
// ─── getProjectDir Tests ─────────────────────────────────────────────────────
|
|
770
767
|
|
|
771
768
|
describe('getProjectDir', () => {
|
|
772
|
-
it('returns absolute path under
|
|
773
|
-
const fixture =
|
|
769
|
+
it('returns absolute path under projects/<slug> for v2 install', () => {
|
|
770
|
+
const fixture = createFixture({
|
|
771
|
+
'config.json': JSON.stringify({}),
|
|
772
|
+
'config.local.json': JSON.stringify({ current_project: 'my-app' }),
|
|
773
|
+
'PROJECTS.md': '# Projects\n',
|
|
774
|
+
'REPOS.md': '# Repos\n',
|
|
775
|
+
'projects/my-app/STATE.md': '# State',
|
|
776
|
+
});
|
|
774
777
|
|
|
775
778
|
try {
|
|
776
779
|
const result = getProjectDir(fixture.cwd, 'my-app');
|
|
777
780
|
assert.ok(path.isAbsolute(result), 'Expected absolute path');
|
|
778
|
-
assert.equal(result, path.join(fixture.cwd, '
|
|
781
|
+
assert.equal(result, path.join(fixture.cwd, 'projects', 'my-app'));
|
|
779
782
|
} finally {
|
|
780
783
|
fixture.cleanup();
|
|
781
784
|
}
|
|
@@ -783,7 +786,7 @@ describe('getProjectDir', () => {
|
|
|
783
786
|
|
|
784
787
|
it('returns absolute path for any slug even if directory does not exist', () => {
|
|
785
788
|
const fixture = createFixture({
|
|
786
|
-
'
|
|
789
|
+
'config.json': JSON.stringify({}),
|
|
787
790
|
});
|
|
788
791
|
|
|
789
792
|
try {
|
|
@@ -808,8 +811,8 @@ describe('config two-file merge', () => {
|
|
|
808
811
|
|
|
809
812
|
it('loadConfig merges config.json and config.local.json', () => {
|
|
810
813
|
fixture = createFixture({
|
|
811
|
-
'
|
|
812
|
-
'
|
|
814
|
+
'config.json': JSON.stringify({ model_profile: 'quality' }),
|
|
815
|
+
'config.local.json': JSON.stringify({ current_project: 'my-app' }),
|
|
813
816
|
});
|
|
814
817
|
const config = loadConfig(fixture.cwd);
|
|
815
818
|
assert.equal(config.model_profile, 'quality');
|
|
@@ -818,8 +821,8 @@ describe('config two-file merge', () => {
|
|
|
818
821
|
|
|
819
822
|
it('loadConfig local overrides shared for overlapping keys', () => {
|
|
820
823
|
fixture = createFixture({
|
|
821
|
-
'
|
|
822
|
-
'
|
|
824
|
+
'config.json': JSON.stringify({ model_profile: 'speed' }),
|
|
825
|
+
'config.local.json': JSON.stringify({ model_profile: 'quality' }),
|
|
823
826
|
});
|
|
824
827
|
const config = loadConfig(fixture.cwd);
|
|
825
828
|
assert.equal(config.model_profile, 'quality');
|
|
@@ -827,17 +830,10 @@ describe('config two-file merge', () => {
|
|
|
827
830
|
|
|
828
831
|
it('loadConfig reads config.json when no config.local.json exists', () => {
|
|
829
832
|
fixture = createFixture({
|
|
830
|
-
'
|
|
833
|
+
'config.json': JSON.stringify({ model_profile: 'speed' }),
|
|
831
834
|
});
|
|
832
835
|
const config = loadConfig(fixture.cwd);
|
|
833
836
|
assert.equal(config.model_profile, 'speed');
|
|
834
837
|
});
|
|
835
838
|
|
|
836
|
-
it('loadConfig falls back to legacy dgs.config.json', () => {
|
|
837
|
-
fixture = createFixture({
|
|
838
|
-
'.planning/dgs.config.json': JSON.stringify({ model_profile: 'quality' }),
|
|
839
|
-
});
|
|
840
|
-
const config = loadConfig(fixture.cwd);
|
|
841
|
-
assert.equal(config.model_profile, 'quality');
|
|
842
|
-
});
|
|
843
839
|
});
|