@workos/oagen-emitters 0.12.1 → 0.12.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/.github/workflows/ci.yml +1 -1
  2. package/.github/workflows/lint-pr-title.yml +1 -1
  3. package/.github/workflows/lint.yml +1 -1
  4. package/.github/workflows/release-please.yml +2 -2
  5. package/.github/workflows/release.yml +1 -1
  6. package/.node-version +1 -1
  7. package/.release-please-manifest.json +1 -1
  8. package/CHANGELOG.md +7 -0
  9. package/dist/index.d.mts.map +1 -1
  10. package/dist/index.mjs +1 -1
  11. package/dist/{plugin-CmfzawTp.mjs → plugin-eCuvoL1T.mjs} +2508 -1474
  12. package/dist/plugin-eCuvoL1T.mjs.map +1 -0
  13. package/dist/plugin.mjs +1 -1
  14. package/package.json +6 -6
  15. package/renovate.json +46 -6
  16. package/src/node/client.ts +19 -32
  17. package/src/node/enums.ts +67 -30
  18. package/src/node/errors.ts +2 -8
  19. package/src/node/field-plan.ts +188 -52
  20. package/src/node/fixtures.ts +11 -33
  21. package/src/node/index.ts +345 -20
  22. package/src/node/live-surface.ts +378 -0
  23. package/src/node/models.ts +540 -351
  24. package/src/node/naming.ts +119 -25
  25. package/src/node/node-overrides.ts +77 -0
  26. package/src/node/options.ts +41 -0
  27. package/src/node/resources.ts +455 -46
  28. package/src/node/sdk-errors.ts +0 -16
  29. package/src/node/tests.ts +108 -83
  30. package/src/node/type-map.ts +40 -18
  31. package/src/node/utils.ts +89 -102
  32. package/src/node/wrappers.ts +0 -20
  33. package/test/node/client.test.ts +106 -1201
  34. package/test/node/enums.test.ts +59 -130
  35. package/test/node/errors.test.ts +2 -3
  36. package/test/node/live-surface.test.ts +240 -0
  37. package/test/node/models.test.ts +396 -765
  38. package/test/node/naming.test.ts +69 -234
  39. package/test/node/resources.test.ts +376 -2036
  40. package/test/node/tests.test.ts +119 -0
  41. package/test/node/type-map.test.ts +49 -54
  42. package/test/node/utils.test.ts +29 -80
  43. package/dist/plugin-CmfzawTp.mjs.map +0 -1
  44. package/test/node/serializers.test.ts +0 -444
@@ -1,7 +1,14 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { generateModels } from '../../src/node/models.js';
3
- import type { EmitterContext, ApiSpec, Model, Service } from '@workos/oagen';
2
+ import type { EmitterContext, ApiSpec, Model } from '@workos/oagen';
4
3
  import { defaultSdkBehavior } from '@workos/oagen';
4
+ import { generateModels, generateSerializers } from '../../src/node/models.js';
5
+ import { nodeEmitter } from '../../src/node/index.js';
6
+ import { buildLiveSurface, emptyLiveSurface, setActiveLiveSurface } from '../../src/node/live-surface.js';
7
+ import { setBaselineInterfaceNames, setBaselineSerializedNames } from '../../src/node/naming.js';
8
+ import * as fs from 'node:fs';
9
+ import * as os from 'node:os';
10
+ import * as path from 'node:path';
11
+ import { execFileSync } from 'node:child_process';
5
12
 
6
13
  const emptySpec: ApiSpec = {
7
14
  name: 'Test',
@@ -19,912 +26,536 @@ const ctx: EmitterContext = {
19
26
  spec: emptySpec,
20
27
  };
21
28
 
29
+ function makeSpec(models: Model[], services?: any[]): ApiSpec {
30
+ return {
31
+ ...emptySpec,
32
+ models,
33
+ services: services ?? [
34
+ {
35
+ name: 'Organizations',
36
+ operations: [
37
+ {
38
+ name: 'getOrganization',
39
+ httpMethod: 'get',
40
+ path: '/organizations/{id}',
41
+ pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
42
+ queryParams: [],
43
+ headerParams: [],
44
+ response: { kind: 'model', name: 'Organization' },
45
+ errors: [],
46
+ injectIdempotencyKey: false,
47
+ },
48
+ ],
49
+ },
50
+ ],
51
+ };
52
+ }
53
+
22
54
  describe('generateModels', () => {
23
55
  it('returns empty for no models', () => {
24
56
  expect(generateModels([], ctx)).toEqual([]);
25
57
  });
26
58
 
27
59
  it('generates domain and response interfaces for a model', () => {
28
- const service: Service = {
29
- name: 'Organizations',
30
- operations: [
31
- {
32
- name: 'getOrganization',
33
- httpMethod: 'get',
34
- path: '/organizations/{id}',
35
- pathParams: [
36
- {
37
- name: 'id',
38
- type: { kind: 'primitive', type: 'string' },
39
- required: true,
40
- },
41
- ],
42
- queryParams: [],
43
- headerParams: [],
44
- response: { kind: 'model', name: 'Organization' },
45
- errors: [],
46
- injectIdempotencyKey: false,
47
- },
48
- ],
49
- };
50
-
51
60
  const models: Model[] = [
52
61
  {
53
62
  name: 'Organization',
54
63
  fields: [
55
- {
56
- name: 'id',
57
- type: { kind: 'primitive', type: 'string' },
58
- required: true,
59
- },
60
- {
61
- name: 'name',
62
- type: { kind: 'primitive', type: 'string' },
63
- required: true,
64
- },
65
- {
66
- name: 'created_at',
67
- type: { kind: 'primitive', type: 'string', format: 'date-time' },
68
- required: true,
69
- },
64
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
65
+ { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
66
+ { name: 'created_at', type: { kind: 'primitive', type: 'string', format: 'date-time' }, required: true },
70
67
  {
71
68
  name: 'external_id',
72
- type: {
73
- kind: 'nullable',
74
- inner: { kind: 'primitive', type: 'string' },
75
- },
76
- required: false,
69
+ type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } },
70
+ required: true,
77
71
  },
78
72
  ],
79
73
  },
80
74
  ];
81
75
 
82
- const ctxWithServices: EmitterContext = {
83
- ...ctx,
84
- spec: { ...emptySpec, services: [service], models },
85
- };
76
+ const spec = makeSpec(models);
77
+ const ctxWithModels: EmitterContext = { ...ctx, spec };
78
+ const result = generateModels(models, ctxWithModels);
86
79
 
87
- const files = generateModels(models, ctxWithServices);
88
- expect(files.length).toBe(1);
89
- expect(files[0].path).toBe('src/organizations/interfaces/organization.interface.ts');
80
+ expect(result.length).toBeGreaterThan(0);
81
+ const file = result[0];
82
+ expect(file.path).toBe('src/organizations/interfaces/organization.interface.ts');
90
83
 
91
84
  // Domain interface has camelCase fields
92
- expect(files[0].content).toContain('export interface Organization {');
93
- expect(files[0].content).toContain(' id: string;');
94
- expect(files[0].content).toContain(' name: string;');
95
- expect(files[0].content).toContain(' createdAt: Date;');
96
- expect(files[0].content).toContain(' externalId?: string | null;');
97
-
98
- // Response interface has snake_case fields
99
- expect(files[0].content).toContain('export interface OrganizationResponse {');
100
- expect(files[0].content).toContain(' created_at: string;');
101
- expect(files[0].content).toContain(' external_id?: string | null;');
85
+ expect(file.content).toContain('export interface Organization {');
86
+ expect(file.content).toContain('id: string;');
87
+ expect(file.content).toContain('name: string;');
88
+ expect(file.content).toContain('createdAt: Date;');
89
+ expect(file.content).toContain('externalId: string | null;');
90
+
91
+ // Wire interface has snake_case fields
92
+ expect(file.content).toContain('export interface OrganizationResponse {');
93
+ expect(file.content).toContain('created_at: string;');
94
+ expect(file.content).toContain('external_id: string | null;');
102
95
  });
103
96
 
104
97
  it('generates imports for referenced models', () => {
105
- const service: Service = {
106
- name: 'Organizations',
107
- operations: [
108
- {
109
- name: 'getOrganization',
110
- httpMethod: 'get',
111
- path: '/organizations/{id}',
112
- pathParams: [
113
- {
114
- name: 'id',
115
- type: { kind: 'primitive', type: 'string' },
116
- required: true,
117
- },
118
- ],
119
- queryParams: [],
120
- headerParams: [],
121
- response: { kind: 'model', name: 'Organization' },
122
- errors: [],
123
- injectIdempotencyKey: false,
124
- },
125
- ],
126
- };
127
-
128
98
  const models: Model[] = [
129
99
  {
130
100
  name: 'Organization',
131
101
  fields: [
132
- {
133
- name: 'id',
134
- type: { kind: 'primitive', type: 'string' },
135
- required: true,
136
- },
137
- {
138
- name: 'domains',
139
- type: {
140
- kind: 'array',
141
- items: { kind: 'model', name: 'OrganizationDomain' },
142
- },
143
- required: true,
144
- },
102
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
103
+ { name: 'domain', type: { kind: 'model', name: 'OrganizationDomain' }, required: true },
145
104
  ],
146
105
  },
147
106
  {
148
107
  name: 'OrganizationDomain',
149
- fields: [
150
- {
151
- name: 'id',
152
- type: { kind: 'primitive', type: 'string' },
153
- required: true,
154
- },
155
- {
156
- name: 'domain',
157
- type: { kind: 'primitive', type: 'string' },
158
- required: true,
159
- },
160
- ],
108
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
161
109
  },
162
110
  ];
163
111
 
164
- const ctxWithServices: EmitterContext = {
165
- ...ctx,
166
- spec: { ...emptySpec, services: [service], models },
167
- };
168
-
169
- const files = generateModels(models, ctxWithServices);
112
+ const spec = makeSpec(models);
113
+ const ctxWithModels: EmitterContext = { ...ctx, spec };
114
+ const result = generateModels(models, ctxWithModels);
170
115
 
171
- // Organization file should import OrganizationDomain
172
- const orgFile = files.find((f) => f.path.includes('organization.interface.ts'))!;
173
- expect(orgFile.content).toContain(
116
+ const orgFile = result.find((f) => f.path.includes('organization.interface.ts'));
117
+ expect(orgFile?.content).toContain(
174
118
  "import type { OrganizationDomain, OrganizationDomainResponse } from './organization-domain.interface';",
175
119
  );
176
-
177
- // Domain interface uses OrganizationDomain[]
178
- expect(orgFile.content).toContain(' domains: OrganizationDomain[];');
179
-
180
- // Response interface uses OrganizationDomainResponse[]
181
- expect(orgFile.content).toContain(' domains: OrganizationDomainResponse[];');
182
120
  });
183
121
 
184
- it('handles generic type params', () => {
185
- const service: Service = {
186
- name: 'DirectorySync',
187
- operations: [
188
- {
189
- name: 'getDirectoryUser',
190
- httpMethod: 'get',
191
- path: '/directory_users/{id}',
192
- pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
193
- queryParams: [],
194
- headerParams: [],
195
- response: { kind: 'model', name: 'DirectoryUser' },
196
- errors: [],
197
- injectIdempotencyKey: false,
198
- },
199
- ],
200
- };
201
-
122
+ it('uses Wire suffix for models already ending in Response', () => {
202
123
  const models: Model[] = [
203
124
  {
204
- name: 'DirectoryUser',
205
- typeParams: [
206
- {
207
- name: 'TCustom',
208
- default: {
209
- kind: 'map',
210
- valueType: { kind: 'primitive', type: 'unknown' },
211
- },
212
- },
213
- ],
214
- fields: [
215
- {
216
- name: 'id',
217
- type: { kind: 'primitive', type: 'string' },
218
- required: true,
219
- },
220
- ],
125
+ name: 'PortalSessionsCreateResponse',
126
+ fields: [{ name: 'link', type: { kind: 'primitive', type: 'string' }, required: true }],
221
127
  },
222
128
  ];
223
129
 
224
- const ctxWithServices: EmitterContext = {
225
- ...ctx,
226
- spec: { ...emptySpec, services: [service], models },
227
- };
228
-
229
- const files = generateModels(models, ctxWithServices);
230
- expect(files[0].content).toContain('export interface DirectoryUser<TCustom = Record<string, any>> {');
231
- expect(files[0].content).toContain('export interface DirectoryUserResponse<TCustom = Record<string, any>> {');
232
- });
233
-
234
- it('uses Wire suffix for models already ending in Response', () => {
235
- const service: Service = {
236
- name: 'PortalSessions',
237
- operations: [
238
- {
239
- name: 'createPortalSession',
240
- httpMethod: 'post',
241
- path: '/portal/sessions',
242
- pathParams: [],
243
- queryParams: [],
244
- headerParams: [],
245
- response: { kind: 'model', name: 'PortalSessionsCreateResponse' },
246
- errors: [],
247
- injectIdempotencyKey: false,
248
- },
249
- ],
250
- };
251
-
252
- const models: Model[] = [
130
+ const spec = makeSpec(models, [
253
131
  {
254
- name: 'PortalSessionsCreateResponse',
255
- fields: [
256
- {
257
- name: 'link',
258
- type: { kind: 'primitive', type: 'string' },
259
- required: true,
132
+ name: 'Portal',
133
+ operations: [
134
+ {
135
+ name: 'createSession',
136
+ httpMethod: 'post',
137
+ path: '/portal/sessions',
138
+ pathParams: [],
139
+ queryParams: [],
140
+ headerParams: [],
141
+ response: { kind: 'model', name: 'PortalSessionsCreateResponse' },
142
+ errors: [],
143
+ injectIdempotencyKey: false,
260
144
  },
261
145
  ],
262
146
  },
263
- ];
147
+ ]);
148
+ const ctxWithModels: EmitterContext = { ...ctx, spec };
149
+ const result = generateModels(models, ctxWithModels);
264
150
 
265
- const ctxWithServices: EmitterContext = {
266
- ...ctx,
267
- spec: { ...emptySpec, services: [service], models },
268
- };
269
-
270
- const files = generateModels(models, ctxWithServices);
271
- const content = files[0].content;
272
-
273
- // Should use Wire suffix, not ResponseResponse
274
- expect(content).toContain('export interface PortalSessionsCreateResponseWire {');
275
- expect(content).not.toContain('PortalSessionsCreateResponseResponse');
151
+ const file = result[0];
152
+ expect(file.content).toContain('export interface PortalSessionsCreateResponseWire {');
276
153
  });
277
154
 
278
155
  it('renders @deprecated on fields', () => {
279
- const service: Service = {
280
- name: 'Organizations',
281
- operations: [
282
- {
283
- name: 'getOrganization',
284
- httpMethod: 'get',
285
- path: '/organizations/{id}',
286
- pathParams: [
287
- {
288
- name: 'id',
289
- type: { kind: 'primitive', type: 'string' },
290
- required: true,
291
- },
292
- ],
293
- queryParams: [],
294
- headerParams: [],
295
- response: { kind: 'model', name: 'Organization' },
296
- errors: [],
297
- injectIdempotencyKey: false,
298
- },
299
- ],
300
- };
301
-
302
156
  const models: Model[] = [
303
157
  {
304
158
  name: 'Organization',
305
159
  fields: [
160
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
161
+ { name: 'old_field', type: { kind: 'primitive', type: 'string' }, required: false, deprecated: true },
306
162
  {
307
- name: 'id',
308
- type: { kind: 'primitive', type: 'string' },
309
- required: true,
310
- },
311
- {
312
- name: 'legacy_slug',
313
- type: { kind: 'primitive', type: 'string' },
314
- required: false,
315
- description: 'Use external_id instead.',
316
- deprecated: true,
317
- },
318
- {
319
- name: 'old_field',
163
+ name: 'legacy',
320
164
  type: { kind: 'primitive', type: 'string' },
321
165
  required: false,
322
166
  deprecated: true,
167
+ description: 'Use external_id instead.',
323
168
  },
324
169
  ],
325
170
  },
326
171
  ];
327
172
 
328
- const ctxWithServices: EmitterContext = {
329
- ...ctx,
330
- spec: { ...emptySpec, services: [service], models },
331
- };
332
-
333
- const files = generateModels(models, ctxWithServices);
334
- const content = files[0].content;
173
+ const spec = makeSpec(models);
174
+ const ctxWithModels: EmitterContext = { ...ctx, spec };
175
+ const result = generateModels(models, ctxWithModels);
335
176
 
336
- // Field with description + deprecated gets multiline JSDoc
337
- expect(content).toContain(' /**\n * Use external_id instead.\n * @deprecated\n */');
338
-
339
- // Field with only deprecated gets single-line JSDoc
340
- expect(content).toContain(' /** @deprecated */');
177
+ expect(result[0].content).toContain('@deprecated');
178
+ expect(result[0].content).toContain('Use external_id instead.');
341
179
  });
342
180
 
343
- it('renders field-level JSDoc from OpenAPI descriptions', () => {
344
- const service: Service = {
345
- name: 'Organizations',
346
- operations: [
347
- {
348
- name: 'getOrganization',
349
- httpMethod: 'get',
350
- path: '/organizations/{id}',
351
- pathParams: [
352
- {
353
- name: 'id',
354
- type: { kind: 'primitive', type: 'string' },
355
- required: true,
356
- },
357
- ],
358
- queryParams: [],
359
- headerParams: [],
360
- response: { kind: 'model', name: 'Organization' },
361
- errors: [],
362
- injectIdempotencyKey: false,
363
- },
364
- ],
365
- };
366
-
181
+ it('skips per-domain ListMetadata models', () => {
367
182
  const models: Model[] = [
368
183
  {
369
184
  name: 'Organization',
370
- description: 'An organization in the WorkOS system.',
185
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
186
+ },
187
+ {
188
+ name: 'OrganizationListMetadata',
371
189
  fields: [
372
- {
373
- name: 'id',
374
- type: { kind: 'primitive', type: 'string' },
375
- required: true,
376
- description: 'Unique identifier for the organization.',
377
- },
378
- {
379
- name: 'name',
380
- type: { kind: 'primitive', type: 'string' },
381
- required: true,
382
- description: 'The display name of the organization.',
383
- },
384
- {
385
- name: 'created_at',
386
- type: { kind: 'primitive', type: 'string', format: 'date-time' },
387
- required: true,
388
- // No description — should not get JSDoc
389
- },
390
- {
391
- name: 'allow_profiles_outside_organization',
392
- type: { kind: 'primitive', type: 'boolean' },
393
- required: false,
394
- description:
395
- 'Whether connections within the organization allow profiles\nthat do not have a domain that is verified by the organization.',
396
- },
190
+ { name: 'before', type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } }, required: false },
191
+ { name: 'after', type: { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } }, required: false },
397
192
  ],
398
193
  },
399
194
  ];
400
195
 
401
- const ctxWithServices: EmitterContext = {
402
- ...ctx,
403
- spec: { ...emptySpec, services: [service], models },
404
- };
196
+ const spec = makeSpec(models);
197
+ const ctxWithModels: EmitterContext = { ...ctx, spec };
198
+ const result = generateModels(models, ctxWithModels);
405
199
 
406
- const files = generateModels(models, ctxWithServices);
407
- const content = files[0].content;
200
+ expect(result.every((f) => !f.path.includes('list-metadata'))).toBe(true);
201
+ });
408
202
 
409
- // Model-level JSDoc is emitted
410
- expect(content).toContain('/** An organization in the WorkOS system. */');
203
+ it('handles generic type params', () => {
204
+ const models: Model[] = [
205
+ {
206
+ name: 'DirectoryUser',
207
+ typeParams: [{ name: 'TCustom', default: { kind: 'map', valueType: { kind: 'primitive', type: 'unknown' } } }],
208
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
209
+ },
210
+ ];
411
211
 
412
- // Fields with description get per-field JSDoc
413
- expect(content).toContain('/** Unique identifier for the organization. */');
414
- expect(content).toContain('/** The display name of the organization. */');
212
+ const spec = makeSpec(models, [
213
+ {
214
+ name: 'DirectorySync',
215
+ operations: [
216
+ {
217
+ name: 'getUser',
218
+ httpMethod: 'get',
219
+ path: '/directory_users/{id}',
220
+ pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
221
+ queryParams: [],
222
+ headerParams: [],
223
+ response: { kind: 'model', name: 'DirectoryUser' },
224
+ errors: [],
225
+ injectIdempotencyKey: false,
226
+ },
227
+ ],
228
+ },
229
+ ]);
230
+ const ctxWithModels: EmitterContext = { ...ctx, spec };
231
+ const result = generateModels(models, ctxWithModels);
415
232
 
416
- // Multiline description renders correctly
417
- expect(content).toContain(
418
- ' /**\n * Whether connections within the organization allow profiles\n * that do not have a domain that is verified by the organization.\n */',
419
- );
233
+ expect(result[0].content).toContain('export interface DirectoryUser<TCustom = Record<string, any>>');
234
+ });
420
235
 
421
- // Field without description does NOT get JSDoc
422
- const lines = content.split('\n');
423
- const createdAtIdx = lines.findIndex((l) => l.includes('createdAt'));
424
- expect(createdAtIdx).toBeGreaterThan(0);
425
- // The line before createdAt should not be a JSDoc closing tag
426
- expect(lines[createdAtIdx - 1].trim()).not.toBe('*/');
236
+ it('does not emit brand-new files into an existing git-tracked SDK', () => {
237
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'node-emitter-live-'));
238
+ try {
239
+ const ifaceDir = path.join(tmpRoot, 'src', 'organizations', 'interfaces');
240
+ fs.mkdirSync(ifaceDir, { recursive: true });
241
+ fs.writeFileSync(
242
+ path.join(ifaceDir, 'organization.interface.ts'),
243
+ ['export interface Organization {', ' id: string;', '}'].join('\n'),
244
+ );
245
+ execFileSync('git', ['init'], { cwd: tmpRoot, stdio: 'ignore' });
246
+ execFileSync('git', ['add', 'src'], { cwd: tmpRoot, stdio: 'ignore' });
247
+
248
+ const models: Model[] = [
249
+ {
250
+ name: 'Organization',
251
+ fields: [
252
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
253
+ { name: 'domain', type: { kind: 'model', name: 'OrganizationDomain' }, required: false },
254
+ ],
255
+ },
256
+ {
257
+ name: 'OrganizationDomain',
258
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
259
+ },
260
+ ];
261
+
262
+ const spec = makeSpec(models);
263
+ const files = nodeEmitter.generateModels(models, { ...ctx, spec, outputDir: tmpRoot });
264
+
265
+ expect(files).toHaveLength(0);
266
+ expect(files.some((f) => f.path.includes('organization-domain.interface.ts'))).toBe(false);
267
+ expect(files.some((f) => f.path.includes('/serializers/'))).toBe(false);
268
+ } finally {
269
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
270
+ }
427
271
  });
428
272
 
429
- it('renders readOnly/writeOnly/default annotations', () => {
430
- const service: Service = {
431
- name: 'Organizations',
432
- operations: [
273
+ it('keeps spec model names for manifest-managed adopted services on rerun', () => {
274
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'node-adopted-model-rerun-'));
275
+ try {
276
+ fs.mkdirSync(path.join(tmpRoot, 'src'), { recursive: true });
277
+ fs.writeFileSync(path.join(tmpRoot, 'src', 'workos.ts'), '// @oagen-ignore-file\nexport class WorkOS {}\n');
278
+ fs.writeFileSync(path.join(tmpRoot, 'src', 'index.ts'), '// @oagen-ignore-file\n');
279
+ fs.mkdirSync(path.join(tmpRoot, 'src', 'connect'), { recursive: true });
280
+ fs.writeFileSync(
281
+ path.join(tmpRoot, 'src', 'connect', 'connect.ts'),
282
+ ['// This file is auto-generated by oagen. Do not edit.', '', 'export class Connect {}'].join('\n'),
283
+ );
284
+ execFileSync('git', ['init'], { cwd: tmpRoot, stdio: 'ignore' });
285
+ execFileSync('git', ['add', 'src/workos.ts', 'src/index.ts'], { cwd: tmpRoot, stdio: 'ignore' });
286
+
287
+ const models: Model[] = [
433
288
  {
434
- name: 'getOrganization',
435
- httpMethod: 'get',
436
- path: '/organizations/{id}',
437
- pathParams: [
289
+ name: 'CreateM2MApplication',
290
+ fields: [
291
+ { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
292
+ { name: 'application_type', type: { kind: 'literal', value: 'm2m' }, required: true },
293
+ ],
294
+ },
295
+ ];
296
+ const spec = makeSpec(models, [
297
+ {
298
+ name: 'Connect',
299
+ operations: [
438
300
  {
439
- name: 'id',
440
- type: { kind: 'primitive', type: 'string' },
441
- required: true,
301
+ name: 'createApplication',
302
+ httpMethod: 'post',
303
+ path: '/connect/applications',
304
+ pathParams: [],
305
+ queryParams: [],
306
+ headerParams: [],
307
+ requestBody: { kind: 'model', name: 'CreateM2MApplication' },
308
+ response: { kind: 'primitive', type: 'unknown' },
309
+ errors: [],
310
+ injectIdempotencyKey: false,
442
311
  },
443
312
  ],
444
- queryParams: [],
445
- headerParams: [],
446
- response: { kind: 'model', name: 'Organization' },
447
- errors: [],
448
- injectIdempotencyKey: false,
449
313
  },
450
- ],
451
- };
314
+ ]);
315
+
316
+ const result = nodeEmitter.generateModels(models, {
317
+ ...ctx,
318
+ spec,
319
+ outputDir: tmpRoot,
320
+ emitterOptions: { adoptMissingServices: true },
321
+ priorTargetManifestPaths: new Set(['src/connect/connect.ts']),
322
+ apiSurface: {
323
+ language: 'node',
324
+ extractedFrom: tmpRoot,
325
+ extractedAt: '2026-05-12T00:00:00Z',
326
+ classes: {},
327
+ interfaces: {
328
+ CreateGroupOptions: {
329
+ name: 'CreateGroupOptions',
330
+ fields: { name: { type: 'string', optional: false } },
331
+ extends: [],
332
+ sourceFile: 'src/groups/interfaces/create-group-options.interface.ts',
333
+ },
334
+ },
335
+ typeAliases: {},
336
+ enums: {},
337
+ exports: {},
338
+ } as any,
339
+ overlayLookup: {
340
+ methodByOperation: new Map(),
341
+ interfaceByName: new Map(),
342
+ modelNameByIR: new Map([['CreateM2MApplication', 'CreateGroupOptions']]),
343
+ } as any,
344
+ } as EmitterContext);
345
+
346
+ const file = result.find((f) => f.path === 'src/connect/interfaces/create-m2m-application.interface.ts');
347
+ expect(file?.content).toContain('export interface CreateM2MApplication');
348
+ expect(file?.content).not.toContain('CreateGroupOptions');
349
+ } finally {
350
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
351
+ }
352
+ });
353
+ });
452
354
 
355
+ describe('generateSerializers', () => {
356
+ it('generates deserializer with camelCase mapping', () => {
453
357
  const models: Model[] = [
454
358
  {
455
359
  name: 'Organization',
456
360
  fields: [
457
- {
458
- name: 'id',
459
- type: { kind: 'primitive', type: 'string' },
460
- required: true,
461
- readOnly: true,
462
- },
463
- {
464
- name: 'secret_key',
465
- type: { kind: 'primitive', type: 'string' },
466
- required: true,
467
- writeOnly: true,
468
- },
469
- {
470
- name: 'status',
471
- type: { kind: 'primitive', type: 'string' },
472
- required: false,
473
- default: 'active',
474
- },
361
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
362
+ { name: 'created_at', type: { kind: 'primitive', type: 'string', format: 'date-time' }, required: true },
475
363
  ],
476
364
  },
477
365
  ];
478
366
 
479
- const ctxWithServices: EmitterContext = {
480
- ...ctx,
481
- spec: { ...emptySpec, services: [service], models },
482
- };
483
-
484
- const files = generateModels(models, ctxWithServices);
485
- const content = files[0].content;
367
+ const spec = makeSpec(models);
368
+ const ctxWithModels: EmitterContext = { ...ctx, spec };
369
+ const result = generateSerializers(models, ctxWithModels);
486
370
 
487
- // readOnly field gets @readonly JSDoc and readonly TS modifier
488
- expect(content).toContain('/** @readonly */');
489
- expect(content).toContain(' readonly id: string;');
490
-
491
- // writeOnly field gets @writeonly JSDoc
492
- expect(content).toContain('/** @writeonly */');
493
-
494
- // default field gets @default JSDoc
495
- expect(content).toContain('@default "active"');
371
+ expect(result.length).toBeGreaterThan(0);
372
+ const file = result[0];
373
+ expect(file.path).toContain('.serializer.ts');
374
+ expect(file.content).toContain('deserializeOrganization');
375
+ expect(file.content).toContain('createdAt: new Date(response.created_at)');
496
376
  });
497
377
 
498
- it('skips per-domain ListMetadata models (Fix #4)', () => {
499
- const service: Service = {
500
- name: 'Connections',
501
- operations: [
502
- {
503
- name: 'listConnections',
504
- httpMethod: 'get',
505
- path: '/connections',
506
- pathParams: [],
507
- queryParams: [],
508
- headerParams: [],
509
- response: { kind: 'model', name: 'ConnectionList' },
510
- errors: [],
511
- injectIdempotencyKey: false,
512
- pagination: {
513
- strategy: 'cursor',
514
- param: 'after',
515
- itemType: { kind: 'model', name: 'Connection' },
516
- },
517
- },
518
- ],
519
- };
520
-
378
+ it('generates nested model deserialization', () => {
521
379
  const models: Model[] = [
522
380
  {
523
- name: 'ConnectionListListMetadata',
381
+ name: 'Organization',
524
382
  fields: [
383
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
525
384
  {
526
- name: 'before',
527
- type: {
528
- kind: 'nullable',
529
- inner: { kind: 'primitive', type: 'string' },
530
- },
531
- required: false,
532
- },
533
- {
534
- name: 'after',
535
- type: {
536
- kind: 'nullable',
537
- inner: { kind: 'primitive', type: 'string' },
538
- },
539
- required: false,
385
+ name: 'domains',
386
+ type: { kind: 'array', items: { kind: 'model', name: 'OrganizationDomain' } },
387
+ required: true,
540
388
  },
541
389
  ],
542
390
  },
543
391
  {
544
- name: 'Connection',
545
- fields: [
546
- {
547
- name: 'id',
548
- type: { kind: 'primitive', type: 'string' },
549
- required: true,
550
- },
551
- ],
392
+ name: 'OrganizationDomain',
393
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
552
394
  },
553
395
  ];
554
396
 
555
- const ctxWithServices: EmitterContext = {
556
- ...ctx,
557
- spec: { ...emptySpec, services: [service], models },
558
- };
559
-
560
- const files = generateModels(models, ctxWithServices);
561
-
562
- // The ListMetadata model should be skipped entirely
563
- const listMetadataFile = files.find((f) => f.path.includes('list-metadata'));
564
- expect(listMetadataFile).toBeUndefined();
397
+ const spec = makeSpec(models);
398
+ const ctxWithModels: EmitterContext = { ...ctx, spec };
399
+ const result = generateSerializers(models, ctxWithModels);
565
400
 
566
- // The Connection model should still be generated
567
- const connectionFile = files.find((f) => f.path.includes('connection.interface.ts'));
568
- expect(connectionFile).toBeDefined();
401
+ const orgSerializer = result.find(
402
+ (f) => f.path.includes('organization.serializer.ts') && !f.path.includes('domain'),
403
+ );
404
+ expect(orgSerializer?.content).toContain('domains: response.domains.map(deserializeOrganizationDomain)');
569
405
  });
570
406
 
571
- it('skips per-domain list wrapper models (Fix #6)', () => {
572
- const service: Service = {
573
- name: 'Connections',
574
- operations: [
575
- {
576
- name: 'listConnections',
577
- httpMethod: 'get',
578
- path: '/connections',
579
- pathParams: [],
580
- queryParams: [],
581
- headerParams: [],
582
- response: { kind: 'model', name: 'ConnectionList' },
583
- errors: [],
584
- injectIdempotencyKey: false,
585
- pagination: {
586
- strategy: 'cursor',
587
- param: 'after',
588
- itemType: { kind: 'model', name: 'Connection' },
589
- },
590
- },
591
- ],
592
- };
593
-
407
+ it('preserves null fallback for optional nullable model fields', () => {
594
408
  const models: Model[] = [
595
409
  {
596
- name: 'ConnectionList',
597
- fields: [
598
- {
599
- name: 'object',
600
- type: { kind: 'literal', value: 'list' },
601
- required: true,
602
- },
603
- {
604
- name: 'data',
605
- type: {
606
- kind: 'array',
607
- items: { kind: 'model', name: 'Connection' },
608
- },
609
- required: true,
610
- },
611
- {
612
- name: 'list_metadata',
613
- type: { kind: 'model', name: 'ConnectionListListMetadata' },
614
- required: true,
615
- },
616
- ],
617
- },
618
- {
619
- name: 'ConnectionListListMetadata',
410
+ name: 'Organization',
620
411
  fields: [
621
- {
622
- name: 'before',
623
- type: {
624
- kind: 'nullable',
625
- inner: { kind: 'primitive', type: 'string' },
626
- },
627
- required: false,
628
- },
629
- {
630
- name: 'after',
631
- type: {
632
- kind: 'nullable',
633
- inner: { kind: 'primitive', type: 'string' },
634
- },
635
- required: false,
636
- },
412
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
413
+ { name: 'parent', type: { kind: 'nullable', inner: { kind: 'model', name: 'ParentOrg' } }, required: false },
637
414
  ],
638
415
  },
639
416
  {
640
- name: 'Connection',
641
- fields: [
642
- {
643
- name: 'id',
644
- type: { kind: 'primitive', type: 'string' },
645
- required: true,
646
- },
647
- ],
417
+ name: 'ParentOrg',
418
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
648
419
  },
649
420
  ];
650
421
 
651
- const ctxWithServices: EmitterContext = {
652
- ...ctx,
653
- spec: { ...emptySpec, services: [service], models },
654
- };
655
-
656
- const files = generateModels(models, ctxWithServices);
657
-
658
- // The list wrapper model should be skipped
659
- const listFile = files.find((f) => f.path.includes('connection-list.interface.ts'));
660
- expect(listFile).toBeUndefined();
661
-
662
- // The ListMetadata model should also be skipped
663
- const listMetadataFile = files.find((f) => f.path.includes('list-metadata'));
664
- expect(listMetadataFile).toBeUndefined();
665
-
666
- // The Connection model should still be generated
667
- const connectionFile = files.find((f) => f.path.includes('connection.interface.ts'));
668
- expect(connectionFile).toBeDefined();
669
- });
670
-
671
- it('does not skip models that only partially match list-metadata shape', () => {
672
- const service: Service = {
673
- name: 'Organizations',
674
- operations: [
675
- {
676
- name: 'getOrganization',
677
- httpMethod: 'get',
678
- path: '/organizations/{id}',
679
- pathParams: [
680
- {
681
- name: 'id',
682
- type: { kind: 'primitive', type: 'string' },
683
- required: true,
684
- },
685
- ],
686
- queryParams: [],
687
- headerParams: [],
688
- response: { kind: 'model', name: 'Pagination' },
689
- errors: [],
690
- injectIdempotencyKey: false,
691
- },
692
- ],
693
- };
694
-
695
- const models: Model[] = [
422
+ const spec = makeSpec(models, [
696
423
  {
697
- name: 'Pagination',
698
- fields: [
699
- {
700
- name: 'before',
701
- type: {
702
- kind: 'nullable',
703
- inner: { kind: 'primitive', type: 'string' },
704
- },
705
- required: false,
706
- },
707
- {
708
- name: 'after',
709
- type: {
710
- kind: 'nullable',
711
- inner: { kind: 'primitive', type: 'string' },
712
- },
713
- required: false,
714
- },
715
- {
716
- name: 'total',
717
- type: { kind: 'primitive', type: 'integer' },
718
- required: true,
424
+ name: 'Organizations',
425
+ operations: [
426
+ {
427
+ name: 'getOrganization',
428
+ httpMethod: 'get',
429
+ path: '/organizations/{id}',
430
+ pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
431
+ queryParams: [],
432
+ headerParams: [],
433
+ response: { kind: 'model', name: 'Organization' },
434
+ errors: [],
435
+ injectIdempotencyKey: false,
719
436
  },
720
437
  ],
721
438
  },
722
- ];
723
-
724
- const ctxWithServices: EmitterContext = {
725
- ...ctx,
726
- spec: { ...emptySpec, services: [service], models },
727
- };
439
+ ]);
440
+ const ctxWithModels: EmitterContext = { ...ctx, spec };
441
+ const result = generateSerializers(models, ctxWithModels);
728
442
 
729
- const files = generateModels(models, ctxWithServices);
730
- // Model with 3 fields should NOT be skipped even if it has before/after
731
- expect(files.length).toBe(1);
732
- expect(files[0].path).toContain('pagination.interface.ts');
443
+ const orgSerializer = result.find(
444
+ (f) => f.path.includes('organization.serializer.ts') && !f.path.includes('parent'),
445
+ );
446
+ expect(orgSerializer?.content).toContain(
447
+ 'parent: response.parent != null ? deserializeParentOrg(response.parent) : null',
448
+ );
733
449
  });
734
- });
735
450
 
736
- describe('model deduplication', () => {
737
- it('emits type alias for structurally identical models', () => {
738
- const service: Service = {
739
- name: 'Roles',
740
- operations: [
451
+ it('skips parent serialization when a structurally matched baseline dependency has no serializer', () => {
452
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'node-serializer-baseline-'));
453
+ try {
454
+ fs.mkdirSync(path.join(tmpRoot, 'src', 'api-keys', 'interfaces'), { recursive: true });
455
+ fs.mkdirSync(path.join(tmpRoot, 'src', 'api-keys', 'serializers'), { recursive: true });
456
+ fs.writeFileSync(
457
+ path.join(tmpRoot, 'src', 'api-keys', 'interfaces', 'created-api-key.interface.ts'),
458
+ [
459
+ 'export interface CreatedApiKey {',
460
+ ' id: string;',
461
+ '}',
462
+ '',
463
+ 'export interface SerializedCreatedApiKey {',
464
+ ' id: string;',
465
+ '}',
466
+ ].join('\n'),
467
+ );
468
+ fs.writeFileSync(
469
+ path.join(tmpRoot, 'src', 'api-keys', 'serializers', 'created-api-key.serializer.ts'),
470
+ [
471
+ "import type { CreatedApiKey, SerializedCreatedApiKey } from '../interfaces/created-api-key.interface';",
472
+ 'export function deserializeCreatedApiKey(apiKey: SerializedCreatedApiKey): CreatedApiKey {',
473
+ ' return { id: apiKey.id };',
474
+ '}',
475
+ ].join('\n'),
476
+ );
477
+
478
+ setActiveLiveSurface(buildLiveSurface(tmpRoot));
479
+ setBaselineSerializedNames(new Set(['SerializedCreatedApiKey']));
480
+ setBaselineInterfaceNames(new Set(['CreatedApiKey', 'SerializedCreatedApiKey']));
481
+
482
+ const models: Model[] = [
741
483
  {
742
- name: 'getRole',
743
- httpMethod: 'get',
744
- path: '/roles/{id}',
745
- pathParams: [
484
+ name: 'OrganizationApiKey',
485
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
486
+ },
487
+ {
488
+ name: 'OrganizationApiKeyList',
489
+ fields: [
490
+ { name: 'object', type: { kind: 'literal', value: 'list' }, required: true },
746
491
  {
747
- name: 'id',
748
- type: { kind: 'primitive', type: 'string' },
492
+ name: 'data',
493
+ type: { kind: 'array', items: { kind: 'model', name: 'OrganizationApiKey' } },
749
494
  required: true,
750
495
  },
751
496
  ],
752
- queryParams: [],
753
- headerParams: [],
754
- response: { kind: 'model', name: 'EnvironmentRole' },
755
- errors: [],
756
- injectIdempotencyKey: false,
757
497
  },
498
+ ];
499
+ const spec = makeSpec(models, [
758
500
  {
759
- name: 'getOrganizationRole',
760
- httpMethod: 'get',
761
- path: '/organization_roles/{id}',
762
- pathParams: [
501
+ name: 'ApiKeys',
502
+ operations: [
763
503
  {
764
- name: 'id',
765
- type: { kind: 'primitive', type: 'string' },
766
- required: true,
504
+ name: 'listOrganizationApiKeys',
505
+ httpMethod: 'get',
506
+ path: '/api_keys',
507
+ pathParams: [],
508
+ queryParams: [],
509
+ headerParams: [],
510
+ response: { kind: 'model', name: 'OrganizationApiKeyList' },
511
+ errors: [],
512
+ injectIdempotencyKey: false,
767
513
  },
768
514
  ],
769
- queryParams: [],
770
- headerParams: [],
771
- response: { kind: 'model', name: 'OrganizationRole' },
772
- errors: [],
773
- injectIdempotencyKey: false,
774
515
  },
775
- ],
776
- };
777
-
778
- const models: Model[] = [
779
- {
780
- name: 'EnvironmentRole',
781
- fields: [
782
- {
783
- name: 'id',
784
- type: { kind: 'primitive', type: 'string' },
785
- required: true,
786
- },
787
- {
788
- name: 'name',
789
- type: { kind: 'primitive', type: 'string' },
790
- required: true,
791
- },
792
- {
793
- name: 'type',
794
- type: { kind: 'literal', value: 'environment_role' },
795
- required: true,
796
- },
797
- ],
798
- },
799
- {
800
- name: 'OrganizationRole',
801
- fields: [
802
- {
803
- name: 'id',
804
- type: { kind: 'primitive', type: 'string' },
805
- required: true,
806
- },
807
- {
808
- name: 'name',
809
- type: { kind: 'primitive', type: 'string' },
810
- required: true,
811
- },
812
- {
813
- name: 'type',
814
- type: { kind: 'literal', value: 'environment_role' },
815
- required: true,
816
- },
817
- ],
818
- },
819
- ];
820
-
821
- const ctxWithServices: EmitterContext = {
822
- ...ctx,
823
- spec: { ...emptySpec, services: [service], models },
824
- };
825
-
826
- const files = generateModels(models, ctxWithServices);
827
- expect(files.length).toBe(2);
828
-
829
- // First model: full interface
830
- expect(files[0].content).toContain('export interface EnvironmentRole');
831
-
832
- // Second model: type alias referencing canonical
833
- expect(files[1].content).toContain('export type OrganizationRole = EnvironmentRole');
834
- expect(files[1].content).toContain('export type OrganizationRoleResponse = EnvironmentRoleResponse');
835
- });
836
-
837
- it('generates Date type for date-time fields even when baseline says string', () => {
838
- const service: Service = {
839
- name: 'Authorization',
840
- operations: [
841
- {
842
- name: 'getRoleAssignment',
843
- httpMethod: 'get',
844
- path: '/role_assignments/{id}',
845
- pathParams: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
846
- queryParams: [],
847
- headerParams: [],
848
- response: { kind: 'model', name: 'RoleAssignment' },
849
- errors: [],
850
- injectIdempotencyKey: false,
851
- },
852
- ],
853
- };
854
-
855
- const models: Model[] = [
856
- {
857
- name: 'RoleAssignment',
858
- fields: [
859
- {
860
- name: 'id',
861
- type: { kind: 'primitive', type: 'string' },
862
- required: true,
863
- },
864
- {
865
- name: 'created_at',
866
- type: { kind: 'primitive', type: 'string', format: 'date-time' },
867
- required: true,
868
- },
869
- {
870
- name: 'updated_at',
871
- type: { kind: 'primitive', type: 'string', format: 'date-time' },
872
- required: true,
873
- },
874
- // Extra field not in baseline so modelHasNewFields returns true
875
- // (allowing the dedup test to proceed with generation)
876
- {
877
- name: 'role_name',
878
- type: { kind: 'primitive', type: 'string' },
879
- required: true,
880
- },
881
- ],
882
- },
883
- ];
884
-
885
- const ctxWithBaseline: EmitterContext = {
886
- ...ctx,
887
- spec: { ...emptySpec, services: [service], models },
888
- apiSurface: {
889
- language: 'node',
890
- extractedFrom: 'test',
891
- extractedAt: '2024-01-01',
892
- classes: {},
893
- typeAliases: {},
894
- enums: {},
895
- exports: {},
896
- interfaces: {
897
- RoleAssignment: {
898
- name: 'RoleAssignment',
899
- fields: {
900
- id: { name: 'id', type: 'string', optional: false },
901
- createdAt: { name: 'createdAt', type: 'string', optional: false },
902
- updatedAt: { name: 'updatedAt', type: 'string', optional: false },
516
+ ]);
517
+ const result = generateSerializers(models, {
518
+ ...ctx,
519
+ spec,
520
+ outputDir: tmpRoot,
521
+ apiSurface: {
522
+ language: 'node',
523
+ extractedFrom: tmpRoot,
524
+ extractedAt: '2026-05-12T00:00:00Z',
525
+ classes: {},
526
+ interfaces: {
527
+ CreatedApiKey: {
528
+ name: 'CreatedApiKey',
529
+ fields: { id: { type: 'string', optional: false } },
530
+ extends: [],
531
+ sourceFile: 'src/api-keys/interfaces/created-api-key.interface.ts',
903
532
  },
904
- extends: [],
905
- },
906
- RoleAssignmentResponse: {
907
- name: 'RoleAssignmentResponse',
908
- fields: {
909
- id: { name: 'id', type: 'string', optional: false },
910
- created_at: { name: 'created_at', type: 'string', optional: false },
911
- updated_at: { name: 'updated_at', type: 'string', optional: false },
533
+ SerializedCreatedApiKey: {
534
+ name: 'SerializedCreatedApiKey',
535
+ fields: { id: { type: 'string', optional: false } },
536
+ extends: [],
537
+ sourceFile: 'src/api-keys/interfaces/created-api-key.interface.ts',
912
538
  },
913
- extends: [],
914
539
  },
915
- },
916
- },
917
- };
918
-
919
- const files = generateModels(models, ctxWithBaseline);
920
- const content = files[0].content;
921
-
922
- // Domain interface should use Date, not string from baseline
923
- expect(content).toContain(' createdAt: Date;');
924
- expect(content).toContain(' updatedAt: Date;');
925
-
926
- // Wire interface should stay as string (JSON native)
927
- expect(content).toContain(' created_at: string;');
928
- expect(content).toContain(' updated_at: string;');
540
+ typeAliases: {},
541
+ enums: {},
542
+ exports: {},
543
+ } as any,
544
+ overlayLookup: {
545
+ methodByOperation: new Map(),
546
+ interfaceByName: new Map(),
547
+ modelNameByIR: new Map([['OrganizationApiKey', 'SerializedCreatedApiKey']]),
548
+ } as any,
549
+ });
550
+
551
+ const listSerializer = result.find((f) => f.path.endsWith('organization-api-key-list.serializer.ts'));
552
+ expect(listSerializer?.content).toContain('deserializeOrganizationApiKeyList');
553
+ expect(listSerializer?.content).not.toContain('export const serializeOrganizationApiKeyList');
554
+ } finally {
555
+ setActiveLiveSurface(emptyLiveSurface());
556
+ setBaselineSerializedNames(new Set());
557
+ setBaselineInterfaceNames(new Set());
558
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
559
+ }
929
560
  });
930
561
  });