fix(domain,infrastructure,web-server,app-tauri,frontend): TUI native noire — route PTY explicite et replay du scrollback pré-attach sans perte — #360 (QA verte)
This commit is contained in:
@ -400,6 +400,69 @@ describe("TauriAgentGateway invoke payloads", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("launchAgent requests and accepts only the PTY route for a native TUI", async () => {
|
||||
invoke.mockResolvedValueOnce({
|
||||
sessionId: "pty-session-1",
|
||||
cwd: "/repo",
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
cellKind: "pty",
|
||||
assignedConversationId: "conversation-1",
|
||||
});
|
||||
|
||||
const out = await new TauriAgentGateway().launchAgent(
|
||||
"proj-1",
|
||||
"agent-2",
|
||||
{
|
||||
cwd: "/repo",
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
conversationId: "conversation-0",
|
||||
nodeId: "node-3",
|
||||
},
|
||||
vi.fn(),
|
||||
);
|
||||
|
||||
expect(invoke).toHaveBeenCalledWith("launch_agent", {
|
||||
request: {
|
||||
projectId: "proj-1",
|
||||
agentId: "agent-2",
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
cellKind: "pty",
|
||||
conversationId: "conversation-0",
|
||||
freshStart: false,
|
||||
nodeId: "node-3",
|
||||
},
|
||||
onOutput: expect.anything(),
|
||||
});
|
||||
expect(out.sessionId).toBe("pty-session-1");
|
||||
expect(out.assignedConversationId).toBe("conversation-1");
|
||||
});
|
||||
|
||||
it("launchAgent rejects a chat-routed launch instead of returning a silent PTY handle", async () => {
|
||||
invoke.mockResolvedValueOnce({
|
||||
sessionId: "chat-session-1",
|
||||
cwd: "/repo",
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
cellKind: "chat",
|
||||
});
|
||||
|
||||
await expect(
|
||||
new TauriAgentGateway().launchAgent(
|
||||
"proj-1",
|
||||
"agent-2",
|
||||
{ cwd: "/repo", rows: 24, cols: 80, nodeId: "node-3" },
|
||||
vi.fn(),
|
||||
),
|
||||
).rejects.toEqual({
|
||||
code: "NATIVE_TUI_ROUTED_TO_NON_PTY",
|
||||
message:
|
||||
"native TUI launch for agent agent-2 in project proj-1 returned cellKind=chat; expected pty. sessionId=chat-session-1; nodeId=node-3",
|
||||
});
|
||||
});
|
||||
|
||||
it("launchAgentChat rejects a PTY-routed launch before returning a fake structured session", async () => {
|
||||
const log = vi.spyOn(console, "error").mockImplementation(() => {});
|
||||
invoke.mockResolvedValueOnce({
|
||||
|
||||
@ -74,6 +74,22 @@ function structuredLaunchRoutedToPtyError(
|
||||
};
|
||||
}
|
||||
|
||||
function nativeLaunchRoutedToNonPtyError(
|
||||
projectId: string,
|
||||
agentId: string,
|
||||
options: OpenTerminalOptions,
|
||||
response: LaunchAgentResponse,
|
||||
): GatewayError {
|
||||
const actual = response.cellKind ?? "missing";
|
||||
return {
|
||||
code: "NATIVE_TUI_ROUTED_TO_NON_PTY",
|
||||
message:
|
||||
`native TUI launch for agent ${agentId} in project ${projectId} ` +
|
||||
`returned cellKind=${actual}; expected pty. ` +
|
||||
`sessionId=${response.sessionId}; nodeId=${options.nodeId ?? "none"}`,
|
||||
};
|
||||
}
|
||||
|
||||
export class TauriAgentGateway implements AgentGateway {
|
||||
listAgents(projectId: string): Promise<Agent[]> {
|
||||
return invoke<Agent[]>("list_agents", { projectId });
|
||||
@ -214,6 +230,11 @@ export class TauriAgentGateway implements AgentGateway {
|
||||
agentId,
|
||||
rows: options.rows,
|
||||
cols: options.cols,
|
||||
// Native TUI is always the raw PTY route, including for profiles that
|
||||
// also support the structured/custom CLI. Omitting this hint lets the
|
||||
// backend select a chat session, whose reply stream can never feed the
|
||||
// xterm output channel and leaves the cell looking blank.
|
||||
cellKind: "pty",
|
||||
// Resume id: the leaf's persisted conversation id, when any (T4b). The
|
||||
// backend resumes it; absent ⇒ a fresh cell that may get a new id.
|
||||
conversationId: options.conversationId ?? null,
|
||||
@ -224,6 +245,10 @@ export class TauriAgentGateway implements AgentGateway {
|
||||
onOutput: channel,
|
||||
});
|
||||
|
||||
if (res.cellKind !== "pty") {
|
||||
throw nativeLaunchRoutedToNonPtyError(projectId, agentId, options, res);
|
||||
}
|
||||
|
||||
const base = makeTerminalHandle(res.sessionId, channel);
|
||||
// Surface the id assigned by this launch (so the caller persists it on the
|
||||
// leaf and resumes next time).
|
||||
|
||||
@ -220,6 +220,7 @@ describe("HttpAgentGateway WS round-trip (B6 frames)", () => {
|
||||
nodeId: "node-1",
|
||||
rows: 24,
|
||||
cols: 80,
|
||||
cellKind: "pty",
|
||||
conversationId: "resume-9",
|
||||
freshStart: false,
|
||||
});
|
||||
|
||||
@ -416,6 +416,10 @@ export class WsLiveClient {
|
||||
nodeId: params.nodeId ?? null,
|
||||
rows: params.rows,
|
||||
cols: params.cols,
|
||||
// The native cell must select the raw PTY route explicitly. Profiles
|
||||
// may also expose a structured chat route; relying on their default can
|
||||
// produce a chat session behind an xterm subscription with no output.
|
||||
cellKind: "pty",
|
||||
conversationId: params.conversationId ?? null,
|
||||
freshStart: params.freshStart ?? false,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user