Files
reverse-engineering-anything/tests/boundary/providers/desktopExportPython.test.ts
T
012861ce1d feat(desktop): draft child-text selectors, member counts and attribute text, and fold in a third batch's findings (#9132)
* fix(desktop): follow a database mapping and own limitation text

A selected database keeps its log when a parent folder moves or the file
sits outside the home. Trash and seed-lock tests compare the owning notice
with the paths, and launch options that are not captured paths stay as written.

* test(desktop): pass the selected log into the draft

The export's selected home paths decide which log stays in the draft.
The line-guard test now names that log instead of relying on its reason.

* fix(desktop): retarget a settings folder that holds captured files

A nested profile directory is a captured path when a seeded file is inside
it. An option value that only resolves under the home stays as written.

* fix(desktop): keep a program word and a directory's neighbor

The program after env stays the program even when a captured file sits in a
folder of that name. A selected directory is not a database, so a neighboring
file keeps the parent folder's spelling.

* fix(desktop): record whether a selected path is a directory

Capture stores the walked path's kind. A neighboring file of a selected
directory keeps the parent folder's spelling even when that directory never
changes and its children are not content roots.

* fix(desktop): mark containing paths without a pairwise scan

A recorded path marks its known ancestors as directories in one walk up
the path, instead of comparing every recorded path with every other.

* fix(desktop): keep a selected folder that is named like a database log

A folder the caller selected stays a selected root when its name ends in
-journal or -wal. A generated log beside a database file still does not.

* fix(desktop): refuse a build command that exceeds the argument vector

Splitting one argument still leaves the whole command under Linux's E2BIG
limit. A request past that budget is refused before the process starts.

* fix(desktop): remap a captured path in an env assignment

The program word stays the program. A NAME=value assignment keeps its name
and follows a captured path value when that path's spelling moves.

* fix(desktop): keep oversized text out of draft guards

A value or line longer than the draft's text bound is not copied into a
guard, so one large frame cannot fill the task by itself.

* docs(desktop): correct dconf and check-preview guidance

A dconf version kept without its bytes is unreadable, not an empty key
list. A values preview is copied into a check only when it is an uncut scalar.

* fix(desktop): keep an empty env value empty

An empty assignment is not a path. Resolving it named the working directory
and rewrote the value when that directory's spelling moved.

* fix(desktop): keep a run's font on the run, not the frame's text

A formatting attribute such as Scribus FONT is not the document's words.
Joining it across runs made a frame-wide text check of ItalicBold that
failed when a run was split. The font stays a value check on its own run.

* fix(desktop): do not pair JSON records by a selector-shaped field

A field named @record_id matched the id suffix and became [@record_id=one],
which reads an XML attribute. Records paired while their pointers matched
nothing. Names that start with @ or > are not id fields.

* fix(desktop): count a save-numbered id once when pairing members

xml:id and draw:id values such as id123456 were both generated and
numbered, so the same selector was counted twice and refused. A text
edit of two elements keyed only by those ids now keeps its check.

* fix(desktop): preserve selected state directory aliases

* fix(desktop): align nested state selections with aliases

* fix(desktop): check build runtime export capability

* fix(desktop): provide complete runtime repack command

* fix(desktop): retain runtime probe failures

* test(desktop): identify capture independently of schema preflight

* fix(desktop): preserve document URLs and value formats

* fix(desktop): bound added checks and omit noisy text guards

* fix(desktop): exclude unseeded locks from task checks

* fix(desktop): preserve draft bounds and launch URLs

* fix(desktop): render draft values using their source format

* fix(desktop): preserve launch URLs and JSON draft values

* fix(desktop): recognize captured file URL documents

* fix(desktop): render captured file URLs for runtime home

* fix(desktop): distinguish phone numbers from save metadata

* fix(desktop): preserve launch URLs and user text

* fix(desktop): preserve captured URLs and user text

* fix(desktop): preserve local file URL authority

* fix(desktop): preserve local file URL authority

* fix(desktop): preserve local file URL authority

* fix(desktop): render captured file URLs at launch

* fix(desktop): classify numeric timestamps using field context

* fix(desktop): classify numeric timestamps using field context

* fix(desktop): classify numeric timestamps using field context

* fix(desktop): retain context for generated draft values

* fix(desktop): retain context for generated draft values

* fix(desktop): retain context for generated draft values

* test(desktop): cover generated IDs in text guards

* test(desktop): cover generated IDs in text guards

* test(desktop): cover generated IDs in text guards

* fix(desktop): isolate launcher URL encoding

* fix(desktop): isolate launcher URL encoding

* fix(desktop): isolate launcher URL encoding

* fix(desktop): isolate launcher URL encoding

* fix(desktop): retain clean text in noisy record fields

* fix(desktop): classify added line values in one pass

* fix(desktop): apply XML quote rules to text drafts

* fix(desktop): distinguish XML attributes from quoted text

* fix(desktop): preserve parsed XML line context

* fix(desktop): retain partial XML syntax facts

* fix(desktop): recognize markdown autolinks in drafts

* fix(desktop): reuse export exclusions in task drafts

* docs(desktop): clarify gzip scrub limits

* style(desktop): format the noisy-record draft files with the pinned oxfmt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(desktop): move launch execution to boundary suite

* fix(desktop): redact build schema failure output

* refactor(desktop): normalize schema failure once

* fix(desktop): omit unsafe schema parser snippets

* fix(desktop): preserve draft change and selected path evidence

* test(desktop): group retained dconf note regressions

* fix(desktop): respect archive entry format in draft checks

* test(desktop): separate unowned-file draft note cases

* fix(desktop): preserve explicitly selected journal documents

* fix(xml): avoid scanning selected ancestors per tag

* fix(desktop): exclude covered text runs from draft counts

* fix: preserve text when drafting desktop checks

* fix: restore nested desktop state aliases

* refactor: share UUID recognition across draft checks

* fix: retain multiline XML attribute context

* fix: retain relationship identifiers in task drafts

* docs: clarify desktop draft identifier handling

* test(desktop): type formatting run cases as pairs

* test(desktop): separate markup boundary regression group

* docs(desktop): clarify task checks before scratch export

* fix(desktop): respell selected paths in file URLs

* test(desktop): group captured document spelling cases

* fix(desktop): bound draft preview serialization

* fix(desktop): restore selected state aliases

* fix(desktop): respect entry format in draft noise

* test(desktop): match exported alias evidence at its JSON boundary

* fix(desktop): retain app state logs in exported seeds

* fix: align desktop numeric seed comparisons

* test(desktop): keep one draft exclusion fixture import

* test(desktop): retain log eligibility across draft test groups

* style: fix desktop launch test formatting

* style: format native command path schema

* fix(desktop): map selected external launch paths

* style(desktop): format seed document regressions

* test(desktop): group launch URL regressions

* fix(desktop): omit redundant nested state aliases

* fix(desktop): compare added free text as a whole

* fix(desktop): check recompressed seeds as shipped

* docs(desktop): clarify recovered seed format

* test(desktop): group shipped gzip seed checks

* fix: retain quoted identifier text in task drafts

* fix: recognize Unicode XML in desktop state readers

* fix(desktop): bound nested draft checks

* fix(desktop): explain missing arrays in truncated views

* fix(desktop): find XML type references in archives

* test(desktop): remove duplicated capture assertions

* fix(desktop): retain nested omitted text boundaries

* style(desktop): format draft value imports

* test(desktop): group model and draft value checks

* fix(desktop): map captured launch paths once

* refactor: remove unused whole-text guard state

* refactor: stop passing unused text children

* test: align free-text checks with whole-note policy

* fix: validate recompressed desktop archive seeds

* style(desktop): format archive seed regression

* fix(desktop): retain placed-item sibling guards

* fix(desktop): redact runtime schema errors

* fix(desktop): normalize captured file URLs

* fix(desktop): report seeded files by capture path

* fix(desktop): retain startup readiness failure stage

* fix(desktop): retain JSON date checks

* fix(desktop): preserve archive formatting checks

* fix(desktop): preserve explicitly selected journal files

* fix(desktop): report build command transport limits

* docs(desktop): clarify unreadable dconf versions

* style(desktop): format capture review fixes

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-09 22:56:44 +08:00

572 lines
19 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
execFileSync,
type ExecFileSyncOptionsWithStringEncoding,
} from "node:child_process";
import { mkdtempSync, writeFileSync, unlinkSync, rmdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it } from "vitest";
import {
PYTHON_HELPERS,
PYTHON_IMPORTS,
} from "../../../src/domain/desktop/appExportPython.js";
import {
detectStateFormat,
keyfileView,
isJsonText,
jsonView,
textualView,
} from "../../../src/domain/desktop/stateFormats.js";
import { safeParseJson } from "../../../src/domain/safeJson.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
/** Keep the exported Python helpers out of the host command-line size limit. */
const execPython = (
args: string[],
options: ExecFileSyncOptionsWithStringEncoding,
): string => {
const directory = mkdtempSync(join(tmpdir(), "rea-export-python-"));
const script = join(directory, "fixture.py");
const codeIndex = args.indexOf("-c");
try {
writeFileSync(script, args[codeIndex + 1]!, "utf8");
return execFileSync(
process.platform === "win32" ? "python" : "python3",
[
"-X",
"utf8",
...args.slice(0, codeIndex),
script,
...args.slice(codeIndex + 2),
],
options,
);
} finally {
unlinkSync(script);
rmdirSync(directory);
}
};
/** The exported module's view format for each file, run with only its standard library imports. */
const pythonFormats = (files: readonly (readonly [string, string])[]) => {
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const program = `${imports}${PYTHON_HELPERS}
for name, text in json.load(sys.stdin):
print(_text_view(name, text, len(text.encode()))["format"])
`;
return execPython(["-I", "-B", "-c", program], {
input: JSON.stringify(files),
encoding: "utf8",
})
.trim()
.split(/\r?\n/u);
};
it("detects Unicode XML names in both state readers while rejecting non-XML text", () => {
const files = [
["map.dat", '<地图 名称="甲"><节点/></地图>'],
["map.dat", '<!DOCTYPE 地图 SYSTEM "map.dtd"><地图/>'],
["map.dat", "<máp/>"],
["map.dat", "<地图><节点></地图>"],
["note.dat", "<ada@example.test>\nBuy milk"],
] as const;
const expected = ["xml", "xml", "xml", "text", "text"];
expect(pythonFormats(files)).toEqual(expected);
expect(
files.map(([name, text]) =>
detectStateFormat(name, new TextEncoder().encode(text)),
),
).toEqual(expected);
});
it("detects key files by the state view's rule rather than a name or any group header", () => {
const files = [
["list.txt", "Groceries\n[x]\nmilk\n"],
["settings.ini", "free text\n"],
["app.conf", "wrap=false\n"],
["settings", "# saved\n[ui]\nwrap=false\n\n; end\n"],
["notes.rc", "[a]\r\nkey = value\r\n"],
] as const;
const formats = pythonFormats(files);
expect(formats).toEqual(["text", "text", "text", "keyfile", "keyfile"]);
expect(formats).toEqual(
files.map(([name, text]) =>
detectStateFormat(name, new TextEncoder().encode(text)),
),
);
});
it("keeps JSON Lines holding a number it cannot read exactly as text, as REA does", () => {
const lines = '{"id":9007199254740993}\n{"id":2}\n';
const files = [
["events.jsonl", lines],
["events.json", lines],
["events.JSON", "42"],
["big.json", '{"id":9007199254740993}'],
["long.json", `{"id":${"9".repeat(5000)}}`],
] as const;
const formats = pythonFormats(files);
expect(formats).toEqual(["text", "text", "json", "json", "json"]);
expect(formats).toEqual(
files.map(([name, text]) =>
detectStateFormat(name, new TextEncoder().encode(text)),
),
);
});
/** The exported module's UTF-16 seed respelling of each file, from /home/ada to /home/user, as hex. */
const pythonUtf16Respelled = (files: readonly string[]) => {
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const program = `${imports}
from pathlib import PurePosixPath
SEED_HOME = "/home/ada"
${PYTHON_HELPERS}
for data in json.load(sys.stdin):
print(_respelled_utf16(bytes.fromhex(data), PurePosixPath("/home/user")).hex())
`;
return execPython(["-I", "-B", "-c", program], {
input: JSON.stringify(files),
encoding: "utf8",
})
.trim()
.split(/\r?\n/u);
};
const utf16 = (text: string, order: "le" | "be") => {
const le = Buffer.from(`${text}`, "utf16le");
return order === "le" ? le : Buffer.from(le).swap16();
};
it("respells the captured home in UTF-16 seeds with a byte-order mark, keeping the mark and order", () => {
const work = "# build\n20260216;1;/home/ada/.varicad\n";
const files = [
utf16(work, "le"),
utf16(work, "be"),
utf16("/home/adam/x\n", "le"),
utf16("no home here\n", "le"),
Buffer.from([0xff, 0xfe, 0x41]),
Buffer.from("/home/ada/plain\n"),
];
const respelled = pythonUtf16Respelled(
files.map((file) => file.toString("hex")),
);
const expected = "# build\n20260216;1;/home/user/.varicad\n";
expect(respelled).toEqual([
utf16(expected, "le").toString("hex"),
utf16(expected, "be").toString("hex"),
...files.slice(2).map((file) => file.toString("hex")),
]);
});
it("seeds each file the bundle lists in SEED_MODES with that mode, as a lock whose app checks it stays 0600", async () => {
const root = await createTestTempDirectory("desktop-seed-modes-");
await mkdir(join(root, "seed", ".config", "notes"), { recursive: true });
await writeFile(
join(root, "seed", ".config", "notes", "secret.ini"),
"[s]\n",
);
await writeFile(join(root, "seed", "run.sh"), "#!/bin/sh\n");
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const program = `${imports}
SEED_HOME = "/home/ada"
TEXT_SEEDS = frozenset()
SEED_MODES = {".config/notes/secret.ini": 0o600, "run.sh": 0o755}
HERE = Path(sys.argv[1])
${PYTHON_HELPERS}
_seed_home(Path(sys.argv[2]))
`;
execFileSync("python3", [
"-I",
"-B",
"-c",
program,
root,
join(root, "home"),
]);
const mode = async (path: string) =>
(await stat(join(root, "home", path))).mode & 0o777;
expect(await mode(".config/notes/secret.ini")).toBe(0o600);
expect(await mode("run.sh")).toBe(0o755);
});
it.each([true, false])(
"restores nested selected aliases in either selection order (%s)",
async (childFirst) => {
const root = await createTestTempDirectory("desktop-nested-alias-");
const home = join(root, "home");
await mkdir(join(home, ".config", "notes"), { recursive: true });
await writeFile(join(home, ".config", "notes", "profile.ini"), "profile");
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const pairs = childFirst
? '[("settings/profile", ".config/notes"), ("settings", ".local/share/notes")]'
: '[("settings", ".local/share/notes"), ("settings/profile", ".config/notes")]';
const program = `${imports}
STATE_ALIASES = ${pairs}
${PYTHON_HELPERS}
home = Path(sys.argv[1])
_ensure_state_aliases(home)
_ensure_state_aliases(home)
`;
execPython(["-I", "-B", "-c", program, home], { encoding: "utf8" });
expect(
await readFile(join(home, "settings", "profile", "profile.ini"), "utf8"),
).toBe("profile");
},
);
it("restores a selected settings alias without replacing an existing path", async () => {
const root = await createTestTempDirectory("desktop-state-alias-");
const home = join(root, "home");
await mkdir(join(root, "seed", ".config", "notes"), { recursive: true });
await writeFile(
join(root, "seed", ".config", "notes", "settings.ini"),
"saved",
);
await mkdir(join(root, "seed", ".config", "notes", "profiles", "work"), {
recursive: true,
});
await writeFile(
join(root, "seed", ".config", "notes", "profiles", "work", "profile.ini"),
"profile",
);
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const program = `${imports}
SEED_HOME = "/home/ada"
TEXT_SEEDS = frozenset()
SEED_MODES = {}
STATE_ALIASES = [("settings", ".config/notes")]
HERE = Path(sys.argv[1])
${PYTHON_HELPERS}
home = Path(sys.argv[2])
_seed_home(home)
_ensure_state_aliases(home)
_ensure_state_aliases(home)
`;
execPython(["-I", "-B", "-c", program, root, home], { encoding: "utf8" });
expect(await readFile(join(home, "settings", "settings.ini"), "utf8")).toBe(
"saved",
);
expect(
await readFile(
join(home, "settings", "profiles", "work", "profile.ini"),
"utf8",
),
).toBe("profile");
const other = join(root, "other-home");
await mkdir(other);
await writeFile(join(other, "settings"), "preserve");
expect(() =>
execPython(["-I", "-B", "-c", program, root, other], {
encoding: "utf8",
stdio: "pipe",
}),
).toThrow();
expect(await readFile(join(other, "settings"), "utf8")).toBe("preserve");
});
/** A stand-in xprop answering the root window list and each window's class and state from a table. */
const fakeXprop = async (windows: Record<string, string>) => {
const bin = await createTestTempDirectory("desktop-xprop-");
const table = JSON.stringify(windows);
await writeFile(
join(bin, "xprop"),
`#!/usr/bin/env python3
import json, sys
windows = json.loads(${JSON.stringify(table)})
if sys.argv[1:3] == ["-root", "_NET_CLIENT_LIST"]:
print("_NET_CLIENT_LIST(WINDOW): window id # " + ", ".join(windows))
else:
print(windows[sys.argv[2]])
`,
);
await chmod(join(bin, "xprop"), 0o755);
return bin;
};
const taskbarWindow = (bin: string, windowClass: string): string => {
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
return execPython(
[
"-I",
"-B",
"-c",
`${imports}\nfrom pathlib import PurePosixPath
SEED_HOME = "/home/ada"\n${PYTHON_HELPERS}\nprint(_taskbar_window(sys.argv[1]))`,
windowClass,
],
{
encoding: "utf8",
env: { ...process.env, PATH: `${bin}:${process.env.PATH ?? ""}` },
},
).trim();
};
it("holds readiness until a window of the app's class that the taskbar lists is shown, not its splash", async () => {
const splash =
'WM_CLASS(STRING) = "varicad", "varicad"\n_NET_WM_STATE(ATOM) = _NET_WM_STATE_SKIP_PAGER, _NET_WM_STATE_SKIP_TASKBAR, _NET_WM_STATE_ABOVE';
const main =
'WM_CLASS(STRING) = "varicad", "varicad"\n_NET_WM_STATE(ATOM) = _NET_WM_STATE_MAXIMIZED_HORZ, _NET_WM_STATE_FOCUSED';
const other =
'WM_CLASS(STRING) = "mousepad", "Mousepad"\n_NET_WM_STATE(ATOM) = ';
expect(taskbarWindow(await fakeXprop({ "0x1": splash }), "varicad")).toBe(
"False",
);
expect(
taskbarWindow(await fakeXprop({ "0x1": splash, "0x2": main }), "varicad"),
).toBe("True");
expect(
taskbarWindow(await fakeXprop({ "0x1": splash, "0x3": other }), "varicad"),
).toBe("False");
expect(taskbarWindow(await fakeXprop({ "0x3": other }), "Mousepad")).toBe(
"True",
);
const typedSplash =
'WM_CLASS(STRING) = "varicad", "varicad"\n_NET_WM_STATE(ATOM) = \n_NET_WM_WINDOW_TYPE(ATOM) = _NET_WM_WINDOW_TYPE_SPLASH';
expect(
taskbarWindow(await fakeXprop({ "0x4": typedSplash }), "varicad"),
).toBe("False");
});
/** The exported module's JSON verdict for each text, with only its standard library imports: its view or its error. */
const pythonJson = (texts: readonly string[]) => {
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const program = `${imports}${PYTHON_HELPERS}
for text in json.load(sys.stdin):
print(json.dumps(_text_view("a.json", text, len(text.encode()))))
`;
return execPython(["-I", "-B", "-c", program], {
input: JSON.stringify(texts),
encoding: "utf8",
})
.trim()
.split(/\r?\n/u)
.map((line) => JSON.parse(line) as { view: unknown; error?: string });
};
it("refuses the JSON numbers REA's state view refuses, as JSON with an error rather than an infinity", () => {
const texts = [
'{"a": 1e999}',
'{"a": -1e999}',
'{"a": 12345678901234567890}',
'{"a": -0}',
'{"a": -0.0}',
'{"a": 0.12345678901234567890}',
'{"a": 1e20}',
'{"a": 0.1, "b": 1e21, "c": 42, "d": 9007199254740991}',
'{"a": 9007199254740993, "a": 1}',
'{"a": 1, "a": 9007199254740993}',
];
const python = pythonJson(texts);
texts.forEach((text, index) => {
const typescript = jsonView(text);
const verdict = python[index];
expect(verdict?.error === undefined).toBe(typescript.ok);
if (typescript.ok) expect(verdict?.view).toEqual(typescript.value);
});
});
/** The exported module's format, view and error for each named text, and REA's for the same bytes. */
const textViews = (files: readonly (readonly [string, string])[]) => {
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const program = `${imports}${PYTHON_HELPERS}
for name, text in json.load(sys.stdin):
view = _text_view(name, text, len(text.encode()))
print(json.dumps({"format": view["format"], "view": view["view"], "error": view.get("error")}))
`;
const python = execPython(["-I", "-B", "-c", program], {
input: JSON.stringify(files),
encoding: "utf8",
maxBuffer: 64 * 1024 * 1024,
})
.trim()
.split("\n")
.map((line) => JSON.parse(line) as unknown);
const rea = files.map(([name, text]) => {
const bytes = new TextEncoder().encode(text);
const format = detectStateFormat(name, bytes);
if (format !== "text" && format !== "json" && format !== "keyfile")
throw new Error(`${name} reads as ${format}`);
const read = textualView(format, bytes);
return read.ok
? { format, view: read.value, error: null }
: { format, view: null, error: read.error };
});
return { python, rea };
};
it("reads text nested past Python's recursion limit as JSON only when all of it is, as REA does", () => {
const brackets = (open: number, close: number) =>
`${"[".repeat(open)}${"]".repeat(close)}`;
const files = [
["deep.txt", brackets(1100, 1099)],
["deep.json", brackets(1100, 1099)],
["deep.txt", brackets(1100, 1100)],
["deep.json", `${brackets(1100, 1100)} x`],
] as const;
const { python, rea } = textViews(files);
expect(python).toEqual(rea);
expect(rea.map(({ format, error }) => [format, error])).toEqual([
["text", null],
["text", null],
["json", "nested deeper than the 500 levels a JSON view keeps"],
["text", null],
]);
});
it("refuses JSON and key files over 4 MiB as REA does, telling JSON apart from text without parsing it", () => {
const objects = `[{}${",{}".repeat(1_400_000)}]`;
const files = [
["objects.json", objects],
["objects", `${objects.slice(0, -1)}}`],
["settings", `[ui]\n${"wrap=false\n".repeat(400_000)}`],
["log", `[0] ${"x".repeat(4_200_000)}`],
] as const;
const { python, rea } = textViews(files);
expect(python).toEqual(rea);
const refused = "larger than the 4 MiB limit for JSON and key file views";
expect(rea.map(({ format, error }) => [format, error])).toEqual([
["json", refused],
["text", null],
["keyfile", refused],
["text", null],
]);
});
it("checks text for JSON the way REA does, token by token, without parsing it", () => {
const [bom, nbsp, lineSeparator] = [0xfeff, 0xa0, 0x2028].map((code) =>
String.fromCharCode(code),
);
const texts = [
["-0", "01", "1.", "1E+5", "-", "[1.5e+]", "truex", "[NaN]"],
['"\\u00e9"', '"\\u00g9"', '"\\x"', '"\t"', '"a', '"\\uD800"'],
["", " [1] ", "[1] x", "{}{}", `${bom}[1]`, `[1]${nbsp}`],
[`["${lineSeparator}"]`, "[]", "[1,]", "[,1]", "[1 2]", "[1]]", "[[1]"],
["[}", "{}", '{"a":1}', '{"a" 1}', "{a:1}", '{"a":1,}', '{"a"::1}'],
["{1:2}", '{"a":[1,{"b":null}],"c":-1.5e-3}'],
].flat();
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const python = JSON.parse(
execPython(
[
"-I",
"-B",
"-c",
`${imports}${PYTHON_HELPERS}\ntexts = json.load(sys.stdin)\nprint(json.dumps({"syntax": [_read_json(text, build=False) is not _INVALID_JSON for text in texts], "built": [_read_json(text) is not _INVALID_JSON for text in texts]}))`,
],
{ input: JSON.stringify(texts), encoding: "utf8" },
),
) as { syntax: boolean[]; built: boolean[] };
const parsed = texts.map((text) => safeParseJson(text).ok);
expect(texts.map((text) => isJsonText(text))).toEqual(parsed);
expect(python.syntax).toEqual(parsed);
expect(python.built).toEqual(parsed);
expect(parsed.filter(Boolean)).toHaveLength(10);
});
it("checks JSON depth with one iterator per level, copying no wide list", () => {
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const program = `${imports}
import tracemalloc
${PYTHON_HELPERS}
wide = [None] * 800_000
tracemalloc.start()
deep = _deeper_than(wide, 500)
peak = tracemalloc.get_traced_memory()[1]
tracemalloc.stop()
nested = []
for _ in range(501):
nested = [nested]
print(json.dumps({"deep": deep, "peak": peak, "edge": _deeper_than(nested, 501), "over": _deeper_than(nested, 500)}))
`;
const result = JSON.parse(
execPython(["-I", "-B", "-c", program], { encoding: "utf8" }),
) as { deep: boolean; peak: number; edge: boolean; over: boolean };
expect(result).toMatchObject({ deep: false, edge: false, over: true });
// Queuing each member of the 800,000-item list traced about 51 MB.
expect(result.peak).toBeLessThan(1024 * 1024);
});
it("spells infinite and negative-zero SQLite reals as REA's SQLite reader does", () => {
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const cells = execPython(
[
"-I",
"-B",
"-c",
`${imports}${PYTHON_HELPERS}\nprint(json.dumps([_cell(v, "utf-8") for v in (math.inf, -math.inf, -0.0, 0.0, 1.5)]))`,
],
{ encoding: "utf8" },
);
expect(JSON.parse(cells)).toEqual(["Infinity", "-Infinity", "-0", 0, 1.5]);
});
it("keeps the same keyfile whitespace as the retained state view", async () => {
const inputs = [
"[ui]\nsuffix=hello \nunicode=\u00a0hello\u00a0\npadded= hello\n",
"[ui]\r\nsuffix=hello \r\nunicode=\u00a0hello\u00a0\u00a0\r\npadded=\thello\r\n",
];
const imports = PYTHON_IMPORTS.split("\n")
.filter((line) => !line.includes("agentenv_protocol"))
.join("\n");
const program = `${imports}${PYTHON_HELPERS}
print(json.dumps([_text_view("settings.ini", text, len(text.encode()))["view"]["groups"] for text in json.load(sys.stdin)]))
`;
const root = await createTestTempDirectory("rea-keyfile-python-");
const script = join(root, "fixture.py");
await writeFile(script, program, "utf8");
const output = execFileSync(
process.platform === "win32" ? "python" : "python3",
["-I", "-B", "-X", "utf8", script],
{
input: JSON.stringify(inputs),
encoding: "utf8",
},
);
expect(JSON.parse(output)).toEqual(inputs.map(keyfileView));
});