import { execFileSync } from "node:child_process"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; import { readZipView } from "../../src/application/desktop/DesktopZipView.js"; import type { DesktopTaskCheck } from "../../src/domain/desktop/appTask.js"; import { evaluateTaskChecks, type TaskCheckOutcome, type TaskFileView, } from "../../src/domain/desktop/appTaskChecks.js"; import { PYTHON_HELPERS, PYTHON_IMPORTS, } from "../../src/domain/desktop/appExportPython.js"; import { PYTHON_TASK_EVALUATOR } from "../../src/domain/desktop/appTaskPython.js"; import { detectStateFormat, textualView, } from "../../src/domain/desktop/stateFormats.js"; import type { JsonValue } from "../../src/domain/jsonValue.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; /** A file as an app saved it: its name under the home and its bytes. */ export interface SavedFile { readonly name: string; readonly bytes: Uint8Array; } /** The exported module's standard library imports, its helpers and the verifier's evaluator, as one program. */ const PYTHON = `${PYTHON_IMPORTS.split("\n") .filter((line) => !line.includes("agentenv_protocol")) .join("\n")}${PYTHON_HELPERS}${PYTHON_TASK_EVALUATOR}`; /** Run a driver after the exported module's helpers and evaluator, with JSON on its stdin; its stdout. */ export const exportedPython = (driver: string, input: unknown): string => execFileSync("python3", ["-I", "-B", "-c", `${PYTHON}\n${driver}`], { input: JSON.stringify(input), encoding: "utf8", maxBuffer: 64 * 1024 * 1024, }); /** One JSON value per line the driver printed. */ const printed = (output: string): Value[] => output .trim() .split("\n") .map((line) => JSON.parse(line) as Value); /** Write each file into a fresh folder, as data/get finds it under the home, keeping its suffix; their paths. */ export const writeFiles = async (files: readonly SavedFile[]) => { const folder = await createTestTempDirectory("desktop-export-python-"); return Promise.all( files.map(async ({ name, bytes }, index) => { const path = join(folder, `${String(index)}-${name}`); await writeFile(path, bytes); return path; }), ); }; /** The exported module's data/get entry for each file: its format and view, or its error. */ export const pythonFileViews = async (files: readonly SavedFile[]) => printed>( exportedPython( `for path in json.load(sys.stdin): print(json.dumps(_view(Path(path))))`, await writeFiles(files), ), ); /** The verifier's results for each file, read by the exported module as the home path `home`. */ const pythonCheckResults = async ( home: string, checks: readonly DesktopTaskCheck[], files: readonly SavedFile[], ) => printed<{ id: string; result: boolean; description: string }[]>( exportedPython( `request = json.load(sys.stdin) for path in request["paths"]: print(json.dumps(evaluate(request["checks"], {request["home"]: _view(Path(path))})))`, { home, checks, paths: await writeFiles(files) }, ), ); /** The exported module's XML view of each text, or null where it refuses one. */ export const pythonXmlViews = (texts: readonly string[]) => printed( exportedPython( `for text in json.load(sys.stdin): print(json.dumps(_xml_view(text)))`, texts, ), ); /** REA's state view of a zip, text, JSON, key file or XML file, in the shape data/get serves it. */ export const typescriptFileView = async ({ name, bytes, }: SavedFile): Promise => { const format = detectStateFormat(name, bytes); if (format === "sqlite" || format === "dconf" || format === "binary") throw new Error(`${name} is ${format}, which these tests do not read`); const read = format === "zip" ? await readZipView(bytes) : textualView(format, bytes); return read.ok ? { format, view: read.value } : { format, view: null, error: read.error }; }; /** Both readers give each file the same format and view, or the same error. */ export const expectSameFileViews = async (files: readonly SavedFile[]) => { const fromPython = await pythonFileViews(files); for (const [index, file] of files.entries()) { const ours = await typescriptFileView(file); const theirs = fromPython[index] ?? {}; expect(theirs.format).toBe(ours.format); expect(theirs.view).toEqual(ours.view); expect(theirs.error).toBe(ours.error); } }; /** Both evaluators give the same results and descriptions on each file read as the home path `home`; REA's. */ export const expectSameCheckResults = async ( home: string, checks: readonly DesktopTaskCheck[], files: readonly SavedFile[], ): Promise<(readonly TaskCheckOutcome[])[]> => { const fromPython = await pythonCheckResults(home, checks, files); return Promise.all( files.map(async (file, index) => { const ours = evaluateTaskChecks( checks, new Map([[home, await typescriptFileView(file)]]), ); expect(fromPython[index]).toEqual( ours.map(({ id, passed, description }) => ({ id, result: passed, description, })), ); return ours; }), ); };