@vellumai/assistant 0.4.30 → 0.4.32
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 +1 -1
- package/Dockerfile +14 -8
- package/README.md +2 -2
- package/docs/architecture/memory.md +28 -29
- package/docs/runbook-trusted-contacts.md +1 -4
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +0 -7
- package/src/__tests__/anthropic-provider.test.ts +86 -1
- package/src/__tests__/assistant-feature-flags-integration.test.ts +2 -2
- package/src/__tests__/checker.test.ts +37 -98
- package/src/__tests__/commit-message-enrichment-service.test.ts +15 -4
- package/src/__tests__/config-schema.test.ts +6 -14
- package/src/__tests__/conflict-policy.test.ts +76 -0
- package/src/__tests__/conflict-store.test.ts +14 -20
- package/src/__tests__/contacts-tools.test.ts +8 -61
- package/src/__tests__/contradiction-checker.test.ts +5 -1
- package/src/__tests__/credential-security-invariants.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +1 -19
- package/src/__tests__/followup-tools.test.ts +0 -30
- package/src/__tests__/gemini-provider.test.ts +79 -1
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-routing-invariants.test.ts +6 -4
- package/src/__tests__/ipc-snapshot.test.ts +0 -4
- package/src/__tests__/managed-proxy-context.test.ts +163 -0
- package/src/__tests__/memory-lifecycle-e2e.test.ts +13 -12
- package/src/__tests__/memory-regressions.test.ts +6 -6
- package/src/__tests__/openai-provider.test.ts +82 -0
- package/src/__tests__/provider-fail-open-selection.test.ts +134 -1
- package/src/__tests__/provider-managed-proxy-integration.test.ts +269 -0
- package/src/__tests__/recurrence-types.test.ts +0 -15
- package/src/__tests__/registry.test.ts +0 -10
- package/src/__tests__/schedule-tools.test.ts +28 -44
- package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
- package/src/__tests__/session-agent-loop.test.ts +0 -2
- package/src/__tests__/session-conflict-gate.test.ts +243 -388
- package/src/__tests__/session-profile-injection.test.ts +0 -2
- package/src/__tests__/session-runtime-assembly.test.ts +2 -3
- package/src/__tests__/session-skill-tools.test.ts +0 -49
- package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
- package/src/__tests__/session-workspace-injection.test.ts +0 -1
- package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
- package/src/__tests__/skill-feature-flags.test.ts +2 -2
- package/src/__tests__/task-management-tools.test.ts +111 -0
- package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
- package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
- package/src/__tests__/twilio-config.test.ts +0 -3
- package/src/amazon/session.ts +30 -91
- package/src/approvals/guardian-decision-primitive.ts +11 -7
- package/src/approvals/guardian-request-resolvers.ts +5 -3
- package/src/calls/call-controller.ts +423 -571
- package/src/calls/finalize-call.ts +20 -0
- package/src/calls/relay-access-wait.ts +340 -0
- package/src/calls/relay-server.ts +269 -899
- package/src/calls/relay-setup-router.ts +307 -0
- package/src/calls/relay-verification.ts +280 -0
- package/src/calls/twilio-config.ts +1 -8
- package/src/calls/voice-control-protocol.ts +184 -0
- package/src/calls/voice-session-bridge.ts +1 -8
- package/src/config/agent-schema.ts +1 -1
- package/src/config/bundled-skills/contacts/SKILL.md +7 -18
- package/src/config/bundled-skills/contacts/TOOLS.json +4 -20
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +2 -4
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +6 -12
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +3 -24
- package/src/config/bundled-skills/followups/TOOLS.json +0 -4
- package/src/config/bundled-skills/schedule/SKILL.md +1 -1
- package/src/config/bundled-skills/schedule/TOOLS.json +2 -10
- package/src/config/bundled-tool-registry.ts +0 -5
- package/src/config/core-schema.ts +1 -1
- package/src/config/env.ts +0 -10
- package/src/config/feature-flag-registry.json +1 -1
- package/src/config/loader.ts +19 -0
- package/src/config/memory-schema.ts +0 -10
- package/src/config/schema.ts +2 -2
- package/src/config/system-prompt.ts +6 -0
- package/src/contacts/contact-store.ts +36 -62
- package/src/contacts/contacts-write.ts +14 -3
- package/src/contacts/types.ts +9 -4
- package/src/daemon/handlers/config-heartbeat.ts +1 -2
- package/src/daemon/handlers/contacts.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +1 -1
- package/src/daemon/handlers/session-history.ts +398 -0
- package/src/daemon/handlers/session-user-message.ts +982 -0
- package/src/daemon/handlers/sessions.ts +9 -1337
- package/src/daemon/ipc-contract/contacts.ts +2 -2
- package/src/daemon/ipc-contract/sessions.ts +0 -6
- package/src/daemon/ipc-contract-inventory.json +0 -1
- package/src/daemon/lifecycle.ts +0 -29
- package/src/daemon/session-agent-loop.ts +1 -45
- package/src/daemon/session-conflict-gate.ts +21 -82
- package/src/daemon/session-memory.ts +7 -52
- package/src/daemon/session-process.ts +3 -1
- package/src/daemon/session-runtime-assembly.ts +18 -35
- package/src/heartbeat/heartbeat-service.ts +5 -1
- package/src/home-base/app-link-store.ts +0 -7
- package/src/memory/conflict-intent.ts +3 -6
- package/src/memory/conflict-policy.ts +34 -0
- package/src/memory/conflict-store.ts +10 -18
- package/src/memory/contradiction-checker.ts +2 -2
- package/src/memory/conversation-attention-store.ts +1 -1
- package/src/memory/conversation-store.ts +0 -51
- package/src/memory/db-init.ts +8 -0
- package/src/memory/job-handlers/conflict.ts +24 -7
- package/src/memory/migrations/105-contacts-and-triage.ts +4 -7
- package/src/memory/migrations/134-contacts-notes-column.ts +68 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
- package/src/memory/migrations/index.ts +2 -0
- package/src/memory/migrations/registry.ts +6 -0
- package/src/memory/recall-cache.ts +0 -5
- package/src/memory/schema/calls.ts +274 -0
- package/src/memory/schema/contacts.ts +125 -0
- package/src/memory/schema/conversations.ts +129 -0
- package/src/memory/schema/guardian.ts +172 -0
- package/src/memory/schema/index.ts +8 -0
- package/src/memory/schema/infrastructure.ts +205 -0
- package/src/memory/schema/memory-core.ts +196 -0
- package/src/memory/schema/notifications.ts +191 -0
- package/src/memory/schema/tasks.ts +78 -0
- package/src/memory/schema.ts +1 -1402
- package/src/memory/slack-thread-store.ts +0 -69
- package/src/messaging/index.ts +0 -1
- package/src/messaging/types.ts +0 -38
- package/src/notifications/decisions-store.ts +2 -105
- package/src/notifications/deliveries-store.ts +0 -11
- package/src/notifications/preferences-store.ts +1 -58
- package/src/permissions/checker.ts +6 -17
- package/src/providers/anthropic/client.ts +6 -2
- package/src/providers/gemini/client.ts +13 -2
- package/src/providers/managed-proxy/constants.ts +55 -0
- package/src/providers/managed-proxy/context.ts +77 -0
- package/src/providers/registry.ts +112 -0
- package/src/runtime/auth/__tests__/guard-tests.test.ts +51 -23
- package/src/runtime/guardian-action-service.ts +3 -2
- package/src/runtime/guardian-outbound-actions.ts +3 -3
- package/src/runtime/guardian-reply-router.ts +4 -4
- package/src/runtime/http-server.ts +83 -710
- package/src/runtime/http-types.ts +0 -16
- package/src/runtime/middleware/auth.ts +0 -12
- package/src/runtime/routes/app-routes.ts +33 -0
- package/src/runtime/routes/approval-routes.ts +32 -0
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
- package/src/runtime/routes/attachment-routes.ts +32 -0
- package/src/runtime/routes/brain-graph-routes.ts +27 -0
- package/src/runtime/routes/call-routes.ts +41 -0
- package/src/runtime/routes/channel-readiness-routes.ts +20 -0
- package/src/runtime/routes/channel-routes.ts +70 -0
- package/src/runtime/routes/contact-routes.ts +371 -29
- package/src/runtime/routes/conversation-attention-routes.ts +15 -0
- package/src/runtime/routes/conversation-routes.ts +192 -194
- package/src/runtime/routes/debug-routes.ts +15 -0
- package/src/runtime/routes/events-routes.ts +16 -0
- package/src/runtime/routes/global-search-routes.ts +17 -2
- package/src/runtime/routes/guardian-action-routes.ts +23 -1
- package/src/runtime/routes/guardian-approval-interception.ts +2 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +26 -1
- package/src/runtime/routes/guardian-refresh-routes.ts +20 -0
- package/src/runtime/routes/identity-routes.ts +20 -0
- package/src/runtime/routes/inbound-message-handler.ts +8 -0
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +5 -1
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
- package/src/runtime/routes/integration-routes.ts +83 -0
- package/src/runtime/routes/invite-routes.ts +31 -0
- package/src/runtime/routes/migration-routes.ts +47 -17
- package/src/runtime/routes/pairing-routes.ts +18 -0
- package/src/runtime/routes/secret-routes.ts +20 -0
- package/src/runtime/routes/surface-action-routes.ts +26 -0
- package/src/runtime/routes/trust-rules-routes.ts +31 -0
- package/src/runtime/routes/twilio-routes.ts +79 -0
- package/src/schedule/recurrence-types.ts +1 -11
- package/src/tools/followups/followup_create.ts +9 -3
- package/src/tools/mcp/mcp-tool-factory.ts +0 -17
- package/src/tools/memory/definitions.ts +0 -6
- package/src/tools/network/script-proxy/session-manager.ts +38 -3
- package/src/tools/schedule/create.ts +1 -3
- package/src/tools/schedule/update.ts +9 -6
- package/src/twitter/session.ts +29 -77
- package/src/util/cookie-session.ts +114 -0
- package/src/workspace/git-service.ts +6 -4
- package/src/__tests__/conversation-routes.test.ts +0 -99
- package/src/__tests__/get-weather.test.ts +0 -393
- package/src/__tests__/task-tools.test.ts +0 -685
- package/src/__tests__/weather-skill-regression.test.ts +0 -276
- package/src/autonomy/autonomy-resolver.ts +0 -62
- package/src/autonomy/autonomy-store.ts +0 -138
- package/src/autonomy/disposition-mapper.ts +0 -31
- package/src/autonomy/index.ts +0 -11
- package/src/autonomy/types.ts +0 -43
- package/src/config/bundled-skills/weather/SKILL.md +0 -38
- package/src/config/bundled-skills/weather/TOOLS.json +0 -36
- package/src/config/bundled-skills/weather/icon.svg +0 -24
- package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
- package/src/contacts/startup-migration.ts +0 -21
- package/src/messaging/triage-engine.ts +0 -344
- package/src/tools/weather/service.ts +0 -712
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
executeGetWeather,
|
|
5
|
-
weatherCodeToDescription,
|
|
6
|
-
} from "../tools/weather/service.js";
|
|
7
|
-
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
// Helper: build a mock fetch that returns predefined geocoding & weather data
|
|
10
|
-
// ---------------------------------------------------------------------------
|
|
11
|
-
|
|
12
|
-
function createMockFetch(options?: {
|
|
13
|
-
geoResults?: unknown[];
|
|
14
|
-
geoStatus?: number;
|
|
15
|
-
geoError?: Error;
|
|
16
|
-
forecastData?: unknown;
|
|
17
|
-
forecastStatus?: number;
|
|
18
|
-
forecastError?: Error;
|
|
19
|
-
}): typeof globalThis.fetch {
|
|
20
|
-
const {
|
|
21
|
-
geoResults,
|
|
22
|
-
geoStatus = 200,
|
|
23
|
-
geoError,
|
|
24
|
-
forecastData,
|
|
25
|
-
forecastStatus = 200,
|
|
26
|
-
forecastError,
|
|
27
|
-
} = options ?? {};
|
|
28
|
-
|
|
29
|
-
const defaultGeoResults = [
|
|
30
|
-
{
|
|
31
|
-
name: "San Francisco",
|
|
32
|
-
latitude: 37.7749,
|
|
33
|
-
longitude: -122.4194,
|
|
34
|
-
country: "United States",
|
|
35
|
-
admin1: "California",
|
|
36
|
-
},
|
|
37
|
-
];
|
|
38
|
-
|
|
39
|
-
// Generate 48 hourly entries starting from 2025-01-15T00:00
|
|
40
|
-
const hourlyTimes: string[] = [];
|
|
41
|
-
const hourlyTemps: number[] = [];
|
|
42
|
-
const hourlyCodes: number[] = [];
|
|
43
|
-
const hourlyIsDay: number[] = [];
|
|
44
|
-
for (let i = 0; i < 48; i++) {
|
|
45
|
-
const h = i % 24;
|
|
46
|
-
hourlyTimes.push(
|
|
47
|
-
`2025-01-${15 + Math.floor(i / 24)}T${String(h).padStart(2, "0")}:00`,
|
|
48
|
-
);
|
|
49
|
-
hourlyTemps.push(10 + Math.sin(i / 4) * 5);
|
|
50
|
-
hourlyCodes.push(i % 3 === 0 ? 0 : 2);
|
|
51
|
-
hourlyIsDay.push(h >= 7 && h < 19 ? 1 : 0);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const defaultForecastData = {
|
|
55
|
-
current: {
|
|
56
|
-
time: "2025-01-15T08:00",
|
|
57
|
-
temperature_2m: 15.0,
|
|
58
|
-
relative_humidity_2m: 72,
|
|
59
|
-
apparent_temperature: 13.5,
|
|
60
|
-
weather_code: 2,
|
|
61
|
-
wind_speed_10m: 18.0,
|
|
62
|
-
wind_direction_10m: 270,
|
|
63
|
-
},
|
|
64
|
-
current_units: {
|
|
65
|
-
temperature_2m: "\u00B0C",
|
|
66
|
-
relative_humidity_2m: "%",
|
|
67
|
-
apparent_temperature: "\u00B0C",
|
|
68
|
-
wind_speed_10m: "km/h",
|
|
69
|
-
wind_direction_10m: "\u00B0",
|
|
70
|
-
},
|
|
71
|
-
hourly: {
|
|
72
|
-
time: hourlyTimes,
|
|
73
|
-
temperature_2m: hourlyTemps,
|
|
74
|
-
weather_code: hourlyCodes,
|
|
75
|
-
is_day: hourlyIsDay,
|
|
76
|
-
},
|
|
77
|
-
hourly_units: {
|
|
78
|
-
temperature_2m: "\u00B0C",
|
|
79
|
-
weather_code: "wmo code",
|
|
80
|
-
is_day: "",
|
|
81
|
-
},
|
|
82
|
-
daily: {
|
|
83
|
-
time: [
|
|
84
|
-
"2025-01-15",
|
|
85
|
-
"2025-01-16",
|
|
86
|
-
"2025-01-17",
|
|
87
|
-
"2025-01-18",
|
|
88
|
-
"2025-01-19",
|
|
89
|
-
],
|
|
90
|
-
weather_code: [2, 61, 3, 0, 1],
|
|
91
|
-
temperature_2m_max: [17.0, 14.0, 16.0, 19.0, 20.0],
|
|
92
|
-
temperature_2m_min: [10.0, 8.0, 9.0, 11.0, 12.0],
|
|
93
|
-
precipitation_probability_max: [10, 80, 30, 0, 5],
|
|
94
|
-
},
|
|
95
|
-
daily_units: {
|
|
96
|
-
temperature_2m_max: "\u00B0C",
|
|
97
|
-
temperature_2m_min: "\u00B0C",
|
|
98
|
-
precipitation_probability_max: "%",
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
return (async (url: string | URL | Request) => {
|
|
103
|
-
const urlStr =
|
|
104
|
-
typeof url === "string" ? url : url instanceof URL ? url.href : url.url;
|
|
105
|
-
|
|
106
|
-
if (urlStr.includes("geocoding-api.open-meteo.com")) {
|
|
107
|
-
if (geoError) throw geoError;
|
|
108
|
-
return new Response(
|
|
109
|
-
JSON.stringify({ results: geoResults ?? defaultGeoResults }),
|
|
110
|
-
{
|
|
111
|
-
status: geoStatus,
|
|
112
|
-
headers: { "content-type": "application/json" },
|
|
113
|
-
},
|
|
114
|
-
);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (urlStr.includes("api.open-meteo.com")) {
|
|
118
|
-
if (forecastError) throw forecastError;
|
|
119
|
-
return new Response(JSON.stringify(forecastData ?? defaultForecastData), {
|
|
120
|
-
status: forecastStatus,
|
|
121
|
-
headers: { "content-type": "application/json" },
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
return new Response("Not found", { status: 404 });
|
|
126
|
-
}) as typeof globalThis.fetch;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
// Weather code mapping
|
|
131
|
-
// ---------------------------------------------------------------------------
|
|
132
|
-
|
|
133
|
-
describe("weatherCodeToDescription", () => {
|
|
134
|
-
test("maps clear sky (code 0)", () => {
|
|
135
|
-
expect(weatherCodeToDescription(0)).toBe("Clear sky");
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
test("maps partly cloudy (code 2)", () => {
|
|
139
|
-
expect(weatherCodeToDescription(2)).toBe("Partly cloudy");
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
test("maps moderate rain (code 63)", () => {
|
|
143
|
-
expect(weatherCodeToDescription(63)).toBe("Moderate rain");
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test("maps heavy snowfall (code 75)", () => {
|
|
147
|
-
expect(weatherCodeToDescription(75)).toBe("Heavy snowfall");
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
test("maps thunderstorm (code 95)", () => {
|
|
151
|
-
expect(weatherCodeToDescription(95)).toBe("Thunderstorm");
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
test("maps thunderstorm with heavy hail (code 99)", () => {
|
|
155
|
-
expect(weatherCodeToDescription(99)).toBe("Thunderstorm with heavy hail");
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test("returns Unknown for unrecognized codes", () => {
|
|
159
|
-
expect(weatherCodeToDescription(42)).toBe("Unknown");
|
|
160
|
-
expect(weatherCodeToDescription(100)).toBe("Unknown");
|
|
161
|
-
expect(weatherCodeToDescription(-1)).toBe("Unknown");
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
// ---------------------------------------------------------------------------
|
|
166
|
-
// Geocoding parsing
|
|
167
|
-
// ---------------------------------------------------------------------------
|
|
168
|
-
|
|
169
|
-
describe("geocoding parsing", () => {
|
|
170
|
-
test("extracts location name, admin1, and country from geocoding results", async () => {
|
|
171
|
-
const mockFetch = createMockFetch();
|
|
172
|
-
const result = await executeGetWeather(
|
|
173
|
-
{ location: "San Francisco" },
|
|
174
|
-
mockFetch,
|
|
175
|
-
);
|
|
176
|
-
|
|
177
|
-
expect(result.isError).toBe(false);
|
|
178
|
-
expect(result.content).toContain(
|
|
179
|
-
"San Francisco, California, United States",
|
|
180
|
-
);
|
|
181
|
-
expect(result.content).toContain("37.7749");
|
|
182
|
-
expect(result.content).toContain("-122.4194");
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
test("handles location without admin1", async () => {
|
|
186
|
-
const mockFetch = createMockFetch({
|
|
187
|
-
geoResults: [
|
|
188
|
-
{
|
|
189
|
-
name: "Tokyo",
|
|
190
|
-
latitude: 35.6762,
|
|
191
|
-
longitude: 139.6503,
|
|
192
|
-
country: "Japan",
|
|
193
|
-
},
|
|
194
|
-
],
|
|
195
|
-
});
|
|
196
|
-
const result = await executeGetWeather({ location: "Tokyo" }, mockFetch);
|
|
197
|
-
|
|
198
|
-
expect(result.isError).toBe(false);
|
|
199
|
-
expect(result.content).toContain("Tokyo, Japan");
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
test("handles location without country", async () => {
|
|
203
|
-
const mockFetch = createMockFetch({
|
|
204
|
-
geoResults: [{ name: "Somewhere", latitude: 0, longitude: 0 }],
|
|
205
|
-
});
|
|
206
|
-
const result = await executeGetWeather(
|
|
207
|
-
{ location: "Somewhere" },
|
|
208
|
-
mockFetch,
|
|
209
|
-
);
|
|
210
|
-
|
|
211
|
-
expect(result.isError).toBe(false);
|
|
212
|
-
expect(result.content).toContain("Weather for Somewhere");
|
|
213
|
-
});
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
// Weather output formatting
|
|
218
|
-
// ---------------------------------------------------------------------------
|
|
219
|
-
|
|
220
|
-
describe("weather output formatting", () => {
|
|
221
|
-
test("returns current conditions in fahrenheit by default", async () => {
|
|
222
|
-
const mockFetch = createMockFetch();
|
|
223
|
-
const result = await executeGetWeather(
|
|
224
|
-
{ location: "San Francisco" },
|
|
225
|
-
mockFetch,
|
|
226
|
-
);
|
|
227
|
-
|
|
228
|
-
expect(result.isError).toBe(false);
|
|
229
|
-
// 15C = 59F
|
|
230
|
-
expect(result.content).toContain("59\u00B0F");
|
|
231
|
-
expect(result.content).toContain("Humidity: 72%");
|
|
232
|
-
expect(result.content).toContain("Partly cloudy");
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test("returns current conditions in celsius when requested", async () => {
|
|
236
|
-
const mockFetch = createMockFetch();
|
|
237
|
-
const result = await executeGetWeather(
|
|
238
|
-
{ location: "San Francisco", units: "celsius" },
|
|
239
|
-
mockFetch,
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
expect(result.isError).toBe(false);
|
|
243
|
-
expect(result.content).toContain("15\u00B0C");
|
|
244
|
-
expect(result.content).toContain("Humidity: 72%");
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test("includes 10-day forecast by default", async () => {
|
|
248
|
-
const mockFetch = createMockFetch();
|
|
249
|
-
const result = await executeGetWeather(
|
|
250
|
-
{ location: "San Francisco" },
|
|
251
|
-
mockFetch,
|
|
252
|
-
);
|
|
253
|
-
|
|
254
|
-
expect(result.isError).toBe(false);
|
|
255
|
-
expect(result.content).toContain("10-Day Forecast");
|
|
256
|
-
expect(result.content).toContain("2025-01-15");
|
|
257
|
-
expect(result.content).toContain("2025-01-19");
|
|
258
|
-
expect(result.content).toContain("Precip");
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
test("wind speed is converted to mph in fahrenheit mode", async () => {
|
|
262
|
-
const mockFetch = createMockFetch();
|
|
263
|
-
const result = await executeGetWeather(
|
|
264
|
-
{ location: "San Francisco" },
|
|
265
|
-
mockFetch,
|
|
266
|
-
);
|
|
267
|
-
|
|
268
|
-
expect(result.isError).toBe(false);
|
|
269
|
-
// 18 km/h ~= 11 mph
|
|
270
|
-
expect(result.content).toContain("11 mph");
|
|
271
|
-
expect(result.content).toContain("W");
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
test("wind speed stays in km/h in celsius mode", async () => {
|
|
275
|
-
const mockFetch = createMockFetch();
|
|
276
|
-
const result = await executeGetWeather(
|
|
277
|
-
{ location: "San Francisco", units: "celsius" },
|
|
278
|
-
mockFetch,
|
|
279
|
-
);
|
|
280
|
-
|
|
281
|
-
expect(result.isError).toBe(false);
|
|
282
|
-
expect(result.content).toContain("18 km/h");
|
|
283
|
-
});
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
// ---------------------------------------------------------------------------
|
|
287
|
-
// Error handling
|
|
288
|
-
// ---------------------------------------------------------------------------
|
|
289
|
-
|
|
290
|
-
describe("error handling", () => {
|
|
291
|
-
test("returns error for missing location", async () => {
|
|
292
|
-
const result = await executeGetWeather({});
|
|
293
|
-
expect(result.isError).toBe(true);
|
|
294
|
-
expect(result.content).toContain("location is required");
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
test("returns error for empty location", async () => {
|
|
298
|
-
const result = await executeGetWeather({ location: "" });
|
|
299
|
-
expect(result.isError).toBe(true);
|
|
300
|
-
expect(result.content).toContain("location is required");
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
test("returns error for whitespace-only location", async () => {
|
|
304
|
-
const result = await executeGetWeather({ location: " " });
|
|
305
|
-
expect(result.isError).toBe(true);
|
|
306
|
-
expect(result.content).toContain("location is required");
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
test("returns error for invalid units", async () => {
|
|
310
|
-
const result = await executeGetWeather({
|
|
311
|
-
location: "NYC",
|
|
312
|
-
units: "kelvin",
|
|
313
|
-
});
|
|
314
|
-
expect(result.isError).toBe(true);
|
|
315
|
-
expect(result.content).toContain('units must be "celsius" or "fahrenheit"');
|
|
316
|
-
});
|
|
317
|
-
|
|
318
|
-
test("returns error when location is not found", async () => {
|
|
319
|
-
const mockFetch = createMockFetch({ geoResults: [] });
|
|
320
|
-
const result = await executeGetWeather(
|
|
321
|
-
{ location: "xyznonexistent" },
|
|
322
|
-
mockFetch,
|
|
323
|
-
);
|
|
324
|
-
|
|
325
|
-
expect(result.isError).toBe(true);
|
|
326
|
-
expect(result.content).toContain("Could not find location");
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
test("returns error when geocoding returns no results array", async () => {
|
|
330
|
-
// Return empty object with no results key
|
|
331
|
-
const emptyGeoFetch = (async () => {
|
|
332
|
-
return new Response(JSON.stringify({}), {
|
|
333
|
-
status: 200,
|
|
334
|
-
headers: { "content-type": "application/json" },
|
|
335
|
-
});
|
|
336
|
-
}) as unknown as typeof globalThis.fetch;
|
|
337
|
-
|
|
338
|
-
const result = await executeGetWeather(
|
|
339
|
-
{ location: "unknown" },
|
|
340
|
-
emptyGeoFetch,
|
|
341
|
-
);
|
|
342
|
-
expect(result.isError).toBe(true);
|
|
343
|
-
expect(result.content).toContain("Could not find location");
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
test("returns error when geocoding API returns non-200", async () => {
|
|
347
|
-
const mockFetch = createMockFetch({ geoStatus: 500 });
|
|
348
|
-
const result = await executeGetWeather(
|
|
349
|
-
{ location: "San Francisco" },
|
|
350
|
-
mockFetch,
|
|
351
|
-
);
|
|
352
|
-
|
|
353
|
-
expect(result.isError).toBe(true);
|
|
354
|
-
expect(result.content).toContain("Geocoding API returned HTTP 500");
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test("returns error when weather API returns non-200", async () => {
|
|
358
|
-
const mockFetch = createMockFetch({ forecastStatus: 503 });
|
|
359
|
-
const result = await executeGetWeather(
|
|
360
|
-
{ location: "San Francisco" },
|
|
361
|
-
mockFetch,
|
|
362
|
-
);
|
|
363
|
-
|
|
364
|
-
expect(result.isError).toBe(true);
|
|
365
|
-
expect(result.content).toContain("Weather API returned HTTP 503");
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
test("returns error when geocoding fetch throws", async () => {
|
|
369
|
-
const mockFetch = createMockFetch({ geoError: new Error("Network error") });
|
|
370
|
-
const result = await executeGetWeather(
|
|
371
|
-
{ location: "San Francisco" },
|
|
372
|
-
mockFetch,
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
expect(result.isError).toBe(true);
|
|
376
|
-
expect(result.content).toContain("Geocoding request failed");
|
|
377
|
-
expect(result.content).toContain("Network error");
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
test("returns error when weather fetch throws", async () => {
|
|
381
|
-
const mockFetch = createMockFetch({
|
|
382
|
-
forecastError: new Error("Connection refused"),
|
|
383
|
-
});
|
|
384
|
-
const result = await executeGetWeather(
|
|
385
|
-
{ location: "San Francisco" },
|
|
386
|
-
mockFetch,
|
|
387
|
-
);
|
|
388
|
-
|
|
389
|
-
expect(result.isError).toBe(true);
|
|
390
|
-
expect(result.content).toContain("Weather forecast request failed");
|
|
391
|
-
expect(result.content).toContain("Connection refused");
|
|
392
|
-
});
|
|
393
|
-
});
|