Download src/routing/session-key.test.ts from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 20.1 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/routing/session-key.test.ts
- Command line
-
hf download hf://SaylorTwift/openclaw/src/routing/session-key.test.ts
-
curl -L -o session-key.test.ts https://huggingface.co/SaylorTwift/openclaw/resolve/main/src/routing/session-key.test.ts
20.1 kB
| // Routing session key tests cover route-derived session key behavior. | |
| import { describe, expect, it, vi } from "vitest"; | |
| vi.unmock("./session-key.js"); | |
| import { | |
| resolveSessionStoreAgentId, | |
| resolveSessionStoreKey, | |
| } from "../gateway/session-store-key.js"; | |
| import { deriveSessionChatTypeFromKey } from "../sessions/session-chat-type-shared.js"; | |
| import { | |
| getSubagentDepth, | |
| isCronSessionKey, | |
| parseCronRunScopeSuffix, | |
| parseThreadSessionSuffix, | |
| } from "../sessions/session-key-utils.js"; | |
| import { | |
| agentSessionKeysMatchByRequestKey, | |
| buildAgentPeerSessionKey, | |
| buildGroupHistoryKey, | |
| classifySessionKeyShape, | |
| isValidAgentId, | |
| parseAgentSessionKey, | |
| resolveAgentIdFromSessionKey, | |
| resolveEventSessionKey, | |
| scopedHeartbeatWakeOptions, | |
| isUnscopedSessionKeySentinel, | |
| scopeLegacySessionKeyToAgent, | |
| toAgentStoreSessionKey, | |
| } from "./session-key.js"; | |
| describe("agent id session-key boundary", () => { | |
| it("keeps legacy keys absent at parse time and resolves them only with a configured default", () => { | |
| expect(parseAgentSessionKey("main")?.agentId).toBeUndefined(); | |
| expect(() => resolveAgentIdFromSessionKey("main")).toThrow("configured default agent"); | |
| expect(() => resolveAgentIdFromSessionKey("main", " ")).toThrow("configured default agent"); | |
| expect(resolveAgentIdFromSessionKey("main", "primary")).toBe("primary"); | |
| expect(resolveAgentIdFromSessionKey("agent:worker:main", "primary")).toBe("worker"); | |
| expect(() => resolveAgentIdFromSessionKey("agent::secret", "primary")).toThrow( | |
| "Malformed agent session key", | |
| ); | |
| }); | |
| }); | |
| describe("classifySessionKeyShape", () => { | |
| it.each([ | |
| { input: undefined, expected: "missing" }, | |
| { input: " ", expected: "missing" }, | |
| { input: "agent:main:main", expected: "agent" }, | |
| { input: "agent:research:subagent:worker", expected: "agent" }, | |
| { input: "agent::broken", expected: "malformed_agent" }, | |
| { input: "agent:main", expected: "malformed_agent" }, | |
| { input: "main", expected: "legacy_or_alias" }, | |
| { input: "custom-main", expected: "legacy_or_alias" }, | |
| { input: "subagent:worker", expected: "legacy_or_alias" }, | |
| ] as const)("classifies %j as $expected", ({ input, expected }) => { | |
| expect(classifySessionKeyShape(input)).toBe(expected); | |
| }); | |
| }); | |
| describe("scopeLegacySessionKeyToAgent", () => { | |
| it("scopes legacy aliases to the requested agent", () => { | |
| expect(scopeLegacySessionKeyToAgent({ agentId: "Ops", sessionKey: "Incident-42" })).toBe( | |
| "agent:ops:incident-42", | |
| ); | |
| }); | |
| it("honors configured main-key aliases when scoping legacy keys", () => { | |
| expect( | |
| scopeLegacySessionKeyToAgent({ agentId: "ops", sessionKey: "main", mainKey: "work" }), | |
| ).toBe("agent:ops:work"); | |
| }); | |
| it("preserves already agent-prefixed keys", () => { | |
| expect( | |
| scopeLegacySessionKeyToAgent({ | |
| agentId: "ops", | |
| sessionKey: "agent:main:incident-42", | |
| }), | |
| ).toBe("agent:main:incident-42"); | |
| }); | |
| it("scopes global and unknown legacy aliases to the requested agent", () => { | |
| expect(scopeLegacySessionKeyToAgent({ agentId: "ops", sessionKey: "global" })).toBe( | |
| "agent:ops:global", | |
| ); | |
| expect(scopeLegacySessionKeyToAgent({ agentId: "ops", sessionKey: "UNKNOWN" })).toBe( | |
| "agent:ops:unknown", | |
| ); | |
| }); | |
| }); | |
| describe("isUnscopedSessionKeySentinel", () => { | |
| it("recognizes literal global and unknown sentinels", () => { | |
| expect(isUnscopedSessionKeySentinel("global")).toBe(true); | |
| expect(isUnscopedSessionKeySentinel("UNKNOWN")).toBe(true); | |
| expect(isUnscopedSessionKeySentinel("agent:ops:global")).toBe(false); | |
| expect(isUnscopedSessionKeySentinel("incident-42")).toBe(false); | |
| }); | |
| }); | |
| describe("agentSessionKeysMatchByRequestKey", () => { | |
| it("matches canonical agent keys against their request-key aliases", () => { | |
| expect(agentSessionKeysMatchByRequestKey("agent:main:main", "main")).toBe(true); | |
| expect(agentSessionKeysMatchByRequestKey("agent:ops:incident-42", "incident-42")).toBe(true); | |
| expect(agentSessionKeysMatchByRequestKey("agent:ops:incident-42", "main")).toBe(false); | |
| }); | |
| }); | |
| describe("resolveSessionStoreKey", () => { | |
| it("scopes unprefixed explicit-agent keys to the requested store agent", () => { | |
| const cfg = { | |
| agents: { list: [{ id: "main", default: true }, { id: "ops" }] }, | |
| session: { mainKey: "primary" }, | |
| }; | |
| expect(resolveSessionStoreKey({ cfg, sessionKey: "main", storeAgentId: "ops" })).toBe( | |
| "agent:ops:primary", | |
| ); | |
| expect(resolveSessionStoreKey({ cfg, sessionKey: "discord:dm:U1", storeAgentId: "ops" })).toBe( | |
| "agent:ops:discord:dm:u1", | |
| ); | |
| }); | |
| }); | |
| describe("session key backward compatibility", () => { | |
| function expectBackwardCompatibleDirectSessionKey(key: string) { | |
| expect(classifySessionKeyShape(key)).toBe("agent"); | |
| } | |
| it.each([ | |
| "agent:main:telegram:dm:123456", | |
| "agent:main:whatsapp:dm:+15551234567", | |
| "agent:main:discord:dm:user123", | |
| "agent:main:telegram:direct:123456", | |
| "agent:main:whatsapp:direct:+15551234567", | |
| "agent:main:discord:direct:user123", | |
| ] as const)("classifies backward-compatible direct session key %s as valid", (key) => { | |
| expectBackwardCompatibleDirectSessionKey(key); | |
| }); | |
| }); | |
| describe("getSubagentDepth", () => { | |
| it.each([ | |
| { key: "agent:main:main", expected: 0 }, | |
| { key: "main", expected: 0 }, | |
| { key: undefined, expected: 0 }, | |
| { key: "agent:main:subagent:parent:subagent:child", expected: 2 }, | |
| { key: "subagent:worker", expected: 1 }, | |
| { key: "subagent:parent:subagent:child", expected: 2 }, | |
| ] as const)("returns $expected for session key %j", ({ key, expected }) => { | |
| expect(getSubagentDepth(key)).toBe(expected); | |
| }); | |
| }); | |
| describe("isCronSessionKey", () => { | |
| it.each([ | |
| { key: "agent:main:cron:job-1", expected: true }, | |
| { key: "agent:main:cron:job-1:run:run-1", expected: true }, | |
| { key: "agent:main:cron:job-1:run:run-1:subagent:worker", expected: true }, | |
| { key: "agent:main:main", expected: false }, | |
| { key: "agent:main:subagent:worker", expected: false }, | |
| { key: "cron:job-1", expected: false }, | |
| { key: undefined, expected: false }, | |
| ] as const)("matches cron key %j => $expected", ({ key, expected }) => { | |
| expect(isCronSessionKey(key)).toBe(expected); | |
| }); | |
| }); | |
| describe("deriveSessionChatTypeFromKey", () => { | |
| it.each([ | |
| { key: "agent:main:direct:user1", expected: "direct" }, | |
| { key: "agent:main:discord:direct:user1", expected: "direct" }, | |
| { key: "agent:main:telegram:group:g1", expected: "group" }, | |
| { key: "agent:main:discord:channel:c1", expected: "channel" }, | |
| { key: "agent:main:discord:guild-123:channel-456", expected: "channel" }, | |
| { key: "agent:main:channel:legacy-room", expected: "channel" }, | |
| { key: "agent:main:channel:!room:example.org", expected: "channel" }, | |
| { key: "agent:main:channel:direct:user", expected: "channel" }, | |
| { key: "agent:main:group:room:part", expected: "group" }, | |
| { key: "agent:main:group:dm:user", expected: "group" }, | |
| { key: "agent:main:whatsapp:123@g.us", expected: "group" }, | |
| { key: "agent:main:telegram:dm:123456", expected: "direct" }, | |
| { key: "telegram:dm:123456", expected: "direct" }, | |
| { key: "agent:main:matrix:channel:!Room:example.org", expected: "channel" }, | |
| { key: "agent:main:matrix:channel:!room:[2001:db8::1]", expected: "channel" }, | |
| { key: "agent:voice:agent:other:matrix:channel:!room:example.org", expected: "unknown" }, | |
| { key: "agent:main:direct", expected: "unknown" }, | |
| { key: "agent:main:demo:acct:channel", expected: "unknown" }, | |
| { key: "agent:main:telegram:group:direct:user", expected: "unknown" }, | |
| { key: "agent:main:direct:group:room", expected: "unknown" }, | |
| { key: "agent:main:dm:account:group:room", expected: "unknown" }, | |
| { key: "agent:main:demo::channel:room", expected: "unknown" }, | |
| { key: "agent::demo:direct:user", expected: "unknown" }, | |
| { key: "agent:main:main", expected: "unknown" }, | |
| { key: "agent:main", expected: "unknown" }, | |
| { key: "", expected: "unknown" }, | |
| ] as const)("derives chat type for %j => $expected", ({ key, expected }) => { | |
| expect(deriveSessionChatTypeFromKey(key)).toBe(expected); | |
| }); | |
| it("uses plugin-owned legacy chat-type hooks after canonical parsing", () => { | |
| expect( | |
| deriveSessionChatTypeFromKey("legacy-room:abc", [ | |
| (sessionKey) => (sessionKey.startsWith("legacy-room:") ? "channel" : undefined), | |
| ]), | |
| ).toBe("channel"); | |
| }); | |
| }); | |
| describe("thread session suffix parsing", () => { | |
| it("preserves feishu conversation ids that embed :topic: in the base id", () => { | |
| expect( | |
| parseThreadSessionSuffix( | |
| "agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user", | |
| ), | |
| ).toEqual({ | |
| baseSessionKey: | |
| "agent:main:feishu:group:oc_group_chat:topic:om_topic_root:sender:ou_topic_user", | |
| threadId: undefined, | |
| }); | |
| }); | |
| it("does not treat telegram :topic: as a generic thread suffix", () => { | |
| expect(parseThreadSessionSuffix("agent:main:telegram:group:-100123:topic:77")).toEqual({ | |
| baseSessionKey: "agent:main:telegram:group:-100123:topic:77", | |
| threadId: undefined, | |
| }); | |
| }); | |
| it("parses mixed-case :thread: markers without lowercasing the stored key", () => { | |
| expect( | |
| parseThreadSessionSuffix("agent:main:slack:channel:General:Thread:1699999999.0001"), | |
| ).toEqual({ | |
| baseSessionKey: "agent:main:slack:channel:General", | |
| threadId: "1699999999.0001", | |
| }); | |
| }); | |
| }); | |
| describe("cron run scope suffix parsing", () => { | |
| it("splits the per-run scope off an isolated cron key", () => { | |
| expect(parseCronRunScopeSuffix("agent:work:cron:nightly-job:run:abc-123")).toEqual({ | |
| baseSessionKey: "agent:work:cron:nightly-job", | |
| runId: "abc-123", | |
| }); | |
| }); | |
| it("parses mixed-case run markers without lowercasing the stored key", () => { | |
| expect(parseCronRunScopeSuffix("AGENT:Work:CRON:Nightly-Job:RUN:ABC-123")).toEqual({ | |
| baseSessionKey: "AGENT:Work:CRON:Nightly-Job", | |
| runId: "ABC-123", | |
| }); | |
| }); | |
| it("leaves keys without a run scope untouched", () => { | |
| expect(parseCronRunScopeSuffix("agent:main:main")).toEqual({ | |
| baseSessionKey: "agent:main:main", | |
| runId: undefined, | |
| }); | |
| }); | |
| it("does not strip a :run: segment from a non-cron key", () => { | |
| // The run scope is only ever appended to cron keys; a channel id that embeds | |
| // `:run:` must keep its identity intact. | |
| expect(parseCronRunScopeSuffix("agent:main:slack:channel:general:run:42")).toEqual({ | |
| baseSessionKey: "agent:main:slack:channel:general:run:42", | |
| runId: undefined, | |
| }); | |
| }); | |
| it("does not treat a non-terminal cron :run: as a run scope", () => { | |
| expect(parseCronRunScopeSuffix("agent:main:cron:run:job:run:abc:extra")).toEqual({ | |
| baseSessionKey: "agent:main:cron:run:job:run:abc:extra", | |
| runId: undefined, | |
| }); | |
| }); | |
| it("returns undefined for empty input", () => { | |
| expect(parseCronRunScopeSuffix(undefined)).toEqual({ | |
| baseSessionKey: undefined, | |
| runId: undefined, | |
| }); | |
| }); | |
| }); | |
| describe("stored session grammar boundaries", () => { | |
| it.each([ | |
| ["agent:ops:room::part", { agentId: "ops", rest: "room::part" }], | |
| ["agent:ops::main", null], | |
| ["agent::ops:main", null], | |
| [":agent:ops:main", null], | |
| ["agent:ops:cron:", { agentId: "ops", rest: "cron:" }], | |
| [ | |
| "Agent:Ops:Matrix:Channel:!Room:Org:Thread:$Event", | |
| { agentId: "ops", rest: "matrix:channel:!Room:Org:thread:$Event" }, | |
| ], | |
| [ | |
| "Agent:Ops:Signal:Group:AbC:Thread:XyZ", | |
| { agentId: "ops", rest: "signal:group:AbC:thread:xyz" }, | |
| ], | |
| [ | |
| "agent:ops:catalog:fixture:Host:Thread", | |
| { agentId: "ops", rest: "catalog:fixture:host:thread" }, | |
| ], | |
| ] as const)("preserves stored identity for %s", (key, expected) => { | |
| expect(parseAgentSessionKey(key)).toEqual(expected); | |
| }); | |
| }); | |
| describe("session key canonicalization", () => { | |
| function expectSessionKeyCanonicalizationCase(params: { run: () => void }) { | |
| params.run(); | |
| } | |
| it.each([ | |
| { | |
| name: "parses agent keys case-insensitively and returns lowercase tokens", | |
| run: () => | |
| expect(parseAgentSessionKey("AGENT:Main:Hook:Webhook:42")).toEqual({ | |
| agentId: "main", | |
| rest: "hook:webhook:42", | |
| }), | |
| }, | |
| { | |
| name: "preserves empty segments inside opaque agent-scoped tails", | |
| run: () => { | |
| expect(parseAgentSessionKey("agent:voice:room::part")).toEqual({ | |
| agentId: "voice", | |
| rest: "room::part", | |
| }); | |
| expect(resolveSessionStoreAgentId({}, "agent:voice:room::part")).toBe("voice"); | |
| }, | |
| }, | |
| { | |
| name: "does not double-prefix already-qualified agent keys", | |
| run: () => | |
| expect( | |
| toAgentStoreSessionKey({ | |
| agentId: "main", | |
| requestKey: "agent:main:main", | |
| }), | |
| ).toBe("agent:main:main"), | |
| }, | |
| { | |
| name: "preserves Signal group ids while lowercasing structural tokens", | |
| run: () => { | |
| const mixedGroupId = "VWATodkf2hc8zdOS76q9Tb0+5Bi522E03qLdaQ/9ypg="; | |
| expect(parseAgentSessionKey(`Agent:Main:Signal:Group:${mixedGroupId}`)).toEqual({ | |
| agentId: "main", | |
| rest: `signal:group:${mixedGroupId}`, | |
| }); | |
| expect( | |
| buildAgentPeerSessionKey({ | |
| agentId: "Main", | |
| channel: "Signal", | |
| peerKind: "group", | |
| peerId: mixedGroupId, | |
| }), | |
| ).toBe(`agent:main:signal:group:${mixedGroupId}`); | |
| expect( | |
| buildGroupHistoryKey({ | |
| channel: "Signal", | |
| peerKind: "group", | |
| peerId: mixedGroupId, | |
| }), | |
| ).toBe(`signal:default:group:${mixedGroupId}`); | |
| }, | |
| }, | |
| { | |
| name: "keeps non-Signal opaque-looking group ids lowercase", | |
| run: () => | |
| expect( | |
| buildAgentPeerSessionKey({ | |
| agentId: "Main", | |
| channel: "Telegram", | |
| peerKind: "group", | |
| peerId: "MiXeDGroup", | |
| }), | |
| ).toBe("agent:main:telegram:group:mixedgroup"), | |
| }, | |
| ] as const)("$name", ({ run }) => { | |
| expectSessionKeyCanonicalizationCase({ run }); | |
| }); | |
| }); | |
| describe("scopedHeartbeatWakeOptions", () => { | |
| it("remaps ephemeral cron run sessions to agent main key", () => { | |
| const result = scopedHeartbeatWakeOptions("agent:main:cron:backup:run:abc", { | |
| reason: "exec:123:exit", | |
| }); | |
| expect(result).toEqual({ reason: "exec:123:exit", sessionKey: "agent:main:main" }); | |
| }); | |
| it("preserves durable cron base sessions (not remapped)", () => { | |
| const result = scopedHeartbeatWakeOptions("agent:main:cron:backup", { | |
| reason: "exec:123:exit", | |
| }); | |
| expect(result).toEqual({ reason: "exec:123:exit", sessionKey: "agent:main:cron:backup" }); | |
| }); | |
| it("preserves sessionKey for regular agent sessions", () => { | |
| const result = scopedHeartbeatWakeOptions("agent:main:main", { | |
| reason: "exec:123:exit", | |
| }); | |
| expect(result).toEqual({ reason: "exec:123:exit", sessionKey: "agent:main:main" }); | |
| }); | |
| it("strips sessionKey for non-agent keys", () => { | |
| const result = scopedHeartbeatWakeOptions("main", { reason: "test" }); | |
| expect(result).toEqual({ reason: "test" }); | |
| expect("sessionKey" in result).toBe(false); | |
| }); | |
| it("strips sessionKey for global-scope sessions to preserve unscoped wake behavior", () => { | |
| // In session.scope = "global" setups, resolveMainSessionKeyFromConfig() returns "global". | |
| // Passing "global" as sessionKey into requestHeartbeatNow would create a targeted wake | |
| // that can fail to resolve, breaking hook-triggered heartbeats. scopedHeartbeatWakeOptions | |
| // must strip it to preserve the old unscoped behavior. | |
| const result = scopedHeartbeatWakeOptions("global", { reason: "hook:wake" }); | |
| expect(result).toEqual({ reason: "hook:wake" }); | |
| expect("sessionKey" in result).toBe(false); | |
| }); | |
| it("drops sessionKey but preserves agentId for cron-run keys when scope is global", () => { | |
| // Global-scope agents drain the "global" queue automatically; a targeted | |
| // wake on agent:<id>:main would be unresolvable. Carry the agent target | |
| // so multi-agent global-scope setups still wake the originating agent. | |
| const result = scopedHeartbeatWakeOptions( | |
| "agent:ops:cron:job-1:run:xyz", | |
| { reason: "exec-event" }, | |
| undefined, | |
| "global", | |
| ); | |
| expect(result).toEqual({ reason: "exec-event", agentId: "ops" }); | |
| expect("sessionKey" in result).toBe(false); | |
| }); | |
| it("threads custom mainKey for cron-run keys under per-sender scope", () => { | |
| const result = scopedHeartbeatWakeOptions( | |
| "agent:main:cron:backup:run:abc", | |
| { reason: "exec-event" }, | |
| "primary", | |
| "per-sender", | |
| ); | |
| expect(result).toEqual({ reason: "exec-event", sessionKey: "agent:main:primary" }); | |
| }); | |
| }); | |
| describe("resolveEventSessionKey", () => { | |
| it("remaps ephemeral cron run session keys to agent main session key", () => { | |
| expect(resolveEventSessionKey("agent:main:cron:backup:run:abc123")).toBe("agent:main:main"); | |
| expect(resolveEventSessionKey("agent:ops:cron:job-1:run:xyz")).toBe("agent:ops:main"); | |
| }); | |
| it("collapses cron-run descendant session keys to the agent main session key", () => { | |
| expect(resolveEventSessionKey("agent:main:cron:backup:run:abc123:subagent:worker")).toBe( | |
| "agent:main:main", | |
| ); | |
| expect(resolveEventSessionKey("agent:ops:cron:job-1:run:xyz:thread:reply")).toBe( | |
| "agent:ops:main", | |
| ); | |
| }); | |
| it("preserves durable cron base session keys", () => { | |
| expect(resolveEventSessionKey("agent:ops:cron:job-1")).toBe("agent:ops:cron:job-1"); | |
| expect(resolveEventSessionKey("agent:main:cron:backup")).toBe("agent:main:cron:backup"); | |
| }); | |
| it("respects custom mainKey for ephemeral cron session remapping", () => { | |
| expect(resolveEventSessionKey("agent:main:cron:backup:run:abc123", "primary")).toBe( | |
| "agent:main:primary", | |
| ); | |
| expect(resolveEventSessionKey("agent:ops:cron:job-1:run:xyz", "primary")).toBe( | |
| "agent:ops:primary", | |
| ); | |
| }); | |
| it("passes through non-cron session keys unchanged", () => { | |
| expect(resolveEventSessionKey("agent:main:main")).toBe("agent:main:main"); | |
| expect(resolveEventSessionKey("agent:main:discord:direct:user1")).toBe( | |
| "agent:main:discord:direct:user1", | |
| ); | |
| }); | |
| it("passes through non-agent keys unchanged", () => { | |
| expect(resolveEventSessionKey("main")).toBe("main"); | |
| expect(resolveEventSessionKey("global")).toBe("global"); | |
| }); | |
| it("routes cron-run keys to the global queue when scope is global", () => { | |
| // resolveHeartbeatSession drains the literal "global" queue for global-scope | |
| // sessions; remapping to agent:<id>:main would strand the event. | |
| expect(resolveEventSessionKey("agent:ops:cron:job-1:run:xyz", undefined, "global")).toBe( | |
| "global", | |
| ); | |
| expect(resolveEventSessionKey("agent:main:cron:backup:run:abc", "primary", "global")).toBe( | |
| "global", | |
| ); | |
| expect( | |
| resolveEventSessionKey("agent:main:cron:backup:run:abc:subagent:worker", "primary", "global"), | |
| ).toBe("global"); | |
| }); | |
| it("treats explicit per-sender scope identically to omitted scope", () => { | |
| expect( | |
| resolveEventSessionKey("agent:main:cron:backup:run:abc123", undefined, "per-sender"), | |
| ).toBe("agent:main:main"); | |
| expect( | |
| resolveEventSessionKey("agent:main:cron:backup:run:abc123", "primary", "per-sender"), | |
| ).toBe("agent:main:primary"); | |
| }); | |
| }); | |
| describe("isValidAgentId", () => { | |
| it.each([ | |
| { input: "main", expected: true }, | |
| { input: "my-research_agent01", expected: true }, | |
| { input: "", expected: false }, | |
| { input: "Agent not found: xyz", expected: false }, | |
| { input: "../../../etc/passwd", expected: false }, | |
| { input: "a".repeat(65), expected: false }, | |
| ] as const)("validates agent id %j => $expected", ({ input, expected }) => { | |
| expect(isValidAgentId(input)).toBe(expected); | |
| }); | |
| }); | |