clawt 3.1.2 → 3.2.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/README.md +3 -3
- package/dist/index.js +291 -229
- package/dist/postinstall.js +33 -1
- package/docs/config.md +2 -1
- package/docs/init.md +16 -7
- package/docs/project-config.md +132 -0
- package/docs/spec.md +32 -22
- package/docs/validate.md +55 -13
- package/package.json +1 -1
- package/src/commands/config.ts +14 -28
- package/src/commands/init.ts +23 -12
- package/src/commands/validate.ts +50 -228
- package/src/constants/index.ts +1 -0
- package/src/constants/messages/init.ts +4 -0
- package/src/constants/messages/validate.ts +3 -0
- package/src/constants/project-config.ts +46 -0
- package/src/types/index.ts +1 -1
- package/src/types/projectConfig.ts +17 -0
- package/src/utils/config-strategy.ts +68 -20
- package/src/utils/index.ts +4 -2
- package/src/utils/project-config.ts +9 -0
- package/src/utils/validate-core.ts +174 -0
- package/src/utils/validate-runner.ts +105 -0
- package/src/utils/validate-snapshot.ts +29 -9
- package/tests/unit/commands/config.test.ts +1 -0
- package/tests/unit/commands/init.test.ts +41 -6
- package/tests/unit/commands/validate.test.ts +96 -243
- package/tests/unit/utils/config-strategy.test.ts +77 -1
- package/tests/unit/utils/project-config.test.ts +32 -0
- package/tests/unit/utils/validate-snapshot.test.ts +8 -3
|
@@ -27,6 +27,7 @@ vi.mock('../../../src/constants/index.js', () => ({
|
|
|
27
27
|
VALIDATE_PATCH_APPLY_FAILED: (branch: string) => `patch 应用失败: ${branch}`,
|
|
28
28
|
INCREMENTAL_VALIDATE_SUCCESS: (branch: string) => `✓ 增量验证 ${branch}`,
|
|
29
29
|
INCREMENTAL_VALIDATE_FALLBACK: '降级为全量模式',
|
|
30
|
+
INCREMENTAL_VALIDATE_NO_CHANGES: (branch: string) => `${branch} 无新变更`,
|
|
30
31
|
DESTRUCTIVE_OP_CANCELLED: '已取消操作',
|
|
31
32
|
VALIDATE_RUN_START: (cmd: string) => `正在执行命令: ${cmd}`,
|
|
32
33
|
VALIDATE_RUN_SUCCESS: (cmd: string) => `✓ 命令执行完成: ${cmd}`,
|
|
@@ -62,22 +63,10 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
62
63
|
getProjectWorktrees: vi.fn(),
|
|
63
64
|
getConfigValue: vi.fn(),
|
|
64
65
|
isWorkingDirClean: vi.fn(),
|
|
65
|
-
|
|
66
|
-
gitCommit: vi.fn(),
|
|
67
|
-
gitStashPush: vi.fn(),
|
|
68
|
-
gitRestoreStaged: vi.fn(),
|
|
66
|
+
gitReadTree: vi.fn(),
|
|
69
67
|
gitResetHard: vi.fn(),
|
|
70
68
|
gitCleanForce: vi.fn(),
|
|
71
|
-
gitDiffBinaryAgainstBranch: vi.fn(),
|
|
72
|
-
gitApplyFromStdin: vi.fn(),
|
|
73
|
-
gitApplyCachedFromStdin: vi.fn(),
|
|
74
|
-
gitResetSoft: vi.fn(),
|
|
75
|
-
gitWriteTree: vi.fn(),
|
|
76
|
-
gitReadTree: vi.fn(),
|
|
77
69
|
getHeadCommitHash: vi.fn(),
|
|
78
|
-
getCommitTreeHash: vi.fn(),
|
|
79
|
-
gitDiffTree: vi.fn(),
|
|
80
|
-
gitApplyCachedCheck: vi.fn(),
|
|
81
70
|
hasLocalCommits: vi.fn(),
|
|
82
71
|
hasSnapshot: vi.fn(),
|
|
83
72
|
readSnapshot: vi.fn(),
|
|
@@ -89,18 +78,20 @@ vi.mock('../../../src/utils/index.js', () => ({
|
|
|
89
78
|
printWarning: vi.fn(),
|
|
90
79
|
printInfo: vi.fn(),
|
|
91
80
|
resolveTargetWorktree: vi.fn(),
|
|
92
|
-
runCommandInherited: vi.fn(),
|
|
93
|
-
printError: vi.fn(),
|
|
94
|
-
printSeparator: vi.fn(),
|
|
95
|
-
parseParallelCommands: vi.fn(),
|
|
96
|
-
runParallelCommands: vi.fn(),
|
|
97
81
|
requireProjectConfig: vi.fn().mockReturnValue({ clawtMainWorkBranch: 'main' }),
|
|
98
|
-
getValidateBranchName: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
99
|
-
gitCheckout: vi.fn(),
|
|
100
82
|
ensureOnMainWorkBranch: vi.fn(),
|
|
101
83
|
handleDirtyWorkingDir: vi.fn(),
|
|
102
84
|
checkBranchExists: vi.fn().mockReturnValue(true),
|
|
103
85
|
getCurrentBranch: vi.fn().mockReturnValue('main'),
|
|
86
|
+
getValidateRunCommand: vi.fn(),
|
|
87
|
+
// validate-core.ts 抽离的函数
|
|
88
|
+
migrateChangesViaPatch: vi.fn().mockReturnValue({ success: true }),
|
|
89
|
+
computeCurrentTreeHash: vi.fn().mockReturnValue('treehash'),
|
|
90
|
+
saveCurrentSnapshotTree: vi.fn().mockReturnValue('treehash'),
|
|
91
|
+
loadOldSnapshotToStage: vi.fn().mockReturnValue({ success: true, stagedTreeHash: '' }),
|
|
92
|
+
switchToValidateBranch: vi.fn((name: string) => `clawt-validate-${name}`),
|
|
93
|
+
// validate-runner.ts 抽离的函数
|
|
94
|
+
executeRunCommand: vi.fn(),
|
|
104
95
|
}));
|
|
105
96
|
|
|
106
97
|
import { registerValidateCommand } from '../../../src/commands/validate.js';
|
|
@@ -110,15 +101,10 @@ import {
|
|
|
110
101
|
getProjectWorktrees,
|
|
111
102
|
getConfigValue,
|
|
112
103
|
isWorkingDirClean,
|
|
113
|
-
gitAddAll,
|
|
114
|
-
gitCommit,
|
|
115
|
-
gitDiffBinaryAgainstBranch,
|
|
116
|
-
gitApplyFromStdin,
|
|
117
|
-
gitResetSoft,
|
|
118
|
-
gitWriteTree,
|
|
119
|
-
gitRestoreStaged,
|
|
120
|
-
getHeadCommitHash,
|
|
121
104
|
gitReadTree,
|
|
105
|
+
gitResetHard,
|
|
106
|
+
gitCleanForce,
|
|
107
|
+
getHeadCommitHash,
|
|
122
108
|
hasLocalCommits,
|
|
123
109
|
hasSnapshot,
|
|
124
110
|
readSnapshot,
|
|
@@ -127,19 +113,15 @@ import {
|
|
|
127
113
|
confirmDestructiveAction,
|
|
128
114
|
printSuccess,
|
|
129
115
|
printInfo,
|
|
130
|
-
resolveTargetWorktree,
|
|
131
|
-
gitResetHard,
|
|
132
|
-
gitCleanForce,
|
|
133
|
-
getCommitTreeHash,
|
|
134
|
-
gitDiffTree,
|
|
135
|
-
gitApplyCachedCheck,
|
|
136
|
-
gitApplyCachedFromStdin,
|
|
137
116
|
printWarning,
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
117
|
+
resolveTargetWorktree,
|
|
118
|
+
migrateChangesViaPatch,
|
|
119
|
+
computeCurrentTreeHash,
|
|
120
|
+
saveCurrentSnapshotTree,
|
|
121
|
+
loadOldSnapshotToStage,
|
|
122
|
+
switchToValidateBranch,
|
|
123
|
+
executeRunCommand,
|
|
124
|
+
getValidateRunCommand,
|
|
143
125
|
} from '../../../src/utils/index.js';
|
|
144
126
|
|
|
145
127
|
const mockedGetProjectName = vi.mocked(getProjectName);
|
|
@@ -147,13 +129,6 @@ const mockedGetGitTopLevel = vi.mocked(getGitTopLevel);
|
|
|
147
129
|
const mockedGetProjectWorktrees = vi.mocked(getProjectWorktrees);
|
|
148
130
|
const mockedGetConfigValue = vi.mocked(getConfigValue);
|
|
149
131
|
const mockedIsWorkingDirClean = vi.mocked(isWorkingDirClean);
|
|
150
|
-
const mockedGitAddAll = vi.mocked(gitAddAll);
|
|
151
|
-
const mockedGitCommit = vi.mocked(gitCommit);
|
|
152
|
-
const mockedGitDiffBinaryAgainstBranch = vi.mocked(gitDiffBinaryAgainstBranch);
|
|
153
|
-
const mockedGitApplyFromStdin = vi.mocked(gitApplyFromStdin);
|
|
154
|
-
const mockedGitResetSoft = vi.mocked(gitResetSoft);
|
|
155
|
-
const mockedGitWriteTree = vi.mocked(gitWriteTree);
|
|
156
|
-
const mockedGitRestoreStaged = vi.mocked(gitRestoreStaged);
|
|
157
132
|
const mockedGetHeadCommitHash = vi.mocked(getHeadCommitHash);
|
|
158
133
|
const mockedGitReadTree = vi.mocked(gitReadTree);
|
|
159
134
|
const mockedHasLocalCommits = vi.mocked(hasLocalCommits);
|
|
@@ -164,19 +139,17 @@ const mockedRemoveSnapshot = vi.mocked(removeSnapshot);
|
|
|
164
139
|
const mockedConfirmDestructiveAction = vi.mocked(confirmDestructiveAction);
|
|
165
140
|
const mockedPrintSuccess = vi.mocked(printSuccess);
|
|
166
141
|
const mockedPrintInfo = vi.mocked(printInfo);
|
|
142
|
+
const mockedPrintWarning = vi.mocked(printWarning);
|
|
167
143
|
const mockedResolveTargetWorktree = vi.mocked(resolveTargetWorktree);
|
|
168
144
|
const mockedGitResetHard = vi.mocked(gitResetHard);
|
|
169
145
|
const mockedGitCleanForce = vi.mocked(gitCleanForce);
|
|
170
|
-
const
|
|
171
|
-
const
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
const
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
const mockedPrintSeparator = vi.mocked(printSeparator);
|
|
178
|
-
const mockedParseParallelCommands = vi.mocked(parseParallelCommands);
|
|
179
|
-
const mockedRunParallelCommands = vi.mocked(runParallelCommands);
|
|
146
|
+
const mockedMigrateChangesViaPatch = vi.mocked(migrateChangesViaPatch);
|
|
147
|
+
const mockedComputeCurrentTreeHash = vi.mocked(computeCurrentTreeHash);
|
|
148
|
+
const mockedSaveCurrentSnapshotTree = vi.mocked(saveCurrentSnapshotTree);
|
|
149
|
+
const mockedLoadOldSnapshotToStage = vi.mocked(loadOldSnapshotToStage);
|
|
150
|
+
const mockedSwitchToValidateBranch = vi.mocked(switchToValidateBranch);
|
|
151
|
+
const mockedExecuteRunCommand = vi.mocked(executeRunCommand);
|
|
152
|
+
const mockedGetValidateRunCommand = vi.mocked(getValidateRunCommand);
|
|
180
153
|
|
|
181
154
|
const worktree = { path: '/path/feature', branch: 'feature' };
|
|
182
155
|
|
|
@@ -188,13 +161,6 @@ beforeEach(() => {
|
|
|
188
161
|
mockedGetConfigValue.mockReturnValue(false);
|
|
189
162
|
mockedHasSnapshot.mockReturnValue(false);
|
|
190
163
|
mockedIsWorkingDirClean.mockReset();
|
|
191
|
-
mockedGitAddAll.mockReset();
|
|
192
|
-
mockedGitCommit.mockReset();
|
|
193
|
-
mockedGitDiffBinaryAgainstBranch.mockReset();
|
|
194
|
-
mockedGitApplyFromStdin.mockReset();
|
|
195
|
-
mockedGitResetSoft.mockReset();
|
|
196
|
-
mockedGitWriteTree.mockReset();
|
|
197
|
-
mockedGitRestoreStaged.mockReset();
|
|
198
164
|
mockedGetHeadCommitHash.mockReset();
|
|
199
165
|
mockedGitReadTree.mockReset();
|
|
200
166
|
mockedHasLocalCommits.mockReset();
|
|
@@ -204,20 +170,16 @@ beforeEach(() => {
|
|
|
204
170
|
mockedConfirmDestructiveAction.mockReset();
|
|
205
171
|
mockedPrintSuccess.mockReset();
|
|
206
172
|
mockedPrintInfo.mockReset();
|
|
173
|
+
mockedPrintWarning.mockReset();
|
|
207
174
|
mockedGitResetHard.mockReset();
|
|
208
175
|
mockedGitCleanForce.mockReset();
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
mockedPrintSeparator.mockReset();
|
|
217
|
-
mockedParseParallelCommands.mockReset();
|
|
218
|
-
mockedRunParallelCommands.mockReset();
|
|
219
|
-
// 默认让 parseParallelCommands 返回单命令数组,保持旧测试兼容
|
|
220
|
-
mockedParseParallelCommands.mockImplementation((cmd: string) => [cmd]);
|
|
176
|
+
mockedMigrateChangesViaPatch.mockReset().mockReturnValue({ success: true });
|
|
177
|
+
mockedComputeCurrentTreeHash.mockReset().mockReturnValue('treehash');
|
|
178
|
+
mockedSaveCurrentSnapshotTree.mockReset().mockReturnValue('treehash');
|
|
179
|
+
mockedLoadOldSnapshotToStage.mockReset().mockReturnValue({ success: true, stagedTreeHash: '' });
|
|
180
|
+
mockedSwitchToValidateBranch.mockReset().mockImplementation((name: string) => `clawt-validate-${name}`);
|
|
181
|
+
mockedExecuteRunCommand.mockReset();
|
|
182
|
+
mockedGetValidateRunCommand.mockReset();
|
|
221
183
|
});
|
|
222
184
|
|
|
223
185
|
describe('registerValidateCommand', () => {
|
|
@@ -240,7 +202,7 @@ describe('handleValidate', () => {
|
|
|
240
202
|
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
241
203
|
|
|
242
204
|
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
243
|
-
expect(
|
|
205
|
+
expect(mockedMigrateChangesViaPatch).not.toHaveBeenCalled();
|
|
244
206
|
});
|
|
245
207
|
|
|
246
208
|
it('首次 validate:有已提交 commit 且主 worktree 干净', async () => {
|
|
@@ -248,57 +210,34 @@ describe('handleValidate', () => {
|
|
|
248
210
|
mockedIsWorkingDirClean.mockReturnValue(true); // 所有调用都返回 true
|
|
249
211
|
mockedHasLocalCommits.mockReturnValue(true);
|
|
250
212
|
mockedHasSnapshot.mockReturnValue(false);
|
|
251
|
-
mockedGitDiffBinaryAgainstBranch.mockReturnValue(Buffer.from('diff'));
|
|
252
|
-
mockedGitWriteTree.mockReturnValue('treehash123');
|
|
253
|
-
mockedGetHeadCommitHash.mockReturnValue('headhash456');
|
|
254
213
|
|
|
255
214
|
const program = new Command();
|
|
256
215
|
program.exitOverride();
|
|
257
216
|
registerValidateCommand(program);
|
|
258
217
|
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
259
218
|
|
|
260
|
-
expect(
|
|
261
|
-
expect(
|
|
262
|
-
expect(
|
|
219
|
+
expect(mockedSwitchToValidateBranch).toHaveBeenCalledWith('feature', '/repo');
|
|
220
|
+
expect(mockedMigrateChangesViaPatch).toHaveBeenCalledWith('/path/feature', '/repo', 'feature', false);
|
|
221
|
+
expect(mockedSaveCurrentSnapshotTree).toHaveBeenCalledWith('/repo', 'test-project', 'feature');
|
|
263
222
|
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
264
223
|
});
|
|
265
224
|
|
|
266
|
-
it('首次 validate
|
|
225
|
+
it('首次 validate:有未提交修改时传入 hasUncommitted=true', async () => {
|
|
267
226
|
// 主 worktree 干净,目标 worktree 有未提交修改
|
|
268
|
-
mockedIsWorkingDirClean
|
|
269
|
-
.mockReturnValueOnce(true) // 主 worktree 调用(collectStatus 或 handleValidate 首次检查目标)
|
|
270
|
-
.mockReturnValueOnce(false); // 目标 worktree 检查
|
|
271
|
-
mockedHasLocalCommits.mockReturnValue(false); // 无已提交 commit,但有未提交修改
|
|
272
|
-
mockedHasSnapshot.mockReturnValue(false);
|
|
273
|
-
mockedGitDiffBinaryAgainstBranch.mockReturnValue(Buffer.from('diff'));
|
|
274
|
-
mockedGitWriteTree.mockReturnValue('treehash');
|
|
275
|
-
mockedGetHeadCommitHash.mockReturnValue('headhash');
|
|
276
|
-
|
|
277
|
-
// 注意:hasUncommitted 来自 !isWorkingDirClean(targetWorktreePath)
|
|
278
|
-
// 这里的 mock 链:
|
|
279
|
-
// 第 1 次调用 isWorkingDirClean: 检查 targetWorktreePath => false(有未提交修改)
|
|
280
|
-
// 但是代码中先检查 isWorkingDirClean(mainWorktreePath)
|
|
281
|
-
// 需要更精确的 mock
|
|
282
|
-
mockedIsWorkingDirClean.mockReset();
|
|
283
227
|
mockedIsWorkingDirClean.mockImplementation((cwd?: string) => {
|
|
284
228
|
if (cwd === '/path/feature') return false; // 目标 worktree 不干净
|
|
285
229
|
return true; // 主 worktree 干净
|
|
286
230
|
});
|
|
287
|
-
// 因为 hasUncommitted 依赖 !isWorkingDirClean(targetWorktreePath),
|
|
288
|
-
// 且 !hasUncommitted && !hasCommitted 需要检查 hasLocalCommits
|
|
289
231
|
mockedHasLocalCommits.mockReturnValue(true); // 让它不走"无变更"路径
|
|
232
|
+
mockedHasSnapshot.mockReturnValue(false);
|
|
290
233
|
|
|
291
234
|
const program = new Command();
|
|
292
235
|
program.exitOverride();
|
|
293
236
|
registerValidateCommand(program);
|
|
294
237
|
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
295
238
|
|
|
296
|
-
//
|
|
297
|
-
expect(
|
|
298
|
-
expect(mockedGitCommit).toHaveBeenCalledWith('clawt:temp-commit-for-validate', '/path/feature');
|
|
299
|
-
// 撤销临时 commit
|
|
300
|
-
expect(mockedGitResetSoft).toHaveBeenCalledWith(1, '/path/feature');
|
|
301
|
-
expect(mockedGitRestoreStaged).toHaveBeenCalledWith('/path/feature');
|
|
239
|
+
// migrateChangesViaPatch 应接收 hasUncommitted=true
|
|
240
|
+
expect(mockedMigrateChangesViaPatch).toHaveBeenCalledWith('/path/feature', '/repo', 'feature', true);
|
|
302
241
|
});
|
|
303
242
|
});
|
|
304
243
|
|
|
@@ -351,53 +290,50 @@ describe('增量 validate', () => {
|
|
|
351
290
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
352
291
|
mockedHasLocalCommits.mockReturnValue(true);
|
|
353
292
|
mockedHasSnapshot.mockReturnValue(true);
|
|
354
|
-
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'headhash' });
|
|
293
|
+
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'headhash', stagedTreeHash: '' });
|
|
355
294
|
mockedGetHeadCommitHash.mockReturnValue('headhash'); // HEAD 未变化
|
|
356
|
-
|
|
357
|
-
mockedGitWriteTree.mockReturnValue('newtree');
|
|
295
|
+
mockedComputeCurrentTreeHash.mockReturnValue('newtree');
|
|
358
296
|
|
|
359
297
|
const program = new Command();
|
|
360
298
|
program.exitOverride();
|
|
361
299
|
registerValidateCommand(program);
|
|
362
300
|
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
363
301
|
|
|
364
|
-
expect(
|
|
302
|
+
expect(mockedSwitchToValidateBranch).toHaveBeenCalledWith('feature', '/repo');
|
|
303
|
+
expect(mockedMigrateChangesViaPatch).toHaveBeenCalled();
|
|
304
|
+
expect(mockedComputeCurrentTreeHash).toHaveBeenCalledWith('/repo');
|
|
305
|
+
// 有新变更(newtree !== oldtree),调用 loadOldSnapshotToStage
|
|
306
|
+
expect(mockedLoadOldSnapshotToStage).toHaveBeenCalledWith('oldtree', 'headhash', 'headhash', '/repo');
|
|
365
307
|
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
366
308
|
});
|
|
367
309
|
|
|
368
|
-
it('HEAD
|
|
310
|
+
it('HEAD 变化时调用 loadOldSnapshotToStage', async () => {
|
|
369
311
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
370
312
|
mockedHasLocalCommits.mockReturnValue(true);
|
|
371
313
|
mockedHasSnapshot.mockReturnValue(true);
|
|
372
|
-
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'oldhead' });
|
|
314
|
+
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'oldhead', stagedTreeHash: '' });
|
|
373
315
|
mockedGetHeadCommitHash.mockReturnValue('newhead'); // HEAD 已变化
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
mockedGetCommitTreeHash.mockReturnValue('oldheadtree');
|
|
377
|
-
mockedGitDiffTree.mockReturnValue(Buffer.from('old change patch'));
|
|
378
|
-
mockedGitApplyCachedCheck.mockReturnValue(true);
|
|
316
|
+
mockedComputeCurrentTreeHash.mockReturnValue('newtree');
|
|
317
|
+
mockedLoadOldSnapshotToStage.mockReturnValue({ success: true, stagedTreeHash: 'staged123' });
|
|
379
318
|
|
|
380
319
|
const program = new Command();
|
|
381
320
|
program.exitOverride();
|
|
382
321
|
registerValidateCommand(program);
|
|
383
322
|
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
384
323
|
|
|
385
|
-
expect(
|
|
386
|
-
expect(
|
|
387
|
-
expect(
|
|
324
|
+
expect(mockedLoadOldSnapshotToStage).toHaveBeenCalledWith('oldtree', 'oldhead', 'newhead', '/repo');
|
|
325
|
+
expect(mockedWriteSnapshot).toHaveBeenCalledWith('test-project', 'feature', 'newtree', 'newhead', 'staged123');
|
|
326
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
388
327
|
});
|
|
389
328
|
|
|
390
|
-
it('
|
|
329
|
+
it('loadOldSnapshotToStage 失败时降级为全量模式', async () => {
|
|
391
330
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
392
331
|
mockedHasLocalCommits.mockReturnValue(true);
|
|
393
332
|
mockedHasSnapshot.mockReturnValue(true);
|
|
394
|
-
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'oldhead' });
|
|
333
|
+
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'oldhead', stagedTreeHash: '' });
|
|
395
334
|
mockedGetHeadCommitHash.mockReturnValue('newhead');
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
mockedGetCommitTreeHash.mockReturnValue('oldheadtree');
|
|
399
|
-
mockedGitDiffTree.mockReturnValue(Buffer.from('conflicting patch'));
|
|
400
|
-
mockedGitApplyCachedCheck.mockReturnValue(false); // 有冲突
|
|
335
|
+
mockedComputeCurrentTreeHash.mockReturnValue('newtree');
|
|
336
|
+
mockedLoadOldSnapshotToStage.mockReturnValue({ success: false, stagedTreeHash: '' });
|
|
401
337
|
|
|
402
338
|
const program = new Command();
|
|
403
339
|
program.exitOverride();
|
|
@@ -405,25 +341,27 @@ describe('增量 validate', () => {
|
|
|
405
341
|
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
406
342
|
|
|
407
343
|
expect(mockedPrintWarning).toHaveBeenCalled();
|
|
408
|
-
expect(
|
|
344
|
+
expect(mockedWriteSnapshot).toHaveBeenCalledWith('test-project', 'feature', 'newtree', 'newhead', '');
|
|
345
|
+
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
409
346
|
});
|
|
410
347
|
|
|
411
|
-
it('
|
|
348
|
+
it('无新变更时恢复旧暂存区状态', async () => {
|
|
412
349
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
413
350
|
mockedHasLocalCommits.mockReturnValue(true);
|
|
414
351
|
mockedHasSnapshot.mockReturnValue(true);
|
|
415
|
-
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'headhash' });
|
|
352
|
+
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'headhash', stagedTreeHash: 'staged456' });
|
|
416
353
|
mockedGetHeadCommitHash.mockReturnValue('headhash');
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
mockedGitReadTree.mockImplementation(() => { throw new Error('gc reclaimed'); });
|
|
354
|
+
// tree hash 相同且 HEAD 未变化 → 无新变更
|
|
355
|
+
mockedComputeCurrentTreeHash.mockReturnValue('oldtree');
|
|
420
356
|
|
|
421
357
|
const program = new Command();
|
|
422
358
|
program.exitOverride();
|
|
423
359
|
registerValidateCommand(program);
|
|
424
360
|
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
425
361
|
|
|
426
|
-
expect(
|
|
362
|
+
expect(mockedGitReadTree).toHaveBeenCalledWith('staged456', '/repo');
|
|
363
|
+
expect(mockedLoadOldSnapshotToStage).not.toHaveBeenCalled();
|
|
364
|
+
expect(mockedPrintInfo).toHaveBeenCalled();
|
|
427
365
|
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
428
366
|
});
|
|
429
367
|
});
|
|
@@ -434,62 +372,17 @@ describe('--run 选项', () => {
|
|
|
434
372
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
435
373
|
mockedHasLocalCommits.mockReturnValue(true);
|
|
436
374
|
mockedHasSnapshot.mockReturnValue(false);
|
|
437
|
-
mockedGitDiffBinaryAgainstBranch.mockReturnValue(Buffer.from('diff'));
|
|
438
|
-
mockedGitWriteTree.mockReturnValue('treehash');
|
|
439
|
-
mockedGetHeadCommitHash.mockReturnValue('headhash');
|
|
440
|
-
}
|
|
441
|
-
|
|
442
|
-
/** 构造 spawnSync 返回值的辅助函数 */
|
|
443
|
-
function createSpawnResult(overrides: { status?: number | null; error?: Error }) {
|
|
444
|
-
return {
|
|
445
|
-
pid: 0,
|
|
446
|
-
output: [],
|
|
447
|
-
stdout: Buffer.alloc(0),
|
|
448
|
-
stderr: Buffer.alloc(0),
|
|
449
|
-
status: overrides.status ?? null,
|
|
450
|
-
signal: null,
|
|
451
|
-
error: overrides.error,
|
|
452
|
-
};
|
|
453
375
|
}
|
|
454
376
|
|
|
455
377
|
it('validate 成功后执行 --run 指定的命令', async () => {
|
|
456
378
|
setupSuccessfulFirstValidate();
|
|
457
|
-
mockedRunCommandInherited.mockReturnValue(createSpawnResult({ status: 0 }));
|
|
458
379
|
|
|
459
380
|
const program = new Command();
|
|
460
381
|
program.exitOverride();
|
|
461
382
|
registerValidateCommand(program);
|
|
462
383
|
await program.parseAsync(['validate', '-b', 'feature', '-r', 'npm test'], { from: 'user' });
|
|
463
384
|
|
|
464
|
-
expect(
|
|
465
|
-
expect(mockedPrintSuccess).toHaveBeenCalledTimes(2);
|
|
466
|
-
});
|
|
467
|
-
|
|
468
|
-
it('--run 命令失败时输出错误信息但不抛异常', async () => {
|
|
469
|
-
setupSuccessfulFirstValidate();
|
|
470
|
-
mockedRunCommandInherited.mockReturnValue(createSpawnResult({ status: 1 }));
|
|
471
|
-
|
|
472
|
-
const program = new Command();
|
|
473
|
-
program.exitOverride();
|
|
474
|
-
registerValidateCommand(program);
|
|
475
|
-
await program.parseAsync(['validate', '-b', 'feature', '--run', 'npm test'], { from: 'user' });
|
|
476
|
-
|
|
477
|
-
expect(mockedRunCommandInherited).toHaveBeenCalledWith('npm test', { cwd: '/repo' });
|
|
478
|
-
expect(mockedPrintError).toHaveBeenCalled();
|
|
479
|
-
});
|
|
480
|
-
|
|
481
|
-
it('--run 命令进程启动失败时输出错误信息', async () => {
|
|
482
|
-
setupSuccessfulFirstValidate();
|
|
483
|
-
mockedRunCommandInherited.mockReturnValue(createSpawnResult({ error: new Error('spawn ENOENT') }));
|
|
484
|
-
|
|
485
|
-
const program = new Command();
|
|
486
|
-
program.exitOverride();
|
|
487
|
-
registerValidateCommand(program);
|
|
488
|
-
await program.parseAsync(['validate', '-b', 'feature', '-r', 'nonexistent'], { from: 'user' });
|
|
489
|
-
|
|
490
|
-
expect(mockedPrintError).toHaveBeenCalled();
|
|
491
|
-
// validate 成功 + run 出错,printSuccess 只被调用 1 次(validate 成功)
|
|
492
|
-
expect(mockedPrintSuccess).toHaveBeenCalledTimes(1);
|
|
385
|
+
expect(mockedExecuteRunCommand).toHaveBeenCalledWith('npm test', '/repo');
|
|
493
386
|
});
|
|
494
387
|
|
|
495
388
|
it('未传 --run 时不执行任何命令', async () => {
|
|
@@ -500,7 +393,7 @@ describe('--run 选项', () => {
|
|
|
500
393
|
registerValidateCommand(program);
|
|
501
394
|
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
502
395
|
|
|
503
|
-
expect(
|
|
396
|
+
expect(mockedExecuteRunCommand).not.toHaveBeenCalled();
|
|
504
397
|
});
|
|
505
398
|
|
|
506
399
|
it('--clean 与 --run 同时传入时只执行 clean 不执行 run', async () => {
|
|
@@ -513,25 +406,23 @@ describe('--run 选项', () => {
|
|
|
513
406
|
await program.parseAsync(['validate', '--clean', '-b', 'feature', '-r', 'npm test'], { from: 'user' });
|
|
514
407
|
|
|
515
408
|
expect(mockedRemoveSnapshot).toHaveBeenCalled();
|
|
516
|
-
expect(
|
|
409
|
+
expect(mockedExecuteRunCommand).not.toHaveBeenCalled();
|
|
517
410
|
});
|
|
518
411
|
|
|
519
412
|
it('增量 validate 成功后也执行 --run 命令', async () => {
|
|
520
413
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
521
414
|
mockedHasLocalCommits.mockReturnValue(true);
|
|
522
415
|
mockedHasSnapshot.mockReturnValue(true);
|
|
523
|
-
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'headhash' });
|
|
416
|
+
mockedReadSnapshot.mockReturnValue({ treeHash: 'oldtree', headCommitHash: 'headhash', stagedTreeHash: '' });
|
|
524
417
|
mockedGetHeadCommitHash.mockReturnValue('headhash');
|
|
525
|
-
|
|
526
|
-
mockedGitWriteTree.mockReturnValue('newtree');
|
|
527
|
-
mockedRunCommandInherited.mockReturnValue(createSpawnResult({ status: 0 }));
|
|
418
|
+
mockedComputeCurrentTreeHash.mockReturnValue('newtree');
|
|
528
419
|
|
|
529
420
|
const program = new Command();
|
|
530
421
|
program.exitOverride();
|
|
531
422
|
registerValidateCommand(program);
|
|
532
423
|
await program.parseAsync(['validate', '-b', 'feature', '-r', 'npm test'], { from: 'user' });
|
|
533
424
|
|
|
534
|
-
expect(
|
|
425
|
+
expect(mockedExecuteRunCommand).toHaveBeenCalledWith('npm test', '/repo');
|
|
535
426
|
});
|
|
536
427
|
|
|
537
428
|
it('目标分支无变更时不执行 --run', async () => {
|
|
@@ -543,92 +434,54 @@ describe('--run 选项', () => {
|
|
|
543
434
|
registerValidateCommand(program);
|
|
544
435
|
await program.parseAsync(['validate', '-b', 'feature', '-r', 'npm test'], { from: 'user' });
|
|
545
436
|
|
|
546
|
-
expect(
|
|
437
|
+
expect(mockedExecuteRunCommand).not.toHaveBeenCalled();
|
|
547
438
|
});
|
|
548
439
|
});
|
|
549
440
|
|
|
550
|
-
describe('
|
|
441
|
+
describe('配置读取 fallback(resolveRunCommand)', () => {
|
|
551
442
|
/** 设置首次 validate 成功的公共 mock */
|
|
552
443
|
function setupSuccessfulFirstValidate(): void {
|
|
553
444
|
mockedIsWorkingDirClean.mockReturnValue(true);
|
|
554
445
|
mockedHasLocalCommits.mockReturnValue(true);
|
|
555
446
|
mockedHasSnapshot.mockReturnValue(false);
|
|
556
|
-
mockedGitDiffBinaryAgainstBranch.mockReturnValue(Buffer.from('diff'));
|
|
557
|
-
mockedGitWriteTree.mockReturnValue('treehash');
|
|
558
|
-
mockedGetHeadCommitHash.mockReturnValue('headhash');
|
|
559
447
|
}
|
|
560
448
|
|
|
561
|
-
it('
|
|
449
|
+
it('未传 -r 但项目配置有 validateRunCommand 时从配置读取执行', async () => {
|
|
562
450
|
setupSuccessfulFirstValidate();
|
|
563
|
-
|
|
564
|
-
mockedRunParallelCommands.mockResolvedValue([
|
|
565
|
-
{ command: 'pnpm test', exitCode: 0 },
|
|
566
|
-
{ command: 'pnpm build', exitCode: 0 },
|
|
567
|
-
]);
|
|
451
|
+
mockedGetValidateRunCommand.mockReturnValue('pnpm test');
|
|
568
452
|
|
|
569
453
|
const program = new Command();
|
|
570
454
|
program.exitOverride();
|
|
571
455
|
registerValidateCommand(program);
|
|
572
|
-
await program.parseAsync(['validate', '-b', 'feature'
|
|
573
|
-
|
|
574
|
-
// 应该调用并行执行而非同步执行
|
|
575
|
-
expect(mockedRunParallelCommands).toHaveBeenCalledWith(['pnpm test', 'pnpm build'], { cwd: '/repo' });
|
|
576
|
-
expect(mockedRunCommandInherited).not.toHaveBeenCalled();
|
|
577
|
-
// 全部成功,printSuccess 被调用(validate 成功 + 各命令成功 + 汇总成功)
|
|
578
|
-
expect(mockedPrintSuccess).toHaveBeenCalled();
|
|
579
|
-
});
|
|
580
|
-
|
|
581
|
-
it('并行执行部分失败时输出错误汇总', async () => {
|
|
582
|
-
setupSuccessfulFirstValidate();
|
|
583
|
-
mockedParseParallelCommands.mockReturnValue(['pnpm test', 'pnpm build']);
|
|
584
|
-
mockedRunParallelCommands.mockResolvedValue([
|
|
585
|
-
{ command: 'pnpm test', exitCode: 1 },
|
|
586
|
-
{ command: 'pnpm build', exitCode: 0 },
|
|
587
|
-
]);
|
|
588
|
-
|
|
589
|
-
const program = new Command();
|
|
590
|
-
program.exitOverride();
|
|
591
|
-
registerValidateCommand(program);
|
|
592
|
-
await program.parseAsync(['validate', '-b', 'feature', '-r', 'pnpm test & pnpm build'], { from: 'user' });
|
|
456
|
+
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
593
457
|
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
expect(mockedPrintError).toHaveBeenCalled();
|
|
458
|
+
// 应该从配置读取命令并执行
|
|
459
|
+
expect(mockedExecuteRunCommand).toHaveBeenCalledWith('pnpm test', '/repo');
|
|
597
460
|
});
|
|
598
461
|
|
|
599
|
-
it('
|
|
462
|
+
it('传了 -r 时以用户参数为准,忽略项目配置', async () => {
|
|
600
463
|
setupSuccessfulFirstValidate();
|
|
601
|
-
|
|
602
|
-
mockedRunCommandInherited.mockReturnValue({
|
|
603
|
-
pid: 0, output: [], stdout: Buffer.alloc(0), stderr: Buffer.alloc(0),
|
|
604
|
-
status: 0, signal: null, error: undefined,
|
|
605
|
-
});
|
|
464
|
+
mockedGetValidateRunCommand.mockReturnValue('pnpm test');
|
|
606
465
|
|
|
607
466
|
const program = new Command();
|
|
608
467
|
program.exitOverride();
|
|
609
468
|
registerValidateCommand(program);
|
|
610
|
-
await program.parseAsync(['validate', '-b', 'feature', '-r', '
|
|
469
|
+
await program.parseAsync(['validate', '-b', 'feature', '-r', 'pnpm build'], { from: 'user' });
|
|
611
470
|
|
|
612
|
-
//
|
|
613
|
-
expect(
|
|
614
|
-
expect(mockedRunParallelCommands).not.toHaveBeenCalled();
|
|
471
|
+
// 应该使用用户传入的命令,而非配置中的
|
|
472
|
+
expect(mockedExecuteRunCommand).toHaveBeenCalledWith('pnpm build', '/repo');
|
|
615
473
|
});
|
|
616
474
|
|
|
617
|
-
it('
|
|
475
|
+
it('未传 -r 且项目配置无 validateRunCommand 时不执行命令', async () => {
|
|
618
476
|
setupSuccessfulFirstValidate();
|
|
619
|
-
|
|
620
|
-
mockedRunCommandInherited.mockReturnValue({
|
|
621
|
-
pid: 0, output: [], stdout: Buffer.alloc(0), stderr: Buffer.alloc(0),
|
|
622
|
-
status: 0, signal: null, error: undefined,
|
|
623
|
-
});
|
|
477
|
+
mockedGetValidateRunCommand.mockReturnValue(undefined);
|
|
624
478
|
|
|
625
479
|
const program = new Command();
|
|
626
480
|
program.exitOverride();
|
|
627
481
|
registerValidateCommand(program);
|
|
628
|
-
await program.parseAsync(['validate', '-b', 'feature'
|
|
482
|
+
await program.parseAsync(['validate', '-b', 'feature'], { from: 'user' });
|
|
629
483
|
|
|
630
|
-
//
|
|
631
|
-
expect(
|
|
632
|
-
expect(mockedRunParallelCommands).not.toHaveBeenCalled();
|
|
484
|
+
// 没有命令可执行
|
|
485
|
+
expect(mockedExecuteRunCommand).not.toHaveBeenCalled();
|
|
633
486
|
});
|
|
634
487
|
});
|