@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.
Files changed (237) hide show
  1. package/ARCHITECTURE.md +39 -37
  2. package/Dockerfile +14 -8
  3. package/README.md +7 -8
  4. package/docs/architecture/memory.md +28 -29
  5. package/docs/runbook-trusted-contacts.md +76 -43
  6. package/package.json +1 -1
  7. package/scripts/ipc/check-swift-decoder-drift.ts +2 -3
  8. package/scripts/test.sh +1 -1
  9. package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +4 -37
  10. package/src/__tests__/actor-token-service.test.ts +4 -3
  11. package/src/__tests__/app-executors.test.ts +7 -17
  12. package/src/__tests__/assistant-feature-flags-integration.test.ts +18 -10
  13. package/src/__tests__/browser-skill-endstate.test.ts +10 -1
  14. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -0
  15. package/src/__tests__/channel-approval-routes.test.ts +44 -44
  16. package/src/__tests__/channel-approval.test.ts +8 -0
  17. package/src/__tests__/channel-approvals.test.ts +39 -1
  18. package/src/__tests__/channel-guardian.test.ts +15 -5
  19. package/src/__tests__/channel-reply-delivery.test.ts +31 -0
  20. package/src/__tests__/config-schema.test.ts +0 -9
  21. package/src/__tests__/conflict-policy.test.ts +76 -0
  22. package/src/__tests__/conflict-store.test.ts +14 -20
  23. package/src/__tests__/contacts-tools.test.ts +8 -61
  24. package/src/__tests__/contradiction-checker.test.ts +5 -1
  25. package/src/__tests__/dynamic-skill-workflow-prompt.test.ts +9 -0
  26. package/src/__tests__/gateway-only-guard.test.ts +1 -0
  27. package/src/__tests__/gemini-image-service.test.ts +2 -2
  28. package/src/__tests__/guardian-decision-primitive-canonical.test.ts +5 -3
  29. package/src/__tests__/guardian-grant-minting.test.ts +6 -6
  30. package/src/__tests__/guardian-routing-invariants.test.ts +40 -15
  31. package/src/__tests__/guardian-verify-setup-skill-regression.test.ts +4 -6
  32. package/src/__tests__/inbound-invite-redemption.test.ts +1 -1
  33. package/src/__tests__/integrations-cli.test.ts +3 -27
  34. package/src/__tests__/intent-routing.test.ts +3 -0
  35. package/src/__tests__/invite-redemption-service.test.ts +1 -1
  36. package/src/__tests__/{ingress-routes-http.test.ts → invite-routes-http.test.ts} +40 -320
  37. package/src/__tests__/ipc-snapshot.test.ts +4 -31
  38. package/src/__tests__/memory-lifecycle-e2e.test.ts +11 -10
  39. package/src/__tests__/nl-approval-parser.test.ts +305 -0
  40. package/src/__tests__/oauth-provider-profiles.test.ts +34 -0
  41. package/src/__tests__/provider-error-scenarios.test.ts +68 -0
  42. package/src/__tests__/registry.test.ts +0 -10
  43. package/src/__tests__/relay-server.test.ts +1 -1
  44. package/src/__tests__/retry-after-extraction.test.ts +111 -0
  45. package/src/__tests__/script-proxy-profile-template-fallback.test.ts +127 -0
  46. package/src/__tests__/script-proxy-session-runtime.test.ts +6 -1
  47. package/src/__tests__/session-agent-loop.test.ts +0 -2
  48. package/src/__tests__/session-conflict-gate.test.ts +243 -388
  49. package/src/__tests__/session-media-retry.test.ts +147 -0
  50. package/src/__tests__/session-profile-injection.test.ts +0 -2
  51. package/src/__tests__/session-runtime-assembly.test.ts +2 -3
  52. package/src/__tests__/session-skill-tools.test.ts +0 -49
  53. package/src/__tests__/session-workspace-cache-state.test.ts +0 -1
  54. package/src/__tests__/session-workspace-injection.test.ts +0 -1
  55. package/src/__tests__/session-workspace-tool-tracking.test.ts +0 -1
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +9 -5
  57. package/src/__tests__/skill-feature-flags.test.ts +18 -12
  58. package/src/__tests__/skill-load-feature-flag.test.ts +4 -3
  59. package/src/__tests__/slack-block-formatting.test.ts +100 -0
  60. package/src/__tests__/slack-inbound-verification.test.ts +346 -0
  61. package/src/__tests__/slack-reaction-approvals.test.ts +77 -0
  62. package/src/__tests__/slack-skill.test.ts +3 -2
  63. package/src/__tests__/starter-task-flow.test.ts +0 -1
  64. package/src/__tests__/tool-grant-request-escalation.test.ts +2 -1
  65. package/src/__tests__/trusted-contact-inline-approval-integration.test.ts +2 -1
  66. package/src/__tests__/trusted-contact-verification.test.ts +3 -1
  67. package/src/__tests__/voice-invite-redemption.test.ts +1 -1
  68. package/src/amazon/client.ts +7 -24
  69. package/src/approvals/guardian-decision-primitive.ts +11 -7
  70. package/src/approvals/guardian-request-resolvers.ts +5 -3
  71. package/src/calls/relay-server.ts +44 -11
  72. package/src/channels/config.ts +1 -1
  73. package/src/cli/integrations.ts +10 -66
  74. package/src/config/bundled-skills/app-builder/SKILL.md +193 -1500
  75. package/src/config/bundled-skills/app-builder/TOOLS.json +70 -18
  76. package/src/config/bundled-skills/browser/TOOLS.json +59 -2
  77. package/src/config/bundled-skills/chatgpt-import/TOOLS.json +4 -0
  78. package/src/config/bundled-skills/computer-use/TOOLS.json +50 -2
  79. package/src/config/bundled-skills/contacts/SKILL.md +49 -53
  80. package/src/config/bundled-skills/contacts/TOOLS.json +26 -22
  81. package/src/config/bundled-skills/contacts/tools/contact-merge.ts +40 -62
  82. package/src/config/bundled-skills/contacts/tools/contact-search.ts +17 -43
  83. package/src/config/bundled-skills/contacts/tools/contact-upsert.ts +18 -57
  84. package/src/config/bundled-skills/document/TOOLS.json +8 -0
  85. package/src/config/bundled-skills/email-setup/SKILL.md +10 -7
  86. package/src/config/bundled-skills/followups/TOOLS.json +12 -0
  87. package/src/config/bundled-skills/google-calendar/TOOLS.json +124 -26
  88. package/src/config/bundled-skills/guardian-verify-setup/SKILL.md +54 -21
  89. package/src/config/bundled-skills/image-studio/TOOLS.json +12 -2
  90. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +14 -8
  91. package/src/config/bundled-skills/knowledge-graph/TOOLS.json +13 -3
  92. package/src/config/bundled-skills/media-processing/SKILL.md +1 -1
  93. package/src/config/bundled-skills/media-processing/TOOLS.json +28 -0
  94. package/src/config/bundled-skills/media-processing/tools/generate-clip.ts +26 -6
  95. package/src/config/bundled-skills/messaging/TOOLS.json +228 -182
  96. package/src/config/bundled-skills/notifications/SKILL.md +3 -2
  97. package/src/config/bundled-skills/notifications/TOOLS.json +7 -13
  98. package/src/config/bundled-skills/phone-calls/TOOLS.json +13 -1
  99. package/src/config/bundled-skills/playbooks/TOOLS.json +16 -0
  100. package/src/config/bundled-skills/reminder/TOOLS.json +15 -2
  101. package/src/config/bundled-skills/schedule/SKILL.md +33 -15
  102. package/src/config/bundled-skills/schedule/TOOLS.json +17 -1
  103. package/src/config/bundled-skills/slack/SKILL.md +30 -1
  104. package/src/config/bundled-skills/slack/TOOLS.json +89 -2
  105. package/src/config/bundled-skills/slack/tools/slack-channel-permissions.ts +146 -0
  106. package/src/config/bundled-skills/slack/tools/slack-scan-digest.ts +120 -0
  107. package/src/config/bundled-skills/slack-app-setup/SKILL.md +200 -0
  108. package/src/config/bundled-skills/subagent/TOOLS.json +22 -2
  109. package/src/config/bundled-skills/tasks/TOOLS.json +86 -14
  110. package/src/config/bundled-skills/transcribe/TOOLS.json +4 -0
  111. package/src/config/bundled-skills/watcher/TOOLS.json +20 -0
  112. package/src/config/bundled-tool-registry.ts +2 -5
  113. package/src/config/channel-permission-profiles.ts +155 -0
  114. package/src/config/env.ts +4 -1
  115. package/src/config/memory-schema.ts +0 -10
  116. package/src/config/system-prompt.ts +6 -0
  117. package/src/contacts/contact-store.ts +221 -56
  118. package/src/contacts/contacts-write.ts +14 -3
  119. package/src/contacts/types.ts +35 -4
  120. package/src/daemon/assistant-attachments.ts +23 -3
  121. package/src/daemon/guardian-verification-intent.ts +7 -4
  122. package/src/daemon/handlers/apps.ts +1 -2
  123. package/src/daemon/handlers/config-heartbeat.ts +1 -2
  124. package/src/daemon/handlers/config-inbox.ts +16 -134
  125. package/src/daemon/handlers/contacts.ts +2 -2
  126. package/src/daemon/handlers/guardian-actions.ts +21 -88
  127. package/src/daemon/handlers/sessions.ts +2 -2
  128. package/src/daemon/ipc-contract/apps.ts +0 -1
  129. package/src/daemon/ipc-contract/contacts.ts +2 -2
  130. package/src/daemon/ipc-contract/inbox.ts +7 -66
  131. package/src/daemon/ipc-contract/sessions.ts +1 -0
  132. package/src/daemon/ipc-contract/surfaces.ts +0 -1
  133. package/src/daemon/ipc-contract-inventory.json +2 -4
  134. package/src/daemon/lifecycle.ts +14 -2
  135. package/src/daemon/session-agent-loop-handlers.ts +9 -0
  136. package/src/daemon/session-agent-loop.ts +2 -45
  137. package/src/daemon/session-attachments.ts +5 -1
  138. package/src/daemon/session-conflict-gate.ts +21 -82
  139. package/src/daemon/session-error.ts +18 -0
  140. package/src/daemon/session-lifecycle.ts +4 -5
  141. package/src/daemon/session-media-retry.ts +15 -1
  142. package/src/daemon/session-memory.ts +7 -52
  143. package/src/daemon/session-process.ts +3 -1
  144. package/src/daemon/session-runtime-assembly.ts +18 -35
  145. package/src/daemon/session-surfaces.ts +0 -1
  146. package/src/daemon/session-tool-setup.ts +7 -4
  147. package/src/events/domain-events.ts +2 -1
  148. package/src/heartbeat/heartbeat-service.ts +5 -1
  149. package/src/home-base/prebuilt/seed.ts +0 -1
  150. package/src/influencer/client.ts +7 -24
  151. package/src/media/gemini-image-service.ts +48 -3
  152. package/src/memory/app-store.ts +0 -4
  153. package/src/memory/conflict-intent.ts +3 -6
  154. package/src/memory/conflict-policy.ts +34 -0
  155. package/src/memory/conflict-store.ts +10 -18
  156. package/src/memory/contradiction-checker.ts +2 -2
  157. package/src/memory/conversation-attention-store.ts +3 -1
  158. package/src/memory/db-init.ts +8 -0
  159. package/src/memory/job-handlers/conflict.ts +0 -7
  160. package/src/memory/migrations/133-assistant-contact-metadata.ts +21 -0
  161. package/src/memory/migrations/134-contacts-notes-column.ts +51 -0
  162. package/src/memory/migrations/135-backfill-contact-interaction-stats.ts +31 -0
  163. package/src/memory/migrations/index.ts +3 -0
  164. package/src/memory/schema.ts +12 -17
  165. package/src/memory/slack-thread-store.ts +187 -0
  166. package/src/messaging/index.ts +0 -1
  167. package/src/messaging/providers/slack/client.ts +84 -26
  168. package/src/messaging/providers/slack/types.ts +4 -0
  169. package/src/messaging/types.ts +0 -38
  170. package/src/notifications/adapters/slack.ts +90 -0
  171. package/src/notifications/destination-resolver.ts +42 -1
  172. package/src/notifications/emit-signal.ts +17 -1
  173. package/src/oauth/provider-profiles.ts +22 -0
  174. package/src/providers/anthropic/client.ts +3 -0
  175. package/src/providers/openai/client.ts +3 -0
  176. package/src/providers/retry.ts +9 -1
  177. package/src/runtime/actor-trust-resolver.ts +8 -0
  178. package/src/runtime/auth/require-bound-guardian.ts +44 -0
  179. package/src/runtime/auth/route-policy.ts +4 -8
  180. package/src/runtime/channel-approval-types.ts +18 -0
  181. package/src/runtime/channel-approvals.ts +8 -0
  182. package/src/runtime/channel-invite-transport.ts +1 -1
  183. package/src/runtime/channel-reply-delivery.ts +62 -3
  184. package/src/runtime/gateway-client.ts +36 -2
  185. package/src/runtime/gateway-internal-client.ts +86 -0
  186. package/src/runtime/guardian-action-service.ts +128 -0
  187. package/src/runtime/guardian-outbound-actions.ts +3 -3
  188. package/src/runtime/guardian-reply-router.ts +4 -4
  189. package/src/runtime/guardian-verification-templates.ts +16 -1
  190. package/src/runtime/http-server.ts +29 -46
  191. package/src/runtime/invite-redemption-service.ts +1 -1
  192. package/src/runtime/{ingress-service.ts → invite-service.ts} +5 -157
  193. package/src/runtime/nl-approval-parser.ts +138 -0
  194. package/src/runtime/routes/approval-routes.ts +1 -40
  195. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +6 -3
  196. package/src/runtime/routes/channel-route-shared.ts +35 -1
  197. package/src/runtime/routes/contact-routes.ts +494 -47
  198. package/src/runtime/routes/conversation-routes.ts +2 -1
  199. package/src/runtime/routes/global-search-routes.ts +2 -2
  200. package/src/runtime/routes/guardian-action-routes.ts +19 -111
  201. package/src/runtime/routes/guardian-approval-interception.ts +78 -1
  202. package/src/runtime/routes/guardian-bootstrap-routes.ts +6 -1
  203. package/src/runtime/routes/inbound-message-handler.ts +40 -12
  204. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +227 -1
  205. package/src/runtime/routes/inbound-stages/background-dispatch.ts +108 -0
  206. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +2 -1
  207. package/src/runtime/routes/{ingress-routes.ts → invite-routes.ts} +10 -110
  208. package/src/runtime/routes/migration-routes.ts +17 -17
  209. package/src/runtime/slack-block-formatting.ts +176 -0
  210. package/src/schedule/scheduler.ts +11 -2
  211. package/src/tools/apps/executors.ts +16 -15
  212. package/src/tools/calls/call-end.ts +1 -1
  213. package/src/tools/computer-use/definitions.ts +16 -0
  214. package/src/tools/credentials/vault.ts +86 -2
  215. package/src/tools/network/script-proxy/session-manager.ts +28 -3
  216. package/src/tools/permission-checker.ts +18 -0
  217. package/src/tools/terminal/shell.ts +15 -5
  218. package/src/tools/tool-approval-handler.ts +48 -4
  219. package/src/tools/types.ts +38 -1
  220. package/src/util/errors.ts +5 -1
  221. package/src/util/retry.ts +21 -0
  222. package/src/watcher/providers/slack.ts +33 -3
  223. package/src/workspace/git-service.ts +6 -4
  224. package/src/__tests__/get-weather.test.ts +0 -393
  225. package/src/__tests__/weather-skill-regression.test.ts +0 -276
  226. package/src/autonomy/autonomy-resolver.ts +0 -62
  227. package/src/autonomy/autonomy-store.ts +0 -138
  228. package/src/autonomy/disposition-mapper.ts +0 -31
  229. package/src/autonomy/index.ts +0 -11
  230. package/src/autonomy/types.ts +0 -43
  231. package/src/config/bundled-skills/weather/SKILL.md +0 -38
  232. package/src/config/bundled-skills/weather/TOOLS.json +0 -32
  233. package/src/config/bundled-skills/weather/icon.svg +0 -24
  234. package/src/config/bundled-skills/weather/tools/get-weather.ts +0 -12
  235. package/src/messaging/triage-engine.ts +0 -344
  236. package/src/tools/weather/service.ts +0 -712
  237. /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
- });