From 298123668ac007d8b1008021f26b3233d9200718 Mon Sep 17 00:00:00 2001 From: Daniel Bodnar <1790726+danielbodnar@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:24:48 -0600 Subject: [PATCH 1/5] refactor: move completions into subdirectory, add test suite - completions/dagger.nu: move from root, export nu-complete helpers - tests/completions.test.nu: 12 tests covering all completion helpers and registered extern commands (std/assert, no mutable closures) - README: update install path --- README.md | 10 +- dagger.nu => completions/dagger.nu | 16 +-- tests/completions.test.nu | 221 +++++++++++++++++++++++++++++ 3 files changed, 231 insertions(+), 16 deletions(-) rename dagger.nu => completions/dagger.nu (97%) create mode 100644 tests/completions.test.nu diff --git a/README.md b/README.md index e6378c1..ae1d7e7 100644 --- a/README.md +++ b/README.md @@ -35,16 +35,10 @@ nupm install --path . ### Manual -Copy `dagger.nu` anywhere on your `NU_LIB_DIRS` path, then add to `config.nu`: +Clone the repo, then add to `config.nu`: ```nushell -use dagger.nu * -``` - -Or source it directly: - -```nushell -use /path/to/dagger.nu * +use /path/to/dagger.nu/completions/dagger.nu * ``` ## Usage diff --git a/dagger.nu b/completions/dagger.nu similarity index 97% rename from dagger.nu rename to completions/dagger.nu index 736997f..8101fd2 100644 --- a/dagger.nu +++ b/completions/dagger.nu @@ -10,7 +10,7 @@ # Completion Helpers # ============================================================================ -def "nu-complete dagger subcommands" [] { +export def "nu-complete dagger subcommands" [] { [ [value description]; [login "Log in to Dagger Cloud"] @@ -33,7 +33,7 @@ def "nu-complete dagger subcommands" [] { ] } -def "nu-complete dagger sdks" [] { +export def "nu-complete dagger sdks" [] { [ [value description]; [go "Go SDK"] @@ -42,7 +42,7 @@ def "nu-complete dagger sdks" [] { ] } -def "nu-complete dagger progress" [] { +export def "nu-complete dagger progress" [] { [ [value description]; [auto "Automatically detect output format (default)"] @@ -52,7 +52,7 @@ def "nu-complete dagger progress" [] { ] } -def "nu-complete dagger models" [] { +export def "nu-complete dagger models" [] { [ [value description]; [claude-sonnet-4-5 "Anthropic Claude Sonnet 4.5"] @@ -65,7 +65,7 @@ def "nu-complete dagger models" [] { ] } -def "nu-complete dagger licenses" [] { +export def "nu-complete dagger licenses" [] { [ [value description]; [Apache-2.0 "Apache License 2.0 (default)"] @@ -78,11 +78,11 @@ def "nu-complete dagger licenses" [] { ] } -def "nu-complete dagger shells" [] { +export def "nu-complete dagger shells" [] { [bash zsh fish powershell] } -def "nu-complete dagger compat" [] { +export def "nu-complete dagger compat" [] { [ [value description]; [latest "Use the latest engine API version (default)"] @@ -90,7 +90,7 @@ def "nu-complete dagger compat" [] { ] } -def "nu-complete dagger toolchain subcommands" [] { +export def "nu-complete dagger toolchain subcommands" [] { [ [value description]; [install "Install a toolchain to the current module"] diff --git a/tests/completions.test.nu b/tests/completions.test.nu new file mode 100644 index 0000000..a0e93b5 --- /dev/null +++ b/tests/completions.test.nu @@ -0,0 +1,221 @@ +#!/usr/bin/env nu +# tests/completions.test.nu — Test suite for dagger.nu completions +# +# Run: +# nu tests/completions.test.nu +# nu tests/completions.test.nu --verbose +# nu tests/completions.test.nu --test sdks + +use std/assert +use ../completions/dagger.nu * + +# ============================================================================ +# Helpers +# ============================================================================ + +# Assert a list contains an entry with the given value field +def assert-has-value [list: list, expected: string] { + let found = $list | where value == $expected + assert ($found | is-not-empty) $"Expected value '($expected)' not found in completions" +} + +# Assert every entry in a completion list has non-empty value and description +def assert-well-formed [list: list] { + assert ($list | is-not-empty) "Completion list must not be empty" + for entry in $list { + assert (($entry.value | str length) > 0) $"Empty value in entry: ($entry | to nuon)" + assert (($entry.description | str length) > 0) $"Empty description for value '($entry.value)'" + } +} + +# ============================================================================ +# Tests +# ============================================================================ + +def "test subcommands" [] { + let result = nu-complete dagger subcommands + assert-well-formed $result + + for cmd in [login logout call config core develop functions init install uninstall update query run completion toolchain version help] { + assert-has-value $result $cmd + } +} + +def "test sdks" [] { + let result = nu-complete dagger sdks + assert-well-formed $result + assert (($result | length) == 3) + + assert-has-value $result "go" + assert-has-value $result "python" + assert-has-value $result "typescript" +} + +def "test progress formats" [] { + let result = nu-complete dagger progress + assert-well-formed $result + assert (($result | length) == 4) + + for fmt in [auto plain tty dots] { + assert-has-value $result $fmt + } +} + +def "test models" [] { + let result = nu-complete dagger models + assert-well-formed $result + assert (($result | length) >= 4) + + # Spot-check a few well-known model IDs + assert-has-value $result "claude-sonnet-4-5" + assert-has-value $result "gpt-4.1" + assert-has-value $result "gemini-2.0-flash" +} + +def "test licenses" [] { + let result = nu-complete dagger licenses + assert-well-formed $result + + for lic in ["Apache-2.0" "MIT" "GPL-3.0" "BSD-3-Clause" "MPL-2.0" "UNLICENSED"] { + assert-has-value $result $lic + } +} + +def "test shells" [] { + let result = nu-complete dagger shells + assert (($result | length) == 4) + + for sh in [bash zsh fish powershell] { + assert ($sh in $result) $"Shell '($sh)' missing from completions" + } +} + +def "test compat values" [] { + let result = nu-complete dagger compat + assert-well-formed $result + assert (($result | length) == 2) + + assert-has-value $result "latest" + assert-has-value $result "skip" +} + +def "test toolchain subcommands" [] { + let result = nu-complete dagger toolchain subcommands + assert-well-formed $result + assert (($result | length) == 4) + + for sub in [install list uninstall update] { + assert-has-value $result $sub + } +} + +def "test subcommands are unique" [] { + let result = nu-complete dagger subcommands + let unique_count = $result | get value | uniq | length + assert ($unique_count == ($result | length)) "Subcommands list has duplicates" +} + +def "test models are unique" [] { + let result = nu-complete dagger models + let unique_count = $result | get value | uniq | length + assert ($unique_count == ($result | length)) "Models list has duplicates" +} + +def "test licenses are unique" [] { + let result = nu-complete dagger licenses + let unique_count = $result | get value | uniq | length + assert ($unique_count == ($result | length)) "Licenses list has duplicates" +} + +def "test registered commands" [] { + # Verify that extern commands are registered in scope after `use` + let cmds = scope commands | where name =~ "^dagger" | get name + + for expected in [ + "dagger" + "dagger call" + "dagger config" + "dagger develop" + "dagger functions" + "dagger init" + "dagger install" + "dagger login" + "dagger logout" + "dagger query" + "dagger run" + "dagger toolchain" + "dagger toolchain install" + "dagger toolchain list" + "dagger toolchain uninstall" + "dagger toolchain update" + "dagger version" + ] { + assert ($expected in $cmds) $"Command '($expected)' not registered" + } +} + +# ============================================================================ +# Runner +# ============================================================================ + +def main [ + --verbose (-v) # Print each test name before running + --test (-t): string # Run only tests matching this substring +] { + let all_tests = [ + "subcommands" + "sdks" + "progress formats" + "models" + "licenses" + "shells" + "compat values" + "toolchain subcommands" + "subcommands are unique" + "models are unique" + "licenses are unique" + "registered commands" + ] + + let tests = if $test != null { + $all_tests | where { $in =~ $test } + } else { + $all_tests + } + + print $"Running ($tests | length) tests...\n" + + let results = $tests | each { |name| + if $verbose { print -n $" test ($name)... " } + let outcome = try { + match $name { + "subcommands" => { test subcommands } + "sdks" => { test sdks } + "progress formats" => { test progress formats } + "models" => { test models } + "licenses" => { test licenses } + "shells" => { test shells } + "compat values" => { test compat values } + "toolchain subcommands" => { test toolchain subcommands } + "subcommands are unique" => { test subcommands are unique } + "models are unique" => { test models are unique } + "licenses are unique" => { test licenses are unique } + "registered commands" => { test registered commands } + } + if $verbose { print "✓" } + {name: $name, passed: true, error: ""} + } catch { |err| + if not $verbose { print -n $" test ($name)... " } + print $"✗ ($err.msg)" + {name: $name, passed: false, error: $err.msg} + } + $outcome + } + + let passed = $results | where passed == true | length + let failed = $results | where passed == false | length + + print $"\n($passed) passed, ($failed) failed" + + if $failed > 0 { exit 1 } +} From c9be66262a79ed9af3d269cfe02b0a7d89ed716d Mon Sep 17 00:00:00 2001 From: Daniel Bodnar <1790726+danielbodnar@users.noreply.github.com> Date: Sat, 28 Feb 2026 12:28:47 -0600 Subject: [PATCH 2/5] fix(tests): export nu-complete helpers, use top-level use in test file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nu-complete helpers are exported (export def) — appropriate for a completions module where helpers are part of the public API. Tests use top-level `use` so scope commands reflects parser-time scope and helpers are callable without subshell gymnastics. Also adds mod.nu entry point following nushell-modules convention. --- mod.nu | 9 ++ tests/completions.test.nu | 236 ++++++++++++++++++++------------------ 2 files changed, 132 insertions(+), 113 deletions(-) create mode 100644 mod.nu diff --git a/mod.nu b/mod.nu new file mode 100644 index 0000000..9cc3918 --- /dev/null +++ b/mod.nu @@ -0,0 +1,9 @@ +# dagger.nu - Nushell completions and utilities for the Dagger CLI +# +# Usage: +# use /path/to/dagger.nu/mod.nu * +# +# Or with nupm (once published): +# nupm install dagger.nu + +export use completions/dagger.nu * diff --git a/tests/completions.test.nu b/tests/completions.test.nu index a0e93b5..35251f6 100644 --- a/tests/completions.test.nu +++ b/tests/completions.test.nu @@ -4,177 +4,186 @@ # Run: # nu tests/completions.test.nu # nu tests/completions.test.nu --verbose -# nu tests/completions.test.nu --test sdks +# nu tests/completions.test.nu --test "sdk" use std/assert + +# Top-level use — brings externs and helpers into parser scope so +# scope commands reflects them and helper calls resolve without a subshell. use ../completions/dagger.nu * # ============================================================================ # Helpers # ============================================================================ -# Assert a list contains an entry with the given value field def assert-has-value [list: list, expected: string] { let found = $list | where value == $expected - assert ($found | is-not-empty) $"Expected value '($expected)' not found in completions" + assert ($found | is-not-empty) $"Expected value '($expected)' not found" } -# Assert every entry in a completion list has non-empty value and description -def assert-well-formed [list: list] { - assert ($list | is-not-empty) "Completion list must not be empty" - for entry in $list { - assert (($entry.value | str length) > 0) $"Empty value in entry: ($entry | to nuon)" - assert (($entry.description | str length) > 0) $"Empty description for value '($entry.value)'" - } +def assert-unique-values [list: list, label: string] { + let dupes = $list | get value | uniq -d + assert ($dupes | is-empty) $"($label) has duplicate values: ($dupes | to nuon)" } # ============================================================================ -# Tests +# Helper function tests — completion values # ============================================================================ -def "test subcommands" [] { - let result = nu-complete dagger subcommands - assert-well-formed $result - - for cmd in [login logout call config core develop functions init install uninstall update query run completion toolchain version help] { - assert-has-value $result $cmd - } -} - def "test sdks" [] { let result = nu-complete dagger sdks - assert-well-formed $result assert (($result | length) == 3) - - assert-has-value $result "go" - assert-has-value $result "python" - assert-has-value $result "typescript" + for v in ["go" "python" "typescript"] { assert-has-value $result $v } + assert-unique-values $result "sdks" } def "test progress formats" [] { let result = nu-complete dagger progress - assert-well-formed $result assert (($result | length) == 4) - - for fmt in [auto plain tty dots] { - assert-has-value $result $fmt - } + for v in ["auto" "plain" "tty" "dots"] { assert-has-value $result $v } + assert-unique-values $result "progress" } -def "test models" [] { - let result = nu-complete dagger models - assert-well-formed $result - assert (($result | length) >= 4) +def "test compat values" [] { + let result = nu-complete dagger compat + assert (($result | length) == 2) + for v in ["latest" "skip"] { assert-has-value $result $v } +} - # Spot-check a few well-known model IDs - assert-has-value $result "claude-sonnet-4-5" - assert-has-value $result "gpt-4.1" - assert-has-value $result "gemini-2.0-flash" +def "test shell names" [] { + let result = nu-complete dagger shells + assert (($result | length) == 4) + for v in ["bash" "zsh" "fish" "powershell"] { + assert ($v in $result) $"Shell '($v)' missing" + } } -def "test licenses" [] { +def "test license spdx identifiers" [] { let result = nu-complete dagger licenses - assert-well-formed $result - - for lic in ["Apache-2.0" "MIT" "GPL-3.0" "BSD-3-Clause" "MPL-2.0" "UNLICENSED"] { - assert-has-value $result $lic + for v in ["Apache-2.0" "MIT" "GPL-3.0" "BSD-3-Clause" "MPL-2.0"] { + assert-has-value $result $v } + assert-unique-values $result "licenses" } -def "test shells" [] { - let result = nu-complete dagger shells - assert (($result | length) == 4) - - for sh in [bash zsh fish powershell] { - assert ($sh in $result) $"Shell '($sh)' missing from completions" +def "test model ids" [] { + let result = nu-complete dagger models + assert (($result | length) >= 4) + for v in ["claude-sonnet-4-5" "gpt-4.1" "gemini-2.0-flash"] { + assert-has-value $result $v } + assert-unique-values $result "models" } -def "test compat values" [] { - let result = nu-complete dagger compat - assert-well-formed $result - assert (($result | length) == 2) - - assert-has-value $result "latest" - assert-has-value $result "skip" +def "test subcommands" [] { + let result = nu-complete dagger subcommands + for v in ["init" "develop" "call" "config" "install" "uninstall" "update" + "query" "run" "login" "logout" "version" "toolchain" "completion"] { + assert-has-value $result $v + } + assert-unique-values $result "subcommands" } def "test toolchain subcommands" [] { let result = nu-complete dagger toolchain subcommands - assert-well-formed $result assert (($result | length) == 4) - - for sub in [install list uninstall update] { - assert-has-value $result $sub - } -} - -def "test subcommands are unique" [] { - let result = nu-complete dagger subcommands - let unique_count = $result | get value | uniq | length - assert ($unique_count == ($result | length)) "Subcommands list has duplicates" + for v in ["install" "list" "uninstall" "update"] { assert-has-value $result $v } + assert-unique-values $result "toolchain subcommands" } -def "test models are unique" [] { - let result = nu-complete dagger models - let unique_count = $result | get value | uniq | length - assert ($unique_count == ($result | length)) "Models list has duplicates" +def "test helpers have descriptions" [] { + # Every table-form helper should have non-empty descriptions + let helpers = [ + (nu-complete dagger subcommands) + (nu-complete dagger sdks) + (nu-complete dagger progress) + (nu-complete dagger models) + (nu-complete dagger licenses) + (nu-complete dagger compat) + (nu-complete dagger toolchain subcommands) + ] + for list in $helpers { + for entry in $list { + assert (($entry.description | str length) > 0) $"Empty description for '($entry.value)'" + } + } } -def "test licenses are unique" [] { - let result = nu-complete dagger licenses - let unique_count = $result | get value | uniq | length - assert ($unique_count == ($result | length)) "Licenses list has duplicates" -} +# ============================================================================ +# Extern registration tests — observable module surface +# ============================================================================ -def "test registered commands" [] { - # Verify that extern commands are registered in scope after `use` +def "test top-level commands registered" [] { let cmds = scope commands | where name =~ "^dagger" | get name + for expected in [ + "dagger" "dagger call" "dagger config" "dagger core" + "dagger develop" "dagger functions" "dagger init" + "dagger install" "dagger uninstall" "dagger update" + "dagger login" "dagger logout" + "dagger query" "dagger q" "dagger run" + "dagger completion" "dagger version" + ] { + assert ($expected in $cmds) $"'($expected)' not registered after use" + } +} +def "test toolchain commands registered" [] { + let cmds = scope commands | where name =~ "^dagger toolchain" | get name for expected in [ - "dagger" - "dagger call" - "dagger config" - "dagger develop" - "dagger functions" - "dagger init" - "dagger install" - "dagger login" - "dagger logout" - "dagger query" - "dagger run" "dagger toolchain" "dagger toolchain install" "dagger toolchain list" "dagger toolchain uninstall" "dagger toolchain update" - "dagger version" ] { - assert ($expected in $cmds) $"Command '($expected)' not registered" + assert ($expected in $cmds) $"'($expected)' not registered after use" } } +def "test no duplicate externs" [] { + let cmds = scope commands | where name =~ "^dagger" | get name + let dupes = $cmds | uniq -d + assert ($dupes | is-empty) $"Duplicate externs: ($dupes | to nuon)" +} + +# ============================================================================ +# mod.nu integration test +# ============================================================================ + +def "test mod.nu entry point" [] { + # Verify the root mod.nu re-exports everything from completions/ + # We load it in a subshell to get an isolated scope reading + let out = ^nu --no-config-file -c $" + use '/workspaces/code/github.com/danielbodnar/dagger.nu/mod.nu' * + scope commands | where name =~ '^dagger' | get name | to json + " | from json + assert ($out | is-not-empty) "mod.nu must re-export dagger commands" + assert ("dagger" in $out) "'dagger' extern must be reachable via mod.nu" + assert ("dagger init" in $out) "'dagger init' must be reachable via mod.nu" +} + # ============================================================================ # Runner # ============================================================================ def main [ - --verbose (-v) # Print each test name before running - --test (-t): string # Run only tests matching this substring + --verbose (-v) # Print each test name as it runs + --test (-t): string # Run only tests whose name contains this substring ] { let all_tests = [ - "subcommands" "sdks" "progress formats" - "models" - "licenses" - "shells" "compat values" + "shell names" + "license spdx identifiers" + "model ids" + "subcommands" "toolchain subcommands" - "subcommands are unique" - "models are unique" - "licenses are unique" - "registered commands" + "helpers have descriptions" + "top-level commands registered" + "toolchain commands registered" + "no duplicate externs" + "mod.nu entry point" ] let tests = if $test != null { @@ -189,18 +198,19 @@ def main [ if $verbose { print -n $" test ($name)... " } let outcome = try { match $name { - "subcommands" => { test subcommands } - "sdks" => { test sdks } - "progress formats" => { test progress formats } - "models" => { test models } - "licenses" => { test licenses } - "shells" => { test shells } - "compat values" => { test compat values } - "toolchain subcommands" => { test toolchain subcommands } - "subcommands are unique" => { test subcommands are unique } - "models are unique" => { test models are unique } - "licenses are unique" => { test licenses are unique } - "registered commands" => { test registered commands } + "sdks" => { test sdks } + "progress formats" => { test progress formats } + "compat values" => { test compat values } + "shell names" => { test shell names } + "license spdx identifiers" => { test license spdx identifiers } + "model ids" => { test model ids } + "subcommands" => { test subcommands } + "toolchain subcommands" => { test toolchain subcommands } + "helpers have descriptions" => { test helpers have descriptions } + "top-level commands registered" => { test top-level commands registered } + "toolchain commands registered" => { test toolchain commands registered } + "no duplicate externs" => { test no duplicate externs } + "mod.nu entry point" => { test mod.nu entry point } } if $verbose { print "✓" } {name: $name, passed: true, error: ""} From 85f79bb3716386e7e5fa229cf59339686b434eec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:57:36 +0000 Subject: [PATCH 3/5] Initial plan From 6c3dff3be713eb338c1d028fbd53187ba173dfa7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 21 Mar 2026 03:58:54 +0000 Subject: [PATCH 4/5] fix: dynamic mod.nu path in tests, recommend mod.nu in README Co-authored-by: danielbodnar <1790726+danielbodnar@users.noreply.github.com> Agent-Logs-Url: https://github.com/danielbodnar/dagger.nu/sessions/af9bae3d-4462-46d8-9910-dfabe586c1a5 --- README.md | 6 ++++++ tests/completions.test.nu | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ae1d7e7..ae84d78 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,12 @@ nupm install --path . Clone the repo, then add to `config.nu`: +```nushell +use /path/to/dagger.nu/mod.nu * +``` + +Or, if you prefer to load only the completions file directly: + ```nushell use /path/to/dagger.nu/completions/dagger.nu * ``` diff --git a/tests/completions.test.nu b/tests/completions.test.nu index 35251f6..ef5774a 100644 --- a/tests/completions.test.nu +++ b/tests/completions.test.nu @@ -153,8 +153,9 @@ def "test no duplicate externs" [] { def "test mod.nu entry point" [] { # Verify the root mod.nu re-exports everything from completions/ # We load it in a subshell to get an isolated scope reading + let mod-path = ([$env.FILE_PWD ".." "mod.nu"] | path join | path expand) let out = ^nu --no-config-file -c $" - use '/workspaces/code/github.com/danielbodnar/dagger.nu/mod.nu' * + use '($mod-path)' * scope commands | where name =~ '^dagger' | get name | to json " | from json assert ($out | is-not-empty) "mod.nu must re-export dagger commands" From 5cba432f012dfe7eb2cf5390a8d0f403d8c639b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 22 Mar 2026 08:23:57 +0000 Subject: [PATCH 5/5] fix(tests): rename mod-path to mod_path for snake_case consistency Co-authored-by: danielbodnar <1790726+danielbodnar@users.noreply.github.com> Agent-Logs-Url: https://github.com/danielbodnar/dagger.nu/sessions/3240f946-4491-400e-9c07-4ea1ddf3a298 --- tests/completions.test.nu | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/completions.test.nu b/tests/completions.test.nu index ef5774a..171ded8 100644 --- a/tests/completions.test.nu +++ b/tests/completions.test.nu @@ -153,9 +153,9 @@ def "test no duplicate externs" [] { def "test mod.nu entry point" [] { # Verify the root mod.nu re-exports everything from completions/ # We load it in a subshell to get an isolated scope reading - let mod-path = ([$env.FILE_PWD ".." "mod.nu"] | path join | path expand) + let mod_path = ([$env.FILE_PWD ".." "mod.nu"] | path join | path expand) let out = ^nu --no-config-file -c $" - use '($mod-path)' * + use '($mod_path)' * scope commands | where name =~ '^dagger' | get name | to json " | from json assert ($out | is-not-empty) "mod.nu must re-export dagger commands"