@vellumai/assistant 0.5.4 → 0.5.6
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/Dockerfile +17 -27
- package/node_modules/@vellumai/ces-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-contracts/src/trust-rules.ts +42 -0
- package/package.json +1 -1
- package/src/__tests__/actor-token-service.test.ts +113 -0
- package/src/__tests__/config-schema.test.ts +2 -2
- package/src/__tests__/context-window-manager.test.ts +78 -0
- package/src/__tests__/conversation-title-service.test.ts +30 -1
- package/src/__tests__/credential-security-invariants.test.ts +2 -0
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +207 -0
- package/src/__tests__/memory-regressions.test.ts +8 -30
- package/src/__tests__/openai-whisper.test.ts +93 -0
- package/src/__tests__/require-fresh-approval.test.ts +4 -0
- package/src/__tests__/slack-messaging-token-resolution.test.ts +319 -0
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -0
- package/src/__tests__/volume-security-guard.test.ts +155 -0
- package/src/cli/commands/conversations.ts +0 -18
- package/src/config/bundled-skills/messaging/tools/shared.ts +1 -0
- package/src/config/bundled-skills/transcribe/tools/transcribe-media.ts +16 -37
- package/src/config/env-registry.ts +9 -0
- package/src/config/env.ts +8 -2
- package/src/config/feature-flag-registry.json +8 -8
- package/src/config/schema.ts +0 -12
- package/src/config/schemas/memory.ts +0 -4
- package/src/config/schemas/platform.ts +1 -1
- package/src/config/schemas/security.ts +4 -0
- package/src/context/window-manager.ts +53 -2
- package/src/credential-execution/managed-catalog.ts +5 -15
- package/src/daemon/conversation-agent-loop.ts +0 -60
- package/src/daemon/conversation-memory.ts +0 -117
- package/src/daemon/conversation-runtime-assembly.ts +0 -2
- package/src/daemon/daemon-control.ts +7 -0
- package/src/daemon/handlers/conversations.ts +0 -11
- package/src/daemon/lifecycle.ts +10 -47
- package/src/daemon/providers-setup.ts +2 -1
- package/src/followups/followup-store.ts +5 -2
- package/src/hooks/manager.ts +7 -0
- package/src/instrument.ts +33 -1
- package/src/memory/conversation-crud.ts +0 -236
- package/src/memory/conversation-title-service.ts +26 -10
- package/src/memory/db-init.ts +5 -13
- package/src/memory/embedding-local.ts +11 -5
- package/src/memory/indexer.ts +15 -106
- package/src/memory/job-handlers/conversation-starters.ts +24 -36
- package/src/memory/job-handlers/embedding.ts +0 -79
- package/src/memory/job-utils.ts +1 -1
- package/src/memory/jobs-store.ts +0 -8
- package/src/memory/jobs-worker.ts +0 -20
- package/src/memory/migrations/189-drop-simplified-memory.ts +42 -0
- package/src/memory/migrations/index.ts +1 -3
- package/src/memory/qdrant-client.ts +4 -6
- package/src/memory/schema/conversations.ts +0 -3
- package/src/memory/schema/index.ts +0 -2
- package/src/messaging/draft-store.ts +2 -2
- package/src/messaging/provider.ts +9 -0
- package/src/messaging/providers/slack/adapter.ts +29 -2
- package/src/oauth/connection-resolver.test.ts +22 -18
- package/src/oauth/connection-resolver.ts +92 -7
- package/src/oauth/platform-connection.test.ts +78 -69
- package/src/oauth/platform-connection.ts +12 -19
- package/src/permissions/defaults.ts +3 -3
- package/src/permissions/trust-client.ts +332 -0
- package/src/permissions/trust-store-interface.ts +105 -0
- package/src/permissions/trust-store.ts +531 -39
- package/src/platform/client.test.ts +148 -0
- package/src/platform/client.ts +71 -0
- package/src/providers/speech-to-text/openai-whisper.test.ts +190 -0
- package/src/providers/speech-to-text/openai-whisper.ts +68 -0
- package/src/providers/speech-to-text/resolve.ts +9 -0
- package/src/providers/speech-to-text/types.ts +17 -0
- package/src/runtime/auth/route-policy.ts +14 -0
- package/src/runtime/auth/token-service.ts +133 -0
- package/src/runtime/http-server.ts +4 -2
- package/src/runtime/routes/conversation-management-routes.ts +0 -36
- package/src/runtime/routes/conversation-query-routes.ts +44 -2
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/inbound-message-handler.ts +27 -3
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +16 -1
- package/src/runtime/routes/inbound-stages/transcribe-audio.test.ts +287 -0
- package/src/runtime/routes/inbound-stages/transcribe-audio.ts +122 -0
- package/src/runtime/routes/log-export-routes.ts +1 -0
- package/src/runtime/routes/memory-item-routes.test.ts +221 -3
- package/src/runtime/routes/memory-item-routes.ts +124 -2
- package/src/runtime/routes/secret-routes.ts +4 -1
- package/src/runtime/routes/upgrade-broadcast-routes.ts +151 -0
- package/src/schedule/schedule-store.ts +0 -21
- package/src/security/ces-credential-client.ts +173 -0
- package/src/security/secure-keys.ts +65 -22
- package/src/signals/bash.ts +3 -0
- package/src/signals/cancel.ts +3 -0
- package/src/signals/confirm.ts +3 -0
- package/src/signals/conversation-undo.ts +3 -0
- package/src/signals/event-stream.ts +7 -0
- package/src/signals/shotgun.ts +3 -0
- package/src/signals/trust-rule.ts +3 -0
- package/src/skills/inline-command-render.ts +5 -1
- package/src/skills/inline-command-runner.ts +30 -2
- package/src/telemetry/usage-telemetry-reporter.test.ts +23 -36
- package/src/telemetry/usage-telemetry-reporter.ts +21 -19
- package/src/tools/memory/handlers.ts +1 -129
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/skills/load.ts +9 -2
- package/src/util/device-id.ts +70 -7
- package/src/util/logger.ts +35 -9
- package/src/util/platform.ts +29 -5
- package/src/util/xml.ts +8 -0
- package/src/workspace/heartbeat-service.ts +5 -24
- package/src/workspace/migrations/migrate-to-workspace-volume.ts +113 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/archive-recall.test.ts +0 -560
- package/src/__tests__/conversation-memory-dirty-tail.test.ts +0 -150
- package/src/__tests__/conversation-switch-memory-reduction.test.ts +0 -474
- package/src/__tests__/db-memory-archive-migration.test.ts +0 -372
- package/src/__tests__/db-memory-brief-state-migration.test.ts +0 -213
- package/src/__tests__/db-memory-reducer-checkpoints.test.ts +0 -273
- package/src/__tests__/memory-brief-open-loops.test.ts +0 -530
- package/src/__tests__/memory-brief-time.test.ts +0 -285
- package/src/__tests__/memory-brief-wrapper.test.ts +0 -311
- package/src/__tests__/memory-chunk-archive.test.ts +0 -400
- package/src/__tests__/memory-chunk-dual-write.test.ts +0 -453
- package/src/__tests__/memory-episode-archive.test.ts +0 -370
- package/src/__tests__/memory-episode-dual-write.test.ts +0 -626
- package/src/__tests__/memory-observation-archive.test.ts +0 -375
- package/src/__tests__/memory-observation-dual-write.test.ts +0 -318
- package/src/__tests__/memory-reducer-job.test.ts +0 -538
- package/src/__tests__/memory-reducer-scheduling.test.ts +0 -473
- package/src/__tests__/memory-reducer-store.test.ts +0 -728
- package/src/__tests__/memory-reducer-types.test.ts +0 -707
- package/src/__tests__/memory-reducer.test.ts +0 -704
- package/src/__tests__/memory-simplified-config.test.ts +0 -281
- package/src/__tests__/simplified-memory-e2e.test.ts +0 -666
- package/src/__tests__/simplified-memory-runtime.test.ts +0 -616
- package/src/config/schemas/memory-simplified.ts +0 -101
- package/src/memory/archive-recall.ts +0 -516
- package/src/memory/archive-store.ts +0 -400
- package/src/memory/brief-formatting.ts +0 -33
- package/src/memory/brief-open-loops.ts +0 -266
- package/src/memory/brief-time.ts +0 -162
- package/src/memory/brief.ts +0 -75
- package/src/memory/job-handlers/backfill-simplified-memory.ts +0 -462
- package/src/memory/job-handlers/reduce-conversation-memory.ts +0 -229
- package/src/memory/migrations/185-memory-brief-state.ts +0 -52
- package/src/memory/migrations/186-memory-archive.ts +0 -109
- package/src/memory/migrations/187-memory-reducer-checkpoints.ts +0 -19
- package/src/memory/reducer-scheduler.ts +0 -242
- package/src/memory/reducer-store.ts +0 -271
- package/src/memory/reducer-types.ts +0 -106
- package/src/memory/reducer.ts +0 -467
- package/src/memory/schema/memory-archive.ts +0 -121
- package/src/memory/schema/memory-brief.ts +0 -55
|
@@ -540,23 +540,13 @@ describe("Memory regressions", () => {
|
|
|
540
540
|
|
|
541
541
|
test("memory_save sets verificationState to user_confirmed", async () => {
|
|
542
542
|
const { handleMemorySave } = await import("../tools/memory/handlers.js");
|
|
543
|
-
const legacyConfig = {
|
|
544
|
-
...DEFAULT_CONFIG,
|
|
545
|
-
memory: {
|
|
546
|
-
...DEFAULT_CONFIG.memory,
|
|
547
|
-
simplified: {
|
|
548
|
-
...DEFAULT_CONFIG.memory.simplified,
|
|
549
|
-
enabled: false,
|
|
550
|
-
},
|
|
551
|
-
},
|
|
552
|
-
};
|
|
553
543
|
|
|
554
544
|
const result = await handleMemorySave(
|
|
555
545
|
{
|
|
556
546
|
statement: "User explicitly saved this preference",
|
|
557
547
|
kind: "preference",
|
|
558
548
|
},
|
|
559
|
-
|
|
549
|
+
DEFAULT_CONFIG,
|
|
560
550
|
"conv-verify-save",
|
|
561
551
|
"msg-verify-save",
|
|
562
552
|
);
|
|
@@ -573,23 +563,13 @@ describe("Memory regressions", () => {
|
|
|
573
563
|
|
|
574
564
|
test("memory_save in different scopes creates separate items", async () => {
|
|
575
565
|
const { handleMemorySave } = await import("../tools/memory/handlers.js");
|
|
576
|
-
const legacyConfig = {
|
|
577
|
-
...DEFAULT_CONFIG,
|
|
578
|
-
memory: {
|
|
579
|
-
...DEFAULT_CONFIG.memory,
|
|
580
|
-
simplified: {
|
|
581
|
-
...DEFAULT_CONFIG.memory.simplified,
|
|
582
|
-
enabled: false,
|
|
583
|
-
},
|
|
584
|
-
},
|
|
585
|
-
};
|
|
586
566
|
|
|
587
567
|
const sharedArgs = { statement: "I prefer dark mode", kind: "preference" };
|
|
588
568
|
|
|
589
569
|
// Save in the default scope
|
|
590
570
|
const r1 = await handleMemorySave(
|
|
591
571
|
sharedArgs,
|
|
592
|
-
|
|
572
|
+
DEFAULT_CONFIG,
|
|
593
573
|
"conv-scope-1",
|
|
594
574
|
"msg-scope-1",
|
|
595
575
|
"default",
|
|
@@ -600,7 +580,7 @@ describe("Memory regressions", () => {
|
|
|
600
580
|
// Save the identical statement in a private scope
|
|
601
581
|
const r2 = await handleMemorySave(
|
|
602
582
|
sharedArgs,
|
|
603
|
-
|
|
583
|
+
DEFAULT_CONFIG,
|
|
604
584
|
"conv-scope-2",
|
|
605
585
|
"msg-scope-2",
|
|
606
586
|
"private-abc",
|
|
@@ -624,7 +604,7 @@ describe("Memory regressions", () => {
|
|
|
624
604
|
// Saving the same statement again in default scope should dedup (not create a third)
|
|
625
605
|
const r3 = await handleMemorySave(
|
|
626
606
|
sharedArgs,
|
|
627
|
-
|
|
607
|
+
DEFAULT_CONFIG,
|
|
628
608
|
"conv-scope-3",
|
|
629
609
|
"msg-scope-3",
|
|
630
610
|
"default",
|
|
@@ -3227,9 +3207,8 @@ describe("Memory regressions", () => {
|
|
|
3227
3207
|
.filter((j) => JSON.parse(j.payload).messageId === "msg-untrusted-gate");
|
|
3228
3208
|
expect(extractJobs.length).toBe(0);
|
|
3229
3209
|
|
|
3230
|
-
// enqueuedJobs
|
|
3231
|
-
|
|
3232
|
-
const expectedJobs = result.indexedSegments * 2 + 1;
|
|
3210
|
+
// enqueuedJobs should reflect: embed jobs + summary (1), no extract (0)
|
|
3211
|
+
const expectedJobs = result.indexedSegments + 1; // embed per segment + summary
|
|
3233
3212
|
expect(result.enqueuedJobs).toBe(expectedJobs);
|
|
3234
3213
|
});
|
|
3235
3214
|
|
|
@@ -3410,9 +3389,8 @@ describe("Memory regressions", () => {
|
|
|
3410
3389
|
.filter((j) => JSON.parse(j.payload).messageId === "msg-unverified-gate");
|
|
3411
3390
|
expect(extractJobs.length).toBe(0);
|
|
3412
3391
|
|
|
3413
|
-
// enqueuedJobs
|
|
3414
|
-
|
|
3415
|
-
const expectedJobs = result.indexedSegments * 2 + 1;
|
|
3392
|
+
// enqueuedJobs should reflect: embed jobs + summary (1), no extract (0)
|
|
3393
|
+
const expectedJobs = result.indexedSegments + 1; // embed per segment + summary
|
|
3416
3394
|
expect(result.enqueuedJobs).toBe(expectedJobs);
|
|
3417
3395
|
});
|
|
3418
3396
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { OpenAIWhisperProvider } from "../providers/speech-to-text/openai-whisper.js";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Mock fetch — capture outgoing FormData so we can assert filenames
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
const originalFetch = globalThis.fetch;
|
|
10
|
+
|
|
11
|
+
let capturedFormData: FormData | null = null;
|
|
12
|
+
|
|
13
|
+
function mockFetch(
|
|
14
|
+
_url: string | URL | Request,
|
|
15
|
+
init?: RequestInit,
|
|
16
|
+
): Promise<Response> {
|
|
17
|
+
if (init?.body instanceof FormData) {
|
|
18
|
+
capturedFormData = init.body;
|
|
19
|
+
}
|
|
20
|
+
return Promise.resolve(
|
|
21
|
+
new Response(JSON.stringify({ text: "hello world" }), {
|
|
22
|
+
status: 200,
|
|
23
|
+
headers: { "Content-Type": "application/json" },
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
capturedFormData = null;
|
|
30
|
+
globalThis.fetch = mockFetch as typeof fetch;
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
globalThis.fetch = originalFetch;
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Tests
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
describe("OpenAIWhisperProvider", () => {
|
|
42
|
+
const provider = new OpenAIWhisperProvider("test-api-key");
|
|
43
|
+
const dummyAudio = Buffer.from("fake-audio-data");
|
|
44
|
+
|
|
45
|
+
describe("extensionFromMime (via transcribe filename)", () => {
|
|
46
|
+
test("plain MIME type resolves to correct extension", async () => {
|
|
47
|
+
await provider.transcribe(dummyAudio, "audio/ogg");
|
|
48
|
+
const file = capturedFormData?.get("file") as File;
|
|
49
|
+
expect(file.name).toBe("audio.ogg");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("MIME type with parameters resolves to correct extension", async () => {
|
|
53
|
+
await provider.transcribe(dummyAudio, "audio/ogg; codecs=opus");
|
|
54
|
+
const file = capturedFormData?.get("file") as File;
|
|
55
|
+
expect(file.name).toBe("audio.ogg");
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("MIME type with extra whitespace around parameters", async () => {
|
|
59
|
+
await provider.transcribe(dummyAudio, "audio/mpeg ; bitrate=128");
|
|
60
|
+
const file = capturedFormData?.get("file") as File;
|
|
61
|
+
expect(file.name).toBe("audio.mp3");
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test("unknown MIME type falls back to .audio", async () => {
|
|
65
|
+
await provider.transcribe(dummyAudio, "audio/unknown-format");
|
|
66
|
+
const file = capturedFormData?.get("file") as File;
|
|
67
|
+
expect(file.name).toBe("audio.audio");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("unknown MIME type with parameters still falls back", async () => {
|
|
71
|
+
await provider.transcribe(dummyAudio, "audio/unknown; foo=bar");
|
|
72
|
+
const file = capturedFormData?.get("file") as File;
|
|
73
|
+
expect(file.name).toBe("audio.audio");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test.each([
|
|
77
|
+
["audio/wav", "audio.wav"],
|
|
78
|
+
["audio/x-wav", "audio.wav"],
|
|
79
|
+
["audio/mpeg", "audio.mp3"],
|
|
80
|
+
["audio/mp3", "audio.mp3"],
|
|
81
|
+
["audio/ogg", "audio.ogg"],
|
|
82
|
+
["audio/opus", "audio.opus"],
|
|
83
|
+
["audio/webm", "audio.webm"],
|
|
84
|
+
["audio/mp4", "audio.m4a"],
|
|
85
|
+
["audio/x-m4a", "audio.m4a"],
|
|
86
|
+
["audio/flac", "audio.flac"],
|
|
87
|
+
])("%s → %s", async (mime, expectedFilename) => {
|
|
88
|
+
await provider.transcribe(dummyAudio, mime);
|
|
89
|
+
const file = capturedFormData?.get("file") as File;
|
|
90
|
+
expect(file.name).toBe(expectedFilename);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -58,6 +58,10 @@ const mockConfig = {
|
|
|
58
58
|
action: "warn" as const,
|
|
59
59
|
entropyThreshold: 4.0,
|
|
60
60
|
},
|
|
61
|
+
permissions: {
|
|
62
|
+
mode: "workspace" as const,
|
|
63
|
+
dangerouslySkipPermissions: false,
|
|
64
|
+
},
|
|
61
65
|
};
|
|
62
66
|
|
|
63
67
|
let fakeToolResult: ToolExecutionResult = { content: "ok", isError: false };
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { OAuthConnection } from "../oauth/connection.js";
|
|
4
|
+
|
|
5
|
+
// ── Mocks ───────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const getSecureKeyAsyncMock = mock(
|
|
8
|
+
async (_key: string): Promise<string | null> => null,
|
|
9
|
+
);
|
|
10
|
+
const isProviderConnectedMock = mock(
|
|
11
|
+
async (_service: string): Promise<boolean> => false,
|
|
12
|
+
);
|
|
13
|
+
const resolveOAuthConnectionMock = mock(
|
|
14
|
+
async (
|
|
15
|
+
_service: string,
|
|
16
|
+
_opts?: { account?: string },
|
|
17
|
+
): Promise<OAuthConnection> =>
|
|
18
|
+
({ accessToken: "oauth-token" }) as unknown as OAuthConnection,
|
|
19
|
+
);
|
|
20
|
+
const getConnectionByProviderMock = mock(
|
|
21
|
+
(_provider: string): { status: string } | undefined => undefined,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
mock.module("../security/secure-keys.js", () => ({
|
|
25
|
+
getSecureKeyAsync: getSecureKeyAsyncMock,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
mock.module("../oauth/oauth-store.js", () => ({
|
|
29
|
+
isProviderConnected: isProviderConnectedMock,
|
|
30
|
+
getConnectionByProvider: getConnectionByProviderMock,
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
mock.module("../oauth/connection-resolver.js", () => ({
|
|
34
|
+
resolveOAuthConnection: resolveOAuthConnectionMock,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
// Telegram adapter imports modules that need more stubs
|
|
38
|
+
mock.module("../config/env.js", () => ({
|
|
39
|
+
getGatewayInternalBaseUrl: () => "http://localhost:3000",
|
|
40
|
+
}));
|
|
41
|
+
mock.module("../memory/conversation-key-store.js", () => ({
|
|
42
|
+
getOrCreateConversation: async () => "conv-1",
|
|
43
|
+
}));
|
|
44
|
+
mock.module("../memory/external-conversation-store.js", () => ({
|
|
45
|
+
getExternalConversation: () => undefined,
|
|
46
|
+
setExternalConversation: () => {},
|
|
47
|
+
}));
|
|
48
|
+
mock.module("../runtime/auth/token-service.js", () => ({
|
|
49
|
+
mintDaemonDeliveryToken: async () => "delivery-token",
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
// Slack client stubs (not exercised in these tests, but required on import)
|
|
53
|
+
mock.module("../messaging/providers/slack/client.js", () => ({}));
|
|
54
|
+
|
|
55
|
+
// Gmail client stubs
|
|
56
|
+
mock.module("../messaging/providers/gmail/client.js", () => ({}));
|
|
57
|
+
mock.module("../messaging/providers/gmail/people-client.js", () => ({}));
|
|
58
|
+
|
|
59
|
+
// Telegram client stubs
|
|
60
|
+
mock.module("../messaging/providers/telegram-bot/client.js", () => ({}));
|
|
61
|
+
|
|
62
|
+
import {
|
|
63
|
+
getProviderConnection,
|
|
64
|
+
resolveProvider,
|
|
65
|
+
} from "../config/bundled-skills/messaging/tools/shared.js";
|
|
66
|
+
import { gmailMessagingProvider } from "../messaging/providers/gmail/adapter.js";
|
|
67
|
+
import { slackProvider } from "../messaging/providers/slack/adapter.js";
|
|
68
|
+
import { telegramBotMessagingProvider } from "../messaging/providers/telegram-bot/adapter.js";
|
|
69
|
+
import {
|
|
70
|
+
getConnectedProviders,
|
|
71
|
+
registerMessagingProvider,
|
|
72
|
+
} from "../messaging/registry.js";
|
|
73
|
+
|
|
74
|
+
// Register providers for integration tests
|
|
75
|
+
registerMessagingProvider(slackProvider);
|
|
76
|
+
registerMessagingProvider(gmailMessagingProvider);
|
|
77
|
+
registerMessagingProvider(telegramBotMessagingProvider);
|
|
78
|
+
|
|
79
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
function resetAllMocks() {
|
|
82
|
+
getSecureKeyAsyncMock.mockReset();
|
|
83
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
84
|
+
isProviderConnectedMock.mockReset();
|
|
85
|
+
isProviderConnectedMock.mockImplementation(async () => false);
|
|
86
|
+
resolveOAuthConnectionMock.mockReset();
|
|
87
|
+
resolveOAuthConnectionMock.mockImplementation(
|
|
88
|
+
async () => ({ accessToken: "oauth-token" }) as unknown as OAuthConnection,
|
|
89
|
+
);
|
|
90
|
+
getConnectionByProviderMock.mockReset();
|
|
91
|
+
getConnectionByProviderMock.mockImplementation(() => undefined);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
describe("Slack messaging token resolution", () => {
|
|
97
|
+
beforeEach(resetAllMocks);
|
|
98
|
+
|
|
99
|
+
// ── slackProvider.isConnected() ─────────────────────────────────────────
|
|
100
|
+
|
|
101
|
+
describe("slackProvider.isConnected()", () => {
|
|
102
|
+
test("returns true when slack_channel bot token exists in credential store", async () => {
|
|
103
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
|
|
104
|
+
key === "credential/slack_channel/bot_token" ? "xoxb-bot-token" : null,
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
expect(await slackProvider.isConnected!()).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("returns true even if slack_channel connection row is missing (backfill failure resilience)", async () => {
|
|
111
|
+
// Bot token exists but no connection row — isConnected should still return true
|
|
112
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
|
|
113
|
+
key === "credential/slack_channel/bot_token" ? "xoxb-bot-token" : null,
|
|
114
|
+
);
|
|
115
|
+
// No getConnectionByProvider call expected — Slack adapter checks token first
|
|
116
|
+
|
|
117
|
+
expect(await slackProvider.isConnected!()).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("returns true when only integration:slack has active OAuth connection (backwards compat)", async () => {
|
|
121
|
+
// No bot token
|
|
122
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
123
|
+
// But OAuth provider is connected
|
|
124
|
+
isProviderConnectedMock.mockImplementation(async (service: string) =>
|
|
125
|
+
service === "integration:slack" ? true : false,
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
expect(await slackProvider.isConnected!()).toBe(true);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("returns false when neither credential path exists", async () => {
|
|
132
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
133
|
+
isProviderConnectedMock.mockImplementation(async () => false);
|
|
134
|
+
|
|
135
|
+
expect(await slackProvider.isConnected!()).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ── slackProvider.resolveConnection() ───────────────────────────────────
|
|
140
|
+
|
|
141
|
+
describe("slackProvider.resolveConnection()", () => {
|
|
142
|
+
test("returns bot token string when Socket Mode credentials exist", async () => {
|
|
143
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
|
|
144
|
+
key === "credential/slack_channel/bot_token"
|
|
145
|
+
? "xoxb-socket-token"
|
|
146
|
+
: null,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const result = await slackProvider.resolveConnection!();
|
|
150
|
+
expect(result).toBe("xoxb-socket-token");
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
test("returns bot token string even without a slack_channel connection row (token-only resilience)", async () => {
|
|
154
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
|
|
155
|
+
key === "credential/slack_channel/bot_token" ? "xoxb-token-only" : null,
|
|
156
|
+
);
|
|
157
|
+
// No connection row — resolveConnection should still return the token
|
|
158
|
+
|
|
159
|
+
const result = await slackProvider.resolveConnection!();
|
|
160
|
+
expect(result).toBe("xoxb-token-only");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("returns OAuthConnection when only OAuth integration:slack credentials exist (backwards compat)", async () => {
|
|
164
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
165
|
+
const oauthConn = {
|
|
166
|
+
accessToken: "xoxp-oauth-token",
|
|
167
|
+
} as unknown as OAuthConnection;
|
|
168
|
+
resolveOAuthConnectionMock.mockImplementation(async () => oauthConn);
|
|
169
|
+
|
|
170
|
+
const result = await slackProvider.resolveConnection!();
|
|
171
|
+
expect(result).toBe(oauthConn);
|
|
172
|
+
expect(resolveOAuthConnectionMock).toHaveBeenCalledWith(
|
|
173
|
+
"integration:slack",
|
|
174
|
+
{ account: undefined },
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("throws when no credentials exist at all (no Socket Mode, no OAuth)", async () => {
|
|
179
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
180
|
+
resolveOAuthConnectionMock.mockImplementation(async () => {
|
|
181
|
+
throw new Error("No OAuth connection found for integration:slack");
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
await expect(slackProvider.resolveConnection!()).rejects.toThrow(
|
|
185
|
+
"No OAuth connection found",
|
|
186
|
+
);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ── getProviderConnection() integration ─────────────────────────────────
|
|
191
|
+
|
|
192
|
+
describe("getProviderConnection()", () => {
|
|
193
|
+
test("returns bot token string for Slack when Socket Mode credentials exist", async () => {
|
|
194
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
|
|
195
|
+
key === "credential/slack_channel/bot_token" ? "xoxb-conn-token" : null,
|
|
196
|
+
);
|
|
197
|
+
|
|
198
|
+
const result = await getProviderConnection(slackProvider);
|
|
199
|
+
expect(result).toBe("xoxb-conn-token");
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("returns OAuthConnection for Slack when only OAuth credentials exist (backwards compat)", async () => {
|
|
203
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
204
|
+
const oauthConn = {
|
|
205
|
+
accessToken: "xoxp-oauth-token",
|
|
206
|
+
} as unknown as OAuthConnection;
|
|
207
|
+
resolveOAuthConnectionMock.mockImplementation(async () => oauthConn);
|
|
208
|
+
|
|
209
|
+
const result = await getProviderConnection(slackProvider);
|
|
210
|
+
expect(result).toBe(oauthConn);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
test("throws when no Slack credentials exist", async () => {
|
|
214
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
215
|
+
resolveOAuthConnectionMock.mockImplementation(async () => {
|
|
216
|
+
throw new Error("No OAuth connection found");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
await expect(getProviderConnection(slackProvider)).rejects.toThrow(
|
|
220
|
+
"No OAuth connection found",
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('Telegram still returns "" (no resolveConnection, uses isConnected path — regression check)', async () => {
|
|
225
|
+
// Telegram has isConnected but no resolveConnection.
|
|
226
|
+
// When isConnected returns true, getProviderConnection returns ""
|
|
227
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) => {
|
|
228
|
+
if (key === "credential/telegram/bot_token") return "bot-token";
|
|
229
|
+
if (key === "credential/telegram/webhook_secret") return "secret";
|
|
230
|
+
return null;
|
|
231
|
+
});
|
|
232
|
+
getConnectionByProviderMock.mockImplementation((provider: string) =>
|
|
233
|
+
provider === "telegram" ? { status: "active" } : undefined,
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const result = await getProviderConnection(telegramBotMessagingProvider);
|
|
237
|
+
expect(result).toBe("");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("Gmail still calls resolveOAuthConnection (no resolveConnection, no isConnected — regression check)", async () => {
|
|
241
|
+
// Gmail has neither resolveConnection nor isConnected.
|
|
242
|
+
// getProviderConnection falls through to resolveOAuthConnection.
|
|
243
|
+
const oauthConn = {
|
|
244
|
+
accessToken: "gmail-oauth-token",
|
|
245
|
+
} as unknown as OAuthConnection;
|
|
246
|
+
resolveOAuthConnectionMock.mockImplementation(async () => oauthConn);
|
|
247
|
+
|
|
248
|
+
const result = await getProviderConnection(gmailMessagingProvider);
|
|
249
|
+
expect(result).toBe(oauthConn);
|
|
250
|
+
expect(resolveOAuthConnectionMock).toHaveBeenCalledWith(
|
|
251
|
+
"integration:google",
|
|
252
|
+
{ account: undefined },
|
|
253
|
+
);
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
// ── resolveProvider() multi-platform behavior ───────────────────────────
|
|
258
|
+
|
|
259
|
+
describe("resolveProvider() multi-platform behavior", () => {
|
|
260
|
+
test('throws "Multiple platforms connected" when both Gmail and Slack (Socket Mode) are connected and no platform is specified', async () => {
|
|
261
|
+
// Slack connected via Socket Mode
|
|
262
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
|
|
263
|
+
key === "credential/slack_channel/bot_token" ? "xoxb-token" : null,
|
|
264
|
+
);
|
|
265
|
+
// Gmail connected via OAuth
|
|
266
|
+
isProviderConnectedMock.mockImplementation(async (service: string) =>
|
|
267
|
+
service === "integration:google" ? true : false,
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
await expect(resolveProvider()).rejects.toThrow(
|
|
271
|
+
"Multiple platforms connected",
|
|
272
|
+
);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("auto-selects Slack when it is the only connected provider", async () => {
|
|
276
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
|
|
277
|
+
key === "credential/slack_channel/bot_token" ? "xoxb-only" : null,
|
|
278
|
+
);
|
|
279
|
+
isProviderConnectedMock.mockImplementation(async () => false);
|
|
280
|
+
|
|
281
|
+
const provider = await resolveProvider();
|
|
282
|
+
expect(provider.id).toBe("slack");
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
test("auto-selects Gmail when it is the only connected provider (no Slack credentials)", async () => {
|
|
286
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
287
|
+
isProviderConnectedMock.mockImplementation(async (service: string) =>
|
|
288
|
+
service === "integration:google" ? true : false,
|
|
289
|
+
);
|
|
290
|
+
|
|
291
|
+
const provider = await resolveProvider();
|
|
292
|
+
expect(provider.id).toBe("gmail");
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
// ── getConnectedProviders() ─────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
describe("getConnectedProviders()", () => {
|
|
299
|
+
test("includes Slack when connected via Socket Mode (slack_channel)", async () => {
|
|
300
|
+
getSecureKeyAsyncMock.mockImplementation(async (key: string) =>
|
|
301
|
+
key === "credential/slack_channel/bot_token" ? "xoxb-token" : null,
|
|
302
|
+
);
|
|
303
|
+
isProviderConnectedMock.mockImplementation(async () => false);
|
|
304
|
+
|
|
305
|
+
const connected = await getConnectedProviders();
|
|
306
|
+
const ids = connected.map((p) => p.id);
|
|
307
|
+
expect(ids).toContain("slack");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("excludes Slack when no bot token exists", async () => {
|
|
311
|
+
getSecureKeyAsyncMock.mockImplementation(async () => null);
|
|
312
|
+
isProviderConnectedMock.mockImplementation(async () => false);
|
|
313
|
+
|
|
314
|
+
const connected = await getConnectedProviders();
|
|
315
|
+
const ids = connected.map((p) => p.id);
|
|
316
|
+
expect(ids).not.toContain("slack");
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -50,6 +50,10 @@ const mockConfig = {
|
|
|
50
50
|
action: "warn" as const,
|
|
51
51
|
entropyThreshold: 4.0,
|
|
52
52
|
},
|
|
53
|
+
permissions: {
|
|
54
|
+
mode: "workspace" as const,
|
|
55
|
+
dangerouslySkipPermissions: false,
|
|
56
|
+
},
|
|
53
57
|
};
|
|
54
58
|
|
|
55
59
|
let fakeToolResult: ToolExecutionResult = { content: "ok", isError: false };
|