dw-kit 1.1.0 → 1.2.1

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.
@@ -1,351 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * dw-kit Smoke Test
4
- * Run: node src/smoke-test.mjs
5
- *
6
- * Tests all CLI commands in an isolated temp directory.
7
- * Exit code 0 = all pass, 1 = failures found.
8
- */
9
-
10
- import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
11
- import { join, resolve } from 'node:path';
12
- import { execSync } from 'node:child_process';
13
- import { fileURLToPath } from 'node:url';
14
- import { copyFileSync } from 'node:fs';
15
-
16
- const __dirname = resolve(fileURLToPath(import.meta.url), '..');
17
- const DW_BIN = resolve(__dirname, '..', 'bin', 'dw.mjs');
18
- const TEMP_BASE = join(resolve(__dirname, '..'), '.smoke-test-tmp');
19
-
20
- let passed = 0;
21
- let failed = 0;
22
-
23
- function test(name, fn) {
24
- try {
25
- fn();
26
- console.log(` ✓ ${name}`);
27
- passed++;
28
- } catch (e) {
29
- console.log(` ✗ ${name}`);
30
- console.log(` ${e.message}`);
31
- failed++;
32
- }
33
- }
34
-
35
- function assert(condition, msg) {
36
- if (!condition) throw new Error(msg || 'Assertion failed');
37
- }
38
-
39
- function dw(args, cwd) {
40
- return execSync(`node "${DW_BIN}" ${args}`, {
41
- cwd,
42
- encoding: 'utf-8',
43
- timeout: 30000,
44
- env: { ...process.env, NO_COLOR: '1' },
45
- });
46
- }
47
-
48
- function freshDir(name) {
49
- const dir = join(TEMP_BASE, name);
50
- if (existsSync(dir)) rmSync(dir, { recursive: true });
51
- mkdirSync(dir, { recursive: true });
52
- execSync('git init', { cwd: dir, stdio: 'pipe' });
53
- return dir;
54
- }
55
-
56
- // ── Setup ────────────────────────────────────────────────────────────────────
57
- console.log();
58
- console.log('══════════════════════════════════════════');
59
- console.log(' dw-kit Smoke Tests');
60
- console.log('══════════════════════════════════════════');
61
- console.log();
62
-
63
- if (existsSync(TEMP_BASE)) rmSync(TEMP_BASE, { recursive: true });
64
- mkdirSync(TEMP_BASE, { recursive: true });
65
-
66
- // ── Test: CLI basics ─────────────────────────────────────────────────────────
67
- console.log('▶ CLI basics');
68
-
69
- test('--version returns semver', () => {
70
- const out = dw('--version', TEMP_BASE).trim();
71
- assert(/^\d+\.\d+\.\d+$/.test(out), `Got: ${out}`);
72
- });
73
-
74
- test('--help lists all commands', () => {
75
- const out = dw('--help', TEMP_BASE);
76
- for (const cmd of ['init', 'upgrade', 'validate', 'doctor', 'prompt', 'claude-vn-fix']) {
77
- assert(out.includes(cmd), `Missing command: ${cmd}`);
78
- }
79
- });
80
-
81
- test('init --help shows options', () => {
82
- const out = dw('init --help', TEMP_BASE);
83
- assert(out.includes('--preset'), 'Missing --preset');
84
- assert(out.includes('--adapter'), 'Missing --adapter');
85
- assert(out.includes('--silent'), 'Missing --silent');
86
- });
87
-
88
- // ── Test: dw init ────────────────────────────────────────────────────────────
89
- console.log();
90
- console.log('▶ dw init');
91
-
92
- test('init --preset solo-quick creates all files', () => {
93
- const dir = freshDir('init-solo');
94
- dw('init --preset solo-quick', dir);
95
-
96
- assert(existsSync(join(dir, '.dw', 'config', 'dw.config.yml')), 'Missing config');
97
- assert(existsSync(join(dir, '.dw', 'config', 'config.schema.json')), 'Missing schema');
98
- assert(existsSync(join(dir, '.dw', 'core', 'WORKFLOW.md')), 'Missing WORKFLOW.md');
99
- assert(existsSync(join(dir, '.dw', 'core', 'THINKING.md')), 'Missing THINKING.md');
100
- assert(existsSync(join(dir, '.dw', 'core', 'QUALITY.md')), 'Missing QUALITY.md');
101
- assert(existsSync(join(dir, '.dw', 'core', 'ROLES.md')), 'Missing ROLES.md');
102
- assert(existsSync(join(dir, '.claude', 'settings.json')), 'Missing settings.json');
103
- assert(existsSync(join(dir, 'CLAUDE.md')), 'Missing CLAUDE.md');
104
- assert(existsSync(join(dir, '.dw', 'tasks')), 'Missing .dw/tasks');
105
- const contextTemplate = readFileSync(join(dir, '.dw', 'core', 'templates', 'vi', 'task-context.md'), 'utf-8');
106
- assert(contextTemplate.includes('Depth Source: default (from config) | override (task-specific)'), 'Missing task depth override guidance in context template');
107
- });
108
-
109
- test('init --preset solo-quick sets correct depth', () => {
110
- const dir = join(TEMP_BASE, 'init-solo');
111
- const config = readFileSync(join(dir, '.dw', 'config', 'dw.config.yml'), 'utf-8');
112
- assert(config.includes('quick'), 'Depth not set to quick');
113
- });
114
-
115
- test('init --preset small-team creates correct roles', () => {
116
- const dir = freshDir('init-team');
117
- dw('init --preset small-team', dir);
118
- const config = readFileSync(join(dir, '.dw', 'config', 'dw.config.yml'), 'utf-8');
119
- assert(config.includes('techlead'), 'Missing techlead role');
120
- });
121
-
122
- test('init --preset enterprise creates all roles', () => {
123
- const dir = freshDir('init-enterprise');
124
- dw('init --preset enterprise', dir);
125
- const config = readFileSync(join(dir, '.dw', 'config', 'dw.config.yml'), 'utf-8');
126
- for (const role of ['dev', 'techlead', 'ba', 'qc', 'pm']) {
127
- assert(config.includes(role), `Missing role: ${role}`);
128
- }
129
- });
130
-
131
- test('init --silent with env vars works', () => {
132
- const dir = freshDir('init-silent');
133
- execSync(
134
- `node "${DW_BIN}" init --silent`,
135
- {
136
- cwd: dir,
137
- encoding: 'utf-8',
138
- timeout: 30000,
139
- env: {
140
- ...process.env,
141
- NO_COLOR: '1',
142
- DW_NAME: 'test-project',
143
- DW_DEPTH: 'thorough',
144
- DW_ROLES: 'dev,ba,qc',
145
- DW_LANG: 'en',
146
- },
147
- },
148
- );
149
- const config = readFileSync(join(dir, '.dw', 'config', 'dw.config.yml'), 'utf-8');
150
- assert(config.includes('test-project'), 'Project name not set');
151
- assert(config.includes('thorough'), 'Depth not thorough');
152
- assert(config.includes('en'), 'Language not en');
153
- });
154
-
155
- test('init --silent auto-adds required roles for depth', () => {
156
- const dir = freshDir('init-silent-role-normalize');
157
- execSync(
158
- `node "${DW_BIN}" init --silent`,
159
- {
160
- cwd: dir,
161
- encoding: 'utf-8',
162
- timeout: 30000,
163
- env: {
164
- ...process.env,
165
- NO_COLOR: '1',
166
- DW_NAME: 'role-normalize',
167
- DW_DEPTH: 'thorough',
168
- DW_ROLES: 'dev',
169
- DW_LANG: 'vi',
170
- },
171
- },
172
- );
173
- const config = readFileSync(join(dir, '.dw', 'config', 'dw.config.yml'), 'utf-8');
174
- for (const role of ['dev', 'techlead', 'ba', 'qc', 'pm']) {
175
- assert(config.includes(role), `Missing role after normalization: ${role}`);
176
- }
177
- });
178
-
179
- test('init --silent fails on invalid DW_DEPTH', () => {
180
- const dir = freshDir('init-silent-invalid-depth');
181
- try {
182
- execSync(
183
- `node "${DW_BIN}" init --silent`,
184
- {
185
- cwd: dir,
186
- encoding: 'utf-8',
187
- timeout: 30000,
188
- env: {
189
- ...process.env,
190
- NO_COLOR: '1',
191
- DW_NAME: 'bad-depth',
192
- DW_DEPTH: 'standrad',
193
- DW_ROLES: 'dev,techlead',
194
- DW_LANG: 'vi',
195
- },
196
- },
197
- );
198
- assert(false, 'Should have thrown');
199
- } catch (e) {
200
- assert(e.status === 1, 'Should exit with code 1');
201
- assert((e.stdout || '').includes('Invalid DW_DEPTH'), 'Should report invalid DW_DEPTH');
202
- }
203
- });
204
-
205
- test('init --adapter generic creates AGENT.md', () => {
206
- const dir = freshDir('init-generic');
207
- dw('init --preset solo-quick --adapter generic', dir);
208
- assert(existsSync(join(dir, 'AGENT.md')), 'Missing AGENT.md');
209
- assert(!existsSync(join(dir, '.claude')), '.claude/ should not exist for generic');
210
- });
211
-
212
- test('init --adapter cursor creates .cursor/rules/', () => {
213
- const dir = freshDir('init-cursor');
214
- dw('init --preset solo-quick --adapter cursor', dir);
215
- assert(existsSync(join(dir, '.cursor', 'rules', 'dw-workflow.mdc')), 'Missing Cursor rules');
216
- assert(existsSync(join(dir, 'AGENT.md')), 'Missing AGENT.md');
217
- });
218
-
219
- test('init warns on reinitialize', () => {
220
- const dir = join(TEMP_BASE, 'init-solo');
221
- const out = dw('init --preset solo-quick', dir);
222
- assert(out.includes('already initialized'), 'Missing reinitialize warning');
223
- });
224
-
225
- // ── Test: dw validate ────────────────────────────────────────────────────────
226
- console.log();
227
- console.log('▶ dw validate');
228
-
229
- test('validate passes on valid config', () => {
230
- const dir = join(TEMP_BASE, 'init-solo');
231
- const out = dw('validate', dir);
232
- assert(out.includes('valid'), 'Should report valid');
233
- });
234
-
235
- test('validate fails on missing config', () => {
236
- const dir = freshDir('validate-missing');
237
- try {
238
- dw('validate', dir);
239
- assert(false, 'Should have thrown');
240
- } catch (e) {
241
- assert(e.status === 1, 'Should exit with code 1');
242
- }
243
- });
244
-
245
- // ── Test: dw doctor ──────────────────────────────────────────────────────────
246
- console.log();
247
- console.log('▶ dw doctor');
248
-
249
- test('doctor passes on fully initialized project', () => {
250
- const dir = join(TEMP_BASE, 'init-solo');
251
- const out = dw('doctor', dir);
252
- assert(out.includes('WORKFLOW.md'), 'Should check core files');
253
- assert(out.includes('dw.config.yml'), 'Should check config');
254
- });
255
-
256
- test('doctor reports issues on empty project', () => {
257
- const dir = freshDir('doctor-empty');
258
- try {
259
- dw('doctor', dir);
260
- assert(false, 'Should have thrown');
261
- } catch (e) {
262
- assert(e.status === 1, 'Should exit with code 1');
263
- }
264
- });
265
-
266
- // ── Test: dw prompt ──────────────────────────────────────────────────────────
267
- console.log();
268
- console.log('▶ dw prompt');
269
-
270
- test('prompt --text outputs structured result', () => {
271
- const out = dw('prompt --text "fix login redirect after OAuth in auth middleware"', TEMP_BASE);
272
- assert(out.includes('fix login redirect'), 'Should include description in output');
273
- assert(!out.includes('Description seems short'), 'Long description should skip wizard');
274
- });
275
-
276
- test('prompt --text with short input expands without error', () => {
277
- const out = dw('prompt --text "fix login"', TEMP_BASE);
278
- assert(out.includes('fix login'), 'Should include description in output');
279
- });
280
-
281
- test('prompt --text empty string exits with error', () => {
282
- try {
283
- dw('prompt --text ""', TEMP_BASE);
284
- assert(false, 'Should have thrown');
285
- } catch (e) {
286
- assert(e.status === 1, 'Should exit with code 1');
287
- }
288
- });
289
-
290
- test('prompt --help shows options', () => {
291
- const out = dw('prompt --help', TEMP_BASE);
292
- assert(out.includes('--text'), 'Missing --text option');
293
- });
294
-
295
- // ── Test: dw claude-vn-fix (fixture patch) ───────────────────────────────────
296
- console.log();
297
- console.log('▶ dw claude-vn-fix');
298
-
299
- test('claude-vn-fix patches known bug fixture', () => {
300
- const dir = freshDir('claude-vn-fix-fixture');
301
- const fixtureSrc = resolve(__dirname, '__fixtures__', 'claude-cli-bug-snippet.js');
302
- const target = join(dir, 'cli.js');
303
- copyFileSync(fixtureSrc, target);
304
-
305
- const out = dw(`claude-vn-fix --path "${target}"`, dir);
306
- assert(out.includes('Patch applied') || out.includes('Already patched'), 'Should apply patch');
307
- const patched = readFileSync(target, 'utf-8');
308
- assert(patched.includes('dw-kit Vietnamese IME fix'), 'Patch marker missing in output file');
309
- });
310
-
311
- // ── Test: dw upgrade ─────────────────────────────────────────────────────────
312
- console.log();
313
- console.log('▶ dw upgrade');
314
-
315
- test('upgrade --check on fresh init reports up to date', () => {
316
- const dir = join(TEMP_BASE, 'init-solo');
317
- const out = dw('upgrade --check', dir);
318
- assert(out.includes('up to date'), 'Should be up to date');
319
- });
320
-
321
- test('upgrade --dry-run shows no changes on fresh init', () => {
322
- const dir = join(TEMP_BASE, 'init-solo');
323
- const out = dw('upgrade --dry-run', dir);
324
- assert(out.includes('DRY RUN'), 'Should say dry run');
325
- });
326
-
327
- test('upgrade fails on project without config', () => {
328
- const dir = freshDir('upgrade-empty');
329
- try {
330
- dw('upgrade', dir);
331
- assert(false, 'Should have thrown');
332
- } catch (e) {
333
- assert(e.status === 1, 'Should exit with code 1');
334
- }
335
- });
336
-
337
- // ── Cleanup ──────────────────────────────────────────────────────────────────
338
- rmSync(TEMP_BASE, { recursive: true });
339
-
340
- // ── Results ──────────────────────────────────────────────────────────────────
341
- console.log();
342
- console.log('══════════════════════════════════════════');
343
- if (failed === 0) {
344
- console.log(` All ${passed} tests passed`);
345
- } else {
346
- console.log(` ${passed} passed, ${failed} failed`);
347
- }
348
- console.log('══════════════════════════════════════════');
349
- console.log();
350
-
351
- process.exit(failed > 0 ? 1 : 0);