fix: bound the activation marker in bytes and give a click one observer
The marker read added a whole chunk to a running total and compared the total afterwards, so a corrupt body could hand activation a 1 MiB chunk against a 257-byte ceiling. It now reads at most the remaining allowance — through a BYOB reader where the source offers one, and by refusing an oversized chunk before copying it otherwise. A declared oversize cancels the body it refuses instead of leaving the stream open, and the reader lock is released on every path. The build generator and the runtime decoder shared only the extension table, not the path grammar. The generator happily emitted `/assets/bad@name-abcdefgh.js`, which the decoder then refused — a correct build failing at install time. Both now use one exported canonical path predicate and the generator decodes its own output before returning it. The notification click handler emitted its terminal record from inside `process` and again from the `waitUntil` wrapper, so an ordinary click was counted twice. Worse, a late rejection downgraded `MAYBE_APPLIED` to `NOT_APPLIED` — telling operators the click had definitely not been applied when nobody knew that — and the late observation ran outside `waitUntil`, so a worker shutdown lost the evidence. There is one observation authority per click now, certainty is monotone, only an explicit null window confirms `NOT_APPLIED`, and the bounded tail is owned by `waitUntil` without extending the public deadline. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
39a4a973a8
commit
d7b35cfca3
@@ -621,3 +621,210 @@ describe("Web Push worker runtime", () => {
|
||||
expect(listeners.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* WP-01. One click is one terminal record. The adapter emitted the terminal
|
||||
* event from inside `process` *and* again from the `waitUntil` wrapper, so an
|
||||
* ordinary click was counted twice. A late rejection also downgraded the
|
||||
* certainty from `MAYBE_APPLIED` to `NOT_APPLIED`, telling operators the click
|
||||
* had definitely not been applied when nobody knew that, and the late
|
||||
* observation ran outside `waitUntil`, so a worker shutdown lost the evidence.
|
||||
*/
|
||||
describe("WP-01 the click handler has one observation authority", () => {
|
||||
type Observation = Readonly<{
|
||||
event: string;
|
||||
outcome: string;
|
||||
reason?: string;
|
||||
nativeEffect?: string;
|
||||
}>;
|
||||
|
||||
async function clickAdapterWith(
|
||||
clients: Readonly<{
|
||||
matchControlledWindowClients(): Promise<readonly unknown[]>;
|
||||
openWindow(target: string): Promise<unknown>;
|
||||
}>,
|
||||
scheduler?: TimeoutScheduler,
|
||||
) {
|
||||
const store = await activeFence();
|
||||
const observations: Observation[] = [];
|
||||
const adapter = createNotificationClickAdapter({
|
||||
fenceStore: store,
|
||||
registry,
|
||||
origin: "https://app.example.test",
|
||||
now: () => now,
|
||||
clients: clients as never,
|
||||
observer: {
|
||||
record(observation) {
|
||||
observations.push(observation as Observation);
|
||||
},
|
||||
},
|
||||
...(scheduler ? { scheduler } : {}),
|
||||
});
|
||||
return { adapter, observations };
|
||||
}
|
||||
|
||||
const dispatched = (observations: readonly Observation[]) =>
|
||||
observations.filter(
|
||||
(observation) => observation.event === "web_push_click_dispatched",
|
||||
);
|
||||
|
||||
it("records exactly one terminal event for an ordinary focus", async () => {
|
||||
const focus = vi.fn(async () => {});
|
||||
const { adapter, observations } = await clickAdapterWith({
|
||||
async matchControlledWindowClients() {
|
||||
return [
|
||||
{
|
||||
url: "https://app.example.test/current",
|
||||
focus,
|
||||
postMessage() {},
|
||||
},
|
||||
];
|
||||
},
|
||||
openWindow: vi.fn(async () => null),
|
||||
});
|
||||
let waited: Promise<void> | null = null;
|
||||
const result = await adapter.handle({
|
||||
notification: { data: clickData(), close() {} },
|
||||
waitUntil(task) {
|
||||
waited = task;
|
||||
},
|
||||
});
|
||||
await waited;
|
||||
|
||||
expect(result).toEqual({ ok: true, value: undefined });
|
||||
expect(dispatched(observations)).toEqual([
|
||||
{
|
||||
event: "web_push_click_dispatched",
|
||||
outcome: "SUCCEEDED",
|
||||
nativeEffect: "CONFIRMED",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("confirms NOT_APPLIED only for an explicit null window", async () => {
|
||||
const { adapter, observations } = await clickAdapterWith({
|
||||
async matchControlledWindowClients() {
|
||||
return [];
|
||||
},
|
||||
openWindow: vi.fn(async () => null),
|
||||
});
|
||||
let waited: Promise<void> | null = null;
|
||||
const result = await adapter.handle({
|
||||
notification: { data: clickData(), close() {} },
|
||||
waitUntil(task) {
|
||||
waited = task;
|
||||
},
|
||||
});
|
||||
await waited;
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(dispatched(observations)).toEqual([
|
||||
expect.objectContaining({
|
||||
outcome: "FAILED",
|
||||
nativeEffect: "NOT_APPLIED",
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps MAYBE_APPLIED when the native effect rejects after the deadline", async () => {
|
||||
const clock = manualScheduler();
|
||||
let rejectFocus: ((reason: unknown) => void) | undefined;
|
||||
const { adapter, observations } = await clickAdapterWith(
|
||||
{
|
||||
async matchControlledWindowClients() {
|
||||
return [
|
||||
{
|
||||
url: "https://app.example.test/current",
|
||||
focus: () =>
|
||||
new Promise<void>((_resolve, reject) => {
|
||||
rejectFocus = reject;
|
||||
}),
|
||||
postMessage() {},
|
||||
},
|
||||
];
|
||||
},
|
||||
openWindow: vi.fn(async () => null),
|
||||
},
|
||||
clock.scheduler,
|
||||
);
|
||||
let waited: Promise<void> | null = null;
|
||||
const handling = adapter.handle({
|
||||
notification: { data: clickData(), close() {} },
|
||||
waitUntil(task) {
|
||||
waited = task;
|
||||
},
|
||||
});
|
||||
await vi.waitFor(() => expect(rejectFocus).toBeDefined());
|
||||
clock.expireAll();
|
||||
const result = await handling;
|
||||
expect(result.ok).toBe(false);
|
||||
|
||||
// The effect lands only now, after the terminal result.
|
||||
rejectFocus?.(new Error("focus failed late"));
|
||||
await waited;
|
||||
|
||||
const records = dispatched(observations);
|
||||
expect(records).toHaveLength(2);
|
||||
// The evidence record never claims the click was definitely not applied.
|
||||
expect(records.at(-1)).toEqual({
|
||||
event: "web_push_click_dispatched",
|
||||
outcome: "DEGRADED",
|
||||
reason: "ABORTED",
|
||||
nativeEffect: "MAYBE_APPLIED",
|
||||
});
|
||||
});
|
||||
|
||||
it("waits for the late effect evidence inside waitUntil", async () => {
|
||||
const clock = manualScheduler();
|
||||
let resolveFocus: (() => void) | undefined;
|
||||
const { adapter, observations } = await clickAdapterWith(
|
||||
{
|
||||
async matchControlledWindowClients() {
|
||||
return [
|
||||
{
|
||||
url: "https://app.example.test/current",
|
||||
focus: () =>
|
||||
new Promise<void>((resolve) => {
|
||||
resolveFocus = resolve;
|
||||
}),
|
||||
postMessage() {},
|
||||
},
|
||||
];
|
||||
},
|
||||
openWindow: vi.fn(async () => null),
|
||||
},
|
||||
clock.scheduler,
|
||||
);
|
||||
let waited: Promise<void> | null = null;
|
||||
const handling = adapter.handle({
|
||||
notification: { data: clickData(), close() {} },
|
||||
waitUntil(task) {
|
||||
waited = task;
|
||||
},
|
||||
});
|
||||
await vi.waitFor(() => expect(resolveFocus).toBeDefined());
|
||||
clock.expireAll();
|
||||
await handling;
|
||||
|
||||
let waitUntilSettled = false;
|
||||
const pendingWait = waited as Promise<void> | null;
|
||||
void pendingWait?.then(() => {
|
||||
waitUntilSettled = true;
|
||||
});
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
// The handler's lifetime is still open because the effect has not landed.
|
||||
expect(waitUntilSettled).toBe(false);
|
||||
|
||||
resolveFocus?.();
|
||||
await waited;
|
||||
expect(
|
||||
dispatched(observations).at(-1),
|
||||
).toEqual({
|
||||
event: "web_push_click_dispatched",
|
||||
outcome: "DEGRADED",
|
||||
reason: "ABORTED",
|
||||
nativeEffect: "CONFIRMED",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user