mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 16:57:19 +00:00
* fix(desktop): keep a lone lock a document and drop one beside its file A selected name such as notes.lck stays a document, so launch and the draft still treat it as the caller's file. A lock beside a recorded sibling is left out of the seed unless that lock path itself was selected. * fix(desktop): require the added record and leave save times out of list checks A record a selector names is checked itself, so its words have to be in that record. When an unnamed record only repeats words the seed already has, a list that holds a save time is checked by the text around that time. A later save of the same records still passes, and a missing or rewritten record fails. * fix(desktop): judge the older index helper by pointer segments only A leading zero in a file name or a URL does not change how the older index helper reads a pointer. The witness still rejects that helper when a pointer segment is a number such as 01. * fix(desktop): keep a document whose name ends in .lock A lock beside a recorded file still stays out of the seed unless that lock was selected. A document such as Cargo.lock in the home is the user's file, so the seed keeps it. An application lock in the home, such as poedit.lock, stays out. * fix(desktop): do not suggest list text the seed already passes A repeated record is shown by text the seed does not hold. The seed's own text is not a check. A pointer inside a check value is not a pointer segment for the older index helper. * fix(desktop): do not let a left-out lock hide settings checks A sibling lock the seed omits is not the document the draft follows. Settings changes stay suggested. The unused sibling-lock name helper is removed. * fix(desktop): keep a linked settings file in the folder the link points at A selected ~/.config alias no longer moves MuseScore settings to that alias. The seed keeps settings/MuseScore or Documents, which is the path the export opens the note from. * fix(desktop): split home fetches by answer size and keep a size refusal A batch of tiny files stops before each path's envelope exceeds the host answer budget. A capture output past that ceiling stays a resource error instead of becoming a generic exit. * fix(desktop): seed a selected link under the path the app uses The selected spelling wins, so ~/.config/MuseScore is not rewritten to the folder it points at and the note stays the one launched document. A database wal and journal follow that database. A launch argument is respelled the same way. Teardown signals a detached PID only when its start identity still matches. * fix(desktop): keep a witness's actions and an incomplete checkpoint A failed witness save retries the action log it first recorded. A deletion left unconfirmed between the seed and the end is not reported as no change. Windows packs npm through a shell, which can start npm.cmd. * fix(desktop): read verifier pointers from the parsed checks A brace inside a check value is not an object boundary. A leading-zero pointer after that value still rejects the older index helper. * fix(desktop): leave out a sibling lock under a selected folder A file under a selected folder inherits selected_path. That does not make the lock beside its map a chosen document, so a settings change stays drafted. * fix(desktop): prefer the most specific home spelling and retarget launch files A selected parent directory no longer hides a settings folder walked through a link. A launch filename under that folder is replaced with the seeded file's absolute path. * fix(desktop): pack the build runtime through npm's JavaScript entry npm pack and npm ci no longer spawn npm.cmd through a shell, so a temporary directory whose path contains spaces stays one argument. * fix(desktop): spell launch settings and keep a database log exact A database log matches only that file, so a neighboring folder is not moved. A launch option and the working directory use the same home spelling as the seed. * fix(desktop): draft a lock the export selected exactly The draft uses the same selected paths as the export. A lock chosen by itself stays in the draft. A lock under a selected folder stays out. * fix(desktop): move a state database's logs with the database The write-ahead log and journal follow the spelling the database received, including a walked state link. A neighboring folder stays put. A separate --config argument uses that spelling too. * fix(desktop): keep a selected file ahead of a database-log guess A file selected as itself keeps that spelling even when its name ends in -journal. Relative launch arguments are resolved in the captured working directory before that directory moves. * fix(desktop): keep outside-home database logs and option values A selected database outside the home keeps its write-ahead log. Launch options are rewritten only when they name a captured file or settings path. * 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. * 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): 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): 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 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): 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 * 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 * 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 * 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 * 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(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): 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>
280 lines
9.2 KiB
TypeScript
280 lines
9.2 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("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);
|
|
}
|
|
});
|
|
});
|