mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(process): preserve sibling path identities
Keep aliases on selected paths at clear text delimiters so a longer sibling filename is not rewritten as the executable. Version the normalization contract and cover source and actual PTY behavior. Fixes #6586
This commit is contained in:
@@ -269,6 +269,13 @@ while comparisons and trace assertions use the normalized text. Retaining the
|
||||
original adds at most `limits.output_bytes` of text, because it comes from the
|
||||
same admitted PTY chunks rather than a second unbounded stream.
|
||||
|
||||
When path normalization is enabled, aliases apply to selected path identities
|
||||
at clear text delimiters. A selected executable path does not alias a longer
|
||||
sibling filename such as `tool-helper`; selected parent directories still
|
||||
normalize within descendant paths. The comparison contract records these rules as
|
||||
`path-component-boundaries-v2`, so captures made with the earlier prefix rules
|
||||
cannot be compared as if the text had the same meaning.
|
||||
|
||||
Rendered frame `lines` hold the visible rows after normalization, with trailing
|
||||
U+0020 spaces removed. Blank rows and internal spaces remain. `columns` records
|
||||
terminal cell width; normalization and Unicode can make string lengths differ
|
||||
|
||||
@@ -133,7 +133,7 @@ export const processComparisonContract = (
|
||||
limits: scenario.limits,
|
||||
normalization: scenario.normalization,
|
||||
...(scenario.normalization.paths
|
||||
? { path_normalization_version: "longest-original-paths-v1" }
|
||||
? { path_normalization_version: "path-component-boundaries-v2" }
|
||||
: {}),
|
||||
...(scenario.normalization.ports
|
||||
? { port_normalization_version: "contextual-endpoints-v2" }
|
||||
|
||||
@@ -520,3 +520,21 @@ it("treats path metacharacters literally while preferring nested observed roots"
|
||||
"<executable> <filesystem-root-1>/item <filesystem-root-0>/file <working-directory>/log <working-directory>/toolll",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not give a selected executable identity to a sibling filename", () => {
|
||||
const declared = parseProcessScenario({
|
||||
executable: "/tmp/process-paths/original/tool",
|
||||
working_directory: "/tmp/process-paths",
|
||||
normalization: { paths: true, pids: false, ports: false, patterns: [] },
|
||||
});
|
||||
expect(
|
||||
normalizeProcessText(
|
||||
"/tmp/process-paths/original/tool-helper /tmp/process-paths/original/tool, /tmp/process-paths/original/tool",
|
||||
declared,
|
||||
"/tmp/rea-private",
|
||||
rootPid,
|
||||
),
|
||||
).toBe(
|
||||
"<working-directory>/original/tool-helper <working-directory>/original/tool, <executable>",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -314,10 +314,16 @@ const createPathRule = (
|
||||
const pattern = paths
|
||||
.map((path) => path.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"))
|
||||
.join("|");
|
||||
const matcher = new RegExp(pattern, "gu");
|
||||
// A selected path is an identity, not an arbitrary text prefix. Only match
|
||||
// it at a clear text delimiter; keep the parent-directory case (`/root` in
|
||||
// `/root/file`) while leaving punctuation-adjacent names untouched.
|
||||
const matcher = new RegExp(
|
||||
`(^|[\\s"'=:(\\[{<])(${pattern})(?=$|/|[\\s"'])`,
|
||||
"gu",
|
||||
);
|
||||
return {
|
||||
matcher,
|
||||
replace: (path) => aliases.get(path) ?? path,
|
||||
replace: (_match, prefix, path) => `${prefix}${aliases.get(path) ?? path}`,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -262,7 +262,7 @@ itWithCaptureCapability(
|
||||
).toBe(original.replaceAll("\n", "\r\n"));
|
||||
expect(capture.manifest.comparison_contract).toHaveProperty(
|
||||
"path_normalization_version",
|
||||
"longest-original-paths-v1",
|
||||
"path-component-boundaries-v2",
|
||||
);
|
||||
const legacyContract = { ...capture.manifest.comparison_contract };
|
||||
delete legacyContract.path_normalization_version;
|
||||
@@ -281,6 +281,25 @@ itWithCaptureCapability(
|
||||
],
|
||||
}),
|
||||
);
|
||||
const formerPathSemantics = {
|
||||
...capture,
|
||||
manifest: {
|
||||
...capture.manifest,
|
||||
comparison_contract: {
|
||||
...capture.manifest.comparison_contract,
|
||||
path_normalization_version: "longest-original-paths-v1",
|
||||
},
|
||||
comparison_contract_sha256: digestProcessCommitment({
|
||||
...capture.manifest.comparison_contract,
|
||||
path_normalization_version: "longest-original-paths-v1",
|
||||
}),
|
||||
},
|
||||
};
|
||||
expect(() => compareProcessCaptures(formerPathSemantics, capture)).toThrow(
|
||||
expect.objectContaining({
|
||||
issues: [expect.objectContaining({ path: ["right"] })],
|
||||
}),
|
||||
);
|
||||
expect(capture.exit).toMatchObject({ code: 0, reason: "exited" });
|
||||
expect(capture.cleanup).toMatchObject({
|
||||
owned_process_group: "verified",
|
||||
@@ -289,3 +308,29 @@ itWithCaptureCapability(
|
||||
},
|
||||
20_000,
|
||||
);
|
||||
|
||||
itWithCaptureCapability(
|
||||
"keeps sibling executable names distinct in actual PTY Evidence",
|
||||
async () => {
|
||||
const root = await createTestTempDirectory("rea-process-sibling-path-");
|
||||
const executable = join(root, "original", "tool");
|
||||
await mkdir(join(root, "original"), { recursive: true });
|
||||
await symlink(process.execPath, executable);
|
||||
const sibling = `${executable}-helper`;
|
||||
const scenario = parseProcessScenario({
|
||||
executable,
|
||||
working_directory: root,
|
||||
arguments: ["-e", `process.stdout.write(${JSON.stringify(sibling)})`],
|
||||
normalization: { paths: true, pids: false, ports: false, patterns: [] },
|
||||
settle_ms: 100,
|
||||
});
|
||||
const result = await captureProcessScenario(scenario);
|
||||
if (!result.ok) throw result.error;
|
||||
const capture = parseProcessCapture(
|
||||
createProcessCaptureEvidence(scenario, result.value).normalized_result,
|
||||
);
|
||||
expect(capture.frames.map(({ data }) => data).join("")).toBe(
|
||||
`<working-directory>/original/tool-helper`,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user