@qjp88995/agent-runtime 0.1.1 → 0.1.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/loop/loop-driver.d.ts +8 -0
- package/dist/loop/loop-driver.d.ts.map +1 -1
- package/dist/loop/loop-driver.js +1 -1
- package/dist/loop/loop-driver.js.map +1 -1
- package/dist/testing/contracts.d.ts +43 -4
- package/dist/testing/contracts.d.ts.map +1 -1
- package/dist/testing/contracts.js +402 -48
- package/dist/testing/contracts.js.map +1 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/index.js.map +1 -1
- package/package.json +1 -1
- package/src/loop/loop-driver.ts +9 -1
- package/src/testing/contracts.ts +519 -55
- package/src/testing/index.ts +3 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
3
|
import { QuestionConflictError, } from '../ports/question-store.js';
|
|
4
|
+
import { COMPACTION_CHECKPOINT_KIND } from '../schema/index.js';
|
|
4
5
|
async function make(factory) {
|
|
5
6
|
const made = await factory();
|
|
6
7
|
if ('impl' in made && typeof made.impl === 'object') {
|
|
@@ -20,6 +21,17 @@ function withImpl(factory, body) {
|
|
|
20
21
|
};
|
|
21
22
|
}
|
|
22
23
|
const id = () => randomUUID();
|
|
24
|
+
function fixturesOf(options) {
|
|
25
|
+
const f = options?.fixtures ?? {};
|
|
26
|
+
return {
|
|
27
|
+
rootSession: () => f.rootSession ? f.rootSession() : Promise.resolve(id()),
|
|
28
|
+
user: () => (f.user ? f.user() : Promise.resolve(id())),
|
|
29
|
+
run: (sessionId) => f.run ? f.run(sessionId) : Promise.resolve(id()),
|
|
30
|
+
runAttrs: f.runAttrs,
|
|
31
|
+
sessionAttrs: f.sessionAttrs ?? { a: 1 },
|
|
32
|
+
sessionMetadata: f.sessionMetadata ?? { k: 'v' },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
23
35
|
function event(streamId, rootSessionId, seq, type = 'text-delta') {
|
|
24
36
|
return {
|
|
25
37
|
seq,
|
|
@@ -265,25 +277,31 @@ export function describeTreeBudgetContract(name, factory) {
|
|
|
265
277
|
});
|
|
266
278
|
}
|
|
267
279
|
export function describeRunStoreContract(name, factory,
|
|
268
|
-
/**
|
|
269
|
-
|
|
280
|
+
/**
|
|
281
|
+
* 数字 = 旧写法的 `staleDelayMs`。`staleDelayMs`:建一条能被 `findStaleRunning(0)`
|
|
282
|
+
* 看到的旧跑次所需的等待(毫秒),缺省 5。
|
|
283
|
+
*/
|
|
284
|
+
options = {}) {
|
|
285
|
+
const opts = typeof options === 'number' ? { staleDelayMs: options } : options;
|
|
286
|
+
const staleDelayMs = opts.staleDelayMs ?? 5;
|
|
287
|
+
const fx = fixturesOf(opts);
|
|
270
288
|
describe(`RunStore contract: ${name}`, () => {
|
|
271
|
-
const open = (store, over = {}) => {
|
|
289
|
+
const open = async (store, over = {}) => {
|
|
272
290
|
const runId = over.id ?? id();
|
|
273
|
-
|
|
274
|
-
.open({
|
|
291
|
+
await store.open({
|
|
275
292
|
id: runId,
|
|
276
293
|
streamId: runId,
|
|
277
294
|
parentRunId: over.parentRunId ?? null,
|
|
278
295
|
depth: over.depth ?? 0,
|
|
279
|
-
sessionId: over.sessionId
|
|
296
|
+
sessionId: over.sessionId ?? (await fx.rootSession()),
|
|
280
297
|
prompt: null,
|
|
281
298
|
model: { modelId: 'm', label: 'v / m', reasoningEffort: 'medium' },
|
|
282
|
-
|
|
283
|
-
|
|
299
|
+
...(fx.runAttrs ? { attrs: fx.runAttrs } : {}),
|
|
300
|
+
});
|
|
301
|
+
return runId;
|
|
284
302
|
};
|
|
285
303
|
it('open 后可查会话与深度;缺记录给缺省值', withImpl(factory, async (store) => {
|
|
286
|
-
const sessionId =
|
|
304
|
+
const sessionId = await fx.rootSession();
|
|
287
305
|
const runId = await open(store, { sessionId, depth: 2 });
|
|
288
306
|
expect(await store.getSessionId(runId)).toBe(sessionId);
|
|
289
307
|
expect(await store.getDepth(runId)).toBe(2);
|
|
@@ -335,33 +353,34 @@ async function waitFor(cond, timeoutMs = 2000) {
|
|
|
335
353
|
* `SessionStore` 契约(子会话行与里程碑游标)。`listSubagentSessionsForSweep` 要看消息与
|
|
336
354
|
* 更新时间,跟宿主的消息表绑在一起,不在这里测。
|
|
337
355
|
*/
|
|
338
|
-
export function describeSessionStoreContract(name, factory) {
|
|
356
|
+
export function describeSessionStoreContract(name, factory, options = {}) {
|
|
357
|
+
const fx = fixturesOf(options);
|
|
339
358
|
describe(`SessionStore contract: ${name}`, () => {
|
|
340
|
-
const child = (store, rootSessionId) => store.createSubSession({
|
|
359
|
+
const child = async (store, rootSessionId) => store.createSubSession({
|
|
341
360
|
rootSessionId,
|
|
342
|
-
parentRunId:
|
|
361
|
+
parentRunId: await fx.run(rootSessionId),
|
|
343
362
|
subagentName: 'general-purpose',
|
|
344
363
|
userId: null,
|
|
345
364
|
title: 't',
|
|
346
|
-
metadata:
|
|
347
|
-
attrs:
|
|
365
|
+
metadata: fx.sessionMetadata,
|
|
366
|
+
attrs: fx.sessionAttrs,
|
|
348
367
|
});
|
|
349
368
|
it('建子会话:存活(游标 null)、读回同一行;不存在的读回 null', withImpl(factory, async (store) => {
|
|
350
|
-
const root =
|
|
369
|
+
const root = await fx.rootSession();
|
|
351
370
|
const made = await child(store, root);
|
|
352
371
|
expect(made).toMatchObject({
|
|
353
372
|
kind: 'subagent',
|
|
354
373
|
rootSessionId: root,
|
|
355
374
|
subagentName: 'general-purpose',
|
|
356
375
|
lastMilestoneSeq: null,
|
|
357
|
-
metadata:
|
|
358
|
-
attrs:
|
|
376
|
+
metadata: fx.sessionMetadata,
|
|
377
|
+
attrs: fx.sessionAttrs,
|
|
359
378
|
});
|
|
360
379
|
expect(await store.getSession(made.id)).toEqual(made);
|
|
361
380
|
expect(await store.getSession(id())).toBeNull();
|
|
362
381
|
}));
|
|
363
382
|
it('advanceMilestone 是 CAS:只有期望值对上才写', withImpl(factory, async (store) => {
|
|
364
|
-
const made = await child(store,
|
|
383
|
+
const made = await child(store, await fx.rootSession());
|
|
365
384
|
expect(await store.advanceMilestone(made.id, 3, 5)).toBe(false);
|
|
366
385
|
expect(await store.advanceMilestone(made.id, null, 5)).toBe(true);
|
|
367
386
|
expect(await store.advanceMilestone(made.id, null, 6)).toBe(false);
|
|
@@ -369,13 +388,13 @@ export function describeSessionStoreContract(name, factory) {
|
|
|
369
388
|
expect((await store.getSession(made.id))?.lastMilestoneSeq).toBe(9);
|
|
370
389
|
}));
|
|
371
390
|
it('并发推进同一个旧游标:恰好一个成功', withImpl(factory, async (store) => {
|
|
372
|
-
const made = await child(store,
|
|
391
|
+
const made = await child(store, await fx.rootSession());
|
|
373
392
|
const results = await Promise.all(Array.from({ length: 5 }, (_, i) => store.advanceMilestone(made.id, null, 10 + i)));
|
|
374
393
|
expect(results.filter(Boolean)).toHaveLength(1);
|
|
375
394
|
}));
|
|
376
395
|
it('countAliveSubsByRoot 只数游标为 null 的子会话', withImpl(factory, async (store) => {
|
|
377
|
-
const r1 =
|
|
378
|
-
const r2 =
|
|
396
|
+
const r1 = await fx.rootSession();
|
|
397
|
+
const r2 = await fx.rootSession();
|
|
379
398
|
const a = await child(store, r1);
|
|
380
399
|
await child(store, r1);
|
|
381
400
|
await child(store, r2);
|
|
@@ -389,7 +408,8 @@ export function describeSessionStoreContract(name, factory) {
|
|
|
389
408
|
}));
|
|
390
409
|
});
|
|
391
410
|
}
|
|
392
|
-
export function describeQuestionStoreContract(name, factory) {
|
|
411
|
+
export function describeQuestionStoreContract(name, factory, options = {}) {
|
|
412
|
+
const fx = fixturesOf(options);
|
|
393
413
|
describe(`QuestionStore contract: ${name}`, () => {
|
|
394
414
|
const item = {
|
|
395
415
|
question: '用哪种开场?',
|
|
@@ -398,9 +418,10 @@ export function describeQuestionStoreContract(name, factory) {
|
|
|
398
418
|
multiSelect: false,
|
|
399
419
|
defaultOption: 'A',
|
|
400
420
|
};
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
421
|
+
// 会话与用户在宿主库里多半是外键:每次现建(缺省随机 id)。
|
|
422
|
+
const input = async (over = {}) => ({
|
|
423
|
+
sessionId: over.sessionId ?? (await fx.rootSession()),
|
|
424
|
+
userId: over.userId ?? (await fx.user()),
|
|
404
425
|
runId: id(),
|
|
405
426
|
toolCallId: id(),
|
|
406
427
|
questions: [item],
|
|
@@ -408,7 +429,7 @@ export function describeQuestionStoreContract(name, factory) {
|
|
|
408
429
|
});
|
|
409
430
|
it('落一行 pending:期限按存储时钟从落行起算;不限时为 null;get / probe 读回同一行', withImpl(factory, async (store) => {
|
|
410
431
|
const before = (await store.now()).getTime();
|
|
411
|
-
const { question, swept } = await store.open(input({ timeoutSeconds: 300 }));
|
|
432
|
+
const { question, swept } = await store.open(await input({ timeoutSeconds: 300 }));
|
|
412
433
|
const after = (await store.now()).getTime();
|
|
413
434
|
expect(swept).toEqual([]);
|
|
414
435
|
expect(question).toMatchObject({
|
|
@@ -427,39 +448,39 @@ export function describeQuestionStoreContract(name, factory) {
|
|
|
427
448
|
due: false,
|
|
428
449
|
overdue: false,
|
|
429
450
|
});
|
|
430
|
-
const untimed = await store.open(input());
|
|
451
|
+
const untimed = await store.open(await input());
|
|
431
452
|
expect(untimed.question.deadlineAt).toBeNull();
|
|
432
453
|
}));
|
|
433
454
|
it('get 限定会话:别的会话当不存在;不存在的 get / probe 都是 null', withImpl(factory, async (store) => {
|
|
434
|
-
const { question } = await store.open(input());
|
|
455
|
+
const { question } = await store.open(await input());
|
|
435
456
|
expect(await store.get(question.id, question.sessionId)).not.toBeNull();
|
|
436
457
|
expect(await store.get(question.id, id())).toBeNull();
|
|
437
458
|
expect(await store.get(id())).toBeNull();
|
|
438
459
|
expect(await store.probe(id(), 60)).toBeNull();
|
|
439
460
|
}));
|
|
440
461
|
it('probe:从落行起过了 maxWaitSeconds 即 overdue', withImpl(factory, async (store) => {
|
|
441
|
-
const { question } = await store.open(input());
|
|
462
|
+
const { question } = await store.open(await input());
|
|
442
463
|
expect((await store.probe(question.id, 0))?.overdue).toBe(true);
|
|
443
464
|
}));
|
|
444
465
|
it('同一会话同一轮已有 pending → QuestionConflictError,原行不动;别的轮次的 pending 作废让位并返回', withImpl(factory, async (store) => {
|
|
445
|
-
const sessionId =
|
|
466
|
+
const sessionId = await fx.rootSession();
|
|
446
467
|
const runId = id();
|
|
447
|
-
const first = await store.open(input({ sessionId, runId }));
|
|
448
|
-
await expect(store.open(input({ sessionId, runId }))).rejects.toBeInstanceOf(QuestionConflictError);
|
|
468
|
+
const first = await store.open(await input({ sessionId, runId }));
|
|
469
|
+
await expect(store.open(await input({ sessionId, runId }))).rejects.toBeInstanceOf(QuestionConflictError);
|
|
449
470
|
expect((await store.get(first.question.id))?.status).toBe('pending');
|
|
450
|
-
const next = await store.open(input({ sessionId }));
|
|
471
|
+
const next = await store.open(await input({ sessionId }));
|
|
451
472
|
expect(next.swept.map(r => [r.id, r.status])).toEqual([
|
|
452
473
|
[first.question.id, 'cancelled'],
|
|
453
474
|
]);
|
|
454
475
|
expect(next.swept[0].resolvedAt).not.toBeNull();
|
|
455
476
|
expect((await store.get(first.question.id))?.status).toBe('cancelled');
|
|
456
477
|
// 别的会话的 pending 不受影响。
|
|
457
|
-
const other = await store.open(input());
|
|
458
|
-
await store.open(input());
|
|
478
|
+
const other = await store.open(await input());
|
|
479
|
+
await store.open(await input());
|
|
459
480
|
expect((await store.get(other.question.id))?.status).toBe('pending');
|
|
460
481
|
}));
|
|
461
482
|
it('settle 是 CAS:只有第一个生效,之后返回 null 且不改写', withImpl(factory, async (store) => {
|
|
462
|
-
const { question } = await store.open(input());
|
|
483
|
+
const { question } = await store.open(await input());
|
|
463
484
|
const answers = [{ header: '开场', labels: ['B'], other: null }];
|
|
464
485
|
const won = await store.settle(question.id, 'answered', answers);
|
|
465
486
|
expect(won).toMatchObject({ status: 'answered', answers });
|
|
@@ -469,7 +490,7 @@ export function describeQuestionStoreContract(name, factory) {
|
|
|
469
490
|
expect(await store.settle(id(), 'cancelled', null)).toBeNull();
|
|
470
491
|
}));
|
|
471
492
|
it('并发 settle 恰有一个生效', withImpl(factory, async (store) => {
|
|
472
|
-
const { question } = await store.open(input());
|
|
493
|
+
const { question } = await store.open(await input());
|
|
473
494
|
const results = await Promise.all([
|
|
474
495
|
store.settle(question.id, 'answered', []),
|
|
475
496
|
store.settle(question.id, 'expired', []),
|
|
@@ -478,11 +499,11 @@ export function describeQuestionStoreContract(name, factory) {
|
|
|
478
499
|
expect(results.filter(r => r !== null)).toHaveLength(1);
|
|
479
500
|
}));
|
|
480
501
|
it('settleSession:只改这个会话的 pending(或只改点名的那一个),返回被改的行', withImpl(factory, async (store) => {
|
|
481
|
-
const sessionId =
|
|
482
|
-
const done = await store.open(input({ sessionId }));
|
|
502
|
+
const sessionId = await fx.rootSession();
|
|
503
|
+
const done = await store.open(await input({ sessionId }));
|
|
483
504
|
await store.settle(done.question.id, 'answered', []);
|
|
484
|
-
const live = await store.open(input({ sessionId }));
|
|
485
|
-
const elsewhere = await store.open(input());
|
|
505
|
+
const live = await store.open(await input({ sessionId }));
|
|
506
|
+
const elsewhere = await store.open(await input());
|
|
486
507
|
expect(await store.settleSession(sessionId, 'superseded', id())).toEqual([]);
|
|
487
508
|
const changed = await store.settleSession(sessionId, 'cancelled');
|
|
488
509
|
expect(changed.map(r => [r.id, r.status])).toEqual([
|
|
@@ -493,8 +514,8 @@ export function describeQuestionStoreContract(name, factory) {
|
|
|
493
514
|
expect(await store.settleSession(sessionId, 'cancelled')).toEqual([]);
|
|
494
515
|
}));
|
|
495
516
|
it('hold:仅 pending 且有期限时摘掉期限;不满足(不限时、已停表、已有结局、别的会话)返回 null', withImpl(factory, async (store) => {
|
|
496
|
-
const sessionId =
|
|
497
|
-
const { question } = await store.open(input({ sessionId, timeoutSeconds: 60 }));
|
|
517
|
+
const sessionId = await fx.rootSession();
|
|
518
|
+
const { question } = await store.open(await input({ sessionId, timeoutSeconds: 60 }));
|
|
498
519
|
expect(await store.hold(question.id, id())).toBeNull();
|
|
499
520
|
expect(await store.hold(question.id, sessionId)).toMatchObject({
|
|
500
521
|
status: 'pending',
|
|
@@ -502,14 +523,14 @@ export function describeQuestionStoreContract(name, factory) {
|
|
|
502
523
|
});
|
|
503
524
|
expect(await store.hold(question.id, sessionId)).toBeNull();
|
|
504
525
|
expect((await store.probe(question.id, 3600))?.due).toBe(false);
|
|
505
|
-
const untimed = await store.open(input());
|
|
526
|
+
const untimed = await store.open(await input());
|
|
506
527
|
expect(await store.hold(untimed.question.id, untimed.question.sessionId)).toBeNull();
|
|
507
528
|
}));
|
|
508
529
|
it('list:这个会话的全部问题,按落行顺序', withImpl(factory, async (store) => {
|
|
509
|
-
const sessionId =
|
|
510
|
-
const a = await store.open(input({ sessionId }));
|
|
511
|
-
const b = await store.open(input({ sessionId }));
|
|
512
|
-
await store.open(input());
|
|
530
|
+
const sessionId = await fx.rootSession();
|
|
531
|
+
const a = await store.open(await input({ sessionId }));
|
|
532
|
+
const b = await store.open(await input({ sessionId }));
|
|
533
|
+
await store.open(await input());
|
|
513
534
|
expect((await store.list(sessionId)).map(r => [r.id, r.status])).toEqual([
|
|
514
535
|
[a.question.id, 'cancelled'],
|
|
515
536
|
[b.question.id, 'pending'],
|
|
@@ -604,4 +625,337 @@ export function describeVendorFileCacheContract(name, factory) {
|
|
|
604
625
|
}));
|
|
605
626
|
});
|
|
606
627
|
}
|
|
628
|
+
/**
|
|
629
|
+
* `ConversationStore` 契约(消息、inbox、两个上下文指针)。
|
|
630
|
+
*
|
|
631
|
+
* 只认端口上看得见的东西:`seq` 只要求会话内单调(数据库实现常是全表一条序列),
|
|
632
|
+
* 「有了新动静」记没记、记在哪是实现细节,不在这里测。
|
|
633
|
+
*/
|
|
634
|
+
export function describeConversationStoreContract(name, factory, options = {}) {
|
|
635
|
+
const fx = fixturesOf(options);
|
|
636
|
+
const text = (t) => [{ type: 'text', text: t }];
|
|
637
|
+
describe(`ConversationStore contract: ${name}`, () => {
|
|
638
|
+
const item = (sessionId, t, userId) => ({
|
|
639
|
+
sessionId,
|
|
640
|
+
kind: 'user_message',
|
|
641
|
+
sourceKind: 'user',
|
|
642
|
+
sourceUserId: userId,
|
|
643
|
+
source: null,
|
|
644
|
+
parts: text(t),
|
|
645
|
+
messageId: id(),
|
|
646
|
+
});
|
|
647
|
+
const asUser = (i) => ({
|
|
648
|
+
id: i.messageId,
|
|
649
|
+
sessionId: i.sessionId,
|
|
650
|
+
role: 'user',
|
|
651
|
+
parts: i.parts,
|
|
652
|
+
});
|
|
653
|
+
it('seq 在会话内单调递增;listMessages 按 seq 升序', withImpl(factory, async (store) => {
|
|
654
|
+
const s = await fx.rootSession();
|
|
655
|
+
const a = await store.appendMessage({
|
|
656
|
+
sessionId: s,
|
|
657
|
+
role: 'user',
|
|
658
|
+
parts: text('1'),
|
|
659
|
+
});
|
|
660
|
+
const b = await store.appendMessage({
|
|
661
|
+
sessionId: s,
|
|
662
|
+
role: 'assistant',
|
|
663
|
+
parts: text('2'),
|
|
664
|
+
metadata: { k: 'v' },
|
|
665
|
+
tokenUsage: { inputTokens: 3 },
|
|
666
|
+
});
|
|
667
|
+
expect(b.seq).toBeGreaterThan(a.seq);
|
|
668
|
+
expect(b).toMatchObject({
|
|
669
|
+
sessionId: s,
|
|
670
|
+
role: 'assistant',
|
|
671
|
+
parts: text('2'),
|
|
672
|
+
metadata: { k: 'v' },
|
|
673
|
+
tokenUsage: { inputTokens: 3 },
|
|
674
|
+
});
|
|
675
|
+
expect((await store.listMessages(s)).map(m => m.id)).toEqual([
|
|
676
|
+
a.id,
|
|
677
|
+
b.id,
|
|
678
|
+
]);
|
|
679
|
+
}));
|
|
680
|
+
it('appendMessage 按 id 幂等:撞主键返回已存在的那一行', withImpl(factory, async (store) => {
|
|
681
|
+
const s = await fx.rootSession();
|
|
682
|
+
const mid = id();
|
|
683
|
+
const first = await store.appendMessage({
|
|
684
|
+
id: mid,
|
|
685
|
+
sessionId: s,
|
|
686
|
+
role: 'user',
|
|
687
|
+
parts: text('1'),
|
|
688
|
+
});
|
|
689
|
+
const again = await store.appendMessage({
|
|
690
|
+
id: mid,
|
|
691
|
+
sessionId: s,
|
|
692
|
+
role: 'user',
|
|
693
|
+
parts: text('2'),
|
|
694
|
+
});
|
|
695
|
+
expect(first.id).toBe(mid);
|
|
696
|
+
expect(again).toEqual(first);
|
|
697
|
+
expect(await store.listMessages(s)).toHaveLength(1);
|
|
698
|
+
}));
|
|
699
|
+
it('基线取最后一条 metadata 带 contextTokens 的 assistant 行', withImpl(factory, async (store) => {
|
|
700
|
+
const s = await fx.rootSession();
|
|
701
|
+
const add = (role, contextTokens) => store.appendMessage({
|
|
702
|
+
sessionId: s,
|
|
703
|
+
role,
|
|
704
|
+
parts: text('x'),
|
|
705
|
+
...(contextTokens !== undefined
|
|
706
|
+
? { metadata: { contextTokens } }
|
|
707
|
+
: {}),
|
|
708
|
+
});
|
|
709
|
+
await add('assistant', 10);
|
|
710
|
+
await add('assistant', 20);
|
|
711
|
+
await add('assistant');
|
|
712
|
+
await add('user');
|
|
713
|
+
const inputs = await store.loadContextInputs(s);
|
|
714
|
+
expect(inputs.baselineContextTokens).toBe(20);
|
|
715
|
+
expect(inputs.rows).toHaveLength(4);
|
|
716
|
+
const empty = await store.loadContextInputs(await fx.rootSession());
|
|
717
|
+
expect(empty).toEqual({
|
|
718
|
+
rows: [],
|
|
719
|
+
prunedUptoSeq: null,
|
|
720
|
+
compactedUptoSeq: null,
|
|
721
|
+
baselineContextTokens: null,
|
|
722
|
+
});
|
|
723
|
+
}));
|
|
724
|
+
it('裁剪指针只增不减:晚到的较小切点不许把它拉回去', withImpl(factory, async (store) => {
|
|
725
|
+
const s = await fx.rootSession();
|
|
726
|
+
await store.advancePrunedUpto(s, 5);
|
|
727
|
+
await store.advancePrunedUpto(s, 3);
|
|
728
|
+
expect((await store.loadContextInputs(s)).prunedUptoSeq).toBe(5);
|
|
729
|
+
}));
|
|
730
|
+
it('checkpoint 行与压缩指针一起落下、kind 摊平到行上;压缩指针只增不减', withImpl(factory, async (store) => {
|
|
731
|
+
const s = await fx.rootSession();
|
|
732
|
+
const rows = [];
|
|
733
|
+
for (let i = 0; i < 4; i++) {
|
|
734
|
+
rows.push(await store.appendMessage({
|
|
735
|
+
sessionId: s,
|
|
736
|
+
role: 'user',
|
|
737
|
+
parts: text(String(i)),
|
|
738
|
+
}));
|
|
739
|
+
}
|
|
740
|
+
const cut = rows[2].seq;
|
|
741
|
+
const row = await store.appendCheckpoint({
|
|
742
|
+
sessionId: s,
|
|
743
|
+
seq: cut,
|
|
744
|
+
summary: '摘要',
|
|
745
|
+
});
|
|
746
|
+
expect(row).toMatchObject({
|
|
747
|
+
role: 'user',
|
|
748
|
+
kind: COMPACTION_CHECKPOINT_KIND,
|
|
749
|
+
parts: text('摘要'),
|
|
750
|
+
});
|
|
751
|
+
expect(row.seq).toBeGreaterThan(rows[3].seq);
|
|
752
|
+
await store.appendCheckpoint({
|
|
753
|
+
sessionId: s,
|
|
754
|
+
seq: rows[1].seq,
|
|
755
|
+
summary: '旧',
|
|
756
|
+
});
|
|
757
|
+
const inputs = await store.loadContextInputs(s);
|
|
758
|
+
expect(inputs.compactedUptoSeq).toBe(cut);
|
|
759
|
+
expect(inputs.rows.at(-2)).toMatchObject({
|
|
760
|
+
seq: row.seq,
|
|
761
|
+
kind: COMPACTION_CHECKPOINT_KIND,
|
|
762
|
+
});
|
|
763
|
+
expect(inputs.rows[0].kind ?? null).toBeNull();
|
|
764
|
+
}));
|
|
765
|
+
it('loadContextInputs 给的是快照:之后的追加不漂进去', withImpl(factory, async (store) => {
|
|
766
|
+
const s = await fx.rootSession();
|
|
767
|
+
await store.appendMessage({
|
|
768
|
+
sessionId: s,
|
|
769
|
+
role: 'user',
|
|
770
|
+
parts: text('1'),
|
|
771
|
+
});
|
|
772
|
+
const snap = await store.loadContextInputs(s);
|
|
773
|
+
await store.appendMessage({
|
|
774
|
+
sessionId: s,
|
|
775
|
+
role: 'user',
|
|
776
|
+
parts: text('2'),
|
|
777
|
+
});
|
|
778
|
+
expect(snap.rows).toHaveLength(1);
|
|
779
|
+
}));
|
|
780
|
+
it('takeInbox:按投递序取出本会话的全部、落成消息,别的会话不动', withImpl(factory, async (store) => {
|
|
781
|
+
const s = await fx.rootSession();
|
|
782
|
+
const t = await fx.rootSession();
|
|
783
|
+
const u = await fx.user();
|
|
784
|
+
const a = item(s, 'a', u);
|
|
785
|
+
const x = item(t, 'x', u);
|
|
786
|
+
const b = item(s, 'b', u);
|
|
787
|
+
await store.enqueue(a);
|
|
788
|
+
await store.enqueue(x);
|
|
789
|
+
await store.enqueue(b);
|
|
790
|
+
expect(await store.listSessionIdsWithPending()).toEqual(expect.arrayContaining([s, t]));
|
|
791
|
+
const pending = await store.listPending(s);
|
|
792
|
+
expect(pending.map(p => p.messageId)).toEqual([
|
|
793
|
+
a.messageId,
|
|
794
|
+
b.messageId,
|
|
795
|
+
]);
|
|
796
|
+
expect(pending[1].seq).toBeGreaterThan(pending[0].seq);
|
|
797
|
+
expect(pending[0]).toMatchObject({
|
|
798
|
+
sessionId: s,
|
|
799
|
+
kind: 'user_message',
|
|
800
|
+
sourceKind: 'user',
|
|
801
|
+
sourceUserId: u,
|
|
802
|
+
source: null,
|
|
803
|
+
parts: text('a'),
|
|
804
|
+
});
|
|
805
|
+
const taken = await store.takeInbox(s, asUser);
|
|
806
|
+
expect(taken.map(e => e.message.id)).toEqual([
|
|
807
|
+
a.messageId,
|
|
808
|
+
b.messageId,
|
|
809
|
+
]);
|
|
810
|
+
expect(taken[0].item).toEqual(pending[0]);
|
|
811
|
+
expect(await store.hasPending(s)).toBe(false);
|
|
812
|
+
expect(await store.hasPending(t)).toBe(true);
|
|
813
|
+
expect((await store.listMessages(s)).map(m => m.id)).toEqual([
|
|
814
|
+
a.messageId,
|
|
815
|
+
b.messageId,
|
|
816
|
+
]);
|
|
817
|
+
const withPending = await store.listSessionIdsWithPending();
|
|
818
|
+
expect(withPending).toContain(t);
|
|
819
|
+
expect(withPending).not.toContain(s);
|
|
820
|
+
expect(await store.takeInbox(s, asUser)).toEqual([]);
|
|
821
|
+
}));
|
|
822
|
+
it('takeInbox:换算抛错 = 整批不取(同事务回滚)', withImpl(factory, async (store) => {
|
|
823
|
+
const s = await fx.rootSession();
|
|
824
|
+
await store.enqueue(item(s, 'a', await fx.user()));
|
|
825
|
+
await expect(store.takeInbox(s, () => {
|
|
826
|
+
throw new Error('bad row');
|
|
827
|
+
})).rejects.toThrow('bad row');
|
|
828
|
+
expect(await store.hasPending(s)).toBe(true);
|
|
829
|
+
expect(await store.listMessages(s)).toEqual([]);
|
|
830
|
+
}));
|
|
831
|
+
it('enqueue 带结构化来源、系统来源无发送人', withImpl(factory, async (store) => {
|
|
832
|
+
const s = await fx.rootSession();
|
|
833
|
+
await store.enqueue({
|
|
834
|
+
sessionId: s,
|
|
835
|
+
kind: 'system_reminder',
|
|
836
|
+
sourceKind: 'system',
|
|
837
|
+
sourceUserId: null,
|
|
838
|
+
source: { kind: 'probe' },
|
|
839
|
+
parts: text('r'),
|
|
840
|
+
messageId: id(),
|
|
841
|
+
});
|
|
842
|
+
const [p] = await store.listPending(s);
|
|
843
|
+
expect(p).toMatchObject({
|
|
844
|
+
kind: 'system_reminder',
|
|
845
|
+
sourceKind: 'system',
|
|
846
|
+
sourceUserId: null,
|
|
847
|
+
source: { kind: 'probe' },
|
|
848
|
+
});
|
|
849
|
+
}));
|
|
850
|
+
it('withdraw 只删自己的、还没被取走的', withImpl(factory, async (store) => {
|
|
851
|
+
const s = await fx.rootSession();
|
|
852
|
+
const u = await fx.user();
|
|
853
|
+
const queued = item(s, 'a', u);
|
|
854
|
+
await store.enqueue(queued);
|
|
855
|
+
const [pending] = await store.listPending(s);
|
|
856
|
+
expect(await store.withdraw(pending.id, id())).toBeNull();
|
|
857
|
+
expect(await store.withdraw(pending.id, u)).toMatchObject({
|
|
858
|
+
id: pending.id,
|
|
859
|
+
messageId: queued.messageId,
|
|
860
|
+
});
|
|
861
|
+
expect(await store.hasPending(s)).toBe(false);
|
|
862
|
+
expect(await store.withdraw(pending.id, u)).toBeNull();
|
|
863
|
+
}));
|
|
864
|
+
it('assistant 段:首次建行、之后追加 parts 并合并 metadata;收尾整体重写', withImpl(factory, async (store) => {
|
|
865
|
+
const s = await fx.rootSession();
|
|
866
|
+
const r = id();
|
|
867
|
+
await store.appendAssistantStep({
|
|
868
|
+
messageId: r,
|
|
869
|
+
runId: r,
|
|
870
|
+
sessionId: s,
|
|
871
|
+
parts: text('a'),
|
|
872
|
+
metadataPatch: { flushedSeq: 1 },
|
|
873
|
+
});
|
|
874
|
+
await store.appendAssistantStep({
|
|
875
|
+
messageId: r,
|
|
876
|
+
runId: r,
|
|
877
|
+
sessionId: s,
|
|
878
|
+
parts: text('b'),
|
|
879
|
+
metadataPatch: { flushedSeq: 3 },
|
|
880
|
+
});
|
|
881
|
+
let [row] = await store.listMessages(s);
|
|
882
|
+
expect(row.role).toBe('assistant');
|
|
883
|
+
expect(row.parts).toEqual([...text('a'), ...text('b')]);
|
|
884
|
+
expect(row.metadata).toEqual({ runId: r, flushedSeq: 3 });
|
|
885
|
+
await store.finalizeAssistantTurn({
|
|
886
|
+
messageId: r,
|
|
887
|
+
runId: r,
|
|
888
|
+
metadata: { status: 'completed' },
|
|
889
|
+
usage: { inputTokens: 1 },
|
|
890
|
+
});
|
|
891
|
+
[row] = await store.listMessages(s);
|
|
892
|
+
expect(row.metadata).toEqual({ runId: r, status: 'completed' });
|
|
893
|
+
expect(row.tokenUsage).toEqual({ inputTokens: 1 });
|
|
894
|
+
}));
|
|
895
|
+
it('finalizeInterruptedTurn 只补没有终态的段、只认 metadata.runId', withImpl(factory, async (store) => {
|
|
896
|
+
const s = await fx.rootSession();
|
|
897
|
+
const r = id();
|
|
898
|
+
const seg1 = id();
|
|
899
|
+
const seg2 = id();
|
|
900
|
+
await store.appendMessage({
|
|
901
|
+
id: seg1,
|
|
902
|
+
sessionId: s,
|
|
903
|
+
role: 'assistant',
|
|
904
|
+
parts: text('1'),
|
|
905
|
+
metadata: { runId: r, status: 'completed' },
|
|
906
|
+
});
|
|
907
|
+
await store.appendMessage({
|
|
908
|
+
id: seg2,
|
|
909
|
+
sessionId: s,
|
|
910
|
+
role: 'assistant',
|
|
911
|
+
parts: text('2'),
|
|
912
|
+
metadata: { runId: r },
|
|
913
|
+
});
|
|
914
|
+
await store.appendMessage({
|
|
915
|
+
sessionId: s,
|
|
916
|
+
role: 'assistant',
|
|
917
|
+
parts: text('3'),
|
|
918
|
+
metadata: { runId: id() },
|
|
919
|
+
});
|
|
920
|
+
expect(await store.listTurnSegmentIds(r)).toEqual([seg1, seg2]);
|
|
921
|
+
expect(await store.finalizeInterruptedTurn(r)).toEqual([seg2]);
|
|
922
|
+
expect(await store.finalizeInterruptedTurn(r)).toEqual([]);
|
|
923
|
+
const rows = await store.listMessages(s);
|
|
924
|
+
expect(rows.map(m => m.metadata?.status)).toEqual([
|
|
925
|
+
'completed',
|
|
926
|
+
'aborted',
|
|
927
|
+
undefined,
|
|
928
|
+
]);
|
|
929
|
+
expect(rows[1].metadata).toEqual({
|
|
930
|
+
runId: r,
|
|
931
|
+
status: 'aborted',
|
|
932
|
+
stopReason: 'aborted',
|
|
933
|
+
});
|
|
934
|
+
}));
|
|
935
|
+
it('listMessages 按角色与 seq 过滤;reserveSeq 取走的号不再分配给任何行', withImpl(factory, async (store) => {
|
|
936
|
+
const s = await fx.rootSession();
|
|
937
|
+
await store.appendMessage({
|
|
938
|
+
sessionId: s,
|
|
939
|
+
role: 'user',
|
|
940
|
+
parts: text('q'),
|
|
941
|
+
});
|
|
942
|
+
const a1 = await store.appendMessage({
|
|
943
|
+
sessionId: s,
|
|
944
|
+
role: 'assistant',
|
|
945
|
+
parts: text('a1'),
|
|
946
|
+
});
|
|
947
|
+
const reserved = await store.reserveSeq(s);
|
|
948
|
+
expect(reserved).toBeGreaterThan(a1.seq);
|
|
949
|
+
const a2 = await store.appendMessage({
|
|
950
|
+
sessionId: s,
|
|
951
|
+
role: 'assistant',
|
|
952
|
+
parts: text('a2'),
|
|
953
|
+
});
|
|
954
|
+
expect(a2.seq).toBeGreaterThan(reserved);
|
|
955
|
+
expect((await store.listMessages(s, { role: 'assistant' })).map(m => m.id)).toEqual([a1.id, a2.id]);
|
|
956
|
+
expect((await store.listMessages(s, { afterSeq: reserved })).map(m => m.id)).toEqual([a2.id]);
|
|
957
|
+
expect(await store.listMessages(s, { afterSeq: null })).toHaveLength(3);
|
|
958
|
+
}));
|
|
959
|
+
});
|
|
960
|
+
}
|
|
607
961
|
//# sourceMappingURL=contracts.js.map
|