@vellumai/assistant 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +32 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/openapi.yaml +538 -3
- package/package.json +5 -1
- package/src/__tests__/anthropic-provider.test.ts +160 -95
- package/src/__tests__/app-dir-path-guard.test.ts +1 -0
- package/src/__tests__/app-executors.test.ts +47 -1
- package/src/__tests__/app-source-watcher.test.ts +159 -0
- package/src/__tests__/checker.test.ts +38 -6
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/conversation-agent-loop-overflow.test.ts +4 -6
- package/src/__tests__/conversation-agent-loop.test.ts +4 -51
- package/src/__tests__/conversation-history-web-search.test.ts +1 -1
- package/src/__tests__/conversation-runtime-assembly.test.ts +653 -832
- package/src/__tests__/conversation-runtime-workspace.test.ts +1 -93
- package/src/__tests__/conversation-tool-setup-app-refresh.test.ts +17 -4
- package/src/__tests__/conversation-wipe.test.ts +2 -6
- package/src/__tests__/conversation-workspace-cache-state.test.ts +6 -12
- package/src/__tests__/conversation-workspace-injection.test.ts +25 -26
- package/src/__tests__/conversation-workspace-tool-tracking.test.ts +1 -1
- package/src/__tests__/copy-composer-tc-templates.test.ts +335 -0
- package/src/__tests__/date-context.test.ts +76 -210
- package/src/__tests__/db-schedule-syntax-migration.test.ts +16 -1
- package/src/__tests__/file-list-tool.test.ts +219 -0
- package/src/__tests__/first-greeting.test.ts +1 -1
- package/src/__tests__/heartbeat-service.test.ts +180 -3
- package/src/__tests__/identity-routes.test.ts +328 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/install-skill-routing.test.ts +7 -6
- package/src/__tests__/jobs-store-qdrant-breaker.test.ts +15 -14
- package/src/__tests__/list-messages-tool-merge.test.ts +300 -0
- package/src/__tests__/llm-context-normalization.test.ts +18 -18
- package/src/__tests__/llm-context-route-provider.test.ts +101 -0
- package/src/__tests__/llm-request-log-turn-query.test.ts +162 -0
- package/src/__tests__/log-export-workspace.test.ts +72 -105
- package/src/__tests__/mcp-abort-signal.test.ts +5 -0
- package/src/__tests__/mcp-client-auth.test.ts +5 -0
- package/src/__tests__/memory-recall-log-store.test.ts +132 -0
- package/src/__tests__/migration-export-streaming.test.ts +304 -0
- package/src/__tests__/migration-import-commit-http.test.ts +11 -10
- package/src/__tests__/mock-fetch.ts +87 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +62 -14
- package/src/__tests__/parser.test.ts +32 -0
- package/src/__tests__/permission-checker-host-gate.test.ts +452 -0
- package/src/__tests__/permission-controls-v2-flag.test.ts +55 -0
- package/src/__tests__/permission-mode-sse.test.ts +418 -0
- package/src/__tests__/permission-mode-store.test.ts +277 -0
- package/src/__tests__/permission-mode.test.ts +101 -0
- package/src/__tests__/platform-bash-auto-approve.test.ts +359 -0
- package/src/__tests__/profiler-routes.test.ts +502 -0
- package/src/__tests__/profiler-run-store.test.ts +441 -0
- package/src/__tests__/proxy-approval-callback.test.ts +4 -75
- package/src/__tests__/registry.test.ts +1 -1
- package/src/__tests__/sandbox-host-parity.test.ts +5 -4
- package/src/__tests__/scheduler-reuse-conversation.test.ts +368 -0
- package/src/__tests__/scrub-corrupted-image-attachments.test.ts +278 -0
- package/src/__tests__/search-skills-unified.test.ts +4 -3
- package/src/__tests__/send-endpoint-busy.test.ts +42 -3
- package/src/__tests__/set-permission-mode.test.ts +274 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +12 -0
- package/src/__tests__/skill-memory.test.ts +2 -783
- package/src/__tests__/strip-memory-injections.test.ts +187 -0
- package/src/__tests__/subagent-detail.test.ts +84 -0
- package/src/__tests__/subagent-disposal.test.ts +308 -0
- package/src/__tests__/subagent-manager-notify.test.ts +19 -10
- package/src/__tests__/subagent-notify-parent.test.ts +390 -0
- package/src/__tests__/subagent-role-registry.test.ts +108 -0
- package/src/__tests__/subagent-tool-filtering.test.ts +71 -0
- package/src/__tests__/subagent-tools.test.ts +464 -4
- package/src/__tests__/system-prompt-ask-mode.test.ts +139 -0
- package/src/__tests__/task-memory-cleanup.test.ts +12 -12
- package/src/__tests__/terminal-tools.test.ts +17 -27
- package/src/__tests__/test-preload.ts +4 -0
- package/src/__tests__/tool-executor.test.ts +4 -26
- package/src/__tests__/tool-side-effects-slack-dm.test.ts +1 -0
- package/src/__tests__/top-level-renderer.test.ts +10 -13
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +116 -2
- package/src/__tests__/workspace-migration-028-recover-conversations-from-disk-view.test.ts +387 -0
- package/src/agent/loop.ts +6 -0
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/cli/__tests__/run-assistant-command.ts +29 -0
- package/src/cli/commands/__tests__/email-download.test.ts +245 -0
- package/src/cli/commands/__tests__/email-list.test.ts +192 -0
- package/src/cli/commands/__tests__/email-register.test.ts +186 -0
- package/src/cli/commands/__tests__/email-send.test.ts +291 -0
- package/src/cli/commands/__tests__/email-status.test.ts +181 -0
- package/src/cli/commands/__tests__/email-unregister.test.ts +139 -0
- package/src/cli/commands/__tests__/routes.test.ts +562 -0
- package/src/cli/commands/conversations.ts +1 -8
- package/src/cli/commands/email.ts +584 -835
- package/src/cli/commands/memory.ts +1 -34
- package/src/cli/commands/notifications.ts +7 -2
- package/src/cli/commands/oauth/connect.ts +14 -5
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +2 -0
- package/src/cli.ts +1 -120
- package/src/config/bundled-skills/app-builder/SKILL.md +4 -1
- package/src/config/bundled-skills/gmail/SKILL.md +2 -2
- package/src/config/bundled-skills/messaging/SKILL.md +7 -0
- package/src/config/bundled-skills/schedule/SKILL.md +22 -2
- package/src/config/bundled-skills/schedule/TOOLS.json +8 -0
- package/src/config/bundled-skills/settings/tools/avatar-get.ts +3 -13
- package/src/config/bundled-skills/settings/tools/avatar-remove.ts +2 -4
- package/src/config/bundled-skills/settings/tools/avatar-update.ts +5 -2
- package/src/config/bundled-skills/slack/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/SKILL.md +43 -3
- package/src/config/bundled-skills/subagent/TOOLS.json +29 -4
- package/src/config/env-registry.ts +63 -0
- package/src/config/feature-flag-registry.json +17 -1
- package/src/config/schema.ts +8 -0
- package/src/config/schemas/filing.ts +51 -0
- package/src/config/schemas/heartbeat.ts +15 -12
- package/src/config/schemas/memory-lifecycle.ts +12 -0
- package/src/config/schemas/security.ts +14 -0
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +79 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +158 -65
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-process.ts +13 -7
- package/src/daemon/conversation-runtime-assembly.ts +300 -306
- package/src/daemon/conversation-tool-setup.ts +44 -14
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +18 -0
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +4 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +63 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +199 -157
- package/src/daemon/message-types/conversations.ts +25 -6
- package/src/daemon/message-types/messages.ts +9 -1
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +6 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +89 -9
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/export/transcript-formatter.ts +148 -0
- package/src/filing/filing-service.ts +228 -0
- package/src/heartbeat/heartbeat-service.ts +96 -7
- package/src/mcp/client.ts +6 -0
- package/src/mcp/mcp-oauth-provider.ts +149 -27
- package/src/memory/admin.ts +33 -32
- package/src/memory/app-store.ts +69 -0
- package/src/memory/conversation-bootstrap.ts +1 -1
- package/src/memory/conversation-crud.ts +136 -107
- package/src/memory/conversation-group-migration.ts +1 -1
- package/src/memory/conversation-queries.ts +58 -12
- package/src/memory/conversation-title-service.ts +1 -0
- package/src/memory/db-init.ts +182 -376
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -15
- package/src/memory/graph/consolidation.ts +38 -4
- package/src/memory/graph/conversation-graph-memory.ts +133 -104
- package/src/memory/graph/extraction-job.ts +9 -4
- package/src/memory/graph/extraction.ts +66 -23
- package/src/memory/graph/graph-memory-state-store.ts +37 -0
- package/src/memory/graph/graph-search.ts +29 -15
- package/src/memory/graph/injection.ts +38 -8
- package/src/memory/graph/inspect.ts +12 -3
- package/src/memory/graph/retriever.ts +365 -262
- package/src/memory/graph/store.test.ts +48 -0
- package/src/memory/graph/store.ts +150 -11
- package/src/memory/graph/tool-handlers.ts +84 -209
- package/src/memory/graph/tools.ts +8 -52
- package/src/memory/graph/types.ts +24 -0
- package/src/memory/job-handlers/cleanup.ts +44 -1
- package/src/memory/jobs-store.ts +70 -60
- package/src/memory/jobs-worker.ts +44 -28
- package/src/memory/llm-request-log-store.ts +96 -12
- package/src/memory/memory-recall-log-store.ts +49 -5
- package/src/memory/migrations/203-drop-memory-items-tables.ts +33 -1
- package/src/memory/migrations/206-memory-graph-node-edits.ts +19 -0
- package/src/memory/migrations/206-scrub-corrupted-image-attachments.ts +131 -0
- package/src/memory/migrations/207-conversation-graph-memory-state.ts +20 -0
- package/src/memory/migrations/208-conversations-last-message-at.ts +35 -0
- package/src/memory/migrations/209-strip-thinking-from-consolidated.ts +85 -0
- package/src/memory/migrations/210-schedule-reuse-conversation.ts +13 -0
- package/src/memory/migrations/211-memory-recall-logs-query-context.ts +21 -0
- package/src/memory/migrations/212-llm-request-logs-created-at-index.ts +19 -0
- package/src/memory/migrations/index.ts +8 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/conversations.ts +14 -0
- package/src/memory/schema/infrastructure.ts +8 -1
- package/src/memory/schema/memory-core.ts +0 -51
- package/src/memory/schema/memory-graph.ts +15 -0
- package/src/memory/task-memory-cleanup.ts +30 -11
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/permissions/checker.ts +12 -1
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +70 -165
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +25 -4
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/http-server.ts +32 -2
- package/src/runtime/http-types.ts +12 -1
- package/src/runtime/migrations/vbundle-builder.ts +389 -3
- package/src/runtime/migrations/vbundle-importer.ts +8 -6
- package/src/runtime/routes/__tests__/user-route-dispatcher.test.ts +378 -0
- package/src/runtime/routes/app-management-routes.ts +1 -11
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +26 -0
- package/src/runtime/routes/archive-utils.ts +29 -0
- package/src/runtime/routes/avatar-routes.ts +2 -9
- package/src/runtime/routes/btw-routes.ts +14 -1
- package/src/runtime/routes/conversation-analysis-routes.ts +173 -0
- package/src/runtime/routes/conversation-management-routes.ts +1 -14
- package/src/runtime/routes/conversation-query-routes.ts +49 -3
- package/src/runtime/routes/conversation-routes.ts +264 -44
- package/src/runtime/routes/heartbeat-routes.ts +4 -10
- package/src/runtime/routes/identity-routes.ts +53 -18
- package/src/runtime/routes/llm-context-normalization.ts +14 -10
- package/src/runtime/routes/log-export-routes.ts +23 -275
- package/src/runtime/routes/memory-item-routes.test.ts +168 -233
- package/src/runtime/routes/migration-routes.ts +18 -7
- package/src/runtime/routes/profiler-routes.ts +350 -0
- package/src/runtime/routes/schedule-routes.ts +27 -12
- package/src/runtime/routes/settings-routes.ts +95 -8
- package/src/runtime/routes/subagents-routes.ts +28 -7
- package/src/runtime/routes/user-route-dispatcher.ts +223 -0
- package/src/runtime/routes/user-routes.ts +41 -0
- package/src/runtime/routes/workspace-routes.ts +0 -1
- package/src/schedule/schedule-store.ts +30 -0
- package/src/schedule/scheduler.ts +45 -18
- package/src/skills/catalog-install.ts +10 -2
- package/src/skills/managed-store.ts +2 -2
- package/src/skills/skill-memory.ts +1 -293
- package/src/subagent/index.ts +13 -3
- package/src/subagent/manager.ts +308 -29
- package/src/subagent/types.ts +68 -0
- package/src/tasks/task-runner.ts +4 -4
- package/src/tools/apps/executors.ts +29 -4
- package/src/tools/filesystem/list.ts +93 -0
- package/src/tools/permission-checker.ts +78 -0
- package/src/tools/registry.ts +4 -0
- package/src/tools/schedule/create.ts +3 -0
- package/src/tools/schedule/list.ts +1 -0
- package/src/tools/schedule/update.ts +6 -0
- package/src/tools/shared/filesystem/errors.ts +5 -0
- package/src/tools/shared/filesystem/file-ops-service.ts +90 -2
- package/src/tools/shared/filesystem/types.ts +17 -0
- package/src/tools/shared/shell-output.ts +31 -2
- package/src/tools/subagent/abort.ts +12 -2
- package/src/tools/subagent/message.ts +9 -2
- package/src/tools/subagent/notify-parent.ts +79 -0
- package/src/tools/subagent/read.ts +29 -8
- package/src/tools/subagent/resolve.ts +21 -0
- package/src/tools/subagent/spawn.ts +2 -0
- package/src/tools/subagent/status.ts +11 -1
- package/src/tools/system/avatar-generator.ts +3 -3
- package/src/tools/system/register.ts +23 -0
- package/src/tools/system/set-permission-mode.ts +103 -0
- package/src/tools/terminal/parser.ts +30 -5
- package/src/tools/terminal/safe-env.ts +16 -1
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -0
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/workspace/migrations/023-move-config-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/024-move-runtime-files-to-workspace.ts +2 -2
- package/src/workspace/migrations/028-recover-conversations-from-disk-view.ts +270 -0
- package/src/workspace/migrations/029-seed-pkb.ts +84 -0
- package/src/workspace/migrations/registry.ts +4 -0
- package/src/workspace/top-level-renderer.ts +5 -9
- package/src/__tests__/cli-memory.test.ts +0 -377
- package/src/__tests__/clipboard.test.ts +0 -88
- package/src/cli/cli-memory.ts +0 -179
- package/src/util/clipboard.ts +0 -34
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for profiler HTTP route handlers: empty-state listings, missing-run
|
|
3
|
+
* 404s, active-run delete rejection, tarball export success, archive failure
|
|
4
|
+
* when a run directory exceeds the configured bundle size cap, and post-delete
|
|
5
|
+
* budget recalculation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { tmpdir } from "node:os";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
12
|
+
|
|
13
|
+
import type { ProfilerRunManifest } from "../daemon/profiler-run-store.js";
|
|
14
|
+
import type { AuthContext } from "../runtime/auth/types.js";
|
|
15
|
+
import type { RouteContext, RouteParams } from "../runtime/http-router.js";
|
|
16
|
+
import { profilerRouteDefinitions } from "../runtime/routes/profiler-routes.js";
|
|
17
|
+
|
|
18
|
+
// ── Test scaffolding ────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
let testDir: string;
|
|
21
|
+
let runsDir: string;
|
|
22
|
+
let origEnv: Record<string, string | undefined>;
|
|
23
|
+
|
|
24
|
+
const routes = profilerRouteDefinitions();
|
|
25
|
+
|
|
26
|
+
function findRoute(
|
|
27
|
+
endpoint: string,
|
|
28
|
+
method: string,
|
|
29
|
+
): (typeof routes)[number] | undefined {
|
|
30
|
+
return routes.find((r) => r.endpoint === endpoint && r.method === method);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Build a minimal RouteContext for testing route handlers.
|
|
35
|
+
*/
|
|
36
|
+
function makeCtx(
|
|
37
|
+
params: RouteParams = {},
|
|
38
|
+
method: string = "GET",
|
|
39
|
+
): RouteContext {
|
|
40
|
+
return {
|
|
41
|
+
req: new Request("http://localhost:7821/v1/profiler/runs", { method }),
|
|
42
|
+
url: new URL("http://localhost:7821/v1/profiler/runs"),
|
|
43
|
+
server: {} as ReturnType<typeof Bun.serve>,
|
|
44
|
+
authContext: {
|
|
45
|
+
subject: "svc:gateway:self",
|
|
46
|
+
principalType: "svc_gateway",
|
|
47
|
+
assistantId: "self",
|
|
48
|
+
scopeProfile: "gateway_service_v1",
|
|
49
|
+
scopes: new Set(["internal.write"]),
|
|
50
|
+
policyEpoch: 1,
|
|
51
|
+
} as AuthContext,
|
|
52
|
+
params,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a fake profiler run directory with some payload files.
|
|
58
|
+
*/
|
|
59
|
+
function createRun(
|
|
60
|
+
runId: string,
|
|
61
|
+
opts?: {
|
|
62
|
+
sizeBytes?: number;
|
|
63
|
+
manifest?: Partial<ProfilerRunManifest>;
|
|
64
|
+
markdownSummary?: string;
|
|
65
|
+
},
|
|
66
|
+
): string {
|
|
67
|
+
const dir = join(runsDir, runId);
|
|
68
|
+
mkdirSync(dir, { recursive: true });
|
|
69
|
+
|
|
70
|
+
// Write a payload file of the requested size
|
|
71
|
+
const size = opts?.sizeBytes ?? 1024;
|
|
72
|
+
writeFileSync(join(dir, "profile.cpuprofile"), Buffer.alloc(size));
|
|
73
|
+
|
|
74
|
+
// Optionally write a pre-existing manifest
|
|
75
|
+
if (opts?.manifest) {
|
|
76
|
+
const m: ProfilerRunManifest = {
|
|
77
|
+
runId,
|
|
78
|
+
status: opts.manifest.status ?? "completed",
|
|
79
|
+
createdAt: opts.manifest.createdAt ?? new Date().toISOString(),
|
|
80
|
+
updatedAt: opts.manifest.updatedAt ?? new Date().toISOString(),
|
|
81
|
+
totalBytes: opts.manifest.totalBytes ?? size,
|
|
82
|
+
};
|
|
83
|
+
writeFileSync(join(dir, "manifest.json"), JSON.stringify(m, null, 2));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Optionally write a markdown summary
|
|
87
|
+
if (opts?.markdownSummary) {
|
|
88
|
+
writeFileSync(join(dir, "profile-summary.md"), opts.markdownSummary);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return dir;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
beforeEach(() => {
|
|
95
|
+
testDir = join(
|
|
96
|
+
tmpdir(),
|
|
97
|
+
`vellum-profiler-routes-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
|
|
98
|
+
);
|
|
99
|
+
runsDir = join(testDir, "data", "profiler", "runs");
|
|
100
|
+
mkdirSync(runsDir, { recursive: true });
|
|
101
|
+
|
|
102
|
+
// Save and override env
|
|
103
|
+
origEnv = {
|
|
104
|
+
VELLUM_WORKSPACE_DIR: process.env.VELLUM_WORKSPACE_DIR,
|
|
105
|
+
VELLUM_PROFILER_RUN_ID: process.env.VELLUM_PROFILER_RUN_ID,
|
|
106
|
+
VELLUM_PROFILER_MAX_BYTES: process.env.VELLUM_PROFILER_MAX_BYTES,
|
|
107
|
+
VELLUM_PROFILER_MAX_RUNS: process.env.VELLUM_PROFILER_MAX_RUNS,
|
|
108
|
+
VELLUM_PROFILER_MIN_FREE_MB: process.env.VELLUM_PROFILER_MIN_FREE_MB,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Point workspace dir to our temp directory
|
|
112
|
+
process.env.VELLUM_WORKSPACE_DIR = testDir;
|
|
113
|
+
|
|
114
|
+
// Clear profiler env vars
|
|
115
|
+
delete process.env.VELLUM_PROFILER_RUN_ID;
|
|
116
|
+
delete process.env.VELLUM_PROFILER_MAX_BYTES;
|
|
117
|
+
delete process.env.VELLUM_PROFILER_MAX_RUNS;
|
|
118
|
+
delete process.env.VELLUM_PROFILER_MIN_FREE_MB;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
afterEach(() => {
|
|
122
|
+
// Restore env
|
|
123
|
+
for (const [key, value] of Object.entries(origEnv)) {
|
|
124
|
+
if (value === undefined) {
|
|
125
|
+
delete process.env[key];
|
|
126
|
+
} else {
|
|
127
|
+
process.env[key] = value;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Clean up temp directory
|
|
132
|
+
if (existsSync(testDir)) {
|
|
133
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ── Tests ───────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
describe("Profiler routes", () => {
|
|
140
|
+
describe("GET /v1/profiler/runs (list)", () => {
|
|
141
|
+
test("returns empty list when no runs exist", async () => {
|
|
142
|
+
const route = findRoute("profiler/runs", "GET")!;
|
|
143
|
+
const response = await route.handler(makeCtx());
|
|
144
|
+
const body = (await response.json()) as {
|
|
145
|
+
runs: unknown[];
|
|
146
|
+
totalRuns: number;
|
|
147
|
+
activeRunId: string | null;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
expect(response.status).toBe(200);
|
|
151
|
+
expect(body.runs).toEqual([]);
|
|
152
|
+
expect(body.totalRuns).toBe(0);
|
|
153
|
+
expect(body.activeRunId).toBeNull();
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("returns runs sorted newest-first", async () => {
|
|
157
|
+
createRun("old-run", {
|
|
158
|
+
sizeBytes: 1024,
|
|
159
|
+
manifest: {
|
|
160
|
+
status: "completed",
|
|
161
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
createRun("new-run", {
|
|
165
|
+
sizeBytes: 1024,
|
|
166
|
+
manifest: {
|
|
167
|
+
status: "completed",
|
|
168
|
+
createdAt: "2025-06-01T00:00:00Z",
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
const route = findRoute("profiler/runs", "GET")!;
|
|
173
|
+
const response = await route.handler(makeCtx());
|
|
174
|
+
const body = (await response.json()) as {
|
|
175
|
+
runs: ProfilerRunManifest[];
|
|
176
|
+
totalRuns: number;
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
expect(response.status).toBe(200);
|
|
180
|
+
expect(body.totalRuns).toBe(2);
|
|
181
|
+
expect(body.runs[0]!.runId).toBe("new-run");
|
|
182
|
+
expect(body.runs[1]!.runId).toBe("old-run");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("reports active run ID when set", async () => {
|
|
186
|
+
process.env.VELLUM_PROFILER_RUN_ID = "active-run";
|
|
187
|
+
createRun("active-run", { sizeBytes: 512 });
|
|
188
|
+
|
|
189
|
+
const route = findRoute("profiler/runs", "GET")!;
|
|
190
|
+
const response = await route.handler(makeCtx());
|
|
191
|
+
const body = (await response.json()) as {
|
|
192
|
+
activeRunId: string | null;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
expect(body.activeRunId).toBe("active-run");
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("path traversal rejection", () => {
|
|
200
|
+
const traversalPayloads = [
|
|
201
|
+
"../../../etc/passwd",
|
|
202
|
+
"..%2F..%2Fetc%2Fpasswd",
|
|
203
|
+
"foo/bar",
|
|
204
|
+
"foo\\bar",
|
|
205
|
+
"..\\..\\windows",
|
|
206
|
+
];
|
|
207
|
+
|
|
208
|
+
for (const payload of traversalPayloads) {
|
|
209
|
+
test(`GET rejects runId "${payload}"`, async () => {
|
|
210
|
+
const route = findRoute("profiler/runs/:runId", "GET")!;
|
|
211
|
+
const response = await route.handler(makeCtx({ runId: payload }));
|
|
212
|
+
expect(response.status).toBe(400);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test(`POST export rejects runId "${payload}"`, async () => {
|
|
216
|
+
const route = findRoute("profiler/runs/:runId/export", "POST")!;
|
|
217
|
+
const response = await route.handler(
|
|
218
|
+
makeCtx({ runId: payload }, "POST"),
|
|
219
|
+
);
|
|
220
|
+
expect(response.status).toBe(400);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test(`DELETE rejects runId "${payload}"`, async () => {
|
|
224
|
+
const route = findRoute("profiler/runs/:runId", "DELETE")!;
|
|
225
|
+
const response = await route.handler(
|
|
226
|
+
makeCtx({ runId: payload }, "DELETE"),
|
|
227
|
+
);
|
|
228
|
+
expect(response.status).toBe(400);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe("GET /v1/profiler/runs/:runId (detail)", () => {
|
|
234
|
+
test("returns 404 for missing run", async () => {
|
|
235
|
+
const route = findRoute("profiler/runs/:runId", "GET")!;
|
|
236
|
+
const response = await route.handler(makeCtx({ runId: "nonexistent" }));
|
|
237
|
+
|
|
238
|
+
expect(response.status).toBe(404);
|
|
239
|
+
const body = (await response.json()) as {
|
|
240
|
+
error: { code: string };
|
|
241
|
+
};
|
|
242
|
+
expect(body.error.code).toBe("NOT_FOUND");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("returns manifest metadata and markdown summary", async () => {
|
|
246
|
+
createRun("run-with-summary", {
|
|
247
|
+
sizeBytes: 2048,
|
|
248
|
+
manifest: {
|
|
249
|
+
status: "completed",
|
|
250
|
+
createdAt: "2025-03-15T10:00:00Z",
|
|
251
|
+
},
|
|
252
|
+
markdownSummary: "# CPU Profile\n\nTop functions by self-time...",
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const route = findRoute("profiler/runs/:runId", "GET")!;
|
|
256
|
+
const response = await route.handler(
|
|
257
|
+
makeCtx({ runId: "run-with-summary" }),
|
|
258
|
+
);
|
|
259
|
+
const body = (await response.json()) as {
|
|
260
|
+
runId: string;
|
|
261
|
+
status: string;
|
|
262
|
+
summary: string | null;
|
|
263
|
+
isActive: boolean;
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
expect(response.status).toBe(200);
|
|
267
|
+
expect(body.runId).toBe("run-with-summary");
|
|
268
|
+
expect(body.status).toBe("completed");
|
|
269
|
+
expect(body.summary).toContain("CPU Profile");
|
|
270
|
+
expect(body.isActive).toBe(false);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
test("returns null summary when no markdown file exists", async () => {
|
|
274
|
+
createRun("run-no-summary", {
|
|
275
|
+
sizeBytes: 1024,
|
|
276
|
+
manifest: { status: "completed" },
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const route = findRoute("profiler/runs/:runId", "GET")!;
|
|
280
|
+
const response = await route.handler(
|
|
281
|
+
makeCtx({ runId: "run-no-summary" }),
|
|
282
|
+
);
|
|
283
|
+
const body = (await response.json()) as {
|
|
284
|
+
summary: string | null;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
expect(response.status).toBe(200);
|
|
288
|
+
expect(body.summary).toBeNull();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test("marks active run correctly", async () => {
|
|
292
|
+
process.env.VELLUM_PROFILER_RUN_ID = "live-run";
|
|
293
|
+
createRun("live-run", {
|
|
294
|
+
sizeBytes: 1024,
|
|
295
|
+
manifest: { status: "active" },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
const route = findRoute("profiler/runs/:runId", "GET")!;
|
|
299
|
+
const response = await route.handler(makeCtx({ runId: "live-run" }));
|
|
300
|
+
const body = (await response.json()) as {
|
|
301
|
+
isActive: boolean;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
expect(response.status).toBe(200);
|
|
305
|
+
expect(body.isActive).toBe(true);
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
describe("POST /v1/profiler/runs/:runId/export", () => {
|
|
310
|
+
test("returns 404 for missing run", async () => {
|
|
311
|
+
const route = findRoute("profiler/runs/:runId/export", "POST")!;
|
|
312
|
+
const response = await route.handler(
|
|
313
|
+
makeCtx({ runId: "nonexistent" }, "POST"),
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
expect(response.status).toBe(404);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("returns tar.gz for a valid run", async () => {
|
|
320
|
+
createRun("exportable-run", {
|
|
321
|
+
sizeBytes: 512,
|
|
322
|
+
manifest: { status: "completed" },
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
const route = findRoute("profiler/runs/:runId/export", "POST")!;
|
|
326
|
+
const response = await route.handler(
|
|
327
|
+
makeCtx({ runId: "exportable-run" }, "POST"),
|
|
328
|
+
);
|
|
329
|
+
|
|
330
|
+
expect(response.status).toBe(200);
|
|
331
|
+
expect(response.headers.get("Content-Type")).toBe("application/gzip");
|
|
332
|
+
expect(response.headers.get("Content-Disposition")).toContain(
|
|
333
|
+
"profiler-exportable-run.tar.gz",
|
|
334
|
+
);
|
|
335
|
+
|
|
336
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
337
|
+
expect(arrayBuffer.byteLength).toBeGreaterThan(0);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("returns 500 when archive exceeds size limit", async () => {
|
|
341
|
+
// Create a run with a very large file that will exceed the 50MB archive cap.
|
|
342
|
+
// We mock this by creating a run directory and writing enough data to push
|
|
343
|
+
// past the limit. Since createTarGz uses MAX_ARCHIVE_BYTES as the maxBuffer,
|
|
344
|
+
// a large-enough payload will trigger the failure path.
|
|
345
|
+
const runDir = join(runsDir, "huge-run");
|
|
346
|
+
mkdirSync(runDir, { recursive: true });
|
|
347
|
+
|
|
348
|
+
// Write manifest so the route can find the run
|
|
349
|
+
const manifest: ProfilerRunManifest = {
|
|
350
|
+
runId: "huge-run",
|
|
351
|
+
status: "completed",
|
|
352
|
+
createdAt: new Date().toISOString(),
|
|
353
|
+
updatedAt: new Date().toISOString(),
|
|
354
|
+
totalBytes: 60 * 1024 * 1024,
|
|
355
|
+
};
|
|
356
|
+
writeFileSync(
|
|
357
|
+
join(runDir, "manifest.json"),
|
|
358
|
+
JSON.stringify(manifest, null, 2),
|
|
359
|
+
);
|
|
360
|
+
|
|
361
|
+
// Write a file large enough that the compressed tar exceeds 50MB.
|
|
362
|
+
// Random data doesn't compress well, so 55MB of random data should
|
|
363
|
+
// produce a tar.gz larger than 50MB.
|
|
364
|
+
const chunkSize = 1024 * 1024; // 1 MB
|
|
365
|
+
const chunks = 55;
|
|
366
|
+
for (let i = 0; i < chunks; i++) {
|
|
367
|
+
const buf = Buffer.alloc(chunkSize);
|
|
368
|
+
// Fill with pseudo-random data to defeat compression
|
|
369
|
+
for (let j = 0; j < chunkSize; j += 4) {
|
|
370
|
+
buf.writeUInt32LE((Math.random() * 0xffffffff) >>> 0, j);
|
|
371
|
+
}
|
|
372
|
+
writeFileSync(join(runDir, `chunk-${i}.bin`), buf);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const route = findRoute("profiler/runs/:runId/export", "POST")!;
|
|
376
|
+
const response = await route.handler(
|
|
377
|
+
makeCtx({ runId: "huge-run" }, "POST"),
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
expect(response.status).toBe(500);
|
|
381
|
+
const body = (await response.json()) as {
|
|
382
|
+
error: { code: string; message: string };
|
|
383
|
+
};
|
|
384
|
+
expect(body.error.code).toBe("INTERNAL_ERROR");
|
|
385
|
+
expect(body.error.message).toContain("archive size");
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe("DELETE /v1/profiler/runs/:runId", () => {
|
|
390
|
+
test("returns 404 for missing run", async () => {
|
|
391
|
+
const route = findRoute("profiler/runs/:runId", "DELETE")!;
|
|
392
|
+
const response = await route.handler(
|
|
393
|
+
makeCtx({ runId: "nonexistent" }, "DELETE"),
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
expect(response.status).toBe(404);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
test("rejects deletion of the currently active run", async () => {
|
|
400
|
+
process.env.VELLUM_PROFILER_RUN_ID = "active-run";
|
|
401
|
+
createRun("active-run", {
|
|
402
|
+
sizeBytes: 1024,
|
|
403
|
+
manifest: { status: "active" },
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const route = findRoute("profiler/runs/:runId", "DELETE")!;
|
|
407
|
+
const response = await route.handler(
|
|
408
|
+
makeCtx({ runId: "active-run" }, "DELETE"),
|
|
409
|
+
);
|
|
410
|
+
|
|
411
|
+
expect(response.status).toBe(409);
|
|
412
|
+
const body = (await response.json()) as {
|
|
413
|
+
error: { code: string };
|
|
414
|
+
};
|
|
415
|
+
expect(body.error.code).toBe("CONFLICT");
|
|
416
|
+
|
|
417
|
+
// Run directory should still exist
|
|
418
|
+
expect(existsSync(join(runsDir, "active-run"))).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("deletes a completed run and returns budget state", async () => {
|
|
422
|
+
process.env.VELLUM_PROFILER_MAX_BYTES = "999999999";
|
|
423
|
+
process.env.VELLUM_PROFILER_MAX_RUNS = "100";
|
|
424
|
+
process.env.VELLUM_PROFILER_MIN_FREE_MB = "0";
|
|
425
|
+
|
|
426
|
+
createRun("completed-run", {
|
|
427
|
+
sizeBytes: 2048,
|
|
428
|
+
manifest: {
|
|
429
|
+
status: "completed",
|
|
430
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
createRun("other-run", {
|
|
434
|
+
sizeBytes: 1024,
|
|
435
|
+
manifest: {
|
|
436
|
+
status: "completed",
|
|
437
|
+
createdAt: "2025-02-01T00:00:00Z",
|
|
438
|
+
},
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
const route = findRoute("profiler/runs/:runId", "DELETE")!;
|
|
442
|
+
const response = await route.handler(
|
|
443
|
+
makeCtx({ runId: "completed-run" }, "DELETE"),
|
|
444
|
+
);
|
|
445
|
+
const body = (await response.json()) as {
|
|
446
|
+
deleted: boolean;
|
|
447
|
+
runId: string;
|
|
448
|
+
remainingRuns: number;
|
|
449
|
+
activeRunOverBudget: boolean;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
expect(response.status).toBe(200);
|
|
453
|
+
expect(body.deleted).toBe(true);
|
|
454
|
+
expect(body.runId).toBe("completed-run");
|
|
455
|
+
expect(body.remainingRuns).toBe(1);
|
|
456
|
+
expect(body.activeRunOverBudget).toBe(false);
|
|
457
|
+
|
|
458
|
+
// Run directory should be gone
|
|
459
|
+
expect(existsSync(join(runsDir, "completed-run"))).toBe(false);
|
|
460
|
+
// Other run should still exist
|
|
461
|
+
expect(existsSync(join(runsDir, "other-run"))).toBe(true);
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
test("post-delete budget recalculation reflects freed space", async () => {
|
|
465
|
+
process.env.VELLUM_PROFILER_MAX_BYTES = "5000";
|
|
466
|
+
process.env.VELLUM_PROFILER_MAX_RUNS = "100";
|
|
467
|
+
process.env.VELLUM_PROFILER_MIN_FREE_MB = "0";
|
|
468
|
+
|
|
469
|
+
// Create two runs that together exceed the 5000 byte budget
|
|
470
|
+
createRun("over-budget-a", {
|
|
471
|
+
sizeBytes: 3000,
|
|
472
|
+
manifest: {
|
|
473
|
+
status: "completed",
|
|
474
|
+
createdAt: "2025-01-01T00:00:00Z",
|
|
475
|
+
},
|
|
476
|
+
});
|
|
477
|
+
createRun("over-budget-b", {
|
|
478
|
+
sizeBytes: 3000,
|
|
479
|
+
manifest: {
|
|
480
|
+
status: "completed",
|
|
481
|
+
createdAt: "2025-02-01T00:00:00Z",
|
|
482
|
+
},
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
// Delete one of the runs
|
|
486
|
+
const route = findRoute("profiler/runs/:runId", "DELETE")!;
|
|
487
|
+
const response = await route.handler(
|
|
488
|
+
makeCtx({ runId: "over-budget-a" }, "DELETE"),
|
|
489
|
+
);
|
|
490
|
+
const body = (await response.json()) as {
|
|
491
|
+
deleted: boolean;
|
|
492
|
+
remainingRuns: number;
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
expect(response.status).toBe(200);
|
|
496
|
+
expect(body.deleted).toBe(true);
|
|
497
|
+
// The remaining run should survive since it's within budget now
|
|
498
|
+
expect(body.remainingRuns).toBe(1);
|
|
499
|
+
expect(existsSync(join(runsDir, "over-budget-b"))).toBe(true);
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
});
|