@vellumai/vellum-gateway 0.7.0 → 0.7.2
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/AGENTS.md +4 -0
- package/ARCHITECTURE.md +67 -25
- package/Dockerfile +2 -0
- package/README.md +50 -13
- package/bun.lock +16 -2
- package/knip.json +3 -1
- package/package.json +3 -1
- package/src/__tests__/auto-approve-thresholds.test.ts +49 -22
- package/src/__tests__/channel-verification-session-proxy.test.ts +0 -1
- package/src/__tests__/config-file-watcher.test.ts +181 -0
- package/src/__tests__/config.test.ts +0 -1
- package/src/__tests__/contacts-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/credential-watcher-managed-bootstrap.test.ts +10 -2
- package/src/__tests__/credential-watcher.test.ts +30 -2
- package/src/__tests__/db-connection-isolation.test.ts +157 -0
- package/src/__tests__/fake-assistant-ipc.ts +39 -0
- package/src/__tests__/feature-flags-route.test.ts +8 -8
- package/src/__tests__/guardian-init-lockfile.test.ts +30 -4
- package/src/__tests__/ipc-feature-flag-routes.test.ts +1 -1
- package/src/__tests__/live-voice-websocket.test.ts +0 -1
- package/src/__tests__/load-guards.test.ts +0 -1
- package/src/__tests__/migration-teleport-gcs-proxy.test.ts +0 -1
- package/src/__tests__/oauth-callback.test.ts +0 -1
- package/src/__tests__/pair-origin-allowlist.test.ts +155 -0
- package/src/__tests__/rate-limit-loopback.test.ts +1 -1
- package/src/__tests__/remote-feature-flag-sync.test.ts +47 -7
- package/src/__tests__/resolve-assistant.test.ts +0 -1
- package/src/__tests__/route-schema-guard.test.ts +42 -6
- package/src/__tests__/runtime-client.test.ts +0 -1
- package/src/__tests__/runtime-health-proxy.test.ts +0 -1
- package/src/__tests__/runtime-proxy-auth.test.ts +0 -1
- package/src/__tests__/runtime-proxy.test.ts +0 -1
- package/src/__tests__/slack-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/slack-display-name.test.ts +66 -1
- package/src/__tests__/slack-normalize.test.ts +158 -4
- package/src/__tests__/slack-reaction-normalize.test.ts +0 -1
- package/src/__tests__/slack-socket-mode-catchup.test.ts +857 -0
- package/src/__tests__/slack-socket-mode-scopes.test.ts +52 -0
- package/src/__tests__/slack-socket-mode-thread-tracking.test.ts +654 -0
- package/src/__tests__/stt-stream-websocket.test.ts +0 -1
- package/src/__tests__/telegram-control-plane-proxy.test.ts +0 -1
- package/src/__tests__/telegram-send-attachments.test.ts +0 -1
- package/src/__tests__/telegram-webhook-handler.test.ts +0 -1
- package/src/__tests__/text-verification-helpers.test.ts +136 -0
- package/src/__tests__/twilio-media-websocket.test.ts +0 -1
- package/src/__tests__/twilio-relay-websocket.test.ts +0 -1
- package/src/__tests__/twilio-webhooks.test.ts +220 -3
- package/src/__tests__/upstream-transport.test.ts +0 -36
- package/src/__tests__/whatsapp-download.test.ts +0 -1
- package/src/__tests__/whatsapp-webhook.test.ts +0 -1
- package/src/auth/guardian-refresh.ts +4 -18
- package/src/auth/ipc-route-policy.ts +217 -0
- package/src/backup/backup-key.ts +138 -0
- package/src/backup/backup-routes.ts +159 -0
- package/src/backup/backup-worker.ts +374 -0
- package/src/backup/list-snapshots.ts +97 -0
- package/src/backup/local-writer.ts +87 -0
- package/src/backup/offsite-writer.ts +182 -0
- package/src/backup/paths.ts +123 -0
- package/src/backup/stream-crypt.ts +258 -0
- package/src/chrome-extension-origins.ts +28 -0
- package/src/cli/enable-proxy.ts +0 -1
- package/src/config-file-cache.ts +3 -19
- package/src/config-file-utils.ts +124 -0
- package/src/config-file-watcher.ts +57 -25
- package/src/config.ts +4 -7
- package/src/db/connection.ts +65 -3
- package/src/db/contact-store.ts +30 -1
- package/src/db/data-migrations/index.ts +2 -0
- package/src/db/data-migrations/m0003-recover-backup-key.ts +71 -0
- package/src/db/schema.ts +92 -0
- package/src/db/slack-store.ts +144 -11
- package/src/feature-flag-registry.json +40 -152
- package/src/handlers/handle-inbound.ts +123 -0
- package/src/http/middleware/auth.ts +44 -1
- package/src/http/middleware/cors.ts +84 -0
- package/src/http/middleware/rate-limit.ts +6 -8
- package/src/http/routes/auto-approve-thresholds.ts +17 -1
- package/src/http/routes/brain-graph-proxy.ts +1 -1
- package/src/http/routes/channel-readiness-proxy.ts +2 -2
- package/src/http/routes/channel-verification-session-proxy.ts +19 -37
- package/src/http/routes/contact-prompt.ts +149 -0
- package/src/http/routes/contacts-control-plane-proxy.ts +2 -2
- package/src/http/routes/email-webhook.test.ts +0 -1
- package/src/http/routes/ipc-runtime-proxy.test.ts +197 -1
- package/src/http/routes/ipc-runtime-proxy.ts +95 -0
- package/src/http/routes/log-export.test.ts +0 -1
- package/src/http/routes/log-tail.test.ts +336 -0
- package/src/http/routes/log-tail.ts +87 -0
- package/src/http/routes/migration-proxy.ts +1 -2
- package/src/http/routes/oauth-apps-proxy.ts +2 -2
- package/src/http/routes/oauth-providers-proxy.ts +2 -2
- package/src/http/routes/pair.ts +322 -0
- package/src/http/routes/privacy-config.ts +65 -79
- package/src/http/routes/runtime-health-proxy.ts +2 -2
- package/src/http/routes/runtime-proxy.ts +3 -1
- package/src/http/routes/slack-control-plane-proxy.ts +3 -20
- package/src/http/routes/stt-stream-websocket.ts +2 -3
- package/src/http/routes/telegram-control-plane-proxy.ts +2 -2
- package/src/http/routes/telegram-webhook.test.ts +0 -1
- package/src/http/routes/telegram-webhook.ts +6 -0
- package/src/http/routes/trust-rules.suggest.test.ts +25 -0
- package/src/http/routes/trust-rules.ts +7 -0
- package/src/http/routes/twilio-control-plane-proxy.ts +2 -2
- package/src/http/routes/twilio-media-websocket.ts +5 -5
- package/src/http/routes/twilio-voice-verify-callback.ts +310 -0
- package/src/http/routes/twilio-voice-webhook.test.ts +65 -1
- package/src/http/routes/twilio-voice-webhook.ts +45 -1
- package/src/http/routes/whatsapp-webhook.test.ts +0 -1
- package/src/index.ts +357 -278
- package/src/ipc/assistant-client.ts +8 -4
- package/src/ipc/contact-handlers.ts +88 -3
- package/src/ipc/threshold-handlers.ts +2 -0
- package/src/post-assistant-ready.ts +5 -3
- package/src/risk/bash-risk-classifier.test.ts +35 -27
- package/src/risk/bash-risk-classifier.ts +44 -14
- package/src/risk/command-registry/commands/assistant.ts +8 -19
- package/src/risk/command-registry.test.ts +0 -15
- package/src/risk/risk-classifier-parity.test.ts +1 -3
- package/src/runtime/client.ts +58 -3
- package/src/schema.ts +277 -104
- package/src/slack/normalize.test.ts +98 -0
- package/src/slack/normalize.ts +107 -32
- package/src/slack/slack-web.ts +213 -0
- package/src/slack/socket-mode.ts +701 -39
- package/src/telegram/send.test.ts +0 -1
- package/src/twilio/validate-webhook.ts +53 -14
- package/src/twilio/webhook-sync-trigger.ts +58 -0
- package/src/twilio/webhook-sync.test.ts +286 -0
- package/src/twilio/webhook-sync.ts +84 -0
- package/src/util/is-loopback-address.ts +27 -0
- package/src/velay/bridge-utils.ts +228 -0
- package/src/velay/client.test.ts +939 -0
- package/src/velay/client.ts +555 -0
- package/src/velay/http-bridge.test.ts +217 -0
- package/src/velay/http-bridge.ts +83 -0
- package/src/velay/protocol.ts +178 -0
- package/src/velay/test-fake-websocket.ts +69 -0
- package/src/velay/websocket-bridge.test.ts +367 -0
- package/src/velay/websocket-bridge.ts +324 -0
- package/src/verification/binding-helpers.ts +107 -0
- package/src/verification/code-parsing.ts +44 -0
- package/src/verification/contact-helpers.ts +342 -0
- package/src/verification/identity-match.ts +68 -0
- package/src/verification/identity.ts +61 -0
- package/src/verification/rate-limit-helpers.ts +205 -0
- package/src/verification/reply-delivery.ts +109 -0
- package/src/verification/session-helpers.ts +164 -0
- package/src/verification/text-verification.ts +372 -0
- package/src/version.ts +35 -0
- package/src/voice/verification.ts +456 -0
- package/src/webhook-pipeline.ts +4 -0
- package/src/__tests__/browser-relay-websocket.test.ts +0 -698
- package/src/__tests__/telegram-only-default.test.ts +0 -133
- package/src/auth/capability-tokens.ts +0 -248
- package/src/http/routes/browser-extension-pair.ts +0 -455
- package/src/http/routes/browser-relay-websocket.ts +0 -381
- package/src/http/routes/config-file-utils.ts +0 -73
- package/src/ipc/capability-token-handlers.ts +0 -30
- package/src/pairing/approved-devices-store.ts +0 -110
- package/src/pairing/pairing-routes.ts +0 -379
- package/src/pairing/pairing-store.ts +0 -218
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { inspectSlackScopes } from "../slack/socket-mode.js";
|
|
4
|
+
|
|
5
|
+
describe("inspectSlackScopes", () => {
|
|
6
|
+
test("flags files:read when missing", () => {
|
|
7
|
+
const result = inspectSlackScopes(
|
|
8
|
+
"app_mentions:read,channels:history,im:history,groups:history,mpim:history",
|
|
9
|
+
);
|
|
10
|
+
expect(result.filesReadMissing).toBe(true);
|
|
11
|
+
expect(result.missingHistoryScopes).toEqual([]);
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("returns exactly the missing *:history scopes", () => {
|
|
15
|
+
const result = inspectSlackScopes(
|
|
16
|
+
"app_mentions:read,files:read,channels:history",
|
|
17
|
+
);
|
|
18
|
+
expect(result.filesReadMissing).toBe(false);
|
|
19
|
+
expect(result.missingHistoryScopes.sort()).toEqual([
|
|
20
|
+
"groups:history",
|
|
21
|
+
"im:history",
|
|
22
|
+
"mpim:history",
|
|
23
|
+
]);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("returns no missing scopes when all required are present", () => {
|
|
27
|
+
const result = inspectSlackScopes(
|
|
28
|
+
"app_mentions:read,files:read,channels:history,im:history,groups:history,mpim:history",
|
|
29
|
+
);
|
|
30
|
+
expect(result.filesReadMissing).toBe(false);
|
|
31
|
+
expect(result.missingHistoryScopes).toEqual([]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("treats an empty scope header as everything missing", () => {
|
|
35
|
+
const result = inspectSlackScopes("");
|
|
36
|
+
expect(result.filesReadMissing).toBe(true);
|
|
37
|
+
expect(result.missingHistoryScopes.sort()).toEqual([
|
|
38
|
+
"channels:history",
|
|
39
|
+
"groups:history",
|
|
40
|
+
"im:history",
|
|
41
|
+
"mpim:history",
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("ignores whitespace and empty entries in the header", () => {
|
|
46
|
+
const result = inspectSlackScopes(
|
|
47
|
+
" files:read , channels:history,, im:history ,groups:history,mpim:history",
|
|
48
|
+
);
|
|
49
|
+
expect(result.filesReadMissing).toBe(false);
|
|
50
|
+
expect(result.missingHistoryScopes).toEqual([]);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,654 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
3
|
+
import { drizzle } from "drizzle-orm/bun-sqlite";
|
|
4
|
+
import type { GatewayConfig } from "../config.js";
|
|
5
|
+
import { SlackStore } from "../db/slack-store.js";
|
|
6
|
+
import * as schema from "../db/schema.js";
|
|
7
|
+
import type { NormalizedSlackEvent } from "../slack/normalize.js";
|
|
8
|
+
|
|
9
|
+
type FetchFn = (
|
|
10
|
+
input: string | URL | Request,
|
|
11
|
+
init?: RequestInit,
|
|
12
|
+
) => Promise<Response>;
|
|
13
|
+
|
|
14
|
+
function makeSlackUserResponse(): Response {
|
|
15
|
+
return new Response(
|
|
16
|
+
JSON.stringify({
|
|
17
|
+
ok: true,
|
|
18
|
+
user: {
|
|
19
|
+
name: "example-user",
|
|
20
|
+
profile: { display_name: "Example User" },
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let fetchMock: ReturnType<typeof mock<FetchFn>> = mock(async () =>
|
|
28
|
+
makeSlackUserResponse(),
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
mock.module("../fetch.js", () => ({
|
|
32
|
+
fetchImpl: (...args: Parameters<FetchFn>) => fetchMock(...args),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const { SlackSocketModeClient } = await import("../slack/socket-mode.js");
|
|
36
|
+
const { clearUserInfoCache, resolveSlackUser } =
|
|
37
|
+
await import("../slack/normalize.js");
|
|
38
|
+
import type { SlackSocketModeConfig } from "../slack/socket-mode.js";
|
|
39
|
+
|
|
40
|
+
type SocketModeHarness = {
|
|
41
|
+
config: SlackSocketModeConfig;
|
|
42
|
+
onEvent: (event: NormalizedSlackEvent) => void;
|
|
43
|
+
store: SlackStore;
|
|
44
|
+
handleMessage(raw: string, originWs: WebSocket): void;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function makeConfig(): GatewayConfig {
|
|
48
|
+
return {
|
|
49
|
+
assistantRuntimeBaseUrl: "http://localhost:7821",
|
|
50
|
+
defaultAssistantId: "ast-default",
|
|
51
|
+
gatewayInternalBaseUrl: "http://127.0.0.1:7830",
|
|
52
|
+
logFile: { dir: undefined, retentionDays: 30 },
|
|
53
|
+
maxAttachmentBytes: {
|
|
54
|
+
telegram: 50 * 1024 * 1024,
|
|
55
|
+
slack: 100 * 1024 * 1024,
|
|
56
|
+
whatsapp: 16 * 1024 * 1024,
|
|
57
|
+
default: 50 * 1024 * 1024,
|
|
58
|
+
},
|
|
59
|
+
maxAttachmentConcurrency: 3,
|
|
60
|
+
maxWebhookPayloadBytes: 1024 * 1024,
|
|
61
|
+
port: 7830,
|
|
62
|
+
routingEntries: [
|
|
63
|
+
{
|
|
64
|
+
type: "conversation_id",
|
|
65
|
+
key: "C-thread",
|
|
66
|
+
assistantId: "ast-slack",
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
runtimeInitialBackoffMs: 500,
|
|
70
|
+
runtimeMaxRetries: 2,
|
|
71
|
+
runtimeProxyRequireAuth: false,
|
|
72
|
+
runtimeTimeoutMs: 30000,
|
|
73
|
+
shutdownDrainMs: 5000,
|
|
74
|
+
unmappedPolicy: "reject",
|
|
75
|
+
trustProxy: false,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createSlackStore(): { rawDb: Database; store: SlackStore } {
|
|
80
|
+
const rawDb = new Database(":memory:");
|
|
81
|
+
rawDb.exec(`
|
|
82
|
+
CREATE TABLE slack_active_threads (
|
|
83
|
+
thread_ts TEXT PRIMARY KEY,
|
|
84
|
+
channel_id TEXT,
|
|
85
|
+
tracked_at INTEGER NOT NULL,
|
|
86
|
+
expires_at INTEGER NOT NULL
|
|
87
|
+
);
|
|
88
|
+
CREATE TABLE slack_seen_events (
|
|
89
|
+
event_id TEXT PRIMARY KEY,
|
|
90
|
+
seen_at INTEGER NOT NULL,
|
|
91
|
+
expires_at INTEGER NOT NULL
|
|
92
|
+
);
|
|
93
|
+
CREATE TABLE slack_last_seen_ts (
|
|
94
|
+
key TEXT PRIMARY KEY,
|
|
95
|
+
ts TEXT NOT NULL,
|
|
96
|
+
updated_at INTEGER NOT NULL
|
|
97
|
+
);
|
|
98
|
+
CREATE TABLE contact_channels (
|
|
99
|
+
id TEXT PRIMARY KEY,
|
|
100
|
+
contact_id TEXT NOT NULL,
|
|
101
|
+
type TEXT NOT NULL,
|
|
102
|
+
address TEXT NOT NULL,
|
|
103
|
+
is_primary INTEGER NOT NULL DEFAULT 0,
|
|
104
|
+
external_user_id TEXT,
|
|
105
|
+
external_chat_id TEXT,
|
|
106
|
+
status TEXT NOT NULL DEFAULT 'unverified',
|
|
107
|
+
policy TEXT NOT NULL DEFAULT 'allow',
|
|
108
|
+
revoked_reason TEXT,
|
|
109
|
+
blocked_reason TEXT,
|
|
110
|
+
last_seen_at INTEGER,
|
|
111
|
+
last_interaction INTEGER,
|
|
112
|
+
interaction_count INTEGER NOT NULL DEFAULT 0,
|
|
113
|
+
created_at INTEGER NOT NULL,
|
|
114
|
+
updated_at INTEGER NOT NULL
|
|
115
|
+
);
|
|
116
|
+
`);
|
|
117
|
+
return { rawDb, store: new SlackStore(drizzle(rawDb, { schema })) };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function createHarness(
|
|
121
|
+
store: SlackStore,
|
|
122
|
+
onEvent: (event: NormalizedSlackEvent) => void,
|
|
123
|
+
): SocketModeHarness {
|
|
124
|
+
const harness = Object.create(
|
|
125
|
+
SlackSocketModeClient.prototype,
|
|
126
|
+
) as SocketModeHarness;
|
|
127
|
+
harness.config = {
|
|
128
|
+
appToken: "xapp-test",
|
|
129
|
+
botToken: "xoxb-test",
|
|
130
|
+
botUserId: "UBOT",
|
|
131
|
+
botUsername: "assistant",
|
|
132
|
+
teamName: "Example Team",
|
|
133
|
+
gatewayConfig: makeConfig(),
|
|
134
|
+
};
|
|
135
|
+
harness.onEvent = onEvent;
|
|
136
|
+
harness.store = store;
|
|
137
|
+
return harness;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function makeOpenSocket(): WebSocket {
|
|
141
|
+
return {
|
|
142
|
+
readyState: WebSocket.OPEN,
|
|
143
|
+
send: mock(() => {}),
|
|
144
|
+
} as unknown as WebSocket;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function flushAsyncEventEmission(): Promise<void> {
|
|
148
|
+
return new Promise((resolve) => setTimeout(resolve, 0));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
beforeEach(() => {
|
|
152
|
+
clearUserInfoCache();
|
|
153
|
+
fetchMock = mock(async () => makeSlackUserResponse());
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe("SlackSocketModeClient thread tracking", () => {
|
|
157
|
+
test("accepts unmentioned thread replies immediately after an app mention", async () => {
|
|
158
|
+
const { rawDb, store } = createSlackStore();
|
|
159
|
+
const emitted: NormalizedSlackEvent[] = [];
|
|
160
|
+
const client = createHarness(store, (event) => emitted.push(event));
|
|
161
|
+
const ws = makeOpenSocket();
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
await Promise.all([
|
|
165
|
+
resolveSlackUser("U-mentioned", "xoxb-test"),
|
|
166
|
+
resolveSlackUser("U-reply", "xoxb-test"),
|
|
167
|
+
]);
|
|
168
|
+
|
|
169
|
+
client.handleMessage(
|
|
170
|
+
JSON.stringify({
|
|
171
|
+
envelope_id: "env-mention",
|
|
172
|
+
type: "events_api",
|
|
173
|
+
payload: {
|
|
174
|
+
event_id: "Ev-mention",
|
|
175
|
+
event: {
|
|
176
|
+
type: "app_mention",
|
|
177
|
+
user: "U-mentioned",
|
|
178
|
+
text: "<@UBOT> can you help here?",
|
|
179
|
+
ts: "1700000000.000100",
|
|
180
|
+
channel: "C-thread",
|
|
181
|
+
thread_ts: "1700000000.000000",
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
}),
|
|
185
|
+
ws,
|
|
186
|
+
);
|
|
187
|
+
await flushAsyncEventEmission();
|
|
188
|
+
|
|
189
|
+
expect(emitted).toHaveLength(1);
|
|
190
|
+
expect(emitted[0].event.source.updateId).toBe("Ev-mention");
|
|
191
|
+
expect(emitted[0].threadTs).toBe("1700000000.000000");
|
|
192
|
+
expect(emitted[0].event.source.threadId).toBe("1700000000.000000");
|
|
193
|
+
|
|
194
|
+
client.handleMessage(
|
|
195
|
+
JSON.stringify({
|
|
196
|
+
envelope_id: "env-reply",
|
|
197
|
+
type: "events_api",
|
|
198
|
+
payload: {
|
|
199
|
+
event_id: "Ev-reply",
|
|
200
|
+
event: {
|
|
201
|
+
type: "message",
|
|
202
|
+
user: "U-reply",
|
|
203
|
+
text: "following up without mentioning the bot",
|
|
204
|
+
ts: "1700000000.000200",
|
|
205
|
+
channel: "C-thread",
|
|
206
|
+
channel_type: "channel",
|
|
207
|
+
thread_ts: "1700000000.000000",
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
}),
|
|
211
|
+
ws,
|
|
212
|
+
);
|
|
213
|
+
await flushAsyncEventEmission();
|
|
214
|
+
|
|
215
|
+
expect(emitted).toHaveLength(2);
|
|
216
|
+
expect(emitted[1].event.source.updateId).toBe("Ev-reply");
|
|
217
|
+
expect(emitted[1].event.message.content).toBe(
|
|
218
|
+
"following up without mentioning the bot",
|
|
219
|
+
);
|
|
220
|
+
expect(emitted[1].event.source.chatType).toBe("channel");
|
|
221
|
+
expect(emitted[1].threadTs).toBe("1700000000.000000");
|
|
222
|
+
expect(emitted[1].event.source.threadId).toBe("1700000000.000000");
|
|
223
|
+
} finally {
|
|
224
|
+
rawDb.close();
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("emits a slow app mention before its immediate thread reply", async () => {
|
|
229
|
+
const { rawDb, store } = createSlackStore();
|
|
230
|
+
const emitted: NormalizedSlackEvent[] = [];
|
|
231
|
+
const client = createHarness(store, (event) => emitted.push(event));
|
|
232
|
+
const ws = makeOpenSocket();
|
|
233
|
+
let resolveDelayedMention: ((response: Response) => void) | undefined;
|
|
234
|
+
|
|
235
|
+
fetchMock = mock(async (input) => {
|
|
236
|
+
const url = new URL(String(input));
|
|
237
|
+
const userId = url.searchParams.get("user");
|
|
238
|
+
if (userId === "ULEO") {
|
|
239
|
+
return new Promise<Response>((resolve) => {
|
|
240
|
+
resolveDelayedMention = resolve;
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
return makeSlackUserResponse();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
client.handleMessage(
|
|
248
|
+
JSON.stringify({
|
|
249
|
+
envelope_id: "env-race-mention",
|
|
250
|
+
type: "events_api",
|
|
251
|
+
payload: {
|
|
252
|
+
event_id: "Ev-race-mention",
|
|
253
|
+
event: {
|
|
254
|
+
type: "app_mention",
|
|
255
|
+
user: "U-actor",
|
|
256
|
+
text: "<@UBOT> <@ULEO> can you help here?",
|
|
257
|
+
ts: "1700000000.000150",
|
|
258
|
+
channel: "C-thread",
|
|
259
|
+
thread_ts: "1700000000.000140",
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
ws,
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
client.handleMessage(
|
|
267
|
+
JSON.stringify({
|
|
268
|
+
envelope_id: "env-race-reply",
|
|
269
|
+
type: "events_api",
|
|
270
|
+
payload: {
|
|
271
|
+
event_id: "Ev-race-reply",
|
|
272
|
+
event: {
|
|
273
|
+
type: "message",
|
|
274
|
+
user: "U-reply",
|
|
275
|
+
text: "following up while lookup is still pending",
|
|
276
|
+
ts: "1700000000.000160",
|
|
277
|
+
channel: "C-thread",
|
|
278
|
+
channel_type: "channel",
|
|
279
|
+
thread_ts: "1700000000.000140",
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
}),
|
|
283
|
+
ws,
|
|
284
|
+
);
|
|
285
|
+
await flushAsyncEventEmission();
|
|
286
|
+
|
|
287
|
+
expect(emitted).toHaveLength(0);
|
|
288
|
+
|
|
289
|
+
expect(resolveDelayedMention).toBeDefined();
|
|
290
|
+
resolveDelayedMention!(makeSlackUserResponse());
|
|
291
|
+
await flushAsyncEventEmission();
|
|
292
|
+
|
|
293
|
+
expect(emitted).toHaveLength(2);
|
|
294
|
+
expect(emitted[0].event.source.updateId).toBe("Ev-race-mention");
|
|
295
|
+
expect(emitted[0].event.message.content).toBe(
|
|
296
|
+
"@Example User can you help here?",
|
|
297
|
+
);
|
|
298
|
+
expect(emitted[1].event.source.updateId).toBe("Ev-race-reply");
|
|
299
|
+
expect(emitted[1].event.message.content).toBe(
|
|
300
|
+
"following up while lookup is still pending",
|
|
301
|
+
);
|
|
302
|
+
} finally {
|
|
303
|
+
rawDb.close();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("does not pre-track unrouted app mention threads during slow mentioned-user lookup", async () => {
|
|
308
|
+
const { rawDb, store } = createSlackStore();
|
|
309
|
+
const emitted: NormalizedSlackEvent[] = [];
|
|
310
|
+
const client = createHarness(store, (event) => emitted.push(event));
|
|
311
|
+
const ws = makeOpenSocket();
|
|
312
|
+
let resolveDelayedMention: ((response: Response) => void) | undefined;
|
|
313
|
+
|
|
314
|
+
fetchMock = mock(async (input) => {
|
|
315
|
+
const url = new URL(String(input));
|
|
316
|
+
const userId = url.searchParams.get("user");
|
|
317
|
+
if (userId === "USLOW") {
|
|
318
|
+
return new Promise<Response>((resolve) => {
|
|
319
|
+
resolveDelayedMention = resolve;
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
return makeSlackUserResponse();
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
client.handleMessage(
|
|
327
|
+
JSON.stringify({
|
|
328
|
+
envelope_id: "env-unrouted-mention",
|
|
329
|
+
type: "events_api",
|
|
330
|
+
payload: {
|
|
331
|
+
event_id: "Ev-unrouted-mention",
|
|
332
|
+
event: {
|
|
333
|
+
type: "app_mention",
|
|
334
|
+
user: "U-actor",
|
|
335
|
+
text: "<@UBOT> <@USLOW> can you help here?",
|
|
336
|
+
ts: "1700000000.000250",
|
|
337
|
+
channel: "C-unrouted",
|
|
338
|
+
thread_ts: "1700000000.000240",
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
}),
|
|
342
|
+
ws,
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
client.handleMessage(
|
|
346
|
+
JSON.stringify({
|
|
347
|
+
envelope_id: "env-unrouted-reply",
|
|
348
|
+
type: "events_api",
|
|
349
|
+
payload: {
|
|
350
|
+
event_id: "Ev-unrouted-reply",
|
|
351
|
+
event: {
|
|
352
|
+
type: "message",
|
|
353
|
+
user: "U-reply",
|
|
354
|
+
text: "reply should not be admitted by rejected mention",
|
|
355
|
+
ts: "1700000000.000260",
|
|
356
|
+
channel: "C-unrouted",
|
|
357
|
+
channel_type: "channel",
|
|
358
|
+
thread_ts: "1700000000.000240",
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
}),
|
|
362
|
+
ws,
|
|
363
|
+
);
|
|
364
|
+
await flushAsyncEventEmission();
|
|
365
|
+
|
|
366
|
+
expect(emitted).toHaveLength(0);
|
|
367
|
+
|
|
368
|
+
expect(resolveDelayedMention).toBeDefined();
|
|
369
|
+
resolveDelayedMention!(makeSlackUserResponse());
|
|
370
|
+
await flushAsyncEventEmission();
|
|
371
|
+
|
|
372
|
+
expect(emitted).toHaveLength(0);
|
|
373
|
+
} finally {
|
|
374
|
+
rawDb.close();
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
test("accepts unmentioned thread replies after a top-level app mention", async () => {
|
|
379
|
+
const { rawDb, store } = createSlackStore();
|
|
380
|
+
const emitted: NormalizedSlackEvent[] = [];
|
|
381
|
+
const client = createHarness(store, (event) => emitted.push(event));
|
|
382
|
+
const ws = makeOpenSocket();
|
|
383
|
+
|
|
384
|
+
try {
|
|
385
|
+
await Promise.all([
|
|
386
|
+
resolveSlackUser("U-mentioned", "xoxb-test"),
|
|
387
|
+
resolveSlackUser("U-reply", "xoxb-test"),
|
|
388
|
+
]);
|
|
389
|
+
|
|
390
|
+
client.handleMessage(
|
|
391
|
+
JSON.stringify({
|
|
392
|
+
envelope_id: "env-top-level-mention",
|
|
393
|
+
type: "events_api",
|
|
394
|
+
payload: {
|
|
395
|
+
event_id: "Ev-top-level-mention",
|
|
396
|
+
event: {
|
|
397
|
+
type: "app_mention",
|
|
398
|
+
user: "U-mentioned",
|
|
399
|
+
text: "<@UBOT> can you help here?",
|
|
400
|
+
ts: "1700000000.000300",
|
|
401
|
+
channel: "C-thread",
|
|
402
|
+
},
|
|
403
|
+
},
|
|
404
|
+
}),
|
|
405
|
+
ws,
|
|
406
|
+
);
|
|
407
|
+
await flushAsyncEventEmission();
|
|
408
|
+
|
|
409
|
+
expect(emitted).toHaveLength(1);
|
|
410
|
+
expect(emitted[0].event.source.updateId).toBe("Ev-top-level-mention");
|
|
411
|
+
expect(emitted[0].threadTs).toBe("1700000000.000300");
|
|
412
|
+
expect(emitted[0].event.source.threadId).toBeUndefined();
|
|
413
|
+
|
|
414
|
+
client.handleMessage(
|
|
415
|
+
JSON.stringify({
|
|
416
|
+
envelope_id: "env-top-level-reply",
|
|
417
|
+
type: "events_api",
|
|
418
|
+
payload: {
|
|
419
|
+
event_id: "Ev-top-level-reply",
|
|
420
|
+
event: {
|
|
421
|
+
type: "message",
|
|
422
|
+
user: "U-reply",
|
|
423
|
+
text: "following up in the new thread",
|
|
424
|
+
ts: "1700000000.000400",
|
|
425
|
+
channel: "C-thread",
|
|
426
|
+
channel_type: "channel",
|
|
427
|
+
thread_ts: "1700000000.000300",
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
}),
|
|
431
|
+
ws,
|
|
432
|
+
);
|
|
433
|
+
await flushAsyncEventEmission();
|
|
434
|
+
|
|
435
|
+
expect(emitted).toHaveLength(2);
|
|
436
|
+
expect(emitted[1].event.source.updateId).toBe("Ev-top-level-reply");
|
|
437
|
+
expect(emitted[1].event.message.content).toBe(
|
|
438
|
+
"following up in the new thread",
|
|
439
|
+
);
|
|
440
|
+
expect(emitted[1].event.source.chatType).toBe("channel");
|
|
441
|
+
expect(emitted[1].threadTs).toBe("1700000000.000300");
|
|
442
|
+
expect(emitted[1].event.source.threadId).toBe("1700000000.000300");
|
|
443
|
+
} finally {
|
|
444
|
+
rawDb.close();
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("emits direct messages with im chat type for assistant backfill", async () => {
|
|
449
|
+
const { rawDb, store } = createSlackStore();
|
|
450
|
+
const emitted: NormalizedSlackEvent[] = [];
|
|
451
|
+
const client = createHarness(store, (event) => emitted.push(event));
|
|
452
|
+
const ws = makeOpenSocket();
|
|
453
|
+
|
|
454
|
+
try {
|
|
455
|
+
await resolveSlackUser("U-dm", "xoxb-test");
|
|
456
|
+
|
|
457
|
+
client.handleMessage(
|
|
458
|
+
JSON.stringify({
|
|
459
|
+
envelope_id: "env-dm",
|
|
460
|
+
type: "events_api",
|
|
461
|
+
payload: {
|
|
462
|
+
event_id: "Ev-dm",
|
|
463
|
+
event: {
|
|
464
|
+
type: "message",
|
|
465
|
+
user: "U-dm",
|
|
466
|
+
text: "hello from dm",
|
|
467
|
+
ts: "1700000000.000500",
|
|
468
|
+
channel: "D-direct",
|
|
469
|
+
channel_type: "im",
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
}),
|
|
473
|
+
ws,
|
|
474
|
+
);
|
|
475
|
+
await flushAsyncEventEmission();
|
|
476
|
+
|
|
477
|
+
expect(emitted).toHaveLength(1);
|
|
478
|
+
expect(emitted[0].event.source.updateId).toBe("Ev-dm");
|
|
479
|
+
expect(emitted[0].event.source.chatType).toBe("im");
|
|
480
|
+
expect(emitted[0].event.message.conversationExternalId).toBe("D-direct");
|
|
481
|
+
expect(emitted[0].threadTs).toBeUndefined();
|
|
482
|
+
expect(emitted[0].event.source.threadId).toBeUndefined();
|
|
483
|
+
} finally {
|
|
484
|
+
rawDb.close();
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
test.each([
|
|
489
|
+
{
|
|
490
|
+
name: "reaction",
|
|
491
|
+
seedEventId: "Ev-reaction",
|
|
492
|
+
seedEvent: {
|
|
493
|
+
type: "reaction_added",
|
|
494
|
+
user: "U-reactor",
|
|
495
|
+
reaction: "eyes",
|
|
496
|
+
item: {
|
|
497
|
+
type: "message",
|
|
498
|
+
channel: "C-thread",
|
|
499
|
+
ts: "1700000000.000500",
|
|
500
|
+
},
|
|
501
|
+
item_user: "U-author",
|
|
502
|
+
event_ts: "1700000000.000501",
|
|
503
|
+
},
|
|
504
|
+
replyThreadTs: "1700000000.000500",
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
name: "message edit",
|
|
508
|
+
seedEventId: "Ev-edit",
|
|
509
|
+
seedEvent: {
|
|
510
|
+
type: "message",
|
|
511
|
+
subtype: "message_changed",
|
|
512
|
+
channel: "C-thread",
|
|
513
|
+
channel_type: "channel",
|
|
514
|
+
message: {
|
|
515
|
+
user: "U-editor",
|
|
516
|
+
text: "edited message",
|
|
517
|
+
ts: "1700000000.000600",
|
|
518
|
+
thread_ts: "1700000000.000550",
|
|
519
|
+
},
|
|
520
|
+
},
|
|
521
|
+
replyThreadTs: "1700000000.000550",
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
name: "message delete",
|
|
525
|
+
seedEventId: "Ev-delete",
|
|
526
|
+
seedEvent: {
|
|
527
|
+
type: "message",
|
|
528
|
+
subtype: "message_deleted",
|
|
529
|
+
channel: "C-thread",
|
|
530
|
+
channel_type: "channel",
|
|
531
|
+
deleted_ts: "1700000000.000700",
|
|
532
|
+
previous_message: {
|
|
533
|
+
user: "U-author",
|
|
534
|
+
text: "deleted message",
|
|
535
|
+
ts: "1700000000.000700",
|
|
536
|
+
thread_ts: "1700000000.000650",
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
replyThreadTs: "1700000000.000650",
|
|
540
|
+
},
|
|
541
|
+
])(
|
|
542
|
+
"does not arm active thread tracking for admitted $name events",
|
|
543
|
+
async ({ seedEventId, seedEvent, replyThreadTs }) => {
|
|
544
|
+
const { rawDb, store } = createSlackStore();
|
|
545
|
+
const emitted: NormalizedSlackEvent[] = [];
|
|
546
|
+
const client = createHarness(store, (event) => emitted.push(event));
|
|
547
|
+
const ws = makeOpenSocket();
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
await Promise.all([
|
|
551
|
+
resolveSlackUser("U-reactor", "xoxb-test"),
|
|
552
|
+
resolveSlackUser("U-editor", "xoxb-test"),
|
|
553
|
+
resolveSlackUser("U-author", "xoxb-test"),
|
|
554
|
+
resolveSlackUser("U-reply", "xoxb-test"),
|
|
555
|
+
]);
|
|
556
|
+
|
|
557
|
+
client.handleMessage(
|
|
558
|
+
JSON.stringify({
|
|
559
|
+
envelope_id: `env-${seedEventId}`,
|
|
560
|
+
type: "events_api",
|
|
561
|
+
payload: {
|
|
562
|
+
event_id: seedEventId,
|
|
563
|
+
event: seedEvent,
|
|
564
|
+
},
|
|
565
|
+
}),
|
|
566
|
+
ws,
|
|
567
|
+
);
|
|
568
|
+
await flushAsyncEventEmission();
|
|
569
|
+
|
|
570
|
+
expect(emitted).toHaveLength(1);
|
|
571
|
+
expect(emitted[0].event.source.updateId).toBe(seedEventId);
|
|
572
|
+
expect(emitted[0].threadTs).toBe(replyThreadTs);
|
|
573
|
+
|
|
574
|
+
client.handleMessage(
|
|
575
|
+
JSON.stringify({
|
|
576
|
+
envelope_id: `env-reply-${seedEventId}`,
|
|
577
|
+
type: "events_api",
|
|
578
|
+
payload: {
|
|
579
|
+
event_id: `Ev-reply-${seedEventId}`,
|
|
580
|
+
event: {
|
|
581
|
+
type: "message",
|
|
582
|
+
user: "U-reply",
|
|
583
|
+
text: "unmentioned reply should stay filtered",
|
|
584
|
+
ts: `${replyThreadTs}-reply`,
|
|
585
|
+
channel: "C-thread",
|
|
586
|
+
channel_type: "channel",
|
|
587
|
+
thread_ts: replyThreadTs,
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
}),
|
|
591
|
+
ws,
|
|
592
|
+
);
|
|
593
|
+
await flushAsyncEventEmission();
|
|
594
|
+
|
|
595
|
+
expect(emitted).toHaveLength(1);
|
|
596
|
+
} finally {
|
|
597
|
+
rawDb.close();
|
|
598
|
+
}
|
|
599
|
+
},
|
|
600
|
+
);
|
|
601
|
+
|
|
602
|
+
test("renders live app mention user IDs as display-name labels", async () => {
|
|
603
|
+
const { rawDb, store } = createSlackStore();
|
|
604
|
+
const emitted: NormalizedSlackEvent[] = [];
|
|
605
|
+
const client = createHarness(store, (event) => emitted.push(event));
|
|
606
|
+
const ws = makeOpenSocket();
|
|
607
|
+
|
|
608
|
+
fetchMock = mock(async (input) => {
|
|
609
|
+
const url = new URL(String(input));
|
|
610
|
+
const userId = url.searchParams.get("user");
|
|
611
|
+
if (userId === "ULEO") {
|
|
612
|
+
return new Response(
|
|
613
|
+
JSON.stringify({
|
|
614
|
+
ok: true,
|
|
615
|
+
user: {
|
|
616
|
+
name: "leo",
|
|
617
|
+
profile: { display_name: "Leo" },
|
|
618
|
+
},
|
|
619
|
+
}),
|
|
620
|
+
{ status: 200, headers: { "content-type": "application/json" } },
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
return makeSlackUserResponse();
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
try {
|
|
627
|
+
client.handleMessage(
|
|
628
|
+
JSON.stringify({
|
|
629
|
+
envelope_id: "env-mention-label",
|
|
630
|
+
type: "events_api",
|
|
631
|
+
payload: {
|
|
632
|
+
event_id: "Ev-mention-label",
|
|
633
|
+
event: {
|
|
634
|
+
type: "app_mention",
|
|
635
|
+
user: "U-actor",
|
|
636
|
+
text: "<@UBOT> <@ULEO> please look",
|
|
637
|
+
ts: "1700000000.000800",
|
|
638
|
+
channel: "C-thread",
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
}),
|
|
642
|
+
ws,
|
|
643
|
+
);
|
|
644
|
+
await flushAsyncEventEmission();
|
|
645
|
+
|
|
646
|
+
expect(emitted).toHaveLength(1);
|
|
647
|
+
expect(emitted[0].event.message.content).toBe("@Leo please look");
|
|
648
|
+
expect(emitted[0].event.message.content).not.toContain("<@ULEO>");
|
|
649
|
+
expect(emitted[0].event.message.content).not.toContain("ULEO");
|
|
650
|
+
} finally {
|
|
651
|
+
rawDb.close();
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
});
|
|
@@ -40,7 +40,6 @@ function makeConfig(overrides: Partial<GatewayConfig> = {}): GatewayConfig {
|
|
|
40
40
|
defaultAssistantId: undefined,
|
|
41
41
|
unmappedPolicy: "reject",
|
|
42
42
|
port: 7830,
|
|
43
|
-
runtimeProxyEnabled: false,
|
|
44
43
|
runtimeProxyRequireAuth: true,
|
|
45
44
|
shutdownDrainMs: 5000,
|
|
46
45
|
runtimeTimeoutMs: 30000,
|