feat(FN-4400): complete Step 7 — add prompt-size api and dashboard sparkline
Fusion-Task-Id: FN-4400 Fusion-Task-Lineage: 0d4f9c48-5b8b-49eb-9cf3-d1d585ffe7fc
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { get } from "../test-request.js";
|
||||
import { createServer } from "../server.js";
|
||||
|
||||
const {
|
||||
mockInit,
|
||||
mockGetAgent,
|
||||
mockIsEphemeralAgent,
|
||||
mockChatStoreInit,
|
||||
mockAll,
|
||||
} = vi.hoisted(() => ({
|
||||
mockInit: vi.fn().mockResolvedValue(undefined),
|
||||
mockGetAgent: vi.fn(),
|
||||
mockIsEphemeralAgent: vi.fn(),
|
||||
mockChatStoreInit: vi.fn().mockResolvedValue(undefined),
|
||||
mockAll: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@fusion/core", () => ({
|
||||
AgentStore: class MockAgentStore {
|
||||
init = mockInit;
|
||||
getAgent = mockGetAgent;
|
||||
},
|
||||
isEphemeralAgent: mockIsEphemeralAgent,
|
||||
ChatStore: class MockChatStore {
|
||||
init = mockChatStoreInit;
|
||||
},
|
||||
}));
|
||||
|
||||
class MockStore extends EventEmitter {
|
||||
getRootDir(): string {
|
||||
return "/tmp/fn-4400-test";
|
||||
}
|
||||
|
||||
getFusionDir(): string {
|
||||
return "/tmp/fn-4400-test/.fusion";
|
||||
}
|
||||
|
||||
getDatabase() {
|
||||
return {
|
||||
exec: vi.fn(),
|
||||
prepare: vi.fn().mockReturnValue({
|
||||
run: vi.fn().mockReturnValue({ changes: 0 }),
|
||||
get: vi.fn(),
|
||||
all: mockAll,
|
||||
}),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
describe("GET /api/agents/:id/prompt-sizes", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
mockGetAgent.mockResolvedValue({ id: "agent-001", role: "executor", metadata: {} });
|
||||
mockIsEphemeralAgent.mockReturnValue(false);
|
||||
mockAll.mockReturnValue([
|
||||
{
|
||||
runId: "run-1",
|
||||
createdAt: "2026-05-14T00:00:00.000Z",
|
||||
systemChars: 120,
|
||||
execChars: 880,
|
||||
totalChars: 1000,
|
||||
},
|
||||
{
|
||||
runId: "run-2",
|
||||
createdAt: "2026-05-13T23:00:00.000Z",
|
||||
systemChars: 0,
|
||||
execChars: 0,
|
||||
totalChars: 0,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns recent prompt size rows", async () => {
|
||||
const app = createServer(new MockStore() as any);
|
||||
const res = await get(app, "/api/agents/agent-001/prompt-sizes");
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([
|
||||
expect.objectContaining({ runId: "run-1", totalChars: 1000 }),
|
||||
expect.objectContaining({ runId: "run-2", systemChars: 0, execChars: 0, totalChars: 0 }),
|
||||
]);
|
||||
});
|
||||
|
||||
it("returns 404 when agent is missing", async () => {
|
||||
mockGetAgent.mockResolvedValueOnce(null);
|
||||
const app = createServer(new MockStore() as any);
|
||||
const res = await get(app, "/api/agents/missing/prompt-sizes");
|
||||
expect(res.status).toBe(404);
|
||||
});
|
||||
|
||||
it("returns 400 for ephemeral agents", async () => {
|
||||
mockIsEphemeralAgent.mockReturnValueOnce(true);
|
||||
const app = createServer(new MockStore() as any);
|
||||
const res = await get(app, "/api/agents/agent-001/prompt-sizes");
|
||||
expect(res.status).toBe(400);
|
||||
});
|
||||
});
|
||||
@@ -631,6 +631,64 @@ export function registerAgentRuntimeRoutes(ctx: ApiRoutesContext, deps: AgentRun
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/agents/:id/prompt-sizes
|
||||
* Get recent prompt-size points for a permanent agent.
|
||||
*/
|
||||
router.get("/agents/:id/prompt-sizes", async (req, res) => {
|
||||
try {
|
||||
const { store: scopedStore } = await getProjectContext(req);
|
||||
const { AgentStore, isEphemeralAgent } = await import("@fusion/core");
|
||||
const agentStore = new AgentStore({ rootDir: scopedStore.getFusionDir() });
|
||||
await agentStore.init();
|
||||
|
||||
const agent = await agentStore.getAgent(req.params.id);
|
||||
if (!agent) {
|
||||
throw notFound("Agent not found");
|
||||
}
|
||||
if (isEphemeralAgent(agent)) {
|
||||
throw badRequest("Prompt sizes are not available for ephemeral agents");
|
||||
}
|
||||
|
||||
const rawLimit = typeof req.query.limit === "string" ? Number.parseInt(req.query.limit, 10) : 7;
|
||||
if (!Number.isInteger(rawLimit) || rawLimit <= 0) {
|
||||
throw badRequest("limit must be a positive integer");
|
||||
}
|
||||
const limit = Math.min(rawLimit, 30);
|
||||
|
||||
const rows = scopedStore.getDatabase().prepare(`
|
||||
SELECT
|
||||
id AS runId,
|
||||
createdAt,
|
||||
COALESCE(length(json_extract(data, '$.systemPrompt')), 0) AS systemChars,
|
||||
COALESCE(length(json_extract(data, '$.executionPrompt')), 0) AS execChars,
|
||||
COALESCE(length(json_extract(data, '$.systemPrompt')), 0)
|
||||
+ COALESCE(length(json_extract(data, '$.executionPrompt')), 0) AS totalChars
|
||||
FROM agentRuns
|
||||
WHERE json_extract(data, '$.agentId') = ?
|
||||
ORDER BY createdAt DESC
|
||||
LIMIT ?
|
||||
`).all(req.params.id, limit) as Array<{
|
||||
runId: string;
|
||||
createdAt: string;
|
||||
systemChars: number;
|
||||
execChars: number;
|
||||
totalChars: number;
|
||||
}>;
|
||||
|
||||
res.json(rows);
|
||||
} catch (err: unknown) {
|
||||
if (err instanceof ApiError) {
|
||||
throw err;
|
||||
}
|
||||
if ((err instanceof Error ? err.message : String(err)).includes("not found")) {
|
||||
throw notFound(err instanceof Error ? err.message : String(err));
|
||||
} else {
|
||||
rethrowAsApiError(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/agents/:id/token-usage
|
||||
* Get cache/token usage summary windows for a permanent agent.
|
||||
|
||||
Reference in New Issue
Block a user