shadow-claw 1.40.0 → 1.40.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.well-known/mcp/server-card.json +2 -2
- package/.well-known/mcp/servers.json +1 -1
- package/.well-known/mcp.json +2 -2
- package/dist/cli/cli.js +2 -2
- package/dist/lib/components/index.js +2 -2
- package/dist/lib/components/shadow-claw-file-viewer/shadow-claw-file-viewer.d.ts +1 -1
- package/dist/lib/components/shadow-claw-file-viewer.js +1 -1
- package/dist/lib/components/shadow-claw.js +1 -1
- package/dist/lib/core/orchestrator/orchestrator-state.d.ts +2 -1
- package/dist/lib/core/orchestrator/orchestrator.d.ts +2 -1
- package/dist/lib/core/orchestrator/utils/operations/task.d.ts +1 -0
- package/dist/lib/index.js +2 -2
- package/dist/lib/{orchestrator-Bq9ft3uG.js → orchestrator-CMN43L0F.js} +9 -13
- package/dist/lib/orchestrator-CMN43L0F.js.map +1 -0
- package/dist/lib/orchestrator-D9f5fDCb.js.map +1 -1
- package/dist/lib/{shadow-claw-MumteAJy.js → shadow-claw-4x3LqlII.js} +2 -2
- package/dist/lib/{shadow-claw-MumteAJy.js.map → shadow-claw-4x3LqlII.js.map} +1 -1
- package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js → shadow-claw-file-viewer-CuDhkVcL.js} +21 -21
- package/dist/lib/{shadow-claw-file-viewer-CMOnFmcP.js.map → shadow-claw-file-viewer-CuDhkVcL.js.map} +1 -1
- package/dist/lib/syncWebMcpRegistration-Bbph0XZl.js.map +1 -1
- package/dist/public/.well-known/mcp/server-card.json +2 -2
- package/dist/public/.well-known/mcp/servers.json +1 -1
- package/dist/public/.well-known/mcp.json +2 -2
- package/dist/public/docs/decisions/native-web-components-and-signals.md +1 -1
- package/dist/public/docs/example/article/index.html +2 -2
- package/dist/public/docs/publishing/index.html +2 -2
- package/dist/public/docs/skill-creator/index.html +2 -2
- package/dist/public/index.html +2 -2
- package/dist/public/index.js +1 -1
- package/dist/public/main/index.html +2 -2
- package/dist/public/main/memory/index.html +2 -2
- package/dist/public/{orchestrator-BLkS_jpm.js → orchestrator-Dc_Q73La.js} +1 -1
- package/dist/public/service-worker.js +1 -1
- package/dist/public/{shadow-claw-DmrxnX3Q.js → shadow-claw-Dabb2AEp.js} +2 -2
- package/dist/public/{shadow-claw-file-viewer-a_uRA30F.js → shadow-claw-file-viewer-CDs1QGXk.js} +1 -1
- package/dist/server.js +1 -1
- package/package.json +1 -1
- package/src/cli/cli.ts +2 -2
- package/src/components/shadow-claw-file-viewer/shadow-claw-file-viewer.ts +31 -31
- package/src/core/effect.test.ts +19 -0
- package/src/core/orchestrator/orchestrator-state.ts +2 -1
- package/src/core/orchestrator/orchestrator.test.ts +686 -5
- package/src/core/orchestrator/orchestrator.ts +3 -10
- package/src/core/orchestrator/utils/compactContext.test.ts +423 -121
- package/src/core/orchestrator/utils/configurePeerJs.test.ts +27 -0
- package/src/core/orchestrator/utils/deliverResponse.test.ts +127 -0
- package/src/core/orchestrator/utils/dispatchSubagentInvoke.test.ts +23 -0
- package/src/core/orchestrator/utils/enqueue.test.ts +520 -86
- package/src/core/orchestrator/utils/handleWorkerMessage.test.ts +664 -144
- package/src/core/orchestrator/utils/initTasks.node.test.ts +147 -0
- package/src/core/orchestrator/utils/initTasks.test.ts +926 -191
- package/src/core/orchestrator/utils/initTasks.ts +5 -4
- package/src/core/orchestrator/utils/invokeAgent.test.ts +563 -110
- package/src/core/orchestrator/utils/invokeAgent.ts +8 -10
- package/src/core/orchestrator/utils/loadChannelConfigurations.test.ts +50 -0
- package/src/core/orchestrator/utils/operations/channel.test.ts +377 -170
- package/src/core/orchestrator/utils/operations/provider.test.ts +663 -85
- package/src/core/orchestrator/utils/operations/room.test.ts +74 -11
- package/src/core/orchestrator/utils/operations/task.test.ts +452 -56
- package/src/core/orchestrator/utils/operations/task.ts +1 -1
- package/src/core/orchestrator/utils/operations/vm.test.ts +66 -7
- package/src/core/orchestrator/utils/parseDirectToolCommand.test.ts +6 -0
- package/src/core/orchestrator/utils/settings.test.ts +248 -81
- package/src/core/orchestrator/utils/setupPushTaskListener.test.ts +58 -0
- package/src/core/orchestrator/utils/syncWebMcpRegistration.test.ts +341 -65
- package/src/core/orchestrator/utils/syncWebMcpRegistration.ts +4 -2
- package/src/server/routes/mcp.ts +2 -2
- package/src/stores/file-viewer.test.ts +11 -0
- package/src/stores/theme.test.ts +41 -2
- package/src/stores/toast.test.ts +25 -0
- package/src/worker/tools/bash/native-bash-executor.test.ts +31 -1
- package/src/worker/tools/git/utils/extractConflictPaths.test.ts +24 -0
- package/src/worker/tools/git/utils/parseConflictRegions.test.ts +46 -0
- package/src/worker/tools/git/utils/resolveCorsProxy.test.ts +26 -5
- package/src/worker/tools/git/utils/truncateSnippet.test.ts +16 -0
- package/src/worker/tools/node-transformers-executor.test.ts +105 -6
- package/src/worker/tools/remote-mcp/remote-mcp.test.ts +172 -47
- package/src/worker/tools/remote-mcp/utils/formatListToolsOutput.test.ts +19 -0
- package/src/worker/tools/remote-mcp/utils/isMcpReauthError.test.ts +45 -0
- package/src/worker/tools/remote-mcp/utils/resolveMcpReauth.test.ts +48 -0
- package/src/worker/tools/rooms/list-room-members.test.ts +24 -10
- package/src/worker/tools/tasks/list-tasks.test.ts +39 -8
- package/src/worker/tools/tasks/update-task.test.ts +78 -14
- package/src/worker/tools/ui/javascript.test.ts +70 -17
- package/src/worker/tools/ui/list-tool-profiles.test.ts +31 -9
- package/dist/lib/orchestrator-Bq9ft3uG.js.map +0 -1
|
@@ -1,52 +1,128 @@
|
|
|
1
|
-
import { jest } from "@jest/globals";
|
|
1
|
+
import { beforeEach, describe, expect, it, jest } from "@jest/globals";
|
|
2
|
+
import { DEFAULT_GROUP_ID } from "../../../../config/config.js";
|
|
3
|
+
import type { MessageAttachment } from "../../../../content/types.js";
|
|
4
|
+
import type { ShadowClawDatabase } from "../../../../db/db.js";
|
|
5
|
+
import type {
|
|
6
|
+
Channel,
|
|
7
|
+
ChannelType,
|
|
8
|
+
} from "../../../../subsystems/channels/types.js";
|
|
9
|
+
import type { OrchestratorState } from "../../orchestrator-state.js";
|
|
2
10
|
|
|
3
|
-
|
|
4
|
-
|
|
11
|
+
const mockDefaultSetConfig =
|
|
12
|
+
jest.fn<
|
|
13
|
+
(_db: ShadowClawDatabase, _key: string, _val: string) => Promise<void>
|
|
14
|
+
>();
|
|
15
|
+
const mockDefaultGetConfig =
|
|
16
|
+
jest.fn<
|
|
17
|
+
(_db: ShadowClawDatabase, _key: string) => Promise<string | undefined>
|
|
18
|
+
>();
|
|
19
|
+
const mockEncryptValue = jest.fn<(val: string) => Promise<string | null>>();
|
|
20
|
+
const mockDecryptValue = jest.fn<(val: string) => Promise<string | null>>();
|
|
21
|
+
const mockSetRemoteAgentTyping =
|
|
22
|
+
jest.fn<(groupId: string, typing: boolean) => void>();
|
|
23
|
+
|
|
24
|
+
jest.unstable_mockModule("../../../../db/setConfig.js", () => ({
|
|
25
|
+
setConfig: mockDefaultSetConfig,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.unstable_mockModule("../../../../db/getConfig.js", () => ({
|
|
29
|
+
getConfig: mockDefaultGetConfig,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
jest.unstable_mockModule("../../../../security/crypto.js", () => ({
|
|
33
|
+
encryptValue: mockEncryptValue,
|
|
34
|
+
decryptValue: mockDecryptValue,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
jest.unstable_mockModule("../../../../stores/orchestrator.js", () => ({
|
|
38
|
+
orchestratorStore: {
|
|
39
|
+
setRemoteAgentTyping: mockSetRemoteAgentTyping,
|
|
40
|
+
},
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const {
|
|
44
|
+
applyAllChannelRunningStates,
|
|
45
|
+
applyChannelRunningState,
|
|
46
|
+
clearPeerJsTypingState,
|
|
47
|
+
configureIMessage,
|
|
48
|
+
configureTelegram,
|
|
5
49
|
getChannelByType,
|
|
6
50
|
getChannelEnabled,
|
|
51
|
+
getChannelEnabledConfigKey,
|
|
52
|
+
getChannelTypeForGroup,
|
|
53
|
+
getIMessageConfig,
|
|
7
54
|
getPeerJsConfig,
|
|
8
55
|
getTelegramConfig,
|
|
9
|
-
getIMessageConfig,
|
|
10
|
-
getChannelEnabledConfigKey,
|
|
11
|
-
shouldRunChannel,
|
|
12
|
-
applyAllChannelRunningStates,
|
|
13
|
-
clearPeerJsTypingState,
|
|
14
56
|
loadChannelEnabled,
|
|
57
|
+
saveSecretConfig,
|
|
15
58
|
setChannelEnabled,
|
|
59
|
+
shouldRunChannel,
|
|
16
60
|
submitMessage,
|
|
17
|
-
|
|
18
|
-
configureIMessage,
|
|
19
|
-
configureTelegram,
|
|
20
|
-
} from "./channel.js";
|
|
61
|
+
} = await import("./channel.js");
|
|
21
62
|
|
|
22
|
-
|
|
23
|
-
|
|
63
|
+
interface MockChannel extends Channel {
|
|
64
|
+
ensureConnected?: (force: boolean) => void;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function makeChannel(
|
|
68
|
+
running = false,
|
|
69
|
+
ensureConnected?: (force: boolean) => void,
|
|
70
|
+
): MockChannel {
|
|
71
|
+
const ch: MockChannel = {
|
|
72
|
+
running,
|
|
73
|
+
start: jest.fn(),
|
|
74
|
+
stop: jest.fn(),
|
|
75
|
+
submit: jest.fn(),
|
|
76
|
+
configure: jest.fn(),
|
|
77
|
+
} as unknown as MockChannel;
|
|
78
|
+
if (ensureConnected) {
|
|
79
|
+
ch.ensureConnected = ensureConnected;
|
|
80
|
+
}
|
|
81
|
+
return ch;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface MockOrchestratorState {
|
|
85
|
+
browserChat: MockChannel;
|
|
86
|
+
telegram: MockChannel;
|
|
87
|
+
imessage: MockChannel;
|
|
88
|
+
peerjs: MockChannel;
|
|
89
|
+
channelEnabledByType: Partial<Record<ChannelType, boolean>>;
|
|
90
|
+
telegramBotToken: string;
|
|
91
|
+
telegramChatIds: string[];
|
|
92
|
+
telegramUseProxy: boolean;
|
|
93
|
+
imessageServerUrl: string;
|
|
94
|
+
imessageApiKey: string;
|
|
95
|
+
imessageChatIds: string[];
|
|
96
|
+
peerjsMyAlias: string;
|
|
97
|
+
peerjsMyPeerId: string;
|
|
98
|
+
peerjsPeerAliases: Record<string, string>;
|
|
99
|
+
peerjsServerHost: string;
|
|
100
|
+
peerjsServerPath: string;
|
|
101
|
+
peerjsServerPort: number;
|
|
102
|
+
peerjsServerSecure: boolean;
|
|
103
|
+
peerjsTrustedPeerIds: string[];
|
|
104
|
+
}
|
|
24
105
|
|
|
25
|
-
function makeState(overrides: Partial<
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
stop: jest.fn(),
|
|
40
|
-
configure: jest.fn(),
|
|
41
|
-
} as any,
|
|
42
|
-
peerjs: { start: jest.fn(), stop: jest.fn(), configure: jest.fn() } as any,
|
|
43
|
-
channelEnabledByType: { telegram: false, imessage: false, peerjs: false },
|
|
106
|
+
function makeState(overrides: Partial<MockOrchestratorState> = {}) {
|
|
107
|
+
const defaultChannelEnabled: Record<ChannelType, boolean> = {
|
|
108
|
+
browser: true,
|
|
109
|
+
telegram: false,
|
|
110
|
+
imessage: false,
|
|
111
|
+
peerjs: false,
|
|
112
|
+
room: false,
|
|
113
|
+
};
|
|
114
|
+
const base: MockOrchestratorState = {
|
|
115
|
+
browserChat: makeChannel(),
|
|
116
|
+
telegram: makeChannel(),
|
|
117
|
+
imessage: makeChannel(),
|
|
118
|
+
peerjs: makeChannel(),
|
|
119
|
+
channelEnabledByType: { ...defaultChannelEnabled },
|
|
44
120
|
telegramBotToken: "",
|
|
45
|
-
telegramChatIds: [],
|
|
121
|
+
telegramChatIds: [] as string[],
|
|
46
122
|
telegramUseProxy: false,
|
|
47
123
|
imessageServerUrl: "",
|
|
48
124
|
imessageApiKey: "",
|
|
49
|
-
imessageChatIds: [],
|
|
125
|
+
imessageChatIds: [] as string[],
|
|
50
126
|
peerjsMyAlias: "",
|
|
51
127
|
peerjsMyPeerId: "",
|
|
52
128
|
peerjsPeerAliases: {},
|
|
@@ -54,30 +130,45 @@ function makeState(overrides: Partial<OrchestratorState> = {}) {
|
|
|
54
130
|
peerjsServerPath: "/",
|
|
55
131
|
peerjsServerPort: 443,
|
|
56
132
|
peerjsServerSecure: true,
|
|
57
|
-
peerjsTrustedPeerIds: [],
|
|
58
|
-
|
|
59
|
-
|
|
133
|
+
peerjsTrustedPeerIds: [] as string[],
|
|
134
|
+
};
|
|
135
|
+
const merged = Object.assign(base, overrides);
|
|
136
|
+
if (overrides.channelEnabledByType) {
|
|
137
|
+
merged.channelEnabledByType = {
|
|
138
|
+
...defaultChannelEnabled,
|
|
139
|
+
...overrides.channelEnabledByType,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
return merged as unknown as OrchestratorState;
|
|
60
143
|
}
|
|
61
144
|
|
|
62
145
|
describe("channel operations", () => {
|
|
146
|
+
const mockDb = {} as ShadowClawDatabase;
|
|
147
|
+
|
|
148
|
+
beforeEach(() => {
|
|
149
|
+
jest.clearAllMocks();
|
|
150
|
+
mockDefaultSetConfig.mockResolvedValue(undefined);
|
|
151
|
+
mockDefaultGetConfig.mockResolvedValue(undefined);
|
|
152
|
+
mockEncryptValue.mockImplementation(async (val) => `enc:${val}`);
|
|
153
|
+
});
|
|
154
|
+
|
|
63
155
|
it("getChannelTypeForGroup returns registered type or default browser", () => {
|
|
64
156
|
const stateWithRegistry = {
|
|
65
157
|
channelRegistry: {
|
|
66
|
-
getChannelType:
|
|
67
|
-
gId.startsWith("tg:") ? "telegram" : null,
|
|
68
|
-
),
|
|
158
|
+
getChannelType: (gId: string) =>
|
|
159
|
+
gId.startsWith("tg:") ? ("telegram" as ChannelType) : null,
|
|
69
160
|
},
|
|
70
161
|
};
|
|
71
|
-
expect(getChannelTypeForGroup(stateWithRegistry
|
|
162
|
+
expect(getChannelTypeForGroup(stateWithRegistry, "tg:123")).toBe(
|
|
72
163
|
"telegram",
|
|
73
164
|
);
|
|
74
|
-
expect(getChannelTypeForGroup(stateWithRegistry
|
|
165
|
+
expect(getChannelTypeForGroup(stateWithRegistry, "br:main")).toBe(
|
|
75
166
|
"browser",
|
|
76
167
|
);
|
|
77
168
|
expect(getChannelTypeForGroup({}, "br:main")).toBe("browser");
|
|
78
169
|
});
|
|
79
170
|
|
|
80
|
-
it("getChannelByType returns the correct channel", () => {
|
|
171
|
+
it("getChannelByType returns the correct channel or null for unknown", () => {
|
|
81
172
|
const state = makeState();
|
|
82
173
|
expect(getChannelByType(state, "browser")).toBe(state.browserChat);
|
|
83
174
|
expect(getChannelByType(state, "telegram")).toBe(state.telegram);
|
|
@@ -86,214 +177,306 @@ describe("channel operations", () => {
|
|
|
86
177
|
expect(getChannelByType(state, "unknown" as ChannelType)).toBeNull();
|
|
87
178
|
});
|
|
88
179
|
|
|
89
|
-
it("getChannelEnabled
|
|
180
|
+
it("getChannelEnabled returns true for browser and checks map for others", () => {
|
|
90
181
|
const state = makeState({
|
|
91
182
|
channelEnabledByType: {
|
|
92
183
|
telegram: true,
|
|
93
184
|
imessage: false,
|
|
94
185
|
peerjs: false,
|
|
95
|
-
}
|
|
186
|
+
},
|
|
96
187
|
});
|
|
97
188
|
expect(getChannelEnabled(state, "browser")).toBe(true);
|
|
98
189
|
expect(getChannelEnabled(state, "telegram")).toBe(true);
|
|
99
190
|
expect(getChannelEnabled(state, "imessage")).toBe(false);
|
|
100
191
|
});
|
|
101
192
|
|
|
102
|
-
it("
|
|
193
|
+
it("getChannelEnabledConfigKey formats key properly", () => {
|
|
194
|
+
expect(getChannelEnabledConfigKey("telegram")).toBe(
|
|
195
|
+
"channel_enabled:telegram",
|
|
196
|
+
);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it("getTelegramConfig, getIMessageConfig, and getPeerJsConfig return current config", () => {
|
|
103
200
|
const state = makeState({
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
imessage: true,
|
|
107
|
-
peerjs: true,
|
|
108
|
-
} as any,
|
|
109
|
-
telegramBotToken: "tok-1",
|
|
110
|
-
telegramChatIds: ["chat-1"],
|
|
201
|
+
telegramBotToken: "tok",
|
|
202
|
+
telegramChatIds: ["tg1"],
|
|
111
203
|
telegramUseProxy: true,
|
|
112
|
-
imessageServerUrl: "http://
|
|
113
|
-
imessageApiKey: "key
|
|
114
|
-
imessageChatIds: ["
|
|
115
|
-
peerjsMyAlias: "
|
|
116
|
-
peerjsMyPeerId: "
|
|
117
|
-
peerjsPeerAliases: {
|
|
118
|
-
peerjsServerHost: "
|
|
119
|
-
peerjsServerPath: "/
|
|
204
|
+
imessageServerUrl: "http://imsg",
|
|
205
|
+
imessageApiKey: "key",
|
|
206
|
+
imessageChatIds: ["im1"],
|
|
207
|
+
peerjsMyAlias: "alias",
|
|
208
|
+
peerjsMyPeerId: "peer1",
|
|
209
|
+
peerjsPeerAliases: { p2: "a2" },
|
|
210
|
+
peerjsServerHost: "host",
|
|
211
|
+
peerjsServerPath: "/p",
|
|
120
212
|
peerjsServerPort: 9000,
|
|
121
213
|
peerjsServerSecure: false,
|
|
122
|
-
peerjsTrustedPeerIds: ["
|
|
214
|
+
peerjsTrustedPeerIds: ["trust1"],
|
|
123
215
|
});
|
|
124
216
|
|
|
125
217
|
expect(getTelegramConfig(state)).toEqual({
|
|
126
|
-
botToken: "tok
|
|
127
|
-
chatIds: ["
|
|
128
|
-
enabled:
|
|
218
|
+
botToken: "tok",
|
|
219
|
+
chatIds: ["tg1"],
|
|
220
|
+
enabled: false,
|
|
129
221
|
useProxy: true,
|
|
130
222
|
});
|
|
131
223
|
|
|
132
224
|
expect(getIMessageConfig(state)).toEqual({
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
225
|
+
apiKey: "key",
|
|
226
|
+
chatIds: ["im1"],
|
|
227
|
+
enabled: false,
|
|
228
|
+
serverUrl: "http://imsg",
|
|
137
229
|
});
|
|
138
230
|
|
|
139
231
|
expect(getPeerJsConfig(state)).toEqual({
|
|
140
|
-
enabled:
|
|
141
|
-
myAlias: "
|
|
142
|
-
myPeerId: "
|
|
143
|
-
peerAliases: {
|
|
144
|
-
serverHost: "
|
|
145
|
-
serverPath: "/
|
|
232
|
+
enabled: false,
|
|
233
|
+
myAlias: "alias",
|
|
234
|
+
myPeerId: "peer1",
|
|
235
|
+
peerAliases: { p2: "a2" },
|
|
236
|
+
serverHost: "host",
|
|
237
|
+
serverPath: "/p",
|
|
146
238
|
serverPort: 9000,
|
|
147
239
|
serverSecure: false,
|
|
148
|
-
trustedPeerIds: ["
|
|
240
|
+
trustedPeerIds: ["trust1"],
|
|
149
241
|
});
|
|
150
242
|
});
|
|
151
243
|
|
|
152
|
-
it("
|
|
153
|
-
expect(getChannelEnabledConfigKey("telegram")).toBe(
|
|
154
|
-
"channel_enabled:telegram",
|
|
155
|
-
);
|
|
156
|
-
expect(getChannelEnabledConfigKey("imessage")).toBe(
|
|
157
|
-
"channel_enabled:imessage",
|
|
158
|
-
);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("shouldRunChannel works", () => {
|
|
244
|
+
it("shouldRunChannel evaluates readiness for each channel type", () => {
|
|
162
245
|
const state = makeState({
|
|
163
246
|
channelEnabledByType: {
|
|
164
247
|
telegram: true,
|
|
165
|
-
peerjs: false,
|
|
166
248
|
imessage: true,
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
249
|
+
peerjs: true,
|
|
250
|
+
custom: true,
|
|
251
|
+
} as unknown as Record<ChannelType, boolean>,
|
|
252
|
+
telegramBotToken: "tok",
|
|
253
|
+
imessageServerUrl: "http://imsg",
|
|
170
254
|
peerjsMyPeerId: "peer1",
|
|
171
255
|
});
|
|
256
|
+
|
|
172
257
|
expect(shouldRunChannel(state, "browser")).toBe(true);
|
|
173
258
|
expect(shouldRunChannel(state, "telegram")).toBe(true);
|
|
174
259
|
expect(shouldRunChannel(state, "imessage")).toBe(true);
|
|
175
|
-
expect(shouldRunChannel(state, "peerjs")).toBe(
|
|
176
|
-
|
|
260
|
+
expect(shouldRunChannel(state, "peerjs")).toBe(true);
|
|
261
|
+
expect(shouldRunChannel(state, "custom" as ChannelType)).toBe(true);
|
|
177
262
|
|
|
178
|
-
|
|
179
|
-
const
|
|
263
|
+
// When disabled
|
|
264
|
+
const disabledState = makeState();
|
|
265
|
+
expect(shouldRunChannel(disabledState, "telegram")).toBe(false);
|
|
266
|
+
|
|
267
|
+
// When enabled but missing token/url/id
|
|
268
|
+
const incompleteState = makeState({
|
|
180
269
|
channelEnabledByType: {
|
|
181
270
|
telegram: true,
|
|
182
|
-
imessage:
|
|
183
|
-
peerjs:
|
|
184
|
-
}
|
|
185
|
-
telegramBotToken: "token",
|
|
271
|
+
imessage: true,
|
|
272
|
+
peerjs: true,
|
|
273
|
+
},
|
|
186
274
|
});
|
|
275
|
+
expect(shouldRunChannel(incompleteState, "telegram")).toBe(false);
|
|
276
|
+
expect(shouldRunChannel(incompleteState, "imessage")).toBe(false);
|
|
277
|
+
expect(shouldRunChannel(incompleteState, "peerjs")).toBe(false);
|
|
278
|
+
});
|
|
187
279
|
|
|
188
|
-
|
|
189
|
-
|
|
280
|
+
it("applyChannelRunningState handles null channel, starting, stopping, and ensureConnected", () => {
|
|
281
|
+
const state = makeState();
|
|
282
|
+
|
|
283
|
+
// Unknown channel does nothing
|
|
284
|
+
expect(() =>
|
|
285
|
+
applyChannelRunningState(state, "unknown" as ChannelType),
|
|
286
|
+
).not.toThrow();
|
|
287
|
+
|
|
288
|
+
// Not running and should run -> starts channel
|
|
289
|
+
state.channelEnabledByType.telegram = true;
|
|
290
|
+
state.telegramBotToken = "token";
|
|
291
|
+
applyChannelRunningState(state, "telegram");
|
|
190
292
|
expect(state.telegram.start).toHaveBeenCalled();
|
|
191
|
-
|
|
192
|
-
|
|
293
|
+
|
|
294
|
+
// Already running and should run -> calls ensureConnected if present
|
|
295
|
+
const ensureMock = jest.fn();
|
|
296
|
+
const runningTelegram = makeChannel(true, ensureMock);
|
|
297
|
+
const runningState = makeState({
|
|
298
|
+
channelEnabledByType: { telegram: true, imessage: false, peerjs: false },
|
|
299
|
+
telegramBotToken: "token",
|
|
300
|
+
telegram: runningTelegram,
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
applyChannelRunningState(runningState, "telegram", true);
|
|
304
|
+
expect(ensureMock).toHaveBeenCalledWith(true);
|
|
305
|
+
|
|
306
|
+
// Running but should not run -> stops channel
|
|
307
|
+
const shouldStopTelegram = makeChannel(true);
|
|
308
|
+
const stopState = makeState({
|
|
309
|
+
telegram: shouldStopTelegram,
|
|
310
|
+
});
|
|
311
|
+
applyChannelRunningState(stopState, "telegram");
|
|
312
|
+
expect(shouldStopTelegram.stop).toHaveBeenCalled();
|
|
193
313
|
});
|
|
194
314
|
|
|
195
|
-
it("
|
|
196
|
-
const ensureConnectedMock = jest.fn();
|
|
315
|
+
it("applyAllChannelRunningStates applies state across all remote channels", () => {
|
|
197
316
|
const state = makeState({
|
|
198
|
-
channelEnabledByType: { telegram: true
|
|
317
|
+
channelEnabledByType: { telegram: true, imessage: false, peerjs: false },
|
|
199
318
|
telegramBotToken: "token",
|
|
200
|
-
telegram: {
|
|
201
|
-
running: true,
|
|
202
|
-
start: jest.fn(),
|
|
203
|
-
stop: jest.fn(),
|
|
204
|
-
ensureConnected: ensureConnectedMock,
|
|
205
|
-
} as any,
|
|
206
319
|
});
|
|
207
320
|
|
|
208
|
-
applyAllChannelRunningStates(state
|
|
209
|
-
expect(
|
|
321
|
+
applyAllChannelRunningStates(state);
|
|
322
|
+
expect(state.telegram.start).toHaveBeenCalled();
|
|
210
323
|
});
|
|
211
324
|
|
|
212
|
-
it("clearPeerJsTypingState
|
|
213
|
-
|
|
325
|
+
it("clearPeerJsTypingState clears typing indicators on orchestratorStore", () => {
|
|
326
|
+
mockSetRemoteAgentTyping.mockClear();
|
|
327
|
+
clearPeerJsTypingState("group-123");
|
|
328
|
+
expect(mockSetRemoteAgentTyping).toHaveBeenCalledWith("group-123", false);
|
|
214
329
|
});
|
|
215
330
|
|
|
216
|
-
it("loadChannelEnabled
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
.mockResolvedValueOnce("false" as never)
|
|
221
|
-
.mockResolvedValueOnce(undefined as never);
|
|
222
|
-
|
|
223
|
-
expect(
|
|
224
|
-
await loadChannelEnabled("telegram", {} as any, mockGetConfig as any),
|
|
225
|
-
).toBe(true);
|
|
226
|
-
expect(
|
|
227
|
-
await loadChannelEnabled("telegram", {} as any, mockGetConfig as any),
|
|
228
|
-
).toBe(false);
|
|
229
|
-
expect(
|
|
230
|
-
await loadChannelEnabled("telegram", {} as any, mockGetConfig as any),
|
|
231
|
-
).toBe(false);
|
|
232
|
-
});
|
|
331
|
+
it("loadChannelEnabled uses default getConfig or passed getConfig", async () => {
|
|
332
|
+
mockDefaultGetConfig.mockResolvedValueOnce("true");
|
|
333
|
+
const loadedDefault = await loadChannelEnabled("telegram", mockDb);
|
|
334
|
+
expect(loadedDefault).toBe(true);
|
|
233
335
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
336
|
+
const customGetConfig = jest
|
|
337
|
+
.fn<
|
|
338
|
+
(_db: ShadowClawDatabase, _key: string) => Promise<string | undefined>
|
|
339
|
+
>()
|
|
340
|
+
.mockResolvedValue("false");
|
|
341
|
+
const loadedCustom = await loadChannelEnabled(
|
|
342
|
+
"imessage",
|
|
343
|
+
mockDb,
|
|
344
|
+
customGetConfig,
|
|
345
|
+
);
|
|
346
|
+
expect(loadedCustom).toBe(false);
|
|
239
347
|
|
|
240
|
-
await
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
true,
|
|
245
|
-
mockSetConfig as any,
|
|
348
|
+
const loadedMissing = await loadChannelEnabled(
|
|
349
|
+
"telegram",
|
|
350
|
+
mockDb,
|
|
351
|
+
async () => undefined,
|
|
246
352
|
);
|
|
247
|
-
expect(
|
|
353
|
+
expect(loadedMissing).toBe(false);
|
|
248
354
|
|
|
249
|
-
await
|
|
250
|
-
state,
|
|
251
|
-
{} as any,
|
|
355
|
+
const loadedEmpty = await loadChannelEnabled(
|
|
252
356
|
"telegram",
|
|
253
|
-
|
|
254
|
-
|
|
357
|
+
mockDb,
|
|
358
|
+
async () => "",
|
|
255
359
|
);
|
|
256
|
-
expect(
|
|
257
|
-
|
|
258
|
-
|
|
360
|
+
expect(loadedEmpty).toBe(false);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("setChannelEnabled uses default setConfig or passed setConfig", async () => {
|
|
364
|
+
const state = makeState();
|
|
365
|
+
await setChannelEnabled(state, mockDb, "telegram", true);
|
|
366
|
+
expect(state.channelEnabledByType.telegram).toBe(true);
|
|
367
|
+
expect(mockDefaultSetConfig).toHaveBeenCalledWith(
|
|
368
|
+
mockDb,
|
|
259
369
|
"channel_enabled:telegram",
|
|
260
370
|
"true",
|
|
261
371
|
);
|
|
262
|
-
|
|
372
|
+
|
|
373
|
+
const customSetConfig = jest
|
|
374
|
+
.fn<
|
|
375
|
+
(_db: ShadowClawDatabase, _key: string, _val: string) => Promise<void>
|
|
376
|
+
>()
|
|
377
|
+
.mockResolvedValue(undefined);
|
|
378
|
+
await setChannelEnabled(state, mockDb, "imessage", false, customSetConfig);
|
|
379
|
+
expect(state.channelEnabledByType.imessage).toBe(false);
|
|
380
|
+
expect(customSetConfig).toHaveBeenCalledWith(
|
|
381
|
+
mockDb,
|
|
382
|
+
"channel_enabled:imessage",
|
|
383
|
+
"false",
|
|
384
|
+
);
|
|
385
|
+
|
|
386
|
+
// channelType === "browser" returns early without setting config
|
|
387
|
+
mockDefaultSetConfig.mockClear();
|
|
388
|
+
await setChannelEnabled(state, mockDb, "browser", true);
|
|
389
|
+
expect(mockDefaultSetConfig).not.toHaveBeenCalled();
|
|
263
390
|
});
|
|
264
391
|
|
|
265
|
-
it("submitMessage proxies call to browserChat", () => {
|
|
392
|
+
it("submitMessage proxies call to browserChat with explicit and default arguments", () => {
|
|
266
393
|
const state = makeState();
|
|
267
|
-
|
|
394
|
+
const attachments: MessageAttachment[] = [];
|
|
395
|
+
submitMessage(state, "Hello agent", "group-1", attachments);
|
|
268
396
|
expect(state.browserChat.submit).toHaveBeenCalledWith(
|
|
269
397
|
"Hello agent",
|
|
270
398
|
"group-1",
|
|
399
|
+
attachments,
|
|
400
|
+
undefined,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
submitMessage(state, "Default params test");
|
|
404
|
+
expect(state.browserChat.submit).toHaveBeenCalledWith(
|
|
405
|
+
"Default params test",
|
|
406
|
+
DEFAULT_GROUP_ID,
|
|
271
407
|
[],
|
|
272
408
|
undefined,
|
|
273
409
|
);
|
|
274
410
|
});
|
|
275
411
|
|
|
276
|
-
it("
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
412
|
+
it("applyChannelRunningState handles running channel without ensureConnected", () => {
|
|
413
|
+
const state = makeState({
|
|
414
|
+
telegram: makeChannel(true),
|
|
415
|
+
channelEnabledByType: { telegram: true, imessage: false, peerjs: false },
|
|
416
|
+
telegramBotToken: "tok",
|
|
417
|
+
});
|
|
418
|
+
applyChannelRunningState(state, "telegram");
|
|
419
|
+
expect(state.telegram.start).not.toHaveBeenCalled();
|
|
280
420
|
});
|
|
281
421
|
|
|
282
|
-
it("
|
|
422
|
+
it("saveSecretConfig handles empty, encrypted values, and encryption errors", async () => {
|
|
423
|
+
const mockSetConfig = jest
|
|
424
|
+
.fn<
|
|
425
|
+
(_db: ShadowClawDatabase, _key: string, _val: string) => Promise<void>
|
|
426
|
+
>()
|
|
427
|
+
.mockResolvedValue(undefined);
|
|
428
|
+
|
|
429
|
+
// Empty value clears config
|
|
430
|
+
await saveSecretConfig(mockDb, "key1", "", mockSetConfig);
|
|
431
|
+
expect(mockSetConfig).toHaveBeenCalledWith(mockDb, "key1", "");
|
|
432
|
+
|
|
433
|
+
// Non-empty value encrypts and sets config
|
|
434
|
+
await saveSecretConfig(mockDb, "key2", "super-secret", mockSetConfig);
|
|
435
|
+
expect(mockEncryptValue).toHaveBeenCalledWith("super-secret");
|
|
436
|
+
expect(mockSetConfig).toHaveBeenCalledWith(
|
|
437
|
+
mockDb,
|
|
438
|
+
"key2",
|
|
439
|
+
"enc:super-secret",
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
// Uses default setConfig when omitted
|
|
443
|
+
await saveSecretConfig(mockDb, "key3", "val");
|
|
444
|
+
expect(mockDefaultSetConfig).toHaveBeenCalledWith(
|
|
445
|
+
mockDb,
|
|
446
|
+
"key3",
|
|
447
|
+
"enc:val",
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// Throws if encryptValue returns null/empty
|
|
451
|
+
mockEncryptValue.mockResolvedValueOnce(null);
|
|
452
|
+
await expect(
|
|
453
|
+
saveSecretConfig(mockDb, "key4", "failed-val"),
|
|
454
|
+
).rejects.toThrow("Failed to encrypt secret config for key4");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it("configureIMessage and configureTelegram use custom and default configs", async () => {
|
|
283
458
|
const state = makeState({
|
|
284
|
-
channelEnabledByType: { telegram: true, imessage: true
|
|
459
|
+
channelEnabledByType: { telegram: true, imessage: true, peerjs: false },
|
|
285
460
|
});
|
|
286
|
-
const mockSetConfig = jest
|
|
287
|
-
|
|
461
|
+
const mockSetConfig = jest
|
|
462
|
+
.fn<
|
|
463
|
+
(_db: ShadowClawDatabase, _key: string, _val: string) => Promise<void>
|
|
464
|
+
>()
|
|
465
|
+
.mockResolvedValue(undefined);
|
|
466
|
+
const mockSaveSecret = jest
|
|
467
|
+
.fn<
|
|
468
|
+
(_db: ShadowClawDatabase, _key: string, _val: string) => Promise<void>
|
|
469
|
+
>()
|
|
470
|
+
.mockResolvedValue(undefined);
|
|
288
471
|
|
|
289
472
|
await configureIMessage(
|
|
290
473
|
state,
|
|
291
|
-
|
|
474
|
+
mockDb,
|
|
292
475
|
"http://imessage.local/",
|
|
293
476
|
"secret-key",
|
|
294
477
|
["chat1", "chat2"],
|
|
295
|
-
mockSetConfig
|
|
296
|
-
mockSaveSecret
|
|
478
|
+
mockSetConfig,
|
|
479
|
+
mockSaveSecret,
|
|
297
480
|
);
|
|
298
481
|
|
|
299
482
|
expect(state.imessage.stop).toHaveBeenCalled();
|
|
@@ -304,14 +487,27 @@ describe("channel operations", () => {
|
|
|
304
487
|
);
|
|
305
488
|
expect(state.imessage.start).toHaveBeenCalled();
|
|
306
489
|
|
|
490
|
+
// configureIMessage with defaults and empty url
|
|
491
|
+
const stateImDisabled = makeState({
|
|
492
|
+
channelEnabledByType: { telegram: false, imessage: false, peerjs: false },
|
|
493
|
+
});
|
|
494
|
+
await configureIMessage(stateImDisabled, mockDb, "", "", []);
|
|
495
|
+
expect(stateImDisabled.imessage.start).not.toHaveBeenCalled();
|
|
496
|
+
|
|
497
|
+
// configureIMessage with url but channel disabled
|
|
498
|
+
await configureIMessage(stateImDisabled, mockDb, "http://imsg.local", "k", [
|
|
499
|
+
"c",
|
|
500
|
+
]);
|
|
501
|
+
expect(stateImDisabled.imessage.start).not.toHaveBeenCalled();
|
|
502
|
+
|
|
307
503
|
await configureTelegram(
|
|
308
504
|
state,
|
|
309
|
-
|
|
505
|
+
mockDb,
|
|
310
506
|
"telegram-token",
|
|
311
507
|
["tg-chat-1"],
|
|
312
508
|
true,
|
|
313
|
-
mockSetConfig
|
|
314
|
-
mockSaveSecret
|
|
509
|
+
mockSetConfig,
|
|
510
|
+
mockSaveSecret,
|
|
315
511
|
);
|
|
316
512
|
|
|
317
513
|
expect(state.telegram.stop).toHaveBeenCalled();
|
|
@@ -321,5 +517,16 @@ describe("channel operations", () => {
|
|
|
321
517
|
true,
|
|
322
518
|
);
|
|
323
519
|
expect(state.telegram.start).toHaveBeenCalled();
|
|
520
|
+
|
|
521
|
+
// Call configureTelegram with defaults and empty token
|
|
522
|
+
const stateTgDisabled = makeState({
|
|
523
|
+
channelEnabledByType: { telegram: false, imessage: false, peerjs: false },
|
|
524
|
+
});
|
|
525
|
+
await configureTelegram(stateTgDisabled, mockDb, "", []);
|
|
526
|
+
expect(stateTgDisabled.telegram.start).not.toHaveBeenCalled();
|
|
527
|
+
|
|
528
|
+
// Call configureTelegram with token but channel disabled
|
|
529
|
+
await configureTelegram(stateTgDisabled, mockDb, "valid-token", ["chat"]);
|
|
530
|
+
expect(stateTgDisabled.telegram.start).not.toHaveBeenCalled();
|
|
324
531
|
});
|
|
325
532
|
});
|