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
|
@@ -48,6 +48,7 @@ vi.mock('../../../src/constants/index.js', async (importOriginal) => {
|
|
|
48
48
|
CONFIG_INVALID_ENUM: (key: string, validValues: readonly string[]) =>
|
|
49
49
|
`配置项 ${key} 仅接受以下值: ${validValues.join(', ')}`,
|
|
50
50
|
CONFIG_INPUT_PROMPT: (key: string) => `输入 ${key} 的新值`,
|
|
51
|
+
CONFIG_SELECT_PROMPT: '选择要修改的配置项',
|
|
51
52
|
},
|
|
52
53
|
};
|
|
53
54
|
});
|
|
@@ -58,6 +59,7 @@ import {
|
|
|
58
59
|
parseConfigValue,
|
|
59
60
|
promptConfigValue,
|
|
60
61
|
formatConfigValue,
|
|
62
|
+
interactiveConfigEditor,
|
|
61
63
|
} from '../../../src/utils/config-strategy.js';
|
|
62
64
|
|
|
63
65
|
beforeEach(() => {
|
|
@@ -184,7 +186,7 @@ describe('promptConfigValue', () => {
|
|
|
184
186
|
|
|
185
187
|
it('字符串 + 有 allowedValues 使用 Select 提示', async () => {
|
|
186
188
|
mockSelectRun.mockResolvedValueOnce('iterm2');
|
|
187
|
-
const result = await promptConfigValue('terminalApp', 'auto');
|
|
189
|
+
const result = await promptConfigValue('terminalApp', 'auto', ['auto', 'iterm2', 'terminal']);
|
|
188
190
|
expect(result).toBe('iterm2');
|
|
189
191
|
expect(mockSelectRun).toHaveBeenCalledTimes(1);
|
|
190
192
|
});
|
|
@@ -217,4 +219,78 @@ describe('formatConfigValue', () => {
|
|
|
217
219
|
const result = formatConfigValue('hello');
|
|
218
220
|
expect(result).toContain('hello');
|
|
219
221
|
});
|
|
222
|
+
|
|
223
|
+
it('undefined 显示为 (未设置)', () => {
|
|
224
|
+
const result = formatConfigValue(undefined);
|
|
225
|
+
expect(result).toContain('未设置');
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('null 显示为 (未设置)', () => {
|
|
229
|
+
const result = formatConfigValue(null);
|
|
230
|
+
expect(result).toContain('未设置');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('interactiveConfigEditor', () => {
|
|
235
|
+
it('选择配置项并返回新值(布尔类型)', async () => {
|
|
236
|
+
// 第一次 Select 选择配置项,第二次 Select 选择布尔值
|
|
237
|
+
mockSelectRun.mockResolvedValueOnce('enabled');
|
|
238
|
+
mockSelectRun.mockResolvedValueOnce('true');
|
|
239
|
+
|
|
240
|
+
const config = { enabled: false, name: 'test' };
|
|
241
|
+
const definitions = {
|
|
242
|
+
enabled: { description: '是否启用' },
|
|
243
|
+
name: { description: '名称' },
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
const result = await interactiveConfigEditor(config, definitions);
|
|
247
|
+
expect(result.key).toBe('enabled');
|
|
248
|
+
expect(result.newValue).toBe(true);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
it('选择配置项并返回新值(字符串类型)', async () => {
|
|
252
|
+
mockSelectRun.mockResolvedValueOnce('name');
|
|
253
|
+
mockInputRun.mockResolvedValueOnce('new-name');
|
|
254
|
+
|
|
255
|
+
const config = { enabled: false, name: 'test' };
|
|
256
|
+
const definitions = {
|
|
257
|
+
enabled: { description: '是否启用' },
|
|
258
|
+
name: { description: '名称' },
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const result = await interactiveConfigEditor(config, definitions);
|
|
262
|
+
expect(result.key).toBe('name');
|
|
263
|
+
expect(result.newValue).toBe('new-name');
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('支持自定义 selectPrompt', async () => {
|
|
267
|
+
mockSelectRun.mockResolvedValueOnce('name');
|
|
268
|
+
mockInputRun.mockResolvedValueOnce('value');
|
|
269
|
+
|
|
270
|
+
const config = { name: 'test' };
|
|
271
|
+
const definitions = { name: { description: '名称' } };
|
|
272
|
+
|
|
273
|
+
await interactiveConfigEditor(config, definitions, {
|
|
274
|
+
selectPrompt: '自定义提示',
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// 验证 Select 被调用(无需检查具体参数,因为 mock 不保留构造参数)
|
|
278
|
+
expect(mockSelectRun).toHaveBeenCalledTimes(1);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('有 allowedValues 的字符串配置项使用 Select 提示', async () => {
|
|
282
|
+
mockSelectRun.mockResolvedValueOnce('mode');
|
|
283
|
+
mockSelectRun.mockResolvedValueOnce('fast');
|
|
284
|
+
|
|
285
|
+
const config = { mode: 'normal' };
|
|
286
|
+
const definitions = {
|
|
287
|
+
mode: { description: '模式', allowedValues: ['normal', 'fast', 'slow'] as const },
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
const result = await interactiveConfigEditor(config, definitions);
|
|
291
|
+
expect(result.key).toBe('mode');
|
|
292
|
+
expect(result.newValue).toBe('fast');
|
|
293
|
+
// 两次 Select:一次选择配置项,一次选择枚举值
|
|
294
|
+
expect(mockSelectRun).toHaveBeenCalledTimes(2);
|
|
295
|
+
});
|
|
220
296
|
});
|
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
saveProjectConfig,
|
|
50
50
|
requireProjectConfig,
|
|
51
51
|
getMainWorkBranch,
|
|
52
|
+
getValidateRunCommand,
|
|
52
53
|
} from '../../../src/utils/project-config.js';
|
|
53
54
|
|
|
54
55
|
const mockedExistsSync = vi.mocked(existsSync);
|
|
@@ -134,3 +135,34 @@ describe('getMainWorkBranch', () => {
|
|
|
134
135
|
expect(getMainWorkBranch()).toBe('develop');
|
|
135
136
|
});
|
|
136
137
|
});
|
|
138
|
+
|
|
139
|
+
describe('getValidateRunCommand', () => {
|
|
140
|
+
it('配置中有 validateRunCommand 时返回对应值', () => {
|
|
141
|
+
mockedExistsSync.mockReturnValue(true);
|
|
142
|
+
mockedReadFileSync.mockReturnValue(JSON.stringify({
|
|
143
|
+
clawtMainWorkBranch: 'main',
|
|
144
|
+
validateRunCommand: 'npm test',
|
|
145
|
+
}));
|
|
146
|
+
expect(getValidateRunCommand()).toBe('npm test');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('配置中无 validateRunCommand 时返回 undefined', () => {
|
|
150
|
+
mockedExistsSync.mockReturnValue(true);
|
|
151
|
+
mockedReadFileSync.mockReturnValue(JSON.stringify({ clawtMainWorkBranch: 'main' }));
|
|
152
|
+
expect(getValidateRunCommand()).toBeUndefined();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('配置文件不存在时返回 undefined', () => {
|
|
156
|
+
mockedExistsSync.mockReturnValue(false);
|
|
157
|
+
expect(getValidateRunCommand()).toBeUndefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('validateRunCommand 为空字符串时返回 undefined', () => {
|
|
161
|
+
mockedExistsSync.mockReturnValue(true);
|
|
162
|
+
mockedReadFileSync.mockReturnValue(JSON.stringify({
|
|
163
|
+
clawtMainWorkBranch: 'main',
|
|
164
|
+
validateRunCommand: '',
|
|
165
|
+
}));
|
|
166
|
+
expect(getValidateRunCommand()).toBeUndefined();
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -106,10 +106,10 @@ describe('readSnapshotTreeHash', () => {
|
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
describe('writeSnapshot', () => {
|
|
109
|
-
it('
|
|
109
|
+
it('正确写入三个文件', () => {
|
|
110
110
|
writeSnapshot('proj', 'branch', 'tree123', 'head456');
|
|
111
111
|
expect(mockedEnsureDir).toHaveBeenCalledWith('/tmp/test-snapshots/proj');
|
|
112
|
-
expect(mockedWriteFileSync).toHaveBeenCalledTimes(
|
|
112
|
+
expect(mockedWriteFileSync).toHaveBeenCalledTimes(3);
|
|
113
113
|
expect(mockedWriteFileSync).toHaveBeenCalledWith(
|
|
114
114
|
'/tmp/test-snapshots/proj/branch.tree',
|
|
115
115
|
'tree123',
|
|
@@ -120,6 +120,11 @@ describe('writeSnapshot', () => {
|
|
|
120
120
|
'head456',
|
|
121
121
|
'utf-8',
|
|
122
122
|
);
|
|
123
|
+
expect(mockedWriteFileSync).toHaveBeenCalledWith(
|
|
124
|
+
'/tmp/test-snapshots/proj/branch.staged',
|
|
125
|
+
'',
|
|
126
|
+
'utf-8',
|
|
127
|
+
);
|
|
123
128
|
});
|
|
124
129
|
});
|
|
125
130
|
|
|
@@ -127,7 +132,7 @@ describe('removeSnapshot', () => {
|
|
|
127
132
|
it('删除存在的文件', () => {
|
|
128
133
|
mockedExistsSync.mockReturnValue(true);
|
|
129
134
|
removeSnapshot('proj', 'branch');
|
|
130
|
-
expect(mockedUnlinkSync).toHaveBeenCalledTimes(
|
|
135
|
+
expect(mockedUnlinkSync).toHaveBeenCalledTimes(3);
|
|
131
136
|
});
|
|
132
137
|
|
|
133
138
|
it('文件不存在时不抛错', () => {
|