Files
hyperframes/packages/core/src/telemetryRedaction.test.ts
T
Vance IngallsandClaude Opus 5 1664fe6ad7 fix(core,producer,skills): unicode paths, non-Error rejections, shell callers
Three R3 findings.

The redactor's segment classes were ASCII `\w`, so `/数据/客户/秘密视频.mp4` and
`/data/客户/secret.mp4` went out verbatim — and the generic redactor also feeds
CLI telemetry and producer observation messages, where no known-path list
compensates. Segments are now defined by their delimiters instead of an
alphabet, which is correct for every script by construction rather than
requiring Unicode classes to be kept correct. The bare-relative lookbehind had
the same ASCII assumption and let a match start mid-token, redacting
`客户/秘密/视频.mp4` to `客户[path]`; it is now a token boundary, and
bare-relative runs before absolute so it claims the whole token.

sanitizeProbeFailure cast the rejection reason to Error and read `.message`.
An injected probe can reject with anything, so `Promise.reject("failed")` gave
`undefined` and threw inside the redactor — converting a returned failure
result into a rejected promise. Normalized at the boundary, and
redactKnownPaths no longer throws on a non-string.

The contract only admitted .ts/.js/.mjs/.cjs, so it missed shipped shell and
Python callers. frame_strip.sh passed a user-controlled path as ffprobe's last
positional with no terminator; render-and-composite.sh had four more. Both
fixed, and the sweep now covers .py/.sh. Python list argvs are bracket
literals so they get the same position check; shell command lines get a
separate presence check, because checking position there needs a shell parser
— stated as the weaker guarantee it is rather than implied to be equal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-04 03:18:21 -07:00

157 lines
6.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { redactKnownPaths, redactTelemetryString } from "./telemetryRedaction.js";
describe("redactTelemetryString", () => {
it("redacts macOS, Linux, Windows, file URLs, and URL query strings", () => {
expect(
redactTelemetryString(
[
"/Users/alice/project/video.mp4",
"/home/ubuntu/project/video.mp4",
"/workspace/app/video.mp4",
"C:\\Users\\Alice\\project\\video.mp4",
"file:///tmp/render/video.mp4",
"https://example.com/video.mp4?token=secret",
].join(" "),
),
).toBe("[path] [path] [path] [path] [file-url] https://example.com/video.mp4?…");
});
// The redactor used to enumerate roots (/Users, /home, /opt, /tmp, …). Any
// root outside that list reached telemetry verbatim, which is most of them.
it.each([
"/data/media/interview.mov",
"/mnt2/nfs/share/take3.wav",
"/srv2/renders/2026/final.mp4",
"/nix/store/abc123/asset.png",
])("redacts the non-allowlisted absolute root in %s", (path) => {
const out = redactTelemetryString(`ffprobe failed reading ${path}`);
expect(out).not.toContain("/");
expect(out).toContain("[path]");
});
it("redacts relative paths, including a dash-prefixed one", () => {
expect(redactTelemetryString("could not open ./assets/-weird-name.mp3")).toBe(
"could not open [path]",
);
expect(redactTelemetryString("could not open ../-out.wav")).toBe("could not open [path]");
expect(redactTelemetryString("could not open .\\tmp\\-x.aac")).toBe("could not open [path]");
});
it("redacts a bare basename — a caller may pass one instead of a path", () => {
expect(redactTelemetryString("Invalid data found in my-client-cut.mp4")).toBe(
"Invalid data found in [file]",
);
});
// Over-redaction is cheap; these are ordinary in ffprobe stderr and turning
// them into [path] would make a diagnostic string useless.
it.each(["N/A", "24/1", "Stream #0:0", "moov atom not found", "48000/1001"])(
"leaves %s alone",
(text) => {
expect(redactTelemetryString(`ffprobe: ${text}`)).toBe(`ffprobe: ${text}`);
},
);
// A `?` is illegal in a Windows filename, so this is not a query string —
// the whole token is path, and must not survive by hiding behind a `?`.
it("consumes the rest of the token once a path is established", () => {
expect(redactTelemetryString("Navigation failed for C:\\Users\\A\\v.mov?not-a-query")).toBe(
"Navigation failed for [path]",
);
});
it("truncates after redacting, so a long path cannot survive by being cut", () => {
const out = redactTelemetryString(`/data/${"x".repeat(500)}/a.mp4`, 40);
expect(out).not.toContain("xxx");
});
// Named explicitly in review: a relative path with NO `./` prefix was
// missed by both the absolute rule (needs a leading slash) and the `./`
// rule (needs the dot), so it reached telemetry completely unredacted.
it.each([
"customer/acme-secret/video.mp4",
"assets/bgm.mp3",
"projects/client-name/cut/final.mov",
"a\\b\\c.wav",
])("redacts the bare relative path %s", (path) => {
const out = redactTelemetryString(`Invalid data found when processing ${path}`);
expect(out).toBe("Invalid data found when processing [path]");
});
it("redacts a dash-prefixed bare basename", () => {
expect(redactTelemetryString("could not open -customer-secret-intro.mp4")).toBe(
"could not open [file]",
);
});
});
// The segment classes were ASCII `\w`, so a non-Latin path went out verbatim.
// This redactor also feeds CLI telemetry and producer observation messages,
// where no known-path list is supplied to compensate.
describe("non-Latin paths", () => {
it.each([
"/数据/客户/秘密视频.mp4",
"/данные/клиент/видео.mp4",
"/data/客户/secret.mp4",
"/Users/alice/проект/видео.mp4",
])("redacts the absolute path %s", (path) => {
const out = redactTelemetryString(`ffprobe failed reading ${path}`);
expect(out).toBe("ffprobe failed reading [path]");
});
it("redacts a non-Latin bare relative path without stranding the first segment", () => {
// The lookbehind used to be `\w`-based, so a match could start mid-token
// when the preceding character was non-ASCII: this redacted to `客户[path]`.
expect(redactTelemetryString("could not open 客户/秘密/视频.mp4")).toBe(
"could not open [path]",
);
});
it.each(["./资产/背景.mp3", "../输出/final.wav"])("redacts the relative path %s", (path) => {
expect(redactTelemetryString(`could not open ${path}`)).toBe("could not open [path]");
});
it("redacts a non-Latin bare basename", () => {
expect(redactTelemetryString("Invalid data found in 秘密视频.mp4")).toBe(
"Invalid data found in [file]",
);
});
});
describe("redactKnownPaths", () => {
// Shape matching has holes by construction. A caller that built the argv
// knows the exact path, so it can name it instead of hoping a regex does.
it("redacts an exact path a regex would not recognise as one", () => {
const weird = "acme_secret_project";
expect(redactKnownPaths(`ffprobe: ${weird}: Invalid data`, [weird])).toBe(
"ffprobe: [path]: Invalid data",
);
});
it("redacts the basename too — ffprobe often reports only that", () => {
const out = redactKnownPaths("moov atom not found in secret-cut.mp4", [
"/data/x/secret-cut.mp4",
]);
expect(out).toContain("[path]");
expect(out).not.toContain("secret-cut");
});
it("leaves the message alone when no path was supplied", () => {
expect(redactKnownPaths("moov atom not found", [])).toBe("moov atom not found");
});
// Guards against a one/two-character basename turning every occurrence of
// that letter into [path].
it("ignores paths too short to be distinctive", () => {
expect(redactKnownPaths("a stream at a rate", ["a"])).toBe("a stream at a rate");
});
// This sits on an error path: throwing here turns a reported failure into an
// unhandled rejection, which is what a non-Error rejection's `undefined`
// message caused.
it.each([undefined, null, 42, {}])("returns a string for the non-string input %s", (value) => {
expect(() => redactKnownPaths(value as unknown as string, ["/tmp/x.mp4"])).not.toThrow();
});
});