@siduri-x/api 1.0.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.
- package/LICENSE +190 -0
- package/dist/app.d.ts +9 -0
- package/dist/app.js +436 -0
- package/dist/auth.d.ts +8 -0
- package/dist/auth.js +40 -0
- package/dist/auth.test.d.ts +1 -0
- package/dist/auth.test.js +48 -0
- package/dist/b0-b6.test.d.ts +1 -0
- package/dist/b0-b6.test.js +121 -0
- package/dist/context-mapper.d.ts +15 -0
- package/dist/context-mapper.js +287 -0
- package/dist/context-mapper.test.d.ts +1 -0
- package/dist/context-mapper.test.js +233 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +167 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/index.test.js +115 -0
- package/dist/runtime.d.ts +1 -0
- package/dist/runtime.js +17 -0
- package/dist/runtime.test.d.ts +1 -0
- package/dist/runtime.test.js +240 -0
- package/dist/smoke.test.d.ts +0 -0
- package/dist/smoke.test.js +6 -0
- package/dist/t4-gating.test.d.ts +1 -0
- package/dist/t4-gating.test.js +193 -0
- package/dist/t5-experience.test.d.ts +1 -0
- package/dist/t5-experience.test.js +156 -0
- package/dist/t6-security.test.d.ts +1 -0
- package/dist/t6-security.test.js +234 -0
- package/dist/t7-release.test.d.ts +1 -0
- package/dist/t7-release.test.js +119 -0
- package/jest.config.json +5 -0
- package/package.json +37 -0
- package/src/app.ts +459 -0
- package/src/auth.test.ts +57 -0
- package/src/auth.ts +49 -0
- package/src/b0-b6.test.ts +137 -0
- package/src/context-mapper.test.ts +258 -0
- package/src/context-mapper.ts +331 -0
- package/src/index.test.ts +129 -0
- package/src/index.ts +161 -0
- package/src/runtime.test.ts +284 -0
- package/src/runtime.ts +1 -0
- package/src/smoke.test.ts +5 -0
- package/src/t4-gating.test.ts +219 -0
- package/src/t5-experience.test.ts +175 -0
- package/src/t6-security.test.ts +257 -0
- package/src/t7-release.test.ts +131 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import request from 'supertest';
|
|
2
|
+
import { createApp } from './app';
|
|
3
|
+
import { SiduriRuntime } from './runtime';
|
|
4
|
+
import { Message, BrainContext, ResponsePlan } from '@siduri-x/core';
|
|
5
|
+
|
|
6
|
+
describe('T0 B0 & B6 Runtime Proof Suite', () => {
|
|
7
|
+
let mockBrain: any;
|
|
8
|
+
let mockMemory: any;
|
|
9
|
+
let mockKnowledge: any;
|
|
10
|
+
let mockBehavior: any;
|
|
11
|
+
let runtime: SiduriRuntime;
|
|
12
|
+
let app: any;
|
|
13
|
+
|
|
14
|
+
beforeEach(async () => {
|
|
15
|
+
mockBrain = {
|
|
16
|
+
generatePlan: jest.fn().mockImplementation(async (ctx: BrainContext): Promise<ResponsePlan> => {
|
|
17
|
+
return {
|
|
18
|
+
speech: 'Hello. I am a neutral companion.',
|
|
19
|
+
language: 'en',
|
|
20
|
+
};
|
|
21
|
+
}),
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
mockMemory = {
|
|
25
|
+
initialize: jest.fn().mockResolvedValue(undefined),
|
|
26
|
+
searchClaims: jest.fn().mockResolvedValue([]),
|
|
27
|
+
getClaims: jest.fn().mockResolvedValue([]),
|
|
28
|
+
getDirectives: jest.fn().mockResolvedValue([]),
|
|
29
|
+
getPendingClaims: jest.fn().mockResolvedValue([]),
|
|
30
|
+
proposeClaim: jest.fn().mockResolvedValue({}),
|
|
31
|
+
approveClaim: jest.fn().mockResolvedValue(undefined),
|
|
32
|
+
rejectClaim: jest.fn().mockResolvedValue(undefined),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
mockKnowledge = {
|
|
36
|
+
search: jest.fn().mockResolvedValue([]),
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
mockBehavior = {
|
|
40
|
+
compile: jest.fn().mockResolvedValue(''),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const config = {
|
|
44
|
+
name: 'NeutralCompanion',
|
|
45
|
+
brain: { provider: 'openrouter' },
|
|
46
|
+
memory: { provider: 'postgres' },
|
|
47
|
+
knowledge: { provider: 'e-knowledge' },
|
|
48
|
+
behavior: { provider: 'active-self' },
|
|
49
|
+
voice: { provider: 'none' },
|
|
50
|
+
vision: { provider: 'none' },
|
|
51
|
+
body: { provider: 'none' },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
runtime = new SiduriRuntime('companion-a', config as any, {
|
|
55
|
+
brain: mockBrain,
|
|
56
|
+
memory: mockMemory,
|
|
57
|
+
knowledge: mockKnowledge,
|
|
58
|
+
behavior: mockBehavior,
|
|
59
|
+
});
|
|
60
|
+
await runtime.initialize();
|
|
61
|
+
|
|
62
|
+
const runtimes = new Map([['companion-a', runtime]]);
|
|
63
|
+
const created = createApp(runtimes);
|
|
64
|
+
app = created.app;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// B0: Fresh companion is empty (no prior claims, no user relationship, no knowledge search on greeting)
|
|
68
|
+
describe('B0 — Fresh companion is empty', () => {
|
|
69
|
+
test('initial state has empty memory and empty directives', async () => {
|
|
70
|
+
const claims = await runtime.memory?.getClaims();
|
|
71
|
+
const directives = await runtime.memory?.getDirectives();
|
|
72
|
+
expect(claims).toEqual([]);
|
|
73
|
+
expect(directives).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test('greeting does not query knowledge or inject prior personal knowledge', async () => {
|
|
77
|
+
const res = await request(app)
|
|
78
|
+
.post('/chat')
|
|
79
|
+
.send({
|
|
80
|
+
companionId: 'companion-a',
|
|
81
|
+
message: 'Hello.',
|
|
82
|
+
history: [],
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
expect(res.status).toBe(200);
|
|
86
|
+
expect(mockKnowledge.search).not.toHaveBeenCalled();
|
|
87
|
+
expect(mockBrain.generatePlan).toHaveBeenCalledWith(
|
|
88
|
+
expect.objectContaining({
|
|
89
|
+
contextPrompt: '',
|
|
90
|
+
recipient: 'VIEWER',
|
|
91
|
+
})
|
|
92
|
+
);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// B6: Identity and relationship are learned, not inferred (self identity questions do not query external knowledge)
|
|
97
|
+
describe('B6 — Identity and relationship are learned, not inferred', () => {
|
|
98
|
+
test('asking "Who are you?" suppresses knowledge query and asserts self identity without external search', async () => {
|
|
99
|
+
mockBrain.generatePlan.mockResolvedValueOnce({
|
|
100
|
+
speech: 'I am NeutralCompanion.',
|
|
101
|
+
language: 'en',
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const res = await request(app)
|
|
105
|
+
.post('/chat')
|
|
106
|
+
.send({
|
|
107
|
+
companionId: 'companion-a',
|
|
108
|
+
message: 'Who are you?',
|
|
109
|
+
history: [],
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
expect(res.status).toBe(200);
|
|
113
|
+
// B6 oracle: self identity chat does not query external knowledge
|
|
114
|
+
expect(mockKnowledge.search).not.toHaveBeenCalled();
|
|
115
|
+
expect(mockMemory.searchClaims).toHaveBeenCalled();
|
|
116
|
+
expect(mockBrain.generatePlan).toHaveBeenCalledWith(
|
|
117
|
+
expect.objectContaining({
|
|
118
|
+
contextPrompt: '',
|
|
119
|
+
})
|
|
120
|
+
);
|
|
121
|
+
expect(res.body.response.subtitle_en).toBe('I am NeutralCompanion.');
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('asking "Tell me about yourself" suppresses knowledge query', async () => {
|
|
125
|
+
const res = await request(app)
|
|
126
|
+
.post('/chat')
|
|
127
|
+
.send({
|
|
128
|
+
companionId: 'companion-a',
|
|
129
|
+
message: 'Tell me about yourself',
|
|
130
|
+
history: [],
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(res.status).toBe(200);
|
|
134
|
+
expect(mockKnowledge.search).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
});
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { mapRequestContext } from './context-mapper';
|
|
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', () => {
|
|
6
|
+
const input = {
|
|
7
|
+
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: [],
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const result = mapRequestContext(input, {
|
|
26
|
+
endpointPolicy: 'public',
|
|
27
|
+
defaultPublicAudience: 'audience-public',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
expect(result.accepted).toBe(true);
|
|
31
|
+
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');
|
|
52
|
+
expect(result.context?.subject).toBeUndefined();
|
|
53
|
+
expect(result.diagnostics).toContain('legacy_role_mapped_to_authorization');
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// T1-03: Private chat without capability
|
|
57
|
+
test('T1-03: Private chat without capability is rejected before organ invocation', () => {
|
|
58
|
+
const input = {
|
|
59
|
+
companionId: 'companion-a',
|
|
60
|
+
context: {
|
|
61
|
+
actor: {
|
|
62
|
+
actorId: 'actor-a',
|
|
63
|
+
sessionId: 'session-a',
|
|
64
|
+
authorizationRole: 'viewer',
|
|
65
|
+
capabilities: ['chat:public'], // Missing 'chat:private'
|
|
66
|
+
authenticated: true,
|
|
67
|
+
},
|
|
68
|
+
conversation: {
|
|
69
|
+
channel: 'private',
|
|
70
|
+
audienceId: 'audience-private-a',
|
|
71
|
+
correlationId: 'corr-private-a',
|
|
72
|
+
},
|
|
73
|
+
},
|
|
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',
|
|
89
|
+
};
|
|
90
|
+
|
|
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
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// T1-05: Global primary_user input
|
|
100
|
+
test('T1-05: Global primary_user input is rejected/quarantined at mapper', () => {
|
|
101
|
+
const input = {
|
|
102
|
+
companionId: 'companion-a',
|
|
103
|
+
context: {
|
|
104
|
+
actor: {
|
|
105
|
+
actorId: 'actor-a',
|
|
106
|
+
sessionId: 'session-a',
|
|
107
|
+
authorizationRole: 'viewer',
|
|
108
|
+
capabilities: ['chat:public'],
|
|
109
|
+
authenticated: true,
|
|
110
|
+
},
|
|
111
|
+
conversation: {
|
|
112
|
+
channel: 'public',
|
|
113
|
+
audienceId: 'audience-public',
|
|
114
|
+
correlationId: 'corr-primary-user',
|
|
115
|
+
},
|
|
116
|
+
subject: {
|
|
117
|
+
subjectId: 'primary_user',
|
|
118
|
+
kind: 'actor',
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = mapRequestContext(input);
|
|
124
|
+
expect(result.accepted).toBe(false);
|
|
125
|
+
expect(result.error?.code).toBe('FORBIDDEN_CONTEXT');
|
|
126
|
+
expect(result.error?.field).toBe('subject.subjectId');
|
|
127
|
+
});
|
|
128
|
+
|
|
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', () => {
|
|
157
|
+
const input = {
|
|
158
|
+
companionId: 'companion-a',
|
|
159
|
+
context: {
|
|
160
|
+
actor: {
|
|
161
|
+
actorId: '',
|
|
162
|
+
sessionId: 'session-a',
|
|
163
|
+
authorizationRole: 'viewer',
|
|
164
|
+
capabilities: ['chat:public'],
|
|
165
|
+
authenticated: false,
|
|
166
|
+
},
|
|
167
|
+
conversation: {
|
|
168
|
+
channel: 'public',
|
|
169
|
+
audienceId: 'audience-public',
|
|
170
|
+
correlationId: 'corr-teach-no-actor',
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const result = mapRequestContext(input);
|
|
176
|
+
expect(result.accepted).toBe(false);
|
|
177
|
+
expect(result.error?.code).toBe('MISSING_CONTEXT');
|
|
178
|
+
expect(result.error?.fields).toContain('actor.actorId');
|
|
179
|
+
});
|
|
180
|
+
|
|
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', () => {
|
|
183
|
+
const actor = {
|
|
184
|
+
actorId: 'actor-a',
|
|
185
|
+
sessionId: 'session-a',
|
|
186
|
+
authorizationRole: 'viewer' as const,
|
|
187
|
+
capabilities: ['chat:public'],
|
|
188
|
+
authenticated: true,
|
|
189
|
+
};
|
|
190
|
+
const conv = {
|
|
191
|
+
channel: 'public' as const,
|
|
192
|
+
audienceId: 'audience-public',
|
|
193
|
+
correlationId: 'corr-iso-1',
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const reqA = { companionId: 'companion-a', context: { actor, conversation: conv } };
|
|
197
|
+
const reqB = { companionId: 'companion-b', context: { actor, conversation: conv } };
|
|
198
|
+
|
|
199
|
+
const resA = mapRequestContext(reqA);
|
|
200
|
+
const resB = mapRequestContext(reqB);
|
|
201
|
+
|
|
202
|
+
expect(resA.accepted).toBe(true);
|
|
203
|
+
expect(resB.accepted).toBe(true);
|
|
204
|
+
expect(resA.context?.companionId).toBe('companion-a');
|
|
205
|
+
expect(resB.context?.companionId).toBe('companion-b');
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
// T1-09: Missing correlation ID
|
|
209
|
+
test('T1-09: Missing correlation ID is rejected before stateful operation', () => {
|
|
210
|
+
const input = {
|
|
211
|
+
companionId: 'companion-a',
|
|
212
|
+
context: {
|
|
213
|
+
actor: {
|
|
214
|
+
actorId: 'actor-a',
|
|
215
|
+
sessionId: 'session-a',
|
|
216
|
+
authorizationRole: 'viewer',
|
|
217
|
+
capabilities: ['chat:public'],
|
|
218
|
+
authenticated: true,
|
|
219
|
+
},
|
|
220
|
+
conversation: {
|
|
221
|
+
channel: 'public',
|
|
222
|
+
audienceId: 'audience-public',
|
|
223
|
+
correlationId: '',
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
const result = mapRequestContext(input);
|
|
229
|
+
expect(result.accepted).toBe(false);
|
|
230
|
+
expect(result.error?.code).toBe('MISSING_CONTEXT');
|
|
231
|
+
expect(result.error?.fields).toContain('conversation.correlationId');
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
// T1-10: Ambiguous legacy request
|
|
235
|
+
test('T1-10: Ambiguous legacy request emits structured diagnostic and rejects ambiguous mapping', () => {
|
|
236
|
+
const input = {
|
|
237
|
+
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
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
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
|
+
);
|
|
257
|
+
});
|
|
258
|
+
});
|