Skip to content

Commit b9201e8

Browse files
committed
refactor: share announce test runtime seams
1 parent 5584af7 commit b9201e8

8 files changed

Lines changed: 161 additions & 121 deletions
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,14 @@
1+
export { loadConfig } from "../config/config.js";
2+
export {
3+
loadSessionStore,
4+
resolveAgentIdFromSessionKey,
5+
resolveMainSessionKey,
6+
resolveStorePath,
7+
} from "../config/sessions.js";
8+
export { callGateway } from "../gateway/call.js";
19
export { resolveQueueSettings } from "../auto-reply/reply/queue.js";
210
export { resolveExternalBestEffortDeliveryTarget } from "../infra/outbound/best-effort-delivery.js";
311
export { createBoundDeliveryRouter } from "../infra/outbound/bound-delivery-router.js";
412
export { resolveConversationIdFromTargets } from "../infra/outbound/conversation-id.js";
13+
export { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
14+
export { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded.js";

src/agents/subagent-announce-delivery.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,5 @@
1-
import { resolveQueueSettings } from "../auto-reply/reply/queue.js";
21
import { getChannelPlugin } from "../channels/plugins/index.js";
3-
import { loadConfig } from "../config/config.js";
4-
import {
5-
loadSessionStore,
6-
resolveAgentIdFromSessionKey,
7-
resolveMainSessionKey,
8-
resolveStorePath,
9-
} from "../config/sessions.js";
10-
import { callGateway } from "../gateway/call.js";
11-
import { resolveExternalBestEffortDeliveryTarget } from "../infra/outbound/best-effort-delivery.js";
12-
import { createBoundDeliveryRouter } from "../infra/outbound/bound-delivery-router.js";
13-
import { resolveConversationIdFromTargets } from "../infra/outbound/conversation-id.js";
142
import type { ConversationRef } from "../infra/outbound/session-binding-service.js";
15-
import { getGlobalHookRunner } from "../plugins/hook-runner-global.js";
163
import { normalizeAccountId, normalizeMainKey } from "../routing/session-key.js";
174
import { defaultRuntime } from "../runtime.js";
185
import { isCronSessionKey } from "../sessions/session-key-utils.js";
@@ -31,7 +18,21 @@ import {
3118
} from "../utils/message-channel.js";
3219
import { buildAnnounceIdempotencyKey, resolveQueueAnnounceId } from "./announce-idempotency.js";
3320
import type { AgentInternalEvent } from "./internal-events.js";
34-
import { isEmbeddedPiRunActive, queueEmbeddedPiMessage } from "./pi-embedded.js";
21+
import {
22+
callGateway,
23+
createBoundDeliveryRouter,
24+
getGlobalHookRunner,
25+
isEmbeddedPiRunActive,
26+
loadConfig,
27+
loadSessionStore,
28+
queueEmbeddedPiMessage,
29+
resolveAgentIdFromSessionKey,
30+
resolveConversationIdFromTargets,
31+
resolveExternalBestEffortDeliveryTarget,
32+
resolveMainSessionKey,
33+
resolveQueueSettings,
34+
resolveStorePath,
35+
} from "./subagent-announce-delivery.runtime.js";
3536
import {
3637
runSubagentAnnounceDispatch,
3738
type SubagentAnnounceDeliveryResult,
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import type { loadConfig } from "../config/config.js";
2+
import type { callGateway } from "../gateway/call.js";
3+
4+
type DeliveryRuntimeMockOptions = {
5+
callGateway: (request: unknown) => Promise<unknown>;
6+
loadConfig: () => ReturnType<typeof loadConfig>;
7+
loadSessionStore: (storePath: string) => unknown;
8+
resolveAgentIdFromSessionKey: (sessionKey: string) => string;
9+
resolveMainSessionKey: (cfg: unknown) => string;
10+
resolveStorePath: (store: unknown, options: unknown) => string;
11+
isEmbeddedPiRunActive: (sessionId: string) => boolean;
12+
queueEmbeddedPiMessage: (sessionId: string, text: string) => boolean;
13+
hasHooks?: () => boolean;
14+
};
15+
16+
function resolveExternalBestEffortDeliveryTarget(params: {
17+
channel?: string;
18+
to?: string;
19+
accountId?: string;
20+
threadId?: string;
21+
}) {
22+
return {
23+
deliver: Boolean(params.channel && params.to),
24+
channel: params.channel,
25+
to: params.to,
26+
accountId: params.accountId,
27+
threadId: params.threadId,
28+
};
29+
}
30+
31+
function resolveQueueSettings(params: {
32+
cfg?: {
33+
messages?: {
34+
queue?: {
35+
byChannel?: Record<string, string>;
36+
};
37+
};
38+
};
39+
channel?: string;
40+
}) {
41+
return {
42+
mode: (params.channel && params.cfg?.messages?.queue?.byChannel?.[params.channel]) ?? "none",
43+
};
44+
}
45+
46+
export function createSubagentAnnounceDeliveryRuntimeMock(options: DeliveryRuntimeMockOptions) {
47+
return {
48+
callGateway: (async <T = Record<string, unknown>>(request: Parameters<typeof callGateway>[0]) =>
49+
(await options.callGateway(request)) as T) as typeof callGateway,
50+
loadConfig: options.loadConfig,
51+
loadSessionStore: options.loadSessionStore,
52+
resolveAgentIdFromSessionKey: options.resolveAgentIdFromSessionKey,
53+
resolveMainSessionKey: options.resolveMainSessionKey,
54+
resolveStorePath: options.resolveStorePath,
55+
isEmbeddedPiRunActive: options.isEmbeddedPiRunActive,
56+
queueEmbeddedPiMessage: options.queueEmbeddedPiMessage,
57+
getGlobalHookRunner: () => ({ hasHooks: () => options.hasHooks?.() ?? false }),
58+
createBoundDeliveryRouter: () => ({
59+
resolveDestination: () => ({ mode: "none" }),
60+
}),
61+
resolveConversationIdFromTargets: () => "",
62+
resolveExternalBestEffortDeliveryTarget,
63+
resolveQueueSettings,
64+
};
65+
}

src/agents/subagent-announce.test.ts

Lines changed: 15 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { createSubagentAnnounceDeliveryRuntimeMock } from "./subagent-announce.test-support.js";
23

34
type AgentCallRequest = { method?: string; params?: Record<string, unknown> };
45

@@ -15,7 +16,7 @@ const readLatestAssistantReplyMock = vi.fn(async (_params?: unknown) => "raw sub
1516
const isEmbeddedPiRunActiveMock = vi.fn((_sessionId: string) => false);
1617
const queueEmbeddedPiMessageMock = vi.fn((_sessionId: string, _text: string) => false);
1718
const waitForEmbeddedPiRunEndMock = vi.fn(async (_sessionId: string, _timeoutMs?: number) => true);
18-
let mockConfig: Record<string, unknown> = {
19+
let mockConfig: ReturnType<(typeof import("../config/config.js"))["loadConfig"]> = {
1920
session: {
2021
mainKey: "main",
2122
scope: "per-sender",
@@ -35,16 +36,6 @@ const { subagentRegistryRuntimeMock } = vi.hoisted(() => ({
3536
},
3637
}));
3738

38-
vi.mock("../plugins/hook-runner-global.js", () => ({
39-
getGlobalHookRunner: () => ({ hasHooks: () => false }),
40-
}));
41-
vi.mock("../config/sessions.js", () => ({
42-
loadSessionStore: (storePath: string) => loadSessionStoreMock(storePath),
43-
resolveAgentIdFromSessionKey: (sessionKey: string) =>
44-
resolveAgentIdFromSessionKeyMock(sessionKey),
45-
resolveMainSessionKey: (cfg: unknown) => resolveMainSessionKeyMock(cfg),
46-
resolveStorePath: (store: unknown, options: unknown) => resolveStorePathMock(store, options),
47-
}));
4839
vi.mock("./subagent-announce.runtime.js", () => ({
4940
callGateway: (request: unknown) => callGatewayMock(request),
5041
isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId),
@@ -64,44 +55,22 @@ vi.mock("./tools/agent-step.js", () => ({
6455
readLatestAssistantReply: (params?: unknown) => readLatestAssistantReplyMock(params),
6556
}));
6657

67-
vi.mock("./subagent-announce-delivery.runtime.js", () => ({
68-
createBoundDeliveryRouter: () => ({
69-
resolveDestination: () => ({ mode: "none" }),
70-
}),
71-
resolveConversationIdFromTargets: () => "",
72-
resolveExternalBestEffortDeliveryTarget: (params: {
73-
channel?: string;
74-
to?: string;
75-
accountId?: string;
76-
threadId?: string;
77-
}) => ({
78-
deliver: Boolean(params.channel && params.to),
79-
channel: params.channel,
80-
to: params.to,
81-
accountId: params.accountId,
82-
threadId: params.threadId,
58+
vi.mock("./subagent-announce-delivery.runtime.js", () =>
59+
createSubagentAnnounceDeliveryRuntimeMock({
60+
callGateway: (request: unknown) => callGatewayMock(request),
61+
loadConfig: () => mockConfig,
62+
loadSessionStore: (storePath: string) => loadSessionStoreMock(storePath),
63+
resolveAgentIdFromSessionKey: (sessionKey: string) =>
64+
resolveAgentIdFromSessionKeyMock(sessionKey),
65+
resolveMainSessionKey: (cfg: unknown) => resolveMainSessionKeyMock(cfg),
66+
resolveStorePath: (store: unknown, options: unknown) => resolveStorePathMock(store, options),
67+
isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId),
68+
queueEmbeddedPiMessage: (sessionId: string, text: string) =>
69+
queueEmbeddedPiMessageMock(sessionId, text),
8370
}),
84-
resolveQueueSettings: (params: {
85-
cfg?: {
86-
messages?: {
87-
queue?: {
88-
byChannel?: Record<string, string>;
89-
};
90-
};
91-
};
92-
channel?: string;
93-
}) => ({
94-
mode: (params.channel && params.cfg?.messages?.queue?.byChannel?.[params.channel]) ?? "none",
95-
}),
96-
}));
97-
vi.mock("./pi-embedded.js", () => ({
98-
isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId),
99-
queueEmbeddedPiMessage: (sessionId: string, text: string) =>
100-
queueEmbeddedPiMessageMock(sessionId, text),
101-
}));
71+
);
10272

10373
vi.mock("./subagent-announce.registry.runtime.js", () => subagentRegistryRuntimeMock);
104-
import { __testing as subagentAnnounceDeliveryTesting } from "./subagent-announce-delivery.js";
10574
import { runSubagentAnnounceFlow } from "./subagent-announce.js";
10675

10776
describe("subagent announce seam flow", () => {
@@ -142,11 +111,6 @@ describe("subagent announce seam flow", () => {
142111
scope: "per-sender",
143112
},
144113
};
145-
subagentAnnounceDeliveryTesting.setDepsForTest({
146-
callGateway: (async <T = Record<string, unknown>>(request: unknown) =>
147-
(await callGatewayMock(request)) as T) as typeof import("../gateway/call.js").callGateway,
148-
loadConfig: () => mockConfig,
149-
});
150114
subagentRegistryRuntimeMock.shouldIgnorePostCompletionAnnounceForSession.mockReset();
151115
subagentRegistryRuntimeMock.shouldIgnorePostCompletionAnnounceForSession.mockReturnValue(false);
152116
subagentRegistryRuntimeMock.isSubagentSessionRunActive.mockReset();

src/agents/subagent-announce.timeout.test.ts

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
import { createSubagentAnnounceDeliveryRuntimeMock } from "./subagent-announce.test-support.js";
23

34
type GatewayCall = {
45
method?: string;
@@ -45,15 +46,6 @@ function createGatewayCallModuleMock() {
4546
};
4647
}
4748

48-
function createSessionsModuleMock() {
49-
return {
50-
loadSessionStore: vi.fn(() => sessionStore),
51-
resolveAgentIdFromSessionKey: () => "main",
52-
resolveStorePath: () => "/tmp/sessions-main.json",
53-
resolveMainSessionKey: () => "agent:main:main",
54-
};
55-
}
56-
5749
function createSubagentDepthModuleMock() {
5850
return {
5951
getSubagentDepthFromSessionStore: (sessionKey?: string) => requesterDepthResolver(sessionKey),
@@ -80,40 +72,32 @@ function createTimeoutHistoryWithNoReply() {
8072

8173
vi.mock("../gateway/call.js", createGatewayCallModuleMock);
8274
vi.mock("./subagent-depth.js", createSubagentDepthModuleMock);
83-
vi.mock("./subagent-announce-delivery.runtime.js", () => ({
84-
createBoundDeliveryRouter: () => ({
85-
resolveDestination: () => ({ mode: "none" }),
86-
}),
87-
resolveConversationIdFromTargets: () => "",
88-
resolveExternalBestEffortDeliveryTarget: (params: {
89-
channel?: string;
90-
to?: string;
91-
accountId?: string;
92-
threadId?: string;
93-
}) => ({
94-
deliver: Boolean(params.channel && params.to),
95-
channel: params.channel,
96-
to: params.to,
97-
accountId: params.accountId,
98-
threadId: params.threadId,
99-
}),
100-
resolveQueueSettings: (params: {
101-
cfg?: {
102-
messages?: {
103-
queue?: {
104-
byChannel?: Record<string, string>;
105-
};
106-
};
107-
};
108-
channel?: string;
109-
}) => ({
110-
mode: (params.channel && params.cfg?.messages?.queue?.byChannel?.[params.channel]) ?? "none",
75+
vi.mock("./subagent-announce-delivery.runtime.js", () =>
76+
createSubagentAnnounceDeliveryRuntimeMock({
77+
callGateway: async (request: unknown) => {
78+
const typed = request as GatewayCall;
79+
gatewayCalls.push(typed);
80+
if (typed.method === "chat.history") {
81+
return { messages: chatHistoryMessages };
82+
}
83+
return await callGatewayImpl(typed);
84+
},
85+
loadConfig: () => configOverride,
86+
loadSessionStore: () => sessionStore,
87+
resolveAgentIdFromSessionKey: () => "main",
88+
resolveMainSessionKey: () => "agent:main:main",
89+
resolveStorePath: () => "/tmp/sessions-main.json",
90+
isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId),
91+
queueEmbeddedPiMessage: () => false,
11192
}),
112-
}));
93+
);
11394
vi.mock("./subagent-announce.runtime.js", () => ({
11495
callGateway: createGatewayCallModuleMock().callGateway,
11596
loadConfig: () => configOverride,
116-
...createSessionsModuleMock(),
97+
loadSessionStore: vi.fn(() => sessionStore),
98+
resolveAgentIdFromSessionKey: () => "main",
99+
resolveStorePath: () => "/tmp/sessions-main.json",
100+
resolveMainSessionKey: () => "agent:main:main",
117101
isEmbeddedPiRunActive: (sessionId: string) => isEmbeddedPiRunActiveMock(sessionId),
118102
queueEmbeddedPiMessage: (_sessionId: string, _text: string) => false,
119103
waitForEmbeddedPiRunEnd: (sessionId: string, timeoutMs?: number) =>

src/gateway/method-scopes.ts

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,24 @@
11
import { getActivePluginRegistry } from "../plugins/runtime.js";
22
import { resolveReservedGatewayMethodScope } from "../shared/gateway-method-policy.js";
3+
import {
4+
ADMIN_SCOPE,
5+
APPROVALS_SCOPE,
6+
PAIRING_SCOPE,
7+
READ_SCOPE,
8+
TALK_SECRETS_SCOPE,
9+
WRITE_SCOPE,
10+
type OperatorScope,
11+
} from "./operator-scopes.js";
312

4-
export const ADMIN_SCOPE = "operator.admin" as const;
5-
export const READ_SCOPE = "operator.read" as const;
6-
export const WRITE_SCOPE = "operator.write" as const;
7-
export const APPROVALS_SCOPE = "operator.approvals" as const;
8-
export const PAIRING_SCOPE = "operator.pairing" as const;
9-
export const TALK_SECRETS_SCOPE = "operator.talk.secrets" as const;
10-
11-
export type OperatorScope =
12-
| typeof ADMIN_SCOPE
13-
| typeof READ_SCOPE
14-
| typeof WRITE_SCOPE
15-
| typeof APPROVALS_SCOPE
16-
| typeof PAIRING_SCOPE
17-
| typeof TALK_SECRETS_SCOPE;
13+
export {
14+
ADMIN_SCOPE,
15+
APPROVALS_SCOPE,
16+
PAIRING_SCOPE,
17+
READ_SCOPE,
18+
TALK_SECRETS_SCOPE,
19+
WRITE_SCOPE,
20+
type OperatorScope,
21+
};
1822

1923
export const CLI_DEFAULT_OPERATOR_SCOPES: OperatorScope[] = [
2024
ADMIN_SCOPE,

src/gateway/operator-scopes.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
export const ADMIN_SCOPE = "operator.admin" as const;
2+
export const READ_SCOPE = "operator.read" as const;
3+
export const WRITE_SCOPE = "operator.write" as const;
4+
export const APPROVALS_SCOPE = "operator.approvals" as const;
5+
export const PAIRING_SCOPE = "operator.pairing" as const;
6+
export const TALK_SECRETS_SCOPE = "operator.talk.secrets" as const;
7+
8+
export type OperatorScope =
9+
| typeof ADMIN_SCOPE
10+
| typeof READ_SCOPE
11+
| typeof WRITE_SCOPE
12+
| typeof APPROVALS_SCOPE
13+
| typeof PAIRING_SCOPE
14+
| typeof TALK_SECRETS_SCOPE;

src/gateway/server-methods/talk.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type { TalkProviderConfig } from "../../config/types.gateway.js";
55
import type { OpenClawConfig, TtsConfig, TtsProviderConfigMap } from "../../config/types.js";
66
import { canonicalizeSpeechProviderId, getSpeechProvider } from "../../tts/provider-registry.js";
77
import { synthesizeSpeech, type TtsDirectiveOverrides } from "../../tts/tts.js";
8+
import { ADMIN_SCOPE, TALK_SECRETS_SCOPE } from "../operator-scopes.js";
89
import {
910
ErrorCodes,
1011
errorShape,
@@ -16,9 +17,6 @@ import {
1617
import { formatForLog } from "../ws-log.js";
1718
import type { GatewayRequestHandlers } from "./types.js";
1819

19-
const ADMIN_SCOPE = "operator.admin";
20-
const TALK_SECRETS_SCOPE = "operator.talk.secrets";
21-
2220
function canReadTalkSecrets(client: { connect?: { scopes?: string[] } } | null): boolean {
2321
const scopes = Array.isArray(client?.connect?.scopes) ? client.connect.scopes : [];
2422
return scopes.includes(ADMIN_SCOPE) || scopes.includes(TALK_SECRETS_SCOPE);

0 commit comments

Comments
 (0)