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.
@@ -0,0 +1,174 @@
1
+ import { logger } from '../logger/index.js';
2
+ import { ClawtError } from '../errors/index.js';
3
+ import { MESSAGES } from '../constants/index.js';
4
+ import {
5
+ gitAddAll,
6
+ gitCommit,
7
+ gitRestoreStaged,
8
+ gitDiffBinaryAgainstBranch,
9
+ gitApplyFromStdin,
10
+ gitApplyCachedFromStdin,
11
+ gitResetSoft,
12
+ gitWriteTree,
13
+ gitReadTree,
14
+ getCommitTreeHash,
15
+ gitDiffTree,
16
+ gitApplyCachedCheck,
17
+ getValidateBranchName,
18
+ checkBranchExists,
19
+ gitCheckout,
20
+ getCurrentBranch,
21
+ getHeadCommitHash,
22
+ writeSnapshot,
23
+ printWarning,
24
+ } from './index.js';
25
+
26
+ /**
27
+ * 通过 patch 将目标分支的全量变更(已提交 + 未提交)迁移到主 worktree
28
+ * 使用 git diff HEAD...branch --binary 获取变更,避免 stash 方式无法检测已提交 commit 的问题
29
+ * @param {string} targetWorktreePath - 目标 worktree 路径
30
+ * @param {string} mainWorktreePath - 主 worktree 路径
31
+ * @param {string} branchName - 分支名
32
+ * @param {boolean} hasUncommitted - 目标 worktree 是否有未提交修改
33
+ * @returns {{ success: boolean }} patch 迁移结果
34
+ */
35
+ export function migrateChangesViaPatch(targetWorktreePath: string, mainWorktreePath: string, branchName: string, hasUncommitted: boolean): { success: boolean } {
36
+ let didTempCommit = false;
37
+
38
+ try {
39
+ // 如果有未提交修改,先做临时 commit 以便 diff 能捕获全部变更
40
+ if (hasUncommitted) {
41
+ gitAddAll(targetWorktreePath);
42
+ gitCommit('clawt:temp-commit-for-validate', targetWorktreePath);
43
+ didTempCommit = true;
44
+ }
45
+
46
+ // 在主 worktree 执行三点 diff,获取目标分支自分叉点以来的全量变更
47
+ const patch = gitDiffBinaryAgainstBranch(branchName, mainWorktreePath);
48
+
49
+ // 应用 patch 到主 worktree 工作目录
50
+ if (patch.length > 0) {
51
+ try {
52
+ gitApplyFromStdin(patch, mainWorktreePath);
53
+ } catch (error) {
54
+ logger.warn(`patch apply 失败: ${error}`);
55
+ printWarning(MESSAGES.VALIDATE_PATCH_APPLY_FAILED(branchName));
56
+ return { success: false };
57
+ }
58
+ }
59
+
60
+ return { success: true };
61
+ } finally {
62
+ // 确保临时 commit 一定会被撤销,恢复目标 worktree 原状
63
+ // 每个操作独立 try-catch,避免前一个失败导致后续操作不执行
64
+ if (didTempCommit) {
65
+ try {
66
+ gitResetSoft(1, targetWorktreePath);
67
+ } catch (error) {
68
+ logger.error(`撤销临时 commit 失败: ${error}`);
69
+ }
70
+ try {
71
+ gitRestoreStaged(targetWorktreePath);
72
+ } catch (error) {
73
+ logger.error(`恢复暂存区失败: ${error}`);
74
+ }
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * 计算当前主 worktree 工作目录变更的 git tree hash
81
+ * 操作序列:git add . → git write-tree → git restore --staged .
82
+ * 仅计算 tree hash,不写入快照文件
83
+ * @param {string} mainWorktreePath - 主 worktree 路径
84
+ * @returns {string} 当前工作目录变更对应的 tree hash
85
+ */
86
+ export function computeCurrentTreeHash(mainWorktreePath: string): string {
87
+ gitAddAll(mainWorktreePath);
88
+ const treeHash = gitWriteTree(mainWorktreePath);
89
+ gitRestoreStaged(mainWorktreePath);
90
+ return treeHash;
91
+ }
92
+
93
+ /**
94
+ * 保存当前主 worktree 工作目录变更为 git tree 对象快照
95
+ * 操作序列:git add . → git write-tree → git restore --staged .
96
+ * 同时保存当前 HEAD commit hash,用于增量 validate 时对齐基准
97
+ * @param {string} mainWorktreePath - 主 worktree 路径
98
+ * @param {string} projectName - 项目名
99
+ * @param {string} branchName - 分支名
100
+ * @param {string} [stagedTreeHash=''] - validate 结束时暂存区对应的 tree hash
101
+ * @returns {string} 生成的 tree hash
102
+ */
103
+ export function saveCurrentSnapshotTree(mainWorktreePath: string, projectName: string, branchName: string, stagedTreeHash = ''): string {
104
+ gitAddAll(mainWorktreePath);
105
+ const treeHash = gitWriteTree(mainWorktreePath);
106
+ gitRestoreStaged(mainWorktreePath);
107
+ const headCommitHash = getHeadCommitHash(mainWorktreePath);
108
+ writeSnapshot(projectName, branchName, treeHash, headCommitHash, stagedTreeHash);
109
+ return treeHash;
110
+ }
111
+
112
+ /**
113
+ * 将旧快照载入暂存区(从 handleIncrementalValidate 步骤 6 提取)
114
+ * 根据 HEAD 是否变化,选择不同的载入策略:
115
+ * - HEAD 变化:将旧变更 patch 重放到当前 HEAD 暂存区上
116
+ * - HEAD 未变化:直接 read-tree 旧快照
117
+ * @param {string} oldTreeHash - 旧快照的 tree hash
118
+ * @param {string} oldHeadCommitHash - 旧快照时的 HEAD commit hash
119
+ * @param {string} currentHeadCommitHash - 当前的 HEAD commit hash
120
+ * @param {string} mainWorktreePath - 主 worktree 路径
121
+ * @returns {{ success: boolean; stagedTreeHash: string }} 载入结果
122
+ */
123
+ export function loadOldSnapshotToStage(oldTreeHash: string, oldHeadCommitHash: string, currentHeadCommitHash: string, mainWorktreePath: string): { success: boolean; stagedTreeHash: string } {
124
+ try {
125
+ if (oldHeadCommitHash && oldHeadCommitHash !== currentHeadCommitHash) {
126
+ // HEAD 发生了变化:
127
+ // 将旧变更 patch(旧 tree 相对于旧 HEAD 的差异)重放到当前 HEAD 暂存区上,
128
+ // 避免新旧 tree 基准不同导致 diff 混入 HEAD 变化的内容
129
+ const oldHeadTreeHash = getCommitTreeHash(oldHeadCommitHash, mainWorktreePath);
130
+ const oldChangePatch = gitDiffTree(oldHeadTreeHash, oldTreeHash, mainWorktreePath);
131
+
132
+ if (oldChangePatch.length > 0 && gitApplyCachedCheck(oldChangePatch, mainWorktreePath)) {
133
+ // 无冲突:apply --cached 到当前 HEAD 暂存区
134
+ gitApplyCachedFromStdin(oldChangePatch, mainWorktreePath);
135
+ // 记录暂存区的 tree hash(gitWriteTree 不修改暂存区内容,仅生成 tree 对象)
136
+ const stagedTreeHash = gitWriteTree(mainWorktreePath);
137
+ return { success: true, stagedTreeHash };
138
+ } else if (oldChangePatch.length > 0) {
139
+ // 有冲突:降级为全量模式(暂存区保持为空)
140
+ logger.warn('旧变更 patch 与当前 HEAD 冲突,降级为全量模式');
141
+ return { success: false, stagedTreeHash: '' };
142
+ }
143
+ // oldChangePatch 为空表示旧变更为空,暂存区保持干净即可
144
+ return { success: true, stagedTreeHash: '' };
145
+ } else {
146
+ // HEAD 未变化(或旧版快照无 HEAD 信息):直接 read-tree 旧快照
147
+ gitReadTree(oldTreeHash, mainWorktreePath);
148
+ return { success: true, stagedTreeHash: oldTreeHash };
149
+ }
150
+ } catch (error) {
151
+ // 旧 tree 对象无法读取(可能被 git gc 回收),降级为全量模式
152
+ logger.warn(`增量 read-tree 失败: ${error}`);
153
+ return { success: false, stagedTreeHash: '' };
154
+ }
155
+ }
156
+
157
+ /**
158
+ * 切换到验证分支(首次/增量 validate 共享逻辑)
159
+ * 校验验证分支是否存在,若当前不在该分支则执行 checkout
160
+ * @param {string} branchName - 原始分支名
161
+ * @param {string} mainWorktreePath - 主 worktree 路径
162
+ * @returns {string} 验证分支名
163
+ */
164
+ export function switchToValidateBranch(branchName: string, mainWorktreePath: string): string {
165
+ const validateBranchName = getValidateBranchName(branchName);
166
+ if (!checkBranchExists(validateBranchName)) {
167
+ throw new ClawtError(MESSAGES.VALIDATE_BRANCH_NOT_FOUND(validateBranchName, branchName));
168
+ }
169
+ const currentBranch = getCurrentBranch(mainWorktreePath);
170
+ if (currentBranch !== validateBranchName) {
171
+ gitCheckout(validateBranchName, mainWorktreePath);
172
+ }
173
+ return validateBranchName;
174
+ }
@@ -0,0 +1,105 @@
1
+ import { MESSAGES } from '../constants/index.js';
2
+ import {
3
+ printInfo,
4
+ printSuccess,
5
+ printError,
6
+ printSeparator,
7
+ runCommandInherited,
8
+ parseParallelCommands,
9
+ runParallelCommands,
10
+ } from './index.js';
11
+ import type { ParallelCommandResult } from './index.js';
12
+
13
+ /**
14
+ * 执行单个命令(同步方式,保持原有行为不变)
15
+ * @param {string} command - 要执行的命令字符串
16
+ * @param {string} mainWorktreePath - 主 worktree 路径
17
+ */
18
+ function executeSingleCommand(command: string, mainWorktreePath: string): void {
19
+ printInfo(MESSAGES.VALIDATE_RUN_START(command));
20
+ printSeparator();
21
+
22
+ const result = runCommandInherited(command, { cwd: mainWorktreePath });
23
+
24
+ printSeparator();
25
+
26
+ if (result.error) {
27
+ // 进程启动失败(如命令不存在)
28
+ printError(MESSAGES.VALIDATE_RUN_ERROR(command, result.error.message));
29
+ return;
30
+ }
31
+
32
+ const exitCode = result.status ?? 1;
33
+ if (exitCode === 0) {
34
+ printSuccess(MESSAGES.VALIDATE_RUN_SUCCESS(command));
35
+ } else {
36
+ printError(MESSAGES.VALIDATE_RUN_FAILED(command, exitCode));
37
+ }
38
+ }
39
+
40
+ /**
41
+ * 汇总输出并行命令的执行结果
42
+ * @param {ParallelCommandResult[]} results - 各命令的执行结果数组
43
+ */
44
+ function reportParallelResults(results: ParallelCommandResult[]): void {
45
+ printSeparator();
46
+
47
+ const successCount = results.filter((r) => r.exitCode === 0 && !r.error).length;
48
+ const failedCount = results.length - successCount;
49
+
50
+ for (const result of results) {
51
+ if (result.error) {
52
+ printError(MESSAGES.VALIDATE_PARALLEL_CMD_ERROR(result.command, result.error));
53
+ } else if (result.exitCode === 0) {
54
+ printSuccess(MESSAGES.VALIDATE_PARALLEL_CMD_SUCCESS(result.command));
55
+ } else {
56
+ printError(MESSAGES.VALIDATE_PARALLEL_CMD_FAILED(result.command, result.exitCode));
57
+ }
58
+ }
59
+
60
+ if (failedCount === 0) {
61
+ printSuccess(MESSAGES.VALIDATE_PARALLEL_RUN_ALL_SUCCESS(results.length));
62
+ } else {
63
+ printError(MESSAGES.VALIDATE_PARALLEL_RUN_SUMMARY(successCount, failedCount));
64
+ }
65
+ }
66
+
67
+ /**
68
+ * 并行执行多个命令并汇总结果
69
+ * @param {string[]} commands - 要并行执行的命令数组
70
+ * @param {string} mainWorktreePath - 主 worktree 路径
71
+ */
72
+ async function executeParallelCommands(commands: string[], mainWorktreePath: string): Promise<void> {
73
+ printInfo(MESSAGES.VALIDATE_PARALLEL_RUN_START(commands.length));
74
+
75
+ for (let i = 0; i < commands.length; i++) {
76
+ printInfo(MESSAGES.VALIDATE_PARALLEL_CMD_START(i + 1, commands.length, commands[i]));
77
+ }
78
+
79
+ printSeparator();
80
+
81
+ const results = await runParallelCommands(commands, { cwd: mainWorktreePath });
82
+
83
+ reportParallelResults(results);
84
+ }
85
+
86
+ /**
87
+ * 在主 worktree 中执行用户指定的命令
88
+ * 根据命令字符串中的 & 分隔符决定是单命令执行还是并行执行
89
+ * 命令执行失败不影响 validate 本身的结果,仅输出提示
90
+ * @param {string} command - 要执行的命令字符串
91
+ * @param {string} mainWorktreePath - 主 worktree 路径
92
+ */
93
+ export async function executeRunCommand(command: string, mainWorktreePath: string): Promise<void> {
94
+ printInfo('');
95
+
96
+ const commands = parseParallelCommands(command);
97
+
98
+ if (commands.length <= 1) {
99
+ // 单命令(包括含 && 的串行命令),走原有同步路径
100
+ executeSingleCommand(commands[0] || command, mainWorktreePath);
101
+ } else {
102
+ // 多命令,并行执行
103
+ await executeParallelCommands(commands, mainWorktreePath);
104
+ }
105
+ }
@@ -24,6 +24,16 @@ function getSnapshotHeadPath(projectName: string, branchName: string): string {
24
24
  return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.head`);
25
25
  }
26
26
 
27
+ /**
28
+ * 获取指定项目和分支的 validate 快照 staged tree 文件路径
29
+ * @param {string} projectName - 项目名
30
+ * @param {string} branchName - 分支名
31
+ * @returns {string} staged tree hash 文件的绝对路径
32
+ */
33
+ function getSnapshotStagedPath(projectName: string, branchName: string): string {
34
+ return join(VALIDATE_SNAPSHOTS_DIR, projectName, `${branchName}.staged`);
35
+ }
36
+
27
37
  /**
28
38
  * 判断指定项目和分支是否存在 validate 快照
29
39
  * @param {string} projectName - 项目名
@@ -58,49 +68,55 @@ export function readSnapshotTreeHash(projectName: string, branchName: string): s
58
68
  }
59
69
 
60
70
  /**
61
- * 读取指定项目和分支的 validate 快照(tree hash + HEAD commit hash)
62
- * tree hash 从 .tree 文件读取,HEAD commit hash 从 .head 文件读取
71
+ * 读取指定项目和分支的 validate 快照(tree hash + HEAD commit hash + staged tree hash
72
+ * tree hash 从 .tree 文件读取,HEAD commit hash 从 .head 文件读取,staged tree hash 从 .staged 文件读取
63
73
  * @param {string} projectName - 项目名
64
74
  * @param {string} branchName - 分支名
65
- * @returns {{ treeHash: string; headCommitHash: string }} 快照数据
75
+ * @returns {{ treeHash: string; headCommitHash: string; stagedTreeHash: string }} 快照数据
66
76
  */
67
- export function readSnapshot(projectName: string, branchName: string): { treeHash: string; headCommitHash: string } {
77
+ export function readSnapshot(projectName: string, branchName: string): { treeHash: string; headCommitHash: string; stagedTreeHash: string } {
68
78
  const snapshotPath = getSnapshotPath(projectName, branchName);
69
79
  const headPath = getSnapshotHeadPath(projectName, branchName);
80
+ const stagedPath = getSnapshotStagedPath(projectName, branchName);
70
81
  logger.debug(`读取 validate 快照: ${snapshotPath}`);
71
82
 
72
83
  const treeHash = existsSync(snapshotPath) ? readFileSync(snapshotPath, 'utf-8').trim() : '';
73
84
  const headCommitHash = existsSync(headPath) ? readFileSync(headPath, 'utf-8').trim() : '';
85
+ const stagedTreeHash = existsSync(stagedPath) ? readFileSync(stagedPath, 'utf-8').trim() : '';
74
86
 
75
- return { treeHash, headCommitHash };
87
+ return { treeHash, headCommitHash, stagedTreeHash };
76
88
  }
77
89
 
78
90
  /**
79
91
  * 写入 validate 快照内容(自动创建目录)
80
- * tree hash 写入 .tree 文件,HEAD commit hash 写入 .head 文件
92
+ * tree hash 写入 .tree 文件,HEAD commit hash 写入 .head 文件,staged tree hash 写入 .staged 文件
81
93
  * @param {string} projectName - 项目名
82
94
  * @param {string} branchName - 分支名
83
95
  * @param {string} treeHash - git tree 对象的 hash
84
96
  * @param {string} headCommitHash - 快照时主 worktree 的 HEAD commit hash
97
+ * @param {string} [stagedTreeHash=''] - validate 结束时暂存区对应的 tree hash
85
98
  */
86
- export function writeSnapshot(projectName: string, branchName: string, treeHash: string, headCommitHash: string): void {
99
+ export function writeSnapshot(projectName: string, branchName: string, treeHash: string, headCommitHash: string, stagedTreeHash = ''): void {
87
100
  const snapshotPath = getSnapshotPath(projectName, branchName);
88
101
  const headPath = getSnapshotHeadPath(projectName, branchName);
102
+ const stagedPath = getSnapshotStagedPath(projectName, branchName);
89
103
  const snapshotDir = join(VALIDATE_SNAPSHOTS_DIR, projectName);
90
104
  ensureDir(snapshotDir);
91
105
  writeFileSync(snapshotPath, treeHash, 'utf-8');
92
106
  writeFileSync(headPath, headCommitHash, 'utf-8');
93
- logger.info(`已保存 validate 快照: ${snapshotPath}, ${headPath}`);
107
+ writeFileSync(stagedPath, stagedTreeHash, 'utf-8');
108
+ logger.info(`已保存 validate 快照: ${snapshotPath}, ${headPath}, ${stagedPath}`);
94
109
  }
95
110
 
96
111
  /**
97
- * 删除指定项目和分支的 validate 快照(.tree + .head)
112
+ * 删除指定项目和分支的 validate 快照(.tree + .head + .staged
98
113
  * @param {string} projectName - 项目名
99
114
  * @param {string} branchName - 分支名
100
115
  */
101
116
  export function removeSnapshot(projectName: string, branchName: string): void {
102
117
  const snapshotPath = getSnapshotPath(projectName, branchName);
103
118
  const headPath = getSnapshotHeadPath(projectName, branchName);
119
+ const stagedPath = getSnapshotStagedPath(projectName, branchName);
104
120
  if (existsSync(snapshotPath)) {
105
121
  unlinkSync(snapshotPath);
106
122
  logger.info(`已删除 validate 快照: ${snapshotPath}`);
@@ -109,6 +125,10 @@ export function removeSnapshot(projectName: string, branchName: string): void {
109
125
  unlinkSync(headPath);
110
126
  logger.info(`已删除 validate 快照: ${headPath}`);
111
127
  }
128
+ if (existsSync(stagedPath)) {
129
+ unlinkSync(stagedPath);
130
+ logger.info(`已删除 validate 快照: ${stagedPath}`);
131
+ }
112
132
  }
113
133
 
114
134
  /**
@@ -38,6 +38,7 @@ vi.mock('../../../src/utils/index.js', async (importOriginal) => {
38
38
  parseConfigValue: original.parseConfigValue,
39
39
  promptConfigValue: original.promptConfigValue,
40
40
  formatConfigValue: original.formatConfigValue,
41
+ interactiveConfigEditor: original.interactiveConfigEditor,
41
42
  };
42
43
  });
43
44
 
@@ -12,6 +12,12 @@ vi.mock('../../../src/constants/index.js', () => ({
12
12
  INIT_SHOW: (configJson: string) => `当前项目配置:\n${configJson}`,
13
13
  PROJECT_NOT_INITIALIZED: '项目尚未初始化,请先执行 clawt init 设置主工作分支',
14
14
  PROJECT_CONFIG_MISSING_BRANCH: '项目配置缺少主工作分支信息,请重新执行 clawt init 设置主工作分支',
15
+ INIT_SELECT_PROMPT: '选择要修改的项目配置项',
16
+ INIT_SET_SUCCESS: (key: string, value: string) => `✓ 项目配置 ${key} 已设置为 ${value}`,
17
+ },
18
+ PROJECT_CONFIG_DEFINITIONS: {
19
+ clawtMainWorkBranch: { defaultValue: '', description: '主 worktree 的工作分支名' },
20
+ validateRunCommand: { defaultValue: undefined, description: 'validate 成功后自动执行的命令' },
15
21
  },
16
22
  }));
17
23
 
@@ -24,6 +30,7 @@ vi.mock('../../../src/utils/index.js', () => ({
24
30
  printSuccess: vi.fn(),
25
31
  printInfo: vi.fn(),
26
32
  safeStringify: vi.fn((value: unknown, indent: number = 2) => JSON.stringify(value, null, indent)),
33
+ interactiveConfigEditor: vi.fn(),
27
34
  }));
28
35
 
29
36
  import { registerInitCommand } from '../../../src/commands/init.js';
@@ -32,16 +39,16 @@ import {
32
39
  saveProjectConfig,
33
40
  requireProjectConfig,
34
41
  printSuccess,
35
- printInfo,
36
42
  getCurrentBranch,
43
+ interactiveConfigEditor,
37
44
  } from '../../../src/utils/index.js';
38
45
 
39
46
  const mockedLoadProjectConfig = vi.mocked(loadProjectConfig);
40
47
  const mockedSaveProjectConfig = vi.mocked(saveProjectConfig);
41
48
  const mockedRequireProjectConfig = vi.mocked(requireProjectConfig);
42
49
  const mockedPrintSuccess = vi.mocked(printSuccess);
43
- const mockedPrintInfo = vi.mocked(printInfo);
44
50
  const mockedGetCurrentBranch = vi.mocked(getCurrentBranch);
51
+ const mockedInteractiveConfigEditor = vi.mocked(interactiveConfigEditor);
45
52
 
46
53
  beforeEach(() => {
47
54
  mockedLoadProjectConfig.mockReset();
@@ -49,8 +56,8 @@ beforeEach(() => {
49
56
  mockedRequireProjectConfig.mockReset();
50
57
  mockedRequireProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'main' });
51
58
  mockedPrintSuccess.mockReset();
52
- mockedPrintInfo.mockReset();
53
59
  mockedGetCurrentBranch.mockReturnValue('main');
60
+ mockedInteractiveConfigEditor.mockReset();
54
61
  });
55
62
 
56
63
  describe('registerInitCommand', () => {
@@ -132,15 +139,43 @@ describe('handleInit', () => {
132
139
  });
133
140
 
134
141
  describe('handleInitShow (show 子命令)', () => {
135
- it('clawt init show 展示当前配置', async () => {
142
+ it('clawt init show 进入交互式面板并保存修改', async () => {
136
143
  mockedRequireProjectConfig.mockReturnValue({ clawtMainWorkBranch: 'develop' });
144
+ mockedInteractiveConfigEditor.mockResolvedValue({ key: 'validateRunCommand', newValue: 'npm test' });
145
+
146
+ const program = new Command();
147
+ program.exitOverride();
148
+ registerInitCommand(program);
149
+ await program.parseAsync(['init', 'show'], { from: 'user' });
150
+
151
+ // 验证调用了交互式配置编辑器
152
+ expect(mockedInteractiveConfigEditor).toHaveBeenCalled();
153
+ // 验证保存了合并后的配置
154
+ expect(mockedSaveProjectConfig).toHaveBeenCalledWith({
155
+ clawtMainWorkBranch: 'develop',
156
+ validateRunCommand: 'npm test',
157
+ });
158
+ // 验证输出了成功消息
159
+ expect(mockedPrintSuccess).toHaveBeenCalledWith(
160
+ expect.stringContaining('validateRunCommand'),
161
+ );
162
+ });
163
+
164
+ it('clawt init show 修改已有配置项', async () => {
165
+ mockedRequireProjectConfig.mockReturnValue({
166
+ clawtMainWorkBranch: 'main',
167
+ validateRunCommand: 'npm test',
168
+ });
169
+ mockedInteractiveConfigEditor.mockResolvedValue({ key: 'clawtMainWorkBranch', newValue: 'develop' });
137
170
 
138
171
  const program = new Command();
139
172
  program.exitOverride();
140
173
  registerInitCommand(program);
141
174
  await program.parseAsync(['init', 'show'], { from: 'user' });
142
175
 
143
- expect(mockedPrintInfo).toHaveBeenCalled();
144
- expect(mockedSaveProjectConfig).not.toHaveBeenCalled();
176
+ expect(mockedSaveProjectConfig).toHaveBeenCalledWith({
177
+ clawtMainWorkBranch: 'develop',
178
+ validateRunCommand: 'npm test',
179
+ });
145
180
  });
146
181
  });