@workos/oagen-emitters 0.12.2 → 0.12.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/plugin.mjs CHANGED
@@ -1,2 +1,2 @@
1
- import { t as workosEmittersPlugin } from "./plugin-eCuvoL1T.mjs";
1
+ import { t as workosEmittersPlugin } from "./plugin-D2N2ZT5W.mjs";
2
2
  export { workosEmittersPlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/oagen-emitters",
3
- "version": "0.12.2",
3
+ "version": "0.12.3",
4
4
  "description": "WorkOS' oagen emitters",
5
5
  "license": "MIT",
6
6
  "author": "WorkOS",
@@ -40,13 +40,13 @@
40
40
  "devDependencies": {
41
41
  "@commitlint/cli": "^21.0.1",
42
42
  "@commitlint/config-conventional": "^21.0.1",
43
- "@types/node": "^25.8.0",
43
+ "@types/node": "^25.9.0",
44
44
  "husky": "^9.1.7",
45
45
  "oxfmt": "^0.50.0",
46
46
  "oxlint": "^1.65.0",
47
47
  "prettier": "^3.8.3",
48
48
  "tsdown": "^0.22.0",
49
- "tsx": "^4.22.0",
49
+ "tsx": "^4.22.2",
50
50
  "typescript": "^6.0.3",
51
51
  "vitest": "^4.1.6"
52
52
  },
@@ -54,6 +54,6 @@
54
54
  "node": ">=24.10.0"
55
55
  },
56
56
  "dependencies": {
57
- "@workos/oagen": "^0.19.0"
57
+ "@workos/oagen": "^0.19.1"
58
58
  }
59
59
  }
package/renovate.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
3
  "extends": [
4
- "config:recommended"
4
+ "github>workos/renovate-config"
5
5
  ],
6
6
  "dependencyDashboard": false,
7
7
  "schedule": [
package/src/node/index.ts CHANGED
@@ -315,10 +315,19 @@ function applyLiveSurface(files: GeneratedFile[], ctx: EmitterContext, surface:
315
315
  // user has hand-tuned, or leave it pointing at sibling files that no
316
316
  // longer have the same shape. Treat existing test/fixture files as
317
317
  // frozen even when they carry the auto-gen header.
318
+ //
319
+ // Adopted-service directories are treated like owned dirs for this
320
+ // purpose: adoption means oagen created the directory from scratch, so
321
+ // by construction there is no hand-written content to preserve and
322
+ // emitting tests/fixtures is safe. Rule (a) still drops files that
323
+ // somehow exist hand-written.
318
324
  if (isUserOwnedAfterFirstEmit(f.path)) {
325
+ const dir = topLevelDir(f.path);
326
+ const isAdoptedDir = dir !== undefined && policy.adoptedServiceDirs.has(dir);
327
+ const isManagedDir = ownedPath || isAdoptedDir;
319
328
  if (surface.files.has(f.path) && !surface.autogenFiles.has(f.path)) continue;
320
- if (!ownedPath && !surface.autogenFiles.has(f.path)) continue;
321
- if (ownedPath && !policy.regenerateOwnedTests) continue;
329
+ if (!isManagedDir && !surface.autogenFiles.has(f.path)) continue;
330
+ if (isManagedDir && !policy.regenerateOwnedTests) continue;
322
331
  }
323
332
 
324
333
  // Previously auto-generated files → fully overwrite so spec changes
@@ -10,6 +10,7 @@ import {
10
10
  resolveInterfaceName,
11
11
  wireInterfaceName,
12
12
  resolveMethodName,
13
+ isAdoptedModelName,
13
14
  } from './naming.js';
14
15
  import {
15
16
  collectFieldDependencies,
@@ -119,6 +120,12 @@ function isSupportedFieldType(
119
120
  if (ref.name === ownerModelName) return true;
120
121
  const resolvedName = resolveInterfaceName(ref.name, ctx);
121
122
  if (ctx.apiSurface?.interfaces?.[resolvedName] || ctx.apiSurface?.typeAliases?.[resolvedName]) return true;
123
+ // Adopted-service models will have their interfaces emitted in this
124
+ // same pass, so the field reference will resolve once writing is done.
125
+ // Without this, fields like `UserManagementLoginRequest.user` get
126
+ // silently dropped on first emission because the target interface
127
+ // (`UserObject` under the adopted `connect/` dir) hasn't landed yet.
128
+ if (isAdoptedModelName(ref.name)) return true;
122
129
  const relPath = `src/${shared.resolveDir(shared.modelToService.get(ref.name))}/interfaces/${fileName(ref.name)}.interface.ts`;
123
130
  return liveSurfaceHasManagedFile(relPath);
124
131
  }
@@ -65,6 +65,9 @@ let adoptedModelNames: Set<string> = new Set();
65
65
  export function setAdoptedModelNames(names: Set<string>): void {
66
66
  adoptedModelNames = names;
67
67
  }
68
+ export function isAdoptedModelName(name: string): boolean {
69
+ return adoptedModelNames.has(name);
70
+ }
68
71
 
69
72
  /**
70
73
  * Wire/response interface name.
@@ -14,8 +14,14 @@ import { fieldName } from './naming.js';
14
14
  * "/orgs" → `'orgs'`
15
15
  * "/orgs/{id}" → `` `orgs/${encodeURIComponent(id)}` ``
16
16
  * "/orgs/{id}/foo" → `` `orgs/${encodeURIComponent(id)}/foo` ``
17
+ *
18
+ * `paramNameMap` lets a caller override the local variable name used for a
19
+ * spec parameter — used by the options-object code path so the URL template
20
+ * references the SDK's public field name (e.g. `organizationMembershipId`)
21
+ * instead of the spec's path-param name (e.g. `omId`), avoiding a
22
+ * destructure rename in the method body.
17
23
  */
18
- export function buildNodePathExpression(rawPath: string): string {
24
+ export function buildNodePathExpression(rawPath: string, paramNameMap?: Map<string, string>): string {
19
25
  const segments = parsePathTemplate(rawPath);
20
26
  if (!hasPathParams(segments)) {
21
27
  return `'${rawPath}'`;
@@ -23,15 +29,16 @@ export function buildNodePathExpression(rawPath: string): string {
23
29
 
24
30
  let body = '';
25
31
  for (const seg of segments) {
26
- body += renderSegment(seg);
32
+ body += renderSegment(seg, paramNameMap);
27
33
  }
28
34
  return `\`${body}\``;
29
35
  }
30
36
 
31
- function renderSegment(seg: PathSegment): string {
37
+ function renderSegment(seg: PathSegment, paramNameMap?: Map<string, string>): string {
32
38
  if (seg.kind === 'literal') {
33
39
  // Template-literal-safe escapes: backtick, backslash, ${
34
40
  return seg.value.replace(/\\/g, '\\\\').replace(/`/g, '\\`').replace(/\$\{/g, '\\${');
35
41
  }
36
- return `\${encodeURIComponent(${fieldName(seg.name)})}`;
42
+ const localName = paramNameMap?.get(seg.name) ?? fieldName(seg.name);
43
+ return `\${encodeURIComponent(${localName})}`;
37
44
  }
@@ -1242,8 +1242,8 @@ function renderOptionsObjectMethod(
1242
1242
  if (!optionParam) return false;
1243
1243
 
1244
1244
  const responseModel = plan.responseModelName ? resolveInterfaceName(plan.responseModelName, ctx) : null;
1245
- const pathStr = buildPathStr(op);
1246
1245
  const pathBindings = buildOptionsObjectPathBindings(op, optionParam.type, ctx);
1246
+ const pathStr = buildPathStr(op, buildOptionsObjectPathParamMap(op, optionParam.type, ctx));
1247
1247
 
1248
1248
  if (plan.isPaginated && op.pagination && op.httpMethod === 'get') {
1249
1249
  let itemRawName = op.pagination.itemType.kind === 'model' ? op.pagination.itemType.name : null;
@@ -1374,11 +1374,27 @@ function renderOptionsObjectDestructure(lines: string[], pathBindings: string[],
1374
1374
  }
1375
1375
 
1376
1376
  function buildOptionsObjectPathBindings(op: Operation, optionType: string, ctx: EmitterContext): string[] {
1377
- return op.pathParams.map((param) => {
1377
+ // Return resolved SDK field names directly — the URL template uses these
1378
+ // names too (via the param-name map threaded into buildNodePathExpression),
1379
+ // so the destructure no longer needs `optionField: localName` renames.
1380
+ return op.pathParams.map((param) => resolveOptionsObjectField(fieldName(param.name), optionType, ctx));
1381
+ }
1382
+
1383
+ /**
1384
+ * Map spec path-param names (e.g. `omId`) to the SDK field name exposed on
1385
+ * the options interface (e.g. `organizationMembershipId`). Empty when every
1386
+ * path param's spec name already matches the SDK field. Consumed by
1387
+ * `buildNodePathExpression` so the URL template binds to the same identifier
1388
+ * the destructure does.
1389
+ */
1390
+ function buildOptionsObjectPathParamMap(op: Operation, optionType: string, ctx: EmitterContext): Map<string, string> {
1391
+ const map = new Map<string, string>();
1392
+ for (const param of op.pathParams) {
1378
1393
  const localName = fieldName(param.name);
1379
- const optionField = resolveOptionsObjectField(localName, optionType, ctx);
1380
- return optionField === localName ? localName : `${optionField}: ${localName}`;
1381
- });
1394
+ const sdkField = resolveOptionsObjectField(localName, optionType, ctx);
1395
+ if (sdkField !== localName) map.set(param.name, sdkField);
1396
+ }
1397
+ return map;
1382
1398
  }
1383
1399
 
1384
1400
  function resolveOptionsObjectField(localName: string, optionType: string, ctx: EmitterContext): string {
@@ -1826,8 +1842,8 @@ function renderQueryExpr(queryParams: { name: string; required: boolean }[]): st
1826
1842
  return `options ? { ${parts.join(', ')} } : undefined`;
1827
1843
  }
1828
1844
 
1829
- function buildPathStr(op: Operation): string {
1830
- return buildNodePathExpression(op.path);
1845
+ function buildPathStr(op: Operation, paramNameMap?: Map<string, string>): string {
1846
+ return buildNodePathExpression(op.path, paramNameMap);
1831
1847
  }
1832
1848
 
1833
1849
  function buildPathParams(op: Operation, specEnumNames?: Set<string>): string {
package/src/node/tests.ts CHANGED
@@ -110,6 +110,15 @@ export function generateTests(spec: ApiSpec, ctx: EmitterContext): GeneratedFile
110
110
  const propName = mountAccessors.get(mountName) ?? servicePropertyName(mountName);
111
111
  if (ctx.apiSurface && baselineWorkOSProps.size > 0 && !baselineWorkOSProps.has(propName)) continue;
112
112
 
113
+ // Skip when the resource class diverges from the mount accessor — this
114
+ // happens for services with constructor-incompatible baselines (e.g.
115
+ // hand-written `Webhooks(crypto)` forks the emitted ops onto
116
+ // `WebhooksEndpoints`). The generated test would do
117
+ // `workos.<propName>.fooMethod(...)`, but those methods live on a
118
+ // different class, so the test would fail to compile.
119
+ const resourceClass = resolveResourceClassName(mergedService, ctx);
120
+ if (resourceClass !== mountName) continue;
121
+
113
122
  const testService = ops.length < operations.length ? { ...mergedService, operations: ops } : mergedService;
114
123
  files.push(generateServiceTest(testService, spec, ctx, modelMap, mountAccessors));
115
124
  }
@@ -409,8 +418,12 @@ function renderBodyTest(
409
418
  const optionsArg = buildOptionsObjectTestArg(op, plan, baselineMethod, modelMap, ctx);
410
419
  const allArgs = optionsArg ?? (pathArgs ? `${pathArgs}, ${payloadArg}` : payloadArg);
411
420
 
421
+ const isArrayResponse = !!plan.isArrayResponse;
422
+ const fixtureExpr = isArrayResponse ? `[${fixture}]` : fixture;
423
+ const accessor = isArrayResponse ? 'result[0]' : 'result';
424
+
412
425
  lines.push(" it('sends the correct request and returns result', async () => {");
413
- lines.push(` fetchOnce(${fixture});`);
426
+ lines.push(` fetchOnce(${fixtureExpr});`);
414
427
  lines.push('');
415
428
  lines.push(` const result = await workos.${serviceProp}.${method}(${allArgs});`);
416
429
  lines.push('');
@@ -427,14 +440,18 @@ function renderBodyTest(
427
440
  lines.push(' expect(fetchBody()).toBeDefined();');
428
441
  }
429
442
 
443
+ if (isArrayResponse) {
444
+ lines.push(' expect(Array.isArray(result)).toBe(true);');
445
+ }
446
+
430
447
  // Use entity helper if available, otherwise inline assertions
431
448
  const bodyHelperName = ctx ? `expect${resolveInterfaceName(responseModelName, ctx)}` : null;
432
449
  if (bodyHelperName && entityHelpers?.has(bodyHelperName)) {
433
- lines.push(` ${bodyHelperName}(result);`);
450
+ lines.push(` ${bodyHelperName}(${accessor});`);
434
451
  } else {
435
452
  const responseModel = modelMap.get(responseModelName);
436
453
  if (responseModel) {
437
- const assertions = buildFieldAssertions(responseModel, 'result', modelMap);
454
+ const assertions = buildFieldAssertions(responseModel, accessor, modelMap);
438
455
  if (assertions.length > 0) {
439
456
  for (const assertion of assertions) {
440
457
  lines.push(` ${assertion}`);
@@ -466,8 +483,12 @@ function renderGetTest(
466
483
  const pathArgs = buildTestPathArgs(op);
467
484
  const optionsArg = buildOptionsObjectTestArg(op, plan, baselineMethod, modelMap, ctx);
468
485
 
486
+ const isArrayResponse = !!plan.isArrayResponse;
487
+ const fixtureExpr = isArrayResponse ? `[${fixture}]` : fixture;
488
+ const accessor = isArrayResponse ? 'result[0]' : 'result';
489
+
469
490
  lines.push(" it('returns the expected result', async () => {");
470
- lines.push(` fetchOnce(${fixture});`);
491
+ lines.push(` fetchOnce(${fixtureExpr});`);
471
492
  lines.push('');
472
493
  lines.push(` const result = await workos.${serviceProp}.${method}(${optionsArg ?? pathArgs});`);
473
494
  lines.push('');
@@ -475,15 +496,18 @@ function renderGetTest(
475
496
  // Fix #12: Full URL path assertion instead of toContain()
476
497
  const expectedPathGet = buildExpectedPath(op);
477
498
  lines.push(` expect(new URL(String(fetchURL())).pathname).toBe('${expectedPathGet}');`);
499
+ if (isArrayResponse) {
500
+ lines.push(' expect(Array.isArray(result)).toBe(true);');
501
+ }
478
502
 
479
503
  // Use entity helper if available, otherwise inline assertions
480
504
  const helperName = ctx ? `expect${resolveInterfaceName(responseModelName, ctx)}` : null;
481
505
  if (helperName && entityHelpers?.has(helperName)) {
482
- lines.push(` ${helperName}(result);`);
506
+ lines.push(` ${helperName}(${accessor});`);
483
507
  } else {
484
508
  const responseModel = modelMap.get(responseModelName);
485
509
  if (responseModel) {
486
- const assertions = buildFieldAssertions(responseModel, 'result', modelMap);
510
+ const assertions = buildFieldAssertions(responseModel, accessor, modelMap);
487
511
  if (assertions.length > 0) {
488
512
  for (const assertion of assertions) {
489
513
  lines.push(` ${assertion}`);
@@ -636,27 +660,37 @@ function generateEntityHelpers(
636
660
  * nested models so we still get meaningful assertions instead of a bare
637
661
  * `toBeDefined()`.
638
662
  */
663
+ function isDateTimeFieldType(type: TypeRef): boolean {
664
+ if (type.kind === 'primitive') return type.format === 'date-time';
665
+ if (type.kind === 'nullable') return isDateTimeFieldType(type.inner);
666
+ return false;
667
+ }
668
+
639
669
  function buildFieldAssertions(model: Model, accessor: string, modelMap?: Map<string, Model>): string[] {
640
670
  const assertions: string[] = [];
641
671
 
642
672
  for (const field of model.fields) {
643
673
  if (!field.required) continue;
674
+ const domainField = fieldName(field.name);
675
+ // `string` + `format: 'date-time'` is deserialized to `Date` by the
676
+ // serializer (see `mapPrimitive` in type-map.ts). Asserting against a
677
+ // string literal would fail Object.is — compare via `.toISOString()`.
678
+ const isDateTime = isDateTimeFieldType(field.type);
679
+ const fieldAccessor = isDateTime ? `${accessor}.${domainField}.toISOString()` : `${accessor}.${domainField}`;
644
680
  // When a field has an example value, use it as the expected assertion value
645
681
  if (field.example !== undefined) {
646
- const domainField = fieldName(field.name);
647
682
  if (typeof field.example === 'object' && field.example !== null) {
648
683
  // Objects and arrays need toEqual with JSON serialization
649
684
  assertions.push(`expect(${accessor}.${domainField}).toEqual(${JSON.stringify(field.example)});`);
650
685
  } else {
651
686
  const exampleLiteral = typeof field.example === 'string' ? `'${field.example}'` : String(field.example);
652
- assertions.push(`expect(${accessor}.${domainField}).toBe(${exampleLiteral});`);
687
+ assertions.push(`expect(${fieldAccessor}).toBe(${exampleLiteral});`);
653
688
  }
654
689
  continue;
655
690
  }
656
691
  const value = fixtureValueForType(field.type, field.name, model.name);
657
692
  if (value === null) continue;
658
- const domainField = fieldName(field.name);
659
- assertions.push(`expect(${accessor}.${domainField}).toBe(${value});`);
693
+ assertions.push(`expect(${fieldAccessor}).toBe(${value});`);
660
694
  }
661
695
 
662
696
  // When no primitive assertions were found (e.g. wrapper types like
@@ -176,6 +176,76 @@ describe('generateResources', () => {
176
176
  expect(resourceFile!.content).toContain('async listGroupsForOrganizationMembership');
177
177
  });
178
178
 
179
+ it('options-object: URL template binds to the SDK field name, not the spec path-param name', () => {
180
+ // When the spec uses `omId` as a path-param name but the baseline options
181
+ // interface exposes `organizationMembershipId`, both the destructure and
182
+ // the URL template should reference `organizationMembershipId` directly —
183
+ // no `organizationMembershipId: omId` rename indirection in the body.
184
+ const operation = {
185
+ name: 'removeOrganizationMembership',
186
+ httpMethod: 'delete' as const,
187
+ path: '/organizations/{organizationId}/groups/{groupId}/organization-memberships/{omId}',
188
+ pathParams: [
189
+ { name: 'organizationId', type: { kind: 'primitive' as const, type: 'string' as const }, required: true },
190
+ { name: 'groupId', type: { kind: 'primitive' as const, type: 'string' as const }, required: true },
191
+ { name: 'omId', type: { kind: 'primitive' as const, type: 'string' as const }, required: true },
192
+ ],
193
+ queryParams: [],
194
+ headerParams: [],
195
+ response: { kind: 'primitive' as const, type: 'unknown' as const },
196
+ errors: [],
197
+ injectIdempotencyKey: false,
198
+ };
199
+ const service: Service = { name: 'Groups', operations: [operation] };
200
+ const spec: ApiSpec = { ...emptySpec, services: [service] };
201
+ const ctxWithBaseline: EmitterContext = {
202
+ ...ctx,
203
+ spec,
204
+ emitterOptions: { ownedServices: ['Groups'] },
205
+ apiSurface: {
206
+ classes: {
207
+ Groups: {
208
+ constructorParams: [{ name: 'workos', type: 'WorkOS' }],
209
+ methods: {
210
+ removeOrganizationMembership: [
211
+ {
212
+ name: 'removeOrganizationMembership',
213
+ params: [
214
+ {
215
+ name: 'options',
216
+ type: 'RemoveGroupOrganizationMembershipOptions',
217
+ passingStyle: 'options_object',
218
+ },
219
+ ],
220
+ returnType: 'Promise<void>',
221
+ async: true,
222
+ },
223
+ ],
224
+ },
225
+ },
226
+ },
227
+ interfaces: {
228
+ RemoveGroupOrganizationMembershipOptions: {
229
+ fields: {
230
+ organizationId: { type: 'string', required: true },
231
+ groupId: { type: 'string', required: true },
232
+ organizationMembershipId: { type: 'string', required: true },
233
+ },
234
+ },
235
+ },
236
+ } as any,
237
+ };
238
+
239
+ const result = generateResources([service], ctxWithBaseline);
240
+ const resourceFile = result.find((f) => f.path === 'src/groups/groups.ts');
241
+ expect(resourceFile).toBeDefined();
242
+ const content = resourceFile!.content;
243
+ expect(content).toContain('const { organizationId, groupId, organizationMembershipId } = options;');
244
+ expect(content).toContain('${encodeURIComponent(organizationMembershipId)}');
245
+ expect(content).not.toContain('organizationMembershipId: omId');
246
+ expect(content).not.toContain('encodeURIComponent(omId)');
247
+ });
248
+
179
249
  it('drops brand-new service paths in an existing SDK by default', () => {
180
250
  const tmpRoot = createTrackedSdkRoot();
181
251
  try {
@@ -116,4 +116,99 @@ describe('node test generation ownership', () => {
116
116
  fs.rmSync(tmpRoot, { recursive: true, force: true });
117
117
  }
118
118
  });
119
+
120
+ it('emits tests and fixtures for adopted services when regenerateOwnedTests is true', () => {
121
+ // Adopted dirs are created by oagen from scratch — no hand-written content
122
+ // to preserve, so scaffolding tests/fixtures is safe and useful.
123
+ const tmpRoot = createTrackedSdkRoot();
124
+ try {
125
+ const result = nodeEmitter.generateTests!(spec, {
126
+ ...ctx,
127
+ outputDir: tmpRoot,
128
+ emitterOptions: { adoptMissingServices: true, regenerateOwnedTests: true },
129
+ } as EmitterContext);
130
+
131
+ const testFile = result.find((f) => f.path === 'src/groups/groups.spec.ts');
132
+ const fixtureFile = result.find((f) => f.path === 'src/groups/fixtures/group.json');
133
+ expect(testFile).toBeDefined();
134
+ expect(fixtureFile).toBeDefined();
135
+ } finally {
136
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
137
+ }
138
+ });
139
+
140
+ it('skips tests for adopted services when regenerateOwnedTests is false', () => {
141
+ const tmpRoot = createTrackedSdkRoot();
142
+ try {
143
+ const result = nodeEmitter.generateTests!(spec, {
144
+ ...ctx,
145
+ outputDir: tmpRoot,
146
+ emitterOptions: { adoptMissingServices: true },
147
+ } as EmitterContext);
148
+
149
+ expect(result.some((f) => f.path.startsWith('src/groups/'))).toBe(false);
150
+ } finally {
151
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
152
+ }
153
+ });
154
+
155
+ it('skips tests when the resource class diverges from the mount accessor', () => {
156
+ // Hand-written `Webhooks` has a constructor incompatible with WorkOS,
157
+ // so the emitter forks endpoint methods onto a `WebhooksEndpoints`
158
+ // helper class. A generated test would call `workos.webhooks.foo(...)`
159
+ // — but those methods live on the helper, not the crypto class. Skip
160
+ // these tests so we don't ship a spec that fails to compile.
161
+ const webhookOp = {
162
+ name: 'listWebhookEndpoints',
163
+ httpMethod: 'get' as const,
164
+ path: '/webhook_endpoints',
165
+ pathParams: [],
166
+ queryParams: [],
167
+ headerParams: [],
168
+ response: { kind: 'primitive' as const, type: 'unknown' as const },
169
+ errors: [],
170
+ injectIdempotencyKey: false,
171
+ };
172
+ const webhookService: Service = { name: 'Webhooks', operations: [webhookOp] };
173
+ const webhookSpec: ApiSpec = {
174
+ ...spec,
175
+ services: [webhookService],
176
+ };
177
+
178
+ const tmpRoot = createTrackedSdkRoot();
179
+ try {
180
+ const result = nodeEmitter.generateTests!(webhookSpec, {
181
+ ...ctx,
182
+ spec: webhookSpec,
183
+ outputDir: tmpRoot,
184
+ emitterOptions: { adoptMissingServices: true, regenerateOwnedTests: true },
185
+ apiSurface: {
186
+ classes: {
187
+ Webhooks: {
188
+ constructorParams: [{ name: 'crypto', type: 'CryptoProvider' }],
189
+ },
190
+ WorkOS: {
191
+ properties: { webhooks: { type: 'Webhooks' } },
192
+ },
193
+ },
194
+ },
195
+ resolvedOperations: [
196
+ {
197
+ operation: webhookOp,
198
+ service: webhookService,
199
+ methodName: 'list_webhook_endpoints',
200
+ mountOn: 'Webhooks',
201
+ defaults: {},
202
+ inferFromClient: [],
203
+ urlBuilder: false,
204
+ },
205
+ ],
206
+ } as unknown as EmitterContext);
207
+
208
+ expect(result.some((f) => f.path === 'src/webhooks/webhooks-endpoints.spec.ts')).toBe(false);
209
+ expect(result.some((f) => f.path === 'src/webhooks/webhooks.spec.ts')).toBe(false);
210
+ } finally {
211
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
212
+ }
213
+ });
119
214
  });