Files
reverse-engineering-anything/tests/boundary/providers/desktopXmlTextPython.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

322 lines
11 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { describe, expect, it } from "vitest";
import type { DesktopTaskCheck } from "../../../src/domain/desktop/appTask.js";
import { xmlView } from "../../../src/domain/desktop/stateFormats.js";
import { elementText } from "../../../src/domain/desktop/viewPointer.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
expectSameCheckResults,
expectSameFileViews,
odtOf,
pythonXmlViews,
savedEnds,
typescriptFileView,
} from "../../support/desktopExportPython.js";
/** The meeting note as AbiWord saves it, before and after its time was changed. */
const notes = () => savedEnds("abiword", "meeting.abw");
/** Writer's flyer before and after an edit split its body paragraph into runs, zipped with its mimetype. */
const flyers = () =>
savedEnds("libreoffice-runs", "content.xml", "flyer.odt", odtOf);
const NOTE = "Documents/meeting.abw";
const PARAGRAPH = "/root/children/[name=section]/children/[name=p][@xid=3]";
const MEETING = "The team meeting starts at 3:30 in room 4.";
/** The paragraph's whole text, a time within it, and its first text node, which the edit split. */
const NOTE_CHECKS: DesktopTaskCheck[] = [
{
id: "time",
kind: "text_equals",
file: NOTE,
pointer: PARAGRAPH,
value: MEETING,
},
{
id: "within",
kind: "text_contains",
file: NOTE,
pointer: PARAGRAPH,
value: "at 3:30 in",
},
{
id: "first-node",
kind: "value_equals",
file: NOTE,
pointer: `${PARAGRAPH}/children/0`,
value: MEETING,
},
];
const FLYER = "Documents/flyer.odt";
const BODY =
"/entries/[name=content.xml]/view/root/children/[name=office:body]/children/[name=office:text]/children/[name=text:p][@text:style-name=Standard]";
const CAKES = "Cakes, biscuits and bread from 9:30 in the village green.";
const FLYER_CHECKS: DesktopTaskCheck[] = [
{ id: "body", kind: "text_equals", file: FLYER, pointer: BODY, value: CAKES },
{
id: "time",
kind: "text_contains",
file: FLYER,
pointer: BODY,
value: " 9:30\n",
normalize_whitespace: true,
},
{
id: "reflowed",
kind: "text_equals",
file: FLYER,
pointer: BODY,
value: " Cakes, biscuits and bread\n from 9:30 in the village green. ",
normalize_whitespace: true,
},
{
id: "first-run",
kind: "value_equals",
file: FLYER,
pointer: `${BODY}/children/0`,
value: CAKES,
},
];
const passes = (outcomes: readonly { passed: boolean }[] | undefined) =>
outcomes?.map(({ passed }) => passed);
describe("XML with a DOCTYPE in the exported module", () => {
it("reads an AbiWord document, which names its DTD, as REA does: the element tree and the declaration", async () => {
const files = await notes();
await expectSameFileViews(files);
const [, after] = files;
if (after === undefined) throw new Error("fixture");
const { format, view } = await typescriptFileView(after);
expect(format).toBe("xml");
expect(view).toMatchObject({
doctype: {
name: "abiword",
public_id: "-//ABISOURCE//DTD AWML 1.0 Strict//EN",
system_id: "http://www.abisource.com/awml.dtd",
},
root: { name: "abiword", attributes: { "xml:space": "preserve" } },
truncated: false,
});
});
it("reads a DOCTYPE that only names a DTD alike, and refuses an internal subset or an undeclared entity in both", () => {
const accepted = [
"<!DOCTYPE a><a/>",
'<?xml version="1.0"?><!--c--><!DOCTYPE b SYSTEM "x[1].dtd"><a>t</a>',
"<!DOCTYPE a PUBLIC ' -//A\r\n B//EN ' 's y'><a/>",
'<!DOCTYPE a PUBLIC "" ""><a b="&lt;&#65;">&amp;&quot;&apos;&gt;</a>',
'<!DOCTYPE svg:svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg:svg xmlns:svg="http://www.w3.org/2000/svg"/>',
'<!DOCTYPE a SYSTEM "x\r\ny\rz\n"><a/>',
];
const refused = [
"<!DOCTYPE a []><a/>",
'<!DOCTYPE a [<!ENTITY e "boom">]><a/>',
'<!DOCTYPE a [<!ENTITY e "boom">]><a>&e;</a>',
'<!DOCTYPE a SYSTEM "a.dtd" [<!ENTITY e SYSTEM "file:///etc/hostname">]><a>&e;</a>',
'<!DOCTYPE a [<!ENTITY % p SYSTEM "file:///etc/hostname"> %p;]><a/>',
'<!DOCTYPE a SYSTEM "a.dtd"><a>&e;</a>',
'<!DOCTYPE a SYSTEM "a.dtd"><a><b c="x &e; y"/></a>',
'<!DOCTYPE a PUBLIC "-//A//EN"><a/>',
'<!DOCTYPE a PUBLIC "x{" "y"><a/>',
"<a/><!DOCTYPE a>",
"<!DOCTYPE a><!DOCTYPE a><a/>",
];
const texts = [...accepted, ...refused];
const ours = texts.map((text) => xmlView(text) ?? null);
expect(pythonXmlViews(texts)).toEqual(ours);
expect(ours.slice(0, accepted.length)).not.toContain(null);
expect(ours.slice(accepted.length)).toEqual(refused.map(() => null));
expect(ours[2]).toMatchObject({
doctype: { name: "a", public_id: "-//A B//EN", system_id: "s y" },
});
expect(ours[5]).toMatchObject({
doctype: { name: "a", public_id: null, system_id: "x\ny\nz\n" },
});
});
it("reads text that opens with an element as XML when it is well formed, whatever the file is called, alike in both", async () => {
const saved = (name: string, text: string) => ({
name,
bytes: new TextEncoder().encode(text),
});
const files = [
saved(
"weekend-trip.mm",
'<map version="freeplane 1.7.0">\n<node TEXT="Weekend Trip" ID="ID_1"><node TEXT="Packing" ID="ID_2"/></node>\n</map>\n',
),
saved("notes.txt", "<b>bold</b> and <i>italic</b>\n"),
saved("plain.txt", " <todo><item>milk</item></todo>\n"),
];
await expectSameFileViews(files);
const formats = await Promise.all(
files.map(async (file) => (await typescriptFileView(file)).format),
);
expect(formats).toEqual(["xml", "text", "xml"]);
});
it("reads text that opens with a DOCTYPE naming no internal subset and then an element as XML, as KmPlot's plots, alike in both", async () => {
const saved = (name: string, text: string) => ({
name,
bytes: new TextEncoder().encode(text),
});
const files = [
saved(
"lesson_plots.fkt",
'<!DOCTYPE kmpdoc>\n<kmpdoc version="4">\n <function type="cartesian"><equation-0 step="0.05">h(x) = 2x + 1</equation-0></function>\n</kmpdoc>\n',
),
saved("entities.fkt", '<!DOCTYPE a [<!ENTITY e "boom">]>\n<a>&e;</a>\n'),
saved("broken.fkt", "<!DOCTYPE a>\n<a><b></a>\n"),
];
await expectSameFileViews(files);
const formats = await Promise.all(
files.map(async (file) => (await typescriptFileView(file)).format),
);
expect(formats).toEqual(["xml", "text", "text"]);
});
it("never reads the DTD a DOCTYPE names: neither its entities nor its default attributes apply", async () => {
const folder = await createTestTempDirectory("desktop-xml-dtd-");
const dtd = join(folder, "a.dtd");
await writeFile(
dtd,
'<!ENTITY e "from the DTD"><!ATTLIST a injected CDATA "from the DTD">',
);
const system = pathToFileURL(dtd).href;
const texts = [
`<!DOCTYPE a SYSTEM "${system}"><a/>`,
`<!DOCTYPE a SYSTEM "${system}"><a>&e;</a>`,
`<!DOCTYPE a SYSTEM "${system}"><a b="&e;"/>`,
];
const ours = texts.map((text) => xmlView(text) ?? null);
expect(pythonXmlViews(texts)).toEqual(ours);
expect(ours).toEqual([
{
doctype: { name: "a", public_id: null, system_id: system },
root: { name: "a", attributes: {}, children: [] },
truncated: false,
},
null,
null,
]);
});
});
describe("whole-text checks in both evaluators", () => {
it("checks the meeting time across AbiWord's runs: the whole text fails before and passes after, its first node does not", async () => {
const [before, after] = await expectSameCheckResults(
NOTE,
NOTE_CHECKS,
await notes(),
);
expect(passes(before)).toEqual([false, false, false]);
for (const fact of [
PARAGRAPH,
JSON.stringify("The team meeting starts at 2:00 in room 4."),
])
expect(before?.[0]?.description).toContain(fact);
expect(passes(after)).toEqual([true, true, false]);
// The first text node holds only the text before the first run.
expect(after?.[2]?.description).toContain(
JSON.stringify("The team meeting starts at "),
);
});
it("reads Writer's runs in its archive alike, the space between two runs included", async () => {
const files = await flyers();
await expectSameFileViews(files);
const [before, after] = await expectSameCheckResults(
FLYER,
FLYER_CHECKS,
files,
);
expect(passes(before)).toEqual([false, false, false, false]);
expect(passes(after)).toEqual([true, true, true, false]);
// The value searched for is the collapsed one, " 9:30\n" read as "9:30".
expect(after?.[1]?.description).toContain(JSON.stringify("9:30"));
});
it("leaves an element's text undecided where the readers dropped text from between its runs, and its attribute decided", async () => {
const deep = "Documents/deep.xml";
/** A paragraph of `before`, then `dropped` nested past the 64 levels both readers keep, then `after`. */
const paragraph = (before: string, dropped: string, after: string) => ({
name: "deep.xml",
bytes: new TextEncoder().encode(
`<p id="t">${before}${"<b>".repeat(65)}${dropped}${"</b>".repeat(65)}${after}</p>`,
),
});
const files = [
paragraph("at 3", "WRONG", ":30 PM"),
paragraph("alpha", "zzz", "beta"),
];
await expectSameFileViews(files);
// The kept runs join into text neither file has.
const kept = await Promise.all(
files.map(async (file) => {
const { view } = await typescriptFileView(file);
return elementText((view as { root: JsonValue }).root);
}),
);
expect(kept).toEqual(["at 3:30 PM", "alphabeta"]);
const results = await expectSameCheckResults(
deep,
[
{
id: "time",
kind: "text_contains",
file: deep,
pointer: "/root",
value: "at 3:30 PM",
},
{
id: "word",
kind: "text_contains",
file: deep,
pointer: "/root",
value: "alphabeta",
},
{
id: "id",
kind: "text_equals",
file: deep,
pointer: "/root/attributes/id",
value: "t",
},
],
files,
);
expect(results).toHaveLength(files.length);
for (const outcomes of results) {
expect(
outcomes.map(({ id, passed, undecided }) => [id, passed, undecided]),
).toEqual([
["time", false, true],
["word", false, true],
["id", true, undefined],
]);
for (const { description } of outcomes)
expect(description).toContain(deep);
}
});
});