@qjp88995/agent-runtime 0.1.0 → 0.1.2

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,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
- /** 建一条能被 `findStaleRunning(0)` 看到的旧跑次所需的等待(毫秒),缺省 5。 */
269
- staleDelayMs = 5) {
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
- return store
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 === undefined ? id() : 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
- .then(() => runId);
299
+ ...(fx.runAttrs ? { attrs: fx.runAttrs } : {}),
300
+ });
301
+ return runId;
284
302
  };
285
303
  it('open 后可查会话与深度;缺记录给缺省值', withImpl(factory, async (store) => {
286
- const sessionId = id();
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: id(),
361
+ parentRunId: await fx.run(rootSessionId),
343
362
  subagentName: 'general-purpose',
344
363
  userId: null,
345
364
  title: 't',
346
- metadata: { k: 'v' },
347
- attrs: { a: 1 },
365
+ metadata: fx.sessionMetadata,
366
+ attrs: fx.sessionAttrs,
348
367
  });
349
368
  it('建子会话:存活(游标 null)、读回同一行;不存在的读回 null', withImpl(factory, async (store) => {
350
- const root = id();
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: { k: 'v' },
358
- attrs: { a: 1 },
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, id());
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, id());
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 = id();
378
- const r2 = id();
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
- const input = (over = {}) => ({
402
- sessionId: id(),
403
- userId: id(),
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 = id();
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 = id();
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 = id();
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 = id();
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