feat(run): wire Workspace to TerminalView, delete the stream-json Run feature
Combines three tasks that couldn't land as separate commits: the pre-commit hook's full test run crashes on any intermediate state where Workspace.tsx still imports the files being deleted, so the deletion (old RunConsole/useRunStream/run-spawner/stream-json-parser), the RunSetup/RunHistory type adjustments, and this file's own TerminalView wiring had to be staged together and committed as one hook-passable unit. - Delete RunConsole.tsx, useRunStream.ts, server/lib/run-spawner.js, server/lib/stream-json-parser.js and their tests (Task 8). - Adjust RunSetup.tsx/RunHistory.tsx to the tmux-backed RunHandle/ RunStartArgs/DashboardRunHistoryItem shapes, remove mode selection UI (Task 9). - Swap Workspace.tsx's chat-bubble run console for TerminalView (xterm.js over /ws-pty/:runId), drop the stream-json envelope plumbing, update Start/Resume to the new RunStartArgs payload. Create onStartFromSetup handler to work with RunSetup's new callback shape. Remove mode state and related plumbing. Remove send/followUp state (no longer using old RunConsole chat interface). - Add promptPlaceholderTerminal i18n key to support RunSetup's new placeholder text (Task 10). - Update Workspace.test.tsx to mock TerminalView component. - Regenerate screens.snapshot.test.tsx snapshot (only Workspace run panel changes: terminal container instead of chat bubbles).
This commit is contained in:
@@ -1,174 +0,0 @@
|
||||
/**
|
||||
* @file useRunStream.test.tsx
|
||||
* @description Covers `useRunStream`, the hook that owns the Run page's live
|
||||
* envelope state: it subscribes to the event bus and folds `run_stream`
|
||||
* envelopes into an array, forwards `run_status` / `run_input_ack` for the
|
||||
* subscribed run id to the caller's callbacks, fires the id-agnostic
|
||||
* `onAnyStatus` for every `run_status`, and disposes its subscription on
|
||||
* unmount. `eventBus` is exercised for real (it is a plain in-memory pub/sub)
|
||||
* with only its `subscribe` spied on, so the disposer assertion pins the real
|
||||
* lifecycle rather than a mock's.
|
||||
*
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
import { act, renderHook } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { eventBus } from "../../lib/eventBus";
|
||||
import type { WSMessage } from "../../lib/types";
|
||||
import { useRunStream, type Envelope } from "../useRunStream";
|
||||
|
||||
/** `run_stream` frame carrying one envelope for `id`. */
|
||||
function streamMsg(id: string, envelope: unknown): WSMessage {
|
||||
return { type: "run_stream", data: { id, envelope } } as WSMessage;
|
||||
}
|
||||
|
||||
function statusMsg(id: string, status: string): WSMessage {
|
||||
return { type: "run_status", data: { id, status, at: 1 } } as WSMessage;
|
||||
}
|
||||
|
||||
const noopOpts = { onStatus: () => {}, onInputAck: () => {}, onAnyStatus: () => {} };
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("useRunStream", () => {
|
||||
it("merges envelopes for the subscribed run id in arrival order", () => {
|
||||
const { result } = renderHook(() => useRunStream("run-1", noopOpts));
|
||||
|
||||
act(() => {
|
||||
eventBus.publish(streamMsg("run-1", { type: "system", subtype: "init" }));
|
||||
eventBus.publish(streamMsg("run-1", { type: "result", subtype: "success" }));
|
||||
});
|
||||
|
||||
expect(result.current.envelopes.map((e) => (e as { type: string }).type)).toEqual([
|
||||
"system",
|
||||
"result",
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores an envelope for a different run id", () => {
|
||||
const { result } = renderHook(() => useRunStream("run-1", noopOpts));
|
||||
|
||||
act(() => {
|
||||
eventBus.publish(streamMsg("run-2", { type: "result" }));
|
||||
});
|
||||
|
||||
expect(result.current.envelopes).toEqual([]);
|
||||
});
|
||||
|
||||
it("updates a streaming assistant envelope in place instead of appending", () => {
|
||||
const { result } = renderHook(() => useRunStream("run-1", noopOpts));
|
||||
|
||||
act(() => {
|
||||
// message_start seeds the kept envelope + a streaming placeholder.
|
||||
eventBus.publish(
|
||||
streamMsg("run-1", {
|
||||
type: "stream_event",
|
||||
event: { type: "message_start", message: { id: "m1" } },
|
||||
})
|
||||
);
|
||||
});
|
||||
expect(result.current.envelopes).toHaveLength(2);
|
||||
|
||||
act(() => {
|
||||
eventBus.publish(
|
||||
streamMsg("run-1", {
|
||||
type: "stream_event",
|
||||
event: {
|
||||
type: "content_block_start",
|
||||
index: 0,
|
||||
message: { id: "m1" },
|
||||
content_block: { type: "text", text: "" },
|
||||
},
|
||||
})
|
||||
);
|
||||
eventBus.publish(
|
||||
streamMsg("run-1", {
|
||||
type: "stream_event",
|
||||
event: {
|
||||
type: "content_block_delta",
|
||||
index: 0,
|
||||
message: { id: "m1" },
|
||||
delta: { type: "text_delta", text: "hi" },
|
||||
},
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
// Still 2 envelopes: the deltas mutated the placeholder, they did not append.
|
||||
expect(result.current.envelopes).toHaveLength(2);
|
||||
const placeholder = result.current.envelopes[1] as {
|
||||
message: { content: { text?: string }[]; _streaming?: boolean };
|
||||
};
|
||||
expect(placeholder.message.content[0]?.text).toBe("hi");
|
||||
expect(placeholder.message._streaming).toBe(true);
|
||||
});
|
||||
|
||||
it("invokes onStatus only for the subscribed run id, onAnyStatus for every run_status", () => {
|
||||
const onStatus = vi.fn();
|
||||
const onAnyStatus = vi.fn();
|
||||
renderHook(() => useRunStream("run-1", { ...noopOpts, onStatus, onAnyStatus }));
|
||||
|
||||
act(() => {
|
||||
eventBus.publish(statusMsg("run-1", "completed"));
|
||||
eventBus.publish(statusMsg("run-2", "completed"));
|
||||
});
|
||||
|
||||
expect(onStatus).toHaveBeenCalledTimes(1);
|
||||
expect(onStatus.mock.calls[0]?.[0]).toMatchObject({ id: "run-1", status: "completed" });
|
||||
expect(onAnyStatus).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("invokes onInputAck only for the subscribed run id", () => {
|
||||
const onInputAck = vi.fn();
|
||||
renderHook(() => useRunStream("run-1", { ...noopOpts, onInputAck }));
|
||||
|
||||
act(() => {
|
||||
eventBus.publish({ type: "run_input_ack", data: { id: "run-2" } } as WSMessage);
|
||||
eventBus.publish({ type: "run_input_ack", data: { id: "run-1" } } as WSMessage);
|
||||
});
|
||||
|
||||
expect(onInputAck).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("ignores every frame while the run id is null", () => {
|
||||
const onAnyStatus = vi.fn();
|
||||
const { result } = renderHook(() => useRunStream(null, { ...noopOpts, onAnyStatus }));
|
||||
|
||||
act(() => {
|
||||
eventBus.publish(streamMsg("run-1", { type: "result" }));
|
||||
eventBus.publish(statusMsg("run-1", "completed"));
|
||||
});
|
||||
|
||||
expect(result.current.envelopes).toEqual([]);
|
||||
expect(onAnyStatus).toHaveBeenCalledTimes(1); // id-agnostic by design
|
||||
});
|
||||
|
||||
it("disposes the event bus subscription on unmount", () => {
|
||||
const dispose = vi.fn();
|
||||
const subscribe = vi.spyOn(eventBus, "subscribe").mockReturnValue(dispose);
|
||||
|
||||
const { unmount } = renderHook(() => useRunStream("run-1", noopOpts));
|
||||
expect(subscribe).toHaveBeenCalledTimes(1);
|
||||
expect(dispose).not.toHaveBeenCalled();
|
||||
|
||||
unmount();
|
||||
expect(dispose).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("exposes setEnvelopes so the page can seed and clear the list", () => {
|
||||
const { result } = renderHook(() => useRunStream("run-1", noopOpts));
|
||||
|
||||
act(() => {
|
||||
result.current.setEnvelopes([{ type: "user", message: { content: "hello" } } as Envelope]);
|
||||
});
|
||||
expect(result.current.envelopes).toHaveLength(1);
|
||||
|
||||
act(() => {
|
||||
result.current.setEnvelopes([]);
|
||||
});
|
||||
expect(result.current.envelopes).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user