-
Notifications
You must be signed in to change notification settings - Fork 292
Expand file tree
/
Copy pathclassic-root-command.test.ts
More file actions
252 lines (216 loc) · 8.86 KB
/
Copy pathclassic-root-command.test.ts
File metadata and controls
252 lines (216 loc) · 8.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { promises as fs } from 'fs';
import os from 'os';
import path from 'path';
import { runClassicCli } from '../../../domains/comet-classic/classic-cli.js';
describe('Classic root show', () => {
let projectRoot: string;
let previousCwd: string;
beforeEach(async () => {
projectRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'comet-classic-root-show-'));
await fs.mkdir(path.join(projectRoot, '.git'));
await fs.mkdir(path.join(projectRoot, '.comet'));
await fs.writeFile(
path.join(projectRoot, '.comet', 'config.yaml'),
[
'schema: comet.project.v1',
'default_workflow: classic',
'workflows: [classic]',
'classic:',
' artifact_layout: legacy',
'',
].join('\n'),
'utf8',
);
previousCwd = process.cwd();
process.chdir(projectRoot);
});
afterEach(async () => {
process.chdir(previousCwd);
await fs.rm(projectRoot, { recursive: true, force: true });
});
it('reports a healthy configured root', async () => {
await fs.mkdir(path.join(projectRoot, 'openspec'), { recursive: true });
const result = await runClassicCli(['root', 'show']);
expect(result.exitCode).toBe(0);
expect(JSON.parse(result.stdout ?? '{}')).toMatchObject({
schema: 'comet.classic-layout.v1',
artifactLayout: 'legacy',
openSpecRoot: 'openspec',
});
});
it('reads the Classic root when Native artifact defaults are missing', async () => {
await fs.writeFile(
path.join(projectRoot, '.comet', 'config.yaml'),
[
'schema: comet.project.v1',
'default_workflow: native',
'workflows: [native, classic]',
'native:',
' language: en',
'classic:',
' artifact_layout: legacy',
'',
].join('\n'),
'utf8',
);
await fs.mkdir(path.join(projectRoot, 'openspec'), { recursive: true });
const result = await runClassicCli(['root', 'show']);
expect(result.exitCode).toBe(0);
expect(JSON.parse(result.stdout ?? '{}')).toMatchObject({
artifactLayout: 'legacy',
openSpecRoot: 'openspec',
});
});
it('fails when the configured root is missing', async () => {
const result = await runClassicCli(['root', 'show']);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toContain('Configured Classic OpenSpec root is missing');
});
it('reports both roots without blocking read-only root inspection', async () => {
await fs.mkdir(path.join(projectRoot, 'openspec'), { recursive: true });
await fs.mkdir(path.join(projectRoot, 'docs', 'openspec'), { recursive: true });
const result = await runClassicCli(['root', 'show']);
expect(result.exitCode).toBe(0);
expect(JSON.parse(result.stdout ?? '{}')).toMatchObject({
artifactLayout: 'legacy',
openSpecRoot: 'openspec',
alternateRoot: 'docs/openspec',
configuredRootExists: true,
alternateRootExists: true,
dualRoots: true,
});
});
it('exposes a safe dry-run path when both roots exist', async () => {
await fs.mkdir(path.join(projectRoot, 'openspec', 'changes', 'legacy'), {
recursive: true,
});
await fs.mkdir(path.join(projectRoot, 'docs', 'openspec', 'changes', 'docs'), {
recursive: true,
});
const result = await runClassicCli(['root', 'move', 'docs', '--dry-run']);
expect(result.exitCode).toBe(0);
expect(result.stdout).toContain('目标初始状态: 非空目录');
expect(result.stdout).toContain('冲突:');
expect(result.stdout).toContain('docs 目标目录非空');
expect(result.stdout).toContain('请先解决上述冲突或阻塞项');
const configBefore = await fs.readFile(path.join(projectRoot, '.comet', 'config.yaml'), 'utf8');
const apply = await runClassicCli(['root', 'move', 'docs', '--apply']);
expect(apply.exitCode).toBe(70);
expect(apply.stderr).toContain('Classic 根目录迁移失败');
expect(await fs.readFile(path.join(projectRoot, '.comet', 'config.yaml'), 'utf8')).toBe(
configBefore,
);
});
it('fails when a managed root is a directory link', async () => {
const outsideRoot = await fs.mkdtemp(path.join(os.tmpdir(), 'comet-root-show-outside-'));
try {
try {
await fs.symlink(
outsideRoot,
path.join(projectRoot, 'openspec'),
process.platform === 'win32' ? 'junction' : 'dir',
);
} catch (error) {
if ((error as NodeJS.ErrnoException).code === 'EPERM') return;
throw error;
}
const result = await runClassicCli(['root', 'show']);
expect(result.exitCode).not.toBe(0);
expect(result.stderr).toMatch(/symbolic link or junction/iu);
} finally {
await fs.rm(outsideRoot, { recursive: true, force: true });
}
});
it('keeps dry-run read-only and applies without exposing a plan ID', async () => {
await fs.mkdir(path.join(projectRoot, 'openspec', 'changes', 'archive'), {
recursive: true,
});
await fs.mkdir(path.join(projectRoot, 'openspec', 'specs'), { recursive: true });
const dryRun = await runClassicCli(['root', 'move', 'docs', '--dry-run']);
expect(dryRun.exitCode).toBe(0);
expect(dryRun.stdout).toContain('Classic 根目录迁移现状');
expect(dryRun.stdout).toContain('仅查看现状,未修改任何文件');
expect(dryRun.stdout).toContain('comet classic root move docs --apply');
expect(dryRun.stdout).not.toContain('plan:');
expect(dryRun.stdout).not.toContain('approved plan ID');
await expect(fs.stat(path.join(projectRoot, 'docs', 'openspec'))).rejects.toMatchObject({
code: 'ENOENT',
});
const legacySyntax = await runClassicCli([
'root',
'move',
'docs',
'--apply',
'--plan',
'a'.repeat(64),
]);
expect(legacySyntax.exitCode).toBe(64);
expect(legacySyntax.stderr).not.toContain('--plan');
const applied = await runClassicCli(['root', 'move', 'docs', '--apply']);
expect(applied.exitCode).toBe(0);
expect(applied.stdout).toContain('Classic 根目录迁移完成');
expect(applied.stdout.trimEnd()).toMatch(/结果:迁移已完成。$/u);
await expect(fs.stat(path.join(projectRoot, 'docs', 'openspec'))).resolves.toBeDefined();
});
it('renders root move output in English when classic.language is en', async () => {
await fs.writeFile(
path.join(projectRoot, '.comet', 'config.yaml'),
[
'schema: comet.project.v1',
'default_workflow: classic',
'workflows: [classic]',
'classic:',
' artifact_layout: legacy',
' language: en',
'',
].join('\n'),
'utf8',
);
await fs.mkdir(path.join(projectRoot, 'openspec', 'changes', 'archive'), {
recursive: true,
});
await fs.mkdir(path.join(projectRoot, 'openspec', 'specs'), { recursive: true });
const dryRun = await runClassicCli(['root', 'move', 'docs', '--dry-run']);
expect(dryRun.exitCode).toBe(0);
expect(dryRun.stdout).toContain('Classic root move status');
expect(dryRun.stdout).toContain('Inspection only; no files were changed');
expect(dryRun.stdout).not.toMatch(/[\u4e00-\u9fff]/u);
});
it('renders invalid migration journal errors in the configured language', async () => {
await fs.writeFile(path.join(projectRoot, '.comet', 'classic-root-move.json'), '{}\n', 'utf8');
await fs.mkdir(path.join(projectRoot, 'openspec'), { recursive: true });
const result = await runClassicCli(['root', 'move', 'docs', '--dry-run']);
expect(result.exitCode).toBe(70);
expect(result.stderr).toContain('Classic 根目录迁移失败');
expect(result.stderr).toContain('迁移记录格式或内容无效');
expect(result.stderr).not.toContain('invalid Classic root move journal');
});
it('reports an already migrated docs layout without failing or mutating files', async () => {
await fs.writeFile(
path.join(projectRoot, '.comet', 'config.yaml'),
[
'schema: comet.project.v1',
'default_workflow: classic',
'workflows: [classic]',
'classic:',
' artifact_layout: docs',
'',
].join('\n'),
'utf8',
);
await fs.mkdir(path.join(projectRoot, 'docs', 'openspec', 'changes', 'archive'), {
recursive: true,
});
await fs.mkdir(path.join(projectRoot, 'docs', 'openspec', 'specs'), { recursive: true });
const dryRun = await runClassicCli(['root', 'move', 'docs', '--dry-run']);
const apply = await runClassicCli(['root', 'move', 'docs', '--apply']);
expect(dryRun).toMatchObject({ exitCode: 0 });
expect(dryRun.stdout).toContain('已经使用 docs/openspec');
expect(apply).toMatchObject({ exitCode: 0 });
expect(apply.stdout).toContain('无需重复迁移');
await expect(fs.stat(path.join(projectRoot, 'openspec'))).rejects.toMatchObject({
code: 'ENOENT',
});
});
});