diff --git a/.agents/skills/clawhub-content-rights-correspondence/SKILL.md b/.agents/skills/clawhub-content-rights-correspondence/SKILL.md index ccae4542..644486e6 100644 --- a/.agents/skills/clawhub-content-rights-correspondence/SKILL.md +++ b/.agents/skills/clawhub-content-rights-correspondence/SKILL.md @@ -94,8 +94,21 @@ bun run admin -- email send \ --body-file /tmp/body.txt ``` -Then, after signoff and successful send, record the exact body with the -provider id: +After explicit signoff, send: + +```bash +bun run admin -- email send \ + --to requester@example.com \ + --username Requester \ + --subject "Update on ClawHub content rights request" \ + --body-file /tmp/body.txt \ + --send \ + --confirm-user-request \ + --confirm-user-signoff \ + --json +``` + +Then record the successful send with the provider id: ```bash bun run admin -- content-rights record-correspondence CHR-000007 \ diff --git a/convex/skillStatEvents.test.ts b/convex/skillStatEvents.test.ts index 002435dd..b3921461 100644 --- a/convex/skillStatEvents.test.ts +++ b/convex/skillStatEvents.test.ts @@ -122,7 +122,7 @@ describe("skill stat events - comment delta handling", () => { ); }); - it("floors action drain batch size so stale small continuations do not crawl", async () => { + it("bounds action drain work so stale continuations do not crawl or time out", async () => { const runMutation = vi.fn(async (_ref: unknown, args: Record) => { if ("leaseMs" in args) { return { @@ -145,15 +145,22 @@ describe("skill stat events - comment delta handling", () => { await expect( processSkillStatEventsInternalHandler( { runMutation, scheduler }, - { batchSize: 10, maxBatches: 1 }, + { batchSize: 10, maxBatches: 100 }, ), ).resolves.toMatchObject({ - processed: 100, + processed: 500, scheduledContinuation: true, }); - expect(runMutation.mock.calls[1]?.[1]).toMatchObject({ batchSize: 100 }); - expect(scheduler.runAfter.mock.calls[0]?.[2]).toMatchObject({ batchSize: 100 }); + const batchCalls = runMutation.mock.calls.filter(([, args]) => { + return args && typeof args === "object" && "leaseOwner" in args && "batchSize" in args; + }); + expect(batchCalls).toHaveLength(5); + expect(batchCalls[0]?.[1]).toMatchObject({ batchSize: 100 }); + expect(scheduler.runAfter.mock.calls[0]?.[2]).toMatchObject({ + batchSize: 100, + maxBatches: 5, + }); }); it("aggregates comment and uncomment events into net deltas", () => { diff --git a/convex/skillStatEvents.ts b/convex/skillStatEvents.ts index ef6eff8f..da4c962d 100644 --- a/convex/skillStatEvents.ts +++ b/convex/skillStatEvents.ts @@ -182,8 +182,8 @@ const DOC_SYNC_LEASE_KEY = "skill_doc_stat_sync"; const DOC_SYNC_LEASE_MS = 2 * 60 * 1_000; const DEFAULT_DOC_SYNC_BATCH_SIZE = 100; const MAX_DOC_SYNC_BATCH_SIZE = 100; -const DEFAULT_DOC_SYNC_MAX_BATCHES = 20; -const MAX_DOC_SYNC_MAX_BATCHES = 100; +const DEFAULT_DOC_SYNC_MAX_BATCHES = 5; +const MAX_DOC_SYNC_MAX_BATCHES = 5; type ClaimSkillStatDocSyncLeaseResult = | { diff --git a/tests/clawhub-content-rights-correspondence.test.ts b/tests/clawhub-content-rights-correspondence.test.ts index b148acb8..1dd13510 100644 --- a/tests/clawhub-content-rights-correspondence.test.ts +++ b/tests/clawhub-content-rights-correspondence.test.ts @@ -1,118 +1,37 @@ -import { describe, expect, it, vi } from "vitest"; -import { - buildAdminEmailArgs, - runCorrespondence, -} from "../.agents/skills/clawhub-content-rights-correspondence/scripts/send-correspondence.js"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +const skill = readFileSync( + join(process.cwd(), ".agents/skills/clawhub-content-rights-correspondence/SKILL.md"), + "utf8", +); + +const section = (heading: string) => { + const start = skill.indexOf(`## ${heading}`); + expect(start).toBeGreaterThanOrEqual(0); + const next = skill.indexOf("\n## ", start + 1); + return skill.slice(start, next === -1 ? undefined : next); +}; describe("ClawHub content rights correspondence skill", () => { - it("builds a dry-run admin email command by default", () => { - expect( - buildAdminEmailArgs({ - to: "legal@example.com", - subject: "Re: CHR-000007", - bodyFile: "/tmp/body.txt", - send: false, - confirmUserSignoff: false, - }), - ).toEqual([ - "run", - "admin", - "--", - "email", - "send", - "--to", - "legal@example.com", - "--subject", - "Re: CHR-000007", - "--body-file", - "/tmp/body.txt", - "--json", - ]); + it("uses direct admin CLI commands instead of the removed helper script", () => { + expect(skill).toMatch(/Do not use helper\s+scripts/); + expect(skill).not.toContain("send-correspondence"); + expect(skill).not.toContain("scripts/"); + expect(skill).toContain("bun run admin -- email send"); }); - it("refuses to send without explicit user signoff", async () => { - await expect( - runCorrespondence( - { - caseId: "CHR-000007", - subject: "Re: CHR-000007", - bodyFile: "/tmp/body.txt", - attachments: [], - send: true, - confirmUserSignoff: false, - }, - { - exec: vi.fn(), - }, - ), - ).rejects.toThrow("Sending requires --confirm-user-signoff."); + it("keeps the explicit send-signoff guard in the requester status update send command", () => { + const replies = section("Requester Status Updates"); + expect(replies).toContain("--send"); + expect(replies).toContain("--confirm-user-request"); + expect(replies).toContain("--confirm-user-signoff"); }); - it("sends to the existing case email and records exact correspondence through Hermit", async () => { - const execMock = vi - .fn() - .mockResolvedValueOnce({ - stdout: JSON.stringify({ - case: { caseId: "CHR-000007", email: "legal@example.com" }, - files: [], - events: [], - }), - stderr: "", - exitCode: 0, - }) - .mockResolvedValueOnce({ - stdout: JSON.stringify({ ok: true, providerId: "email-123" }), - stderr: "", - exitCode: 0, - }) - .mockResolvedValueOnce({ - stdout: JSON.stringify({ ok: true, caseId: "CHR-000007", storedFiles: 2 }), - stderr: "", - exitCode: 0, - }); - - await runCorrespondence( - { - caseId: "CHR-000007", - subject: "Re: CHR-000007", - bodyFile: "/tmp/body.txt", - attachments: ["/tmp/response.pdf"], - send: true, - confirmUserSignoff: true, - }, - { - exec: execMock, - }, - ); - - expect(execMock).toHaveBeenNthCalledWith(1, [ - "run", - "admin", - "--", - "content-rights", - "get", - "CHR-000007", - "--json", - ]); - expect(execMock).toHaveBeenNthCalledWith( - 2, - expect.arrayContaining(["--to", "legal@example.com", "--send", "--confirm-user-signoff"]), - ); - expect(execMock).toHaveBeenNthCalledWith( - 3, - expect.arrayContaining([ - "content-rights", - "record-correspondence", - "CHR-000007", - "--to", - "legal@example.com", - "--body-file", - "/tmp/body.txt", - "--provider-message-id", - "email-123", - "--attachment", - "/tmp/response.pdf", - ]), - ); + it("documents preserving outbound correspondence through the admin CLI", () => { + expect(skill).toContain("content-rights record-correspondence"); + expect(skill).toContain("--provider-message-id"); + expect(skill).toContain("--attachment"); }); });