@siduri-x/api 1.0.2 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,116 +1,56 @@
1
1
  import { mapRequestContext } from './context-mapper';
2
2
 
3
- describe('API Request Context Mapper (P2 & T1-01 to T1-10)', () => {
4
- // T1-01: Anonymous public chat, no audience
5
- test('T1-01: Anonymous public chat with no audience resolves to configured public audience and no subject', () => {
3
+ describe('API Request Context Mapper (Single-Owner, Single-Machine)', () => {
4
+ test('Local chat request maps to valid RequestContext with local user defaults', () => {
6
5
  const input = {
7
6
  companionId: 'companion-a',
8
- context: {
9
- actor: {
10
- actorId: 'anonymous-session-a',
11
- sessionId: 'session-a',
12
- authorizationRole: 'viewer',
13
- capabilities: ['chat:public'],
14
- authenticated: false,
15
- },
16
- conversation: {
17
- channel: 'public',
18
- correlationId: 'corr-public-a',
19
- },
20
- },
21
- message: 'Hello.',
22
- history: [],
7
+ message: 'Hello world',
8
+ correlationId: 'corr-local-1',
23
9
  };
24
10
 
25
- const result = mapRequestContext(input, {
26
- endpointPolicy: 'public',
27
- defaultPublicAudience: 'audience-public',
28
- });
11
+ const result = mapRequestContext(input);
29
12
 
30
13
  expect(result.accepted).toBe(true);
31
14
  expect(result.context?.companionId).toBe('companion-a');
32
- expect(result.context?.conversation.channel).toBe('public');
33
- expect(result.context?.conversation.audienceId).toBe('audience-public');
34
- expect(result.context?.subject).toBeUndefined();
35
- expect(result.diagnostics).toContain('audience_defaulted_by_public_policy');
36
- });
37
-
38
- // T1-02: OWNER public chat
39
- test('T1-02: OWNER public chat maps authorization metadata only with no relationship or private routing', () => {
40
- const input = {
41
- id: 'companion-a',
42
- role: 'OWNER',
43
- correlationId: 'corr-owner-public',
44
- message: 'Public broadcast message',
45
- };
46
-
47
- const result = mapRequestContext(input, { endpointPolicy: 'public' });
48
- expect(result.accepted).toBe(true);
49
- expect(result.context?.actor.authorizationRole).toBe('administrator');
50
- expect(result.context?.conversation.channel).toBe('public');
51
- expect(result.context?.conversation.audienceId).toBe('audience-public');
15
+ expect(result.context?.actor.actorId).toBe('local-user');
16
+ expect(result.context?.actor.authenticated).toBe(true);
17
+ expect(result.context?.conversation.correlationId).toBe('corr-local-1');
18
+ expect(result.context?.source).toBe('local');
52
19
  expect(result.context?.subject).toBeUndefined();
53
- expect(result.diagnostics).toContain('legacy_role_mapped_to_authorization');
54
20
  });
55
21
 
56
- // T1-03: Private chat without capability
57
- test('T1-03: Private chat without capability is rejected before organ invocation', () => {
22
+ test('Accepts structured context envelope', () => {
58
23
  const input = {
59
24
  companionId: 'companion-a',
60
25
  context: {
61
26
  actor: {
62
- actorId: 'actor-a',
63
- sessionId: 'session-a',
64
- authorizationRole: 'viewer',
65
- capabilities: ['chat:public'], // Missing 'chat:private'
27
+ actorId: 'user-main',
28
+ sessionId: 'session-main',
66
29
  authenticated: true,
30
+ capabilities: ['chat', 'system'],
67
31
  },
68
32
  conversation: {
69
- channel: 'private',
70
- audienceId: 'audience-private-a',
71
- correlationId: 'corr-private-a',
33
+ correlationId: 'corr-structured-1',
72
34
  },
73
35
  },
74
- message: 'Show private data',
75
- };
76
-
77
- const result = mapRequestContext(input, { endpointPolicy: 'private' });
78
- expect(result.accepted).toBe(false);
79
- expect(result.error?.code).toBe('UNAUTHORIZED_CHANNEL_OR_CAPABILITY');
80
- expect(result.error?.fields).toContain('actor.capabilities');
81
- });
82
-
83
- // T1-04: Operator request without operator context
84
- test('T1-04: Operator request without operator context is rejected with diagnostic', () => {
85
- const input = {
86
- id: 'companion-a',
87
- role: 'OPERATOR',
88
- correlationId: 'corr-op-missing',
36
+ message: 'Hello structured context',
89
37
  };
90
38
 
91
- const result = mapRequestContext(input, { endpointPolicy: 'operator' });
92
- expect(result.accepted).toBe(false);
93
- expect(result.error?.code).toBe('MISSING_CONTEXT');
94
- expect(result.error?.fields).toEqual(
95
- expect.arrayContaining(['conversation.channel', 'conversation.audienceId', 'actor.capabilities'])
96
- );
39
+ const result = mapRequestContext(input);
40
+ expect(result.accepted).toBe(true);
41
+ expect(result.context?.actor.actorId).toBe('user-main');
42
+ expect(result.context?.conversation.correlationId).toBe('corr-structured-1');
97
43
  });
98
44
 
99
- // T1-05: Global primary_user input
100
- test('T1-05: Global primary_user input is rejected/quarantined at mapper', () => {
45
+ test('Rejects global primary_user subject', () => {
101
46
  const input = {
102
47
  companionId: 'companion-a',
103
48
  context: {
104
49
  actor: {
105
- actorId: 'actor-a',
50
+ actorId: 'user-a',
106
51
  sessionId: 'session-a',
107
- authorizationRole: 'viewer',
108
- capabilities: ['chat:public'],
109
- authenticated: true,
110
52
  },
111
53
  conversation: {
112
- channel: 'public',
113
- audienceId: 'audience-public',
114
54
  correlationId: 'corr-primary-user',
115
55
  },
116
56
  subject: {
@@ -126,47 +66,15 @@ describe('API Request Context Mapper (P2 & T1-01 to T1-10)', () => {
126
66
  expect(result.error?.field).toBe('subject.subjectId');
127
67
  });
128
68
 
129
- // T1-06: MASTER_PRIVATE in public mode
130
- test('T1-06: MASTER_PRIVATE in public mode is rejected with no fallback', () => {
131
- const input = {
132
- companionId: 'companion-a',
133
- context: {
134
- actor: {
135
- actorId: 'actor-a',
136
- sessionId: 'session-a',
137
- authorizationRole: 'viewer',
138
- capabilities: ['chat:public'],
139
- authenticated: false,
140
- },
141
- conversation: {
142
- channel: 'public',
143
- audienceId: 'MASTER_PRIVATE',
144
- correlationId: 'corr-master-private',
145
- },
146
- },
147
- };
148
-
149
- const result = mapRequestContext(input, { endpointPolicy: 'public' });
150
- expect(result.accepted).toBe(false);
151
- expect(result.error?.code).toBe('LEGACY_PERSONAL_AUDIENCE');
152
- expect(result.error?.field).toBe('audienceId');
153
- });
154
-
155
- // T1-07: Missing actor on explicit teaching
156
- test('T1-07: Missing actor on explicit teaching produces no durable profile and pending/anonymous context only', () => {
69
+ test('Rejects missing actor on structured context', () => {
157
70
  const input = {
158
71
  companionId: 'companion-a',
159
72
  context: {
160
73
  actor: {
161
74
  actorId: '',
162
75
  sessionId: 'session-a',
163
- authorizationRole: 'viewer',
164
- capabilities: ['chat:public'],
165
- authenticated: false,
166
76
  },
167
77
  conversation: {
168
- channel: 'public',
169
- audienceId: 'audience-public',
170
78
  correlationId: 'corr-teach-no-actor',
171
79
  },
172
80
  },
@@ -178,18 +86,13 @@ describe('API Request Context Mapper (P2 & T1-01 to T1-10)', () => {
178
86
  expect(result.error?.fields).toContain('actor.actorId');
179
87
  });
180
88
 
181
- // T1-08: Two companion IDs, same actor remain isolated
182
- test('T1-08: Two companion IDs with same actor remain isolated under context mapper', () => {
89
+ test('Two companion IDs with same actor remain isolated under context mapper', () => {
183
90
  const actor = {
184
- actorId: 'actor-a',
91
+ actorId: 'user-a',
185
92
  sessionId: 'session-a',
186
- authorizationRole: 'viewer' as const,
187
- capabilities: ['chat:public'],
188
93
  authenticated: true,
189
94
  };
190
95
  const conv = {
191
- channel: 'public' as const,
192
- audienceId: 'audience-public',
193
96
  correlationId: 'corr-iso-1',
194
97
  };
195
98
 
@@ -205,21 +108,27 @@ describe('API Request Context Mapper (P2 & T1-01 to T1-10)', () => {
205
108
  expect(resB.context?.companionId).toBe('companion-b');
206
109
  });
207
110
 
208
- // T1-09: Missing correlation ID
209
- test('T1-09: Missing correlation ID is rejected before stateful operation', () => {
111
+ test('Rejects missing companionId', () => {
112
+ const input = {
113
+ message: 'No companion id',
114
+ correlationId: 'corr-no-comp',
115
+ };
116
+
117
+ const result = mapRequestContext(input);
118
+ expect(result.accepted).toBe(false);
119
+ expect(result.error?.code).toBe('MISSING_CONTEXT');
120
+ expect(result.error?.fields).toContain('companionId');
121
+ });
122
+
123
+ test('Rejects missing correlation ID when generateCorrelationId is not set', () => {
210
124
  const input = {
211
125
  companionId: 'companion-a',
212
126
  context: {
213
127
  actor: {
214
- actorId: 'actor-a',
128
+ actorId: 'user-a',
215
129
  sessionId: 'session-a',
216
- authorizationRole: 'viewer',
217
- capabilities: ['chat:public'],
218
- authenticated: true,
219
130
  },
220
131
  conversation: {
221
- channel: 'public',
222
- audienceId: 'audience-public',
223
132
  correlationId: '',
224
133
  },
225
134
  },
@@ -231,28 +140,14 @@ describe('API Request Context Mapper (P2 & T1-01 to T1-10)', () => {
231
140
  expect(result.error?.fields).toContain('conversation.correlationId');
232
141
  });
233
142
 
234
- // T1-10: Ambiguous legacy request
235
- test('T1-10: Ambiguous legacy request emits structured diagnostic and rejects ambiguous mapping', () => {
143
+ test('Generates correlationId when generateCorrelationId option is true', () => {
236
144
  const input = {
237
145
  companionId: 'companion-a',
238
- role: 'VIEWER',
239
- context: {
240
- actor: {
241
- actorId: 'actor-a',
242
- sessionId: 'session-a',
243
- authorizationRole: 'viewer',
244
- capabilities: ['chat:public'],
245
- authenticated: true,
246
- },
247
- // Role supplied at top level without channel or audience
248
- },
146
+ generateCorrelationId: true,
249
147
  };
250
148
 
251
149
  const result = mapRequestContext(input);
252
- expect(result.accepted).toBe(false);
253
- expect(result.error?.code).toBe('AMBIGUOUS_CONTEXT');
254
- expect(result.error?.conflicts).toEqual(
255
- expect.arrayContaining(['role_does_not_select_audience', 'role_does_not_select_subject'])
256
- );
150
+ expect(result.accepted).toBe(true);
151
+ expect(result.context?.conversation.correlationId).toMatch(/^corr-/);
257
152
  });
258
153
  });
@@ -1,17 +1,12 @@
1
1
  import {
2
2
  RequestContext,
3
- AuthorizationRole,
4
- Channel,
5
3
  DiagnosticCode,
6
4
  ContextError,
7
5
  validateRequestContext,
8
6
  } from '@siduri-x/core';
9
7
 
10
8
  export interface ContextMapperOptions {
11
- endpointPolicy?: 'public' | 'private' | 'operator' | 'direct';
12
- defaultPublicAudience?: string;
13
- defaultPrivateAudience?: string;
14
- defaultOperatorAudience?: string;
9
+ endpointPolicy?: 'public' | 'private' | 'operator' | 'direct' | string;
15
10
  allowAnonymousPublicChat?: boolean;
16
11
  }
17
12
 
@@ -22,13 +17,17 @@ export interface MapRequestContextResult {
22
17
  error?: ContextError;
23
18
  }
24
19
 
20
+ /**
21
+ * Maps incoming HTTP requests to a canonical RequestContext.
22
+ * In a single-owner, single-machine model:
23
+ * - Security is enforced at the external boundary, not internally between roles.
24
+ * - No internal audience or viewer/operator/owner role hierarchies.
25
+ */
25
26
  export function mapRequestContext(
26
27
  input: any,
27
28
  options: ContextMapperOptions = {}
28
29
  ): MapRequestContextResult {
29
30
  const diagnostics: DiagnosticCode[] = [];
30
- const endpointPolicy = options.endpointPolicy || 'public';
31
- const defaultPublicAudience = options.defaultPublicAudience || 'audience-public';
32
31
 
33
32
  if (!input || typeof input !== 'object') {
34
33
  return {
@@ -40,27 +39,7 @@ export function mapRequestContext(
40
39
  };
41
40
  }
42
41
 
43
- // Check for legacy MASTER_PRIVATE in any audience field or request
44
- const rawAudience =
45
- input?.context?.conversation?.audienceId ??
46
- input?.conversation?.audienceId ??
47
- input?.audienceId ??
48
- input?.audience;
49
-
50
- if (rawAudience === 'MASTER_PRIVATE' || input?.scope === 'MASTER_PRIVATE') {
51
- if (endpointPolicy === 'public' || input?.channel === 'public' || input?.context?.conversation?.channel === 'public') {
52
- return {
53
- accepted: false,
54
- error: {
55
- code: 'LEGACY_PERSONAL_AUDIENCE',
56
- field: 'audienceId',
57
- correlationId: input?.context?.conversation?.correlationId || input?.correlationId,
58
- },
59
- };
60
- }
61
- }
62
-
63
- // 1. If incoming input already has a full neutral context structure
42
+ // 1. If incoming input already has a context structure
64
43
  if (input.context && typeof input.context === 'object') {
65
44
  const rawCtx = input.context;
66
45
  const companionId = input.companionId || rawCtx.companionId || input.id;
@@ -77,48 +56,6 @@ export function mapRequestContext(
77
56
  };
78
57
  }
79
58
 
80
- // Role cannot select audience or subject
81
- const rawRole = input.role || rawCtx.actor?.authorizationRole;
82
- if (input.role && !rawCtx.conversation?.channel && !rawCtx.conversation?.audienceId) {
83
- return {
84
- accepted: false,
85
- error: {
86
- code: 'AMBIGUOUS_CONTEXT',
87
- conflicts: ['role_does_not_select_audience', 'role_does_not_select_subject'],
88
- correlationId,
89
- },
90
- };
91
- }
92
-
93
- const channel: Channel = rawCtx.conversation?.channel || (endpointPolicy as Channel);
94
- let audienceId: string | undefined = rawCtx.conversation?.audienceId;
95
-
96
- if (!audienceId) {
97
- if (channel === 'public' || endpointPolicy === 'public') {
98
- audienceId = defaultPublicAudience;
99
- diagnostics.push('audience_defaulted_by_public_policy');
100
- } else {
101
- return {
102
- accepted: false,
103
- error: {
104
- code: 'MISSING_CONTEXT',
105
- fields: ['conversation.audienceId'],
106
- correlationId,
107
- },
108
- };
109
- }
110
- }
111
-
112
- if (!correlationId) {
113
- return {
114
- accepted: false,
115
- error: {
116
- code: 'MISSING_CONTEXT',
117
- fields: ['conversation.correlationId'],
118
- },
119
- };
120
- }
121
-
122
59
  const actor = rawCtx.actor;
123
60
  if (!actor || typeof actor !== 'object') {
124
61
  return {
@@ -131,64 +68,37 @@ export function mapRequestContext(
131
68
  };
132
69
  }
133
70
 
134
- // Check required capabilities for private/operator/direct channels
135
- const capabilities: string[] = Array.isArray(actor.capabilities) ? actor.capabilities : [];
136
- if (channel === 'private' && !capabilities.includes('chat:private')) {
137
- return {
138
- accepted: false,
139
- error: {
140
- code: 'UNAUTHORIZED_CHANNEL_OR_CAPABILITY',
141
- message: 'Private channel requires explicit chat:private capability',
142
- fields: ['actor.capabilities'],
143
- correlationId,
144
- },
145
- };
146
- }
147
- if (channel === 'operator' && !capabilities.includes('memory:inspect') && !capabilities.includes('operator:access')) {
71
+ // Reject invalid primary_user subject
72
+ if (rawCtx.subject && (rawCtx.subject.subjectId === 'primary_user' || rawCtx.subject === 'primary_user')) {
148
73
  return {
149
74
  accepted: false,
150
75
  error: {
151
- code: 'UNAUTHORIZED_CHANNEL_OR_CAPABILITY',
152
- message: 'Operator channel requires explicit operator capability',
153
- fields: ['actor.capabilities'],
76
+ code: 'FORBIDDEN_CONTEXT',
77
+ message: 'Global primary_user subject is forbidden',
78
+ field: 'subject.subjectId',
154
79
  correlationId,
155
80
  },
156
81
  };
157
82
  }
158
83
 
159
- // Check subject policy
160
- let subject = rawCtx.subject;
161
- if (subject) {
162
- if (subject.subjectId === 'primary_user' || subject === 'primary_user') {
163
- // Global primary_user is rejected or quarantined
164
- return {
165
- accepted: false,
166
- error: {
167
- code: 'FORBIDDEN_CONTEXT',
168
- message: 'Global primary_user subject is forbidden',
169
- field: 'subject.subjectId',
170
- correlationId,
171
- },
172
- };
173
- }
174
- }
175
-
176
84
  const constructed: RequestContext = {
177
85
  companionId,
178
86
  actor: {
179
87
  actorId: actor.actorId,
180
88
  sessionId: actor.sessionId,
89
+ capabilities: Array.isArray(actor.capabilities) ? actor.capabilities : ['chat'],
90
+ authenticated: actor.authenticated !== undefined ? Boolean(actor.authenticated) : true,
181
91
  authorizationRole: actor.authorizationRole,
182
- capabilities,
183
- authenticated: Boolean(actor.authenticated),
92
+ ...actor,
184
93
  },
185
94
  conversation: {
186
- channel,
187
- audienceId,
188
- isLive: rawCtx.conversation?.isLive,
189
95
  correlationId,
96
+ channel: rawCtx.conversation?.channel || input.channel || 'direct',
97
+ isLive: rawCtx.conversation?.isLive,
98
+ ...rawCtx.conversation,
190
99
  },
191
- subject,
100
+ source: input.source || rawCtx.source || 'local',
101
+ subject: rawCtx.subject,
192
102
  };
193
103
 
194
104
  const validated = validateRequestContext(constructed);
@@ -203,9 +113,9 @@ export function mapRequestContext(
203
113
  };
204
114
  }
205
115
 
206
- // 2. Legacy compatibility envelope mapping
116
+ // 2. Synthesize clean RequestContext from request envelope
207
117
  const companionId = input.companionId || input.id;
208
- const correlationId = input.correlationId || input.conversation?.correlationId;
118
+ const correlationId = input.correlationId || input.conversation?.correlationId || (input.generateCorrelationId ? `corr-${Date.now()}` : undefined);
209
119
 
210
120
  if (!companionId) {
211
121
  return {
@@ -222,49 +132,16 @@ export function mapRequestContext(
222
132
  diagnostics.push('companion_default_mapped_for_bootstrap');
223
133
  }
224
134
 
225
- // Map legacy role to authorizationRole
226
- const legacyRole = input.role?.toString().toUpperCase();
227
- let authRole: AuthorizationRole = 'viewer';
228
- if (legacyRole === 'OWNER') {
229
- authRole = 'administrator';
230
- diagnostics.push('legacy_role_mapped_to_authorization');
231
- } else if (legacyRole === 'OPERATOR') {
232
- authRole = 'operator';
233
- diagnostics.push('legacy_role_mapped_to_authorization');
234
- } else if (legacyRole === 'VIEWER') {
235
- authRole = 'viewer';
236
- diagnostics.push('legacy_role_mapped_to_authorization');
237
- } else if (input.role) {
238
- return {
239
- accepted: false,
240
- error: {
241
- code: 'INVALID_CONTEXT',
242
- field: 'role',
243
- correlationId,
244
- },
245
- };
246
- }
247
-
248
- // Check endpoint policy vs legacy request
249
- if (endpointPolicy === 'private' || endpointPolicy === 'operator' || endpointPolicy === 'direct') {
250
- // Missing explicit channel, audience, or capability on private/operator endpoint is an error
251
- const fields: string[] = [];
252
- if (!input.channel) fields.push('conversation.channel');
253
- if (!input.audienceId) fields.push('conversation.audienceId');
254
- if (!input.capabilities && !input.actor?.capabilities) fields.push('actor.capabilities');
255
- if (!correlationId) fields.push('conversation.correlationId');
256
-
135
+ if (!correlationId) {
257
136
  return {
258
137
  accepted: false,
259
138
  error: {
260
139
  code: 'MISSING_CONTEXT',
261
- fields: fields.length > 0 ? fields : ['conversation.audienceId', 'actor.capabilities'],
262
- correlationId,
140
+ fields: ['conversation.correlationId'],
263
141
  },
264
142
  };
265
143
  }
266
144
 
267
- // Ambiguity check: if legacy input specifies role without correlationId or endpoint context for stateful op
268
145
  if (input.subject === 'primary_user' || input.subjectId === 'primary_user') {
269
146
  return {
270
147
  accepted: false,
@@ -277,55 +154,43 @@ export function mapRequestContext(
277
154
  };
278
155
  }
279
156
 
280
- // Missing correlationId for stateful requests
281
- const finalCorrelationId = correlationId || (input.generateCorrelationId ? `corr-${Date.now()}` : undefined);
282
- if (!finalCorrelationId) {
283
- return {
284
- accepted: false,
285
- error: {
286
- code: 'MISSING_CONTEXT',
287
- fields: ['conversation.correlationId'],
288
- },
289
- };
290
- }
291
-
292
- // Build anonymous public context
293
- const actorId = input.actorId || input.actor?.actorId || 'anonymous-session-a';
294
- const sessionId = input.sessionId || input.actor?.sessionId || 'session-a';
157
+ const actorId = input.actorId || input.actor?.actorId || 'local-user';
158
+ const sessionId = input.sessionId || input.actor?.sessionId || `session-${Date.now()}`;
295
159
  if (!input.actorId && !input.actor?.actorId) {
296
160
  diagnostics.push('anonymous_session_generated');
297
161
  }
298
162
 
299
- const channel: Channel = 'public';
300
- const audienceId = defaultPublicAudience;
301
- diagnostics.push('audience_defaulted_by_public_policy');
302
-
303
- const capabilities = authRole === 'administrator'
304
- ? ['chat:public', 'admin:access']
305
- : authRole === 'operator'
306
- ? ['chat:public', 'operator:access']
307
- : ['chat:public'];
163
+ const capabilities = Array.isArray(input.capabilities)
164
+ ? input.capabilities
165
+ : Array.isArray(input.actor?.capabilities)
166
+ ? input.actor.capabilities
167
+ : ['chat', 'system'];
308
168
 
309
169
  const mappedContext: RequestContext = {
310
170
  companionId,
311
171
  actor: {
312
172
  actorId,
313
173
  sessionId,
314
- authorizationRole: authRole,
315
174
  capabilities,
316
- authenticated: Boolean(input.authenticated),
175
+ authenticated: input.authenticated !== undefined ? Boolean(input.authenticated) : true,
176
+ authorizationRole: input.role ? (input.role.toLowerCase() === 'viewer' ? 'viewer' : 'administrator') : undefined,
317
177
  },
318
178
  conversation: {
319
- channel,
320
- audienceId,
321
- correlationId: finalCorrelationId,
179
+ channel: input.channel || input.conversation?.channel || 'direct',
180
+ correlationId,
322
181
  },
323
- subject: undefined, // Anonymous public chat has no subject
182
+ source: input.source || 'local',
183
+ subject: input.subject,
324
184
  };
325
185
 
186
+ const validated = validateRequestContext(mappedContext);
187
+ if (!validated.accepted) {
188
+ return validated;
189
+ }
190
+
326
191
  return {
327
192
  accepted: true,
328
- context: mappedContext,
193
+ context: validated.context,
329
194
  diagnostics: diagnostics.length > 0 ? diagnostics : undefined,
330
195
  };
331
196
  }
package/src/index.test.ts CHANGED
@@ -31,7 +31,10 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
31
31
  expect(res.status).toBe(200);
32
32
  expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith(
33
33
  'Hello neutral world',
34
- 'OWNER',
34
+ expect.objectContaining({
35
+ companionId: 'companion-a',
36
+ conversation: expect.objectContaining({ channel: 'direct' }),
37
+ }),
35
38
  []
36
39
  );
37
40
  });
@@ -51,7 +54,6 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
51
54
  },
52
55
  conversation: {
53
56
  channel: 'public',
54
- audienceId: 'audience-public',
55
57
  correlationId: 'corr-route-1',
56
58
  },
57
59
  },
@@ -62,39 +64,14 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
62
64
  expect(res.status).toBe(200);
63
65
  expect(fakeRuntime.handleUserMessage).toHaveBeenCalledWith(
64
66
  'Hello structured context',
65
- 'VIEWER',
67
+ expect.objectContaining({
68
+ companionId: 'companion-a',
69
+ conversation: expect.objectContaining({ channel: 'public' }),
70
+ }),
66
71
  []
67
72
  );
68
73
  });
69
74
 
70
- test('rejects MASTER_PRIVATE in public request with 400 and structured error', async () => {
71
- const res = await request(app)
72
- .post('/chat')
73
- .send({
74
- companionId: 'companion-a',
75
- context: {
76
- actor: {
77
- actorId: 'actor-a',
78
- sessionId: 'session-a',
79
- authorizationRole: 'viewer',
80
- capabilities: ['chat:public'],
81
- authenticated: false,
82
- },
83
- conversation: {
84
- channel: 'public',
85
- audienceId: 'MASTER_PRIVATE',
86
- correlationId: 'corr-err-1',
87
- },
88
- },
89
- message: 'Forbidden audience test',
90
- });
91
-
92
- expect(res.status).toBe(400);
93
- expect(res.body.accepted).toBe(false);
94
- expect(res.body.error.code).toBe('LEGACY_PERSONAL_AUDIENCE');
95
- expect(fakeRuntime.handleUserMessage).not.toHaveBeenCalled();
96
- });
97
-
98
75
  test('rejects global primary_user subject with 400 and structured error', async () => {
99
76
  const res = await request(app)
100
77
  .post('/chat')
@@ -110,7 +87,6 @@ describe('API Boundary Context Validation (P2 Route Integration)', () => {
110
87
  },
111
88
  conversation: {
112
89
  channel: 'public',
113
- audienceId: 'audience-public',
114
90
  correlationId: 'corr-err-2',
115
91
  },
116
92
  subject: {