mirror of
https://github.com/morluto/rea.git
synced 2026-11-10 08:47:00 +00:00
fix(ci): restore main workflows and captured-state replay (#9763)
* fix(cli): record the caller HAR path in environment-build evidence environment-build inspected a HAR without the caller path that inspect-web-network-capture records, so the published evidence.json omitted caller_input_path and failed the byte-for-byte portable check. Also update three stale portable expectations that already fail on main: the no-reader verifier digest matches the #9185 selector parent, uncapped IDA callee pages report complete discovery, and a CLI refusal of a malformed artifact inspection includes the undelivered source closure. * fix: restore portable docs, alias, offset, and Windows cleanup checks The witness page linked task anchors that were never written, so any docs-touching PR failed the route check. A selected state directory was restored as a dangling symlink by a duplicated helper. An empty safetensors file reports the extent as outside the file before offset conversion. Windows cleanup refusal uses the runtime's in-progress message. * fix(cli): keep the caller's HAR spelling in environment-build evidence inspect-web-network-capture records the path the caller typed. The one-shot build had already absolutized that path, so a relative capture disagreed with the three-step bundle. The byte-for-byte check now runs from the capture directory and requires caller_input_path grafana.har. * fix(desktop): create alias targets only for directory selections A missing settings file was created as a directory because the helper treated any absent path as a directory. The capture's selected path kind now travels with the alias, and only a directory alias creates its target. An absent file alias stays a file the app can create. * fix(desktop): keep legacy alias kind unknown Captures written before selected_paths.kind leave the alias unknown. Defaulting those to a directory makes an omitted settings file a directory, so a later write through the alias fails with EISDIR. * fix(desktop): keep alias kind a file, directory, or unknown The export plan was widening a missing selected path kind to string. Record file or directory only when the capture says so, and unknown otherwise. * fix(desktop): narrow a missing alias kind to unknown The selected path kind was widened to string, so the export plan could not keep the file, directory, or unknown distinction. * test(desktop): expect alias kind in the rendered app module The export now records whether a selected alias is a directory. The rendered STATE_ALIASES tuples include that kind. * fix(ghidra): reject an oversized Windows TCP endpoint as invalid The POSIX read maps a record past the compact endpoint bound to "Ghidra TCP endpoint is invalid". The Windows lease reported that same bound as a generic observation failure, so an expanded endpoint failed the Windows lane with a different message. * fix(desktop): create a legacy alias when a child proves it is a directory A capture written before selected path kind can still nest one alias inside another. That parent is a directory. A legacy leaf stays unknown so a missing settings file is not created as a directory. * fix(desktop): restore selected aliases when the app owns no files An empty state directory does not become an owned path, so the class was ownless and never called _ensure_state_aliases. The ownless class now restores aliases whenever the export recorded some. * fix(desktop): create the parent of a missing file alias A file alias must stay a file. When its parent directory was not seeded, the symlink was dangling and a write through it failed with ENOENT. * ci: retrigger checks after an unrelated Windows timeout The Windows lane timed out in webModuleCli, which this diff does not touch, and the rerun API is unavailable. * test(evidence): preserve complete CLI refusal diagnostics --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: gensokyo-dev <gensokyo-dev@users.noreply.github.com>
This commit is contained in:
13 files changed
+222
-62
No files matched your search
@@ -222,11 +222,9 @@ Then, with the task exactly as exported:
|
||||
`check_build_task` reads the app's `data/get`, takes the views it returns
|
||||
beside the files and evaluates each check with REA's evaluator, the one the
|
||||
export validates tasks with and the exported verifier mirrors, one result per
|
||||
check. Once the files settle it runs the task's
|
||||
[readers](desktop-application-tasks.md#readers) in the computer through
|
||||
check. Once the files settle it runs the task's readers in the computer through
|
||||
`computer-exec/v1`, as the verifier does, and a check through a reader reads
|
||||
that reader's view. A task of [screen judges](desktop-application-tasks.md#screen-judges)
|
||||
reads no files: REA takes a screenshot and runs the bundle's own judge on it
|
||||
that reader's view. A task of screen judges reads no files: REA takes a screenshot and runs the bundle's own judge on it
|
||||
with the agentenv runner's Python and model endpoint. Like the verifier, it grades only once the app's files settle, since an
|
||||
app such as Gnote saves a few seconds after the last key. It reads `data/get`
|
||||
every 0.5 s and grades once every file's bytes have held for 3 s, counted
|
||||
|
||||
@@ -622,7 +622,7 @@ process.stdout.write(JSON.stringify({executable:basename(process.execPath),abiVe
|
||||
assert.equal(await exists(heldPath), true);
|
||||
assert.throws(
|
||||
() => retryRuntime.writeFile("after-close.txt", "forbidden"),
|
||||
/cleanup is pending/u,
|
||||
/cleanup is in progress or complete/u,
|
||||
);
|
||||
holder.child.stdin.end("\n");
|
||||
const [exitCode] = await holder.closed;
|
||||
|
||||
@@ -315,7 +315,31 @@ it("seeds the app's state at its own path and retains a selected alias", async (
|
||||
join(directory, "envs/notes/notes_app.py"),
|
||||
"utf8",
|
||||
);
|
||||
expect(module).toContain('("settings", ".config/notes")');
|
||||
expect(module).toContain('("settings", ".config/notes", "directory")');
|
||||
});
|
||||
|
||||
it("restores a selected alias when the state directory has no files", async () => {
|
||||
const local = capture();
|
||||
const physical = `${HOME}/.config/notes`;
|
||||
const { result, directory } = await exportCapture({
|
||||
...local,
|
||||
state: {
|
||||
...local.state,
|
||||
content_roots: [physical],
|
||||
state_directories: [physical, `${HOME}/.local/share/notes`],
|
||||
selected_paths: [
|
||||
{ path: `${HOME}/settings`, physical, kind: "directory" },
|
||||
],
|
||||
checkpoints: [checkpoint("baseline", "baseline", [], [])],
|
||||
},
|
||||
});
|
||||
if (result instanceof Error) throw result;
|
||||
const module = await readFile(
|
||||
join(directory, "envs/notes/notes_app.py"),
|
||||
"utf8",
|
||||
);
|
||||
expect(module).toContain("_ensure_state_aliases(Path.home())");
|
||||
expect(module).toContain('("settings", ".config/notes", "directory")');
|
||||
});
|
||||
|
||||
it.each(["settings/notes", "chosen/notes"])(
|
||||
@@ -357,7 +381,7 @@ it.each(["settings/notes", "chosen/notes"])(
|
||||
join(directory, "envs/notes/notes_app.py"),
|
||||
"utf8",
|
||||
);
|
||||
expect(module).toContain(`("${selected}", ".config/notes")`);
|
||||
expect(module).toContain(`("${selected}", ".config/notes", "directory")`);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -406,13 +430,13 @@ it.each([true, false])(
|
||||
join(directory, "envs/notes/notes_app.py"),
|
||||
"utf8",
|
||||
);
|
||||
expect(module).toContain('("settings", ".config/notes")');
|
||||
expect(module).toContain('("settings", ".config/notes", "directory")');
|
||||
expect(module).not.toContain(
|
||||
'("settings/state/notes", ".config/notes/state/notes")',
|
||||
'("settings/state/notes", ".config/notes/state/notes", "directory")',
|
||||
);
|
||||
if (!redundant)
|
||||
expect(module).toContain(
|
||||
'("settings/state/notes", ".local/share/separate")',
|
||||
'("settings/state/notes", ".local/share/separate", "directory")',
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -335,6 +335,7 @@ const conflictingRules = (): JsonValue =>
|
||||
/** Inspect a HAR as inspect-web-network-capture does and serialize the Evidence as it prints it; a refusal is the command's output. */
|
||||
const inspectBuildHar = async (
|
||||
capturePath: string,
|
||||
callerCapture: string,
|
||||
captureTimeoutMs: number | undefined,
|
||||
environment: Readonly<NodeJS.ProcessEnv>,
|
||||
signal: AbortSignal,
|
||||
@@ -357,6 +358,9 @@ const inspectBuildHar = async (
|
||||
: { capture_timeout_ms: captureTimeoutMs }),
|
||||
},
|
||||
{ signal },
|
||||
// Provenance keeps the caller's spelling. The absolute path is only
|
||||
// the file that was read, matching inspect-web-network-capture.
|
||||
{ capture: callerCapture },
|
||||
),
|
||||
service,
|
||||
"web-network-capture",
|
||||
@@ -418,6 +422,7 @@ const buildEnvironmentCommand = async (
|
||||
progress("inspect");
|
||||
const inspected = await inspectBuildHar(
|
||||
capturePath,
|
||||
input.capture,
|
||||
input.captureTimeoutMs,
|
||||
environment,
|
||||
signal,
|
||||
|
||||
@@ -94,6 +94,11 @@ export interface DesktopAppExportPlan {
|
||||
readonly stateAliases?: readonly {
|
||||
readonly alias: string;
|
||||
readonly target: string;
|
||||
/**
|
||||
* Whether the selected path is a directory. Captures written before kind
|
||||
* was recorded leave this unknown; the helper must not invent a directory.
|
||||
*/
|
||||
readonly kind: "file" | "directory" | "unknown";
|
||||
}[];
|
||||
/** Files under the owned paths that existed before the capture but whose bytes were not retained. */
|
||||
readonly unseededFiles: readonly string[];
|
||||
@@ -298,7 +303,13 @@ export const capturedHomePath = (capture: DesktopApplicationCapture) =>
|
||||
capturedHomePlan(capture).homePath;
|
||||
|
||||
/** Selected links to an app state root must keep both the app's name and the selected name. */
|
||||
const capturedStateAliases = (capture: DesktopApplicationCapture) => {
|
||||
const capturedStateAliases = (
|
||||
capture: DesktopApplicationCapture,
|
||||
): {
|
||||
readonly alias: string;
|
||||
readonly target: string;
|
||||
readonly kind: "file" | "directory" | "unknown";
|
||||
}[] => {
|
||||
const { home } = capture.host;
|
||||
const walked = capture.state.state_directories ?? [];
|
||||
const roots = capture.application.state_directories;
|
||||
@@ -306,17 +317,27 @@ const capturedStateAliases = (capture: DesktopApplicationCapture) => {
|
||||
const relative = (path: string) =>
|
||||
path.startsWith(`${home}/`) ? path.slice(home.length + 1) : null;
|
||||
const aliases = (capture.state.selected_paths ?? []).flatMap(
|
||||
({ path, physical }) => {
|
||||
({ path, physical, kind }) => {
|
||||
const index = walked.indexOf(physical);
|
||||
const state = index < 0 ? undefined : roots[index]?.path;
|
||||
const alias = relative(path);
|
||||
const target = state === undefined ? null : relative(state);
|
||||
const recorded: "file" | "directory" | "unknown" =
|
||||
kind === "directory"
|
||||
? "directory"
|
||||
: kind === "file"
|
||||
? "file"
|
||||
: "unknown";
|
||||
return alias === null || target === null || alias === target
|
||||
? []
|
||||
: [{ alias, target }];
|
||||
: [{ alias, target, kind: recorded }];
|
||||
},
|
||||
);
|
||||
const retained: typeof aliases = [];
|
||||
const retained: {
|
||||
alias: string;
|
||||
target: string;
|
||||
kind: "file" | "directory" | "unknown";
|
||||
}[] = [];
|
||||
for (const entry of aliases.toSorted(
|
||||
(left, right) => left.alias.length - right.alias.length,
|
||||
)) {
|
||||
@@ -535,6 +556,7 @@ ${withheld.length === 0 ? PYTHON_DATA_HOOKS : PYTHON_WITHHELD_DATA_HOOKS}${PYTHO
|
||||
title,
|
||||
plan.dconf.length > 0,
|
||||
plan.relaunchOnReset === true,
|
||||
(plan.stateAliases ?? []).length > 0,
|
||||
);
|
||||
};
|
||||
|
||||
@@ -616,7 +638,7 @@ TEXT_SEEDS = frozenset(${pyList(plan.textSeeds)})
|
||||
SEED_MODES = {${Object.entries(plan.seedModes)
|
||||
.map(([home, mode]) => `${py(home)}: 0o${mode.toString(8)}`)
|
||||
.join(", ")}}
|
||||
STATE_ALIASES = [${(plan.stateAliases ?? []).map(({ alias, target }) => `(${py(alias)}, ${py(target)})`).join(", ")}]
|
||||
STATE_ALIASES = [${(plan.stateAliases ?? []).map(({ alias, target, kind }) => `(${py(alias)}, ${py(target)}, ${py(kind)})`).join(", ")}]
|
||||
WINDOW_CLASS = ${py(plan.windowClass)}
|
||||
WITHHELD = {${(plan.withheld ?? []).map(({ setting, file }) => `${py(setting)}: ${py(file)}`).join(", ")}}
|
||||
${PYTHON_HELPERS}
|
||||
|
||||
@@ -153,38 +153,25 @@ def _seed_home(home: Path) -> None:
|
||||
def _ensure_state_aliases(home: Path) -> None:
|
||||
"""Restore selected aliases without replacing any existing user data."""
|
||||
aliases = sorted(STATE_ALIASES, key=lambda pair: len(Path(pair[0]).parts))
|
||||
for alias, target in aliases:
|
||||
for alias, target, kind in aliases:
|
||||
link = home / alias
|
||||
destination = home / target
|
||||
has_children = any(Path(alias) in Path(child).parents for child, _ in aliases)
|
||||
# A recorded directory is created. A legacy alias with no kind is
|
||||
# created only when another alias is inside it, which proves it is a
|
||||
# directory. A legacy leaf stays unclassified so a missing file is not
|
||||
# turned into a directory.
|
||||
has_children = any(Path(alias) in Path(child).parents for child, *_ in aliases)
|
||||
if kind == "directory" or (kind == "unknown" and has_children):
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
elif kind == "file":
|
||||
# The file itself stays absent so the app can create it. Its parent
|
||||
# has to exist or that write fails with ENOENT.
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
if link.is_symlink() and link.resolve() == destination.resolve():
|
||||
if has_children:
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
continue
|
||||
if os.path.lexists(link):
|
||||
raise FileExistsError(f"Cannot restore selected state alias; path already exists: {link}")
|
||||
link.parent.mkdir(parents=True, exist_ok=True)
|
||||
if has_children:
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
link.symlink_to(destination)
|
||||
|
||||
|
||||
def _ensure_state_aliases(home: Path) -> None:
|
||||
"""Restore selected aliases without replacing any existing user data."""
|
||||
aliases = sorted(STATE_ALIASES, key=lambda pair: len(Path(pair[0]).parts))
|
||||
for alias, target in aliases:
|
||||
link = home / alias
|
||||
destination = home / target
|
||||
has_children = any(Path(alias) in Path(child).parents for child, _ in aliases)
|
||||
if link.is_symlink() and link.resolve() == destination.resolve():
|
||||
if has_children:
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
continue
|
||||
if os.path.lexists(link):
|
||||
raise FileExistsError(f"Cannot restore selected state alias; path already exists: {link}")
|
||||
link.parent.mkdir(parents=True, exist_ok=True)
|
||||
if has_children:
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
link.symlink_to(destination)
|
||||
|
||||
|
||||
@@ -455,6 +442,7 @@ export const ownlessBody = (
|
||||
title: string,
|
||||
dconf: boolean,
|
||||
relaunch: boolean,
|
||||
aliases = false,
|
||||
): string => {
|
||||
const reset = relaunch
|
||||
? `
|
||||
@@ -466,14 +454,22 @@ export const ownlessBody = (
|
||||
const serves = relaunch
|
||||
? "and keeps its task's state on screen, so its reset only relaunches it"
|
||||
: "so it serves no data operations";
|
||||
const restore = aliases ? "\n _ensure_state_aliases(Path.home())" : "";
|
||||
if (dconf)
|
||||
return ` """${title}, launched at boot with its captured settings. It owns no files, ${serves}."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
_load_dconf(Path.home())
|
||||
_load_dconf(Path.home())${restore}
|
||||
${reset}${PYTHON_READY_CHECK}`;
|
||||
if (!aliases)
|
||||
return ` """${title}, launched at boot. No state was observed, ${serves}."""\n${reset}${PYTHON_READY_CHECK}`;
|
||||
return ` """${title}, launched at boot. No state was observed, ${serves}."""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
_ensure_state_aliases(Path.home())
|
||||
${reset}${PYTHON_READY_CHECK}`;
|
||||
return ` """${title}, launched at boot. No state was observed, ${serves}."""\n${reset}${PYTHON_READY_CHECK}`;
|
||||
};
|
||||
|
||||
/** The data hooks of an app whose withheld files no get serves: neither as parts nor in the views. */
|
||||
|
||||
@@ -322,10 +322,11 @@ describe("a verifier without readers", () => {
|
||||
);
|
||||
|
||||
expect(rendered).not.toContain("computer-exec");
|
||||
// The digest rendered by the #9185 selector parent (9072516c2f), before readers.
|
||||
// Digest of the no-reader verifier rendered by the #9185 selector parent
|
||||
// (61ab3f0c4). A task without readers must stay byte-for-byte that artifact.
|
||||
// Older evaluator bodies remain subject to sameTaskVerifier's equivalence rules.
|
||||
expect(createHash("sha256").update(rendered).digest("hex")).toBe(
|
||||
"f23f2f374a0fd774d36b76c1cc382d29a748da53647252c9a831493bfa220142",
|
||||
"5de3e07500eab060a35e211a97e471e8e7942ab9e4b60e6ddb7df7851fa9cf00",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -95,10 +95,26 @@ export const observeGhidraEndpoint = async (
|
||||
return { path: endpoint.path };
|
||||
}
|
||||
if (process.platform === "win32") {
|
||||
const encoded = readWindowsPrivateRuntimeFile(
|
||||
endpoint.path,
|
||||
MAX_TCP_ENDPOINT_BYTES,
|
||||
);
|
||||
let encoded: string;
|
||||
try {
|
||||
encoded = readWindowsPrivateRuntimeFile(
|
||||
endpoint.path,
|
||||
MAX_TCP_ENDPOINT_BYTES,
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
// The POSIX read rejects an oversized record as an invalid endpoint.
|
||||
// The Windows lease reports the same bound as a reader limit.
|
||||
if (
|
||||
cause instanceof ArtifactReaderFailure &&
|
||||
cause.reason === "limit"
|
||||
)
|
||||
throw endpointFailure(
|
||||
endpoint,
|
||||
"Ghidra TCP endpoint is invalid",
|
||||
cause,
|
||||
);
|
||||
throw cause;
|
||||
}
|
||||
assertArtifactReadActive(options.signal);
|
||||
return parseTcpEndpoint(endpoint, encoded);
|
||||
}
|
||||
|
||||
@@ -520,10 +520,13 @@ cliTest(
|
||||
const offsetFailure = parseMcpToolError(
|
||||
await call("inspect_model_artifact", { path }),
|
||||
);
|
||||
// The raw extent is outside this empty file, and that check runs before
|
||||
// the offset is converted to a host number.
|
||||
expect(offsetFailure.error).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
code: "target_unavailable",
|
||||
details: {
|
||||
reported_limits: { maximum_exact_offset: Number.MAX_SAFE_INTEGER },
|
||||
constraint: "file_format_rejected",
|
||||
reason: "Declared extent is outside the file",
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
@@ -51,7 +51,13 @@ cliTest(
|
||||
const har = join(root, "grafana.har");
|
||||
await writeFile(har, JSON.stringify(environmentManifestHar()));
|
||||
const built = await cli.run({
|
||||
arguments: ["environment-build", har, join(root, "built"), "--json"],
|
||||
arguments: [
|
||||
"environment-build",
|
||||
"grafana.har",
|
||||
join(root, "built"),
|
||||
"--json",
|
||||
],
|
||||
cwd: root,
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
expect(built.exitCode, built.stderr).toBe(0);
|
||||
@@ -67,11 +73,12 @@ cliTest(
|
||||
const inspected = await cli.run({
|
||||
arguments: [
|
||||
"inspect-web-network-capture",
|
||||
har,
|
||||
"grafana.har",
|
||||
"har",
|
||||
"--format",
|
||||
"json",
|
||||
],
|
||||
cwd: root,
|
||||
});
|
||||
expect(inspected.exitCode, inspected.stderr).toBe(0);
|
||||
const capture = join(root, "capture.json");
|
||||
@@ -114,6 +121,13 @@ cliTest(
|
||||
),
|
||||
file,
|
||||
).toBe(0);
|
||||
const published = JSON.parse(
|
||||
await readFile(
|
||||
join(root, "built", "envs", "grafana-example", "evidence.json"),
|
||||
"utf8",
|
||||
),
|
||||
) as { caller_input_path?: string };
|
||||
expect(published.caller_input_path).toBe("grafana.har");
|
||||
expect(built.json).toMatchObject({
|
||||
tool_count: (exported.json as { tool_count: number }).tool_count,
|
||||
files: (exported.json as { files: unknown[] }).files,
|
||||
|
||||
@@ -297,7 +297,22 @@ it("retains a malformed artifact parent and refuses publication through both del
|
||||
evidenceOutput: join(root, "cli.json"),
|
||||
},
|
||||
);
|
||||
expect(cli).toEqual(projected);
|
||||
// Preserve every diagnostic from the owner; CLI delivery only adds closure.
|
||||
expect(cli).toMatchObject(projected);
|
||||
expect(cli).toMatchObject({
|
||||
details: {
|
||||
// A malformed inspection has no readable embedded source list. The CLI
|
||||
// reports that unknown closure; publication still does not happen.
|
||||
source_closure: {
|
||||
publication: "not_published",
|
||||
complete: false,
|
||||
records: [],
|
||||
omitted_count: 0,
|
||||
omitted: [],
|
||||
collection_failure: expect.any(String),
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(projected)).toContain(evidence.evidence_id);
|
||||
expect(await readdir(root)).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -237,8 +237,8 @@ it.each([true, false])(
|
||||
.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")]';
|
||||
? '[("settings/profile", ".config/notes", "directory"), ("settings", ".local/share/notes", "directory")]'
|
||||
: '[("settings", ".local/share/notes", "directory"), ("settings/profile", ".config/notes", "directory")]';
|
||||
const program = `${imports}
|
||||
STATE_ALIASES = ${pairs}
|
||||
${PYTHON_HELPERS}
|
||||
@@ -253,6 +253,29 @@ _ensure_state_aliases(home)
|
||||
},
|
||||
);
|
||||
|
||||
it("creates a legacy parent alias when a child alias proves it is a directory", async () => {
|
||||
const root = await createTestTempDirectory("desktop-legacy-parent-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 program = `${imports}
|
||||
STATE_ALIASES = [("settings", ".local/share/notes", "unknown"), ("settings/profile", ".config/notes", "directory")]
|
||||
${PYTHON_HELPERS}
|
||||
home = Path(sys.argv[1])
|
||||
_ensure_state_aliases(home)
|
||||
`;
|
||||
execPython(["-I", "-B", "-c", program, home], { encoding: "utf8" });
|
||||
expect(
|
||||
await readFile(join(home, "settings", "profile", "profile.ini"), "utf8"),
|
||||
).toBe("profile");
|
||||
expect(
|
||||
(await stat(join(home, ".local", "share", "notes"))).isDirectory(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("restores a selected alias to an existing settings file", async () => {
|
||||
const root = await createTestTempDirectory("desktop-file-alias-");
|
||||
const home = join(root, "home");
|
||||
@@ -262,7 +285,7 @@ it("restores a selected alias to an existing settings file", async () => {
|
||||
.filter((line) => !line.includes("agentenv_protocol"))
|
||||
.join("\n");
|
||||
const program = `${imports}
|
||||
STATE_ALIASES = [("settings", ".config/tellicorc")]
|
||||
STATE_ALIASES = [("settings", ".config/tellicorc", "file")]
|
||||
${PYTHON_HELPERS}
|
||||
home = Path(sys.argv[1])
|
||||
_ensure_state_aliases(home)
|
||||
@@ -270,6 +293,47 @@ _ensure_state_aliases(home)
|
||||
`;
|
||||
execPython(["-I", "-B", "-c", program, home], { encoding: "utf8" });
|
||||
expect(await readFile(join(home, "settings"), "utf8")).toBe("saved");
|
||||
|
||||
const absent = join(root, "absent");
|
||||
await mkdir(join(absent, ".config"), { recursive: true });
|
||||
const absentProgram = `${imports}
|
||||
STATE_ALIASES = [("settings", ".config/tellicorc", "file")]
|
||||
${PYTHON_HELPERS}
|
||||
home = Path(sys.argv[1])
|
||||
_ensure_state_aliases(home)
|
||||
`;
|
||||
execPython(["-I", "-B", "-c", absentProgram, absent], { encoding: "utf8" });
|
||||
await writeFile(join(absent, "settings"), "created");
|
||||
expect(await readFile(join(absent, ".config", "tellicorc"), "utf8")).toBe(
|
||||
"created",
|
||||
);
|
||||
expect((await stat(join(absent, ".config", "tellicorc"))).isFile()).toBe(
|
||||
true,
|
||||
);
|
||||
const bare = join(root, "bare");
|
||||
await mkdir(bare);
|
||||
execPython(["-I", "-B", "-c", absentProgram, bare], { encoding: "utf8" });
|
||||
await writeFile(join(bare, "settings"), "created");
|
||||
expect(await readFile(join(bare, ".config", "tellicorc"), "utf8")).toBe(
|
||||
"created",
|
||||
);
|
||||
|
||||
const legacy = join(root, "legacy");
|
||||
await mkdir(join(legacy, ".config"), { recursive: true });
|
||||
const legacyProgram = `${imports}
|
||||
STATE_ALIASES = [("settings", ".config/tellicorc", "unknown")]
|
||||
${PYTHON_HELPERS}
|
||||
home = Path(sys.argv[1])
|
||||
_ensure_state_aliases(home)
|
||||
`;
|
||||
execPython(["-I", "-B", "-c", legacyProgram, legacy], { encoding: "utf8" });
|
||||
await writeFile(join(legacy, "settings"), "legacy");
|
||||
expect(await readFile(join(legacy, ".config", "tellicorc"), "utf8")).toBe(
|
||||
"legacy",
|
||||
);
|
||||
expect((await stat(join(legacy, ".config", "tellicorc"))).isFile()).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it("restores a selected settings alias without replacing an existing path", async () => {
|
||||
@@ -294,7 +358,7 @@ it("restores a selected settings alias without replacing an existing path", asyn
|
||||
SEED_HOME = "/home/ada"
|
||||
TEXT_SEEDS = frozenset()
|
||||
SEED_MODES = {}
|
||||
STATE_ALIASES = [("settings", ".config/notes")]
|
||||
STATE_ALIASES = [("settings", ".config/notes", "directory")]
|
||||
HERE = Path(sys.argv[1])
|
||||
${PYTHON_HELPERS}
|
||||
home = Path(sys.argv[2])
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { rm } from "node:fs/promises";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { z } from "zod";
|
||||
import { UNCLASSIFIED_CALL_SITE_LIMITATION } from "../../../../src/ida/IdaCallGraph.js";
|
||||
import { IDA_CALLEE_DISCOVERY } from "../../../../src/ida/IdaCallGraph.js";
|
||||
import { IdaOperationRunner } from "../../../../src/ida/IdaOperationRunner.js";
|
||||
import { IdaSessionClient } from "../../../../src/ida/IdaSessionClient.js";
|
||||
import { createIdaTarget, RecordingIdaMcp } from "../../../fixtures/idaMcp.js";
|
||||
@@ -12,11 +12,13 @@ afterEach(async () => {
|
||||
await rm(root, { recursive: true, force: true });
|
||||
});
|
||||
const queriesSchema = z.object({ queries: z.array(z.string()) });
|
||||
/** An uncapped page still cannot classify the call sites behind its edges. */
|
||||
/**
|
||||
* An uncapped page is complete as a function-edge list and still reports
|
||||
* computed call sites as unknown discovery, which is what the runner emits.
|
||||
*/
|
||||
const uncappedCalleeCoverage = {
|
||||
status: "partial",
|
||||
limitations: [UNCLASSIFIED_CALL_SITE_LIMITATION],
|
||||
limited_directions: ["callees"],
|
||||
status: "complete",
|
||||
discovery: IDA_CALLEE_DISCOVERY,
|
||||
};
|
||||
const addresses = Array.from(
|
||||
{ length: 20 },
|
||||
|
||||
Reference in new issue
Block a user