Skip to content

fs: treat negative/non-number position in readv/writev as current offset - #32292

Closed
robobun wants to merge 4 commits into
mainfrom
farm/dc6d4893/fs-readv-writev-negative-position
Closed

robobun wants to merge 4 commits into
mainfrom
farm/dc6d4893/fs-readv-writev-negative-position

Conversation

@robobun

@robobun robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator

Reproduction

const fs = require("fs");
const fd = fs.openSync("/tmp/fsvec.txt", "w+");
fs.writevSync(fd, [Buffer.from("A")], -5);   // bun: EINVAL, preadv   node: writes 1 byte
fs.writevSync(fd, [Buffer.from("B")], "3");  // bun: TypeError ERR_INVALID_ARG_TYPE   node: writes 1 byte
fs.closeSync(fd);
console.log(fs.readFileSync("/tmp/fsvec.txt", "utf8"));  // bun: ""   node: "AB"

Same divergence on readvSync, callback fs.readv/fs.writev, and FileHandle.readv/FileHandle.writev.

Cause

FdVectorIo::from_js (the shared position parser for every fs.readv/fs.writev entry point in src/runtime/node/node_fs.rs) diverged from Node on two inputs:

  • A negative number was cast to u64 and kept as Some(..), routing to preadv(2)/pwritev(2) with a negative offset, which the kernel rejects with EINVAL. libuv's uv__fs_read/uv__fs_write treat any offset < 0 as plain non-positional readv/writev.
  • A non-number value threw ERR_INVALID_ARG_TYPE. Node's JS layer (lib/fs.js writev/writevSync/readv/readvSync, and internal/fs/promises.js FileHandle#writev/readv) does if (typeof position !== 'number') position = null; and never rejects.

Either idiom meant Node code that uses position: -1 ("current offset", a documented libuv convention) or passes a stray non-number silently wrote nothing on Bun.

Fix

Only a non-negative number now selects preadv/pwritev. Every other shape (undefined, null, negative number, string, object, boolean, bigint) leaves position = None so dispatch picks readv/writev at the current file offset, matching Node and libuv. This removes the only error return after buffers is constructed, so the buffers.release() cleanup on that path is dead and removed along with it. The sibling args::Read/args::Write parsers already gate on position >= 0 the same way.

Verification

New describe.each block in test/js/node/fs/fs.test.ts runs writevSync / readvSync / callback fs.writev / callback fs.readv / FileHandle.writev / FileHandle.readv against each of -1, -5, "3", {}, true, 3n, asserting the read/write happens at the current file offset and advances it (36 cases). A guard test confirms a non-negative number position is still positional. All 36 matrix cases fail on the unfixed build (EINVAL for negatives, ERR_INVALID_ARG_TYPE for non-numbers) and pass with this change.

The fs.writev keeps buffers attached while the write is in flight test previously asserted that a string position throws; it now asserts the call succeeds and the buffer pin is released after completion, matching Node.

Existing readv/writev/preadv/pwritev tests and the Node parallel test-fs-readv* / test-fs-writev* suites pass unchanged.


no test proof · iteration 6 · Platform-specific test(s) that do not run on this machine. Deferring to CI, which covers all platforms: test/js/node/fs/fs.test.ts

@coderabbitai

coderabbitai Bot commented Jun 15, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 34021b5e-3a33-41bb-93a1-7b1237d6e93d

📥 Commits

Reviewing files that changed from the base of the PR and between e0acad3 and 60b4ecf.

📒 Files selected for processing (2)
  • src/runtime/node/node_fs.rs
  • test/js/node/fs/fs.test.ts

Walkthrough

FdVectorIo::from_js is updated to parse the position argument as i64 and only assign it when the value is non-negative, preventing negative offsets from wrapping into large u64 values. Regression tests are added for readvSync, writevSync, fs.readv, and fs.writev covering negative-position semantics.

Changes

Negative position fix for scatter/gather I/O

Layer / File(s) Summary
FdVectorIo position parsing fix
src/runtime/node/node_fs.rs
FdVectorIo::from_js now parses pos_value as i64 and only sets position when pos >= 0, leaving it unset for negative inputs instead of wrapping via as u64. Inline comments document the negative-offset semantics.
Negative position regression tests
test/js/node/fs/fs.test.ts
Adds sync readvSync and writevSync tests that verify a negative position reads/writes at the current file offset and advances it across successive calls. Adds matching async fs.readv and fs.writev callback tests using Promise.withResolvers() to verify byte counts and buffer contents.
🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main change: fixing how negative position values in readv/writev operations are treated as the current file position rather than being rejected.
Description check ✅ Passed The description thoroughly covers what the PR does (with a reproduction case), the root cause, the fix, and verification steps, addressing both template sections well.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@robobun

robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 1:43 PM PT - Jul 16th, 2026

❌ @autofix-ci[bot], your commit 31e8a57 has 1 failures in Build #74036 (All Failures):


🧪   To try this PR locally:

bunx bun-pr 32292

That installs a local version of the PR into your bun-32292 executable, so you can run:

bun-32292 --bun

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — small, well-tested fix that mirrors the existing position_int >= 0 gate in args::Read/args::Write.

Extended reasoning...

Overview

This PR fixes fs.readv/fs.writev (sync and async) to treat a negative position argument as "use the current file position" instead of passing a wrapped u64::MAX to preadv/pwritev and failing with EINVAL. The change is a single logic edit in FdVectorIo::from_js in src/runtime/node/node_fs.rs — replacing pos_value.to_int64() as u64 with a sign check that only sets position = Some(..) when the value is non-negative — plus four new regression tests in test/js/node/fs/fs.test.ts.

Security risks

None. This is argument parsing for vectored file I/O on an already-open fd. The change narrows behavior (negative → current position, matching Node/libuv) rather than expanding any capability, and there's no auth, crypto, or untrusted-input surface involved.

Level of scrutiny

Low. The effective code change is a few lines and is a direct copy of the established pattern already used for args::Read/args::Write in the same file (the position_int >= 0 gate at ~line 4062). Negative positions now leave position as None, so dispatch correctly selects readv/writev instead of preadv/pwritev. Non-negative positions are unchanged.

Other factors

The four new tests cover readvSync, writevSync, fs.readv, and fs.writev with position: -1, asserting both byte counts and resulting buffer/file contents, and additionally verify the file cursor advances across successive calls. No CODEOWNERS apply to the touched files, the bug-hunting system found no issues, and there are no outstanding reviewer comments.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — straightforward Node-compat fix mirroring the existing position_int >= 0 gate in args::Read/args::Write.

Extended reasoning...

Overview

This PR changes one branch in FdVectorIo::from_js (src/runtime/node/node_fs.rs), the shared argument parser for fs.readv/fs.writev and their sync variants. Previously a negative position was cast to_int64() as u64, wrapping to a huge value and routing to preadv/pwritev, which then failed with EINVAL. The fix only sets position = Some(..) when the parsed int is non-negative, leaving it None otherwise so dispatch picks the non-positional readv/writev path. Four new tests in test/js/node/fs/fs.test.ts cover readvSync, writevSync, fs.readv, and fs.writev with position: -1.

Security risks

None. This is pure argument-parsing for a file descriptor the caller already holds; the change narrows behavior from "error" to "use current file position", matching Node/libuv semantics. No new syscall surface, no auth/crypto/permissions involved.

Level of scrutiny

Low. The functional change is ~4 lines and is a direct copy of the pattern already used at line ~4062 in the same file for args::Read/args::Write (if position_int >= 0 { Some(..) } else { None }). The PR description accurately traces the root cause and the fix is the obvious one.

Other factors

  • No CODEOWNERS cover the touched files.
  • Bug hunting system found no issues.
  • Tests are well-constructed: they advance the fd cursor with readSync(..., null) first, then assert that -1 reads/writes from that cursor and (for readv) that a second call continues advancing — verifying both that EINVAL is gone and that the semantics match Node.
  • No outstanding human review comments on the PR.

@robobun

robobun commented Jun 15, 2026 •

Copy link
Copy Markdown
Collaborator Author

CI status: the diff is green on every lane except one unrelated ASAN crash.

Build #74036 (current, at 31e8a57): the only failure is test/js/web/timers/timer-heap-race.test.ts SIGABRT on debian-13 x64-asan, a Worker/VM-teardown race in JSC WaiterListManager::unregister that is already being tracked separately. All other test lanes passed, including fs.test.ts with the full 66-case position matrix. Ready for review.

FdVectorIo::from_js (the shared args parser for fs.readv/writev,
readvSync/writevSync, and FileHandle.readv/writev) diverged from Node
on two position inputs:

  - A negative number was passed through as-is and wrapped to a huge
    u64, routing to preadv(2)/pwritev(2) with a negative offset and
    failing with EINVAL. libuv treats any offset < 0 as non-positional
    readv/writev (i.e. current file offset).
  - A non-number value threw ERR_INVALID_ARG_TYPE. Node's JS layer
    coerces typeof position !== 'number' to null (current offset) and
    never rejects.

Either input meant the write silently never happened on Bun for code
ported from Node.

Only a non-negative number now selects preadv/pwritev; every other
shape (undefined/null/negative/string/object/boolean/bigint) leaves
position = None so dispatch picks readv/writev at the current offset,
matching Node and libuv. This removes the only error return after
buffers are constructed, so the buffers.release() cleanup on that path
is dead and removed along with it.

Tests cover writevSync / readvSync / callback writev / callback readv /
FileHandle.writev / FileHandle.readv across -1, -5, "3", {}, true and
3n, plus a guard that a non-negative number position is still
positional. The fs.writev buffer-pinning test is updated: a non-number
position no longer throws, so it now asserts the call succeeds and the
pin is released on completion.
@robobun
robobun force-pushed the farm/dc6d4893/fs-readv-writev-negative-position branch from 60b4ecf to 67acef5 Compare July 16, 2026 18:52
@robobun robobun changed the title fs: treat negative position in readv/writev as current file position fs: treat negative/non-number position in readv/writev as current offset Jul 16, 2026
Comment thread test/js/node/fs/fs.test.ts
Comment thread src/runtime/node/node_fs.rs
robobun and others added 2 commits July 16, 2026 19:14
Node's native GetOffset() returns -1 (non-positional) unless
IsSafeJsInt(value) holds: finite, integral, and within
Number.MAX_SAFE_INTEGER. The previous is_number()/to_int64() path let
NaN (-> 0), +Infinity (-> i64::MAX), fractional values, and out-of-range
doubles through as positional offsets. Gate on the same three conditions
Node does.

Also restore the per-write disk-content assertion in the writev buffer-pin
test: the second (non-positional) write lands at offset 0 and overwrites
the first write's bytes, so check the file contents between the two
writes.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — both earlier nits are addressed (the CCCCCCCC disk read now sits between the two writevs, and the position gate now mirrors Node's IsSafeJsInt via get_number() + finite/integral/≤MAX_SAFE_INTEGER, with NaN/±Infinity/3.7/MAX_SAFE_INTEGER+1 added to the matrix).

Extended reasoning...

Overview

The PR rewrites the position parser in FdVectorIo::from_js (src/runtime/node/node_fs.rs), which is shared by every fs.readv/fs.writev entry point (sync, callback, and FileHandle). Previously a negative number was cast to u64 and sent to preadv/pwritev (kernel EINVAL), and any non-number threw ERR_INVALID_ARG_TYPE. Now only a value for which get_number() returns a non-negative, integral f64 ≤ MAX_SAFE_INTEGER selects positional I/O; every other shape leaves position = None and dispatches non-positional readv/writev. This mirrors Node's lib/fs.js (typeof position !== 'number' → null) plus native GetOffset()'s IsSafeJsInt gate. The now-unreachable buffers.release() error path and the mut on buffers are removed. Tests add a 66-case describe.each matrix over 11 position values × 6 entry points, plus a positive control that a non-negative integer is still positional, and update the "keeps buffers attached" test to reflect that a string position now succeeds.

Security risks

None. This loosens argument validation on a local file-descriptor operation to match Node — inputs that previously threw or EINVAL'd now fall back to non-positional I/O on an fd the caller already opened. get_number() is a pure tag check + bit extraction (no JS coercion, no getters/Proxy traps), so there's no re-entrancy between buffers construction and the return. No untrusted-length arithmetic, no new allocation, no path handling.

Level of scrutiny

Medium-low. The native change is ~10 lines in a single function, replaces a lossy as u64 cast with an explicit range gate, and removes a dead error path. NaN >= 0.0 is false, Infinity <= MAX_SAFE_INTEGER is false, and 3.7.trunc() != 3.7, so all the newly-added matrix values correctly fall through to None. The sibling args::Read/args::Write parsers already gate on >= 0 the same way, so this brings FdVectorIo in line with existing precedent in the file rather than introducing a novel pattern.

Other factors

Both nits from my prior review were addressed in e64468d: (1) the readFileSync == "CCCCCCCC" assertion now sits between the two writevs so the pwritev-path disk bytes are still verified before the non-positional write overwrites offset 0, and (2) the is_number()+to_int64() gate that let NaN→0 and +Infinity→i64::MAX slip through was replaced with the IsSafeJsInt-equivalent check, with those inputs added to the test matrix. Verifier agents separately examined whether the modified "keeps buffers attached" test's final DDDDDDDD assertion would fail on Windows (where libuv positional writes advance the fd offset) and ruled it out. The bug-hunting system found nothing on the current revision.

@robobun

robobun commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator Author

The fs.write path has the same NaN-position bug (coerced to offset 0), which is what makes fs.createWriteStream silently corrupt the head of a file on a short kernel write. #36135 fixes the whole class (write/writev/readv + the writeAll retry loop) and includes this change's readv/writev cases.

Jarred-Sumner pushed a commit that referenced this pull request Aug 11, 2026
…ition coerced to 0) (#36135)

## Reproduction

```sh
$ cat repro.mjs
import fs from "node:fs"; import os from "node:os"; import { spawnSync } from "node:child_process";
const MB = 1 << 20;
if (process.argv[2] !== "child") {
  const r = spawnSync("sh", ["-c", `ulimit -f 2048; exec "${process.execPath}" "${process.argv[1]}" child`], { stdio: "inherit" });
  process.exit(r.status ?? 1);
}
const p = `${os.tmpdir()}/pw-${process.pid}.bin`;
const big = Buffer.concat([..."ABCD"].map(c => Buffer.alloc(MB, c)));
const s = fs.createWriteStream(p), ev = [];
s.on("error", e => ev.push("error:" + e.code)).on("finish", () => ev.push("finish"));
s.on("close", () => {
  const b = fs.readFileSync(p);
  console.log(JSON.stringify({ ev, bytesWritten: s.bytesWritten, size: b.length, head: b.subarray(0, 8).toString() }));
});
s.write(big); s.end();

$ node repro.mjs
{"ev":["error:EFBIG"],"bytesWritten":1048576,"size":1048576,"head":"AAAAAAAA"}

$ bun repro.mjs    # before
{"ev":["finish"],"bytesWritten":4194304,"size":1048576,"head":"DDDDDDDD"}
```

Any short write from the kernel (disk full / `ENOSPC`, quota / `EDQUOT`,
`RLIMIT_FSIZE`) triggers this: the unwritten tail is re-written at
offset 0, the head of the file is destroyed, and the stream emits
`'finish'` with `bytesWritten` reporting the full size.

The primitive without `createWriteStream`:

```js
const fd = fs.openSync(p, "w");
fs.writeSync(fd, "AAAAAAAAAA");
fs.writeSync(fd, Buffer.from("XX"), 0, 2, NaN);
// node: "AAAAAAAAAAXX"   bun (before): "XXAAAAAAAA"
```

## Cause

`writeAll` in `src/js/internal/fs/streams.ts` retries a short write with
`pos += bytesWritten`. `pos` starts as `undefined` (no `start` option)
so the retry passes `NaN`. Node's `writeAll` does the same; it works
there because Node's native `GetOffset`
([src/node_file.cc](https://github.com/nodejs/node/blob/main/src/node_file.cc))
returns `-1` (current file offset) for any value that isn't a safe JS
integer.

Bun's `fs.write` argument parser instead ran `i52::from_js(NaN)`, which
is `(to_int64(NaN) << 12) >> 12 = 0`, and set `args.position = Some(0)`.
strace confirms: `write(4 MiB) = 1048576`, then `pwrite64(3 MiB, 0)`,
`pwrite64(2 MiB, 0)`, `pwrite64(1 MiB, 0)`; the remaining size reaches
zero so the loop reports success. `-Infinity`, fractional values, and
numbers past `MAX_SAFE_INTEGER` mis-coerced the same way, and the
`fs.readv`/`fs.writev` `position` parser had the same defect (plus it
threw on non-number values Node accepts).

## Fix

`i52::offset_from_js` now implements Node's `GetOffset`: a `position`
selects `pwrite`/`pwritev`/`preadv` only when
`Number.isSafeInteger(position)`. Every other shape (NaN, ±Infinity,
fractional, out-of-range, non-number) leaves `position = None` so
dispatch picks the non-positional syscall at the current offset. The
`fs.write` buffer and string overloads and the shared `readv`/`writev`
parser all go through it. This includes BigInt, which Bun previously
honored as a positional write on the buffer overload; the `writeSync
works with bigint` test now asserts the Node behavior (current offset).

`writeAll` also now keeps `pos` undefined across retries, so a custom
`options.fs.write` observes the same argument Node passes.

## Verification

New `describe.each` in `test/js/node/fs/fs.test.ts` runs `writeSync`
(buffer and string), async `fs.write`, `writevSync`, `readvSync`,
`FileHandle.write`, and `FileHandle.writev` against each of `NaN`,
`±Infinity`, `1.5`, `MAX_SAFE_INTEGER + 1`, and `5n` and asserts the I/O
happens at the current offset; a second block covers non-number
`writevSync` positions. Two `createWriteStream` tests cover the headline
bug: one simulates a short write via `options.fs.write` and asserts the
retry position stays `undefined` and the file reads `AAAABBBBCCCCDDDD`;
one (Linux) runs the `ulimit -f` repro and asserts `{ev:
["error:EFBIG"], bytesWritten: 1048576, head: "AAAAAAAA"}`.

40 of the 49 new cases fail on the unfixed build (the others are guard
cases or +Infinity which already happened to truncate to `-1`). All pass
with this change, along with the existing `writeSync`/`writev`/`readv`
suites and Node's `test-fs-writev*`/`test-fs-readv*` parallel tests.

Supersedes #32292 (the `readv`/`writev` half of this same class).

<!-- robobun:evidence:begin -->

---

**no test proof** · iteration 3 · Platform-specific test(s) that do not
run on this machine. Deferring to CI, which covers all platforms:
test/js/node/fs/fs.test.ts

<!-- robobun:evidence:end -->

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Jarred-Sumner pushed a commit that referenced this pull request Aug 22, 2026
### Problem
- GitHub closes only the first reference after a keyword, so "Fixes #1,
#2" leaves #2 open. "Supersedes #3" links nothing, and no reference
closes a pull request.
- The last 1000 merged PRs name 274 such references. PR #32292 is open
although merged #36135 says "Supersedes #32292".

### Fix
- `.github/workflows/close-linked-issues.yml` runs on
`pull_request_target` `closed` (a merge into the default branch of
`oven-sh/bun`) and on `workflow_dispatch` with a PR number and
`dry_run`. Everything is inline in one `actions/github-script` step,
with no checkout.
- Each open target is closed as `completed` with the comment "Closed as
completed by #N." or "Superseded by #N.". Closed or missing targets, the
PR itself and other repositories are skipped.
- The parser has no regex. A closing keyword (close, fix, resolve,
supersede, replace, any tense) must lead the reference, alone or in a
list. A negated, hedged or noun keyword, or one whose subject is another
reference, does not count ("may fix", "the rm fix #1", "#100 supersedes
#1").
- Verified: `test/internal/close-linked-issues.test.ts` (333 cases) runs
the YAML's script against fake `github`, `context` and `core`. Also the
1000-PR parse (Notes).

### Background
- GitHub's own keywords are close, fix and resolve (-s, -ed). Each links
one reference, and only a merge into the default branch closes it.
- `pull_request_target` runs in the base repository with a write token,
also for fork PRs. That is safe only when no PR-controlled code runs.
Here the description is the only PR input, parsed as text.

<details><summary>Notes</summary>

A close through the API does not create the "closed this in #N" timeline
link that GitHub makes for its own closes. The comment carries the PR
number instead.

How the parser was calibrated. I pulled the descriptions of the last
1000 merged PRs and listed every line with a keyword next to a
reference. The keyword families, list shapes and reference forms in the
script are the ones that appear there. A reference is `#1`,
`owner/repo#1`, an issue or pull URL (bare or in `<>`), or a markdown
link. Four lines would have been wrong with a plain
keyword-then-reference rule, and each led to a rule:

- "the open `rm` fix #37521" (#38379): "fix" as a noun. Base forms (fix,
close, resolve, supersede, replace) count only at the start of a
sentence or line, or after will, should, does, and, and a few similar
words. "to" is not one of them ("unable to fix #1", "how to fix #1").
- "May also fix #12318 / #10046, untested" (#38242): hedged. may, might,
could, would, partially and the negations disqualify the keyword,
looking past adverbs such as "also".
- "Supersedes the closed #26040" (#36289) and "a comment on closed
#35351" (#35365): "closed" as an adjective. A determiner or preposition
before the keyword disqualifies it.
- "supersedes #33130's optimisation" (#35843): a number that continues
into a word is not a reference.

Review added: a reference before the keyword is the subject ("#100
supersedes #1"), also through "which" or "that" ("reverts #100, which
fixed #1") and across a removed span ("#100 ~~also~~ fixes #1"). A hedge
two words before the keyword disqualifies it ("hopefully this fixes #1",
"could this fix #1?"). A clause that starts with if, when, once, until
or unless is not a statement. The tokenizer keeps a line break as a
token so that "Fixes #1" on one line and "Fixes #2" on the next stay two
statements. Code spans, fences, indented code, blockquotes, HTML
comments and strikethrough are skipped. The block stripping follows
CommonMark for fences (also inside a blockquote), indented code,
blockquotes with lazy continuation, setext underlines and HTML comments,
and GFM for `~~` flanking.

Result over the 1000 descriptions: 274 distinct references in 135 PRs. I
checked the current state of all of them through GraphQL. All but one
are closed (202 issues completed, 5 duplicates, 66 pull requests). The
one open target is PR #32292, superseded by merged #36135. No open
target is a false positive. Every review change kept this result.

Patterns that are deliberately not handled: a bulleted list under
"Closes:" on its own line (not seen in the sample), references separated
by whitespace only ("#1 #2"), "fix for #1", and GH-1 style references. A
`?` after the list is not treated as a question. The block parser tracks
no list containers, so a second paragraph of a list item indented by
four spaces is read as an indented code block and skipped. A removed
span or inline comment reads as one word, so "Fixes <!-- n --> #1" finds
nothing.

The test suite covers: the phrases above, stopping at the right place in
real sentences, CRLF descriptions, URLs with fragments or a `/files`
suffix, case-insensitive `Owner/Repo#1`, the fake API where a lookup, an
update or a comment fails, the `dry_run` input, an invalid `pr_number`
input, an unmerged PR, a PR merged into a non-default branch, the merge
event body against a later edit, and a description with no closing
statement.

The first revision of this PR checked out the repository and ran
`scripts/close-linked-issues.ts`. Jarred asked for no checkout and no
script file, so the script moved inline into the workflow and the test
now reads it out of the YAML.
</details>

<!-- robobun:evidence:begin -->

---

**[stamp-90s]** gate passed · iteration 9 · 2 files touched

<details><summary>passes on PR (with fix)</summary>

```console
Test-only change.

Debug/ASAN (expected pass):
$ bun bd test 'test/internal/close-linked-issues.test.ts'
$ BUN_DEBUG_QUIET_LOGS=1 bun scripts/build.ts --profile=debug --quiet test test/internal/close-linked-issues.test.ts
bun test v1.4.1 (4448a2e)

test/internal/close-linked-issues.test.ts:
(pass) finds "Fixes #39852" [176.21ms]
(pass) finds "Closes #31772. Fixes #31771." [22.28ms]
(pass) finds "- Fixes #39930" [12.28ms]
(pass) finds "Fixes: #30429" [10.46ms]
(pass) finds "FIXES #1" [7.86ms]
(pass) finds "(Fixes #1)" [8.97ms]
(pass) finds "**Fixes #1**" [10.20ms]
(pass) finds "__Fixes #1__" [9.83ms]
(pass) finds "_Fixes #1_" [11.25ms]
(pass) finds "Fixes **#1**" [9.72ms]
(pass) finds "**Fixes** #1" [7.13ms]
(pass) finds "**Fixes:** #1" [8.11ms]
(pass) finds "Fixes #1 and **#2**" [11.47ms]
(pass) finds "Fixes **#1**, **#2**" [9.13ms]
(pass) finds "## Why (fixes #13771, closes #30543)" [16.08ms]
(pass) finds "Closes #11418" [19.46ms]
(pass) finds "Resolves #1. Resolved #2. Resolve #3." [12.09ms]
(pass) finds "Fixes #34055, #30327, #24394, #20816, #32403, #11898, #10056." [17.11ms]
(pass) finds "Fixes #18192 and #31675 as a consequence" [10.45ms]
(pass) finds "Fixes #1, #2, and #3" [10.96ms]
(pass) finds "Fixes #1 & #2" [7.63ms]
(pass) finds "Closes #33280,  Closes #32864 and Closes #29696 (the timer in #32949 is orthogonal)" [20.29ms]
(pass) finds "Closes #33182 and #32947 on top of current main (which already has #36304 for catalogs)." [16.12ms]
(pass) finds "Fixes #1,\n#2" [7.76ms]
(pass) finds "Fixes #1, #2,\nand #3" [9.27ms]
(pass) finds "Fixes #1\nand #2" [8.57ms]
(pass) finds "Fixes #1\n& #2" [6.80ms]
(pass) finds "Fixes #1 and\n#2" [7.31ms]
(pass) finds "Supersedes #39908 (same change, moved from a fork branch)" [13.21ms]
(pass) finds "Supersedes #38778 and #38391. Carries the entry point arm of #35053." [14.43ms]
(pass) finds "Supersedes #39193 and keeps its three tests." [11.48ms]
(pass) finds "This supersedes #33306 and #32803. Their tests are kept here." [13.73ms]
(pass) finds "- This replaces #33793. Its 
... (truncated)
Exit: 0
```

</details>

<details><summary>diff hotspot</summary>

```
.github/workflows/close-linked-issues.yml | 950 ++++++++++++++++++++++++++++++
 test/internal/close-linked-issues.test.ts | 598 +++++++++++++++++++
 2 files changed, 1548 insertions(+)
```

</details>

**gate history** · 29 passed · 0 rejected · iteration 9

<details><summary>evidence per changed file</summary>

```
file                                       reads  edits  tests
.github/workflows/close-linked-issues.yml      6     12      0
test/internal/close-linked-issues.test.ts      3     11      0
```

</details>

<!-- robobun:evidence:end -->
@github-actions github-actions Bot closed this Aug 22, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Superseded by #40064.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant