























@@ -447,6 +447,148 @@ describe("cron tool", () => {
447447expect(params).toEqual({ includeDisabled: true, agentId: "ops" });
448448});
449449450+describe("wake routing", () => {
451+// Pin the agentId / sessionKey resolution contract for `action: "wake"`.
452+// The gateway target resolver treats `agentId` as authoritative, so
453+// pairing the caller's inferred agentId with a foreign explicit
454+// sessionKey would canonicalize the wake back to the caller agent's
455+// main lane.
456+457+it("infers sessionKey + agentId from the calling agent's session when neither is supplied", async () => {
458+const tool = createTestCronTool({
459+agentSessionKey: "agent:agent-123:telegram:direct:channing",
460+});
461+await tool.execute("call-wake-default", { action: "wake", text: "ping" });
462+const params = expectSingleGatewayCallMethod("wake");
463+expect(params).toEqual({
464+mode: "next-heartbeat",
465+text: "ping",
466+sessionKey: "agent:agent-123:telegram:direct:channing",
467+agentId: "agent-123",
468+});
469+});
470+471+it("derives agentId from an explicit cross-agent sessionKey instead of the caller's agentId", async () => {
472+// A caller in agent-123 explicitly waking an agent-456 session must
473+// NOT have agent-123's agentId paired with agent-456's sessionKey —
474+// that would canonicalize back to agent-123's main lane on the
475+// gateway side.
476+const tool = createTestCronTool({
477+agentSessionKey: "agent:agent-123:telegram:direct:channing",
478+});
479+await tool.execute("call-wake-cross-agent", {
480+action: "wake",
481+text: "follow up",
482+sessionKey: "agent:agent-456:discord:thread-xyz",
483+});
484+const params = expectSingleGatewayCallMethod("wake");
485+expect(params).toEqual({
486+mode: "next-heartbeat",
487+text: "follow up",
488+sessionKey: "agent:agent-456:discord:thread-xyz",
489+agentId: "agent-456",
490+});
491+});
492+493+it("rejects a contradictory explicit agentId + agent-prefixed sessionKey pair", async () => {
494+// The gateway target resolver treats agentId as authoritative, so a
495+// contradictory pair would silently canonicalize the wake onto a session
496+// the caller never named. The tool rejects instead of guessing.
497+const tool = createTestCronTool({
498+agentSessionKey: "agent:agent-123:telegram:direct:channing",
499+});
500+await expect(
501+tool.execute("call-wake-explicit-pair", {
502+action: "wake",
503+text: "manual",
504+sessionKey: "agent:agent-456:discord:thread-xyz",
505+agentId: "ops",
506+}),
507+).rejects.toThrow(/contradicts/);
508+expect(callGatewayMock).not.toHaveBeenCalled();
509+});
510+511+it("accepts an explicit agentId that matches the agent owning the explicit sessionKey", async () => {
512+const tool = createTestCronTool({
513+agentSessionKey: "agent:agent-123:telegram:direct:channing",
514+});
515+await tool.execute("call-wake-matching-pair", {
516+action: "wake",
517+text: "manual",
518+sessionKey: "agent:agent-456:discord:thread-xyz",
519+agentId: "agent-456",
520+});
521+const params = expectSingleGatewayCallMethod("wake");
522+expect(params).toEqual({
523+mode: "next-heartbeat",
524+text: "manual",
525+sessionKey: "agent:agent-456:discord:thread-xyz",
526+agentId: "agent-456",
527+});
528+});
529+530+it("omits agentId when explicit sessionKey is not in agent:<id>:* form and no explicit agentId is given", async () => {
531+// Defence-in-depth: if the explicit sessionKey can't be parsed for an
532+// agentId, we'd rather omit it (gateway falls back to default routing
533+// for that session) than incorrectly attach the caller's agentId.
534+const tool = createTestCronTool({
535+agentSessionKey: "agent:agent-123:telegram:direct:channing",
536+});
537+await tool.execute("call-wake-unparseable", {
538+action: "wake",
539+text: "x",
540+sessionKey: "subagent:weird:format",
541+});
542+const params = expectSingleGatewayCallMethod("wake");
543+expect(params).toEqual({
544+mode: "next-heartbeat",
545+text: "x",
546+sessionKey: "subagent:weird:format",
547+// No agentId — explicit sessionKey wasn't parseable + no explicit
548+// override, so we deliberately drop agentId rather than inherit
549+// the caller's.
550+});
551+});
552+553+it("requires text for action wake", async () => {
554+// Mutation-test survivor: `required: true` -> false silently sent an
555+// undefined-text wake. Pin the guard.
556+const tool = createTestCronTool({
557+agentSessionKey: "agent:agent-123:telegram:direct:channing",
558+});
559+await expect(tool.execute("call-wake-no-text", { action: "wake" })).rejects.toThrow();
560+expect(callGatewayMock).not.toHaveBeenCalled();
561+});
562+563+it("sends a bare wake when no calling-session context exists", async () => {
564+// Mutation-test survivor: `opts?.agentSessionKey` -> `opts.agentSessionKey`
565+// crashed context-less callers. A tool created without session context
566+// must fall through to default routing, not throw.
567+const tool = createTestCronTool();
568+await tool.execute("call-wake-no-context", { action: "wake", text: "ping" });
569+const params = expectSingleGatewayCallMethod("wake");
570+expect(params).toEqual({ mode: "next-heartbeat", text: "ping" });
571+});
572+573+it('honours an explicit mode: "next-heartbeat"', async () => {
574+const tool = createTestCronTool({
575+agentSessionKey: "agent:agent-123:telegram:direct:channing",
576+});
577+await tool.execute("call-wake-nh", { action: "wake", text: "tick", mode: "next-heartbeat" });
578+const params = expectSingleGatewayCallMethod("wake");
579+expect(params).toMatchObject({ mode: "next-heartbeat", text: "tick" });
580+});
581+582+it('threads mode: "now" through unchanged', async () => {
583+const tool = createTestCronTool({
584+agentSessionKey: "agent:agent-123:telegram:direct:channing",
585+});
586+await tool.execute("call-wake-now", { action: "wake", text: "ping", mode: "now" });
587+const params = expectSingleGatewayCallMethod("wake");
588+expect(params).toMatchObject({ mode: "now", text: "ping" });
589+});
590+});
591+450592it("documents deferred follow-up guidance in the tool description", () => {
451593const tool = createTestCronTool();
452594expect(tool.description).toContain(
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。