Skip to content

Commit fefbe9d

Browse files
committed
refactor(test): apply cleanup review to the snapshot harness
Reuse: milestone hex encoding via Buffer.toString('hex'); the migrator now imports the legacy Steps schema from snap-test.ts instead of redeclaring it. Simplification: shared makeTodo/redirect handling and a verbatim-vpt set in the migrator; dead NewStep fields dropped; shared find_beside_test_exe and manifest_dir helpers in the harness; the always-true separator guard from the upstream port removed. Efficiency: redaction regexes compiled once per run (LazyLock), diagnostic sort skipped when no blocks exist, fixture staging filters harness metadata instead of copy-then-delete, per-step env only cloned when a step overrides it, and the prompts milestone flag is cached at module load (per-keystroke path). Suite output is unchanged: all 8 trials pass against existing snapshots and the migrator reproduces byte-identical fixtures. Claude-Session: https://claude.ai/code/session_01NRgjMi2Vus3iJctudGEWPT
1 parent a8616a6 commit fefbe9d

8 files changed

Lines changed: 167 additions & 157 deletions

File tree

crates/vite_cli_snapshots/tests/cli_snapshots/flavor.rs

Lines changed: 34 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,32 @@ pub struct FlavorRuntime {
4040
pub path_env: OsString,
4141
}
4242

43+
/// The harness crate's manifest dir. The runtime env var wins: cargo sets it
44+
/// for test processes, and nextest rewrites it when running a relocated
45+
/// archive (`--workspace-remap`), where the compile-time path is a
46+
/// build-machine path that no longer exists.
47+
pub fn manifest_dir() -> PathBuf {
48+
std::env::var_os("CARGO_MANIFEST_DIR")
49+
.map_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")), PathBuf::from)
50+
}
51+
4352
pub fn repo_root() -> PathBuf {
44-
// Runtime env preferred over the compile-time path for relocated nextest
45-
// archives (see main.rs).
46-
let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR")
47-
.map_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")), PathBuf::from);
48-
manifest_dir.parent().unwrap().parent().unwrap().to_path_buf()
53+
manifest_dir().parent().unwrap().parent().unwrap().to_path_buf()
54+
}
55+
56+
/// Searches for `name` next to the test executable (`target/<profile>/deps/`)
57+
/// and one directory up (`target/<profile>/`), where cargo puts bin targets
58+
/// and where nextest extracts archived binaries.
59+
fn find_beside_test_exe(name: &str) -> Result<Option<PathBuf>, String> {
60+
let exe = std::env::current_exe().map_err(|e| format!("current_exe failed: {e}"))?;
61+
let deps_dir = exe.parent().ok_or("test executable has no parent dir")?;
62+
for dir in [deps_dir, deps_dir.parent().unwrap_or(deps_dir)] {
63+
let candidate = dir.join(name);
64+
if candidate.is_file() {
65+
return Ok(Some(candidate));
66+
}
67+
}
68+
Ok(None)
4969
}
5070

5171
/// Locates the freshly built global `vp` binary next to this test executable
@@ -64,20 +84,12 @@ fn global_vp_path() -> Result<PathBuf, String> {
6484
}
6585
return Err(format!("VP_SNAP_GLOBAL_VP is set but {} does not exist", vp.display()));
6686
}
67-
let exe = std::env::current_exe().map_err(|e| format!("current_exe failed: {e}"))?;
68-
let deps_dir = exe.parent().ok_or("test executable has no parent dir")?;
6987
let name = format!("vp{}", std::env::consts::EXE_SUFFIX);
70-
for dir in [deps_dir, deps_dir.parent().unwrap_or(deps_dir)] {
71-
let candidate = dir.join(&name);
72-
if candidate.is_file() {
73-
return Ok(candidate);
74-
}
75-
}
76-
Err(format!(
77-
"global `vp` binary not found next to {}; run `just snapshot-test` \
78-
(or `cargo build -p vite_global_cli`) first",
79-
exe.display()
80-
))
88+
find_beside_test_exe(&name)?.ok_or_else(|| {
89+
"global `vp` binary not found next to the test executable; run \
90+
`just snapshot-test` (or `cargo build -p vite_global_cli`) first"
91+
.to_owned()
92+
})
8193
}
8294

8395
/// Locates the local JS CLI bin directory. `VP_SNAP_LOCAL_CLI_BIN_DIR`
@@ -113,18 +125,12 @@ fn vpt_path() -> Result<PathBuf, String> {
113125
if compile_time.is_file() {
114126
return Ok(compile_time);
115127
}
116-
let exe = std::env::current_exe().map_err(|e| format!("current_exe failed: {e}"))?;
117128
let name = format!("vpt{}", std::env::consts::EXE_SUFFIX);
118-
let deps_dir = exe.parent().ok_or("test executable has no parent dir")?;
119-
for dir in [deps_dir, deps_dir.parent().unwrap_or(deps_dir)] {
120-
let candidate = dir.join(&name);
121-
if candidate.is_file() {
122-
return Ok(candidate);
123-
}
124-
}
125-
Err("`vpt` binary not found (checked CARGO_BIN_EXE_vpt, the compile-time \
129+
find_beside_test_exe(&name)?.ok_or_else(|| {
130+
"`vpt` binary not found (checked CARGO_BIN_EXE_vpt, the compile-time \
126131
path, and next to the test executable)"
127-
.to_owned())
132+
.to_owned()
133+
})
128134
}
129135

130136
/// Directory holding an already-provisioned managed JS runtime that each

crates/vite_cli_snapshots/tests/cli_snapshots/main.rs

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -600,11 +600,12 @@ fn run_case(
600600
let case_root = tmpdir.join(format!("{fixture_name}_case_{case_index}_{}", flavor.as_str()));
601601
let stage = case_root.join("workspace");
602602
std::fs::create_dir_all(&stage).unwrap();
603-
CopyOptions::new().copy_tree(fixture_path, &stage).unwrap();
604603
// The case definition and recorded snapshots are harness metadata, not
605-
// part of the workspace under test.
606-
let _ = std::fs::remove_dir_all(stage.join("snapshots"));
607-
let _ = std::fs::remove_file(stage.join("snapshots.toml"));
604+
// part of the workspace under test, so they are never copied in.
605+
CopyOptions::new()
606+
.filter(|path, _| Ok(path != Path::new("snapshots") && path != Path::new("snapshots.toml")))
607+
.copy_tree(fixture_path, &stage)
608+
.unwrap();
608609

609610
let case_home = CaseHome::provision(&case_root, case.seed_runtime);
610611

@@ -643,10 +644,18 @@ fn run_case(
643644
assert!(!argv.is_empty(), "step argv must not be empty");
644645
let program = runtime.resolve_program(&argv[0])?;
645646

646-
let mut step_env = case_env.clone();
647-
for (k, v) in step.envs() {
648-
step_env.insert(k.clone(), v.into());
649-
}
647+
// Most steps add no env of their own; borrow the case env then.
648+
let step_env_override;
649+
let step_env: &BTreeMap<String, OsString> = if step.envs().is_empty() {
650+
&case_env
651+
} else {
652+
let mut env = case_env.clone();
653+
for (k, v) in step.envs() {
654+
env.insert(k.clone(), v.into());
655+
}
656+
step_env_override = env;
657+
&step_env_override
658+
};
650659
let step_cwd = stage.join(step.cwd().unwrap_or(case.cwd.as_str()));
651660
let timeout = step.timeout();
652661

@@ -656,7 +665,7 @@ fn run_case(
656665
cmd.arg(arg);
657666
}
658667
cmd.env_clear();
659-
for (k, v) in &step_env {
668+
for (k, v) in step_env {
660669
cmd.env(k, v);
661670
}
662671
cmd.cwd(&step_cwd);
@@ -745,7 +754,7 @@ fn run_case(
745754
step.interactions().is_empty(),
746755
"interactions require a PTY; remove `tty = false` or the interactions"
747756
);
748-
let (state, raw) = run_step_piped(&program, &argv[1..], &step_env, &step_cwd, timeout);
757+
let (state, raw) = run_step_piped(&program, &argv[1..], step_env, &step_cwd, timeout);
749758
let mut block = String::new();
750759
push_fenced_block(&mut block, &raw);
751760
(state, block)
@@ -813,12 +822,7 @@ fn main() {
813822
let tmp_dir = tempfile::tempdir().unwrap();
814823
let tmp_dir_path: Arc<Path> = Arc::from(tmp_dir.path().canonicalize().unwrap());
815824

816-
// Prefer the runtime env var: cargo sets it for test processes, and
817-
// nextest rewrites it when running a relocated archive
818-
// (--workspace-remap), where the compile-time path no longer exists.
819-
let manifest_dir = std::env::var_os("CARGO_MANIFEST_DIR")
820-
.map_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")), PathBuf::from);
821-
let fixtures_dir = manifest_dir.join("tests/cli_snapshots/fixtures");
825+
let fixtures_dir = flavor::manifest_dir().join("tests/cli_snapshots/fixtures");
822826

823827
let mut fixture_paths = std::fs::read_dir(&fixtures_dir)
824828
.unwrap_or_else(|e| panic!("failed to read {}: {e}", fixtures_dir.display()))

crates/vite_cli_snapshots/tests/cli_snapshots/redact.rs

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,28 @@
55
//! stdout/stderr interleaving, so every rule here should correspond to a real
66
//! source of nondeterminism (paths, durations, versions, machine parallelism).
77
8-
use std::borrow::Cow;
8+
use std::{borrow::Cow, sync::LazyLock};
9+
10+
// Compiled once per run: redaction runs on every snapshotted step, and regex
11+
// compilation dominates matching cost at that frequency.
12+
static UUID_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
13+
regex::Regex::new(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}").unwrap()
14+
});
15+
static DURATION_RE: LazyLock<regex::Regex> =
16+
LazyLock::new(|| regex::Regex::new(r"\b\d+(\.\d+)?(ns|µs|ms|s)\b").unwrap());
17+
static VERSION_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
18+
regex::Regex::new(r"\bv?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b").unwrap()
19+
});
20+
static THREAD_RE: LazyLock<regex::Regex> =
21+
LazyLock::new(|| regex::Regex::new(r"\d+ threads").unwrap());
22+
static NODE_WARNING_RE: LazyLock<regex::Regex> =
23+
LazyLock::new(|| regex::Regex::new(r"(?m)^\(node:\d+\) ExperimentalWarning:.*\n?").unwrap());
24+
static NODE_TRACE_WARNING_RE: LazyLock<regex::Regex> = LazyLock::new(|| {
25+
regex::Regex::new(
26+
r"(?m)^\(Use `node --trace-warnings \.\.\.` to show where the warning was created\)\n?",
27+
)
28+
.unwrap()
29+
});
930

1031
#[expect(
1132
clippy::disallowed_types,
@@ -72,33 +93,21 @@ pub fn redact_output(mut output: String, paths: &[(&str, &'static str)]) -> Stri
7293
redact_string(&mut output, &borrowed);
7394

7495
// Redact UUIDs to "<uuid>"
75-
let uuid_regex =
76-
regex::Regex::new(r"[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}").unwrap();
77-
output = uuid_regex.replace_all(&output, "<uuid>").into_owned();
96+
output = UUID_RE.replace_all(&output, "<uuid>").into_owned();
7897

7998
// Redact durations like "0ns", "123ms" or "1.23s" to "<duration>".
8099
// Runs before version redaction so "1.23s" never half-matches as a version.
81-
let duration_regex = regex::Regex::new(r"\b\d+(\.\d+)?(ns|µs|ms|s)\b").unwrap();
82-
output = duration_regex.replace_all(&output, "<duration>").into_owned();
100+
output = DURATION_RE.replace_all(&output, "<duration>").into_owned();
83101

84102
// Redact semver-shaped versions (bundled tool versions, Node versions).
85-
let version_regex =
86-
regex::Regex::new(r"\bv?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?\b").unwrap();
87-
output = version_regex.replace_all(&output, "<version>").into_owned();
103+
output = VERSION_RE.replace_all(&output, "<version>").into_owned();
88104

89105
// Redact thread counts like "16 threads" to "<n> threads"
90-
let thread_regex = regex::Regex::new(r"\d+ threads").unwrap();
91-
output = thread_regex.replace_all(&output, "<n> threads").into_owned();
106+
output = THREAD_RE.replace_all(&output, "<n> threads").into_owned();
92107

93108
// Remove Node.js experimental warnings (e.g., Type Stripping warnings)
94-
let node_warning_regex =
95-
regex::Regex::new(r"(?m)^\(node:\d+\) ExperimentalWarning:.*\n?").unwrap();
96-
output = node_warning_regex.replace_all(&output, "").into_owned();
97-
let node_trace_warning_regex = regex::Regex::new(
98-
r"(?m)^\(Use `node --trace-warnings \.\.\.` to show where the warning was created\)\n?",
99-
)
100-
.unwrap();
101-
output = node_trace_warning_regex.replace_all(&output, "").into_owned();
109+
output = NODE_WARNING_RE.replace_all(&output, "").into_owned();
110+
output = NODE_TRACE_WARNING_RE.replace_all(&output, "").into_owned();
102111

103112
// Remove ^C echo that Unix terminal drivers emit when ETX (0x03) is written
104113
// to the PTY. Windows ConPTY does not echo it.
@@ -111,8 +120,11 @@ pub fn redact_output(mut output: String, paths: &[(&str, &'static str)]) -> Stri
111120

112121
// Sort consecutive diagnostic blocks to handle non-deterministic tool output
113122
// (e.g., oxlint reports warnings in arbitrary order due to multi-threading).
114-
// Each block starts with " ! " and ends at the next empty line.
115-
output = sort_diagnostic_blocks(&output);
123+
// Each block starts with " ! " and ends at the next empty line. Most
124+
// screens have none, so skip the split/rejoin allocation entirely then.
125+
if output.contains(" ! ") {
126+
output = sort_diagnostic_blocks(&output);
127+
}
116128

117129
output
118130
}
@@ -148,12 +160,12 @@ fn sort_diagnostic_blocks(output: &str) -> String {
148160

149161
blocks.sort();
150162

151-
for (j, block) in blocks.iter().enumerate() {
163+
// Restore an empty-line separator after every block (`i` never
164+
// exceeds parts.len(), so the upstream guard here was always
165+
// true; keep the behavior, drop the misleading condition).
166+
for block in &blocks {
152167
result.extend_from_slice(block);
153-
// Restore empty line separators (between blocks + trailing)
154-
if j < blocks.len() - 1 || i <= parts.len() {
155-
result.push("");
156-
}
168+
result.push("");
157169
}
158170
} else {
159171
result.push(parts[i]);

packages/prompts/src/__tests__/milestone.spec.ts

Lines changed: 22 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,19 @@
1-
import { afterEach, describe, expect, it } from 'vitest';
2-
3-
import { milestone, promptMilestone } from '../milestone.js';
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
42

53
const originalEnv = process.env.VP_EMIT_MILESTONES;
64

5+
// The enabled flag is cached at module load (the harness sets the env before
6+
// spawning the CLI), so each test imports a fresh module copy under its env.
7+
async function loadMilestone(value: string | undefined) {
8+
vi.resetModules();
9+
if (value === undefined) {
10+
delete process.env.VP_EMIT_MILESTONES;
11+
} else {
12+
process.env.VP_EMIT_MILESTONES = value;
13+
}
14+
return import('../milestone.js');
15+
}
16+
717
afterEach(() => {
818
if (originalEnv === undefined) {
919
delete process.env.VP_EMIT_MILESTONES;
@@ -13,22 +23,22 @@ afterEach(() => {
1323
});
1424

1525
describe('milestone', () => {
16-
it('emits nothing unless VP_EMIT_MILESTONES=1', () => {
17-
delete process.env.VP_EMIT_MILESTONES;
18-
expect(milestone('vp')).toBe('');
19-
process.env.VP_EMIT_MILESTONES = '0';
20-
expect(milestone('vp')).toBe('');
26+
it('emits nothing unless VP_EMIT_MILESTONES=1', async () => {
27+
const unset = await loadMilestone(undefined);
28+
expect(unset.milestone('vp')).toBe('');
29+
const disabled = await loadMilestone('0');
30+
expect(disabled.milestone('vp')).toBe('');
2131
});
2232

23-
it('encodes the name as a hex OSC 8 hyperlink with a zero-width anchor', () => {
24-
process.env.VP_EMIT_MILESTONES = '1';
33+
it('encodes the name as a hex OSC 8 hyperlink with a zero-width anchor', async () => {
34+
const { milestone } = await loadMilestone('1');
2535
// "vp" is 0x76 0x70; the sequence must match vite-task's
2636
// pty_terminal_test_client protocol byte for byte.
2737
expect(milestone('vp')).toBe('\x1b]8;;https://milestone.invalid/7670\x1b\\​\x1b]8;;\x1b\\');
2838
});
2939

30-
it('formats prompt milestones as <kind>:<id>:<state>', () => {
31-
process.env.VP_EMIT_MILESTONES = '1';
40+
it('formats prompt milestones as <kind>:<id>:<state>', async () => {
41+
const { milestone, promptMilestone } = await loadMilestone('1');
3242
expect(promptMilestone('select', 'template', '1')).toBe(milestone('select:template:1'));
3343
expect(promptMilestone('confirm', undefined, 'yes')).toBe(milestone('confirm:confirm:yes'));
3444
});

packages/prompts/src/milestone.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@ const OSC8_OPEN = '\x1b]8;;';
1616
const ST = '\x1b\\';
1717
const ZERO_WIDTH_ANCHOR = '​';
1818

19+
// Cached at module load: the harness sets the env before spawning the CLI,
20+
// and milestone() runs on every prompt render (every keystroke), so the
21+
// disabled path must stay a single branch.
22+
const MILESTONES_ENABLED = process.env.VP_EMIT_MILESTONES === '1';
23+
1924
export function milestonesEnabled(): boolean {
20-
return process.env.VP_EMIT_MILESTONES === '1';
25+
return MILESTONES_ENABLED;
2126
}
2227

2328
/**
@@ -26,13 +31,10 @@ export function milestonesEnabled(): boolean {
2631
* the marker arrives in the output stream together with the render it marks.
2732
*/
2833
export function milestone(name: string): string {
29-
if (!milestonesEnabled()) {
34+
if (!MILESTONES_ENABLED) {
3035
return '';
3136
}
32-
let hex = '';
33-
for (const byte of Buffer.from(name, 'utf8')) {
34-
hex += byte.toString(16).padStart(2, '0');
35-
}
37+
const hex = Buffer.from(name, 'utf8').toString('hex');
3638
return `${OSC8_OPEN}${MILESTONE_URI_PREFIX}${hex}${ST}${ZERO_WIDTH_ANCHOR}${OSC8_OPEN}${ST}`;
3739
}
3840

0 commit comments

Comments
 (0)