@workos/oagen-emitters 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -84,6 +84,165 @@ describe('resolveInterfaceName', () => {
84
84
  });
85
85
  });
86
86
 
87
+ // ---------------------------------------------------------------------------
88
+ // Structural name resolution must be INJECTIVE: a live-surface name may be
89
+ // claimed by at most one IR model per run. Reconstructs the workos-node
90
+ // AuditLogs incident: the spec has two near-identical models
91
+ // (AuditLogEventActor / AuditLogEventTarget) and the live SDK declares a
92
+ // hand-written AuditLogActor with the same shape. The structural fallback
93
+ // mapped BOTH IR models onto AuditLogActor, so
94
+ // audit-log-event-target.interface.ts was emitted declaring
95
+ // `export interface AuditLogActor` (file stem and declaration disagree),
96
+ // with duplicate imports/describe blocks and two serializeAuditLogActor
97
+ // definitions downstream.
98
+ // ---------------------------------------------------------------------------
99
+ describe('resolveInterfaceName structural injectivity', () => {
100
+ const field = (name: string, required = false) => ({
101
+ name,
102
+ type: { kind: 'primitive', type: 'string' },
103
+ required,
104
+ });
105
+
106
+ // Shape ~ { id?, name, type?, metadata? } — matches the live AuditLogActor.
107
+ const eventShape = (extra: string) => [
108
+ field('id'),
109
+ field('name', true),
110
+ field('type'),
111
+ field('metadata'),
112
+ field(extra),
113
+ ];
114
+
115
+ const liveActorFields = {
116
+ id: { type: 'string', optional: true },
117
+ name: { type: 'string', optional: false },
118
+ type: { type: 'string', optional: true },
119
+ metadata: { type: 'string', optional: true },
120
+ };
121
+
122
+ function auditCtx(opts: {
123
+ models?: { name: string; fields: unknown[] }[];
124
+ modelNameByIR: [string, string][];
125
+ interfaceByName?: [string, string][];
126
+ extraInterfaces?: Record<string, unknown>;
127
+ }): EmitterContext {
128
+ const models = opts.models ?? [
129
+ { name: 'AuditLogEventActor', fields: eventShape('ip_address') },
130
+ { name: 'AuditLogEventTarget', fields: eventShape('domain') },
131
+ ];
132
+ return {
133
+ ...ctx,
134
+ spec: { ...ctx.spec, models },
135
+ apiSurface: {
136
+ language: 'node',
137
+ extractedFrom: '/tmp/workos-node',
138
+ extractedAt: '2026-06-10T00:00:00Z',
139
+ classes: {},
140
+ interfaces: {
141
+ AuditLogActor: { fields: liveActorFields },
142
+ ...opts.extraInterfaces,
143
+ },
144
+ typeAliases: {},
145
+ enums: {},
146
+ exports: {},
147
+ },
148
+ overlayLookup: {
149
+ methodByOperation: new Map(),
150
+ interfaceByName: new Map(opts.interfaceByName ?? []),
151
+ modelNameByIR: new Map(opts.modelNameByIR),
152
+ },
153
+ } as unknown as EmitterContext;
154
+ }
155
+
156
+ it('never lets two IR models collapse onto one live name', () => {
157
+ const c = auditCtx({
158
+ modelNameByIR: [
159
+ ['AuditLogEventActor', 'AuditLogActor'],
160
+ ['AuditLogEventTarget', 'AuditLogActor'],
161
+ ],
162
+ });
163
+
164
+ const actor = resolveInterfaceName('AuditLogEventActor', c);
165
+ const target = resolveInterfaceName('AuditLogEventTarget', c);
166
+
167
+ // The closer name wins the contested live name; the loser keeps its
168
+ // canonical IR-derived name — it must NEVER unify onto AuditLogActor.
169
+ expect(actor).toBe('AuditLogActor');
170
+ expect(target).toBe('AuditLogEventTarget');
171
+ expect(actor).not.toBe(target);
172
+ });
173
+
174
+ it('awards a contested name independently of overlay insertion order', () => {
175
+ const c = auditCtx({
176
+ models: [
177
+ { name: 'AuditLogEventTarget', fields: eventShape('domain') },
178
+ { name: 'AuditLogEventActor', fields: eventShape('ip_address') },
179
+ ],
180
+ modelNameByIR: [
181
+ ['AuditLogEventTarget', 'AuditLogActor'],
182
+ ['AuditLogEventActor', 'AuditLogActor'],
183
+ ],
184
+ });
185
+
186
+ expect(resolveInterfaceName('AuditLogEventActor', c)).toBe('AuditLogActor');
187
+ expect(resolveInterfaceName('AuditLogEventTarget', c)).toBe('AuditLogEventTarget');
188
+ });
189
+
190
+ it('stays injective when Serialized* normalization collapses two distinct raw matches', () => {
191
+ // The engine overlay itself is injective on raw names (actor →
192
+ // AuditLogActor, target → SerializedAuditLogActor), but the resolver
193
+ // normalizes Serialized* down to the bare name — that post-processing
194
+ // must not re-introduce a collision.
195
+ const c = auditCtx({
196
+ modelNameByIR: [
197
+ ['AuditLogEventActor', 'AuditLogActor'],
198
+ ['AuditLogEventTarget', 'SerializedAuditLogActor'],
199
+ ],
200
+ extraInterfaces: { SerializedAuditLogActor: { fields: liveActorFields } },
201
+ });
202
+
203
+ expect(resolveInterfaceName('AuditLogEventActor', c)).toBe('AuditLogActor');
204
+ expect(resolveInterfaceName('AuditLogEventTarget', c)).toBe('AuditLogEventTarget');
205
+ });
206
+
207
+ it('prefers the structurally closer claimant over the closer name', () => {
208
+ // Target matches the live shape exactly; actor only shares two fields.
209
+ // Similarity outranks name distance, so target wins even though
210
+ // "AuditLogEventActor" is the closer name.
211
+ const c = auditCtx({
212
+ models: [
213
+ { name: 'AuditLogEventActor', fields: [field('id'), field('name', true), field('ip'), field('agent')] },
214
+ { name: 'AuditLogEventTarget', fields: [field('id'), field('name', true), field('type'), field('metadata')] },
215
+ ],
216
+ modelNameByIR: [
217
+ ['AuditLogEventActor', 'AuditLogActor'],
218
+ ['AuditLogEventTarget', 'AuditLogActor'],
219
+ ],
220
+ });
221
+
222
+ expect(resolveInterfaceName('AuditLogEventTarget', c)).toBe('AuditLogActor');
223
+ expect(resolveInterfaceName('AuditLogEventActor', c)).toBe('AuditLogEventActor');
224
+ });
225
+
226
+ it('blocks structural claims on names already claimed by an exact-name override', () => {
227
+ const c = auditCtx({
228
+ interfaceByName: [['AuditLogEventActor', 'AuditLogActor']],
229
+ modelNameByIR: [['AuditLogEventTarget', 'AuditLogActor']],
230
+ });
231
+
232
+ expect(resolveInterfaceName('AuditLogEventActor', c)).toBe('AuditLogActor');
233
+ expect(resolveInterfaceName('AuditLogEventTarget', c)).toBe('AuditLogEventTarget');
234
+ });
235
+
236
+ it('still applies a single-model structural rename (the legitimate overlay case)', () => {
237
+ const c = auditCtx({
238
+ models: [{ name: 'AuditLogEventActor', fields: eventShape('ip_address') }],
239
+ modelNameByIR: [['AuditLogEventActor', 'AuditLogActor']],
240
+ });
241
+
242
+ expect(resolveInterfaceName('AuditLogEventActor', c)).toBe('AuditLogActor');
243
+ });
244
+ });
245
+
87
246
  describe('wireInterfaceName', () => {
88
247
  it('emits *Wire for a fresh `*Response`-named IR model with an empty baseline', () => {
89
248
  expect(wireInterfaceName('CreateDataKeyResponse')).toBe('CreateDataKeyResponseWire');