Files
tutti/packages/agent/gui/agent-gui/agentGuiNode/controller/AgentGUIConversationRailQueryController.reattach.spec.ts
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

224 lines
7.2 KiB
TypeScript

import { normalizeAgentActivitySession } from "@tutti-os/agent-activity-core";
import { describe, expect, it, vi } from "vitest";
import { createTestAgentSessionEngine } from "../../../shared/testing/createTestAgentSessionEngine";
import {
AgentGUIConversationRailQueryController,
type ConversationRailQueryRuntime
} from "./AgentGUIConversationRailQueryController";
import { createConversationRailConversationsSelector } from "./agentGuiConversationRailQuerySnapshot";
describe("AgentGUIConversationRailQueryController reattach", () => {
it("recovers an interrupted draft from an authoritative scoped refresh", async () => {
const session = normalizeAgentActivitySession({
activeTurnId: null,
agentSessionId: "session-1",
agentTargetId: "local:codex",
cwd: "/workspace",
latestTurnInteractions: [],
pendingInteractions: [],
provider: "codex",
railSectionKey: "conversations",
title: "Session",
updatedAtUnixMs: 1,
workspaceId: "test-workspace"
});
const engine = createTestAgentSessionEngine();
const pinnedSession = {
...session,
pinnedAtUnixMs: 10,
updatedAtUnixMs: 2
};
const listPinnedSessionsPage = vi.fn(() => new Promise<never>(() => {}));
const listSessionSectionPage = vi.fn(() => new Promise<never>(() => {}));
let firstPagesRequestCount = 0;
const runtime: ConversationRailQueryRuntime = {
listPinnedSessionsPage,
listSessionSectionPage,
listSessionSections: async (input) => {
firstPagesRequestCount += 1;
return {
pinned:
firstPagesRequestCount > 1
? {
hasMore: false,
sessions: [pinnedSession],
totalCount: 1
}
: undefined,
sections: [
{
hasMore: false,
kind: "conversations",
sectionKey: "conversations",
sessions: firstPagesRequestCount > 1 ? [] : [session],
totalCount: firstPagesRequestCount > 1 ? 0 : 1
}
],
workspaceId: input.workspaceId
};
}
};
const controller = new AgentGUIConversationRailQueryController({
engine,
getActiveConversationId: () => null,
runtime,
workspaceId: "test-workspace"
});
controller.configure({
conversationFilter: { kind: "all" },
userProjects: []
});
const detach = controller.attach();
await vi.waitFor(() =>
expect(controller.getSnapshot().runtimeRailSectionsPending).toBe(false)
);
engine.dispatch({ session: pinnedSession, type: "session/upserted" });
await vi.waitFor(() =>
expect(listPinnedSessionsPage).toHaveBeenCalledTimes(1)
);
expect(listSessionSectionPage).toHaveBeenCalledTimes(1);
expect(controller.isInteractionLocked()).toBe(true);
detach();
const detachAgain = controller.attach();
expect(controller.isInteractionLocked()).toBe(true);
await vi.waitFor(() =>
expect(controller.isInteractionLocked()).toBe(false)
);
expect(firstPagesRequestCount).toBe(2);
expect(
createConversationRailConversationsSelector()({
engineState: engine.getSnapshot(),
interactionLocked: controller.isInteractionLocked(),
querySnapshot: controller.getSnapshot()
})[0]?.pinnedAtUnixMs
).toBe(10);
detachAgain();
engine.dispose();
});
it("retries a failed publication search after detach and reattach", async () => {
const session = normalizeAgentActivitySession({
activeTurnId: null,
agentSessionId: "search-session",
agentTargetId: "local:codex",
cwd: "/workspace",
latestTurnInteractions: [],
pendingInteractions: [],
provider: "codex",
railSectionKey: "conversations",
title: "Search session",
updatedAtUnixMs: 1,
workspaceId: "test-workspace"
});
const engine = createTestAgentSessionEngine();
let scheduledSearch: (() => void) | null = null;
let sectionRequestCount = 0;
let searchRequestCount = 0;
const controller = new AgentGUIConversationRailQueryController({
engine,
getActiveConversationId: () => null,
runtime: {
listPinnedSessionsPage: async () => ({
hasMore: false,
sessions: [{ ...session, pinnedAtUnixMs: 10, updatedAtUnixMs: 2 }],
totalCount: 1
}),
listSessionSectionPage: async (input) => ({
hasMore: false,
kind: "conversations",
sectionKey: input.sectionKey,
sessions: [],
totalCount: 0
}),
listSessionSections: async (input) => {
sectionRequestCount += 1;
return {
pinned:
sectionRequestCount > 1
? {
hasMore: false,
sessions: [
{ ...session, pinnedAtUnixMs: 10, updatedAtUnixMs: 2 }
],
totalCount: 1
}
: undefined,
sections: [
{
hasMore: false,
kind: "conversations",
sectionKey: "conversations",
sessions: sectionRequestCount > 1 ? [] : [session],
totalCount: sectionRequestCount > 1 ? 0 : 1
}
],
workspaceId: input.workspaceId
};
},
listSessionsPage: async (input) => {
searchRequestCount += 1;
if (searchRequestCount === 2) {
return new Promise((_resolve, reject) => {
input.signal?.addEventListener(
"abort",
() => reject(new DOMException("Aborted", "AbortError")),
{ once: true }
);
});
}
return {
hasMore: false,
sessions: [],
workspaceId: input.workspaceId
};
}
},
scheduler: {
schedule: (_delayMs, task) => {
scheduledSearch = task;
return { cancel: () => undefined };
}
},
workspaceId: "test-workspace"
});
controller.configure({
conversationFilter: { kind: "all" },
userProjects: []
});
const detach = controller.attach();
await vi.waitFor(() =>
expect(controller.getSnapshot().runtimeRailSectionsPending).toBe(false)
);
controller.setSearchQuery("session");
(scheduledSearch as (() => void) | null)?.();
await vi.waitFor(() => expect(searchRequestCount).toBe(1));
await vi.waitFor(() =>
expect(controller.getSnapshot().railSearch.pending).toBe(false)
);
engine.dispatch({
session: {
...session,
title: "Renamed search session",
updatedAtUnixMs: 2
},
type: "session/upserted"
});
await vi.waitFor(() => expect(searchRequestCount).toBe(2));
detach();
expect(controller.isInteractionLocked()).toBe(true);
const detachAgain = controller.attach();
await vi.waitFor(() => expect(searchRequestCount).toBe(3));
await vi.waitFor(() =>
expect(controller.isInteractionLocked()).toBe(false)
);
detachAgain();
engine.dispose();
});
});