@workos/oagen-emitters 0.12.0 → 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 (53) 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 +14 -0
  9. package/dist/index.d.mts.map +1 -1
  10. package/dist/index.mjs +1 -1
  11. package/dist/{plugin-C408Wh-o.mjs → plugin-eCuvoL1T.mjs} +3914 -2121
  12. package/dist/plugin-eCuvoL1T.mjs.map +1 -0
  13. package/dist/plugin.d.mts.map +1 -1
  14. package/dist/plugin.mjs +1 -1
  15. package/package.json +10 -10
  16. package/renovate.json +46 -6
  17. package/src/node/client.ts +19 -32
  18. package/src/node/enums.ts +67 -30
  19. package/src/node/errors.ts +2 -8
  20. package/src/node/field-plan.ts +188 -52
  21. package/src/node/fixtures.ts +11 -33
  22. package/src/node/index.ts +345 -20
  23. package/src/node/live-surface.ts +378 -0
  24. package/src/node/models.ts +540 -351
  25. package/src/node/naming.ts +119 -25
  26. package/src/node/node-overrides.ts +77 -0
  27. package/src/node/options.ts +41 -0
  28. package/src/node/resources.ts +455 -46
  29. package/src/node/sdk-errors.ts +0 -16
  30. package/src/node/tests.ts +108 -83
  31. package/src/node/type-map.ts +40 -18
  32. package/src/node/utils.ts +89 -102
  33. package/src/node/wrappers.ts +0 -20
  34. package/src/rust/fixtures.ts +87 -1
  35. package/src/rust/models.ts +17 -2
  36. package/src/rust/resources.ts +697 -62
  37. package/src/rust/tests.ts +540 -20
  38. package/test/node/client.test.ts +106 -1201
  39. package/test/node/enums.test.ts +59 -130
  40. package/test/node/errors.test.ts +2 -3
  41. package/test/node/live-surface.test.ts +240 -0
  42. package/test/node/models.test.ts +396 -765
  43. package/test/node/naming.test.ts +69 -234
  44. package/test/node/resources.test.ts +376 -2036
  45. package/test/node/tests.test.ts +119 -0
  46. package/test/node/type-map.test.ts +49 -54
  47. package/test/node/utils.test.ts +29 -80
  48. package/test/rust/fixtures.test.ts +227 -0
  49. package/test/rust/models.test.ts +38 -0
  50. package/test/rust/resources.test.ts +505 -2
  51. package/test/rust/tests.test.ts +504 -0
  52. package/dist/plugin-C408Wh-o.mjs.map +0 -1
  53. package/test/node/serializers.test.ts +0 -444
@@ -0,0 +1,119 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as os from 'node:os';
4
+ import * as path from 'node:path';
5
+ import { execFileSync } from 'node:child_process';
6
+ import type { ApiSpec, EmitterContext, Service, Model } from '@workos/oagen';
7
+ import { defaultSdkBehavior } from '@workos/oagen';
8
+ import { nodeEmitter } from '../../src/node/index.js';
9
+
10
+ const groupModel: Model = {
11
+ name: 'Group',
12
+ fields: [
13
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
14
+ { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
15
+ ],
16
+ };
17
+
18
+ const groupService: Service = {
19
+ name: 'Groups',
20
+ operations: [
21
+ {
22
+ name: 'getGroup',
23
+ httpMethod: 'get',
24
+ path: '/organizations/{organizationId}/groups/{groupId}',
25
+ pathParams: [
26
+ { name: 'organizationId', type: { kind: 'primitive', type: 'string' }, required: true },
27
+ { name: 'groupId', type: { kind: 'primitive', type: 'string' }, required: true },
28
+ ],
29
+ queryParams: [],
30
+ headerParams: [],
31
+ response: { kind: 'model', name: 'Group' },
32
+ errors: [],
33
+ injectIdempotencyKey: false,
34
+ },
35
+ ],
36
+ };
37
+
38
+ const spec: ApiSpec = {
39
+ name: 'Test',
40
+ version: '1.0.0',
41
+ baseUrl: '',
42
+ services: [groupService],
43
+ models: [groupModel],
44
+ enums: [],
45
+ sdk: defaultSdkBehavior(),
46
+ };
47
+
48
+ const ctx: EmitterContext = {
49
+ namespace: 'workos',
50
+ namespacePascal: 'WorkOS',
51
+ spec,
52
+ };
53
+
54
+ function createTrackedSdkRoot(withHandTests = false): string {
55
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'node-owned-tests-'));
56
+ fs.mkdirSync(path.join(tmpRoot, 'src', 'groups'), { recursive: true });
57
+ fs.mkdirSync(path.join(tmpRoot, 'src', 'groups', 'fixtures'), { recursive: true });
58
+ fs.writeFileSync(path.join(tmpRoot, 'src', 'workos.ts'), '// @oagen-ignore-file\nexport class WorkOS {}\n');
59
+ if (withHandTests) {
60
+ fs.writeFileSync(path.join(tmpRoot, 'src', 'groups', 'groups.spec.ts'), "describe('old', () => {});\n");
61
+ fs.writeFileSync(path.join(tmpRoot, 'src', 'groups', 'fixtures', 'group.json'), '{"id":"old"}\n');
62
+ }
63
+ execFileSync('git', ['init'], { cwd: tmpRoot, stdio: 'ignore' });
64
+ execFileSync('git', ['add', 'src'], { cwd: tmpRoot, stdio: 'ignore' });
65
+ return tmpRoot;
66
+ }
67
+
68
+ describe('node test generation ownership', () => {
69
+ it('regenerates tests and fixtures for owned services', () => {
70
+ const tmpRoot = createTrackedSdkRoot();
71
+ try {
72
+ const result = nodeEmitter.generateTests!(spec, {
73
+ ...ctx,
74
+ outputDir: tmpRoot,
75
+ emitterOptions: { ownedServices: ['Groups'], regenerateOwnedTests: true },
76
+ } as EmitterContext);
77
+
78
+ const testFile = result.find((f) => f.path === 'src/groups/groups.spec.ts');
79
+ const fixtureFile = result.find((f) => f.path === 'src/groups/fixtures/group.json');
80
+ expect(testFile).toBeDefined();
81
+ expect(testFile!.overwriteExisting).toBe(true);
82
+ expect(fixtureFile).toBeDefined();
83
+ expect(fixtureFile!.overwriteExisting).toBe(true);
84
+ } finally {
85
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
86
+ }
87
+ });
88
+
89
+ it('preserves existing hand-written tests and fixtures for owned services', () => {
90
+ const tmpRoot = createTrackedSdkRoot(true);
91
+ try {
92
+ const result = nodeEmitter.generateTests!(spec, {
93
+ ...ctx,
94
+ outputDir: tmpRoot,
95
+ emitterOptions: { ownedServices: ['Groups'], regenerateOwnedTests: true },
96
+ } as EmitterContext);
97
+
98
+ expect(result.some((f) => f.path === 'src/groups/groups.spec.ts')).toBe(false);
99
+ expect(result.some((f) => f.path === 'src/groups/fixtures/group.json')).toBe(false);
100
+ } finally {
101
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
102
+ }
103
+ });
104
+
105
+ it('skips tests and fixtures when the service is not owned', () => {
106
+ const tmpRoot = createTrackedSdkRoot();
107
+ try {
108
+ const result = nodeEmitter.generateTests!(spec, {
109
+ ...ctx,
110
+ outputDir: tmpRoot,
111
+ emitterOptions: { regenerateOwnedTests: true },
112
+ } as EmitterContext);
113
+
114
+ expect(result.some((f) => f.path.startsWith('src/groups/'))).toBe(false);
115
+ } finally {
116
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
117
+ }
118
+ });
119
+ });
@@ -3,49 +3,53 @@ import { mapTypeRef, mapWireTypeRef } from '../../src/node/type-map.js';
3
3
  import type { TypeRef } from '@workos/oagen';
4
4
 
5
5
  describe('mapTypeRef', () => {
6
- it('maps primitive string', () => {
6
+ it('maps string primitive', () => {
7
7
  const ref: TypeRef = { kind: 'primitive', type: 'string' };
8
8
  expect(mapTypeRef(ref)).toBe('string');
9
9
  });
10
10
 
11
- it('maps primitive integer to number', () => {
11
+ it('maps integer primitive', () => {
12
12
  const ref: TypeRef = { kind: 'primitive', type: 'integer' };
13
13
  expect(mapTypeRef(ref)).toBe('number');
14
14
  });
15
15
 
16
- it('maps primitive boolean', () => {
16
+ it('maps boolean primitive', () => {
17
17
  const ref: TypeRef = { kind: 'primitive', type: 'boolean' };
18
18
  expect(mapTypeRef(ref)).toBe('boolean');
19
19
  });
20
20
 
21
- it('maps unknown to any', () => {
21
+ it('maps unknown primitive to any', () => {
22
22
  const ref: TypeRef = { kind: 'primitive', type: 'unknown' };
23
23
  expect(mapTypeRef(ref)).toBe('any');
24
24
  });
25
25
 
26
- it('maps array of strings', () => {
27
- const ref: TypeRef = {
28
- kind: 'array',
29
- items: { kind: 'primitive', type: 'string' },
30
- };
31
- expect(mapTypeRef(ref)).toBe('string[]');
26
+ it('maps date-time to Date', () => {
27
+ const ref: TypeRef = { kind: 'primitive', type: 'string', format: 'date-time' };
28
+ expect(mapTypeRef(ref)).toBe('Date');
32
29
  });
33
30
 
34
- it('maps model reference', () => {
31
+ it('maps model ref to model name', () => {
35
32
  const ref: TypeRef = { kind: 'model', name: 'Organization' };
36
33
  expect(mapTypeRef(ref)).toBe('Organization');
37
34
  });
38
35
 
39
- it('maps enum reference', () => {
40
- const ref: TypeRef = { kind: 'enum', name: 'Status' };
36
+ it('maps enum ref to enum name', () => {
37
+ const ref: TypeRef = { kind: 'enum', name: 'Status', values: ['active', 'inactive'] };
41
38
  expect(mapTypeRef(ref)).toBe('Status');
42
39
  });
43
40
 
41
+ it('maps array of primitives', () => {
42
+ const ref: TypeRef = { kind: 'array', items: { kind: 'primitive', type: 'string' } };
43
+ expect(mapTypeRef(ref)).toBe('string[]');
44
+ });
45
+
46
+ it('maps array of models', () => {
47
+ const ref: TypeRef = { kind: 'array', items: { kind: 'model', name: 'Org' } };
48
+ expect(mapTypeRef(ref)).toBe('Org[]');
49
+ });
50
+
44
51
  it('maps nullable type', () => {
45
- const ref: TypeRef = {
46
- kind: 'nullable',
47
- inner: { kind: 'primitive', type: 'string' },
48
- };
52
+ const ref: TypeRef = { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } };
49
53
  expect(mapTypeRef(ref)).toBe('string | null');
50
54
  });
51
55
 
@@ -64,32 +68,14 @@ describe('mapTypeRef', () => {
64
68
  const ref: TypeRef = {
65
69
  kind: 'union',
66
70
  variants: [
67
- { kind: 'model', name: 'AuthenticationFactorTotp' },
68
- { kind: 'model', name: 'AuthenticationFactorTotp' },
71
+ { kind: 'primitive', type: 'string' },
72
+ { kind: 'primitive', type: 'string' },
69
73
  ],
70
74
  };
71
- expect(mapTypeRef(ref)).toBe('AuthenticationFactorTotp');
72
- });
73
-
74
- it('maps map type', () => {
75
- const ref: TypeRef = {
76
- kind: 'map',
77
- valueType: { kind: 'primitive', type: 'string' },
78
- };
79
- expect(mapTypeRef(ref)).toBe('Record<string, string>');
80
- });
81
-
82
- it('maps literal string', () => {
83
- const ref: TypeRef = { kind: 'literal', value: 'organization' };
84
- expect(mapTypeRef(ref)).toBe("'organization'");
85
- });
86
-
87
- it('maps literal number', () => {
88
- const ref: TypeRef = { kind: 'literal', value: 42 };
89
- expect(mapTypeRef(ref)).toBe('42');
75
+ expect(mapTypeRef(ref)).toBe('string');
90
76
  });
91
77
 
92
- it('parenthesizes union in array', () => {
78
+ it('parenthesizes unions in arrays', () => {
93
79
  const ref: TypeRef = {
94
80
  kind: 'array',
95
81
  items: {
@@ -102,37 +88,46 @@ describe('mapTypeRef', () => {
102
88
  };
103
89
  expect(mapTypeRef(ref)).toBe('(string | number)[]');
104
90
  });
91
+
92
+ it('maps map type', () => {
93
+ const ref: TypeRef = { kind: 'map', valueType: { kind: 'primitive', type: 'string' } };
94
+ expect(mapTypeRef(ref)).toBe('Record<string, string>');
95
+ });
96
+
97
+ it('maps string literal', () => {
98
+ const ref: TypeRef = { kind: 'literal', value: 'active' };
99
+ expect(mapTypeRef(ref)).toBe("'active'");
100
+ });
101
+
102
+ it('maps number literal', () => {
103
+ const ref: TypeRef = { kind: 'literal', value: 42 };
104
+ expect(mapTypeRef(ref)).toBe('42');
105
+ });
105
106
  });
106
107
 
107
108
  describe('mapWireTypeRef', () => {
108
- it('maps model reference with Response suffix', () => {
109
+ it('maps model ref with Response suffix', () => {
109
110
  const ref: TypeRef = { kind: 'model', name: 'Organization' };
110
111
  expect(mapWireTypeRef(ref)).toBe('OrganizationResponse');
111
112
  });
112
113
 
113
114
  it('maps array of models with Response suffix', () => {
114
- const ref: TypeRef = {
115
- kind: 'array',
116
- items: { kind: 'model', name: 'OrganizationDomain' },
117
- };
118
- expect(mapWireTypeRef(ref)).toBe('OrganizationDomainResponse[]');
115
+ const ref: TypeRef = { kind: 'array', items: { kind: 'model', name: 'Org' } };
116
+ expect(mapWireTypeRef(ref)).toBe('OrgResponse[]');
119
117
  });
120
118
 
121
- it('keeps primitives unchanged', () => {
122
- const ref: TypeRef = { kind: 'primitive', type: 'string' };
119
+ it('maps date-time as string in wire type', () => {
120
+ const ref: TypeRef = { kind: 'primitive', type: 'string', format: 'date-time' };
123
121
  expect(mapWireTypeRef(ref)).toBe('string');
124
122
  });
125
123
 
126
- it('keeps enum references unchanged', () => {
127
- const ref: TypeRef = { kind: 'enum', name: 'Status' };
124
+ it('maps enum ref unchanged', () => {
125
+ const ref: TypeRef = { kind: 'enum', name: 'Status', values: ['active'] };
128
126
  expect(mapWireTypeRef(ref)).toBe('Status');
129
127
  });
130
128
 
131
129
  it('maps nullable model with Response suffix', () => {
132
- const ref: TypeRef = {
133
- kind: 'nullable',
134
- inner: { kind: 'model', name: 'Organization' },
135
- };
136
- expect(mapWireTypeRef(ref)).toBe('OrganizationResponse | null');
130
+ const ref: TypeRef = { kind: 'nullable', inner: { kind: 'model', name: 'Org' } };
131
+ expect(mapWireTypeRef(ref)).toBe('OrgResponse | null');
137
132
  });
138
133
  });
@@ -1,7 +1,7 @@
1
1
  import { describe, it, expect } from 'vitest';
2
- import { modelHasNewFields } from '../../src/node/utils.js';
3
2
  import type { EmitterContext, ApiSpec, Model } from '@workos/oagen';
4
3
  import { defaultSdkBehavior } from '@workos/oagen';
4
+ import { modelHasNewFields } from '../../src/node/utils.js';
5
5
 
6
6
  const emptySpec: ApiSpec = {
7
7
  name: 'Test',
@@ -20,121 +20,70 @@ const ctx: EmitterContext = {
20
20
  };
21
21
 
22
22
  describe('modelHasNewFields', () => {
23
- const model: Model = {
24
- name: 'Organization',
25
- fields: [
26
- { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
27
- { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
28
- ],
29
- };
30
-
31
- it('returns true when no apiSurface exists (Scenario B)', () => {
23
+ it('returns true when no apiSurface (Scenario B)', () => {
24
+ const model: Model = {
25
+ name: 'Organization',
26
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
27
+ };
32
28
  expect(modelHasNewFields(model, ctx)).toBe(true);
33
29
  });
34
30
 
35
- it('returns true when model has no baseline entry (new model)', () => {
31
+ it('returns true when model not in baseline', () => {
32
+ const model: Model = {
33
+ name: 'NewModel',
34
+ fields: [{ name: 'id', type: { kind: 'primitive', type: 'string' }, required: true }],
35
+ };
36
36
  const ctxWithSurface: EmitterContext = {
37
37
  ...ctx,
38
- apiSurface: {
39
- language: 'node',
40
- extractedFrom: 'test',
41
- extractedAt: '2024-01-01',
42
- classes: {},
43
- interfaces: {},
44
- typeAliases: {},
45
- enums: {},
46
- exports: {},
47
- },
38
+ apiSurface: { interfaces: { Organization: { fields: {} } } } as any,
48
39
  };
49
40
  expect(modelHasNewFields(model, ctxWithSurface)).toBe(true);
50
41
  });
51
42
 
52
- it('returns false when all fields exist in baseline', () => {
53
- const ctxWithBaseline: EmitterContext = {
54
- ...ctx,
55
- apiSurface: {
56
- language: 'node',
57
- extractedFrom: 'test',
58
- extractedAt: '2024-01-01',
59
- classes: {},
60
- typeAliases: {},
61
- enums: {},
62
- exports: {},
63
- interfaces: {
64
- Organization: {
65
- name: 'Organization',
66
- fields: {
67
- id: { name: 'id', type: 'string', optional: false },
68
- name: { name: 'name', type: 'string', optional: false },
69
- },
70
- extends: [],
71
- },
72
- },
73
- },
74
- };
75
- expect(modelHasNewFields(model, ctxWithBaseline)).toBe(false);
76
- });
77
-
78
- it('returns true when model has one new field not in baseline', () => {
79
- const modelWithNewField: Model = {
43
+ it('returns false when all fields in baseline', () => {
44
+ const model: Model = {
80
45
  name: 'Organization',
81
46
  fields: [
82
47
  { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
83
48
  { name: 'name', type: { kind: 'primitive', type: 'string' }, required: true },
84
- { name: 'slug', type: { kind: 'primitive', type: 'string' }, required: false },
85
49
  ],
86
50
  };
87
- const ctxWithBaseline: EmitterContext = {
51
+ const ctxWithSurface: EmitterContext = {
88
52
  ...ctx,
89
53
  apiSurface: {
90
- language: 'node',
91
- extractedFrom: 'test',
92
- extractedAt: '2024-01-01',
93
- classes: {},
94
- typeAliases: {},
95
- enums: {},
96
- exports: {},
97
54
  interfaces: {
98
55
  Organization: {
99
- name: 'Organization',
100
56
  fields: {
101
- id: { name: 'id', type: 'string', optional: false },
102
- name: { name: 'name', type: 'string', optional: false },
57
+ id: { type: 'string', optional: false },
58
+ name: { type: 'string', optional: false },
103
59
  },
104
- extends: [],
105
60
  },
106
61
  },
107
- },
62
+ } as any,
108
63
  };
109
- expect(modelHasNewFields(modelWithNewField, ctxWithBaseline)).toBe(true);
64
+ expect(modelHasNewFields(model, ctxWithSurface)).toBe(false);
110
65
  });
111
66
 
112
- it('converts snake_case IR field names to camelCase for baseline comparison', () => {
113
- const snakeModel: Model = {
67
+ it('returns true when new field added', () => {
68
+ const model: Model = {
114
69
  name: 'Organization',
115
- fields: [{ name: 'organization_id', type: { kind: 'primitive', type: 'string' }, required: true }],
70
+ fields: [
71
+ { name: 'id', type: { kind: 'primitive', type: 'string' }, required: true },
72
+ { name: 'new_field', type: { kind: 'primitive', type: 'string' }, required: false },
73
+ ],
116
74
  };
117
- const ctxWithBaseline: EmitterContext = {
75
+ const ctxWithSurface: EmitterContext = {
118
76
  ...ctx,
119
77
  apiSurface: {
120
- language: 'node',
121
- extractedFrom: 'test',
122
- extractedAt: '2024-01-01',
123
- classes: {},
124
- typeAliases: {},
125
- enums: {},
126
- exports: {},
127
78
  interfaces: {
128
79
  Organization: {
129
- name: 'Organization',
130
80
  fields: {
131
- organizationId: { name: 'organizationId', type: 'string', optional: false },
81
+ id: { type: 'string', optional: false },
132
82
  },
133
- extends: [],
134
83
  },
135
84
  },
136
- },
85
+ } as any,
137
86
  };
138
- expect(modelHasNewFields(snakeModel, ctxWithBaseline)).toBe(false);
87
+ expect(modelHasNewFields(model, ctxWithSurface)).toBe(true);
139
88
  });
140
89
  });
@@ -0,0 +1,227 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { ApiSpec, Enum, Model } from '@workos/oagen';
3
+ import { defaultSdkBehavior } from '@workos/oagen';
4
+ import { exampleFromSpec, generateFixtures, generateModelFixture } from '../../src/rust/fixtures.js';
5
+
6
+ function spec(models: Model[], enums: Enum[] = []): ApiSpec {
7
+ return {
8
+ name: 'Test',
9
+ version: '1.0.0',
10
+ baseUrl: '',
11
+ services: [],
12
+ models,
13
+ enums,
14
+ sdk: defaultSdkBehavior(),
15
+ };
16
+ }
17
+
18
+ describe('rust/fixtures', () => {
19
+ it('prefers a spec `example` over the generated placeholder for a primitive field', () => {
20
+ const models: Model[] = [
21
+ {
22
+ name: 'Event',
23
+ fields: [
24
+ {
25
+ name: 'id',
26
+ type: { kind: 'primitive', type: 'string' },
27
+ required: true,
28
+ example: 'event_01XXXX',
29
+ },
30
+ {
31
+ name: 'created_at',
32
+ type: { kind: 'primitive', type: 'string', format: 'date-time' },
33
+ required: true,
34
+ example: '2026-02-02T16:35:39.317Z',
35
+ },
36
+ ],
37
+ },
38
+ ];
39
+ const files = generateFixtures(spec(models));
40
+ const file = files.find((f) => f.path === 'tests/fixtures/event.json')!;
41
+ expect(file).toBeDefined();
42
+ const parsed = JSON.parse(file.content);
43
+ expect(parsed.id).toBe('event_01XXXX');
44
+ expect(parsed.created_at).toBe('2026-02-02T16:35:39.317Z');
45
+ });
46
+
47
+ it('falls back to the placeholder when the example shape does not match the type', () => {
48
+ const models: Model[] = [
49
+ {
50
+ name: 'Wrong',
51
+ fields: [
52
+ // Type is integer but example is a string — must fall back.
53
+ {
54
+ name: 'count',
55
+ type: { kind: 'primitive', type: 'integer' },
56
+ required: true,
57
+ example: 'not-a-number',
58
+ },
59
+ ],
60
+ },
61
+ ];
62
+ const files = generateFixtures(spec(models));
63
+ const file = files.find((f) => f.path === 'tests/fixtures/wrong.json')!;
64
+ const parsed = JSON.parse(file.content);
65
+ expect(parsed.count).toBe(0); // placeholder fallback
66
+ });
67
+
68
+ it('uses an example array of strings for an array<string> field', () => {
69
+ const models: Model[] = [
70
+ {
71
+ name: 'Org',
72
+ fields: [
73
+ {
74
+ name: 'domains',
75
+ type: {
76
+ kind: 'array',
77
+ items: { kind: 'primitive', type: 'string' },
78
+ },
79
+ required: true,
80
+ example: ['example.com', 'foo.com'],
81
+ },
82
+ ],
83
+ },
84
+ ];
85
+ const files = generateFixtures(spec(models));
86
+ const file = files.find((f) => f.path === 'tests/fixtures/org.json')!;
87
+ const parsed = JSON.parse(file.content);
88
+ expect(parsed.domains).toEqual(['example.com', 'foo.com']);
89
+ });
90
+
91
+ it('skips a model-shaped example to avoid mis-shaped nested structs', () => {
92
+ const models: Model[] = [
93
+ {
94
+ name: 'Outer',
95
+ fields: [
96
+ {
97
+ name: 'actor',
98
+ type: { kind: 'model', name: 'Actor' },
99
+ required: true,
100
+ // Provided as a free-form example; we should NOT use it verbatim.
101
+ example: { not_a_real_field: 'whoops' },
102
+ },
103
+ ],
104
+ },
105
+ {
106
+ name: 'Actor',
107
+ fields: [
108
+ {
109
+ name: 'id',
110
+ type: { kind: 'primitive', type: 'string' },
111
+ required: true,
112
+ example: 'user_TF4C5938',
113
+ },
114
+ {
115
+ name: 'type',
116
+ type: { kind: 'primitive', type: 'string' },
117
+ required: true,
118
+ example: 'user',
119
+ },
120
+ ],
121
+ },
122
+ ];
123
+ const files = generateFixtures(spec(models));
124
+ const file = files.find((f) => f.path === 'tests/fixtures/outer.json')!;
125
+ const parsed = JSON.parse(file.content);
126
+ // The nested model is regenerated from its own fields' examples, not from
127
+ // the parent's free-form example blob.
128
+ expect(parsed.actor).toEqual({ id: 'user_TF4C5938', type: 'user' });
129
+ });
130
+
131
+ it('uses an enum example only when it matches a known enum value', () => {
132
+ const enums: Enum[] = [
133
+ {
134
+ name: 'Status',
135
+ values: [
136
+ { name: 'Active', value: 'active' },
137
+ { name: 'Pending', value: 'pending' },
138
+ ],
139
+ },
140
+ ];
141
+ const models: Model[] = [
142
+ {
143
+ name: 'GoodEx',
144
+ fields: [
145
+ {
146
+ name: 'status',
147
+ type: { kind: 'enum', name: 'Status' },
148
+ required: true,
149
+ example: 'pending',
150
+ },
151
+ ],
152
+ },
153
+ {
154
+ name: 'BadEx',
155
+ fields: [
156
+ {
157
+ name: 'status',
158
+ type: { kind: 'enum', name: 'Status' },
159
+ required: true,
160
+ example: 'something_unknown',
161
+ },
162
+ ],
163
+ },
164
+ ];
165
+ const files = generateFixtures(spec(models, enums));
166
+ const good = JSON.parse(files.find((f) => f.path === 'tests/fixtures/good_ex.json')!.content);
167
+ const bad = JSON.parse(files.find((f) => f.path === 'tests/fixtures/bad_ex.json')!.content);
168
+ expect(good.status).toBe('pending'); // valid example wins
169
+ expect(bad.status).toBe('active'); // unknown example → first enum value
170
+ });
171
+
172
+ it('treats null examples as unusable so required fields keep a value', () => {
173
+ const models: Model[] = [
174
+ {
175
+ name: 'Nullish',
176
+ fields: [
177
+ {
178
+ name: 'name',
179
+ type: { kind: 'primitive', type: 'string' },
180
+ required: true,
181
+ example: null,
182
+ },
183
+ ],
184
+ },
185
+ ];
186
+ const files = generateFixtures(spec(models));
187
+ const parsed = JSON.parse(files.find((f) => f.path === 'tests/fixtures/nullish.json')!.content);
188
+ expect(parsed.name).toBe('test_name');
189
+ });
190
+
191
+ it('exampleFromSpec exposes the shape-checking helper for reuse', () => {
192
+ const enums = new Map<string, Enum>();
193
+ // Primitives are unwrapped through nullable.
194
+ expect(exampleFromSpec('hello', { kind: 'nullable', inner: { kind: 'primitive', type: 'string' } }, enums)).toBe(
195
+ 'hello',
196
+ );
197
+ // Integer floats are rejected (they would corrupt typed deserialisation).
198
+ expect(exampleFromSpec(1.5, { kind: 'primitive', type: 'integer' }, enums)).toBeUndefined();
199
+ // Empty arrays fall back so the placeholder can emit a one-element array.
200
+ expect(exampleFromSpec([], { kind: 'array', items: { kind: 'primitive', type: 'string' } }, enums)).toBeUndefined();
201
+ });
202
+
203
+ it('threads required-only field selection through generateModelFixture', () => {
204
+ const models: Model[] = [
205
+ {
206
+ name: 'Mixed',
207
+ fields: [
208
+ {
209
+ name: 'kept',
210
+ type: { kind: 'primitive', type: 'string' },
211
+ required: true,
212
+ example: 'real',
213
+ },
214
+ {
215
+ name: 'dropped',
216
+ type: { kind: 'primitive', type: 'string' },
217
+ required: false,
218
+ example: 'ignored',
219
+ },
220
+ ],
221
+ },
222
+ ];
223
+ const modelMap = new Map(models.map((m) => [m.name, m]));
224
+ const fixture = generateModelFixture(models[0]!, modelMap, new Map(), new Set());
225
+ expect(fixture).toEqual({ kept: 'real' });
226
+ });
227
+ });