mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 12:56:01 -06:00
cec17abe55
* feat(ui): calm the working claw and add turn status whimsy The working indicator claw now claws in place (jaw snips, no lateral shadowboxing) with rare seeded stances: mirrored southpaw, double-time flurry, fake-3D turntable spin, the old punch combo as a rare shadowbox, and a somersault backflip. After 30s of quiet the status line adds rotating crab-gerund phrases (Clawing…), and a completed run leaves a sticky 'Done in 51s · 485 tokens' recap row fed by the session row's per-run runtime and output tokens. Recap attribution is guarded by a watch/settle state machine (baseline terminal stamps, absorb, expiry); the residual without gateway run identity is documented in code. * fix(ui): unexport test-only working-phrase timing constants knip's production scan forbids exports only consumed by tests; the test mirrors the two literals with a pointer comment.
87 lines
2.7 KiB
TypeScript
87 lines
2.7 KiB
TypeScript
// @vitest-environment jsdom
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import "./working-phrase.ts";
|
|
|
|
// Mirrors WORKING_PHRASE_SHOW_AFTER_MS / WORKING_PHRASE_ROTATE_EVERY_MS in
|
|
// working-phrase.ts (knip forbids test-only exports).
|
|
const WORKING_PHRASE_SHOW_AFTER_MS = 30_000;
|
|
const WORKING_PHRASE_ROTATE_EVERY_MS = 45_000;
|
|
|
|
type WorkingPhraseElement = HTMLElement & {
|
|
startMs: number | null;
|
|
seed: string;
|
|
updateComplete: Promise<boolean>;
|
|
requestUpdate: () => void;
|
|
};
|
|
|
|
const NOW = 2_000_000_000;
|
|
// "· Clawing…" — a middot, one gerund, an ellipsis.
|
|
const PHRASE_TEXT = /^·\s\S+…$/;
|
|
|
|
function mountPhrase(seed = "stream-working:test"): WorkingPhraseElement {
|
|
const element = document.createElement("openclaw-working-phrase") as WorkingPhraseElement;
|
|
element.seed = seed;
|
|
element.startMs = NOW;
|
|
document.body.appendChild(element);
|
|
return element;
|
|
}
|
|
|
|
async function textAt(element: WorkingPhraseElement, elapsedMs: number): Promise<string> {
|
|
vi.setSystemTime(NOW + elapsedMs);
|
|
element.requestUpdate();
|
|
await element.updateComplete;
|
|
return element.textContent?.replace(/\s+/g, " ").trim() ?? "";
|
|
}
|
|
|
|
describe("openclaw-working-phrase", () => {
|
|
let element: WorkingPhraseElement;
|
|
|
|
beforeEach(() => {
|
|
vi.useFakeTimers({ now: NOW });
|
|
element = mountPhrase();
|
|
});
|
|
|
|
afterEach(() => {
|
|
element.remove();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("stays silent before the quiet threshold", async () => {
|
|
expect(await textAt(element, WORKING_PHRASE_SHOW_AFTER_MS - 5_000)).toBe("");
|
|
});
|
|
|
|
it("shows a phrase once the wait drags on", async () => {
|
|
expect(await textAt(element, WORKING_PHRASE_SHOW_AFTER_MS + 1_000)).toMatch(PHRASE_TEXT);
|
|
});
|
|
|
|
it("is stable within a rotation and changes across rotations", async () => {
|
|
const first = await textAt(element, WORKING_PHRASE_SHOW_AFTER_MS + 1_000);
|
|
const stillFirst = await textAt(element, WORKING_PHRASE_SHOW_AFTER_MS + 10_000);
|
|
expect(stillFirst).toBe(first);
|
|
|
|
// Adjacent rotations must differ even when raw hashes collide, across
|
|
// many buckets (the displayed word feeds the next bucket's dodge).
|
|
let previous = first;
|
|
for (let bucket = 1; bucket <= 8; bucket++) {
|
|
const next = await textAt(
|
|
element,
|
|
WORKING_PHRASE_SHOW_AFTER_MS + bucket * WORKING_PHRASE_ROTATE_EVERY_MS + 1_000,
|
|
);
|
|
expect(next).toMatch(PHRASE_TEXT);
|
|
expect(next).not.toBe(previous);
|
|
previous = next;
|
|
}
|
|
});
|
|
|
|
it("is deterministic per seed", async () => {
|
|
const twin = mountPhrase();
|
|
try {
|
|
const a = await textAt(element, WORKING_PHRASE_SHOW_AFTER_MS + 1_000);
|
|
const b = await textAt(twin, WORKING_PHRASE_SHOW_AFTER_MS + 1_000);
|
|
expect(b).toBe(a);
|
|
} finally {
|
|
twin.remove();
|
|
}
|
|
});
|
|
});
|