@postman-cse/onboarding-bootstrap 0.9.1 → 0.11.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.
@@ -1,4 +1,6 @@
1
- import { describe, expect, it, vi } from 'vitest';
1
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { stringify as stringifyYaml } from 'yaml';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
4
 
3
5
  import {
4
6
  lintSpecViaCli,
@@ -58,21 +60,24 @@ function createCoreStub(values: Record<string, string> = {}) {
58
60
  function createInputs(overrides: Partial<ResolvedInputs> = {}): ResolvedInputs {
59
61
  return {
60
62
  projectName: 'core-payments',
63
+ syncExamples: true,
64
+ collectionSyncMode: 'refresh',
65
+ specSyncMode: 'update',
66
+ releaseLabel: undefined,
61
67
  domain: 'core-banking',
62
68
  domainCode: 'AF',
63
69
  requesterEmail: 'owner@example.com',
64
70
  workspaceAdminUserIds: '101,102',
65
71
  repoUrl: 'https://github.com/postman-cs/bootstrap-action-test',
66
72
  specUrl: 'https://example.test/openapi.yaml',
67
- environmentsJson: '["prod","stage"]',
68
- systemEnvMapJson: '{"prod":"sys-prod","stage":"sys-stage"}',
69
73
  governanceMappingJson: '{"core-banking":"Core Banking"}',
70
74
  postmanApiKey: 'pmak-test',
71
75
  postmanAccessToken: 'postman-access-token',
72
- githubToken: 'github-token',
73
- ghFallbackToken: 'github-fallback-token',
74
- githubAuthMode: 'github_token_first',
75
76
  integrationBackend: 'bifrost',
77
+ githubRefName: undefined,
78
+ githubHeadRef: undefined,
79
+ githubRef: undefined,
80
+ githubSha: undefined,
76
81
  ...overrides
77
82
  };
78
83
  }
@@ -95,14 +100,16 @@ function createIoStub(): IOLike {
95
100
  }
96
101
 
97
102
  describe('bootstrap action', () => {
103
+ afterEach(() => {
104
+ rmSync('.postman', { recursive: true, force: true });
105
+ });
106
+
98
107
  it('marks secrets as early as input resolution', () => {
99
108
  const { core, secrets } = createCoreStub({
100
109
  'project-name': 'core-payments',
101
110
  'spec-url': 'https://example.test/openapi.yaml',
102
111
  'postman-api-key': 'pmak-test',
103
- 'postman-access-token': 'postman-access-token',
104
- 'github-token': 'github-token',
105
- 'gh-fallback-token': 'github-fallback-token'
112
+ 'postman-access-token': 'postman-access-token'
106
113
  });
107
114
 
108
115
  const inputs = readActionInputs(core);
@@ -110,9 +117,7 @@ describe('bootstrap action', () => {
110
117
  expect(inputs.postmanApiKey).toBe('pmak-test');
111
118
  expect(secrets).toEqual([
112
119
  'pmak-test',
113
- 'postman-access-token',
114
- 'github-token',
115
- 'github-fallback-token'
120
+ 'postman-access-token'
116
121
  ]);
117
122
  });
118
123
 
@@ -134,6 +139,7 @@ describe('bootstrap action', () => {
134
139
  return 'col-contract';
135
140
  }),
136
141
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
142
+ getTeams: vi.fn().mockResolvedValue([]),
137
143
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
138
144
  injectTests: vi.fn().mockResolvedValue(undefined),
139
145
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -143,11 +149,9 @@ describe('bootstrap action', () => {
143
149
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
144
150
  };
145
151
  const internalIntegration = {
146
- assignWorkspaceToGovernanceGroup: vi.fn().mockResolvedValue(undefined)
147
- };
148
- const github = {
149
- setRepositoryVariable: vi.fn().mockResolvedValue(undefined),
150
- getRepositoryVariable: vi.fn().mockResolvedValue('')
152
+ assignWorkspaceToGovernanceGroup: vi.fn().mockResolvedValue(undefined),
153
+ linkCollectionsToSpecification: vi.fn().mockResolvedValue(undefined),
154
+ syncCollection: vi.fn().mockResolvedValue(undefined)
151
155
  };
152
156
  const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
153
157
  new Response('openapi: 3.1.0', {
@@ -158,7 +162,6 @@ describe('bootstrap action', () => {
158
162
  const result = await runBootstrap(createInputs(), {
159
163
  core,
160
164
  exec: execStub,
161
- github,
162
165
  io: ioStub,
163
166
  internalIntegration,
164
167
  postman,
@@ -177,21 +180,28 @@ describe('bootstrap action', () => {
177
180
  );
178
181
  expect(postman.addAdminsToWorkspace).toHaveBeenCalledWith('ws-123', '101,102');
179
182
  expect(order).toEqual(['[Baseline]', '[Smoke]', '[Contract]']);
180
- expect(github.setRepositoryVariable).toHaveBeenCalledWith(
181
- 'LINT_WARNINGS',
182
- '0'
183
+ expect(internalIntegration.linkCollectionsToSpecification).toHaveBeenCalledWith(
184
+ 'spec-123',
185
+ [
186
+ { collectionId: 'col-baseline', syncOptions: { syncExamples: true } },
187
+ { collectionId: 'col-smoke', syncOptions: { syncExamples: true } },
188
+ { collectionId: 'col-contract', syncOptions: { syncExamples: true } }
189
+ ]
183
190
  );
184
- expect(github.setRepositoryVariable).toHaveBeenCalledWith(
185
- 'LINT_ERRORS',
186
- '0'
191
+ expect(internalIntegration.syncCollection).toHaveBeenNthCalledWith(
192
+ 1,
193
+ 'spec-123',
194
+ 'col-baseline'
187
195
  );
188
- expect(github.setRepositoryVariable).toHaveBeenCalledWith(
189
- 'POSTMAN_WORKSPACE_ID',
190
- 'ws-123'
196
+ expect(internalIntegration.syncCollection).toHaveBeenNthCalledWith(
197
+ 2,
198
+ 'spec-123',
199
+ 'col-smoke'
191
200
  );
192
- expect(github.setRepositoryVariable).toHaveBeenCalledWith(
193
- 'POSTMAN_CORE_PAYMENTS_WORKSPACE_ID',
194
- 'ws-123'
201
+ expect(internalIntegration.syncCollection).toHaveBeenNthCalledWith(
202
+ 3,
203
+ 'spec-123',
204
+ 'col-contract'
195
205
  );
196
206
  expect(result).toMatchObject({
197
207
  'workspace-id': 'ws-123',
@@ -238,6 +248,7 @@ describe('bootstrap action', () => {
238
248
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
239
249
  generateCollection: vi.fn(),
240
250
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
251
+ getTeams: vi.fn().mockResolvedValue([]),
241
252
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
242
253
  injectTests: vi.fn(),
243
254
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -282,6 +293,7 @@ describe('bootstrap action', () => {
282
293
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
283
294
  generateCollection: vi.fn(),
284
295
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
296
+ getTeams: vi.fn().mockResolvedValue([]),
285
297
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.0.0\ninfo:\n title: old\n'),
286
298
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
287
299
  injectTests: vi.fn(),
@@ -331,6 +343,7 @@ describe('bootstrap action', () => {
331
343
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
332
344
  generateCollection: vi.fn(),
333
345
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
346
+ getTeams: vi.fn().mockResolvedValue([]),
334
347
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0'),
335
348
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
336
349
  injectTests: vi.fn(),
@@ -366,6 +379,7 @@ describe('bootstrap action', () => {
366
379
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
367
380
  generateCollection: vi.fn(),
368
381
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
382
+ getTeams: vi.fn().mockResolvedValue([]),
369
383
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
370
384
  injectTests: vi.fn().mockResolvedValue(undefined),
371
385
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -374,23 +388,18 @@ describe('bootstrap action', () => {
374
388
  updateSpec: vi.fn().mockResolvedValue(undefined),
375
389
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
376
390
  };
377
- const github = {
378
- setRepositoryVariable: vi.fn().mockResolvedValue(undefined),
379
- getRepositoryVariable: vi.fn()
380
- };
381
-
382
391
  const result = await runBootstrap(
383
392
  createInputs({
384
393
  workspaceId: 'ws-existing',
385
394
  specId: 'spec-existing',
386
395
  baselineCollectionId: 'col-baseline-existing',
387
396
  smokeCollectionId: 'col-smoke-existing',
388
- contractCollectionId: 'col-contract-existing'
397
+ contractCollectionId: 'col-contract-existing',
398
+ collectionSyncMode: 'reuse'
389
399
  }),
390
400
  {
391
401
  core,
392
402
  exec: execStub,
393
- github,
394
403
  io: ioStub,
395
404
  postman,
396
405
  specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
@@ -399,7 +408,6 @@ describe('bootstrap action', () => {
399
408
  }
400
409
  );
401
410
 
402
- expect(github.getRepositoryVariable).not.toHaveBeenCalled();
403
411
  expect(postman.createWorkspace).not.toHaveBeenCalled();
404
412
  expect(postman.uploadSpec).not.toHaveBeenCalled();
405
413
  expect(postman.generateCollection).not.toHaveBeenCalled();
@@ -424,7 +432,57 @@ describe('bootstrap action', () => {
424
432
  expect(infos).toContain('Using existing contract collection: col-contract-existing');
425
433
  });
426
434
 
427
- it('falls back to repository variables for reruns before creating new assets', async () => {
435
+ it('refresh mode regenerates collections even when ids already exist', async () => {
436
+ const { core } = createCoreStub();
437
+ const execStub = createExecStub();
438
+ const ioStub = createIoStub();
439
+ const generatedIds = ['col-baseline-refresh', 'col-smoke-refresh', 'col-contract-refresh'];
440
+ const postman = {
441
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
442
+ createWorkspace: vi.fn(),
443
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
444
+ generateCollection: vi
445
+ .fn()
446
+ .mockImplementation(async () => generatedIds.shift() || 'col-fallback'),
447
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
448
+ getTeams: vi.fn().mockResolvedValue([]),
449
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
450
+ injectTests: vi.fn().mockResolvedValue(undefined),
451
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
452
+ tagCollection: vi.fn().mockResolvedValue(undefined),
453
+ uploadSpec: vi.fn(),
454
+ updateSpec: vi.fn().mockResolvedValue(undefined),
455
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
456
+ };
457
+ const result = await runBootstrap(
458
+ createInputs({
459
+ workspaceId: 'ws-existing',
460
+ specId: 'spec-existing',
461
+ baselineCollectionId: 'col-baseline-existing',
462
+ smokeCollectionId: 'col-smoke-existing',
463
+ contractCollectionId: 'col-contract-existing',
464
+ collectionSyncMode: 'refresh',
465
+ }),
466
+ {
467
+ core,
468
+ exec: execStub,
469
+ io: ioStub,
470
+ postman,
471
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
472
+ new Response('openapi: 3.1.0', { status: 200 })
473
+ )
474
+ }
475
+ );
476
+
477
+ expect(postman.generateCollection).toHaveBeenCalledTimes(3);
478
+ expect(result).toMatchObject({
479
+ 'baseline-collection-id': 'col-baseline-refresh',
480
+ 'smoke-collection-id': 'col-smoke-refresh',
481
+ 'contract-collection-id': 'col-contract-refresh'
482
+ });
483
+ });
484
+
485
+ it('version mode reuses the current ref resources.yaml mappings instead of a release manifest', async () => {
428
486
  const { core } = createCoreStub();
429
487
  const execStub = createExecStub();
430
488
  const ioStub = createIoStub();
@@ -434,6 +492,7 @@ describe('bootstrap action', () => {
434
492
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
435
493
  generateCollection: vi.fn(),
436
494
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
495
+ getTeams: vi.fn().mockResolvedValue([]),
437
496
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
438
497
  injectTests: vi.fn().mockResolvedValue(undefined),
439
498
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -442,24 +501,190 @@ describe('bootstrap action', () => {
442
501
  updateSpec: vi.fn().mockResolvedValue(undefined),
443
502
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
444
503
  };
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] ?? '';
504
+ mkdirSync('.postman', { recursive: true });
505
+ writeFileSync(
506
+ '.postman/resources.yaml',
507
+ stringifyYaml({
508
+ workspace: { id: 'ws-existing' },
509
+ cloudResources: {
510
+ specs: { '../index.yaml': 'spec-v111' },
511
+ collections: {
512
+ '../postman/collections/[Baseline] core-payments release-v1.1.1': 'col-baseline-v111',
513
+ '../postman/collections/[Smoke] core-payments release-v1.1.1': 'col-smoke-v111',
514
+ '../postman/collections/[Contract] core-payments release-v1.1.1': 'col-contract-v111'
515
+ }
516
+ }
456
517
  })
518
+ );
519
+ const result = await runBootstrap(
520
+ createInputs({
521
+ workspaceId: 'ws-existing',
522
+ collectionSyncMode: 'version',
523
+ specSyncMode: 'version',
524
+ releaseLabel: 'release/v1.1.1',
525
+ githubRefName: 'release/v1.1.1',
526
+ githubSha: 'deadbeef'
527
+ }),
528
+ {
529
+ core,
530
+ exec: execStub,
531
+ io: ioStub,
532
+ postman,
533
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
534
+ new Response('openapi: 3.1.0', { status: 200 })
535
+ )
536
+ }
537
+ );
538
+
539
+ expect(postman.updateSpec).toHaveBeenCalledWith(
540
+ 'spec-v111',
541
+ 'openapi: 3.1.0',
542
+ 'ws-existing'
543
+ );
544
+ expect(postman.uploadSpec).not.toHaveBeenCalled();
545
+ expect(postman.generateCollection).not.toHaveBeenCalled();
546
+ expect(result['spec-id']).toBe('spec-v111');
547
+ expect(result['baseline-collection-id']).toBe('col-baseline-v111');
548
+ expect(result['smoke-collection-id']).toBe('col-smoke-v111');
549
+ expect(result['contract-collection-id']).toBe('col-contract-v111');
550
+ });
551
+
552
+ it('version mode does not persist asset identifiers to repository variables', async () => {
553
+ const { core } = createCoreStub();
554
+ const execStub = createExecStub();
555
+ const ioStub = createIoStub();
556
+ const postman = {
557
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
558
+ createWorkspace: vi.fn(),
559
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
560
+ generateCollection: vi
561
+ .fn()
562
+ .mockResolvedValueOnce('col-baseline-v111')
563
+ .mockResolvedValueOnce('col-smoke-v111')
564
+ .mockResolvedValueOnce('col-contract-v111'),
565
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
566
+ getTeams: vi.fn().mockResolvedValue([]),
567
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
568
+ injectTests: vi.fn().mockResolvedValue(undefined),
569
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
570
+ tagCollection: vi.fn().mockResolvedValue(undefined),
571
+ uploadSpec: vi.fn().mockResolvedValue('spec-v111'),
572
+ updateSpec: vi.fn().mockResolvedValue(undefined),
573
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
457
574
  };
575
+ await runBootstrap(
576
+ createInputs({
577
+ workspaceId: 'ws-existing',
578
+ collectionSyncMode: 'version',
579
+ specSyncMode: 'version',
580
+ releaseLabel: 'v1.1.1',
581
+ githubRefName: 'v1.1.1'
582
+ }),
583
+ {
584
+ core,
585
+ exec: execStub,
586
+ io: ioStub,
587
+ postman,
588
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
589
+ new Response('openapi: 3.1.0', { status: 200 })
590
+ )
591
+ }
592
+ );
593
+ });
458
594
 
459
- const result = await runBootstrap(createInputs(), {
595
+ it("version mode doesn't fall back to singleton current spec uid", async () => {
596
+ const { core } = createCoreStub();
597
+ const execStub = createExecStub();
598
+ const ioStub = createIoStub();
599
+ const postman = {
600
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
601
+ createWorkspace: vi.fn(),
602
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
603
+ generateCollection: vi
604
+ .fn()
605
+ .mockResolvedValueOnce('col-baseline-v112')
606
+ .mockResolvedValueOnce('col-smoke-v112')
607
+ .mockResolvedValueOnce('col-contract-v112'),
608
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
609
+ getTeams: vi.fn().mockResolvedValue([]),
610
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
611
+ injectTests: vi.fn().mockResolvedValue(undefined),
612
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
613
+ tagCollection: vi.fn().mockResolvedValue(undefined),
614
+ uploadSpec: vi.fn().mockResolvedValue('spec-v112'),
615
+ updateSpec: vi.fn().mockResolvedValue(undefined),
616
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
617
+ };
618
+ const result = await runBootstrap(
619
+ createInputs({
620
+ workspaceId: 'ws-existing',
621
+ collectionSyncMode: 'version',
622
+ specSyncMode: 'version',
623
+ releaseLabel: 'v1.1.2',
624
+ githubRefName: 'v1.1.2'
625
+ }),
626
+ {
627
+ core,
628
+ exec: execStub,
629
+ io: ioStub,
630
+ postman,
631
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
632
+ new Response('openapi: 3.1.0', { status: 200 })
633
+ )
634
+ }
635
+ );
636
+
637
+ expect(postman.updateSpec).not.toHaveBeenCalledWith(
638
+ 'spec-current',
639
+ 'openapi: 3.1.0',
640
+ 'ws-existing'
641
+ );
642
+ expect(postman.uploadSpec).toHaveBeenCalledWith(
643
+ 'ws-existing',
644
+ 'core-payments v1.1.2',
645
+ 'openapi: 3.1.0'
646
+ );
647
+ expect(result['spec-id']).toBe('spec-v112');
648
+ });
649
+
650
+ it('reuses .postman/resources.yaml for reruns before creating new assets', async () => {
651
+ const { core } = createCoreStub();
652
+ const execStub = createExecStub();
653
+ const ioStub = createIoStub();
654
+ const postman = {
655
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
656
+ createWorkspace: vi.fn(),
657
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
658
+ generateCollection: vi.fn(),
659
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
660
+ getTeams: vi.fn().mockResolvedValue([]),
661
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
662
+ injectTests: vi.fn().mockResolvedValue(undefined),
663
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
664
+ tagCollection: vi.fn().mockResolvedValue(undefined),
665
+ uploadSpec: vi.fn(),
666
+ updateSpec: vi.fn().mockResolvedValue(undefined),
667
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
668
+ };
669
+ mkdirSync('.postman', { recursive: true });
670
+ writeFileSync(
671
+ '.postman/resources.yaml',
672
+ stringifyYaml({
673
+ workspace: { id: 'ws-from-file' },
674
+ cloudResources: {
675
+ specs: { '../index.yaml': 'spec-from-file' },
676
+ collections: {
677
+ '../postman/collections/[Baseline] core-payments': 'col-baseline-from-file',
678
+ '../postman/collections/[Smoke] core-payments': 'col-smoke-from-file',
679
+ '../postman/collections/[Contract] core-payments': 'col-contract-from-file'
680
+ }
681
+ }
682
+ })
683
+ );
684
+
685
+ const result = await runBootstrap(createInputs({ collectionSyncMode: 'reuse' }), {
460
686
  core,
461
687
  exec: execStub,
462
- github,
463
688
  io: ioStub,
464
689
  postman,
465
690
  specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
@@ -470,19 +695,17 @@ describe('bootstrap action', () => {
470
695
  expect(postman.createWorkspace).not.toHaveBeenCalled();
471
696
  expect(postman.uploadSpec).not.toHaveBeenCalled();
472
697
  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');
698
+ expect(postman.updateSpec).toHaveBeenCalledWith('spec-from-file', 'openapi: 3.1.0', 'ws-from-file');
476
699
  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'
700
+ 'workspace-id': 'ws-from-file',
701
+ 'spec-id': 'spec-from-file',
702
+ 'baseline-collection-id': 'col-baseline-from-file',
703
+ 'smoke-collection-id': 'col-smoke-from-file',
704
+ 'contract-collection-id': 'col-contract-from-file'
482
705
  });
483
706
  });
484
707
 
485
- it('creates a new workspace when the repo-variable workspace is linked to a different repository', async () => {
708
+ it('ignores repository-variable asset state when .postman/resources.yaml is absent', async () => {
486
709
  const previousRepository = process.env.GITHUB_REPOSITORY;
487
710
  process.env.GITHUB_REPOSITORY = 'postman-cs/bootstrap-action-test';
488
711
 
@@ -502,6 +725,7 @@ describe('bootstrap action', () => {
502
725
  return 'col-contract';
503
726
  }),
504
727
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
728
+ getTeams: vi.fn().mockResolvedValue([]),
505
729
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue('https://github.com/postman-cs/different-repo'),
506
730
  injectTests: vi.fn().mockResolvedValue(undefined),
507
731
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -510,20 +734,9 @@ describe('bootstrap action', () => {
510
734
  updateSpec: vi.fn().mockResolvedValue(undefined),
511
735
  getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
512
736
  };
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
737
  const result = await runBootstrap(createInputs(), {
524
738
  core,
525
739
  exec: execStub,
526
- github,
527
740
  io: ioStub,
528
741
  postman,
529
742
  specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
@@ -531,10 +744,8 @@ describe('bootstrap action', () => {
531
744
  )
532
745
  });
533
746
 
534
- expect(postman.getWorkspaceGitRepoUrl).toHaveBeenCalledWith('ws-from-vars', '12345', 'postman-access-token');
535
747
  expect(postman.createWorkspace).toHaveBeenCalled();
536
748
  expect(result['workspace-id']).toBe('ws-new');
537
- expect(postman.updateSpec).not.toHaveBeenCalled();
538
749
  } finally {
539
750
  if (previousRepository === undefined) {
540
751
  delete process.env.GITHUB_REPOSITORY;
@@ -545,7 +756,7 @@ describe('bootstrap action', () => {
545
756
  });
546
757
 
547
758
  it('skips governance assignment when postman-access-token is absent', async () => {
548
- const { core } = createCoreStub();
759
+ const { core, warnings } = createCoreStub();
549
760
  const execStub = createExecStub();
550
761
  const ioStub = createIoStub();
551
762
  const postman = {
@@ -554,6 +765,7 @@ describe('bootstrap action', () => {
554
765
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
555
766
  generateCollection: vi.fn().mockResolvedValue('col-id'),
556
767
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
768
+ getTeams: vi.fn().mockResolvedValue([]),
557
769
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
558
770
  injectTests: vi.fn().mockResolvedValue(undefined),
559
771
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -578,9 +790,67 @@ describe('bootstrap action', () => {
578
790
 
579
791
  expect(result['workspace-id']).toBe('ws-123');
580
792
  expect(postman.createWorkspace).toHaveBeenCalled();
793
+ expect(
794
+ warnings.some((warning) =>
795
+ warning.includes('Skipping cloud spec-to-collection linking and sync because postman-access-token is not configured')
796
+ )
797
+ ).toBe(true);
798
+ });
799
+
800
+ it('passes syncExamples=false when configured', async () => {
801
+ const { core } = createCoreStub();
802
+ const execStub = createExecStub();
803
+ const ioStub = createIoStub();
804
+ const postman = {
805
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
806
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
807
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
808
+ generateCollection: vi
809
+ .fn()
810
+ .mockResolvedValueOnce('col-baseline')
811
+ .mockResolvedValueOnce('col-smoke')
812
+ .mockResolvedValueOnce('col-contract'),
813
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
814
+ getTeams: vi.fn().mockResolvedValue([]),
815
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
816
+ injectTests: vi.fn().mockResolvedValue(undefined),
817
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
818
+ tagCollection: vi.fn().mockResolvedValue(undefined),
819
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
820
+ updateSpec: vi.fn().mockResolvedValue(undefined),
821
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
822
+ };
823
+ const internalIntegration = {
824
+ assignWorkspaceToGovernanceGroup: vi.fn().mockResolvedValue(undefined),
825
+ linkCollectionsToSpecification: vi.fn().mockResolvedValue(undefined),
826
+ syncCollection: vi.fn().mockResolvedValue(undefined)
827
+ };
828
+
829
+ await runBootstrap(
830
+ createInputs({ syncExamples: false }),
831
+ {
832
+ core,
833
+ exec: execStub,
834
+ io: ioStub,
835
+ internalIntegration,
836
+ postman,
837
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
838
+ new Response('openapi: 3.1.0', { status: 200 })
839
+ )
840
+ }
841
+ );
842
+
843
+ expect(internalIntegration.linkCollectionsToSpecification).toHaveBeenCalledWith(
844
+ 'spec-123',
845
+ [
846
+ { collectionId: 'col-baseline', syncOptions: { syncExamples: false } },
847
+ { collectionId: 'col-smoke', syncOptions: { syncExamples: false } },
848
+ { collectionId: 'col-contract', syncOptions: { syncExamples: false } }
849
+ ]
850
+ );
581
851
  });
582
852
 
583
- it('skips repo variable persistence when github dependency is absent', async () => {
853
+ it('runs without any GitHub dependency', async () => {
584
854
  const { core } = createCoreStub();
585
855
  const execStub = createExecStub();
586
856
  const ioStub = createIoStub();
@@ -590,6 +860,7 @@ describe('bootstrap action', () => {
590
860
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
591
861
  generateCollection: vi.fn().mockResolvedValue('col-id'),
592
862
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
863
+ getTeams: vi.fn().mockResolvedValue([]),
593
864
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
594
865
  injectTests: vi.fn().mockResolvedValue(undefined),
595
866
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -600,7 +871,7 @@ describe('bootstrap action', () => {
600
871
  };
601
872
 
602
873
  const result = await runBootstrap(
603
- createInputs({ githubToken: undefined }),
874
+ createInputs(),
604
875
  {
605
876
  core,
606
877
  exec: execStub,
@@ -616,6 +887,117 @@ describe('bootstrap action', () => {
616
887
  expect(result['spec-id']).toBe('spec-123');
617
888
  });
618
889
 
890
+ it('version mode reuses current resources.yaml collections on the checked-out ref', async () => {
891
+ const { core, infos } = createCoreStub();
892
+ const execStub = createExecStub();
893
+ const ioStub = createIoStub();
894
+ const postman = {
895
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
896
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
897
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
898
+ generateCollection: vi
899
+ .fn()
900
+ .mockImplementation(async (_specId: string, _projectName: string, prefix: string) => {
901
+ if (prefix === '[Baseline]') return 'col-baseline-v2';
902
+ if (prefix === '[Smoke]') return 'col-smoke-v2';
903
+ return 'col-contract-v2';
904
+ }),
905
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
906
+ getTeams: vi.fn().mockResolvedValue([]),
907
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
908
+ injectTests: vi.fn().mockResolvedValue(undefined),
909
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
910
+ tagCollection: vi.fn().mockResolvedValue(undefined),
911
+ uploadSpec: vi.fn().mockResolvedValue('spec-v2'),
912
+ updateSpec: vi.fn().mockResolvedValue(undefined),
913
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
914
+ };
915
+ const resources = {
916
+ workspace: { id: 'ws-current' },
917
+ cloudResources: {
918
+ collections: {
919
+ '../postman/collections/[Baseline] core-payments': 'col-baseline-current',
920
+ '../postman/collections/[Smoke] core-payments': 'col-smoke-current',
921
+ '../postman/collections/[Contract] core-payments': 'col-contract-current'
922
+ }
923
+ }
924
+ };
925
+ mkdirSync('.postman', { recursive: true });
926
+ writeFileSync('.postman/resources.yaml', stringifyYaml(resources));
927
+
928
+ const result = await runBootstrap(
929
+ createInputs({
930
+ collectionSyncMode: 'version',
931
+ specSyncMode: 'version',
932
+ releaseLabel: 'v2.0.0'
933
+ }),
934
+ {
935
+ core,
936
+ exec: execStub,
937
+ io: ioStub,
938
+ postman,
939
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
940
+ new Response('openapi: 3.1.0', { status: 200 })
941
+ )
942
+ }
943
+ );
944
+
945
+ expect(postman.generateCollection).not.toHaveBeenCalled();
946
+ expect(result['baseline-collection-id']).toBe('col-baseline-current');
947
+ expect(result['smoke-collection-id']).toBe('col-smoke-current');
948
+ expect(result['contract-collection-id']).toBe('col-contract-current');
949
+ expect(infos).toContain('Resolved baseline-collection-id from .postman/resources.yaml');
950
+ expect(infos).toContain('Resolved smoke-collection-id from .postman/resources.yaml');
951
+ expect(infos).toContain('Resolved contract-collection-id from .postman/resources.yaml');
952
+ });
953
+
954
+ it('versioned runs do not emit releases-json or write releases.yaml', async () => {
955
+ const { core } = createCoreStub();
956
+ const execStub = createExecStub();
957
+ const ioStub = createIoStub();
958
+ const postman = {
959
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
960
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
961
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
962
+ generateCollection: vi
963
+ .fn()
964
+ .mockImplementation(async (_specId: string, _projectName: string, prefix: string) => {
965
+ if (prefix === '[Baseline]') return 'col-baseline-v2';
966
+ if (prefix === '[Smoke]') return 'col-smoke-v2';
967
+ return 'col-contract-v2';
968
+ }),
969
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
970
+ getTeams: vi.fn().mockResolvedValue([]),
971
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
972
+ injectTests: vi.fn().mockResolvedValue(undefined),
973
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
974
+ tagCollection: vi.fn().mockResolvedValue(undefined),
975
+ uploadSpec: vi.fn().mockResolvedValue('spec-v2'),
976
+ updateSpec: vi.fn().mockResolvedValue(undefined),
977
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
978
+ };
979
+
980
+ const result = await runBootstrap(
981
+ createInputs({
982
+ collectionSyncMode: 'version',
983
+ specSyncMode: 'version',
984
+ releaseLabel: 'v2.0.0'
985
+ }),
986
+ {
987
+ core,
988
+ exec: execStub,
989
+ io: ioStub,
990
+ postman,
991
+ specFetcher: vi.fn<typeof fetch>().mockResolvedValue(
992
+ new Response('openapi: 3.1.0', { status: 200 })
993
+ )
994
+ }
995
+ );
996
+
997
+ expect('releases-json' in result).toBe(false);
998
+ expect(existsSync('.postman/releases.yaml')).toBe(false);
999
+ });
1000
+
619
1001
  it('emits warnings for lint violations but does not fail', async () => {
620
1002
  const { core, warnings } = createCoreStub();
621
1003
  const execStub = createExecStub(
@@ -632,6 +1014,7 @@ describe('bootstrap action', () => {
632
1014
  findWorkspacesByName: vi.fn().mockResolvedValue([]),
633
1015
  generateCollection: vi.fn().mockResolvedValue('col-id'),
634
1016
  getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
1017
+ getTeams: vi.fn().mockResolvedValue([]),
635
1018
  getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
636
1019
  injectTests: vi.fn().mockResolvedValue(undefined),
637
1020
  inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
@@ -729,3 +1112,190 @@ describe('lintSpecViaCli', () => {
729
1112
  });
730
1113
  });
731
1114
  });
1115
+
1116
+ it('fails with team list when org-mode detected and no workspace-team-id', async () => {
1117
+ const { core } = createCoreStub();
1118
+ const execStub = createExecStub();
1119
+ const ioStub = createIoStub();
1120
+ const postman = {
1121
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
1122
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
1123
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
1124
+ generateCollection: vi.fn().mockResolvedValue('col-1'),
1125
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('13347347'),
1126
+ getTeams: vi.fn().mockResolvedValue([
1127
+ { id: 132109, name: 'Field Services', handle: 'fs', organizationId: 13347347 },
1128
+ { id: 132118, name: 'Customer Ed', handle: 'ce', organizationId: 13347347 }
1129
+ ]),
1130
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
1131
+ injectTests: vi.fn().mockResolvedValue(undefined),
1132
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
1133
+ tagCollection: vi.fn().mockResolvedValue(undefined),
1134
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
1135
+ updateSpec: vi.fn().mockResolvedValue(undefined),
1136
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
1137
+ };
1138
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
1139
+ new Response('openapi: 3.1.0', { status: 200 })
1140
+ );
1141
+
1142
+ await expect(
1143
+ runBootstrap(createInputs(), {
1144
+ core,
1145
+ exec: execStub,
1146
+ io: ioStub,
1147
+ postman,
1148
+ specFetcher
1149
+ })
1150
+ ).rejects.toThrow('Org-mode account detected');
1151
+
1152
+ expect(postman.createWorkspace).not.toHaveBeenCalled();
1153
+ });
1154
+
1155
+ it('passes workspace-team-id to createWorkspace for org-mode accounts', async () => {
1156
+ const { core, outputs } = createCoreStub();
1157
+ const execStub = createExecStub();
1158
+ const ioStub = createIoStub();
1159
+ const postman = {
1160
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
1161
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-org' }),
1162
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
1163
+ generateCollection: vi.fn().mockResolvedValue('col-1'),
1164
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('13347347'),
1165
+ getTeams: vi.fn().mockResolvedValue([]),
1166
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
1167
+ injectTests: vi.fn().mockResolvedValue(undefined),
1168
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
1169
+ tagCollection: vi.fn().mockResolvedValue(undefined),
1170
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
1171
+ updateSpec: vi.fn().mockResolvedValue(undefined),
1172
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
1173
+ };
1174
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
1175
+ new Response('openapi: 3.1.0', { status: 200 })
1176
+ );
1177
+
1178
+ await runBootstrap(createInputs({ workspaceTeamId: '132319' }), {
1179
+ core,
1180
+ exec: execStub,
1181
+ io: ioStub,
1182
+ postman,
1183
+ specFetcher
1184
+ });
1185
+
1186
+ expect(postman.createWorkspace).toHaveBeenCalledWith(
1187
+ expect.any(String),
1188
+ expect.any(String),
1189
+ 132319
1190
+ );
1191
+ expect(outputs['workspace-id']).toBe('ws-org');
1192
+ });
1193
+
1194
+ it('skips org-mode detection when workspace-id is already provided', async () => {
1195
+ const { core, outputs } = createCoreStub();
1196
+ const execStub = createExecStub();
1197
+ const ioStub = createIoStub();
1198
+ const postman = {
1199
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
1200
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
1201
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
1202
+ generateCollection: vi.fn().mockResolvedValue('col-1'),
1203
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('13347347'),
1204
+ getTeams: vi.fn().mockResolvedValue([
1205
+ { id: 132109, name: 'FS', handle: 'fs', organizationId: 13347347 },
1206
+ { id: 132118, name: 'CE', handle: 'ce', organizationId: 13347347 }
1207
+ ]),
1208
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
1209
+ injectTests: vi.fn().mockResolvedValue(undefined),
1210
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
1211
+ tagCollection: vi.fn().mockResolvedValue(undefined),
1212
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
1213
+ updateSpec: vi.fn().mockResolvedValue(undefined),
1214
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
1215
+ };
1216
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
1217
+ new Response('openapi: 3.1.0', { status: 200 })
1218
+ );
1219
+
1220
+ await runBootstrap(createInputs({ workspaceId: 'ws-existing' }), {
1221
+ core,
1222
+ exec: execStub,
1223
+ io: ioStub,
1224
+ postman,
1225
+ specFetcher
1226
+ });
1227
+
1228
+ expect(postman.createWorkspace).not.toHaveBeenCalled();
1229
+ expect(outputs['workspace-id']).toBe('ws-existing');
1230
+ });
1231
+
1232
+ it('warns and proceeds when getTeams fails', async () => {
1233
+ const { core } = createCoreStub();
1234
+ const warnings: string[] = [];
1235
+ core.warning = vi.fn((msg: string) => warnings.push(msg));
1236
+ const execStub = createExecStub();
1237
+ const ioStub = createIoStub();
1238
+ const postman = {
1239
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
1240
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
1241
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
1242
+ generateCollection: vi.fn().mockResolvedValue('col-1'),
1243
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
1244
+ getTeams: vi.fn().mockRejectedValue(new Error('Network error')),
1245
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
1246
+ injectTests: vi.fn().mockResolvedValue(undefined),
1247
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
1248
+ tagCollection: vi.fn().mockResolvedValue(undefined),
1249
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
1250
+ updateSpec: vi.fn().mockResolvedValue(undefined),
1251
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
1252
+ };
1253
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
1254
+ new Response('openapi: 3.1.0', { status: 200 })
1255
+ );
1256
+
1257
+ await runBootstrap(createInputs(), {
1258
+ core,
1259
+ exec: execStub,
1260
+ io: ioStub,
1261
+ postman,
1262
+ specFetcher
1263
+ });
1264
+
1265
+ expect(warnings.some(w => w.includes('Could not check for org-mode'))).toBe(true);
1266
+ expect(postman.createWorkspace).toHaveBeenCalled();
1267
+ });
1268
+
1269
+ it('throws when workspace-team-id is non-numeric', async () => {
1270
+ const { core } = createCoreStub();
1271
+ const execStub = createExecStub();
1272
+ const ioStub = createIoStub();
1273
+ const postman = {
1274
+ addAdminsToWorkspace: vi.fn().mockResolvedValue(undefined),
1275
+ createWorkspace: vi.fn().mockResolvedValue({ id: 'ws-123' }),
1276
+ findWorkspacesByName: vi.fn().mockResolvedValue([]),
1277
+ generateCollection: vi.fn().mockResolvedValue('col-1'),
1278
+ getAutoDerivedTeamId: vi.fn().mockResolvedValue('12345'),
1279
+ getTeams: vi.fn().mockResolvedValue([]),
1280
+ getWorkspaceGitRepoUrl: vi.fn().mockResolvedValue(null),
1281
+ injectTests: vi.fn().mockResolvedValue(undefined),
1282
+ inviteRequesterToWorkspace: vi.fn().mockResolvedValue(undefined),
1283
+ tagCollection: vi.fn().mockResolvedValue(undefined),
1284
+ uploadSpec: vi.fn().mockResolvedValue('spec-123'),
1285
+ updateSpec: vi.fn().mockResolvedValue(undefined),
1286
+ getSpecContent: vi.fn().mockResolvedValue('openapi: 3.1.0')
1287
+ };
1288
+ const specFetcher = vi.fn<typeof fetch>().mockResolvedValue(
1289
+ new Response('openapi: 3.1.0', { status: 200 })
1290
+ );
1291
+
1292
+ await expect(
1293
+ runBootstrap(createInputs({ workspaceTeamId: 'not-a-number' }), {
1294
+ core,
1295
+ exec: execStub,
1296
+ io: ioStub,
1297
+ postman,
1298
+ specFetcher
1299
+ })
1300
+ ).rejects.toThrow('workspace-team-id must be a numeric sub-team ID');
1301
+ });