Skip to content

Commit 0255a11

Browse files
NiKrauseoz-agent
andauthored
test: move server-channel integration to server and fix client typechecks (#410)
## Summary - move server-as-channel integration tests from `packages/client/test/client.spec.js` to `packages/server/test/client-channel.spec.js` - keep client tests package-local (no `@ucanto/server` / `@ucanto/validator` imports) - add local client execute/decode tests to restore `packages/client` 100% coverage gates - run package workflows on pushes to all branches (`push.branches: ["**"]`) for `client`, `core`, `interface`, `principal`, `server`, `transport`, and `validator` ## Why The previous client test changes mixed server integration concerns into the client package, which caused fragile TypeScript behavior in isolated package checks. This PR keeps boundaries clear: - server package owns server-channel integration behavior - client package keeps self-contained tests and coverage ## Why checks failed after merge previously On PR #407, package CI typecheck jobs did not run on the PR head commit (only `Validate PR title` was recorded). On merge commit `60c70da`, push workflows ran package typechecks and surfaced failures. So the issue was check coverage/enforcement at PR time (branch protection/ruleset/settings), not new source changes introduced between PR creation and merge. ## Remark This PR now also removes `main`-only `push.branches` constraints for package workflows by switching them to `"**"`. Combined with `pull_request` checks and required status checks on `main`, this reduces merge-time surprises by surfacing failures earlier on feature-branch pushes. --- Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 60c70da commit 0255a11

File tree

9 files changed

+273
-168
lines changed

9 files changed

+273
-168
lines changed

.github/workflows/client.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,6 @@ on:
1111
- 'packages/transport/**'
1212
- 'packages/client/**'
1313
pull_request:
14-
branches:
15-
- main
1614
paths:
1715
- 'packages/interface/**'
1816
- 'packages/core/**'

.github/workflows/core.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ on:
99
- 'packages/core/**'
1010
- 'packages/interface/**'
1111
pull_request:
12-
branches:
13-
- main
1412
paths:
1513
- 'packages/core/**'
1614
- 'packages/interface/**'

.github/workflows/interface.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@ on:
88
paths:
99
- 'packages/interface/**'
1010
pull_request:
11-
branches:
12-
- main
1311
paths:
1412
- 'packages/interface/**'
1513
- '.github/workflows/interface.yml'

.github/workflows/principal.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@ on:
99
- 'packages/interface/**'
1010
- 'packages/principal/**'
1111
pull_request:
12-
branches:
13-
- main
1412
paths:
1513
- 'packages/interface/**'
1614
- 'packages/principal/**'

.github/workflows/server.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ on:
1313
- 'packages/validator/**'
1414
- 'packages/server/**'
1515
pull_request:
16-
branches:
17-
- main
1816
paths:
1917
- 'packages/interface/**'
2018
- 'packages/core/**'

.github/workflows/transport.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ on:
1010
- 'packages/interface/**'
1111
- 'packages/transport/**'
1212
pull_request:
13-
branches:
14-
- main
1513
paths:
1614
- 'packages/core/**'
1715
- 'packages/interface/**'

.github/workflows/validator.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ on:
1212
- 'packages/client/**'
1313
- 'packages/validator/**'
1414
pull_request:
15-
branches:
16-
- main
1715
paths:
1816
- 'packages/interface/**'
1917
- 'packages/core/**'

packages/client/test/client.spec.js

Lines changed: 51 additions & 154 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import * as Service from './service.js'
66
import { Receipt, Message, CBOR } from '@ucanto/core'
77
import { alice, bob, mallory, service as w3 } from './fixtures.js'
88
import fetch from '@web-std/fetch'
9-
import * as Server from '@ucanto/server'
10-
import { Schema } from '@ucanto/validator'
119

1210
test('encode invocation', async () => {
1311
/** @type {Client.ConnectionView<Service.Service>} */
@@ -142,90 +140,7 @@ test('encode delegated invocation', async () => {
142140
}
143141
})
144142

145-
// Create the service instance
146-
const service = Service.create()
147-
148-
// Define capabilities
149-
const storeAddCapability = Server.capability({
150-
can: 'store/add',
151-
with: Server.URI.match({ protocol: 'did:' }),
152-
nb: Schema.struct({
153-
link: Server.Link.match().optional(),
154-
}),
155-
derives: (claimed, delegated) => {
156-
if (claimed.with !== delegated.with) {
157-
return Server.fail(
158-
`Expected 'with: "${delegated.with}"' instead got '${claimed.with}'`
159-
)
160-
} else if (
161-
delegated.nb.link &&
162-
`${delegated.nb.link}` !== `${claimed.nb.link}`
163-
) {
164-
return Server.fail(
165-
`Link ${
166-
claimed.nb.link == null ? '' : `${claimed.nb.link} `
167-
}violates imposed ${delegated.nb.link} constraint`
168-
)
169-
} else {
170-
return Server.ok({})
171-
}
172-
},
173-
})
174-
175-
const storeRemoveCapability = Server.capability({
176-
can: 'store/remove',
177-
with: Server.URI.match({ protocol: 'did:' }),
178-
nb: Schema.struct({
179-
link: Server.Link.match().optional(),
180-
}),
181-
derives: (claimed, delegated) => {
182-
if (claimed.with !== delegated.with) {
183-
return Server.fail(
184-
`Expected 'with: "${delegated.with}"' instead got '${claimed.with}'`
185-
)
186-
} else if (
187-
delegated.nb.link &&
188-
`${delegated.nb.link}` !== `${claimed.nb.link}`
189-
) {
190-
return Server.fail(
191-
`Link ${
192-
claimed.nb.link == null ? '' : `${claimed.nb.link} `
193-
}violates imposed ${delegated.nb.link} constraint`
194-
)
195-
} else {
196-
return Server.ok({})
197-
}
198-
},
199-
})
200-
201-
// Create server with service handlers using Server.provide
202-
const server = Server.create({
203-
id: w3,
204-
service: {
205-
store: {
206-
add: Server.provide(storeAddCapability, async ({ capability, invocation }) => {
207-
// Call the existing service method with the invocation
208-
return await service.store.add(/** @type {Client.Invocation<any>} */ (invocation))
209-
}),
210-
remove: Server.provide(storeRemoveCapability, async ({ capability, invocation }) => {
211-
// Call the existing service method with the invocation
212-
return await service.store.remove(/** @type {Client.Invocation<any>} */ (invocation))
213-
}),
214-
},
215-
},
216-
codec: CAR.inbound,
217-
validateAuthorization: () => ({ ok: {} }),
218-
})
219-
220-
// Use server directly as channel (no HTTP, no mock fetch!)
221-
/** @type {Client.ConnectionView<Service.Service>} */
222-
const connection = Client.connect({
223-
id: w3,
224-
channel: server, // 🎯 Server directly as channel - validates delegation chains!
225-
codec: CAR.outbound,
226-
})
227-
228-
test('execute', async () => {
143+
test('execute invocation', async () => {
229144
const car = await CAR.codec.write({
230145
roots: [await CBOR.write({ hello: 'world ' })],
231146
})
@@ -241,34 +156,38 @@ test('execute', async () => {
241156
proofs: [],
242157
})
243158

244-
const remove = Client.invoke({
245-
issuer: alice,
246-
audience: w3,
247-
capability: {
248-
can: 'store/remove',
249-
with: alice.did(),
250-
nb: { link: car.cid },
159+
const channel = {
160+
request: async (/** @type {any} */ input) => {
161+
const { invocations } = await CAR.request.decode(input)
162+
const receipts = await Promise.all(
163+
invocations.map(invocation =>
164+
Receipt.issue({
165+
ran: invocation.cid,
166+
issuer: w3,
167+
result: {
168+
ok: {
169+
with: invocation.capabilities[0].with,
170+
link: car.cid,
171+
status: 'upload',
172+
url: 'http://localhost:9090/',
173+
},
174+
},
175+
})
176+
)
177+
)
178+
const message = await Message.build({
179+
receipts: /** @type {any} */ (receipts),
180+
})
181+
return CAR.response.encode(message)
251182
},
252-
})
253-
254-
const e1 = await add.execute(connection)
183+
}
255184

256-
assert.deepEqual(e1.out, {
257-
error: {
258-
// @ts-expect-error
259-
name: 'UnknownDIDError',
260-
message: `DID ${alice.did()} has no account`,
261-
did: alice.did(),
262-
},
185+
const connection = Client.connect({
186+
id: w3,
187+
channel: /** @type {any} */ (channel),
188+
codec: CAR.outbound,
263189
})
264190

265-
// fake register alice
266-
service.access.accounts.register(
267-
alice.did(),
268-
'did:email:alice@web.mail',
269-
car.cid
270-
)
271-
272191
const [r1] = await connection.execute(add)
273192
assert.deepEqual(r1.out, {
274193
ok: {
@@ -280,51 +199,44 @@ test('execute', async () => {
280199
})
281200
})
282201

283-
test('execute with delegations', async () => {
202+
test('decode error', async () => {
284203
const car = await CAR.codec.write({
285204
roots: [await CBOR.write({ hello: 'world ' })],
286205
})
287206

288207
const add = Client.invoke({
289-
issuer: bob,
208+
issuer: alice,
290209
audience: w3,
291210
capability: {
292211
can: 'store/add',
293-
with: bob.did(),
212+
with: alice.did(),
294213
nb: { link: car.cid },
295214
},
296215
proofs: [],
297216
})
298217

299-
const [e1] = await connection.execute(await add.delegate())
300-
301-
assert.deepEqual(e1.out, {
302-
error: {
303-
// @ts-expect-error
304-
name: 'UnknownDIDError',
305-
message: `DID ${bob.did()} has no account`,
306-
did: bob.did(),
307-
},
308-
})
309-
310-
// fake register alice
311-
service.access.accounts.register(bob.did(), 'did:email:bob@web.mail', car.cid)
312-
313-
const [r1] = await connection.execute(await add.delegate())
314-
assert.deepEqual(r1.out, {
315-
ok: {
316-
with: bob.did(),
317-
link: car.cid,
318-
status: 'upload',
319-
url: 'http://localhost:9090/',
218+
const channel = {
219+
request: async (/** @type {any} */ input) => {
220+
const { invocations } = await CAR.request.decode(input)
221+
const receipts = await Promise.all(
222+
invocations.map(invocation =>
223+
Receipt.issue({
224+
ran: invocation.cid,
225+
issuer: w3,
226+
result: { ok: {} },
227+
})
228+
)
229+
)
230+
const message = await Message.build({
231+
receipts: /** @type {any} */ (receipts),
232+
})
233+
return CAR.response.encode(message)
320234
},
321-
})
322-
})
235+
}
323236

324-
test('decode error', async () => {
325237
const client = Client.connect({
326238
id: w3,
327-
channel: server,
239+
channel: /** @type {any} */ (channel),
328240
codec: Codec.outbound({
329241
encoders: {
330242
'application/car': CAR.request,
@@ -335,22 +247,7 @@ test('decode error', async () => {
335247
}),
336248
})
337249

338-
const car = await CAR.codec.write({
339-
roots: [await CBOR.write({ hello: 'world ' })],
340-
})
341-
342-
const add = Client.invoke({
343-
issuer: alice,
344-
audience: w3,
345-
capability: {
346-
can: 'store/add',
347-
with: alice.did(),
348-
nb: { link: car.cid },
349-
},
350-
proofs: [],
351-
})
352-
353-
const [e1] = await client.execute(await add.delegate())
250+
const [e1] = await client.execute(add)
354251

355252
assert.deepEqual(
356253
{

0 commit comments

Comments
 (0)