From 905e992b4cef7c68a5190953e227d1a722178f36 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 02:45:00 +0000 Subject: [PATCH 1/6] fix: embed output for a capture point on the first line embeds-update inserted each result by matching "\n" + line + "\n", but the first line of a script has no leading newline, so its replacement silently no-op'd even though the output was computed and zipped. Seed the fold with a prepended newline to anchor the first line like any interior line, then strip that one newline back off. Add a regression test. Co-Authored-By: Claude Opus 4.8 (1M context) --- dotnu/commands.nu | 5 ++++- tests/test_commands.nu | 9 +++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/dotnu/commands.nu b/dotnu/commands.nu index 22c1863..c881aa2 100644 --- a/dotnu/commands.nu +++ b/dotnu/commands.nu @@ -222,9 +222,12 @@ export def 'embeds-update' [ let prevent_second_replacement = " # to-not-be-replaced-again" $replacements - | reduce --fold $script {|it| + # Why: prepend a newline so a capture point on the very first line is anchored + # by "\n" on both sides like any interior line; without it the match silently no-ops + | reduce --fold ("\n" + $script) {|it| str replace ("\n" + $it.0 + "\n") ("\n" + $it.0 + $prevent_second_replacement + "\n" + $it.1 + "\n") } + | str replace -r '^\n' '' # strip the single leading newline added above | str replace -a $prevent_second_replacement '' | str replace -ar '\n{3,}' "\n\n" | str replace -r "\n*$" "\n" diff --git a/tests/test_commands.nu b/tests/test_commands.nu index 02d0e18..5ce73d2 100644 --- a/tests/test_commands.nu +++ b/tests/test_commands.nu @@ -393,6 +393,15 @@ def "embeds-update updates embeds in piped script" [] { assert ($result =~ '# => HELLO') } +@test +def "embeds-update annotates a capture point on the first line" [] { + # No leading newline before the capture point + let script = "'hello' | str upcase | print $in\n" + let result = $script | embeds-update + + assert ($result =~ '# => HELLO') +} + @test def "embeds-update preserves script structure" [] { let script = "# comment\n\n1 + 1 | print $in\n\n# another" From 47422c4679601eaf529687e334721f7a6912d237 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 02:58:29 +0000 Subject: [PATCH 2/6] fix: forward-fill command names without std `scan` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `module-commands-code-to-record` used `scan --noinit null` to forward-fill the last-seen command name down the lines of a module. Nushell's std `scan` now delegates to `generate`, which treats a `null` seed as "no initial value" and errors out — so the command (and its two unit tests) broke entirely. dotnu can't patch std, so stop seeding `scan` with null: forward-fill the last non-null name manually with a `reduce` instead. `[] | last` returns null on the empty accumulator, so no optional handling is needed. Drops the now-dead `use std/iter scan` import. Co-Authored-By: Claude Opus 4.8 (1M context) --- dotnu/commands.nu | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/dotnu/commands.nu b/dotnu/commands.nu index c881aa2..f9031f3 100644 --- a/dotnu/commands.nu +++ b/dotnu/commands.nu @@ -4,8 +4,6 @@ # Public API is controlled by mod.nu which selectively re-exports user-facing commands. # To make a command public, add it to the export list in mod.nu. -use std/iter scan - # Check .nu module files to determine which commands depend on other commands. @example 'Analyze command dependencies in a module' { dotnu dependencies ...(glob tests/assets/module-say/say/*.nu) @@ -805,8 +803,11 @@ export def 'module-commands-code-to-record' [ } | merge ( $in.command - | scan --noinit null {|i acc| - if $i == null { $acc } else { $i } + # Why: std `scan` with a null seed now errors — its internal `generate` treats + # null as "no initial value". Forward-fill the last non-null name manually instead. + | reduce --fold [] {|i acc| + let prev = $acc | last + $acc | append (if $i == null { $prev } else { $i }) } | wrap command ) From 5c884ee915a0b7ddeb521f62a5ad37533619065d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 02:59:49 +0000 Subject: [PATCH 3/6] fix: single source of truth for the capture-point regex MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `find-capture-points` (the detector) matched `\|\s?print \$in *$` while `execute-and-parse-results` (the rewriter) matched `\| *print +\$in *`. The two disagreed on real inputs — e.g. two spaces after `|`, which the detector missed but the rewriter still rewrote, and `foo | print $in | bar`, which the rewriter would rewrite but the detector ignored. Since `embeds-update` zips the detector's points against the rewriter's outputs, any disagreement silently embeds output against the wrong line. Extract one module-level `const capture_point` used by both. Anchor it to end-of-line so a mid-pipeline `| print $in` is consistently treated as not a capture point by both sides. Co-Authored-By: Claude Opus 4.8 (1M context) --- dotnu/commands.nu | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/dotnu/commands.nu b/dotnu/commands.nu index f9031f3..a88f39d 100644 --- a/dotnu/commands.nu +++ b/dotnu/commands.nu @@ -4,6 +4,12 @@ # Public API is controlled by mod.nu which selectively re-exports user-facing commands. # To make a command public, add it to the export list in mod.nu. +# Regex matching a capture point: a pipeline line ending in `| print $in`. +# Why: `find-capture-points` and `execute-and-parse-results` must agree on what a +# capture point is — they are zipped together in `embeds-update`, so any disagreement +# misaligns outputs against the wrong lines. +const capture_point = '\|\s*print\s+\$in\s*$' + # Check .nu module files to determine which commands depend on other commands. @example 'Analyze command dependencies in a module' { dotnu dependencies ...(glob tests/assets/module-say/say/*.nu) @@ -962,7 +968,7 @@ export def execute-and-parse-results [ | each { if $in !~ '^\s*#' { # don't search for `print $in` inside of commented lines - str replace -r '\| *print +\$in *' '| embed-in-script' + str replace -r $capture_point '| embed-in-script' } else { } } | prepend $embed_in_script_src @@ -982,7 +988,7 @@ export def execute-and-parse-results [ # Finds lines where embed-in-script is used in the script export def find-capture-points [] { lines - | where $it !~ '^\s*#' and $it =~ '\|\s?print \$in *$' + | where $it !~ '^\s*#' and $it =~ $capture_point } # Removes annotation lines starting with "# => " from the script From f1eac60ff49495113e014aa886ea0e6109b69088 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 03:00:38 +0000 Subject: [PATCH 4/6] fix: don't report untracked snapshots as passed MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `run-snapshot-test` decided pass/changed with `git diff --quiet`, which only considers tracked files. A brand-new snapshot file is untracked, so its first run produced no diff and was reported 'passed' — a false green that hides an unreviewed baseline. Check `git ls-files --error-unmatch` first: an untracked output is 'changed', so it surfaces in the summary and `--update` stages it like any other change. Co-Authored-By: Claude Opus 4.8 (1M context) --- toolkit.nu | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/toolkit.nu b/toolkit.nu index a6bd9bf..c268da4 100644 --- a/toolkit.nu +++ b/toolkit.nu @@ -177,9 +177,17 @@ def run-snapshot-test [name: string output_file: string command_src: closure] { $command_text + (char nl) + (do $command_src) | save -f $output_file - # Check git diff to determine status - let diff_result = do { ^git diff --quiet $output_file } | complete - let status = if $diff_result.exit_code == 0 { 'passed' } else { 'changed' } + # Check git diff to determine status. + # Why: `git diff` ignores untracked files, so a brand-new snapshot would + # report 'passed' without ever being compared to a baseline. Treat an + # untracked file as 'changed' so it surfaces and `--update` stages it. + let tracked = do { ^git ls-files --error-unmatch $output_file } | complete | get exit_code | $in == 0 + let status = if not $tracked { + 'changed' + } else { + let diff_result = do { ^git diff --quiet $output_file } | complete + if $diff_result.exit_code == 0 { 'passed' } else { 'changed' } + } {type: 'integration' name: $name status: $status file: $output_file} } catch {|err| From 850b4e66c4c5ddfd7a03bdb2d5560e8b703d6e61 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 03:09:45 +0000 Subject: [PATCH 5/6] test: make the embeds-update snapshot deterministic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The embeds-update / embeds-remove integration tests ran on a fixture containing `random int` and `ls | sort-by modified` — random and filesystem-dependent — so the snapshot could never match and was flagged 'changed' on every run. Replace those two capture points with deterministic equivalents (`40 + 2` and a literal table) and keep the multi-line str-replace pipeline. Coverage is unchanged: scalar capture, multi-line table capture, and multi-physical-line pipeline are all still exercised, and the subprocess `table -e` is width-stable. The input `# =>` values are intentionally stale/wrong (`0`, `stale`, `WRONG`). Why: `embeds-update` strips every `# =>` then re-executes and re-embeds, so a fresh value can only appear if the round-trip actually ran. Wrong inputs mean a silent no-op or a skipped capture point would leave the wrong value and fail the snapshot — keeping the end-to-end test honest under determinism. Co-Authored-By: Claude Opus 4.8 (1M context) --- tests/assets/dotnu-capture-clean.nu | 4 ++-- tests/assets/dotnu-capture-updated.nu | 18 +++++++++--------- tests/assets/dotnu-capture.nu | 13 +++++-------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/tests/assets/dotnu-capture-clean.nu b/tests/assets/dotnu-capture-clean.nu index c573cf4..8d8aca9 100644 --- a/tests/assets/dotnu-capture-clean.nu +++ b/tests/assets/dotnu-capture-clean.nu @@ -7,9 +7,9 @@ # And and this is the link on dotnu module: # https://github.com/nushell-prophet/dotnu -ls | sort-by modified -r | last 2 | print $in +40 + 2 | print $in -random int | print $in +[[name type]; [foo file] [bar dir]] | print $in 'Say hello to the core team of the Nushell' | str replace 'Nushell' 'Best shell' diff --git a/tests/assets/dotnu-capture-updated.nu b/tests/assets/dotnu-capture-updated.nu index fa10eec..3c568e1 100644 --- a/tests/assets/dotnu-capture-updated.nu +++ b/tests/assets/dotnu-capture-updated.nu @@ -6,16 +6,16 @@ # And and this is the link on dotnu module: # https://github.com/nushell-prophet/dotnu -ls | sort-by modified -r | last 2 | print $in -# => ╭───┬──────────────────────────────┬──────┬───────┬────────────╮ -# => │ # │ name │ type │ size │ modified │ -# => ├───┼──────────────────────────────┼──────┼───────┼────────────┤ -# => │ 0 │ set-x-demo.nu │ file │ 41 B │ a year ago │ -# => │ 1 │ parsing-pipe-in-docstring.nu │ file │ 923 B │ a year ago │ -# => ╰───┴──────────────────────────────┴──────┴───────┴────────────╯ +40 + 2 | print $in +# => 42 -random int | print $in -# => 2835756183325042638 +[[name type]; [foo file] [bar dir]] | print $in +# => ╭───┬──────┬──────╮ +# => │ # │ name │ type │ +# => ├───┼──────┼──────┤ +# => │ 0 │ foo │ file │ +# => │ 1 │ bar │ dir │ +# => ╰───┴──────┴──────╯ 'Say hello to the core team of the Nushell' | str replace 'Nushell' 'Best shell' diff --git a/tests/assets/dotnu-capture.nu b/tests/assets/dotnu-capture.nu index 3dc2917..29eb60d 100644 --- a/tests/assets/dotnu-capture.nu +++ b/tests/assets/dotnu-capture.nu @@ -5,16 +5,13 @@ # And and this is the link on dotnu module: # https://github.com/nushell-prophet/dotnu -ls | sort-by modified -r | last 2 | print $in -# => ╭─#─┬──────name──────┬─type─┬─size──┬───modified───╮ -# => │ 0 │ zzz_md_backups │ dir │ 160 B │ 2 months ago │ -# => │ 1 │ test.nu │ file │ 45 B │ 3 months ago │ -# => ╰─#─┴──────name──────┴─type─┴─size──┴───modified───╯ +40 + 2 | print $in +# => 0 -random int | print $in -# => 6970240173764648305 +[[name type]; [foo file] [bar dir]] | print $in +# => stale 'Say hello to the core team of the Nushell' | str replace 'Nushell' 'Best shell' | print $in -# => Say hello to the core team of the Best shell +# => WRONG From b429bdb0340d5528bac2d264fe21d9bf3d97bec3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 31 May 2026 03:10:57 +0000 Subject: [PATCH 6/6] test: make coverage snapshot format-robust (yaml -> nuon) The coverage snapshot drifted between nushell versions purely because `to yaml` changed how it indents nested scalar lists (`- x` vs ` - x`); the data was identical. Serialize the coverage record as nuon instead, so the snapshot compares the parsed structure rather than yaml's incidental formatting. Renames the fixture coverage-untested.yaml -> .nuon (kept in output-yaml/). Co-Authored-By: Claude Opus 4.8 (1M context) --- ...e-untested.yaml => coverage-untested.nuon} | 21 ++++++++++++------- toolkit.nu | 8 ++++--- 2 files changed, 18 insertions(+), 11 deletions(-) rename tests/output-yaml/{coverage-untested.yaml => coverage-untested.nuon} (62%) diff --git a/tests/output-yaml/coverage-untested.yaml b/tests/output-yaml/coverage-untested.nuon similarity index 62% rename from tests/output-yaml/coverage-untested.yaml rename to tests/output-yaml/coverage-untested.nuon index 9e9463b..b5b5d2d 100644 --- a/tests/output-yaml/coverage-untested.yaml +++ b/tests/output-yaml/coverage-untested.nuon @@ -13,16 +13,21 @@ # | where caller in $public_api # | select caller # -# # Output as yaml +# # Why: serialize as nuon, not yaml — `to yaml`'s list indentation +# # changed between nushell versions and drifted this snapshot on +# # identical data. nuon compares the parsed structure stably. # { # public_api_count: ($public_api | length) # tested_count: (($public_api | length) - ($untested | length)) # untested: ($untested | get caller) # } -# | to yaml -public_api_count: 15 -tested_count: 12 -untested: -- embed-add -- embeds-capture-start -- embeds-capture-stop +# | to nuon --indent 2 +{ + public_api_count: 15, + tested_count: 12, + untested: [ + embed-add, + embeds-capture-start, + embeds-capture-stop + ] +} \ No newline at end of file diff --git a/toolkit.nu b/toolkit.nu index c268da4..9545a8d 100644 --- a/toolkit.nu +++ b/toolkit.nu @@ -103,7 +103,7 @@ export def 'main test-integration' [ } ) ( - run-snapshot-test 'coverage' ([tests output-yaml coverage-untested.yaml] | path join) { + run-snapshot-test 'coverage' ([tests output-yaml coverage-untested.nuon] | path join) { # Public API from mod.nu let public_api = open ([dotnu mod.nu] | path join) | lines @@ -119,13 +119,15 @@ export def 'main test-integration' [ | where caller in $public_api | select caller - # Output as yaml + # Why: serialize as nuon, not yaml — `to yaml`'s list indentation + # changed between nushell versions and drifted this snapshot on + # identical data. nuon compares the parsed structure stably. { public_api_count: ($public_api | length) tested_count: (($public_api | length) - ($untested | length)) untested: ($untested | get caller) } - | to yaml + | to nuon --indent 2 } ) ]