docs(FN-823): clarify agent log content is never truncated per-entry

- Clarify in dashboard README that agent log entries are never truncated
- Add maxLength safeguards in store (getAgentLog), routes, useAgentLogs, and useMultiAgentLogs hooks
- Add tests for store, AgentLogViewer component, useAgentLogs hook, useMultiAgentLogs hook, and dashboard routes
- All tests verify that log text and detail fields are preserved in full without per-entry truncation
This commit is contained in:
gsxdsm
2026-04-04 01:41:13 -07:00
parent 3516394e9f
commit c78f49a8db
11 changed files with 280 additions and 2 deletions

View File

@@ -430,6 +430,71 @@ describe("AgentLogViewer", () => {
});
});
describe("long content preservation", () => {
it("renders very long text entries without truncation", () => {
const longText = "A".repeat(5000);
const entries = [makeEntry({ text: longText })];
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
const textSpans = container.querySelectorAll(".agent-log-text");
expect(textSpans).toHaveLength(1);
expect(textSpans[0].textContent).toBe(longText);
expect(textSpans[0].textContent!.length).toBe(5000);
});
it("renders very long detail text without truncation", () => {
const longDetail = "B".repeat(5000);
const entries = [makeEntry({ text: "Read", type: "tool", detail: longDetail })];
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
const detail = container.querySelector(".agent-log-tool-detail");
expect(detail).toBeTruthy();
expect(detail!.textContent).toContain(longDetail);
expect(detail!.textContent!.length).toBeGreaterThan(5000); // " — " prefix adds a few chars
});
it("renders multiline text content without truncation", () => {
const multilineText = [
"## Analysis",
"",
"After reviewing the codebase:",
"",
"1. First issue found",
"2. Second issue found",
"",
"```typescript",
"const x = 1;",
"```",
"",
"Line " + "C".repeat(2000) + " end",
].join("\n");
const entries = [makeEntry({ text: multilineText })];
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
const textSpans = container.querySelectorAll(".agent-log-text");
expect(textSpans).toHaveLength(1);
// The markdown-rendered content should still contain the essential parts
expect(textSpans[0].textContent).toContain("Analysis");
expect(textSpans[0].textContent).toContain("First issue found");
expect(textSpans[0].textContent).toContain("const x = 1");
});
it("renders long tool_result detail without truncation", () => {
const longDetail = "D".repeat(5000);
const entries = [makeEntry({ text: "Bash", type: "tool_result", detail: longDetail })];
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
const detail = container.querySelector(".agent-log-tool-detail");
expect(detail).toBeTruthy();
expect(detail!.textContent).toContain(longDetail);
});
it("renders long tool_error detail without truncation", () => {
const longDetail = "E".repeat(5000);
const entries = [makeEntry({ text: "Write", type: "tool_error", detail: longDetail })];
const { container } = render(<AgentLogViewer entries={entries} loading={false} />);
const detail = container.querySelector(".agent-log-tool-detail");
expect(detail).toBeTruthy();
expect(detail!.textContent).toContain(longDetail);
});
});
describe("markdown rendering", () => {
it("renders plain text without markdown correctly", () => {
const entries = [

View File

@@ -191,4 +191,56 @@ describe("useAgentLogs", () => {
expect(mockFetchAgentLogs).not.toHaveBeenCalled();
expect(MockEventSource.instances).toHaveLength(0);
});
it("preserves long text and detail in historical log entries without truncation", async () => {
const longText = "A".repeat(5000);
const longDetail = "B".repeat(5000);
mockFetchAgentLogs.mockResolvedValueOnce([
{ timestamp: "2026-01-01T00:00:00Z", taskId: "FN-001", text: longText, type: "text" as const },
{ timestamp: "2026-01-01T00:00:01Z", taskId: "FN-001", text: "Read", type: "tool" as const, detail: longDetail },
]);
const { result } = renderHook(() => useAgentLogs("FN-001", true));
await waitFor(() => {
expect(result.current.entries).toHaveLength(2);
});
expect(result.current.entries[0].text).toBe(longText);
expect(result.current.entries[0].text.length).toBe(5000);
expect(result.current.entries[1].detail).toBe(longDetail);
expect(result.current.entries[1].detail!.length).toBe(5000);
});
it("preserves long text and detail in live SSE entries without truncation", async () => {
mockFetchAgentLogs.mockResolvedValueOnce([]);
const { result } = renderHook(() => useAgentLogs("FN-001", true));
await waitFor(() => {
expect(MockEventSource.instances).toHaveLength(1);
});
const longText = "X".repeat(5000);
const longDetail = "Y".repeat(5000);
const es = MockEventSource.instances[0];
act(() => {
es._emit("agent:log", {
timestamp: "2026-01-01T00:01:00Z",
taskId: "FN-001",
text: longText,
type: "text",
detail: longDetail,
});
});
await waitFor(() => {
expect(result.current.entries).toHaveLength(1);
});
expect(result.current.entries[0].text).toBe(longText);
expect(result.current.entries[0].text.length).toBe(5000);
expect(result.current.entries[0].detail).toBe(longDetail);
expect(result.current.entries[0].detail!.length).toBe(5000);
});
});

View File

@@ -460,4 +460,58 @@ describe("useMultiAgentLogs", () => {
expect(result.current["FN-001"].entries[1].text).toBe("task1-new");
expect(result.current["FN-002"].entries[1].text).toBe("task2-new");
});
it("preserves long text and detail in historical log entries without truncation", async () => {
const longText = "A".repeat(5000);
const longDetail = "B".repeat(5000);
mockFetchAgentLogs.mockResolvedValue([
{ timestamp: "2026-01-01T00:00:00Z", taskId: "FN-001", text: longText, type: "text" as const },
{ timestamp: "2026-01-01T00:00:01Z", taskId: "FN-001", text: "Read", type: "tool" as const, detail: longDetail },
]);
const { result } = renderHook(() => useMultiAgentLogs(["FN-001"]));
await waitFor(() => {
expect(result.current["FN-001"].entries).toHaveLength(2);
});
expect(result.current["FN-001"].entries[0].text).toBe(longText);
expect(result.current["FN-001"].entries[0].text.length).toBe(5000);
expect(result.current["FN-001"].entries[1].detail).toBe(longDetail);
expect(result.current["FN-001"].entries[1].detail!.length).toBe(5000);
});
it("preserves long text and detail in live SSE entries without truncation", async () => {
mockFetchAgentLogs.mockResolvedValue([]);
const { result } = renderHook(() => useMultiAgentLogs(["FN-001"]));
await waitFor(() => {
expect(MockEventSource.instances.length).toBeGreaterThanOrEqual(1);
});
const es = getConnection("FN-001");
expect(es).toBeDefined();
const longText = "X".repeat(5000);
const longDetail = "Y".repeat(5000);
act(() => {
es!._emit("agent:log", {
timestamp: "2026-01-01T00:01:00Z",
taskId: "FN-001",
text: longText,
type: "text",
detail: longDetail,
});
});
await waitFor(() => {
expect(result.current["FN-001"].entries).toHaveLength(1);
});
expect(result.current["FN-001"].entries[0].text).toBe(longText);
expect(result.current["FN-001"].entries[0].text.length).toBe(5000);
expect(result.current["FN-001"].entries[0].detail).toBe(longDetail);
expect(result.current["FN-001"].entries[0].detail!.length).toBe(5000);
});
});

View File

@@ -4,6 +4,14 @@ import { fetchAgentLogs } from "../api";
export const MAX_LOG_ENTRIES = 500;
/**
* Cap the total number of log entries to `MAX_LOG_ENTRIES`.
*
* This is a **whole-list cap** — it limits how many entries are kept
* in memory, not the content of any individual entry. Per-entry `text`
* and `detail` fields are never truncated anywhere in the pipeline
* (persistence → API → SSE → hook → rendering).
*/
function capLogEntries(entries: AgentLogEntry[]): AgentLogEntry[] {
return entries.length > MAX_LOG_ENTRIES
? entries.slice(-MAX_LOG_ENTRIES)

View File

@@ -4,6 +4,14 @@ import { fetchAgentLogs } from "../api";
export const MAX_LOG_ENTRIES = 500;
/**
* Cap the total number of log entries to `MAX_LOG_ENTRIES`.
*
* This is a **whole-list cap** — it limits how many entries are kept
* in memory, not the content of any individual entry. Per-entry `text`
* and `detail` fields are never truncated anywhere in the pipeline
* (persistence → API → SSE → hook → rendering).
*/
function capLogEntries(entries: AgentLogEntry[]): AgentLogEntry[] {
return entries.length > MAX_LOG_ENTRIES
? entries.slice(-MAX_LOG_ENTRIES)