mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 16:57:19 +00:00
* 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>
322 lines
11 KiB
TypeScript
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="<A">&"'></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);
|
|
}
|
|
});
|
|
});
|