@vellumai/assistant 0.4.37 → 0.4.41
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/ARCHITECTURE.md +3 -3
- package/README.md +13 -13
- package/bun.lock +80 -24
- package/docs/architecture/integrations.md +126 -128
- package/docs/runbook-trusted-contacts.md +1 -1
- package/docs/trusted-contact-access.md +12 -12
- package/package.json +3 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -14
- package/src/__tests__/app-bundler.test.ts +209 -0
- package/src/__tests__/app-compiler.test.ts +279 -0
- package/src/__tests__/app-executors.test.ts +293 -483
- package/src/__tests__/app-migration.test.ts +148 -0
- package/src/__tests__/app-routes-csp.test.ts +202 -0
- package/src/__tests__/avatar-e2e.test.ts +452 -0
- package/src/__tests__/avatar-generator.test.ts +193 -0
- package/src/__tests__/avatar-router.test.ts +186 -0
- package/src/__tests__/browser-download-timeout.test.ts +28 -0
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +9 -9
- package/src/__tests__/call-domain.test.ts +3 -7
- package/src/__tests__/credential-security-e2e.test.ts +19 -12
- package/src/__tests__/credentials-cli.test.ts +30 -4
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +1 -1
- package/src/__tests__/handlers-slack-config.test.ts +0 -72
- package/src/__tests__/handlers-telegram-config.test.ts +19 -12
- package/src/__tests__/handlers-twitter-config.test.ts +105 -48
- package/src/__tests__/inbound-invite-redemption.test.ts +4 -4
- package/src/__tests__/integration-status.test.ts +15 -5
- package/src/__tests__/integrations-cli.test.ts +1 -1
- package/src/__tests__/invite-redemption-service.test.ts +62 -7
- package/src/__tests__/ipc-snapshot.test.ts +0 -8
- package/src/__tests__/managed-avatar-client.test.ts +280 -0
- package/src/__tests__/mcp-cli.test.ts +3 -3
- package/src/__tests__/oauth-cli.test.ts +203 -0
- package/src/__tests__/relay-server.test.ts +3 -3
- package/src/__tests__/secret-onetime-send.test.ts +19 -12
- package/src/__tests__/secure-keys.test.ts +78 -0
- package/src/__tests__/session-messaging-secret-redirect.test.ts +3 -0
- package/src/__tests__/slack-channel-config.test.ts +23 -16
- package/src/__tests__/slack-share-routes.test.ts +263 -0
- package/src/__tests__/sms-messaging-provider.test.ts +3 -1
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +7 -7
- package/src/__tests__/trusted-contact-multichannel.test.ts +3 -3
- package/src/__tests__/trusted-contact-verification.test.ts +10 -10
- package/src/__tests__/twilio-config.test.ts +15 -36
- package/src/__tests__/twilio-provider.test.ts +4 -0
- package/src/__tests__/twitter-auth-handler.test.ts +27 -14
- package/src/__tests__/twitter-cli-error-shaping.test.ts +1 -1
- package/src/__tests__/twitter-cli-routing.test.ts +38 -53
- package/src/__tests__/twitter-oauth-client.test.ts +18 -47
- package/src/__tests__/voice-invite-redemption.test.ts +27 -3
- package/src/amazon/cart.ts +1 -1
- package/src/amazon/client.ts +89 -7
- package/src/approvals/guardian-request-resolvers.ts +2 -2
- package/src/bundler/app-bundler.ts +77 -32
- package/src/bundler/app-compiler.ts +195 -0
- package/src/bundler/manifest.ts +1 -1
- package/src/bundler/package-resolver.ts +185 -0
- package/src/calls/call-domain.ts +4 -14
- package/src/calls/relay-server.ts +2 -2
- package/src/calls/twilio-config.ts +5 -24
- package/src/calls/twilio-rest.ts +19 -5
- package/src/cli/amazon.ts +74 -249
- package/src/cli/audit.ts +2 -2
- package/src/cli/autonomy.ts +9 -9
- package/src/cli/channels.ts +5 -5
- package/src/cli/completions.ts +27 -27
- package/src/cli/config.ts +14 -14
- package/src/cli/contacts.ts +27 -27
- package/src/cli/credentials.ts +28 -28
- package/src/cli/dev.ts +2 -2
- package/src/cli/doctor.ts +2 -2
- package/src/cli/email.ts +82 -82
- package/src/cli/influencer.ts +13 -13
- package/src/cli/integrations.ts +19 -144
- package/src/cli/keys.ts +10 -10
- package/src/cli/map.ts +4 -4
- package/src/cli/mcp.ts +17 -17
- package/src/cli/memory.ts +18 -18
- package/src/cli/notifications.ts +13 -13
- package/src/cli/oauth.ts +77 -0
- package/src/cli/program.ts +2 -0
- package/src/cli/sequence.ts +27 -27
- package/src/cli/sessions.ts +12 -12
- package/src/cli/trust.ts +8 -8
- package/src/cli/twitter.ts +124 -70
- package/src/config/bundled-skills/_shared/CLI_RETRIEVAL_PATTERN.md +1 -1
- package/src/config/bundled-skills/agentmail/SKILL.md +34 -34
- package/src/config/bundled-skills/amazon/SKILL.md +54 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +137 -3
- package/src/config/bundled-skills/app-builder/tools/app-create.ts +10 -4
- package/src/config/bundled-skills/configure-settings/SKILL.md +18 -18
- package/src/config/bundled-skills/contacts/SKILL.md +12 -12
- package/src/config/bundled-skills/doordash/lib/client.ts +7 -9
- package/src/config/bundled-skills/email-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/frontend-design/icon.svg +16 -0
- package/src/config/bundled-skills/google-oauth-setup/SKILL.md +143 -162
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +4 -4
- package/src/config/bundled-skills/influencer/SKILL.md +13 -13
- package/src/config/bundled-skills/mcp-setup/SKILL.md +11 -11
- package/src/config/bundled-skills/phone-calls/SKILL.md +48 -54
- package/src/config/bundled-skills/public-ingress/SKILL.md +6 -6
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +1 -1
- package/src/config/bundled-skills/sms-setup/SKILL.md +3 -3
- package/src/config/bundled-skills/telegram-setup/SKILL.md +2 -2
- package/src/config/bundled-skills/twilio-setup/SKILL.md +136 -225
- package/src/config/bundled-skills/twitter/SKILL.md +68 -44
- package/src/config/bundled-skills/voice-setup/SKILL.md +2 -2
- package/src/config/core-schema.ts +26 -0
- package/src/config/env.ts +4 -0
- package/src/config/feature-flag-registry.json +9 -1
- package/src/config/schema.ts +8 -0
- package/src/config/system-prompt.ts +6 -3
- package/src/config/templates/BOOTSTRAP.md +7 -5
- package/src/contacts/contacts-write.ts +5 -1
- package/src/daemon/handlers/apps.ts +31 -4
- package/src/daemon/handlers/config-ingress.ts +3 -3
- package/src/daemon/handlers/config-integrations.ts +120 -49
- package/src/daemon/handlers/config-slack-channel.ts +26 -7
- package/src/daemon/handlers/config-slack.ts +1 -54
- package/src/daemon/handlers/config-telegram.ts +28 -10
- package/src/daemon/handlers/config.ts +1 -4
- package/src/daemon/handlers/twitter-auth.ts +11 -4
- package/src/daemon/ipc-contract/apps.ts +0 -13
- package/src/daemon/ipc-contract-inventory.json +0 -2
- package/src/daemon/lifecycle.ts +8 -1
- package/src/daemon/session-messaging.ts +2 -2
- package/src/daemon/tool-side-effects.ts +30 -0
- package/src/email/providers/agentmail.ts +1 -1
- package/src/email/providers/index.ts +1 -1
- package/src/email/service.ts +1 -1
- package/src/gallery/default-gallery.ts +538 -0
- package/src/gallery/gallery-manifest.ts +5 -1
- package/src/influencer/client.ts +8 -6
- package/src/mcp/client.ts +1 -1
- package/src/media/avatar-router.ts +99 -0
- package/src/media/avatar-types.ts +60 -0
- package/src/media/managed-avatar-client.ts +189 -0
- package/src/memory/app-migration.ts +114 -0
- package/src/memory/app-store.ts +11 -0
- package/src/memory/qdrant-client.ts +1 -1
- package/src/messaging/providers/slack/client.ts +12 -2
- package/src/messaging/providers/sms/adapter.ts +6 -10
- package/src/migrations/data-layout.ts +8 -1
- package/src/oauth/token-persistence.ts +9 -6
- package/src/runtime/assistant-scope.ts +5 -0
- package/src/runtime/auth/route-policy.ts +4 -0
- package/src/runtime/channel-readiness-service.ts +9 -4
- package/src/runtime/gateway-internal-client.ts +11 -3
- package/src/runtime/http-server.ts +2 -0
- package/src/runtime/invite-redemption-service.ts +23 -13
- package/src/runtime/middleware/twilio-validation.ts +2 -2
- package/src/runtime/routes/app-routes.ts +131 -3
- package/src/runtime/routes/inbound-stages/verification-intercept.ts +3 -3
- package/src/runtime/routes/integration-routes.ts +2 -2
- package/src/runtime/routes/slack-share-routes.ts +235 -0
- package/src/runtime/routes/twilio-routes.ts +47 -34
- package/src/schedule/integration-status.ts +2 -3
- package/src/security/token-manager.ts +11 -3
- package/src/tools/apps/executors.ts +116 -8
- package/src/tools/browser/browser-manager.ts +30 -2
- package/src/tools/browser/chrome-cdp.ts +31 -3
- package/src/tools/credentials/vault.ts +9 -7
- package/src/tools/executor.ts +4 -0
- package/src/tools/system/avatar-generator.ts +55 -34
- package/src/twitter/client.ts +1 -1
- package/src/twitter/oauth-client.ts +31 -43
- package/src/twitter/router.ts +25 -23
- package/src/util/platform.ts +5 -0
- package/src/slack/slack-webhook.ts +0 -66
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock state
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
|
|
7
|
+
let mockStrategy: string | undefined;
|
|
8
|
+
let mockGeminiKey: string | undefined = "test-gemini-key";
|
|
9
|
+
let mockManagedAvailable = true;
|
|
10
|
+
let mockManagedResult: unknown;
|
|
11
|
+
let mockManagedError: Error | undefined;
|
|
12
|
+
let mockGeminiResult: unknown;
|
|
13
|
+
|
|
14
|
+
const generateManagedAvatarFn = mock(async () => {
|
|
15
|
+
if (mockManagedError) throw mockManagedError;
|
|
16
|
+
return mockManagedResult;
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
const generateImageFn = mock(async () => mockGeminiResult);
|
|
20
|
+
|
|
21
|
+
const isManagedAvailableFn = mock(() => mockManagedAvailable);
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Mock modules — before importing module under test
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
mock.module("../config/loader.js", () => ({
|
|
28
|
+
getConfig: () => ({
|
|
29
|
+
apiKeys: { gemini: mockGeminiKey },
|
|
30
|
+
avatar: { generationStrategy: mockStrategy ?? "local_only" },
|
|
31
|
+
}),
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
mock.module("../util/logger.js", () => ({
|
|
35
|
+
getLogger: () => ({
|
|
36
|
+
debug: () => {},
|
|
37
|
+
info: () => {},
|
|
38
|
+
warn: () => {},
|
|
39
|
+
error: () => {},
|
|
40
|
+
}),
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module("../media/managed-avatar-client.js", () => ({
|
|
44
|
+
isManagedAvailable: isManagedAvailableFn,
|
|
45
|
+
generateManagedAvatar: generateManagedAvatarFn,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
mock.module("../media/gemini-image-service.js", () => ({
|
|
49
|
+
generateImage: generateImageFn,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Import after mocking
|
|
53
|
+
import {
|
|
54
|
+
getAvatarStrategy,
|
|
55
|
+
routedGenerateAvatar,
|
|
56
|
+
} from "../media/avatar-router.js";
|
|
57
|
+
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Helpers
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
|
|
62
|
+
function managedResponse() {
|
|
63
|
+
return {
|
|
64
|
+
image: {
|
|
65
|
+
mime_type: "image/png",
|
|
66
|
+
data_base64: "base64data",
|
|
67
|
+
bytes: 1024,
|
|
68
|
+
sha256: "abc",
|
|
69
|
+
},
|
|
70
|
+
usage: { billable: false, class_name: "assistant_avatar_system" },
|
|
71
|
+
generation_source: "vertex",
|
|
72
|
+
profile: "avatar_v1",
|
|
73
|
+
correlation_id: "test-correlation-id",
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function geminiResponse() {
|
|
78
|
+
return {
|
|
79
|
+
images: [{ mimeType: "image/png", dataBase64: "base64data" }],
|
|
80
|
+
resolvedModel: "gemini-2.5-flash-image",
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Tests
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
|
|
88
|
+
describe("avatar-router", () => {
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
mockStrategy = undefined;
|
|
91
|
+
mockGeminiKey = "test-gemini-key";
|
|
92
|
+
mockManagedAvailable = true;
|
|
93
|
+
mockManagedResult = managedResponse();
|
|
94
|
+
mockManagedError = undefined;
|
|
95
|
+
mockGeminiResult = geminiResponse();
|
|
96
|
+
generateManagedAvatarFn.mockClear();
|
|
97
|
+
generateImageFn.mockClear();
|
|
98
|
+
isManagedAvailableFn.mockClear();
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// 1. managed_required — managed success
|
|
102
|
+
test("managed_required returns pathUsed managed on success", async () => {
|
|
103
|
+
mockStrategy = "managed_required";
|
|
104
|
+
const result = await routedGenerateAvatar("a cute cat");
|
|
105
|
+
expect(result.pathUsed).toBe("managed");
|
|
106
|
+
expect(result.imageBase64).toBe("base64data");
|
|
107
|
+
expect(result.mimeType).toBe("image/png");
|
|
108
|
+
expect(generateManagedAvatarFn).toHaveBeenCalledTimes(1);
|
|
109
|
+
expect(generateImageFn).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
// 2. managed_required — managed failure throws, no fallback
|
|
113
|
+
test("managed_required throws on managed failure without fallback", async () => {
|
|
114
|
+
mockStrategy = "managed_required";
|
|
115
|
+
mockManagedError = new Error("upstream error");
|
|
116
|
+
await expect(routedGenerateAvatar("a cute cat")).rejects.toThrow(
|
|
117
|
+
"upstream error",
|
|
118
|
+
);
|
|
119
|
+
expect(generateImageFn).not.toHaveBeenCalled();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 3. local_only — calls local Gemini, never managed
|
|
123
|
+
test("local_only calls local Gemini and never managed", async () => {
|
|
124
|
+
mockStrategy = "local_only";
|
|
125
|
+
const result = await routedGenerateAvatar("a cute cat");
|
|
126
|
+
expect(result.pathUsed).toBe("local");
|
|
127
|
+
expect(result.imageBase64).toBe("base64data");
|
|
128
|
+
expect(generateImageFn).toHaveBeenCalledTimes(1);
|
|
129
|
+
expect(generateManagedAvatarFn).not.toHaveBeenCalled();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// 4. local_only — missing Gemini API key throws
|
|
133
|
+
test("local_only throws when Gemini API key is missing", async () => {
|
|
134
|
+
mockStrategy = "local_only";
|
|
135
|
+
mockGeminiKey = undefined;
|
|
136
|
+
// Also clear the env var to ensure no fallback
|
|
137
|
+
const saved = process.env.GEMINI_API_KEY;
|
|
138
|
+
delete process.env.GEMINI_API_KEY;
|
|
139
|
+
try {
|
|
140
|
+
await expect(routedGenerateAvatar("a cute cat")).rejects.toThrow(
|
|
141
|
+
"Gemini API key is not configured",
|
|
142
|
+
);
|
|
143
|
+
expect(generateImageFn).not.toHaveBeenCalled();
|
|
144
|
+
} finally {
|
|
145
|
+
if (saved !== undefined) process.env.GEMINI_API_KEY = saved;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 5. managed_prefer — managed success
|
|
150
|
+
test("managed_prefer returns pathUsed managed on success", async () => {
|
|
151
|
+
mockStrategy = "managed_prefer";
|
|
152
|
+
const result = await routedGenerateAvatar("a cute cat");
|
|
153
|
+
expect(result.pathUsed).toBe("managed");
|
|
154
|
+
expect(generateManagedAvatarFn).toHaveBeenCalledTimes(1);
|
|
155
|
+
expect(generateImageFn).not.toHaveBeenCalled();
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
// 6. managed_prefer — managed failure falls back to local
|
|
159
|
+
test("managed_prefer falls back to local on managed failure", async () => {
|
|
160
|
+
mockStrategy = "managed_prefer";
|
|
161
|
+
mockManagedError = new Error("managed failed");
|
|
162
|
+
const result = await routedGenerateAvatar("a cute cat");
|
|
163
|
+
expect(result.pathUsed).toBe("local");
|
|
164
|
+
expect(generateManagedAvatarFn).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(generateImageFn).toHaveBeenCalledTimes(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// 7. managed_prefer — managed unavailable goes directly to local
|
|
169
|
+
test("managed_prefer goes to local when managed unavailable", async () => {
|
|
170
|
+
mockStrategy = "managed_prefer";
|
|
171
|
+
mockManagedAvailable = false;
|
|
172
|
+
const result = await routedGenerateAvatar("a cute cat");
|
|
173
|
+
expect(result.pathUsed).toBe("local");
|
|
174
|
+
expect(generateManagedAvatarFn).not.toHaveBeenCalled();
|
|
175
|
+
expect(generateImageFn).toHaveBeenCalledTimes(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// 8. Default strategy is local_only when config key absent
|
|
179
|
+
test("defaults to local_only when config key is absent", () => {
|
|
180
|
+
mockStrategy = undefined;
|
|
181
|
+
expect(getAvatarStrategy()).toBe("local_only");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
// 9. Removed: Invalid strategy values are now rejected at config parse time
|
|
185
|
+
// by the Zod schema, so they cannot reach getAvatarStrategy().
|
|
186
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { withTimeout } from "../tools/browser/browser-manager.js";
|
|
4
|
+
|
|
5
|
+
describe("withTimeout", () => {
|
|
6
|
+
it("resolves normally for a fast promise", async () => {
|
|
7
|
+
const result = await withTimeout(Promise.resolve("ok"), 1_000, "fast");
|
|
8
|
+
expect(result).toBe("ok");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it("rejects with timeout error for a hanging promise", async () => {
|
|
12
|
+
const hanging = new Promise<string>(() => {
|
|
13
|
+
// never resolves
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
await expect(withTimeout(hanging, 50, "slow op")).rejects.toThrow(
|
|
17
|
+
"slow op timed out after 50ms",
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("propagates the original rejection if it happens before the timeout", async () => {
|
|
22
|
+
const failing = Promise.reject(new Error("original error"));
|
|
23
|
+
|
|
24
|
+
await expect(withTimeout(failing, 1_000, "failing")).rejects.toThrow(
|
|
25
|
+
"original error",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -90,16 +90,16 @@ const GATEWAY_RETRIEVAL_BANLIST: Array<{
|
|
|
90
90
|
{
|
|
91
91
|
skillPath: "voice-setup/SKILL.md",
|
|
92
92
|
bannedSnippets: [
|
|
93
|
-
"
|
|
94
|
-
"
|
|
93
|
+
"assistant config get elevenlabs.voiceId",
|
|
94
|
+
"assistant config get calls.enabled",
|
|
95
95
|
],
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
skillPath: "email-setup/SKILL.md",
|
|
99
99
|
bannedSnippets: [
|
|
100
100
|
"host_bash",
|
|
101
|
-
"
|
|
102
|
-
"
|
|
101
|
+
"assistant email create",
|
|
102
|
+
"assistant config set email.address",
|
|
103
103
|
],
|
|
104
104
|
},
|
|
105
105
|
];
|
|
@@ -118,11 +118,11 @@ const HOST_BASH_RETRIEVAL_ALLOWLIST = new Set<string>([
|
|
|
118
118
|
]);
|
|
119
119
|
|
|
120
120
|
const RETRIEVAL_MARKERS = [
|
|
121
|
-
"
|
|
122
|
-
"
|
|
123
|
-
"
|
|
124
|
-
"
|
|
125
|
-
"
|
|
121
|
+
"assistant integrations ",
|
|
122
|
+
"assistant config get",
|
|
123
|
+
"assistant email status",
|
|
124
|
+
"assistant email inbox list",
|
|
125
|
+
"assistant email provider get",
|
|
126
126
|
];
|
|
127
127
|
|
|
128
128
|
describe("bundled skill retrieval guard", () => {
|
|
@@ -207,16 +207,12 @@ describe("resolveCallerIdentity — strict implicit-default policy", () => {
|
|
|
207
207
|
}
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
-
test("assistant_number resolves from
|
|
211
|
-
const result = await resolveCallerIdentity(
|
|
212
|
-
makeConfig(),
|
|
213
|
-
undefined,
|
|
214
|
-
"ast-alpha",
|
|
215
|
-
);
|
|
210
|
+
test("assistant_number resolves from twilio config phone number", async () => {
|
|
211
|
+
const result = await resolveCallerIdentity(makeConfig());
|
|
216
212
|
expect(result.ok).toBe(true);
|
|
217
213
|
if (result.ok) {
|
|
218
214
|
expect(result.mode).toBe("assistant_number");
|
|
219
|
-
expect(result.fromNumber).toBe("+
|
|
215
|
+
expect(result.fromNumber).toBe("+15550001111");
|
|
220
216
|
expect(result.source).toBe("implicit_default");
|
|
221
217
|
}
|
|
222
218
|
});
|
|
@@ -30,23 +30,30 @@ mock.module("../util/logger.js", () => ({
|
|
|
30
30
|
|
|
31
31
|
// Track keychain writes
|
|
32
32
|
const storedKeys = new Map<string, string>();
|
|
33
|
-
mock.module("../security/secure-keys.js", () =>
|
|
34
|
-
|
|
35
|
-
setSecureKey: (key: string, value: string) => {
|
|
33
|
+
mock.module("../security/secure-keys.js", () => {
|
|
34
|
+
const syncSet = (key: string, value: string) => {
|
|
36
35
|
storedKeys.set(key, value);
|
|
37
36
|
return true;
|
|
38
|
-
}
|
|
39
|
-
|
|
37
|
+
};
|
|
38
|
+
const syncDelete = (key: string) => {
|
|
40
39
|
if (storedKeys.has(key)) {
|
|
41
40
|
storedKeys.delete(key);
|
|
42
|
-
return "deleted";
|
|
41
|
+
return "deleted" as const;
|
|
43
42
|
}
|
|
44
|
-
return "not-found";
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
return "not-found" as const;
|
|
44
|
+
};
|
|
45
|
+
return {
|
|
46
|
+
getSecureKey: (key: string) => storedKeys.get(key) ?? null,
|
|
47
|
+
setSecureKey: syncSet,
|
|
48
|
+
setSecureKeyAsync: async (key: string, value: string) =>
|
|
49
|
+
syncSet(key, value),
|
|
50
|
+
deleteSecureKey: syncDelete,
|
|
51
|
+
deleteSecureKeyAsync: async (key: string) => syncDelete(key),
|
|
52
|
+
listSecureKeys: () => [...storedKeys.keys()],
|
|
53
|
+
getBackendType: () => "encrypted",
|
|
54
|
+
isDowngradedFromKeychain: () => false,
|
|
55
|
+
};
|
|
56
|
+
});
|
|
50
57
|
|
|
51
58
|
// In-memory metadata store that mirrors storedKeys for list/get operations
|
|
52
59
|
const metadataStore = new Map<
|
|
@@ -39,17 +39,43 @@ mock.module("../security/secure-keys.js", () => ({
|
|
|
39
39
|
secureKeyStore.set(account, value);
|
|
40
40
|
return true;
|
|
41
41
|
},
|
|
42
|
-
|
|
42
|
+
setSecureKeyAsync: async (
|
|
43
|
+
account: string,
|
|
44
|
+
value: string,
|
|
45
|
+
): Promise<boolean> => {
|
|
46
|
+
_setSecureKeyCalls += 1;
|
|
47
|
+
secureKeyStore.set(account, value);
|
|
48
|
+
return true;
|
|
49
|
+
},
|
|
50
|
+
deleteSecureKey: (account: string): "deleted" | "not-found" | "error" => {
|
|
51
|
+
_deleteSecureKeyCalls += 1;
|
|
52
|
+
if (secureKeyStore.has(account)) {
|
|
53
|
+
secureKeyStore.delete(account);
|
|
54
|
+
return "deleted";
|
|
55
|
+
}
|
|
56
|
+
return "not-found";
|
|
57
|
+
},
|
|
58
|
+
deleteSecureKeyAsync: async (
|
|
59
|
+
account: string,
|
|
60
|
+
): Promise<"deleted" | "not-found" | "error"> => {
|
|
43
61
|
_deleteSecureKeyCalls += 1;
|
|
44
62
|
if (secureKeyStore.has(account)) {
|
|
45
63
|
secureKeyStore.delete(account);
|
|
46
|
-
return "deleted"
|
|
64
|
+
return "deleted";
|
|
47
65
|
}
|
|
48
|
-
return "not-found"
|
|
66
|
+
return "not-found";
|
|
49
67
|
},
|
|
50
68
|
listSecureKeys: (): string[] => {
|
|
51
69
|
return [...secureKeyStore.keys()];
|
|
52
70
|
},
|
|
71
|
+
getSecureKeyAsync: async (account: string): Promise<string | undefined> => {
|
|
72
|
+
_getSecureKeyCalls += 1;
|
|
73
|
+
return secureKeyStore.get(account);
|
|
74
|
+
},
|
|
75
|
+
getBackendType: (): "broker" | "encrypted" | null => null,
|
|
76
|
+
isDowngradedFromKeychain: (): boolean => false,
|
|
77
|
+
_resetBackend: (): void => {},
|
|
78
|
+
_setBackend: (): void => {},
|
|
53
79
|
}));
|
|
54
80
|
|
|
55
81
|
// ---------------------------------------------------------------------------
|
|
@@ -243,7 +269,7 @@ function seedMetadataOnly(
|
|
|
243
269
|
// Tests
|
|
244
270
|
// ---------------------------------------------------------------------------
|
|
245
271
|
|
|
246
|
-
describe("
|
|
272
|
+
describe("assistant credentials CLI", () => {
|
|
247
273
|
beforeEach(() => {
|
|
248
274
|
secureKeyStore = new Map();
|
|
249
275
|
metadataStore = [];
|
|
@@ -55,7 +55,7 @@ describe("guardian-verify-setup skill — voice auto-followup", () => {
|
|
|
55
55
|
.split("## Voice Auto-Check Polling")[1]
|
|
56
56
|
?.split("## Step 6")[0] ?? "";
|
|
57
57
|
expect(pollingSection).toContain(
|
|
58
|
-
"
|
|
58
|
+
"assistant integrations guardian status --channel voice --json",
|
|
59
59
|
);
|
|
60
60
|
});
|
|
61
61
|
|
|
@@ -55,33 +55,9 @@ mock.module("../util/logger.js", () => ({
|
|
|
55
55
|
}),
|
|
56
56
|
}));
|
|
57
57
|
|
|
58
|
-
// Mock app-store so getApp returns a fake app for share tests
|
|
59
|
-
mock.module("../memory/app-store.js", () => ({
|
|
60
|
-
queryAppRecords: () => [],
|
|
61
|
-
createAppRecord: () => {},
|
|
62
|
-
updateAppRecord: () => {},
|
|
63
|
-
deleteAppRecord: () => {},
|
|
64
|
-
listApps: () => [],
|
|
65
|
-
getApp: (id: string) =>
|
|
66
|
-
id === "test-app"
|
|
67
|
-
? { id: "test-app", name: "Test App", description: "A test app" }
|
|
68
|
-
: undefined,
|
|
69
|
-
createApp: () => {},
|
|
70
|
-
updateApp: () => {},
|
|
71
|
-
}));
|
|
72
|
-
|
|
73
|
-
// Mock Slack webhook poster
|
|
74
|
-
const postedWebhooks: { url: string; name: string }[] = [];
|
|
75
|
-
mock.module("../slack/slack-webhook.js", () => ({
|
|
76
|
-
postToSlackWebhook: async (url: string, name: string) => {
|
|
77
|
-
postedWebhooks.push({ url, name });
|
|
78
|
-
},
|
|
79
|
-
}));
|
|
80
|
-
|
|
81
58
|
import { handleMessage, type HandlerContext } from "../daemon/handlers.js";
|
|
82
59
|
import type {
|
|
83
60
|
ServerMessage,
|
|
84
|
-
ShareToSlackRequest,
|
|
85
61
|
SlackWebhookConfigRequest,
|
|
86
62
|
} from "../daemon/ipc-contract.js";
|
|
87
63
|
import { DebouncerMap } from "../util/debounce.js";
|
|
@@ -161,52 +137,4 @@ describe("Slack handlers use workspace config (not hardcoded path)", () => {
|
|
|
161
137
|
"https://hooks.slack.com/new",
|
|
162
138
|
);
|
|
163
139
|
});
|
|
164
|
-
|
|
165
|
-
test("share_to_slack reads webhook URL from loadRawConfig", async () => {
|
|
166
|
-
rawConfigStore = { slackWebhookUrl: "https://hooks.slack.com/share" };
|
|
167
|
-
postedWebhooks.length = 0;
|
|
168
|
-
|
|
169
|
-
const msg: ShareToSlackRequest = {
|
|
170
|
-
type: "share_to_slack",
|
|
171
|
-
appId: "test-app",
|
|
172
|
-
};
|
|
173
|
-
|
|
174
|
-
const { ctx, sent } = createTestContext();
|
|
175
|
-
await handleMessage(msg, {} as net.Socket, ctx);
|
|
176
|
-
|
|
177
|
-
// Wait a tick for async handler to complete
|
|
178
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
179
|
-
|
|
180
|
-
const res = sent.find(
|
|
181
|
-
(m) => (m as { type: string }).type === "share_to_slack_response",
|
|
182
|
-
) as { type: string; success: boolean } | undefined;
|
|
183
|
-
expect(res).toBeDefined();
|
|
184
|
-
expect(res!.success).toBe(true);
|
|
185
|
-
|
|
186
|
-
// Verify the webhook was posted with the URL from loadRawConfig
|
|
187
|
-
expect(postedWebhooks).toHaveLength(1);
|
|
188
|
-
expect(postedWebhooks[0]!.url).toBe("https://hooks.slack.com/share");
|
|
189
|
-
expect(postedWebhooks[0]!.name).toBe("Test App");
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
test("share_to_slack fails gracefully when no webhook URL configured", async () => {
|
|
193
|
-
rawConfigStore = {};
|
|
194
|
-
|
|
195
|
-
const msg: ShareToSlackRequest = {
|
|
196
|
-
type: "share_to_slack",
|
|
197
|
-
appId: "test-app",
|
|
198
|
-
};
|
|
199
|
-
|
|
200
|
-
const { ctx, sent } = createTestContext();
|
|
201
|
-
await handleMessage(msg, {} as net.Socket, ctx);
|
|
202
|
-
|
|
203
|
-
await new Promise((r) => setTimeout(r, 50));
|
|
204
|
-
|
|
205
|
-
const res = sent.find(
|
|
206
|
-
(m) => (m as { type: string }).type === "share_to_slack_response",
|
|
207
|
-
) as { type: string; success: boolean; error?: string } | undefined;
|
|
208
|
-
expect(res).toBeDefined();
|
|
209
|
-
expect(res!.success).toBe(false);
|
|
210
|
-
expect(res!.error).toContain("No Slack webhook URL configured");
|
|
211
|
-
});
|
|
212
140
|
});
|
|
@@ -66,20 +66,27 @@ let secureKeyStore: Record<string, string> = {};
|
|
|
66
66
|
let setSecureKeyOverride: ((account: string, value: string) => boolean) | null =
|
|
67
67
|
null;
|
|
68
68
|
|
|
69
|
+
function syncSet(account: string, value: string): boolean {
|
|
70
|
+
if (setSecureKeyOverride) return setSecureKeyOverride(account, value);
|
|
71
|
+
secureKeyStore[account] = value;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function syncDelete(account: string): "deleted" | "not-found" {
|
|
76
|
+
if (account in secureKeyStore) {
|
|
77
|
+
delete secureKeyStore[account];
|
|
78
|
+
return "deleted";
|
|
79
|
+
}
|
|
80
|
+
return "not-found";
|
|
81
|
+
}
|
|
82
|
+
|
|
69
83
|
mock.module("../security/secure-keys.js", () => ({
|
|
70
84
|
getSecureKey: (account: string) => secureKeyStore[account] ?? undefined,
|
|
71
|
-
setSecureKey:
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
deleteSecureKey: (account: string) => {
|
|
77
|
-
if (account in secureKeyStore) {
|
|
78
|
-
delete secureKeyStore[account];
|
|
79
|
-
return "deleted";
|
|
80
|
-
}
|
|
81
|
-
return "not-found";
|
|
82
|
-
},
|
|
85
|
+
setSecureKey: syncSet,
|
|
86
|
+
deleteSecureKey: syncDelete,
|
|
87
|
+
setSecureKeyAsync: async (account: string, value: string) =>
|
|
88
|
+
syncSet(account, value),
|
|
89
|
+
deleteSecureKeyAsync: async (account: string) => syncDelete(account),
|
|
83
90
|
listSecureKeys: () => Object.keys(secureKeyStore),
|
|
84
91
|
getBackendType: () => "encrypted",
|
|
85
92
|
isDowngradedFromKeychain: () => false,
|