@vellumai/assistant 0.4.29 → 0.4.31
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 +39 -37
- package/Dockerfile +14 -8
- package/README.md +7 -8
- package/docs/architecture/memory.md +28 -29
- package/docs/runbook-trusted-contacts.md +76 -43
- package/package.json +1 -1
- package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
- package/scripts/test.sh +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
- package/src/__tests__/actor-token-service.test.ts +4 -3
- package/src/__tests__/app-executors.test.ts +7 -17
- package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
- package/src/__tests__/browser-skill-endstate.test.ts +10 -1
- package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
- package/src/__tests__/channel-approval-routes.test.ts +44 -44
- package/src/__tests__/channel-approval.test.ts +8 -0
- package/src/__tests__/channel-approvals.test.ts +39 -1
- package/src/__tests__/channel-guardian.test.ts +15 -5
- package/src/__tests__/channel-reply-delivery.test.ts +31 -0
- package/src/__tests__/config-schema.test.ts +0 -9
- 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__/dynamic-skill-workflow-prompt.test.ts +9 -0
- package/src/__tests__/gateway-only-guard.test.ts +1 -0
- package/src/__tests__/gemini-image-service.test.ts +2 -2
- package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
- package/src/__tests__/guardian-grant-minting.test.ts +6 -6
- package/src/__tests__/guardian-routing-invariants.test.ts +40 -15
- package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
- package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
- package/src/__tests__/integrations-cli.test.ts +3 -27
- package/src/__tests__/intent-routing.test.ts +3 -0
- package/src/__tests__/invite-redemption-service.test.ts +1 -1
- package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
- package/src/__tests__/ipc-snapshot.test.ts +4 -31
- package/src/__tests__/memory-lifecycle-e2e.test.ts +11 -10
- package/src/__tests__/nl-approval-parser.test.ts +305 -0
- package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
- package/src/__tests__/provider-error-scenarios.test.ts +68 -0
- package/src/__tests__/registry.test.ts +0 -10
- package/src/__tests__/relay-server.test.ts +1 -1
- package/src/__tests__/retry-after-extraction.test.ts +111 -0
- package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
- 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-media-retry.test.ts +147 -0
- 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-integration.test.ts +9 -5
- package/src/__tests__/skill-feature-flags.test.ts +18 -12
- package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
- package/src/__tests__/slack-block-formatting.test.ts +100 -0
- package/src/__tests__/slack-inbound-verification.test.ts +346 -0
- package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
- package/src/__tests__/slack-skill.test.ts +3 -2
- package/src/__tests__/starter-task-flow.test.ts +0 -1
- 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__/trusted-contact-verification.test.ts +3 -1
- package/src/__tests__/voice-invite-redemption.test.ts +1 -1
- package/src/amazon/client.ts +7 -24
- package/src/approvals/guardian-decision-primitive.ts +11 -7
- package/src/approvals/guardian-request-resolvers.ts +5 -3
- package/src/calls/relay-server.ts +44 -11
- package/src/channels/config.ts +1 -1
- package/src/cli/integrations.ts +10 -66
- package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
- package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
- package/src/config/bundled-skills/browser/TOOLS.json +59 -2
- package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
- package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
- package/src/config/bundled-skills/contacts/SKILL.md +49 -53
- package/src/config/bundled-skills/contacts/TOOLS.json +26 -22
- package/src/config/bundled-skills/contacts/tools/contact-merge.ts +40 -62
- package/src/config/bundled-skills/contacts/tools/contact-search.ts +17 -43
- package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +18 -57
- package/src/config/bundled-skills/document/TOOLS.json +8 -0
- package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
- package/src/config/bundled-skills/followups/TOOLS.json +12 -0
- package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
- package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
- package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
- package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
- package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
- package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
- package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
- package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
- package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
- package/src/config/bundled-skills/notifications/SKILL.md +3 -2
- package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
- package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
- package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
- package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
- package/src/config/bundled-skills/schedule/SKILL.md +33 -15
- package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
- package/src/config/bundled-skills/slack/SKILL.md +30 -1
- package/src/config/bundled-skills/slack/TOOLS.json +89 -2
- package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
- package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
- package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
- package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
- package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
- package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
- package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
- package/src/config/bundled-tool-registry.ts +2 -5
- package/src/config/channel-permission-profiles.ts +155 -0
- package/src/config/env.ts +4 -1
- package/src/config/memory-schema.ts +0 -10
- package/src/config/system-prompt.ts +6 -0
- package/src/contacts/contact-store.ts +221 -56
- package/src/contacts/contacts-write.ts +14 -3
- package/src/contacts/types.ts +35 -4
- package/src/daemon/assistant-attachments.ts +23 -3
- package/src/daemon/guardian-verification-intent.ts +7 -4
- package/src/daemon/handlers/apps.ts +1 -2
- package/src/daemon/handlers/config-heartbeat.ts +1 -2
- package/src/daemon/handlers/config-inbox.ts +16 -134
- package/src/daemon/handlers/contacts.ts +2 -2
- package/src/daemon/handlers/guardian-actions.ts +21 -88
- package/src/daemon/handlers/sessions.ts +2 -2
- package/src/daemon/ipc-contract/apps.ts +0 -1
- package/src/daemon/ipc-contract/contacts.ts +2 -2
- package/src/daemon/ipc-contract/inbox.ts +7 -66
- package/src/daemon/ipc-contract/sessions.ts +1 -0
- package/src/daemon/ipc-contract/surfaces.ts +0 -1
- package/src/daemon/ipc-contract-inventory.json +2 -4
- package/src/daemon/lifecycle.ts +14 -2
- package/src/daemon/session-agent-loop-handlers.ts +9 -0
- package/src/daemon/session-agent-loop.ts +2 -45
- package/src/daemon/session-attachments.ts +5 -1
- package/src/daemon/session-conflict-gate.ts +21 -82
- package/src/daemon/session-error.ts +18 -0
- package/src/daemon/session-lifecycle.ts +4 -5
- package/src/daemon/session-media-retry.ts +15 -1
- 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/daemon/session-surfaces.ts +0 -1
- package/src/daemon/session-tool-setup.ts +7 -4
- package/src/events/domain-events.ts +2 -1
- package/src/heartbeat/heartbeat-service.ts +5 -1
- package/src/home-base/prebuilt/seed.ts +0 -1
- package/src/influencer/client.ts +7 -24
- package/src/media/gemini-image-service.ts +48 -3
- package/src/memory/app-store.ts +0 -4
- 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 +3 -1
- package/src/memory/db-init.ts +8 -0
- package/src/memory/job-handlers/conflict.ts +0 -7
- package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
- package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
- package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/schema.ts +12 -17
- package/src/memory/slack-thread-store.ts +187 -0
- package/src/messaging/index.ts +0 -1
- package/src/messaging/providers/slack/client.ts +84 -26
- package/src/messaging/providers/slack/types.ts +4 -0
- package/src/messaging/types.ts +0 -38
- package/src/notifications/adapters/slack.ts +90 -0
- package/src/notifications/destination-resolver.ts +42 -1
- package/src/notifications/emit-signal.ts +17 -1
- package/src/oauth/provider-profiles.ts +22 -0
- package/src/providers/anthropic/client.ts +3 -0
- package/src/providers/openai/client.ts +3 -0
- package/src/providers/retry.ts +9 -1
- package/src/runtime/actor-trust-resolver.ts +8 -0
- package/src/runtime/auth/require-bound-guardian.ts +44 -0
- package/src/runtime/auth/route-policy.ts +4 -8
- package/src/runtime/channel-approval-types.ts +18 -0
- package/src/runtime/channel-approvals.ts +8 -0
- package/src/runtime/channel-invite-transport.ts +1 -1
- package/src/runtime/channel-reply-delivery.ts +62 -3
- package/src/runtime/gateway-client.ts +36 -2
- package/src/runtime/gateway-internal-client.ts +86 -0
- package/src/runtime/guardian-action-service.ts +128 -0
- package/src/runtime/guardian-outbound-actions.ts +3 -3
- package/src/runtime/guardian-reply-router.ts +4 -4
- package/src/runtime/guardian-verification-templates.ts +16 -1
- package/src/runtime/http-server.ts +29 -46
- package/src/runtime/invite-redemption-service.ts +1 -1
- package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
- package/src/runtime/nl-approval-parser.ts +138 -0
- package/src/runtime/routes/approval-routes.ts +1 -40
- package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
- package/src/runtime/routes/channel-route-shared.ts +35 -1
- package/src/runtime/routes/contact-routes.ts +494 -47
- package/src/runtime/routes/conversation-routes.ts +2 -1
- package/src/runtime/routes/global-search-routes.ts +2 -2
- package/src/runtime/routes/guardian-action-routes.ts +19 -111
- package/src/runtime/routes/guardian-approval-interception.ts +78 -1
- package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
- package/src/runtime/routes/inbound-message-handler.ts +40 -12
- package/src/runtime/routes/inbound-stages/acl-enforcement.ts +227 -1
- package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
- package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
- package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
- package/src/runtime/routes/migration-routes.ts +17 -17
- package/src/runtime/slack-block-formatting.ts +176 -0
- package/src/schedule/scheduler.ts +11 -2
- package/src/tools/apps/executors.ts +16 -15
- package/src/tools/calls/call-end.ts +1 -1
- package/src/tools/computer-use/definitions.ts +16 -0
- package/src/tools/credentials/vault.ts +86 -2
- package/src/tools/network/script-proxy/session-manager.ts +28 -3
- package/src/tools/permission-checker.ts +18 -0
- package/src/tools/terminal/shell.ts +15 -5
- package/src/tools/tool-approval-handler.ts +48 -4
- package/src/tools/types.ts +38 -1
- package/src/util/errors.ts +5 -1
- package/src/util/retry.ts +21 -0
- package/src/watcher/providers/slack.ts +33 -3
- package/src/workspace/git-service.ts +6 -4
- package/src/__tests__/get-weather.test.ts +0 -393
- 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 -32
- package/src/config/bundled-skills/weather/icon.svg +0 -24
- package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
- package/src/messaging/triage-engine.ts +0 -344
- package/src/tools/weather/service.ts +0 -712
- /package/src/memory/{ingress-invite-store.ts → invite-store.ts} +0 -0
|
@@ -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
|
-
});
|
|
@@ -1,276 +0,0 @@
|
|
|
1
|
-
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
-
|
|
3
|
-
import { readFileSync } from "fs";
|
|
4
|
-
import { dirname, join } from "path";
|
|
5
|
-
|
|
6
|
-
import { __resetRegistryForTesting, getTool } from "../tools/registry.js";
|
|
7
|
-
import type { ToolContext } from "../tools/types.js";
|
|
8
|
-
import {
|
|
9
|
-
weatherCodeToDescription,
|
|
10
|
-
weatherCodeToSFSymbol,
|
|
11
|
-
} from "../tools/weather/service.js";
|
|
12
|
-
|
|
13
|
-
// ---------------------------------------------------------------------------
|
|
14
|
-
// Regression tests: ensure the skill-loaded path produces the same results
|
|
15
|
-
// as the legacy hardcoded path after the weather tool migration.
|
|
16
|
-
// ---------------------------------------------------------------------------
|
|
17
|
-
|
|
18
|
-
// Clean up after this file to prevent contamination of later test files.
|
|
19
|
-
afterAll(() => {
|
|
20
|
-
__resetRegistryForTesting();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
const CONFIG_DIR = join(
|
|
24
|
-
dirname(import.meta.dirname!),
|
|
25
|
-
"config",
|
|
26
|
-
"bundled-skills",
|
|
27
|
-
"weather",
|
|
28
|
-
);
|
|
29
|
-
|
|
30
|
-
describe("weather skill script wrapper", () => {
|
|
31
|
-
test("exports a run function without registering get_weather in the tool registry", async () => {
|
|
32
|
-
// Before importing the wrapper, verify get_weather is not in the registry.
|
|
33
|
-
expect(getTool("get_weather")).toBeUndefined();
|
|
34
|
-
|
|
35
|
-
// Dynamic import of the skill wrapper — it should NOT trigger any
|
|
36
|
-
// registerTool side-effect (the wrapper delegates to the service module).
|
|
37
|
-
const mod =
|
|
38
|
-
await import("../config/bundled-skills/weather/tools/get-weather.js");
|
|
39
|
-
expect(typeof mod.run).toBe("function");
|
|
40
|
-
|
|
41
|
-
// After importing, the registry should still be clean — no side-effect.
|
|
42
|
-
expect(getTool("get_weather")).toBeUndefined();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("run function delegates to executeGetWeather from the service module", async () => {
|
|
46
|
-
const mod =
|
|
47
|
-
await import("../config/bundled-skills/weather/tools/get-weather.js");
|
|
48
|
-
|
|
49
|
-
// Provide a minimal mock fetch and minimal input to verify delegation.
|
|
50
|
-
const mockFetch = (async (url: string | URL | Request) => {
|
|
51
|
-
const urlStr =
|
|
52
|
-
typeof url === "string"
|
|
53
|
-
? url
|
|
54
|
-
: url instanceof URL
|
|
55
|
-
? url.href
|
|
56
|
-
: (url as Request).url;
|
|
57
|
-
|
|
58
|
-
if (urlStr.includes("geocoding-api.open-meteo.com")) {
|
|
59
|
-
return new Response(
|
|
60
|
-
JSON.stringify({
|
|
61
|
-
results: [
|
|
62
|
-
{
|
|
63
|
-
name: "TestCity",
|
|
64
|
-
latitude: 0,
|
|
65
|
-
longitude: 0,
|
|
66
|
-
country: "Testland",
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
}),
|
|
70
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
if (urlStr.includes("api.open-meteo.com")) {
|
|
75
|
-
return new Response(
|
|
76
|
-
JSON.stringify({
|
|
77
|
-
current: {
|
|
78
|
-
time: "2025-01-15T08:00",
|
|
79
|
-
temperature_2m: 20,
|
|
80
|
-
relative_humidity_2m: 50,
|
|
81
|
-
apparent_temperature: 19,
|
|
82
|
-
weather_code: 0,
|
|
83
|
-
wind_speed_10m: 10,
|
|
84
|
-
wind_direction_10m: 180,
|
|
85
|
-
},
|
|
86
|
-
current_units: {
|
|
87
|
-
temperature_2m: "°C",
|
|
88
|
-
relative_humidity_2m: "%",
|
|
89
|
-
apparent_temperature: "°C",
|
|
90
|
-
wind_speed_10m: "km/h",
|
|
91
|
-
wind_direction_10m: "°",
|
|
92
|
-
},
|
|
93
|
-
hourly: {
|
|
94
|
-
time: [],
|
|
95
|
-
temperature_2m: [],
|
|
96
|
-
weather_code: [],
|
|
97
|
-
is_day: [],
|
|
98
|
-
},
|
|
99
|
-
hourly_units: {
|
|
100
|
-
temperature_2m: "°C",
|
|
101
|
-
weather_code: "wmo code",
|
|
102
|
-
is_day: "",
|
|
103
|
-
},
|
|
104
|
-
daily: {
|
|
105
|
-
time: [],
|
|
106
|
-
weather_code: [],
|
|
107
|
-
temperature_2m_max: [],
|
|
108
|
-
temperature_2m_min: [],
|
|
109
|
-
precipitation_probability_max: [],
|
|
110
|
-
},
|
|
111
|
-
daily_units: {
|
|
112
|
-
temperature_2m_max: "°C",
|
|
113
|
-
temperature_2m_min: "°C",
|
|
114
|
-
precipitation_probability_max: "%",
|
|
115
|
-
},
|
|
116
|
-
}),
|
|
117
|
-
{ status: 200, headers: { "content-type": "application/json" } },
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return new Response("Not found", { status: 404 });
|
|
122
|
-
}) as typeof globalThis.fetch;
|
|
123
|
-
|
|
124
|
-
// Temporarily replace globalThis.fetch so the wrapper picks it up
|
|
125
|
-
const originalFetch = globalThis.fetch;
|
|
126
|
-
globalThis.fetch = mockFetch;
|
|
127
|
-
try {
|
|
128
|
-
const result = await mod.run({ location: "TestCity" }, {
|
|
129
|
-
proxyToolResolver: undefined,
|
|
130
|
-
} as unknown as ToolContext);
|
|
131
|
-
expect(result.isError).toBe(false);
|
|
132
|
-
expect(result.content).toContain("TestCity");
|
|
133
|
-
expect(result.content).toContain("Clear sky");
|
|
134
|
-
} finally {
|
|
135
|
-
globalThis.fetch = originalFetch;
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
describe("weather TOOLS.json manifest", () => {
|
|
141
|
-
const manifest = JSON.parse(
|
|
142
|
-
readFileSync(join(CONFIG_DIR, "TOOLS.json"), "utf-8"),
|
|
143
|
-
);
|
|
144
|
-
|
|
145
|
-
test("has version 1", () => {
|
|
146
|
-
expect(manifest.version).toBe(1);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
test("declares exactly one tool", () => {
|
|
150
|
-
expect(manifest.tools).toHaveLength(1);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
test("tool is named get_weather", () => {
|
|
154
|
-
expect(manifest.tools[0].name).toBe("get_weather");
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test("tool has correct description", () => {
|
|
158
|
-
expect(manifest.tools[0].description).toBe(
|
|
159
|
-
"Get current weather conditions and forecast for a location",
|
|
160
|
-
);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
test("tool executor points to the skill script wrapper", () => {
|
|
164
|
-
expect(manifest.tools[0].executor).toBe("tools/get-weather.ts");
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
test("tool execution_target is host", () => {
|
|
168
|
-
expect(manifest.tools[0].execution_target).toBe("host");
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
test("input schema matches the legacy tool definition", () => {
|
|
172
|
-
const schema = manifest.tools[0].input_schema;
|
|
173
|
-
expect(schema.type).toBe("object");
|
|
174
|
-
expect(schema.required).toEqual(["location"]);
|
|
175
|
-
|
|
176
|
-
// location property
|
|
177
|
-
expect(schema.properties.location).toBeDefined();
|
|
178
|
-
expect(schema.properties.location.type).toBe("string");
|
|
179
|
-
|
|
180
|
-
// units property
|
|
181
|
-
expect(schema.properties.units).toBeDefined();
|
|
182
|
-
expect(schema.properties.units.type).toBe("string");
|
|
183
|
-
expect(schema.properties.units.enum).toEqual(["celsius", "fahrenheit"]);
|
|
184
|
-
|
|
185
|
-
// days property
|
|
186
|
-
expect(schema.properties.days).toBeDefined();
|
|
187
|
-
expect(schema.properties.days.type).toBe("number");
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
describe("weather service module isolation", () => {
|
|
192
|
-
test("executeGetWeather is importable without registerTool side effects", async () => {
|
|
193
|
-
// Importing the service module should NOT call registerTool — only the
|
|
194
|
-
// legacy get-weather.ts module does that.
|
|
195
|
-
const mod = await import("../tools/weather/service.js");
|
|
196
|
-
expect(typeof mod.executeGetWeather).toBe("function");
|
|
197
|
-
expect(typeof mod.weatherCodeToDescription).toBe("function");
|
|
198
|
-
expect(typeof mod.weatherCodeToSFSymbol).toBe("function");
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
test("weatherCodeToDescription returns correct values for all major code families", () => {
|
|
202
|
-
// Clear/cloudy family
|
|
203
|
-
expect(weatherCodeToDescription(0)).toBe("Clear sky");
|
|
204
|
-
expect(weatherCodeToDescription(1)).toBe("Mainly clear");
|
|
205
|
-
expect(weatherCodeToDescription(2)).toBe("Partly cloudy");
|
|
206
|
-
expect(weatherCodeToDescription(3)).toBe("Overcast");
|
|
207
|
-
|
|
208
|
-
// Fog family
|
|
209
|
-
expect(weatherCodeToDescription(45)).toBe("Foggy");
|
|
210
|
-
expect(weatherCodeToDescription(48)).toBe("Depositing rime fog");
|
|
211
|
-
|
|
212
|
-
// Drizzle family
|
|
213
|
-
expect(weatherCodeToDescription(51)).toBe("Light drizzle");
|
|
214
|
-
expect(weatherCodeToDescription(53)).toBe("Moderate drizzle");
|
|
215
|
-
expect(weatherCodeToDescription(55)).toBe("Dense drizzle");
|
|
216
|
-
|
|
217
|
-
// Freezing drizzle
|
|
218
|
-
expect(weatherCodeToDescription(56)).toBe("Light freezing drizzle");
|
|
219
|
-
expect(weatherCodeToDescription(57)).toBe("Dense freezing drizzle");
|
|
220
|
-
|
|
221
|
-
// Rain family
|
|
222
|
-
expect(weatherCodeToDescription(61)).toBe("Slight rain");
|
|
223
|
-
expect(weatherCodeToDescription(63)).toBe("Moderate rain");
|
|
224
|
-
expect(weatherCodeToDescription(65)).toBe("Heavy rain");
|
|
225
|
-
|
|
226
|
-
// Freezing rain
|
|
227
|
-
expect(weatherCodeToDescription(66)).toBe("Light freezing rain");
|
|
228
|
-
expect(weatherCodeToDescription(67)).toBe("Heavy freezing rain");
|
|
229
|
-
|
|
230
|
-
// Snow family
|
|
231
|
-
expect(weatherCodeToDescription(71)).toBe("Slight snowfall");
|
|
232
|
-
expect(weatherCodeToDescription(73)).toBe("Moderate snowfall");
|
|
233
|
-
expect(weatherCodeToDescription(75)).toBe("Heavy snowfall");
|
|
234
|
-
expect(weatherCodeToDescription(77)).toBe("Snow grains");
|
|
235
|
-
|
|
236
|
-
// Shower family
|
|
237
|
-
expect(weatherCodeToDescription(80)).toBe("Slight rain showers");
|
|
238
|
-
expect(weatherCodeToDescription(81)).toBe("Moderate rain showers");
|
|
239
|
-
expect(weatherCodeToDescription(82)).toBe("Violent rain showers");
|
|
240
|
-
expect(weatherCodeToDescription(85)).toBe("Slight snow showers");
|
|
241
|
-
expect(weatherCodeToDescription(86)).toBe("Heavy snow showers");
|
|
242
|
-
|
|
243
|
-
// Thunderstorm family
|
|
244
|
-
expect(weatherCodeToDescription(95)).toBe("Thunderstorm");
|
|
245
|
-
expect(weatherCodeToDescription(96)).toBe("Thunderstorm with slight hail");
|
|
246
|
-
expect(weatherCodeToDescription(99)).toBe("Thunderstorm with heavy hail");
|
|
247
|
-
|
|
248
|
-
// Unknown codes
|
|
249
|
-
expect(weatherCodeToDescription(-1)).toBe("Unknown");
|
|
250
|
-
expect(weatherCodeToDescription(42)).toBe("Unknown");
|
|
251
|
-
expect(weatherCodeToDescription(100)).toBe("Unknown");
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
test("weatherCodeToSFSymbol returns correct icons and respects isDay", () => {
|
|
255
|
-
// Day/night variants for clear sky
|
|
256
|
-
expect(weatherCodeToSFSymbol(0, true)).toBe("sun.max.fill");
|
|
257
|
-
expect(weatherCodeToSFSymbol(0, false)).toBe("moon.fill");
|
|
258
|
-
|
|
259
|
-
// Day/night variants for partly cloudy
|
|
260
|
-
expect(weatherCodeToSFSymbol(2, true)).toBe("cloud.sun.fill");
|
|
261
|
-
expect(weatherCodeToSFSymbol(2, false)).toBe("cloud.moon.fill");
|
|
262
|
-
|
|
263
|
-
// Overcast has no day/night variant
|
|
264
|
-
expect(weatherCodeToSFSymbol(3, true)).toBe("cloud.fill");
|
|
265
|
-
expect(weatherCodeToSFSymbol(3, false)).toBe("cloud.fill");
|
|
266
|
-
|
|
267
|
-
// Snow
|
|
268
|
-
expect(weatherCodeToSFSymbol(75, true)).toBe("snowflake");
|
|
269
|
-
|
|
270
|
-
// Thunderstorm
|
|
271
|
-
expect(weatherCodeToSFSymbol(95, true)).toBe("cloud.bolt.fill");
|
|
272
|
-
|
|
273
|
-
// Default isDay=true when omitted
|
|
274
|
-
expect(weatherCodeToSFSymbol(0)).toBe("sun.max.fill");
|
|
275
|
-
});
|
|
276
|
-
});
|