@ktpartners/dgs-platform 2.7.5 → 2.9.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 +30 -0
- package/README.md +15 -12
- 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 +55 -84
- 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-milestone.md +16 -6
- package/deliver-great-systems/workflows/new-project.md +22 -681
- 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,580 +1,525 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Tests for
|
|
2
|
+
* Tests for .planning/ to root layout migration engine.
|
|
3
|
+
*
|
|
4
|
+
* Covers all MIG requirements (01-11), OPT-02 (dry-run),
|
|
5
|
+
* and OPT-03 (rollback on commit failure).
|
|
3
6
|
*/
|
|
4
7
|
|
|
5
8
|
const { describe, it, beforeEach, afterEach } = require('node:test');
|
|
6
9
|
const assert = require('node:assert');
|
|
7
10
|
const fs = require('fs');
|
|
8
11
|
const path = require('path');
|
|
9
|
-
const os = require('os');
|
|
10
12
|
const { execSync } = require('child_process');
|
|
11
13
|
|
|
12
14
|
const { createTempDir, cleanupDir, writeFile, initGitRepo } = require('./test-helpers.cjs');
|
|
15
|
+
const { resetPaths } = require('./paths.cjs');
|
|
16
|
+
const { migrateDotPlanningToRoot } = require('./migration.cjs');
|
|
13
17
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
const libDir = path.resolve(__dirname);
|
|
19
|
+
|
|
20
|
+
// ─── Test Helpers ─────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a temp directory with git init, returns realpathSync'd path
|
|
24
|
+
* (macOS /var -> /private/var symlink fix from Phase 118).
|
|
25
|
+
*/
|
|
26
|
+
function makeGitDir() {
|
|
27
|
+
const tmpDir = createTempDir('dgs-mig-test-');
|
|
28
|
+
initGitRepo(tmpDir);
|
|
29
|
+
return fs.realpathSync(tmpDir);
|
|
25
30
|
}
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
} = require('./migration.cjs');
|
|
32
|
+
/**
|
|
33
|
+
* Creates a temp directory without git, returns realpathSync'd path.
|
|
34
|
+
*/
|
|
35
|
+
function makePlainDir() {
|
|
36
|
+
const tmpDir = createTempDir('dgs-mig-test-');
|
|
37
|
+
return fs.realpathSync(tmpDir);
|
|
38
|
+
}
|
|
35
39
|
|
|
36
|
-
|
|
40
|
+
/**
|
|
41
|
+
* Spawns a subprocess to call migrateDotPlanningToRoot.
|
|
42
|
+
* Used for tests where error() calls process.exit(1).
|
|
43
|
+
*
|
|
44
|
+
* @param {string} cwd - Directory to pass
|
|
45
|
+
* @param {Object} [options] - Options to pass to migrateDotPlanningToRoot
|
|
46
|
+
* @returns {{ exitCode: number, stdout: string, stderr: string }}
|
|
47
|
+
*/
|
|
48
|
+
function callMigrateSubprocess(cwd, options) {
|
|
49
|
+
const optsStr = options ? JSON.stringify(options) : '{}';
|
|
50
|
+
// Write script OUTSIDE the git repo to avoid dirtying the working tree
|
|
51
|
+
const scriptPath = path.join(require('os').tmpdir(), '_test_migrate_' + Date.now() + '.js');
|
|
52
|
+
const scriptContent = [
|
|
53
|
+
'const { migrateDotPlanningToRoot } = require(' + JSON.stringify(path.join(libDir, 'migration.cjs')) + ');',
|
|
54
|
+
'const result = migrateDotPlanningToRoot(' + JSON.stringify(cwd) + ', ' + optsStr + ');',
|
|
55
|
+
'process.stdout.write(JSON.stringify(result));',
|
|
56
|
+
].join('\n');
|
|
57
|
+
fs.writeFileSync(scriptPath, scriptContent);
|
|
58
|
+
try {
|
|
59
|
+
const stdout = execSync('node ' + JSON.stringify(scriptPath), {
|
|
60
|
+
encoding: 'utf-8',
|
|
61
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
62
|
+
timeout: 10000,
|
|
63
|
+
});
|
|
64
|
+
return { exitCode: 0, stdout: stdout, stderr: '' };
|
|
65
|
+
} catch (err) {
|
|
66
|
+
return {
|
|
67
|
+
exitCode: err.status ?? 1,
|
|
68
|
+
stdout: (err.stdout ?? '').toString(),
|
|
69
|
+
stderr: (err.stderr ?? '').toString(),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
}
|
|
37
73
|
|
|
38
|
-
|
|
39
|
-
let tmpDir;
|
|
40
|
-
beforeEach(() => { tmpDir = createTempDir(); });
|
|
41
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
74
|
+
// ─── Suite 1: Idempotency (MIG-07) ───────────────────────────────────────────
|
|
42
75
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const result = detectV1Install(tmpDir);
|
|
46
|
-
assert.strictEqual(result.isV1, true);
|
|
47
|
-
assert.strictEqual(result.projectName, 'My Cool App');
|
|
48
|
-
assert.strictEqual(result.slug, 'my-cool-app');
|
|
49
|
-
});
|
|
76
|
+
describe('idempotency (MIG-07)', () => {
|
|
77
|
+
let dir;
|
|
50
78
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
writeFile(tmpDir, '.planning/PROJECTS.md', '# Projects\n');
|
|
54
|
-
const result = detectV1Install(tmpDir);
|
|
55
|
-
assert.strictEqual(result.isV1, false);
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
dir = makeGitDir();
|
|
56
81
|
});
|
|
57
82
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
const result = detectV1Install(tmpDir);
|
|
62
|
-
assert.strictEqual(result.isV1, false);
|
|
83
|
+
afterEach(() => {
|
|
84
|
+
resetPaths();
|
|
85
|
+
cleanupDir(dir);
|
|
63
86
|
});
|
|
64
87
|
|
|
65
|
-
it('returns
|
|
66
|
-
const result =
|
|
67
|
-
assert.strictEqual(result.
|
|
88
|
+
it('returns migrated:false when no .planning/ directory exists', () => {
|
|
89
|
+
const result = migrateDotPlanningToRoot(dir);
|
|
90
|
+
assert.strictEqual(result.migrated, false);
|
|
91
|
+
assert.strictEqual(result.filesMoved, 0);
|
|
92
|
+
assert.strictEqual(result.identicalResolved, 0);
|
|
93
|
+
assert.strictEqual(result.commitHash, null);
|
|
94
|
+
assert.deepStrictEqual(result.actions, []);
|
|
68
95
|
});
|
|
69
96
|
|
|
70
|
-
it('returns
|
|
71
|
-
fs.
|
|
72
|
-
|
|
73
|
-
const result =
|
|
74
|
-
assert.strictEqual(result.
|
|
97
|
+
it('returns migrated:false when .planning is a file not a directory', () => {
|
|
98
|
+
fs.writeFileSync(path.join(dir, '.planning'), 'not a directory');
|
|
99
|
+
execSync('git add .planning && git commit -m "add file"', { cwd: dir, stdio: 'pipe' });
|
|
100
|
+
const result = migrateDotPlanningToRoot(dir);
|
|
101
|
+
assert.strictEqual(result.migrated, false);
|
|
102
|
+
assert.strictEqual(result.filesMoved, 0);
|
|
75
103
|
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// ─── Suite 2: Clean working tree (git only) ──────────────────────────────────
|
|
107
|
+
|
|
108
|
+
describe('clean working tree (git only)', () => {
|
|
109
|
+
let dir;
|
|
76
110
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const result = detectV1Install(tmpDir);
|
|
80
|
-
assert.strictEqual(result.projectName, 'Auth Overhaul');
|
|
81
|
-
assert.strictEqual(result.slug, 'auth-overhaul');
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
dir = makeGitDir();
|
|
82
113
|
});
|
|
83
114
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
assert.strictEqual(result.isV1, true);
|
|
88
|
-
// slug should be derived from the directory basename
|
|
89
|
-
assert.ok(result.slug);
|
|
90
|
-
assert.ok(result.slug.length > 0);
|
|
115
|
+
afterEach(() => {
|
|
116
|
+
resetPaths();
|
|
117
|
+
cleanupDir(dir);
|
|
91
118
|
});
|
|
92
119
|
|
|
93
|
-
it('
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
//
|
|
98
|
-
|
|
120
|
+
it('aborts when git working tree has uncommitted changes', () => {
|
|
121
|
+
// Create .planning dir and commit it
|
|
122
|
+
writeFile(dir, '.planning/ROADMAP.md', '# Roadmap\n');
|
|
123
|
+
execSync('git add .planning && git commit -m "add planning"', { cwd: dir, stdio: 'pipe' });
|
|
124
|
+
// Create an uncommitted file to dirty the tree
|
|
125
|
+
fs.writeFileSync(path.join(dir, 'dirty.txt'), 'uncommitted');
|
|
126
|
+
|
|
127
|
+
const result = callMigrateSubprocess(dir);
|
|
128
|
+
assert.strictEqual(result.exitCode, 1, 'Should exit with code 1');
|
|
129
|
+
assert.ok(
|
|
130
|
+
result.stderr.includes('clean working tree'),
|
|
131
|
+
'stderr should mention clean working tree, got: ' + result.stderr
|
|
132
|
+
);
|
|
99
133
|
});
|
|
100
134
|
});
|
|
101
135
|
|
|
102
|
-
// ───
|
|
136
|
+
// ─── Suite 3: File moves (MIG-01, MIG-04) ────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
describe('file moves (MIG-01, MIG-04)', () => {
|
|
139
|
+
let dir;
|
|
103
140
|
|
|
104
|
-
describe('validateMigrationPreconditions', () => {
|
|
105
|
-
let tmpDir;
|
|
106
141
|
beforeEach(() => {
|
|
107
|
-
|
|
108
|
-
initGitRepo(tmpDir);
|
|
142
|
+
dir = makeGitDir();
|
|
109
143
|
});
|
|
110
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
111
144
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
execSync('git commit -m "add planning"', { cwd: tmpDir, stdio: 'pipe' });
|
|
116
|
-
const result = validateMigrationPreconditions(tmpDir);
|
|
117
|
-
assert.strictEqual(result.valid, true);
|
|
145
|
+
afterEach(() => {
|
|
146
|
+
resetPaths();
|
|
147
|
+
cleanupDir(dir);
|
|
118
148
|
});
|
|
119
149
|
|
|
120
|
-
it('
|
|
121
|
-
writeFile(
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
150
|
+
it('moves all files from .planning/ to root', () => {
|
|
151
|
+
writeFile(dir, '.planning/ROADMAP.md', '# Roadmap\n');
|
|
152
|
+
writeFile(dir, '.planning/STATE.md', '# State\n');
|
|
153
|
+
writeFile(dir, '.planning/phases/01-foo/01-01-PLAN.md', '# Plan\n');
|
|
154
|
+
execSync('git add .planning && git commit -m "add planning"', { cwd: dir, stdio: 'pipe' });
|
|
155
|
+
|
|
156
|
+
const result = migrateDotPlanningToRoot(dir);
|
|
127
157
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
assert.strictEqual(result.valid, false);
|
|
135
|
-
assert.ok(result.reason.toLowerCase().includes('v2') || result.reason.toLowerCase().includes('already'));
|
|
158
|
+
assert.strictEqual(result.migrated, true);
|
|
159
|
+
assert.strictEqual(result.filesMoved, 3);
|
|
160
|
+
assert.ok(fs.existsSync(path.join(dir, 'ROADMAP.md')), 'ROADMAP.md should exist at root');
|
|
161
|
+
assert.ok(fs.existsSync(path.join(dir, 'STATE.md')), 'STATE.md should exist at root');
|
|
162
|
+
assert.ok(fs.existsSync(path.join(dir, 'phases/01-foo/01-01-PLAN.md')), 'Nested file should exist at root');
|
|
163
|
+
assert.ok(!fs.existsSync(path.join(dir, '.planning')), '.planning/ should be gone');
|
|
136
164
|
});
|
|
137
165
|
|
|
138
|
-
it('
|
|
139
|
-
|
|
140
|
-
|
|
166
|
+
it('handles partial migration (only files still in .planning/)', () => {
|
|
167
|
+
// ROADMAP.md already at root, STATE.md only in .planning
|
|
168
|
+
writeFile(dir, 'ROADMAP.md', '# Roadmap\n');
|
|
169
|
+
writeFile(dir, '.planning/STATE.md', '# State\n');
|
|
170
|
+
execSync('git add . && git commit -m "partial setup"', { cwd: dir, stdio: 'pipe' });
|
|
171
|
+
|
|
172
|
+
const result = migrateDotPlanningToRoot(dir);
|
|
173
|
+
|
|
174
|
+
assert.strictEqual(result.migrated, true);
|
|
175
|
+
assert.strictEqual(result.filesMoved, 1);
|
|
176
|
+
assert.ok(fs.existsSync(path.join(dir, 'STATE.md')), 'STATE.md should be at root');
|
|
177
|
+
assert.ok(fs.existsSync(path.join(dir, 'ROADMAP.md')), 'ROADMAP.md should still be at root');
|
|
141
178
|
});
|
|
142
179
|
});
|
|
143
180
|
|
|
144
|
-
// ───
|
|
181
|
+
// ─── Suite 4: Conflict detection (MIG-02, MIG-03) ────────────────────────────
|
|
182
|
+
|
|
183
|
+
describe('conflict detection (MIG-02, MIG-03)', () => {
|
|
184
|
+
let dir;
|
|
145
185
|
|
|
146
|
-
describe('createBackupTag', () => {
|
|
147
|
-
let tmpDir;
|
|
148
186
|
beforeEach(() => {
|
|
149
|
-
|
|
150
|
-
initGitRepo(tmpDir);
|
|
187
|
+
dir = makeGitDir();
|
|
151
188
|
});
|
|
152
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
153
189
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
// Verify tag actually exists
|
|
158
|
-
const tags = execSync('git tag', { cwd: tmpDir, encoding: 'utf-8' }).trim();
|
|
159
|
-
assert.ok(tags.includes('dgs-pre-v2-migration'));
|
|
190
|
+
afterEach(() => {
|
|
191
|
+
resetPaths();
|
|
192
|
+
cleanupDir(dir);
|
|
160
193
|
});
|
|
161
194
|
|
|
162
|
-
it('
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
assert.ok(result.reason);
|
|
167
|
-
});
|
|
195
|
+
it('aborts when files have different content in both locations', () => {
|
|
196
|
+
writeFile(dir, '.planning/ROADMAP.md', 'old content');
|
|
197
|
+
writeFile(dir, 'ROADMAP.md', 'new content');
|
|
198
|
+
execSync('git add . && git commit -m "conflict setup"', { cwd: dir, stdio: 'pipe' });
|
|
168
199
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
assert.strictEqual(result.tagged, false);
|
|
174
|
-
assert.ok(result.reason);
|
|
175
|
-
} finally {
|
|
176
|
-
cleanupDir(noGitDir);
|
|
177
|
-
}
|
|
200
|
+
const result = callMigrateSubprocess(dir);
|
|
201
|
+
assert.strictEqual(result.exitCode, 1, 'Should exit with code 1');
|
|
202
|
+
assert.ok(result.stderr.includes('Migration aborted'), 'stderr should mention Migration aborted');
|
|
203
|
+
assert.ok(result.stderr.includes('ROADMAP.md'), 'stderr should list conflicting file');
|
|
178
204
|
});
|
|
179
|
-
});
|
|
180
205
|
|
|
181
|
-
|
|
206
|
+
it('resolves identical-content conflicts by keeping root copy', () => {
|
|
207
|
+
const content = '# Roadmap\nSame content\n';
|
|
208
|
+
writeFile(dir, '.planning/ROADMAP.md', content);
|
|
209
|
+
writeFile(dir, 'ROADMAP.md', content);
|
|
210
|
+
execSync('git add . && git commit -m "identical setup"', { cwd: dir, stdio: 'pipe' });
|
|
182
211
|
|
|
183
|
-
|
|
184
|
-
let tmpDir;
|
|
185
|
-
beforeEach(() => {
|
|
186
|
-
tmpDir = createTempDir();
|
|
187
|
-
initGitRepo(tmpDir);
|
|
188
|
-
});
|
|
189
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
212
|
+
const result = migrateDotPlanningToRoot(dir);
|
|
190
213
|
|
|
191
|
-
it('moves PROJECT.md, REQUIREMENTS.md, ROADMAP.md, STATE.md into project subfolder', () => {
|
|
192
|
-
createV1Install(tmpDir, 'Test App');
|
|
193
|
-
const result = migrateV1ToV2(tmpDir, 'test-app');
|
|
194
214
|
assert.strictEqual(result.migrated, true);
|
|
195
|
-
assert.strictEqual(result.
|
|
215
|
+
assert.strictEqual(result.identicalResolved, 1);
|
|
216
|
+
assert.ok(!fs.existsSync(path.join(dir, '.planning')), '.planning/ should be gone');
|
|
217
|
+
const rootContent = fs.readFileSync(path.join(dir, 'ROADMAP.md'), 'utf-8');
|
|
218
|
+
assert.strictEqual(rootContent, content, 'Root copy should be unchanged');
|
|
219
|
+
});
|
|
220
|
+
});
|
|
196
221
|
|
|
197
|
-
|
|
198
|
-
assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'test-app', 'PROJECT.md')));
|
|
199
|
-
assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'test-app', 'REQUIREMENTS.md')));
|
|
200
|
-
assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'test-app', 'ROADMAP.md')));
|
|
201
|
-
assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'test-app', 'STATE.md')));
|
|
222
|
+
// ─── Suite 5: .gitignore rewrite (MIG-05) ────────────────────────────────────
|
|
202
223
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
assert.ok(!fs.existsSync(path.join(tmpDir, '.planning', 'REQUIREMENTS.md')));
|
|
206
|
-
assert.ok(!fs.existsSync(path.join(tmpDir, '.planning', 'ROADMAP.md')));
|
|
207
|
-
assert.ok(!fs.existsSync(path.join(tmpDir, '.planning', 'STATE.md')));
|
|
208
|
-
});
|
|
224
|
+
describe('.gitignore rewrite (MIG-05)', () => {
|
|
225
|
+
let dir;
|
|
209
226
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
assert.strictEqual(result.migrated, true);
|
|
227
|
+
beforeEach(() => {
|
|
228
|
+
dir = makeGitDir();
|
|
229
|
+
});
|
|
214
230
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
assert.ok(!fs.existsSync(path.join(tmpDir, '.planning', 'phases')));
|
|
231
|
+
afterEach(() => {
|
|
232
|
+
resetPaths();
|
|
233
|
+
cleanupDir(dir);
|
|
219
234
|
});
|
|
220
235
|
|
|
221
|
-
it('
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
236
|
+
it('rewrites .gitignore entries to strip .planning/ prefix', () => {
|
|
237
|
+
writeFile(dir, '.planning/ROADMAP.md', '# Roadmap\n');
|
|
238
|
+
writeFile(dir, '.gitignore', '.planning/config.local.json\n.planning/archive/\n!.planning/keep.txt\n');
|
|
239
|
+
execSync('git add . && git commit -m "gitignore setup"', { cwd: dir, stdio: 'pipe' });
|
|
225
240
|
|
|
226
|
-
|
|
227
|
-
|
|
241
|
+
migrateDotPlanningToRoot(dir);
|
|
242
|
+
|
|
243
|
+
const gitignore = fs.readFileSync(path.join(dir, '.gitignore'), 'utf-8');
|
|
244
|
+
assert.ok(gitignore.includes('config.local.json'), 'Should contain config.local.json without .planning/ prefix');
|
|
245
|
+
assert.ok(gitignore.includes('archive/'), 'Should contain archive/ without .planning/ prefix');
|
|
246
|
+
assert.ok(gitignore.includes('!keep.txt'), 'Should contain !keep.txt without .planning/ prefix');
|
|
247
|
+
assert.ok(!gitignore.includes('.planning/'), 'Should not contain .planning/ prefix');
|
|
228
248
|
});
|
|
249
|
+
});
|
|
229
250
|
|
|
230
|
-
|
|
231
|
-
// Create minimal v1 install with only PROJECT.md and STATE.md
|
|
232
|
-
writeFile(tmpDir, '.planning/PROJECT.md', '# Project: Minimal\n');
|
|
233
|
-
writeFile(tmpDir, '.planning/STATE.md', '# Project State\n\nPhase: 1\nStatus: Ready\nProgress: [░░░░░░░░░░] 0%\n');
|
|
234
|
-
writeFile(tmpDir, '.planning/config.json', '{}');
|
|
235
|
-
execSync('git add .', { cwd: tmpDir, stdio: 'pipe' });
|
|
236
|
-
execSync('git commit -m "partial v1"', { cwd: tmpDir, stdio: 'pipe' });
|
|
251
|
+
// ─── Suite 6: config.local.json cleanup (MIG-06) ─────────────────────────────
|
|
237
252
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
// Moved files should be in new location
|
|
241
|
-
assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'minimal', 'PROJECT.md')));
|
|
242
|
-
assert.ok(fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'minimal', 'STATE.md')));
|
|
243
|
-
// Skipped files should not cause errors
|
|
244
|
-
assert.ok(!fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'minimal', 'REQUIREMENTS.md')));
|
|
245
|
-
});
|
|
253
|
+
describe('config.local.json cleanup (MIG-06)', () => {
|
|
254
|
+
let dir;
|
|
246
255
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
256
|
+
beforeEach(() => {
|
|
257
|
+
dir = makeGitDir();
|
|
258
|
+
});
|
|
250
259
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
assert.ok(!fs.existsSync(path.join(tmpDir, '.planning', 'projects', 'test-app', 'config.json')));
|
|
260
|
+
afterEach(() => {
|
|
261
|
+
resetPaths();
|
|
262
|
+
cleanupDir(dir);
|
|
255
263
|
});
|
|
256
264
|
|
|
257
|
-
it('
|
|
258
|
-
writeFile(
|
|
259
|
-
|
|
260
|
-
|
|
265
|
+
it('removes planningRoot key from config.local.json', () => {
|
|
266
|
+
writeFile(dir, '.planning/config.local.json', JSON.stringify({ planningRoot: '.', other: 'keep' }));
|
|
267
|
+
execSync('git add . && git commit -m "config setup"', { cwd: dir, stdio: 'pipe' });
|
|
268
|
+
|
|
269
|
+
migrateDotPlanningToRoot(dir);
|
|
261
270
|
|
|
262
|
-
|
|
263
|
-
assert.ok(fs.existsSync(
|
|
271
|
+
const configPath = path.join(dir, 'config.local.json');
|
|
272
|
+
assert.ok(fs.existsSync(configPath), 'config.local.json should exist at root');
|
|
273
|
+
const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
274
|
+
assert.ok(!('planningRoot' in config), 'planningRoot key should be removed');
|
|
275
|
+
assert.strictEqual(config.other, 'keep', 'Other keys should be preserved');
|
|
264
276
|
});
|
|
277
|
+
});
|
|
265
278
|
|
|
266
|
-
|
|
267
|
-
writeFile(tmpDir, '.planning/milestones/v1.0.md', '# Milestone v1.0\n');
|
|
268
|
-
createV1Install(tmpDir, 'Test App');
|
|
269
|
-
migrateV1ToV2(tmpDir, 'test-app');
|
|
279
|
+
// ─── Suite 7: dgs.config.json rename (MIG-10) ────────────────────────────────
|
|
270
280
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
});
|
|
281
|
+
describe('dgs.config.json rename (MIG-10)', () => {
|
|
282
|
+
let dir;
|
|
274
283
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
284
|
+
beforeEach(() => {
|
|
285
|
+
dir = makeGitDir();
|
|
286
|
+
});
|
|
278
287
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
assert.ok(content.startsWith('# Repos'));
|
|
283
|
-
assert.ok(content.includes('.')); // path is '.'
|
|
288
|
+
afterEach(() => {
|
|
289
|
+
resetPaths();
|
|
290
|
+
cleanupDir(dir);
|
|
284
291
|
});
|
|
285
292
|
|
|
286
|
-
it('
|
|
287
|
-
|
|
288
|
-
|
|
293
|
+
it('renames dgs.config.json to config.json during migration', () => {
|
|
294
|
+
writeFile(dir, '.planning/dgs.config.json', JSON.stringify({ model_profile: 'balanced' }));
|
|
295
|
+
execSync('git add . && git commit -m "dgs config setup"', { cwd: dir, stdio: 'pipe' });
|
|
289
296
|
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
assert.ok(
|
|
297
|
+
const result = migrateDotPlanningToRoot(dir);
|
|
298
|
+
|
|
299
|
+
assert.ok(fs.existsSync(path.join(dir, 'config.json')), 'config.json should exist at root');
|
|
300
|
+
assert.ok(!fs.existsSync(path.join(dir, 'dgs.config.json')), 'dgs.config.json should not exist');
|
|
301
|
+
const config = JSON.parse(fs.readFileSync(path.join(dir, 'config.json'), 'utf-8'));
|
|
302
|
+
assert.strictEqual(config.model_profile, 'balanced', 'Content should be preserved');
|
|
303
|
+
assert.strictEqual(result.migrated, true);
|
|
294
304
|
});
|
|
305
|
+
});
|
|
295
306
|
|
|
296
|
-
|
|
297
|
-
createV1Install(tmpDir, 'Test App');
|
|
298
|
-
migrateV1ToV2(tmpDir, 'test-app');
|
|
307
|
+
// ─── Suite 8: Atomic commit (MIG-11) ─────────────────────────────────────────
|
|
299
308
|
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
309
|
+
describe('atomic commit (MIG-11)', () => {
|
|
310
|
+
let dir;
|
|
311
|
+
|
|
312
|
+
beforeEach(() => {
|
|
313
|
+
dir = makeGitDir();
|
|
304
314
|
});
|
|
305
315
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
assert.strictEqual(result.migrated, true);
|
|
310
|
-
assert.strictEqual(result.slug, 'test-app');
|
|
311
|
-
assert.ok(Array.isArray(result.files_moved));
|
|
312
|
-
assert.ok(result.files_moved.length > 0);
|
|
313
|
-
assert.strictEqual(result.repos_created, true);
|
|
314
|
-
assert.strictEqual(result.project_created, true);
|
|
316
|
+
afterEach(() => {
|
|
317
|
+
resetPaths();
|
|
318
|
+
cleanupDir(dir);
|
|
315
319
|
});
|
|
316
320
|
|
|
317
|
-
it('
|
|
318
|
-
|
|
319
|
-
|
|
321
|
+
it('creates a single commit with migration message', () => {
|
|
322
|
+
writeFile(dir, '.planning/ROADMAP.md', '# Roadmap\n');
|
|
323
|
+
execSync('git add .planning && git commit -m "add planning"', { cwd: dir, stdio: 'pipe' });
|
|
320
324
|
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
assert.ok(log.includes('rename'));
|
|
325
|
+
const result = migrateDotPlanningToRoot(dir);
|
|
326
|
+
|
|
327
|
+
assert.ok(result.commitHash, 'commitHash should be non-empty');
|
|
328
|
+
assert.strictEqual(typeof result.commitHash, 'string');
|
|
329
|
+
const log = execSync('git log --oneline -1', { cwd: dir, encoding: 'utf-8' });
|
|
330
|
+
assert.ok(log.includes('chore: migrate .planning/ to root layout'), 'Commit message should match');
|
|
328
331
|
});
|
|
332
|
+
});
|
|
329
333
|
|
|
330
|
-
|
|
331
|
-
writeFile(tmpDir, '.planning/todos/pending/task1.md', '# Todo 1\n');
|
|
332
|
-
createV1Install(tmpDir, 'Test App');
|
|
333
|
-
migrateV1ToV2(tmpDir, 'test-app');
|
|
334
|
+
// ─── Suite 9: Non-git fallback (MIG-09) ──────────────────────────────────────
|
|
334
335
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
});
|
|
336
|
+
describe('non-git fallback (MIG-09)', () => {
|
|
337
|
+
let dir;
|
|
338
338
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
migrateV1ToV2(tmpDir, 'test-app');
|
|
339
|
+
beforeEach(() => {
|
|
340
|
+
dir = makePlainDir();
|
|
341
|
+
});
|
|
343
342
|
|
|
344
|
-
|
|
345
|
-
|
|
343
|
+
afterEach(() => {
|
|
344
|
+
resetPaths();
|
|
345
|
+
cleanupDir(dir);
|
|
346
346
|
});
|
|
347
347
|
|
|
348
|
-
it('moves
|
|
349
|
-
writeFile(
|
|
350
|
-
|
|
351
|
-
migrateV1ToV2(tmpDir, 'test-app');
|
|
348
|
+
it('moves files using fs.rename when not in a git repo', () => {
|
|
349
|
+
writeFile(dir, '.planning/ROADMAP.md', '# Roadmap\n');
|
|
350
|
+
writeFile(dir, '.planning/STATE.md', '# State\n');
|
|
352
351
|
|
|
353
|
-
|
|
354
|
-
|
|
352
|
+
const result = migrateDotPlanningToRoot(dir);
|
|
353
|
+
|
|
354
|
+
assert.strictEqual(result.migrated, true);
|
|
355
|
+
assert.strictEqual(result.commitHash, null, 'commitHash should be null for non-git');
|
|
356
|
+
assert.ok(fs.existsSync(path.join(dir, 'ROADMAP.md')), 'ROADMAP.md should exist at root');
|
|
357
|
+
assert.ok(fs.existsSync(path.join(dir, 'STATE.md')), 'STATE.md should exist at root');
|
|
358
|
+
assert.ok(!fs.existsSync(path.join(dir, '.planning')), '.planning/ should be gone');
|
|
355
359
|
});
|
|
356
360
|
});
|
|
357
361
|
|
|
358
|
-
// ───
|
|
362
|
+
// ─── Suite 10: Dry-run (OPT-02) ──────────────────────────────────────────────
|
|
359
363
|
|
|
360
|
-
describe('
|
|
361
|
-
let
|
|
362
|
-
beforeEach(() => { tmpDir = createTempDir(); });
|
|
363
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
364
|
+
describe('dry-run (OPT-02)', () => {
|
|
365
|
+
let dir;
|
|
364
366
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
writeFile(tmpDir, '.planning/REQUIREMENTS.md', '# Requirements\n');
|
|
369
|
-
writeFile(tmpDir, '.planning/ROADMAP.md', '# Roadmap\n');
|
|
370
|
-
writeFile(tmpDir, '.planning/STATE.md', '# State\n');
|
|
371
|
-
writeFile(tmpDir, '.planning/phases/01-setup/01-01-PLAN.md', '---\n---\n');
|
|
372
|
-
writeFile(tmpDir, '.planning/research/notes.md', '# Notes\n');
|
|
367
|
+
beforeEach(() => {
|
|
368
|
+
dir = makeGitDir();
|
|
369
|
+
});
|
|
373
370
|
|
|
374
|
-
|
|
371
|
+
afterEach(() => {
|
|
372
|
+
resetPaths();
|
|
373
|
+
cleanupDir(dir);
|
|
374
|
+
});
|
|
375
375
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
376
|
+
it('dry-run returns actions without modifying files', () => {
|
|
377
|
+
writeFile(dir, '.planning/ROADMAP.md', '# Roadmap\n');
|
|
378
|
+
execSync('git add .planning && git commit -m "add planning"', { cwd: dir, stdio: 'pipe' });
|
|
379
379
|
|
|
380
|
-
|
|
381
|
-
const dirMoves = moves.filter(m => m.isDir);
|
|
382
|
-
const fileMoves = moves.filter(m => !m.isDir);
|
|
383
|
-
assert.strictEqual(dirMoves.length, 2); // phases, research
|
|
384
|
-
assert.strictEqual(fileMoves.length, 4); // PROJECT.md, REQUIREMENTS.md, ROADMAP.md, STATE.md
|
|
380
|
+
const result = migrateDotPlanningToRoot(dir, { dryRun: true });
|
|
385
381
|
|
|
386
|
-
|
|
387
|
-
assert.strictEqual(
|
|
388
|
-
assert.
|
|
382
|
+
assert.strictEqual(result.dryRun, true);
|
|
383
|
+
assert.strictEqual(result.migrated, false, 'migrated should be false in dry-run');
|
|
384
|
+
assert.ok(result.actions.length > 0, 'Should have actions');
|
|
385
|
+
assert.strictEqual(result.filesMoved, 1, 'filesMoved should count planned moves');
|
|
386
|
+
assert.strictEqual(result.commitHash, null, 'No commit in dry-run');
|
|
387
|
+
// Files should NOT have moved
|
|
388
|
+
assert.ok(fs.existsSync(path.join(dir, '.planning/ROADMAP.md')), '.planning/ROADMAP.md should still exist');
|
|
389
|
+
assert.ok(!fs.existsSync(path.join(dir, 'ROADMAP.md')), 'ROADMAP.md should not exist at root yet');
|
|
389
390
|
});
|
|
391
|
+
});
|
|
390
392
|
|
|
391
|
-
|
|
392
|
-
// Create only PROJECT.md and STATE.md
|
|
393
|
-
writeFile(tmpDir, '.planning/PROJECT.md', '# Project: Partial\n');
|
|
394
|
-
writeFile(tmpDir, '.planning/STATE.md', '# State\n');
|
|
393
|
+
// ─── CLI Helper ──────────────────────────────────────────────────────────────
|
|
395
394
|
|
|
396
|
-
|
|
395
|
+
const { spawnSync } = require('child_process');
|
|
397
396
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
397
|
+
/**
|
|
398
|
+
* Spawns dgs-tools.cjs in a subprocess with given args.
|
|
399
|
+
* Uses spawnSync to capture both stdout and stderr on success.
|
|
400
|
+
*
|
|
401
|
+
* @param {string} cwd - Working directory for the subprocess
|
|
402
|
+
* @param {string} args - CLI arguments string
|
|
403
|
+
* @returns {{ exitCode: number, stdout: string, stderr: string }}
|
|
404
|
+
*/
|
|
405
|
+
function callDgsTools(cwd, args) {
|
|
406
|
+
const dgsToolsPath = path.resolve(__dirname, '..', 'dgs-tools.cjs');
|
|
407
|
+
const result = spawnSync('node', [dgsToolsPath, ...args.split(' ')], {
|
|
408
|
+
cwd,
|
|
409
|
+
encoding: 'utf-8',
|
|
410
|
+
timeout: 10000,
|
|
411
|
+
});
|
|
412
|
+
return {
|
|
413
|
+
exitCode: result.status ?? 1,
|
|
414
|
+
stdout: (result.stdout ?? '').toString(),
|
|
415
|
+
stderr: (result.stderr ?? '').toString(),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
405
418
|
|
|
406
|
-
|
|
407
|
-
writeFile(tmpDir, '.planning/PROJECT.md', '# Project: Test\n');
|
|
408
|
-
writeFile(tmpDir, '.planning/phases/01-setup/01-01-PLAN.md', '---\n---\n');
|
|
409
|
-
writeFile(tmpDir, '.planning/research/notes.md', '# Notes\n');
|
|
419
|
+
// ─── Suite 11: CLI: migrate command (MIG-08, OPT-02) ─────────────────────────
|
|
410
420
|
|
|
411
|
-
|
|
421
|
+
describe('CLI: migrate command (MIG-08, OPT-02)', () => {
|
|
422
|
+
let dir;
|
|
412
423
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
424
|
+
beforeEach(() => {
|
|
425
|
+
dir = makeGitDir();
|
|
426
|
+
});
|
|
416
427
|
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
428
|
+
afterEach(() => {
|
|
429
|
+
resetPaths();
|
|
430
|
+
cleanupDir(dir);
|
|
420
431
|
});
|
|
421
|
-
});
|
|
422
432
|
|
|
423
|
-
|
|
433
|
+
it('migrate --layout root migrates .planning/ directory', () => {
|
|
434
|
+
writeFile(dir, '.planning/ROADMAP.md', '# Roadmap\n');
|
|
435
|
+
writeFile(dir, 'PROJECTS.md', '# Projects\n');
|
|
436
|
+
writeFile(dir, 'REPOS.md', '# Repos\n');
|
|
437
|
+
execSync('git add . && git commit -m "setup"', { cwd: dir, stdio: 'pipe' });
|
|
424
438
|
|
|
425
|
-
|
|
426
|
-
let tmpDir;
|
|
427
|
-
beforeEach(() => { tmpDir = createTempDir(); });
|
|
428
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
439
|
+
const result = callDgsTools(dir, 'migrate --layout root');
|
|
429
440
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
441
|
+
assert.strictEqual(result.exitCode, 0, 'Should exit with code 0, stderr: ' + result.stderr);
|
|
442
|
+
assert.ok(fs.existsSync(path.join(dir, 'ROADMAP.md')), 'ROADMAP.md should exist at root');
|
|
443
|
+
assert.ok(!fs.existsSync(path.join(dir, '.planning')), '.planning/ should be gone');
|
|
444
|
+
});
|
|
433
445
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
446
|
+
it('migrate --layout root --dry-run shows actions without moving files', () => {
|
|
447
|
+
writeFile(dir, '.planning/STATE.md', '# State\n');
|
|
448
|
+
writeFile(dir, 'PROJECTS.md', '# Projects\n');
|
|
449
|
+
writeFile(dir, 'REPOS.md', '# Repos\n');
|
|
450
|
+
execSync('git add . && git commit -m "setup"', { cwd: dir, stdio: 'pipe' });
|
|
438
451
|
|
|
439
|
-
const result =
|
|
440
|
-
|
|
441
|
-
assert.strictEqual(result.
|
|
452
|
+
const result = callDgsTools(dir, 'migrate --layout root --dry-run');
|
|
453
|
+
|
|
454
|
+
assert.strictEqual(result.exitCode, 0, 'Should exit with code 0, stderr: ' + result.stderr);
|
|
455
|
+
assert.ok(fs.existsSync(path.join(dir, '.planning/STATE.md')), '.planning/STATE.md should still exist');
|
|
456
|
+
assert.ok(result.stderr.includes('Dry run') || result.stderr.includes('would be made'),
|
|
457
|
+
'stderr should mention dry run, got: ' + result.stderr);
|
|
442
458
|
});
|
|
443
459
|
|
|
444
|
-
it('
|
|
445
|
-
writeFile(
|
|
446
|
-
|
|
447
|
-
writeFile(
|
|
460
|
+
it('migrate --layout root --raw returns JSON', () => {
|
|
461
|
+
writeFile(dir, '.planning/ROADMAP.md', '# Roadmap\n');
|
|
462
|
+
writeFile(dir, 'PROJECTS.md', '# Projects\n');
|
|
463
|
+
writeFile(dir, 'REPOS.md', '# Repos\n');
|
|
464
|
+
execSync('git add . && git commit -m "setup"', { cwd: dir, stdio: 'pipe' });
|
|
448
465
|
|
|
449
|
-
const
|
|
450
|
-
{ relSource: '.planning/PROJECT.md', relTarget: '.planning/test/PROJECT.md', isDir: false },
|
|
451
|
-
];
|
|
466
|
+
const result = callDgsTools(dir, 'migrate --layout root --raw');
|
|
452
467
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
assert.
|
|
456
|
-
assert.ok(
|
|
468
|
+
assert.strictEqual(result.exitCode, 0, 'Should exit with code 0, stderr: ' + result.stderr);
|
|
469
|
+
const parsed = JSON.parse(result.stdout);
|
|
470
|
+
assert.strictEqual(parsed.migrated, true, 'JSON should indicate migrated:true');
|
|
471
|
+
assert.ok(typeof parsed.filesMoved === 'number', 'filesMoved should be a number');
|
|
472
|
+
assert.ok(typeof parsed.commitHash === 'string', 'commitHash should be a string');
|
|
457
473
|
});
|
|
458
|
-
});
|
|
459
474
|
|
|
460
|
-
|
|
475
|
+
it('migrate without --layout root shows usage error', () => {
|
|
476
|
+
const result = callDgsTools(dir, 'migrate');
|
|
461
477
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
beforeEach(() => {
|
|
465
|
-
tmpDir = createTempDir();
|
|
466
|
-
initGitRepo(tmpDir);
|
|
478
|
+
assert.strictEqual(result.exitCode, 1, 'Should exit with code 1');
|
|
479
|
+
assert.ok(result.stderr.includes('Usage'), 'stderr should mention Usage, got: ' + result.stderr);
|
|
467
480
|
});
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
it('returns migrated false and rolls back when git mv fails mid-migration', () => {
|
|
471
|
-
createV1Install(tmpDir, 'Test App');
|
|
481
|
+
});
|
|
472
482
|
|
|
473
|
-
|
|
474
|
-
// The phases/ dir move will fail because the target .planning/bad-slug/phases
|
|
475
|
-
// already exists as a FILE, preventing the directory move.
|
|
476
|
-
const targetPhasesPath = path.join(tmpDir, '.planning', 'projects', 'bad-slug', 'phases');
|
|
477
|
-
fs.mkdirSync(path.join(tmpDir, '.planning', 'projects', 'bad-slug'), { recursive: true });
|
|
478
|
-
fs.writeFileSync(targetPhasesPath, 'blocker file');
|
|
479
|
-
execSync('git add .', { cwd: tmpDir, stdio: 'pipe' });
|
|
480
|
-
execSync('git commit -m "add blocker"', { cwd: tmpDir, stdio: 'pipe' });
|
|
483
|
+
// ─── Suite 12: CLI: auto-trigger migration ────────────────────────────────────
|
|
481
484
|
|
|
482
|
-
|
|
485
|
+
describe('CLI: auto-trigger migration', () => {
|
|
486
|
+
let dir;
|
|
483
487
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
assert.ok(result.error);
|
|
488
|
+
beforeEach(() => {
|
|
489
|
+
dir = makeGitDir();
|
|
487
490
|
});
|
|
488
491
|
|
|
489
|
-
|
|
490
|
-
|
|
492
|
+
afterEach(() => {
|
|
493
|
+
resetPaths();
|
|
494
|
+
cleanupDir(dir);
|
|
495
|
+
});
|
|
491
496
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
execSync('git commit -m "
|
|
497
|
+
it('auto-triggers migration and prints banner on normal command', () => {
|
|
498
|
+
writeFile(dir, '.planning/ROADMAP.md', '# Roadmap\n');
|
|
499
|
+
writeFile(dir, 'PROJECTS.md', '# Projects\n');
|
|
500
|
+
writeFile(dir, 'REPOS.md', '# Repos\n');
|
|
501
|
+
writeFile(dir, 'config.json', '{}');
|
|
502
|
+
execSync('git add . && git commit -m "setup"', { cwd: dir, stdio: 'pipe' });
|
|
498
503
|
|
|
499
|
-
|
|
504
|
+
const result = callDgsTools(dir, 'current-timestamp');
|
|
500
505
|
|
|
501
|
-
|
|
502
|
-
assert.ok(
|
|
503
|
-
|
|
504
|
-
assert.ok(fs.existsSync(path.join(
|
|
506
|
+
assert.strictEqual(result.exitCode, 0, 'Should exit with code 0, stderr: ' + result.stderr);
|
|
507
|
+
assert.ok(result.stderr.includes('DGS migrated .planning/ to root layout'),
|
|
508
|
+
'stderr should contain migration banner, got: ' + result.stderr);
|
|
509
|
+
assert.ok(fs.existsSync(path.join(dir, 'ROADMAP.md')), 'ROADMAP.md should exist at root');
|
|
510
|
+
assert.ok(!fs.existsSync(path.join(dir, '.planning')), '.planning/ should be gone');
|
|
505
511
|
});
|
|
506
|
-
});
|
|
507
512
|
|
|
508
|
-
|
|
513
|
+
it('auto-trigger is silent when no .planning/ directory', () => {
|
|
514
|
+
writeFile(dir, 'PROJECTS.md', '# Projects\n');
|
|
515
|
+
writeFile(dir, 'REPOS.md', '# Repos\n');
|
|
516
|
+
writeFile(dir, 'config.json', '{}');
|
|
517
|
+
execSync('git add . && git commit -m "setup"', { cwd: dir, stdio: 'pipe' });
|
|
509
518
|
|
|
510
|
-
|
|
511
|
-
let tmpDir;
|
|
512
|
-
beforeEach(() => {
|
|
513
|
-
tmpDir = createTempDir();
|
|
514
|
-
initGitRepo(tmpDir);
|
|
515
|
-
});
|
|
516
|
-
afterEach(() => { cleanupDir(tmpDir); });
|
|
517
|
-
|
|
518
|
-
it('migrates 60+ phase files and git detects renames with renameLimit=1000', () => {
|
|
519
|
-
// Create base v1 install files
|
|
520
|
-
writeFile(tmpDir, '.planning/PROJECT.md', '# Project: Scale Test\n\nA large project.\n');
|
|
521
|
-
writeFile(tmpDir, '.planning/REQUIREMENTS.md', '# Requirements\n\n- REQ-01\n');
|
|
522
|
-
writeFile(tmpDir, '.planning/ROADMAP.md', '# Roadmap\n\n## Phase 1\n');
|
|
523
|
-
writeFile(tmpDir, '.planning/STATE.md', '# Project State\n\nPhase: 1\nStatus: Active\nProgress: [░░░░░░░░░░] 0%\n');
|
|
524
|
-
writeFile(tmpDir, '.planning/config.json', '{"model_profile":"balanced"}');
|
|
525
|
-
|
|
526
|
-
// Create 60 plan files across 10 phases (6 files each)
|
|
527
|
-
for (let phase = 1; phase <= 10; phase++) {
|
|
528
|
-
const phaseNum = String(phase).padStart(2, '0');
|
|
529
|
-
const phaseName = phaseNum + '-phase-' + phase;
|
|
530
|
-
for (let plan = 1; plan <= 6; plan++) {
|
|
531
|
-
const planNum = String(plan).padStart(2, '0');
|
|
532
|
-
const filename = phaseNum + '-' + planNum + '-PLAN.md';
|
|
533
|
-
writeFile(
|
|
534
|
-
tmpDir,
|
|
535
|
-
'.planning/phases/' + phaseName + '/' + filename,
|
|
536
|
-
'---\nphase: ' + phaseNum + '\nplan: ' + planNum + '\n---\n\n# Plan ' + phaseNum + '-' + planNum + '\n\nContent for plan ' + phase + '.' + plan + '\n'
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
|
-
// Commit the v1 install
|
|
542
|
-
execSync('git add .', { cwd: tmpDir, stdio: 'pipe' });
|
|
543
|
-
execSync('git commit -m "v1 install with 60 plan files"', { cwd: tmpDir, stdio: 'pipe' });
|
|
544
|
-
|
|
545
|
-
// Run migration
|
|
546
|
-
const result = migrateV1ToV2(tmpDir, 'scale-test');
|
|
547
|
-
assert.strictEqual(result.migrated, true);
|
|
519
|
+
const result = callDgsTools(dir, 'current-timestamp');
|
|
548
520
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
const phaseNum = String(phase).padStart(2, '0');
|
|
553
|
-
const phaseName = phaseNum + '-phase-' + phase;
|
|
554
|
-
for (let plan = 1; plan <= 6; plan++) {
|
|
555
|
-
const planNum = String(plan).padStart(2, '0');
|
|
556
|
-
const filename = phaseNum + '-' + planNum + '-PLAN.md';
|
|
557
|
-
const filePath = path.join(tmpDir, '.planning', 'projects', 'scale-test', 'phases', phaseName, filename);
|
|
558
|
-
if (fs.existsSync(filePath)) {
|
|
559
|
-
foundCount++;
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
}
|
|
563
|
-
assert.strictEqual(foundCount, 60, 'All 60 plan files should be in new location');
|
|
564
|
-
|
|
565
|
-
// Verify git detects renames (not delete+add)
|
|
566
|
-
// Look at the migration commit (HEAD~1 is the move commit, HEAD is the markers commit)
|
|
567
|
-
const renameLog = execSync(
|
|
568
|
-
'git log --diff-filter=R --summary --format="" HEAD~1',
|
|
569
|
-
{ cwd: tmpDir, encoding: 'utf-8' }
|
|
570
|
-
).trim();
|
|
571
|
-
assert.ok(renameLog.includes('rename'), 'Git should detect renames in the migration commit');
|
|
572
|
-
|
|
573
|
-
// Count rename entries — should be at least 50 (60 plan files + 4 markdown files + directories)
|
|
574
|
-
const renameLines = renameLog.split('\n').filter(line => line.includes('rename'));
|
|
575
|
-
assert.ok(
|
|
576
|
-
renameLines.length >= 50,
|
|
577
|
-
'Should detect at least 50 renames, got ' + renameLines.length
|
|
578
|
-
);
|
|
521
|
+
assert.strictEqual(result.exitCode, 0, 'Should exit with code 0, stderr: ' + result.stderr);
|
|
522
|
+
assert.ok(!result.stderr.includes('migrated'),
|
|
523
|
+
'stderr should NOT contain migration message, got: ' + result.stderr);
|
|
579
524
|
});
|
|
580
525
|
});
|