@vellumai/assistant 0.8.2 → 0.8.3
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 +11 -12
- package/docker-entrypoint.sh +13 -1
- package/docker-init-apt-root.sh +79 -6
- package/openapi.yaml +336 -21
- package/package.json +1 -1
- package/src/__tests__/agent-loop-exit-reason.test.ts +272 -0
- package/src/__tests__/agent-loop-provider-error-recording.test.ts +195 -0
- package/src/__tests__/compactor-tail-resolution.test.ts +107 -1
- package/src/__tests__/config-get-vision-flag.test.ts +136 -0
- package/src/__tests__/config-loader-backfill.test.ts +115 -18
- package/src/__tests__/context-token-estimator.test.ts +30 -65
- package/src/__tests__/conversation-agent-loop.test.ts +57 -1
- package/src/__tests__/conversation-media-retry.test.ts +19 -8
- package/src/__tests__/conversation-runtime-assembly.test.ts +26 -4
- package/src/__tests__/date-context.test.ts +45 -0
- package/src/__tests__/external-plugin-loader.test.ts +91 -19
- package/src/__tests__/guardian-action-no-hardcoded-copy.test.ts +0 -1
- package/src/__tests__/guardian-dispatch.test.ts +1 -0
- package/src/__tests__/heartbeat-service.test.ts +24 -164
- package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
- package/src/__tests__/host-app-control-proxy.test.ts +241 -0
- package/src/__tests__/host-proxy-preactivation.test.ts +200 -13
- package/src/__tests__/injector-background-turn.test.ts +153 -0
- package/src/__tests__/injector-chain.test.ts +5 -0
- package/src/__tests__/lifecycle-memory-v2-seed.test.ts +9 -2
- package/src/__tests__/llm-callsite-catalog.test.ts +25 -0
- package/src/__tests__/llm-catalog-parity.test.ts +3 -0
- package/src/__tests__/llm-request-log-agent-loop-exit-reason.test.ts +116 -0
- package/src/__tests__/llm-request-log-error-payload.test.ts +138 -0
- package/src/__tests__/llm-request-log-source-clickhouse.test.ts +2 -0
- package/src/__tests__/llm-resolver.test.ts +255 -2
- package/src/__tests__/managed-profile-guard.test.ts +10 -0
- package/src/__tests__/notification-decision-fallback.test.ts +0 -91
- package/src/__tests__/notification-decision-strategy.test.ts +14 -31
- package/src/__tests__/notification-deep-link.test.ts +15 -0
- package/src/__tests__/notification-guardian-path.test.ts +1 -2
- package/src/__tests__/notification-platform-adapter.test.ts +5 -4
- package/src/__tests__/notification-telegram-adapter.test.ts +1 -0
- package/src/__tests__/notification-vellum-adapter.test.ts +113 -0
- package/src/__tests__/openai-provider.test.ts +218 -3
- package/src/__tests__/openai-responses-cutover-guard.test.ts +3 -3
- package/src/__tests__/openrouter-provider-only.test.ts +51 -3
- package/src/__tests__/openrouter-token-estimation.test.ts +34 -25
- package/src/__tests__/platform-proxy-context.test.ts +6 -1
- package/src/__tests__/plugin-tool-contribution.test.ts +3 -3
- package/src/__tests__/plugin-types.test.ts +2 -2
- package/src/__tests__/provider-catalog-visibility.test.ts +16 -0
- package/src/__tests__/provider-platform-proxy-integration.test.ts +27 -25
- package/src/__tests__/secret-routes-platform-proxy.test.ts +1 -1
- package/src/__tests__/system-prompt.test.ts +6 -73
- package/src/__tests__/workspace-migration-087-memory-router-balanced-profile.test.ts +228 -0
- package/src/a2a/__tests__/agent-card.test.ts +98 -0
- package/src/a2a/__tests__/e2e-a2a-channel.test.ts +597 -0
- package/src/a2a/__tests__/protocol-helpers.test.ts +113 -0
- package/src/a2a/__tests__/task-store.test.ts +246 -0
- package/src/a2a/agent-card.ts +58 -0
- package/src/a2a/feature-gate.ts +8 -0
- package/src/a2a/protocol-constants.ts +21 -0
- package/src/a2a/protocol-errors.ts +50 -0
- package/src/a2a/protocol-types.ts +162 -0
- package/src/a2a/task-store.ts +168 -0
- package/src/agent/loop.ts +167 -18
- package/src/channels/config.ts +9 -0
- package/src/channels/types.ts +14 -0
- package/src/cli/{__tests__ → commands/__tests__}/notifications.test.ts +201 -28
- package/src/cli/commands/__tests__/schedules.test.ts +469 -0
- package/src/cli/commands/notifications.ts +65 -35
- package/src/cli/commands/plugins.ts +67 -0
- package/src/cli/commands/schedules.ts +297 -5
- package/src/cli/lib/__tests__/search-plugins.test.ts +261 -0
- package/src/cli/lib/install-from-github.ts +8 -9
- package/src/cli/lib/search-plugins.ts +163 -0
- package/src/cli/program.ts +14 -0
- package/src/config/assistant-feature-flags.ts +24 -54
- package/src/config/bundled-skills/app-builder/SKILL.md +117 -1
- package/src/config/bundled-skills/phone-calls/SKILL.md +1 -1
- package/src/config/call-site-defaults.ts +105 -0
- package/src/config/feature-flag-registry.json +21 -29
- package/src/config/llm-resolver.ts +52 -1
- package/src/config/schema.ts +2 -0
- package/src/config/schemas/__tests__/memory-v2.test.ts +3 -3
- package/src/config/schemas/channels.ts +9 -0
- package/src/config/schemas/conversations.ts +10 -0
- package/src/config/schemas/heartbeat.ts +14 -0
- package/src/config/schemas/llm.ts +1 -3
- package/src/config/schemas/memory-retrospective.ts +1 -1
- package/src/config/schemas/memory-v2.ts +4 -4
- package/src/config/schemas/memory.ts +3 -1
- package/src/config/seed-inference-profiles.ts +99 -29
- package/src/context/compactor.ts +72 -12
- package/src/context/token-estimator.ts +32 -34
- package/src/daemon/__tests__/conversation-lifecycle-auto-analyze.test.ts +3 -22
- package/src/daemon/conversation-agent-loop-handlers.ts +78 -0
- package/src/daemon/conversation-agent-loop.ts +29 -2
- package/src/daemon/conversation-runtime-assembly.ts +9 -0
- package/src/daemon/conversation.ts +0 -7
- package/src/daemon/date-context.ts +40 -0
- package/src/daemon/guardian-action-generators.ts +1 -125
- package/src/daemon/handlers/__tests__/config-a2a-complete.test.ts +248 -0
- package/src/daemon/handlers/__tests__/config-a2a-invite.test.ts +154 -0
- package/src/daemon/handlers/__tests__/config-a2a-redeem.test.ts +133 -0
- package/src/daemon/handlers/__tests__/config-a2a.test.ts +95 -0
- package/src/daemon/handlers/config-a2a.ts +289 -0
- package/src/daemon/handlers/conversations.ts +1 -0
- package/src/daemon/host-app-control-proxy.ts +69 -18
- package/src/daemon/host-proxy-preactivation.ts +85 -18
- package/src/daemon/lifecycle.ts +49 -61
- package/src/daemon/memory-v2-startup.ts +49 -13
- package/src/daemon/message-types/notifications.ts +21 -0
- package/src/daemon/pkb-reminder-builder.test.ts +10 -53
- package/src/daemon/pkb-reminder-builder.ts +4 -19
- package/src/daemon/process-message.ts +3 -0
- package/src/daemon/skill-memory-refresh.ts +5 -1
- package/src/daemon/wake-target-adapter.ts +2 -0
- package/src/export/__tests__/transcript-formatter.test.ts +121 -0
- package/src/export/transcript-formatter.ts +54 -20
- package/src/heartbeat/__tests__/heartbeat-service.test.ts +44 -0
- package/src/heartbeat/heartbeat-service.ts +34 -191
- package/src/home/__tests__/feed-types.test.ts +40 -0
- package/src/home/feed-types.ts +14 -2
- package/src/ipc/cli-client.ts +147 -45
- package/src/memory/__tests__/conversation-queries.test.ts +220 -0
- package/src/memory/__tests__/memory-retrospective-enqueue.test.ts +2 -50
- package/src/memory/__tests__/memory-retrospective-job.test.ts +87 -4
- package/src/memory/conversation-queries.ts +87 -1
- package/src/memory/conversation-title-service.ts +26 -4
- package/src/memory/db-init.ts +6 -0
- package/src/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +84 -3
- package/src/memory/graph/conversation-graph-memory.ts +18 -6
- package/src/memory/graph/tools.ts +6 -37
- package/src/memory/invite-store.ts +53 -0
- package/src/memory/llm-request-log-source-clickhouse.ts +7 -2
- package/src/memory/llm-request-log-store.ts +92 -1
- package/src/memory/memory-retrospective-enqueue.ts +1 -20
- package/src/memory/memory-retrospective-job.ts +33 -6
- package/src/memory/migrations/250-provider-connection-base-url-and-models.ts +28 -0
- package/src/memory/migrations/251-a2a-tasks.ts +49 -0
- package/src/memory/migrations/252-llm-request-log-agent-loop-exit-reason.ts +32 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/migrations/registry.ts +8 -0
- package/src/memory/schema/a2a.ts +15 -0
- package/src/memory/schema/index.ts +1 -0
- package/src/memory/schema/inference.ts +2 -0
- package/src/memory/schema/infrastructure.ts +1 -0
- package/src/memory/v2/__tests__/activation-store.test.ts +25 -23
- package/src/memory/v2/__tests__/cli-command-store.test.ts +404 -0
- package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +25 -4
- package/src/memory/v2/__tests__/injection.test.ts +190 -3
- package/src/memory/v2/__tests__/static-context.test.ts +12 -1
- package/src/memory/v2/activation-store.ts +14 -16
- package/src/memory/v2/cli-command-content.ts +19 -0
- package/src/memory/v2/cli-command-store.ts +304 -0
- package/src/memory/v2/frontmatter-sweep.ts +7 -1
- package/src/memory/v2/injection.ts +49 -20
- package/src/memory/v2/page-index.ts +38 -13
- package/src/memory/v2/static-context.ts +4 -4
- package/src/memory/v2/types.ts +23 -0
- package/src/messaging/providers/a2a/__tests__/deliver.test.ts +274 -0
- package/src/messaging/providers/a2a/deliver.ts +156 -0
- package/src/messaging/providers/gmail/client.ts +9 -2
- package/src/messaging/providers/index.ts +11 -2
- package/src/notifications/__tests__/broadcaster.test.ts +203 -0
- package/src/notifications/__tests__/decision-engine.test.ts +283 -0
- package/src/notifications/__tests__/deterministic-checks.test.ts +286 -0
- package/src/notifications/__tests__/emit-signal-home-feed.test.ts +1 -0
- package/src/notifications/__tests__/home-feed-side-effect.test.ts +430 -7
- package/src/notifications/adapters/macos.ts +12 -2
- package/src/notifications/broadcaster.ts +29 -4
- package/src/notifications/copy-composer.ts +17 -64
- package/src/notifications/decision-engine.ts +111 -44
- package/src/notifications/deterministic-checks.ts +96 -0
- package/src/notifications/emit-signal.ts +1 -0
- package/src/notifications/home-feed-side-effect.ts +85 -6
- package/src/notifications/signal.ts +0 -4
- package/src/notifications/types.ts +8 -0
- package/src/oauth/platform-connection.test.ts +43 -3
- package/src/oauth/platform-connection.ts +13 -4
- package/src/plugins/defaults/injectors.ts +38 -19
- package/src/plugins/external-plugin-loader.ts +82 -10
- package/src/plugins/types.ts +16 -7
- package/src/prompts/__tests__/system-prompt.test.ts +6 -51
- package/src/prompts/__tests__/task-progress-hint-section.test.ts +4 -8
- package/src/prompts/system-prompt.ts +0 -8
- package/src/prompts/templates/BOOTSTRAP.md +5 -5
- package/src/prompts/templates/system-sections.ts +0 -9
- package/src/providers/__tests__/inference.test.ts +2 -0
- package/src/providers/call-site-routing.ts +24 -6
- package/src/providers/connection-resolution.ts +63 -13
- package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +74 -0
- package/src/providers/inference/__tests__/connections-openai-compatible.test.ts +175 -0
- package/src/providers/inference/__tests__/connections-status-label.test.ts +15 -0
- package/src/providers/inference/adapter-factory.ts +9 -20
- package/src/providers/inference/auth.ts +12 -0
- package/src/providers/inference/backfill.ts +14 -1
- package/src/providers/inference/connections.ts +85 -5
- package/src/providers/inference/resolve-auth.ts +2 -0
- package/src/providers/model-catalog.ts +199 -244
- package/src/providers/model-intents.ts +3 -3
- package/src/providers/openai/__tests__/chat-completions-provider-reasoning.test.ts +235 -0
- package/src/providers/openai/chat-completions-provider.ts +159 -6
- package/src/providers/openrouter/client.ts +42 -4
- package/src/providers/platform-proxy/constants.ts +3 -4
- package/src/providers/provider-catalog-visibility.ts +3 -1
- package/src/providers/provider-send-message.ts +27 -12
- package/src/providers/registry.ts +30 -1
- package/src/runtime/agent-wake.ts +61 -1
- package/src/runtime/auth/route-policy.ts +13 -0
- package/src/runtime/http-server.ts +7 -16
- package/src/runtime/http-types.ts +0 -47
- package/src/runtime/routes/__tests__/consolidation-routes.test.ts +258 -0
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +66 -4
- package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +275 -44
- package/src/runtime/routes/__tests__/llm-call-sites-routes.test.ts +12 -0
- package/src/runtime/routes/channel-availability-routes.ts +5 -0
- package/src/runtime/routes/consolidation-routes.ts +100 -0
- package/src/runtime/routes/conversation-query-routes.ts +70 -11
- package/src/runtime/routes/conversation-routes.ts +7 -0
- package/src/runtime/routes/index.ts +2 -0
- package/src/runtime/routes/inference-provider-connection-routes.ts +134 -1
- package/src/runtime/routes/integrations/a2a.ts +235 -0
- package/src/runtime/routes/llm-call-sites-routes.ts +11 -1
- package/src/runtime/routes/subagents-routes.ts +41 -0
- package/src/subagent/manager.ts +2 -0
- package/src/tools/memory/register.ts +1 -9
- package/src/tools/registry.ts +2 -2
- package/src/tools/types.ts +37 -2
- package/src/workspace/migrations/087-memory-router-balanced-profile.ts +91 -0
- package/src/workspace/migrations/registry.ts +2 -0
- package/src/__tests__/guardian-action-conversation-turn.test.ts +0 -441
- package/src/memory/graph/__tests__/remember-description.test.ts +0 -55
- package/src/runtime/guardian-action-conversation-turn.ts +0 -99
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-end A2A channel integration tests.
|
|
3
|
+
*
|
|
4
|
+
* Tests the assistant-side A2A lifecycle: connection initiation, task store +
|
|
5
|
+
* delivery adapter flow, ACL enforcement via trusted contacts, push
|
|
6
|
+
* notifications, and the feature toggle.
|
|
7
|
+
*
|
|
8
|
+
* Because the gateway and assistant are separate processes, we test the
|
|
9
|
+
* assistant-side integration with mocked HTTP for inter-gateway calls and
|
|
10
|
+
* mocked config for feature flag checks.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { afterEach, beforeEach, describe, expect, mock, test } from "bun:test";
|
|
14
|
+
|
|
15
|
+
import type { ChannelReplyPayload } from "@vellumai/gateway-client";
|
|
16
|
+
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
// Mock state
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
const fetchCalls: Array<{ url: string; init: RequestInit }> = [];
|
|
22
|
+
let fetchResponseMap: Record<
|
|
23
|
+
string,
|
|
24
|
+
{ ok: boolean; status: number; body: string }
|
|
25
|
+
> = {};
|
|
26
|
+
let defaultFetchResponse = { ok: true, status: 200, body: "{}" };
|
|
27
|
+
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// Mocks — must be set up before importing modules under test
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
mock.module("../../util/logger.js", () => ({
|
|
33
|
+
getLogger: () =>
|
|
34
|
+
new Proxy({} as Record<string, unknown>, {
|
|
35
|
+
get: () => () => {},
|
|
36
|
+
}),
|
|
37
|
+
truncateForLog: (value: string) => value,
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
// The config-a2a handler uses these config functions directly (not mocked via
|
|
41
|
+
// module replacement) because it calls loadRawConfig/saveRawConfig to toggle
|
|
42
|
+
// the a2a.enabled flag. We use the real config system backed by initializeDb's
|
|
43
|
+
// workspace directory.
|
|
44
|
+
|
|
45
|
+
import {
|
|
46
|
+
invalidateConfigCache,
|
|
47
|
+
loadRawConfig,
|
|
48
|
+
saveRawConfig,
|
|
49
|
+
setNestedValue,
|
|
50
|
+
} from "../../config/loader.js";
|
|
51
|
+
import {
|
|
52
|
+
findContactByAddress,
|
|
53
|
+
upsertContact,
|
|
54
|
+
} from "../../contacts/contact-store.js";
|
|
55
|
+
import {
|
|
56
|
+
clearA2AConfig,
|
|
57
|
+
getA2AConfig,
|
|
58
|
+
setA2AConfig,
|
|
59
|
+
} from "../../daemon/handlers/config-a2a.js";
|
|
60
|
+
import { getSqlite } from "../../memory/db-connection.js";
|
|
61
|
+
import { initializeDb } from "../../memory/db-init.js";
|
|
62
|
+
import type { A2AMessage, Artifact } from "../protocol-types.js";
|
|
63
|
+
import {
|
|
64
|
+
completeWithArtifacts,
|
|
65
|
+
createTask,
|
|
66
|
+
getPushUrl,
|
|
67
|
+
getTask,
|
|
68
|
+
updateState,
|
|
69
|
+
} from "../task-store.js";
|
|
70
|
+
|
|
71
|
+
initializeDb();
|
|
72
|
+
|
|
73
|
+
// ---------------------------------------------------------------------------
|
|
74
|
+
// Global fetch intercept
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
const originalFetch = globalThis.fetch;
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
// Helpers
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
function resetTables(): void {
|
|
84
|
+
const sqlite = getSqlite();
|
|
85
|
+
sqlite.run("DELETE FROM a2a_tasks");
|
|
86
|
+
sqlite.run("DELETE FROM assistant_contact_metadata");
|
|
87
|
+
sqlite.run("DELETE FROM contact_channels");
|
|
88
|
+
sqlite.run("DELETE FROM contacts");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function setConfigEnabled(enabled: boolean): void {
|
|
92
|
+
const raw = loadRawConfig();
|
|
93
|
+
setNestedValue(raw, "a2a.enabled", enabled);
|
|
94
|
+
setNestedValue(raw, "ingress.publicBaseUrl", "https://self.example.com");
|
|
95
|
+
saveRawConfig(raw);
|
|
96
|
+
invalidateConfigCache();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function makeRequestMessage(overrides?: Partial<A2AMessage>): A2AMessage {
|
|
100
|
+
return {
|
|
101
|
+
message_id: crypto.randomUUID(),
|
|
102
|
+
role: "user",
|
|
103
|
+
parts: [{ kind: "text", text: "Hello from sender" }],
|
|
104
|
+
...overrides,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function installFetchMock(): void {
|
|
109
|
+
fetchCalls.length = 0;
|
|
110
|
+
fetchResponseMap = {};
|
|
111
|
+
defaultFetchResponse = { ok: true, status: 200, body: "{}" };
|
|
112
|
+
|
|
113
|
+
globalThis.fetch = (async (
|
|
114
|
+
input: string | URL | Request,
|
|
115
|
+
init?: RequestInit,
|
|
116
|
+
) => {
|
|
117
|
+
const url = typeof input === "string" ? input : input.toString();
|
|
118
|
+
fetchCalls.push({ url, init: init ?? {} });
|
|
119
|
+
|
|
120
|
+
// Check for URL-specific responses
|
|
121
|
+
for (const [pattern, response] of Object.entries(fetchResponseMap)) {
|
|
122
|
+
if (url.includes(pattern)) {
|
|
123
|
+
return new Response(response.body, {
|
|
124
|
+
status: response.status,
|
|
125
|
+
statusText: response.ok ? "OK" : "Error",
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return new Response(defaultFetchResponse.body, {
|
|
131
|
+
status: defaultFetchResponse.status,
|
|
132
|
+
statusText: defaultFetchResponse.ok ? "OK" : "Error",
|
|
133
|
+
});
|
|
134
|
+
}) as typeof fetch;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Setup / teardown
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
beforeEach(() => {
|
|
142
|
+
resetTables();
|
|
143
|
+
setConfigEnabled(false);
|
|
144
|
+
installFetchMock();
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
afterEach(() => {
|
|
148
|
+
globalThis.fetch = originalFetch;
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// ===========================================================================
|
|
152
|
+
// Test: trusted contact setup (platform-mediated)
|
|
153
|
+
// ===========================================================================
|
|
154
|
+
|
|
155
|
+
describe("e2e: trusted contact setup", () => {
|
|
156
|
+
test("upsertContact creates a2a channel for assistant contact", () => {
|
|
157
|
+
setConfigEnabled(true);
|
|
158
|
+
|
|
159
|
+
upsertContact({
|
|
160
|
+
displayName: "Peer Assistant",
|
|
161
|
+
contactType: "assistant",
|
|
162
|
+
role: "contact",
|
|
163
|
+
channels: [
|
|
164
|
+
{
|
|
165
|
+
type: "a2a",
|
|
166
|
+
address: "assistant-b",
|
|
167
|
+
externalUserId: "assistant-b",
|
|
168
|
+
status: "active",
|
|
169
|
+
policy: "allow",
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const contact = findContactByAddress("a2a", "assistant-b");
|
|
175
|
+
expect(contact).not.toBeNull();
|
|
176
|
+
expect(contact!.channels.some((ch) => ch.type === "a2a")).toBe(true);
|
|
177
|
+
const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
|
|
178
|
+
expect(a2aChannel!.status).toBe("active");
|
|
179
|
+
expect(a2aChannel!.address).toBe("assistant-b");
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// ===========================================================================
|
|
184
|
+
// Test: message after trusted contact established
|
|
185
|
+
// ===========================================================================
|
|
186
|
+
|
|
187
|
+
describe("e2e: message delivery after trusted contact established", () => {
|
|
188
|
+
test("task store lifecycle: create -> working -> complete with artifacts", () => {
|
|
189
|
+
// Create a task as if an inbound A2A message arrived
|
|
190
|
+
const msg = makeRequestMessage({
|
|
191
|
+
parts: [{ kind: "text", text: "Order a coffee for me" }],
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const task = createTask({
|
|
195
|
+
senderAssistantId: "assistant-a",
|
|
196
|
+
requestMessage: msg,
|
|
197
|
+
pushUrl: "https://requester.example.com/a2a/push",
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(task.status.state).toBe("submitted");
|
|
201
|
+
|
|
202
|
+
// Transition to working
|
|
203
|
+
const working = updateState(task.id, "working", "Processing request...");
|
|
204
|
+
expect(working.status.state).toBe("working");
|
|
205
|
+
|
|
206
|
+
// Complete with artifacts (simulating the assistant's response)
|
|
207
|
+
const artifacts: Artifact[] = [
|
|
208
|
+
{
|
|
209
|
+
artifact_id: crypto.randomUUID(),
|
|
210
|
+
parts: [{ kind: "text", text: "I'll have a latte" }],
|
|
211
|
+
},
|
|
212
|
+
];
|
|
213
|
+
const completed = completeWithArtifacts(task.id, artifacts);
|
|
214
|
+
|
|
215
|
+
expect(completed.status.state).toBe("completed");
|
|
216
|
+
expect(completed.artifacts).toHaveLength(1);
|
|
217
|
+
expect(completed.artifacts![0].parts[0]).toEqual({
|
|
218
|
+
kind: "text",
|
|
219
|
+
text: "I'll have a latte",
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
// Verify via fresh getTask
|
|
223
|
+
const fetched = getTask(task.id);
|
|
224
|
+
expect(fetched).not.toBeNull();
|
|
225
|
+
expect(fetched!.status.state).toBe("completed");
|
|
226
|
+
expect(fetched!.artifacts).toEqual(completed.artifacts);
|
|
227
|
+
|
|
228
|
+
// Push URL is stored and retrievable
|
|
229
|
+
const pushUrl = getPushUrl(task.id);
|
|
230
|
+
expect(pushUrl).toBe("https://requester.example.com/a2a/push");
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test("delivery adapter completes task and triggers push notification", async () => {
|
|
234
|
+
// Create a task that simulates an inbound A2A request
|
|
235
|
+
const task = createTask({
|
|
236
|
+
senderAssistantId: "assistant-a",
|
|
237
|
+
requestMessage: makeRequestMessage(),
|
|
238
|
+
pushUrl: "https://requester.example.com/a2a/push",
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// Move to working state (as the runtime would)
|
|
242
|
+
updateState(task.id, "working");
|
|
243
|
+
|
|
244
|
+
// Import the delivery adapter
|
|
245
|
+
const { deliverA2AReply } =
|
|
246
|
+
await import("../../messaging/providers/a2a/deliver.js");
|
|
247
|
+
|
|
248
|
+
// Simulate the assistant's response via the delivery adapter
|
|
249
|
+
const payload: ChannelReplyPayload = {
|
|
250
|
+
chatId: "chat-1",
|
|
251
|
+
text: "I'll have a latte",
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
const callbackUrl = `https://example.com/deliver/a2a?taskId=${task.id}`;
|
|
255
|
+
const result = await deliverA2AReply(callbackUrl, payload);
|
|
256
|
+
|
|
257
|
+
expect(result.ok).toBe(true);
|
|
258
|
+
|
|
259
|
+
// Task should be completed in the store
|
|
260
|
+
const completedTask = getTask(task.id);
|
|
261
|
+
expect(completedTask).not.toBeNull();
|
|
262
|
+
expect(completedTask!.status.state).toBe("completed");
|
|
263
|
+
expect(completedTask!.artifacts).toHaveLength(1);
|
|
264
|
+
expect(completedTask!.artifacts![0].parts[0]).toEqual({
|
|
265
|
+
kind: "text",
|
|
266
|
+
text: "I'll have a latte",
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// Wait for the fire-and-forget push notification
|
|
270
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
271
|
+
|
|
272
|
+
// Verify push notification was sent
|
|
273
|
+
const pushCall = fetchCalls.find((c) =>
|
|
274
|
+
c.url.includes("requester.example.com/a2a/push"),
|
|
275
|
+
);
|
|
276
|
+
expect(pushCall).toBeTruthy();
|
|
277
|
+
expect(pushCall!.init.method).toBe("POST");
|
|
278
|
+
|
|
279
|
+
const headers = pushCall!.init.headers as Record<string, string>;
|
|
280
|
+
expect(headers["Content-Type"]).toBe("application/a2a+json");
|
|
281
|
+
expect(headers["A2A-Version"]).toBe("1.0");
|
|
282
|
+
|
|
283
|
+
// Push body should contain the completed task
|
|
284
|
+
const pushBody = JSON.parse(pushCall!.init.body as string);
|
|
285
|
+
expect(pushBody.status.state).toBe("completed");
|
|
286
|
+
expect(pushBody.artifacts).toHaveLength(1);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
// ===========================================================================
|
|
291
|
+
// Test: disabled channel
|
|
292
|
+
// ===========================================================================
|
|
293
|
+
|
|
294
|
+
describe("e2e: disabled channel", () => {
|
|
295
|
+
test("getA2AConfig returns disabled when a2a.enabled is false", () => {
|
|
296
|
+
const result = getA2AConfig();
|
|
297
|
+
expect(result.success).toBe(true);
|
|
298
|
+
expect(result.enabled).toBe(false);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
test("clearA2AConfig disables the channel", () => {
|
|
302
|
+
setConfigEnabled(true);
|
|
303
|
+
const before = getA2AConfig();
|
|
304
|
+
expect(before.enabled).toBe(true);
|
|
305
|
+
|
|
306
|
+
const result = clearA2AConfig();
|
|
307
|
+
expect(result.success).toBe(true);
|
|
308
|
+
expect(result.enabled).toBe(false);
|
|
309
|
+
|
|
310
|
+
const after = getA2AConfig();
|
|
311
|
+
expect(after.enabled).toBe(false);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("setA2AConfig enables the channel and clearA2AConfig disables it", () => {
|
|
315
|
+
// Start disabled
|
|
316
|
+
expect(getA2AConfig().enabled).toBe(false);
|
|
317
|
+
|
|
318
|
+
// Enable
|
|
319
|
+
setA2AConfig();
|
|
320
|
+
expect(getA2AConfig().enabled).toBe(true);
|
|
321
|
+
|
|
322
|
+
// Disable
|
|
323
|
+
clearA2AConfig();
|
|
324
|
+
expect(getA2AConfig().enabled).toBe(false);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
// ===========================================================================
|
|
329
|
+
// Test: unknown sender blocked
|
|
330
|
+
// ===========================================================================
|
|
331
|
+
|
|
332
|
+
describe("e2e: unknown sender blocked (ACL enforcement)", () => {
|
|
333
|
+
test("no trusted contact exists for the sender assistant", () => {
|
|
334
|
+
// Verify there is no contact for the unknown sender
|
|
335
|
+
const contact = findContactByAddress("a2a", "unknown-assistant");
|
|
336
|
+
expect(contact).toBeNull();
|
|
337
|
+
|
|
338
|
+
// Create a task from the unknown sender (as the gateway would)
|
|
339
|
+
const msg = makeRequestMessage({
|
|
340
|
+
parts: [{ kind: "text", text: "Hey, do something for me" }],
|
|
341
|
+
});
|
|
342
|
+
const task = createTask({
|
|
343
|
+
senderAssistantId: "unknown-assistant",
|
|
344
|
+
requestMessage: msg,
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
// The task is created (the gateway always creates a task), but
|
|
348
|
+
// the runtime's ACL check would reject it because there is no
|
|
349
|
+
// trusted contact_channel for "unknown-assistant".
|
|
350
|
+
expect(task.status.state).toBe("submitted");
|
|
351
|
+
|
|
352
|
+
// The ACL layer performs findContactByAddress to resolve trust.
|
|
353
|
+
// For an unknown sender, this returns null — blocking the message.
|
|
354
|
+
const senderContact = findContactByAddress("a2a", "unknown-assistant");
|
|
355
|
+
expect(senderContact).toBeNull();
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("trusted contact exists with active a2a channel — ACL passes", async () => {
|
|
359
|
+
const { upsertContact } = await import("../../contacts/contact-store.js");
|
|
360
|
+
|
|
361
|
+
// Pre-create a trusted contact for the sender
|
|
362
|
+
upsertContact({
|
|
363
|
+
displayName: "Trusted Bot",
|
|
364
|
+
contactType: "assistant",
|
|
365
|
+
role: "contact",
|
|
366
|
+
channels: [
|
|
367
|
+
{
|
|
368
|
+
type: "a2a",
|
|
369
|
+
address: "trusted-assistant",
|
|
370
|
+
externalUserId: "trusted-assistant",
|
|
371
|
+
status: "active",
|
|
372
|
+
policy: "allow",
|
|
373
|
+
},
|
|
374
|
+
],
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Verify the contact exists (the ACL check the runtime performs)
|
|
378
|
+
const contact = findContactByAddress("a2a", "trusted-assistant");
|
|
379
|
+
expect(contact).not.toBeNull();
|
|
380
|
+
|
|
381
|
+
const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
|
|
382
|
+
expect(a2aChannel).toBeTruthy();
|
|
383
|
+
expect(a2aChannel!.status).toBe("active");
|
|
384
|
+
expect(a2aChannel!.policy).toBe("allow");
|
|
385
|
+
|
|
386
|
+
// A task from this sender would pass the ACL check
|
|
387
|
+
const msg = makeRequestMessage();
|
|
388
|
+
const task = createTask({
|
|
389
|
+
senderAssistantId: "trusted-assistant",
|
|
390
|
+
requestMessage: msg,
|
|
391
|
+
});
|
|
392
|
+
expect(task.status.state).toBe("submitted");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("contact exists but channel is blocked — ACL would reject", async () => {
|
|
396
|
+
const { upsertContact } = await import("../../contacts/contact-store.js");
|
|
397
|
+
|
|
398
|
+
upsertContact({
|
|
399
|
+
displayName: "Blocked Bot",
|
|
400
|
+
contactType: "assistant",
|
|
401
|
+
role: "contact",
|
|
402
|
+
channels: [
|
|
403
|
+
{
|
|
404
|
+
type: "a2a",
|
|
405
|
+
address: "blocked-assistant",
|
|
406
|
+
externalUserId: "blocked-assistant",
|
|
407
|
+
status: "blocked",
|
|
408
|
+
policy: "deny",
|
|
409
|
+
},
|
|
410
|
+
],
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
const contact = findContactByAddress("a2a", "blocked-assistant");
|
|
414
|
+
expect(contact).not.toBeNull();
|
|
415
|
+
|
|
416
|
+
const a2aChannel = contact!.channels.find((ch) => ch.type === "a2a");
|
|
417
|
+
expect(a2aChannel!.status).toBe("blocked");
|
|
418
|
+
expect(a2aChannel!.policy).toBe("deny");
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
// ===========================================================================
|
|
423
|
+
// Test: push notification failure gracefully degrades
|
|
424
|
+
// ===========================================================================
|
|
425
|
+
|
|
426
|
+
describe("e2e: push notification failure graceful degradation", () => {
|
|
427
|
+
test("task completes even when push URL returns 500", async () => {
|
|
428
|
+
// Set up a task with a push URL that will fail
|
|
429
|
+
const task = createTask({
|
|
430
|
+
senderAssistantId: "assistant-a",
|
|
431
|
+
requestMessage: makeRequestMessage(),
|
|
432
|
+
pushUrl: "https://failing-push.example.com/a2a/push",
|
|
433
|
+
});
|
|
434
|
+
updateState(task.id, "working");
|
|
435
|
+
|
|
436
|
+
// Mock: all push requests fail with 500
|
|
437
|
+
fetchResponseMap["failing-push.example.com"] = {
|
|
438
|
+
ok: false,
|
|
439
|
+
status: 500,
|
|
440
|
+
body: "Internal Server Error",
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const { deliverA2AReply } =
|
|
444
|
+
await import("../../messaging/providers/a2a/deliver.js");
|
|
445
|
+
|
|
446
|
+
const callbackUrl = `https://example.com/deliver/a2a?taskId=${task.id}`;
|
|
447
|
+
const result = await deliverA2AReply(callbackUrl, {
|
|
448
|
+
chatId: "chat-1",
|
|
449
|
+
text: "Here is your response",
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
// Delivery still succeeds — push failure is fire-and-forget
|
|
453
|
+
expect(result.ok).toBe(true);
|
|
454
|
+
|
|
455
|
+
// Task is completed in the store regardless of push failure
|
|
456
|
+
const completedTask = getTask(task.id);
|
|
457
|
+
expect(completedTask).not.toBeNull();
|
|
458
|
+
expect(completedTask!.status.state).toBe("completed");
|
|
459
|
+
expect(completedTask!.artifacts).toHaveLength(1);
|
|
460
|
+
expect(completedTask!.artifacts![0].parts[0]).toEqual({
|
|
461
|
+
kind: "text",
|
|
462
|
+
text: "Here is your response",
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Wait for push retry attempts to fully settle (3 retries with exponential backoff)
|
|
466
|
+
await new Promise((r) => setTimeout(r, 10_000));
|
|
467
|
+
|
|
468
|
+
// Push was attempted (initial + retries)
|
|
469
|
+
const pushCalls = fetchCalls.filter((c) =>
|
|
470
|
+
c.url.includes("failing-push.example.com"),
|
|
471
|
+
);
|
|
472
|
+
expect(pushCalls.length).toBeGreaterThanOrEqual(1);
|
|
473
|
+
}, 15_000);
|
|
474
|
+
|
|
475
|
+
test("task completes when no push URL is configured", async () => {
|
|
476
|
+
const task = createTask({
|
|
477
|
+
senderAssistantId: "assistant-a",
|
|
478
|
+
requestMessage: makeRequestMessage(),
|
|
479
|
+
// No pushUrl
|
|
480
|
+
});
|
|
481
|
+
updateState(task.id, "working");
|
|
482
|
+
|
|
483
|
+
const { deliverA2AReply } =
|
|
484
|
+
await import("../../messaging/providers/a2a/deliver.js");
|
|
485
|
+
|
|
486
|
+
const callbackUrl = `https://example.com/deliver/a2a?taskId=${task.id}`;
|
|
487
|
+
const result = await deliverA2AReply(callbackUrl, {
|
|
488
|
+
chatId: "chat-1",
|
|
489
|
+
text: "Response without push",
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
expect(result.ok).toBe(true);
|
|
493
|
+
|
|
494
|
+
// No push URL means no fetch calls for push
|
|
495
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
496
|
+
expect(fetchCalls).toHaveLength(0);
|
|
497
|
+
|
|
498
|
+
// Task is still completed
|
|
499
|
+
const completedTask = getTask(task.id);
|
|
500
|
+
expect(completedTask!.status.state).toBe("completed");
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
// ===========================================================================
|
|
505
|
+
// Full round-trip: connect -> trusted contact -> send message -> response -> push
|
|
506
|
+
// ===========================================================================
|
|
507
|
+
|
|
508
|
+
describe("e2e: full A2A round-trip", () => {
|
|
509
|
+
test("connect, establish trust, send message, deliver response, push notification", async () => {
|
|
510
|
+
setConfigEnabled(true);
|
|
511
|
+
|
|
512
|
+
// Step 1: Create trusted contact for Assistant B (platform-mediated)
|
|
513
|
+
upsertContact({
|
|
514
|
+
displayName: "Assistant B",
|
|
515
|
+
contactType: "assistant",
|
|
516
|
+
role: "contact",
|
|
517
|
+
channels: [
|
|
518
|
+
{
|
|
519
|
+
type: "a2a",
|
|
520
|
+
address: "assistant-b",
|
|
521
|
+
externalUserId: "assistant-b",
|
|
522
|
+
status: "active",
|
|
523
|
+
policy: "allow",
|
|
524
|
+
},
|
|
525
|
+
],
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Step 2: Verify trusted contact was created
|
|
529
|
+
const contact = findContactByAddress("a2a", "assistant-b");
|
|
530
|
+
expect(contact).not.toBeNull();
|
|
531
|
+
expect(contact!.channels.find((ch) => ch.type === "a2a")!.status).toBe(
|
|
532
|
+
"active",
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
// Step 3: Simulate inbound A2A message from B (as if B sent us a request)
|
|
536
|
+
const inboundMsg = makeRequestMessage({
|
|
537
|
+
parts: [{ kind: "text", text: "Can you help me with something?" }],
|
|
538
|
+
});
|
|
539
|
+
const task = createTask({
|
|
540
|
+
senderAssistantId: "assistant-b",
|
|
541
|
+
requestMessage: inboundMsg,
|
|
542
|
+
pushUrl: "https://b.example.com/a2a/push",
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
expect(task.status.state).toBe("submitted");
|
|
546
|
+
|
|
547
|
+
// ACL check: trusted contact exists
|
|
548
|
+
const senderContact = findContactByAddress("a2a", "assistant-b");
|
|
549
|
+
expect(senderContact).not.toBeNull();
|
|
550
|
+
|
|
551
|
+
// Step 4: Runtime processes the task
|
|
552
|
+
updateState(task.id, "working");
|
|
553
|
+
|
|
554
|
+
// Step 5: Deliver the response via the delivery adapter
|
|
555
|
+
// Clear previous fetch calls
|
|
556
|
+
fetchCalls.length = 0;
|
|
557
|
+
fetchResponseMap = {};
|
|
558
|
+
defaultFetchResponse = { ok: true, status: 200, body: "{}" };
|
|
559
|
+
|
|
560
|
+
const { deliverA2AReply } =
|
|
561
|
+
await import("../../messaging/providers/a2a/deliver.js");
|
|
562
|
+
|
|
563
|
+
const callbackUrl = `https://example.com/deliver/a2a?taskId=${task.id}`;
|
|
564
|
+
const result = await deliverA2AReply(callbackUrl, {
|
|
565
|
+
chatId: "chat-1",
|
|
566
|
+
text: "Sure, I can help!",
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
expect(result.ok).toBe(true);
|
|
570
|
+
|
|
571
|
+
// Step 6: Verify task completed with artifact
|
|
572
|
+
const completedTask = getTask(task.id);
|
|
573
|
+
expect(completedTask!.status.state).toBe("completed");
|
|
574
|
+
expect(completedTask!.artifacts).toHaveLength(1);
|
|
575
|
+
expect(completedTask!.artifacts![0].parts[0]).toEqual({
|
|
576
|
+
kind: "text",
|
|
577
|
+
text: "Sure, I can help!",
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// Step 7: Verify push notification was sent to B
|
|
581
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
582
|
+
|
|
583
|
+
const pushCall = fetchCalls.find((c) =>
|
|
584
|
+
c.url.includes("b.example.com/a2a/push"),
|
|
585
|
+
);
|
|
586
|
+
expect(pushCall).toBeTruthy();
|
|
587
|
+
expect(pushCall!.init.method).toBe("POST");
|
|
588
|
+
|
|
589
|
+
const pushHeaders = pushCall!.init.headers as Record<string, string>;
|
|
590
|
+
expect(pushHeaders["Content-Type"]).toBe("application/a2a+json");
|
|
591
|
+
expect(pushHeaders["A2A-Version"]).toBe("1.0");
|
|
592
|
+
|
|
593
|
+
const pushBody = JSON.parse(pushCall!.init.body as string);
|
|
594
|
+
expect(pushBody.status.state).toBe("completed");
|
|
595
|
+
expect(pushBody.artifacts).toHaveLength(1);
|
|
596
|
+
});
|
|
597
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { TERMINAL_TASK_STATES } from "../protocol-constants.js";
|
|
4
|
+
import {
|
|
5
|
+
INTERNAL_ERROR,
|
|
6
|
+
INVALID_PARAMS,
|
|
7
|
+
INVALID_REQUEST,
|
|
8
|
+
makeJsonRpcError,
|
|
9
|
+
makeJsonRpcSuccess,
|
|
10
|
+
METHOD_NOT_FOUND,
|
|
11
|
+
PARSE_ERROR,
|
|
12
|
+
TASK_NOT_CANCELABLE,
|
|
13
|
+
TASK_NOT_FOUND,
|
|
14
|
+
UNSUPPORTED_OPERATION,
|
|
15
|
+
} from "../protocol-errors.js";
|
|
16
|
+
|
|
17
|
+
describe("makeJsonRpcError", () => {
|
|
18
|
+
test("produces a valid JSON-RPC 2.0 error response", () => {
|
|
19
|
+
const response = makeJsonRpcError(1, PARSE_ERROR, "Parse error");
|
|
20
|
+
|
|
21
|
+
expect(response).toEqual({
|
|
22
|
+
jsonrpc: "2.0",
|
|
23
|
+
id: 1,
|
|
24
|
+
error: {
|
|
25
|
+
code: -32700,
|
|
26
|
+
message: "Parse error",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("includes data field when provided", () => {
|
|
32
|
+
const response = makeJsonRpcError(
|
|
33
|
+
"req-1",
|
|
34
|
+
INVALID_PARAMS,
|
|
35
|
+
"Invalid params",
|
|
36
|
+
{ field: "message" },
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
expect(response).toEqual({
|
|
40
|
+
jsonrpc: "2.0",
|
|
41
|
+
id: "req-1",
|
|
42
|
+
error: {
|
|
43
|
+
code: -32602,
|
|
44
|
+
message: "Invalid params",
|
|
45
|
+
data: { field: "message" },
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("omits data field when not provided", () => {
|
|
51
|
+
const response = makeJsonRpcError(null, INTERNAL_ERROR, "Internal error");
|
|
52
|
+
|
|
53
|
+
expect(response.error).toBeDefined();
|
|
54
|
+
expect("data" in response.error!).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("accepts null id", () => {
|
|
58
|
+
const response = makeJsonRpcError(null, PARSE_ERROR, "Parse error");
|
|
59
|
+
|
|
60
|
+
expect(response.id).toBeNull();
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("makeJsonRpcSuccess", () => {
|
|
65
|
+
test("produces a valid JSON-RPC 2.0 success response", () => {
|
|
66
|
+
const response = makeJsonRpcSuccess(1, { status: "ok" });
|
|
67
|
+
|
|
68
|
+
expect(response).toEqual({
|
|
69
|
+
jsonrpc: "2.0",
|
|
70
|
+
id: 1,
|
|
71
|
+
result: { status: "ok" },
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("does not include an error field", () => {
|
|
76
|
+
const response = makeJsonRpcSuccess("req-2", null);
|
|
77
|
+
|
|
78
|
+
expect("error" in response).toBe(false);
|
|
79
|
+
expect(response.result).toBeNull();
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("error code constants", () => {
|
|
84
|
+
test("standard JSON-RPC codes", () => {
|
|
85
|
+
expect(PARSE_ERROR).toBe(-32700);
|
|
86
|
+
expect(INVALID_REQUEST).toBe(-32600);
|
|
87
|
+
expect(METHOD_NOT_FOUND).toBe(-32601);
|
|
88
|
+
expect(INVALID_PARAMS).toBe(-32602);
|
|
89
|
+
expect(INTERNAL_ERROR).toBe(-32603);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("A2A-specific codes", () => {
|
|
93
|
+
expect(TASK_NOT_FOUND).toBe(-32001);
|
|
94
|
+
expect(TASK_NOT_CANCELABLE).toBe(-32002);
|
|
95
|
+
expect(UNSUPPORTED_OPERATION).toBe(-32004);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe("TERMINAL_TASK_STATES", () => {
|
|
100
|
+
test("contains exactly the four terminal states", () => {
|
|
101
|
+
expect(TERMINAL_TASK_STATES.size).toBe(4);
|
|
102
|
+
expect(TERMINAL_TASK_STATES.has("completed")).toBe(true);
|
|
103
|
+
expect(TERMINAL_TASK_STATES.has("failed")).toBe(true);
|
|
104
|
+
expect(TERMINAL_TASK_STATES.has("canceled")).toBe(true);
|
|
105
|
+
expect(TERMINAL_TASK_STATES.has("rejected")).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("does not contain non-terminal states", () => {
|
|
109
|
+
expect(TERMINAL_TASK_STATES.has("submitted")).toBe(false);
|
|
110
|
+
expect(TERMINAL_TASK_STATES.has("working")).toBe(false);
|
|
111
|
+
expect(TERMINAL_TASK_STATES.has("input_required")).toBe(false);
|
|
112
|
+
});
|
|
113
|
+
});
|