Files
openclaw/src/utils/queue-helpers.test.ts
Andy Ye 44c6ad7dce fix(subagents): collect unresolved announce batches (#83701)
Summary:
- The PR changes collect-mode follow-up queue routing so unresolved-origin items can batch with a single resolved route and later compatible items can resume batching after a true cross-channel drain.
- Reproducibility: yes. at source level: current main treats unkeyed-plus-same-keyed queue items as cross-chan ... failing path is directly visible in `src/utils/queue-helpers.ts` and `src/auto-reply/reply/queue/drain.ts`.

Automerge notes:
- PR branch already contained follow-up commit before automerge: Merge remote-tracking branch 'origin/main' into maint-83701-20260518

Validation:
- ClawSweeper review passed for head e6ad029e23.
- Required merge gates passed before the squash merge.

Prepared head SHA: e6ad029e23
Review: https://github.com/openclaw/openclaw/pull/83701#issuecomment-4479943100

Co-authored-by: Andy Ye <35905412+TurboTheTurtle@users.noreply.github.com>
Co-authored-by: clawsweeper[bot] <274271284+clawsweeper[bot]@users.noreply.github.com>
Approved-by: takhoffman
Co-authored-by: takhoffman <781889+takhoffman@users.noreply.github.com>
2026-05-18 18:34:58 +00:00

194 lines
4.7 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
applyQueueRuntimeSettings,
buildQueueSummaryPrompt,
clearQueueSummaryState,
drainCollectItemIfNeeded,
hasCrossChannelItems,
previewQueueSummaryPrompt,
} from "./queue-helpers.js";
describe("applyQueueRuntimeSettings", () => {
it("updates runtime queue settings with normalization", () => {
const target = {
mode: "followup" as const,
debounceMs: 1000,
cap: 20,
dropPolicy: "summarize" as const,
};
applyQueueRuntimeSettings({
target,
settings: {
mode: "collect",
debounceMs: -12,
cap: 9.8,
dropPolicy: "new",
},
});
expect(target).toEqual({
mode: "collect",
debounceMs: 0,
cap: 9,
dropPolicy: "new",
});
});
it("keeps existing values when optional settings are missing/invalid", () => {
const target = {
mode: "followup" as const,
debounceMs: 1000,
cap: 20,
dropPolicy: "summarize" as const,
};
applyQueueRuntimeSettings({
target,
settings: {
mode: "queue",
cap: 0,
},
});
expect(target).toEqual({
mode: "queue",
debounceMs: 1000,
cap: 20,
dropPolicy: "summarize",
});
});
});
describe("queue summary helpers", () => {
it("previewQueueSummaryPrompt does not mutate state", () => {
const state = {
dropPolicy: "summarize" as const,
droppedCount: 2,
summaryLines: ["first", "second"],
};
const prompt = previewQueueSummaryPrompt({
state,
noun: "message",
});
expect(prompt).toContain("[Queue overflow] Dropped 2 messages due to cap.");
expect(prompt).toContain("first");
expect(state).toEqual({
dropPolicy: "summarize",
droppedCount: 2,
summaryLines: ["first", "second"],
});
});
it("buildQueueSummaryPrompt clears state after rendering", () => {
const state = {
dropPolicy: "summarize" as const,
droppedCount: 1,
summaryLines: ["line"],
};
const prompt = buildQueueSummaryPrompt({
state,
noun: "announce",
});
expect(prompt).toContain("[Queue overflow] Dropped 1 announce due to cap.");
expect(state).toEqual({
dropPolicy: "summarize",
droppedCount: 0,
summaryLines: [],
});
});
it("clearQueueSummaryState resets summary counters", () => {
const state = {
dropPolicy: "summarize" as const,
droppedCount: 5,
summaryLines: ["a", "b"],
};
clearQueueSummaryState(state);
expect(state.droppedCount).toBe(0);
expect(state.summaryLines).toStrictEqual([]);
});
});
describe("drainCollectItemIfNeeded", () => {
it("skips when neither force mode nor cross-channel routing is active", async () => {
const seen: number[] = [];
const items = [1];
const result = await drainCollectItemIfNeeded({
forceIndividualCollect: false,
isCrossChannel: false,
items,
run: async (item) => {
seen.push(item);
},
});
expect(result).toBe("skipped");
expect(seen).toStrictEqual([]);
expect(items).toEqual([1]);
});
it("drains one item in force mode", async () => {
const seen: number[] = [];
const items = [1, 2];
const result = await drainCollectItemIfNeeded({
forceIndividualCollect: true,
isCrossChannel: false,
items,
run: async (item) => {
seen.push(item);
},
});
expect(result).toBe("drained");
expect(seen).toEqual([1]);
expect(items).toEqual([2]);
});
it("switches to force mode and returns empty when cross-channel with no queued item", async () => {
let forced = false;
const result = await drainCollectItemIfNeeded({
forceIndividualCollect: false,
isCrossChannel: true,
setForceIndividualCollect: (next) => {
forced = next;
},
items: [],
run: async () => {},
});
expect(result).toBe("empty");
expect(forced).toBe(true);
});
});
describe("hasCrossChannelItems", () => {
it("lets unresolved items join an otherwise single keyed route", () => {
const items = [
{ id: "unresolved" },
{ id: "first", key: "slack:channel:A" },
{ id: "second", key: "slack:channel:A" },
];
expect(hasCrossChannelItems(items, (item) => ({ key: item.key }))).toBe(false);
});
it("still treats distinct keyed routes and explicit cross items as cross-channel", () => {
expect(
hasCrossChannelItems([{ key: "slack:channel:A" }, { key: "slack:channel:B" }], (item) => ({
key: item.key,
})),
).toBe(true);
expect(
hasCrossChannelItems([{ key: "slack:channel:A" }, { cross: true }], (item) => item),
).toBe(true);
});
});