Skip to content

Commit b2da20c

Browse files
committed
test: list the consumer half of the auxiliary HTTP boundaries (#1432)
Addresses the remaining review P1 on #1717. The manifest claimed both sides of response/upload/artifact framing while listing nothing from upload-client.ts, daemon-artifacts.ts, or the health consumer in daemon-client-transport.ts, so those parsers could narrow without moving a listed digest or protocol 2. Now listed (117 -> 141 declarations): - /health consumer: RemoteDaemonHealth, readHealthPayload, readDaemonHttpHealth, readRemoteDaemonHealth. This is the sharpest of the three — narrowing the reader or the comparison disables the very refusal ADR 0006 exists to guarantee, and nothing else in the repo would notice. - /upload consumer: UploadResponse, UploadPreflightResponse, UploadPreflightResult, parseUploadPreflightResult, requestUploadPreflight, uploadDirectArtifact, tryDirectUploadWithResume, shouldRetryDirectUpload, finalizeDirectUpload, uploadLegacyArtifact, ARTIFACT_HASH_ALGORITHM, isStringRecord, and PreparedUploadArtifact — whose sha256/sizeBytes/fileName/artifactType/ contentType fields ARE the preflight body the daemon parses. - /artifacts/* consumer: DaemonArtifactEndpoint, buildDaemonArtifactUrl, isRemoteDaemon, DownloadRemoteArtifactParams, downloadRemoteArtifact, materializeRemoteArtifacts, resolveMaterializedArtifactPath. Running the closure fail-closed over the new files surfaced three more stops, each decided rather than skipped: PreparedUploadArtifact listed (it is payload), UploadProgressSink waived (client-local rendering, never leaves the process), and src/daemon/types.ts#DaemonArtifact waived as a re-export alias of the listed kernel type, matching its DaemonRequest/DaemonResponse siblings. 10 more planted-red mutations cover the new seams: health version-read and mismatch-refusal defeated, RemoteDaemonHealth field dropped, preflight parser narrowed, preflight/legacy response shapes narrowed, finalize body key renamed, ticket field renamed, artifact tenant header dropped, artifact URL moved. A fourth closure probe proves the upload-consumer files are genuinely reached by the walk rather than merely listed. 22 -> 33 tests. The README now states the coverage as a producer/consumer table per boundary, so the claim is checkable at a glance instead of asserted in prose.
1 parent 1b401d5 commit b2da20c

5 files changed

Lines changed: 172 additions & 9 deletions

File tree

‎test/wire-compat/README.md‎

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,12 @@ place version skew is designed in was the one place with no gate.
3333

3434
The mutation lane exists because the first two answer "the manifest describes
3535
today's source", never "the manifest describes the *right* source". A gate can
36-
list 117 declarations, pass every check, and still miss the seam that breaks a
36+
list 141 declarations, pass every check, and still miss the seam that breaks a
3737
skewed peer — which is exactly what review found in the first version of this
3838
directory. Each case there mutates one real listed declaration the way a real
3939
break would (method renamed, envelope unframed, auth header dropped, upload
40-
ticket field renamed, 308 downgraded, artifact route narrowed) and asserts the
40+
ticket field renamed, 308 downgraded, artifact route narrowed, health-version
41+
check defeated) and asserts the
4142
digest moves. Two guards keep them honest: the mutation is applied **inside the
4243
declaration's own span** so it cannot silently hit a sibling that shares the
4344
substring, and the unmutated digest must equal the ledger's so the case is
@@ -88,12 +89,20 @@ bullet is only partly digestible the group carries an `uncovered` note saying wh
8889
reviewer-owned and why — a gate that implies coverage it does not provide is worse than one that
8990
admits the gap (AGENTS.md, "a registry claim is not a semantic check").
9091

91-
**Both sides of every boundary are listed.** The first version of this directory digested only the
92-
payload *types* while quoting all four bullets, which review correctly called an overclaim: method
93-
sets, response serialization, auth projection, upload ticket and 308 framing, artifact framing, and
94-
the client's own parsers could all break a skewed peer without moving a listed digest. A
95-
client-only change breaks an older daemon just as surely as the reverse, so producer and consumer
96-
seams are both here.
92+
**Both sides of every boundary are listed** — four boundaries, producer and consumer each:
93+
94+
| Boundary | Producer | Consumer |
95+
| --- | --- | --- |
96+
| `/rpc` | method sets, request projections, `createRpcError`, envelope serializers | `buildHttpRpcPayload`, lease-method mapping, `parseDaemonHttpResponseBody`, `toDaemonHttpRpcError` |
97+
| `/health` | `DaemonHealthPayload`, `buildDaemonHealthPayload` | `RemoteDaemonHealth`, `readHealthPayload`, `readRemoteDaemonHealth` |
98+
| `/upload` | route resolver, preflight/finalize/308 handlers, body parsers | `UploadPreflightResponse`, `parseUploadPreflightResult`, direct/legacy/finalize senders, `PreparedUploadArtifact` |
99+
| `/artifacts/*` | route resolver, inventory and download framing | `buildDaemonArtifactUrl`, `downloadRemoteArtifact`, materialization |
100+
101+
A client-only change breaks an older daemon just as surely as the reverse — and the `/health`
102+
consumer is the sharpest case of all, since narrowing `readHealthPayload` or `readRemoteDaemonHealth`
103+
disables the very refusal ADR 0006 was written to guarantee. Both took two review rounds to get
104+
right: the first version digested only payload *types*, the second still omitted the auxiliary
105+
consumers while claiming both sides.
97106

98107
The one remaining gap: `createDaemonHttpServer`, the 200-line dispatcher, and the `/health` and
99108
`/rpc` path literals inside it. Everything it dispatches *with* is digested individually, so what is

‎test/wire-compat/closure-policy.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ export const WIRE_CLOSURE_WAIVERS: Readonly<Record<string, string>> = {
4747
'Re-export alias of the listed kernel DaemonRequest; the declaration that fixes the shape is packages/kernel/src/contracts.ts#DaemonRequest.',
4848
'src/daemon/types.ts#DaemonResponse':
4949
'Re-export alias of the listed kernel DaemonResponse; the shape is fixed by packages/kernel/src/contracts.ts#DaemonResponse.',
50+
'src/daemon/types.ts#DaemonArtifact':
51+
'Re-export alias of the listed kernel DaemonArtifact; the shape is fixed by packages/kernel/src/contracts.ts#DaemonArtifact.',
52+
'src/remote/upload-progress.ts#UploadProgressSink':
53+
'A callback for client-local upload progress rendering. Its events never leave the client, unlike the daemon progress stream, whose envelope and events are listed.',
5054
'packages/contracts/src/request-progress.ts#RequestProgressSink':
5155
'A callback type (event) => void. The event it receives is RequestProgressEvent, which is listed; the sink itself never crosses the wire.',
5256

‎test/wire-compat/ledger.json‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@
4747
"src/daemon/client/daemon-client-rpc.ts#parseDaemonHttpResponseBody": "sha256:0cbe39dbd09e321ec00854a1626a7d89b6344e59c6faabe2fe02504268b2d886",
4848
"src/daemon/client/daemon-client-rpc.ts#resolveDaemonHttpResult": "sha256:296f9d376ce67c8cb20209bdf1c59c2423ffcfa35b5565a99e70271263149048",
4949
"src/daemon/client/daemon-client-rpc.ts#toDaemonHttpRpcError": "sha256:32c27a34c2c72c74367bf865159ee2f0c1235589584408ac7c7d2de6d077c4c0",
50+
"src/daemon/client/daemon-client-transport.ts#RemoteDaemonHealth": "sha256:38fe712390d247de59a74b9dc209fdf407ff5f9b1e32b698ae71ebc57569b66d",
51+
"src/daemon/client/daemon-client-transport.ts#readDaemonHttpHealth": "sha256:5e75af39e96045ce7faba986c900744cee76cdaa1e0df360ea38f3c020520711",
52+
"src/daemon/client/daemon-client-transport.ts#readHealthPayload": "sha256:5b15e14319b16aebf32d07336986a7f1f2a6b13cec1823ca9aa7c9f01bf1b0b6",
53+
"src/daemon/client/daemon-client-transport.ts#readRemoteDaemonHealth": "sha256:b22111f693ecb65705195a66bce617e58441483ebfd67cf264d6900ba1dcb401",
5054
"src/daemon/downloadable-artifact-http.ts#DownloadableArtifactHttpAuthorizer": "sha256:1b2702a929ca9170db2ca97c08e3ab67e17edb3ee75325a576c4c1b9cdbebb44",
5155
"src/daemon/downloadable-artifact-http.ts#DownloadableArtifactHttpRoute": "sha256:e63c4581ccde668913914149c9092d16ecf8a5cbd7ab33c6eb8617e77fc1015e",
5256
"src/daemon/downloadable-artifact-http.ts#handleArtifactDownload": "sha256:2fe9778960869a80d99517c9bc6a18093b96ac4c9d7346e185dd1a7fecf06201",
@@ -117,7 +121,27 @@
117121
"src/daemon/upload-http.ts#resolveHttpRequestBaseUrl": "sha256:0d06968c7577c421573281bc574485dc964e98f484afd1dc022fbba24d1face3",
118122
"src/daemon/upload-http.ts#resolveUploadHttpRoute": "sha256:8837e62933cf57111a8070454877c5859aed65b4dcdba755f145346e30f82835",
119123
"src/daemon/upload-http.ts#sendJson": "sha256:c2317b4b14a10731684c9160038005965856150f05f8923765a3fa833d2ecae9",
120-
"src/daemon/upload-http.ts#sendUploadedArtifactResponse": "sha256:9e418403448d58a96284fed605762fa71f9349bf2c70ad79df69b2b3e27487d6"
124+
"src/daemon/upload-http.ts#sendUploadedArtifactResponse": "sha256:9e418403448d58a96284fed605762fa71f9349bf2c70ad79df69b2b3e27487d6",
125+
"src/remote/daemon-artifacts.ts#DaemonArtifactEndpoint": "sha256:45226717ac52c0af2a62f74e91549582d95f4a4abc49be87989e026291a88c82",
126+
"src/remote/daemon-artifacts.ts#DownloadRemoteArtifactParams": "sha256:24f3e577471874ae9147373d6ea0a9108e5a21a39742d2e3931ff1a6a323cd8e",
127+
"src/remote/daemon-artifacts.ts#buildDaemonArtifactUrl": "sha256:06dcde057809b507f70f7bb5cdef9bde6515be84b67e45865cf19e70b8636208",
128+
"src/remote/daemon-artifacts.ts#downloadRemoteArtifact": "sha256:3db8ad2b4660f813583aa9b87e1c3927adbc3ca1e84cdd0d181d82fdc85d747a",
129+
"src/remote/daemon-artifacts.ts#isRemoteDaemon": "sha256:0f6a714c5f60ce4e8548d811b516d8b6241eca9f7e5414733732da395b8a00e7",
130+
"src/remote/daemon-artifacts.ts#materializeRemoteArtifacts": "sha256:1dcab98fba2674f87053ae888c2235b6a6780405aa70648dc57eb6b9077a02fd",
131+
"src/remote/daemon-artifacts.ts#resolveMaterializedArtifactPath": "sha256:db5c44effc41ff9fa40bd094945e56ac60fac89a42fa68c65c9293e6de6fb1dc",
132+
"src/remote/upload-client-artifact.ts#PreparedUploadArtifact": "sha256:5c40027d47feda3c946e836372b9f08ad866ab1612708b0d506551545f931ea4",
133+
"src/remote/upload-client.ts#ARTIFACT_HASH_ALGORITHM": "sha256:328c135ea5f9d89fd2c142b440d5402a61d19f9044bf0504dfc2c525e58c1909",
134+
"src/remote/upload-client.ts#UploadPreflightResponse": "sha256:196a1d9ef41a1aec192589fa7ccb30cf71d971a005349d5d73eed42e6ab21327",
135+
"src/remote/upload-client.ts#UploadPreflightResult": "sha256:bd8fc6d617c0630325455fd64d0fe319daaced198ee6745e4673a8ace3df460d",
136+
"src/remote/upload-client.ts#UploadResponse": "sha256:499e01466b5e0e03413f8d3f1986f5af6bc120704c50a3747d84de422b3f0314",
137+
"src/remote/upload-client.ts#finalizeDirectUpload": "sha256:5e32c87214f9f6140535b48d05de8565cb121370fdc334c05f7f0ad476b73a07",
138+
"src/remote/upload-client.ts#isStringRecord": "sha256:b6d6b571b90baf0976d9c347c4a45a4363a4df5ad184ea1087cb3b9febd4b624",
139+
"src/remote/upload-client.ts#parseUploadPreflightResult": "sha256:7ce3f43b45c03df8b7f2690432beedc7a537c1bba540961716862f8a632d5623",
140+
"src/remote/upload-client.ts#requestUploadPreflight": "sha256:df42c0eaef3a54f4388c99d74bf6030885239780c2ddc78117151dbd391afedd",
141+
"src/remote/upload-client.ts#shouldRetryDirectUpload": "sha256:29059f076ff7f2b9540ad15408be48792b2d63a601ef2b9fb2d54f5576d49bbd",
142+
"src/remote/upload-client.ts#tryDirectUploadWithResume": "sha256:0f57cf9bb1eefa3b988e5d723b2eebf2c4a622c3f7c808f35eae070771acafa6",
143+
"src/remote/upload-client.ts#uploadDirectArtifact": "sha256:5f26f8f7b6ada2fe2089a214fb8350e3a9f65725212952c17f6787b3ca8fe232",
144+
"src/remote/upload-client.ts#uploadLegacyArtifact": "sha256:f3ccab9c5ba18fc2f2000ea8e2a4bbb3cc97f7b50f6111192e513013f635cdd7"
121145
},
122146
"compatibleChanges": []
123147
}

‎test/wire-compat/surface.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ const ARTIFACT_HTTP = 'src/daemon/downloadable-artifact-http.ts';
5151
const PROGRESS_PROTOCOL = 'src/daemon/request-progress-protocol.ts';
5252
const CLIENT_RPC = 'src/daemon/client/daemon-client-rpc.ts';
5353
const CLIENT_PROGRESS = 'src/daemon/client/daemon-client-progress.ts';
54+
const CLIENT_TRANSPORT = 'src/daemon/client/daemon-client-transport.ts';
55+
const UPLOAD_CLIENT = 'src/remote/upload-client.ts';
56+
const REMOTE_ARTIFACTS = 'src/remote/daemon-artifacts.ts';
5457

5558
function from(file: string, ...names: string[]): WireDeclarationRef[] {
5659
return names.map((name) => ({ file, name }));
@@ -89,6 +92,16 @@ export const WIRE_SURFACE: readonly WireSurfaceGroup[] = [
8992
'readArtifactId',
9093
'readRequestPathname',
9194
),
95+
// Consumer side of /health: the client reads this payload and refuses a
96+
// mismatched peer from it, so a narrowed reader defeats the very check
97+
// ADR 0006 built. `readRemoteDaemonHealth` is where the comparison lives.
98+
...from(
99+
CLIENT_TRANSPORT,
100+
'RemoteDaemonHealth',
101+
'readHealthPayload',
102+
'readDaemonHttpHealth',
103+
'readRemoteDaemonHealth',
104+
),
92105
],
93106
// What is left is `createDaemonHttpServer`, a 200+ line dispatcher whose
94107
// body churns for reasons that are not protocol changes. Everything it
@@ -269,6 +282,40 @@ export const WIRE_SURFACE: readonly WireSurfaceGroup[] = [
269282
'shouldReadDaemonProgressStream',
270283
'createInvalidDaemonResponseError',
271284
),
285+
// Consumer side of the auxiliary /upload boundary: the shapes the client
286+
// expects back from preflight, direct/resumable, legacy and finalize, and
287+
// the parser that decides whether a daemon's preflight is usable at all.
288+
...from(
289+
UPLOAD_CLIENT,
290+
'ARTIFACT_HASH_ALGORITHM',
291+
'UploadResponse',
292+
'UploadPreflightResponse',
293+
'UploadPreflightResult',
294+
'parseUploadPreflightResult',
295+
'isStringRecord',
296+
'requestUploadPreflight',
297+
'uploadDirectArtifact',
298+
'tryDirectUploadWithResume',
299+
'shouldRetryDirectUpload',
300+
'finalizeDirectUpload',
301+
'uploadLegacyArtifact',
302+
),
303+
// The prepared artifact whose fields (sha256, sizeBytes, fileName,
304+
// artifactType, contentType) ARE the preflight body the daemon parses.
305+
...from('src/remote/upload-client-artifact.ts', 'PreparedUploadArtifact'),
306+
// Consumer side of /artifacts/*: URL construction, the inventory/header/
307+
// body materialization the client performs, and the artifact fields it
308+
// rewrites on the way through.
309+
...from(
310+
REMOTE_ARTIFACTS,
311+
'DaemonArtifactEndpoint',
312+
'buildDaemonArtifactUrl',
313+
'isRemoteDaemon',
314+
'DownloadRemoteArtifactParams',
315+
'downloadRemoteArtifact',
316+
'materializeRemoteArtifacts',
317+
'resolveMaterializedArtifactPath',
318+
),
272319
],
273320
},
274321
];

‎test/wire-compat/wire-mutations.test.ts‎

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,79 @@ const MUTATIONS: readonly WireMutation[] = [
140140
from: "type: 'progress';",
141141
to: "kind: 'progress';",
142142
},
143+
// Consumer half of the auxiliary boundaries. A client-only narrowing here
144+
// rejects a released daemon's response with no server change at all, which is
145+
// why claiming "both sides" required these to be listed and proved.
146+
{
147+
breakClass: 'health consumer: the client stops reading the advertised protocol version',
148+
file: 'src/daemon/client/daemon-client-transport.ts',
149+
name: 'readHealthPayload',
150+
from: "typeof parsed.rpcProtocolVersion === 'number' ? parsed.rpcProtocolVersion : undefined",
151+
to: 'undefined',
152+
},
153+
{
154+
breakClass: 'health consumer: the mismatch refusal ADR 0006 built is weakened',
155+
file: 'src/daemon/client/daemon-client-transport.ts',
156+
name: 'readRemoteDaemonHealth',
157+
from: 'health.rpcProtocolVersion !== DAEMON_RPC_PROTOCOL_VERSION',
158+
to: 'false',
159+
},
160+
{
161+
breakClass: 'health consumer: the parsed health shape drops a released field',
162+
file: 'src/daemon/client/daemon-client-transport.ts',
163+
name: 'RemoteDaemonHealth',
164+
from: 'rpcProtocolVersion?: number;',
165+
to: '',
166+
},
167+
{
168+
breakClass: 'upload consumer: the preflight parser stops accepting a released response',
169+
file: 'src/remote/upload-client.ts',
170+
name: 'parseUploadPreflightResult',
171+
from: "preflight.ok !== true || typeof preflight.uploadId !== 'string'",
172+
to: 'preflight.ok !== true',
173+
},
174+
{
175+
breakClass: 'upload consumer: the preflight response shape narrows',
176+
file: 'src/remote/upload-client.ts',
177+
name: 'UploadPreflightResponse',
178+
from: 'cacheHit?: boolean;',
179+
to: '',
180+
},
181+
{
182+
breakClass: 'upload consumer: the legacy/finalize response shape narrows',
183+
file: 'src/remote/upload-client.ts',
184+
name: 'UploadResponse',
185+
from: 'uploadId: string;',
186+
to: 'id: string;',
187+
},
188+
{
189+
breakClass: 'upload consumer: the finalize request body key is renamed',
190+
file: 'src/remote/upload-client.ts',
191+
name: 'finalizeDirectUpload',
192+
from: 'JSON.stringify({ uploadId: options.uploadId })',
193+
to: 'JSON.stringify({ id: options.uploadId })',
194+
},
195+
{
196+
breakClass: 'upload consumer: the preflight ticket fields the daemon parses are renamed',
197+
file: 'src/remote/upload-client-artifact.ts',
198+
name: 'PreparedUploadArtifact',
199+
from: 'sha256: string;',
200+
to: 'checksum: string;',
201+
},
202+
{
203+
breakClass: 'artifact consumer: the download request drops its tenant header',
204+
file: 'src/remote/daemon-artifacts.ts',
205+
name: 'downloadRemoteArtifact',
206+
from: '...buildDaemonHttpTenantHeaders(params.requestScope?.tenantId),',
207+
to: '',
208+
},
209+
{
210+
breakClass: 'artifact consumer: the artifact URL the client requests moves',
211+
file: 'src/remote/daemon-artifacts.ts',
212+
name: 'buildDaemonArtifactUrl',
213+
from: 'artifacts/',
214+
to: 'artifact/',
215+
},
143216
];
144217

145218
for (const mutation of MUTATIONS) {
@@ -196,6 +269,12 @@ const CLOSURE_PROBES: readonly { reachedFrom: string; omit: string }[] = [
196269
reachedFrom: 'DaemonResponse',
197270
omit: 'packages/kernel/src/errors.ts#DaemonError',
198271
},
272+
{
273+
// The consumer half of the auxiliary upload boundary, added after review:
274+
// proves those files are genuinely wired into the walk, not just listed.
275+
reachedFrom: 'requestUploadPreflight',
276+
omit: 'src/remote/upload-client-artifact.ts#PreparedUploadArtifact',
277+
},
199278
];
200279

201280
for (const probe of CLOSURE_PROBES) {

0 commit comments

Comments
 (0)