diff --git a/.github/workflows/dirsql-rust-ci.yml b/.github/workflows/dirsql-rust-ci.yml index 011c0bf41..9d389994e 100644 --- a/.github/workflows/dirsql-rust-ci.yml +++ b/.github/workflows/dirsql-rust-ci.yml @@ -90,6 +90,27 @@ jobs: - name: Run integration tests run: cargo test --manifest-path packages/rust/Cargo.toml --features cli --tests + windows: + name: Windows tests + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-stable-${{ hashFiles('Cargo.lock') }} + + - name: Run unit and integration tests + run: cargo test --manifest-path packages/rust/Cargo.toml --features cli --tests --no-fail-fast + coverage: name: Rust unit coverage floor runs-on: ubuntu-latest diff --git a/README.md b/README.md index 80f7427b5..605768be3 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ cargo install dirsql --features cli ``` > The npm CLI requires **Node ≥ 20.11**. +> +> Supported on Linux, macOS, and Windows (x64). CI runs the Rust test suite on Linux and Windows; the macOS artifacts are built but not tested in CI. ## Quick start diff --git a/packages/python/e2e-attestations/claude-1207-win-argv-quote.json b/packages/python/e2e-attestations/claude-1207-win-argv-quote.json new file mode 100644 index 000000000..66256009f --- /dev/null +++ b/packages/python/e2e-attestations/claude-1207-win-argv-quote.json @@ -0,0 +1,7 @@ +{ + "command": "just test-e2e", + "ran_at": 1790939617, + "exit_code": 0, + "commit": "009c015ffa36453da6d92ddec17b5b655b51372f", + "branch": "claude/1207-win-argv-quote" +} diff --git a/packages/python/e2e-attestations/claude-1208-win-nonverbatim-paths.json b/packages/python/e2e-attestations/claude-1208-win-nonverbatim-paths.json new file mode 100644 index 000000000..20688b5e6 --- /dev/null +++ b/packages/python/e2e-attestations/claude-1208-win-nonverbatim-paths.json @@ -0,0 +1,7 @@ +{ + "command": "just test-e2e", + "ran_at": 1790939958, + "exit_code": 0, + "commit": "64a48bb1eff07865a52fc25e1ad50722aced9e45", + "branch": "claude/1208-win-nonverbatim-paths" +} diff --git a/packages/rust/changelog.d/2026-10-02-windows-non-verbatim-hook-paths.md b/packages/rust/changelog.d/2026-10-02-windows-non-verbatim-hook-paths.md new file mode 100644 index 000000000..7a5f8c11d --- /dev/null +++ b/packages/rust/changelog.d/2026-10-02-windows-non-verbatim-hook-paths.md @@ -0,0 +1 @@ +**Fixed** On Windows, `on-file` hooks and `parsed()` parsers receive `{path}` and `{root}` in plain form (`C:\data\a.txt`, `\\server\share\a.txt`) instead of the verbatim `\\?\C:\...` / `\\?\UNC\...` form, which MSYS tools mangle and many Windows tools reject. Verbatim paths with no plain equivalent are passed through unchanged. diff --git a/packages/rust/changelog.d/2026-10-02-windows-on-file-argv-quoting.md b/packages/rust/changelog.d/2026-10-02-windows-on-file-argv-quoting.md new file mode 100644 index 000000000..0b504b260 --- /dev/null +++ b/packages/rust/changelog.d/2026-10-02-windows-on-file-argv-quoting.md @@ -0,0 +1 @@ +**Fixed** On Windows, every argument of an `on-file` command is now passed double-quoted by the MS C-runtime rules, so an MSYS/Cygwin child (Git for Windows' `sh`, `printf`, `cat`) no longer glob- or brace-expands it. `on-file = "printf '[{}]'"` now yields its rows instead of none. diff --git a/packages/rust/migrations.d/2026-10-02-windows-non-verbatim-hook-paths.md b/packages/rust/migrations.d/2026-10-02-windows-non-verbatim-hook-paths.md new file mode 100644 index 000000000..44ccfd674 --- /dev/null +++ b/packages/rust/migrations.d/2026-10-02-windows-non-verbatim-hook-paths.md @@ -0,0 +1,43 @@ +### Hooks receive non-verbatim paths on Windows + +**Summary** + +On Windows, the `{path}` and `{root}` values substituted into `on-file` and +`parsed()` commands could carry the verbatim prefix (`\\?\C:\...`, +`\\?\UNC\server\share\...`). They are now the plain form. No signature +changed. Output on Unix is unchanged. + +**Required changes** + +_None._ + +**Deprecations removed** + +_None._ + +**Behavior changes without code changes** + +| Surface (Windows only) | Before | After | +|---|---|---| +| `{path}` / `{root}` on a drive | `\\?\C:\data\a.txt` | `C:\data\a.txt` | +| `{path}` / `{root}` on a share | `\\?\UNC\server\share\a.txt` | `\\server\share\a.txt` | + +A verbatim path whose plain spelling would name a different file (a reserved +device name, a trailing dot or space, a path of 260 or more UTF-16 units, or a +`\\?\GLOBALROOT` / `\\?\Volume{...}` form) is still passed verbatim. + +**Verification** + +```toml +[[table]] +name = "seen" +ddl = "CREATE TABLE seen (n INTEGER)" +glob = "*.txt" +on-file = "sh -c 'printf \"%s\\n\" \"$1\" >> seen.log; echo []' sh {path}" +``` + +```bash +dirsql "SELECT count(*) FROM seen" +type seen.log +# each line starts with C:\, not \\?\ +``` diff --git a/packages/rust/migrations.d/2026-10-02-windows-on-file-argv-quoting.md b/packages/rust/migrations.d/2026-10-02-windows-on-file-argv-quoting.md new file mode 100644 index 000000000..81c1102fd --- /dev/null +++ b/packages/rust/migrations.d/2026-10-02-windows-on-file-argv-quoting.md @@ -0,0 +1,42 @@ +### Windows on-file arguments reach MSYS children unexpanded + +**Summary** + +On Windows, every argument of an `on-file` command is now passed double-quoted +by the MS C-runtime rules. An MSYS/Cygwin child (Git for Windows' `sh`, +`printf`, `cat`) previously glob- and brace-expanded any argument without a +space or tab; it now receives the argument as written. MSVCRT children parse +the same values as before. No signature changed, and Unix is unchanged. + +**Required changes** + +_None._ + +**Deprecations removed** + +_None._ + +**Behavior changes without code changes** + +| Surface (Windows, MSYS child) | Before | After | +|---|---|---| +| `on-file = "printf '[{}]'"` | child prints `[]`, zero rows | child prints `[{}]`, one row | +| an argument `*.txt` | expanded to matching files | the literal `*.txt` | + +A config that relied on an MSYS child expanding a glob in its argv must ask +for a shell explicitly: `sh -c 'cat *.txt'`. + +**Verification** + +```toml +[[table]] +name = "t" +ddl = "CREATE TABLE t (n INTEGER)" +glob = "*.txt" +on-file = "printf '[{\"n\":1}]'" +``` + +```bash +dirsql query "SELECT n FROM t LIMIT 1" -c .dirsql.toml +# [{"n":1}] +``` diff --git a/packages/rust/src/command.rs b/packages/rust/src/command.rs index ecef38e42..e795beef7 100644 --- a/packages/rust/src/command.rs +++ b/packages/rust/src/command.rs @@ -49,6 +49,63 @@ impl Placeholder { value: value.into(), } } + + /// A placeholder whose value is a filesystem path, handed to the child in + /// its plain (non-verbatim) form; see [`non_verbatim`]. + pub fn path(name: impl Into, path: &str) -> Self { + Self::new(name, non_verbatim(path)) + } +} + +/// The plain form of a Windows verbatim path: `\\?\C:\x` becomes `C:\x` and +/// `\\?\UNC\server\share\x` becomes `\\server\share\x`. Anything else, and any +/// verbatim path whose plain spelling would name a different file, is returned +/// unchanged. +pub(crate) fn non_verbatim(path: &str) -> String { + plain_form(path).unwrap_or_else(|| path.to_string()) +} + +fn plain_form(path: &str) -> Option { + let rest = path.strip_prefix(r"\\?\")?; + let (plain, tail) = if let Some(unc) = rest.strip_prefix(r"UNC\") { + if unc.split('\\').count() < 2 { + return None; + } + (format!(r"\\{unc}"), unc) + } else { + let drive = rest.as_bytes(); + let is_drive = drive.len() >= 3 + && drive[0].is_ascii_alphabetic() + && drive[1] == b':' + && drive[2] == b'\\'; + if !is_drive { + return None; + } + (rest.to_string(), &rest[3..]) + }; + // Win32 rejects or rewrites these, so the plain spelling would not name + // the same file; MAX_PATH counts UTF-16 units including the terminator. + let same_file = (tail.is_empty() || tail.split('\\').all(is_plain_component)) + && plain.encode_utf16().count() < 260; + same_file.then_some(plain) +} + +fn is_plain_component(name: &str) -> bool { + let invalid_char = |c: char| c < ' ' || r#"/<>:"|?*"#.contains(c); + let stem = name + .split_once('.') + .map_or(name, |(stem, _)| stem) + .trim_end_matches(' '); + let reserved = matches!( + stem.to_ascii_uppercase().as_bytes(), + b"CON" + | b"PRN" + | b"AUX" + | b"NUL" + | [b'C', b'O', b'M', b'1'..=b'9'] + | [b'L', b'P', b'T', b'1'..=b'9'] + ); + !name.is_empty() && !name.ends_with(['.', ' ']) && !name.contains(invalid_char) && !reserved } /// A successful command run. @@ -119,8 +176,8 @@ pub fn run_command( let argv = build_argv(command, placeholders)?; // `build_argv` guarantees a non-empty argv. let mut cmd = Command::new(&argv[0]); - cmd.args(&argv[1..]) - .current_dir(cwd) + push_args(&mut cmd, &argv[1..]); + cmd.current_dir(cwd) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .stdin(if stdin_payload.is_some() { @@ -193,6 +250,20 @@ pub fn run_command( } } +// One fn with cfg blocks rather than two cfg'd fns: cargo-mutants mutates +// the uncompiled twin too, and no test on a Linux gate can kill that mutant. +fn push_args(cmd: &mut Command, args: &[String]) { + #[cfg(windows)] + { + use std::os::windows::process::CommandExt; + for arg in args { + cmd.raw_arg(quote_windows_arg(arg)); + } + } + #[cfg(not(windows))] + cmd.args(args); +} + fn spawn_error(program: &str, source: std::io::Error, cwd: &Path) -> CommandError { // A missing cwd fails the child's chdir with `NotFound` too. if !searched_path(program, source.kind()) || !cwd.is_dir() { @@ -244,6 +315,35 @@ pub(crate) fn build_argv( Ok(argv) } +/// Quote `arg` for a Windows command line by the MS C-runtime rules, always +/// wrapped in double quotes: an MSYS/Cygwin child globs, brace-expands and +/// unescapes any unquoted word, while an MSVCRT child parses the quoted form +/// back to `arg` exactly. +#[cfg_attr(not(windows), allow(dead_code))] +pub(crate) fn quote_windows_arg(arg: &str) -> String { + let mut out = String::with_capacity(arg.len() + 2); + out.push('"'); + let mut backslashes = 0; + for ch in arg.chars() { + match ch { + '\\' => backslashes += 1, + '"' => { + out.extend(std::iter::repeat_n('\\', backslashes * 2 + 1)); + out.push('"'); + backslashes = 0; + } + _ => { + out.extend(std::iter::repeat_n('\\', backslashes)); + out.push(ch); + backslashes = 0; + } + } + } + out.extend(std::iter::repeat_n('\\', backslashes * 2)); + out.push('"'); + out +} + /// Replace every `{name}` in `token` with its placeholder value in a single /// left-to-right pass. Injected values are never re-scanned, so an untrusted /// value containing `{…}` is inert. Unknown `{…}` sequences are left literal. @@ -315,6 +415,117 @@ mod tests { build_argv(command, placeholders).expect("valid command") } + fn unchanged(path: &str) { + assert_eq!(non_verbatim(path), path); + } + + #[test] + fn non_verbatim_strips_a_verbatim_drive_path() { + assert_eq!( + non_verbatim(r"\\?\C:\Users\runner\in_cwd.txt"), + r"C:\Users\runner\in_cwd.txt" + ); + assert_eq!(non_verbatim(r"\\?\d:\x"), r"d:\x"); + assert_eq!(non_verbatim(r"\\?\C:\my file.txt"), r"C:\my file.txt"); + } + + #[test] + fn non_verbatim_strips_a_verbatim_drive_root() { + assert_eq!(non_verbatim(r"\\?\C:\"), r"C:\"); + } + + #[test] + fn non_verbatim_rewrites_a_verbatim_unc_path() { + assert_eq!( + non_verbatim(r"\\?\UNC\server\share\dir\f.txt"), + r"\\server\share\dir\f.txt" + ); + assert_eq!(non_verbatim(r"\\?\UNC\server\share"), r"\\server\share"); + } + + #[test] + fn non_verbatim_leaves_plain_paths_unchanged() { + unchanged(r"C:\Users\runner\f.txt"); + unchanged(r"\\server\share\f.txt"); + unchanged("/tmp/root/f.txt"); + unchanged("relative/f.txt"); + unchanged(""); + } + + #[test] + fn non_verbatim_leaves_verbatim_forms_without_a_drive_or_unc_unchanged() { + unchanged(r"\\?\GLOBALROOT\Device\HarddiskVolume1\f.txt"); + unchanged(r"\\?\Volume{0b1c2d3e-0000-0000-0000-100000000000}\f.txt"); + unchanged(r"\\?\C:"); + unchanged(r"\\?\C:x"); + unchanged(r"\\?\1:\x"); + unchanged(r"\\?\"); + } + + #[test] + fn non_verbatim_leaves_an_incomplete_unc_unchanged() { + unchanged(r"\\?\UNC\"); + unchanged(r"\\?\UNC\server"); + unchanged(r"\\?\UNC\server\"); + } + + #[test] + fn non_verbatim_leaves_components_win32_would_reinterpret_unchanged() { + unchanged(r"\\?\C:\a\..\b"); + unchanged(r"\\?\C:\a\.\b"); + unchanged(r"\\?\C:\a\\b"); + unchanged(r"\\?\C:\a\"); + unchanged(r"\\?\C:\trailing."); + unchanged(r"\\?\C:\trailing "); + unchanged(r"\\?\C:\a/b"); + unchanged(r"\\?\C:\a?b"); + unchanged(r"\\?\C:\a*b"); + unchanged(r"\\?\C:\a:b"); + unchanged(r#"\\?\C:\a"b"#); + unchanged(r"\\?\C:\ab"); + unchanged(r"\\?\C:\a|b"); + unchanged("\\\\?\\C:\\a\u{1}b"); + unchanged(r"\\?\UNC\server\share\..\x"); + } + + #[test] + fn non_verbatim_leaves_reserved_device_names_unchanged() { + for name in [ + "CON", "con", "PRN", "AUX", "NUL", "nul.txt", "NUL .txt", "COM1", "COM9", "LPT1", + "lpt9.log", + ] { + unchanged(&format!(r"\\?\C:\dir\{name}")); + } + } + + #[test] + fn non_verbatim_strips_names_that_only_resemble_device_names() { + for name in [ + "CONSOLE", "NULL.txt", "COM0", "COM10", "LPT", "xCON", "AUXa", + ] { + assert_eq!( + non_verbatim(&format!(r"\\?\C:\dir\{name}")), + format!(r"C:\dir\{name}") + ); + } + } + + #[test] + fn non_verbatim_strips_up_to_max_path_and_no_further() { + let fits = format!(r"C:\{}", "a".repeat(256)); + assert_eq!(fits.encode_utf16().count(), 259); + assert_eq!(non_verbatim(&format!(r"\\?\{fits}")), fits); + unchanged(&format!(r"\\?\C:\{}", "a".repeat(257))); + } + + #[test] + fn path_placeholder_carries_the_non_verbatim_value() { + let placeholder = Placeholder::path("path", r"\\?\C:\a\f.txt"); + assert_eq!(placeholder.name, "path"); + assert_eq!(placeholder.value, r"C:\a\f.txt"); + } + #[test] fn splits_on_whitespace_without_placeholders() { assert_eq!(argv("echo hello world", &[]), ["echo", "hello", "world"]); @@ -402,6 +613,47 @@ mod tests { assert_eq!(argv("run", &[Placeholder::new("path", "/tmp/x")]), ["run"]); } + #[test] + fn windows_quoting_wraps_a_plain_arg_in_quotes() { + assert_eq!(quote_windows_arg("hello"), r#""hello""#); + } + + #[test] + fn windows_quoting_keeps_an_empty_arg_as_an_empty_quoted_word() { + assert_eq!(quote_windows_arg(""), r#""""#); + } + + #[test] + fn windows_quoting_quotes_glob_and_brace_characters() { + assert_eq!(quote_windows_arg("[{}]"), r#""[{}]""#); + assert_eq!(quote_windows_arg("a?b*c"), r#""a?b*c""#); + } + + #[test] + fn windows_quoting_leaves_backslashes_not_before_a_quote_alone() { + assert_eq!( + quote_windows_arg(r"\\?\C:\Users\x.txt"), + r#""\\?\C:\Users\x.txt""# + ); + } + + #[test] + fn windows_quoting_doubles_trailing_backslashes_before_the_closing_quote() { + assert_eq!(quote_windows_arg(r"C:\dir\"), r#""C:\dir\\""#); + assert_eq!(quote_windows_arg(r"a\\"), r#""a\\\\""#); + } + + #[test] + fn windows_quoting_escapes_embedded_quotes() { + assert_eq!(quote_windows_arg(r#"[{"n":1}]"#), r#""[{\"n\":1}]""#); + } + + #[test] + fn windows_quoting_doubles_backslashes_before_an_embedded_quote() { + assert_eq!(quote_windows_arg(r#"a\"b"#), r#""a\\\"b""#); + assert_eq!(quote_windows_arg(r#"a\\"b"#), r#""a\\\\\"b""#); + } + #[test] fn extract_payload_takes_the_last_non_empty_line() { assert_eq!( diff --git a/packages/rust/src/extension_resolution.rs b/packages/rust/src/extension_resolution.rs index ab5f729c7..f21763fa1 100644 --- a/packages/rust/src/extension_resolution.rs +++ b/packages/rust/src/extension_resolution.rs @@ -512,7 +512,11 @@ mod tests { ); assert_eq!( err.to_string(), - "multiple loadable extension files found in package 'vec': /pkg/a.so, /pkg/z.so; disambiguate with a literal path" + format!( + "multiple loadable extension files found in package 'vec': {}, {}; disambiguate with a literal path", + dir.join("a.so").display(), + dir.join("z.so").display() + ) ); } } diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index 30479e5eb..78d5bce61 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -1938,8 +1938,8 @@ fn run_on_file( root: &Path, ) -> std::result::Result, BoxError> { let placeholders = [ - Placeholder::new("path", abs_path), - Placeholder::new("root", root.to_string_lossy().into_owned()), + Placeholder::path("path", abs_path), + Placeholder::path("root", &root.to_string_lossy()), ]; // Errors travel to the caller rather than being logged and flattened to an @@ -2936,7 +2936,12 @@ mod internal_tests { /// so `notify` never sees `.`. #[test] fn relative_root_canonicalizes_watch_root_only() { - let fake = FakeFs::default().with_canonical_root(".", "/ws/canonical"); + let canonical = if cfg!(windows) { + r"C:\ws\canonical" + } else { + "/ws/canonical" + }; + let fake = FakeFs::default().with_canonical_root(".", canonical); let db = DirSQL::with_ignore_and_fs( ".", vec![Table::new( @@ -2956,7 +2961,7 @@ mod internal_tests { "watch_root must be absolute, got {:?}", db.inner.watch_root ); - assert_eq!(db.inner.watch_root, PathBuf::from("/ws/canonical")); + assert_eq!(db.inner.watch_root, PathBuf::from(canonical)); } /// With an absolute root, `process_file_event` strips the `watch_root` @@ -4376,6 +4381,22 @@ mod internal_tests { ); } + #[test] + fn run_on_file_hands_the_hook_non_verbatim_path_and_root() { + let dir = TempDir::new().unwrap(); + run_on_file( + r#"sh -c 'printf "%s|%s" "$1" "$2" > seen; echo "[]"' sh {path} {root}"#, + r"\\?\C:\r\f.txt", + dir.path(), + Path::new(r"\\?\D:\r"), + ) + .expect("a well-formed payload parses"); + assert_eq!( + std::fs::read_to_string(dir.path().join("seen")).unwrap(), + r"C:\r\f.txt|D:\r" + ); + } + /// A command that cannot be spawned is an error the caller records, not an /// empty row set: a skip and a file that legitimately produced no rows must /// stay distinguishable. diff --git a/packages/rust/src/parsed_vtab.rs b/packages/rust/src/parsed_vtab.rs index 6b4b18a58..64b9c53a3 100644 --- a/packages/rust/src/parsed_vtab.rs +++ b/packages/rust/src/parsed_vtab.rs @@ -292,8 +292,8 @@ fn no_rows_message(pattern: &str) -> String { fn run_parser(command: &str, root: &Path, rel_path: &Path) -> std::result::Result { let abs_path = root.join(rel_path); let placeholders = [ - Placeholder::new("path", abs_path.to_string_lossy().into_owned()), - Placeholder::new("root", root.to_string_lossy().into_owned()), + Placeholder::path("path", &abs_path.to_string_lossy()), + Placeholder::path("root", &root.to_string_lossy()), ]; run_command(command, &placeholders, root, None) diff --git a/packages/rust/tests/path_table_on_file_e2e.rs b/packages/rust/tests/path_table_on_file_e2e.rs index 76e0f1a88..7468b1862 100644 --- a/packages/rust/tests/path_table_on_file_e2e.rs +++ b/packages/rust/tests/path_table_on_file_e2e.rs @@ -2,7 +2,7 @@ //! directory, a real parser script, nothing mocked. Pins the surface a user //! actually types: `dirsql query "" --on-file ''`. -#![cfg(feature = "cli")] +#![cfg(all(feature = "cli", unix))] use std::fs; use std::os::unix::fs::PermissionsExt; diff --git a/packages/rust/tests/vtab.rs b/packages/rust/tests/vtab.rs index 19d957c53..6495fe074 100644 --- a/packages/rust/tests/vtab.rs +++ b/packages/rust/tests/vtab.rs @@ -5,7 +5,6 @@ use std::fs; use std::io::Write; -use std::os::unix::fs::PermissionsExt; use std::path::Path; use dirsql::vtab::load_module; @@ -16,11 +15,18 @@ use tempfile::TempDir; /// enforces the result. A privileged process -- root, or anything holding /// `CAP_DAC_OVERRIDE` -- reads the file regardless, so the unreadable /// precondition cannot hold there and the caller has nothing to assert. +#[cfg(unix)] fn make_unreadable(path: &Path) -> bool { + use std::os::unix::fs::PermissionsExt; fs::set_permissions(path, fs::Permissions::from_mode(0o000)).unwrap(); fs::read(path).is_err() } +#[cfg(not(unix))] +fn make_unreadable(_path: &Path) -> bool { + false +} + /// A connection with the path-table module registered and one vtab named `t` /// spanning `glob` under `dir`. fn open_over(dir: &TempDir, glob: &str) -> Connection { diff --git a/packages/ts/e2e-attestations/claude-1207-win-argv-quote.json b/packages/ts/e2e-attestations/claude-1207-win-argv-quote.json new file mode 100644 index 000000000..7995af5a2 --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1207-win-argv-quote.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1790939689, + "exit_code": 0, + "commit": "d2e6cb09a48afa50f165ca6466233e0446dc7de7", + "branch": "claude/1207-win-argv-quote" +} diff --git a/packages/ts/e2e-attestations/claude-1208-win-nonverbatim-paths.json b/packages/ts/e2e-attestations/claude-1208-win-nonverbatim-paths.json new file mode 100644 index 000000000..14e33cf9d --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1208-win-nonverbatim-paths.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1790940093, + "exit_code": 0, + "commit": "b44665ee97a12c56691723ae9d5c3812f110d90c", + "branch": "claude/1208-win-nonverbatim-paths" +} diff --git a/packages/ts/e2e-attestations/claude-1213-win-test-fixtures.json b/packages/ts/e2e-attestations/claude-1213-win-test-fixtures.json new file mode 100644 index 000000000..a1963bbd7 --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1213-win-test-fixtures.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1790941675, + "exit_code": 0, + "commit": "386b888bd87d3518af64bda2dfb958d98e403ad8", + "branch": "claude/1213-win-test-fixtures" +}