Spaces:
Paused
Paused
| import os from "node:os"; | |
| import path from "node:path"; | |
| import { mkdtemp, readFile } from "node:fs/promises"; | |
| import { Command } from "commander"; | |
| import { describe, expect, it } from "vitest"; | |
| import type { FeedbackTrace } from "@paperclipai/shared"; | |
| import { readZipArchive } from "../commands/client/zip.js"; | |
| import { | |
| buildFeedbackTraceQuery, | |
| registerFeedbackCommands, | |
| renderFeedbackReport, | |
| summarizeFeedbackTraces, | |
| writeFeedbackExportBundle, | |
| } from "../commands/client/feedback.js"; | |
| function makeTrace(overrides: Partial<FeedbackTrace> = {}): FeedbackTrace { | |
| return { | |
| id: "trace-12345678", | |
| companyId: "company-123", | |
| feedbackVoteId: "vote-12345678", | |
| issueId: "issue-123", | |
| projectId: "project-123", | |
| issueIdentifier: "PAP-123", | |
| issueTitle: "Fix the feedback command", | |
| authorUserId: "user-123", | |
| targetType: "issue_comment", | |
| targetId: "comment-123", | |
| vote: "down", | |
| status: "pending", | |
| destination: "paperclip_labs_feedback_v1", | |
| exportId: null, | |
| consentVersion: "feedback-data-sharing-v1", | |
| schemaVersion: "1", | |
| bundleVersion: "1", | |
| payloadVersion: "1", | |
| payloadDigest: null, | |
| payloadSnapshot: { | |
| vote: { | |
| value: "down", | |
| reason: "Needed more detail", | |
| }, | |
| }, | |
| targetSummary: { | |
| label: "Comment", | |
| excerpt: "The first answer was too vague.", | |
| authorAgentId: "agent-123", | |
| authorUserId: null, | |
| createdAt: new Date("2026-03-31T12:00:00.000Z"), | |
| documentKey: null, | |
| documentTitle: null, | |
| revisionNumber: null, | |
| }, | |
| redactionSummary: null, | |
| attemptCount: 0, | |
| lastAttemptedAt: null, | |
| exportedAt: null, | |
| failureReason: null, | |
| createdAt: new Date("2026-03-31T12:01:00.000Z"), | |
| updatedAt: new Date("2026-03-31T12:02:00.000Z"), | |
| ...overrides, | |
| }; | |
| } | |
| describe("registerFeedbackCommands", () => { | |
| it("registers the top-level feedback commands", () => { | |
| const program = new Command(); | |
| expect(() => registerFeedbackCommands(program)).not.toThrow(); | |
| const feedback = program.commands.find((command) => command.name() === "feedback"); | |
| expect(feedback).toBeDefined(); | |
| expect(feedback?.commands.map((command) => command.name())).toEqual(["report", "export"]); | |
| expect(feedback?.commands[0]?.options.filter((option) => option.long === "--company-id")).toHaveLength(1); | |
| }); | |
| }); | |
| describe("buildFeedbackTraceQuery", () => { | |
| it("encodes all supported filters", () => { | |
| expect( | |
| buildFeedbackTraceQuery({ | |
| targetType: "issue_comment", | |
| vote: "down", | |
| status: "pending", | |
| projectId: "project-123", | |
| issueId: "issue-123", | |
| from: "2026-03-31T00:00:00.000Z", | |
| to: "2026-03-31T23:59:59.999Z", | |
| sharedOnly: true, | |
| }), | |
| ).toBe( | |
| "?targetType=issue_comment&vote=down&status=pending&projectId=project-123&issueId=issue-123&from=2026-03-31T00%3A00%3A00.000Z&to=2026-03-31T23%3A59%3A59.999Z&sharedOnly=true&includePayload=true", | |
| ); | |
| }); | |
| }); | |
| describe("renderFeedbackReport", () => { | |
| it("includes summary counts and the optional reason", () => { | |
| const traces = [ | |
| makeTrace(), | |
| makeTrace({ | |
| id: "trace-87654321", | |
| feedbackVoteId: "vote-87654321", | |
| vote: "up", | |
| status: "local_only", | |
| payloadSnapshot: { | |
| vote: { | |
| value: "up", | |
| reason: null, | |
| }, | |
| }, | |
| }), | |
| ]; | |
| const report = renderFeedbackReport({ | |
| apiBase: "http://127.0.0.1:3100", | |
| companyId: "company-123", | |
| traces, | |
| summary: summarizeFeedbackTraces(traces), | |
| includePayloads: false, | |
| }); | |
| expect(report).toContain("Paperclip Feedback Report"); | |
| expect(report).toContain("thumbs up"); | |
| expect(report).toContain("thumbs down"); | |
| expect(report).toContain("Needed more detail"); | |
| }); | |
| }); | |
| describe("writeFeedbackExportBundle", () => { | |
| it("writes votes, traces, a manifest, and a zip archive", async () => { | |
| const tempDir = await mkdtemp(path.join(os.tmpdir(), "paperclip-feedback-export-")); | |
| const outputDir = path.join(tempDir, "feedback-export"); | |
| const traces = [ | |
| makeTrace(), | |
| makeTrace({ | |
| id: "trace-abcdef12", | |
| feedbackVoteId: "vote-abcdef12", | |
| issueIdentifier: "PAP-124", | |
| issueId: "issue-124", | |
| vote: "up", | |
| status: "local_only", | |
| payloadSnapshot: { | |
| vote: { | |
| value: "up", | |
| reason: null, | |
| }, | |
| }, | |
| }), | |
| ]; | |
| const exported = await writeFeedbackExportBundle({ | |
| apiBase: "http://127.0.0.1:3100", | |
| companyId: "company-123", | |
| traces, | |
| outputDir, | |
| }); | |
| expect(exported.manifest.summary.total).toBe(2); | |
| expect(exported.manifest.summary.withReason).toBe(1); | |
| const manifest = JSON.parse(await readFile(path.join(outputDir, "index.json"), "utf8")) as { | |
| files: { votes: string[]; traces: string[]; zip: string }; | |
| }; | |
| expect(manifest.files.votes).toHaveLength(2); | |
| expect(manifest.files.traces).toHaveLength(2); | |
| const archive = await readFile(exported.zipPath); | |
| const zip = await readZipArchive(archive); | |
| expect(Object.keys(zip.files)).toEqual( | |
| expect.arrayContaining([ | |
| "index.json", | |
| `votes/${manifest.files.votes[0]}`, | |
| `traces/${manifest.files.traces[0]}`, | |
| ]), | |
| ); | |
| }); | |
| }); | |