@notionhq/workers 0.5.0 → 0.7.0

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 (182) hide show
  1. package/dist/alpha/ajv-formats.d.js +0 -0
  2. package/dist/alpha/builder.d.ts +149 -0
  3. package/dist/alpha/builder.d.ts.map +1 -0
  4. package/dist/alpha/builder.js +260 -0
  5. package/dist/alpha/builder.test.d.ts +2 -0
  6. package/dist/alpha/builder.test.d.ts.map +1 -0
  7. package/dist/alpha/cli/build.d.ts +13 -0
  8. package/dist/alpha/cli/build.d.ts.map +1 -0
  9. package/dist/alpha/cli/build.js +36 -0
  10. package/dist/alpha/cli/build.test.d.ts +2 -0
  11. package/dist/alpha/cli/build.test.d.ts.map +1 -0
  12. package/dist/alpha/cli/codegen.d.ts +10 -0
  13. package/dist/alpha/cli/codegen.d.ts.map +1 -0
  14. package/dist/alpha/cli/codegen.js +41 -0
  15. package/dist/alpha/cli/codegen.test.d.ts +2 -0
  16. package/dist/alpha/cli/codegen.test.d.ts.map +1 -0
  17. package/dist/alpha/cli/discover.d.ts +35 -0
  18. package/dist/alpha/cli/discover.d.ts.map +1 -0
  19. package/dist/alpha/cli/discover.js +39 -0
  20. package/dist/alpha/cli/discover.test.d.ts +2 -0
  21. package/dist/alpha/cli/discover.test.d.ts.map +1 -0
  22. package/dist/alpha/cli/emit-manifest.d.ts +33 -0
  23. package/dist/alpha/cli/emit-manifest.d.ts.map +1 -0
  24. package/dist/alpha/cli/emit-manifest.js +266 -0
  25. package/dist/alpha/cli/emit-manifest.test.d.ts +2 -0
  26. package/dist/alpha/cli/emit-manifest.test.d.ts.map +1 -0
  27. package/dist/alpha/cli/index.d.ts +3 -0
  28. package/dist/alpha/cli/index.d.ts.map +1 -0
  29. package/dist/alpha/cli/index.js +36 -0
  30. package/dist/alpha/context.d.ts +8 -0
  31. package/dist/alpha/context.d.ts.map +1 -0
  32. package/dist/alpha/context.js +33 -0
  33. package/dist/alpha/database.d.ts +90 -0
  34. package/dist/alpha/database.d.ts.map +1 -0
  35. package/dist/alpha/database.js +9 -0
  36. package/dist/alpha/error.d.ts +41 -0
  37. package/dist/alpha/error.d.ts.map +1 -0
  38. package/dist/alpha/error.js +25 -0
  39. package/dist/alpha/icon-names.d.ts +6 -0
  40. package/dist/alpha/icon-names.d.ts.map +1 -0
  41. package/dist/alpha/icon-names.js +0 -0
  42. package/dist/alpha/json-schema.d.ts +117 -0
  43. package/dist/alpha/json-schema.d.ts.map +1 -0
  44. package/dist/alpha/json-schema.js +0 -0
  45. package/dist/alpha/manifest.d.ts +44 -0
  46. package/dist/alpha/manifest.d.ts.map +1 -0
  47. package/dist/alpha/manifest.js +0 -0
  48. package/dist/alpha/output.d.ts +7 -0
  49. package/dist/alpha/output.d.ts.map +1 -0
  50. package/dist/alpha/output.js +8 -0
  51. package/dist/alpha/pacer.d.ts +74 -0
  52. package/dist/alpha/pacer.d.ts.map +1 -0
  53. package/dist/alpha/pacer.js +66 -0
  54. package/dist/alpha/pacer.test.d.ts +2 -0
  55. package/dist/alpha/pacer.test.d.ts.map +1 -0
  56. package/dist/alpha/schedule.d.ts +6 -0
  57. package/dist/alpha/schedule.d.ts.map +1 -0
  58. package/dist/alpha/schedule.js +43 -0
  59. package/dist/alpha/schedule.test.d.ts +2 -0
  60. package/dist/alpha/schedule.test.d.ts.map +1 -0
  61. package/dist/alpha/schema-builder.d.ts +79 -0
  62. package/dist/alpha/schema-builder.d.ts.map +1 -0
  63. package/dist/alpha/schema-builder.js +135 -0
  64. package/dist/alpha/schema.d.ts +185 -0
  65. package/dist/alpha/schema.d.ts.map +1 -0
  66. package/dist/alpha/schema.js +115 -0
  67. package/dist/alpha/sync.d.ts +233 -0
  68. package/dist/alpha/sync.d.ts.map +1 -0
  69. package/dist/alpha/sync.js +70 -0
  70. package/dist/alpha/sync.test.d.ts +2 -0
  71. package/dist/alpha/sync.test.d.ts.map +1 -0
  72. package/dist/alpha/tool.d.ts +132 -0
  73. package/dist/alpha/tool.d.ts.map +1 -0
  74. package/dist/alpha/tool.js +156 -0
  75. package/dist/alpha/tool.test.d.ts +2 -0
  76. package/dist/alpha/tool.test.d.ts.map +1 -0
  77. package/dist/alpha/triggers.d.ts +365 -0
  78. package/dist/alpha/triggers.d.ts.map +1 -0
  79. package/dist/alpha/triggers.generated.d.ts +246 -0
  80. package/dist/alpha/triggers.generated.d.ts.map +1 -0
  81. package/dist/alpha/triggers.generated.js +239 -0
  82. package/dist/alpha/triggers.js +0 -0
  83. package/dist/alpha/types.d.ts +228 -0
  84. package/dist/alpha/types.d.ts.map +1 -0
  85. package/dist/alpha/types.js +0 -0
  86. package/dist/alpha/workflow.d.ts +70 -0
  87. package/dist/alpha/workflow.d.ts.map +1 -0
  88. package/dist/alpha/workflow.js +39 -0
  89. package/dist/alpha/workflow.test.d.ts +2 -0
  90. package/dist/alpha/workflow.test.d.ts.map +1 -0
  91. package/dist/block.d.ts.map +1 -1
  92. package/dist/builder.d.ts.map +1 -1
  93. package/dist/builder.js +2 -6
  94. package/dist/capabilities/ai_connector.d.ts.map +1 -1
  95. package/dist/capabilities/automation.d.ts.map +1 -1
  96. package/dist/capabilities/context.d.ts.map +1 -1
  97. package/dist/capabilities/oauth.d.ts.map +1 -1
  98. package/dist/capabilities/output.d.ts.map +1 -1
  99. package/dist/capabilities/output.js +2 -4
  100. package/dist/capabilities/stateful-capability.d.ts.map +1 -1
  101. package/dist/capabilities/stateful-capability.js +2 -6
  102. package/dist/capabilities/sync.d.ts.map +1 -1
  103. package/dist/capabilities/sync.js +2 -8
  104. package/dist/capabilities/tool.d.ts.map +1 -1
  105. package/dist/capabilities/tool.js +6 -20
  106. package/dist/capabilities/webhook.d.ts.map +1 -1
  107. package/dist/capabilities/workflow.d.ts +2 -2
  108. package/dist/capabilities/workflow.d.ts.map +1 -1
  109. package/dist/index.d.ts +2 -2
  110. package/dist/index.d.ts.map +1 -1
  111. package/dist/index.js +1 -7
  112. package/dist/json-schema.d.ts.map +1 -1
  113. package/dist/schema-builder.d.ts.map +1 -1
  114. package/dist/schema.d.ts.map +1 -1
  115. package/dist/types.d.ts.map +1 -1
  116. package/dist/worker.d.ts.map +1 -1
  117. package/dist/worker.js +1 -4
  118. package/package.json +74 -14
  119. package/src/alpha/ajv-formats.d.ts +7 -0
  120. package/src/alpha/builder.test.ts +197 -0
  121. package/src/alpha/builder.ts +338 -0
  122. package/src/alpha/cli/build.test.ts +240 -0
  123. package/src/alpha/cli/build.ts +57 -0
  124. package/src/alpha/cli/codegen.test.ts +31 -0
  125. package/src/alpha/cli/codegen.ts +51 -0
  126. package/src/alpha/cli/discover.test.ts +43 -0
  127. package/src/alpha/cli/discover.ts +81 -0
  128. package/src/alpha/cli/emit-manifest.test.ts +45 -0
  129. package/src/alpha/cli/emit-manifest.ts +409 -0
  130. package/src/alpha/cli/index.ts +41 -0
  131. package/src/alpha/context.ts +61 -0
  132. package/src/alpha/database.ts +102 -0
  133. package/src/alpha/error.ts +49 -0
  134. package/src/alpha/icon-names.ts +890 -0
  135. package/src/alpha/json-schema.ts +180 -0
  136. package/src/alpha/manifest.ts +47 -0
  137. package/src/alpha/output.ts +8 -0
  138. package/src/alpha/pacer.test.ts +81 -0
  139. package/src/alpha/pacer.ts +181 -0
  140. package/src/alpha/schedule.test.ts +31 -0
  141. package/src/alpha/schedule.ts +53 -0
  142. package/src/alpha/schema-builder.ts +193 -0
  143. package/src/alpha/schema.ts +222 -0
  144. package/src/alpha/sync.test.ts +191 -0
  145. package/src/alpha/sync.ts +340 -0
  146. package/src/alpha/tool.test.ts +116 -0
  147. package/src/alpha/tool.ts +319 -0
  148. package/src/alpha/triggers.generated.ts +489 -0
  149. package/src/alpha/triggers.ts +500 -0
  150. package/src/alpha/types.ts +284 -0
  151. package/src/alpha/workflow.test.ts +84 -0
  152. package/src/alpha/workflow.ts +131 -0
  153. package/src/block.ts +1 -5
  154. package/src/builder.test.ts +28 -60
  155. package/src/builder.ts +3 -10
  156. package/src/capabilities/ai_connector.test.ts +9 -32
  157. package/src/capabilities/ai_connector.ts +5 -17
  158. package/src/capabilities/automation.test.ts +3 -12
  159. package/src/capabilities/automation.ts +3 -11
  160. package/src/capabilities/context.ts +3 -6
  161. package/src/capabilities/oauth.test.ts +1 -0
  162. package/src/capabilities/oauth.ts +1 -3
  163. package/src/capabilities/output.ts +1 -3
  164. package/src/capabilities/stateful-capability.test.ts +1 -0
  165. package/src/capabilities/stateful-capability.ts +2 -6
  166. package/src/capabilities/sync.test.ts +15 -45
  167. package/src/capabilities/sync.ts +10 -40
  168. package/src/capabilities/tool.test.ts +106 -146
  169. package/src/capabilities/tool.ts +17 -39
  170. package/src/capabilities/webhook.ts +3 -10
  171. package/src/capabilities/workflow.test.ts +10 -31
  172. package/src/capabilities/workflow.ts +5 -12
  173. package/src/index.ts +2 -12
  174. package/src/json-schema.test.ts +2 -4
  175. package/src/json-schema.ts +7 -6
  176. package/src/pacer.test.ts +1 -3
  177. package/src/pacer_internal.ts +1 -5
  178. package/src/schema-builder.test.ts +10 -35
  179. package/src/schema-builder.ts +5 -16
  180. package/src/schema.ts +2 -10
  181. package/src/types.ts +1 -7
  182. package/src/worker.ts +8 -34
@@ -0,0 +1,340 @@
1
+ import type { CapabilityContext } from "./context.js";
2
+ import { createCapabilityContext } from "./context.js";
3
+ import type { Database } from "./database.js";
4
+ import { ExecutionError, RateLimitError } from "./error.js";
5
+ import { writeOutput } from "./output.js";
6
+ import { initPacerState, pacerStates, type PacerEntry } from "./pacer.js";
7
+ import { parseSchedule } from "./schedule.js";
8
+ import type { PropertyConfiguration, PropertySchema, Schema } from "./schema.js";
9
+ import type {
10
+ Cover,
11
+ HandlerOptions,
12
+ Icon,
13
+ NormalizedSchedule,
14
+ PeopleValue,
15
+ PlaceValue,
16
+ RelationValue,
17
+ Schedule,
18
+ TextValue,
19
+ } from "./types.js";
20
+
21
+ /**
22
+ * Maps a property configuration to its corresponding value type.
23
+ */
24
+ type PropertyValueType<T extends PropertyConfiguration> = T extends {
25
+ type: "people";
26
+ }
27
+ ? PeopleValue
28
+ : T extends { type: "place" }
29
+ ? PlaceValue
30
+ : T extends { type: "relation" }
31
+ ? RelationValue
32
+ : TextValue;
33
+
34
+ /**
35
+ * Sync mode determines how the sync handles data lifecycle.
36
+ */
37
+ export type SyncMode = "replace" | "incremental";
38
+
39
+ /**
40
+ * A change representing a record to be created or updated.
41
+ */
42
+ export type SyncChangeUpsert<S extends PropertySchema = PropertySchema> = {
43
+ /** The type of change. Use `"upsert"` to create or update a record. */
44
+ type: "upsert";
45
+ /** A unique identifier for this record, used to match against existing pages. */
46
+ key: string;
47
+ /**
48
+ * The property values for this record.
49
+ * Keys must match the property names defined in the schema.
50
+ * Use the Builder helpers (e.g. `Builder.title()`, `Builder.richText()`)
51
+ * to create values.
52
+ */
53
+ properties: { [Property in keyof S]: PropertyValueType<S[Property]> };
54
+ /**
55
+ * When this record was last updated in the upstream system.
56
+ * ISO 8601 string (e.g. `"2026-03-19T12:00:00Z"`).
57
+ * Used for conflict resolution when multiple syncs write to the same
58
+ * database. Recommended, but optional.
59
+ */
60
+ upstreamUpdatedAt?: string;
61
+ /**
62
+ * Optional icon to use as the icon for this row's page.
63
+ * Use the `Builder.emojiIcon()`, `Builder.notionIcon()`, or
64
+ * `Builder.imageIcon()` helpers.
65
+ */
66
+ icon?: Icon;
67
+ /**
68
+ * Optional cover to use as the cover for this row's page.
69
+ * Use the `Builder.imageCover()` helper.
70
+ */
71
+ cover?: Cover;
72
+ /**
73
+ * Optional markdown content to add to the page body.
74
+ * This will be converted to Notion blocks and added as page content.
75
+ */
76
+ pageContentMarkdown?: string;
77
+ };
78
+
79
+ /**
80
+ * A change representing a record to be deleted.
81
+ * Only applicable when using `mode: "incremental"`.
82
+ */
83
+ export type SyncChangeDelete = {
84
+ /** The type of change. Use `"delete"` to remove a record. */
85
+ type: "delete";
86
+ /** The unique identifier of the record to delete. */
87
+ key: string;
88
+ };
89
+
90
+ /**
91
+ * A change to be applied to the synced database.
92
+ */
93
+ export type SyncChange<S extends PropertySchema = PropertySchema> =
94
+ | SyncChangeUpsert<S>
95
+ | SyncChangeDelete;
96
+
97
+ /**
98
+ * Result returned from a sync handler.
99
+ */
100
+ export type SyncHandlerResult<S extends PropertySchema = PropertySchema, State = unknown> = {
101
+ /** The batch of changes to apply in this execution. */
102
+ changes: SyncChange<S>[];
103
+ /**
104
+ * Indicates whether there is more data to fetch. When `true`, the
105
+ * handler is called again with `nextState`.
106
+ *
107
+ * @default false
108
+ */
109
+ hasMore?: boolean;
110
+ /**
111
+ * Optional state data to pass to the next execution.
112
+ * Required if `hasMore` is `true`, ignored otherwise.
113
+ * This can be any type of data (cursor, page number, timestamp, etc.);
114
+ * the same data is provided as `state` in the next execution.
115
+ */
116
+ nextState?: State;
117
+ };
118
+
119
+ /**
120
+ * Configuration passed to {@link createSync}.
121
+ */
122
+ export type SyncConfiguration<S extends PropertySchema = PropertySchema, State = unknown> = {
123
+ /**
124
+ * The database to sync data into, declared with `createDatabase(...)`.
125
+ * Declare it inline for a database private to this sync, or import a
126
+ * shared instance that other syncs (and `Schema.relation(...)`) also
127
+ * reference.
128
+ */
129
+ database: Database<S>;
130
+
131
+ /**
132
+ * How the sync handles data lifecycle:
133
+ * - "replace": Each run returns the complete dataset; unseen pages are deleted.
134
+ * - "incremental": Each run returns changes only; use delete markers to remove pages.
135
+ *
136
+ * @default "replace"
137
+ */
138
+ mode?: SyncMode;
139
+
140
+ /**
141
+ * How often the sync should run.
142
+ * - "continuous": Run as frequently as the system allows
143
+ * - "manual": Only run when explicitly triggered
144
+ * - Interval string: Run at specified intervals, e.g. "1h", "30m", "1d"
145
+ *
146
+ * @default "30m"
147
+ */
148
+ schedule?: Schedule;
149
+
150
+ /**
151
+ * Fetches the data to sync from the third-party service.
152
+ *
153
+ * This function can return all data at once, or implement pagination by
154
+ * returning a batch of changes with `hasMore: true` and a `nextState`;
155
+ * the runtime calls it again with that state until `hasMore` is `false`.
156
+ *
157
+ * @param state - User-defined state from the previous execution (undefined on first call)
158
+ * @param context - Runtime context, including the Notion client
159
+ */
160
+ handler: (
161
+ state: State | undefined,
162
+ context: CapabilityContext,
163
+ ) => Promise<SyncHandlerResult<S, State>>;
164
+ };
165
+
166
+ /**
167
+ * The pre-manifest configuration of a sync: everything from
168
+ * {@link SyncConfiguration} except the handler, with the schedule
169
+ * normalized for the backend and the database's schema type erased.
170
+ *
171
+ * The `database` here is still an object reference; the build resolves it
172
+ * to the database's *name* when emitting the manifest.
173
+ */
174
+ export type SyncManifestConfig = {
175
+ database: {
176
+ _tag: "database";
177
+ name: string;
178
+ config: { type: "managed"; initialTitle?: string; primaryKey: string; schema: Schema };
179
+ };
180
+ mode: SyncMode | undefined;
181
+ schedule: NormalizedSchedule | undefined;
182
+ };
183
+
184
+ /**
185
+ * The runtime context the platform passes when invoking a sync.
186
+ */
187
+ export type SyncRuntimeContext = {
188
+ /** The user-defined state (cursor, pagination state, etc.). */
189
+ state?: unknown;
190
+ /** Legacy field for user-defined state. */
191
+ userContext?: unknown;
192
+ /** Pacer state from the server for rate limiting. */
193
+ pacers?: Record<string, PacerEntry>;
194
+ };
195
+
196
+ /**
197
+ * The result a sync run reports to the platform, via the notion_output
198
+ * envelope on stdout (or directly, under `concreteOutput`). This is the
199
+ * same wire shape v1 syncs produce.
200
+ */
201
+ export type SyncRunResult = {
202
+ changes: (SyncChange & { targetDatabaseKey: string })[];
203
+ hasMore: boolean;
204
+ nextUserContext?: unknown;
205
+ nextPacerStates: Record<string, PacerEntry>;
206
+ };
207
+
208
+ type SyncErrorEnvelope =
209
+ | { _tag: "error"; error: { _tag: "generic"; name: string; message: string } }
210
+ | {
211
+ _tag: "error";
212
+ error: { _tag: "rate_limit"; name: string; message: string; retryAfter?: number };
213
+ };
214
+
215
+ /**
216
+ * A sync capability as returned by {@link createSync}.
217
+ *
218
+ * Note that a sync carries no key of its own — the build tool derives the
219
+ * capability key from the file the sync is default-exported from.
220
+ */
221
+ export type Sync = {
222
+ _tag: "sync";
223
+ config: SyncManifestConfig;
224
+ handler: (
225
+ runtimeContext?: SyncRuntimeContext,
226
+ options?: HandlerOptions,
227
+ ) => Promise<SyncRunResult>;
228
+ };
229
+
230
+ /**
231
+ * Create a sync capability.
232
+ *
233
+ * Default-export the result from a file directly under `src/syncs/` —
234
+ * the file's basename becomes the sync's key.
235
+ *
236
+ * @example
237
+ * ```ts
238
+ * // src/syncs/tasks.ts
239
+ * import { createDatabase } from "@notionhq/workers/alpha/database";
240
+ * import { Schema } from "@notionhq/workers/alpha/schema";
241
+ * import { createSync } from "@notionhq/workers/alpha/sync";
242
+ *
243
+ * export default createSync({
244
+ * database: createDatabase("tasks", {
245
+ * type: "managed",
246
+ * primaryKey: "Task ID",
247
+ * schema: {
248
+ * properties: {
249
+ * Title: Schema.title(),
250
+ * "Task ID": Schema.richText(),
251
+ * },
252
+ * },
253
+ * }),
254
+ * mode: "replace",
255
+ * schedule: "24h",
256
+ * handler: async () => ({
257
+ * changes: [
258
+ * {
259
+ * type: "upsert",
260
+ * key: "1",
261
+ * properties: { Title: [["Item 1"]], "Task ID": [["1"]] },
262
+ * },
263
+ * ],
264
+ * }),
265
+ * });
266
+ * ```
267
+ */
268
+ export function createSync<S extends PropertySchema, State = unknown>(
269
+ configuration: SyncConfiguration<S, State>,
270
+ ): Sync {
271
+ return {
272
+ _tag: "sync",
273
+ config: {
274
+ database: configuration.database,
275
+ mode: configuration.mode,
276
+ schedule: configuration.schedule ? parseSchedule(configuration.schedule) : undefined,
277
+ },
278
+ async handler(runtimeContext?: SyncRuntimeContext, options?: HandlerOptions) {
279
+ // The platform guarantees the state it passes back is whatever the
280
+ // previous run returned as nextState.
281
+ const state = (runtimeContext?.state ?? runtimeContext?.userContext) as State | undefined;
282
+
283
+ initPacerState(runtimeContext?.pacers);
284
+
285
+ let handlerResult: SyncHandlerResult<S, State>;
286
+ try {
287
+ handlerResult = await configuration.handler(state, createCapabilityContext("sync"));
288
+ } catch (err) {
289
+ if (err instanceof RateLimitError) {
290
+ if (!options?.concreteOutput) {
291
+ const envelope: SyncErrorEnvelope = {
292
+ _tag: "error",
293
+ error: {
294
+ _tag: "rate_limit",
295
+ name: err.name,
296
+ message: err.message,
297
+ ...(err.retryAfter !== undefined ? { retryAfter: err.retryAfter } : {}),
298
+ },
299
+ };
300
+ writeOutput(envelope);
301
+ }
302
+ throw err;
303
+ }
304
+
305
+ const error = new ExecutionError(err);
306
+ if (!options?.concreteOutput) {
307
+ const envelope: SyncErrorEnvelope = {
308
+ _tag: "error",
309
+ error: {
310
+ _tag: "generic",
311
+ name: error.name,
312
+ message: error.message,
313
+ },
314
+ };
315
+ writeOutput(envelope);
316
+ }
317
+ throw error;
318
+ }
319
+
320
+ const changes = handlerResult.changes.map((change) => ({
321
+ ...change,
322
+ targetDatabaseKey: configuration.database.name,
323
+ }));
324
+
325
+ const result: SyncRunResult = {
326
+ changes,
327
+ hasMore: handlerResult.hasMore ?? false,
328
+ nextUserContext: handlerResult.nextState,
329
+ nextPacerStates: pacerStates(),
330
+ };
331
+
332
+ if (options?.concreteOutput) {
333
+ return result;
334
+ }
335
+
336
+ writeOutput({ _tag: "success", value: result });
337
+ return result;
338
+ },
339
+ };
340
+ }
@@ -0,0 +1,116 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi, type Mock } from "vitest";
2
+
3
+ import { j } from "./schema-builder.js";
4
+ import { createTool, InvalidToolInputError, ToolExecutionError } from "./tool.js";
5
+
6
+ let stdoutSpy: Mock<typeof process.stdout.write>;
7
+
8
+ beforeEach(() => {
9
+ stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
10
+ });
11
+
12
+ afterEach(() => {
13
+ stdoutSpy.mockRestore();
14
+ });
15
+
16
+ const sayHello = createTool({
17
+ title: "Say Hello",
18
+ description: "Say hello to the user",
19
+ schema: j.object({ name: j.string() }),
20
+ execute: ({ name }) => `Hello, ${name}!`,
21
+ });
22
+
23
+ describe("createTool", () => {
24
+ it("is tagged and exposes resolved schemas in config", () => {
25
+ expect(sayHello._tag).toBe("tool");
26
+ expect(sayHello.config).toEqual({
27
+ title: "Say Hello",
28
+ description: "Say hello to the user",
29
+ schema: {
30
+ type: "object",
31
+ properties: { name: { type: "string" } },
32
+ required: ["name"],
33
+ additionalProperties: false,
34
+ },
35
+ outputSchema: undefined,
36
+ hints: undefined,
37
+ });
38
+ });
39
+
40
+ it("writes a success envelope and returns the tagged result", async () => {
41
+ const result = await sayHello.handler({ name: "Alice" });
42
+
43
+ expect(result).toEqual({ _tag: "success", value: "Hello, Alice!" });
44
+ expect(stdoutSpy).toHaveBeenCalledWith(
45
+ `\n<__notion_output__>{"_tag":"success","value":"Hello, Alice!"}</__notion_output__>\n`,
46
+ );
47
+ });
48
+
49
+ it("returns the raw result under concreteOutput", async () => {
50
+ const result = await sayHello.handler({ name: "Alice" }, { concreteOutput: true });
51
+
52
+ expect(result).toBe("Hello, Alice!");
53
+ expect(stdoutSpy).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it("rejects input that does not match the schema", async () => {
57
+ const result = await sayHello.handler({ name: 42 } as never);
58
+
59
+ expect(result).toMatchObject({
60
+ _tag: "error",
61
+ error: { name: "InvalidToolInputError" },
62
+ });
63
+ expect(stdoutSpy).toHaveBeenCalledWith(
64
+ expect.stringContaining(
65
+ `<__notion_output__>{"_tag":"error","error":{"name":"InvalidToolInputError"`,
66
+ ),
67
+ );
68
+
69
+ await expect(
70
+ sayHello.handler({ name: 42 } as never, { concreteOutput: true }),
71
+ ).rejects.toBeInstanceOf(InvalidToolInputError);
72
+ });
73
+
74
+ it("rejects output that does not match the output schema", async () => {
75
+ const badOutput = createTool({
76
+ title: "Bad Output",
77
+ description: "Returns output that fails validation",
78
+ schema: j.object({ id: j.number() }),
79
+ outputSchema: j.object({ data: j.string() }),
80
+ execute: () => ({ data: 42 }) as never,
81
+ });
82
+
83
+ const result = await badOutput.handler({ id: 1 });
84
+ expect(result).toMatchObject({
85
+ _tag: "error",
86
+ error: { name: "InvalidToolOutputError" },
87
+ });
88
+
89
+ // Mirrors v1: under concreteOutput the output-validation throw happens
90
+ // inside the try block, so it surfaces wrapped in ToolExecutionError.
91
+ await expect(badOutput.handler({ id: 1 }, { concreteOutput: true })).rejects.toBeInstanceOf(
92
+ ToolExecutionError,
93
+ );
94
+ });
95
+
96
+ it("wraps execution failures in ToolExecutionError", async () => {
97
+ const explodes = createTool({
98
+ title: "Explodes",
99
+ description: "Always throws",
100
+ schema: j.object({}),
101
+ execute: () => {
102
+ throw new Error("boom");
103
+ },
104
+ });
105
+
106
+ const result = await explodes.handler({});
107
+ expect(result).toMatchObject({
108
+ _tag: "error",
109
+ error: { name: "ToolExecutionError", message: "boom" },
110
+ });
111
+
112
+ await expect(explodes.handler({}, { concreteOutput: true })).rejects.toBeInstanceOf(
113
+ ToolExecutionError,
114
+ );
115
+ });
116
+ });