@knpkv/clockify-api-client 1.0.3 → 2.0.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.
package/src/index.ts CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  export {
8
8
  ClockifyApiClient,
9
- type ClockifyApiClientShape,
9
+ type ClockifyApiClientContract,
10
10
  type ClockifyClientError,
11
11
  layer,
12
12
  make
@@ -28,6 +28,6 @@ export type {
28
28
  WorkspaceUser
29
29
  } from "./ClockifyApiClient.js"
30
30
 
31
- export { ClockifyApiConfig, type ClockifyApiConfigShape } from "./ClockifyApiConfig.js"
31
+ export { ClockifyApiConfig, type ClockifyApiConfigContract } from "./ClockifyApiConfig.js"
32
32
 
33
33
  export * as ClockifyApi from "./generated/ClockifyApi.js"
@@ -1,44 +1,67 @@
1
- import { describe, expect, it } from "@effect/vitest"
1
+ import { assert, describe, expect, it } from "@effect/vitest"
2
2
  import * as Effect from "effect/Effect"
3
+ import * as HttpClient from "effect/http/HttpClient"
4
+ import type * as HttpClientRequest from "effect/http/HttpClientRequest"
5
+ import * as HttpClientResponse from "effect/http/HttpClientResponse"
3
6
  import * as Layer from "effect/Layer"
7
+ import * as Predicate from "effect/Predicate"
4
8
  import * as Redacted from "effect/Redacted"
5
- import * as HttpClient from "effect/unstable/http/HttpClient"
6
- import type * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"
7
- import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse"
8
9
  import { ClockifyApiClient, ClockifyApiConfig, make } from "../src/index.js"
9
10
 
10
- const clientLayer = (
11
- response: { readonly status: number; readonly body: unknown },
12
- requests: Array<HttpClientRequest.HttpClientRequest>
13
- ) =>
14
- ClockifyApiClient.layer.pipe(
15
- Layer.provide(Layer.succeed(ClockifyApiConfig, {
16
- apiKey: Redacted.make("secret"),
17
- workspaceId: "workspace-1",
18
- userId: "user-1",
19
- baseUrl: "https://clockify.test/api"
20
- })),
21
- Layer.provide(Layer.succeed(
22
- HttpClient.HttpClient,
23
- HttpClient.make((request) =>
24
- Effect.sync(() => {
25
- requests.push(request)
26
- return HttpClientResponse.fromWeb(
27
- request,
28
- new Response(JSON.stringify(response.body), {
29
- status: response.status,
30
- headers: { "content-type": "application/json" }
31
- })
32
- )
33
- })
34
- )
35
- ))
36
- )
11
+ /**
12
+ * What the fake transport answers next, and what it was asked.
13
+ *
14
+ * Held at module scope rather than built per test so the client can be composed once, by
15
+ * `it.layer`, instead of provided inside each test body. Each test calls {@link reply} first, which
16
+ * both sets its response and clears the recorded requests, so no test can read another's.
17
+ */
18
+ let requests: Array<HttpClientRequest.HttpClientRequest> = []
19
+ let respond: (request: HttpClientRequest.HttpClientRequest, callIndex: number) => Response = () =>
20
+ new Response("null", { status: 200, headers: { "content-type": "application/json" } })
37
21
 
38
- describe("ClockifyApiClient", () => {
39
- it.effect("authenticates requests and decodes responses with Schema", () => {
40
- const requests: Array<HttpClientRequest.HttpClientRequest> = []
41
- return Effect.gen(function*() {
22
+ const json = <ResponseBody>(status: number, body: ResponseBody): Response =>
23
+ new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } })
24
+
25
+ /** Answer every request in this test the same way. */
26
+ const reply = <ResponseBody>(status: number, body: ResponseBody): void => {
27
+ requests = []
28
+ respond = () => json(status, body)
29
+ }
30
+
31
+ /** Answer request `n` of this test from a function of its index — for paging. */
32
+ const replyPerCall = <ResponseBody>(
33
+ answer: (callIndex: number) => { status: number; body: ResponseBody }
34
+ ): void => {
35
+ requests = []
36
+ respond = (_request, callIndex) => {
37
+ const { body, status } = answer(callIndex)
38
+ return json(status, body)
39
+ }
40
+ }
41
+
42
+ const suite = ClockifyApiClient.layer.pipe(
43
+ Layer.provide(Layer.succeed(ClockifyApiConfig, {
44
+ apiKey: Redacted.make("secret"),
45
+ workspaceId: "workspace-1",
46
+ userId: "user-1",
47
+ baseUrl: "https://clockify.test/api"
48
+ })),
49
+ Layer.provide(Layer.succeed(
50
+ HttpClient.HttpClient,
51
+ HttpClient.make((request) =>
52
+ Effect.sync(() => {
53
+ const callIndex = requests.length
54
+ requests.push(request)
55
+ return HttpClientResponse.fromWeb(request, respond(request, callIndex))
56
+ })
57
+ )
58
+ ))
59
+ )
60
+
61
+ it.layer(suite)("ClockifyApiClient", (it) => {
62
+ it.effect("authenticates requests and decodes responses with Schema", () =>
63
+ Effect.gen(function*() {
64
+ reply(200, { id: "user-1", name: "Ada", email: "ada@example.com", status: "ACTIVE" })
42
65
  const client = yield* ClockifyApiClient
43
66
  const user = yield* client.getUser()
44
67
  expect(user.id).toBe("user-1")
@@ -46,43 +69,38 @@ describe("ClockifyApiClient", () => {
46
69
  expect(requests).toHaveLength(1)
47
70
  expect(requests[0]?.url).toBe("https://clockify.test/api/v1/user")
48
71
  expect(requests[0]?.headers["x-api-key"]).toBe("secret")
49
- }).pipe(
50
- Effect.provide(clientLayer({
51
- status: 200,
52
- body: { id: "user-1", name: "Ada", email: "ada@example.com", status: "ACTIVE" }
53
- }, requests))
54
- )
55
- })
72
+ }))
56
73
 
57
- it.effect("decodes nullable interval fields for a running time entry", () => {
58
- const requests: Array<HttpClientRequest.HttpClientRequest> = []
59
- return Effect.gen(function*() {
74
+ it.effect("decodes nullable interval fields for a running time entry", () =>
75
+ Effect.gen(function*() {
76
+ reply(200, [{
77
+ id: "entry-1",
78
+ description: "In progress",
79
+ billable: false,
80
+ userId: "user-1",
81
+ workspaceId: "workspace-1",
82
+ timeInterval: { start: "2026-07-11T08:00:00Z", end: null, duration: null }
83
+ }])
60
84
  const client = yield* ClockifyApiClient
61
85
  const entry = yield* client.getRunningTimer("workspace-1", "user-1")
62
86
  expect(entry?.timeInterval?.end).toBeNull()
63
87
  expect(entry?.timeInterval?.duration).toBeNull()
64
- }).pipe(
65
- Effect.provide(clientLayer({
66
- status: 200,
67
- body: [{
68
- id: "entry-1",
69
- description: "In progress",
70
- billable: false,
71
- userId: "user-1",
72
- workspaceId: "workspace-1",
73
- timeInterval: {
74
- start: "2026-07-11T08:00:00Z",
75
- end: null,
76
- duration: null
77
- }
78
- }]
79
- }, requests))
80
- )
81
- })
88
+ }))
82
89
 
83
- it.effect("requests hydrated time-entry details when required by the caller", () => {
84
- const requests: Array<HttpClientRequest.HttpClientRequest> = []
85
- return Effect.gen(function*() {
90
+ it.effect("requests hydrated time-entry details when required by the caller", () =>
91
+ Effect.gen(function*() {
92
+ reply(200, {
93
+ id: "entry-1",
94
+ workspaceId: "workspace-1",
95
+ userId: "user-1",
96
+ description: "Review payment safeguards",
97
+ billable: true,
98
+ customFieldValues: [],
99
+ projectId: "project-1",
100
+ taskId: "task-1",
101
+ tagIds: ["delivery"],
102
+ timeInterval: { start: "2026-07-11T08:00:00Z", end: "2026-07-11T09:00:00Z", duration: "PT1H" }
103
+ })
86
104
  const client = yield* ClockifyApiClient
87
105
  const entry = yield* client.getTimeEntry("workspace-1", "entry-1", { hydrated: true })
88
106
  yield* client.getTimeEntry("workspace-1", "entry-1")
@@ -91,81 +109,33 @@ describe("ClockifyApiClient", () => {
91
109
  expect(new Map(requests[0]?.urlParams ?? []).get("hydrated")).toBe("true")
92
110
  expect(new Map(requests[1]?.urlParams ?? []).has("hydrated")).toBe(false)
93
111
  expect(new Map(requests[2]?.urlParams ?? []).get("hydrated")).toBe("false")
94
- }).pipe(
95
- Effect.provide(clientLayer({
96
- status: 200,
97
- body: {
98
- id: "entry-1",
99
- workspaceId: "workspace-1",
100
- userId: "user-1",
101
- description: "Review payment safeguards",
102
- billable: true,
103
- customFieldValues: [],
104
- projectId: "project-1",
105
- taskId: "task-1",
106
- tagIds: ["delivery"],
107
- timeInterval: {
108
- start: "2026-07-11T08:00:00Z",
109
- end: "2026-07-11T09:00:00Z",
110
- duration: "PT1H"
111
- }
112
- }
113
- }, requests))
114
- )
115
- })
112
+ }))
116
113
 
117
- it.effect("forwards hydration when listing canonical time-entry snapshots", () => {
118
- const requests: Array<HttpClientRequest.HttpClientRequest> = []
119
- return Effect.gen(function*() {
114
+ it.effect("forwards hydration when listing canonical time-entry snapshots", () =>
115
+ Effect.gen(function*() {
116
+ reply(200, [])
120
117
  const client = yield* ClockifyApiClient
121
- yield* client.getTimeEntries("workspace-1", "user-1", {
122
- hydrated: true,
123
- page: 2,
124
- pageSize: 10
125
- })
118
+ yield* client.getTimeEntries("workspace-1", "user-1", { hydrated: true, page: 2, pageSize: 10 })
126
119
  expect(new Map(requests[0]?.urlParams ?? []).get("hydrated")).toBe("true")
127
120
  expect(new Map(requests[0]?.urlParams ?? []).get("page")).toBe("2")
128
121
  expect(new Map(requests[0]?.urlParams ?? []).get("page-size")).toBe("10")
129
- }).pipe(
130
- Effect.provide(clientLayer({ status: 200, body: [] }, requests))
131
- )
132
- })
133
-
134
- it.effect("pages through the complete workspace user directory", () => {
135
- const requests: Array<HttpClientRequest.HttpClientRequest> = []
136
- const firstPage = Array.from({ length: 500 }, (_, index) => ({
137
- id: `user-${index}`,
138
- name: `User ${index}`,
139
- email: `user-${index}@example.test`,
140
- status: "ACTIVE"
141
122
  }))
142
- const layer = ClockifyApiClient.layer.pipe(
143
- Layer.provide(Layer.succeed(ClockifyApiConfig, {
144
- apiKey: Redacted.make("secret"),
145
- workspaceId: "workspace-1",
146
- userId: "user-1",
147
- baseUrl: "https://clockify.test/api"
148
- })),
149
- Layer.provide(Layer.succeed(
150
- HttpClient.HttpClient,
151
- HttpClient.make((request) =>
152
- Effect.sync(() => {
153
- requests.push(request)
154
- const body = requests.length <= 20
155
- ? firstPage
156
- : [{ id: "configured-user", name: "Configured User", email: "configured@example.test", status: "ACTIVE" }]
157
- return HttpClientResponse.fromWeb(
158
- request,
159
- new Response(JSON.stringify(body), {
160
- status: 200,
161
- headers: { "content-type": "application/json" }
162
- })
163
- )
164
- })
165
- )
166
- ))
167
- )
168
- return Effect.gen(function*() {
123
+
124
+ it.effect("pages through the complete workspace user directory", () =>
125
+ Effect.gen(function*() {
126
+ const firstPage = Array.from({ length: 500 }, (_, index) => ({
127
+ id: `user-${index}`,
128
+ name: `User ${index}`,
129
+ email: `user-${index}@example.test`,
130
+ status: "ACTIVE"
131
+ }))
132
+ // Twenty full pages, then a short one — the only thing that ends the walk.
133
+ replyPerCall((callIndex) => ({
134
+ status: 200,
135
+ body: callIndex < 20
136
+ ? firstPage
137
+ : [{ id: "configured-user", name: "Configured User", email: "configured@example.test", status: "ACTIVE" }]
138
+ }))
169
139
  const client = yield* ClockifyApiClient
170
140
  if (client.getWorkspaceUsers === undefined) return assert.fail("expected workspace user discovery")
171
141
  const users = yield* client.getWorkspaceUsers("workspace-1")
@@ -178,16 +148,26 @@ describe("ClockifyApiClient", () => {
178
148
  "ACTIVE,PENDING_EMAIL_VERIFICATION,DELETED,NOT_REGISTERED,LIMITED,LIMITED_DELETED"
179
149
  )
180
150
  expect(new Map(requests[0]?.urlParams ?? []).get("memberships")).toBe("ALL")
181
- }).pipe(Effect.provide(layer))
182
- })
151
+ }))
183
152
 
184
- it.effect("decodes a created time entry whose optional id references are null", () => {
185
- const requests: Array<HttpClientRequest.HttpClientRequest> = []
186
- return Effect.gen(function*() {
187
- const client = yield* ClockifyApiClient
153
+ it.effect("decodes a created time entry whose optional id references are null", () =>
154
+ Effect.gen(function*() {
188
155
  // Clockify returns kioskId/projectId/taskId as explicit null (not absent) when unset.
156
+ reply(201, {
157
+ id: "entry-1",
158
+ description: "[ENG-6169] review transaction mapper",
159
+ billable: true,
160
+ userId: "user-1",
161
+ workspaceId: "workspace-1",
162
+ kioskId: null,
163
+ projectId: null,
164
+ taskId: null,
165
+ tagIds: null,
166
+ timeInterval: { start: "2026-07-20T09:00:00Z", end: null, duration: null }
167
+ })
168
+ const client = yield* ClockifyApiClient
189
169
  const entry = yield* client.createTimeEntry("workspace-1", {
190
- description: "[RPS-6169] review transaction mapper",
170
+ description: "[ENG-6169] review transaction mapper",
191
171
  start: "2026-07-20T09:00:00Z"
192
172
  })
193
173
  expect(entry.id).toBe("entry-1")
@@ -195,69 +175,55 @@ describe("ClockifyApiClient", () => {
195
175
  expect(entry.projectId).toBeNull()
196
176
  expect(entry.taskId).toBeNull()
197
177
  expect(entry.tagIds).toBeNull()
198
- }).pipe(
199
- Effect.provide(clientLayer({
200
- status: 201,
201
- body: {
202
- id: "entry-1",
203
- description: "[RPS-6169] review transaction mapper",
204
- billable: true,
205
- userId: "user-1",
206
- workspaceId: "workspace-1",
207
- kioskId: null,
208
- projectId: null,
209
- taskId: null,
210
- tagIds: null,
211
- timeInterval: {
212
- start: "2026-07-20T09:00:00Z",
213
- end: null,
214
- duration: null
215
- }
216
- }
217
- }, requests))
218
- )
219
- })
178
+ }))
220
179
 
221
- it.effect("decodes a time-entries list whose entries have null tagIds", () => {
222
- const requests: Array<HttpClientRequest.HttpClientRequest> = []
223
- return Effect.gen(function*() {
224
- const client = yield* ClockifyApiClient
180
+ it.effect("decodes a time-entries list whose entries have null tagIds", () =>
181
+ Effect.gen(function*() {
225
182
  // `jcf sync reconcile` fetches time entries; Clockify returns tagIds as
226
183
  // null (not an empty array) for entries with no tags.
184
+ reply(200, [{
185
+ id: "entry-1",
186
+ description: "[ENG-6169] review transaction mapper",
187
+ billable: true,
188
+ userId: "user-1",
189
+ workspaceId: "workspace-1",
190
+ tagIds: null,
191
+ timeInterval: { start: "2026-07-20T09:00:00Z", end: "2026-07-20T11:00:00Z", duration: "PT2H" }
192
+ }])
193
+ const client = yield* ClockifyApiClient
227
194
  const entries = yield* client.getTimeEntries("workspace-1", "user-1")
228
195
  expect(entries).toHaveLength(1)
229
196
  expect(entries[0]?.tagIds).toBeNull()
230
- }).pipe(
231
- Effect.provide(clientLayer({
232
- status: 200,
233
- body: [{
234
- id: "entry-1",
235
- description: "[RPS-6169] review transaction mapper",
236
- billable: true,
237
- userId: "user-1",
238
- workspaceId: "workspace-1",
239
- tagIds: null,
240
- timeInterval: {
241
- start: "2026-07-20T09:00:00Z",
242
- end: "2026-07-20T11:00:00Z",
243
- duration: "PT2H"
244
- }
245
- }]
246
- }, requests))
247
- )
248
- })
197
+ }))
249
198
 
199
+ it.effect("fails when a successful response violates the generated schema", () =>
200
+ Effect.gen(function*() {
201
+ reply(200, { id: "user-1" })
202
+ const client = yield* ClockifyApiClient
203
+ const result = yield* Effect.result(client.getUser())
204
+ expect(result._tag).toBe("Failure")
205
+ }))
206
+
207
+ it.effect("fails on non-success status codes", () =>
208
+ Effect.gen(function*() {
209
+ reply(401, { message: "Unauthorized" })
210
+ const client = yield* ClockifyApiClient
211
+ const result = yield* Effect.result(client.getUser())
212
+ expect(result._tag).toBe("Failure")
213
+ }))
214
+ })
215
+
216
+ // Builds its own client rather than taking the suite's: the boundary under test is `make` itself,
217
+ // which is where the multipart request is constructed.
218
+ describe("ClockifyApiClient multipart", () => {
250
219
  it.effect("leaves multipart content type unset so the transport can add its boundary", () => {
251
- const requests: Array<HttpClientRequest.HttpClientRequest> = []
220
+ const seen: Array<HttpClientRequest.HttpClientRequest> = []
252
221
  const httpClient = HttpClient.make((request) =>
253
222
  Effect.sync(() => {
254
- requests.push(request)
223
+ seen.push(request)
255
224
  return HttpClientResponse.fromWeb(
256
225
  request,
257
- new Response(JSON.stringify({ name: "avatar.png", url: "https://clockify.test/avatar.png" }), {
258
- status: 200,
259
- headers: { "content-type": "application/json" }
260
- })
226
+ json(200, { name: "avatar.png", url: "https://clockify.test/avatar.png" })
261
227
  )
262
228
  })
263
229
  )
@@ -268,29 +234,14 @@ describe("ClockifyApiClient", () => {
268
234
 
269
235
  return Effect.gen(function*() {
270
236
  yield* client.uploadImage({ file: new Blob(["avatar bytes"], { type: "image/png" }) })
271
- expect(requests[0]?.headers["content-type"]).toBeUndefined()
272
- expect(requests[0]?.body._tag).toBe("FormData")
273
- if (requests[0]?.body._tag !== "FormData") throw new Error("Expected a FormData request body")
274
- const file = requests[0].body.formData.get("file")
275
- expect(requests[0].body.formData).toBeInstanceOf(FormData)
276
- expect(typeof file).not.toBe("string")
277
- if (file === null || typeof file === "string") throw new Error("Expected a file field")
237
+ expect(seen[0]?.headers["content-type"]).toBeUndefined()
238
+ expect(seen[0]?.body._tag).toBe("FormData")
239
+ if (seen[0]?.body._tag !== "FormData") throw new Error("Expected a FormData request body")
240
+ const file = seen[0].body.formData.get("file")
241
+ expect(seen[0].body.formData).toBeInstanceOf(FormData)
242
+ if (file === null || Predicate.isString(file)) throw new Error("Expected a file field")
278
243
  expect(yield* Effect.promise(() => file.text())).toBe("avatar bytes")
279
- expect(requests[0]?.headers["x-api-key"]).toBe("secret")
244
+ expect(seen[0]?.headers["x-api-key"]).toBe("secret")
280
245
  })
281
246
  })
282
-
283
- it.effect("fails when a successful response violates the generated schema", () =>
284
- Effect.gen(function*() {
285
- const client = yield* ClockifyApiClient
286
- const result = yield* Effect.result(client.getUser())
287
- expect(result._tag).toBe("Failure")
288
- }).pipe(Effect.provide(clientLayer({ status: 200, body: { id: "user-1" } }, []))))
289
-
290
- it.effect("fails on non-success status codes", () =>
291
- Effect.gen(function*() {
292
- const client = yield* ClockifyApiClient
293
- const result = yield* Effect.result(client.getUser())
294
- expect(result._tag).toBe("Failure")
295
- }).pipe(Effect.provide(clientLayer({ status: 401, body: { message: "Unauthorized" } }, []))))
296
247
  })
@@ -0,0 +1,18 @@
1
+ import { assert, describe, it } from "@effect/vitest"
2
+ import * as Schema from "effect/Schema"
3
+ import { ClockifyApi } from "../src/index.js"
4
+
5
+ // The spec declares custom-field values as objects, but Clockify sends and accepts scalars and arrays.
6
+ describe("custom field values", () => {
7
+ it("decode the scalar values Clockify returns", () => {
8
+ for (const value of ["new value", 42, true, ["a", "b"]]) {
9
+ const decoded = Schema.decodeUnknownSync(ClockifyApi.CustomFieldValueDtoV1)({ customFieldId: "cf", value })
10
+ assert.deepStrictEqual(decoded.value, value)
11
+ }
12
+ })
13
+
14
+ it("encode a scalar update value", () => {
15
+ const encoded = Schema.encodeSync(ClockifyApi.UpdateCustomFieldRequest)({ customFieldId: "cf", value: "new value" })
16
+ assert.strictEqual(encoded.value, "new value")
17
+ })
18
+ })
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from "@effect/vitest"
2
+ import * as Effect from "effect/Effect"
3
+ import * as Schema from "effect/Schema"
4
+ import { type FeaturePlan, WorkspaceDtoV1 } from "../src/generated/ClockifyApi.js"
5
+
6
+ describe("generated workspace schema", () => {
7
+ // Preserve both upstream spellings in responses without rewriting either value.
8
+ it.effect.each(["ADMINS", "ADMINS_AND_PROJECT_MANAGERS", "ANYONE", "EVERYONE"])(
9
+ "preserves scheduling assignment creator %s in workspace responses",
10
+ (creator) =>
11
+ Effect.gen(function*() {
12
+ const workspace = yield* Schema.decodeUnknownEffect(WorkspaceDtoV1)({
13
+ id: "workspace-1",
14
+ name: "Delivery",
15
+ workspaceSettings: {
16
+ schedulingSettings: { whoCanCreateAssignments: creator }
17
+ }
18
+ })
19
+ expect(workspace.workspaceSettings?.schedulingSettings?.whoCanCreateAssignments).toBe(creator)
20
+ })
21
+ )
22
+
23
+ it.effect("rejects undocumented scheduling assignment creators", () =>
24
+ Effect.gen(function*() {
25
+ const failure = yield* Schema.decodeUnknownEffect(WorkspaceDtoV1)({
26
+ id: "workspace-1",
27
+ name: "Delivery",
28
+ workspaceSettings: {
29
+ schedulingSettings: { whoCanCreateAssignments: "OWNER_ONLY" }
30
+ }
31
+ }).pipe(Effect.flip)
32
+ expect(failure._tag).toBe("SchemaError")
33
+ }))
34
+
35
+ it.effect("decodes feature plans after removing the upstream recursive oneOf", () =>
36
+ Effect.gen(function*() {
37
+ const plan: FeaturePlan = {
38
+ addonSubscriptionPlan: "PRO",
39
+ featurePermissions: ["AUDIT_LOG"],
40
+ paidPlan: true,
41
+ weight: 3
42
+ }
43
+ const workspace = yield* Schema.decodeUnknownEffect(WorkspaceDtoV1)({
44
+ id: "workspace-1",
45
+ name: "Delivery",
46
+ featureSubscriptionType: plan
47
+ })
48
+ expect(workspace.featureSubscriptionType).toEqual(plan)
49
+ }))
50
+
51
+ it.effect("rejects feature plans with a fractional weight", () =>
52
+ Effect.gen(function*() {
53
+ const failure = yield* Schema.decodeUnknownEffect(WorkspaceDtoV1)({
54
+ id: "workspace-1",
55
+ name: "Delivery",
56
+ featureSubscriptionType: { weight: 1.5 }
57
+ }).pipe(Effect.flip)
58
+ expect(failure._tag).toBe("SchemaError")
59
+ }))
60
+ })