@postman-cse/onboarding-bootstrap 0.9.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.
@@ -0,0 +1,731 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+
3
+ import {
4
+ lintSpecViaCli,
5
+ normalizeSpecDocument,
6
+ readActionInputs,
7
+ runBootstrap,
8
+ type CoreLike,
9
+ type ExecLike,
10
+ type IOLike,
11
+ type ResolvedInputs
12
+ } from '../src/index.js';
13
+
14
+ function createCoreStub(values: Record<string, string> = {}) {
15
+ const outputs: Record<string, string> = {};
16
+ const secrets: string[] = [];
17
+ const warnings: string[] = [];
18
+ const infos: string[] = [];
19
+ const errors: string[] = [];
20
+
21
+ const core: CoreLike = {
22
+ error: (message: string) => {
23
+ errors.push(message);
24
+ },
25
+ getInput: (name: string, options?: { required?: boolean }) => {
26
+ const value = values[name] ?? '';
27
+ if (options?.required && !value) {
28
+ throw new Error(`Input required and not supplied: ${name}`);
29
+ }
30
+ return value;
31
+ },
32
+ group: async (_name: string, fn: () => Promise<any>) => fn(),
33
+ info: (message: string) => {
34
+ infos.push(message);
35
+ },
36
+ setFailed: vi.fn(),
37
+ setOutput: (name: string, value: string) => {
38
+ outputs[name] = value;
39
+ },
40
+ setSecret: (secret: string) => {
41
+ secrets.push(secret);
42
+ },
43
+ warning: (message: string) => {
44
+ warnings.push(message);
45
+ }
46
+ };
47
+
48
+ return {
49
+ core,
50
+ errors,
51
+ infos,
52
+ outputs,
53
+ secrets,
54
+ warnings
55
+ };
56
+ }
57
+
58
+ function createInputs(overrides: Partial<ResolvedInputs> = {}): ResolvedInputs {
59
+ return {
60
+ projectName: 'core-payments',
61
+ domain: 'core-banking',
62
+ domainCode: 'AF',
63
+ requesterEmail: 'owner@example.com',
64
+ workspaceAdminUserIds: '101,102',
65
+ repoUrl: 'https://github.com/postman-cs/bootstrap-action-test',
66
+ specUrl: 'https://example.test/openapi.yaml',
67
+ environmentsJson: '["prod","stage"]',
68
+ systemEnvMapJson: '{"prod":"sys-prod","stage":"sys-stage"}',
69
+ governanceMappingJson: '{"core-banking":"Core Banking"}',
70
+ postmanApiKey: 'pmak-test',
71
+ postmanAccessToken: 'postman-access-token',
72
+ githubToken: 'github-token',
73
+ ghFallbackToken: 'github-fallback-token',
74
+ githubAuthMode: 'github_token_first',
75
+ integrationBackend: 'bifrost',
76
+ ...overrides
77
+ };
78
+ }
79
+
80
+ function createExecStub(stdout = '{"violations":[]}'): ExecLike {
81
+ return {
82
+ exec: vi.fn().mockResolvedValue(0),
83
+ getExecOutput: vi.fn().mockResolvedValue({
84
+ exitCode: 0,
85
+ stdout,
86
+ stderr: ''
87
+ })
88
+ };
89
+ }
90
+
91
+ function createIoStub(): IOLike {
92
+ return {
93
+ which: vi.fn().mockResolvedValue('/usr/local/bin/postman')
94
+ };
95
+ }
96
+
97
+ describe('bootstrap action', () => {
98
+ it('marks secrets as early as input resolution', () => {
99
+ const { core, secrets } = createCoreStub({
100
+ 'project-name': 'core-payments',
101
+ 'spec-url': 'https://example.test/openapi.yaml',
102
+ 'postman-api-key': 'pmak-test',
103
+ 'postman-access-token': 'postman-access-token',
104
+ 'github-token': 'github-token',
105
+ 'gh-fallback-token': 'github-fallback-token'
106
+ });
107
+
108
+ const inputs = readActionInputs(core);
109
+
110
+ expect(inputs.postmanApiKey).toBe('pmak-test');
111
+ expect(secrets).toEqual([
112
+ 'pmak-test',
113
+ 'postman-access-token',
114
+ 'github-token',
115
+ 'github-fallback-token'
116
+ ]);
117
+ });
118
+
119
+ it('runs the bootstrap flow end to end and emits outputs', async () => {
120
+ const { core, outputs } = createCoreStub();
121
+ const execStub = createExecStub();
122
+ const ioStub = createIoStub();
123
+ const order: string[] = [];
124
+ const postman = {
125
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
126
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
127
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
128
+ generateCollection: vi
129
+ .fn()
130
+ .mockImplementation(async (_specId: string, _projectName: string, prefix: string) => {
131
+ order.push(prefix);
132
+ if (prefix === '[Baseline]') return 'col-baseline';
133
+ if (prefix === '[Smoke]') return 'col-smoke';
134
+ return 'col-contract';
135
+ }),
136
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
137
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
138
+ injectTests: vi.fn().mockResolvedValue(undefined),
139
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
140
+ tagCollection: vi.fn().mockResolvedValue(undefined),
141
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
142
+ updateSpec: vi.fn().mockResolvedValue(undefined),
143
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
144
+ };
145
+ const internalIntegration = {
146
+ assignWorkspaceToGovernanceGroup: vi.fn().mockResolvedValue(undefined)
147
+ };
148
+ const github = {
149
+ setRepositoryVariable: vi.fn().mockResolvedValue(undefined),
150
+ getRepositoryVariable: vi.fn().mockResolvedValue('')
151
+ };
152
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
153
+ new Response('openapi: 3.1.0', {
154
+ status: 200
155
+ })
156
+ );
157
+
158
+ const result = await runBootstrap(createInputs(), {
159
+ core,
160
+ exec: execStub,
161
+ github,
162
+ io: ioStub,
163
+ internalIntegration,
164
+ postman,
165
+ specFetcher
166
+ });
167
+
168
+ expect(execStub.exec).toHaveBeenCalledWith('postman', ['login', '--with-api-key', 'pmak-test']);
169
+ expect(internalIntegration.assignWorkspaceToGovernanceGroup).toHaveBeenCalledWith(
170
+ 'ws-123',
171
+ 'core-banking',
172
+ '{"core-banking":"Core Banking"}'
173
+ );
174
+ expect(postman.inviteRequesterToWorkspace).toHaveBeenCalledWith(
175
+ 'ws-123',
176
+ 'owner@example.com'
177
+ );
178
+ expect(postman.addAdminsToWorkspace).toHaveBeenCalledWith('ws-123', '101,102');
179
+ expect(order).toEqual(['[Baseline]', '[Smoke]', '[Contract]']);
180
+ expect(github.setRepositoryVariable).toHaveBeenCalledWith(
181
+ 'LINT_WARNINGS',
182
+ '0'
183
+ );
184
+ expect(github.setRepositoryVariable).toHaveBeenCalledWith(
185
+ 'LINT_ERRORS',
186
+ '0'
187
+ );
188
+ expect(github.setRepositoryVariable).toHaveBeenCalledWith(
189
+ 'POSTMAN_WORKSPACE_ID',
190
+ 'ws-123'
191
+ );
192
+ expect(github.setRepositoryVariable).toHaveBeenCalledWith(
193
+ 'POSTMAN_CORE_PAYMENTS_WORKSPACE_ID',
194
+ 'ws-123'
195
+ );
196
+ expect(result).toMatchObject({
197
+ 'workspace-id': 'ws-123',
198
+ 'workspace-name': '[AF] core-payments',
199
+ 'spec-id': 'spec-123',
200
+ 'baseline-collection-id': 'col-baseline',
201
+ 'smoke-collection-id': 'col-smoke',
202
+ 'contract-collection-id': 'col-contract'
203
+ });
204
+ expect(outputs['collections-json']).toBe(
205
+ JSON.stringify({
206
+ baseline: 'col-baseline',
207
+ contract: 'col-contract',
208
+ smoke: 'col-smoke'
209
+ })
210
+ );
211
+ expect(outputs['lint-summary-json']).toBe(
212
+ JSON.stringify({
213
+ errors: 0,
214
+ total: 0,
215
+ violations: [],
216
+ warnings: 0
217
+ })
218
+ );
219
+ });
220
+
221
+ it('fails when spec lint returns errors', async () => {
222
+ const { core } = createCoreStub();
223
+ const execStub = createExecStub(
224
+ JSON.stringify({
225
+ violations: [
226
+ {
227
+ issue: 'Missing operationId',
228
+ path: '$.paths./payments.get',
229
+ severity: 'ERROR'
230
+ }
231
+ ]
232
+ })
233
+ );
234
+ const ioStub = createIoStub();
235
+ const postman = {
236
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
237
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
238
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
239
+ generateCollection: vi.fn(),
240
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
241
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
242
+ injectTests: vi.fn(),
243
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
244
+ tagCollection: vi.fn(),
245
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
246
+ updateSpec: vi.fn().mockResolvedValue(undefined),
247
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
248
+ };
249
+
250
+ await expect(
251
+ runBootstrap(createInputs(), {
252
+ core,
253
+ exec: execStub,
254
+ io: ioStub,
255
+ postman,
256
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
257
+ new Response('openapi: 3.1.0', { status: 200 })
258
+ )
259
+ })
260
+ ).rejects.toThrow('Spec lint found 1 errors');
261
+
262
+ expect(postman.generateCollection).not.toHaveBeenCalled();
263
+ });
264
+
265
+ it('restores previous spec content when lint fails after an update', async () => {
266
+ const { core } = createCoreStub();
267
+ const execStub = createExecStub(
268
+ JSON.stringify({
269
+ violations: [
270
+ {
271
+ issue: 'Broken schema',
272
+ path: '$.paths./payments.get',
273
+ severity: 'ERROR'
274
+ }
275
+ ]
276
+ })
277
+ );
278
+ const ioStub = createIoStub();
279
+ const postman = {
280
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
281
+ createWorkspace: vi.fn(),
282
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
283
+ generateCollection: vi.fn(),
284
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
285
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.0.0\ninfo:\n title: old\n'),
286
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
287
+ injectTests: vi.fn(),
288
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
289
+ tagCollection: vi.fn(),
290
+ uploadSpec: vi.fn(),
291
+ updateSpec: vi.fn().mockResolvedValue(undefined)
292
+ };
293
+
294
+ await expect(
295
+ runBootstrap(
296
+ createInputs({ workspaceId: 'ws-existing', specId: 'spec-existing' }),
297
+ {
298
+ core,
299
+ exec: execStub,
300
+ io: ioStub,
301
+ postman,
302
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
303
+ new Response('openapi: 3.1.0', { status: 200 })
304
+ )
305
+ }
306
+ )
307
+ ).rejects.toThrow('Spec lint found 1 errors');
308
+
309
+ expect(postman.getSpecContent).toHaveBeenCalledWith('spec-existing');
310
+ expect(postman.updateSpec).toHaveBeenNthCalledWith(
311
+ 1,
312
+ 'spec-existing',
313
+ 'openapi: 3.1.0',
314
+ 'ws-existing'
315
+ );
316
+ expect(postman.updateSpec).toHaveBeenNthCalledWith(
317
+ 2,
318
+ 'spec-existing',
319
+ 'openapi: 3.0.0\ninfo:\n title: old\n',
320
+ 'ws-existing'
321
+ );
322
+ });
323
+
324
+ it('validates normalized spec structure before upload or update', async () => {
325
+ const { core } = createCoreStub();
326
+ const execStub = createExecStub();
327
+ const ioStub = createIoStub();
328
+ const postman = {
329
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
330
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
331
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
332
+ generateCollection: vi.fn(),
333
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
334
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0'),
335
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
336
+ injectTests: vi.fn(),
337
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
338
+ tagCollection: vi.fn(),
339
+ uploadSpec: vi.fn(),
340
+ updateSpec: vi.fn()
341
+ };
342
+
343
+ await expect(
344
+ runBootstrap(createInputs(), {
345
+ core,
346
+ exec: execStub,
347
+ io: ioStub,
348
+ postman,
349
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
350
+ new Response('info:\n title: Missing version\n', { status: 200 })
351
+ )
352
+ })
353
+ ).rejects.toThrow('Spec is missing "openapi" or "swagger" version field');
354
+
355
+ expect(postman.uploadSpec).not.toHaveBeenCalled();
356
+ expect(postman.updateSpec).not.toHaveBeenCalled();
357
+ });
358
+
359
+ it('reuses existing workspace, spec, and collection ids from explicit inputs', async () => {
360
+ const { core, infos, outputs } = createCoreStub();
361
+ const execStub = createExecStub();
362
+ const ioStub = createIoStub();
363
+ const postman = {
364
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
365
+ createWorkspace: vi.fn(),
366
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
367
+ generateCollection: vi.fn(),
368
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
369
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
370
+ injectTests: vi.fn().mockResolvedValue(undefined),
371
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
372
+ tagCollection: vi.fn().mockResolvedValue(undefined),
373
+ uploadSpec: vi.fn(),
374
+ updateSpec: vi.fn().mockResolvedValue(undefined),
375
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
376
+ };
377
+ const github = {
378
+ setRepositoryVariable: vi.fn().mockResolvedValue(undefined),
379
+ getRepositoryVariable: vi.fn()
380
+ };
381
+
382
+ const result = await runBootstrap(
383
+ createInputs({
384
+ workspaceId: 'ws-existing',
385
+ specId: 'spec-existing',
386
+ baselineCollectionId: 'col-baseline-existing',
387
+ smokeCollectionId: 'col-smoke-existing',
388
+ contractCollectionId: 'col-contract-existing'
389
+ }),
390
+ {
391
+ core,
392
+ exec: execStub,
393
+ github,
394
+ io: ioStub,
395
+ postman,
396
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
397
+ new Response('openapi: 3.1.0', { status: 200 })
398
+ )
399
+ }
400
+ );
401
+
402
+ expect(github.getRepositoryVariable).not.toHaveBeenCalled();
403
+ expect(postman.createWorkspace).not.toHaveBeenCalled();
404
+ expect(postman.uploadSpec).not.toHaveBeenCalled();
405
+ expect(postman.generateCollection).not.toHaveBeenCalled();
406
+ expect(postman.updateSpec).toHaveBeenCalledWith('spec-existing', 'openapi: 3.1.0', 'ws-existing');
407
+ expect(result).toMatchObject({
408
+ 'workspace-id': 'ws-existing',
409
+ 'spec-id': 'spec-existing',
410
+ 'baseline-collection-id': 'col-baseline-existing',
411
+ 'smoke-collection-id': 'col-smoke-existing',
412
+ 'contract-collection-id': 'col-contract-existing'
413
+ });
414
+ expect(outputs['collections-json']).toBe(
415
+ JSON.stringify({
416
+ baseline: 'col-baseline-existing',
417
+ contract: 'col-contract-existing',
418
+ smoke: 'col-smoke-existing'
419
+ })
420
+ );
421
+ expect(infos).toContain('Using existing workspace: ws-existing');
422
+ expect(infos).toContain('Using existing baseline collection: col-baseline-existing');
423
+ expect(infos).toContain('Using existing smoke collection: col-smoke-existing');
424
+ expect(infos).toContain('Using existing contract collection: col-contract-existing');
425
+ });
426
+
427
+ it('falls back to repository variables for reruns before creating new assets', async () => {
428
+ const { core } = createCoreStub();
429
+ const execStub = createExecStub();
430
+ const ioStub = createIoStub();
431
+ const postman = {
432
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
433
+ createWorkspace: vi.fn(),
434
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
435
+ generateCollection: vi.fn(),
436
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
437
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
438
+ injectTests: vi.fn().mockResolvedValue(undefined),
439
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
440
+ tagCollection: vi.fn().mockResolvedValue(undefined),
441
+ uploadSpec: vi.fn(),
442
+ updateSpec: vi.fn().mockResolvedValue(undefined),
443
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
444
+ };
445
+ const github = {
446
+ setRepositoryVariable: vi.fn().mockResolvedValue(undefined),
447
+ getRepositoryVariable: vi.fn(async (name: string) => {
448
+ const values: Record<string, string> = {
449
+ POSTMAN_WORKSPACE_ID: 'ws-from-vars',
450
+ POSTMAN_SPEC_UID: 'spec-from-vars',
451
+ POSTMAN_BASELINE_COLLECTION_UID: 'col-baseline-from-vars',
452
+ POSTMAN_SMOKE_COLLECTION_UID: 'col-smoke-from-vars',
453
+ POSTMAN_CONTRACT_COLLECTION_UID: 'col-contract-from-vars'
454
+ };
455
+ return values[name] ?? '';
456
+ })
457
+ };
458
+
459
+ const result = await runBootstrap(createInputs(), {
460
+ core,
461
+ exec: execStub,
462
+ github,
463
+ io: ioStub,
464
+ postman,
465
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
466
+ new Response('openapi: 3.1.0', { status: 200 })
467
+ )
468
+ });
469
+
470
+ expect(postman.createWorkspace).not.toHaveBeenCalled();
471
+ expect(postman.uploadSpec).not.toHaveBeenCalled();
472
+ expect(postman.generateCollection).not.toHaveBeenCalled();
473
+ expect(postman.updateSpec).toHaveBeenCalledWith('spec-from-vars', 'openapi: 3.1.0', 'ws-from-vars');
474
+ expect(github.getRepositoryVariable).toHaveBeenCalledWith('POSTMAN_CORE_PAYMENTS_WORKSPACE_ID');
475
+ expect(github.getRepositoryVariable).toHaveBeenCalledWith('POSTMAN_WORKSPACE_ID');
476
+ expect(result).toMatchObject({
477
+ 'workspace-id': 'ws-from-vars',
478
+ 'spec-id': 'spec-from-vars',
479
+ 'baseline-collection-id': 'col-baseline-from-vars',
480
+ 'smoke-collection-id': 'col-smoke-from-vars',
481
+ 'contract-collection-id': 'col-contract-from-vars'
482
+ });
483
+ });
484
+
485
+ it('creates a new workspace when the repo-variable workspace is linked to a different repository', async () => {
486
+ const previousRepository = process.env.GITHUB_REPOSITORY;
487
+ process.env.GITHUB_REPOSITORY = 'postman-cs/bootstrap-action-test';
488
+
489
+ try {
490
+ const { core } = createCoreStub();
491
+ const execStub = createExecStub();
492
+ const ioStub = createIoStub();
493
+ const postman = {
494
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
495
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-new' }),
496
+ findWorkspacesByName: vi.fn().mockResolvedValue([{ id: 'ws-from-vars', name: '[AF] core-payments' }]),
497
+ generateCollection: vi
498
+ .fn()
499
+ .mockImplementation(async (_specId: string, _projectName: string, prefix: string) => {
500
+ if (prefix === '[Baseline]') return 'col-baseline';
501
+ if (prefix === '[Smoke]') return 'col-smoke';
502
+ return 'col-contract';
503
+ }),
504
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
505
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue('https://github.com/postman-cs/different-repo'),
506
+ injectTests: vi.fn().mockResolvedValue(undefined),
507
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
508
+ tagCollection: vi.fn().mockResolvedValue(undefined),
509
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
510
+ updateSpec: vi.fn().mockResolvedValue(undefined),
511
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
512
+ };
513
+ const github = {
514
+ setRepositoryVariable: vi.fn().mockResolvedValue(undefined),
515
+ getRepositoryVariable: vi.fn(async (name: string) => {
516
+ const values: Record<string, string> = {
517
+ POSTMAN_WORKSPACE_ID: 'ws-from-vars'
518
+ };
519
+ return values[name] ?? '';
520
+ })
521
+ };
522
+
523
+ const result = await runBootstrap(createInputs(), {
524
+ core,
525
+ exec: execStub,
526
+ github,
527
+ io: ioStub,
528
+ postman,
529
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
530
+ new Response('openapi: 3.1.0', { status: 200 })
531
+ )
532
+ });
533
+
534
+ expect(postman.getWorkspaceGitRepoUrl).toHaveBeenCalledWith('ws-from-vars', '12345', 'postman-access-token');
535
+ expect(postman.createWorkspace).toHaveBeenCalled();
536
+ expect(result['workspace-id']).toBe('ws-new');
537
+ expect(postman.updateSpec).not.toHaveBeenCalled();
538
+ } finally {
539
+ if (previousRepository === undefined) {
540
+ delete process.env.GITHUB_REPOSITORY;
541
+ } else {
542
+ process.env.GITHUB_REPOSITORY = previousRepository;
543
+ }
544
+ }
545
+ });
546
+
547
+ it('skips governance assignment when postman-access-token is absent', async () => {
548
+ const { core } = createCoreStub();
549
+ const execStub = createExecStub();
550
+ const ioStub = createIoStub();
551
+ const postman = {
552
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
553
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
554
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
555
+ generateCollection: vi.fn().mockResolvedValue('col-id'),
556
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
557
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
558
+ injectTests: vi.fn().mockResolvedValue(undefined),
559
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
560
+ tagCollection: vi.fn().mockResolvedValue(undefined),
561
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
562
+ updateSpec: vi.fn().mockResolvedValue(undefined),
563
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
564
+ };
565
+
566
+ const result = await runBootstrap(
567
+ createInputs({ postmanAccessToken: undefined }),
568
+ {
569
+ core,
570
+ exec: execStub,
571
+ io: ioStub,
572
+ postman,
573
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
574
+ new Response('openapi: 3.1.0', { status: 200 })
575
+ )
576
+ }
577
+ );
578
+
579
+ expect(result['workspace-id']).toBe('ws-123');
580
+ expect(postman.createWorkspace).toHaveBeenCalled();
581
+ });
582
+
583
+ it('skips repo variable persistence when github dependency is absent', async () => {
584
+ const { core } = createCoreStub();
585
+ const execStub = createExecStub();
586
+ const ioStub = createIoStub();
587
+ const postman = {
588
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
589
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
590
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
591
+ generateCollection: vi.fn().mockResolvedValue('col-id'),
592
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
593
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
594
+ injectTests: vi.fn().mockResolvedValue(undefined),
595
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
596
+ tagCollection: vi.fn().mockResolvedValue(undefined),
597
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
598
+ updateSpec: vi.fn().mockResolvedValue(undefined),
599
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
600
+ };
601
+
602
+ const result = await runBootstrap(
603
+ createInputs({ githubToken: undefined }),
604
+ {
605
+ core,
606
+ exec: execStub,
607
+ io: ioStub,
608
+ postman,
609
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
610
+ new Response('openapi: 3.1.0', { status: 200 })
611
+ )
612
+ }
613
+ );
614
+
615
+ expect(result['workspace-id']).toBe('ws-123');
616
+ expect(result['spec-id']).toBe('spec-123');
617
+ });
618
+
619
+ it('emits warnings for lint violations but does not fail', async () => {
620
+ const { core, warnings } = createCoreStub();
621
+ const execStub = createExecStub(
622
+ JSON.stringify({
623
+ violations: [
624
+ { issue: 'Missing description', path: '$.paths./payments.get', severity: 'WARNING' }
625
+ ]
626
+ })
627
+ );
628
+ const ioStub = createIoStub();
629
+ const postman = {
630
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
631
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
632
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
633
+ generateCollection: vi.fn().mockResolvedValue('col-id'),
634
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
635
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
636
+ injectTests: vi.fn().mockResolvedValue(undefined),
637
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
638
+ tagCollection: vi.fn().mockResolvedValue(undefined),
639
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
640
+ updateSpec: vi.fn().mockResolvedValue(undefined),
641
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
642
+ };
643
+
644
+ const result = await runBootstrap(createInputs(), {
645
+ core,
646
+ exec: execStub,
647
+ io: ioStub,
648
+ postman,
649
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
650
+ new Response('openapi: 3.1.0', { status: 200 })
651
+ )
652
+ });
653
+
654
+ expect(result['workspace-id']).toBe('ws-123');
655
+ expect(warnings.some((w) => w.includes('Missing description'))).toBe(true);
656
+ const lintSummary = JSON.parse(result['lint-summary-json']);
657
+ expect(lintSummary.warnings).toBe(1);
658
+ expect(lintSummary.errors).toBe(0);
659
+ });
660
+
661
+ it('normalizeSpecDocument adds summary from operationId or METHOD+path', () => {
662
+ const warn = vi.fn();
663
+ const withId = normalizeSpecDocument(
664
+ JSON.stringify({
665
+ openapi: '3.0.0',
666
+ info: { title: 'T', version: '1' },
667
+ paths: { '/pets': { get: { operationId: 'listPets' } } }
668
+ }),
669
+ warn
670
+ );
671
+ expect(JSON.parse(withId).paths['/pets'].get.summary).toBe('listPets');
672
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('operationId'));
673
+
674
+ const warn2 = vi.fn();
675
+ const bare = normalizeSpecDocument(
676
+ JSON.stringify({
677
+ openapi: '3.0.0',
678
+ info: { title: 'T', version: '1' },
679
+ paths: { '/x': { post: {} } }
680
+ }),
681
+ warn2
682
+ );
683
+ expect(JSON.parse(bare).paths['/x'].post.summary).toBe('POST /x');
684
+ expect(warn2).toHaveBeenCalledWith(expect.stringContaining('method + path'));
685
+ });
686
+
687
+ it('normalizeSpecDocument truncates long summaries', () => {
688
+ const long = 'x'.repeat(250);
689
+ const warn = vi.fn();
690
+ const out = normalizeSpecDocument(
691
+ JSON.stringify({
692
+ openapi: '3.0.0',
693
+ info: { title: 'T', version: '1' },
694
+ paths: { '/a': { get: { summary: long } } }
695
+ }),
696
+ warn
697
+ );
698
+ const s = JSON.parse(out).paths['/a'].get.summary as string;
699
+ expect(s.length).toBe(200);
700
+ expect(s.endsWith('…')).toBe(true);
701
+ expect(warn).toHaveBeenCalledWith(expect.stringContaining('truncated'));
702
+ });
703
+ });
704
+
705
+ describe('lintSpecViaCli', () => {
706
+ it('parses warning and error counts from postman cli json output', async () => {
707
+ const summary = await lintSpecViaCli(
708
+ {
709
+ exec: createExecStub(
710
+ JSON.stringify({
711
+ violations: [
712
+ { severity: 'ERROR', issue: 'broken' },
713
+ { severity: 'WARNING', issue: 'warn' }
714
+ ]
715
+ })
716
+ )
717
+ },
718
+ 'ws-123',
719
+ 'spec-123'
720
+ );
721
+
722
+ expect(summary).toEqual({
723
+ errors: 1,
724
+ violations: [
725
+ { severity: 'ERROR', issue: 'broken' },
726
+ { severity: 'WARNING', issue: 'warn' }
727
+ ],
728
+ warnings: 1
729
+ });
730
+ });
731
+ });