@marcusrbrown/infra 0.13.1 → 0.13.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.
Files changed (108) hide show
  1. package/dist/cli.js +11965 -0
  2. package/package.json +7 -3
  3. package/src/__snapshots__/cli.test.ts.snap +0 -219
  4. package/src/__test__/mcp-ctx-fixture.test.ts +0 -108
  5. package/src/__test__/mcp-ctx-fixture.ts +0 -104
  6. package/src/cli.test.ts +0 -66
  7. package/src/cli.ts +0 -43
  8. package/src/commands/cliproxy/config.test.ts +0 -475
  9. package/src/commands/cliproxy/config.ts +0 -238
  10. package/src/commands/cliproxy/deploy.test.ts +0 -181
  11. package/src/commands/cliproxy/deploy.ts +0 -157
  12. package/src/commands/cliproxy/host.test.ts +0 -73
  13. package/src/commands/cliproxy/host.ts +0 -31
  14. package/src/commands/cliproxy/index.ts +0 -21
  15. package/src/commands/cliproxy/keys.test.ts +0 -326
  16. package/src/commands/cliproxy/keys.ts +0 -201
  17. package/src/commands/cliproxy/login.test.ts +0 -415
  18. package/src/commands/cliproxy/login.ts +0 -131
  19. package/src/commands/cliproxy/models.test.ts +0 -592
  20. package/src/commands/cliproxy/models.ts +0 -237
  21. package/src/commands/cliproxy/open.test.ts +0 -132
  22. package/src/commands/cliproxy/open.ts +0 -78
  23. package/src/commands/cliproxy/setup/gh.test.ts +0 -218
  24. package/src/commands/cliproxy/setup/gh.ts +0 -250
  25. package/src/commands/cliproxy/setup/preview.test.ts +0 -159
  26. package/src/commands/cliproxy/setup/preview.ts +0 -41
  27. package/src/commands/cliproxy/setup/prompts.test.ts +0 -58
  28. package/src/commands/cliproxy/setup/prompts.ts +0 -99
  29. package/src/commands/cliproxy/setup/providers.test.ts +0 -245
  30. package/src/commands/cliproxy/setup/providers.ts +0 -136
  31. package/src/commands/cliproxy/setup/smoke-test.test.ts +0 -821
  32. package/src/commands/cliproxy/setup/smoke-test.ts +0 -223
  33. package/src/commands/cliproxy/setup/templates.test.ts +0 -357
  34. package/src/commands/cliproxy/setup/templates.ts +0 -145
  35. package/src/commands/cliproxy/setup/validation.test.ts +0 -567
  36. package/src/commands/cliproxy/setup/validation.ts +0 -209
  37. package/src/commands/cliproxy/setup/workflow-analyzer.test.ts +0 -363
  38. package/src/commands/cliproxy/setup/workflow-analyzer.ts +0 -136
  39. package/src/commands/cliproxy/setup.test.ts +0 -2366
  40. package/src/commands/cliproxy/setup.ts +0 -809
  41. package/src/commands/cliproxy/shared.test.ts +0 -118
  42. package/src/commands/cliproxy/shared.ts +0 -84
  43. package/src/commands/cliproxy/status.test.ts +0 -1157
  44. package/src/commands/cliproxy/status.ts +0 -597
  45. package/src/commands/dashboard/deploy.test.ts +0 -215
  46. package/src/commands/dashboard/deploy.ts +0 -135
  47. package/src/commands/dashboard/host.test.ts +0 -79
  48. package/src/commands/dashboard/host.ts +0 -33
  49. package/src/commands/dashboard/index.ts +0 -13
  50. package/src/commands/dashboard/logs.test.ts +0 -317
  51. package/src/commands/dashboard/logs.ts +0 -180
  52. package/src/commands/dashboard/status.test.ts +0 -708
  53. package/src/commands/dashboard/status.ts +0 -293
  54. package/src/commands/gateway/backup.test.ts +0 -426
  55. package/src/commands/gateway/backup.ts +0 -205
  56. package/src/commands/gateway/deploy.test.ts +0 -491
  57. package/src/commands/gateway/deploy.ts +0 -173
  58. package/src/commands/gateway/host.test.ts +0 -89
  59. package/src/commands/gateway/host.ts +0 -33
  60. package/src/commands/gateway/index.ts +0 -17
  61. package/src/commands/gateway/logs.test.ts +0 -263
  62. package/src/commands/gateway/logs.ts +0 -160
  63. package/src/commands/gateway/restore.test.ts +0 -650
  64. package/src/commands/gateway/restore.ts +0 -308
  65. package/src/commands/gateway/status.test.ts +0 -755
  66. package/src/commands/gateway/status.ts +0 -255
  67. package/src/commands/keeweb/deploy.test.ts +0 -177
  68. package/src/commands/keeweb/deploy.ts +0 -191
  69. package/src/commands/keeweb/index.ts +0 -11
  70. package/src/commands/keeweb/open.test.ts +0 -54
  71. package/src/commands/keeweb/open.ts +0 -30
  72. package/src/commands/keeweb/status.test.ts +0 -509
  73. package/src/commands/keeweb/status.ts +0 -359
  74. package/src/commands/mcp.test.ts +0 -260
  75. package/src/commands/mcp.ts +0 -58
  76. package/src/commands/status.test.ts +0 -222
  77. package/src/commands/status.ts +0 -161
  78. package/src/commands/umami/deploy.test.ts +0 -202
  79. package/src/commands/umami/deploy.ts +0 -132
  80. package/src/commands/umami/host.test.ts +0 -79
  81. package/src/commands/umami/host.ts +0 -33
  82. package/src/commands/umami/index.ts +0 -13
  83. package/src/commands/umami/logs.test.ts +0 -191
  84. package/src/commands/umami/logs.ts +0 -163
  85. package/src/commands/umami/status.test.ts +0 -633
  86. package/src/commands/umami/status.ts +0 -285
  87. package/src/commands/vpn/client.test.ts +0 -949
  88. package/src/commands/vpn/client.ts +0 -467
  89. package/src/commands/vpn/deploy.test.ts +0 -208
  90. package/src/commands/vpn/deploy.ts +0 -143
  91. package/src/commands/vpn/host.test.ts +0 -89
  92. package/src/commands/vpn/host.ts +0 -36
  93. package/src/commands/vpn/index.ts +0 -15
  94. package/src/commands/vpn/logs.test.ts +0 -234
  95. package/src/commands/vpn/logs.ts +0 -136
  96. package/src/commands/vpn/peers.test.ts +0 -478
  97. package/src/commands/vpn/shared.test.ts +0 -454
  98. package/src/commands/vpn/shared.ts +0 -170
  99. package/src/commands/vpn/status.test.ts +0 -142
  100. package/src/commands/vpn/status.ts +0 -78
  101. package/src/conventions.test.ts +0 -1180
  102. package/src/lib/action-ctx.ts +0 -25
  103. package/src/lib/known-hosts.test.ts +0 -192
  104. package/src/lib/known-hosts.ts +0 -76
  105. package/src/lib/redact.ts +0 -21
  106. package/src/lib/ssh-identity.test.ts +0 -134
  107. package/src/lib/ssh-identity.ts +0 -76
  108. /package/{src/resources/known_hosts → dist/known_hosts-tk724cp0.} +0 -0
@@ -1,821 +0,0 @@
1
- /// <reference types="bun" />
2
-
3
- import {afterEach, describe, expect, it, spyOn} from 'bun:test'
4
-
5
- import {runSmokeTest} from './smoke-test'
6
-
7
- // Helper to build a fake Bun.spawn child process result
8
- type MockSpawnResult = ReturnType<typeof Bun.spawn>
9
-
10
- function makeSmokeChild(stdout: string, stderr: string, exitCode: number): MockSpawnResult {
11
- return {
12
- stdout: new Blob([stdout]).stream(),
13
- stderr: new Blob([stderr]).stream(),
14
- exited: Promise.resolve(exitCode),
15
- } as unknown as MockSpawnResult
16
- }
17
-
18
- // Helper to build a gh run list JSON response
19
- function makeSmokeRunList(
20
- runs: {databaseId: number; status: string; conclusion: string | null; url: string; createdAt: string}[],
21
- ): string {
22
- return JSON.stringify(runs)
23
- }
24
-
25
- describe('smoke test runner', () => {
26
- const REPO = 'owner/test-repo'
27
- const MODEL = 'anthropic/claude-sonnet-4-6'
28
- const RUN_URL = 'https://github.com/owner/test-repo/actions/runs/105'
29
-
30
- let spawnSpy: ReturnType<typeof spyOn>
31
-
32
- afterEach(() => {
33
- spawnSpy?.mockRestore()
34
- })
35
-
36
- it('happy path — pass with log grep finding "ack"', async () => {
37
- // Sequence of Bun.spawn calls:
38
- // 1. gh run list (baseline) → [{databaseId: 100, ...}]
39
- // 2. gh workflow run (trigger) → exit 0
40
- // 3. gh run list (poll 1) → [{databaseId: 105, status: completed, conclusion: success}, {databaseId: 100}]
41
- // 4. gh run view --log → text containing "ack"
42
- const triggerTime = new Date('2026-05-25T10:00:00Z')
43
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
44
-
45
- let callIndex = 0
46
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
47
- callIndex++
48
- if (callIndex === 1) {
49
- // baseline gh run list
50
- return makeSmokeChild(
51
- makeSmokeRunList([
52
- {
53
- databaseId: 100,
54
- status: 'completed',
55
- conclusion: 'success',
56
- url: 'https://github.com/owner/test-repo/actions/runs/100',
57
- createdAt: '2026-05-25T09:00:00Z',
58
- },
59
- ]),
60
- '',
61
- 0,
62
- )
63
- }
64
- if (callIndex === 2) {
65
- // gh workflow run trigger
66
- return makeSmokeChild('', '', 0)
67
- }
68
- if (callIndex === 3) {
69
- // poll 1 — new run visible
70
- return makeSmokeChild(
71
- makeSmokeRunList([
72
- {databaseId: 105, status: 'completed', conclusion: 'success', url: RUN_URL, createdAt},
73
- {
74
- databaseId: 100,
75
- status: 'completed',
76
- conclusion: 'success',
77
- url: 'https://github.com/owner/test-repo/actions/runs/100',
78
- createdAt: '2026-05-25T09:00:00Z',
79
- },
80
- ]),
81
- '',
82
- 0,
83
- )
84
- }
85
- if (callIndex === 4) {
86
- // gh run view --log
87
- return makeSmokeChild('Step output: reply with exactly: ack\nack', '', 0)
88
- }
89
- return makeSmokeChild('', '', 0)
90
- })
91
-
92
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
93
-
94
- expect(result.kind).toBe('pass')
95
- expect(result.message).toContain('passed')
96
- expect(result.runUrl).toBe(RUN_URL)
97
- })
98
-
99
- it('happy path — pass without log grep (log fetch fails, still pass)', async () => {
100
- const triggerTime = new Date('2026-05-25T10:00:00Z')
101
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
102
-
103
- let callIndex = 0
104
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
105
- callIndex++
106
- if (callIndex === 1) {
107
- return makeSmokeChild(
108
- makeSmokeRunList([
109
- {
110
- databaseId: 100,
111
- status: 'completed',
112
- conclusion: 'success',
113
- url: 'https://github.com/owner/test-repo/actions/runs/100',
114
- createdAt: '2026-05-25T09:00:00Z',
115
- },
116
- ]),
117
- '',
118
- 0,
119
- )
120
- }
121
- if (callIndex === 2) {
122
- return makeSmokeChild('', '', 0)
123
- }
124
- if (callIndex === 3) {
125
- return makeSmokeChild(
126
- makeSmokeRunList([{databaseId: 105, status: 'completed', conclusion: 'success', url: RUN_URL, createdAt}]),
127
- '',
128
- 0,
129
- )
130
- }
131
- if (callIndex === 4) {
132
- // log fetch fails
133
- return makeSmokeChild('', 'error fetching logs', 1)
134
- }
135
- return makeSmokeChild('', '', 0)
136
- })
137
-
138
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
139
-
140
- expect(result.kind).toBe('pass')
141
- expect(result.runUrl).toBe(RUN_URL)
142
- })
143
-
144
- it('error path — fail: run completed with conclusion=failure', async () => {
145
- const triggerTime = new Date('2026-05-25T10:00:00Z')
146
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
147
-
148
- let callIndex = 0
149
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
150
- callIndex++
151
- if (callIndex === 1) {
152
- return makeSmokeChild(
153
- makeSmokeRunList([
154
- {
155
- databaseId: 100,
156
- status: 'completed',
157
- conclusion: 'success',
158
- url: 'https://github.com/owner/test-repo/actions/runs/100',
159
- createdAt: '2026-05-25T09:00:00Z',
160
- },
161
- ]),
162
- '',
163
- 0,
164
- )
165
- }
166
- if (callIndex === 2) {
167
- return makeSmokeChild('', '', 0)
168
- }
169
- if (callIndex === 3) {
170
- return makeSmokeChild(
171
- makeSmokeRunList([{databaseId: 105, status: 'completed', conclusion: 'failure', url: RUN_URL, createdAt}]),
172
- '',
173
- 0,
174
- )
175
- }
176
- return makeSmokeChild('', '', 0)
177
- })
178
-
179
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
180
-
181
- expect(result.kind).toBe('fail')
182
- expect(result.message).toContain('failure')
183
- expect(result.runUrl).toBe(RUN_URL)
184
- })
185
-
186
- it('edge case — env approval: status=waiting returns unverified with approval message', async () => {
187
- const triggerTime = new Date('2026-05-25T10:00:00Z')
188
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
189
-
190
- let callIndex = 0
191
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
192
- callIndex++
193
- if (callIndex === 1) {
194
- return makeSmokeChild(
195
- makeSmokeRunList([
196
- {
197
- databaseId: 100,
198
- status: 'completed',
199
- conclusion: 'success',
200
- url: 'https://github.com/owner/test-repo/actions/runs/100',
201
- createdAt: '2026-05-25T09:00:00Z',
202
- },
203
- ]),
204
- '',
205
- 0,
206
- )
207
- }
208
- if (callIndex === 2) {
209
- return makeSmokeChild('', '', 0)
210
- }
211
- // poll — status=waiting
212
- return makeSmokeChild(
213
- makeSmokeRunList([
214
- {databaseId: 105, status: 'waiting', conclusion: 'action_required', url: RUN_URL, createdAt},
215
- ]),
216
- '',
217
- 0,
218
- )
219
- })
220
-
221
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
222
-
223
- expect(result.kind).toBe('unverified')
224
- expect(result.message).toContain('approval')
225
- expect(result.runUrl).toBe(RUN_URL)
226
- })
227
-
228
- // dead env-approval branch removed — status=pending with approval-like conclusion
229
- // does NOT trigger the unverified gate (the old dead branch is gone).
230
- it('status=pending with conclusion=approval_pending does NOT return unverified from env-approval gate', async () => {
231
- // The old code had: status === 'waiting' || (status === 'pending' && /approval/i.test(conclusion ?? ''))
232
- // The second OR branch was dead: when status=pending, gh returns conclusion=null, so /approval/i.test('') = false.
233
- // After simplification, only status=waiting triggers the env-approval gate.
234
- // This test asserts that status=pending + conclusion='approval_pending' does NOT hit the gate.
235
- const triggerTime = new Date('2026-05-25T10:00:00Z')
236
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
237
-
238
- let callIndex = 0
239
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
240
- callIndex++
241
- if (callIndex === 1) {
242
- return makeSmokeChild(
243
- makeSmokeRunList([
244
- {
245
- databaseId: 100,
246
- status: 'completed',
247
- conclusion: 'success',
248
- url: 'https://github.com/owner/test-repo/actions/runs/100',
249
- createdAt: '2026-05-25T09:00:00Z',
250
- },
251
- ]),
252
- '',
253
- 0,
254
- )
255
- }
256
- if (callIndex === 2) {
257
- // trigger succeeds
258
- return makeSmokeChild('', '', 0)
259
- }
260
- // poll — status=pending, conclusion='approval_pending' (the formerly dead branch scenario)
261
- // After simplification, this should NOT return unverified from the env-approval gate.
262
- // Instead it falls through to "still in progress" and continues polling.
263
- // All subsequent polls also return pending → eventually exhausts → unverified with timeout.
264
- return makeSmokeChild(
265
- makeSmokeRunList([
266
- {databaseId: 105, status: 'pending', conclusion: 'approval_pending', url: RUN_URL, createdAt},
267
- ]),
268
- '',
269
- 0,
270
- )
271
- })
272
-
273
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
274
-
275
- // Must NOT return unverified with "environment approval" message (that's the dead branch).
276
- // It should return unverified with timeout message (exhausted all polls).
277
- expect(result.kind).toBe('unverified')
278
- expect(result.message).not.toContain('environment approval')
279
- // The run was visible, so it should reference the run URL (timeout path)
280
- expect(result.runUrl).toBe(RUN_URL)
281
- })
282
-
283
- it('edge case — timeout: all polls return queued → unverified with timeout message', async () => {
284
- const triggerTime = new Date('2026-05-25T10:00:00Z')
285
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
286
-
287
- let callIndex = 0
288
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
289
- callIndex++
290
- if (callIndex === 1) {
291
- return makeSmokeChild(
292
- makeSmokeRunList([
293
- {
294
- databaseId: 100,
295
- status: 'completed',
296
- conclusion: 'success',
297
- url: 'https://github.com/owner/test-repo/actions/runs/100',
298
- createdAt: '2026-05-25T09:00:00Z',
299
- },
300
- ]),
301
- '',
302
- 0,
303
- )
304
- }
305
- if (callIndex === 2) {
306
- return makeSmokeChild('', '', 0)
307
- }
308
- // All polls return queued
309
- return makeSmokeChild(
310
- makeSmokeRunList([{databaseId: 105, status: 'queued', conclusion: '', url: RUN_URL, createdAt}]),
311
- '',
312
- 0,
313
- )
314
- })
315
-
316
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
317
-
318
- expect(result.kind).toBe('unverified')
319
- expect(result.message).toContain('5 minutes')
320
- expect(result.runUrl).toBe(RUN_URL)
321
- })
322
-
323
- it('edge case — trigger fails: gh workflow run exits non-zero → unverified with redacted stderr', async () => {
324
- let callIndex = 0
325
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
326
- callIndex++
327
- if (callIndex === 1) {
328
- // baseline
329
- return makeSmokeChild('[]', '', 0)
330
- }
331
- if (callIndex === 2) {
332
- // trigger fails
333
- return makeSmokeChild('', 'gh: authentication required — run gh auth login first', 1)
334
- }
335
- return makeSmokeChild('', '', 0)
336
- })
337
-
338
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0})
339
-
340
- expect(result.kind).toBe('unverified')
341
- expect(result.message).toContain('gh workflow run failed')
342
- // stderr is included but truncated to 200 chars
343
- expect(result.message).toContain('authentication required')
344
- })
345
-
346
- it('security hygiene — returned messages do not contain the bearer token / key value', async () => {
347
- const SECRET_KEY = 'sk-super-secret-bearer-token-12345'
348
- const triggerTime = new Date('2026-05-25T10:00:00Z')
349
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
350
-
351
- let callIndex = 0
352
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
353
- callIndex++
354
- if (callIndex === 1) {
355
- return makeSmokeChild('[]', '', 0)
356
- }
357
- if (callIndex === 2) {
358
- return makeSmokeChild('', '', 0)
359
- }
360
- return makeSmokeChild(
361
- makeSmokeRunList([{databaseId: 1, status: 'completed', conclusion: 'failure', url: RUN_URL, createdAt}]),
362
- '',
363
- 0,
364
- )
365
- })
366
-
367
- // runSmokeTest doesn't take a key — it uses gh CLI which handles auth via GH_TOKEN env
368
- // This test verifies the function signature doesn't accept or leak a key
369
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
370
-
371
- // The result message should not contain any secret-looking value
372
- expect(result.message).not.toContain(SECRET_KEY)
373
- expect(result.message).not.toContain('Bearer')
374
- expect(result.message).not.toContain('sk-')
375
- })
376
-
377
- it('race safety — picks highest databaseId above baseline (our run, not concurrent run)', async () => {
378
- // Baseline=100, trigger succeeds.
379
- // Poll 1 returns [id=102 (ours, success), id=101 (other contributor, failure), id=100 (baseline)]
380
- // Function must pick 102 (highest above baseline) and report pass.
381
- const triggerTime = new Date('2026-05-25T10:00:00Z')
382
- const createdAt102 = new Date(triggerTime.getTime() + 10000).toISOString()
383
- const createdAt101 = new Date(triggerTime.getTime() + 3000).toISOString()
384
-
385
- let callIndex = 0
386
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
387
- callIndex++
388
- if (callIndex === 1) {
389
- return makeSmokeChild(
390
- makeSmokeRunList([
391
- {
392
- databaseId: 100,
393
- status: 'completed',
394
- conclusion: 'success',
395
- url: 'https://github.com/owner/test-repo/actions/runs/100',
396
- createdAt: '2026-05-25T09:00:00Z',
397
- },
398
- ]),
399
- '',
400
- 0,
401
- )
402
- }
403
- if (callIndex === 2) {
404
- return makeSmokeChild('', '', 0)
405
- }
406
- if (callIndex === 3) {
407
- // Poll: our run (102) and concurrent run (101) both visible
408
- return makeSmokeChild(
409
- makeSmokeRunList([
410
- {
411
- databaseId: 102,
412
- status: 'completed',
413
- conclusion: 'success',
414
- url: 'https://github.com/owner/test-repo/actions/runs/102',
415
- createdAt: createdAt102,
416
- },
417
- {
418
- databaseId: 101,
419
- status: 'completed',
420
- conclusion: 'failure',
421
- url: 'https://github.com/owner/test-repo/actions/runs/101',
422
- createdAt: createdAt101,
423
- },
424
- {
425
- databaseId: 100,
426
- status: 'completed',
427
- conclusion: 'success',
428
- url: 'https://github.com/owner/test-repo/actions/runs/100',
429
- createdAt: '2026-05-25T09:00:00Z',
430
- },
431
- ]),
432
- '',
433
- 0,
434
- )
435
- }
436
- // log fetch
437
- return makeSmokeChild('ack', '', 0)
438
- })
439
-
440
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
441
-
442
- // Must pick run 102 (highest above baseline=100), not 101
443
- expect(result.kind).toBe('pass')
444
- expect(result.runUrl).toBe('https://github.com/owner/test-repo/actions/runs/102')
445
- })
446
-
447
- it('race safety — known edge case: only concurrent run visible, picks it (best-effort heuristic)', async () => {
448
- // Baseline=100, trigger succeeds.
449
- // Poll 1: only id=101 (other contributor's run) visible, ours not yet.
450
- // Function picks 101 (highest above baseline) — this is a known misattribution edge case.
451
- const triggerTime = new Date('2026-05-25T10:00:00Z')
452
- const createdAt101 = new Date(triggerTime.getTime() + 3000).toISOString()
453
-
454
- let callIndex = 0
455
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
456
- callIndex++
457
- if (callIndex === 1) {
458
- return makeSmokeChild(
459
- makeSmokeRunList([
460
- {
461
- databaseId: 100,
462
- status: 'completed',
463
- conclusion: 'success',
464
- url: 'https://github.com/owner/test-repo/actions/runs/100',
465
- createdAt: '2026-05-25T09:00:00Z',
466
- },
467
- ]),
468
- '',
469
- 0,
470
- )
471
- }
472
- if (callIndex === 2) {
473
- return makeSmokeChild('', '', 0)
474
- }
475
- // All polls: only 101 visible (ours never appears)
476
- return makeSmokeChild(
477
- makeSmokeRunList([
478
- {
479
- databaseId: 101,
480
- status: 'completed',
481
- conclusion: 'failure',
482
- url: 'https://github.com/owner/test-repo/actions/runs/101',
483
- createdAt: createdAt101,
484
- },
485
- {
486
- databaseId: 100,
487
- status: 'completed',
488
- conclusion: 'success',
489
- url: 'https://github.com/owner/test-repo/actions/runs/100',
490
- createdAt: '2026-05-25T09:00:00Z',
491
- },
492
- ]),
493
- '',
494
- 0,
495
- )
496
- })
497
-
498
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
499
-
500
- // Picks 101 (best-effort heuristic — known misattribution edge case)
501
- expect(result.runUrl).toBe('https://github.com/owner/test-repo/actions/runs/101')
502
- })
503
-
504
- it('edge case — no prior runs: baselineId=null, uses createdAt heuristic', async () => {
505
- const triggerTime = new Date('2026-05-25T10:00:00Z')
506
- // Run created AFTER trigger time
507
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
508
-
509
- let callIndex = 0
510
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
511
- callIndex++
512
- if (callIndex === 1) {
513
- // baseline: no prior runs
514
- return makeSmokeChild('[]', '', 0)
515
- }
516
- if (callIndex === 2) {
517
- return makeSmokeChild('', '', 0)
518
- }
519
- if (callIndex === 3) {
520
- return makeSmokeChild(
521
- makeSmokeRunList([{databaseId: 1, status: 'completed', conclusion: 'success', url: RUN_URL, createdAt}]),
522
- '',
523
- 0,
524
- )
525
- }
526
- // log fetch
527
- return makeSmokeChild('ack', '', 0)
528
- })
529
-
530
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
531
-
532
- expect(result.kind).toBe('pass')
533
- expect(result.runUrl).toBe(RUN_URL)
534
- })
535
-
536
- it('edge case — baseline list call fails: still triggers, uses createdAt heuristic', async () => {
537
- const triggerTime = new Date('2026-05-25T10:00:00Z')
538
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
539
-
540
- let callIndex = 0
541
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
542
- callIndex++
543
- if (callIndex === 1) {
544
- // baseline fails
545
- return makeSmokeChild('', 'gh: network error', 1)
546
- }
547
- if (callIndex === 2) {
548
- return makeSmokeChild('', '', 0)
549
- }
550
- if (callIndex === 3) {
551
- return makeSmokeChild(
552
- makeSmokeRunList([{databaseId: 1, status: 'completed', conclusion: 'success', url: RUN_URL, createdAt}]),
553
- '',
554
- 0,
555
- )
556
- }
557
- return makeSmokeChild('ack', '', 0)
558
- })
559
-
560
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
561
-
562
- expect(result.kind).toBe('pass')
563
- })
564
-
565
- it('edge case — trigger never produces visible run: unverified with repo URL hint', async () => {
566
- let callIndex = 0
567
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
568
- callIndex++
569
- if (callIndex === 1) {
570
- return makeSmokeChild('[]', '', 0)
571
- }
572
- if (callIndex === 2) {
573
- return makeSmokeChild('', '', 0)
574
- }
575
- // All polls: no new runs visible
576
- return makeSmokeChild('[]', '', 0)
577
- })
578
-
579
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0})
580
-
581
- expect(result.kind).toBe('unverified')
582
- expect(result.message).toContain('not yet visible')
583
- })
584
-
585
- // Race-attribution documentation test.
586
- // This test documents a KNOWN LIMITATION of the current heuristic.
587
- it('race attribution: concurrent run with createdAt < triggerTime but databaseId > baseline is picked (known limitation)', async () => {
588
- // Scenario: baselineId = 100. Trigger time = 2026-05-26T12:00:00Z.
589
- // A concurrent contributor's run started just before us (createdAt < triggerTime)
590
- // but got a higher databaseId (105) due to clock skew or out-of-order ID assignment.
591
- // The heuristic picks this run because databaseId > baselineId, IGNORING the
592
- // createdAt-before-trigger mismatch.
593
- const triggerTime = new Date('2026-05-26T12:00:00Z')
594
- // createdAt is BEFORE the trigger time — simulates concurrent contributor's run
595
- const concurrentCreatedAt = '2026-05-26T11:59:59Z'
596
-
597
- let callIndex = 0
598
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
599
- callIndex++
600
- if (callIndex === 1) {
601
- // baseline: run 100 exists
602
- return makeSmokeChild(
603
- makeSmokeRunList([
604
- {
605
- databaseId: 100,
606
- status: 'completed',
607
- conclusion: 'success',
608
- url: 'https://github.com/owner/test-repo/actions/runs/100',
609
- createdAt: '2026-05-26T11:00:00Z',
610
- },
611
- ]),
612
- '',
613
- 0,
614
- )
615
- }
616
- if (callIndex === 2) {
617
- // trigger succeeds
618
- return makeSmokeChild('', '', 0)
619
- }
620
- // poll — concurrent contributor's run: databaseId=105 > baseline=100, but createdAt < triggerTime
621
- return makeSmokeChild(
622
- makeSmokeRunList([
623
- {
624
- databaseId: 105,
625
- status: 'completed',
626
- conclusion: 'success',
627
- url: 'https://github.com/owner/test-repo/actions/runs/105',
628
- createdAt: concurrentCreatedAt,
629
- },
630
- ]),
631
- '',
632
- 0,
633
- )
634
- })
635
-
636
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
637
-
638
- // KNOWN LIMITATION: heuristic picks this run despite createdAt < triggerTime.
639
- // A true fix requires upstream correlation token from gh workflow run.
640
- expect(result.kind).toBe('pass')
641
- expect(result.runUrl).toBe('https://github.com/owner/test-repo/actions/runs/105')
642
- })
643
-
644
- // ── Zod schema validation hardening ──────────────────────────────────────
645
-
646
- it('poll JSON validation — non-array response degrades to unverified without throwing', async () => {
647
- const triggerTime = new Date('2026-05-25T10:00:00Z')
648
-
649
- let callIndex = 0
650
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
651
- callIndex++
652
- if (callIndex === 1) {
653
- // baseline: valid empty list
654
- return makeSmokeChild('[]', '', 0)
655
- }
656
- if (callIndex === 2) {
657
- // trigger succeeds
658
- return makeSmokeChild('', '', 0)
659
- }
660
- // poll returns a non-array object instead of an array — schema rejects it
661
- return makeSmokeChild('{"error":"unexpected"}', '', 0)
662
- })
663
-
664
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
665
-
666
- // Schema validation fails → pollRuns stays [] → no candidates → all polls exhaust → unverified
667
- expect(result.kind).toBe('unverified')
668
- })
669
-
670
- it('poll JSON validation — missing required databaseId field degrades gracefully', async () => {
671
- const triggerTime = new Date('2026-05-25T10:00:00Z')
672
-
673
- let callIndex = 0
674
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
675
- callIndex++
676
- if (callIndex === 1) {
677
- return makeSmokeChild('[]', '', 0)
678
- }
679
- if (callIndex === 2) {
680
- return makeSmokeChild('', '', 0)
681
- }
682
- // poll returns array entries missing databaseId — schema rejects it
683
- return makeSmokeChild(
684
- '[{"status":"completed","conclusion":"success","url":"https://x","createdAt":"2026-05-25T10:00:05Z"}]',
685
- '',
686
- 0,
687
- )
688
- })
689
-
690
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
691
-
692
- expect(result.kind).toBe('unverified')
693
- })
694
-
695
- it('poll JSON validation — wrong type for databaseId (string instead of number) degrades gracefully', async () => {
696
- const triggerTime = new Date('2026-05-25T10:00:00Z')
697
-
698
- let callIndex = 0
699
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
700
- callIndex++
701
- if (callIndex === 1) {
702
- return makeSmokeChild('[]', '', 0)
703
- }
704
- if (callIndex === 2) {
705
- return makeSmokeChild('', '', 0)
706
- }
707
- // databaseId is a string, not a number — schema rejects it
708
- return makeSmokeChild(
709
- '[{"databaseId":"not-a-number","status":"completed","conclusion":"success","url":"https://x","createdAt":"2026-05-25T10:00:05Z"}]',
710
- '',
711
- 0,
712
- )
713
- })
714
-
715
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
716
-
717
- expect(result.kind).toBe('unverified')
718
- })
719
-
720
- it('poll JSON validation — one malformed entry does not discard a valid matching run', async () => {
721
- const triggerTime = new Date('2026-05-25T10:00:00Z')
722
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
723
-
724
- let callIndex = 0
725
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
726
- callIndex++
727
- if (callIndex === 1) {
728
- // baseline: valid empty list → createdAt heuristic
729
- return makeSmokeChild('[]', '', 0)
730
- }
731
- if (callIndex === 2) {
732
- // trigger succeeds
733
- return makeSmokeChild('', '', 0)
734
- }
735
- if (callIndex === 3) {
736
- // poll: one malformed entry (databaseId is a string) PLUS one valid matching
737
- // completed-success run. Per-entry validation must drop only the bad row and
738
- // keep the good one — whole-batch rejection would lose our run.
739
- return makeSmokeChild(
740
- `[{"databaseId":"bad","status":"completed","conclusion":"success","url":"https://x","createdAt":"${
741
- createdAt
742
- }"},{"databaseId":105,"status":"completed","conclusion":"success","url":"${RUN_URL}","createdAt":"${
743
- createdAt
744
- }"}]`,
745
- '',
746
- 0,
747
- )
748
- }
749
- // log view for the matched run → contains "ack"
750
- return makeSmokeChild('some log output with ack in it', '', 0)
751
- })
752
-
753
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
754
-
755
- // The valid entry survives per-entry validation and is matched → pass.
756
- expect(result.kind).toBe('pass')
757
- expect(result.runUrl).toBe(RUN_URL)
758
- })
759
-
760
- it('baseline JSON validation — non-array baseline falls back to createdAt heuristic', async () => {
761
- const triggerTime = new Date('2026-05-25T10:00:00Z')
762
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
763
-
764
- let callIndex = 0
765
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
766
- callIndex++
767
- if (callIndex === 1) {
768
- // baseline returns a non-array object — schema rejects it, baselineId stays null
769
- return makeSmokeChild('{"databaseId":100}', '', 0)
770
- }
771
- if (callIndex === 2) {
772
- return makeSmokeChild('', '', 0)
773
- }
774
- if (callIndex === 3) {
775
- return makeSmokeChild(
776
- makeSmokeRunList([{databaseId: 1, status: 'completed', conclusion: 'success', url: RUN_URL, createdAt}]),
777
- '',
778
- 0,
779
- )
780
- }
781
- return makeSmokeChild('ack', '', 0)
782
- })
783
-
784
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
785
-
786
- // Schema rejects non-array → baselineId stays null → createdAt heuristic → run found → pass
787
- expect(result.kind).toBe('pass')
788
- expect(result.runUrl).toBe(RUN_URL)
789
- })
790
-
791
- it('baseline JSON validation — missing databaseId in baseline entry falls back to createdAt heuristic', async () => {
792
- const triggerTime = new Date('2026-05-25T10:00:00Z')
793
- const createdAt = new Date(triggerTime.getTime() + 5000).toISOString()
794
-
795
- let callIndex = 0
796
- spawnSpy = spyOn(Bun, 'spawn').mockImplementation((..._args: unknown[]) => {
797
- callIndex++
798
- if (callIndex === 1) {
799
- // baseline entry missing databaseId — schema rejects it, baselineId stays null
800
- return makeSmokeChild('[{"id":100}]', '', 0)
801
- }
802
- if (callIndex === 2) {
803
- return makeSmokeChild('', '', 0)
804
- }
805
- if (callIndex === 3) {
806
- return makeSmokeChild(
807
- makeSmokeRunList([{databaseId: 1, status: 'completed', conclusion: 'success', url: RUN_URL, createdAt}]),
808
- '',
809
- 0,
810
- )
811
- }
812
- return makeSmokeChild('ack', '', 0)
813
- })
814
-
815
- const result = await runSmokeTest(REPO, MODEL, {_testDelayMs: 0, _testTriggerTime: triggerTime})
816
-
817
- // Schema rejects missing databaseId → baselineId stays null → createdAt heuristic → run found → pass
818
- expect(result.kind).toBe('pass')
819
- expect(result.runUrl).toBe(RUN_URL)
820
- })
821
- })