@vellumai/assistant 0.5.14 → 0.5.16
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 +2 -2
- package/docs/architecture/integrations.md +15 -14
- package/knip.json +3 -1
- package/openapi.yaml +11 -43
- package/package.json +1 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +3 -375
- package/src/__tests__/ces-rpc-credential-backend.test.ts +4 -1
- package/src/__tests__/checker.test.ts +59 -0
- package/src/__tests__/cli-command-risk-guard.test.ts +98 -10
- package/src/__tests__/cli-memory.test.ts +372 -0
- package/src/__tests__/computer-use-skill-manifest-regression.test.ts +12 -2
- package/src/__tests__/config-schema.test.ts +0 -2
- package/src/__tests__/config-watcher-feature-flags.test.ts +211 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +7 -4
- package/src/__tests__/conversation-slash-commands.test.ts +2 -6
- package/src/__tests__/conversation-usage.test.ts +1 -0
- package/src/__tests__/credential-security-e2e.test.ts +4 -1
- package/src/__tests__/docker-signing-key-bootstrap.test.ts +7 -73
- package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +6 -7
- package/src/__tests__/guardian-routing-invariants.test.ts +151 -0
- package/src/__tests__/heartbeat-service.test.ts +1 -3
- package/src/__tests__/intent-routing.test.ts +6 -18
- package/src/__tests__/log-export-workspace.test.ts +2 -28
- package/src/__tests__/managed-skill-lifecycle.test.ts +7 -37
- package/src/__tests__/managed-store.test.ts +2 -10
- package/src/__tests__/messaging-send-tool.test.ts +6 -6
- package/src/__tests__/migration-cross-version-compatibility.test.ts +1 -29
- package/src/__tests__/migration-export-http.test.ts +3 -34
- package/src/__tests__/migration-import-commit-http.test.ts +1 -29
- package/src/__tests__/migration-import-preflight-http.test.ts +3 -34
- package/src/__tests__/no-domain-routing-in-prompt-guard.test.ts +2 -1
- package/src/__tests__/oauth-apps-routes.test.ts +120 -10
- package/src/__tests__/oauth-connect-orchestrator.test.ts +709 -0
- package/src/__tests__/oauth-provider-serializer.test.ts +2 -1
- package/src/__tests__/oauth-provider-visibility.test.ts +149 -0
- package/src/__tests__/oauth-providers-routes.test.ts +5 -2
- package/src/__tests__/oauth-store.test.ts +0 -5
- package/src/__tests__/outlook-messaging-provider.test.ts +576 -0
- package/src/__tests__/path-policy.test.ts +2 -17
- package/src/__tests__/permission-types.test.ts +0 -1
- package/src/__tests__/platform-callback-registration.test.ts +3 -7
- package/src/__tests__/provider-commit-message-generator.test.ts +0 -1
- package/src/__tests__/provider-error-scenarios.test.ts +0 -2
- package/src/__tests__/qdrant-manager.test.ts +68 -21
- package/src/__tests__/require-fresh-approval.test.ts +0 -1
- package/src/__tests__/sandbox-diagnostics.test.ts +20 -29
- package/src/__tests__/scaffold-managed-skill-tool.test.ts +2 -10
- package/src/__tests__/secret-allowlist.test.ts +20 -35
- package/src/__tests__/shell-credential-ref.test.ts +0 -5
- package/src/__tests__/skill-load-feature-flag.test.ts +2 -43
- package/src/__tests__/skill-load-inline-command.test.ts +3 -65
- package/src/__tests__/skill-load-inline-includes.test.ts +3 -65
- package/src/__tests__/skill-load-tool.test.ts +3 -67
- package/src/__tests__/skill-memory.test.ts +362 -119
- package/src/__tests__/skills.test.ts +22 -49
- package/src/__tests__/slack-channel-config.test.ts +2 -21
- package/src/__tests__/starter-bundle.test.ts +2 -8
- package/src/__tests__/stt-hints.test.ts +7 -2
- package/src/__tests__/system-prompt.test.ts +25 -45
- package/src/__tests__/task-compiler.test.ts +0 -21
- package/src/__tests__/task-management-tools.test.ts +0 -21
- package/src/__tests__/task-memory-cleanup.test.ts +0 -21
- package/src/__tests__/task-runner.test.ts +0 -21
- package/src/__tests__/task-scheduler.test.ts +0 -21
- package/src/__tests__/terminal-tools.test.ts +1 -17
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +0 -79
- package/src/__tests__/tool-approval-handler.test.ts +1 -20
- package/src/__tests__/tool-execution-abort-cleanup.test.ts +2 -11
- package/src/__tests__/tool-execution-pipeline.benchmark.test.ts +1 -25
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +0 -1
- package/src/__tests__/tool-executor.test.ts +0 -1
- package/src/__tests__/tool-grant-request-escalation.test.ts +1 -20
- package/src/__tests__/tool-preview-lifecycle.test.ts +0 -20
- package/src/__tests__/trust-store.test.ts +9 -41
- package/src/__tests__/trusted-contact-approval-notifier.test.ts +1 -30
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +1 -21
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +0 -22
- package/src/__tests__/trusted-contact-multichannel.test.ts +0 -22
- package/src/__tests__/trusted-contact-verification.test.ts +0 -22
- package/src/__tests__/turn-boundary-resolution.test.ts +0 -28
- package/src/__tests__/twilio-provider.test.ts +0 -16
- package/src/__tests__/twilio-routes-twiml.test.ts +7 -12
- package/src/__tests__/twilio-routes.test.ts +0 -24
- package/src/__tests__/update-bulletin.test.ts +17 -89
- package/src/__tests__/usage-cache-backfill-migration.test.ts +0 -20
- package/src/__tests__/usage-routes.test.ts +0 -21
- package/src/__tests__/user-reference.test.ts +1 -5
- package/src/__tests__/vbundle-pax-and-symlink.test.ts +4 -34
- package/src/__tests__/vellum-self-knowledge-inline-command.test.ts +2 -53
- package/src/__tests__/voice-invite-redemption.test.ts +0 -21
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +0 -24
- package/src/__tests__/voice-session-bridge.test.ts +0 -21
- package/src/__tests__/workspace-migration-009-backfill-conversation-disk-view.test.ts +2 -23
- package/src/__tests__/workspace-migration-012-rename-conversation-disk-view-dirs.test.ts +2 -2
- package/src/__tests__/workspace-migration-013-repair-conversation-disk-view.test.ts +2 -23
- package/src/__tests__/workspace-migration-down-functions.test.ts +0 -6
- package/src/acp/client-handler.ts +1 -2
- package/src/cli/__tests__/notifications.test.ts +0 -22
- package/src/cli/cli-memory.ts +176 -0
- package/src/cli/commands/oauth/__tests__/providers-update.test.ts +1 -1
- package/src/cli/commands/oauth/connect.ts +15 -0
- package/src/cli/commands/oauth/providers.ts +49 -42
- package/src/cli/commands/platform/__tests__/connect.test.ts +2 -48
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +2 -48
- package/src/cli/commands/platform/__tests__/status.test.ts +0 -50
- package/src/config/bundled-skills/computer-use/TOOLS.json +7 -7
- package/src/config/bundled-skills/messaging/SKILL.md +17 -2
- package/src/config/bundled-skills/settings/TOOLS.json +3 -3
- package/src/config/feature-flag-registry.json +16 -0
- package/src/config/loader.ts +4 -0
- package/src/config/schemas/security.ts +0 -6
- package/src/config/schemas/services.ts +8 -0
- package/src/context/window-manager.ts +28 -9
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/daemon/config-watcher.ts +51 -0
- package/src/daemon/conversation-agent-loop.ts +3 -2
- package/src/daemon/conversation-process.ts +1 -0
- package/src/daemon/conversation-usage.ts +1 -0
- package/src/daemon/handlers/skills.ts +9 -1
- package/src/daemon/lifecycle.ts +13 -4
- package/src/daemon/message-types/conversations.ts +1 -0
- package/src/daemon/providers-setup.ts +2 -0
- package/src/daemon/server.ts +26 -22
- package/src/events/domain-events.ts +1 -2
- package/src/memory/db-init.ts +9 -0
- package/src/memory/job-handlers/batch-extraction.ts +16 -4
- package/src/memory/job-handlers/embedding.test.ts +3 -27
- package/src/memory/job-handlers/journal-carry-forward.test.ts +1 -29
- package/src/memory/llm-usage-store.ts +35 -2
- package/src/memory/migrations/201-oauth-providers-feature-flag.ts +11 -0
- package/src/memory/migrations/202-drop-callback-transport-column.ts +13 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/qdrant-manager.ts +26 -5
- package/src/memory/query-expansion.ts +1 -1
- package/src/memory/retriever.test.ts +22 -20
- package/src/memory/retriever.ts +10 -2
- package/src/memory/schema/oauth.ts +1 -1
- package/src/memory/search/mmr.ts +8 -5
- package/src/memory/slack-thread-store.ts +17 -0
- package/src/messaging/providers/outlook/adapter.ts +193 -0
- package/src/messaging/providers/outlook/client.ts +311 -0
- package/src/messaging/providers/outlook/types.ts +83 -0
- package/src/notifications/adapters/slack.ts +1 -1
- package/src/oauth/__tests__/identity-verifier.test.ts +1 -1
- package/src/oauth/connect-orchestrator.ts +10 -3
- package/src/oauth/oauth-store.ts +10 -11
- package/src/oauth/provider-serializer.ts +3 -0
- package/src/oauth/provider-visibility.ts +16 -0
- package/src/oauth/seed-providers.ts +49 -17
- package/src/permissions/checker.ts +39 -7
- package/src/permissions/types.ts +2 -4
- package/src/prompts/journal-context.ts +9 -11
- package/src/prompts/system-prompt.ts +3 -64
- package/src/prompts/templates/UPDATES.md +6 -0
- package/src/runtime/auth/__tests__/credential-service.test.ts +1 -27
- package/src/runtime/auth/__tests__/token-service.test.ts +1 -25
- package/src/runtime/auth/route-policy.ts +0 -4
- package/src/runtime/guardian-reply-router.ts +6 -2
- package/src/runtime/routes/conversation-query-routes.ts +2 -58
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +43 -2
- package/src/runtime/routes/memory-item-routes.test.ts +0 -17
- package/src/runtime/routes/memory-item-routes.ts +103 -12
- package/src/runtime/routes/oauth-apps.ts +18 -1
- package/src/runtime/routes/oauth-providers.ts +13 -1
- package/src/runtime/routes/settings-routes.ts +1 -0
- package/src/runtime/routes/usage-routes.ts +19 -2
- package/src/runtime/routes/work-items-routes.test.ts +0 -21
- package/src/runtime/routes/workspace-routes.test.ts +3 -27
- package/src/security/secret-allowlist.ts +4 -4
- package/src/skills/skill-memory.ts +62 -23
- package/src/tools/memory/handlers.test.ts +1 -29
- package/src/tools/permission-checker.ts +0 -18
- package/src/tools/skills/skill-script-runner.ts +1 -1
- package/src/util/device-id.ts +3 -65
- package/src/workspace/git-service.ts +27 -6
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import type { OutlookMailFolder } from "../messaging/providers/outlook/types.js";
|
|
4
|
+
import type { OutlookMessage } from "../messaging/providers/outlook/types.js";
|
|
5
|
+
import type { OAuthConnection } from "../oauth/connection.js";
|
|
6
|
+
|
|
7
|
+
// ── Mocks ───────────────────────────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
const mockGetProfile = mock(() =>
|
|
10
|
+
Promise.resolve({
|
|
11
|
+
displayName: "Test User",
|
|
12
|
+
mail: "test@outlook.com",
|
|
13
|
+
userPrincipalName: "test@outlook.com",
|
|
14
|
+
}),
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
const mockListMailFolders = mock(() =>
|
|
18
|
+
Promise.resolve([
|
|
19
|
+
{
|
|
20
|
+
id: "inbox-id",
|
|
21
|
+
displayName: "Inbox",
|
|
22
|
+
totalItemCount: 100,
|
|
23
|
+
unreadItemCount: 5,
|
|
24
|
+
childFolderCount: 2,
|
|
25
|
+
} satisfies OutlookMailFolder,
|
|
26
|
+
]),
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const mockListMessages = mock(() =>
|
|
30
|
+
Promise.resolve({ value: [] as OutlookMessage[] }),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const mockSearchMessages = mock(() =>
|
|
34
|
+
Promise.resolve({
|
|
35
|
+
value: [] as OutlookMessage[],
|
|
36
|
+
"@odata.count": 0 as number | undefined,
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
const mockSendMessage = mock(() => Promise.resolve(undefined));
|
|
41
|
+
const mockMarkMessageRead = mock(() => Promise.resolve(undefined));
|
|
42
|
+
const mockReplyToMessage = mock(() => Promise.resolve(undefined));
|
|
43
|
+
|
|
44
|
+
mock.module("../messaging/providers/outlook/client.js", () => ({
|
|
45
|
+
getProfile: mockGetProfile,
|
|
46
|
+
listMailFolders: mockListMailFolders,
|
|
47
|
+
listMessages: mockListMessages,
|
|
48
|
+
searchMessages: mockSearchMessages,
|
|
49
|
+
sendMessage: mockSendMessage,
|
|
50
|
+
markMessageRead: mockMarkMessageRead,
|
|
51
|
+
replyToMessage: mockReplyToMessage,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
import { outlookMessagingProvider } from "../messaging/providers/outlook/adapter.js";
|
|
55
|
+
|
|
56
|
+
// ── Helpers ─────────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
function createMockConnection(): OAuthConnection {
|
|
59
|
+
return {
|
|
60
|
+
id: "outlook-conn-1",
|
|
61
|
+
providerKey: "outlook",
|
|
62
|
+
accountInfo: "test@outlook.com",
|
|
63
|
+
request: mock(() =>
|
|
64
|
+
Promise.resolve({ status: 200, headers: {}, body: {} }),
|
|
65
|
+
),
|
|
66
|
+
withToken: <T>(fn: (token: string) => Promise<T>) =>
|
|
67
|
+
fn("mock-access-token"),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function createMockOutlookMessage(
|
|
72
|
+
overrides?: Partial<OutlookMessage>,
|
|
73
|
+
): OutlookMessage {
|
|
74
|
+
return {
|
|
75
|
+
id: "msg-1",
|
|
76
|
+
conversationId: "conv-1",
|
|
77
|
+
subject: "Test Subject",
|
|
78
|
+
bodyPreview: "Preview of the message body",
|
|
79
|
+
body: { contentType: "text", content: "Full message body text" },
|
|
80
|
+
from: {
|
|
81
|
+
emailAddress: { name: "Sender Name", address: "sender@example.com" },
|
|
82
|
+
},
|
|
83
|
+
toRecipients: [
|
|
84
|
+
{
|
|
85
|
+
emailAddress: {
|
|
86
|
+
name: "Recipient",
|
|
87
|
+
address: "recipient@example.com",
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
],
|
|
91
|
+
ccRecipients: [],
|
|
92
|
+
receivedDateTime: "2024-06-15T10:30:00Z",
|
|
93
|
+
isRead: false,
|
|
94
|
+
hasAttachments: false,
|
|
95
|
+
parentFolderId: "inbox-id",
|
|
96
|
+
categories: ["important"],
|
|
97
|
+
flag: { flagStatus: "notFlagged" },
|
|
98
|
+
...overrides,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Tests ───────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
describe("Outlook messaging provider", () => {
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
mockGetProfile.mockClear();
|
|
107
|
+
mockListMailFolders.mockClear();
|
|
108
|
+
mockListMessages.mockClear();
|
|
109
|
+
mockSearchMessages.mockClear();
|
|
110
|
+
mockSendMessage.mockClear();
|
|
111
|
+
mockMarkMessageRead.mockClear();
|
|
112
|
+
mockReplyToMessage.mockClear();
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// ── testConnection ──────────────────────────────────────────────────────
|
|
116
|
+
|
|
117
|
+
describe("testConnection", () => {
|
|
118
|
+
test("returns connected info when connection is valid", async () => {
|
|
119
|
+
const conn = createMockConnection();
|
|
120
|
+
const result = await outlookMessagingProvider.testConnection(conn);
|
|
121
|
+
|
|
122
|
+
expect(mockGetProfile).toHaveBeenCalledWith(conn);
|
|
123
|
+
expect(result).toEqual({
|
|
124
|
+
connected: true,
|
|
125
|
+
user: "test@outlook.com",
|
|
126
|
+
platform: "outlook",
|
|
127
|
+
});
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("uses userPrincipalName when mail is empty", async () => {
|
|
131
|
+
mockGetProfile.mockImplementation(() =>
|
|
132
|
+
Promise.resolve({
|
|
133
|
+
displayName: "Test User",
|
|
134
|
+
mail: "",
|
|
135
|
+
userPrincipalName: "upn@outlook.com",
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const conn = createMockConnection();
|
|
140
|
+
const result = await outlookMessagingProvider.testConnection(conn);
|
|
141
|
+
|
|
142
|
+
expect(result.user).toBe("upn@outlook.com");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("throws when connection is undefined", async () => {
|
|
146
|
+
await expect(
|
|
147
|
+
outlookMessagingProvider.testConnection(undefined),
|
|
148
|
+
).rejects.toThrow("Outlook requires an OAuth connection");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── listConversations ──────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
describe("listConversations", () => {
|
|
155
|
+
test("maps mail folders to conversations", async () => {
|
|
156
|
+
const conn = createMockConnection();
|
|
157
|
+
const conversations =
|
|
158
|
+
await outlookMessagingProvider.listConversations(conn);
|
|
159
|
+
|
|
160
|
+
expect(mockListMailFolders).toHaveBeenCalledWith(conn);
|
|
161
|
+
expect(conversations).toHaveLength(1);
|
|
162
|
+
expect(conversations[0]).toMatchObject({
|
|
163
|
+
id: "inbox-id",
|
|
164
|
+
name: "Inbox",
|
|
165
|
+
type: "inbox",
|
|
166
|
+
platform: "outlook",
|
|
167
|
+
unreadCount: 5,
|
|
168
|
+
metadata: {
|
|
169
|
+
totalItemCount: 100,
|
|
170
|
+
childFolderCount: 2,
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("throws when connection is undefined", async () => {
|
|
176
|
+
await expect(
|
|
177
|
+
outlookMessagingProvider.listConversations(undefined),
|
|
178
|
+
).rejects.toThrow("Outlook requires an OAuth connection");
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ── getHistory ─────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
describe("getHistory", () => {
|
|
185
|
+
test("calls listMessages with folder ID and maps results", async () => {
|
|
186
|
+
const msg = createMockOutlookMessage();
|
|
187
|
+
mockListMessages.mockImplementation(() =>
|
|
188
|
+
Promise.resolve({ value: [msg] }),
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const conn = createMockConnection();
|
|
192
|
+
const messages = await outlookMessagingProvider.getHistory(
|
|
193
|
+
conn,
|
|
194
|
+
"folder-id",
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(mockListMessages).toHaveBeenCalledWith(conn, {
|
|
198
|
+
folderId: "folder-id",
|
|
199
|
+
top: 50,
|
|
200
|
+
orderby: "receivedDateTime desc",
|
|
201
|
+
select: expect.stringContaining("id,conversationId"),
|
|
202
|
+
});
|
|
203
|
+
expect(messages).toHaveLength(1);
|
|
204
|
+
expect(messages[0].id).toBe("msg-1");
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test("respects limit option", async () => {
|
|
208
|
+
mockListMessages.mockImplementation(() => Promise.resolve({ value: [] }));
|
|
209
|
+
|
|
210
|
+
const conn = createMockConnection();
|
|
211
|
+
await outlookMessagingProvider.getHistory(conn, "folder-id", {
|
|
212
|
+
limit: 10,
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
expect(mockListMessages).toHaveBeenCalledWith(
|
|
216
|
+
conn,
|
|
217
|
+
expect.objectContaining({ top: 10 }),
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("returns empty array when response has no value", async () => {
|
|
222
|
+
mockListMessages.mockImplementation(
|
|
223
|
+
() => Promise.resolve({}) as ReturnType<typeof mockListMessages>,
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
const conn = createMockConnection();
|
|
227
|
+
const messages = await outlookMessagingProvider.getHistory(
|
|
228
|
+
conn,
|
|
229
|
+
"folder-id",
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
expect(messages).toEqual([]);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("throws when connection is undefined", async () => {
|
|
236
|
+
await expect(
|
|
237
|
+
outlookMessagingProvider.getHistory(undefined, "folder-id"),
|
|
238
|
+
).rejects.toThrow("Outlook requires an OAuth connection");
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ── search ─────────────────────────────────────────────────────────────
|
|
243
|
+
|
|
244
|
+
describe("search", () => {
|
|
245
|
+
test("calls searchMessages with query and returns SearchResult", async () => {
|
|
246
|
+
const msg = createMockOutlookMessage();
|
|
247
|
+
mockSearchMessages.mockImplementation(() =>
|
|
248
|
+
Promise.resolve({
|
|
249
|
+
value: [msg],
|
|
250
|
+
"@odata.count": 1,
|
|
251
|
+
}),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
const conn = createMockConnection();
|
|
255
|
+
const result = await outlookMessagingProvider.search(conn, "test query");
|
|
256
|
+
|
|
257
|
+
expect(mockSearchMessages).toHaveBeenCalledWith(conn, "test query", {
|
|
258
|
+
top: 20,
|
|
259
|
+
});
|
|
260
|
+
expect(result).toEqual({
|
|
261
|
+
total: 1,
|
|
262
|
+
messages: expect.arrayContaining([
|
|
263
|
+
expect.objectContaining({ id: "msg-1" }),
|
|
264
|
+
]),
|
|
265
|
+
hasMore: false,
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("sets hasMore when @odata.nextLink is present", async () => {
|
|
270
|
+
mockSearchMessages.mockImplementation(() =>
|
|
271
|
+
Promise.resolve({
|
|
272
|
+
value: [],
|
|
273
|
+
"@odata.count": 50,
|
|
274
|
+
"@odata.nextLink": "https://graph.microsoft.com/next",
|
|
275
|
+
}),
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
const conn = createMockConnection();
|
|
279
|
+
const result = await outlookMessagingProvider.search(conn, "test query");
|
|
280
|
+
|
|
281
|
+
expect(result.hasMore).toBe(true);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test("uses count option for top parameter", async () => {
|
|
285
|
+
mockSearchMessages.mockImplementation(() =>
|
|
286
|
+
Promise.resolve({
|
|
287
|
+
value: [] as OutlookMessage[],
|
|
288
|
+
"@odata.count": 0 as number | undefined,
|
|
289
|
+
}),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
const conn = createMockConnection();
|
|
293
|
+
await outlookMessagingProvider.search(conn, "query", { count: 5 });
|
|
294
|
+
|
|
295
|
+
expect(mockSearchMessages).toHaveBeenCalledWith(conn, "query", {
|
|
296
|
+
top: 5,
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("throws when connection is undefined", async () => {
|
|
301
|
+
await expect(
|
|
302
|
+
outlookMessagingProvider.search(undefined, "query"),
|
|
303
|
+
).rejects.toThrow("Outlook requires an OAuth connection");
|
|
304
|
+
});
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// ── sendMessage ────────────────────────────────────────────────────────
|
|
308
|
+
|
|
309
|
+
describe("sendMessage", () => {
|
|
310
|
+
test("sends a new message with correct recipient, subject, and body", async () => {
|
|
311
|
+
const conn = createMockConnection();
|
|
312
|
+
const result = await outlookMessagingProvider.sendMessage(
|
|
313
|
+
conn,
|
|
314
|
+
"recipient@example.com",
|
|
315
|
+
"Hello!",
|
|
316
|
+
{ subject: "Test Subject" },
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
expect(mockSendMessage).toHaveBeenCalledWith(conn, {
|
|
320
|
+
message: {
|
|
321
|
+
subject: "Test Subject",
|
|
322
|
+
body: { contentType: "text", content: "Hello!" },
|
|
323
|
+
toRecipients: [
|
|
324
|
+
{ emailAddress: { address: "recipient@example.com" } },
|
|
325
|
+
],
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
expect(result).toMatchObject({
|
|
329
|
+
conversationId: "recipient@example.com",
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("uses empty string for subject when not provided", async () => {
|
|
334
|
+
const conn = createMockConnection();
|
|
335
|
+
await outlookMessagingProvider.sendMessage(
|
|
336
|
+
conn,
|
|
337
|
+
"recipient@example.com",
|
|
338
|
+
"Hello!",
|
|
339
|
+
);
|
|
340
|
+
|
|
341
|
+
expect(mockSendMessage).toHaveBeenCalledWith(conn, {
|
|
342
|
+
message: {
|
|
343
|
+
subject: "",
|
|
344
|
+
body: { contentType: "text", content: "Hello!" },
|
|
345
|
+
toRecipients: [
|
|
346
|
+
{ emailAddress: { address: "recipient@example.com" } },
|
|
347
|
+
],
|
|
348
|
+
},
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("calls replyToMessage when inReplyTo is provided", async () => {
|
|
353
|
+
const conn = createMockConnection();
|
|
354
|
+
const result = await outlookMessagingProvider.sendMessage(
|
|
355
|
+
conn,
|
|
356
|
+
"conv-id",
|
|
357
|
+
"Reply text",
|
|
358
|
+
{ inReplyTo: "original-msg-id", threadId: "thread-123" },
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
expect(mockReplyToMessage).toHaveBeenCalledWith(
|
|
362
|
+
conn,
|
|
363
|
+
"original-msg-id",
|
|
364
|
+
"Reply text",
|
|
365
|
+
);
|
|
366
|
+
expect(mockSendMessage).not.toHaveBeenCalled();
|
|
367
|
+
expect(result).toMatchObject({
|
|
368
|
+
conversationId: "conv-id",
|
|
369
|
+
threadId: "thread-123",
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("throws when connection is undefined", async () => {
|
|
374
|
+
await expect(
|
|
375
|
+
outlookMessagingProvider.sendMessage(
|
|
376
|
+
undefined,
|
|
377
|
+
"recipient@example.com",
|
|
378
|
+
"Hello!",
|
|
379
|
+
),
|
|
380
|
+
).rejects.toThrow("Outlook requires an OAuth connection");
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
// ── markRead ───────────────────────────────────────────────────────────
|
|
385
|
+
|
|
386
|
+
describe("markRead", () => {
|
|
387
|
+
test("calls markMessageRead with the message ID", async () => {
|
|
388
|
+
const conn = createMockConnection();
|
|
389
|
+
await outlookMessagingProvider.markRead!(conn, "folder-id", "msg-42");
|
|
390
|
+
|
|
391
|
+
expect(mockMarkMessageRead).toHaveBeenCalledWith(conn, "msg-42");
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
test("does not call markMessageRead when messageId is undefined", async () => {
|
|
395
|
+
const conn = createMockConnection();
|
|
396
|
+
await outlookMessagingProvider.markRead!(conn, "folder-id");
|
|
397
|
+
|
|
398
|
+
expect(mockMarkMessageRead).not.toHaveBeenCalled();
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("throws when connection is undefined", async () => {
|
|
402
|
+
await expect(
|
|
403
|
+
outlookMessagingProvider.markRead!(undefined, "folder-id", "msg-42"),
|
|
404
|
+
).rejects.toThrow("Outlook requires an OAuth connection");
|
|
405
|
+
});
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
// ── getThreadReplies ──────────────────────────────────────────────────
|
|
409
|
+
|
|
410
|
+
describe("getThreadReplies", () => {
|
|
411
|
+
test("calls listMessages with conversationId filter", async () => {
|
|
412
|
+
const msg = createMockOutlookMessage({ conversationId: "thread-abc" });
|
|
413
|
+
mockListMessages.mockImplementation(() =>
|
|
414
|
+
Promise.resolve({ value: [msg] }),
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
const conn = createMockConnection();
|
|
418
|
+
const messages = await outlookMessagingProvider.getThreadReplies!(
|
|
419
|
+
conn,
|
|
420
|
+
"folder-id",
|
|
421
|
+
"thread-abc",
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
expect(mockListMessages).toHaveBeenCalledWith(conn, {
|
|
425
|
+
filter: "conversationId eq 'thread-abc'",
|
|
426
|
+
top: 50,
|
|
427
|
+
orderby: "receivedDateTime asc",
|
|
428
|
+
select: expect.stringContaining("id,conversationId"),
|
|
429
|
+
});
|
|
430
|
+
expect(messages).toHaveLength(1);
|
|
431
|
+
expect(messages[0].id).toBe("msg-1");
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
test("respects limit option", async () => {
|
|
435
|
+
mockListMessages.mockImplementation(() => Promise.resolve({ value: [] }));
|
|
436
|
+
|
|
437
|
+
const conn = createMockConnection();
|
|
438
|
+
await outlookMessagingProvider.getThreadReplies!(
|
|
439
|
+
conn,
|
|
440
|
+
"folder-id",
|
|
441
|
+
"thread-abc",
|
|
442
|
+
{ limit: 25 },
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
expect(mockListMessages).toHaveBeenCalledWith(
|
|
446
|
+
conn,
|
|
447
|
+
expect.objectContaining({ top: 25 }),
|
|
448
|
+
);
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
test("throws when connection is undefined", async () => {
|
|
452
|
+
await expect(
|
|
453
|
+
outlookMessagingProvider.getThreadReplies!(
|
|
454
|
+
undefined,
|
|
455
|
+
"folder-id",
|
|
456
|
+
"thread-abc",
|
|
457
|
+
),
|
|
458
|
+
).rejects.toThrow("Outlook requires an OAuth connection");
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
|
|
462
|
+
// ── message mapping ───────────────────────────────────────────────────
|
|
463
|
+
|
|
464
|
+
describe("message mapping (via getHistory)", () => {
|
|
465
|
+
test("maps all fields correctly from OutlookMessage to Message", async () => {
|
|
466
|
+
const msg = createMockOutlookMessage();
|
|
467
|
+
mockListMessages.mockImplementation(() =>
|
|
468
|
+
Promise.resolve({ value: [msg] }),
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
const conn = createMockConnection();
|
|
472
|
+
const messages = await outlookMessagingProvider.getHistory(
|
|
473
|
+
conn,
|
|
474
|
+
"folder-id",
|
|
475
|
+
);
|
|
476
|
+
|
|
477
|
+
const mapped = messages[0];
|
|
478
|
+
expect(mapped).toEqual({
|
|
479
|
+
id: "msg-1",
|
|
480
|
+
conversationId: "conv-1",
|
|
481
|
+
sender: {
|
|
482
|
+
id: "sender@example.com",
|
|
483
|
+
name: "Sender Name",
|
|
484
|
+
email: "sender@example.com",
|
|
485
|
+
},
|
|
486
|
+
text: "Full message body text",
|
|
487
|
+
timestamp: new Date("2024-06-15T10:30:00Z").getTime(),
|
|
488
|
+
threadId: "conv-1",
|
|
489
|
+
platform: "outlook",
|
|
490
|
+
hasAttachments: false,
|
|
491
|
+
metadata: {
|
|
492
|
+
subject: "Test Subject",
|
|
493
|
+
categories: ["important"],
|
|
494
|
+
isRead: false,
|
|
495
|
+
parentFolderId: "inbox-id",
|
|
496
|
+
},
|
|
497
|
+
});
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
test("uses bodyPreview when body contentType is html", async () => {
|
|
501
|
+
const msg = createMockOutlookMessage({
|
|
502
|
+
body: {
|
|
503
|
+
contentType: "html",
|
|
504
|
+
content: "<p>HTML content</p>",
|
|
505
|
+
},
|
|
506
|
+
bodyPreview: "HTML preview text",
|
|
507
|
+
});
|
|
508
|
+
mockListMessages.mockImplementation(() =>
|
|
509
|
+
Promise.resolve({ value: [msg] }),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
const conn = createMockConnection();
|
|
513
|
+
const messages = await outlookMessagingProvider.getHistory(
|
|
514
|
+
conn,
|
|
515
|
+
"folder-id",
|
|
516
|
+
);
|
|
517
|
+
|
|
518
|
+
expect(messages[0].text).toBe("HTML preview text");
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
test("uses email address as sender name when name is missing", async () => {
|
|
522
|
+
const msg = createMockOutlookMessage({
|
|
523
|
+
from: {
|
|
524
|
+
emailAddress: { address: "noname@example.com" },
|
|
525
|
+
},
|
|
526
|
+
});
|
|
527
|
+
mockListMessages.mockImplementation(() =>
|
|
528
|
+
Promise.resolve({ value: [msg] }),
|
|
529
|
+
);
|
|
530
|
+
|
|
531
|
+
const conn = createMockConnection();
|
|
532
|
+
const messages = await outlookMessagingProvider.getHistory(
|
|
533
|
+
conn,
|
|
534
|
+
"folder-id",
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
expect(messages[0].sender.name).toBe("noname@example.com");
|
|
538
|
+
expect(messages[0].sender.id).toBe("noname@example.com");
|
|
539
|
+
expect(messages[0].sender.email).toBe("noname@example.com");
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("maps hasAttachments correctly", async () => {
|
|
543
|
+
const msg = createMockOutlookMessage({ hasAttachments: true });
|
|
544
|
+
mockListMessages.mockImplementation(() =>
|
|
545
|
+
Promise.resolve({ value: [msg] }),
|
|
546
|
+
);
|
|
547
|
+
|
|
548
|
+
const conn = createMockConnection();
|
|
549
|
+
const messages = await outlookMessagingProvider.getHistory(
|
|
550
|
+
conn,
|
|
551
|
+
"folder-id",
|
|
552
|
+
);
|
|
553
|
+
|
|
554
|
+
expect(messages[0].hasAttachments).toBe(true);
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
|
|
558
|
+
// ── provider metadata ─────────────────────────────────────────────────
|
|
559
|
+
|
|
560
|
+
describe("provider metadata", () => {
|
|
561
|
+
test("has correct id and displayName", () => {
|
|
562
|
+
expect(outlookMessagingProvider.id).toBe("outlook");
|
|
563
|
+
expect(outlookMessagingProvider.displayName).toBe("Outlook");
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
test("has correct credential service", () => {
|
|
567
|
+
expect(outlookMessagingProvider.credentialService).toBe("outlook");
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
test("has correct capabilities", () => {
|
|
571
|
+
expect(outlookMessagingProvider.capabilities.has("threads")).toBe(true);
|
|
572
|
+
expect(outlookMessagingProvider.capabilities.has("folders")).toBe(true);
|
|
573
|
+
expect(outlookMessagingProvider.capabilities.has("archive")).toBe(false);
|
|
574
|
+
});
|
|
575
|
+
});
|
|
576
|
+
});
|
|
@@ -21,24 +21,10 @@ import {
|
|
|
21
21
|
// We create and realpath the root eagerly so that macOS's /tmp -> /private/tmp
|
|
22
22
|
// symlink doesn't cause prefix mismatches between normalized roots and paths.
|
|
23
23
|
|
|
24
|
-
const CLASSIFIER_TEST_ROOT =
|
|
25
|
-
|
|
26
|
-
);
|
|
27
|
-
const MOCK_MANAGED_DIR = join(CLASSIFIER_TEST_ROOT, "workspace", "skills");
|
|
24
|
+
const CLASSIFIER_TEST_ROOT = process.env.VELLUM_WORKSPACE_DIR!;
|
|
25
|
+
const MOCK_MANAGED_DIR = join(CLASSIFIER_TEST_ROOT, "skills");
|
|
28
26
|
const MOCK_BUNDLED_DIR = join(CLASSIFIER_TEST_ROOT, "bundled-skills");
|
|
29
27
|
|
|
30
|
-
mock.module("../util/platform.js", () => ({
|
|
31
|
-
getWorkspaceSkillsDir: () => MOCK_MANAGED_DIR,
|
|
32
|
-
getProtectedDir: () => join(CLASSIFIER_TEST_ROOT, "protected"),
|
|
33
|
-
getWorkspaceDir: () => join(CLASSIFIER_TEST_ROOT, "workspace"),
|
|
34
|
-
getDataDir: () => join(CLASSIFIER_TEST_ROOT, "data"),
|
|
35
|
-
isMacOS: () => process.platform === "darwin",
|
|
36
|
-
isLinux: () => process.platform === "linux",
|
|
37
|
-
isWindows: () => process.platform === "win32",
|
|
38
|
-
getPlatformName: () => process.platform,
|
|
39
|
-
ensureDataDir: () => {},
|
|
40
|
-
}));
|
|
41
|
-
|
|
42
28
|
mock.module("../config/skills.js", () => ({
|
|
43
29
|
getBundledSkillsDir: () => MOCK_BUNDLED_DIR,
|
|
44
30
|
}));
|
|
@@ -350,7 +336,6 @@ describe("getManagedSkillsRoot / getBundledSkillsRoot", () => {
|
|
|
350
336
|
test("returns managed skills dir with trailing separator", () => {
|
|
351
337
|
const root = getManagedSkillsRoot();
|
|
352
338
|
expect(root.endsWith(sep)).toBe(true);
|
|
353
|
-
expect(root).toContain("workspace");
|
|
354
339
|
expect(root).toContain("skills");
|
|
355
340
|
});
|
|
356
341
|
|
|
@@ -33,10 +33,8 @@ mock.module("../util/logger.js", () => ({
|
|
|
33
33
|
|
|
34
34
|
const originalFetch = globalThis.fetch;
|
|
35
35
|
|
|
36
|
-
const {
|
|
37
|
-
|
|
38
|
-
resolvePlatformCallbackRegistrationContext,
|
|
39
|
-
} = await import("../inbound/platform-callback-registration.js");
|
|
36
|
+
const { registerCallbackRoute, resolvePlatformCallbackRegistrationContext } =
|
|
37
|
+
await import("../inbound/platform-callback-registration.js");
|
|
40
38
|
|
|
41
39
|
describe("platform callback registration", () => {
|
|
42
40
|
beforeEach(() => {
|
|
@@ -65,9 +63,7 @@ describe("platform callback registration", () => {
|
|
|
65
63
|
expect(context.enabled).toBe(true);
|
|
66
64
|
expect(context.containerized).toBe(true);
|
|
67
65
|
expect(context.platformBaseUrl).toBe("https://platform.example.com");
|
|
68
|
-
expect(context.assistantId).toBe(
|
|
69
|
-
"11111111-2222-4333-8444-555555555555",
|
|
70
|
-
);
|
|
66
|
+
expect(context.assistantId).toBe("11111111-2222-4333-8444-555555555555");
|
|
71
67
|
expect(context.hasInternalApiKey).toBe(false);
|
|
72
68
|
expect(context.hasAssistantApiKey).toBe(true);
|
|
73
69
|
expect(context.authHeader).toBe("Api-Key ast-managed-key");
|
|
@@ -633,7 +633,6 @@ describe("RetryProvider — streaming response handling", () => {
|
|
|
633
633
|
});
|
|
634
634
|
});
|
|
635
635
|
|
|
636
|
-
|
|
637
636
|
// ---------------------------------------------------------------------------
|
|
638
637
|
// createStreamTimeout — edge cases
|
|
639
638
|
// ---------------------------------------------------------------------------
|
|
@@ -682,4 +681,3 @@ describe("createStreamTimeout — edge cases", () => {
|
|
|
682
681
|
cleanup();
|
|
683
682
|
});
|
|
684
683
|
});
|
|
685
|
-
|