Merge pull request #1291 from morluto/fix/process-port-normalization-sentence-period

fix(process): normalize ports that end a sentence
This commit is contained in:
Kaoru0822 2026-10-09 07:34:54 -07:00 committed by GitHub
commit 098d76900e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 42 additions and 6 deletions

View File

@ -112,8 +112,11 @@ same admitted PTY chunks rather than a second unbounded stream.
Port normalization recognizes explicit `port`, `tcp_port`, `udp_port`, and
`listen` fields, URL authorities, IP endpoints, and `localhost` endpoints, with
ports from 0 through 65535. It preserves ordinary counters, dimensions, file
line numbers, and ambiguous bare host labels. Use explicit literal `patterns`
ports from 0 through 65535. A port may end a sentence, as in `port: 8080.`;
a period followed by a letter or digit continues a decimal, version, or host
name instead. Each PTY chunk is normalized on its own, so a port whose period
ends the chunk is left unchanged because its continuation is not yet known. It preserves ordinary counters, dimensions, file line numbers,
and ambiguous bare host labels. Use explicit literal `patterns`
for a different application-specific spelling. Set `ports: false` to preserve
all endpoint numbers in comparison text. Captures commit the port-normalization
version so older broad numeric normalization cannot silently compare as the

View File

@ -85,7 +85,7 @@ export const processComparisonContract = (
limits: scenario.limits,
normalization: scenario.normalization,
...(scenario.normalization.ports
? { port_normalization_version: "contextual-endpoints-v1" }
? { port_normalization_version: "contextual-endpoints-v2" }
: {}),
});

View File

@ -99,6 +99,34 @@ it("normalizes only contextual ports before overlapping PID values and honors di
).toBe(input);
});
it("normalizes ports that end a sentence but not decimal or host continuations", () => {
const input = [
"Listening on port: 8080.",
"Server at http://localhost:8080. Ready",
"Bound [::1]:3000.",
"listen=9000.\n",
"port: 80.5 localhost:8080.5 localhost:8080.example.test",
"port: 8080.β localhost:8080.é port: 8080é",
].join("\n");
expect(normalizeProcessText(input, scenario({}), "/temporary", rootPid)).toBe(
[
"Listening on port: <port>.",
"Server at http://localhost:<port>. Ready",
"Bound [::1]:<port>.",
"listen=<port>.\n",
"port: 80.5 localhost:8080.5 localhost:8080.example.test",
"port: 8080.β localhost:8080.é port: 8080é",
].join("\n"),
);
});
it("keeps a port before a chunk-final period, whose continuation is unknown", () => {
for (const chunk of ["port: 80.", "http://localhost:8080.", "[::1]:3000."])
expect(
normalizeProcessText(chunk, scenario({}), "/temporary", rootPid),
).toBe(chunk);
});
it("normalizes representative process identities and preserves null, zero, and unrelated numbers", () => {
const before = JSON.stringify(samples);
const normalized = normalizeProcessSamples(samples, scenario({}), rootPid);

View File

@ -41,9 +41,14 @@ export const normalizeProcessText = (
return normalized;
};
// A port may end a sentence, but a period followed by a letter, digit, or
// underscore in any script continues a decimal, version, or host name and is
// not a port boundary.
// PTY chunks are normalized independently, so a period that ends the chunk
// stays unnormalized: its continuation may arrive in the next chunk.
const normalizePortTokens = (value: string): string => {
const endpointNormalized = value.replaceAll(
/(\b[a-z][a-z0-9+.-]*:\/\/[^\s/?#"'<>]+|\blocalhost|\b(?:\d{1,3}\.){3}\d{1,3}|\[[\da-f:.%]+\]):(\d{1,5})(?![\w.@:])/giu,
/(\b[a-z][a-z0-9+.-]*:\/\/[^\s/?#"'<>]+|\blocalhost|\b(?:\d{1,3}\.){3}\d{1,3}|\[[\da-f:.%]+\]):(\d{1,5})(?![\p{L}\p{N}_@:]|\.(?:[\p{L}\p{N}_]|$))/giu,
(match: string, endpoint: string, port: string) => {
if (Number(port) > 65_535) return match;
const isEndpoint = endpoint.includes("://")
@ -54,7 +59,7 @@ const normalizePortTokens = (value: string): string => {
},
);
return endpointNormalized.replaceAll(
/(\b(?:port|tcp_port|udp_port|listen)\b["']?\s*[:=]\s*)(\d{1,5})(?![\w.])/giu,
/(\b(?:port|tcp_port|udp_port|listen)\b["']?\s*[:=]\s*)(\d{1,5})(?![\p{L}\p{N}_]|\.(?:[\p{L}\p{N}_]|$))/giu,
(match: string, prefix: string, port: string) =>
Number(port) <= 65_535 ? `${prefix}<port>` : match,
);

View File

@ -63,7 +63,7 @@ itWithCaptureCapability(
),
).toBe(true);
expect(capture.manifest.comparison_contract).toMatchObject({
port_normalization_version: "contextual-endpoints-v1",
port_normalization_version: "contextual-endpoints-v2",
});
expect(capture.exit).toMatchObject({ code: 0, reason: "exited" });
expect(capture.cleanup).toMatchObject({