Skip to content

Commit 7e51f80

Browse files
authored
Merge pull request #25 from wangjk9527/feat/production-password-auth
Fix tool result display normalization
2 parents 342ec93 + 96aba76 commit 7e51f80

17 files changed

Lines changed: 1702 additions & 228 deletions

apps/web/src/app/data-tasks/__tests__/conversation-restore.test.ts

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
import { ConfigApiError } from "../../../lib/config-api/types";
1919
import {
2020
createInitialLiveRun,
21+
formatWorkspaceMetadataSummary,
2122
reconcileLiveRunArtifacts,
2223
reduceLiveRunEvent,
2324
} from "../live-run-state";
@@ -1567,6 +1568,177 @@ describe("replayRestorableCustomEvents", () => {
15671568
expect(run.audits).toHaveLength(1);
15681569
expect(run.tokenUsageEvents).toHaveLength(1);
15691570
});
1571+
1572+
it("replays workspace and sandbox custom events scoped to the active run", () => {
1573+
let run = reduceLiveRunEvent(createInitialLiveRun(), {
1574+
type: "RUN_STARTED",
1575+
runId: "run-1",
1576+
});
1577+
const dto: SessionConversationDto = {
1578+
sessionId: "thread-1",
1579+
messages: [],
1580+
runEventRefs: [],
1581+
toolCalls: [],
1582+
restorableCustomEvents: [
1583+
{
1584+
runId: "run-1",
1585+
seq: 1,
1586+
name: "workspace.metadata",
1587+
value: { toolCallId: "tc-write", toolName: "write_file", path: "reports/summary.md" },
1588+
},
1589+
{
1590+
runId: "run-2",
1591+
seq: 2,
1592+
name: "workspace.metadata",
1593+
value: { toolCallId: "tc-other", toolName: "write_file", path: "other.md" },
1594+
},
1595+
{
1596+
runId: "run-1",
1597+
seq: 3,
1598+
name: "sandbox.output",
1599+
value: { kind: "stdout", text: "verify-ok\n" },
1600+
},
1601+
],
1602+
};
1603+
1604+
run = replayRestorableCustomEvents(run, dto);
1605+
expect(run.workspaceMetadata).toHaveLength(1);
1606+
expect(run.workspaceMetadata[0]).toMatchObject({
1607+
toolCallId: "tc-write",
1608+
toolName: "write_file",
1609+
});
1610+
expect(run.sandboxOutputs).toHaveLength(1);
1611+
});
1612+
});
1613+
1614+
describe("hydrateLiveRunFromConversation workspace signals", () => {
1615+
it("derives workspace metadata from restored write_file tool calls", () => {
1616+
const dto: SessionConversationDto = {
1617+
sessionId: "thread-workspace",
1618+
messages: [],
1619+
runEventRefs: [],
1620+
toolCalls: [
1621+
{
1622+
runId: "run-1",
1623+
toolCallId: "tc-write",
1624+
status: "completed",
1625+
toolName: "write_file",
1626+
callEventSeq: 1,
1627+
resultPreview: "Wrote 128 bytes to reports/summary.md",
1628+
},
1629+
],
1630+
};
1631+
1632+
const run = hydrateLiveRunFromConversation(createInitialLiveRun(), dto);
1633+
expect(run.workspaceMetadata).toHaveLength(1);
1634+
expect(run.workspaceMetadata[0]).toMatchObject({
1635+
toolCallId: "tc-write",
1636+
toolName: "write_file",
1637+
});
1638+
expect(formatWorkspaceMetadataSummary(run.workspaceMetadata[0]!)).toContain(
1639+
"reports/summary.md",
1640+
);
1641+
});
1642+
1643+
it("keeps persisted workspace signals from earlier restored run segments", () => {
1644+
const dto: SessionConversationDto = {
1645+
sessionId: "thread-workspace-history",
1646+
messages: [],
1647+
runEventRefs: [],
1648+
toolCalls: [
1649+
{
1650+
runId: "run-1",
1651+
toolCallId: "tc-write-1",
1652+
status: "completed",
1653+
toolName: "write_file",
1654+
callEventSeq: 1,
1655+
resultPreview: "Workspace write completed.",
1656+
},
1657+
{
1658+
runId: "run-1",
1659+
toolCallId: "tc-ask",
1660+
status: "pending",
1661+
toolName: "ask_user",
1662+
callEventSeq: 2,
1663+
},
1664+
{
1665+
runId: "run-2",
1666+
toolCallId: "tc-write-2",
1667+
status: "completed",
1668+
toolName: "write_file",
1669+
callEventSeq: 3,
1670+
resultPreview: "Wrote 128 bytes to reports/second.md",
1671+
},
1672+
],
1673+
restorableCustomEvents: [
1674+
{
1675+
runId: "run-1",
1676+
seq: 1,
1677+
name: "workspace.metadata",
1678+
value: {
1679+
toolCallId: "tc-write-1",
1680+
toolName: "write_file",
1681+
path: "reports/from-custom-first.md",
1682+
},
1683+
},
1684+
{
1685+
runId: "run-2",
1686+
seq: 2,
1687+
name: "workspace.metadata",
1688+
value: {
1689+
toolCallId: "tc-write-2",
1690+
toolName: "write_file",
1691+
path: "reports/second.md",
1692+
},
1693+
},
1694+
],
1695+
};
1696+
1697+
const run = hydrateLiveRunFromConversation(createInitialLiveRun(), dto);
1698+
expect(
1699+
run.workspaceMetadata
1700+
.map((entry) => ({
1701+
toolCallId: entry.toolCallId,
1702+
path: (entry.payload as { path?: string }).path,
1703+
}))
1704+
.sort((left, right) => String(left.toolCallId).localeCompare(String(right.toolCallId))),
1705+
).toEqual([
1706+
{ toolCallId: "tc-write-1", path: "reports/from-custom-first.md" },
1707+
{ toolCallId: "tc-write-2", path: "reports/second.md" },
1708+
]);
1709+
});
1710+
1711+
it("derives sandbox outputs from every restored execute_command tool call", () => {
1712+
const dto: SessionConversationDto = {
1713+
sessionId: "thread-sandbox-history",
1714+
messages: [],
1715+
runEventRefs: [],
1716+
toolCalls: [
1717+
{
1718+
runId: "run-1",
1719+
toolCallId: "tc-command-1",
1720+
status: "completed",
1721+
toolName: "execute_command",
1722+
callEventSeq: 1,
1723+
resultPreview: "first-ok\n",
1724+
},
1725+
{
1726+
runId: "run-1",
1727+
toolCallId: "tc-command-2",
1728+
status: "completed",
1729+
toolName: "execute_command",
1730+
callEventSeq: 2,
1731+
resultPreview: "second-ok\n",
1732+
},
1733+
],
1734+
};
1735+
1736+
const run = hydrateLiveRunFromConversation(createInitialLiveRun(), dto);
1737+
expect(run.sandboxOutputs.map((output) => output.payload)).toEqual([
1738+
{ kind: "stdout", text: "first-ok" },
1739+
{ kind: "stdout", text: "second-ok" },
1740+
]);
1741+
});
15701742
});
15711743

15721744
describe("hydrateSessionUsageFromConversation", () => {
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
import React from "react";
2+
import { readFileSync } from "node:fs";
3+
import { describe, expect, it } from "vitest";
4+
import { renderToStaticMarkup } from "react-dom/server";
5+
6+
import { buildTraceTimeline } from "../trace-timeline";
7+
import { createInitialLiveRun, reduceLiveRunEvent } from "../live-run-state";
8+
import {
9+
parseSchemaToolResult,
10+
parseSqlToolResult,
11+
} from "../tool-result-normalize";
12+
import {
13+
ToolFailureResult,
14+
ToolFormattedResult,
15+
} from "../tool-result-format";
16+
import {
17+
resolveToolFailurePresentation,
18+
toolResultLooksLikeError,
19+
} from "../tool-call-display";
20+
import { toolResultFixtures } from "../tool-display-fixtures";
21+
22+
const renderTool = (toolName: string, result: unknown, variant: "chat" | "console" = "console") =>
23+
renderToStaticMarkup(
24+
ToolFormattedResult({ toolName, result, variant, showRawFallback: false }),
25+
);
26+
27+
describe("observation-wrapped chat vs console parity", () => {
28+
const wrappedCases = [
29+
{
30+
tool: "list_data_sources",
31+
result: toolResultFixtures.list_data_sources.observationWrapped,
32+
expectText: "API DuckDB Demo",
33+
},
34+
{
35+
tool: "inspect_schema",
36+
result: toolResultFixtures.inspect_schema.observationWrapped,
37+
expectText: "orders",
38+
},
39+
{
40+
tool: "run_sql_readonly",
41+
result: toolResultFixtures.run_sql_readonly.observationWrapped,
42+
expectText: "128",
43+
},
44+
] as const;
45+
46+
it.each(wrappedCases)("renders wrapped $tool in chat and console", ({ tool, result, expectText }) => {
47+
expect(renderTool(tool, result, "chat")).toContain(expectText);
48+
expect(renderTool(tool, result, "console")).toContain(expectText);
49+
});
50+
51+
it("parses stringified payloads for chat card data paths", () => {
52+
expect(
53+
parseSqlToolResult(JSON.stringify(toolResultFixtures.run_sql_readonly.observationWrapped)),
54+
).toMatchObject({ columns: ["total_orders"], row_count: 1 });
55+
expect(
56+
parseSchemaToolResult(JSON.stringify(toolResultFixtures.inspect_schema.observationWrapped)),
57+
).toMatchObject({ tables: [{ name: "orders" }] });
58+
});
59+
});
60+
61+
describe("failure presentation parity", () => {
62+
it("recognizes protocol and delivery errors", () => {
63+
const protocol = JSON.stringify(toolResultFixtures.protocolError);
64+
const delivery = JSON.stringify(toolResultFixtures.toolNotDelivered);
65+
expect(toolResultLooksLikeError(protocol)).toBe(true);
66+
expect(toolResultLooksLikeError(delivery)).toBe(true);
67+
expect(resolveToolFailurePresentation(protocol).title).toBe("Result sync failed");
68+
expect(resolveToolFailurePresentation(delivery).title).toBe("Result not delivered");
69+
});
70+
71+
it("renders shared failure UI for console and chat paths", () => {
72+
const protocol = JSON.stringify(toolResultFixtures.protocolError);
73+
const markup = renderToStaticMarkup(
74+
ToolFailureResult({ toolName: "run_sql_readonly", result: protocol }),
75+
);
76+
expect(markup).toContain("Result sync failed");
77+
expect(markup).toContain("run_sql_readonly");
78+
expect(markup).not.toContain('"status": "error"');
79+
});
80+
});
81+
82+
describe("empty and malformed tool payloads", () => {
83+
it("shows a zero-row message for empty SQL results", () => {
84+
const markup = renderTool("run_sql_readonly", toolResultFixtures.run_sql_readonly.emptyRows);
85+
expect(markup).toContain("The query returned no rows.");
86+
expect(markup).toContain("Rows");
87+
});
88+
89+
it("keeps the middle-column SQL card on the shared result renderer", () => {
90+
const pageSource = readFileSync(new URL("../page.tsx", import.meta.url), "utf8");
91+
const sqlCardSource = pageSource.slice(
92+
pageSource.indexOf("function SqlToolCard"),
93+
pageSource.indexOf("type SchemaColumn"),
94+
);
95+
expect(sqlCardSource).toContain("<ToolFormattedResult");
96+
expect(sqlCardSource).toContain("toolName={name}");
97+
expect(sqlCardSource).toContain('variant="chat"');
98+
});
99+
100+
it("returns null parsers for malformed schema payloads", () => {
101+
expect(parseSchemaToolResult(JSON.stringify({ datasource_id: "x" }))).toBeNull();
102+
});
103+
});
104+
105+
describe("trace-aligned tool result rendering", () => {
106+
it("formats workspace tool raw results the same way trace cards should", () => {
107+
const markup = renderToStaticMarkup(
108+
ToolFormattedResult({
109+
toolName: "write_file",
110+
result: toolResultFixtures.write_file,
111+
variant: "console",
112+
showRawFallback: false,
113+
}),
114+
);
115+
expect(markup).toContain("reports/summary.md");
116+
});
117+
118+
it("uses shared failure UI for errored trace payloads", () => {
119+
const protocol = JSON.stringify(toolResultFixtures.protocolError);
120+
const markup = renderToStaticMarkup(
121+
ToolFailureResult({ toolName: "run_sql_readonly", result: protocol }),
122+
);
123+
expect(markup).toContain("Result sync failed");
124+
});
125+
126+
it("shows schema chips from wrapped restore payloads in timeline data", () => {
127+
let run = reduceLiveRunEvent(createInitialLiveRun(), { type: "RUN_STARTED" });
128+
run = reduceLiveRunEvent(run, {
129+
type: "TOOL_CALL_START",
130+
toolCallId: "tc-schema-wrap",
131+
toolCallName: "inspect_schema",
132+
});
133+
run = reduceLiveRunEvent(run, {
134+
type: "TOOL_CALL_RESULT",
135+
toolCallId: "tc-schema-wrap",
136+
toolCallName: "inspect_schema",
137+
result: JSON.stringify(toolResultFixtures.inspect_schema.observationWrapped),
138+
});
139+
run = reduceLiveRunEvent(run, { type: "RUN_FINISHED" });
140+
141+
const entry = buildTraceTimeline(run).find((item) => item.toolCallId === "tc-schema-wrap");
142+
expect(entry?.schemaTables?.[0]?.name).toBe("orders");
143+
});
144+
});

0 commit comments

Comments
 (0)