perf: route ClawSweeper comments immediately
This commit is contained in:
parent
435ec933dc
commit
ee50422df5
35
.github/workflows/repair-comment-router.yml
vendored
35
.github/workflows/repair-comment-router.yml
vendored
@ -1,6 +1,8 @@
|
||||
name: repair comment router
|
||||
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [clawsweeper_comment]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
execute:
|
||||
@ -33,6 +35,11 @@ on:
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
comment_ids:
|
||||
description: "Optional comma-separated issue comment ids to route directly"
|
||||
required: false
|
||||
default: ""
|
||||
type: string
|
||||
runner:
|
||||
description: "Runner label for repair planning/review work"
|
||||
required: true
|
||||
@ -44,7 +51,7 @@ on:
|
||||
default: blacksmith-16vcpu-ubuntu-2404
|
||||
type: string
|
||||
schedule:
|
||||
- cron: "*/10 * * * *"
|
||||
- cron: "*/5 * * * *"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@ -65,6 +72,15 @@ jobs:
|
||||
with:
|
||||
filter: blob:none
|
||||
fetch-depth: 0
|
||||
sparse-checkout: |
|
||||
.github
|
||||
jobs
|
||||
results
|
||||
src/repair
|
||||
package.json
|
||||
pnpm-lock.yaml
|
||||
tsconfig.repair.json
|
||||
sparse-checkout-cone-mode: false
|
||||
|
||||
- name: Create GitHub App token
|
||||
id: app_token
|
||||
@ -95,8 +111,17 @@ jobs:
|
||||
since="${{ github.event_name == 'workflow_dispatch' && inputs.since || '' }}"
|
||||
max_comments="${{ github.event_name == 'workflow_dispatch' && inputs.max_comments || vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '100' }}"
|
||||
item_numbers="${{ github.event_name == 'workflow_dispatch' && inputs.item_numbers || '' }}"
|
||||
comment_ids="${{ github.event_name == 'workflow_dispatch' && inputs.comment_ids || '' }}"
|
||||
runner="${{ github.event_name == 'workflow_dispatch' && inputs.runner || vars.CLAWSWEEPER_WORKER_RUNNER || 'blacksmith-4vcpu-ubuntu-2404' }}"
|
||||
execution_runner="${{ github.event_name == 'workflow_dispatch' && inputs.execution_runner || vars.CLAWSWEEPER_EXECUTION_RUNNER || 'blacksmith-16vcpu-ubuntu-2404' }}"
|
||||
if [ "${{ github.event_name }}" = "repository_dispatch" ]; then
|
||||
target_repo="${{ github.event.client_payload.target_repo || 'openclaw/openclaw' }}"
|
||||
lookback_minutes="${{ github.event.client_payload.lookback_minutes || vars.CLAWSWEEPER_COMMENT_LOOKBACK_MINUTES || '180' }}"
|
||||
since=""
|
||||
max_comments="${{ github.event.client_payload.max_comments || vars.CLAWSWEEPER_COMMENT_MAX_COMMENTS || '20' }}"
|
||||
item_numbers="${{ github.event.client_payload.item_number || '' }}"
|
||||
comment_ids="${{ github.event.client_payload.comment_id || '' }}"
|
||||
fi
|
||||
|
||||
args=(
|
||||
--write-report
|
||||
@ -113,7 +138,11 @@ jobs:
|
||||
if [ -n "$item_numbers" ]; then
|
||||
args+=(--item-numbers "$item_numbers")
|
||||
fi
|
||||
if [ -n "$comment_ids" ]; then
|
||||
args+=(--comment-ids "$comment_ids")
|
||||
fi
|
||||
if { [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ "${{ inputs.execute }}" = "true" ]; } ||
|
||||
{ [ "${{ github.event_name }}" = "repository_dispatch" ]; } ||
|
||||
{ [ "${{ github.event_name }}" = "schedule" ] && [ "${{ vars.CLAWSWEEPER_COMMENT_ROUTER_EXECUTE || '0' }}" = "1" ]; }; then
|
||||
args+=(--execute)
|
||||
fi
|
||||
@ -122,6 +151,10 @@ jobs:
|
||||
- name: Commit comment router ledger
|
||||
if: always()
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain -- results/comment-router.json jobs)" ]; then
|
||||
echo "No durable comment-router ledger or job changes to publish."
|
||||
exit 0
|
||||
fi
|
||||
pnpm run repair:publish-main -- \
|
||||
--message "chore: record ClawSweeper comment routing" \
|
||||
--path results/comment-router.json \
|
||||
|
||||
4
.github/workflows/sweep.yml
vendored
4
.github/workflows/sweep.yml
vendored
@ -349,6 +349,10 @@ jobs:
|
||||
- name: Commit event comment router ledger
|
||||
if: always()
|
||||
run: |
|
||||
if [ -z "$(git status --porcelain -- results/comment-router.json jobs)" ]; then
|
||||
echo "No durable event comment-router ledger or job changes to publish."
|
||||
exit 0
|
||||
fi
|
||||
pnpm run repair:publish-main -- \
|
||||
--message "chore: record ClawSweeper event comment routing" \
|
||||
--path results/comment-router.json \
|
||||
|
||||
@ -174,6 +174,10 @@ Live preflight hydrates job-provided refs by default and records linked refs wit
|
||||
## Maintainer Comment Routing
|
||||
|
||||
`pnpm run repair:comment-router` scans recent issue and PR comments in the target repo.
|
||||
Target repositories can also forward matching `issue_comment` events as
|
||||
`clawsweeper_comment` repository dispatches with the exact comment id. Those
|
||||
comments get an immediate `eyes` reaction from the ClawSweeper app, and the
|
||||
scheduled sweep remains a five-minute fallback.
|
||||
It accepts only maintainer-authored commands, gated by GitHub
|
||||
`author_association` values `OWNER`, `MEMBER`, or `COLLABORATOR` by default.
|
||||
Contributor comments are ignored without a reply.
|
||||
@ -210,10 +214,13 @@ include the target PR head SHA. That lets edited ClawSweeper comments wake
|
||||
ClawSweeper again after the PR branch changes while unchanged comment versions
|
||||
remain idempotent.
|
||||
|
||||
Use `--item-number <number>` or `--item-numbers <a,b>` to route only specific
|
||||
open issue or PR comments. The event review workflow uses this targeted path
|
||||
after syncing its durable ClawSweeper verdict so automerge can act on a fresh
|
||||
`pass` marker without waiting for the scheduled comment-router sweep.
|
||||
Use `--comment-id <id>`, `--comment-ids <a,b>`, `--item-number <number>`, or
|
||||
`--item-numbers <a,b>` to route only specific comments or specific open issue
|
||||
or PR comments. The event review workflow uses this targeted path after syncing
|
||||
its durable ClawSweeper verdict so automerge can act on a fresh `pass` marker
|
||||
without waiting for the scheduled comment-router sweep. No-op targeted
|
||||
acknowledgements, such as already-processed commands or already-enabled
|
||||
automerge commands, do not publish a durable ledger commit.
|
||||
|
||||
If the adopted automerge worker returns no executable fix artifact, the
|
||||
executor posts one idempotent outcome comment on the opted-in PR. That status
|
||||
|
||||
@ -22,6 +22,8 @@ name: ClawSweeper Dispatch
|
||||
on:
|
||||
issues:
|
||||
types: [opened, reopened, edited, labeled, unlabeled]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
pull_request_target: # zizmor: ignore[dangerous-triggers] maintainer-owned external dispatch; no checkout or untrusted PR code execution
|
||||
types: [opened, reopened, synchronize, ready_for_review, edited, labeled, unlabeled]
|
||||
|
||||
@ -54,8 +56,22 @@ jobs:
|
||||
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
|
||||
owner: openclaw
|
||||
repositories: clawsweeper
|
||||
permission-contents: write
|
||||
|
||||
- name: Create target comment token
|
||||
id: target_token
|
||||
if: ${{ github.event_name == 'issue_comment' && env.HAS_CLAWSWEEPER_APP_PRIVATE_KEY == 'true' }}
|
||||
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||
with:
|
||||
client-id: ${{ env.CLAWSWEEPER_APP_CLIENT_ID }}
|
||||
private-key: ${{ secrets.CLAWSWEEPER_APP_PRIVATE_KEY }}
|
||||
owner: ${{ github.repository_owner }}
|
||||
repositories: ${{ github.event.repository.name }}
|
||||
permission-issues: write
|
||||
permission-pull-requests: read
|
||||
|
||||
- name: Dispatch exact ClawSweeper review
|
||||
if: ${{ github.event_name != 'issue_comment' }}
|
||||
env:
|
||||
GH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
TARGET_REPO: ${{ github.repository }}
|
||||
@ -79,9 +95,50 @@ jobs:
|
||||
gh api repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"
|
||||
|
||||
- name: Acknowledge and dispatch ClawSweeper comment
|
||||
if: ${{ github.event_name == 'issue_comment' }}
|
||||
env:
|
||||
DISPATCH_TOKEN: ${{ steps.token.outputs.token }}
|
||||
TARGET_TOKEN: ${{ steps.target_token.outputs.token }}
|
||||
TARGET_REPO: ${{ github.repository }}
|
||||
ITEM_NUMBER: ${{ github.event.issue.number }}
|
||||
COMMENT_ID: ${{ github.event.comment.id }}
|
||||
COMMENT_BODY: ${{ github.event.comment.body }}
|
||||
SOURCE_ACTION: ${{ github.event.action }}
|
||||
run: |
|
||||
if [ -z "$DISPATCH_TOKEN" ]; then
|
||||
echo "::notice::Skipping ClawSweeper dispatch because no dispatch credential is configured."
|
||||
exit 0
|
||||
fi
|
||||
body_file="$RUNNER_TEMP/clawsweeper-comment-body.txt"
|
||||
printf '%s\n' "$COMMENT_BODY" > "$body_file"
|
||||
if ! grep -Eiq '(^|[[:space:]])@clawsweeper\b|(^|[[:space:]])/(clawsweeper|review|automerge|autoclose)\b' "$body_file"; then
|
||||
echo "No ClawSweeper command found in comment."
|
||||
exit 0
|
||||
fi
|
||||
if [ -n "$TARGET_TOKEN" ]; then
|
||||
GH_TOKEN="$TARGET_TOKEN" gh api -X POST \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"repos/$TARGET_REPO/issues/comments/$COMMENT_ID/reactions" \
|
||||
-f content="eyes" >/dev/null || true
|
||||
fi
|
||||
payload="$(jq -nc \
|
||||
--arg target_repo "$TARGET_REPO" \
|
||||
--argjson item_number "$ITEM_NUMBER" \
|
||||
--argjson comment_id "$COMMENT_ID" \
|
||||
--arg source_event "issue_comment" \
|
||||
--arg source_action "$SOURCE_ACTION" \
|
||||
'{event_type:"clawsweeper_comment",client_payload:{target_repo:$target_repo,item_number:$item_number,comment_id:$comment_id,source_event:$source_event,source_action:$source_action}}')"
|
||||
GH_TOKEN="$DISPATCH_TOKEN" gh api repos/openclaw/clawsweeper/dispatches \
|
||||
--method POST \
|
||||
--input - <<< "$payload"
|
||||
```
|
||||
|
||||
Comments are intentionally not a trigger. Bot-authored label churn is also
|
||||
Comments are a lightweight trigger only when the body contains a ClawSweeper
|
||||
command. The target workflow reacts with `eyes` immediately and dispatches
|
||||
`clawsweeper_comment` to the comment router with the exact comment id, so it
|
||||
does not need to wait for the scheduled sweep. Bot-authored label churn is also
|
||||
ignored. Human label changes are debounced and may run after an active
|
||||
dispatcher, but they must not cancel a content-changing dispatch before it posts
|
||||
to ClawSweeper. Content-changing events such as issue edits and PR synchronizes
|
||||
|
||||
@ -125,6 +125,7 @@ export function readLedger(file: JsonValue) {
|
||||
export function appendLedger(current: LooseRecord, entries: LooseRecord[]) {
|
||||
const compact = entries
|
||||
.filter((entry: JsonValue) => ["executed", "skipped"].includes(entry.status))
|
||||
.filter((entry: JsonValue) => !isNoopSkip(entry))
|
||||
.map((entry: JsonValue) => {
|
||||
const actions = compactLedgerActions(entry.actions);
|
||||
return {
|
||||
@ -161,12 +162,36 @@ export function appendLedger(current: LooseRecord, entries: LooseRecord[]) {
|
||||
...(actions.length > 0 ? { actions } : {}),
|
||||
};
|
||||
});
|
||||
if (compact.length === 0) return false;
|
||||
const byCommentVersion = new Map(
|
||||
(current.commands ?? []).map((entry: JsonValue) => [ledgerEntryKey(entry), entry]),
|
||||
);
|
||||
for (const entry of compact) byCommentVersion.set(ledgerEntryKey(entry), entry);
|
||||
let changed = false;
|
||||
for (const entry of compact) {
|
||||
const key = ledgerEntryKey(entry);
|
||||
const previous = byCommentVersion.get(key);
|
||||
if (previous && stableLedgerEntry(previous) === stableLedgerEntry(entry)) continue;
|
||||
byCommentVersion.set(key, entry);
|
||||
changed = true;
|
||||
}
|
||||
if (!changed) return false;
|
||||
current.updated_at = new Date().toISOString();
|
||||
current.commands = [...byCommentVersion.values()].slice(-1000);
|
||||
return true;
|
||||
}
|
||||
|
||||
function isNoopSkip(entry: LooseRecord) {
|
||||
if (String(entry.status ?? "") !== "skipped") return false;
|
||||
const reason = String(entry.reason ?? "");
|
||||
return (
|
||||
reason === "comment version already processed in ledger" ||
|
||||
reason === "matching ClawSweeper response comment already exists" ||
|
||||
/already enabled for this PR/i.test(reason)
|
||||
);
|
||||
}
|
||||
|
||||
function stableLedgerEntry(entry: LooseRecord) {
|
||||
return JSON.stringify({ ...entry, processed_at: null });
|
||||
}
|
||||
|
||||
function ledgerEntryKey(entry: LooseRecord) {
|
||||
|
||||
@ -90,11 +90,14 @@ const {
|
||||
lookupConcurrency,
|
||||
since,
|
||||
itemNumbers,
|
||||
commentIds,
|
||||
allowedAssociations,
|
||||
allowedRepositoryPermissions,
|
||||
trustedBots,
|
||||
} = config;
|
||||
|
||||
const startedAtMs = Date.now();
|
||||
const timings: LooseRecord[] = [];
|
||||
const ledger = readLedger(ledgerPath());
|
||||
const TARGET_LOOKUP_RETRY_ATTEMPTS = 3;
|
||||
const processedCommentVersions = new Set(
|
||||
@ -107,7 +110,7 @@ const plannedAutoRepairHeads = new Set<string>();
|
||||
const collaboratorPermissionCache = new Map();
|
||||
const liveTargetCache = new Map<number, LooseRecord>();
|
||||
const issueCommentsCache = new Map<number, JsonValue[]>();
|
||||
const comments = listCandidateComments();
|
||||
const comments = measure("list_candidate_comments", () => listCandidateComments());
|
||||
const rawCommands: LooseRecord[] = [];
|
||||
|
||||
for (const comment of comments) {
|
||||
@ -148,8 +151,10 @@ for (const comment of comments) {
|
||||
rawCommands.push(command);
|
||||
}
|
||||
|
||||
await prehydrateCommandLookups(rawCommands);
|
||||
const commands = rawCommands.map((command) => classifyCommand(command));
|
||||
await measureAsync("prehydrate_command_lookups", () => prehydrateCommandLookups(rawCommands));
|
||||
const commands = measure("classify_commands", () =>
|
||||
rawCommands.map((command) => classifyCommand(command)),
|
||||
);
|
||||
|
||||
const actionable = commands.filter((command: JsonValue) => command.status === "ready");
|
||||
const report: LooseRecord = {
|
||||
@ -162,6 +167,7 @@ const report: LooseRecord = {
|
||||
execute,
|
||||
max_comments: maxComments,
|
||||
item_numbers: [...itemNumbers],
|
||||
comment_ids: [...commentIds],
|
||||
max_autoclose_targets: maxAutocloseTargets,
|
||||
scanned_comments: comments.length,
|
||||
commands_seen: commands.length,
|
||||
@ -175,34 +181,59 @@ const report: LooseRecord = {
|
||||
};
|
||||
|
||||
if (execute) {
|
||||
assertMutationActorIsClawsweeperBot();
|
||||
for (const command of commands) acknowledgeSkippedMaintainerCommand(command);
|
||||
const dispatchCount = actionable.filter((command: JsonValue) =>
|
||||
REPAIR_INTENTS.has(command.intent),
|
||||
).length;
|
||||
if (dispatchCount > 0) {
|
||||
report.live_worker_capacity_before_dispatch = waitForCapacity
|
||||
? waitForLiveWorkerCapacity({
|
||||
repo: repairRepo,
|
||||
workflow,
|
||||
requested: dispatchCount,
|
||||
maxLiveWorkers,
|
||||
})
|
||||
: assertLiveWorkerCapacity({
|
||||
repo: repairRepo,
|
||||
workflow,
|
||||
requested: dispatchCount,
|
||||
maxLiveWorkers,
|
||||
});
|
||||
}
|
||||
for (const command of actionable) executeCommand(command);
|
||||
appendLedger(ledger, commands);
|
||||
writeLedger(ledgerPath(), ledger);
|
||||
await measureAsync("execute_commands", async () => {
|
||||
assertMutationActorIsClawsweeperBot();
|
||||
for (const command of commands) acknowledgeSkippedMaintainerCommand(command);
|
||||
const dispatchCount = actionable.filter(
|
||||
(command: JsonValue) =>
|
||||
REPAIR_INTENTS.has(command.intent) || MERGE_INTENTS.has(command.intent),
|
||||
).length;
|
||||
if (dispatchCount > 0) {
|
||||
report.live_worker_capacity_before_dispatch = waitForCapacity
|
||||
? waitForLiveWorkerCapacity({
|
||||
repo: repairRepo,
|
||||
workflow,
|
||||
requested: dispatchCount,
|
||||
maxLiveWorkers,
|
||||
})
|
||||
: assertLiveWorkerCapacity({
|
||||
repo: repairRepo,
|
||||
workflow,
|
||||
requested: dispatchCount,
|
||||
maxLiveWorkers,
|
||||
});
|
||||
}
|
||||
for (const command of actionable) executeCommand(command);
|
||||
});
|
||||
report.ledger_changed = measure("append_ledger", () => appendLedger(ledger, commands));
|
||||
if (report.ledger_changed) writeLedger(ledgerPath(), ledger);
|
||||
}
|
||||
|
||||
report.timings = {
|
||||
total_ms: Date.now() - startedAtMs,
|
||||
phases: timings,
|
||||
};
|
||||
if (writeReport) writeReportFile(repoRoot(), report);
|
||||
console.log(JSON.stringify(report, null, 2));
|
||||
|
||||
function measure<T>(name: string, fn: () => T): T {
|
||||
const start = Date.now();
|
||||
try {
|
||||
return fn();
|
||||
} finally {
|
||||
timings.push({ name, ms: Date.now() - start });
|
||||
}
|
||||
}
|
||||
|
||||
async function measureAsync<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
||||
const start = Date.now();
|
||||
try {
|
||||
return await fn();
|
||||
} finally {
|
||||
timings.push({ name, ms: Date.now() - start });
|
||||
}
|
||||
}
|
||||
|
||||
function assertMutationActorIsClawsweeperBot() {
|
||||
try {
|
||||
const viewer = ghJson<LooseRecord>(["api", "user"]);
|
||||
@ -1407,6 +1438,16 @@ function executeAutomerge(command: LooseRecord) {
|
||||
};
|
||||
const block = validateAutomergeReadiness({ command, view, target: latestTarget });
|
||||
if (block) {
|
||||
if (isAutomergeChangelogBlock(block)) {
|
||||
return {
|
||||
action: "merge",
|
||||
status: "repair_needed",
|
||||
reason: block,
|
||||
repair_reason:
|
||||
"CHANGELOG.md entry is required before automerge; dispatch a focused changelog repair",
|
||||
merge_method: "squash",
|
||||
};
|
||||
}
|
||||
if (isTransientAutomergeBlock(block, view)) {
|
||||
return { action: "merge", status: "waiting", reason: block, merge_method: "squash" };
|
||||
}
|
||||
@ -1624,6 +1665,10 @@ function validateAutomergeReadiness({ command, view, target }: LooseRecord) {
|
||||
return "";
|
||||
}
|
||||
|
||||
function isAutomergeChangelogBlock(reason: string) {
|
||||
return /CHANGELOG\.md entry is required/i.test(String(reason ?? ""));
|
||||
}
|
||||
|
||||
function isTransientAutomergeBlock(reason: string, view: LooseRecord) {
|
||||
const text = String(reason ?? "").toLowerCase();
|
||||
if (text.includes("checks are not green")) return hasPendingChecks(view.statusCheckRollup ?? []);
|
||||
@ -1713,6 +1758,13 @@ function listRecentComments() {
|
||||
}
|
||||
|
||||
function listCandidateComments() {
|
||||
if (commentIds.size > 0) {
|
||||
return selectCommentsForRouting({
|
||||
recentComments: [...commentIds].map((commentId) => fetchIssueComment(commentId)),
|
||||
durableComments: [],
|
||||
maxComments,
|
||||
});
|
||||
}
|
||||
if (itemNumbers.size > 0) {
|
||||
return selectCommentsForRouting({
|
||||
recentComments: [...itemNumbers].flatMap((number) => issueCommentsFor(number)),
|
||||
@ -1766,6 +1818,12 @@ function listRepairLoopReviewComments() {
|
||||
);
|
||||
}
|
||||
|
||||
function fetchIssueComment(commentId: JsonValue) {
|
||||
return ghJson(["api", `repos/${targetRepo}/issues/comments/${commentId}`], {
|
||||
attempts: TARGET_LOOKUP_RETRY_ATTEMPTS,
|
||||
});
|
||||
}
|
||||
|
||||
function listOpenIssueNumbersWithLabel(label: string) {
|
||||
return ghPaged<JsonValue>(
|
||||
`repos/${targetRepo}/issues?state=open&labels=${encodeURIComponent(label)}&per_page=100`,
|
||||
|
||||
@ -35,6 +35,7 @@ export type CommentRouterConfig = {
|
||||
lookbackMinutes: number;
|
||||
since: string;
|
||||
itemNumbers: Set<number>;
|
||||
commentIds: Set<number>;
|
||||
allowedAssociations: Set<string>;
|
||||
allowedRepositoryPermissions: Set<string>;
|
||||
trustedBots: Set<string>;
|
||||
@ -128,6 +129,12 @@ export function readCommentRouterConfig(args: LooseRecord): CommentRouterConfig
|
||||
.join(","),
|
||||
"item-numbers",
|
||||
),
|
||||
commentIds: numberSet(
|
||||
[args["comment-id"], args["comment-ids"], process.env.CLAWSWEEPER_COMMENT_IDS]
|
||||
.filter((value) => value !== undefined && value !== null)
|
||||
.join(","),
|
||||
"comment-ids",
|
||||
),
|
||||
allowedAssociations: upperCaseSet(
|
||||
process.env.CLAWSWEEPER_COMMENT_ALLOWED_ASSOCIATIONS ??
|
||||
DEFAULT_ALLOWED_ASSOCIATIONS.join(","),
|
||||
|
||||
@ -48,22 +48,70 @@ test("appendLedger keeps edited comment versions separate", () => {
|
||||
test("appendLedger leaves waiting commands retryable", () => {
|
||||
const ledger = { updated_at: null, commands: [] };
|
||||
|
||||
appendLedger(ledger, [
|
||||
{
|
||||
idempotency_key: "transient",
|
||||
comment_id: "124",
|
||||
comment_version_key: "124:2026-04-29T03:00:00Z",
|
||||
comment_updated_at: "2026-04-29T03:00:00Z",
|
||||
status: "waiting",
|
||||
intent: "clawsweeper_re_review",
|
||||
issue_number: 74499,
|
||||
repo: "openclaw/openclaw",
|
||||
},
|
||||
]);
|
||||
assert.equal(
|
||||
appendLedger(ledger, [
|
||||
{
|
||||
idempotency_key: "transient",
|
||||
comment_id: "124",
|
||||
comment_version_key: "124:2026-04-29T03:00:00Z",
|
||||
comment_updated_at: "2026-04-29T03:00:00Z",
|
||||
status: "waiting",
|
||||
intent: "clawsweeper_re_review",
|
||||
issue_number: 74499,
|
||||
repo: "openclaw/openclaw",
|
||||
},
|
||||
]),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(ledger.commands.length, 0);
|
||||
});
|
||||
|
||||
test("appendLedger ignores no-op skipped command versions", () => {
|
||||
const ledger = { updated_at: null, commands: [] };
|
||||
|
||||
assert.equal(
|
||||
appendLedger(ledger, [
|
||||
{
|
||||
idempotency_key: "already-processed",
|
||||
comment_id: "124",
|
||||
comment_version_key: "124:2026-04-29T03:00:00Z",
|
||||
comment_updated_at: "2026-04-29T03:00:00Z",
|
||||
status: "skipped",
|
||||
reason: "comment version already processed in ledger",
|
||||
intent: "automerge",
|
||||
issue_number: 74499,
|
||||
repo: "openclaw/openclaw",
|
||||
},
|
||||
]),
|
||||
false,
|
||||
);
|
||||
|
||||
assert.equal(ledger.commands.length, 0);
|
||||
});
|
||||
|
||||
test("appendLedger reports compact executed writes", () => {
|
||||
const ledger = { updated_at: null, commands: [] };
|
||||
|
||||
assert.equal(
|
||||
appendLedger(ledger, [
|
||||
{
|
||||
idempotency_key: "processed",
|
||||
comment_id: "125",
|
||||
comment_version_key: "125:2026-04-29T03:01:00Z",
|
||||
comment_updated_at: "2026-04-29T03:01:00Z",
|
||||
status: "executed",
|
||||
intent: "clawsweeper_re_review",
|
||||
issue_number: 74499,
|
||||
repo: "openclaw/openclaw",
|
||||
},
|
||||
]),
|
||||
true,
|
||||
);
|
||||
|
||||
assert.equal(ledger.commands.length, 1);
|
||||
});
|
||||
|
||||
test("appendLedger preserves compact executed actions for repair caps", () => {
|
||||
const ledger = { updated_at: null, commands: [] };
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user