@vellumai/assistant 0.11.8 → 0.11.9-staging.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +2 -2
- package/Dockerfile +8 -48
- package/docker-entrypoint.sh +5 -1
- package/docker-kata-apt-env.sh +3 -0
- package/docker-kata-apt-shims.sh +127 -0
- package/docker-kata-apt-wrapper.sh +45 -0
- package/docker-kata-chroot-exec.sh +35 -0
- package/docker-kata-pip.sh +8 -2
- package/docs/architecture/memory.md +8 -4
- package/docs/guardian-request-flow.md +18 -14
- package/docs/trusted-contact-access.md +1 -7
- package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
- package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
- package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
- package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
- package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
- package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
- package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
- package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
- package/node_modules/@vellumai/service-contracts/package.json +4 -1
- package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
- package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
- package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
- package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
- package/openapi.yaml +193 -4
- package/package.json +2 -2
- package/src/__tests__/access-request-card-view.test.ts +6 -5
- package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
- package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
- package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
- package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
- package/src/__tests__/attachments-store.test.ts +27 -0
- package/src/__tests__/attachments.test.ts +43 -0
- package/src/__tests__/canned-reply-release.test.ts +48 -5
- package/src/__tests__/channel-delivery-store.test.ts +129 -18
- package/src/__tests__/channel-reply-delivery.test.ts +553 -93
- package/src/__tests__/channel-retry-sweep.test.ts +199 -0
- package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
- package/src/__tests__/conversation-error.test.ts +15 -0
- package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
- package/src/__tests__/conversation-pairing.test.ts +141 -1
- package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
- package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
- package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
- package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
- package/src/__tests__/conversation-sync-tags.test.ts +2 -37
- package/src/__tests__/credential-health-service.test.ts +98 -10
- package/src/__tests__/delete-propagation.test.ts +469 -0
- package/src/__tests__/dm-persistence.test.ts +16 -0
- package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
- package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
- package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
- package/src/__tests__/guardian-gateway-sim.ts +0 -23
- package/src/__tests__/guardian-question-mode.test.ts +108 -0
- package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
- package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
- package/src/__tests__/host-proxy-interface.test.ts +10 -0
- package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
- package/src/__tests__/injector-v3-suppression.test.ts +43 -9
- package/src/__tests__/list-messages-page-latest.test.ts +127 -0
- package/src/__tests__/list-messages-system-card.test.ts +103 -0
- package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
- package/src/__tests__/notification-decision-fallback.test.ts +199 -77
- package/src/__tests__/notification-decision-strategy.test.ts +198 -213
- package/src/__tests__/notification-discord-adapter.test.ts +25 -0
- package/src/__tests__/notification-slack-adapter.test.ts +133 -0
- package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
- package/src/__tests__/openai-provider.test.ts +5 -5
- package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
- package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
- package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
- package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
- package/src/__tests__/process-message-display-content.test.ts +20 -9
- package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
- package/src/__tests__/provider-meta-persistence.test.ts +67 -0
- package/src/__tests__/reaction-persistence.test.ts +22 -198
- package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
- package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
- package/src/__tests__/skill-load-tool.test.ts +27 -0
- package/src/__tests__/skills.test.ts +34 -1
- package/src/__tests__/strip-memory-injections.test.ts +3 -4
- package/src/__tests__/terminal-tools.test.ts +9 -0
- package/src/__tests__/thread-backfill.test.ts +5 -3
- package/src/__tests__/unified-turn-context-location.test.ts +76 -0
- package/src/__tests__/voice-session-bridge.test.ts +18 -0
- package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
- package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
- package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
- package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
- package/src/agent/attachments.ts +13 -2
- package/src/agent/loop.ts +3 -19
- package/src/api/README.md +9 -5
- package/src/api/index.ts +9 -8
- package/src/api/package.json +1 -0
- package/src/api/responses/conversation-message.ts +8 -0
- package/src/api/responses/home.ts +7 -18
- package/src/api/surfaces.ts +114 -7
- package/src/approvals/AGENTS.md +1 -1
- package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
- package/src/channels/__tests__/types.test.ts +22 -1
- package/src/channels/gateway-guardian-requests.ts +0 -22
- package/src/channels/types.ts +8 -6
- package/src/cli/__tests__/catalog-search-help.test.ts +12 -0
- package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
- package/src/cli/commands/channels/index.help.ts +15 -1
- package/src/cli/commands/channels/index.ts +6 -2
- package/src/cli/commands/db/__tests__/status.test.ts +22 -0
- package/src/cli/commands/db/index.help.ts +1 -1
- package/src/cli/commands/db/status.ts +172 -1
- package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
- package/src/cli/commands/platform/connect.ts +14 -5
- package/src/cli/commands/plugins.help.ts +6 -5
- package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
- package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
- package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
- package/src/cli/lib/bundled-marketplace.json +14 -0
- package/src/cli/lib/install-from-github.ts +9 -5
- package/src/cli/lib/install-from-platform.ts +12 -1
- package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
- package/src/config/assistant-initiated-threads-gate.ts +50 -0
- package/src/config/bundled-skills/acp/SKILL.md +10 -3
- package/src/config/bundled-skills/schedule/SKILL.md +25 -11
- package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
- package/src/config/call-site-defaults.ts +4 -1
- package/src/config/feature-flag-registry.json +21 -4
- package/src/config/schemas/memory-v3.ts +4 -3
- package/src/context/strip-injections.ts +17 -56
- package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
- package/src/conversations/message-consolidation.ts +4 -3
- package/src/credential-health/credential-health-service.ts +130 -0
- package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
- package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
- package/src/daemon/conversation-error.ts +2 -0
- package/src/daemon/conversation-messaging.ts +195 -47
- package/src/daemon/conversation-process.ts +20 -5
- package/src/daemon/conversation-runtime-assembly.ts +48 -39
- package/src/daemon/conversation-store.ts +159 -11
- package/src/daemon/conversation-tool-setup.ts +9 -2
- package/src/daemon/conversation.ts +290 -19
- package/src/daemon/dictation-text-processing.ts +2 -7
- package/src/daemon/handlers/config-channels.ts +33 -3
- package/src/daemon/handlers/shared.ts +9 -0
- package/src/daemon/port-oversized-content.test.ts +117 -0
- package/src/daemon/port-oversized-content.ts +110 -0
- package/src/daemon/process-message.ts +87 -16
- package/src/daemon/reaction-record.test.ts +23 -11
- package/src/daemon/reaction-record.ts +6 -9
- package/src/documents/document-store.ts +1 -5
- package/src/home/conversation-starter-validation.ts +1 -5
- package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
- package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
- package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
- package/src/live-voice/live-voice-photo.ts +544 -61
- package/src/live-voice/live-voice-session.ts +6 -2
- package/src/live-voice/protocol.ts +20 -1
- package/src/messaging/provider-message-metadata.ts +40 -1
- package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
- package/src/messaging/providers/channel-transport.ts +28 -0
- package/src/messaging/providers/discord/send.ts +3 -2
- package/src/messaging/providers/slack/api.ts +11 -1
- package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
- package/src/messaging/providers/slack/message-metadata.ts +141 -1
- package/src/messaging/providers/slack/send.test.ts +58 -0
- package/src/messaging/providers/slack/send.ts +4 -4
- package/src/messaging/providers/slack/transport.ts +28 -2
- package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
- package/src/messaging/providers/telegram-bot/send.ts +93 -0
- package/src/messaging/providers/telegram-bot/transport.ts +71 -0
- package/src/messaging/reaction-envelopes.test.ts +73 -0
- package/src/messaging/reaction-envelopes.ts +33 -5
- package/src/messaging/read-provider-metadata.ts +4 -3
- package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
- package/src/monitoring/recovery/db.ts +35 -0
- package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
- package/src/monitoring/recovery/run-recovery.ts +6 -2
- package/src/monitoring/recovery/stale-processing.ts +3 -20
- package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
- package/src/notifications/AGENTS.md +2 -2
- package/src/notifications/README.md +2 -2
- package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
- package/src/notifications/__tests__/broadcaster.test.ts +35 -4
- package/src/notifications/__tests__/notification-utils.test.ts +31 -0
- package/src/notifications/access-request-copy.ts +126 -114
- package/src/notifications/adapters/discord.ts +9 -5
- package/src/notifications/adapters/shared.ts +17 -4
- package/src/notifications/adapters/slack.ts +14 -4
- package/src/notifications/adapters/telegram.ts +8 -4
- package/src/notifications/approval-card-data.ts +5 -2
- package/src/notifications/assistant-reply-producer.ts +7 -7
- package/src/notifications/broadcaster.ts +57 -25
- package/src/notifications/conversation-pairing.ts +68 -2
- package/src/notifications/copy-composer.ts +13 -32
- package/src/notifications/decision-engine.ts +56 -237
- package/src/notifications/guardian-delivery-recorder.ts +5 -9
- package/src/notifications/guardian-feed-projection.ts +0 -15
- package/src/notifications/guardian-question-mode.ts +147 -6
- package/src/notifications/notification-utils.ts +117 -1
- package/src/permissions/confirmation-guardian-request.ts +2 -2
- package/src/permissions/prompter.ts +1 -1
- package/src/persistence/conversation-crud.ts +93 -14
- package/src/persistence/conversation-queries.ts +144 -17
- package/src/persistence/conversation-types.ts +39 -5
- package/src/persistence/delivery-crud.ts +224 -28
- package/src/persistence/delivery-status.ts +21 -2
- package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
- package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
- package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
- package/src/persistence/schema/conversations.ts +71 -2
- package/src/persistence/schema-contract.test.ts +78 -0
- package/src/persistence/schema-contract.ts +96 -0
- package/src/persistence/steps.ts +4 -0
- package/src/platform/client.ts +55 -0
- package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
- package/src/plugins/defaults/injector-order.ts +2 -1
- package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
- package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
- package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
- package/src/plugins/defaults/memory/injectors.ts +1 -1
- package/src/plugins/defaults/memory/memory-marker.ts +17 -7
- package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
- package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
- package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
- package/src/plugins/defaults/memory/v3/injector.ts +14 -15
- package/src/plugins/defaults/memory/v3/types.ts +15 -4
- package/src/plugins/defaults/turn-context/injectors.ts +3 -0
- package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
- package/src/plugins/mcp-servers.ts +14 -2
- package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
- package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
- package/src/providers/anthropic/client.ts +21 -24
- package/src/providers/call-site-routing.ts +5 -4
- package/src/providers/connection-resolution.ts +29 -1
- package/src/providers/content-block-size.test.ts +82 -0
- package/src/providers/content-block-size.ts +99 -0
- package/src/providers/file-block-text.test.ts +64 -0
- package/src/providers/file-block-text.ts +28 -0
- package/src/providers/gemini/client.ts +12 -7
- package/src/providers/inference/auth.ts +6 -6
- package/src/providers/media-resolve.ts +14 -0
- package/src/providers/model-catalog.ts +31 -34
- package/src/providers/openai/chat-completions-provider.ts +9 -25
- package/src/providers/openai/responses-provider.ts +11 -18
- package/src/providers/registry.ts +10 -7
- package/src/providers/routing-identity.ts +2 -1
- package/src/providers/types.ts +1 -4
- package/src/providers/vellum-model-routing.ts +3 -2
- package/src/runtime/AGENTS.md +40 -15
- package/src/runtime/approval-message-composer.ts +1 -6
- package/src/runtime/assistant-event-hub.ts +2 -39
- package/src/runtime/channel-approval-types.ts +1 -5
- package/src/runtime/channel-reply-delivery.ts +182 -115
- package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
- package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
- package/src/runtime/channel-retry-sweep.ts +102 -1
- package/src/runtime/finalize-event-delivery.ts +4 -4
- package/src/runtime/guardian-action-message-composer.ts +1 -4
- package/src/runtime/guardian-reply-router.ts +4 -75
- package/src/runtime/http-router.ts +1 -5
- package/src/runtime/question-request-guardian-bridge.ts +2 -3
- package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
- package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
- package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
- package/src/runtime/routes/acp-routes.ts +1 -1
- package/src/runtime/routes/canned-reply-release.ts +19 -9
- package/src/runtime/routes/channel-route-shared.ts +0 -39
- package/src/runtime/routes/channel-verification-routes.ts +4 -1
- package/src/runtime/routes/conversation-list-routes.ts +44 -3
- package/src/runtime/routes/conversation-management-routes.ts +2 -1
- package/src/runtime/routes/conversation-routes.ts +138 -59
- package/src/runtime/routes/diagnostics-routes.ts +111 -60
- package/src/runtime/routes/guardian-action-routes.ts +10 -14
- package/src/runtime/routes/guardian-approval-interception.ts +0 -5
- package/src/runtime/routes/inbound-message-handler.ts +170 -48
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
- package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
- package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
- package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/platform-routes.ts +99 -10
- package/src/runtime/routes/sight-frame-routes.ts +136 -0
- package/src/runtime/sync/resource-sync-events.ts +14 -37
- package/src/runtime/sync/sync-publisher.test.ts +5 -4
- package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
- package/src/tools/client-os.ts +11 -1
- package/src/tools/host-filesystem/edit.ts +2 -2
- package/src/tools/host-filesystem/read.ts +2 -2
- package/src/tools/host-filesystem/transfer.ts +2 -2
- package/src/tools/host-filesystem/write.ts +2 -2
- package/src/tools/host-terminal/host-shell.ts +2 -2
- package/src/tools/skills/load.ts +1 -1
- package/src/tools/terminal/safe-env.ts +4 -0
- package/src/tools/tool-input-schemas.ts +2 -0
- package/src/tools/tool-manifest.ts +2 -0
- package/src/tools/ui-surface/definitions.ts +4 -3
- package/src/tools/watch/watch-retro-report.ts +205 -0
- package/src/watch/__tests__/watch-retro.test.ts +268 -40
- package/src/watch/watch-retro.ts +190 -77
- package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
- package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
- package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
- package/src/workspace/migrations/registry.ts +4 -0
- package/docker-kata-pip-chroot.sh +0 -22
- package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
- package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
- package/src/api/events/conversation-list-invalidated.ts +0 -38
|
@@ -16,10 +16,23 @@ mock.module("../config/env.js", () => ({
|
|
|
16
16
|
getGatewayInternalBaseUrl: () => "http://127.0.0.1:7830",
|
|
17
17
|
}));
|
|
18
18
|
|
|
19
|
+
// A client showing the conversation learns of the deletion through the
|
|
20
|
+
// messages-changed invalidation; capture it rather than opening the SSE hub.
|
|
21
|
+
const publishedConversationIds: string[] = [];
|
|
22
|
+
const actualSyncEvents =
|
|
23
|
+
await import("../runtime/sync/resource-sync-events.js");
|
|
24
|
+
mock.module("../runtime/sync/resource-sync-events.js", () => ({
|
|
25
|
+
...actualSyncEvents,
|
|
26
|
+
publishConversationMessagesChanged: (conversationId: string) => {
|
|
27
|
+
publishedConversationIds.push(conversationId);
|
|
28
|
+
},
|
|
29
|
+
}));
|
|
30
|
+
|
|
19
31
|
import { eq } from "drizzle-orm";
|
|
20
32
|
|
|
21
33
|
import {
|
|
22
34
|
readSlackMetadata,
|
|
35
|
+
readSlackMetadataFromMessageMetadata,
|
|
23
36
|
writeSlackMetadata,
|
|
24
37
|
} from "../messaging/providers/slack/message-metadata.js";
|
|
25
38
|
import { getDb } from "../persistence/db-connection.js";
|
|
@@ -43,6 +56,7 @@ const SLACK_DELETE_ACTOR_ID = "U_DELETE_ACTOR";
|
|
|
43
56
|
const SLACK_DELETE_ACTOR_DISPLAY_NAME = "Delete Actor";
|
|
44
57
|
|
|
45
58
|
function resetState(): void {
|
|
59
|
+
publishedConversationIds.length = 0;
|
|
46
60
|
const db = getDb();
|
|
47
61
|
db.run("DELETE FROM channel_inbound_events");
|
|
48
62
|
db.run("DELETE FROM messages");
|
|
@@ -223,6 +237,307 @@ describe("Discord delete propagation (unattributed)", () => {
|
|
|
223
237
|
expect(neutral!.deletedAt).toBeDefined();
|
|
224
238
|
});
|
|
225
239
|
|
|
240
|
+
test("deleting the assistant's own post stamps its row via the stored provider id", async () => {
|
|
241
|
+
// A moderator removing the assistant's reply: the post opened no inbound
|
|
242
|
+
// event, so resolution can only go through the provider id its row
|
|
243
|
+
// carries (stamped at reserve, reconciled at delivery). The retry window
|
|
244
|
+
// is not paid: no inbound event exists to wait on.
|
|
245
|
+
const chatId = "999888777666555444";
|
|
246
|
+
const sentId = "111222333444555777";
|
|
247
|
+
const minted = recordInbound("discord", chatId, "evt-earlier-user-msg");
|
|
248
|
+
const messageId = "assistant-reply-1";
|
|
249
|
+
getDb()
|
|
250
|
+
.insert(messages)
|
|
251
|
+
.values({
|
|
252
|
+
id: messageId,
|
|
253
|
+
conversationId: minted.conversationId,
|
|
254
|
+
role: "assistant",
|
|
255
|
+
content: "The reply a moderator later removed",
|
|
256
|
+
metadata: JSON.stringify({
|
|
257
|
+
assistantMessageChannel: "discord",
|
|
258
|
+
providerMeta: JSON.stringify({
|
|
259
|
+
source: "discord",
|
|
260
|
+
conversationExternalId: chatId,
|
|
261
|
+
messageId: sentId,
|
|
262
|
+
eventKind: "message",
|
|
263
|
+
}),
|
|
264
|
+
}),
|
|
265
|
+
createdAt: Date.now(),
|
|
266
|
+
})
|
|
267
|
+
.run();
|
|
268
|
+
|
|
269
|
+
const req = new Request("http://localhost:8080/channels/inbound", {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: {
|
|
272
|
+
"Content-Type": "application/json",
|
|
273
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
274
|
+
},
|
|
275
|
+
body: JSON.stringify({
|
|
276
|
+
sourceChannel: "discord",
|
|
277
|
+
interface: "discord",
|
|
278
|
+
conversationExternalId: chatId,
|
|
279
|
+
externalMessageId: `${sentId}:delete`,
|
|
280
|
+
eventKind: "delete",
|
|
281
|
+
content: "",
|
|
282
|
+
actorExternalId: "discord-system",
|
|
283
|
+
sourceMetadata: {
|
|
284
|
+
messageId: sentId,
|
|
285
|
+
actorUnattributed: true,
|
|
286
|
+
},
|
|
287
|
+
}),
|
|
288
|
+
});
|
|
289
|
+
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
290
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
291
|
+
|
|
292
|
+
expect(json.accepted).toBe(true);
|
|
293
|
+
expect(json.deleted).toBe(true);
|
|
294
|
+
expect(json.messageId).toBe(messageId);
|
|
295
|
+
|
|
296
|
+
const row = getDb()
|
|
297
|
+
.select()
|
|
298
|
+
.from(messages)
|
|
299
|
+
.where(eq(messages.id, messageId))
|
|
300
|
+
.get();
|
|
301
|
+
// Content survives: the stamp records loss of channel visibility, not an
|
|
302
|
+
// erasure of what the assistant said.
|
|
303
|
+
expect(row!.content).toBe("The reply a moderator later removed");
|
|
304
|
+
const neutral = readProviderMetadata(row!.metadata);
|
|
305
|
+
expect(neutral!.deletedAt).toBeDefined();
|
|
306
|
+
expect(neutral!.messageId).toBe(sentId);
|
|
307
|
+
expect(publishedConversationIds).toEqual([minted.conversationId]);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
test("deleting one post of a split reply keeps the surviving posts live", async () => {
|
|
311
|
+
const chatId = "999888777666555444";
|
|
312
|
+
const minted = recordInbound("discord", chatId, "evt-split-seed");
|
|
313
|
+
const primaryId = "111222333444555801";
|
|
314
|
+
const additionalId = "111222333444555802";
|
|
315
|
+
getDb()
|
|
316
|
+
.insert(messages)
|
|
317
|
+
.values({
|
|
318
|
+
id: "assistant-split-reply",
|
|
319
|
+
conversationId: minted.conversationId,
|
|
320
|
+
role: "assistant",
|
|
321
|
+
content: "part one and part two",
|
|
322
|
+
metadata: JSON.stringify({
|
|
323
|
+
providerMeta: JSON.stringify({
|
|
324
|
+
source: "discord",
|
|
325
|
+
conversationExternalId: chatId,
|
|
326
|
+
messageId: primaryId,
|
|
327
|
+
additionalMessageIds: [additionalId],
|
|
328
|
+
eventKind: "message",
|
|
329
|
+
}),
|
|
330
|
+
}),
|
|
331
|
+
createdAt: Date.now(),
|
|
332
|
+
})
|
|
333
|
+
.run();
|
|
334
|
+
|
|
335
|
+
const deleteReq = (postId: string) =>
|
|
336
|
+
new Request("http://localhost:8080/channels/inbound", {
|
|
337
|
+
method: "POST",
|
|
338
|
+
headers: {
|
|
339
|
+
"Content-Type": "application/json",
|
|
340
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
341
|
+
},
|
|
342
|
+
body: JSON.stringify({
|
|
343
|
+
sourceChannel: "discord",
|
|
344
|
+
interface: "discord",
|
|
345
|
+
conversationExternalId: chatId,
|
|
346
|
+
externalMessageId: `${postId}:delete`,
|
|
347
|
+
eventKind: "delete",
|
|
348
|
+
content: "",
|
|
349
|
+
actorExternalId: "discord-system",
|
|
350
|
+
sourceMetadata: { messageId: postId, actorUnattributed: true },
|
|
351
|
+
}),
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
// Deleting the ADDITIONAL post: tracked per id, the row stays visible.
|
|
355
|
+
const first = await handleChannelInbound(
|
|
356
|
+
deleteReq(additionalId),
|
|
357
|
+
undefined,
|
|
358
|
+
TEST_BEARER_TOKEN,
|
|
359
|
+
);
|
|
360
|
+
expect(((await first.json()) as Record<string, unknown>).deleted).toBe(
|
|
361
|
+
true,
|
|
362
|
+
);
|
|
363
|
+
const afterFirst = readProviderMetadata(
|
|
364
|
+
getDb()
|
|
365
|
+
.select()
|
|
366
|
+
.from(messages)
|
|
367
|
+
.where(eq(messages.id, "assistant-split-reply"))
|
|
368
|
+
.get()!.metadata,
|
|
369
|
+
);
|
|
370
|
+
expect(afterFirst!.deletedMessageIds).toEqual([additionalId]);
|
|
371
|
+
expect(afterFirst!.deletedAt).toBeUndefined();
|
|
372
|
+
|
|
373
|
+
// Deleting the PRIMARY post too: every post is gone, the row-level
|
|
374
|
+
// marker lands.
|
|
375
|
+
const second = await handleChannelInbound(
|
|
376
|
+
deleteReq(primaryId),
|
|
377
|
+
undefined,
|
|
378
|
+
TEST_BEARER_TOKEN,
|
|
379
|
+
);
|
|
380
|
+
expect(((await second.json()) as Record<string, unknown>).deleted).toBe(
|
|
381
|
+
true,
|
|
382
|
+
);
|
|
383
|
+
const afterSecond = readProviderMetadata(
|
|
384
|
+
getDb()
|
|
385
|
+
.select()
|
|
386
|
+
.from(messages)
|
|
387
|
+
.where(eq(messages.id, "assistant-split-reply"))
|
|
388
|
+
.get()!.metadata,
|
|
389
|
+
);
|
|
390
|
+
expect(new Set(afterSecond!.deletedMessageIds)).toEqual(
|
|
391
|
+
new Set([additionalId, primaryId]),
|
|
392
|
+
);
|
|
393
|
+
expect(afterSecond!.deletedAt).toBeDefined();
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("a delete racing the outbound id reconciliation resolves on retry", async () => {
|
|
397
|
+
_setDeleteLookupConfigForTests(3, 30);
|
|
398
|
+
const chatId = "999888777666555444";
|
|
399
|
+
const sentId = "111222333444555803";
|
|
400
|
+
const minted = recordInbound("discord", chatId, "evt-race-seed");
|
|
401
|
+
// The pre-send stamp: the envelope names no id yet, exactly the state a
|
|
402
|
+
// fast automated deletion races.
|
|
403
|
+
getDb()
|
|
404
|
+
.insert(messages)
|
|
405
|
+
.values({
|
|
406
|
+
id: "assistant-racing-reply",
|
|
407
|
+
conversationId: minted.conversationId,
|
|
408
|
+
role: "assistant",
|
|
409
|
+
content: "posted a moment ago",
|
|
410
|
+
metadata: JSON.stringify({
|
|
411
|
+
providerMeta: JSON.stringify({
|
|
412
|
+
source: "discord",
|
|
413
|
+
conversationExternalId: chatId,
|
|
414
|
+
eventKind: "message",
|
|
415
|
+
}),
|
|
416
|
+
}),
|
|
417
|
+
createdAt: Date.now(),
|
|
418
|
+
})
|
|
419
|
+
.run();
|
|
420
|
+
// The post-send reconciliation lands while the delete lookup is in its
|
|
421
|
+
// backoff window.
|
|
422
|
+
setTimeout(() => {
|
|
423
|
+
getDb()
|
|
424
|
+
.update(messages)
|
|
425
|
+
.set({
|
|
426
|
+
metadata: JSON.stringify({
|
|
427
|
+
providerMeta: JSON.stringify({
|
|
428
|
+
source: "discord",
|
|
429
|
+
conversationExternalId: chatId,
|
|
430
|
+
messageId: sentId,
|
|
431
|
+
eventKind: "message",
|
|
432
|
+
}),
|
|
433
|
+
}),
|
|
434
|
+
})
|
|
435
|
+
.where(eq(messages.id, "assistant-racing-reply"))
|
|
436
|
+
.run();
|
|
437
|
+
}, 45);
|
|
438
|
+
|
|
439
|
+
const resp = await handleChannelInbound(
|
|
440
|
+
new Request("http://localhost:8080/channels/inbound", {
|
|
441
|
+
method: "POST",
|
|
442
|
+
headers: {
|
|
443
|
+
"Content-Type": "application/json",
|
|
444
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
445
|
+
},
|
|
446
|
+
body: JSON.stringify({
|
|
447
|
+
sourceChannel: "discord",
|
|
448
|
+
interface: "discord",
|
|
449
|
+
conversationExternalId: chatId,
|
|
450
|
+
externalMessageId: `${sentId}:delete`,
|
|
451
|
+
eventKind: "delete",
|
|
452
|
+
content: "",
|
|
453
|
+
actorExternalId: "discord-system",
|
|
454
|
+
sourceMetadata: { messageId: sentId, actorUnattributed: true },
|
|
455
|
+
}),
|
|
456
|
+
}),
|
|
457
|
+
undefined,
|
|
458
|
+
TEST_BEARER_TOKEN,
|
|
459
|
+
);
|
|
460
|
+
expect(((await resp.json()) as Record<string, unknown>).deleted).toBe(true);
|
|
461
|
+
const meta = readProviderMetadata(
|
|
462
|
+
getDb()
|
|
463
|
+
.select()
|
|
464
|
+
.from(messages)
|
|
465
|
+
.where(eq(messages.id, "assistant-racing-reply"))
|
|
466
|
+
.get()!.metadata,
|
|
467
|
+
);
|
|
468
|
+
expect(meta!.deletedAt).toBeDefined();
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("deleting an old post beyond the recency window resolves via the deep scan", async () => {
|
|
472
|
+
const chatId = "999888777666555444";
|
|
473
|
+
const oldId = "111222333444555804";
|
|
474
|
+
const minted = recordInbound("discord", chatId, "evt-old-seed");
|
|
475
|
+
const db = getDb();
|
|
476
|
+
db.insert(messages)
|
|
477
|
+
.values({
|
|
478
|
+
id: "assistant-old-post",
|
|
479
|
+
conversationId: minted.conversationId,
|
|
480
|
+
role: "assistant",
|
|
481
|
+
content: "an old reply",
|
|
482
|
+
metadata: JSON.stringify({
|
|
483
|
+
providerMeta: JSON.stringify({
|
|
484
|
+
source: "discord",
|
|
485
|
+
conversationExternalId: chatId,
|
|
486
|
+
messageId: oldId,
|
|
487
|
+
eventKind: "message",
|
|
488
|
+
}),
|
|
489
|
+
}),
|
|
490
|
+
createdAt: 1_000_000,
|
|
491
|
+
})
|
|
492
|
+
.run();
|
|
493
|
+
// Bury it under more metadata-bearing rows than the recency-capped
|
|
494
|
+
// reaction lookup examines.
|
|
495
|
+
const insert = db.$client.prepare(
|
|
496
|
+
`INSERT INTO messages (id, conversation_id, role, content, created_at, metadata)
|
|
497
|
+
VALUES (?, ?, 'user', 'filler', ?, ?)`,
|
|
498
|
+
);
|
|
499
|
+
for (let i = 0; i < 450; i++) {
|
|
500
|
+
insert.run(
|
|
501
|
+
`filler-${i}`,
|
|
502
|
+
minted.conversationId,
|
|
503
|
+
2_000_000 + i,
|
|
504
|
+
JSON.stringify({
|
|
505
|
+
providerMeta: JSON.stringify({
|
|
506
|
+
source: "discord",
|
|
507
|
+
conversationExternalId: chatId,
|
|
508
|
+
messageId: `filler-post-${i}`,
|
|
509
|
+
eventKind: "message",
|
|
510
|
+
}),
|
|
511
|
+
}),
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const resp = await handleChannelInbound(
|
|
516
|
+
new Request("http://localhost:8080/channels/inbound", {
|
|
517
|
+
method: "POST",
|
|
518
|
+
headers: {
|
|
519
|
+
"Content-Type": "application/json",
|
|
520
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
521
|
+
},
|
|
522
|
+
body: JSON.stringify({
|
|
523
|
+
sourceChannel: "discord",
|
|
524
|
+
interface: "discord",
|
|
525
|
+
conversationExternalId: chatId,
|
|
526
|
+
externalMessageId: `${oldId}:delete`,
|
|
527
|
+
eventKind: "delete",
|
|
528
|
+
content: "",
|
|
529
|
+
actorExternalId: "discord-system",
|
|
530
|
+
sourceMetadata: { messageId: oldId, actorUnattributed: true },
|
|
531
|
+
}),
|
|
532
|
+
}),
|
|
533
|
+
undefined,
|
|
534
|
+
TEST_BEARER_TOKEN,
|
|
535
|
+
);
|
|
536
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
537
|
+
expect(json.deleted).toBe(true);
|
|
538
|
+
expect(json.messageId).toBe("assistant-old-post");
|
|
539
|
+
});
|
|
540
|
+
|
|
226
541
|
test("an unattributed delete for a never-ingested message is a no-op", async () => {
|
|
227
542
|
const req = new Request("http://localhost:8080/channels/inbound", {
|
|
228
543
|
method: "POST",
|
|
@@ -313,6 +628,160 @@ describe("Slack delete propagation", () => {
|
|
|
313
628
|
expect(slackMeta!.channelTs).toBe("1234.5678");
|
|
314
629
|
expect(slackMeta!.eventKind).toBe("message");
|
|
315
630
|
expect(slackMeta!.displayName).toBe("Test User");
|
|
631
|
+
// A client showing the conversation is told to refetch.
|
|
632
|
+
expect(publishedConversationIds).toEqual([seeded.conversationId]);
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
test("deleting the assistant's own Slack post is recorded without an actor", async () => {
|
|
636
|
+
// Slack names a deleted post's author and never who deleted it, so the
|
|
637
|
+
// gateway forwards a deletion of the assistant's own post unattributed
|
|
638
|
+
// (LUM-3521). The post opened no inbound event; the row resolves through
|
|
639
|
+
// the `channelTs` its Slack envelope carries, and the stamp lands on
|
|
640
|
+
// that envelope, where the Slack renderers read it.
|
|
641
|
+
const chatId = "C0123CHANNEL";
|
|
642
|
+
const postTs = "1725100000.000900";
|
|
643
|
+
const minted = recordInbound("slack", chatId, "evt-earlier-user-msg");
|
|
644
|
+
const messageId = "assistant-slack-reply-1";
|
|
645
|
+
getDb()
|
|
646
|
+
.insert(messages)
|
|
647
|
+
.values({
|
|
648
|
+
id: messageId,
|
|
649
|
+
conversationId: minted.conversationId,
|
|
650
|
+
role: "assistant",
|
|
651
|
+
content: "The reply that was later deleted from the thread",
|
|
652
|
+
metadata: JSON.stringify({
|
|
653
|
+
assistantMessageChannel: "slack",
|
|
654
|
+
slackMeta: writeSlackMetadata({
|
|
655
|
+
source: "slack",
|
|
656
|
+
channelId: chatId,
|
|
657
|
+
channelTs: postTs,
|
|
658
|
+
threadTs: "1725100000.000100",
|
|
659
|
+
eventKind: "message",
|
|
660
|
+
}),
|
|
661
|
+
}),
|
|
662
|
+
createdAt: Date.now(),
|
|
663
|
+
})
|
|
664
|
+
.run();
|
|
665
|
+
|
|
666
|
+
const req = new Request("http://localhost:8080/channels/inbound", {
|
|
667
|
+
method: "POST",
|
|
668
|
+
headers: {
|
|
669
|
+
"Content-Type": "application/json",
|
|
670
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
671
|
+
},
|
|
672
|
+
body: JSON.stringify({
|
|
673
|
+
sourceChannel: "slack",
|
|
674
|
+
interface: "slack",
|
|
675
|
+
conversationExternalId: chatId,
|
|
676
|
+
externalMessageId: "evt-del-own-post",
|
|
677
|
+
eventKind: "delete",
|
|
678
|
+
content: "",
|
|
679
|
+
actorExternalId: "slack-system",
|
|
680
|
+
sourceMetadata: {
|
|
681
|
+
messageId: postTs,
|
|
682
|
+
threadId: "1725100000.000100",
|
|
683
|
+
actorUnattributed: true,
|
|
684
|
+
},
|
|
685
|
+
}),
|
|
686
|
+
});
|
|
687
|
+
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
688
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
689
|
+
|
|
690
|
+
expect(json.accepted).toBe(true);
|
|
691
|
+
expect(json.deleted).toBe(true);
|
|
692
|
+
expect(json.messageId).toBe(messageId);
|
|
693
|
+
|
|
694
|
+
const row = getDb()
|
|
695
|
+
.select()
|
|
696
|
+
.from(messages)
|
|
697
|
+
.where(eq(messages.id, messageId))
|
|
698
|
+
.get();
|
|
699
|
+
// Content survives: the stamp records loss of channel visibility, not an
|
|
700
|
+
// erasure of what the assistant said.
|
|
701
|
+
expect(row!.content).toBe(
|
|
702
|
+
"The reply that was later deleted from the thread",
|
|
703
|
+
);
|
|
704
|
+
const parsed = JSON.parse(row!.metadata!) as Record<string, unknown>;
|
|
705
|
+
const slackMeta = readSlackMetadata(parsed.slackMeta as string);
|
|
706
|
+
expect(slackMeta!.deletedAt).toBeDefined();
|
|
707
|
+
expect(slackMeta!.channelTs).toBe(postTs);
|
|
708
|
+
expect(readProviderMetadata(row!.metadata)!.deletedAt).toBeDefined();
|
|
709
|
+
});
|
|
710
|
+
|
|
711
|
+
test("deleting the assistant's own Slack post stamps its neutral envelope per post", async () => {
|
|
712
|
+
// A Slack reply row carries the neutral envelope every channel writes,
|
|
713
|
+
// so its deletion takes the same per-post path as any channel's: the
|
|
714
|
+
// gateway forwards the delete unattributed (Slack names the author,
|
|
715
|
+
// never the deleter), the row resolves through the id its envelope
|
|
716
|
+
// names, and the Slack renderers read the stamp through the Slack view.
|
|
717
|
+
const chatId = "C0123CHANNEL";
|
|
718
|
+
const postTs = "1725100000.000900";
|
|
719
|
+
const minted = recordInbound("slack", chatId, "evt-earlier-user-msg");
|
|
720
|
+
const messageId = "assistant-slack-reply-1";
|
|
721
|
+
getDb()
|
|
722
|
+
.insert(messages)
|
|
723
|
+
.values({
|
|
724
|
+
id: messageId,
|
|
725
|
+
conversationId: minted.conversationId,
|
|
726
|
+
role: "assistant",
|
|
727
|
+
content: "The reply that was later deleted from the thread",
|
|
728
|
+
metadata: JSON.stringify({
|
|
729
|
+
assistantMessageChannel: "slack",
|
|
730
|
+
providerMeta: JSON.stringify({
|
|
731
|
+
source: "slack",
|
|
732
|
+
conversationExternalId: chatId,
|
|
733
|
+
messageId: postTs,
|
|
734
|
+
threadId: "1725100000.000100",
|
|
735
|
+
eventKind: "message",
|
|
736
|
+
timestampTimezone: "America/New_York",
|
|
737
|
+
}),
|
|
738
|
+
}),
|
|
739
|
+
createdAt: Date.now(),
|
|
740
|
+
})
|
|
741
|
+
.run();
|
|
742
|
+
|
|
743
|
+
const req = new Request("http://localhost:8080/channels/inbound", {
|
|
744
|
+
method: "POST",
|
|
745
|
+
headers: {
|
|
746
|
+
"Content-Type": "application/json",
|
|
747
|
+
"X-Gateway-Origin": TEST_BEARER_TOKEN,
|
|
748
|
+
},
|
|
749
|
+
body: JSON.stringify({
|
|
750
|
+
sourceChannel: "slack",
|
|
751
|
+
interface: "slack",
|
|
752
|
+
conversationExternalId: chatId,
|
|
753
|
+
externalMessageId: "evt-del-own-post",
|
|
754
|
+
eventKind: "delete",
|
|
755
|
+
content: "",
|
|
756
|
+
actorExternalId: "slack-system",
|
|
757
|
+
sourceMetadata: {
|
|
758
|
+
messageId: postTs,
|
|
759
|
+
threadId: "1725100000.000100",
|
|
760
|
+
actorUnattributed: true,
|
|
761
|
+
},
|
|
762
|
+
}),
|
|
763
|
+
});
|
|
764
|
+
const resp = await handleChannelInbound(req, undefined, TEST_BEARER_TOKEN);
|
|
765
|
+
const json = (await resp.json()) as Record<string, unknown>;
|
|
766
|
+
expect(json.accepted).toBe(true);
|
|
767
|
+
expect(json.deleted).toBe(true);
|
|
768
|
+
expect(json.messageId).toBe(messageId);
|
|
769
|
+
|
|
770
|
+
const row = getDb()
|
|
771
|
+
.select()
|
|
772
|
+
.from(messages)
|
|
773
|
+
.where(eq(messages.id, messageId))
|
|
774
|
+
.get();
|
|
775
|
+
expect(row!.content).toBe(
|
|
776
|
+
"The reply that was later deleted from the thread",
|
|
777
|
+
);
|
|
778
|
+
const neutral = readProviderMetadata(row!.metadata);
|
|
779
|
+
expect(neutral!.deletedMessageIds).toEqual([postTs]);
|
|
780
|
+
expect(neutral!.deletedAt).toBeDefined();
|
|
781
|
+
const view = readSlackMetadataFromMessageMetadata(row!.metadata);
|
|
782
|
+
expect(view!.channelTs).toBe(postTs);
|
|
783
|
+
expect(view!.deletedAt).toBe(neutral!.deletedAt);
|
|
784
|
+
expect(view!.timestampTimezone).toBe("America/New_York");
|
|
316
785
|
});
|
|
317
786
|
|
|
318
787
|
test("delete for unknown ts is a no-op", async () => {
|
|
@@ -92,6 +92,7 @@ function createSlackTurnContext(): MessagingConversationContext {
|
|
|
92
92
|
size: () => 0,
|
|
93
93
|
} as unknown as MessageQueue;
|
|
94
94
|
let processing = false;
|
|
95
|
+
let owner = 0;
|
|
95
96
|
return {
|
|
96
97
|
conversationId: "conv-dm-test",
|
|
97
98
|
messages: [],
|
|
@@ -99,6 +100,21 @@ function createSlackTurnContext(): MessagingConversationContext {
|
|
|
99
100
|
setProcessing: (value: boolean) => {
|
|
100
101
|
processing = value;
|
|
101
102
|
},
|
|
103
|
+
acquireProcessingFenced: async () => {
|
|
104
|
+
if (processing) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
processing = true;
|
|
108
|
+
owner += 1;
|
|
109
|
+
return owner;
|
|
110
|
+
},
|
|
111
|
+
releaseProcessing: (claim: number) => {
|
|
112
|
+
if (claim !== owner) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
processing = false;
|
|
116
|
+
return true;
|
|
117
|
+
},
|
|
102
118
|
abortController: null,
|
|
103
119
|
queue: queueStub,
|
|
104
120
|
getTurnChannelContext: () => channel,
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
existsSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
mkdtempSync,
|
|
7
|
+
readFileSync,
|
|
8
|
+
rmSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { basename, join } from "node:path";
|
|
13
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
const scriptPath = join(
|
|
16
|
+
import.meta.dir,
|
|
17
|
+
"..",
|
|
18
|
+
"..",
|
|
19
|
+
"docker-kata-apt-shims.sh",
|
|
20
|
+
);
|
|
21
|
+
const testRoots: string[] = [];
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
for (const root of testRoots.splice(0)) {
|
|
25
|
+
rmSync(root, { recursive: true, force: true });
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
function writeExecutable(path: string, contents: string): void {
|
|
30
|
+
writeFileSync(path, contents);
|
|
31
|
+
chmodSync(path, 0o755);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function createDataRoot(): string {
|
|
35
|
+
const dataRoot = mkdtempSync(join(tmpdir(), "kata-apt-shims-"));
|
|
36
|
+
testRoots.push(dataRoot);
|
|
37
|
+
for (const dir of [
|
|
38
|
+
"usr/bin",
|
|
39
|
+
"usr/sbin",
|
|
40
|
+
"usr/games",
|
|
41
|
+
"usr/local/bin",
|
|
42
|
+
"usr/local/sbin",
|
|
43
|
+
"var/lib/dpkg",
|
|
44
|
+
]) {
|
|
45
|
+
mkdirSync(join(dataRoot, dir), { recursive: true });
|
|
46
|
+
}
|
|
47
|
+
writeFileSync(join(dataRoot, "var/lib/dpkg/status"), "unchanged\n");
|
|
48
|
+
return dataRoot;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function refreshShims(dataRoot: string, pathPrefix?: string): void {
|
|
52
|
+
execFileSync("/bin/sh", [scriptPath], {
|
|
53
|
+
env: {
|
|
54
|
+
...process.env,
|
|
55
|
+
PATH: pathPrefix
|
|
56
|
+
? `${pathPrefix}:${process.env.PATH ?? ""}`
|
|
57
|
+
: process.env.PATH,
|
|
58
|
+
VELLUM_APT_DATA_ROOT: dataRoot,
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
describe("docker-kata-apt-shims", () => {
|
|
64
|
+
test.skipIf(process.platform !== "linux")(
|
|
65
|
+
"refreshes a shim when a same-version reinstall retargets its trampoline",
|
|
66
|
+
() => {
|
|
67
|
+
const dataRoot = createDataRoot();
|
|
68
|
+
|
|
69
|
+
const suffix = basename(dataRoot);
|
|
70
|
+
const firstTarget = `/vellum-apt-shim-test/${suffix}-1`;
|
|
71
|
+
const secondTarget = `/vellum-apt-shim-test/${suffix}-2`;
|
|
72
|
+
const source = join(dataRoot, "usr/bin/example-tool");
|
|
73
|
+
const shim = join(dataRoot, ".host-shims/example-tool");
|
|
74
|
+
|
|
75
|
+
for (const target of [firstTarget, secondTarget]) {
|
|
76
|
+
const chrootTarget = `${dataRoot}${target}`;
|
|
77
|
+
mkdirSync(join(chrootTarget, ".."), { recursive: true });
|
|
78
|
+
writeExecutable(chrootTarget, "#!/bin/sh\nexit 0\n");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
writeExecutable(source, `#!/bin/sh\nexec ${firstTarget} "$@"\n`);
|
|
82
|
+
refreshShims(dataRoot);
|
|
83
|
+
expect(readFileSync(shim, "utf8")).toContain(firstTarget);
|
|
84
|
+
|
|
85
|
+
writeExecutable(source, `#!/bin/sh\nexec ${secondTarget} "$@"\n`);
|
|
86
|
+
const fakeBin = join(dataRoot, "fake-bin");
|
|
87
|
+
const failedOnce = join(dataRoot, "mv-failed-once");
|
|
88
|
+
mkdirSync(fakeBin);
|
|
89
|
+
writeExecutable(
|
|
90
|
+
join(fakeBin, "mv"),
|
|
91
|
+
`#!/bin/sh
|
|
92
|
+
if [ ! -e "${failedOnce}" ]; then
|
|
93
|
+
touch "${failedOnce}"
|
|
94
|
+
exit 91
|
|
95
|
+
fi
|
|
96
|
+
[ -f "$3" ] || exit 92
|
|
97
|
+
exec /bin/mv "$@"
|
|
98
|
+
`,
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
expect(() => refreshShims(dataRoot, fakeBin)).toThrow();
|
|
102
|
+
expect(readFileSync(shim, "utf8")).toContain(firstTarget);
|
|
103
|
+
refreshShims(dataRoot, fakeBin);
|
|
104
|
+
|
|
105
|
+
const refreshedShim = readFileSync(shim, "utf8");
|
|
106
|
+
expect(refreshedShim).toContain(secondTarget);
|
|
107
|
+
expect(refreshedShim).not.toContain(firstTarget);
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
test.skipIf(process.platform !== "linux")(
|
|
112
|
+
"ignores non-executable entries and refreshes when they become runnable",
|
|
113
|
+
() => {
|
|
114
|
+
const dataRoot = createDataRoot();
|
|
115
|
+
const suffix = basename(dataRoot);
|
|
116
|
+
const target = `/vellum-apt-shim-test/${suffix}`;
|
|
117
|
+
const chrootTarget = `${dataRoot}${target}`;
|
|
118
|
+
const lowerSource = join(dataRoot, "usr/sbin/example-tool");
|
|
119
|
+
const higherSource = join(dataRoot, "usr/local/bin/example-tool");
|
|
120
|
+
const shim = join(dataRoot, ".host-shims/example-tool");
|
|
121
|
+
|
|
122
|
+
mkdirSync(join(chrootTarget, ".."), { recursive: true });
|
|
123
|
+
writeExecutable(chrootTarget, "#!/bin/sh\nexit 0\n");
|
|
124
|
+
writeExecutable(lowerSource, `#!/bin/sh\nexec ${target} "$@"\n`);
|
|
125
|
+
writeFileSync(higherSource, "#!/bin/sh\nexit 0\n", { mode: 0o644 });
|
|
126
|
+
|
|
127
|
+
refreshShims(dataRoot);
|
|
128
|
+
expect(readFileSync(shim, "utf8")).toContain(target);
|
|
129
|
+
|
|
130
|
+
chmodSync(higherSource, 0o755);
|
|
131
|
+
refreshShims(dataRoot);
|
|
132
|
+
expect(existsSync(shim)).toBe(false);
|
|
133
|
+
},
|
|
134
|
+
);
|
|
135
|
+
});
|
|
@@ -34,11 +34,23 @@ describe("forbidden legacy symbols", () => {
|
|
|
34
34
|
"twilio_webhook_config",
|
|
35
35
|
"calls.webhookBaseUrl",
|
|
36
36
|
];
|
|
37
|
+
// This pattern is POSIX ERE for `git grep -E`, not a JavaScript RegExp:
|
|
38
|
+
// ERE has no `\x` escapes, so `RegExp.escape` would turn the pattern
|
|
39
|
+
// into one that matches nothing and the test would pass empty.
|
|
37
40
|
const escapedPattern = forbiddenSymbols
|
|
38
41
|
.map((symbol) => symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
|
|
39
42
|
.join("|");
|
|
40
43
|
|
|
41
44
|
const repoRoot = resolve(__dirname, "..", "..", "..");
|
|
45
|
+
// Positive control: the pattern must still match the symbols as written
|
|
46
|
+
// in this very file, so a broken pattern fails loudly instead of
|
|
47
|
+
// matching nothing.
|
|
48
|
+
const selfMatches = execSync(
|
|
49
|
+
`git grep -c -E "${escapedPattern}" -- "assistant/src/__tests__/forbidden-legacy-symbols.test.ts"`,
|
|
50
|
+
{ cwd: repoRoot, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] },
|
|
51
|
+
);
|
|
52
|
+
expect(selfMatches.trim()).not.toBe("");
|
|
53
|
+
|
|
42
54
|
let matches = "";
|
|
43
55
|
try {
|
|
44
56
|
// Use git grep so only tracked files are searched. This automatically
|