@marcusrbrown/infra 0.7.0 → 0.8.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/package.json +1 -1
- package/src/__snapshots__/cli.test.ts.snap +3 -2
- package/src/commands/cliproxy/config.ts +2 -26
- package/src/commands/cliproxy/keys.ts +8 -43
- package/src/commands/cliproxy/setup/gh.test.ts +218 -0
- package/src/commands/cliproxy/setup/gh.ts +250 -0
- package/src/commands/cliproxy/setup/preview.test.ts +159 -0
- package/src/commands/cliproxy/setup/preview.ts +41 -0
- package/src/commands/cliproxy/setup/prompts.test.ts +58 -0
- package/src/commands/cliproxy/setup/prompts.ts +99 -0
- package/src/commands/cliproxy/setup/providers.test.ts +228 -0
- package/src/commands/cliproxy/setup/providers.ts +136 -0
- package/src/commands/cliproxy/setup/smoke-test.test.ts +643 -0
- package/src/commands/cliproxy/setup/smoke-test.ts +205 -0
- package/src/commands/cliproxy/setup/templates.test.ts +358 -0
- package/src/commands/cliproxy/setup/templates.ts +158 -0
- package/src/commands/cliproxy/setup/validation.test.ts +399 -0
- package/src/commands/cliproxy/setup/validation.ts +182 -0
- package/src/commands/cliproxy/setup/workflow-analyzer.test.ts +341 -0
- package/src/commands/cliproxy/setup/workflow-analyzer.ts +137 -0
- package/src/commands/cliproxy/setup.test.ts +1581 -1983
- package/src/commands/cliproxy/setup.ts +440 -1374
- package/src/commands/cliproxy/shared.test.ts +118 -0
- package/src/commands/cliproxy/shared.ts +84 -0
- package/src/commands/cliproxy/status.ts +2 -7
|
@@ -0,0 +1,643 @@
|
|
|
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
|
+
})
|