fix: hold transfer inputs and raw transfer work to what was verified
The capability vault checked an issuer's registration and then read it again to store it, including its nested header rows. A stateful issuer could show an allowed header set to the forbidden-header check and hand `Authorization` to the copy, so the vault stored — and the executor sent — a credential no rule had ever seen. The registration and everything nested in it is now snapshotted once, and only that snapshot is validated, frozen and stored. The upload control plane had the same shape one level down: a `sessionId` that answered `session_01` to the regex and `../../unsafe` to the result snapshot reached a success receipt. Two lifetimes were also unowned. A download source lease that resolved after the caller's abort never reached the holder, so nothing closed it and its fetch reader and capability lease outlived the terminal result; a compensator sharing the holder's close-once latch now closes it exactly once. And `dispose()` proved quiescence from the wrapper registry alone, so a provider that ignored its attempt deadline let teardown report a drained runtime and close the checkpoint store while the provider was still running. Raw provider promises are now their own registry and the drain must prove both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
aa8ac35600
commit
39a4a973a8
@@ -1343,3 +1343,114 @@ describe("TR-RR-06 bounded resumable teardown", () => {
|
||||
expect(checkpoints.closed).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* TR-04. `dispose()` proved quiescence from the wrapper registry only. A
|
||||
* provider that ignored its attempt deadline let the wrapper settle first and
|
||||
* leave the set empty, so teardown reported a drained runtime — and closed the
|
||||
* checkpoint store — while the provider was still running.
|
||||
*/
|
||||
describe("TR-04 teardown waits for raw provider work, not only its wrapper", () => {
|
||||
it("refuses to report a drained runtime while a provider is still running", async () => {
|
||||
const harness = createControlHarness();
|
||||
const checkpoints = new MemoryCheckpointStore();
|
||||
const closeStore = vi.spyOn(checkpoints, "close");
|
||||
let releaseProvider: (() => void) | undefined;
|
||||
// Ignores the attempt signal entirely and outlives its own deadline.
|
||||
harness.control.createSession = () =>
|
||||
new Promise((resolve) => {
|
||||
releaseProvider = () =>
|
||||
resolve(
|
||||
browserDataFailure("UNAVAILABLE", "UPLOAD_SESSION", {
|
||||
retryable: true,
|
||||
recovery: "RESUME",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const runtime = createResumableUploadRuntime({
|
||||
controlPlane: harness.control,
|
||||
partExecutor: executorFor(harness),
|
||||
checkpoints,
|
||||
mutationLock: noContentionLock,
|
||||
crypto,
|
||||
policy: runtimePolicy({
|
||||
providerAttemptTimeoutMs: 5,
|
||||
cleanupDeadlineMs: 25,
|
||||
maxRetries: 0,
|
||||
}),
|
||||
now: () => 1_000,
|
||||
random: () => 0,
|
||||
sleep: async () => {},
|
||||
});
|
||||
|
||||
const uploading = runtime.upload({
|
||||
uploadKey: "upload_key_raw",
|
||||
purpose: "attachment",
|
||||
mediaType: "application/octet-stream",
|
||||
source: byteStreamSource(new Uint8Array([1, 2, 3, 4, 5])),
|
||||
signal: activeSignal,
|
||||
});
|
||||
await vi.waitFor(() => expect(releaseProvider).toBeDefined());
|
||||
// The wrapper has already given up on the attempt.
|
||||
await uploading;
|
||||
|
||||
const disposed = await runtime.dispose();
|
||||
expect(disposed.ok).toBe(false);
|
||||
if (!disposed.ok) {
|
||||
expect(disposed.error.code).toBe("UNAVAILABLE");
|
||||
expect(disposed.error.recovery).toBe("RESUME");
|
||||
}
|
||||
// The store stays open while something could still write a checkpoint.
|
||||
expect(closeStore).not.toHaveBeenCalled();
|
||||
|
||||
releaseProvider?.();
|
||||
});
|
||||
|
||||
it("reports a drained runtime once the raw provider settles", async () => {
|
||||
const harness = createControlHarness();
|
||||
const checkpoints = new MemoryCheckpointStore();
|
||||
let releaseProvider: (() => void) | undefined;
|
||||
harness.control.createSession = () =>
|
||||
new Promise((resolve) => {
|
||||
releaseProvider = () =>
|
||||
resolve(
|
||||
browserDataFailure("UNAVAILABLE", "UPLOAD_SESSION", {
|
||||
retryable: true,
|
||||
recovery: "RESUME",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
const runtime = createResumableUploadRuntime({
|
||||
controlPlane: harness.control,
|
||||
partExecutor: executorFor(harness),
|
||||
checkpoints,
|
||||
mutationLock: noContentionLock,
|
||||
crypto,
|
||||
policy: runtimePolicy({
|
||||
providerAttemptTimeoutMs: 5,
|
||||
cleanupDeadlineMs: 1_000,
|
||||
maxRetries: 0,
|
||||
}),
|
||||
now: () => 1_000,
|
||||
random: () => 0,
|
||||
sleep: async () => {},
|
||||
});
|
||||
|
||||
const uploading = runtime.upload({
|
||||
uploadKey: "upload_key_raw_2",
|
||||
purpose: "attachment",
|
||||
mediaType: "application/octet-stream",
|
||||
source: byteStreamSource(new Uint8Array([1, 2, 3, 4, 5])),
|
||||
signal: activeSignal,
|
||||
});
|
||||
await vi.waitFor(() => expect(releaseProvider).toBeDefined());
|
||||
await uploading;
|
||||
|
||||
const disposing = runtime.dispose();
|
||||
releaseProvider?.();
|
||||
await expect(disposing).resolves.toMatchObject({ ok: true });
|
||||
expect(runtime.lifecycle()).toBe("CLOSED");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user