mirror of
https://github.com/openclaw/clawhub.git
synced 2026-08-14 00:47:57 +00:00
Cap skill stat drain action batches (#2671)
* fix: cap skill stat drain action batches * test: align content rights correspondence guard
This commit is contained in:
@@ -94,8 +94,21 @@ bun run admin -- email send \
|
|||||||
--body-file /tmp/body.txt
|
--body-file /tmp/body.txt
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, after signoff and successful send, record the exact body with the
|
After explicit signoff, send:
|
||||||
provider id:
|
|
||||||
|
```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
|
```bash
|
||||||
bun run admin -- content-rights record-correspondence CHR-000007 \
|
bun run admin -- content-rights record-correspondence CHR-000007 \
|
||||||
|
|||||||
@@ -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<string, unknown>) => {
|
const runMutation = vi.fn(async (_ref: unknown, args: Record<string, unknown>) => {
|
||||||
if ("leaseMs" in args) {
|
if ("leaseMs" in args) {
|
||||||
return {
|
return {
|
||||||
@@ -145,15 +145,22 @@ describe("skill stat events - comment delta handling", () => {
|
|||||||
await expect(
|
await expect(
|
||||||
processSkillStatEventsInternalHandler(
|
processSkillStatEventsInternalHandler(
|
||||||
{ runMutation, scheduler },
|
{ runMutation, scheduler },
|
||||||
{ batchSize: 10, maxBatches: 1 },
|
{ batchSize: 10, maxBatches: 100 },
|
||||||
),
|
),
|
||||||
).resolves.toMatchObject({
|
).resolves.toMatchObject({
|
||||||
processed: 100,
|
processed: 500,
|
||||||
scheduledContinuation: true,
|
scheduledContinuation: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(runMutation.mock.calls[1]?.[1]).toMatchObject({ batchSize: 100 });
|
const batchCalls = runMutation.mock.calls.filter(([, args]) => {
|
||||||
expect(scheduler.runAfter.mock.calls[0]?.[2]).toMatchObject({ batchSize: 100 });
|
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", () => {
|
it("aggregates comment and uncomment events into net deltas", () => {
|
||||||
|
|||||||
@@ -182,8 +182,8 @@ const DOC_SYNC_LEASE_KEY = "skill_doc_stat_sync";
|
|||||||
const DOC_SYNC_LEASE_MS = 2 * 60 * 1_000;
|
const DOC_SYNC_LEASE_MS = 2 * 60 * 1_000;
|
||||||
const DEFAULT_DOC_SYNC_BATCH_SIZE = 100;
|
const DEFAULT_DOC_SYNC_BATCH_SIZE = 100;
|
||||||
const MAX_DOC_SYNC_BATCH_SIZE = 100;
|
const MAX_DOC_SYNC_BATCH_SIZE = 100;
|
||||||
const DEFAULT_DOC_SYNC_MAX_BATCHES = 20;
|
const DEFAULT_DOC_SYNC_MAX_BATCHES = 5;
|
||||||
const MAX_DOC_SYNC_MAX_BATCHES = 100;
|
const MAX_DOC_SYNC_MAX_BATCHES = 5;
|
||||||
|
|
||||||
type ClaimSkillStatDocSyncLeaseResult =
|
type ClaimSkillStatDocSyncLeaseResult =
|
||||||
| {
|
| {
|
||||||
|
|||||||
@@ -1,118 +1,37 @@
|
|||||||
import { describe, expect, it, vi } from "vitest";
|
import { readFileSync } from "node:fs";
|
||||||
import {
|
import { join } from "node:path";
|
||||||
buildAdminEmailArgs,
|
import { describe, expect, it } from "vitest";
|
||||||
runCorrespondence,
|
|
||||||
} from "../.agents/skills/clawhub-content-rights-correspondence/scripts/send-correspondence.js";
|
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", () => {
|
describe("ClawHub content rights correspondence skill", () => {
|
||||||
it("builds a dry-run admin email command by default", () => {
|
it("uses direct admin CLI commands instead of the removed helper script", () => {
|
||||||
expect(
|
expect(skill).toMatch(/Do not use helper\s+scripts/);
|
||||||
buildAdminEmailArgs({
|
expect(skill).not.toContain("send-correspondence");
|
||||||
to: "legal@example.com",
|
expect(skill).not.toContain("scripts/");
|
||||||
subject: "Re: CHR-000007",
|
expect(skill).toContain("bun run admin -- email send");
|
||||||
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("refuses to send without explicit user signoff", async () => {
|
it("keeps the explicit send-signoff guard in the requester status update send command", () => {
|
||||||
await expect(
|
const replies = section("Requester Status Updates");
|
||||||
runCorrespondence(
|
expect(replies).toContain("--send");
|
||||||
{
|
expect(replies).toContain("--confirm-user-request");
|
||||||
caseId: "CHR-000007",
|
expect(replies).toContain("--confirm-user-signoff");
|
||||||
subject: "Re: CHR-000007",
|
|
||||||
bodyFile: "/tmp/body.txt",
|
|
||||||
attachments: [],
|
|
||||||
send: true,
|
|
||||||
confirmUserSignoff: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
exec: vi.fn(),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
).rejects.toThrow("Sending requires --confirm-user-signoff.");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("sends to the existing case email and records exact correspondence through Hermit", async () => {
|
it("documents preserving outbound correspondence through the admin CLI", () => {
|
||||||
const execMock = vi
|
expect(skill).toContain("content-rights record-correspondence");
|
||||||
.fn()
|
expect(skill).toContain("--provider-message-id");
|
||||||
.mockResolvedValueOnce({
|
expect(skill).toContain("--attachment");
|
||||||
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",
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user