Просмотр исходного кода

test(lib): extract snapshot normalizer utility for cross-OS stability (#28356)

Kit Langton 3 месяцев назад
Родитель
Сommit
55baa16fbc

+ 16 - 26
packages/opencode/test/cli/help/help-snapshots.test.ts

@@ -13,36 +13,26 @@
 // version (changes per release), so we'd snapshot a moving target.
 import { describe, expect } from "bun:test"
 import { Effect } from "effect"
-import fs from "node:fs"
-import os from "node:os"
 import { cliIt } from "../../lib/cli-process"
+import { normalizeForSnapshot, PATH_SEP } from "../../lib/snapshot"
 
-// Strips dynamic content that varies per run so snapshots are stable.
-// Currently only the tmpdir prefix bleeds in (via `--cwd` defaults that
-// resolve to `process.cwd()`). Add new patterns here as they surface.
+// Composes `normalizeForSnapshot` (CRLF + tmpdir) with two help-specific
+// rules:
 //
-// On macOS `os.tmpdir()` returns `/var/folders/...` but `process.cwd()`
-// inside the child returns the realpath `/private/var/folders/...` — so
-// we strip both forms.
-const TMP = os.tmpdir()
-const REAL_TMP = fs.realpathSync(TMP)
+//   1. The harness's `oc-cli-XXX` subdir under TMPDIR collapses to `<HOME>`.
+//      `PATH_SEP` matches `/` and `\\` so the rule works on POSIX + Windows.
+//
+//   2. yargs wraps the `[string] [default: "..."]` clause based on the
+//      pre-normalized default's character length, so different random home
+//      path widths produce different leading-whitespace counts (or even
+//      line-wraps onto a fresh line on Windows). `\s+` matches both forms.
 function normalize(text: string): string {
-  return (
-    text
-      // Windows emits CRLF on stderr; collapse first so the rest of the
-      // pipeline doesn't need separate Windows-vs-POSIX branches.
-      .replaceAll("\r\n", "\n")
-      .replaceAll(REAL_TMP, "<TMPDIR>")
-      .replaceAll(TMP, "<TMPDIR>")
-      // The harness writes the random home dir at `<TMPDIR>/oc-cli-XXX` on
-      // POSIX, `<TMPDIR>\oc-cli-XXX` on Windows. Strip either form.
-      .replace(/<TMPDIR>[/\\]oc-cli-[a-z0-9]+/g, "<HOME>")
-      // yargs wraps the `[string] [default: "..."]` clause based on the
-      // pre-normalized default's character length, so different random home
-      // path widths produce different leading-whitespace counts (or even
-      // line-wraps onto a fresh line on Windows). `\s+` matches both forms.
-      .replace(/\s+\[string\] \[default: "<HOME>"\]/g, ' [string] [default: "<HOME>"]')
-  )
+  return normalizeForSnapshot(text, {
+    pathReplacements: [
+      [new RegExp(`<TMPDIR>${PATH_SEP}oc-cli-[a-z0-9]+`, "g"), "<HOME>"],
+      [/\s+\[string\] \[default: "<HOME>"\]/g, ' [string] [default: "<HOME>"]'],
+    ],
+  })
 }
 
 // Top-level commands. Order matches what `opencode --help` prints today;

+ 73 - 0
packages/opencode/test/lib/snapshot.ts

@@ -0,0 +1,73 @@
+// Shared normalization helpers for cross-OS-stable snapshot tests.
+//
+// Every snapshot test that captures subprocess output, file paths, or other
+// OS-flavored strings hits the same two issues:
+//   1. Bun emits CRLF line endings on Windows stderr; LF elsewhere.
+//   2. Path separators differ (\ on Windows, / on POSIX), and macOS's
+//      /var/folders symlink resolves to /private/var/folders.
+//
+// These helpers exist so each test doesn't reinvent the same regexes.
+//
+// Use individually for fine-grained control, or compose them via
+// `normalizeForSnapshot` for the common "snapshot subprocess output" path.
+import fs from "node:fs"
+import os from "node:os"
+
+const TMP = os.tmpdir()
+const REAL_TMP = fs.realpathSync(TMP)
+
+/**
+ * Collapses CRLF to LF. Bun's subprocess pipes emit native line endings —
+ * snapshots captured on macOS/Linux contain LF, so a Windows run without
+ * this step always diffs.
+ */
+export function stripCrlf(text: string): string {
+  return text.replaceAll("\r\n", "\n")
+}
+
+/**
+ * Converts Windows-style `\` separators to POSIX `/` so paths render
+ * identically across OSes. Use for path strings you want stable in a
+ * snapshot, not for filesystem operations.
+ */
+export function toPosixPath(p: string): string {
+  return p.replaceAll("\\", "/")
+}
+
+/**
+ * Strips both the OS-level `os.tmpdir()` and its realpath form (macOS
+ * `/var/folders` → `/private/var/folders`) from text, replacing each
+ * occurrence with `marker` (default `<TMPDIR>`).
+ */
+export function withTmpdirStripped(text: string, marker = "<TMPDIR>"): string {
+  return text.replaceAll(REAL_TMP, marker).replaceAll(TMP, marker)
+}
+
+/**
+ * Separator-agnostic match class for path-style strings. Use inside a
+ * larger regex when you want to match both `/` (POSIX) and `\` (Windows)
+ * boundaries — e.g. `<TMPDIR>${PATH_SEP}oc-cli-[a-z0-9]+`.
+ */
+export const PATH_SEP = "[/\\\\]"
+
+/**
+ * One-shot normalization for the common case: strip CRLF, strip tmpdir,
+ * then apply any caller-supplied path regex substitutions. Does NOT
+ * blanket-replace `\` with `/` — that would mangle non-path backslash
+ * content (regex literals in help text, etc.). Use `toPosixPath` or
+ * `PATH_SEP` in your own regex when you need separator agnosticism.
+ */
+export function normalizeForSnapshot(
+  text: string,
+  options?: {
+    readonly tmpdirMarker?: string
+    readonly pathReplacements?: ReadonlyArray<readonly [RegExp, string]>
+  },
+): string {
+  let out = stripCrlf(text)
+  out = withTmpdirStripped(out, options?.tmpdirMarker)
+  for (const [pattern, replacement] of options?.pathReplacements ?? []) {
+    out = out.replace(pattern, replacement)
+  }
+  return out
+}