Files
reverse-engineering-anything/tests/evaluation/noRawFileHandleCloseRule.test.ts
T
morluto 428e006f66 fix(filesystem): close file handles only through the owned-handle owners (#10578)
* fix(filesystem): close file handles only through the owned-handle owners

Three sites still closed a fs/promises FileHandle in a finally block, where a
failed close replaced the operation's own failure or result: the agentenv
runner head read, the Linux cgroup kernel-file read and the JavaScript
analysis transfer writer. They now use withOwnedFileHandle, which retains the
operation outcome alongside a failed close.

A lint rule, rea/no-raw-file-handle-close, refuses close() on a handle opened
from fs/promises or declared as FileHandle, and fs close/closeSync on a
descriptor, in src outside src/filesystem/Owned*.

* fix(lint): distinguish owned handle class fields

* fix(filesystem): retain diagnostic file cleanup owners

* test(hopper): retry after confirmed application exit

* fix(filesystem): settle doctor peers, sample cgroup outside admission, and bind lint this

Desktop doctor waits for every client inspection before reporting a retained close failure. Ghidra memory cgroup sampling no longer passes the resource scope's admission gate, so a pending owner cannot mask the provider failure; failed closes are still retained for session cleanup. The raw FileHandle close rule resolves this through arrow functions only, treats static blocks as static, and keeps #name distinct from constructor parameter properties.

* fix(ghidra): join in-flight cgroup samples before session cleanup

ArtifactResourceScope.track runs optional work outside the admission gate while cleanup drains still join it, so a cancelled request's final retry cannot finish before a sample retains a failed close.

* fix(filesystem): join work tracked during a cleanup drain and report doctor file cleanup as typed errors

A cleanup retry now repeats until no tracked work is active and every retained owner has had one attempt, so diagnostic work admitted while an earlier owner is releasing is joined. Doctor reports a retained runner or configuration file close as a ProviderCleanupError bound to its lease instead of rethrowing a raw file failure, so the error document keeps the retained file.

* fix(desktop): read the agentenv runner head through the artifact file owner

The runner read retained a failed close under one lease while the reported failure carried another, so a successful cleanup retry left the doctor error reporting an already-closed file. withArtifactFileHandle binds one lease to both.
2026-11-15 18:08:56 +08:00

83 lines
4.0 KiB
TypeScript

import { describe, it } from "vitest";
import { RuleTester } from "oxlint/plugins-dev";
import rule from "../../scripts/lint/rules/no-raw-file-handle-close.mjs";
RuleTester.describe = describe;
RuleTester.it = it;
new RuleTester({ languageOptions: { parserOptions: { lang: "ts" } } }).run(
"no-raw-file-handle-close",
rule,
{
valid: [
// Owned closes, unrelated close methods and shadowed names.
'import { open } from "node:fs/promises"; const owner = new OwnedFileHandle(await open("x")); await owner.close();',
"server.close(); transport.close();",
'import { open } from "./local.js"; const h = await open("x"); await h.close();',
'const open = async (p: string) => ({ close() {} }); const h = await open("x"); h.close();',
'import type { FileHandle } from "./local.js"; const f = (h: FileHandle) => h.close();',
"class C { readonly x: { close(): void }; d() { this.x.close(); } }",
'import type { FileHandle } from "node:fs/promises"; class C { static h: FileHandle; h = { close() {} }; d() { this.h.close(); } }',
'import { close } from "./local.js"; close(1);',
// `#h` and a constructor parameter property `h` are distinct fields.
'import type { FileHandle } from "node:fs/promises"; class C { #h = { close() {} }; constructor(private h: FileHandle) {} d() { this.#h.close(); } }',
// Object methods and ordinary functions bind their own `this`.
'import type { FileHandle } from "node:fs/promises"; class C { h!: FileHandle; d() { const other = { h: { close() {} }, d() { this.h.close(); } }; other.d(); } }',
'import type { FileHandle } from "node:fs/promises"; class C { h!: FileHandle; d() { return function (this: { h: { close(): void } }) { this.h.close(); }; } }',
],
invalid: [
{
code: 'import type { FileHandle } from "node:fs/promises"; class C { constructor(private h: FileHandle) {} d() { this.h.close(); } }',
errors: [{ messageId: "raw" }],
},
{
code: 'import type { FileHandle } from "node:fs/promises"; class C { static h: FileHandle; h = { close() {} }; static d() { this.h.close(); } }',
errors: [{ messageId: "raw" }],
},
{
code: 'import type { FileHandle } from "node:fs/promises"; class C { static h: FileHandle; static { this.h.close(); } }',
errors: [{ messageId: "raw" }],
},
{
code: 'import type { FileHandle } from "node:fs/promises"; class C { h!: FileHandle; d() { return () => this.h.close(); } }',
errors: [{ messageId: "raw" }],
},
{
code: 'import { open } from "node:fs/promises"; const h = await open("x"); await h.close();',
errors: [{ messageId: "raw" }],
},
{
code: 'import * as fsp from "node:fs/promises"; let h; h = await fsp.open("x"); await h.close();',
errors: [{ messageId: "raw" }],
},
{
code: 'import { open } from "fs/promises"; await (await open("x")).close();',
errors: [{ messageId: "raw" }],
},
{
code: 'import { open } from "node:fs/promises"; const h = await open("x").catch(() => { throw new Error(); }); await h.close();',
errors: [{ messageId: "raw" }],
},
{
code: 'import type { FileHandle } from "node:fs/promises"; const f = async (h: FileHandle | undefined) => h?.close();',
errors: [{ messageId: "raw" }],
},
{
code: 'import type * as fsp from "node:fs/promises"; const f = (h: fsp.FileHandle) => h.close();',
errors: [{ messageId: "raw" }],
},
{
code: 'import type { FileHandle } from "node:fs/promises"; class C { #h: FileHandle; async d() { await this.#h.close(); } }',
errors: [{ messageId: "raw" }],
},
{
code: 'import { closeSync } from "node:fs"; closeSync(3);',
errors: [{ messageId: "raw" }],
},
{
code: 'import * as fs from "fs"; fs.close(3, () => {});',
errors: [{ messageId: "raw" }],
},
],
},
);