@notionhq/workers 0.0.85 → 0.1.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.
@@ -1 +1 @@
1
- {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,KAAK,EACX,+BAA+B,EAC/B,eAAe,EACf,kBAAkB,EAClB,6BAA6B,EAC7B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5D,YAAY,EACX,uBAAuB,EACvB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,+BAA+B,EAC/B,6BAA6B,EAC7B,iBAAiB,EACjB,iBAAiB,GACjB,CAAC;AAMF,KAAK,UAAU,GACZ,cAAc,GAEd,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,GACxB,oBAAoB,GACpB,eAAe,CAAC;AAMnB,qBAAa,MAAM;;IAGlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8CG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,EAC9D,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,iBAAiB,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC,GACvC,cAAc;IAOjB;;;;;;;;;;;;;;;;;;;;;;;;;;OA0BG;IACH,IAAI,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,SAAS,GAAG,SAAS,EACxD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC,GAC7B,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC;IAQvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACH,UAAU,CACT,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,uBAAuB,GAC7B,oBAAoB;IAOvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAwCG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,eAAe;IAO/D;;OAEG;IACH,IAAI,YAAY,IAAI,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC,EAAE,CAMhE;IAED;;;;;;;OAOG;IACG,GAAG,CACR,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,cAAmB,GAC1B,OAAO,CAAC,OAAO,CAAC;CAyBnB"}
1
+ {"version":3,"file":"worker.d.ts","sourceRoot":"","sources":["../src/worker.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACX,oBAAoB,EACpB,uBAAuB,EACvB,eAAe,EACf,MAAM,8BAA8B,CAAC;AAEtC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,KAAK,EACX,+BAA+B,EAC/B,eAAe,EACf,kBAAkB,EAClB,6BAA6B,EAC7B,MAAM,yBAAyB,CAAC;AAEjC,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAEhF,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,KAAK,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAG5D,YAAY,EACX,uBAAuB,EACvB,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,+BAA+B,EAC/B,6BAA6B,EAC7B,iBAAiB,EACjB,iBAAiB,GACjB,CAAC;AAMF,KAAK,UAAU,GACZ,cAAc,GAEd,cAAc,CAAC,GAAG,EAAE,GAAG,CAAC,GACxB,oBAAoB,GACpB,eAAe,CAAC;AAMnB,qBAAa,MAAM;;IAGlB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8CG;IACH,IAAI,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC,SAAS,MAAM,CAAC,EAAE,CAAC,EAAE,OAAO,GAAG,OAAO,EAC9D,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,iBAAiB,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,CAAC,GACvC,cAAc;IAOjB;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,IAAI,CAAC,CAAC,SAAS,SAAS,EAAE,CAAC,SAAS,SAAS,GAAG,SAAS,EACxD,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC,GAC7B,cAAc,CAAC,CAAC,EAAE,CAAC,CAAC;IAQvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA+BG;IACH,UAAU,CACT,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,uBAAuB,GAC7B,oBAAoB;IAOvB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAwCG;IACH,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,eAAe;IAO/D;;OAEG;IACH,IAAI,YAAY,IAAI,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,KAAK,GAAG,QAAQ,CAAC,EAAE,CAMhE;IAED;;;;;;;OAOG;IACG,GAAG,CACR,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,cAAmB,GAC1B,OAAO,CAAC,OAAO,CAAC;CAyBnB"}
package/dist/worker.js CHANGED
@@ -63,23 +63,18 @@ class Worker {
63
63
  * Example:
64
64
  *
65
65
  * ```ts
66
+ * import { j } from "@notionhq/workers/schema-builder";
67
+ *
66
68
  * worker.tool<{ name: string }, string>("sayHello", {
67
69
  * title: "Say Hello",
68
70
  * description: "Say hello to the user",
69
- * schema: {
70
- * type: "object",
71
- * properties: {
72
- * name: { type: "string" },
73
- * },
74
- * required: ["name"],
75
- * },
71
+ * schema: j.object({ name: j.string() }),
76
72
  * execute: ({ name }, { notion }) => {
77
73
  * return `Hello, ${name}!`;
78
74
  * },
79
75
  * })
80
76
  * ```
81
77
  *
82
- *
83
78
  * @param key - The unique key for this capability.
84
79
  * @param config - The tool configuration.
85
80
  * @returns The capability object.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@notionhq/workers",
3
- "version": "0.0.85",
3
+ "version": "0.1.0",
4
4
  "description": "An SDK for building workers for the Notion Workers platform",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -25,6 +25,10 @@
25
25
  "types": "./dist/error.d.ts",
26
26
  "default": "./dist/error.js"
27
27
  },
28
+ "./schema-builder": {
29
+ "types": "./dist/schema-builder.d.ts",
30
+ "default": "./dist/schema-builder.js"
31
+ },
28
32
  "./types": {
29
33
  "types": "./dist/types.d.ts",
30
34
  "default": "./dist/types.js"
@@ -0,0 +1,229 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import * as Builder from "./builder.js";
3
+
4
+ /**
5
+ * Helper to extract the DateValue from a date token.
6
+ * Date tokens have the shape [["\u2023", [["d", dateValue]]]].
7
+ */
8
+ // biome-ignore lint/suspicious/noExplicitAny: test helper
9
+ function extractDateValue(token: unknown): any {
10
+ // biome-ignore lint/suspicious/noExplicitAny: test helper
11
+ const arr = token as any;
12
+ return arr[0][1][0][1];
13
+ }
14
+
15
+ describe("Builder.date", () => {
16
+ it("creates a date token from YYYY-MM-DD", () => {
17
+ expect(extractDateValue(Builder.date("2024-01-15"))).toEqual({
18
+ type: "date",
19
+ start_date: "2024-01-15",
20
+ });
21
+ });
22
+
23
+ it("rejects non-YYYY-MM-DD strings", () => {
24
+ expect(() => Builder.date("01-15-2024")).toThrow("Expected YYYY-MM-DD");
25
+ expect(() => Builder.date("2024/01/15")).toThrow("Expected YYYY-MM-DD");
26
+ expect(() => Builder.date("2024-1-5")).toThrow("Expected YYYY-MM-DD");
27
+ });
28
+
29
+ it("rejects invalid calendar dates", () => {
30
+ expect(() => Builder.date("2024-13-01")).toThrow();
31
+ // Note: "2024-02-30" is silently coerced by JS Date (rolls to March 1st),
32
+ // so the current validator does not catch it.
33
+ });
34
+ });
35
+
36
+ describe("Builder.dateTime", () => {
37
+ it("parses minimal ISO 8601 (YYYY-MM-DDTHH:mm)", () => {
38
+ expect(extractDateValue(Builder.dateTime("2024-01-15T10:30"))).toEqual({
39
+ type: "datetime",
40
+ start_date: "2024-01-15",
41
+ start_time: "10:30",
42
+ });
43
+ });
44
+
45
+ it("parses ISO 8601 with seconds", () => {
46
+ expect(extractDateValue(Builder.dateTime("2024-01-15T10:30:45"))).toEqual({
47
+ type: "datetime",
48
+ start_date: "2024-01-15",
49
+ start_time: "10:30",
50
+ });
51
+ });
52
+
53
+ it("parses ISO 8601 with Z suffix", () => {
54
+ expect(extractDateValue(Builder.dateTime("2024-01-15T10:30:00Z"))).toEqual({
55
+ type: "datetime",
56
+ start_date: "2024-01-15",
57
+ start_time: "10:30",
58
+ });
59
+ });
60
+
61
+ it("parses ISO 8601 with positive timezone offset", () => {
62
+ expect(
63
+ extractDateValue(Builder.dateTime("2024-01-15T10:30:00+05:30")),
64
+ ).toEqual({
65
+ type: "datetime",
66
+ start_date: "2024-01-15",
67
+ start_time: "10:30",
68
+ });
69
+ });
70
+
71
+ it("parses ISO 8601 with negative timezone offset", () => {
72
+ expect(
73
+ extractDateValue(Builder.dateTime("2024-01-15T10:30:00-08:00")),
74
+ ).toEqual({
75
+ type: "datetime",
76
+ start_date: "2024-01-15",
77
+ start_time: "10:30",
78
+ });
79
+ });
80
+
81
+ it("parses ISO 8601 with milliseconds", () => {
82
+ expect(
83
+ extractDateValue(Builder.dateTime("2024-01-15T10:30:00.000Z")),
84
+ ).toEqual({
85
+ type: "datetime",
86
+ start_date: "2024-01-15",
87
+ start_time: "10:30",
88
+ });
89
+ });
90
+
91
+ it("includes timeZone when provided", () => {
92
+ expect(
93
+ extractDateValue(
94
+ Builder.dateTime("2024-01-15T10:30", "America/New_York"),
95
+ ),
96
+ ).toEqual({
97
+ type: "datetime",
98
+ start_date: "2024-01-15",
99
+ start_time: "10:30",
100
+ time_zone: "America/New_York",
101
+ });
102
+ });
103
+
104
+ it("handles midnight", () => {
105
+ expect(extractDateValue(Builder.dateTime("2024-01-15T00:00"))).toEqual({
106
+ type: "datetime",
107
+ start_date: "2024-01-15",
108
+ start_time: "00:00",
109
+ });
110
+ });
111
+
112
+ it("handles end of day", () => {
113
+ expect(extractDateValue(Builder.dateTime("2024-01-15T23:59"))).toEqual({
114
+ type: "datetime",
115
+ start_date: "2024-01-15",
116
+ start_time: "23:59",
117
+ });
118
+ });
119
+
120
+ it("rejects date-only strings", () => {
121
+ expect(() => Builder.dateTime("2024-01-15")).toThrow(
122
+ "Invalid ISO 8601 datetime",
123
+ );
124
+ });
125
+
126
+ it("rejects plain text", () => {
127
+ expect(() => Builder.dateTime("not a date")).toThrow(
128
+ "Invalid ISO 8601 datetime",
129
+ );
130
+ });
131
+
132
+ it("rejects empty string", () => {
133
+ expect(() => Builder.dateTime("")).toThrow("Invalid ISO 8601 datetime");
134
+ });
135
+
136
+ it("rejects date with space instead of T", () => {
137
+ expect(() => Builder.dateTime("2024-01-15 10:30")).toThrow(
138
+ "Invalid ISO 8601 datetime",
139
+ );
140
+ });
141
+ });
142
+
143
+ describe("Builder.dateRange", () => {
144
+ it("creates a date range token", () => {
145
+ expect(
146
+ extractDateValue(Builder.dateRange("2024-01-15", "2024-01-20")),
147
+ ).toEqual({
148
+ type: "daterange",
149
+ start_date: "2024-01-15",
150
+ end_date: "2024-01-20",
151
+ });
152
+ });
153
+
154
+ it("rejects invalid start date", () => {
155
+ expect(() => Builder.dateRange("bad", "2024-01-20")).toThrow(
156
+ "Expected YYYY-MM-DD",
157
+ );
158
+ });
159
+
160
+ it("rejects invalid end date", () => {
161
+ expect(() => Builder.dateRange("2024-01-15", "bad")).toThrow(
162
+ "Expected YYYY-MM-DD",
163
+ );
164
+ });
165
+ });
166
+
167
+ describe("Builder.dateTimeRange", () => {
168
+ it("creates a datetime range token from ISO strings", () => {
169
+ expect(
170
+ extractDateValue(
171
+ Builder.dateTimeRange("2024-01-15T10:30", "2024-01-16T14:00"),
172
+ ),
173
+ ).toEqual({
174
+ type: "datetimerange",
175
+ start_date: "2024-01-15",
176
+ start_time: "10:30",
177
+ end_date: "2024-01-16",
178
+ end_time: "14:00",
179
+ });
180
+ });
181
+
182
+ it("parses mixed ISO formats", () => {
183
+ expect(
184
+ extractDateValue(
185
+ Builder.dateTimeRange(
186
+ "2024-01-15T10:30:00Z",
187
+ "2024-01-16T14:00:00.000+05:00",
188
+ ),
189
+ ),
190
+ ).toEqual({
191
+ type: "datetimerange",
192
+ start_date: "2024-01-15",
193
+ start_time: "10:30",
194
+ end_date: "2024-01-16",
195
+ end_time: "14:00",
196
+ });
197
+ });
198
+
199
+ it("includes timeZone when provided", () => {
200
+ expect(
201
+ extractDateValue(
202
+ Builder.dateTimeRange(
203
+ "2024-01-15T10:30",
204
+ "2024-01-16T14:00",
205
+ "Europe/London",
206
+ ),
207
+ ),
208
+ ).toEqual({
209
+ type: "datetimerange",
210
+ start_date: "2024-01-15",
211
+ start_time: "10:30",
212
+ end_date: "2024-01-16",
213
+ end_time: "14:00",
214
+ time_zone: "Europe/London",
215
+ });
216
+ });
217
+
218
+ it("rejects invalid start datetime", () => {
219
+ expect(() =>
220
+ Builder.dateTimeRange("2024-01-15", "2024-01-16T14:00"),
221
+ ).toThrow("Invalid ISO 8601 datetime");
222
+ });
223
+
224
+ it("rejects invalid end datetime", () => {
225
+ expect(() =>
226
+ Builder.dateTimeRange("2024-01-15T10:30", "2024-01-16"),
227
+ ).toThrow("Invalid ISO 8601 datetime");
228
+ });
229
+ });
package/src/builder.ts CHANGED
@@ -92,13 +92,17 @@ export function date(dateString: string): TextValue {
92
92
  }
93
93
 
94
94
  /**
95
- * Creates a datetime value from a Date object.
95
+ * Creates a datetime value from an ISO 8601 datetime string.
96
+ * @param isoString - An ISO 8601 datetime string (e.g., "2024-01-15T10:30", "2024-01-15T10:30:00Z")
97
+ * @param timeZone - Optional IANA timezone name (e.g., "America/New_York")
96
98
  */
97
- export function dateTime(date: Date, timeZone?: string): TextValue {
99
+ export function dateTime(isoString: string, timeZone?: string): TextValue {
100
+ const { date, time } = parseISODateTime(isoString);
101
+
98
102
  const dateValue: DateValue = {
99
103
  type: "datetime",
100
- start_date: formatDate(date),
101
- start_time: formatTime(date),
104
+ start_date: date,
105
+ start_time: time,
102
106
  };
103
107
 
104
108
  if (timeZone) {
@@ -125,19 +129,25 @@ export function dateRange(startDate: string, endDate: string): TextValue {
125
129
  }
126
130
 
127
131
  /**
128
- * Creates a datetime range value from Date objects.
132
+ * Creates a datetime range value from ISO 8601 datetime strings.
133
+ * @param startDateTime - An ISO 8601 datetime string for the range start
134
+ * @param endDateTime - An ISO 8601 datetime string for the range end
135
+ * @param timeZone - Optional IANA timezone name (e.g., "America/New_York")
129
136
  */
130
137
  export function dateTimeRange(
131
- startDate: Date,
132
- endDate: Date,
138
+ startDateTime: string,
139
+ endDateTime: string,
133
140
  timeZone?: string,
134
141
  ): TextValue {
142
+ const start = parseISODateTime(startDateTime);
143
+ const end = parseISODateTime(endDateTime);
144
+
135
145
  const dateValue: DateValue = {
136
146
  type: "datetimerange",
137
- start_date: formatDate(startDate),
138
- start_time: formatTime(startDate),
139
- end_date: formatDate(endDate),
140
- end_time: formatTime(endDate),
147
+ start_date: start.date,
148
+ start_time: start.time,
149
+ end_date: end.date,
150
+ end_time: end.time,
141
151
  };
142
152
 
143
153
  if (timeZone) {
@@ -242,22 +252,25 @@ function createDateToken(dateValue: DateValue): TextValue {
242
252
  }
243
253
 
244
254
  /**
245
- * Formats a Date object to YYYY-MM-DD string.
255
+ * Parses an ISO 8601 datetime string into date and time components.
256
+ * Accepts formats like "2024-01-15T10:30", "2024-01-15T10:30:00Z",
257
+ * "2024-01-15T10:30:00.000+05:00", etc.
246
258
  */
247
- function formatDate(date: Date): string {
248
- const year = date.getFullYear();
249
- const month = String(date.getMonth() + 1).padStart(2, "0");
250
- const day = String(date.getDate()).padStart(2, "0");
251
- return `${year}-${month}-${day}`;
252
- }
259
+ function parseISODateTime(isoString: string): { date: string; time: string } {
260
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}/.test(isoString)) {
261
+ throw new Error(
262
+ `Invalid ISO 8601 datetime: ${isoString}. Expected format like 2024-01-15T10:30 or 2024-01-15T10:30:00Z`,
263
+ );
264
+ }
253
265
 
254
- /**
255
- * Formats a Date object to HH:mm string.
256
- */
257
- function formatTime(date: Date): string {
258
- const hours = String(date.getHours()).padStart(2, "0");
259
- const minutes = String(date.getMinutes()).padStart(2, "0");
260
- return `${hours}:${minutes}`;
266
+ const date = isoString.slice(0, 10);
267
+ const time = isoString.slice(11, 16);
268
+
269
+ if (Number.isNaN(new Date(isoString).getTime())) {
270
+ throw new Error(`Invalid datetime: ${isoString}`);
271
+ }
272
+
273
+ return { date, time };
261
274
  }
262
275
 
263
276
  /**
@@ -59,7 +59,7 @@ describe("createAutomationCapability", () => {
59
59
  }),
60
60
  );
61
61
  expect(stdoutSpy).toHaveBeenCalledWith(
62
- `\n<output>{"status":"success"}</output>\n`,
62
+ `\n<output>{"_tag":"success","value":{"status":"success"}}</output>\n`,
63
63
  );
64
64
  });
65
65
 
@@ -105,7 +105,7 @@ describe("createAutomationCapability", () => {
105
105
  }),
106
106
  );
107
107
  expect(stdoutSpy).toHaveBeenCalledWith(
108
- `\n<output>{"status":"success"}</output>\n`,
108
+ `\n<output>{"_tag":"success","value":{"status":"success"}}</output>\n`,
109
109
  );
110
110
  });
111
111
 
@@ -126,7 +126,9 @@ describe("createAutomationCapability", () => {
126
126
  await expect(capability.handler(event)).rejects.toThrow(ExecutionError);
127
127
 
128
128
  expect(stdoutSpy).toHaveBeenCalledWith(
129
- `\n<output>{"status":"error","error":{"name":"ExecutionError","message":"Error during worker execution: Error: Something went wrong"}}</output>\n`,
129
+ expect.stringContaining(
130
+ `"_tag":"error","error":{"name":"ExecutionError","message":"Error during worker execution: Error: Something went wrong"`,
131
+ ),
130
132
  );
131
133
  });
132
134
 
@@ -146,7 +148,9 @@ describe("createAutomationCapability", () => {
146
148
  await expect(capability.handler(event)).rejects.toThrow(ExecutionError);
147
149
 
148
150
  expect(stdoutSpy).toHaveBeenCalledWith(
149
- `\n<output>{"status":"error","error":{"name":"ExecutionError","message":"Error during worker execution: String error"}}</output>\n`,
151
+ expect.stringContaining(
152
+ `"_tag":"error","error":{"name":"ExecutionError","message":"Error during worker execution: String error"`,
153
+ ),
150
154
  );
151
155
  });
152
156
  });
@@ -111,7 +111,7 @@ export function createAutomationCapability(
111
111
  return { status: "success" };
112
112
  } else {
113
113
  process.stdout.write(
114
- `\n<output>${JSON.stringify({ status: "success" })}</output>\n`,
114
+ `\n<output>${JSON.stringify({ _tag: "success", value: { status: "success" } })}</output>\n`,
115
115
  );
116
116
  }
117
117
  } catch (err) {
@@ -119,7 +119,7 @@ export function createAutomationCapability(
119
119
 
120
120
  if (!options?.concreteOutput) {
121
121
  process.stdout.write(
122
- `\n<output>${JSON.stringify({ status: "error", error: { name: error.name, message: error.message } })}</output>\n`,
122
+ `\n<output>${JSON.stringify({ _tag: "error", error: { name: error.name, message: error.message, trace: error.stack } })}</output>\n`,
123
123
  );
124
124
  }
125
125
 
@@ -1,9 +1,30 @@
1
- import { describe, expect, it } from "vitest";
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ type Mock,
8
+ vi,
9
+ } from "vitest";
2
10
  import * as Builder from "../builder.js";
11
+ import { ExecutionError } from "../error.js";
3
12
  import * as Schema from "../schema.js";
4
13
  import { createSyncCapability } from "./sync.js";
5
14
 
6
15
  describe("Worker.sync", () => {
16
+ let stdoutSpy: Mock<typeof process.stdout.write>;
17
+
18
+ beforeEach(() => {
19
+ stdoutSpy = vi
20
+ .spyOn(process.stdout, "write")
21
+ .mockImplementation(() => true);
22
+ });
23
+
24
+ afterEach(() => {
25
+ vi.restoreAllMocks();
26
+ });
27
+
7
28
  describe("Schema.relation", () => {
8
29
  it("creates a relation property configuration", () => {
9
30
  const relationProp = Schema.relation("projectsSync");
@@ -101,4 +122,52 @@ describe("Worker.sync", () => {
101
122
  expect(capability.config.primaryKeyProperty).toBe("Task ID");
102
123
  });
103
124
  });
125
+
126
+ describe("handler output envelope", () => {
127
+ it("wraps successful result in success envelope", async () => {
128
+ const capability = createSyncCapability("testSync", {
129
+ primaryKeyProperty: "ID",
130
+ schema: {
131
+ defaultName: "Test",
132
+ properties: {
133
+ ID: Schema.title(),
134
+ },
135
+ },
136
+ execute: async () => ({
137
+ changes: [],
138
+ hasMore: false,
139
+ }),
140
+ });
141
+
142
+ await capability.handler();
143
+
144
+ expect(stdoutSpy).toHaveBeenCalledWith(
145
+ `\n<output>${JSON.stringify({ _tag: "success", value: { changes: [], hasMore: false, nextUserContext: undefined } })}</output>\n`,
146
+ );
147
+ });
148
+
149
+ it("wraps error in error envelope", async () => {
150
+ const capability = createSyncCapability("errorSync", {
151
+ primaryKeyProperty: "ID",
152
+ schema: {
153
+ defaultName: "Test",
154
+ properties: {
155
+ ID: Schema.title(),
156
+ },
157
+ },
158
+ execute: async () => {
159
+ throw new Error("sync failed");
160
+ },
161
+ });
162
+
163
+ await expect(capability.handler()).rejects.toThrow(ExecutionError);
164
+
165
+ expect(stdoutSpy).toHaveBeenCalledWith(
166
+ expect.stringContaining(`"_tag":"error"`),
167
+ );
168
+ expect(stdoutSpy).toHaveBeenCalledWith(
169
+ expect.stringContaining(`"name":"ExecutionError"`),
170
+ );
171
+ });
172
+ });
104
173
  });
@@ -227,11 +227,21 @@ export function createSyncCapability<
227
227
  ) {
228
228
  const capabilityContext = createCapabilityContext();
229
229
  const state = runtimeContext?.state ?? runtimeContext?.userContext;
230
- const executionResult = await syncConfiguration
231
- .execute(state, capabilityContext)
232
- .catch((err) => {
233
- throw new ExecutionError(err);
234
- });
230
+ let executionResult: SyncExecutionResult<PK, Context>;
231
+ try {
232
+ executionResult = await syncConfiguration.execute(
233
+ state,
234
+ capabilityContext,
235
+ );
236
+ } catch (err) {
237
+ const error = new ExecutionError(err);
238
+ if (!options?.concreteOutput) {
239
+ process.stdout.write(
240
+ `\n<output>${JSON.stringify({ _tag: "error", error: { name: error.name, message: error.message } })}</output>\n`,
241
+ );
242
+ }
243
+ throw error;
244
+ }
235
245
 
236
246
  const result = {
237
247
  changes: executionResult.changes,
@@ -242,7 +252,9 @@ export function createSyncCapability<
242
252
  if (options?.concreteOutput) {
243
253
  return result;
244
254
  } else {
245
- process.stdout.write(`\n<output>${JSON.stringify(result)}</output>\n`);
255
+ process.stdout.write(
256
+ `\n<output>${JSON.stringify({ _tag: "success", value: result })}</output>\n`,
257
+ );
246
258
  }
247
259
 
248
260
  return result;