@vellumai/assistant 0.6.0 → 0.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +4 -0
- package/ARCHITECTURE.md +68 -15
- package/Dockerfile +2 -2
- package/bun.lock +6 -2
- package/docker-entrypoint.sh +42 -1
- package/docs/architecture/integrations.md +1 -1
- package/docs/architecture/memory.md +21 -24
- package/node_modules/@vellumai/ces-contracts/src/handles.ts +7 -9
- package/openapi.yaml +539 -4
- 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__/assistant-event-hub.test.ts +30 -0
- package/src/__tests__/checker.test.ts +138 -172
- package/src/__tests__/cli-command-risk-guard.test.ts +1 -1
- package/src/__tests__/config-schema.test.ts +5 -0
- package/src/__tests__/context-overflow-approval.test.ts +5 -5
- 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-analysis-routes.test.ts +169 -0
- package/src/__tests__/conversation-directories-parse.test.ts +105 -0
- 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__/credential-execution-approval-bridge.test.ts +0 -2
- 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__/init-feature-flag-overrides.test.ts +167 -0
- package/src/__tests__/injection-block.test.ts +24 -0
- package/src/__tests__/inline-command-runner.test.ts +7 -5
- 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 +257 -100
- package/src/__tests__/managed-credential-catalog-cli.test.ts +12 -14
- 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__/navigate-settings-tab.test.ts +14 -1
- package/src/__tests__/notification-broadcaster.test.ts +65 -0
- package/src/__tests__/notification-decision-recipient-context.test.ts +282 -0
- package/src/__tests__/onboarding-template-contract.test.ts +63 -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__/pkb-autoinject.test.ts +96 -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__/require-fresh-approval.test.ts +0 -2
- package/src/__tests__/sandbox-diagnostics.test.ts +1 -32
- 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-sandbox.test.ts +1 -1
- package/src/__tests__/terminal-tools.test.ts +16 -29
- package/src/__tests__/test-preload.ts +18 -0
- package/src/__tests__/tool-domain-event-publisher.test.ts +0 -1
- package/src/__tests__/tool-executor-lifecycle-events.test.ts +1 -8
- package/src/__tests__/tool-executor.test.ts +4 -27
- 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__/transport-hints-queue.test.ts +77 -0
- package/src/__tests__/trust-store.test.ts +4 -4
- 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/__tests__/workspace-migration-030-seed-pkb-autoinject.test.ts +168 -0
- package/src/__tests__/workspace-policy.test.ts +2 -7
- package/src/agent/loop.ts +6 -29
- package/src/approvals/guardian-request-resolvers.ts +24 -0
- package/src/avatar/traits-png-sync.ts +3 -3
- package/src/channels/types.ts +5 -0
- package/src/cli/__tests__/run-assistant-command.ts +56 -0
- package/src/cli/__tests__/unknown-command.test.ts +33 -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/default-action.ts +68 -1
- 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/__tests__/connect.test.ts +27 -0
- package/src/cli/commands/oauth/connect.ts +25 -5
- package/src/cli/commands/platform/__tests__/connect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/disconnect.test.ts +1 -1
- package/src/cli/commands/platform/__tests__/status.test.ts +1 -1
- package/src/cli/commands/routes.ts +396 -0
- package/src/cli/commands/skills.ts +130 -20
- package/src/cli/program.ts +11 -2
- package/src/cli.ts +1 -120
- package/src/config/assistant-feature-flags.ts +59 -55
- package/src/config/bundled-skills/app-builder/SKILL.md +91 -5
- package/src/config/bundled-skills/gmail/SKILL.md +13 -8
- package/src/config/bundled-skills/gmail/TOOLS.json +1 -1
- package/src/config/bundled-skills/gmail/tools/gmail-sender-digest.ts +2 -1
- 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.json +1 -1
- 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/settings/tools/navigate-settings-tab.ts +8 -3
- 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/config/schemas/services.ts +8 -0
- package/src/credential-execution/approval-bridge.ts +0 -1
- package/src/credential-execution/managed-catalog.ts +3 -7
- package/src/daemon/app-source-watcher.ts +93 -0
- package/src/daemon/config-watcher.ts +85 -3
- package/src/daemon/context-overflow-approval.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +20 -0
- package/src/daemon/conversation-agent-loop.ts +179 -65
- package/src/daemon/conversation-attachments.ts +0 -1
- package/src/daemon/conversation-history.ts +4 -19
- package/src/daemon/conversation-lifecycle.ts +8 -14
- package/src/daemon/conversation-messaging.ts +3 -0
- package/src/daemon/conversation-process.ts +30 -8
- package/src/daemon/conversation-queue-manager.ts +8 -0
- package/src/daemon/conversation-runtime-assembly.ts +359 -308
- package/src/daemon/conversation-surfaces.ts +65 -0
- package/src/daemon/conversation-tool-setup.ts +44 -17
- package/src/daemon/conversation-workspace.ts +1 -2
- package/src/daemon/conversation.ts +19 -3
- package/src/daemon/date-context.ts +26 -53
- package/src/daemon/first-greeting.ts +1 -1
- package/src/daemon/handlers/conversations.ts +5 -7
- package/src/daemon/handlers/shared.test.ts +143 -0
- package/src/daemon/handlers/shared.ts +70 -5
- package/src/daemon/handlers/skills.ts +11 -18
- package/src/daemon/lifecycle.ts +220 -158
- package/src/daemon/message-types/conversations.ts +29 -6
- package/src/daemon/message-types/messages.ts +9 -2
- package/src/daemon/message-types/notifications.ts +12 -0
- package/src/daemon/message-types/schedules.ts +1 -0
- package/src/daemon/message-types/settings.ts +18 -0
- package/src/daemon/profiler-run-store.ts +557 -0
- package/src/daemon/server.ts +87 -10
- package/src/daemon/shutdown-handlers.ts +5 -0
- package/src/daemon/tool-side-effects.ts +23 -3
- package/src/daemon/transport-hints.ts +33 -0
- 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/index.ts +1 -1
- 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 +151 -117
- package/src/memory/conversation-directories.ts +39 -0
- package/src/memory/conversation-group-migration.ts +66 -6
- 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/embedding-local.ts +1 -1
- package/src/memory/graph/bootstrap.ts +75 -66
- package/src/memory/graph/capability-seed.ts +167 -17
- 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/group-crud.ts +25 -9
- 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/messaging/provider.ts +1 -1
- package/src/notifications/broadcaster.ts +6 -0
- package/src/notifications/conversation-pairing.ts +12 -4
- package/src/notifications/copy-composer.ts +86 -0
- package/src/notifications/decision-engine.ts +35 -0
- package/src/notifications/emit-signal.ts +14 -0
- package/src/notifications/signal.ts +11 -0
- package/src/oauth/platform-connection.test.ts +2 -2
- package/src/oauth/seed-providers.ts +1 -0
- package/src/permissions/checker.ts +15 -4
- package/src/permissions/defaults.ts +7 -8
- package/src/permissions/permission-mode-store.ts +180 -0
- package/src/permissions/permission-mode.ts +31 -0
- package/src/permissions/prompter.ts +0 -2
- package/src/permissions/workspace-policy.ts +9 -0
- package/src/platform/client.ts +1 -1
- package/src/prompts/system-prompt.ts +59 -7
- package/src/prompts/templates/BOOTSTRAP-REFERENCE.md +100 -0
- package/src/prompts/templates/BOOTSTRAP.md +76 -162
- package/src/prompts/templates/HEARTBEAT.md +3 -1
- package/src/prompts/templates/SOUL.md +30 -9
- package/src/prompts/templates/UPDATES.md +8 -0
- package/src/providers/anthropic/client.ts +107 -219
- package/src/runtime/assistant-event-hub.ts +22 -0
- package/src/runtime/auth/route-policy.ts +23 -0
- package/src/runtime/auth/token-service.ts +8 -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 +185 -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 +270 -44
- package/src/runtime/routes/group-routes.ts +22 -8
- 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/AGENTS.md +104 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist-error-contract.test.ts +103 -0
- package/src/runtime/routes/log-export/__tests__/workspace-allowlist.test.ts +716 -0
- package/src/runtime/routes/log-export/workspace-allowlist.ts +458 -0
- package/src/runtime/routes/log-export-routes.ts +41 -278
- 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/inline-command-runner.ts +12 -14
- 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 -18
- 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/secret-detection-handler.ts +0 -1
- 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/skills/sandbox-runner.ts +3 -6
- 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/terminal/sandbox-diagnostics.ts +4 -4
- package/src/tools/terminal/sandbox.ts +4 -1
- package/src/tools/terminal/shell.ts +3 -5
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +2 -3
- package/src/util/logger.ts +1 -1
- package/src/util/platform.ts +50 -17
- package/src/watcher/provider-types.ts +1 -1
- 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 +85 -0
- package/src/workspace/migrations/030-seed-pkb-autoinject.ts +73 -0
- package/src/workspace/migrations/registry.ts +6 -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
|
@@ -301,6 +301,168 @@ describe("getRequestLogsByMessageId — turn-aware query", () => {
|
|
|
301
301
|
expect(logs[2]?.id).toBe("log-surviving");
|
|
302
302
|
});
|
|
303
303
|
|
|
304
|
+
test("recovers unlinked logs (messageId IS NULL) within the turn time range", () => {
|
|
305
|
+
// Simulate the race: logs recorded with NULL messageId, backfill hasn't run yet.
|
|
306
|
+
const T = 1_700_000_000_000;
|
|
307
|
+
const db = getDb();
|
|
308
|
+
const conv = createConversation("unlinked-test");
|
|
309
|
+
|
|
310
|
+
db.run(
|
|
311
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-ul', ${conv.id}, 'user', '"Do the task"', ${T})`,
|
|
312
|
+
);
|
|
313
|
+
db.run(
|
|
314
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-ul', ${conv.id}, 'assistant', '"Done!"', ${T + 30000})`,
|
|
315
|
+
);
|
|
316
|
+
|
|
317
|
+
// Unlinked log: messageId is NULL (backfill hasn't run yet)
|
|
318
|
+
db.insert(llmRequestLogs)
|
|
319
|
+
.values({
|
|
320
|
+
id: "log-unlinked-1",
|
|
321
|
+
conversationId: conv.id,
|
|
322
|
+
messageId: null,
|
|
323
|
+
provider: "anthropic",
|
|
324
|
+
requestPayload: '{"step":1}',
|
|
325
|
+
responsePayload: '{"tool":"bash"}',
|
|
326
|
+
createdAt: T + 5000,
|
|
327
|
+
})
|
|
328
|
+
.run();
|
|
329
|
+
|
|
330
|
+
// Linked log: already backfilled to the assistant message
|
|
331
|
+
db.insert(llmRequestLogs)
|
|
332
|
+
.values({
|
|
333
|
+
id: "log-linked-1",
|
|
334
|
+
conversationId: conv.id,
|
|
335
|
+
messageId: "a1-ul",
|
|
336
|
+
provider: "anthropic",
|
|
337
|
+
requestPayload: '{"step":2}',
|
|
338
|
+
responsePayload: '{"text":"Done!"}',
|
|
339
|
+
createdAt: T + 29_000,
|
|
340
|
+
})
|
|
341
|
+
.run();
|
|
342
|
+
|
|
343
|
+
const logs = getRequestLogsByMessageId("a1-ul");
|
|
344
|
+
expect(logs).toHaveLength(2);
|
|
345
|
+
expect(logs[0]?.id).toBe("log-unlinked-1");
|
|
346
|
+
expect(logs[1]?.id).toBe("log-linked-1");
|
|
347
|
+
|
|
348
|
+
// Verify opportunistic backfill ran: the unlinked log should now have a messageId
|
|
349
|
+
const backfilledLog = db
|
|
350
|
+
.select({ messageId: llmRequestLogs.messageId })
|
|
351
|
+
.from(llmRequestLogs)
|
|
352
|
+
.where(sql`${llmRequestLogs.id} = 'log-unlinked-1'`)
|
|
353
|
+
.get();
|
|
354
|
+
expect(backfilledLog?.messageId).toBe("a1-ul");
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
test("unlinked logs from different conversations don't bleed", () => {
|
|
358
|
+
const T = 1_700_000_000_000;
|
|
359
|
+
const db = getDb();
|
|
360
|
+
const convA = createConversation("conv-a");
|
|
361
|
+
const convB = createConversation("conv-b");
|
|
362
|
+
|
|
363
|
+
// Conversation A: user + assistant + unlinked log
|
|
364
|
+
db.run(
|
|
365
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-a', ${convA.id}, 'user', '"Hello A"', ${T})`,
|
|
366
|
+
);
|
|
367
|
+
db.run(
|
|
368
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-a', ${convA.id}, 'assistant', '"Hi A"', ${T + 10000})`,
|
|
369
|
+
);
|
|
370
|
+
db.insert(llmRequestLogs)
|
|
371
|
+
.values({
|
|
372
|
+
id: "log-conv-a",
|
|
373
|
+
conversationId: convA.id,
|
|
374
|
+
messageId: null,
|
|
375
|
+
provider: "anthropic",
|
|
376
|
+
requestPayload: '{"conv":"A"}',
|
|
377
|
+
responsePayload: '{"r":"A"}',
|
|
378
|
+
createdAt: T + 5000,
|
|
379
|
+
})
|
|
380
|
+
.run();
|
|
381
|
+
|
|
382
|
+
// Conversation B: user + assistant + unlinked log (overlapping timestamps)
|
|
383
|
+
db.run(
|
|
384
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-b', ${convB.id}, 'user', '"Hello B"', ${T})`,
|
|
385
|
+
);
|
|
386
|
+
db.run(
|
|
387
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-b', ${convB.id}, 'assistant', '"Hi B"', ${T + 10000})`,
|
|
388
|
+
);
|
|
389
|
+
db.insert(llmRequestLogs)
|
|
390
|
+
.values({
|
|
391
|
+
id: "log-conv-b",
|
|
392
|
+
conversationId: convB.id,
|
|
393
|
+
messageId: null,
|
|
394
|
+
provider: "anthropic",
|
|
395
|
+
requestPayload: '{"conv":"B"}',
|
|
396
|
+
responsePayload: '{"r":"B"}',
|
|
397
|
+
createdAt: T + 5000,
|
|
398
|
+
})
|
|
399
|
+
.run();
|
|
400
|
+
|
|
401
|
+
// Query from conv A → should only find conv A's log
|
|
402
|
+
const logsA = getRequestLogsByMessageId("a1-a");
|
|
403
|
+
expect(logsA).toHaveLength(1);
|
|
404
|
+
expect(logsA[0]?.id).toBe("log-conv-a");
|
|
405
|
+
|
|
406
|
+
// Query from conv B → should only find conv B's log
|
|
407
|
+
const logsB = getRequestLogsByMessageId("a1-b");
|
|
408
|
+
expect(logsB).toHaveLength(1);
|
|
409
|
+
expect(logsB[0]?.id).toBe("log-conv-b");
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("unlinked logs from different turns don't bleed", () => {
|
|
413
|
+
const T = 1_700_000_000_000;
|
|
414
|
+
const db = getDb();
|
|
415
|
+
const conv = createConversation("two-turn-unlinked");
|
|
416
|
+
|
|
417
|
+
// Turn 1: user → assistant (with linked log)
|
|
418
|
+
db.run(
|
|
419
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u1-t', ${conv.id}, 'user', '"Turn 1"', ${T})`,
|
|
420
|
+
);
|
|
421
|
+
db.run(
|
|
422
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a1-t', ${conv.id}, 'assistant', '"Answer 1"', ${T + 10000})`,
|
|
423
|
+
);
|
|
424
|
+
db.insert(llmRequestLogs)
|
|
425
|
+
.values({
|
|
426
|
+
id: "log-turn1-unlinked",
|
|
427
|
+
conversationId: conv.id,
|
|
428
|
+
messageId: null,
|
|
429
|
+
provider: "anthropic",
|
|
430
|
+
requestPayload: '{"turn":1}',
|
|
431
|
+
responsePayload: '{"r":1}',
|
|
432
|
+
createdAt: T + 5000,
|
|
433
|
+
})
|
|
434
|
+
.run();
|
|
435
|
+
|
|
436
|
+
// Turn 2: user → assistant (with unlinked log)
|
|
437
|
+
db.run(
|
|
438
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('u2-t', ${conv.id}, 'user', '"Turn 2"', ${T + 60000})`,
|
|
439
|
+
);
|
|
440
|
+
db.run(
|
|
441
|
+
sql`INSERT INTO messages (id, conversation_id, role, content, created_at) VALUES ('a2-t', ${conv.id}, 'assistant', '"Answer 2"', ${T + 70000})`,
|
|
442
|
+
);
|
|
443
|
+
db.insert(llmRequestLogs)
|
|
444
|
+
.values({
|
|
445
|
+
id: "log-turn2-unlinked",
|
|
446
|
+
conversationId: conv.id,
|
|
447
|
+
messageId: null,
|
|
448
|
+
provider: "anthropic",
|
|
449
|
+
requestPayload: '{"turn":2}',
|
|
450
|
+
responsePayload: '{"r":2}',
|
|
451
|
+
createdAt: T + 65000,
|
|
452
|
+
})
|
|
453
|
+
.run();
|
|
454
|
+
|
|
455
|
+
// Query turn 2 → should only find turn 2's unlinked log
|
|
456
|
+
const turn2Logs = getRequestLogsByMessageId("a2-t");
|
|
457
|
+
expect(turn2Logs).toHaveLength(1);
|
|
458
|
+
expect(turn2Logs[0]?.id).toBe("log-turn2-unlinked");
|
|
459
|
+
|
|
460
|
+
// Query turn 1 → should only find turn 1's unlinked log
|
|
461
|
+
const turn1Logs = getRequestLogsByMessageId("a1-t");
|
|
462
|
+
expect(turn1Logs).toHaveLength(1);
|
|
463
|
+
expect(turn1Logs[0]?.id).toBe("log-turn1-unlinked");
|
|
464
|
+
});
|
|
465
|
+
|
|
304
466
|
test("relinkLlmRequestLogs moves logs from deleted messages to consolidated message", async () => {
|
|
305
467
|
const conv = createConversation("relink-test");
|
|
306
468
|
|
|
@@ -5,8 +5,6 @@
|
|
|
5
5
|
* - audit-data.json with tool invocation records
|
|
6
6
|
* - daemon-logs/ with log file contents
|
|
7
7
|
* - config-snapshot.json with sanitized config
|
|
8
|
-
* - workspace/ with text files, SQL dumps for .db files, and proper
|
|
9
|
-
* filtering (excluded directories, binary files, symlinks).
|
|
10
8
|
*/
|
|
11
9
|
|
|
12
10
|
import { spawnSync } from "node:child_process";
|
|
@@ -16,7 +14,6 @@ import {
|
|
|
16
14
|
readdirSync,
|
|
17
15
|
readFileSync,
|
|
18
16
|
rmSync,
|
|
19
|
-
symlinkSync,
|
|
20
17
|
writeFileSync,
|
|
21
18
|
} from "node:fs";
|
|
22
19
|
import { tmpdir } from "node:os";
|
|
@@ -24,8 +21,7 @@ import { join } from "node:path";
|
|
|
24
21
|
import { describe, expect, mock, test } from "bun:test";
|
|
25
22
|
|
|
26
23
|
// Set up temp directories before mocking
|
|
27
|
-
const
|
|
28
|
-
const testWorkspaceDir = testDir;
|
|
24
|
+
const testWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR!;
|
|
29
25
|
mkdirSync(testWorkspaceDir, { recursive: true });
|
|
30
26
|
|
|
31
27
|
mock.module("../util/logger.js", () => ({
|
|
@@ -52,11 +48,13 @@ initializeDb();
|
|
|
52
48
|
const routes = logExportRouteDefinitions();
|
|
53
49
|
const exportRoute = routes.find((r) => r.endpoint === "export")!;
|
|
54
50
|
|
|
55
|
-
async function callExport(
|
|
51
|
+
async function callExport(
|
|
52
|
+
body: Record<string, unknown> = {},
|
|
53
|
+
): Promise<Response> {
|
|
56
54
|
const req = new Request("http://localhost/v1/export", {
|
|
57
55
|
method: "POST",
|
|
58
56
|
headers: { "Content-Type": "application/json" },
|
|
59
|
-
body: JSON.stringify(
|
|
57
|
+
body: JSON.stringify(body),
|
|
60
58
|
});
|
|
61
59
|
const url = new URL(req.url);
|
|
62
60
|
return exportRoute.handler({
|
|
@@ -85,73 +83,66 @@ async function extractArchive(res: Response): Promise<string> {
|
|
|
85
83
|
return extractDir;
|
|
86
84
|
}
|
|
87
85
|
|
|
88
|
-
/** Recursively lists all files under a directory as relative paths. */
|
|
89
|
-
function listFiles(dir: string, base = dir): string[] {
|
|
90
|
-
const result: string[] = [];
|
|
91
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
92
|
-
const full = join(dir, entry.name);
|
|
93
|
-
if (entry.isDirectory()) {
|
|
94
|
-
result.push(...listFiles(full, base));
|
|
95
|
-
} else {
|
|
96
|
-
result.push(full.slice(base.length + 1));
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
return result;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
86
|
// ---------------------------------------------------------------------------
|
|
103
|
-
// Seed
|
|
87
|
+
// Seed test data
|
|
104
88
|
// ---------------------------------------------------------------------------
|
|
105
89
|
|
|
106
|
-
//
|
|
107
|
-
writeFileSync(join(testWorkspaceDir, "IDENTITY.md"), "# My Identity\nHello");
|
|
108
|
-
mkdirSync(join(testWorkspaceDir, "notes"), { recursive: true });
|
|
109
|
-
writeFileSync(join(testWorkspaceDir, "notes", "daily.txt"), "Some daily notes");
|
|
110
|
-
|
|
111
|
-
// SQLite DB file — should be dumped as .sql
|
|
112
|
-
mkdirSync(join(testWorkspaceDir, "data", "db"), { recursive: true });
|
|
113
|
-
// Create a real sqlite db with a table
|
|
114
|
-
import { Database } from "bun:sqlite";
|
|
115
|
-
const wsDbPath = join(testWorkspaceDir, "data", "db", "assistant.db");
|
|
116
|
-
const wsDb = new Database(wsDbPath);
|
|
117
|
-
wsDb.run("CREATE TABLE test_table (id INTEGER PRIMARY KEY, name TEXT)");
|
|
118
|
-
wsDb.run("INSERT INTO test_table (name) VALUES ('hello')");
|
|
119
|
-
wsDb.close();
|
|
120
|
-
|
|
121
|
-
// Excluded directory: embedding-models/
|
|
122
|
-
mkdirSync(join(testWorkspaceDir, "embedding-models"), { recursive: true });
|
|
90
|
+
// config.json at workspace root — needed for config-snapshot test
|
|
123
91
|
writeFileSync(
|
|
124
|
-
join(testWorkspaceDir, "
|
|
125
|
-
|
|
92
|
+
join(testWorkspaceDir, "config.json"),
|
|
93
|
+
JSON.stringify({ provider: "anthropic" }),
|
|
126
94
|
);
|
|
127
95
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
96
|
+
// Conversation directories — used for workspace allowlist tests
|
|
97
|
+
const conversationsDir = join(testWorkspaceDir, "conversations");
|
|
98
|
+
mkdirSync(conversationsDir, { recursive: true });
|
|
99
|
+
|
|
100
|
+
function seedConversation(name: string, body: string) {
|
|
101
|
+
const dir = join(conversationsDir, name);
|
|
102
|
+
mkdirSync(dir, { recursive: true });
|
|
103
|
+
writeFileSync(join(dir, "meta.json"), "{}\n");
|
|
104
|
+
writeFileSync(join(dir, "messages.jsonl"), body);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
seedConversation(
|
|
108
|
+
"2025-01-10T00-00-00.000Z_conv-jan10",
|
|
109
|
+
'{"role":"user","content":"jan 10"}\n',
|
|
110
|
+
);
|
|
111
|
+
seedConversation(
|
|
112
|
+
"2025-01-15T00-00-00.000Z_conv-jan15",
|
|
113
|
+
'{"role":"user","content":"jan 15"}\n',
|
|
133
114
|
);
|
|
115
|
+
seedConversation(
|
|
116
|
+
"2025-01-20T00-00-00.000Z_conv-jan20",
|
|
117
|
+
'{"role":"user","content":"jan 20"}\n',
|
|
118
|
+
);
|
|
119
|
+
seedConversation(
|
|
120
|
+
"2025-01-25T00-00-00.000Z_conv-jan25",
|
|
121
|
+
'{"role":"user","content":"jan 25"}\n',
|
|
122
|
+
);
|
|
123
|
+
seedConversation("malformed-name", '{"role":"user","content":"x"}\n');
|
|
134
124
|
|
|
135
|
-
//
|
|
125
|
+
// Daemon log files — used for date filtering tests
|
|
126
|
+
const logsDir = join(testWorkspaceDir, "data", "logs");
|
|
127
|
+
mkdirSync(logsDir, { recursive: true });
|
|
136
128
|
writeFileSync(
|
|
137
|
-
join(
|
|
138
|
-
|
|
129
|
+
join(logsDir, "assistant-2025-01-10.log"),
|
|
130
|
+
"log entry from Jan 10\n",
|
|
139
131
|
);
|
|
140
|
-
|
|
141
|
-
// config.json at workspace root — should be skipped (already in configSnapshot)
|
|
142
132
|
writeFileSync(
|
|
143
|
-
join(
|
|
144
|
-
|
|
133
|
+
join(logsDir, "assistant-2025-01-15.log"),
|
|
134
|
+
"log entry from Jan 15\n",
|
|
145
135
|
);
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
136
|
+
writeFileSync(
|
|
137
|
+
join(logsDir, "assistant-2025-01-20.log"),
|
|
138
|
+
"log entry from Jan 20\n",
|
|
139
|
+
);
|
|
140
|
+
writeFileSync(
|
|
141
|
+
join(logsDir, "assistant-2025-01-25.log"),
|
|
142
|
+
"log entry from Jan 25\n",
|
|
143
|
+
);
|
|
144
|
+
// Non-dated log file — should always be included regardless of time filter
|
|
145
|
+
writeFileSync(join(logsDir, "vellum.log"), "non-dated log content\n");
|
|
155
146
|
|
|
156
147
|
// ---------------------------------------------------------------------------
|
|
157
148
|
// Tests
|
|
@@ -185,90 +176,256 @@ describe("POST /v1/export — tar.gz archive", () => {
|
|
|
185
176
|
}
|
|
186
177
|
});
|
|
187
178
|
|
|
188
|
-
test("archive contains
|
|
179
|
+
test("archive contains config-snapshot.json when config exists", async () => {
|
|
189
180
|
const res = await callExport();
|
|
190
181
|
const dir = await extractArchive(res);
|
|
191
182
|
try {
|
|
192
|
-
const
|
|
193
|
-
join(dir, "
|
|
183
|
+
const configContent = readFileSync(
|
|
184
|
+
join(dir, "config-snapshot.json"),
|
|
194
185
|
"utf-8",
|
|
195
186
|
);
|
|
196
|
-
|
|
187
|
+
const parsed = JSON.parse(configContent);
|
|
188
|
+
expect(parsed.provider).toBe("anthropic");
|
|
189
|
+
} finally {
|
|
190
|
+
rmSync(dir, { recursive: true, force: true });
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
});
|
|
197
194
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
195
|
+
describe("POST /v1/export — daemon log date filtering", () => {
|
|
196
|
+
test("excludes log files before startTime", async () => {
|
|
197
|
+
// startTime = Jan 14 — should exclude assistant-2025-01-10.log
|
|
198
|
+
const startTime = new Date("2025-01-14T00:00:00.000Z").getTime();
|
|
199
|
+
const res = await callExport({ startTime });
|
|
200
|
+
const dir = await extractArchive(res);
|
|
201
|
+
try {
|
|
202
|
+
const logFiles = readdirSync(join(dir, "daemon-logs"));
|
|
203
|
+
expect(logFiles).not.toContain("assistant-2025-01-10.log");
|
|
204
|
+
expect(logFiles).toContain("assistant-2025-01-15.log");
|
|
205
|
+
expect(logFiles).toContain("assistant-2025-01-20.log");
|
|
206
|
+
expect(logFiles).toContain("assistant-2025-01-25.log");
|
|
203
207
|
} finally {
|
|
204
208
|
rmSync(dir, { recursive: true, force: true });
|
|
205
209
|
}
|
|
206
210
|
});
|
|
207
211
|
|
|
208
|
-
test("
|
|
209
|
-
|
|
212
|
+
test("excludes log files after endTime", async () => {
|
|
213
|
+
// endTime = Jan 22 — should exclude assistant-2025-01-25.log
|
|
214
|
+
const endTime = new Date("2025-01-22T00:00:00.000Z").getTime();
|
|
215
|
+
const res = await callExport({ endTime });
|
|
210
216
|
const dir = await extractArchive(res);
|
|
211
217
|
try {
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
);
|
|
216
|
-
expect(
|
|
217
|
-
expect(sqlContent).toContain("test_table");
|
|
218
|
+
const logFiles = readdirSync(join(dir, "daemon-logs"));
|
|
219
|
+
expect(logFiles).toContain("assistant-2025-01-10.log");
|
|
220
|
+
expect(logFiles).toContain("assistant-2025-01-15.log");
|
|
221
|
+
expect(logFiles).toContain("assistant-2025-01-20.log");
|
|
222
|
+
expect(logFiles).not.toContain("assistant-2025-01-25.log");
|
|
218
223
|
} finally {
|
|
219
224
|
rmSync(dir, { recursive: true, force: true });
|
|
220
225
|
}
|
|
221
226
|
});
|
|
222
227
|
|
|
223
|
-
test("
|
|
228
|
+
test("filters log files by both startTime and endTime", async () => {
|
|
229
|
+
// startTime = Jan 14, endTime = Jan 22 — should only include Jan 15 and Jan 20
|
|
230
|
+
const startTime = new Date("2025-01-14T00:00:00.000Z").getTime();
|
|
231
|
+
const endTime = new Date("2025-01-22T00:00:00.000Z").getTime();
|
|
232
|
+
const res = await callExport({ startTime, endTime });
|
|
233
|
+
const dir = await extractArchive(res);
|
|
234
|
+
try {
|
|
235
|
+
const logFiles = readdirSync(join(dir, "daemon-logs"));
|
|
236
|
+
expect(logFiles).not.toContain("assistant-2025-01-10.log");
|
|
237
|
+
expect(logFiles).toContain("assistant-2025-01-15.log");
|
|
238
|
+
expect(logFiles).toContain("assistant-2025-01-20.log");
|
|
239
|
+
expect(logFiles).not.toContain("assistant-2025-01-25.log");
|
|
240
|
+
} finally {
|
|
241
|
+
rmSync(dir, { recursive: true, force: true });
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("always includes non-dated log files regardless of time filter", async () => {
|
|
246
|
+
const startTime = new Date("2025-01-14T00:00:00.000Z").getTime();
|
|
247
|
+
const endTime = new Date("2025-01-22T00:00:00.000Z").getTime();
|
|
248
|
+
const res = await callExport({ startTime, endTime });
|
|
249
|
+
const dir = await extractArchive(res);
|
|
250
|
+
try {
|
|
251
|
+
const logFiles = readdirSync(join(dir, "daemon-logs"));
|
|
252
|
+
expect(logFiles).toContain("vellum.log");
|
|
253
|
+
} finally {
|
|
254
|
+
rmSync(dir, { recursive: true, force: true });
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("includes all log files when no time filter is specified", async () => {
|
|
224
259
|
const res = await callExport();
|
|
225
260
|
const dir = await extractArchive(res);
|
|
226
261
|
try {
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
);
|
|
231
|
-
|
|
232
|
-
expect(
|
|
233
|
-
expect(qdrantFiles).toHaveLength(0);
|
|
262
|
+
const logFiles = readdirSync(join(dir, "daemon-logs"));
|
|
263
|
+
expect(logFiles).toContain("assistant-2025-01-10.log");
|
|
264
|
+
expect(logFiles).toContain("assistant-2025-01-15.log");
|
|
265
|
+
expect(logFiles).toContain("assistant-2025-01-20.log");
|
|
266
|
+
expect(logFiles).toContain("assistant-2025-01-25.log");
|
|
267
|
+
expect(logFiles).toContain("vellum.log");
|
|
234
268
|
} finally {
|
|
235
269
|
rmSync(dir, { recursive: true, force: true });
|
|
236
270
|
}
|
|
237
271
|
});
|
|
272
|
+
});
|
|
238
273
|
|
|
239
|
-
|
|
274
|
+
describe("POST /v1/export — workspace allowlist", () => {
|
|
275
|
+
test("includes all valid conversation dirs by default", async () => {
|
|
240
276
|
const res = await callExport();
|
|
241
277
|
const dir = await extractArchive(res);
|
|
242
278
|
try {
|
|
243
|
-
const
|
|
244
|
-
expect(
|
|
245
|
-
expect(
|
|
279
|
+
const convs = readdirSync(join(dir, "workspace", "conversations"));
|
|
280
|
+
expect(convs).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
|
|
281
|
+
expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
|
|
282
|
+
expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
|
|
283
|
+
expect(convs).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
|
|
284
|
+
expect(convs).not.toContain("malformed-name");
|
|
246
285
|
} finally {
|
|
247
286
|
rmSync(dir, { recursive: true, force: true });
|
|
248
287
|
}
|
|
249
288
|
});
|
|
250
289
|
|
|
251
|
-
test("
|
|
290
|
+
test("skips malformed conversation dir names", async () => {
|
|
252
291
|
const res = await callExport();
|
|
253
292
|
const dir = await extractArchive(res);
|
|
254
293
|
try {
|
|
255
|
-
const
|
|
256
|
-
expect(
|
|
294
|
+
const convs = readdirSync(join(dir, "workspace", "conversations"));
|
|
295
|
+
expect(convs).not.toContain("malformed-name");
|
|
257
296
|
} finally {
|
|
258
297
|
rmSync(dir, { recursive: true, force: true });
|
|
259
298
|
}
|
|
260
299
|
});
|
|
261
300
|
|
|
262
|
-
test("
|
|
301
|
+
test("filters conversation dirs by startTime", async () => {
|
|
302
|
+
const startTime = Date.parse("2025-01-14T00:00:00Z");
|
|
303
|
+
const res = await callExport({ startTime });
|
|
304
|
+
const dir = await extractArchive(res);
|
|
305
|
+
try {
|
|
306
|
+
const convs = readdirSync(join(dir, "workspace", "conversations"));
|
|
307
|
+
expect(convs).not.toContain("2025-01-10T00-00-00.000Z_conv-jan10");
|
|
308
|
+
expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
|
|
309
|
+
expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
|
|
310
|
+
expect(convs).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
|
|
311
|
+
} finally {
|
|
312
|
+
rmSync(dir, { recursive: true, force: true });
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
test("filters conversation dirs by endTime", async () => {
|
|
317
|
+
const endTime = Date.parse("2025-01-22T00:00:00Z");
|
|
318
|
+
const res = await callExport({ endTime });
|
|
319
|
+
const dir = await extractArchive(res);
|
|
320
|
+
try {
|
|
321
|
+
const convs = readdirSync(join(dir, "workspace", "conversations"));
|
|
322
|
+
expect(convs).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
|
|
323
|
+
expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
|
|
324
|
+
expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
|
|
325
|
+
expect(convs).not.toContain("2025-01-25T00-00-00.000Z_conv-jan25");
|
|
326
|
+
} finally {
|
|
327
|
+
rmSync(dir, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("filters conversation dirs by both startTime and endTime", async () => {
|
|
332
|
+
const startTime = Date.parse("2025-01-14T00:00:00Z");
|
|
333
|
+
const endTime = Date.parse("2025-01-22T00:00:00Z");
|
|
334
|
+
const res = await callExport({ startTime, endTime });
|
|
335
|
+
const dir = await extractArchive(res);
|
|
336
|
+
try {
|
|
337
|
+
const convs = readdirSync(join(dir, "workspace", "conversations"));
|
|
338
|
+
expect(convs).not.toContain("2025-01-10T00-00-00.000Z_conv-jan10");
|
|
339
|
+
expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
|
|
340
|
+
expect(convs).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
|
|
341
|
+
expect(convs).not.toContain("2025-01-25T00-00-00.000Z_conv-jan25");
|
|
342
|
+
} finally {
|
|
343
|
+
rmSync(dir, { recursive: true, force: true });
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
test("filters conversation dirs by conversationId", async () => {
|
|
348
|
+
const res = await callExport({ conversationId: "conv-jan15" });
|
|
349
|
+
const dir = await extractArchive(res);
|
|
350
|
+
try {
|
|
351
|
+
const convs = readdirSync(join(dir, "workspace", "conversations"));
|
|
352
|
+
expect(convs).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
|
|
353
|
+
expect(convs).not.toContain("2025-01-10T00-00-00.000Z_conv-jan10");
|
|
354
|
+
expect(convs).not.toContain("2025-01-20T00-00-00.000Z_conv-jan20");
|
|
355
|
+
expect(convs).not.toContain("2025-01-25T00-00-00.000Z_conv-jan25");
|
|
356
|
+
expect(convs).not.toContain("malformed-name");
|
|
357
|
+
} finally {
|
|
358
|
+
rmSync(dir, { recursive: true, force: true });
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("conversationId + time filter intersect", async () => {
|
|
363
|
+
const res = await callExport({
|
|
364
|
+
conversationId: "conv-jan15",
|
|
365
|
+
startTime: Date.parse("2025-02-01T00:00:00Z"),
|
|
366
|
+
});
|
|
367
|
+
const dir = await extractArchive(res);
|
|
368
|
+
try {
|
|
369
|
+
const conversationsPath = join(dir, "workspace", "conversations");
|
|
370
|
+
let convs: string[] = [];
|
|
371
|
+
try {
|
|
372
|
+
convs = readdirSync(conversationsPath);
|
|
373
|
+
} catch {
|
|
374
|
+
// Directory does not exist — acceptable per the test contract.
|
|
375
|
+
}
|
|
376
|
+
expect(convs).toEqual([]);
|
|
377
|
+
} finally {
|
|
378
|
+
rmSync(dir, { recursive: true, force: true });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("conversation dir contents survive the round trip", async () => {
|
|
263
383
|
const res = await callExport();
|
|
264
384
|
const dir = await extractArchive(res);
|
|
265
385
|
try {
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
"
|
|
386
|
+
const messagesPath = join(
|
|
387
|
+
dir,
|
|
388
|
+
"workspace",
|
|
389
|
+
"conversations",
|
|
390
|
+
"2025-01-15T00-00-00.000Z_conv-jan15",
|
|
391
|
+
"messages.jsonl",
|
|
269
392
|
);
|
|
270
|
-
const
|
|
271
|
-
expect(
|
|
393
|
+
const content = readFileSync(messagesPath, "utf-8");
|
|
394
|
+
expect(content).toBe('{"role":"user","content":"jan 15"}\n');
|
|
395
|
+
} finally {
|
|
396
|
+
rmSync(dir, { recursive: true, force: true });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
test("treats empty-string conversationId as no filter", async () => {
|
|
401
|
+
const res = await callExport({ conversationId: "" });
|
|
402
|
+
const dir = await extractArchive(res);
|
|
403
|
+
try {
|
|
404
|
+
// With conversationId === "" (which the rest of handleExport treats as
|
|
405
|
+
// unfiltered), workspace conversations should also be unfiltered. All
|
|
406
|
+
// four canonical conversation dirs should be present.
|
|
407
|
+
const conversationsDir = join(dir, "workspace", "conversations");
|
|
408
|
+
const entries = readdirSync(conversationsDir);
|
|
409
|
+
expect(entries).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
|
|
410
|
+
expect(entries).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
|
|
411
|
+
expect(entries).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
|
|
412
|
+
expect(entries).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
|
|
413
|
+
} finally {
|
|
414
|
+
rmSync(dir, { recursive: true, force: true });
|
|
415
|
+
}
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("treats startTime=0 and endTime=0 as no filter", async () => {
|
|
419
|
+
const res = await callExport({ startTime: 0, endTime: 0 });
|
|
420
|
+
const dir = await extractArchive(res);
|
|
421
|
+
try {
|
|
422
|
+
const conversationsDir = join(dir, "workspace", "conversations");
|
|
423
|
+
const entries = readdirSync(conversationsDir);
|
|
424
|
+
// All four canonical conversation dirs should be present (no filtering).
|
|
425
|
+
expect(entries).toContain("2025-01-10T00-00-00.000Z_conv-jan10");
|
|
426
|
+
expect(entries).toContain("2025-01-15T00-00-00.000Z_conv-jan15");
|
|
427
|
+
expect(entries).toContain("2025-01-20T00-00-00.000Z_conv-jan20");
|
|
428
|
+
expect(entries).toContain("2025-01-25T00-00-00.000Z_conv-jan25");
|
|
272
429
|
} finally {
|
|
273
430
|
rmSync(dir, { recursive: true, force: true });
|
|
274
431
|
}
|