Restore durable log ingest and typed plugin projections
This commit is contained in:
@@ -86,9 +86,7 @@ describe("ServerManagementTerminalDrawer", () => {
|
||||
expect(Array.from(container?.querySelectorAll(".terminal-text") ?? []).filter((node) => node.textContent === "generation A live output")).toHaveLength(1);
|
||||
expect(apiMocks.openServerLogEvents).toHaveBeenCalledTimes(1);
|
||||
expect(apiMocks.listLogStreams).not.toHaveBeenCalled();
|
||||
expect(apiMocks.queryLogStream).not.toHaveBeenCalled();
|
||||
expect(apiMocks.dispatchSourceRCONCommand).not.toHaveBeenCalled();
|
||||
expect(container?.textContent).not.toContain("查看历史");
|
||||
});
|
||||
|
||||
it("uses the server-provided clock for terminal system lines", async () => {
|
||||
@@ -101,21 +99,6 @@ describe("ServerManagementTerminalDrawer", () => {
|
||||
expect(systemLine?.querySelector("time")?.textContent).toBe(formatTerminalServerTime(serverTime));
|
||||
});
|
||||
|
||||
it("does not load stored output when current stream metadata arrives", async () => {
|
||||
const stream = logStream("stdout-current", "session-current", "process.stdout");
|
||||
stream.latestSeq = 42;
|
||||
apiMocks.queryLogStream.mockResolvedValue({ logStreamId: stream.id, entries: [logEntry(41, "tail before drawer opened"), logEntry(42, "latest stored output")], nextSeq: 42, latestSeq: 42 });
|
||||
await renderDrawer();
|
||||
|
||||
await emitSession("session-current");
|
||||
await emitStream(stream);
|
||||
await flushPromises();
|
||||
|
||||
expect(container?.textContent).not.toContain("tail before drawer opened");
|
||||
expect(container?.textContent).not.toContain("latest stored output");
|
||||
expect(apiMocks.queryLogStream).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("renders an empty current session without accepting unrelated or sessionless logs", async () => {
|
||||
await renderDrawer();
|
||||
|
||||
@@ -128,7 +111,7 @@ describe("ServerManagementTerminalDrawer", () => {
|
||||
expect(apiMocks.dispatchSourceRCONCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps visible output on a new session and rejects late generation A events", async () => {
|
||||
it("clears generation A on a new session and rejects late generation A events", async () => {
|
||||
await renderDrawer();
|
||||
await emitSession("session-a");
|
||||
await emitStream(logStream("stdout-a", "session-a", "process.stdout"));
|
||||
@@ -139,14 +122,14 @@ describe("ServerManagementTerminalDrawer", () => {
|
||||
await emitStream(logStream("stdout-b", "session-b", "process.stdout"));
|
||||
await emitLog("stdout-b", "session-b", "process.stdout", logEntry(1, "generation B output"));
|
||||
|
||||
expect(container?.textContent).toContain("generation A output");
|
||||
expect(container?.textContent).not.toContain("generation A output");
|
||||
expect(container?.textContent).not.toContain("late generation A output");
|
||||
expect(container?.textContent).toContain("generation B output");
|
||||
expect(container?.textContent).toContain("Run 已切换到新的受管进程输出会话");
|
||||
expect(apiMocks.openServerLogEvents).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps visible output through a stopped boundary and restores the running session on the same SSE connection", async () => {
|
||||
it("clears a stopped session and restores the running session on the same SSE connection", async () => {
|
||||
await renderDrawer();
|
||||
await emitSession("session-a");
|
||||
await emitStream(logStream("stdout-a", "session-a", "process.stdout"));
|
||||
@@ -154,29 +137,46 @@ describe("ServerManagementTerminalDrawer", () => {
|
||||
|
||||
await emitSession();
|
||||
await emitLog("stdout-a", "session-a", "process.stdout", logEntry(2, "late output after stop"));
|
||||
expect(container?.textContent).toContain("running output before stop");
|
||||
expect(container?.textContent).not.toContain("running output before stop");
|
||||
expect(container?.textContent).not.toContain("late output after stop");
|
||||
expect(container?.textContent).toContain("当前没有可跟随的受管进程输出");
|
||||
|
||||
await emitSession("session-b");
|
||||
await emitStream(logStream("stdout-b", "session-b", "process.stdout"));
|
||||
await emitLog("stdout-b", "session-b", "process.stdout", logEntry(1, "running output after recovery"));
|
||||
expect(container?.textContent).toContain("running output before stop");
|
||||
expect(container?.textContent).not.toContain("running output before stop");
|
||||
expect(container?.textContent).toContain("running output after recovery");
|
||||
expect(apiMocks.openServerLogEvents).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps the terminal as a pure current live stream and never opens platform history", async () => {
|
||||
it("keeps selected historical output separate while live output continues in the background", async () => {
|
||||
const oldStream = logStream("stdout-old", "session-old", "process.stdout", "2026-08-01T00:00:00Z");
|
||||
oldStream.latestSeq = 900;
|
||||
apiMocks.listLogStreams.mockResolvedValue({ items: [oldStream], count: 1 });
|
||||
apiMocks.queryLogStream.mockResolvedValue({ logStreamId: oldStream.id, entries: [logEntry(1, "selected historical output", "2026-08-01T00:00:01Z")], nextSeq: 1, latestSeq: 1 });
|
||||
await renderDrawer();
|
||||
await emitSession("session-current");
|
||||
await emitStream(logStream("stdout-current", "session-current", "process.stdout"));
|
||||
await emitLog("stdout-current", "session-current", "process.stdout", logEntry(1, "current live output"));
|
||||
|
||||
await clickButton("查看历史");
|
||||
await flushPromises();
|
||||
const select = container?.querySelector<HTMLSelectElement>('select[aria-label="选择历史日志流"]');
|
||||
if (!select) throw new Error("history stream selector not found");
|
||||
await act(async () => setSelectValue(select, oldStream.id));
|
||||
await flushPromises();
|
||||
|
||||
expect(container?.textContent).toContain("selected historical output");
|
||||
expect(container?.textContent).not.toContain("current live output");
|
||||
await emitLog("stdout-current", "session-current", "process.stdout", logEntry(2, "new live output while viewing history"));
|
||||
expect(container?.textContent).not.toContain("new live output while viewing history");
|
||||
|
||||
await clickButton("实时输出");
|
||||
expect(container?.textContent).toContain("current live output");
|
||||
expect(container?.textContent).not.toContain("查看历史");
|
||||
expect(container?.querySelector('select[aria-label="选择历史日志流"]')).toBeNull();
|
||||
expect(apiMocks.listLogStreams).not.toHaveBeenCalled();
|
||||
expect(apiMocks.queryLogStream).not.toHaveBeenCalled();
|
||||
expect(container?.textContent).toContain("new live output while viewing history");
|
||||
expect(container?.textContent).not.toContain("selected historical output");
|
||||
expect(apiMocks.listLogStreams).toHaveBeenCalledWith("server-1");
|
||||
expect(apiMocks.queryLogStream).toHaveBeenCalledWith({ logStreamId: oldStream.id, afterSeq: 400, limit: 500 });
|
||||
expect(apiMocks.dispatchSourceRCONCommand).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -245,6 +245,11 @@ function setInputValue(input: HTMLInputElement, value: string) {
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
|
||||
function setSelectValue(select: HTMLSelectElement, value: string) {
|
||||
Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype, "value")?.set?.call(select, value);
|
||||
select.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
}
|
||||
|
||||
function logStream(id: string, logSessionId: string, streamKey: string, updatedAt = "2026-08-14T00:00:00Z"): LogStreamResponse {
|
||||
return { id, serverInstanceId: "server-1", source: "process", streamKey, logSessionId, sessionStartedAt: updatedAt, latestSeq: 1, storageBackend: "database", retentionPolicy: "default", createdAt: updatedAt, updatedAt };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user