

























@@ -2,12 +2,14 @@ import { afterEach, describe, expect, it, vi } from "vitest";
22import {
33abortChatRunById,
44abortChatRunsForProvider,
5+boundInFlightRunSnapshotForChatHistory,
56isChatStopCommandText,
67registerChatAbortController,
78resolveAgentRunExpiresAtMs,
89resolveChatRunExpiresAtMs,
910type ChatAbortOps,
1011type ChatAbortControllerEntry,
12+resolveInFlightRunSnapshot,
1113updateChatRunProvider,
1214} from "./chat-abort.js";
1315@@ -349,3 +351,262 @@ describe("abortChatRunsForProvider", () => {
349351);
350352});
351353});
354+355+describe("resolveInFlightRunSnapshot", () => {
356+const inFlightEntry = (
357+sessionKey: string,
358+opts?: {
359+agentId?: string;
360+aborted?: boolean;
361+projectSessionActive?: boolean;
362+startedAtMs?: number;
363+kind?: ChatAbortControllerEntry["kind"];
364+},
365+): ChatAbortControllerEntry => {
366+const now = Date.now();
367+const controller = new AbortController();
368+if (opts?.aborted) {
369+controller.abort();
370+}
371+const startedAtMs = opts?.startedAtMs ?? now;
372+return {
373+ controller,
374+sessionId: "sess-1",
375+ sessionKey,
376+agentId: opts?.agentId,
377+ startedAtMs,
378+expiresAtMs: startedAtMs + 10_000,
379+projectSessionActive: opts?.projectSessionActive ?? true,
380+kind: opts?.kind,
381+};
382+};
383+384+// Most cases request with requestedKey === canonicalKey; default canonical to
385+// the requested key unless a case exercises the requested/canonical split.
386+const snap = (p: {
387+chatAbortControllers: Map<string, ChatAbortControllerEntry>;
388+chatRunBuffers: Map<string, string>;
389+sessionKey: string;
390+canonicalSessionKey?: string;
391+agentId?: string;
392+defaultAgentId?: string;
393+}) =>
394+resolveInFlightRunSnapshot({
395+chatAbortControllers: p.chatAbortControllers,
396+chatRunBuffers: p.chatRunBuffers,
397+requestedSessionKey: p.sessionKey,
398+canonicalSessionKey: p.canonicalSessionKey ?? p.sessionKey,
399+agentId: p.agentId,
400+defaultAgentId: p.defaultAgentId,
401+});
402+403+it("returns the live assistant text of a matching active run", () => {
404+const result = snap({
405+chatAbortControllers: new Map([["run-1", inFlightEntry("agent:main:tui-x")]]),
406+chatRunBuffers: new Map([["run-1", "partial answer so far"]]),
407+sessionKey: "agent:main:tui-x",
408+});
409+expect(result).toEqual({ runId: "run-1", text: "partial answer so far" });
410+});
411+412+it("is a no-op when chatAbortControllers is not a Map (unpopulated context)", () => {
413+expect(
414+snap({
415+chatAbortControllers: undefined as never,
416+chatRunBuffers: undefined as never,
417+sessionKey: "agent:main:s",
418+}),
419+).toBeUndefined();
420+});
421+422+it("matches a run stored under the canonical key when requested with a different key", () => {
423+// Abort entry holds the canonical store key; the client requests history with
424+// a different (requested) key for the same logical session.
425+const result = snap({
426+chatAbortControllers: new Map([["run-1", inFlightEntry("agent:main:main")]]),
427+chatRunBuffers: new Map([["run-1", "partial"]]),
428+sessionKey: "main",
429+canonicalSessionKey: "agent:main:main",
430+});
431+expect(result).toEqual({ runId: "run-1", text: "partial" });
432+});
433+434+it("ignores aborted, completed (not projected active), and other-session runs", () => {
435+const variants: ChatAbortControllerEntry[] = [
436+inFlightEntry("agent:main:s", { aborted: true }),
437+inFlightEntry("agent:main:s", { projectSessionActive: false }),
438+inFlightEntry("agent:main:other"),
439+];
440+for (const entry of variants) {
441+expect(
442+snap({
443+chatAbortControllers: new Map([["run", entry]]),
444+chatRunBuffers: new Map([["run", "text"]]),
445+sessionKey: "agent:main:s",
446+}),
447+).toBeUndefined();
448+}
449+});
450+451+it("ignores hidden agent runs that are not visible chat sends", () => {
452+expect(
453+snap({
454+chatAbortControllers: new Map([
455+["run-agent", inFlightEntry("agent:main:s", { kind: "agent" })],
456+]),
457+chatRunBuffers: new Map([["run-agent", "hidden partial"]]),
458+sessionKey: "agent:main:s",
459+}),
460+).toBeUndefined();
461+});
462+463+it("treats an entry with undefined projectSessionActive as active (sessions.list contract)", () => {
464+const entry = inFlightEntry("agent:main:s");
465+delete (entry as { projectSessionActive?: boolean }).projectSessionActive;
466+expect(
467+snap({
468+chatAbortControllers: new Map([["run", entry]]),
469+chatRunBuffers: new Map([["run", "live partial"]]),
470+sessionKey: "agent:main:s",
471+}),
472+).toEqual({ runId: "run", text: "live partial" });
473+});
474+475+it("returns an active run with empty text (Codex streams no incremental text mid-run)", () => {
476+expect(
477+snap({
478+chatAbortControllers: new Map([["run", inFlightEntry("agent:main:s")]]),
479+chatRunBuffers: new Map(),
480+sessionKey: "agent:main:s",
481+}),
482+).toEqual({ runId: "run", text: "" });
483+});
484+485+it("does not surface suppressed control-token lead fragments from the live buffer", () => {
486+expect(
487+snap({
488+chatAbortControllers: new Map([["run", inFlightEntry("agent:main:s")]]),
489+chatRunBuffers: new Map([["run", "NO_"]]),
490+sessionKey: "agent:main:s",
491+}),
492+).toEqual({ runId: "run", text: "" });
493+});
494+495+it("scopes the shared global session by agent so one agent's run is not restored into another", () => {
496+const controllers = new Map<string, ChatAbortControllerEntry>([
497+["run-a", inFlightEntry("global", { agentId: "main" })],
498+["run-b", inFlightEntry("global", { agentId: "work" })],
499+]);
500+const buffers = new Map([
501+["run-a", "main agent global text"],
502+["run-b", "work agent global text"],
503+]);
504+expect(
505+snap({
506+chatAbortControllers: controllers,
507+chatRunBuffers: buffers,
508+sessionKey: "global",
509+agentId: "work",
510+}),
511+).toEqual({ runId: "run-b", text: "work agent global text" });
512+expect(
513+snap({
514+chatAbortControllers: controllers,
515+chatRunBuffers: buffers,
516+sessionKey: "global",
517+agentId: "main",
518+}),
519+).toEqual({ runId: "run-a", text: "main agent global text" });
520+});
521+522+it("resolves bare global history snapshots to the default agent", () => {
523+const controllers = new Map<string, ChatAbortControllerEntry>([
524+["run-main", inFlightEntry("global", { agentId: "main", startedAtMs: 1_000 })],
525+["run-work", inFlightEntry("global", { agentId: "work", startedAtMs: 2_000 })],
526+]);
527+const buffers = new Map([
528+["run-main", "main default text"],
529+["run-work", "work global text"],
530+]);
531+532+expect(
533+snap({
534+chatAbortControllers: controllers,
535+chatRunBuffers: buffers,
536+sessionKey: "global",
537+defaultAgentId: "main",
538+}),
539+).toEqual({ runId: "run-main", text: "main default text" });
540+});
541+542+it("prefers the newest startedAtMs when several runs match the same session+agent", () => {
543+// A fast restart/retry/stale-controller race can leave two active entries for
544+// the same key; selection must not depend on Map insertion order. Insert the
545+// older run first so a first-match selector would return the wrong one.
546+const controllers = new Map<string, ChatAbortControllerEntry>([
547+["run-old", inFlightEntry("agent:main:s", { startedAtMs: 1_000 })],
548+["run-new", inFlightEntry("agent:main:s", { startedAtMs: 2_000 })],
549+]);
550+const buffers = new Map([
551+["run-old", "stale partial"],
552+["run-new", "current partial"],
553+]);
554+expect(
555+snap({
556+chatAbortControllers: controllers,
557+chatRunBuffers: buffers,
558+sessionKey: "agent:main:s",
559+}),
560+).toEqual({ runId: "run-new", text: "current partial" });
561+});
562+563+it("breaks startedAtMs ties deterministically by runId regardless of insertion order", () => {
564+const buffers = new Map([
565+["run-a", "a"],
566+["run-b", "b"],
567+]);
568+const ascending = new Map<string, ChatAbortControllerEntry>([
569+["run-a", inFlightEntry("agent:main:s", { startedAtMs: 5_000 })],
570+["run-b", inFlightEntry("agent:main:s", { startedAtMs: 5_000 })],
571+]);
572+const descending = new Map<string, ChatAbortControllerEntry>([
573+["run-b", inFlightEntry("agent:main:s", { startedAtMs: 5_000 })],
574+["run-a", inFlightEntry("agent:main:s", { startedAtMs: 5_000 })],
575+]);
576+// Same winner ("run-b" > "run-a") no matter which order the map was built in.
577+expect(
578+snap({
579+chatAbortControllers: ascending,
580+chatRunBuffers: buffers,
581+sessionKey: "agent:main:s",
582+}),
583+).toEqual({ runId: "run-b", text: "b" });
584+expect(
585+snap({
586+chatAbortControllers: descending,
587+chatRunBuffers: buffers,
588+sessionKey: "agent:main:s",
589+}),
590+).toEqual({ runId: "run-b", text: "b" });
591+});
592+593+it("keeps in-flight text when it fits the chat history budget", () => {
594+expect(
595+boundInFlightRunSnapshotForChatHistory({
596+snapshot: { runId: "run-1", text: "partial" },
597+messages: [],
598+maxBytes: 1_000,
599+}),
600+).toEqual({ runId: "run-1", text: "partial" });
601+});
602+603+it("drops oversized in-flight text but keeps the run id for adoption", () => {
604+expect(
605+boundInFlightRunSnapshotForChatHistory({
606+snapshot: { runId: "run-1", text: "x".repeat(1_000) },
607+messages: [],
608+maxBytes: 100,
609+}),
610+).toEqual({ runId: "run-1", text: "" });
611+});
612+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。