Files
reverse-engineering-anything/src/desktop/DesktopContentStore.test.ts
T
morluto ab84ba6839 fix(process)!: retain inspection cancellation and cleanup ownership (#10113)
* fix(process): preserve producer-owned inspection cancellation

Prototype the explicit inspection origin and retained file/batch owner protocol. Downstream adapter migration and lease binding remain required before integration.

* fix(process): type inspection ownership handoffs precisely

* fix(process): retain plain inspection preparation cancellation

* fix(process): retain explicit inspection cleanup and cancellation facts

* fix(process): require explicit inspection facts and cleanup scope

* refactor(process): remove redundant cleanup forwarding wrapper

* fix(firmware): retain inspection cancellation and cleanup ownership

* fix(wakaru): retain typed inspection outcomes and caller scope

* fix(ghidra): preserve inspection facts and launch cleanup scope

* fix(providers): preserve typed process inspection outcomes

* refactor(process): remove obsolete inspection cancellation re-export

* fix(native): preserve inspection preparation cleanup and cancellation

* fix(registration): retain typed inspection cleanup during Git discovery

* fix(process): preserve inspection deadline origin in adapter handoff

* fix(process): consume the canonical inspection selector in binding

* fix(native): preserve inspection deadlines and cleanup retry completion

* test(process): narrow the retained Darwin preparation owner

* fix(providers): preserve preparation ownership and timeout policy

Narrow preparation facts through their canonical owner, preserve actual cleanup capabilities and recorded cancellation, and keep enclosing command deadlines authoritative. Retain raw inspection records through Java refusal and configuration projections without changing availability fallback policy.

* refactor(process): use one inspection failure type owner

* fix(providers): retain recorded secondary failures when rebinding

* fix(ghidra): preserve normalized inspection facts through public projection

* fix(process): normalize native process listing cancellation

* docs(development): clarify failure provenance and cleanup verification

* fix(process): preserve direct inspection cancellation facts

* fix(process): retain inspection owners through termination

* fix(process): preserve command inspection cleanup provenance

* fix(desktop): preserve inspection facts after cleanup retry

* refactor(process)!: migrate desktop capture with inspection producers

* fix(desktop)!: preserve capture failure origins and cleanup owners (#10115)

* fix(desktop): preserve capture failure origins and cleanup

* fix(desktop): type keyring cleanup cancellation

* fix(kuna)!: retain copy cleanup before retiring workspaces (#10114)

* fix(kuna): own copy cancellation and descriptor cleanup

* fix(kuna): close copy descriptors before workspace removal

* test(kuna): type copy lifecycle gates for project library

* docs(kuna): explain retained copy-file cleanup and staging ownership

* docs(kuna): separate cache publication, integrity and cleanup guidance

* fix(desktop): retain sampling inspection owners before release

* fix(process): preserve capture file read cancellation origin

* fix(process): retain capture inspection cancellation and cleanup owners

* fix(process): retain sampling inspection failures in capture scope

* fix(process): retain inspection failures through mandatory cleanup

* fix(process): distinguish sampler shutdown from capture cancellation

* fix(browser): retain startup EOF before later cancellation

* fix(process): bind capture inspection cleanup to the owning run

* test(process): migrate cancellation fixtures to typed owners

Record explicit preparation and command cancellation facts, retain exact opaque reasons and physical causes, and guard the mocked version probe from native preparation. Keep the live Wakaru command control while adding a recording equivalent for bounded source verification.

* fix(process): own sampling reads and settle inspection peers

* fix(errors): retain authored recovery through cleanup composition

* fix(process): retain all sampling inspection leaves for final cleanup

* test(browser): report recording cleanup signal outcome

* fix(process): keep inspection error context out of implicit JSON

* fix(desktop): retain normalized sampling cleanup ownership

* test(process): assert tagged startup cancellation
2026-11-11 16:37:48 -05:00

252 lines
9.7 KiB
TypeScript

import { createHash } from "node:crypto";
import { mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, expect, it, vi } from "vitest";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
import type { DesktopPathState } from "../domain/desktop/desktopStateDiff.js";
import { DesktopContentStore } from "./DesktopContentStore.js";
import { walkHomeState } from "./HomeStateWalker.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
const sha256 = (text: string) =>
createHash("sha256").update(text).digest("hex");
/** The walked state of every path under the root. */
const walked = async (root: string) => {
const { entries } = await walkHomeState([root], [], 100);
return (path: string): DesktopPathState => {
const state = entries.get(join(root, path));
if (state === undefined) throw new Error(`not walked: ${path}`);
return state;
};
};
const store = async (root: string, maxFileBytes: number, maxBytes: number) => {
const created = new DesktopContentStore(
join(root, "capture"),
maxFileBytes,
maxBytes,
);
await mkdir(join(root, "capture"));
await created.prepare();
return created;
};
it("DesktopContentStore leaves an occupied partial filename unowned", async () => {
const root = await createTestTempDirectory("desktop-content-occupied-");
const content = await store(root, 1024, 1024);
const bytes = Buffer.from("new content");
const partial = join(content.directory, `${sha256("new content")}.partial`);
await writeFile(partial, "foreign content");
await expect(content.retainBytes(bytes)).rejects.toMatchObject({
code: "EEXIST",
});
await expect(content.discard()).rejects.toThrow();
expect(await readFile(partial, "utf8")).toBe("foreign content");
});
it("DesktopContentStore retains every unpublished file until discard", async () => {
const root = await createTestTempDirectory("desktop-content-unpublished-");
const content = await store(root, 1024, 1024);
const digests = [sha256("first"), sha256("second")];
for (const [index, text] of ["first", "second"].entries()) {
await mkdir(join(content.directory, digests[index]!));
await expect(content.retainBytes(Buffer.from(text))).rejects.toThrow();
}
await expect(content.discard()).rejects.toThrow();
expect((await readdir(content.directory)).sort()).toEqual(digests.sort());
});
it("DesktopContentStore retains a failed output close until cleanup retry", async () => {
const root = await createTestTempDirectory("desktop-content-close-");
const content = await store(root, 1024, 1024);
const closeFailure = new Error("output descriptor close failed");
const close = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockRejectedValue(closeFailure);
try {
const failure = await content
.retainBytes(Buffer.from("pending"))
.catch((cause: unknown) => cause);
expect(failure).toBeInstanceOf(RegularFileCleanupFailure);
if (!(failure instanceof RegularFileCleanupFailure))
throw new Error("Expected retained output close failure");
await expect(content.close()).rejects.toMatchObject({
cleanup: {
resources: [join(content.directory, `${sha256("pending")}.partial`)],
},
});
close.mockRestore();
await content.close();
await expect(failure.owner.handle.stat()).rejects.toMatchObject({
code: "EBADF",
});
await content.discard();
expect(await readdir(join(root, "capture"))).toEqual([]);
} finally {
close.mockRestore();
await content.close();
}
});
describe("DesktopContentStore", () => {
it("stores each distinct content once and charges the budget once", async () => {
const root = await createTestTempDirectory("desktop-content-");
await mkdir(join(root, "files"));
await writeFile(join(root, "files/a.txt"), "same");
await writeFile(join(root, "files/b.txt"), "same");
const state = await walked(join(root, "files"));
const content = await store(root, 1024, 4);
const first = await content.retain(
join(root, "files/a.txt"),
state("a.txt"),
);
const second = await content.retain(
join(root, "files/b.txt"),
state("b.txt"),
);
expect(first).toEqual({
kind: "retained",
sha256: sha256("same"),
size: 4,
});
expect(second).toEqual(first);
expect(await readdir(join(root, "capture/content"))).toEqual([
sha256("same"),
]);
expect(
await readFile(join(root, "capture/content", sha256("same")), "utf8"),
).toBe("same");
expect([content.files, content.bytes]).toEqual([1, 4]);
});
it("records why a file was left out", async () => {
const root = await createTestTempDirectory("desktop-content-omit-");
await mkdir(join(root, "files/folder"), { recursive: true });
await writeFile(join(root, "files/big.bin"), "0123456789");
await writeFile(join(root, "files/a.txt"), "aaaa");
await writeFile(join(root, "files/b.txt"), "bbbb");
await writeFile(join(root, "files/later.txt"), "v1");
await mkdir(join(root, "files/sub"));
await writeFile(join(root, "files/sub/x.txt"), "x");
const state = await walked(join(root, "files"));
await writeFile(join(root, "files/later.txt"), "version 2");
await rm(join(root, "files/sub"), { recursive: true });
await writeFile(join(root, "files/sub"), "now a file");
const content = await store(root, 8, 6);
const retain = (name: string) =>
content.retain(join(root, "files", name), state(name));
expect(await retain("big.bin")).toEqual({
kind: "omitted",
reason: "too_large",
});
expect((await retain("a.txt")).kind).toBe("retained");
expect(await retain("b.txt")).toEqual({
kind: "omitted",
reason: "budget_exhausted",
});
expect(await retain("folder")).toEqual({
kind: "omitted",
reason: "unreadable",
});
expect(await retain("later.txt")).toEqual({
kind: "omitted",
reason: "changed_since_walk",
});
expect(await retain("sub/x.txt")).toEqual({
kind: "omitted",
reason: "changed_since_walk",
});
expect(content.omissions.map((omission) => omission.reason)).toEqual([
"too_large",
"budget_exhausted",
"unreadable",
"changed_since_walk",
"changed_since_walk",
]);
});
it("does not read a file larger than the remaining budget", async () => {
const root = await createTestTempDirectory("desktop-content-budget-");
await mkdir(join(root, "files"));
await writeFile(join(root, "files/notes.txt"), "abcdef");
const state = await walked(join(root, "files"));
// A read would find the file changed; the budget refuses it first.
await writeFile(join(root, "files/notes.txt"), "abc");
const content = await store(root, 1024, 4);
expect(
await content.retain(join(root, "files/notes.txt"), state("notes.txt")),
).toEqual({ kind: "omitted", reason: "budget_exhausted" });
});
it("discards only the files it wrote and refuses to remove other content", async () => {
const root = await createTestTempDirectory("desktop-content-discard-");
await mkdir(join(root, "files"));
await writeFile(join(root, "files/a.txt"), "secret");
const state = await walked(join(root, "files"));
const content = await store(root, 1024, 1024);
await content.retain(join(root, "files/a.txt"), state("a.txt"));
await content.discard();
expect(await readdir(join(root, "capture"))).toEqual([]);
const again = new DesktopContentStore(join(root, "capture"), 1024, 1024);
await again.prepare();
await again.retain(join(root, "files/a.txt"), state("a.txt"));
await writeFile(join(root, "capture/content/foreign"), "kept");
await expect(again.discard()).rejects.toThrow();
expect(await readdir(join(root, "capture/content"))).toEqual(["foreign"]);
});
});
it.each([false, true])(
"retains read-close cancellation and any actual pending descriptor (close fails: %s)",
async (closeFails) => {
const root = await createTestTempDirectory("desktop-content-cancel-");
await writeFile(join(root, "state.ini"), "state");
const state = await walked(root);
const content = await store(root, 1024, 1024);
const controller = new AbortController();
const reason = new Error("cancel during actual source descriptor close");
const close = OwnedFileHandle.prototype.close;
const closing = vi
.spyOn(OwnedFileHandle.prototype, "close")
.mockImplementation(async function (this: OwnedFileHandle) {
if (!closeFails) await close.call(this);
controller.abort(reason);
if (closeFails) throw new Error("source descriptor close failed");
});
try {
const failure = await content
.retain(join(root, "state.ini"), state("state.ini"), controller.signal)
.catch((cause: unknown) => cause);
expect(failure).toBeInstanceOf(ArtifactReaderFailure);
expect(failure).toMatchObject({ reason: "cancelled", cause: reason });
if (!(failure instanceof ArtifactReaderFailure))
throw new Error("expected actual owner cancellation");
expect(failure.cause).toBe(reason);
expect(content.files).toBe(0);
if (closeFails)
await expect(content.close()).rejects.toMatchObject({
cleanup: { resources: [join(root, "state.ini")] },
});
closing.mockRestore();
await content.close();
await content.discard();
expect(await readFile(join(root, "state.ini"), "utf8")).toBe("state");
} finally {
closing.mockRestore();
await content.close();
await content.discard();
}
},
);