@vellumai/assistant 0.5.15 → 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
|
@@ -432,6 +432,12 @@ describe("Permission Checker", () => {
|
|
|
432
432
|
|
|
433
433
|
// shell commands - high risk
|
|
434
434
|
describe("shell — high risk", () => {
|
|
435
|
+
test("assistant trust clear is high risk", async () => {
|
|
436
|
+
expect(
|
|
437
|
+
await classifyRisk("bash", { command: "assistant trust clear" }),
|
|
438
|
+
).toBe(RiskLevel.High);
|
|
439
|
+
});
|
|
440
|
+
|
|
435
441
|
test("sudo is high risk", async () => {
|
|
436
442
|
expect(await classifyRisk("bash", { command: "sudo rm -rf /" })).toBe(
|
|
437
443
|
RiskLevel.High,
|
|
@@ -2017,6 +2023,9 @@ describe("Permission Checker", () => {
|
|
|
2017
2023
|
function ensureSkillsDir(): void {
|
|
2018
2024
|
mkdirSync(join(checkerTestDir, "skills"), { recursive: true });
|
|
2019
2025
|
}
|
|
2026
|
+
function ensureHooksDir(): void {
|
|
2027
|
+
mkdirSync(join(checkerTestDir, "hooks"), { recursive: true });
|
|
2028
|
+
}
|
|
2020
2029
|
|
|
2021
2030
|
test("file_write to skill directory is High risk", async () => {
|
|
2022
2031
|
ensureSkillsDir();
|
|
@@ -2147,6 +2156,56 @@ describe("Permission Checker", () => {
|
|
|
2147
2156
|
expect(risk).toBe(RiskLevel.Low);
|
|
2148
2157
|
});
|
|
2149
2158
|
|
|
2159
|
+
test("file_write to hooks directory is High risk", async () => {
|
|
2160
|
+
ensureHooksDir();
|
|
2161
|
+
const hookPath = join(
|
|
2162
|
+
checkerTestDir,
|
|
2163
|
+
"hooks",
|
|
2164
|
+
"post-tool-use",
|
|
2165
|
+
"hook.sh",
|
|
2166
|
+
);
|
|
2167
|
+
const risk = await classifyRisk("file_write", { path: hookPath });
|
|
2168
|
+
expect(risk).toBe(RiskLevel.High);
|
|
2169
|
+
});
|
|
2170
|
+
|
|
2171
|
+
test("file_edit of hooks config is High risk", async () => {
|
|
2172
|
+
ensureHooksDir();
|
|
2173
|
+
const configPath = join(checkerTestDir, "hooks", "config.json");
|
|
2174
|
+
const risk = await classifyRisk("file_edit", { path: configPath });
|
|
2175
|
+
expect(risk).toBe(RiskLevel.High);
|
|
2176
|
+
});
|
|
2177
|
+
|
|
2178
|
+
test("file_write to hooks directory prompts as High risk", async () => {
|
|
2179
|
+
ensureHooksDir();
|
|
2180
|
+
const hookPath = join(
|
|
2181
|
+
checkerTestDir,
|
|
2182
|
+
"hooks",
|
|
2183
|
+
"post-tool-use",
|
|
2184
|
+
"hook.sh",
|
|
2185
|
+
);
|
|
2186
|
+
const result = await check("file_write", { path: hookPath }, "/tmp");
|
|
2187
|
+
expect(result.decision).toBe("prompt");
|
|
2188
|
+
});
|
|
2189
|
+
|
|
2190
|
+
test("host_file_write to hooks directory is High risk", async () => {
|
|
2191
|
+
ensureHooksDir();
|
|
2192
|
+
const hookPath = join(
|
|
2193
|
+
checkerTestDir,
|
|
2194
|
+
"hooks",
|
|
2195
|
+
"post-tool-use",
|
|
2196
|
+
"hook.sh",
|
|
2197
|
+
);
|
|
2198
|
+
const risk = await classifyRisk("host_file_write", { path: hookPath });
|
|
2199
|
+
expect(risk).toBe(RiskLevel.High);
|
|
2200
|
+
});
|
|
2201
|
+
|
|
2202
|
+
test("host_file_edit of hooks config is High risk", async () => {
|
|
2203
|
+
ensureHooksDir();
|
|
2204
|
+
const configPath = join(checkerTestDir, "hooks", "config.json");
|
|
2205
|
+
const risk = await classifyRisk("host_file_edit", { path: configPath });
|
|
2206
|
+
expect(risk).toBe(RiskLevel.High);
|
|
2207
|
+
});
|
|
2208
|
+
|
|
2150
2209
|
test("host_file_write to non-skill path remains Medium risk (via registry)", async () => {
|
|
2151
2210
|
const normalPath = "/tmp/some-file.txt";
|
|
2152
2211
|
const risk = await classifyRisk("host_file_write", { path: normalPath });
|
|
@@ -157,27 +157,55 @@ describe("CLI command risk guard: elevated assistant subcommands", () => {
|
|
|
157
157
|
expect(risk).toBe(RiskLevel.Medium);
|
|
158
158
|
});
|
|
159
159
|
|
|
160
|
-
test("--help on elevated subcommands
|
|
161
|
-
|
|
160
|
+
test("--help on non-elevated subcommands remains Low risk", async () => {
|
|
161
|
+
// GIVEN non-elevated subcommands with --help / -h flags
|
|
162
|
+
const lowRiskWithHelp = [
|
|
163
|
+
"assistant oauth --help",
|
|
164
|
+
"assistant credentials --help",
|
|
165
|
+
"assistant trust -h",
|
|
166
|
+
"assistant keys --help",
|
|
167
|
+
"assistant config --help",
|
|
168
|
+
];
|
|
169
|
+
|
|
170
|
+
// WHEN classifying risk
|
|
171
|
+
// THEN they remain Low since the subcommand itself is Low
|
|
172
|
+
for (const command of lowRiskWithHelp) {
|
|
173
|
+
const risk = await classifyRisk("bash", { command });
|
|
174
|
+
expectLowRisk(command, risk);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("--help does not downgrade risk on elevated subcommands", async () => {
|
|
179
|
+
// GIVEN elevated subcommands with --help / -h flags appended
|
|
180
|
+
const highRiskWithHelp = [
|
|
162
181
|
"assistant oauth token --help",
|
|
163
182
|
"assistant oauth mode --set --help",
|
|
164
183
|
"assistant credentials reveal --help",
|
|
184
|
+
"assistant trust clear --help",
|
|
185
|
+
"assistant trust remove -h",
|
|
186
|
+
"assistant credentials set --help",
|
|
187
|
+
"assistant credentials delete -h",
|
|
188
|
+
"assistant keys set --help",
|
|
189
|
+
"assistant keys delete -h",
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
const mediumRiskWithHelp = [
|
|
165
193
|
"assistant oauth request --help",
|
|
166
194
|
"assistant oauth connect --help",
|
|
167
195
|
"assistant oauth disconnect -h",
|
|
168
196
|
];
|
|
169
197
|
|
|
170
|
-
|
|
198
|
+
// WHEN classifying risk
|
|
199
|
+
// THEN --help does not bypass the elevated risk level
|
|
200
|
+
for (const command of highRiskWithHelp) {
|
|
171
201
|
const risk = await classifyRisk("bash", { command });
|
|
172
|
-
|
|
202
|
+
expect(risk).toBe(RiskLevel.High);
|
|
173
203
|
}
|
|
174
|
-
});
|
|
175
204
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
}
|
|
180
|
-
expect(risk).toBe(RiskLevel.High);
|
|
205
|
+
for (const command of mediumRiskWithHelp) {
|
|
206
|
+
const risk = await classifyRisk("bash", { command });
|
|
207
|
+
expect(risk).toBe(RiskLevel.Medium);
|
|
208
|
+
}
|
|
181
209
|
});
|
|
182
210
|
|
|
183
211
|
test("non-sensitive oauth subcommands remain Low risk", async () => {
|
|
@@ -205,6 +233,66 @@ describe("CLI command risk guard: elevated assistant subcommands", () => {
|
|
|
205
233
|
expectLowRisk(command, risk);
|
|
206
234
|
}
|
|
207
235
|
});
|
|
236
|
+
|
|
237
|
+
test("assistant credentials set is High risk (modifies stored credentials)", async () => {
|
|
238
|
+
const risk = await classifyRisk("bash", {
|
|
239
|
+
command: "assistant credentials set",
|
|
240
|
+
});
|
|
241
|
+
expect(risk).toBe(RiskLevel.High);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("assistant credentials delete is High risk (removes stored credentials)", async () => {
|
|
245
|
+
const risk = await classifyRisk("bash", {
|
|
246
|
+
command: "assistant credentials delete",
|
|
247
|
+
});
|
|
248
|
+
expect(risk).toBe(RiskLevel.High);
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("assistant keys set is High risk (modifies API keys)", async () => {
|
|
252
|
+
const risk = await classifyRisk("bash", {
|
|
253
|
+
command: "assistant keys set anthropic sk-ant-xxx",
|
|
254
|
+
});
|
|
255
|
+
expect(risk).toBe(RiskLevel.High);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("assistant keys delete is High risk (removes API keys)", async () => {
|
|
259
|
+
const risk = await classifyRisk("bash", {
|
|
260
|
+
command: "assistant keys delete openai",
|
|
261
|
+
});
|
|
262
|
+
expect(risk).toBe(RiskLevel.High);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("non-sensitive keys subcommands remain Low risk", async () => {
|
|
266
|
+
const lowRiskKeysCommands = ["assistant keys", "assistant keys list"];
|
|
267
|
+
|
|
268
|
+
for (const command of lowRiskKeysCommands) {
|
|
269
|
+
const risk = await classifyRisk("bash", { command });
|
|
270
|
+
expectLowRisk(command, risk);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("assistant trust remove is High risk (removes trust rules)", async () => {
|
|
275
|
+
const risk = await classifyRisk("bash", {
|
|
276
|
+
command: "assistant trust remove abc123",
|
|
277
|
+
});
|
|
278
|
+
expect(risk).toBe(RiskLevel.High);
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test("assistant trust clear is High risk (clears all trust rules)", async () => {
|
|
282
|
+
const risk = await classifyRisk("bash", {
|
|
283
|
+
command: "assistant trust clear",
|
|
284
|
+
});
|
|
285
|
+
expect(risk).toBe(RiskLevel.High);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("non-sensitive trust subcommands remain Low risk", async () => {
|
|
289
|
+
const lowRiskTrustCommands = ["assistant trust", "assistant trust list"];
|
|
290
|
+
|
|
291
|
+
for (const command of lowRiskTrustCommands) {
|
|
292
|
+
const risk = await classifyRisk("bash", { command });
|
|
293
|
+
expectLowRisk(command, risk);
|
|
294
|
+
}
|
|
295
|
+
});
|
|
208
296
|
});
|
|
209
297
|
|
|
210
298
|
describe("CLI command risk guard: wrapper program propagation", () => {
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { rmSync } from "node:fs";
|
|
2
|
+
import { afterAll, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
3
|
+
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { eq } from "drizzle-orm";
|
|
6
|
+
|
|
7
|
+
mock.module("../util/logger.js", () => ({
|
|
8
|
+
getLogger: () =>
|
|
9
|
+
new Proxy({} as Record<string, unknown>, {
|
|
10
|
+
get: () => () => {},
|
|
11
|
+
}),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
mock.module("../memory/qdrant-client.js", () => ({
|
|
15
|
+
getQdrantClient: () => ({
|
|
16
|
+
searchWithFilter: async () => [],
|
|
17
|
+
hybridSearch: async () => [],
|
|
18
|
+
upsertPoints: async () => {},
|
|
19
|
+
deletePoints: async () => {},
|
|
20
|
+
}),
|
|
21
|
+
initQdrantClient: () => {},
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
// Controllable mock for buildCliProgram
|
|
25
|
+
let mockCommands: { name: string; description: string }[] = [];
|
|
26
|
+
|
|
27
|
+
function makeMockProgram(): Command {
|
|
28
|
+
const program = new Command();
|
|
29
|
+
for (const cmd of mockCommands) {
|
|
30
|
+
program.command(cmd.name).description(cmd.description);
|
|
31
|
+
}
|
|
32
|
+
return program;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
mock.module("../cli/program.js", () => ({
|
|
36
|
+
buildCliProgram: () => makeMockProgram(),
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
import { DEFAULT_CONFIG } from "../config/defaults.js";
|
|
40
|
+
|
|
41
|
+
const TEST_CONFIG = {
|
|
42
|
+
...DEFAULT_CONFIG,
|
|
43
|
+
memory: {
|
|
44
|
+
...DEFAULT_CONFIG.memory,
|
|
45
|
+
enabled: true,
|
|
46
|
+
extraction: {
|
|
47
|
+
...DEFAULT_CONFIG.memory.extraction,
|
|
48
|
+
useLLM: false,
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
mock.module("../config/loader.js", () => ({
|
|
54
|
+
loadConfig: () => TEST_CONFIG,
|
|
55
|
+
getConfig: () => TEST_CONFIG,
|
|
56
|
+
loadRawConfig: () => ({}),
|
|
57
|
+
saveRawConfig: () => {},
|
|
58
|
+
invalidateConfigCache: () => {},
|
|
59
|
+
}));
|
|
60
|
+
|
|
61
|
+
import {
|
|
62
|
+
buildCliCapabilityStatement,
|
|
63
|
+
seedCliCommandMemories,
|
|
64
|
+
upsertCliCapabilityMemory,
|
|
65
|
+
} from "../cli/cli-memory.js";
|
|
66
|
+
import { getDb, initializeDb, resetDb } from "../memory/db.js";
|
|
67
|
+
import { memoryItems, memoryJobs } from "../memory/schema.js";
|
|
68
|
+
import { ensureDataDir, getDbPath } from "../util/platform.js";
|
|
69
|
+
|
|
70
|
+
ensureDataDir();
|
|
71
|
+
initializeDb();
|
|
72
|
+
|
|
73
|
+
afterAll(() => {
|
|
74
|
+
resetDb();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
function resetTables() {
|
|
78
|
+
const db = getDb();
|
|
79
|
+
db.run("DELETE FROM memory_item_sources");
|
|
80
|
+
db.run("DELETE FROM memory_embeddings");
|
|
81
|
+
db.run("DELETE FROM memory_items");
|
|
82
|
+
db.run("DELETE FROM memory_jobs");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ─── buildCliCapabilityStatement ────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
describe("buildCliCapabilityStatement", () => {
|
|
88
|
+
test("includes 'assistant' prefix, name, and description", () => {
|
|
89
|
+
const result = buildCliCapabilityStatement("doctor", "Run diagnostic checks");
|
|
90
|
+
expect(result).toContain('"assistant doctor"');
|
|
91
|
+
expect(result).toContain("Run diagnostic checks");
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("truncates long statements to 500 chars", () => {
|
|
95
|
+
const longDesc = "x".repeat(600);
|
|
96
|
+
const result = buildCliCapabilityStatement("test", longDesc);
|
|
97
|
+
expect(result.length).toBe(500);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// ─── upsertCliCapabilityMemory ──────────────────────────────────────────────
|
|
102
|
+
|
|
103
|
+
describe("upsertCliCapabilityMemory", () => {
|
|
104
|
+
beforeEach(resetTables);
|
|
105
|
+
|
|
106
|
+
test("inserts with correct kind, subject, confidence, importance", () => {
|
|
107
|
+
upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
|
|
108
|
+
|
|
109
|
+
const db = getDb();
|
|
110
|
+
const items = db.select().from(memoryItems).all();
|
|
111
|
+
expect(items).toHaveLength(1);
|
|
112
|
+
expect(items[0].kind).toBe("capability");
|
|
113
|
+
expect(items[0].subject).toBe("cli:doctor");
|
|
114
|
+
expect(items[0].confidence).toBe(1.0);
|
|
115
|
+
expect(items[0].importance).toBe(0.7);
|
|
116
|
+
expect(items[0].status).toBe("active");
|
|
117
|
+
expect(items[0].scopeId).toBe("default");
|
|
118
|
+
|
|
119
|
+
// Should also enqueue an embed_item job
|
|
120
|
+
const jobs = db.select().from(memoryJobs).all();
|
|
121
|
+
expect(jobs).toHaveLength(1);
|
|
122
|
+
expect(jobs[0].type).toBe("embed_item");
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("is idempotent (same entry only touches lastSeenAt)", () => {
|
|
126
|
+
upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
|
|
127
|
+
|
|
128
|
+
const db = getDb();
|
|
129
|
+
const before = db.select().from(memoryItems).all();
|
|
130
|
+
expect(before).toHaveLength(1);
|
|
131
|
+
const originalLastSeen = before[0].lastSeenAt;
|
|
132
|
+
|
|
133
|
+
// Upsert again
|
|
134
|
+
upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
|
|
135
|
+
|
|
136
|
+
const after = db.select().from(memoryItems).all();
|
|
137
|
+
expect(after).toHaveLength(1);
|
|
138
|
+
// Fingerprint should be the same, so only lastSeenAt changes
|
|
139
|
+
expect(after[0].fingerprint).toBe(before[0].fingerprint);
|
|
140
|
+
expect(after[0].lastSeenAt).toBeGreaterThanOrEqual(originalLastSeen);
|
|
141
|
+
|
|
142
|
+
// Should NOT enqueue a second embed job (only 1 from initial insert)
|
|
143
|
+
const jobs = db.select().from(memoryJobs).all();
|
|
144
|
+
expect(jobs).toHaveLength(1);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("updates statement when description changes", () => {
|
|
148
|
+
upsertCliCapabilityMemory("doctor", "Original description");
|
|
149
|
+
|
|
150
|
+
const db = getDb();
|
|
151
|
+
const before = db.select().from(memoryItems).all();
|
|
152
|
+
expect(before).toHaveLength(1);
|
|
153
|
+
expect(before[0].statement).toContain("Original description");
|
|
154
|
+
|
|
155
|
+
// Change description
|
|
156
|
+
upsertCliCapabilityMemory("doctor", "Updated description");
|
|
157
|
+
|
|
158
|
+
const after = db.select().from(memoryItems).all();
|
|
159
|
+
expect(after).toHaveLength(1);
|
|
160
|
+
expect(after[0].statement).toContain("Updated description");
|
|
161
|
+
expect(after[0].fingerprint).not.toBe(before[0].fingerprint);
|
|
162
|
+
|
|
163
|
+
// Should enqueue a second embed job
|
|
164
|
+
const jobs = db.select().from(memoryJobs).all();
|
|
165
|
+
expect(jobs).toHaveLength(2);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("reactivates soft-deleted items", () => {
|
|
169
|
+
upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
|
|
170
|
+
|
|
171
|
+
const db = getDb();
|
|
172
|
+
// Soft-delete the item
|
|
173
|
+
db.update(memoryItems)
|
|
174
|
+
.set({ status: "deleted" })
|
|
175
|
+
.where(eq(memoryItems.subject, "cli:doctor"))
|
|
176
|
+
.run();
|
|
177
|
+
|
|
178
|
+
const deleted = db.select().from(memoryItems).all();
|
|
179
|
+
expect(deleted[0].status).toBe("deleted");
|
|
180
|
+
|
|
181
|
+
// Clear jobs from initial insert
|
|
182
|
+
db.run("DELETE FROM memory_jobs");
|
|
183
|
+
|
|
184
|
+
// Upsert again — should reactivate
|
|
185
|
+
upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
|
|
186
|
+
|
|
187
|
+
const reactivated = db.select().from(memoryItems).all();
|
|
188
|
+
expect(reactivated).toHaveLength(1);
|
|
189
|
+
expect(reactivated[0].status).toBe("active");
|
|
190
|
+
|
|
191
|
+
// Should enqueue embed job for reactivated item
|
|
192
|
+
const jobs = db.select().from(memoryJobs).all();
|
|
193
|
+
expect(jobs).toHaveLength(1);
|
|
194
|
+
expect(jobs[0].type).toBe("embed_item");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("does not throw on DB error", () => {
|
|
198
|
+
resetDb();
|
|
199
|
+
const db = getDb();
|
|
200
|
+
db.run("DROP TABLE IF EXISTS memory_items");
|
|
201
|
+
|
|
202
|
+
expect(() => {
|
|
203
|
+
upsertCliCapabilityMemory("doctor", "Run diagnostic checks");
|
|
204
|
+
}).not.toThrow();
|
|
205
|
+
|
|
206
|
+
// Restore DB state for subsequent tests.
|
|
207
|
+
resetDb();
|
|
208
|
+
const dbPath = getDbPath();
|
|
209
|
+
for (const ext of ["", "-wal", "-shm"]) {
|
|
210
|
+
rmSync(`${dbPath}${ext}`, { force: true });
|
|
211
|
+
}
|
|
212
|
+
initializeDb();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// ─── seedCliCommandMemories ─────────────────────────────────────────────────
|
|
217
|
+
|
|
218
|
+
describe("seedCliCommandMemories", () => {
|
|
219
|
+
beforeEach(() => {
|
|
220
|
+
resetTables();
|
|
221
|
+
// Reset mock commands
|
|
222
|
+
mockCommands = [];
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
test("upserts capability memories for all commands", () => {
|
|
226
|
+
mockCommands = [
|
|
227
|
+
{ name: "doctor", description: "Run diagnostic checks" },
|
|
228
|
+
{ name: "config", description: "Manage configuration" },
|
|
229
|
+
{ name: "keys", description: "Manage API keys" },
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
seedCliCommandMemories();
|
|
233
|
+
|
|
234
|
+
const db = getDb();
|
|
235
|
+
const items = db
|
|
236
|
+
.select()
|
|
237
|
+
.from(memoryItems)
|
|
238
|
+
.where(eq(memoryItems.kind, "capability"))
|
|
239
|
+
.all();
|
|
240
|
+
expect(items).toHaveLength(3);
|
|
241
|
+
|
|
242
|
+
const subjects = items.map((i) => i.subject).sort();
|
|
243
|
+
expect(subjects).toEqual([
|
|
244
|
+
"cli:config",
|
|
245
|
+
"cli:doctor",
|
|
246
|
+
"cli:keys",
|
|
247
|
+
]);
|
|
248
|
+
|
|
249
|
+
// All should be active
|
|
250
|
+
for (const item of items) {
|
|
251
|
+
expect(item.status).toBe("active");
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("prunes stale capabilities for commands no longer registered", () => {
|
|
256
|
+
// First seed with three commands
|
|
257
|
+
mockCommands = [
|
|
258
|
+
{ name: "doctor", description: "Run diagnostic checks" },
|
|
259
|
+
{ name: "config", description: "Manage configuration" },
|
|
260
|
+
{ name: "keys", description: "Manage API keys" },
|
|
261
|
+
];
|
|
262
|
+
seedCliCommandMemories();
|
|
263
|
+
|
|
264
|
+
const db = getDb();
|
|
265
|
+
const beforeItems = db
|
|
266
|
+
.select()
|
|
267
|
+
.from(memoryItems)
|
|
268
|
+
.where(eq(memoryItems.kind, "capability"))
|
|
269
|
+
.all();
|
|
270
|
+
expect(beforeItems).toHaveLength(3);
|
|
271
|
+
expect(beforeItems.every((i) => i.status === "active")).toBe(true);
|
|
272
|
+
|
|
273
|
+
// Now seed with only doctor — config and keys should be pruned
|
|
274
|
+
mockCommands = [
|
|
275
|
+
{ name: "doctor", description: "Run diagnostic checks" },
|
|
276
|
+
];
|
|
277
|
+
seedCliCommandMemories();
|
|
278
|
+
|
|
279
|
+
const afterItems = db
|
|
280
|
+
.select()
|
|
281
|
+
.from(memoryItems)
|
|
282
|
+
.where(eq(memoryItems.kind, "capability"))
|
|
283
|
+
.all();
|
|
284
|
+
expect(afterItems).toHaveLength(3); // still 3 rows, but 2 are soft-deleted
|
|
285
|
+
|
|
286
|
+
const active = afterItems.filter((i) => i.status === "active");
|
|
287
|
+
const deleted = afterItems.filter((i) => i.status === "deleted");
|
|
288
|
+
|
|
289
|
+
expect(active).toHaveLength(1);
|
|
290
|
+
expect(active[0].subject).toBe("cli:doctor");
|
|
291
|
+
|
|
292
|
+
expect(deleted).toHaveLength(2);
|
|
293
|
+
const deletedSubjects = deleted.map((i) => i.subject).sort();
|
|
294
|
+
expect(deletedSubjects).toEqual(["cli:config", "cli:keys"]);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test("handles empty command list without errors", () => {
|
|
298
|
+
// Pre-populate a CLI command so we can verify it gets pruned
|
|
299
|
+
upsertCliCapabilityMemory("old-command", "An old command");
|
|
300
|
+
|
|
301
|
+
const db = getDb();
|
|
302
|
+
const beforeItems = db.select().from(memoryItems).all();
|
|
303
|
+
expect(beforeItems).toHaveLength(1);
|
|
304
|
+
expect(beforeItems[0].status).toBe("active");
|
|
305
|
+
|
|
306
|
+
// Seed with empty commands
|
|
307
|
+
mockCommands = [];
|
|
308
|
+
seedCliCommandMemories();
|
|
309
|
+
|
|
310
|
+
// The existing command should be pruned (soft-deleted)
|
|
311
|
+
const afterItems = db.select().from(memoryItems).all();
|
|
312
|
+
expect(afterItems).toHaveLength(1);
|
|
313
|
+
expect(afterItems[0].status).toBe("deleted");
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("does not prune non-cli capability memories", () => {
|
|
317
|
+
// Pre-insert a skill capability memory directly into the DB
|
|
318
|
+
const db = getDb();
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
db.insert(memoryItems)
|
|
321
|
+
.values({
|
|
322
|
+
id: "skill-test-item",
|
|
323
|
+
kind: "capability",
|
|
324
|
+
subject: "skill:test-skill",
|
|
325
|
+
statement: "The test skill does things.",
|
|
326
|
+
status: "active",
|
|
327
|
+
confidence: 1.0,
|
|
328
|
+
importance: 0.7,
|
|
329
|
+
fingerprint: "skill-test-fp",
|
|
330
|
+
sourceType: "extraction",
|
|
331
|
+
scopeId: "default",
|
|
332
|
+
firstSeenAt: now,
|
|
333
|
+
lastSeenAt: now,
|
|
334
|
+
})
|
|
335
|
+
.run();
|
|
336
|
+
|
|
337
|
+
// Seed with empty commands — CLI pruner runs but should skip skill:* items
|
|
338
|
+
mockCommands = [];
|
|
339
|
+
seedCliCommandMemories();
|
|
340
|
+
|
|
341
|
+
const item = db
|
|
342
|
+
.select()
|
|
343
|
+
.from(memoryItems)
|
|
344
|
+
.where(eq(memoryItems.subject, "skill:test-skill"))
|
|
345
|
+
.get();
|
|
346
|
+
expect(item).toBeDefined();
|
|
347
|
+
expect(item!.status).toBe("active");
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
test("does not throw on error", () => {
|
|
351
|
+
mockCommands = [
|
|
352
|
+
{ name: "doctor", description: "Run diagnostic checks" },
|
|
353
|
+
];
|
|
354
|
+
|
|
355
|
+
// Drop memory_items to force a DB error during the prune phase
|
|
356
|
+
resetDb();
|
|
357
|
+
const db = getDb();
|
|
358
|
+
db.run("DROP TABLE IF EXISTS memory_items");
|
|
359
|
+
|
|
360
|
+
expect(() => {
|
|
361
|
+
seedCliCommandMemories();
|
|
362
|
+
}).not.toThrow();
|
|
363
|
+
|
|
364
|
+
// Restore DB state for subsequent tests.
|
|
365
|
+
resetDb();
|
|
366
|
+
const dbPath = getDbPath();
|
|
367
|
+
for (const ext of ["", "-wal", "-shm"]) {
|
|
368
|
+
rmSync(`${dbPath}${ext}`, { force: true });
|
|
369
|
+
}
|
|
370
|
+
initializeDb();
|
|
371
|
+
});
|
|
372
|
+
});
|
|
@@ -52,9 +52,19 @@ describe("computer-use skill manifest regression", () => {
|
|
|
52
52
|
}
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
test("
|
|
55
|
+
test("read-only tools have risk: low, side-effect tools have risk: medium", () => {
|
|
56
|
+
const readOnlyTools = new Set([
|
|
57
|
+
"computer_use_observe",
|
|
58
|
+
"computer_use_wait",
|
|
59
|
+
"computer_use_done",
|
|
60
|
+
"computer_use_respond",
|
|
61
|
+
]);
|
|
56
62
|
for (const tool of manifest.tools) {
|
|
57
|
-
|
|
63
|
+
if (readOnlyTools.has(tool.name)) {
|
|
64
|
+
expect(tool.risk).toBe("low");
|
|
65
|
+
} else {
|
|
66
|
+
expect(tool.risk).toBe("medium");
|
|
67
|
+
}
|
|
58
68
|
}
|
|
59
69
|
});
|
|
60
70
|
|
|
@@ -421,7 +421,6 @@ describe("AssistantConfigSchema", () => {
|
|
|
421
421
|
const result = AssistantConfigSchema.parse({});
|
|
422
422
|
expect(result.permissions).toEqual({
|
|
423
423
|
mode: "workspace",
|
|
424
|
-
dangerouslySkipPermissions: false,
|
|
425
424
|
});
|
|
426
425
|
});
|
|
427
426
|
|
|
@@ -1129,7 +1128,6 @@ describe("loadConfig with schema validation", () => {
|
|
|
1129
1128
|
const config = loadConfig();
|
|
1130
1129
|
expect(config.permissions).toEqual({
|
|
1131
1130
|
mode: "workspace",
|
|
1132
|
-
dangerouslySkipPermissions: false,
|
|
1133
1131
|
});
|
|
1134
1132
|
});
|
|
1135
1133
|
|