diff --git a/.prettierrc.json b/.prettierrc.json index 8a0f27e..93520a5 100644 --- a/.prettierrc.json +++ b/.prettierrc.json @@ -3,4 +3,5 @@ "singleQuote": false, "trailingComma": "none", "printWidth": 100 + } diff --git a/apps/agent-client/src/cli.test.ts b/apps/agent-client/src/cli.test.ts index 82e4197..0ee20bd 100644 --- a/apps/agent-client/src/cli.test.ts +++ b/apps/agent-client/src/cli.test.ts @@ -42,6 +42,92 @@ describe("CLI Validation", () => { expect(error.stdout).toContain("Usage:"); } }); + + it("exits with error when query is missing with --receipt flag", async () => { + try { + await execAsync(`${tsx} "${cliPath}" search --receipt`); + expect.fail("Should have failed"); + } catch (error: any) { + expect(error.code).toBe(1); + expect(error.stderr).toContain("Missing query for search mode."); + } + }); + + it("exits with error when query is missing with --json flag", async () => { + try { + await execAsync(`${tsx} "${cliPath}" news --json`); + expect.fail("Should have failed"); + } catch (error: any) { + expect(error.code).toBe(1); + expect(error.stderr).toContain("Missing query for news mode."); + } + }); +}); + +describe("redactInput", () => { + it("returns short inputs unchanged", async () => { + const { redactInput } = await import("./cli.js"); + expect(redactInput("short query")).toBe("short query"); + expect(redactInput("a".repeat(50))).toBe("a".repeat(50)); + }); + + it("truncates long inputs with ellipsis", async () => { + const { redactInput } = await import("./cli.js"); + const long = "a".repeat(100); + expect(redactInput(long)).toBe("a".repeat(47) + "..."); + }); +}); + +describe("buildReceipt", () => { + it("builds correct receipt structure", async () => { + const { buildReceipt } = await import("./cli.js"); + const receipt = buildReceipt({ + mode: "search", + provider: "search.basic", + term: "test query", + price: 0.01, + traceId: "trace_abc123", + }); + + expect(receipt).toEqual({ + command: "search", + provider: "search.basic", + input: "test query", + price: 0.01, + traceId: "trace_abc123", + }); + }); + + it("handles missing price and traceId", async () => { + const { buildReceipt } = await import("./cli.js"); + const receipt = buildReceipt({ + mode: "scrape", + provider: "scrape.page", + term: "https://example.com", + }); + + expect(receipt).toEqual({ + command: "scrape", + provider: "scrape.page", + input: "https://example.com", + price: null, + traceId: null, + }); + }); + + it("redacts long inputs in receipt", async () => { + const { buildReceipt } = await import("./cli.js"); + const long = "a".repeat(100); + const receipt = buildReceipt({ + mode: "search", + provider: "search.basic", + term: long, + price: 0.05, + traceId: "trace_xyz", + }); + + expect(receipt.input).toBe("a".repeat(47) + "..."); + }); }); describe("formatSummary", () => { diff --git a/apps/agent-client/src/cli.ts b/apps/agent-client/src/cli.ts index 0d91d85..7cfbcb0 100644 --- a/apps/agent-client/src/cli.ts +++ b/apps/agent-client/src/cli.ts @@ -42,6 +42,9 @@ function usage() { console.log(' npm run cli -- search "latest soroban updates" --provider search.basic'); console.log(' npm run cli -- news "stablecoin micropayments" --provider news.fast'); console.log(' npm run cli -- scrape "https://example.com" --provider scrape.page'); + console.log("Options:"); + console.log(" --provider Provider ID (default: search.basic / news.fast / scrape.page)"); + console.log(" --receipt Output structured JSON receipt only"); } function readArg(flag: string, args: string[]) { @@ -52,10 +55,36 @@ function readArg(flag: string, args: string[]) { return args[index + 1]; } +function hasFlag(flag: string, args: string[]) { + return args.includes(flag); +} + +export function redactInput(input: string): string { + if (input.length <= 50) return input; + return input.slice(0, 47) + "..."; +} + +export function buildReceipt(input: { + mode: QueryMode; + provider: string; + term: string; + price?: number; + traceId?: string; +}) { + return { + command: input.mode, + provider: input.provider, + input: redactInput(input.term), + price: input.price ?? null, + traceId: input.traceId ?? null, + }; +} + async function main() { const args = process.argv.slice(2); const modeArg = args[0]; const term = args[1]; + const receiptMode = hasFlag("--receipt", args) || hasFlag("--json", args); if (!modeArg || !["search", "news", "scrape"].includes(modeArg)) { usage();