@openchambery/web 1.19.13-beta.1 → 1.19.13-beta.3
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/dist/assets/{AgentAvatar-CiQIIM0d.js → AgentAvatar-mbvX7YlW.js} +1 -1
- package/dist/assets/AssistantView-DOZPK9oh.js +2 -0
- package/dist/assets/DiagramView-W0IHFk-1.js +1 -0
- package/dist/assets/FadeInOnReveal-moBbL8Ll.js +1 -0
- package/dist/assets/{Icon-CeKHYyKZ.js → Icon-CkzzhaLr.js} +1 -1
- package/dist/assets/MarkdownRenderer-v7n_D6nL.js +2 -0
- package/dist/assets/MarkdownRendererImpl-Buz5LZsa.js +7 -0
- package/dist/assets/{MarkstreamRendererImpl-B-odd2Gz.js → MarkstreamRendererImpl-D4nUXYpP.js} +1 -1
- package/dist/assets/MobileDetailNavigation-Bfs4CTUh.js +1 -0
- package/dist/assets/MobileShareBridge-DtwVLkBL.js +4 -0
- package/dist/assets/MobileSurface-BmYYdY3g.js +2 -0
- package/dist/assets/{MultiRunWindow-BT5Dwgzu.js → MultiRunWindow-yWIHgr89.js} +1 -1
- package/dist/assets/OnboardingScreen-j0iCyH7f.js +1 -0
- package/dist/assets/{OverlayScrollbar-DUghqFGg.js → OverlayScrollbar-CDC5B3RH.js} +1 -1
- package/dist/assets/SettingsView-C3BbLpHT.js +25 -0
- package/dist/assets/{SettingsWindow-DfEGfyr8.js → SettingsWindow-b0JWdygM.js} +1 -1
- package/dist/assets/TerminalView-C74IvmOt.js +8 -0
- package/dist/assets/ToolOutputDialog-BORu8CtR.js +10 -0
- package/dist/assets/{appThemeRegistry-CIZ-shMD.js → appThemeRegistry-DOwhB93n.js} +1 -1
- package/dist/assets/assistant-attachment-cache-C0aRnFkj.js +1 -0
- package/dist/assets/{button-DFPOdZ0g.js → button-xx1UOBA5.js} +1 -1
- package/dist/assets/{clipboard-C05anBwU.js → clipboard-CUasgPiF.js} +1 -1
- package/dist/assets/{collapsible-oGbEvbe1.js → collapsible-CACHBRYZ.js} +1 -1
- package/dist/assets/{context-menu-DGz4_2hs.js → context-menu-CYbm4wBZ.js} +1 -1
- package/dist/assets/desktopHosts-DILvrAok.js +1 -0
- package/dist/assets/desktopRelayRestore-t8asCr3m.js +1 -0
- package/dist/assets/{diagram-Csk5GpDC.js → diagram-u0BC9uuY.js} +1 -1
- package/dist/assets/{dialog-rrqB-H6q.js → dialog-BwQ3rY_x.js} +1 -1
- package/dist/assets/dualLimitLru-D8Sphgua.js +1 -0
- package/dist/assets/{es-FE0sJ9EC.js → es-CRjOwvYM.js} +1 -1
- package/dist/assets/fileQueries-Bgly-E8f.js +1 -0
- package/dist/assets/{fr-DNDCSfGt.js → fr-D9taEN9L.js} +1 -1
- package/dist/assets/{gitApi-_P0FsoXe.js → gitApi-D1mvv14J.js} +1 -1
- package/dist/assets/imageSource-BlAJJUEI.js +1 -0
- package/dist/assets/{input-BO3r20op.js → input-CI730njs.js} +1 -1
- package/dist/assets/insertionBoundaries-BBn8YW82.js +4 -0
- package/dist/assets/{ja-BDwrk0cM.js → ja-CYQ5aoWp.js} +1 -1
- package/dist/assets/{ko-BeeU2o4I.js → ko-BwASV0Q0.js} +1 -1
- package/dist/assets/main-Cd7NwXx5.js +169 -0
- package/dist/assets/main-phpzFgMN.js +2 -0
- package/dist/assets/markdown-worker-CptoICmq.js +2 -0
- package/dist/assets/markdownHeightCache-DEjGKLv8.js +3 -0
- package/dist/assets/markdownTheme-DOfGg-A7.js +1 -0
- package/dist/assets/miniChat-D6UYe50H.js +2 -0
- package/dist/assets/mobile-w2Ed9I5O.js +2 -0
- package/dist/assets/{multirun-CxhTsmRq.js → multirun-CYuxJPBX.js} +1 -1
- package/dist/assets/persistence-YbQKzHUQ.js +3 -0
- package/dist/assets/{pl-BYuzpcI5.js → pl-DUI1vPil.js} +1 -1
- package/dist/assets/platform-DzsGqlGW.js +2 -0
- package/dist/assets/projectMeta-Cir66uF3.js +3 -0
- package/dist/assets/{pt-BR-Bn7cwEzv.js → pt-BR-CZQFJllP.js} +1 -1
- package/dist/assets/relay-image-stream-DM7atqJs.js +2 -0
- package/dist/assets/renderElectronMiniChatApp-BzYqqr9n.js +2 -0
- package/dist/assets/renderMobileApp-BvIE86_O.js +28 -0
- package/dist/assets/runtime-auth-D66yq6ie.js +1 -0
- package/dist/assets/runtime-fetch-zIalUnbb.js +1 -0
- package/dist/assets/runtime-switch-LvWDU-SL.js +1 -0
- package/dist/assets/runtimeConfig-BXf9GQ0Q.js +2 -0
- package/dist/assets/runtimeEndpointReset-lF5S7U4I.js +147 -0
- package/dist/assets/{sessionChangeSummary-DDVT7O5J.js → sessionChangeSummary-DQDZ5HW-.js} +1 -1
- package/dist/assets/{sessionLookup-C8VZn0gJ.js → sessionLookup-CNRDupYq.js} +1 -1
- package/dist/assets/sortable-tabs-strip-Dom6Sthu.js +1 -0
- package/dist/assets/src-AA-5nXsF.css +2 -0
- package/dist/assets/{store-n94dSb3M.js → store-CSnH_QXM.js} +2 -2
- package/dist/assets/terminalApi-noa4Cdbh.js +1 -0
- package/dist/assets/{textarea-BjX-KhMN.js → textarea-BfNtUdJE.js} +1 -1
- package/dist/assets/{tooltip-CQGqGAG_.js → tooltip-DPxumadF.js} +1 -1
- package/dist/assets/{uk-CwpbHEa3.js → uk-C1FYZlmA.js} +1 -1
- package/dist/assets/{url-BMjqccCo.js → url-CVhsI9kR.js} +1 -1
- package/dist/assets/{useAppFontEffects-BIFw36SA.js → useAppFontEffects-NVyoI_Ei.js} +32 -32
- package/dist/assets/useAssistantUIStore-CLPCNoj_.js +1 -0
- package/dist/assets/useDesktopWindowControlsLayout-CP3NmMYG.js +1 -0
- package/dist/assets/{useEffectiveDirectory-DJSYYkVp.js → useEffectiveDirectory-AUoqSyOJ.js} +1 -1
- package/dist/assets/useFontPreferences-CB-cGo3I.js +1 -0
- package/dist/assets/useMobileNavigationStore-DANrWekj.js +413 -0
- package/dist/assets/useSessionAutoCleanup-ClvQQPqQ.js +2 -0
- package/dist/assets/useUIStore-CxOoCv7B.js +1 -0
- package/dist/assets/useWorkerHighlightedLines-DOk4cAxv.js +7 -0
- package/dist/assets/{utils-81nUI_xp.js → utils-BHTVlVov.js} +1 -1
- package/dist/assets/{zh-CN-BpzFZ-LU.js → zh-CN-C96qP1ip.js} +1 -1
- package/dist/assets/{zh-TW-Bopgc5Eo.js → zh-TW-BsRk1Rbb.js} +1 -1
- package/dist/index.html +12 -9
- package/dist/mini-chat.html +12 -9
- package/dist/mobile.html +12 -9
- package/package.json +1 -1
- package/server/lib/assistant-delivery-parts.js +19 -1
- package/server/lib/assistants/DOCUMENTATION.md +39 -7
- package/server/lib/assistants/cards.js +16 -2
- package/server/lib/assistants/cards.test.js +16 -1
- package/server/lib/assistants/contact-attachments.http.test.js +436 -0
- package/server/lib/assistants/contact-attachments.js +814 -0
- package/server/lib/assistants/contact-attachments.p1.test.js +307 -0
- package/server/lib/assistants/contact-attachments.test.js +249 -0
- package/server/lib/assistants/contact-store.js +494 -25
- package/server/lib/assistants/contact-store.test.js +403 -1
- package/server/lib/assistants/contact-tools.js +6 -5
- package/server/lib/assistants/contact-tools.test.js +24 -6
- package/server/lib/assistants/contracts.js +41 -1
- package/server/lib/assistants/harness.test.js +124 -0
- package/server/lib/assistants/routes.js +46 -1
- package/server/lib/assistants/service.js +258 -66
- package/server/lib/assistants/service.test.js +400 -30
- package/server/lib/fs/prompt-attachment-store.js +663 -0
- package/server/lib/fs/prompt-attachment-store.test.js +263 -0
- package/server/lib/fs/routes.js +28 -72
- package/server/lib/fs/routes.test.js +15 -30
- package/server/lib/opencode/core-routes.js +2 -1
- package/dist/assets/AssistantView-YmeyxehG.js +0 -2
- package/dist/assets/DiagramView-BpZb-G9P.js +0 -1
- package/dist/assets/FadeInOnReveal-nf6-ZCrs.js +0 -1
- package/dist/assets/MarkdownRenderer-wpeGdciL.js +0 -2
- package/dist/assets/MarkdownRendererImpl-Peu0GVSQ.js +0 -7
- package/dist/assets/MobileDetailNavigation-CtYke_1C.js +0 -1
- package/dist/assets/MobileShareBridge-DIfOl8EZ.js +0 -4
- package/dist/assets/MobileSurface-_KoX75Ff.js +0 -2
- package/dist/assets/OnboardingScreen-BPusUgFG.js +0 -1
- package/dist/assets/SettingsView-BwQpbIZl.js +0 -25
- package/dist/assets/TerminalView-8402vpcO.js +0 -8
- package/dist/assets/ToolOutputDialog-B5RdkhsG.js +0 -10
- package/dist/assets/desktopHosts-BVJXQq1S.js +0 -1
- package/dist/assets/desktopRelayRestore-RDfuj1M7.js +0 -1
- package/dist/assets/fileQueries-BcdWOPlD.js +0 -1
- package/dist/assets/insertionBoundaries-CWoioTdE.js +0 -4
- package/dist/assets/main-CLj_WS1o.js +0 -2
- package/dist/assets/main-CnSZ89pD.js +0 -169
- package/dist/assets/markdown-worker-CZWE1F1M.js +0 -2
- package/dist/assets/markdownHeightCache-Cdd3hkMl.js +0 -3
- package/dist/assets/markdownTheme-DIB8WVmz.js +0 -2
- package/dist/assets/miniChat-B9c3y2pD.js +0 -2
- package/dist/assets/mobile-DvbXUfNS.js +0 -2
- package/dist/assets/persistence-CVTWFaqe.js +0 -3
- package/dist/assets/platform-BY_Gtsqr.js +0 -2
- package/dist/assets/projectMeta-Dl5iPCB6.js +0 -3
- package/dist/assets/renderElectronMiniChatApp-CkJ51qCh.js +0 -2
- package/dist/assets/renderMobileApp-DcjQPhWw.js +0 -28
- package/dist/assets/runtimeConfig-Bsk2kWsW.js +0 -2
- package/dist/assets/runtimeEndpointReset-BBLFgl85.js +0 -147
- package/dist/assets/sortable-tabs-strip-s7UUxsa6.js +0 -1
- package/dist/assets/src-BpHDMgud.css +0 -2
- package/dist/assets/terminalApi-C54_rKEK.js +0 -1
- package/dist/assets/useAssistantUIStore-Cg9bsT4b.js +0 -1
- package/dist/assets/useDesktopWindowControlsLayout-DoygEcN4.js +0 -1
- package/dist/assets/useFontPreferences-B5_apbBC.js +0 -1
- package/dist/assets/useMobileNavigationStore-DuTu0Mzh.js +0 -413
- package/dist/assets/useSessionAutoCleanup-C-L9teva.js +0 -1
- package/dist/assets/useUIStore-DwpQTjLO.js +0 -1
- package/dist/assets/useWorkerHighlightedLines-DxTfgdEi.js +0 -7
|
@@ -12,6 +12,21 @@ import {
|
|
|
12
12
|
|
|
13
13
|
const require = createRequire(import.meta.url);
|
|
14
14
|
const root = () => fs.mkdtempSync(path.join(os.tmpdir(), 'assistants-'));
|
|
15
|
+
/** Minimal valid 1×1 PNG (strict data-URL / image magic fixtures). */
|
|
16
|
+
const FIXTURE_PNG = Buffer.from([
|
|
17
|
+
0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
|
|
18
|
+
0x00, 0x00, 0x00, 0x0d, 0x49, 0x48, 0x44, 0x52,
|
|
19
|
+
0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01,
|
|
20
|
+
0x08, 0x02, 0x00, 0x00, 0x00, 0x90, 0x77, 0x53,
|
|
21
|
+
0xde, 0x00, 0x00, 0x00, 0x0c, 0x49, 0x44, 0x41,
|
|
22
|
+
0x54, 0x08, 0xd7, 0x63, 0xf8, 0xcf, 0xc0, 0x00,
|
|
23
|
+
0x00, 0x00, 0x03, 0x00, 0x01, 0x00, 0x05, 0xfe,
|
|
24
|
+
0xd4, 0xef, 0x00, 0x00, 0x00, 0x00, 0x49, 0x45,
|
|
25
|
+
0x4e, 0x44, 0xae, 0x42, 0x60, 0x82,
|
|
26
|
+
]);
|
|
27
|
+
const FIXTURE_PNG_DATA_URL = `data:image/png;base64,${FIXTURE_PNG.toString('base64')}`;
|
|
28
|
+
/** Valid text/plain data URL body "x". */
|
|
29
|
+
const FIXTURE_TEXT_DATA_URL = 'data:text/plain;base64,eA==';
|
|
15
30
|
// Behavioral tests enable the global switch after boot; pass enabled:false to assert the fresh-install default.
|
|
16
31
|
const setup = (directory = root(), client = {}, options = {}) => {
|
|
17
32
|
const { enabled = true, ...serviceOptions } = options;
|
|
@@ -129,6 +144,80 @@ describe('assistants service', () => {
|
|
|
129
144
|
service.close();
|
|
130
145
|
});
|
|
131
146
|
|
|
147
|
+
it('exposes latestMessagePreview on create/update/snapshot from contact transcript', async () => {
|
|
148
|
+
const directory = root();
|
|
149
|
+
const service = setup(directory, {}, {
|
|
150
|
+
runContactTurn: async ({ userText }) => ({ text: `ok ${userText}`, bubbles: [`ok ${userText}`] }),
|
|
151
|
+
});
|
|
152
|
+
const created = service.createAssistant(assistantInput);
|
|
153
|
+
expect(created.latestMessagePreview).toBeNull();
|
|
154
|
+
expect(service.snapshot().assistants[0].latestMessagePreview).toBeNull();
|
|
155
|
+
|
|
156
|
+
await settleSend(service, created.id, { messageID: 'prev_u1', parts: [{ type: 'text', text: 'hello preview' }] });
|
|
157
|
+
const afterUser = service.snapshot().assistants[0].latestMessagePreview;
|
|
158
|
+
// After turn settle the assistant reply is newest.
|
|
159
|
+
expect(afterUser).toMatchObject({
|
|
160
|
+
role: 'assistant',
|
|
161
|
+
text: 'ok hello preview',
|
|
162
|
+
fallbackKind: null,
|
|
163
|
+
});
|
|
164
|
+
expect(typeof afterUser.messageID).toBe('string');
|
|
165
|
+
expect(Number.isFinite(afterUser.ordinal)).toBe(true);
|
|
166
|
+
|
|
167
|
+
const updated = await service.updateAssistant(created.id, {
|
|
168
|
+
expectedRevision: service.snapshot().assistants[0].revision,
|
|
169
|
+
name: 'Renamed',
|
|
170
|
+
});
|
|
171
|
+
expect(updated.latestMessagePreview).toMatchObject({ text: 'ok hello preview', role: 'assistant' });
|
|
172
|
+
|
|
173
|
+
// Clear-memory keeps transcript → preview stays on last bubble.
|
|
174
|
+
const memServiceClose = service;
|
|
175
|
+
// use existing service with clear tool path
|
|
176
|
+
memServiceClose.close();
|
|
177
|
+
const service2 = setup(directory, {}, {
|
|
178
|
+
runContactTurn: async ({ userText, tools }) => {
|
|
179
|
+
if (userText === '开新对话') {
|
|
180
|
+
await tools.find((item) => item.name === 'new_conversation').execute('mem', {});
|
|
181
|
+
return { text: NEW_CONVERSATION_CONFIRM_BUBBLE, bubbles: [NEW_CONVERSATION_CONFIRM_BUBBLE], reset: true };
|
|
182
|
+
}
|
|
183
|
+
if (userText === '清空聊天记录') {
|
|
184
|
+
const tool = tools.find((item) => item.name === 'clear_chat_history');
|
|
185
|
+
const result = await tool.execute('wipe', {});
|
|
186
|
+
return {
|
|
187
|
+
text: result.content[0].text,
|
|
188
|
+
bubbles: [result.content[0].text],
|
|
189
|
+
reset: true,
|
|
190
|
+
historyCleared: true,
|
|
191
|
+
messages: [{ role: 'toolResult', toolName: 'clear_chat_history', details: result.details }],
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
return { text: `ok ${userText}`, bubbles: [`ok ${userText}`] };
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
const id = service2.snapshot().assistants[0].id;
|
|
198
|
+
expect(service2.snapshot().assistants[0].latestMessagePreview?.text).toBe('ok hello preview');
|
|
199
|
+
await settleSend(service2, id, { messageID: 'prev_mem', parts: [{ type: 'text', text: '开新对话' }] });
|
|
200
|
+
expect(service2.snapshot().assistants[0].latestMessagePreview?.text).toBe(NEW_CONVERSATION_CONFIRM_BUBBLE);
|
|
201
|
+
// Transcript still has older rows after clear-memory.
|
|
202
|
+
expect(service2.contactMessages(id, { limit: 50 }).messages.some((m) => m.text === 'hello preview')).toBe(true);
|
|
203
|
+
|
|
204
|
+
await settleSend(service2, id, { messageID: 'prev_wipe', parts: [{ type: 'text', text: '清空聊天记录' }] });
|
|
205
|
+
expect(service2.snapshot().assistants[0].latestMessagePreview).toMatchObject({
|
|
206
|
+
text: CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE,
|
|
207
|
+
role: 'assistant',
|
|
208
|
+
});
|
|
209
|
+
expect(service2.contactMessages(id, { limit: 50 }).messages.some((m) => m.text === 'hello preview')).toBe(false);
|
|
210
|
+
|
|
211
|
+
service2.close();
|
|
212
|
+
const cold = setup(directory, {}, {
|
|
213
|
+
runContactTurn: async ({ userText }) => ({ text: `ok ${userText}`, bubbles: [`ok ${userText}`] }),
|
|
214
|
+
});
|
|
215
|
+
expect(cold.snapshot().assistants[0].latestMessagePreview).toMatchObject({
|
|
216
|
+
text: CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE,
|
|
217
|
+
});
|
|
218
|
+
cold.close();
|
|
219
|
+
});
|
|
220
|
+
|
|
132
221
|
it('new_conversation clears LLM memory while keeping the full transcript', async () => {
|
|
133
222
|
const directory = root();
|
|
134
223
|
let creates = 0;
|
|
@@ -197,8 +286,8 @@ describe('assistants service', () => {
|
|
|
197
286
|
messageID: 'attach_1',
|
|
198
287
|
parts: [
|
|
199
288
|
{ type: 'text', text: 'look at these' },
|
|
200
|
-
{ type: 'file', mime: 'image/png', url:
|
|
201
|
-
{ type: 'file', mime: 'text/plain', url:
|
|
289
|
+
{ type: 'file', mime: 'image/png', url: FIXTURE_PNG_DATA_URL, filename: 'dot.png' },
|
|
290
|
+
{ type: 'file', mime: 'text/plain', url: FIXTURE_TEXT_DATA_URL, filename: 'note.txt' },
|
|
202
291
|
],
|
|
203
292
|
});
|
|
204
293
|
await settleSend(service, assistant.id, { messageID: 'reset_leftover', parts: [{ type: 'text', text: '开新对话' }] });
|
|
@@ -250,11 +339,13 @@ describe('assistants service', () => {
|
|
|
250
339
|
assistantID: assistant.id,
|
|
251
340
|
reset: true,
|
|
252
341
|
historyCleared: true,
|
|
342
|
+
generation: 1,
|
|
253
343
|
});
|
|
254
344
|
expect(creates).toBe(1);
|
|
255
345
|
expect(service.contactMessages(assistant.id, { limit: 50 })).toMatchObject({
|
|
256
346
|
messages: [],
|
|
257
347
|
complete: true,
|
|
348
|
+
generation: 1,
|
|
258
349
|
});
|
|
259
350
|
await settleSend(service, assistant.id, { messageID: 'again_1', parts: [{ type: 'text', text: 'hello again' }] });
|
|
260
351
|
await settleSend(service, assistant.id, { messageID: 'wipe_1', parts: [{ type: 'text', text: '清空聊天记录' }] });
|
|
@@ -277,22 +368,26 @@ describe('assistants service', () => {
|
|
|
277
368
|
service.close();
|
|
278
369
|
});
|
|
279
370
|
|
|
280
|
-
it('
|
|
371
|
+
it('clear_chat_history executes when the model calls it without a user-text auth gate', async () => {
|
|
281
372
|
const directory = root();
|
|
282
373
|
const wipeAttempts = [];
|
|
283
374
|
const service = setup(directory, {}, {
|
|
284
375
|
runContactTurn: async ({ userText, tools }) => {
|
|
285
|
-
if (userText === '
|
|
376
|
+
if (userText === '请处理一下') {
|
|
286
377
|
const tool = tools.find((item) => item.name === 'clear_chat_history');
|
|
287
|
-
const result = await tool.execute(
|
|
378
|
+
const result = await tool.execute('model_chose_wipe', {});
|
|
288
379
|
wipeAttempts.push({
|
|
289
|
-
userText,
|
|
290
380
|
error: result.details?.error || null,
|
|
381
|
+
denied: result.details?.denied === true,
|
|
291
382
|
historyCleared: result.details?.historyCleared === true,
|
|
383
|
+
terminate: result.terminate,
|
|
384
|
+
text: result.content?.[0]?.text,
|
|
292
385
|
});
|
|
293
386
|
return {
|
|
294
|
-
text:
|
|
295
|
-
bubbles: [
|
|
387
|
+
text: result.content[0].text,
|
|
388
|
+
bubbles: [result.content[0].text],
|
|
389
|
+
reset: true,
|
|
390
|
+
historyCleared: true,
|
|
296
391
|
messages: [{
|
|
297
392
|
role: 'toolResult',
|
|
298
393
|
toolName: 'clear_chat_history',
|
|
@@ -305,17 +400,187 @@ describe('assistants service', () => {
|
|
|
305
400
|
});
|
|
306
401
|
const assistant = service.createAssistant(assistantInput);
|
|
307
402
|
await settleSend(service, assistant.id, { messageID: 'seed_keep', parts: [{ type: 'text', text: 'keep-me' }] });
|
|
308
|
-
await settleSend(service, assistant.id, { messageID: '
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
403
|
+
await settleSend(service, assistant.id, { messageID: 'model_wipe', parts: [{ type: 'text', text: '请处理一下' }] });
|
|
404
|
+
expect(wipeAttempts).toEqual([{
|
|
405
|
+
error: null,
|
|
406
|
+
denied: false,
|
|
407
|
+
historyCleared: true,
|
|
408
|
+
terminate: true,
|
|
409
|
+
text: CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE,
|
|
410
|
+
}]);
|
|
314
411
|
const texts = service.contactMessages(assistant.id, { limit: 50 }).messages.map((message) => message.text);
|
|
315
|
-
expect(texts).toContain('keep-me');
|
|
316
|
-
expect(texts).
|
|
317
|
-
expect(texts).
|
|
318
|
-
|
|
412
|
+
expect(texts).not.toContain('keep-me');
|
|
413
|
+
expect(texts).toEqual(['请处理一下', CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE]);
|
|
414
|
+
expect(texts.some((text) => /not cleared|denied|new_conversation instead/i.test(text))).toBe(false);
|
|
415
|
+
service.close();
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('tool wipe keeps later-admitted queued B and replays B only once', async () => {
|
|
419
|
+
const directory = root();
|
|
420
|
+
const turnRuns = [];
|
|
421
|
+
let aReachedTurn;
|
|
422
|
+
const aReachedGate = new Promise((resolve) => { aReachedTurn = resolve; });
|
|
423
|
+
let bAdmitted;
|
|
424
|
+
const bAdmittedGate = new Promise((resolve) => { bAdmitted = resolve; });
|
|
425
|
+
const service = setup(directory, {}, {
|
|
426
|
+
runContactTurn: async ({ userText, tools }) => {
|
|
427
|
+
turnRuns.push(userText);
|
|
428
|
+
if (userText === '清空聊天记录') {
|
|
429
|
+
aReachedTurn();
|
|
430
|
+
await bAdmittedGate;
|
|
431
|
+
const tool = tools.find((item) => item.name === 'clear_chat_history');
|
|
432
|
+
const result = await tool.execute('wipe_queue_a', {});
|
|
433
|
+
return {
|
|
434
|
+
text: result.content[0].text,
|
|
435
|
+
bubbles: [result.content[0].text],
|
|
436
|
+
reset: true,
|
|
437
|
+
historyCleared: true,
|
|
438
|
+
messages: [{
|
|
439
|
+
role: 'toolResult',
|
|
440
|
+
toolName: 'clear_chat_history',
|
|
441
|
+
details: result.details,
|
|
442
|
+
}],
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
446
|
+
},
|
|
447
|
+
});
|
|
448
|
+
const assistant = service.createAssistant(assistantInput);
|
|
449
|
+
await settleSend(service, assistant.id, { messageID: 'seed_queue', parts: [{ type: 'text', text: 'seed before wipe' }] });
|
|
450
|
+
// A pause -> B admission -> A wipe -> B complete -> replay B once.
|
|
451
|
+
const aSend = service.send(assistant.id, { messageID: 'wipe_a', parts: [{ type: 'text', text: '清空聊天记录' }] });
|
|
452
|
+
await aReachedGate;
|
|
453
|
+
const bSend = service.send(assistant.id, { messageID: 'queued_b', parts: [{ type: 'text', text: 'queued B keep me' }] });
|
|
454
|
+
await bSend;
|
|
455
|
+
bAdmitted();
|
|
456
|
+
await aSend;
|
|
457
|
+
await service.whenContactTurnSettled('wipe_a');
|
|
458
|
+
await service.whenContactTurnSettled('queued_b');
|
|
459
|
+
// Same messageID + payload replays admission only — no second lane run.
|
|
460
|
+
const replay = await service.send(assistant.id, { messageID: 'queued_b', parts: [{ type: 'text', text: 'queued B keep me' }] });
|
|
461
|
+
expect(replay).toMatchObject({ admitted: true, replayed: true, messageID: 'queued_b' });
|
|
462
|
+
const page = service.contactMessages(assistant.id, { limit: 50 });
|
|
463
|
+
const texts = page.messages.map((message) => message.text);
|
|
464
|
+
expect(texts).not.toContain('seed before wipe');
|
|
465
|
+
expect(texts).toContain('queued B keep me');
|
|
466
|
+
expect(texts).toContain('reply:queued B keep me');
|
|
467
|
+
expect(texts).toContain(CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE);
|
|
468
|
+
// Wipe user kept in place (no re-admit reorder before B).
|
|
469
|
+
const wipeIdx = page.messages.findIndex((message) => message.messageID === 'wipe_a');
|
|
470
|
+
const bIdx = page.messages.findIndex((message) => message.messageID === 'queued_b');
|
|
471
|
+
expect(wipeIdx).toBeGreaterThanOrEqual(0);
|
|
472
|
+
expect(bIdx).toBeGreaterThan(wipeIdx);
|
|
473
|
+
expect(turnRuns.filter((text) => text === 'queued B keep me')).toHaveLength(1);
|
|
474
|
+
expect(page.messages.filter((message) => message.messageID === 'queued_b')).toHaveLength(1);
|
|
475
|
+
service.close();
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
it('tool wipe drops late prior-lane assistant/cards after reverse admit order and keeps C once', async () => {
|
|
479
|
+
const directory = root();
|
|
480
|
+
const turnRuns = [];
|
|
481
|
+
const cHistories = [];
|
|
482
|
+
let aReachedTurn;
|
|
483
|
+
const aReachedGate = new Promise((resolve) => { aReachedTurn = resolve; });
|
|
484
|
+
let lateWritten;
|
|
485
|
+
const lateWrittenGate = new Promise((resolve) => { lateWritten = resolve; });
|
|
486
|
+
let wipeMayRun;
|
|
487
|
+
const wipeMayRunGate = new Promise((resolve) => { wipeMayRun = resolve; });
|
|
488
|
+
const service = setup(directory, {}, {
|
|
489
|
+
runContactTurn: async ({ history, userText, tools }) => {
|
|
490
|
+
turnRuns.push(userText);
|
|
491
|
+
if (userText === 'ordinary A still running') {
|
|
492
|
+
aReachedTurn();
|
|
493
|
+
await lateWrittenGate;
|
|
494
|
+
// A completes after wipe already ran — its returned bubbles must not
|
|
495
|
+
// resurrect deleted late residue (wipe already cleared transcript).
|
|
496
|
+
return { text: 'late-A-final-bubble-should-not-matter', bubbles: ['late-A-final-bubble-should-not-matter'] };
|
|
497
|
+
}
|
|
498
|
+
if (userText === '清空聊天记录') {
|
|
499
|
+
await wipeMayRunGate;
|
|
500
|
+
const tool = tools.find((item) => item.name === 'clear_chat_history');
|
|
501
|
+
const result = await tool.execute('wipe_reverse', {});
|
|
502
|
+
return {
|
|
503
|
+
text: result.content[0].text,
|
|
504
|
+
bubbles: [result.content[0].text],
|
|
505
|
+
reset: true,
|
|
506
|
+
historyCleared: true,
|
|
507
|
+
messages: [{
|
|
508
|
+
role: 'toolResult',
|
|
509
|
+
toolName: 'clear_chat_history',
|
|
510
|
+
details: result.details,
|
|
511
|
+
}],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
if (userText === 'queued C keep me') {
|
|
515
|
+
cHistories.push(history.map((item) => item.content));
|
|
516
|
+
}
|
|
517
|
+
return { text: `reply:${userText}`, bubbles: [`reply:${userText}`] };
|
|
518
|
+
},
|
|
519
|
+
});
|
|
520
|
+
const assistant = service.createAssistant(assistantInput);
|
|
521
|
+
await settleSend(service, assistant.id, { messageID: 'seed_rev', parts: [{ type: 'text', text: 'seed secret reverse' }] });
|
|
522
|
+
// A pause -> wipe B admit -> C admit -> late A reply/card/watch -> B wipe -> C.
|
|
523
|
+
const aSend = service.send(assistant.id, { messageID: 'ord_a', parts: [{ type: 'text', text: 'ordinary A still running' }] });
|
|
524
|
+
await aReachedGate;
|
|
525
|
+
const bSend = service.send(assistant.id, { messageID: 'wipe_b', parts: [{ type: 'text', text: '清空聊天记录' }] });
|
|
526
|
+
await bSend;
|
|
527
|
+
const cSend = service.send(assistant.id, { messageID: 'queued_c', parts: [{ type: 'text', text: 'queued C keep me' }] });
|
|
528
|
+
await cSend;
|
|
529
|
+
// Late residue from A while still paused (ordinal after wipe+C users).
|
|
530
|
+
service.appendContactCard(assistant.id, {
|
|
531
|
+
cardType: 'session',
|
|
532
|
+
sessionID: 'ses_late_a',
|
|
533
|
+
directory: directory,
|
|
534
|
+
title: 'late A card residue',
|
|
535
|
+
status: 'busy',
|
|
536
|
+
messageID: 'late_a_card',
|
|
537
|
+
});
|
|
538
|
+
const Database = require('better-sqlite3');
|
|
539
|
+
const db = new Database(path.join(directory, 'assistants.sqlite'));
|
|
540
|
+
const ordinal = Number(db.prepare(
|
|
541
|
+
'SELECT COALESCE(MAX(ordinal), 0) + 1 AS next FROM assistant_contact_message WHERE assistant_id=?',
|
|
542
|
+
).get(assistant.id).next);
|
|
543
|
+
db.prepare(
|
|
544
|
+
'INSERT INTO assistant_contact_message(message_id,assistant_id,role,turn_id,bubble_index,created_at,ordinal,status) VALUES (?,?,?,?,?,?,?,?)',
|
|
545
|
+
).run('late_a_reply', assistant.id, 'assistant', 'ord_a', 0, Date.now(), ordinal, 'complete');
|
|
546
|
+
db.prepare(
|
|
547
|
+
'INSERT INTO assistant_contact_part(message_id,part_id,ordinal,part_json) VALUES (?,?,?,?)',
|
|
548
|
+
).run('late_a_reply', 'p1', 1, JSON.stringify({ type: 'text', text: 'late-A-reply-residue' }));
|
|
549
|
+
db.close();
|
|
550
|
+
lateWritten();
|
|
551
|
+
wipeMayRun();
|
|
552
|
+
await aSend;
|
|
553
|
+
await service.whenContactTurnSettled('ord_a');
|
|
554
|
+
await service.whenContactTurnSettled('wipe_b');
|
|
555
|
+
await service.whenContactTurnSettled('queued_c');
|
|
556
|
+
const replay = await service.send(assistant.id, { messageID: 'queued_c', parts: [{ type: 'text', text: 'queued C keep me' }] });
|
|
557
|
+
expect(replay).toMatchObject({ admitted: true, replayed: true, messageID: 'queued_c' });
|
|
558
|
+
const page = service.contactMessages(assistant.id, { limit: 50 });
|
|
559
|
+
const texts = page.messages.map((message) => message.text);
|
|
560
|
+
expect(texts).not.toContain('seed secret reverse');
|
|
561
|
+
expect(texts).not.toContain('late-A-reply-residue');
|
|
562
|
+
expect(texts).not.toContain('late-A-final-bubble-should-not-matter');
|
|
563
|
+
expect(page.messages.some((message) => (message.cards || []).some((card) => card.sessionID === 'ses_late_a'))).toBe(false);
|
|
564
|
+
expect(texts).toContain('queued C keep me');
|
|
565
|
+
expect(texts).toContain('reply:queued C keep me');
|
|
566
|
+
expect(texts).toContain(CLEAR_CHAT_HISTORY_CONFIRM_BUBBLE);
|
|
567
|
+
// Wipe user kept before C (no re-admit reorder).
|
|
568
|
+
const wipeIdx = page.messages.findIndex((message) => message.messageID === 'wipe_b');
|
|
569
|
+
const cIdx = page.messages.findIndex((message) => message.messageID === 'queued_c');
|
|
570
|
+
expect(wipeIdx).toBeGreaterThanOrEqual(0);
|
|
571
|
+
expect(cIdx).toBeGreaterThan(wipeIdx);
|
|
572
|
+
expect(turnRuns.filter((text) => text === 'queued C keep me')).toHaveLength(1);
|
|
573
|
+
expect(page.messages.filter((message) => message.messageID === 'queued_c')).toHaveLength(1);
|
|
574
|
+
// C LLM history must not see late A residue or pre-wipe seed.
|
|
575
|
+
const cHist = cHistories[0] || [];
|
|
576
|
+
expect(cHist.some((content) => String(content).includes('late-A'))).toBe(false);
|
|
577
|
+
expect(cHist.some((content) => String(content).includes('seed secret reverse'))).toBe(false);
|
|
578
|
+
const dbAfter = new Database(path.join(directory, 'assistants.sqlite'));
|
|
579
|
+
const watches = dbAfter.prepare(
|
|
580
|
+
'SELECT session_id FROM assistant_contact_watch WHERE assistant_id=?',
|
|
581
|
+
).all(assistant.id);
|
|
582
|
+
expect(watches).toEqual([]);
|
|
583
|
+
dbAfter.close();
|
|
319
584
|
service.close();
|
|
320
585
|
});
|
|
321
586
|
|
|
@@ -485,8 +750,8 @@ describe('assistants service', () => {
|
|
|
485
750
|
it('persists mixed text+image+file parts and forwards them to the contact harness', async () => {
|
|
486
751
|
const directory = root();
|
|
487
752
|
let harness;
|
|
488
|
-
const image = { type: 'file', mime: 'image/png', url:
|
|
489
|
-
const file = { type: 'file', mime: 'text/plain', url:
|
|
753
|
+
const image = { type: 'file', mime: 'image/png', url: FIXTURE_PNG_DATA_URL, filename: 'shot.png' };
|
|
754
|
+
const file = { type: 'file', mime: 'text/plain', url: FIXTURE_TEXT_DATA_URL, filename: 'notes.txt' };
|
|
490
755
|
const service = setup(directory, {}, {
|
|
491
756
|
runContactTurn: async (input) => {
|
|
492
757
|
harness = input;
|
|
@@ -499,11 +764,17 @@ describe('assistants service', () => {
|
|
|
499
764
|
parts: [{ type: 'text', text: 'look' }, image, file],
|
|
500
765
|
});
|
|
501
766
|
expect(harness.userText).toBe('look');
|
|
502
|
-
|
|
767
|
+
// Harness sees materialized data URLs; DB stores attachment descriptors (no raw url).
|
|
768
|
+
expect(harness.userParts[0]).toEqual({ type: 'text', text: 'look' });
|
|
769
|
+
expect(harness.userParts[1]).toMatchObject({ type: 'file', mime: 'image/png', filename: 'shot.png', url: expect.stringMatching(/^data:image\/png;base64,/) });
|
|
770
|
+
expect(harness.userParts[2]).toMatchObject({ type: 'file', mime: 'text/plain', filename: 'notes.txt', url: expect.stringMatching(/^data:text\/plain;base64,/) });
|
|
503
771
|
expect(service.contactMessages(assistant.id, { limit: 50 }).messages[0]).toMatchObject({
|
|
504
772
|
role: 'user',
|
|
505
|
-
|
|
506
|
-
|
|
773
|
+
parts: [
|
|
774
|
+
{ type: 'text', text: 'look' },
|
|
775
|
+
{ type: 'file', mime: 'image/png', filename: 'shot.png', attachmentID: expect.stringMatching(/^att_/) },
|
|
776
|
+
{ type: 'file', mime: 'text/plain', filename: 'notes.txt', attachmentID: expect.stringMatching(/^att_/) },
|
|
777
|
+
],
|
|
507
778
|
});
|
|
508
779
|
service.close();
|
|
509
780
|
const restarted = setup(directory, {}, {
|
|
@@ -511,14 +782,14 @@ describe('assistants service', () => {
|
|
|
511
782
|
});
|
|
512
783
|
expect(restarted.contactMessages(assistant.id, { limit: 50 }).messages[0].parts).toEqual([
|
|
513
784
|
{ type: 'text', text: 'look' },
|
|
514
|
-
image,
|
|
515
|
-
file,
|
|
785
|
+
expect.objectContaining({ type: 'file', mime: 'image/png', filename: 'shot.png', attachmentID: expect.stringMatching(/^att_/) }),
|
|
786
|
+
expect.objectContaining({ type: 'file', mime: 'text/plain', filename: 'notes.txt', attachmentID: expect.stringMatching(/^att_/) }),
|
|
516
787
|
]);
|
|
517
788
|
restarted.close();
|
|
518
789
|
});
|
|
519
790
|
|
|
520
791
|
it('admits a file-only contact send and stores the file part without a fake text row', async () => {
|
|
521
|
-
const image = { type: 'file', mime: 'image/png', url:
|
|
792
|
+
const image = { type: 'file', mime: 'image/png', url: FIXTURE_PNG_DATA_URL, filename: 'shot.png' };
|
|
522
793
|
let harness;
|
|
523
794
|
const service = setup(root(), {}, {
|
|
524
795
|
runContactTurn: async (input) => {
|
|
@@ -529,9 +800,14 @@ describe('assistants service', () => {
|
|
|
529
800
|
const assistant = service.createAssistant(assistantInput);
|
|
530
801
|
await settleSend(service, assistant.id, { messageID: 'file_only_1', parts: [image] });
|
|
531
802
|
expect(harness.userText).toBe('[attachment]');
|
|
532
|
-
expect(harness.userParts).toEqual([
|
|
803
|
+
expect(harness.userParts).toEqual([
|
|
804
|
+
expect.objectContaining({ type: 'file', mime: 'image/png', filename: 'shot.png', url: expect.stringMatching(/^data:image\/png;base64,/) }),
|
|
805
|
+
]);
|
|
533
806
|
const user = service.contactMessages(assistant.id).messages.find((message) => message.role === 'user');
|
|
534
|
-
expect(user.parts).toEqual([
|
|
807
|
+
expect(user.parts).toEqual([
|
|
808
|
+
expect.objectContaining({ type: 'file', mime: 'image/png', filename: 'shot.png', attachmentID: expect.stringMatching(/^att_/) }),
|
|
809
|
+
]);
|
|
810
|
+
expect(user.parts[0].url).toBeUndefined();
|
|
535
811
|
expect(user.text).toBe('');
|
|
536
812
|
service.close();
|
|
537
813
|
});
|
|
@@ -1318,7 +1594,7 @@ describe('assistants service', () => {
|
|
|
1318
1594
|
const directory = root();
|
|
1319
1595
|
const project = path.join(directory, 'app');
|
|
1320
1596
|
fs.mkdirSync(project, { recursive: true });
|
|
1321
|
-
const image = { type: 'file', mime: 'image/png', url:
|
|
1597
|
+
const image = { type: 'file', mime: 'image/png', url: FIXTURE_PNG_DATA_URL, filename: 'card.png' };
|
|
1322
1598
|
const creates = [];
|
|
1323
1599
|
const prompts = [];
|
|
1324
1600
|
const deletes = [];
|
|
@@ -1396,7 +1672,12 @@ describe('assistants service', () => {
|
|
|
1396
1672
|
model: { providerID: 'xai', modelID: 'grok-4.6' },
|
|
1397
1673
|
parts: [
|
|
1398
1674
|
{ type: 'text', text: '修移动端卡片宽度' },
|
|
1399
|
-
|
|
1675
|
+
expect.objectContaining({
|
|
1676
|
+
type: 'file',
|
|
1677
|
+
mime: 'image/png',
|
|
1678
|
+
filename: 'card.png',
|
|
1679
|
+
url: expect.stringMatching(/^data:image\/png;base64,/),
|
|
1680
|
+
}),
|
|
1400
1681
|
],
|
|
1401
1682
|
})]);
|
|
1402
1683
|
expect(deletes).toEqual([]);
|
|
@@ -2618,4 +2899,93 @@ describe('assistants service', () => {
|
|
|
2618
2899
|
db.close();
|
|
2619
2900
|
service.close();
|
|
2620
2901
|
});
|
|
2902
|
+
|
|
2903
|
+
it('pages contact messages 20/20/5 with generation+revision and exact messageID admission', async () => {
|
|
2904
|
+
const directory = root();
|
|
2905
|
+
const service = setup(directory, {}, {
|
|
2906
|
+
runContactTurn: async ({ userText }) => ({ text: `r:${userText}`, bubbles: [`r:${userText}`] }),
|
|
2907
|
+
});
|
|
2908
|
+
const assistant = service.createAssistant(assistantInput);
|
|
2909
|
+
const other = service.createAssistant({ ...assistantInput, name: 'B' });
|
|
2910
|
+
// 45 user admits → 45 user + 45 assistant = 90 rows; page three windows of 20/20/5 on users alone
|
|
2911
|
+
// Seed via direct store through many settled turns would be slow; insert via contactMessages path
|
|
2912
|
+
// by settling 22 turns (44 rows) then more via appendContactCard for density.
|
|
2913
|
+
for (let i = 0; i < 22; i += 1) {
|
|
2914
|
+
await settleSend(service, assistant.id, {
|
|
2915
|
+
messageID: `page_u_${i}`,
|
|
2916
|
+
parts: [{ type: 'text', text: `u-${i}` }],
|
|
2917
|
+
});
|
|
2918
|
+
}
|
|
2919
|
+
// 22 users + 22 assistants = 44; add one more card-only for 45th
|
|
2920
|
+
service.appendContactCard(assistant.id, {
|
|
2921
|
+
cardType: 'session',
|
|
2922
|
+
sessionID: 'ses_page_extra',
|
|
2923
|
+
directory,
|
|
2924
|
+
title: 'extra',
|
|
2925
|
+
status: 'complete',
|
|
2926
|
+
messageID: 'page_card_45',
|
|
2927
|
+
});
|
|
2928
|
+
|
|
2929
|
+
// Newest window first (chat tail), ascending within the page.
|
|
2930
|
+
const defaultPage = service.contactMessages(assistant.id);
|
|
2931
|
+
expect(defaultPage.messages).toHaveLength(20);
|
|
2932
|
+
expect(defaultPage.complete).toBe(false);
|
|
2933
|
+
expect(defaultPage.generation).toBe(0);
|
|
2934
|
+
expect(defaultPage.revision).toEqual(expect.any(Number));
|
|
2935
|
+
expect(defaultPage.messages.at(-1).messageID).toBe('page_card_45');
|
|
2936
|
+
|
|
2937
|
+
const second = service.contactMessages(assistant.id, { before: defaultPage.nextCursor, limit: 20 });
|
|
2938
|
+
expect(second.messages).toHaveLength(20);
|
|
2939
|
+
expect(second.complete).toBe(false);
|
|
2940
|
+
|
|
2941
|
+
const third = service.contactMessages(assistant.id, { before: second.nextCursor, limit: 20 });
|
|
2942
|
+
expect(third.messages.length).toBeGreaterThanOrEqual(5);
|
|
2943
|
+
expect(third.complete).toBe(true);
|
|
2944
|
+
expect(third.nextCursor).toBeNull();
|
|
2945
|
+
expect(third.messages[0].text).toBe('u-0');
|
|
2946
|
+
|
|
2947
|
+
// Concurrent append does not break a prior cursor keyset for older pages.
|
|
2948
|
+
await settleSend(service, assistant.id, {
|
|
2949
|
+
messageID: 'page_concurrent',
|
|
2950
|
+
parts: [{ type: 'text', text: 'concurrent-append' }],
|
|
2951
|
+
});
|
|
2952
|
+
const stillSecond = service.contactMessages(assistant.id, { before: defaultPage.nextCursor, limit: 20 });
|
|
2953
|
+
expect(stillSecond.messages[0].messageID).toBe(second.messages[0].messageID);
|
|
2954
|
+
|
|
2955
|
+
// Exact admission: deep/old message still found; missing / cross-assistant empty.
|
|
2956
|
+
const deepID = third.messages[0].messageID;
|
|
2957
|
+
const exact = service.contactMessages(assistant.id, { messageID: deepID });
|
|
2958
|
+
expect(exact).toMatchObject({ nextCursor: null, complete: true, generation: 0 });
|
|
2959
|
+
expect(exact.messages).toHaveLength(1);
|
|
2960
|
+
expect(exact.messages[0].messageID).toBe(deepID);
|
|
2961
|
+
expect(service.contactMessages(assistant.id, { messageID: 'nope' }).messages).toEqual([]);
|
|
2962
|
+
expect(service.contactMessages(other.id, { messageID: deepID }).messages).toEqual([]);
|
|
2963
|
+
|
|
2964
|
+
// clear-memory keeps generation + cursor validity
|
|
2965
|
+
const beforeGen = service.contactMessages(assistant.id).generation;
|
|
2966
|
+
service.clearContactMemory(assistant.id);
|
|
2967
|
+
expect(service.contactMessages(assistant.id, { before: defaultPage.nextCursor, limit: 5 }).generation).toBe(beforeGen);
|
|
2968
|
+
|
|
2969
|
+
// reset bumps generation → old cursor conflicts
|
|
2970
|
+
const wiped = service.resetContact(assistant.id);
|
|
2971
|
+
expect(wiped.generation).toBe(1);
|
|
2972
|
+
expect(() => service.contactMessages(assistant.id, { before: defaultPage.nextCursor })).toThrowError(
|
|
2973
|
+
expect.objectContaining({ code: 'contact_generation_conflict' }),
|
|
2974
|
+
);
|
|
2975
|
+
expect(service.contactMessages(assistant.id)).toMatchObject({
|
|
2976
|
+
messages: [],
|
|
2977
|
+
complete: true,
|
|
2978
|
+
generation: 1,
|
|
2979
|
+
});
|
|
2980
|
+
|
|
2981
|
+
// invalid cursor
|
|
2982
|
+
expect(() => service.contactMessages(assistant.id, { before: '%%%' })).toThrowError(
|
|
2983
|
+
expect.objectContaining({ code: 'validation_error' }),
|
|
2984
|
+
);
|
|
2985
|
+
expect(() => service.contactMessages(assistant.id, { before: 'x', messageID: 'y' })).toThrowError(
|
|
2986
|
+
expect.objectContaining({ code: 'validation_error' }),
|
|
2987
|
+
);
|
|
2988
|
+
|
|
2989
|
+
service.close();
|
|
2990
|
+
});
|
|
2621
2991
|
});
|