mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 10:07:41 +00:00
test: table-drive status reactions and session key cases
This commit is contained in:
@@ -28,55 +28,61 @@ const createMockAdapter = () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createEnabledController = (
|
||||||
|
overrides: Partial<Parameters<typeof createStatusReactionController>[0]> = {},
|
||||||
|
) => {
|
||||||
|
const { adapter, calls } = createMockAdapter();
|
||||||
|
const controller = createStatusReactionController({
|
||||||
|
enabled: true,
|
||||||
|
adapter,
|
||||||
|
initialEmoji: "👀",
|
||||||
|
...overrides,
|
||||||
|
});
|
||||||
|
return { adapter, calls, controller };
|
||||||
|
};
|
||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// Tests
|
// Tests
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
describe("resolveToolEmoji", () => {
|
describe("resolveToolEmoji", () => {
|
||||||
it("should return coding emoji for exec tool", () => {
|
const cases: Array<{
|
||||||
const result = resolveToolEmoji("exec", DEFAULT_EMOJIS);
|
name: string;
|
||||||
expect(result).toBe(DEFAULT_EMOJIS.coding);
|
tool: string | undefined;
|
||||||
});
|
expected: string;
|
||||||
|
}> = [
|
||||||
|
{ name: "returns coding emoji for exec tool", tool: "exec", expected: DEFAULT_EMOJIS.coding },
|
||||||
|
{
|
||||||
|
name: "returns coding emoji for process tool",
|
||||||
|
tool: "process",
|
||||||
|
expected: DEFAULT_EMOJIS.coding,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "returns web emoji for web_search tool",
|
||||||
|
tool: "web_search",
|
||||||
|
expected: DEFAULT_EMOJIS.web,
|
||||||
|
},
|
||||||
|
{ name: "returns web emoji for browser tool", tool: "browser", expected: DEFAULT_EMOJIS.web },
|
||||||
|
{
|
||||||
|
name: "returns tool emoji for unknown tool",
|
||||||
|
tool: "unknown_tool",
|
||||||
|
expected: DEFAULT_EMOJIS.tool,
|
||||||
|
},
|
||||||
|
{ name: "returns tool emoji for empty string", tool: "", expected: DEFAULT_EMOJIS.tool },
|
||||||
|
{ name: "returns tool emoji for undefined", tool: undefined, expected: DEFAULT_EMOJIS.tool },
|
||||||
|
{ name: "is case-insensitive", tool: "EXEC", expected: DEFAULT_EMOJIS.coding },
|
||||||
|
{
|
||||||
|
name: "matches tokens within tool names",
|
||||||
|
tool: "my_exec_wrapper",
|
||||||
|
expected: DEFAULT_EMOJIS.coding,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
it("should return coding emoji for process tool", () => {
|
for (const testCase of cases) {
|
||||||
const result = resolveToolEmoji("process", DEFAULT_EMOJIS);
|
it(`should ${testCase.name}`, () => {
|
||||||
expect(result).toBe(DEFAULT_EMOJIS.coding);
|
expect(resolveToolEmoji(testCase.tool, DEFAULT_EMOJIS)).toBe(testCase.expected);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
it("should return web emoji for web_search tool", () => {
|
|
||||||
const result = resolveToolEmoji("web_search", DEFAULT_EMOJIS);
|
|
||||||
expect(result).toBe(DEFAULT_EMOJIS.web);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return web emoji for browser tool", () => {
|
|
||||||
const result = resolveToolEmoji("browser", DEFAULT_EMOJIS);
|
|
||||||
expect(result).toBe(DEFAULT_EMOJIS.web);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return tool emoji for unknown tool", () => {
|
|
||||||
const result = resolveToolEmoji("unknown_tool", DEFAULT_EMOJIS);
|
|
||||||
expect(result).toBe(DEFAULT_EMOJIS.tool);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return tool emoji for empty string", () => {
|
|
||||||
const result = resolveToolEmoji("", DEFAULT_EMOJIS);
|
|
||||||
expect(result).toBe(DEFAULT_EMOJIS.tool);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return tool emoji for undefined", () => {
|
|
||||||
const result = resolveToolEmoji(undefined, DEFAULT_EMOJIS);
|
|
||||||
expect(result).toBe(DEFAULT_EMOJIS.tool);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should be case-insensitive", () => {
|
|
||||||
const result = resolveToolEmoji("EXEC", DEFAULT_EMOJIS);
|
|
||||||
expect(result).toBe(DEFAULT_EMOJIS.coding);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should match tokens within tool names", () => {
|
|
||||||
const result = resolveToolEmoji("my_exec_wrapper", DEFAULT_EMOJIS);
|
|
||||||
expect(result).toBe(DEFAULT_EMOJIS.coding);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createStatusReactionController", () => {
|
describe("createStatusReactionController", () => {
|
||||||
@@ -105,12 +111,7 @@ describe("createStatusReactionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call setReaction with initialEmoji for setQueued immediately", async () => {
|
it("should call setReaction with initialEmoji for setQueued immediately", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController();
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setQueued();
|
void controller.setQueued();
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
@@ -119,12 +120,7 @@ describe("createStatusReactionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should debounce setThinking and eventually call adapter", async () => {
|
it("should debounce setThinking and eventually call adapter", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController();
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setThinking();
|
void controller.setThinking();
|
||||||
|
|
||||||
@@ -138,12 +134,7 @@ describe("createStatusReactionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should classify tool name and debounce", async () => {
|
it("should classify tool name and debounce", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController();
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setTool("exec");
|
void controller.setTool("exec");
|
||||||
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||||
@@ -151,75 +142,64 @@ describe("createStatusReactionController", () => {
|
|||||||
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.coding });
|
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.coding });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should execute setDone immediately without debounce", async () => {
|
const immediateTerminalCases = [
|
||||||
const { adapter, calls } = createMockAdapter();
|
{
|
||||||
const controller = createStatusReactionController({
|
name: "setDone",
|
||||||
enabled: true,
|
run: (controller: ReturnType<typeof createStatusReactionController>) => controller.setDone(),
|
||||||
adapter,
|
expected: DEFAULT_EMOJIS.done,
|
||||||
initialEmoji: "👀",
|
},
|
||||||
|
{
|
||||||
|
name: "setError",
|
||||||
|
run: (controller: ReturnType<typeof createStatusReactionController>) => controller.setError(),
|
||||||
|
expected: DEFAULT_EMOJIS.error,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const testCase of immediateTerminalCases) {
|
||||||
|
it(`should execute ${testCase.name} immediately without debounce`, async () => {
|
||||||
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
|
await testCase.run(controller);
|
||||||
|
await vi.runAllTimersAsync();
|
||||||
|
|
||||||
|
expect(calls).toContainEqual({ method: "set", emoji: testCase.expected });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await controller.setDone();
|
const terminalIgnoreCases = [
|
||||||
await vi.runAllTimersAsync();
|
{
|
||||||
|
name: "ignore setThinking after setDone (terminal state)",
|
||||||
|
terminal: (controller: ReturnType<typeof createStatusReactionController>) =>
|
||||||
|
controller.setDone(),
|
||||||
|
followup: (controller: ReturnType<typeof createStatusReactionController>) => {
|
||||||
|
void controller.setThinking();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ignore setTool after setError (terminal state)",
|
||||||
|
terminal: (controller: ReturnType<typeof createStatusReactionController>) =>
|
||||||
|
controller.setError(),
|
||||||
|
followup: (controller: ReturnType<typeof createStatusReactionController>) => {
|
||||||
|
void controller.setTool("exec");
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.done });
|
for (const testCase of terminalIgnoreCases) {
|
||||||
});
|
it(`should ${testCase.name}`, async () => {
|
||||||
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
it("should execute setError immediately without debounce", async () => {
|
await testCase.terminal(controller);
|
||||||
const { adapter, calls } = createMockAdapter();
|
const callsAfterTerminal = calls.length;
|
||||||
const controller = createStatusReactionController({
|
testCase.followup(controller);
|
||||||
enabled: true,
|
await vi.advanceTimersByTimeAsync(1000);
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
expect(calls.length).toBe(callsAfterTerminal);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
await controller.setError();
|
|
||||||
await vi.runAllTimersAsync();
|
|
||||||
|
|
||||||
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.error });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should ignore setThinking after setDone (terminal state)", async () => {
|
|
||||||
const { adapter, calls } = createMockAdapter();
|
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
await controller.setDone();
|
|
||||||
const callsAfterDone = calls.length;
|
|
||||||
|
|
||||||
void controller.setThinking();
|
|
||||||
await vi.advanceTimersByTimeAsync(1000);
|
|
||||||
|
|
||||||
expect(calls.length).toBe(callsAfterDone);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should ignore setTool after setError (terminal state)", async () => {
|
|
||||||
const { adapter, calls } = createMockAdapter();
|
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
await controller.setError();
|
|
||||||
const callsAfterError = calls.length;
|
|
||||||
|
|
||||||
void controller.setTool("exec");
|
|
||||||
await vi.advanceTimersByTimeAsync(1000);
|
|
||||||
|
|
||||||
expect(calls.length).toBe(callsAfterError);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should only fire last state when rapidly changing (debounce)", async () => {
|
it("should only fire last state when rapidly changing (debounce)", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController();
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setThinking();
|
void controller.setThinking();
|
||||||
await vi.advanceTimersByTimeAsync(100);
|
await vi.advanceTimersByTimeAsync(100);
|
||||||
@@ -236,12 +216,7 @@ describe("createStatusReactionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should deduplicate same emoji calls", async () => {
|
it("should deduplicate same emoji calls", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController();
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setThinking();
|
void controller.setThinking();
|
||||||
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||||
@@ -256,12 +231,7 @@ describe("createStatusReactionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should call removeReaction when adapter supports it and emoji changes", async () => {
|
it("should call removeReaction when adapter supports it and emoji changes", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController();
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setQueued();
|
void controller.setQueued();
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
@@ -302,12 +272,7 @@ describe("createStatusReactionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should clear all known emojis when adapter supports removeReaction", async () => {
|
it("should clear all known emojis when adapter supports removeReaction", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController();
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setQueued();
|
void controller.setQueued();
|
||||||
await vi.runAllTimersAsync();
|
await vi.runAllTimersAsync();
|
||||||
@@ -341,12 +306,7 @@ describe("createStatusReactionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should restore initial emoji", async () => {
|
it("should restore initial emoji", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController();
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setThinking();
|
void controller.setThinking();
|
||||||
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||||
@@ -357,17 +317,11 @@ describe("createStatusReactionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should use custom emojis when provided", async () => {
|
it("should use custom emojis when provided", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController({
|
||||||
const customEmojis = {
|
emojis: {
|
||||||
thinking: "🤔",
|
thinking: "🤔",
|
||||||
done: "🎉",
|
done: "🎉",
|
||||||
};
|
},
|
||||||
|
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
emojis: customEmojis,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
void controller.setThinking();
|
void controller.setThinking();
|
||||||
@@ -381,16 +335,10 @@ describe("createStatusReactionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should use custom timing when provided", async () => {
|
it("should use custom timing when provided", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController({
|
||||||
const customTiming = {
|
timing: {
|
||||||
debounceMs: 100,
|
debounceMs: 100,
|
||||||
};
|
},
|
||||||
|
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
timing: customTiming,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
void controller.setThinking();
|
void controller.setThinking();
|
||||||
@@ -404,47 +352,33 @@ describe("createStatusReactionController", () => {
|
|||||||
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.thinking });
|
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.thinking });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should trigger soft stall timer after stallSoftMs", async () => {
|
const stallCases = [
|
||||||
const { adapter, calls } = createMockAdapter();
|
{
|
||||||
const controller = createStatusReactionController({
|
name: "soft stall timer after stallSoftMs",
|
||||||
enabled: true,
|
delayMs: DEFAULT_TIMING.stallSoftMs,
|
||||||
adapter,
|
expected: DEFAULT_EMOJIS.stallSoft,
|
||||||
initialEmoji: "👀",
|
},
|
||||||
|
{
|
||||||
|
name: "hard stall timer after stallHardMs",
|
||||||
|
delayMs: DEFAULT_TIMING.stallHardMs,
|
||||||
|
expected: DEFAULT_EMOJIS.stallHard,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
for (const testCase of stallCases) {
|
||||||
|
it(`should trigger ${testCase.name}`, async () => {
|
||||||
|
const { calls, controller } = createEnabledController();
|
||||||
|
|
||||||
|
void controller.setThinking();
|
||||||
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||||
|
await vi.advanceTimersByTimeAsync(testCase.delayMs);
|
||||||
|
|
||||||
|
expect(calls).toContainEqual({ method: "set", emoji: testCase.expected });
|
||||||
});
|
});
|
||||||
|
}
|
||||||
void controller.setThinking();
|
|
||||||
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
|
||||||
|
|
||||||
// Advance to soft stall threshold
|
|
||||||
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.stallSoftMs);
|
|
||||||
|
|
||||||
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.stallSoft });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should trigger hard stall timer after stallHardMs", async () => {
|
|
||||||
const { adapter, calls } = createMockAdapter();
|
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setThinking();
|
|
||||||
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
|
||||||
|
|
||||||
// Advance to hard stall threshold
|
|
||||||
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.stallHardMs);
|
|
||||||
|
|
||||||
expect(calls).toContainEqual({ method: "set", emoji: DEFAULT_EMOJIS.stallHard });
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should reset stall timers on phase change", async () => {
|
it("should reset stall timers on phase change", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController();
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setThinking();
|
void controller.setThinking();
|
||||||
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||||
@@ -464,12 +398,7 @@ describe("createStatusReactionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should reset stall timers on repeated same-phase updates", async () => {
|
it("should reset stall timers on repeated same-phase updates", async () => {
|
||||||
const { adapter, calls } = createMockAdapter();
|
const { calls, controller } = createEnabledController();
|
||||||
const controller = createStatusReactionController({
|
|
||||||
enabled: true,
|
|
||||||
adapter,
|
|
||||||
initialEmoji: "👀",
|
|
||||||
});
|
|
||||||
|
|
||||||
void controller.setThinking();
|
void controller.setThinking();
|
||||||
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
await vi.advanceTimersByTimeAsync(DEFAULT_TIMING.debounceMs);
|
||||||
@@ -511,33 +440,37 @@ describe("createStatusReactionController", () => {
|
|||||||
|
|
||||||
describe("constants", () => {
|
describe("constants", () => {
|
||||||
it("should export CODING_TOOL_TOKENS", () => {
|
it("should export CODING_TOOL_TOKENS", () => {
|
||||||
expect(CODING_TOOL_TOKENS).toContain("exec");
|
for (const token of ["exec", "read", "write"]) {
|
||||||
expect(CODING_TOOL_TOKENS).toContain("read");
|
expect(CODING_TOOL_TOKENS).toContain(token);
|
||||||
expect(CODING_TOOL_TOKENS).toContain("write");
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should export WEB_TOOL_TOKENS", () => {
|
it("should export WEB_TOOL_TOKENS", () => {
|
||||||
expect(WEB_TOOL_TOKENS).toContain("web_search");
|
for (const token of ["web_search", "browser"]) {
|
||||||
expect(WEB_TOOL_TOKENS).toContain("browser");
|
expect(WEB_TOOL_TOKENS).toContain(token);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should export DEFAULT_EMOJIS with all required keys", () => {
|
it("should export DEFAULT_EMOJIS with all required keys", () => {
|
||||||
expect(DEFAULT_EMOJIS).toHaveProperty("queued");
|
const emojiKeys = [
|
||||||
expect(DEFAULT_EMOJIS).toHaveProperty("thinking");
|
"queued",
|
||||||
expect(DEFAULT_EMOJIS).toHaveProperty("tool");
|
"thinking",
|
||||||
expect(DEFAULT_EMOJIS).toHaveProperty("coding");
|
"tool",
|
||||||
expect(DEFAULT_EMOJIS).toHaveProperty("web");
|
"coding",
|
||||||
expect(DEFAULT_EMOJIS).toHaveProperty("done");
|
"web",
|
||||||
expect(DEFAULT_EMOJIS).toHaveProperty("error");
|
"done",
|
||||||
expect(DEFAULT_EMOJIS).toHaveProperty("stallSoft");
|
"error",
|
||||||
expect(DEFAULT_EMOJIS).toHaveProperty("stallHard");
|
"stallSoft",
|
||||||
|
"stallHard",
|
||||||
|
] as const;
|
||||||
|
for (const key of emojiKeys) {
|
||||||
|
expect(DEFAULT_EMOJIS).toHaveProperty(key);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should export DEFAULT_TIMING with all required keys", () => {
|
it("should export DEFAULT_TIMING with all required keys", () => {
|
||||||
expect(DEFAULT_TIMING).toHaveProperty("debounceMs");
|
for (const key of ["debounceMs", "stallSoftMs", "stallHardMs", "doneHoldMs", "errorHoldMs"]) {
|
||||||
expect(DEFAULT_TIMING).toHaveProperty("stallSoftMs");
|
expect(DEFAULT_TIMING).toHaveProperty(key);
|
||||||
expect(DEFAULT_TIMING).toHaveProperty("stallHardMs");
|
}
|
||||||
expect(DEFAULT_TIMING).toHaveProperty("doneHoldMs");
|
|
||||||
expect(DEFAULT_TIMING).toHaveProperty("errorHoldMs");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,39 +37,44 @@ describe("sessions", () => {
|
|||||||
const withStateDir = <T>(stateDir: string, fn: () => T): T =>
|
const withStateDir = <T>(stateDir: string, fn: () => T): T =>
|
||||||
withEnv({ OPENCLAW_STATE_DIR: stateDir }, fn);
|
withEnv({ OPENCLAW_STATE_DIR: stateDir }, fn);
|
||||||
|
|
||||||
it("returns normalized per-sender key", () => {
|
const deriveSessionKeyCases = [
|
||||||
expect(deriveSessionKey("per-sender", { From: "whatsapp:+1555" })).toBe("+1555");
|
{
|
||||||
});
|
name: "returns normalized per-sender key",
|
||||||
|
scope: "per-sender" as const,
|
||||||
|
ctx: { From: "whatsapp:+1555" },
|
||||||
|
expected: "+1555",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "falls back to unknown when sender missing",
|
||||||
|
scope: "per-sender" as const,
|
||||||
|
ctx: {},
|
||||||
|
expected: "unknown",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "global scope returns global",
|
||||||
|
scope: "global" as const,
|
||||||
|
ctx: { From: "+1" },
|
||||||
|
expected: "global",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keeps group chats distinct",
|
||||||
|
scope: "per-sender" as const,
|
||||||
|
ctx: { From: "12345-678@g.us" },
|
||||||
|
expected: "whatsapp:group:12345-678@g.us",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "prefixes group keys with provider when available",
|
||||||
|
scope: "per-sender" as const,
|
||||||
|
ctx: { From: "12345-678@g.us", ChatType: "group", Provider: "whatsapp" },
|
||||||
|
expected: "whatsapp:group:12345-678@g.us",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
it("falls back to unknown when sender missing", () => {
|
for (const testCase of deriveSessionKeyCases) {
|
||||||
expect(deriveSessionKey("per-sender", {})).toBe("unknown");
|
it(testCase.name, () => {
|
||||||
});
|
expect(deriveSessionKey(testCase.scope, testCase.ctx)).toBe(testCase.expected);
|
||||||
|
});
|
||||||
it("global scope returns global", () => {
|
}
|
||||||
expect(deriveSessionKey("global", { From: "+1" })).toBe("global");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps group chats distinct", () => {
|
|
||||||
expect(deriveSessionKey("per-sender", { From: "12345-678@g.us" })).toBe(
|
|
||||||
"whatsapp:group:12345-678@g.us",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("prefixes group keys with provider when available", () => {
|
|
||||||
expect(
|
|
||||||
deriveSessionKey("per-sender", {
|
|
||||||
From: "12345-678@g.us",
|
|
||||||
ChatType: "group",
|
|
||||||
Provider: "whatsapp",
|
|
||||||
}),
|
|
||||||
).toBe("whatsapp:group:12345-678@g.us");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps explicit provider when provided in group key", () => {
|
|
||||||
expect(
|
|
||||||
resolveSessionKey("per-sender", { From: "discord:group:12345", ChatType: "group" }, "main"),
|
|
||||||
).toBe("agent:main:discord:group:12345");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("builds discord display name with guild+channel slugs", () => {
|
it("builds discord display name with guild+channel slugs", () => {
|
||||||
expect(
|
expect(
|
||||||
@@ -83,35 +88,65 @@ describe("sessions", () => {
|
|||||||
).toBe("discord:friends-of-openclaw#general");
|
).toBe("discord:friends-of-openclaw#general");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("collapses direct chats to main by default", () => {
|
const resolveSessionKeyCases = [
|
||||||
expect(resolveSessionKey("per-sender", { From: "+1555" })).toBe("agent:main:main");
|
{
|
||||||
});
|
name: "keeps explicit provider when provided in group key",
|
||||||
|
scope: "per-sender" as const,
|
||||||
|
ctx: { From: "discord:group:12345", ChatType: "group" },
|
||||||
|
mainKey: "main",
|
||||||
|
expected: "agent:main:discord:group:12345",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "collapses direct chats to main by default",
|
||||||
|
scope: "per-sender" as const,
|
||||||
|
ctx: { From: "+1555" },
|
||||||
|
mainKey: undefined,
|
||||||
|
expected: "agent:main:main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "collapses direct chats to main even when sender missing",
|
||||||
|
scope: "per-sender" as const,
|
||||||
|
ctx: {},
|
||||||
|
mainKey: undefined,
|
||||||
|
expected: "agent:main:main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "maps direct chats to main key when provided",
|
||||||
|
scope: "per-sender" as const,
|
||||||
|
ctx: { From: "whatsapp:+1555" },
|
||||||
|
mainKey: "main",
|
||||||
|
expected: "agent:main:main",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "uses custom main key when provided",
|
||||||
|
scope: "per-sender" as const,
|
||||||
|
ctx: { From: "+1555" },
|
||||||
|
mainKey: "primary",
|
||||||
|
expected: "agent:main:primary",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "keeps global scope untouched",
|
||||||
|
scope: "global" as const,
|
||||||
|
ctx: { From: "+1555" },
|
||||||
|
mainKey: undefined,
|
||||||
|
expected: "global",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "leaves groups untouched even with main key",
|
||||||
|
scope: "per-sender" as const,
|
||||||
|
ctx: { From: "12345-678@g.us" },
|
||||||
|
mainKey: "main",
|
||||||
|
expected: "agent:main:whatsapp:group:12345-678@g.us",
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
it("collapses direct chats to main even when sender missing", () => {
|
for (const testCase of resolveSessionKeyCases) {
|
||||||
expect(resolveSessionKey("per-sender", {})).toBe("agent:main:main");
|
it(testCase.name, () => {
|
||||||
});
|
expect(resolveSessionKey(testCase.scope, testCase.ctx, testCase.mainKey)).toBe(
|
||||||
|
testCase.expected,
|
||||||
it("maps direct chats to main key when provided", () => {
|
);
|
||||||
expect(resolveSessionKey("per-sender", { From: "whatsapp:+1555" }, "main")).toBe(
|
});
|
||||||
"agent:main:main",
|
}
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("uses custom main key when provided", () => {
|
|
||||||
expect(resolveSessionKey("per-sender", { From: "+1555" }, "primary")).toBe(
|
|
||||||
"agent:main:primary",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("keeps global scope untouched", () => {
|
|
||||||
expect(resolveSessionKey("global", { From: "+1555" })).toBe("global");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("leaves groups untouched even with main key", () => {
|
|
||||||
expect(resolveSessionKey("per-sender", { From: "12345-678@g.us" }, "main")).toBe(
|
|
||||||
"agent:main:whatsapp:group:12345-678@g.us",
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("updateLastRoute persists channel and target", async () => {
|
it("updateLastRoute persists channel and target", async () => {
|
||||||
const mainSessionKey = "agent:main:main";
|
const mainSessionKey = "agent:main:main";
|
||||||
|
|||||||
Reference in New Issue
Block a user