@boyingliu01/xp-gate 0.10.1 → 0.10.3

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,289 @@
1
+ /**
2
+ * @test Gate 10 runGate10 orchestrator + main CLI
3
+ * @intent Verify that runGate10 combines tsc/pack/import checks correctly,
4
+ * and that main() parses CLI args and returns correct exit codes.
5
+ * @covers runGate10, main
6
+ */
7
+
8
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
9
+ import fs from 'fs/promises';
10
+ import path from 'path';
11
+ import os from 'os';
12
+
13
+ import { runGate10, main } from '../gate-10';
14
+
15
+ vi.setConfig({ testTimeout: 30000 });
16
+
17
+ describe('runGate10', () => {
18
+ let tmpDir: string;
19
+
20
+ beforeEach(async () => {
21
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gate10-orch-'));
22
+ const projectNodeModules = path.join(process.cwd(), 'node_modules');
23
+ const testNodeModules = path.join(tmpDir, 'node_modules');
24
+ try {
25
+ await fs.symlink(projectNodeModules, testNodeModules, 'dir');
26
+ } catch { /* may already exist */ }
27
+ });
28
+
29
+ afterEach(async () => {
30
+ await fs.rm(tmpDir, { recursive: true, force: true });
31
+ });
32
+
33
+ it('returns skip status when no package.json exists', async () => {
34
+ const result = await runGate10({
35
+ changedFiles: [],
36
+ projectRoot: tmpDir,
37
+ timeoutMs: 30000,
38
+ });
39
+
40
+ expect(result.exitCode).toBe(0);
41
+ expect(result.checks.tsc.status).toBe('skip');
42
+ expect(result.checks.pack.status).toBe('skip');
43
+ expect(result.checks.imports.status).toBe('pass');
44
+ expect(result.errors).toHaveLength(0);
45
+ });
46
+
47
+ it('returns skip when no changed TS/JS files and no tsconfig', async () => {
48
+ await fs.writeFile(
49
+ path.join(tmpDir, 'package.json'),
50
+ JSON.stringify({ name: 'test', version: '1.0.0' })
51
+ );
52
+
53
+ const result = await runGate10({
54
+ changedFiles: [],
55
+ projectRoot: tmpDir,
56
+ timeoutMs: 30000,
57
+ });
58
+
59
+ expect(result.exitCode).toBe(0);
60
+ expect(result.checks.tsc.status).toBe('skip');
61
+ expect(result.checks.imports.status).toBe('pass');
62
+ });
63
+
64
+ it('returns pass when all checks pass', async () => {
65
+ // Create a minimal valid project
66
+ await fs.writeFile(
67
+ path.join(tmpDir, 'tsconfig.json'),
68
+ JSON.stringify({
69
+ compilerOptions: { target: 'ES2020', module: 'commonjs', strict: true, noEmit: true },
70
+ include: ['*.ts'],
71
+ })
72
+ );
73
+ await fs.writeFile(path.join(tmpDir, 'valid.ts'), 'const x: number = 42;\n');
74
+ await fs.writeFile(
75
+ path.join(tmpDir, 'package.json'),
76
+ JSON.stringify({ name: 'test-pkg', version: '1.0.0' })
77
+ );
78
+
79
+ const validFile = path.join(tmpDir, 'valid.ts');
80
+ const result = await runGate10({
81
+ changedFiles: [validFile],
82
+ projectRoot: tmpDir,
83
+ timeoutMs: 60000,
84
+ });
85
+
86
+ expect(result.exitCode).toBe(0);
87
+ expect(result.status).toBe('pass');
88
+ expect(result.checks.tsc.status).toBe('pass');
89
+ expect(result.checks.imports.status).toBe('pass');
90
+ expect(result.errors).toHaveLength(0);
91
+ });
92
+
93
+ it('returns block (exitCode 1) when tsc check fails', async () => {
94
+ // Create a project with type errors
95
+ await fs.writeFile(
96
+ path.join(tmpDir, 'tsconfig.json'),
97
+ JSON.stringify({
98
+ compilerOptions: { target: 'ES2020', module: 'commonjs', strict: true, noEmit: true },
99
+ include: ['*.ts'],
100
+ })
101
+ );
102
+ await fs.writeFile(path.join(tmpDir, 'bad.ts'), 'const x: number = "string";\n');
103
+ await fs.writeFile(
104
+ path.join(tmpDir, 'package.json'),
105
+ JSON.stringify({ name: 'test-pkg', version: '1.0.0' })
106
+ );
107
+
108
+ const badFile = path.join(tmpDir, 'bad.ts');
109
+ const result = await runGate10({
110
+ changedFiles: [badFile],
111
+ projectRoot: tmpDir,
112
+ timeoutMs: 60000,
113
+ });
114
+
115
+ expect(result.exitCode).toBe(1);
116
+ expect(result.status).toBe('block');
117
+ expect(result.checks.tsc.status).toBe('fail');
118
+ expect(result.errors.length).toBeGreaterThan(0);
119
+ });
120
+
121
+ it('returns block when import check fails', async () => {
122
+ // Create a project with a broken import
123
+ const srcDir = path.join(tmpDir, 'src');
124
+ await fs.mkdir(srcDir);
125
+ await fs.writeFile(
126
+ path.join(tmpDir, 'tsconfig.json'),
127
+ JSON.stringify({
128
+ compilerOptions: { target: 'ES2020', module: 'commonjs', strict: true, noEmit: true },
129
+ include: ['src/*.ts'],
130
+ })
131
+ );
132
+ await fs.writeFile(
133
+ path.join(srcDir, 'bad-import.ts'),
134
+ `import { x } from './nonexistent';\n`
135
+ );
136
+ await fs.writeFile(
137
+ path.join(tmpDir, 'package.json'),
138
+ JSON.stringify({ name: 'test-pkg', version: '1.0.0' })
139
+ );
140
+
141
+ const badFile = path.join(srcDir, 'bad-import.ts');
142
+ const result = await runGate10({
143
+ changedFiles: [badFile],
144
+ projectRoot: tmpDir,
145
+ timeoutMs: 60000,
146
+ });
147
+
148
+ expect(result.exitCode).toBe(1);
149
+ expect(result.status).toBe('block');
150
+ expect(result.checks.imports.status).toBe('fail');
151
+ expect(result.checks.imports.violations.length).toBeGreaterThan(0);
152
+ });
153
+
154
+ it('includes warnings array in result', async () => {
155
+ const result = await runGate10({
156
+ changedFiles: [],
157
+ projectRoot: tmpDir,
158
+ timeoutMs: 30000,
159
+ });
160
+
161
+ expect(Array.isArray(result.warnings)).toBe(true);
162
+ });
163
+
164
+ it('includes errors array in result', async () => {
165
+ const result = await runGate10({
166
+ changedFiles: [],
167
+ projectRoot: tmpDir,
168
+ timeoutMs: 30000,
169
+ });
170
+
171
+ expect(Array.isArray(result.errors)).toBe(true);
172
+ });
173
+
174
+ it('runs all checks in parallel (total time < sum of individual times)', async () => {
175
+ // This is a soft check — we just verify the result has all three check results
176
+ await fs.writeFile(
177
+ path.join(tmpDir, 'tsconfig.json'),
178
+ JSON.stringify({
179
+ compilerOptions: { target: 'ES2020', module: 'commonjs', strict: true, noEmit: true },
180
+ include: ['*.ts'],
181
+ })
182
+ );
183
+ await fs.writeFile(path.join(tmpDir, 'valid.ts'), 'const x: number = 42;\n');
184
+ await fs.writeFile(
185
+ path.join(tmpDir, 'package.json'),
186
+ JSON.stringify({ name: 'test-pkg', version: '1.0.0' })
187
+ );
188
+
189
+ const result = await runGate10({
190
+ changedFiles: [path.join(tmpDir, 'valid.ts')],
191
+ projectRoot: tmpDir,
192
+ timeoutMs: 60000,
193
+ });
194
+
195
+ // All three checks should have been run
196
+ expect(result.checks.tsc).toBeDefined();
197
+ expect(result.checks.pack).toBeDefined();
198
+ expect(result.checks.imports).toBeDefined();
199
+ // All should have non-negative duration
200
+ expect(result.checks.tsc.durationMs).toBeGreaterThanOrEqual(0);
201
+ expect(result.checks.pack.durationMs).toBeGreaterThanOrEqual(0);
202
+ expect(result.checks.imports.durationMs).toBeGreaterThanOrEqual(0);
203
+ });
204
+ });
205
+
206
+ /**
207
+ * ─── main CLI ─────────────────────────────────────────────────────────────────
208
+ */
209
+ describe('main', () => {
210
+ let tmpDir: string;
211
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
212
+ let consoleSpy: any;
213
+
214
+ beforeEach(async () => {
215
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gate10-cli-'));
216
+ consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => { /* intentional no-op for spy */ });
217
+ const projectNodeModules = path.join(process.cwd(), 'node_modules');
218
+ const testNodeModules = path.join(tmpDir, 'node_modules');
219
+ try {
220
+ await fs.symlink(projectNodeModules, testNodeModules, 'dir');
221
+ } catch { /* may already exist */ }
222
+ });
223
+
224
+ afterEach(async () => {
225
+ consoleSpy.mockRestore();
226
+ await fs.rm(tmpDir, { recursive: true, force: true });
227
+ });
228
+
229
+ it('returns 0 when no changed files provided', async () => {
230
+ const exitCode = await main([
231
+ '--changed-files', '',
232
+ '--project-root', tmpDir,
233
+ ]);
234
+ expect(exitCode).toBe(0);
235
+ });
236
+
237
+ it('parses comma-separated changed files', async () => {
238
+ const file1 = path.join(tmpDir, 'a.ts');
239
+ const file2 = path.join(tmpDir, 'b.ts');
240
+ await fs.writeFile(file1, 'const a = 1;\n');
241
+ await fs.writeFile(file2, 'const b = 2;\n');
242
+
243
+ const exitCode = await main([
244
+ '--changed-files', `${file1},${file2}`,
245
+ '--project-root', tmpDir,
246
+ ]);
247
+
248
+ expect(exitCode).toBe(0);
249
+ });
250
+
251
+ it('returns 1 when a check fails', async () => {
252
+ // Create a project with type errors
253
+ await fs.writeFile(
254
+ path.join(tmpDir, 'tsconfig.json'),
255
+ JSON.stringify({
256
+ compilerOptions: { target: 'ES2020', module: 'commonjs', strict: true, noEmit: true },
257
+ include: ['*.ts'],
258
+ })
259
+ );
260
+ const badFile = path.join(tmpDir, 'bad.ts');
261
+ await fs.writeFile(badFile, 'const x: number = "string";\n');
262
+
263
+ const exitCode = await main([
264
+ '--changed-files', badFile,
265
+ '--project-root', tmpDir,
266
+ ]);
267
+
268
+ expect(exitCode).toBe(1);
269
+ });
270
+
271
+ it('prints formatted results table', async () => {
272
+ const exitCode = await main([
273
+ '--changed-files', '',
274
+ '--project-root', tmpDir,
275
+ ]);
276
+
277
+ // Check that console.log was called with table-like output
278
+ expect(exitCode).toBe(0);
279
+ const output = consoleSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n');
280
+ expect(output).toMatch(/tsc|pack|import/i);
281
+ });
282
+
283
+ it('returns 0 for --help flag', async () => {
284
+ const exitCode = await main(['--help']);
285
+ expect(exitCode).toBe(0);
286
+ const output = consoleSpy.mock.calls.map((c: unknown[]) => c[0]).join('\n');
287
+ expect(output).toMatch(/usage|help|changed-files/i);
288
+ });
289
+ });