@crewhaus/gateway-protocol 0.1.0 → 0.1.2

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 (2) hide show
  1. package/package.json +6 -11
  2. package/src/index.test.ts +155 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@crewhaus/gateway-protocol",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "type": "module",
5
5
  "description": "JSON-RPC wire protocol for the managed-daemon gateway — versioned envelope + Zod schemas",
6
6
  "main": "src/index.ts",
@@ -12,14 +12,14 @@
12
12
  "test": "bun test src"
13
13
  },
14
14
  "dependencies": {
15
- "@crewhaus/errors": "0.0.0",
15
+ "@crewhaus/errors": "0.1.2",
16
16
  "zod": "^3.23.8"
17
17
  },
18
18
  "license": "Apache-2.0",
19
19
  "author": {
20
20
  "name": "Max Meier",
21
- "email": "max@studiomax.io",
22
- "url": "https://studiomax.io"
21
+ "email": "max@crewhaus.ai",
22
+ "url": "https://crewhaus.ai"
23
23
  },
24
24
  "repository": {
25
25
  "type": "git",
@@ -31,12 +31,7 @@
31
31
  "url": "https://github.com/crewhaus/factory/issues"
32
32
  },
33
33
  "publishConfig": {
34
- "access": "restricted"
34
+ "access": "public"
35
35
  },
36
- "files": [
37
- "src",
38
- "README.md",
39
- "LICENSE",
40
- "NOTICE"
41
- ]
36
+ "files": ["src", "README.md", "LICENSE", "NOTICE"]
42
37
  }
package/src/index.test.ts CHANGED
@@ -2,7 +2,10 @@ import { describe, expect, test } from "bun:test";
2
2
  import {
3
3
  ErrorCode,
4
4
  GatewayProtocolError,
5
+ Method,
5
6
  PROTOCOL_VERSION,
7
+ RequestEnvelope,
8
+ ResponseEnvelope,
6
9
  decodeRequest,
7
10
  encodeError,
8
11
  encodeSuccess,
@@ -102,3 +105,155 @@ describe("standard error codes are wire-stable", () => {
102
105
  expect(ErrorCode.InternalError).toBe("internal_error");
103
106
  });
104
107
  });
108
+
109
+ describe("error-message path formatting", () => {
110
+ // The issue-formatter uses `i.path.join(".") || "<root>"`. A top-level
111
+ // failure (raw is not an object at all) yields an empty Zod path, which
112
+ // must surface as "<root>" rather than an empty string.
113
+ test("envelope failure on a non-object surfaces <root>", () => {
114
+ expect(() => decodeRequest("not-an-object")).toThrow(/<root>: /);
115
+ });
116
+
117
+ test("param failure on a non-object surfaces <root>", () => {
118
+ const raw = {
119
+ protocol: "crewhaus.v1",
120
+ id: "abc",
121
+ method: "runs.cancel",
122
+ params: null,
123
+ };
124
+ expect(() => decodeRequest(raw)).toThrow(/invalid params for runs.cancel: <root>: /);
125
+ });
126
+
127
+ test("named-field failure surfaces the field path, not <root>", () => {
128
+ const raw = {
129
+ protocol: "crewhaus.v1",
130
+ id: "abc",
131
+ method: "runs.create",
132
+ params: { spec: 1, input: "hi" },
133
+ };
134
+ // `spec` is the offending path; it must appear and <root> must not.
135
+ expect(() => decodeRequest(raw)).toThrow(/spec: /);
136
+ try {
137
+ decodeRequest(raw);
138
+ } catch (e) {
139
+ expect((e as Error).message).not.toContain("<root>");
140
+ }
141
+ });
142
+ });
143
+
144
+ describe("decodeRequest accepts every declared method", () => {
145
+ const cases: Array<[string, unknown]> = [
146
+ ["runs.create", { spec: "s", input: "" }],
147
+ ["runs.continue", { sessionId: "sess", input: "more" }],
148
+ ["runs.cancel", { runId: "r1" }],
149
+ ["runs.subscribe", { runId: "r1" }],
150
+ ["sessions.list", {}],
151
+ ["sessions.fork", { sessionId: "sess", atEventTs: 0 }],
152
+ ["audit.tail", { tenantId: "t1" }],
153
+ ];
154
+ for (const [method, params] of cases) {
155
+ test(`decodes ${method}`, () => {
156
+ const r = decodeRequest({ protocol: "crewhaus.v1", id: "id", method, params });
157
+ expect(r.method).toBe(method as (typeof r)["method"]);
158
+ expect(r.protocol).toBe("crewhaus.v1");
159
+ expect(r.params).toEqual(params);
160
+ });
161
+ }
162
+
163
+ test("Method enum lists exactly the routed methods", () => {
164
+ expect([...Method.options].sort()).toEqual(
165
+ [...cases.map(([m]) => m as (typeof Method.options)[number])].sort(),
166
+ );
167
+ });
168
+ });
169
+
170
+ describe("envelope id/method minimums", () => {
171
+ test("rejects empty id", () => {
172
+ const raw = {
173
+ protocol: "crewhaus.v1",
174
+ id: "",
175
+ method: "runs.create",
176
+ params: { spec: "x", input: "" },
177
+ };
178
+ expect(() => decodeRequest(raw)).toThrow(GatewayProtocolError);
179
+ });
180
+
181
+ test("rejects empty method", () => {
182
+ const raw = { protocol: "crewhaus.v1", id: "abc", method: "", params: {} };
183
+ expect(() => decodeRequest(raw)).toThrow(GatewayProtocolError);
184
+ });
185
+
186
+ test("optional sessionId is accepted on runs.create", () => {
187
+ const r = decodeRequest({
188
+ protocol: "crewhaus.v1",
189
+ id: "abc",
190
+ method: "runs.create",
191
+ params: { spec: "x", input: "hi", sessionId: "sess" },
192
+ });
193
+ expect((r.params as { sessionId?: string }).sessionId).toBe("sess");
194
+ });
195
+ });
196
+
197
+ describe("encoder ↔ schema round-trips", () => {
198
+ test("encodeSuccess output validates against ResponseEnvelope", () => {
199
+ expect(ResponseEnvelope.safeParse(encodeSuccess("i", { ok: true })).success).toBe(true);
200
+ });
201
+
202
+ test("encodeError output validates against ResponseEnvelope (no data)", () => {
203
+ expect(ResponseEnvelope.safeParse(encodeError("i", "not_found", "missing")).success).toBe(true);
204
+ });
205
+
206
+ test("encodeError output validates against ResponseEnvelope (with data)", () => {
207
+ const parsed = ResponseEnvelope.safeParse(encodeError("i", "bad_request", "bad", { f: 1 }));
208
+ expect(parsed.success).toBe(true);
209
+ });
210
+
211
+ // Distinct from the no-arg call: passing `data: undefined` explicitly must
212
+ // still omit the key (the `data !== undefined` guard), keeping the wire
213
+ // form identical to the success/no-data shape.
214
+ test("encodeError with explicit undefined data omits the data key", () => {
215
+ const r = encodeError("id-1", ErrorCode.InternalError, "boom", undefined);
216
+ expect(r).toEqual({
217
+ protocol: "crewhaus.v1",
218
+ id: "id-1",
219
+ error: { code: "internal_error", message: "boom" },
220
+ });
221
+ expect("data" in (r as { error: Record<string, unknown> }).error).toBe(false);
222
+ });
223
+
224
+ test("encodeError preserves a falsy-but-defined data value", () => {
225
+ const r = encodeError("id-1", ErrorCode.BadRequest, "bad", null);
226
+ expect((r as { error: { data?: unknown } }).error.data).toBeNull();
227
+ expect("data" in (r as { error: Record<string, unknown> }).error).toBe(true);
228
+ });
229
+ });
230
+
231
+ describe("RequestEnvelope is the exported request schema", () => {
232
+ test("validates a well-formed envelope shape directly", () => {
233
+ const parsed = RequestEnvelope.safeParse({
234
+ protocol: "crewhaus.v1",
235
+ id: "abc",
236
+ method: "anything.goes",
237
+ params: 42,
238
+ });
239
+ expect(parsed.success).toBe(true);
240
+ });
241
+ });
242
+
243
+ describe("GatewayProtocolError", () => {
244
+ test("carries the config code and preserves its cause", () => {
245
+ const cause = new Error("underlying");
246
+ const err = new GatewayProtocolError("nope", cause);
247
+ expect(err).toBeInstanceOf(GatewayProtocolError);
248
+ expect(err.name).toBe("GatewayProtocolError");
249
+ expect(err.code).toBe("config");
250
+ expect(err.message).toBe("nope");
251
+ expect(err.cause).toBe(cause);
252
+ });
253
+
254
+ test("is constructible without a cause", () => {
255
+ const err = new GatewayProtocolError("solo");
256
+ expect(err.cause).toBeUndefined();
257
+ expect(err.code).toBe("config");
258
+ });
259
+ });