File
Blob: src/workerd/api/tests/warnings-tail.js
| 1 | // Copyright (c) 2026 Cloudflare, Inc. |
| 2 | // Licensed under the Apache 2.0 license found in the LICENSE file or at: |
| 3 | // https://opensource.org/licenses/Apache-2.0 |
| 4 | import * as assert from 'node:assert'; |
| 5 | |
| 6 | // ================================================================================================ |
| 7 | // Expected warnings registry. |
| 8 | // |
| 9 | // Add new entries here when introducing logWarning() calls. Each entry is a substring that must |
| 10 | // appear in at least one warn-level tail event's message. The trigger worker (warnings-test.js) |
| 11 | // must exercise a code path that produces each warning. |
| 12 | // |
| 13 | // NOTE: Some logWarning() calls go through jsg::Lock::logWarning() instead of |
| 14 | // IoContext::logWarning(). The jsg path does not currently emit to the tracer, so those warnings |
| 15 | // are not observable via tail workers and cannot be tested here (e.g. .text() on non-text body). |
| 16 | const EXPECTED_WARNINGS = [ |
| 17 | // From Body::Body (http.c++) โ FormData body with a custom Content-Type header. |
| 18 | 'FormData body was provided', |
| 19 | // From Response constructor (http.c++) โ null-body status with zero-length body. |
| 20 | 'Constructing a Response with a null body status', |
| 21 | ]; |
| 22 | // ================================================================================================ |
| 23 | |
| 24 | let allEvents = []; |
| 25 | |
| 26 | export default { |
| 27 | tailStream(event) { |
| 28 | allEvents.push(event.event); |
| 29 | return (event) => { |
| 30 | allEvents.push(event.event); |
| 31 | }; |
| 32 | }, |
| 33 | }; |
| 34 | |
| 35 | export const test = { |
| 36 | async test() { |
| 37 | // HACK: Tail events from the trigger worker's fetch invocation are delivered asynchronously |
| 38 | // across service boundaries. There is no JS-level synchronization primitive to await their |
| 39 | // arrival โ a plain `await promise` doesn't drive the I/O loop that delivers them. We must |
| 40 | // use scheduler.wait() to yield while the runtime processes pending I/O. |
| 41 | const TIMEOUT_MS = 5000; |
| 42 | const POLL_MS = 10; |
| 43 | const deadline = Date.now() + TIMEOUT_MS; |
| 44 | |
| 45 | const unseen = () => |
| 46 | EXPECTED_WARNINGS.filter( |
| 47 | (substring) => |
| 48 | !allEvents.some( |
| 49 | (e) => |
| 50 | e.type === 'log' && |
| 51 | e.level === 'warn' && |
| 52 | e.message?.[0]?.includes(substring) |
| 53 | ) |
| 54 | ); |
| 55 | |
| 56 | while (unseen().length > 0 && Date.now() < deadline) { |
| 57 | await scheduler.wait(POLL_MS); |
| 58 | } |
| 59 | |
| 60 | const missing = unseen(); |
| 61 | assert.strictEqual( |
| 62 | missing.length, |
| 63 | 0, |
| 64 | `${missing.length} expected warning(s) were not observed after ${TIMEOUT_MS}ms:\n` + |
| 65 | missing.map((s) => ` - "${s}"`).join('\n') |
| 66 | ); |
| 67 | }, |
| 68 | }; |