@vellumai/assistant 0.8.11 → 0.8.12-staging.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/ARCHITECTURE.md +15 -17
- package/README.md +0 -6
- package/bun.lock +6 -122
- package/node_modules/@vellumai/gateway-client/bun.lock +1 -0
- package/node_modules/@vellumai/gateway-client/package.json +3 -1
- package/node_modules/@vellumai/gateway-client/src/__tests__/gateway-client.test.ts +1 -1
- package/node_modules/@vellumai/gateway-client/src/gateway-ipc-contracts.ts +87 -0
- package/node_modules/@vellumai/gateway-client/src/index.ts +3 -5
- package/openapi.yaml +633 -4
- package/package.json +1 -3
- package/src/__tests__/adaptive-thinking-repair.test.ts +185 -0
- package/src/__tests__/agent-loop-compaction-events.test.ts +7 -6
- package/src/__tests__/anthropic-provider.test.ts +129 -0
- package/src/__tests__/background-workers-disk-pressure.test.ts +4 -1
- package/src/__tests__/btw-routes.test.ts +7 -34
- package/src/__tests__/checker.test.ts +6 -12
- package/src/__tests__/config-loader-backfill.test.ts +4 -2
- package/src/__tests__/config-loader-quarantine-notice.test.ts +167 -0
- package/src/__tests__/config-watcher.test.ts +2 -2
- package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +1 -1
- package/src/__tests__/conversation-error.test.ts +2 -6
- package/src/__tests__/conversation-history-web-search.test.ts +8 -0
- package/src/__tests__/conversation-title-service.test.ts +2 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -1
- package/src/__tests__/disk-pressure-tools.test.ts +1 -1
- package/src/__tests__/exploration-drift-hook.test.ts +692 -0
- package/src/__tests__/filing-service.test.ts +8 -3
- package/src/__tests__/guardian-action-store.test.ts +0 -167
- package/src/__tests__/handlers-skills-memory-v2-reseed.test.ts +1 -1
- package/src/__tests__/heartbeat-disk-pressure.test.ts +4 -1
- package/src/__tests__/heartbeat-service.test.ts +5 -2
- package/src/__tests__/identity-intro-cache.test.ts +12 -5
- package/src/__tests__/identity-routes.test.ts +16 -57
- package/src/__tests__/injector-chain.test.ts +8 -3
- package/src/__tests__/injector-config-quarantine-notice.test.ts +115 -0
- package/src/__tests__/llm-catalog-parity.test.ts +16 -0
- package/src/__tests__/llm-usage-store.test.ts +11 -0
- package/src/__tests__/log-export-workspace.test.ts +468 -3
- package/src/__tests__/memory-v2-static-injector.test.ts +22 -0
- package/src/__tests__/model-intents.test.ts +1 -1
- package/src/__tests__/oauth-cli.test.ts +19 -8
- package/src/__tests__/openai-provider.test.ts +34 -0
- package/src/__tests__/prechat-onboarding-contract.test.ts +0 -1
- package/src/__tests__/recurrence-engine.test.ts +45 -0
- package/src/__tests__/schedule-routes.test.ts +34 -0
- package/src/__tests__/scheduler-disk-pressure.test.ts +1 -1
- package/src/__tests__/script-proxy-conversation-manager.test.ts +10 -5
- package/src/__tests__/secret-fixtures.ts +20 -0
- package/src/__tests__/skill-tool-factory.test.ts +49 -0
- package/src/__tests__/subagent-role-registry.test.ts +24 -1
- package/src/__tests__/subagent-tools.test.ts +1 -0
- package/src/__tests__/system-prompt.test.ts +109 -11
- package/src/__tests__/tool-approval-handler.test.ts +85 -0
- package/src/__tests__/tool-audit-listener.test.ts +86 -0
- package/src/__tests__/tool-error-hook.test.ts +1 -0
- package/src/__tests__/tool-result-spool.test.ts +337 -0
- package/src/__tests__/tool-result-truncate-hook.test.ts +1 -0
- package/src/__tests__/validate-input.test.ts +95 -1
- package/src/__tests__/workspace-migration-098-remove-stale-updates-bulletin-file.test.ts +65 -0
- package/src/__tests__/workspace-migration-099-disable-cache-one-shot-callsites.test.ts +139 -0
- package/src/__tests__/workspace-migration-100-upgrade-quality-profile-to-fable-5.test.ts +174 -0
- package/src/__tests__/workspace-migration-101-upgrade-balanced-economy-to-minimax-m3.test.ts +162 -0
- package/src/__tests__/workspace-release-notes-feature-flag-guard.test.ts +45 -95
- package/src/acp/__tests__/agent-process.test.ts +315 -2
- package/src/acp/__tests__/prepare-agent-env.test.ts +79 -5
- package/src/acp/agent-process.ts +163 -34
- package/src/acp/prepare-agent-env.ts +55 -15
- package/src/agent/loop.ts +81 -24
- package/src/api/events/usage-progress.ts +28 -0
- package/src/api/index.ts +6 -0
- package/src/background-wake/wake-intent-hooks.test.ts +2 -0
- package/src/bundler/app-bundler.ts +25 -42
- package/src/bundler/app-compiler.ts +8 -0
- package/src/calls/call-controller.ts +1 -1
- package/src/cli/commands/plugins.ts +248 -15
- package/src/cli/lib/__tests__/inspect-plugin.test.ts +318 -0
- package/src/cli/lib/__tests__/install-from-github.test.ts +16 -9
- package/src/cli/lib/__tests__/plugin-artifact.test.ts +183 -0
- package/src/cli/lib/__tests__/plugin-details.test.ts +158 -0
- package/src/cli/lib/__tests__/plugin-fingerprint.test.ts +245 -0
- package/src/cli/lib/__tests__/upgrade-plugin.test.ts +307 -0
- package/src/cli/lib/inspect-plugin.ts +252 -0
- package/src/cli/lib/install-from-github.ts +214 -21
- package/src/cli/lib/list-installed-plugins.ts +17 -6
- package/src/cli/lib/plugin-artifact.ts +103 -0
- package/src/cli/lib/plugin-details.ts +18 -1
- package/src/cli/lib/plugin-fingerprint.ts +197 -0
- package/src/cli/lib/upgrade-plugin.ts +225 -0
- package/src/config/bundled-skills/subagent/SKILL.md +2 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +8 -2
- package/src/config/call-site-defaults.ts +13 -2
- package/src/config/feature-flag-registry.json +8 -16
- package/src/config/loader.ts +52 -59
- package/src/config/schema.ts +0 -2
- package/src/config/schemas/__tests__/memory-v2.test.ts +1 -0
- package/src/config/schemas/__tests__/memory-v3.test.ts +10 -0
- package/src/config/schemas/llm.ts +10 -0
- package/src/config/schemas/memory-v2.ts +13 -0
- package/src/config/schemas/memory-v3.ts +92 -0
- package/src/config/seed-inference-profiles.ts +4 -8
- package/src/context/post-turn-tool-result-truncation.ts +32 -18
- package/src/context/tool-result-spool.ts +104 -0
- package/src/credential-execution/feature-gates.ts +0 -1
- package/src/daemon/conversation-agent-loop-handlers.ts +41 -16
- package/src/daemon/conversation-error.ts +6 -15
- package/src/daemon/conversation.ts +9 -0
- package/src/daemon/disk-pressure-policy.ts +0 -1
- package/src/daemon/lifecycle.ts +1 -20
- package/src/daemon/message-types/conversations.ts +2 -15
- package/src/daemon/trust-context.ts +1 -1
- package/src/events/tool-audit-listener.ts +40 -9
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +1 -1
- package/src/home/__tests__/home-content-refresh.test.ts +114 -0
- package/src/home/__tests__/suggested-prompts.test.ts +86 -5
- package/src/home/home-content-refresh.ts +43 -31
- package/src/home/home-greeting-cache.ts +8 -1
- package/src/home/home-greeting.ts +13 -9
- package/src/home/suggested-prompts.ts +77 -24
- package/src/ipc/routes/trust-rules.test.ts +66 -72
- package/src/media/image-credentials.ts +2 -2
- package/src/memory/__tests__/compaction-log-store-clickhouse.test.ts +432 -0
- package/src/memory/{compaction-log-writer-clickhouse.ts → compaction-log-store-clickhouse.ts} +264 -55
- package/src/memory/conversation-attention-store.ts +1 -0
- package/src/memory/conversation-bootstrap.ts +18 -9
- package/src/memory/conversation-crud.ts +12 -2
- package/src/memory/conversation-title-service.ts +53 -9
- package/src/memory/delivery-channels.ts +0 -69
- package/src/memory/graph/extraction-job.ts +0 -15
- package/src/memory/guardian-action-store.ts +1 -376
- package/src/memory/llm-usage-store.ts +5 -1
- package/src/memory/migrations/181-rename-thread-starters-checkpoints.ts +2 -2
- package/src/memory/v2/__tests__/consolidation-job.test.ts +183 -2
- package/src/memory/v2/__tests__/injection.test.ts +70 -0
- package/src/memory/v2/__tests__/static-context.test.ts +12 -0
- package/src/memory/v2/consolidation-job.ts +93 -9
- package/src/memory/v2/injection.ts +53 -0
- package/src/memory/v2/prompts/consolidation.ts +1 -0
- package/src/memory/v2/static-context.ts +13 -1
- package/src/memory/v2/sweep-job.ts +1 -1
- package/src/memory/v2/types.ts +5 -0
- package/src/plugin-api/types.ts +7 -0
- package/src/plugins/defaults/exploration-drift/hooks/post-tool-use.ts +300 -0
- package/src/plugins/defaults/exploration-drift/package.json +15 -0
- package/src/plugins/defaults/index.ts +25 -0
- package/src/plugins/defaults/memory-retrieval/injectors.ts +132 -4
- package/src/plugins/defaults/memory-v3-shadow/__tests__/card.test.ts +92 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/fresh-set.test.ts +52 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +1 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/live-integration.test.ts +2 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/orchestrate.test.ts +136 -5
- package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +17 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +6 -0
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +5 -1
- package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +68 -4
- package/src/plugins/defaults/memory-v3-shadow/card.ts +49 -5
- package/src/plugins/defaults/memory-v3-shadow/fresh-set.ts +59 -0
- package/src/plugins/defaults/memory-v3-shadow/injector.ts +4 -2
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.test.ts +169 -0
- package/src/plugins/defaults/memory-v3-shadow/learned-edges.ts +178 -0
- package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +115 -26
- package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +13 -9
- package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +144 -22
- package/src/plugins/defaults/memory-v3-shadow/types.ts +24 -6
- package/src/plugins/defaults/title-generate/hooks/stop.ts +13 -0
- package/src/plugins/defaults/title-generate/hooks/user-prompt-submit.ts +16 -0
- package/src/prompts/cache-boundary.ts +17 -0
- package/src/prompts/sections.ts +50 -17
- package/src/prompts/system-prompt.ts +12 -4
- package/src/prompts/templates/system-sections.ts +22 -0
- package/src/providers/__tests__/unparseable-tool-args.test.ts +53 -0
- package/src/providers/anthropic/client.ts +74 -28
- package/src/providers/gemini/client.ts +5 -1
- package/src/providers/minimax/client.ts +9 -0
- package/src/providers/model-catalog.ts +28 -0
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/chat-completions-provider.ts +4 -2
- package/src/providers/openai/responses-provider.ts +7 -2
- package/src/providers/retry.ts +8 -0
- package/src/providers/types.ts +11 -0
- package/src/providers/unparseable-tool-args.ts +56 -0
- package/src/runtime/AGENTS.md +6 -0
- package/src/runtime/__tests__/agent-wake.test.ts +2 -2
- package/src/runtime/agent-wake.ts +5 -5
- package/src/runtime/background-job-runner.ts +2 -2
- package/src/runtime/migrations/__tests__/vbundle-legacy-user-md.test.ts +150 -3
- package/src/runtime/migrations/vbundle-import-analyzer.ts +29 -6
- package/src/runtime/migrations/vbundle-import-policy.ts +23 -0
- package/src/runtime/migrations/vbundle-importer.ts +9 -4
- package/src/runtime/migrations/vbundle-streaming-importer.ts +8 -3
- package/src/runtime/pre-first-message-gate.ts +1 -1
- package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +241 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +132 -0
- package/src/runtime/routes/__tests__/gateway-log-routes.test.ts +97 -185
- package/src/runtime/routes/__tests__/home-feed-routes.test.ts +17 -0
- package/src/runtime/routes/__tests__/plugins-routes.test.ts +348 -0
- package/src/runtime/routes/__tests__/task-routes.test.ts +3 -3
- package/src/runtime/routes/btw-routes.ts +0 -14
- package/src/runtime/routes/conversation-compaction-routes.ts +86 -19
- package/src/runtime/routes/conversation-list-routes.ts +77 -5
- package/src/runtime/routes/conversation-management-routes.ts +54 -0
- package/src/runtime/routes/conversation-query-routes.ts +79 -4
- package/src/runtime/routes/gateway-log-routes.ts +14 -64
- package/src/runtime/routes/home-feed-routes.ts +10 -0
- package/src/runtime/routes/identity-intro-cache.ts +1 -1
- package/src/runtime/routes/identity-routes.ts +76 -20
- package/src/runtime/routes/inbound-message-handler.ts +0 -36
- package/src/runtime/routes/log-export-routes.ts +143 -96
- package/src/runtime/routes/plugins-routes.ts +380 -0
- package/src/runtime/routes/redact-staged-export.ts +259 -0
- package/src/runtime/routes/schedule-routes.ts +19 -2
- package/src/runtime/routes/trust-rules-routes.ts +14 -67
- package/src/schedule/recurrence-engine.ts +34 -0
- package/src/schedule/scheduler.ts +1 -0
- package/src/security/redact-json.ts +61 -0
- package/src/skills/validate-input.ts +41 -1
- package/src/subagent/types.ts +26 -1
- package/src/telemetry/types.ts +15 -1
- package/src/telemetry/usage-telemetry-reporter.test.ts +6 -1
- package/src/telemetry/usage-telemetry-reporter.ts +1 -0
- package/src/tools/apps/executors.ts +1 -1
- package/src/tools/skills/skill-tool-factory.ts +19 -8
- package/src/tools/tool-approval-handler.ts +31 -0
- package/src/usage/types.ts +8 -1
- package/src/util/platform.ts +16 -0
- package/src/watcher/engine.ts +1 -0
- package/src/workspace/adaptive-thinking-repair.ts +113 -0
- package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +70 -67
- package/src/workspace/migrations/098-remove-stale-updates-bulletin-file.ts +31 -0
- package/src/workspace/migrations/099-disable-cache-one-shot-callsites.ts +81 -0
- package/src/workspace/migrations/100-upgrade-quality-profile-to-fable-5.ts +86 -0
- package/src/workspace/migrations/101-upgrade-balanced-economy-to-minimax-m3.ts +70 -0
- package/src/workspace/migrations/registry.ts +8 -0
- package/src/__tests__/config-loader-quarantine-bulletin.test.ts +0 -202
- package/src/__tests__/conversation-starters-cadence.test.ts +0 -161
- package/src/__tests__/guardian-action-followup-executor.test.ts +0 -322
- package/src/__tests__/guardian-action-followup-store.test.ts +0 -373
- package/src/__tests__/guardian-action-late-reply.test.ts +0 -1083
- package/src/__tests__/update-bulletin-job.test.ts +0 -292
- package/src/config/schemas/updates.ts +0 -14
- package/src/memory/__tests__/compaction-log-writer-clickhouse.test.ts +0 -227
- package/src/memory/conversation-starters-cadence.ts +0 -78
- package/src/prompts/update-bulletin-job.ts +0 -180
- package/src/runtime/guardian-action-followup-executor.ts +0 -306
|
@@ -1,1083 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, mock, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
mock.module("../util/logger.js", () => ({
|
|
4
|
-
getLogger: () =>
|
|
5
|
-
new Proxy({} as Record<string, unknown>, {
|
|
6
|
-
get: () => () => {},
|
|
7
|
-
}),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
mock.module("../runtime/gateway-client.js", () => ({
|
|
11
|
-
deliverChannelReply: async () => {},
|
|
12
|
-
}));
|
|
13
|
-
|
|
14
|
-
import { isTerminalState } from "../calls/call-state-machine.js";
|
|
15
|
-
import {
|
|
16
|
-
createCallSession,
|
|
17
|
-
createPendingQuestion,
|
|
18
|
-
getCallSession,
|
|
19
|
-
updateCallSession,
|
|
20
|
-
} from "../calls/call-store.js";
|
|
21
|
-
import { getDb } from "../memory/db-connection.js";
|
|
22
|
-
import { initializeDb } from "../memory/db-init.js";
|
|
23
|
-
import {
|
|
24
|
-
createGuardianActionDelivery,
|
|
25
|
-
createGuardianActionRequest,
|
|
26
|
-
expireGuardianActionRequest,
|
|
27
|
-
getExpiredDeliveriesByConversation,
|
|
28
|
-
getExpiredDeliveriesByDestination,
|
|
29
|
-
getFollowupDeliveriesByConversation,
|
|
30
|
-
getGuardianActionRequest,
|
|
31
|
-
getPendingDeliveriesByConversation,
|
|
32
|
-
getPendingRequestByCallSessionId,
|
|
33
|
-
resolveGuardianActionRequest,
|
|
34
|
-
startFollowupFromExpiredRequest,
|
|
35
|
-
supersedeGuardianActionRequest,
|
|
36
|
-
updateDeliveryStatus,
|
|
37
|
-
} from "../memory/guardian-action-store.js";
|
|
38
|
-
import { conversations } from "../memory/schema.js";
|
|
39
|
-
|
|
40
|
-
initializeDb();
|
|
41
|
-
|
|
42
|
-
function ensureConversation(id: string): void {
|
|
43
|
-
const db = getDb();
|
|
44
|
-
const now = Date.now();
|
|
45
|
-
db.insert(conversations)
|
|
46
|
-
.values({
|
|
47
|
-
id,
|
|
48
|
-
title: `Conversation ${id}`,
|
|
49
|
-
createdAt: now,
|
|
50
|
-
updatedAt: now,
|
|
51
|
-
})
|
|
52
|
-
.run();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function resetTables(): void {
|
|
56
|
-
const db = getDb();
|
|
57
|
-
db.run("DELETE FROM guardian_action_deliveries");
|
|
58
|
-
db.run("DELETE FROM guardian_action_requests");
|
|
59
|
-
db.run("DELETE FROM call_pending_questions");
|
|
60
|
-
db.run("DELETE FROM call_events");
|
|
61
|
-
db.run("DELETE FROM call_sessions");
|
|
62
|
-
db.run("DELETE FROM messages");
|
|
63
|
-
db.run("DELETE FROM conversations");
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function createExpiredRequest(
|
|
67
|
-
convId: string,
|
|
68
|
-
opts?: { chatId?: string; externalUserId?: string; conversationId?: string },
|
|
69
|
-
) {
|
|
70
|
-
ensureConversation(convId);
|
|
71
|
-
const session = createCallSession({
|
|
72
|
-
conversationId: convId,
|
|
73
|
-
provider: "twilio",
|
|
74
|
-
fromNumber: "+15550001111",
|
|
75
|
-
toNumber: "+15550002222",
|
|
76
|
-
});
|
|
77
|
-
const pq = createPendingQuestion(session.id, "What is the gate code?");
|
|
78
|
-
const request = createGuardianActionRequest({
|
|
79
|
-
kind: "ask_guardian",
|
|
80
|
-
sourceChannel: "phone",
|
|
81
|
-
sourceConversationId: convId,
|
|
82
|
-
callSessionId: session.id,
|
|
83
|
-
pendingQuestionId: pq.id,
|
|
84
|
-
questionText: pq.questionText,
|
|
85
|
-
expiresAt: Date.now() - 10_000, // already expired
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
// Create delivery
|
|
89
|
-
const deliveryConvId = opts?.conversationId ?? `delivery-conv-${request.id}`;
|
|
90
|
-
if (opts?.conversationId) {
|
|
91
|
-
ensureConversation(opts.conversationId);
|
|
92
|
-
} else {
|
|
93
|
-
ensureConversation(deliveryConvId);
|
|
94
|
-
}
|
|
95
|
-
const delivery = createGuardianActionDelivery({
|
|
96
|
-
requestId: request.id,
|
|
97
|
-
destinationChannel: "telegram",
|
|
98
|
-
destinationChatId: opts?.chatId ?? "chat-123",
|
|
99
|
-
destinationExternalUserId: opts?.externalUserId ?? "user-456",
|
|
100
|
-
destinationConversationId: deliveryConvId,
|
|
101
|
-
});
|
|
102
|
-
updateDeliveryStatus(delivery.id, "sent");
|
|
103
|
-
|
|
104
|
-
// Expire the request and delivery
|
|
105
|
-
expireGuardianActionRequest(request.id, "sweep_timeout");
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
request: getGuardianActionRequest(request.id)!,
|
|
109
|
-
delivery,
|
|
110
|
-
deliveryConvId,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
describe("guardian-action-late-reply", () => {
|
|
115
|
-
beforeEach(() => {
|
|
116
|
-
resetTables();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ── getExpiredDeliveriesByDestination ──────────────────────────────
|
|
120
|
-
|
|
121
|
-
test("getExpiredDeliveriesByDestination returns expired deliveries for follow-up eligible requests", () => {
|
|
122
|
-
const { request } = createExpiredRequest("conv-late-1", {
|
|
123
|
-
chatId: "chat-abc",
|
|
124
|
-
externalUserId: "user-xyz",
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
const deliveries = getExpiredDeliveriesByDestination(
|
|
128
|
-
"telegram",
|
|
129
|
-
"chat-abc",
|
|
130
|
-
);
|
|
131
|
-
expect(deliveries).toHaveLength(1);
|
|
132
|
-
expect(deliveries[0].requestId).toBe(request.id);
|
|
133
|
-
expect(deliveries[0].status).toBe("expired");
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
test("getExpiredDeliveriesByDestination returns empty for non-matching channel", () => {
|
|
137
|
-
createExpiredRequest("conv-late-2", { chatId: "chat-abc" });
|
|
138
|
-
|
|
139
|
-
const deliveries = getExpiredDeliveriesByDestination("phone", "chat-abc");
|
|
140
|
-
expect(deliveries).toHaveLength(0);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
test("getExpiredDeliveriesByDestination returns empty when followup already started", () => {
|
|
144
|
-
const { request } = createExpiredRequest("conv-late-3", {
|
|
145
|
-
chatId: "chat-started",
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
// Start a follow-up, transitioning followup_state from 'none' to 'awaiting_guardian_choice'
|
|
149
|
-
startFollowupFromExpiredRequest(request.id, "late answer text");
|
|
150
|
-
|
|
151
|
-
const deliveries = getExpiredDeliveriesByDestination(
|
|
152
|
-
"telegram",
|
|
153
|
-
"chat-started",
|
|
154
|
-
);
|
|
155
|
-
expect(deliveries).toHaveLength(0);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
// ── getExpiredDeliveriesByConversation (singular-to-plural migration) ──
|
|
159
|
-
|
|
160
|
-
test("getExpiredDeliveriesByConversation returns expired delivery for mac channel", () => {
|
|
161
|
-
const { delivery, deliveryConvId } = createExpiredRequest("conv-late-4", {
|
|
162
|
-
conversationId: "mac-conv-1",
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const found = getExpiredDeliveriesByConversation(deliveryConvId);
|
|
166
|
-
expect(found).toHaveLength(1);
|
|
167
|
-
expect(found[0].id).toBe(delivery.id);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
test("getExpiredDeliveriesByConversation returns empty for non-matching conversation", () => {
|
|
171
|
-
createExpiredRequest("conv-late-5", { conversationId: "mac-conv-2" });
|
|
172
|
-
|
|
173
|
-
const found = getExpiredDeliveriesByConversation("nonexistent-conv");
|
|
174
|
-
expect(found).toHaveLength(0);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
test("getExpiredDeliveriesByConversation returns empty when followup already started", () => {
|
|
178
|
-
const { request, deliveryConvId } = createExpiredRequest("conv-late-6", {
|
|
179
|
-
conversationId: "mac-conv-3",
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
startFollowupFromExpiredRequest(request.id, "already answered");
|
|
183
|
-
|
|
184
|
-
const found = getExpiredDeliveriesByConversation(deliveryConvId);
|
|
185
|
-
expect(found).toHaveLength(0);
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
// ── startFollowupFromExpiredRequest ───────────────────────────────
|
|
189
|
-
|
|
190
|
-
test("startFollowupFromExpiredRequest transitions to awaiting_guardian_choice and records late answer", () => {
|
|
191
|
-
const { request } = createExpiredRequest("conv-late-7");
|
|
192
|
-
|
|
193
|
-
const updated = startFollowupFromExpiredRequest(
|
|
194
|
-
request.id,
|
|
195
|
-
"The gate code is 1234",
|
|
196
|
-
);
|
|
197
|
-
expect(updated).not.toBeNull();
|
|
198
|
-
expect(updated!.followupState).toBe("awaiting_guardian_choice");
|
|
199
|
-
expect(updated!.lateAnswerText).toBe("The gate code is 1234");
|
|
200
|
-
expect(updated!.lateAnsweredAt).toBeGreaterThan(0);
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
test("startFollowupFromExpiredRequest returns null if followup already started", () => {
|
|
204
|
-
const { request } = createExpiredRequest("conv-late-8");
|
|
205
|
-
|
|
206
|
-
// First call succeeds
|
|
207
|
-
const first = startFollowupFromExpiredRequest(request.id, "answer 1");
|
|
208
|
-
expect(first).not.toBeNull();
|
|
209
|
-
|
|
210
|
-
// Second call fails — already in awaiting_guardian_choice
|
|
211
|
-
const second = startFollowupFromExpiredRequest(request.id, "answer 2");
|
|
212
|
-
expect(second).toBeNull();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
test("startFollowupFromExpiredRequest returns null for pending requests (not expired)", () => {
|
|
216
|
-
const convId = "conv-late-9";
|
|
217
|
-
ensureConversation(convId);
|
|
218
|
-
const session = createCallSession({
|
|
219
|
-
conversationId: convId,
|
|
220
|
-
provider: "twilio",
|
|
221
|
-
fromNumber: "+15550001111",
|
|
222
|
-
toNumber: "+15550002222",
|
|
223
|
-
});
|
|
224
|
-
const pq = createPendingQuestion(session.id, "Still pending question");
|
|
225
|
-
const request = createGuardianActionRequest({
|
|
226
|
-
kind: "ask_guardian",
|
|
227
|
-
sourceChannel: "phone",
|
|
228
|
-
sourceConversationId: convId,
|
|
229
|
-
callSessionId: session.id,
|
|
230
|
-
pendingQuestionId: pq.id,
|
|
231
|
-
questionText: pq.questionText,
|
|
232
|
-
expiresAt: Date.now() + 60_000, // not expired
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
const result = startFollowupFromExpiredRequest(request.id, "late answer");
|
|
236
|
-
expect(result).toBeNull();
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
// ── Follow-up flow for already-answered requests ──────────────────
|
|
240
|
-
|
|
241
|
-
test("already-answered requests do not appear in expired delivery queries", () => {
|
|
242
|
-
const convId = "conv-late-10";
|
|
243
|
-
ensureConversation(convId);
|
|
244
|
-
const session = createCallSession({
|
|
245
|
-
conversationId: convId,
|
|
246
|
-
provider: "twilio",
|
|
247
|
-
fromNumber: "+15550001111",
|
|
248
|
-
toNumber: "+15550002222",
|
|
249
|
-
});
|
|
250
|
-
const pq = createPendingQuestion(session.id, "Already answered question");
|
|
251
|
-
const request = createGuardianActionRequest({
|
|
252
|
-
kind: "ask_guardian",
|
|
253
|
-
sourceChannel: "phone",
|
|
254
|
-
sourceConversationId: convId,
|
|
255
|
-
callSessionId: session.id,
|
|
256
|
-
pendingQuestionId: pq.id,
|
|
257
|
-
questionText: pq.questionText,
|
|
258
|
-
expiresAt: Date.now() + 60_000,
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
const answeredConvId = "answered-conv-1";
|
|
262
|
-
ensureConversation(answeredConvId);
|
|
263
|
-
const delivery = createGuardianActionDelivery({
|
|
264
|
-
requestId: request.id,
|
|
265
|
-
destinationChannel: "telegram",
|
|
266
|
-
destinationChatId: "chat-answered",
|
|
267
|
-
destinationExternalUserId: "user-answered",
|
|
268
|
-
destinationConversationId: answeredConvId,
|
|
269
|
-
});
|
|
270
|
-
updateDeliveryStatus(delivery.id, "sent");
|
|
271
|
-
|
|
272
|
-
// Answer the request (transitions to 'answered', not 'expired')
|
|
273
|
-
resolveGuardianActionRequest(
|
|
274
|
-
request.id,
|
|
275
|
-
"the code is 5678",
|
|
276
|
-
"telegram",
|
|
277
|
-
"user-answered",
|
|
278
|
-
);
|
|
279
|
-
|
|
280
|
-
// Should not appear in expired queries
|
|
281
|
-
const expiredByDest = getExpiredDeliveriesByDestination(
|
|
282
|
-
"telegram",
|
|
283
|
-
"chat-answered",
|
|
284
|
-
);
|
|
285
|
-
expect(expiredByDest).toHaveLength(0);
|
|
286
|
-
|
|
287
|
-
const expiredByConv = getExpiredDeliveriesByConversation(answeredConvId);
|
|
288
|
-
expect(expiredByConv).toHaveLength(0);
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
// ── Composed follow-up text verification ──────────────────────────
|
|
292
|
-
|
|
293
|
-
test("composeGuardianActionMessageGenerative produces follow-up text for late answer scenario", async () => {
|
|
294
|
-
// The composer is tested directly rather than through the handler
|
|
295
|
-
const { composeGuardianActionMessageGenerative } =
|
|
296
|
-
await import("../runtime/guardian-action-message-composer.js");
|
|
297
|
-
|
|
298
|
-
const text = await composeGuardianActionMessageGenerative({
|
|
299
|
-
scenario: "guardian_late_answer_followup",
|
|
300
|
-
questionText: "What is the gate code?",
|
|
301
|
-
lateAnswerText: "The gate code is 1234",
|
|
302
|
-
});
|
|
303
|
-
|
|
304
|
-
// In test mode, the deterministic fallback is used
|
|
305
|
-
expect(text).toContain("called earlier");
|
|
306
|
-
expect(text).toContain("call them back");
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test("composeGuardianActionMessageGenerative produces stale text for expired scenario", async () => {
|
|
310
|
-
const { composeGuardianActionMessageGenerative } =
|
|
311
|
-
await import("../runtime/guardian-action-message-composer.js");
|
|
312
|
-
|
|
313
|
-
const text = await composeGuardianActionMessageGenerative({
|
|
314
|
-
scenario: "guardian_stale_expired",
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
expect(text).toContain("expired");
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// ── Multiple deliveries in one conversation (disambiguation) ──────
|
|
321
|
-
|
|
322
|
-
describe("multi-delivery disambiguation in reused conversations", () => {
|
|
323
|
-
// Helper to create a pending request with delivery in a shared conversation
|
|
324
|
-
function createPendingInSharedConv(
|
|
325
|
-
sourceConvId: string,
|
|
326
|
-
sharedDeliveryConvId: string,
|
|
327
|
-
) {
|
|
328
|
-
ensureConversation(sourceConvId);
|
|
329
|
-
const session = createCallSession({
|
|
330
|
-
conversationId: sourceConvId,
|
|
331
|
-
provider: "twilio",
|
|
332
|
-
fromNumber: "+15550001111",
|
|
333
|
-
toNumber: "+15550002222",
|
|
334
|
-
});
|
|
335
|
-
const pq = createPendingQuestion(
|
|
336
|
-
session.id,
|
|
337
|
-
`Question from ${sourceConvId}`,
|
|
338
|
-
);
|
|
339
|
-
const request = createGuardianActionRequest({
|
|
340
|
-
kind: "ask_guardian",
|
|
341
|
-
sourceChannel: "phone",
|
|
342
|
-
sourceConversationId: sourceConvId,
|
|
343
|
-
callSessionId: session.id,
|
|
344
|
-
pendingQuestionId: pq.id,
|
|
345
|
-
questionText: pq.questionText,
|
|
346
|
-
expiresAt: Date.now() + 60_000,
|
|
347
|
-
});
|
|
348
|
-
const delivery = createGuardianActionDelivery({
|
|
349
|
-
requestId: request.id,
|
|
350
|
-
destinationChannel: "vellum",
|
|
351
|
-
destinationConversationId: sharedDeliveryConvId,
|
|
352
|
-
});
|
|
353
|
-
updateDeliveryStatus(delivery.id, "sent");
|
|
354
|
-
return { request, delivery };
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
test("multiple pending deliveries in same conversation are returned by getPendingDeliveriesByConversation", () => {
|
|
358
|
-
const sharedConv = "shared-reused-conv-pending";
|
|
359
|
-
ensureConversation(sharedConv);
|
|
360
|
-
|
|
361
|
-
const { request: req1 } = createPendingInSharedConv("src-p1", sharedConv);
|
|
362
|
-
const { request: req2 } = createPendingInSharedConv("src-p2", sharedConv);
|
|
363
|
-
|
|
364
|
-
const deliveries = getPendingDeliveriesByConversation(sharedConv);
|
|
365
|
-
expect(deliveries).toHaveLength(2);
|
|
366
|
-
|
|
367
|
-
const requestIds = deliveries.map((d) => d.requestId);
|
|
368
|
-
expect(requestIds).toContain(req1.id);
|
|
369
|
-
expect(requestIds).toContain(req2.id);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test("request codes are unique across multiple requests in same conversation", () => {
|
|
373
|
-
const sharedConv = "shared-reused-conv-codes";
|
|
374
|
-
ensureConversation(sharedConv);
|
|
375
|
-
|
|
376
|
-
const { request: req1 } = createPendingInSharedConv(
|
|
377
|
-
"src-code1",
|
|
378
|
-
sharedConv,
|
|
379
|
-
);
|
|
380
|
-
const { request: req2 } = createPendingInSharedConv(
|
|
381
|
-
"src-code2",
|
|
382
|
-
sharedConv,
|
|
383
|
-
);
|
|
384
|
-
|
|
385
|
-
expect(req1.requestCode).not.toBe(req2.requestCode);
|
|
386
|
-
expect(req1.requestCode).toHaveLength(6);
|
|
387
|
-
expect(req2.requestCode).toHaveLength(6);
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test("multiple expired deliveries in same conversation are returned by getExpiredDeliveriesByConversation", () => {
|
|
391
|
-
const sharedConv = "shared-reused-conv-expired";
|
|
392
|
-
ensureConversation(sharedConv);
|
|
393
|
-
|
|
394
|
-
const { request: req1 } = createPendingInSharedConv("src-e1", sharedConv);
|
|
395
|
-
const { request: req2 } = createPendingInSharedConv("src-e2", sharedConv);
|
|
396
|
-
|
|
397
|
-
expireGuardianActionRequest(req1.id, "sweep_timeout");
|
|
398
|
-
expireGuardianActionRequest(req2.id, "sweep_timeout");
|
|
399
|
-
|
|
400
|
-
const deliveries = getExpiredDeliveriesByConversation(sharedConv);
|
|
401
|
-
expect(deliveries).toHaveLength(2);
|
|
402
|
-
|
|
403
|
-
const requestIds = deliveries.map((d) => d.requestId);
|
|
404
|
-
expect(requestIds).toContain(req1.id);
|
|
405
|
-
expect(requestIds).toContain(req2.id);
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
test("multiple followup deliveries in same conversation are returned by getFollowupDeliveriesByConversation", () => {
|
|
409
|
-
const sharedConv = "shared-reused-conv-followup";
|
|
410
|
-
ensureConversation(sharedConv);
|
|
411
|
-
|
|
412
|
-
const { request: req1 } = createPendingInSharedConv(
|
|
413
|
-
"src-fu1",
|
|
414
|
-
sharedConv,
|
|
415
|
-
);
|
|
416
|
-
const { request: req2 } = createPendingInSharedConv(
|
|
417
|
-
"src-fu2",
|
|
418
|
-
sharedConv,
|
|
419
|
-
);
|
|
420
|
-
|
|
421
|
-
expireGuardianActionRequest(req1.id, "sweep_timeout");
|
|
422
|
-
expireGuardianActionRequest(req2.id, "sweep_timeout");
|
|
423
|
-
startFollowupFromExpiredRequest(req1.id, "late answer 1");
|
|
424
|
-
startFollowupFromExpiredRequest(req2.id, "late answer 2");
|
|
425
|
-
|
|
426
|
-
const deliveries = getFollowupDeliveriesByConversation(sharedConv);
|
|
427
|
-
expect(deliveries).toHaveLength(2);
|
|
428
|
-
|
|
429
|
-
const requestIds = deliveries.map((d) => d.requestId);
|
|
430
|
-
expect(requestIds).toContain(req1.id);
|
|
431
|
-
expect(requestIds).toContain(req2.id);
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
test("resolving one pending request leaves the other still pending in shared conversation", () => {
|
|
435
|
-
const sharedConv = "shared-reused-conv-resolve-one";
|
|
436
|
-
ensureConversation(sharedConv);
|
|
437
|
-
|
|
438
|
-
const { request: req1 } = createPendingInSharedConv("src-r1", sharedConv);
|
|
439
|
-
const { request: req2 } = createPendingInSharedConv("src-r2", sharedConv);
|
|
440
|
-
|
|
441
|
-
resolveGuardianActionRequest(req1.id, "answer to first", "vellum");
|
|
442
|
-
|
|
443
|
-
const remaining = getPendingDeliveriesByConversation(sharedConv);
|
|
444
|
-
expect(remaining).toHaveLength(1);
|
|
445
|
-
expect(remaining[0].requestId).toBe(req2.id);
|
|
446
|
-
});
|
|
447
|
-
|
|
448
|
-
test("request code prefix matching is case-insensitive", () => {
|
|
449
|
-
const sharedConv = "shared-reused-conv-case";
|
|
450
|
-
ensureConversation(sharedConv);
|
|
451
|
-
|
|
452
|
-
const { request: req1 } = createPendingInSharedConv(
|
|
453
|
-
"src-case1",
|
|
454
|
-
sharedConv,
|
|
455
|
-
);
|
|
456
|
-
const code = req1.requestCode; // e.g. "A1B2C3"
|
|
457
|
-
|
|
458
|
-
// Simulate case-insensitive prefix matching as done in conversation-process.ts
|
|
459
|
-
const userInput = `${code.toLowerCase()} the answer is 42`;
|
|
460
|
-
const matched = userInput.toUpperCase().startsWith(code);
|
|
461
|
-
expect(matched).toBe(true);
|
|
462
|
-
|
|
463
|
-
// After stripping the code prefix, the answer text is extracted
|
|
464
|
-
const answerText = userInput.slice(code.length).trim();
|
|
465
|
-
expect(answerText).toBe("the answer is 42");
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
// ── Superseded late-approval remap semantics ──────────────────────
|
|
470
|
-
|
|
471
|
-
describe("superseded late-approval remap", () => {
|
|
472
|
-
/**
|
|
473
|
-
* Helper: create two guardian action requests on the same call session.
|
|
474
|
-
* The first is superseded by the second (which stays pending).
|
|
475
|
-
* Returns the superseded request, the current pending request, and the call session.
|
|
476
|
-
*/
|
|
477
|
-
function createSupersededScenario(
|
|
478
|
-
convId: string,
|
|
479
|
-
opts?: {
|
|
480
|
-
chatId?: string;
|
|
481
|
-
externalUserId?: string;
|
|
482
|
-
conversationId?: string;
|
|
483
|
-
},
|
|
484
|
-
) {
|
|
485
|
-
ensureConversation(convId);
|
|
486
|
-
const session = createCallSession({
|
|
487
|
-
conversationId: convId,
|
|
488
|
-
provider: "twilio",
|
|
489
|
-
fromNumber: "+15550001111",
|
|
490
|
-
toNumber: "+15550002222",
|
|
491
|
-
});
|
|
492
|
-
// Keep call in 'initiated' status (non-terminal) — simulates active call
|
|
493
|
-
const pqOld = createPendingQuestion(
|
|
494
|
-
session.id,
|
|
495
|
-
"What is the old gate code?",
|
|
496
|
-
);
|
|
497
|
-
const oldRequest = createGuardianActionRequest({
|
|
498
|
-
kind: "ask_guardian",
|
|
499
|
-
sourceChannel: "phone",
|
|
500
|
-
sourceConversationId: convId,
|
|
501
|
-
callSessionId: session.id,
|
|
502
|
-
pendingQuestionId: pqOld.id,
|
|
503
|
-
questionText: pqOld.questionText,
|
|
504
|
-
expiresAt: Date.now() + 60_000,
|
|
505
|
-
toolName: "check_gate",
|
|
506
|
-
inputDigest: "digest-old",
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
// Create delivery for the old request
|
|
510
|
-
const deliveryConvId =
|
|
511
|
-
opts?.conversationId ?? `delivery-conv-${oldRequest.id}`;
|
|
512
|
-
if (opts?.conversationId) {
|
|
513
|
-
ensureConversation(opts.conversationId);
|
|
514
|
-
} else {
|
|
515
|
-
ensureConversation(deliveryConvId);
|
|
516
|
-
}
|
|
517
|
-
const oldDelivery = createGuardianActionDelivery({
|
|
518
|
-
requestId: oldRequest.id,
|
|
519
|
-
destinationChannel: "telegram",
|
|
520
|
-
destinationChatId: opts?.chatId ?? "chat-123",
|
|
521
|
-
destinationExternalUserId: opts?.externalUserId ?? "user-456",
|
|
522
|
-
destinationConversationId: deliveryConvId,
|
|
523
|
-
});
|
|
524
|
-
updateDeliveryStatus(oldDelivery.id, "sent");
|
|
525
|
-
|
|
526
|
-
// Create the new (current) pending request
|
|
527
|
-
const pqNew = createPendingQuestion(
|
|
528
|
-
session.id,
|
|
529
|
-
"What is the new gate code?",
|
|
530
|
-
);
|
|
531
|
-
const newRequest = createGuardianActionRequest({
|
|
532
|
-
kind: "ask_guardian",
|
|
533
|
-
sourceChannel: "phone",
|
|
534
|
-
sourceConversationId: convId,
|
|
535
|
-
callSessionId: session.id,
|
|
536
|
-
pendingQuestionId: pqNew.id,
|
|
537
|
-
questionText: pqNew.questionText,
|
|
538
|
-
expiresAt: Date.now() + 60_000,
|
|
539
|
-
toolName: "check_gate",
|
|
540
|
-
inputDigest: "digest-new",
|
|
541
|
-
});
|
|
542
|
-
|
|
543
|
-
// Supersede the old request
|
|
544
|
-
supersedeGuardianActionRequest(oldRequest.id, newRequest.id);
|
|
545
|
-
|
|
546
|
-
return {
|
|
547
|
-
session,
|
|
548
|
-
supersededRequest: getGuardianActionRequest(oldRequest.id)!,
|
|
549
|
-
currentRequest: getGuardianActionRequest(newRequest.id)!,
|
|
550
|
-
oldDelivery,
|
|
551
|
-
deliveryConvId,
|
|
552
|
-
};
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
test("superseded request has expired_reason=superseded and links to replacement", () => {
|
|
556
|
-
const { supersededRequest, currentRequest } =
|
|
557
|
-
createSupersededScenario("conv-supersede-1");
|
|
558
|
-
|
|
559
|
-
expect(supersededRequest.status).toBe("expired");
|
|
560
|
-
expect(supersededRequest.expiredReason).toBe("superseded");
|
|
561
|
-
expect(supersededRequest.supersededByRequestId).toBe(currentRequest.id);
|
|
562
|
-
expect(currentRequest.status).toBe("pending");
|
|
563
|
-
});
|
|
564
|
-
|
|
565
|
-
test("superseded request with active call and pending request is remap-eligible", () => {
|
|
566
|
-
const { session, supersededRequest, currentRequest } =
|
|
567
|
-
createSupersededScenario("conv-supersede-2");
|
|
568
|
-
|
|
569
|
-
// Call should still be active (non-terminal)
|
|
570
|
-
const callSession = getCallSession(session.id);
|
|
571
|
-
expect(callSession).not.toBeNull();
|
|
572
|
-
expect(isTerminalState(callSession!.status)).toBe(false);
|
|
573
|
-
|
|
574
|
-
// Should find current pending request for the same call session
|
|
575
|
-
const pending = getPendingRequestByCallSessionId(
|
|
576
|
-
supersededRequest.callSessionId,
|
|
577
|
-
);
|
|
578
|
-
expect(pending).not.toBeNull();
|
|
579
|
-
expect(pending!.id).toBe(currentRequest.id);
|
|
580
|
-
|
|
581
|
-
// The superseded request is expired with reason 'superseded' and followup_state 'none'
|
|
582
|
-
expect(supersededRequest.expiredReason).toBe("superseded");
|
|
583
|
-
expect(supersededRequest.followupState).toBe("none");
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
test("superseded request with completed call is NOT remap-eligible — falls through to follow-up", () => {
|
|
587
|
-
const { session, supersededRequest } =
|
|
588
|
-
createSupersededScenario("conv-supersede-3");
|
|
589
|
-
|
|
590
|
-
// Transition the call to a terminal state
|
|
591
|
-
updateCallSession(session.id, { status: "in_progress" });
|
|
592
|
-
updateCallSession(session.id, {
|
|
593
|
-
status: "completed",
|
|
594
|
-
endedAt: Date.now(),
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
// Call is now terminal
|
|
598
|
-
const callSession = getCallSession(session.id);
|
|
599
|
-
expect(callSession).not.toBeNull();
|
|
600
|
-
expect(isTerminalState(callSession!.status)).toBe(true);
|
|
601
|
-
|
|
602
|
-
// Even though expired_reason is 'superseded', the remap should not apply
|
|
603
|
-
// because the call has ended. The follow-up path should be used instead.
|
|
604
|
-
expect(supersededRequest.expiredReason).toBe("superseded");
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
test("timeout-expired request is NOT remap-eligible even with active call", () => {
|
|
608
|
-
const convId = "conv-timeout-no-remap";
|
|
609
|
-
ensureConversation(convId);
|
|
610
|
-
const session = createCallSession({
|
|
611
|
-
conversationId: convId,
|
|
612
|
-
provider: "twilio",
|
|
613
|
-
fromNumber: "+15550001111",
|
|
614
|
-
toNumber: "+15550002222",
|
|
615
|
-
});
|
|
616
|
-
|
|
617
|
-
const pq = createPendingQuestion(session.id, "What is the code?");
|
|
618
|
-
const request = createGuardianActionRequest({
|
|
619
|
-
kind: "ask_guardian",
|
|
620
|
-
sourceChannel: "phone",
|
|
621
|
-
sourceConversationId: convId,
|
|
622
|
-
callSessionId: session.id,
|
|
623
|
-
pendingQuestionId: pq.id,
|
|
624
|
-
questionText: pq.questionText,
|
|
625
|
-
expiresAt: Date.now() - 10_000,
|
|
626
|
-
});
|
|
627
|
-
|
|
628
|
-
const deliveryConvId = `delivery-conv-${request.id}`;
|
|
629
|
-
ensureConversation(deliveryConvId);
|
|
630
|
-
const delivery = createGuardianActionDelivery({
|
|
631
|
-
requestId: request.id,
|
|
632
|
-
destinationChannel: "telegram",
|
|
633
|
-
destinationChatId: "chat-timeout",
|
|
634
|
-
destinationExternalUserId: "user-timeout",
|
|
635
|
-
destinationConversationId: deliveryConvId,
|
|
636
|
-
});
|
|
637
|
-
updateDeliveryStatus(delivery.id, "sent");
|
|
638
|
-
|
|
639
|
-
// Expire with sweep_timeout (NOT superseded)
|
|
640
|
-
expireGuardianActionRequest(request.id, "sweep_timeout");
|
|
641
|
-
|
|
642
|
-
const expired = getGuardianActionRequest(request.id)!;
|
|
643
|
-
expect(expired.expiredReason).toBe("sweep_timeout");
|
|
644
|
-
|
|
645
|
-
// Even if the call is active, this should follow the callback/message path
|
|
646
|
-
// because it's a real timeout, not a supersession
|
|
647
|
-
const callSession = getCallSession(session.id);
|
|
648
|
-
expect(callSession).not.toBeNull();
|
|
649
|
-
expect(isTerminalState(callSession!.status)).toBe(false);
|
|
650
|
-
|
|
651
|
-
// startFollowupFromExpiredRequest should work normally for timeouts
|
|
652
|
-
const followup = startFollowupFromExpiredRequest(
|
|
653
|
-
request.id,
|
|
654
|
-
"late answer",
|
|
655
|
-
);
|
|
656
|
-
expect(followup).not.toBeNull();
|
|
657
|
-
expect(followup!.followupState).toBe("awaiting_guardian_choice");
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
test("call_timeout-expired request is NOT remap-eligible", () => {
|
|
661
|
-
const convId = "conv-call-timeout-no-remap";
|
|
662
|
-
ensureConversation(convId);
|
|
663
|
-
const session = createCallSession({
|
|
664
|
-
conversationId: convId,
|
|
665
|
-
provider: "twilio",
|
|
666
|
-
fromNumber: "+15550001111",
|
|
667
|
-
toNumber: "+15550002222",
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
const pq = createPendingQuestion(session.id, "What is the code?");
|
|
671
|
-
const request = createGuardianActionRequest({
|
|
672
|
-
kind: "ask_guardian",
|
|
673
|
-
sourceChannel: "phone",
|
|
674
|
-
sourceConversationId: convId,
|
|
675
|
-
callSessionId: session.id,
|
|
676
|
-
pendingQuestionId: pq.id,
|
|
677
|
-
questionText: pq.questionText,
|
|
678
|
-
expiresAt: Date.now() - 10_000,
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
const deliveryConvId = `delivery-conv-${request.id}`;
|
|
682
|
-
ensureConversation(deliveryConvId);
|
|
683
|
-
const delivery = createGuardianActionDelivery({
|
|
684
|
-
requestId: request.id,
|
|
685
|
-
destinationChannel: "telegram",
|
|
686
|
-
destinationChatId: "chat-call-timeout",
|
|
687
|
-
destinationExternalUserId: "user-call-timeout",
|
|
688
|
-
destinationConversationId: deliveryConvId,
|
|
689
|
-
});
|
|
690
|
-
updateDeliveryStatus(delivery.id, "sent");
|
|
691
|
-
|
|
692
|
-
// Expire with call_timeout (NOT superseded)
|
|
693
|
-
expireGuardianActionRequest(request.id, "call_timeout");
|
|
694
|
-
|
|
695
|
-
const expired = getGuardianActionRequest(request.id)!;
|
|
696
|
-
expect(expired.expiredReason).toBe("call_timeout");
|
|
697
|
-
|
|
698
|
-
// call_timeout should follow the callback/message path regardless
|
|
699
|
-
const followup = startFollowupFromExpiredRequest(
|
|
700
|
-
request.id,
|
|
701
|
-
"late answer for timeout",
|
|
702
|
-
);
|
|
703
|
-
expect(followup).not.toBeNull();
|
|
704
|
-
expect(followup!.followupState).toBe("awaiting_guardian_choice");
|
|
705
|
-
});
|
|
706
|
-
|
|
707
|
-
test("superseded request with no pending replacement falls through to follow-up", () => {
|
|
708
|
-
const { supersededRequest, currentRequest } = createSupersededScenario(
|
|
709
|
-
"conv-supersede-no-pending",
|
|
710
|
-
);
|
|
711
|
-
|
|
712
|
-
// Resolve the current pending request so there's no pending replacement
|
|
713
|
-
resolveGuardianActionRequest(
|
|
714
|
-
currentRequest.id,
|
|
715
|
-
"answered already",
|
|
716
|
-
"telegram",
|
|
717
|
-
);
|
|
718
|
-
|
|
719
|
-
// No pending request for this call session anymore
|
|
720
|
-
const pending = getPendingRequestByCallSessionId(
|
|
721
|
-
supersededRequest.callSessionId,
|
|
722
|
-
);
|
|
723
|
-
expect(pending).toBeNull();
|
|
724
|
-
|
|
725
|
-
// The superseded request should fall through to follow-up since
|
|
726
|
-
// there's no pending request to remap to
|
|
727
|
-
const followup = startFollowupFromExpiredRequest(
|
|
728
|
-
supersededRequest.id,
|
|
729
|
-
"late answer",
|
|
730
|
-
);
|
|
731
|
-
expect(followup).not.toBeNull();
|
|
732
|
-
expect(followup!.followupState).toBe("awaiting_guardian_choice");
|
|
733
|
-
});
|
|
734
|
-
|
|
735
|
-
test("composeGuardianActionMessageGenerative produces remap text for superseded scenario", async () => {
|
|
736
|
-
const { composeGuardianActionMessageGenerative } =
|
|
737
|
-
await import("../runtime/guardian-action-message-composer.js");
|
|
738
|
-
|
|
739
|
-
const text = await composeGuardianActionMessageGenerative({
|
|
740
|
-
scenario: "guardian_superseded_remap",
|
|
741
|
-
questionText: "What is the new gate code?",
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
// In test mode, the deterministic fallback is used
|
|
745
|
-
expect(text).toContain("current active request");
|
|
746
|
-
expect(text).toContain("What is the new gate code?");
|
|
747
|
-
});
|
|
748
|
-
|
|
749
|
-
test("composeGuardianActionMessageGenerative produces remap text without question", async () => {
|
|
750
|
-
const { composeGuardianActionMessageGenerative } =
|
|
751
|
-
await import("../runtime/guardian-action-message-composer.js");
|
|
752
|
-
|
|
753
|
-
const text = await composeGuardianActionMessageGenerative({
|
|
754
|
-
scenario: "guardian_superseded_remap",
|
|
755
|
-
});
|
|
756
|
-
|
|
757
|
-
expect(text).toContain("current active request");
|
|
758
|
-
});
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
// ── Disambiguation hardening across states ──────────────────────────
|
|
762
|
-
|
|
763
|
-
describe("disambiguation hardening across states", () => {
|
|
764
|
-
// Helper to create a pending request with delivery in a shared conversation
|
|
765
|
-
function createPendingInSharedConv(
|
|
766
|
-
sourceConvId: string,
|
|
767
|
-
sharedDeliveryConvId: string,
|
|
768
|
-
opts?: { chatId?: string; externalUserId?: string },
|
|
769
|
-
) {
|
|
770
|
-
ensureConversation(sourceConvId);
|
|
771
|
-
const session = createCallSession({
|
|
772
|
-
conversationId: sourceConvId,
|
|
773
|
-
provider: "twilio",
|
|
774
|
-
fromNumber: "+15550001111",
|
|
775
|
-
toNumber: "+15550002222",
|
|
776
|
-
});
|
|
777
|
-
const pq = createPendingQuestion(
|
|
778
|
-
session.id,
|
|
779
|
-
`Question from ${sourceConvId}`,
|
|
780
|
-
);
|
|
781
|
-
const request = createGuardianActionRequest({
|
|
782
|
-
kind: "ask_guardian",
|
|
783
|
-
sourceChannel: "phone",
|
|
784
|
-
sourceConversationId: sourceConvId,
|
|
785
|
-
callSessionId: session.id,
|
|
786
|
-
pendingQuestionId: pq.id,
|
|
787
|
-
questionText: pq.questionText,
|
|
788
|
-
expiresAt: Date.now() + 60_000,
|
|
789
|
-
});
|
|
790
|
-
const delivery = createGuardianActionDelivery({
|
|
791
|
-
requestId: request.id,
|
|
792
|
-
destinationChannel: "telegram",
|
|
793
|
-
destinationChatId: opts?.chatId ?? "chat-disambig",
|
|
794
|
-
destinationExternalUserId: opts?.externalUserId ?? "user-disambig",
|
|
795
|
-
destinationConversationId: sharedDeliveryConvId,
|
|
796
|
-
});
|
|
797
|
-
updateDeliveryStatus(delivery.id, "sent");
|
|
798
|
-
return {
|
|
799
|
-
request: getGuardianActionRequest(request.id)!,
|
|
800
|
-
delivery,
|
|
801
|
-
session,
|
|
802
|
-
};
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
test("single pending request auto-matches without code prefix", () => {
|
|
806
|
-
// When there is only ONE pending request and no expired/follow-up,
|
|
807
|
-
// the guardian's message should auto-match without needing a code prefix.
|
|
808
|
-
const sharedConv = "shared-auto-match-single";
|
|
809
|
-
ensureConversation(sharedConv);
|
|
810
|
-
const { request } = createPendingInSharedConv("src-auto-1", sharedConv);
|
|
811
|
-
|
|
812
|
-
// There should be exactly one pending delivery
|
|
813
|
-
const pending = getPendingDeliveriesByConversation(sharedConv);
|
|
814
|
-
expect(pending).toHaveLength(1);
|
|
815
|
-
|
|
816
|
-
// No expired or follow-up deliveries
|
|
817
|
-
const expired = getExpiredDeliveriesByConversation(sharedConv);
|
|
818
|
-
const followup = getFollowupDeliveriesByConversation(sharedConv);
|
|
819
|
-
expect(expired).toHaveLength(0);
|
|
820
|
-
expect(followup).toHaveLength(0);
|
|
821
|
-
|
|
822
|
-
// Total actionable is 1, so auto-match should apply
|
|
823
|
-
const totalActionable = pending.length + expired.length + followup.length;
|
|
824
|
-
expect(totalActionable).toBe(1);
|
|
825
|
-
|
|
826
|
-
// The request is pending and ready for direct answer
|
|
827
|
-
expect(request.status).toBe("pending");
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
test("multiple pending requests requires disambiguation", () => {
|
|
831
|
-
// When multiple pending requests exist, the guardian must prefix with a code.
|
|
832
|
-
const sharedConv = "shared-multi-pending-disambig";
|
|
833
|
-
ensureConversation(sharedConv);
|
|
834
|
-
const { request: req1 } = createPendingInSharedConv(
|
|
835
|
-
"src-mp1",
|
|
836
|
-
sharedConv,
|
|
837
|
-
);
|
|
838
|
-
const { request: req2 } = createPendingInSharedConv(
|
|
839
|
-
"src-mp2",
|
|
840
|
-
sharedConv,
|
|
841
|
-
);
|
|
842
|
-
|
|
843
|
-
const pending = getPendingDeliveriesByConversation(sharedConv);
|
|
844
|
-
expect(pending).toHaveLength(2);
|
|
845
|
-
|
|
846
|
-
// Both have unique codes
|
|
847
|
-
expect(req1.requestCode).not.toBe(req2.requestCode);
|
|
848
|
-
|
|
849
|
-
// Content without a valid code prefix should require disambiguation
|
|
850
|
-
const testContent = "just a plain answer";
|
|
851
|
-
const upperContent = testContent.toUpperCase();
|
|
852
|
-
const matchesPending = pending.some((d) => {
|
|
853
|
-
const req = getGuardianActionRequest(d.requestId);
|
|
854
|
-
return req && upperContent.startsWith(req.requestCode);
|
|
855
|
-
});
|
|
856
|
-
expect(matchesPending).toBe(false);
|
|
857
|
-
|
|
858
|
-
// Content with a valid code prefix should match
|
|
859
|
-
const prefixedContent = `${req1.requestCode} the answer is 42`;
|
|
860
|
-
const upperPrefixed = prefixedContent.toUpperCase();
|
|
861
|
-
const matchesPrefixed = pending.some((d) => {
|
|
862
|
-
const req = getGuardianActionRequest(d.requestId);
|
|
863
|
-
return req && upperPrefixed.startsWith(req.requestCode);
|
|
864
|
-
});
|
|
865
|
-
expect(matchesPrefixed).toBe(true);
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
test("explicit code to superseded request with active call remaps with explanation", async () => {
|
|
869
|
-
// When a guardian uses a code for a superseded request and the call is
|
|
870
|
-
// still active with a current pending request, the system should remap.
|
|
871
|
-
const convId = "conv-remap-with-code";
|
|
872
|
-
ensureConversation(convId);
|
|
873
|
-
const session = createCallSession({
|
|
874
|
-
conversationId: convId,
|
|
875
|
-
provider: "twilio",
|
|
876
|
-
fromNumber: "+15550001111",
|
|
877
|
-
toNumber: "+15550002222",
|
|
878
|
-
});
|
|
879
|
-
const pqOld = createPendingQuestion(session.id, "Old question?");
|
|
880
|
-
const oldRequest = createGuardianActionRequest({
|
|
881
|
-
kind: "ask_guardian",
|
|
882
|
-
sourceChannel: "phone",
|
|
883
|
-
sourceConversationId: convId,
|
|
884
|
-
callSessionId: session.id,
|
|
885
|
-
pendingQuestionId: pqOld.id,
|
|
886
|
-
questionText: pqOld.questionText,
|
|
887
|
-
expiresAt: Date.now() + 60_000,
|
|
888
|
-
toolName: "check_gate",
|
|
889
|
-
inputDigest: "digest-old-code",
|
|
890
|
-
});
|
|
891
|
-
|
|
892
|
-
const deliveryConvId = "delivery-remap-code";
|
|
893
|
-
ensureConversation(deliveryConvId);
|
|
894
|
-
const oldDelivery = createGuardianActionDelivery({
|
|
895
|
-
requestId: oldRequest.id,
|
|
896
|
-
destinationChannel: "telegram",
|
|
897
|
-
destinationChatId: "chat-remap-code",
|
|
898
|
-
destinationExternalUserId: "user-remap-code",
|
|
899
|
-
destinationConversationId: deliveryConvId,
|
|
900
|
-
});
|
|
901
|
-
updateDeliveryStatus(oldDelivery.id, "sent");
|
|
902
|
-
|
|
903
|
-
// Create new pending request that supersedes the old one
|
|
904
|
-
const pqNew = createPendingQuestion(session.id, "New question?");
|
|
905
|
-
const newRequest = createGuardianActionRequest({
|
|
906
|
-
kind: "ask_guardian",
|
|
907
|
-
sourceChannel: "phone",
|
|
908
|
-
sourceConversationId: convId,
|
|
909
|
-
callSessionId: session.id,
|
|
910
|
-
pendingQuestionId: pqNew.id,
|
|
911
|
-
questionText: pqNew.questionText,
|
|
912
|
-
expiresAt: Date.now() + 60_000,
|
|
913
|
-
toolName: "check_gate",
|
|
914
|
-
inputDigest: "digest-new-code",
|
|
915
|
-
});
|
|
916
|
-
supersedeGuardianActionRequest(oldRequest.id, newRequest.id);
|
|
917
|
-
|
|
918
|
-
// Verify the old request is superseded and the call is active
|
|
919
|
-
const superseded = getGuardianActionRequest(oldRequest.id)!;
|
|
920
|
-
expect(superseded.status).toBe("expired");
|
|
921
|
-
expect(superseded.expiredReason).toBe("superseded");
|
|
922
|
-
|
|
923
|
-
const callSession = getCallSession(session.id);
|
|
924
|
-
expect(isTerminalState(callSession!.status)).toBe(false);
|
|
925
|
-
|
|
926
|
-
// There should be a pending request for this call session (the new one)
|
|
927
|
-
const currentPending = getPendingRequestByCallSessionId(session.id);
|
|
928
|
-
expect(currentPending).not.toBeNull();
|
|
929
|
-
expect(currentPending!.id).toBe(newRequest.id);
|
|
930
|
-
|
|
931
|
-
// Compose the remap message
|
|
932
|
-
const { composeGuardianActionMessageGenerative } =
|
|
933
|
-
await import("../runtime/guardian-action-message-composer.js");
|
|
934
|
-
const remapText = await composeGuardianActionMessageGenerative({
|
|
935
|
-
scenario: "guardian_superseded_remap",
|
|
936
|
-
questionText: currentPending!.questionText,
|
|
937
|
-
});
|
|
938
|
-
expect(remapText).toContain("current active request");
|
|
939
|
-
expect(remapText).toContain("New question?");
|
|
940
|
-
});
|
|
941
|
-
|
|
942
|
-
test("explicit code to expired/timeout request returns terminal notice", async () => {
|
|
943
|
-
// When a guardian uses a code for a timed-out expired request,
|
|
944
|
-
// the system should follow the normal expired path (follow-up or stale).
|
|
945
|
-
const { request } = createExpiredRequest("conv-expired-terminal", {
|
|
946
|
-
chatId: "chat-expired-term",
|
|
947
|
-
externalUserId: "user-expired-term",
|
|
948
|
-
});
|
|
949
|
-
|
|
950
|
-
expect(request.status).toBe("expired");
|
|
951
|
-
expect(request.expiredReason).toBe("sweep_timeout");
|
|
952
|
-
|
|
953
|
-
// When a follow-up can't be started (e.g. already handled), a stale notice is returned
|
|
954
|
-
startFollowupFromExpiredRequest(request.id, "first answer");
|
|
955
|
-
const secondAttempt = startFollowupFromExpiredRequest(
|
|
956
|
-
request.id,
|
|
957
|
-
"second answer",
|
|
958
|
-
);
|
|
959
|
-
expect(secondAttempt).toBeNull();
|
|
960
|
-
|
|
961
|
-
// The stale message should be a terminal notice
|
|
962
|
-
const { composeGuardianActionMessageGenerative } =
|
|
963
|
-
await import("../runtime/guardian-action-message-composer.js");
|
|
964
|
-
const staleText = await composeGuardianActionMessageGenerative({
|
|
965
|
-
scenario: "guardian_stale_expired",
|
|
966
|
-
});
|
|
967
|
-
expect(staleText).toContain("expired");
|
|
968
|
-
expect(staleText).toContain("No further action");
|
|
969
|
-
});
|
|
970
|
-
|
|
971
|
-
test("unknown code returns clear error message instead of loop", async () => {
|
|
972
|
-
// When a guardian provides a code that doesn't match any known request,
|
|
973
|
-
// the system should return a clear "unknown code" message.
|
|
974
|
-
const { composeGuardianActionMessageGenerative } =
|
|
975
|
-
await import("../runtime/guardian-action-message-composer.js");
|
|
976
|
-
|
|
977
|
-
const unknownText = await composeGuardianActionMessageGenerative({
|
|
978
|
-
scenario: "guardian_unknown_code",
|
|
979
|
-
unknownCode: "XYZ999",
|
|
980
|
-
});
|
|
981
|
-
|
|
982
|
-
expect(unknownText).toContain("XYZ999");
|
|
983
|
-
expect(unknownText).toContain("don't recognize");
|
|
984
|
-
// Should NOT ask to prefix with code — that would create a loop
|
|
985
|
-
expect(unknownText).not.toContain("prefix your reply");
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
test("priority order: pending is matched before follow-up before expired", () => {
|
|
989
|
-
// Create deliveries in all three states in a shared conversation
|
|
990
|
-
const sharedConv = "shared-priority-order";
|
|
991
|
-
ensureConversation(sharedConv);
|
|
992
|
-
|
|
993
|
-
// Create a pending request
|
|
994
|
-
const { request: pendingReq } = createPendingInSharedConv(
|
|
995
|
-
"src-prio-pending",
|
|
996
|
-
sharedConv,
|
|
997
|
-
);
|
|
998
|
-
|
|
999
|
-
// Create an expired request
|
|
1000
|
-
const { request: expReq } = createPendingInSharedConv(
|
|
1001
|
-
"src-prio-expired",
|
|
1002
|
-
sharedConv,
|
|
1003
|
-
);
|
|
1004
|
-
expireGuardianActionRequest(expReq.id, "sweep_timeout");
|
|
1005
|
-
|
|
1006
|
-
// Create a follow-up request (expired then started follow-up)
|
|
1007
|
-
const { request: fuReq } = createPendingInSharedConv(
|
|
1008
|
-
"src-prio-followup",
|
|
1009
|
-
sharedConv,
|
|
1010
|
-
);
|
|
1011
|
-
expireGuardianActionRequest(fuReq.id, "sweep_timeout");
|
|
1012
|
-
startFollowupFromExpiredRequest(fuReq.id, "late answer");
|
|
1013
|
-
|
|
1014
|
-
// Gather all deliveries
|
|
1015
|
-
const pending = getPendingDeliveriesByConversation(sharedConv);
|
|
1016
|
-
const followup = getFollowupDeliveriesByConversation(sharedConv);
|
|
1017
|
-
const expired = getExpiredDeliveriesByConversation(sharedConv);
|
|
1018
|
-
|
|
1019
|
-
expect(pending.length).toBeGreaterThan(0);
|
|
1020
|
-
expect(followup.length).toBeGreaterThan(0);
|
|
1021
|
-
expect(expired.length).toBeGreaterThan(0);
|
|
1022
|
-
|
|
1023
|
-
// Simulate the priority matching order from the unified handler:
|
|
1024
|
-
// pending → follow-up → expired
|
|
1025
|
-
const orderedSets = [
|
|
1026
|
-
{ deliveries: pending, state: "pending" },
|
|
1027
|
-
{ deliveries: followup, state: "followup" },
|
|
1028
|
-
{ deliveries: expired, state: "expired" },
|
|
1029
|
-
];
|
|
1030
|
-
|
|
1031
|
-
// When prefixed with the pending request's code, it should match pending first
|
|
1032
|
-
const pendingCode = pendingReq.requestCode;
|
|
1033
|
-
const pendingMessage = `${pendingCode} approve`;
|
|
1034
|
-
let matchedState: string | null = null;
|
|
1035
|
-
for (const { deliveries, state } of orderedSets) {
|
|
1036
|
-
for (const d of deliveries) {
|
|
1037
|
-
const req = getGuardianActionRequest(d.requestId);
|
|
1038
|
-
if (req && pendingMessage.toUpperCase().startsWith(req.requestCode)) {
|
|
1039
|
-
matchedState = state;
|
|
1040
|
-
break;
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
if (matchedState) break;
|
|
1044
|
-
}
|
|
1045
|
-
expect(matchedState).toBe("pending");
|
|
1046
|
-
|
|
1047
|
-
// When prefixed with the follow-up request's code, it should match follow-up
|
|
1048
|
-
// (because the pending check won't match that code)
|
|
1049
|
-
const fuRequest = getGuardianActionRequest(fuReq.id)!;
|
|
1050
|
-
const fuCode = fuRequest.requestCode;
|
|
1051
|
-
const fuMessage = `${fuCode} call back`;
|
|
1052
|
-
matchedState = null;
|
|
1053
|
-
for (const { deliveries, state } of orderedSets) {
|
|
1054
|
-
for (const d of deliveries) {
|
|
1055
|
-
const req = getGuardianActionRequest(d.requestId);
|
|
1056
|
-
if (req && fuMessage.toUpperCase().startsWith(req.requestCode)) {
|
|
1057
|
-
matchedState = state;
|
|
1058
|
-
break;
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
|
-
if (matchedState) break;
|
|
1062
|
-
}
|
|
1063
|
-
expect(matchedState).toBe("followup");
|
|
1064
|
-
|
|
1065
|
-
// When prefixed with the expired request's code, it should match expired
|
|
1066
|
-
const expRequest = getGuardianActionRequest(expReq.id)!;
|
|
1067
|
-
const expCode = expRequest.requestCode;
|
|
1068
|
-
const expMessage = `${expCode} yes`;
|
|
1069
|
-
matchedState = null;
|
|
1070
|
-
for (const { deliveries, state } of orderedSets) {
|
|
1071
|
-
for (const d of deliveries) {
|
|
1072
|
-
const req = getGuardianActionRequest(d.requestId);
|
|
1073
|
-
if (req && expMessage.toUpperCase().startsWith(req.requestCode)) {
|
|
1074
|
-
matchedState = state;
|
|
1075
|
-
break;
|
|
1076
|
-
}
|
|
1077
|
-
}
|
|
1078
|
-
if (matchedState) break;
|
|
1079
|
-
}
|
|
1080
|
-
expect(matchedState).toBe("expired");
|
|
1081
|
-
});
|
|
1082
|
-
});
|
|
1083
|
-
});
|