@vellumai/assistant 0.5.12 → 0.5.13
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 +41 -9
- package/node_modules/@vellumai/ces-contracts/src/index.ts +7 -0
- package/node_modules/@vellumai/ces-contracts/src/rpc.ts +5 -0
- package/openapi.yaml +1 -1
- package/package.json +1 -1
- package/src/__tests__/first-greeting.test.ts +7 -0
- package/src/__tests__/navigate-settings-tab.test.ts +6 -2
- package/src/__tests__/platform.test.ts +3 -168
- package/src/__tests__/skill-feature-flags.test.ts +8 -0
- package/src/__tests__/skill-secret-handling-guard.test.ts +212 -0
- package/src/__tests__/token-estimator-accuracy.benchmark.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +224 -0
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +237 -0
- package/src/cli/commands/platform/__tests__/status.test.ts +246 -0
- package/src/config/bundled-skills/messaging/SKILL.md +1 -1
- package/src/config/bundled-skills/settings/TOOLS.json +5 -3
- package/src/config/bundled-skills/settings/tools/navigate-settings-tab.ts +4 -2
- package/src/config/feature-flag-registry.json +1 -1
- package/src/credential-execution/client.ts +14 -2
- package/src/daemon/first-greeting.ts +6 -1
- package/src/daemon/lifecycle.ts +13 -8
- package/src/index.ts +0 -12
- package/src/memory/conversation-queries.ts +6 -6
- package/src/memory/journal-memory.ts +8 -2
- package/src/prompts/journal-context.ts +4 -1
- package/src/prompts/system-prompt.ts +11 -0
- package/src/runtime/http-server.ts +7 -15
- package/src/runtime/migrations/rebind-secrets-screen.ts +2 -2
- package/src/runtime/routes/secret-routes.ts +9 -2
- package/src/tools/browser/browser-manager.ts +2 -2
- package/src/util/platform.ts +1 -91
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { mkdtempSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
const testDir = mkdtempSync(join(tmpdir(), "platform-connect-test-"));
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Mock state
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
let mockGetSecureKeyViaDaemon: (
|
|
13
|
+
account: string,
|
|
14
|
+
) => Promise<string | undefined> = async () => undefined;
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Mocks
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
mock.module("../../../lib/daemon-credential-client.js", () => ({
|
|
21
|
+
getSecureKeyViaDaemon: (account: string) =>
|
|
22
|
+
mockGetSecureKeyViaDaemon(account),
|
|
23
|
+
deleteSecureKeyViaDaemon: async () => "not-found" as const,
|
|
24
|
+
setSecureKeyViaDaemon: async () => false,
|
|
25
|
+
getProviderKeyViaDaemon: async () => undefined,
|
|
26
|
+
getSecureKeyResultViaDaemon: async () => ({
|
|
27
|
+
value: undefined,
|
|
28
|
+
unreachable: false,
|
|
29
|
+
}),
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
mock.module("../../../../inbound/platform-callback-registration.js", () => ({
|
|
33
|
+
resolvePlatformCallbackRegistrationContext: async () => ({
|
|
34
|
+
containerized: false,
|
|
35
|
+
platformBaseUrl: "",
|
|
36
|
+
assistantId: "",
|
|
37
|
+
hasInternalApiKey: false,
|
|
38
|
+
hasAssistantApiKey: false,
|
|
39
|
+
authHeader: null,
|
|
40
|
+
enabled: false,
|
|
41
|
+
}),
|
|
42
|
+
registerCallbackRoute: async () => "",
|
|
43
|
+
shouldUsePlatformCallbacks: () => false,
|
|
44
|
+
resolveCallbackUrl: async () => "",
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
mock.module("../../../../util/logger.js", () => ({
|
|
48
|
+
getLogger: () => ({
|
|
49
|
+
info: () => {},
|
|
50
|
+
warn: () => {},
|
|
51
|
+
error: () => {},
|
|
52
|
+
debug: () => {},
|
|
53
|
+
}),
|
|
54
|
+
getCliLogger: () => ({
|
|
55
|
+
info: () => {},
|
|
56
|
+
warn: () => {},
|
|
57
|
+
error: () => {},
|
|
58
|
+
debug: () => {},
|
|
59
|
+
}),
|
|
60
|
+
initLogger: () => {},
|
|
61
|
+
truncateForLog: (value: string, maxLen = 500) =>
|
|
62
|
+
value.length > maxLen ? value.slice(0, maxLen) + "..." : value,
|
|
63
|
+
pruneOldLogFiles: () => 0,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
mock.module("../../../../util/platform.js", () => ({
|
|
67
|
+
getRootDir: () => testDir,
|
|
68
|
+
getDataDir: () => join(testDir, "data"),
|
|
69
|
+
getWorkspaceSkillsDir: () => join(testDir, "skills"),
|
|
70
|
+
getWorkspaceDir: () => join(testDir, "workspace"),
|
|
71
|
+
getWorkspaceHooksDir: () => join(testDir, "workspace", "hooks"),
|
|
72
|
+
getWorkspaceConfigPath: () => join(testDir, "workspace", "config.json"),
|
|
73
|
+
getHooksDir: () => join(testDir, "hooks"),
|
|
74
|
+
getSignalsDir: () => join(testDir, "signals"),
|
|
75
|
+
getConversationsDir: () => join(testDir, "conversations"),
|
|
76
|
+
getEmbeddingModelsDir: () => join(testDir, "models"),
|
|
77
|
+
getSandboxRootDir: () => join(testDir, "sandbox"),
|
|
78
|
+
getSandboxWorkingDir: () => join(testDir, "sandbox", "work"),
|
|
79
|
+
getInterfacesDir: () => join(testDir, "interfaces"),
|
|
80
|
+
getSoundsDir: () => join(testDir, "sounds"),
|
|
81
|
+
getHistoryPath: () => join(testDir, "history"),
|
|
82
|
+
isMacOS: () => process.platform === "darwin",
|
|
83
|
+
isLinux: () => process.platform === "linux",
|
|
84
|
+
isWindows: () => process.platform === "win32",
|
|
85
|
+
getPlatformName: () => "linux",
|
|
86
|
+
getClipboardCommand: () => null,
|
|
87
|
+
resolveInstanceDataDir: () => undefined,
|
|
88
|
+
normalizeAssistantId: (id: string) => id,
|
|
89
|
+
getTCPPort: () => 0,
|
|
90
|
+
isTCPEnabled: () => false,
|
|
91
|
+
getTCPHost: () => "127.0.0.1",
|
|
92
|
+
isIOSPairingEnabled: () => false,
|
|
93
|
+
getPlatformTokenPath: () => join(testDir, "token"),
|
|
94
|
+
readPlatformToken: () => null,
|
|
95
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
96
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
97
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
98
|
+
getWorkspaceDirDisplay: () => testDir,
|
|
99
|
+
getWorkspacePromptPath: (file: string) => join(testDir, file),
|
|
100
|
+
ensureDataDir: () => {},
|
|
101
|
+
}));
|
|
102
|
+
|
|
103
|
+
mock.module("../../../../config/loader.js", () => ({
|
|
104
|
+
API_KEY_PROVIDERS: [] as const,
|
|
105
|
+
getConfig: () => ({
|
|
106
|
+
permissions: { mode: "workspace" },
|
|
107
|
+
skills: { load: { extraDirs: [] } },
|
|
108
|
+
sandbox: { enabled: true },
|
|
109
|
+
}),
|
|
110
|
+
loadConfig: () => ({}),
|
|
111
|
+
invalidateConfigCache: () => {},
|
|
112
|
+
saveConfig: () => {},
|
|
113
|
+
loadRawConfig: () => ({}),
|
|
114
|
+
saveRawConfig: () => {},
|
|
115
|
+
getNestedValue: () => undefined,
|
|
116
|
+
setNestedValue: () => {},
|
|
117
|
+
applyNestedDefaults: (config: unknown) => config,
|
|
118
|
+
deepMergeMissing: () => false,
|
|
119
|
+
deepMergeOverwrite: () => {},
|
|
120
|
+
mergeDefaultWorkspaceConfig: () => {},
|
|
121
|
+
}));
|
|
122
|
+
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
// Import module under test (after mocks are registered)
|
|
125
|
+
// ---------------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
const { buildCliProgram } = await import("../../../program.js");
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Test helper
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
async function runCommand(
|
|
134
|
+
args: string[],
|
|
135
|
+
): Promise<{ stdout: string; exitCode: number }> {
|
|
136
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
137
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
138
|
+
const stdoutChunks: string[] = [];
|
|
139
|
+
|
|
140
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
141
|
+
stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
142
|
+
return true;
|
|
143
|
+
}) as typeof process.stdout.write;
|
|
144
|
+
|
|
145
|
+
process.stderr.write = (() => true) as typeof process.stderr.write;
|
|
146
|
+
|
|
147
|
+
process.exitCode = 0;
|
|
148
|
+
|
|
149
|
+
try {
|
|
150
|
+
const program = buildCliProgram();
|
|
151
|
+
program.exitOverride();
|
|
152
|
+
program.configureOutput({
|
|
153
|
+
writeErr: () => {},
|
|
154
|
+
writeOut: (str: string) => stdoutChunks.push(str),
|
|
155
|
+
});
|
|
156
|
+
await program.parseAsync(["node", "assistant", ...args]);
|
|
157
|
+
} catch {
|
|
158
|
+
if (process.exitCode === 0) process.exitCode = 1;
|
|
159
|
+
} finally {
|
|
160
|
+
process.stdout.write = originalStdoutWrite;
|
|
161
|
+
process.stderr.write = originalStderrWrite;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const exitCode = process.exitCode ?? 0;
|
|
165
|
+
process.exitCode = 0;
|
|
166
|
+
|
|
167
|
+
return { exitCode, stdout: stdoutChunks.join("") };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ---------------------------------------------------------------------------
|
|
171
|
+
// Tests
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
describe("assistant platform connect", () => {
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
mockGetSecureKeyViaDaemon = async () => undefined;
|
|
177
|
+
process.exitCode = 0;
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test.todo(
|
|
181
|
+
"already connected returns success with existing base URL",
|
|
182
|
+
async () => {
|
|
183
|
+
/**
|
|
184
|
+
* When the assistant already has stored platform credentials (base
|
|
185
|
+
* URL and API key), the connect command should short-circuit and
|
|
186
|
+
* report that it is already connected, returning the existing base
|
|
187
|
+
* URL.
|
|
188
|
+
*
|
|
189
|
+
* NOTE: The connect command is currently stubbed — the full
|
|
190
|
+
* credential-collection flow via a secure UI component is not yet
|
|
191
|
+
* implemented. This test validates the already-connected early-exit
|
|
192
|
+
* path, which IS implemented. It is skipped because the stub path
|
|
193
|
+
* still sets a non-zero exit code before reaching this branch
|
|
194
|
+
* under certain conditions. Unskip once the full connect flow
|
|
195
|
+
* lands.
|
|
196
|
+
*/
|
|
197
|
+
|
|
198
|
+
// GIVEN stored platform credentials already exist
|
|
199
|
+
mockGetSecureKeyViaDaemon = async (account: string) => {
|
|
200
|
+
if (account === "credential/vellum/platform_base_url")
|
|
201
|
+
return "https://platform.vellum.ai";
|
|
202
|
+
if (account === "credential/vellum/assistant_api_key")
|
|
203
|
+
return "sk-existing-key";
|
|
204
|
+
return undefined;
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
// WHEN the connect command is run with --json
|
|
208
|
+
const { exitCode, stdout } = await runCommand([
|
|
209
|
+
"platform",
|
|
210
|
+
"connect",
|
|
211
|
+
"--json",
|
|
212
|
+
]);
|
|
213
|
+
|
|
214
|
+
// THEN the command succeeds
|
|
215
|
+
expect(exitCode).toBe(0);
|
|
216
|
+
|
|
217
|
+
// AND the output indicates already connected with the base URL
|
|
218
|
+
const parsed = JSON.parse(stdout);
|
|
219
|
+
expect(parsed.ok).toBe(true);
|
|
220
|
+
expect(parsed.alreadyConnected).toBe(true);
|
|
221
|
+
expect(parsed.baseUrl).toBe("https://platform.vellum.ai");
|
|
222
|
+
},
|
|
223
|
+
);
|
|
224
|
+
});
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
import { mkdtempSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
5
|
+
|
|
6
|
+
const testDir = mkdtempSync(join(tmpdir(), "platform-disconnect-test-"));
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Mock state
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
let mockGetSecureKeyViaDaemon: (
|
|
13
|
+
account: string,
|
|
14
|
+
) => Promise<string | undefined> = async () => undefined;
|
|
15
|
+
|
|
16
|
+
let mockDeleteSecureKeyViaDaemonCalls: Array<{
|
|
17
|
+
type: string;
|
|
18
|
+
name: string;
|
|
19
|
+
}> = [];
|
|
20
|
+
|
|
21
|
+
let mockDeleteSecureKeyViaDaemonResult: "deleted" | "not-found" | "error" =
|
|
22
|
+
"deleted";
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Mocks
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
mock.module("../../../../inbound/platform-callback-registration.js", () => ({
|
|
29
|
+
resolvePlatformCallbackRegistrationContext: async () => ({
|
|
30
|
+
containerized: false,
|
|
31
|
+
platformBaseUrl: "",
|
|
32
|
+
assistantId: "",
|
|
33
|
+
hasInternalApiKey: false,
|
|
34
|
+
hasAssistantApiKey: false,
|
|
35
|
+
authHeader: null,
|
|
36
|
+
enabled: false,
|
|
37
|
+
}),
|
|
38
|
+
registerCallbackRoute: async () => "",
|
|
39
|
+
shouldUsePlatformCallbacks: () => false,
|
|
40
|
+
resolveCallbackUrl: async () => "",
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
mock.module("../../../lib/daemon-credential-client.js", () => ({
|
|
44
|
+
getSecureKeyViaDaemon: (account: string) =>
|
|
45
|
+
mockGetSecureKeyViaDaemon(account),
|
|
46
|
+
deleteSecureKeyViaDaemon: async (type: string, name: string) => {
|
|
47
|
+
mockDeleteSecureKeyViaDaemonCalls.push({ type, name });
|
|
48
|
+
return mockDeleteSecureKeyViaDaemonResult;
|
|
49
|
+
},
|
|
50
|
+
setSecureKeyViaDaemon: async () => false,
|
|
51
|
+
getProviderKeyViaDaemon: async () => undefined,
|
|
52
|
+
getSecureKeyResultViaDaemon: async () => ({
|
|
53
|
+
value: undefined,
|
|
54
|
+
unreachable: false,
|
|
55
|
+
}),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
mock.module("../../../../util/logger.js", () => ({
|
|
59
|
+
getLogger: () => ({
|
|
60
|
+
info: () => {},
|
|
61
|
+
warn: () => {},
|
|
62
|
+
error: () => {},
|
|
63
|
+
debug: () => {},
|
|
64
|
+
}),
|
|
65
|
+
getCliLogger: () => ({
|
|
66
|
+
info: () => {},
|
|
67
|
+
warn: () => {},
|
|
68
|
+
error: () => {},
|
|
69
|
+
debug: () => {},
|
|
70
|
+
}),
|
|
71
|
+
initLogger: () => {},
|
|
72
|
+
truncateForLog: (value: string, maxLen = 500) =>
|
|
73
|
+
value.length > maxLen ? value.slice(0, maxLen) + "..." : value,
|
|
74
|
+
pruneOldLogFiles: () => 0,
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
mock.module("../../../../util/platform.js", () => ({
|
|
78
|
+
getRootDir: () => testDir,
|
|
79
|
+
getDataDir: () => join(testDir, "data"),
|
|
80
|
+
getWorkspaceSkillsDir: () => join(testDir, "skills"),
|
|
81
|
+
getWorkspaceDir: () => join(testDir, "workspace"),
|
|
82
|
+
getWorkspaceHooksDir: () => join(testDir, "workspace", "hooks"),
|
|
83
|
+
getWorkspaceConfigPath: () => join(testDir, "workspace", "config.json"),
|
|
84
|
+
getHooksDir: () => join(testDir, "hooks"),
|
|
85
|
+
getSignalsDir: () => join(testDir, "signals"),
|
|
86
|
+
getConversationsDir: () => join(testDir, "conversations"),
|
|
87
|
+
getEmbeddingModelsDir: () => join(testDir, "models"),
|
|
88
|
+
getSandboxRootDir: () => join(testDir, "sandbox"),
|
|
89
|
+
getSandboxWorkingDir: () => join(testDir, "sandbox", "work"),
|
|
90
|
+
getInterfacesDir: () => join(testDir, "interfaces"),
|
|
91
|
+
getSoundsDir: () => join(testDir, "sounds"),
|
|
92
|
+
getHistoryPath: () => join(testDir, "history"),
|
|
93
|
+
isMacOS: () => process.platform === "darwin",
|
|
94
|
+
isLinux: () => process.platform === "linux",
|
|
95
|
+
isWindows: () => process.platform === "win32",
|
|
96
|
+
getPlatformName: () => "linux",
|
|
97
|
+
getClipboardCommand: () => null,
|
|
98
|
+
resolveInstanceDataDir: () => undefined,
|
|
99
|
+
normalizeAssistantId: (id: string) => id,
|
|
100
|
+
getTCPPort: () => 0,
|
|
101
|
+
isTCPEnabled: () => false,
|
|
102
|
+
getTCPHost: () => "127.0.0.1",
|
|
103
|
+
isIOSPairingEnabled: () => false,
|
|
104
|
+
getPlatformTokenPath: () => join(testDir, "token"),
|
|
105
|
+
readPlatformToken: () => null,
|
|
106
|
+
getPidPath: () => join(testDir, "test.pid"),
|
|
107
|
+
getDbPath: () => join(testDir, "test.db"),
|
|
108
|
+
getLogPath: () => join(testDir, "test.log"),
|
|
109
|
+
getWorkspaceDirDisplay: () => testDir,
|
|
110
|
+
getWorkspacePromptPath: (file: string) => join(testDir, file),
|
|
111
|
+
ensureDataDir: () => {},
|
|
112
|
+
}));
|
|
113
|
+
|
|
114
|
+
mock.module("../../../../config/loader.js", () => ({
|
|
115
|
+
API_KEY_PROVIDERS: [] as const,
|
|
116
|
+
getConfig: () => ({
|
|
117
|
+
permissions: { mode: "workspace" },
|
|
118
|
+
skills: { load: { extraDirs: [] } },
|
|
119
|
+
sandbox: { enabled: true },
|
|
120
|
+
}),
|
|
121
|
+
loadConfig: () => ({}),
|
|
122
|
+
invalidateConfigCache: () => {},
|
|
123
|
+
saveConfig: () => {},
|
|
124
|
+
loadRawConfig: () => ({}),
|
|
125
|
+
saveRawConfig: () => {},
|
|
126
|
+
getNestedValue: () => undefined,
|
|
127
|
+
setNestedValue: () => {},
|
|
128
|
+
applyNestedDefaults: (config: unknown) => config,
|
|
129
|
+
deepMergeMissing: () => false,
|
|
130
|
+
deepMergeOverwrite: () => {},
|
|
131
|
+
mergeDefaultWorkspaceConfig: () => {},
|
|
132
|
+
}));
|
|
133
|
+
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Import module under test (after mocks are registered)
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
const { buildCliProgram } = await import("../../../program.js");
|
|
139
|
+
|
|
140
|
+
// ---------------------------------------------------------------------------
|
|
141
|
+
// Test helper
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
|
|
144
|
+
async function runCommand(
|
|
145
|
+
args: string[],
|
|
146
|
+
): Promise<{ stdout: string; exitCode: number }> {
|
|
147
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
148
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
149
|
+
const stdoutChunks: string[] = [];
|
|
150
|
+
|
|
151
|
+
process.stdout.write = ((chunk: unknown) => {
|
|
152
|
+
stdoutChunks.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
153
|
+
return true;
|
|
154
|
+
}) as typeof process.stdout.write;
|
|
155
|
+
|
|
156
|
+
process.stderr.write = (() => true) as typeof process.stderr.write;
|
|
157
|
+
|
|
158
|
+
process.exitCode = 0;
|
|
159
|
+
|
|
160
|
+
try {
|
|
161
|
+
const program = buildCliProgram();
|
|
162
|
+
program.exitOverride();
|
|
163
|
+
program.configureOutput({
|
|
164
|
+
writeErr: () => {},
|
|
165
|
+
writeOut: (str: string) => stdoutChunks.push(str),
|
|
166
|
+
});
|
|
167
|
+
await program.parseAsync(["node", "assistant", ...args]);
|
|
168
|
+
} catch {
|
|
169
|
+
if (process.exitCode === 0) process.exitCode = 1;
|
|
170
|
+
} finally {
|
|
171
|
+
process.stdout.write = originalStdoutWrite;
|
|
172
|
+
process.stderr.write = originalStderrWrite;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const exitCode = process.exitCode ?? 0;
|
|
176
|
+
process.exitCode = 0;
|
|
177
|
+
|
|
178
|
+
return { exitCode, stdout: stdoutChunks.join("") };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
// Tests
|
|
183
|
+
// ---------------------------------------------------------------------------
|
|
184
|
+
|
|
185
|
+
describe("assistant platform disconnect", () => {
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
mockGetSecureKeyViaDaemon = async () => undefined;
|
|
188
|
+
mockDeleteSecureKeyViaDaemonCalls = [];
|
|
189
|
+
mockDeleteSecureKeyViaDaemonResult = "deleted";
|
|
190
|
+
process.exitCode = 0;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("successfully removes all stored platform credentials", async () => {
|
|
194
|
+
/**
|
|
195
|
+
* When a connected platform has stored credentials, the disconnect
|
|
196
|
+
* command should delete all credential keys and report success with
|
|
197
|
+
* the previous base URL.
|
|
198
|
+
*/
|
|
199
|
+
|
|
200
|
+
// GIVEN stored platform credentials exist
|
|
201
|
+
mockGetSecureKeyViaDaemon = async (account: string) => {
|
|
202
|
+
if (account === "credential/vellum/platform_base_url")
|
|
203
|
+
return "https://platform.vellum.ai";
|
|
204
|
+
if (account === "credential/vellum/assistant_api_key")
|
|
205
|
+
return "sk-test-key";
|
|
206
|
+
return undefined;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// AND credential deletion succeeds
|
|
210
|
+
mockDeleteSecureKeyViaDaemonResult = "deleted";
|
|
211
|
+
|
|
212
|
+
// WHEN the disconnect command is run with --json
|
|
213
|
+
const { exitCode, stdout } = await runCommand([
|
|
214
|
+
"platform",
|
|
215
|
+
"disconnect",
|
|
216
|
+
"--json",
|
|
217
|
+
]);
|
|
218
|
+
|
|
219
|
+
// THEN the command succeeds
|
|
220
|
+
expect(exitCode).toBe(0);
|
|
221
|
+
|
|
222
|
+
// AND the output confirms disconnection with the previous base URL
|
|
223
|
+
const parsed = JSON.parse(stdout);
|
|
224
|
+
expect(parsed.ok).toBe(true);
|
|
225
|
+
expect(parsed.disconnected).toBe(true);
|
|
226
|
+
expect(parsed.previousBaseUrl).toBe("https://platform.vellum.ai");
|
|
227
|
+
|
|
228
|
+
// AND all five credential keys were deleted
|
|
229
|
+
expect(mockDeleteSecureKeyViaDaemonCalls).toHaveLength(5);
|
|
230
|
+
const deletedNames = mockDeleteSecureKeyViaDaemonCalls.map((c) => c.name);
|
|
231
|
+
expect(deletedNames).toContain("vellum:platform_base_url");
|
|
232
|
+
expect(deletedNames).toContain("vellum:assistant_api_key");
|
|
233
|
+
expect(deletedNames).toContain("vellum:platform_assistant_id");
|
|
234
|
+
expect(deletedNames).toContain("vellum:platform_organization_id");
|
|
235
|
+
expect(deletedNames).toContain("vellum:platform_user_id");
|
|
236
|
+
});
|
|
237
|
+
});
|