feat(FN-2465): stabilize agent log handling and add OpenClaw adapter

- Make agent log ordering deterministic in core store and dev-server retrieval paths
- Stabilize AgentLogViewer row identity and hook ordering behavior to prevent regressions
- Add targeted tests for store ordering, useAgentLogs hook behavior, and AgentLogViewer rendering
- Introduce executable OpenClaw runtime adapter modules, types, and updated plugin packaging/docs
This commit is contained in:
Fusion
2026-04-24 11:30:27 -07:00
committed by gsxdsm
parent e40430c9bd
commit bafcbd6c39
7 changed files with 201 additions and 67 deletions

View File

@@ -127,6 +127,35 @@ describe("useAgentLogs", () => {
expect(result.current.entries[1].text).toBe("new");
});
it("keeps deterministic chronological order when history has tied timestamps and SSE appends tied timestamps", async () => {
mockFetchAgentLogsWithMeta.mockResolvedValueOnce({
entries: [
{ timestamp: "2026-01-01T00:00:00Z", taskId: "FN-001", text: "hist-1", type: "text" as const },
{ timestamp: "2026-01-01T00:00:00Z", taskId: "FN-001", text: "hist-2", type: "text" as const },
],
total: 3,
hasMore: false,
});
const { result } = renderHook(() => useAgentLogs("FN-001", true));
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.text)).toEqual(["hist-1", "hist-2"]);
});
const es = MockEventSource.instances[0];
act(() => {
es._emit("agent:log", {
timestamp: "2026-01-01T00:00:00Z",
taskId: "FN-001",
text: "live-3",
type: "text",
});
});
expect(result.current.entries.map((entry) => entry.text)).toEqual(["hist-1", "hist-2", "live-3"]);
});
it("closes SSE when enabled changes to false", async () => {
mockFetchAgentLogsWithMeta.mockResolvedValueOnce({ entries: [], total: 0, hasMore: false });
@@ -309,6 +338,38 @@ describe("useAgentLogs", () => {
expect(result.current.hasMore).toBe(false);
});
it("loadMore preserves chronological order with near-equal timestamps", async () => {
const initialLogs = [
{ timestamp: "2026-01-01T00:00:01.001Z", taskId: "FN-001", text: "middle", type: "text" as const },
{ timestamp: "2026-01-01T00:00:01.002Z", taskId: "FN-001", text: "newest", type: "text" as const },
];
const olderLogs = [
{ timestamp: "2026-01-01T00:00:00.999Z", taskId: "FN-001", text: "oldest-a", type: "text" as const },
{ timestamp: "2026-01-01T00:00:00.999Z", taskId: "FN-001", text: "oldest-b", type: "text" as const },
];
mockFetchAgentLogsWithMeta
.mockResolvedValueOnce({ entries: initialLogs, total: 4, hasMore: true })
.mockResolvedValueOnce({ entries: olderLogs, total: 4, hasMore: false });
const { result } = renderHook(() => useAgentLogs("FN-001", true));
await waitFor(() => {
expect(result.current.entries.map((entry) => entry.text)).toEqual(["middle", "newest"]);
});
await act(async () => {
await result.current.loadMore();
});
expect(result.current.entries.map((entry) => entry.text)).toEqual([
"oldest-a",
"oldest-b",
"middle",
"newest",
]);
});
it("loadMore does not trigger when already loading more", async () => {
mockFetchAgentLogsWithMeta.mockResolvedValue({ entries: [], total: 200, hasMore: true });