@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
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the trusted contact lifecycle fallback copy templates.
|
|
3
|
+
*
|
|
4
|
+
* Verifies that `ingress.trusted_contact.guardian_decision` and
|
|
5
|
+
* `ingress.trusted_contact.denied` templates in copy-composer.ts render
|
|
6
|
+
* display names when available and fall back to Slack <@ID> mention format
|
|
7
|
+
* for raw user IDs on the Slack source channel.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, test } from "bun:test";
|
|
10
|
+
|
|
11
|
+
import { composeFallbackCopy } from "../notifications/copy-composer.js";
|
|
12
|
+
import type { NotificationSignal } from "../notifications/signal.js";
|
|
13
|
+
|
|
14
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
function buildGuardianDecisionSignal(
|
|
17
|
+
payloadOverrides: Record<string, unknown> = {},
|
|
18
|
+
sourceChannel: "slack" | "telegram" | "vellum" = "slack",
|
|
19
|
+
): NotificationSignal {
|
|
20
|
+
return {
|
|
21
|
+
signalId: "test-signal-gd",
|
|
22
|
+
createdAt: Date.now(),
|
|
23
|
+
sourceChannel,
|
|
24
|
+
sourceContextId: "test-ctx-1",
|
|
25
|
+
sourceEventName: "ingress.trusted_contact.guardian_decision",
|
|
26
|
+
contextPayload: {
|
|
27
|
+
sourceChannel,
|
|
28
|
+
requesterExternalUserId: "U07CLDQ4TB3",
|
|
29
|
+
requesterChatId: "D0AQ9C5PPPF",
|
|
30
|
+
decidedByExternalUserId: "U099H19C0KA",
|
|
31
|
+
requesterDisplayName: null,
|
|
32
|
+
decidedByDisplayName: null,
|
|
33
|
+
decision: "denied",
|
|
34
|
+
...payloadOverrides,
|
|
35
|
+
},
|
|
36
|
+
attentionHints: {
|
|
37
|
+
requiresAction: false,
|
|
38
|
+
urgency: "medium",
|
|
39
|
+
isAsyncBackground: false,
|
|
40
|
+
visibleInSourceNow: false,
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildDeniedSignal(
|
|
46
|
+
payloadOverrides: Record<string, unknown> = {},
|
|
47
|
+
sourceChannel: "slack" | "telegram" | "vellum" = "slack",
|
|
48
|
+
): NotificationSignal {
|
|
49
|
+
return {
|
|
50
|
+
signalId: "test-signal-denied",
|
|
51
|
+
createdAt: Date.now(),
|
|
52
|
+
sourceChannel,
|
|
53
|
+
sourceContextId: "test-ctx-2",
|
|
54
|
+
sourceEventName: "ingress.trusted_contact.denied",
|
|
55
|
+
contextPayload: {
|
|
56
|
+
sourceChannel,
|
|
57
|
+
requesterExternalUserId: "U07CLDQ4TB3",
|
|
58
|
+
requesterChatId: "D0AQ9C5PPPF",
|
|
59
|
+
decidedByExternalUserId: "U099H19C0KA",
|
|
60
|
+
requesterDisplayName: null,
|
|
61
|
+
decidedByDisplayName: null,
|
|
62
|
+
decision: "denied",
|
|
63
|
+
...payloadOverrides,
|
|
64
|
+
},
|
|
65
|
+
attentionHints: {
|
|
66
|
+
requiresAction: false,
|
|
67
|
+
urgency: "low",
|
|
68
|
+
isAsyncBackground: false,
|
|
69
|
+
visibleInSourceNow: false,
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── guardian_decision template ────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe("guardian_decision fallback copy", () => {
|
|
77
|
+
test("uses display names when both are present", () => {
|
|
78
|
+
const signal = buildGuardianDecisionSignal({
|
|
79
|
+
requesterDisplayName: "Alice",
|
|
80
|
+
decidedByDisplayName: "Bob",
|
|
81
|
+
decision: "denied",
|
|
82
|
+
});
|
|
83
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
84
|
+
const copy = result.vellum!;
|
|
85
|
+
|
|
86
|
+
expect(copy.title).toBe("Trusted Contact Decision");
|
|
87
|
+
expect(copy.body).toBe("Alice's access request has been denied by Bob.");
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("falls back to Slack <@ID> mention format when display names are absent on Slack", () => {
|
|
91
|
+
const signal = buildGuardianDecisionSignal({
|
|
92
|
+
requesterDisplayName: null,
|
|
93
|
+
decidedByDisplayName: null,
|
|
94
|
+
requesterExternalUserId: "U07CLDQ4TB3",
|
|
95
|
+
decidedByExternalUserId: "U099H19C0KA",
|
|
96
|
+
sourceChannel: "slack",
|
|
97
|
+
decision: "denied",
|
|
98
|
+
});
|
|
99
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
100
|
+
const copy = result.vellum!;
|
|
101
|
+
|
|
102
|
+
expect(copy.body).toBe(
|
|
103
|
+
"<@U07CLDQ4TB3>'s access request has been denied by <@U099H19C0KA>.",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("uses raw user IDs without Slack formatting on non-Slack channels", () => {
|
|
108
|
+
const signal = buildGuardianDecisionSignal(
|
|
109
|
+
{
|
|
110
|
+
requesterDisplayName: null,
|
|
111
|
+
decidedByDisplayName: null,
|
|
112
|
+
requesterExternalUserId: "U07CLDQ4TB3",
|
|
113
|
+
decidedByExternalUserId: "U099H19C0KA",
|
|
114
|
+
sourceChannel: "telegram",
|
|
115
|
+
decision: "denied",
|
|
116
|
+
},
|
|
117
|
+
"telegram",
|
|
118
|
+
);
|
|
119
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
120
|
+
const copy = result.vellum!;
|
|
121
|
+
|
|
122
|
+
expect(copy.body).toBe(
|
|
123
|
+
"U07CLDQ4TB3's access request has been denied by U099H19C0KA.",
|
|
124
|
+
);
|
|
125
|
+
expect(copy.body).not.toContain("<@");
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("produces distinct copy for approved vs denied decisions", () => {
|
|
129
|
+
const deniedSignal = buildGuardianDecisionSignal({
|
|
130
|
+
requesterDisplayName: "Alice",
|
|
131
|
+
decidedByDisplayName: "Bob",
|
|
132
|
+
decision: "denied",
|
|
133
|
+
});
|
|
134
|
+
const approvedSignal = buildGuardianDecisionSignal({
|
|
135
|
+
requesterDisplayName: "Alice",
|
|
136
|
+
decidedByDisplayName: "Bob",
|
|
137
|
+
decision: "approved",
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const deniedCopy = composeFallbackCopy(deniedSignal, ["vellum"]).vellum!;
|
|
141
|
+
const approvedCopy = composeFallbackCopy(approvedSignal, [
|
|
142
|
+
"vellum",
|
|
143
|
+
]).vellum!;
|
|
144
|
+
|
|
145
|
+
expect(deniedCopy.body).toContain("denied");
|
|
146
|
+
expect(deniedCopy.body).not.toContain("approved");
|
|
147
|
+
expect(approvedCopy.body).toContain("approved");
|
|
148
|
+
expect(approvedCopy.body).not.toContain("denied");
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("does not expose raw conversation IDs (requesterChatId) in output", () => {
|
|
152
|
+
const signal = buildGuardianDecisionSignal({
|
|
153
|
+
requesterDisplayName: null,
|
|
154
|
+
decidedByDisplayName: null,
|
|
155
|
+
requesterChatId: "D0AQ9C5PPPF",
|
|
156
|
+
requesterExternalUserId: "U07CLDQ4TB3",
|
|
157
|
+
decidedByExternalUserId: "U099H19C0KA",
|
|
158
|
+
});
|
|
159
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
160
|
+
const copy = result.vellum!;
|
|
161
|
+
|
|
162
|
+
expect(copy.body).not.toContain("D0AQ9C5PPPF");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("falls back to 'Someone' when requester identity is entirely absent", () => {
|
|
166
|
+
const signal = buildGuardianDecisionSignal({
|
|
167
|
+
requesterDisplayName: null,
|
|
168
|
+
requesterExternalUserId: null,
|
|
169
|
+
decidedByDisplayName: "Bob",
|
|
170
|
+
decision: "denied",
|
|
171
|
+
});
|
|
172
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
173
|
+
const copy = result.vellum!;
|
|
174
|
+
|
|
175
|
+
expect(copy.body).toBe("Someone's access request has been denied by Bob.");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("falls back to 'a guardian' when decider identity is entirely absent", () => {
|
|
179
|
+
const signal = buildGuardianDecisionSignal({
|
|
180
|
+
requesterDisplayName: "Alice",
|
|
181
|
+
decidedByDisplayName: null,
|
|
182
|
+
decidedByExternalUserId: null,
|
|
183
|
+
decision: "approved",
|
|
184
|
+
});
|
|
185
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
186
|
+
const copy = result.vellum!;
|
|
187
|
+
|
|
188
|
+
expect(copy.body).toBe(
|
|
189
|
+
"Alice's access request has been approved by a guardian.",
|
|
190
|
+
);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("prefers display name over Slack user ID when both are present", () => {
|
|
194
|
+
const signal = buildGuardianDecisionSignal({
|
|
195
|
+
requesterDisplayName: "Alice",
|
|
196
|
+
requesterExternalUserId: "U07CLDQ4TB3",
|
|
197
|
+
decidedByDisplayName: "Bob",
|
|
198
|
+
decidedByExternalUserId: "U099H19C0KA",
|
|
199
|
+
sourceChannel: "slack",
|
|
200
|
+
decision: "denied",
|
|
201
|
+
});
|
|
202
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
203
|
+
const copy = result.vellum!;
|
|
204
|
+
|
|
205
|
+
expect(copy.body).toBe("Alice's access request has been denied by Bob.");
|
|
206
|
+
expect(copy.body).not.toContain("<@");
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("sanitizes control characters from display names", () => {
|
|
210
|
+
const signal = buildGuardianDecisionSignal({
|
|
211
|
+
requesterDisplayName: "Alice\x00\x07\nEvil",
|
|
212
|
+
decidedByDisplayName: "Bob\r\nMalicious",
|
|
213
|
+
decision: "approved",
|
|
214
|
+
});
|
|
215
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
216
|
+
const copy = result.vellum!;
|
|
217
|
+
|
|
218
|
+
expect(copy.body).not.toMatch(/[\x00-\x1f\x7f-\x9f]/);
|
|
219
|
+
expect(copy.body).toContain("Alice");
|
|
220
|
+
expect(copy.body).toContain("Bob");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("clamps excessively long display names", () => {
|
|
224
|
+
const longName = "A".repeat(200);
|
|
225
|
+
const signal = buildGuardianDecisionSignal({
|
|
226
|
+
requesterDisplayName: longName,
|
|
227
|
+
decidedByDisplayName: "Bob",
|
|
228
|
+
decision: "denied",
|
|
229
|
+
});
|
|
230
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
231
|
+
const copy = result.vellum!;
|
|
232
|
+
|
|
233
|
+
// sanitizeIdentityField clamps to 120 chars + ellipsis
|
|
234
|
+
expect(copy.body.length).toBeLessThan(longName.length);
|
|
235
|
+
expect(copy.body).toContain("…");
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
// ── denied template ──────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
describe("trusted_contact.denied fallback copy", () => {
|
|
242
|
+
test("uses display name when present", () => {
|
|
243
|
+
const signal = buildDeniedSignal({
|
|
244
|
+
requesterDisplayName: "Alice",
|
|
245
|
+
});
|
|
246
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
247
|
+
const copy = result.vellum!;
|
|
248
|
+
|
|
249
|
+
expect(copy.title).toBe("Trusted Contact Denied");
|
|
250
|
+
expect(copy.body).toBe(
|
|
251
|
+
"A trusted contact request from Alice has been denied.",
|
|
252
|
+
);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("falls back to Slack <@ID> mention format on Slack when display name absent", () => {
|
|
256
|
+
const signal = buildDeniedSignal({
|
|
257
|
+
requesterDisplayName: null,
|
|
258
|
+
requesterExternalUserId: "U07CLDQ4TB3",
|
|
259
|
+
sourceChannel: "slack",
|
|
260
|
+
});
|
|
261
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
262
|
+
const copy = result.vellum!;
|
|
263
|
+
|
|
264
|
+
expect(copy.body).toBe(
|
|
265
|
+
"A trusted contact request from <@U07CLDQ4TB3> has been denied.",
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("uses raw user ID without Slack formatting on non-Slack channels", () => {
|
|
270
|
+
const signal = buildDeniedSignal(
|
|
271
|
+
{
|
|
272
|
+
requesterDisplayName: null,
|
|
273
|
+
requesterExternalUserId: "U07CLDQ4TB3",
|
|
274
|
+
sourceChannel: "telegram",
|
|
275
|
+
},
|
|
276
|
+
"telegram",
|
|
277
|
+
);
|
|
278
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
279
|
+
const copy = result.vellum!;
|
|
280
|
+
|
|
281
|
+
expect(copy.body).toBe(
|
|
282
|
+
"A trusted contact request from U07CLDQ4TB3 has been denied.",
|
|
283
|
+
);
|
|
284
|
+
expect(copy.body).not.toContain("<@");
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("falls back to 'Someone' when requester identity is absent", () => {
|
|
288
|
+
const signal = buildDeniedSignal({
|
|
289
|
+
requesterDisplayName: null,
|
|
290
|
+
requesterExternalUserId: null,
|
|
291
|
+
});
|
|
292
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
293
|
+
const copy = result.vellum!;
|
|
294
|
+
|
|
295
|
+
expect(copy.body).toBe(
|
|
296
|
+
"A trusted contact request from Someone has been denied.",
|
|
297
|
+
);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("does not expose raw conversation IDs (requesterChatId) in output", () => {
|
|
301
|
+
const signal = buildDeniedSignal({
|
|
302
|
+
requesterDisplayName: null,
|
|
303
|
+
requesterExternalUserId: "U07CLDQ4TB3",
|
|
304
|
+
requesterChatId: "D0AQ9C5PPPF",
|
|
305
|
+
});
|
|
306
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
307
|
+
const copy = result.vellum!;
|
|
308
|
+
|
|
309
|
+
expect(copy.body).not.toContain("D0AQ9C5PPPF");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("sanitizes control characters from display names", () => {
|
|
313
|
+
const signal = buildDeniedSignal({
|
|
314
|
+
requesterDisplayName: "Alice\x00\x07\nEvil",
|
|
315
|
+
});
|
|
316
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
317
|
+
const copy = result.vellum!;
|
|
318
|
+
|
|
319
|
+
expect(copy.body).not.toMatch(/[\x00-\x1f\x7f-\x9f]/);
|
|
320
|
+
expect(copy.body).toContain("Alice");
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
test("clamps excessively long display names", () => {
|
|
324
|
+
const longName = "A".repeat(200);
|
|
325
|
+
const signal = buildDeniedSignal({
|
|
326
|
+
requesterDisplayName: longName,
|
|
327
|
+
});
|
|
328
|
+
const result = composeFallbackCopy(signal, ["vellum"]);
|
|
329
|
+
const copy = result.vellum!;
|
|
330
|
+
|
|
331
|
+
// sanitizeIdentityField clamps to 120 chars + ellipsis
|
|
332
|
+
expect(copy.body.length).toBeLessThan(longName.length);
|
|
333
|
+
expect(copy.body).toContain("…");
|
|
334
|
+
});
|
|
335
|
+
});
|
|
@@ -95,7 +95,6 @@ function makePrompter(
|
|
|
95
95
|
allowlistOptions: unknown[],
|
|
96
96
|
scopeOptions: unknown[],
|
|
97
97
|
diff: unknown,
|
|
98
|
-
sandboxed: unknown,
|
|
99
98
|
sessionId: string | undefined,
|
|
100
99
|
executionTarget: unknown,
|
|
101
100
|
persistentDecisionsAllowed: unknown,
|
|
@@ -109,7 +108,6 @@ function makePrompter(
|
|
|
109
108
|
allowlistOptions,
|
|
110
109
|
scopeOptions,
|
|
111
110
|
diff,
|
|
112
|
-
sandboxed,
|
|
113
111
|
sessionId,
|
|
114
112
|
executionTarget,
|
|
115
113
|
persistentDecisionsAllowed,
|
|
@@ -1,219 +1,10 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
|
-
buildTemporalContext,
|
|
5
4
|
extractUserTimeZoneFromRecall,
|
|
5
|
+
formatTurnTimestamp,
|
|
6
6
|
} from "../daemon/date-context.js";
|
|
7
7
|
|
|
8
|
-
// Fixed timestamps for deterministic assertions (all UTC midday to avoid DST edge cases).
|
|
9
|
-
|
|
10
|
-
/** Wednesday 2026-02-18 12:00 UTC */
|
|
11
|
-
const WED_FEB_18 = Date.UTC(2026, 1, 18, 12, 0, 0);
|
|
12
|
-
|
|
13
|
-
/** Tuesday 2026-12-29 12:00 UTC - year boundary */
|
|
14
|
-
const TUE_DEC_29 = Date.UTC(2026, 11, 29, 12, 0, 0);
|
|
15
|
-
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
// Basic structure
|
|
18
|
-
// ---------------------------------------------------------------------------
|
|
19
|
-
|
|
20
|
-
describe("buildTemporalContext", () => {
|
|
21
|
-
test("returns output wrapped in <temporal_context> tags", () => {
|
|
22
|
-
const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: "UTC" });
|
|
23
|
-
expect(result).toStartWith("<temporal_context>");
|
|
24
|
-
expect(result).toEndWith("</temporal_context>");
|
|
25
|
-
});
|
|
26
|
-
|
|
27
|
-
test("includes today date, weekday, time and offset on one line", () => {
|
|
28
|
-
const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: "UTC" });
|
|
29
|
-
expect(result).toContain("Today: 2026-02-18 (Wed) 12:00 +00:00");
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
test("includes timezone", () => {
|
|
33
|
-
const result = buildTemporalContext({
|
|
34
|
-
nowMs: WED_FEB_18,
|
|
35
|
-
timeZone: "America/New_York",
|
|
36
|
-
});
|
|
37
|
-
expect(result).toContain("TZ: America/New_York");
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("does not include UTC time, timezone source, or seconds", () => {
|
|
41
|
-
const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: "UTC" });
|
|
42
|
-
expect(result).not.toContain("Current UTC time");
|
|
43
|
-
expect(result).not.toContain("Current local time");
|
|
44
|
-
expect(result).not.toContain("Timezone source:");
|
|
45
|
-
// No seconds in the time
|
|
46
|
-
expect(result).not.toContain("12:00:00");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("does not include week definitions, next weekend, next work week, or horizon dates", () => {
|
|
50
|
-
const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: "UTC" });
|
|
51
|
-
expect(result).not.toContain("Week definitions");
|
|
52
|
-
expect(result).not.toContain("Next weekend");
|
|
53
|
-
expect(result).not.toContain("Next work week");
|
|
54
|
-
expect(result).not.toContain("Upcoming dates");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("uses user timezone when provided", () => {
|
|
58
|
-
const result = buildTemporalContext({
|
|
59
|
-
nowMs: WED_FEB_18,
|
|
60
|
-
hostTimeZone: "UTC",
|
|
61
|
-
userTimeZone: "America/New_York",
|
|
62
|
-
});
|
|
63
|
-
expect(result).toContain("TZ: America/New_York");
|
|
64
|
-
expect(result).toContain("Today: 2026-02-18 (Wed) 07:00 -05:00");
|
|
65
|
-
expect(result).not.toContain("(host fallback)");
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("shows user TZ only when different from primary timezone", () => {
|
|
69
|
-
// When user timezone equals the primary timezone, omit it
|
|
70
|
-
const sameResult = buildTemporalContext({
|
|
71
|
-
nowMs: WED_FEB_18,
|
|
72
|
-
hostTimeZone: "UTC",
|
|
73
|
-
configuredUserTimeZone: "UTC",
|
|
74
|
-
});
|
|
75
|
-
expect(sameResult).not.toContain("User TZ:");
|
|
76
|
-
|
|
77
|
-
// When user timezone differs from host, it becomes the primary timezone
|
|
78
|
-
// and the host timezone is shown as a secondary annotation
|
|
79
|
-
const diffResult = buildTemporalContext({
|
|
80
|
-
nowMs: WED_FEB_18,
|
|
81
|
-
hostTimeZone: "UTC",
|
|
82
|
-
userTimeZone: "America/New_York",
|
|
83
|
-
});
|
|
84
|
-
expect(diffResult).toContain("TZ: America/New_York");
|
|
85
|
-
expect(diffResult).toContain("Host TZ: UTC");
|
|
86
|
-
expect(diffResult).not.toContain("User TZ:");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
test("shows host TZ only when different from primary timezone", () => {
|
|
90
|
-
// When host timezone equals the primary timezone, omit it
|
|
91
|
-
const sameResult = buildTemporalContext({
|
|
92
|
-
nowMs: WED_FEB_18,
|
|
93
|
-
hostTimeZone: "UTC",
|
|
94
|
-
timeZone: "UTC",
|
|
95
|
-
});
|
|
96
|
-
expect(sameResult).not.toContain("Host TZ:");
|
|
97
|
-
|
|
98
|
-
// When different, include it
|
|
99
|
-
const diffResult = buildTemporalContext({
|
|
100
|
-
nowMs: WED_FEB_18,
|
|
101
|
-
hostTimeZone: "UTC",
|
|
102
|
-
userTimeZone: "America/New_York",
|
|
103
|
-
});
|
|
104
|
-
expect(diffResult).toContain("Host TZ: UTC");
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test("uses configured user timezone when profile timezone is unavailable", () => {
|
|
108
|
-
const result = buildTemporalContext({
|
|
109
|
-
nowMs: WED_FEB_18,
|
|
110
|
-
hostTimeZone: "UTC",
|
|
111
|
-
configuredUserTimeZone: "America/Chicago",
|
|
112
|
-
userTimeZone: null,
|
|
113
|
-
});
|
|
114
|
-
expect(result).toContain("TZ: America/Chicago");
|
|
115
|
-
expect(result).toContain("Today: 2026-02-18 (Wed) 06:00 -06:00");
|
|
116
|
-
expect(result).not.toContain("(host fallback)");
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
test("configured user timezone takes precedence over profile timezone", () => {
|
|
120
|
-
const result = buildTemporalContext({
|
|
121
|
-
nowMs: WED_FEB_18,
|
|
122
|
-
hostTimeZone: "UTC",
|
|
123
|
-
configuredUserTimeZone: "America/Los_Angeles",
|
|
124
|
-
userTimeZone: "America/New_York",
|
|
125
|
-
});
|
|
126
|
-
expect(result).toContain("TZ: America/Los_Angeles");
|
|
127
|
-
expect(result).toContain("Today: 2026-02-18 (Wed) 04:00 -08:00");
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
test("falls back to host timezone with (host fallback) suffix", () => {
|
|
131
|
-
const result = buildTemporalContext({
|
|
132
|
-
nowMs: WED_FEB_18,
|
|
133
|
-
hostTimeZone: "UTC",
|
|
134
|
-
userTimeZone: null,
|
|
135
|
-
});
|
|
136
|
-
expect(result).toContain("TZ: UTC (host fallback)");
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test("accepts UTC/GMT offset-style user timezone values", () => {
|
|
140
|
-
const result = buildTemporalContext({
|
|
141
|
-
nowMs: WED_FEB_18,
|
|
142
|
-
hostTimeZone: "UTC",
|
|
143
|
-
userTimeZone: "UTC+2",
|
|
144
|
-
});
|
|
145
|
-
expect(result).toContain("TZ: Etc/GMT-2");
|
|
146
|
-
expect(result).toContain("Today: 2026-02-18 (Wed) 14:00 +02:00");
|
|
147
|
-
expect(result).not.toContain("(host fallback)");
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("accepts fractional UTC/GMT offset-style user timezone values", () => {
|
|
151
|
-
const result = buildTemporalContext({
|
|
152
|
-
nowMs: WED_FEB_18,
|
|
153
|
-
hostTimeZone: "UTC",
|
|
154
|
-
userTimeZone: "UTC+5:30",
|
|
155
|
-
});
|
|
156
|
-
expect(result).toContain("TZ: +05:30");
|
|
157
|
-
expect(result).toContain("Today: 2026-02-18 (Wed) 17:30 +05:30");
|
|
158
|
-
expect(result).not.toContain("(host fallback)");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
test("formats midnight hours as 00 (never 24)", () => {
|
|
162
|
-
const justAfterMidnight = Date.UTC(2026, 1, 19, 0, 5, 0);
|
|
163
|
-
const result = buildTemporalContext({
|
|
164
|
-
nowMs: justAfterMidnight,
|
|
165
|
-
timeZone: "UTC",
|
|
166
|
-
});
|
|
167
|
-
expect(result).toContain("00:05 +00:00");
|
|
168
|
-
expect(result).not.toContain("24:05");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("Today line includes full YYYY-MM-DD format with year", () => {
|
|
172
|
-
const result = buildTemporalContext({ nowMs: WED_FEB_18, timeZone: "UTC" });
|
|
173
|
-
expect(result).toMatch(/Today: \d{4}-\d{2}-\d{2} \(\w{3}\) \d{2}:\d{2}/);
|
|
174
|
-
expect(result).toContain("2026-02-18");
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("handles year boundary correctly", () => {
|
|
178
|
-
const result = buildTemporalContext({ nowMs: TUE_DEC_29, timeZone: "UTC" });
|
|
179
|
-
expect(result).toContain("Today: 2026-12-29 (Tue)");
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
// ---------------------------------------------------------------------------
|
|
184
|
-
// DST-safe timezone behavior
|
|
185
|
-
// ---------------------------------------------------------------------------
|
|
186
|
-
|
|
187
|
-
describe("DST-safe timezone behavior", () => {
|
|
188
|
-
test("date labels are correct in US Eastern timezone", () => {
|
|
189
|
-
const result = buildTemporalContext({
|
|
190
|
-
nowMs: WED_FEB_18,
|
|
191
|
-
timeZone: "America/New_York",
|
|
192
|
-
});
|
|
193
|
-
expect(result).toContain("Today: 2026-02-18 (Wed) 07:00 -05:00");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
test("date labels are correct in timezone ahead of UTC", () => {
|
|
197
|
-
// Feb 18 23:00 UTC = Feb 19 08:00 JST
|
|
198
|
-
const nearMidnight = Date.UTC(2026, 1, 18, 23, 0, 0);
|
|
199
|
-
const result = buildTemporalContext({
|
|
200
|
-
nowMs: nearMidnight,
|
|
201
|
-
timeZone: "Asia/Tokyo",
|
|
202
|
-
});
|
|
203
|
-
expect(result).toContain("Today: 2026-02-19 (Thu) 08:00 +09:00");
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test("local offset tracks daylight saving changes", () => {
|
|
207
|
-
// Jul 1 12:00 UTC = Jul 1 08:00 EDT
|
|
208
|
-
const summer = Date.UTC(2026, 6, 1, 12, 0, 0);
|
|
209
|
-
const result = buildTemporalContext({
|
|
210
|
-
nowMs: summer,
|
|
211
|
-
timeZone: "America/New_York",
|
|
212
|
-
});
|
|
213
|
-
expect(result).toContain("Today: 2026-07-01 (Wed) 08:00 -04:00");
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
8
|
// ---------------------------------------------------------------------------
|
|
218
9
|
// extractUserTimeZoneFromRecall
|
|
219
10
|
// ---------------------------------------------------------------------------
|
|
@@ -293,3 +84,78 @@ describe("extractUserTimeZoneFromRecall", () => {
|
|
|
293
84
|
expect(extractUserTimeZoneFromRecall(text)).toBe("America/Denver");
|
|
294
85
|
});
|
|
295
86
|
});
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// formatTurnTimestamp
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
describe("formatTurnTimestamp", () => {
|
|
93
|
+
/** 2026-04-02 06:52:33 UTC (Thursday) */
|
|
94
|
+
const THU_APR_02_0652 = Date.UTC(2026, 3, 2, 6, 52, 33);
|
|
95
|
+
|
|
96
|
+
test("includes seconds in the timestamp", () => {
|
|
97
|
+
const result = formatTurnTimestamp({
|
|
98
|
+
nowMs: THU_APR_02_0652,
|
|
99
|
+
timeZone: "America/Chicago",
|
|
100
|
+
});
|
|
101
|
+
expect(result).toContain("01:52:33");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("timezone name appears in parentheses", () => {
|
|
105
|
+
const result = formatTurnTimestamp({
|
|
106
|
+
nowMs: THU_APR_02_0652,
|
|
107
|
+
timeZone: "America/Chicago",
|
|
108
|
+
});
|
|
109
|
+
expect(result).toContain("(America/Chicago)");
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test("produces expected full format", () => {
|
|
113
|
+
const result = formatTurnTimestamp({
|
|
114
|
+
nowMs: THU_APR_02_0652,
|
|
115
|
+
timeZone: "America/Chicago",
|
|
116
|
+
});
|
|
117
|
+
expect(result).toBe(
|
|
118
|
+
"2026-04-02 (Thu) 01:52:33 -05:00 (America/Chicago)",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("handles UTC fallback when no timezone provided", () => {
|
|
123
|
+
const result = formatTurnTimestamp({
|
|
124
|
+
nowMs: THU_APR_02_0652,
|
|
125
|
+
hostTimeZone: "UTC",
|
|
126
|
+
});
|
|
127
|
+
expect(result).toBe("2026-04-02 (Thu) 06:52:33 +00:00 (UTC)");
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("handles user timezone override", () => {
|
|
131
|
+
const result = formatTurnTimestamp({
|
|
132
|
+
nowMs: THU_APR_02_0652,
|
|
133
|
+
hostTimeZone: "UTC",
|
|
134
|
+
userTimeZone: "Asia/Tokyo",
|
|
135
|
+
});
|
|
136
|
+
expect(result).toBe("2026-04-02 (Thu) 15:52:33 +09:00 (Asia/Tokyo)");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("handles DST correctly", () => {
|
|
140
|
+
// Jul 1 12:00:30 UTC = Jul 1 08:00:30 EDT (Eastern Daylight Time, -04:00)
|
|
141
|
+
const summerWithSeconds = Date.UTC(2026, 6, 1, 12, 0, 30);
|
|
142
|
+
const result = formatTurnTimestamp({
|
|
143
|
+
nowMs: summerWithSeconds,
|
|
144
|
+
timeZone: "America/New_York",
|
|
145
|
+
});
|
|
146
|
+
expect(result).toBe(
|
|
147
|
+
"2026-07-01 (Wed) 08:00:30 -04:00 (America/New_York)",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("formats midnight as 00", () => {
|
|
152
|
+
// 2026-02-19 00:00:15 UTC
|
|
153
|
+
const justAfterMidnight = Date.UTC(2026, 1, 19, 0, 0, 15);
|
|
154
|
+
const result = formatTurnTimestamp({
|
|
155
|
+
nowMs: justAfterMidnight,
|
|
156
|
+
timeZone: "UTC",
|
|
157
|
+
});
|
|
158
|
+
expect(result).toContain("00:00:15");
|
|
159
|
+
expect(result).not.toContain("24:");
|
|
160
|
+
});
|
|
161
|
+
});
|
|
@@ -42,6 +42,7 @@ describe("schedule_syntax column migration", () => {
|
|
|
42
42
|
routing_hints_json TEXT NOT NULL DEFAULT '{}',
|
|
43
43
|
status TEXT NOT NULL DEFAULT 'active',
|
|
44
44
|
quiet INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
reuse_conversation INTEGER NOT NULL DEFAULT 0,
|
|
45
46
|
created_at INTEGER NOT NULL,
|
|
46
47
|
updated_at INTEGER NOT NULL
|
|
47
48
|
)
|
|
@@ -108,7 +109,7 @@ describe("schedule_syntax column migration", () => {
|
|
|
108
109
|
`INSERT INTO cron_jobs (id, name, enabled, cron_expression, timezone, message, next_run_at, last_run_at, last_status, retry_count, created_by, created_at, updated_at) VALUES ('old-1', 'Old Job', 1, '0 9 * * *', NULL, 'hello', ${now + 60000}, NULL, NULL, 0, 'agent', ${now}, ${now})`,
|
|
109
110
|
);
|
|
110
111
|
|
|
111
|
-
// Run the
|
|
112
|
+
// Run the migrations
|
|
112
113
|
try {
|
|
113
114
|
raw.exec(
|
|
114
115
|
`ALTER TABLE cron_jobs ADD COLUMN schedule_syntax TEXT NOT NULL DEFAULT 'cron'`,
|
|
@@ -116,6 +117,13 @@ describe("schedule_syntax column migration", () => {
|
|
|
116
117
|
} catch {
|
|
117
118
|
/* already exists */
|
|
118
119
|
}
|
|
120
|
+
try {
|
|
121
|
+
raw.exec(
|
|
122
|
+
`ALTER TABLE cron_jobs ADD COLUMN reuse_conversation INTEGER NOT NULL DEFAULT 0`,
|
|
123
|
+
);
|
|
124
|
+
} catch {
|
|
125
|
+
/* already exists */
|
|
126
|
+
}
|
|
119
127
|
|
|
120
128
|
const row = db
|
|
121
129
|
.select()
|
|
@@ -167,6 +175,13 @@ describe("schedule_syntax column migration", () => {
|
|
|
167
175
|
} catch {
|
|
168
176
|
/* ok */
|
|
169
177
|
}
|
|
178
|
+
try {
|
|
179
|
+
raw.exec(
|
|
180
|
+
`ALTER TABLE cron_jobs ADD COLUMN reuse_conversation INTEGER NOT NULL DEFAULT 0`,
|
|
181
|
+
);
|
|
182
|
+
} catch {
|
|
183
|
+
/* ok */
|
|
184
|
+
}
|
|
170
185
|
|
|
171
186
|
const now = Date.now();
|
|
172
187
|
raw.exec(
|