{"slug": "an-ai-agent-skill-for-writing-tests-in-the-nushell-repo", "title": "An AI agent skill for writing tests in the `nushell` repo.", "summary": "A developer created an AI agent skill for writing tests in the Nushell repository, emphasizing the use of modern `nu_test_support` infrastructure over older subprocess-style testing. The skill provides guidelines for in-process tests, direct value assertions, and explicit dependencies.", "body_md": "| name | nushell-testing | ||||||\n|---|---|---|---|---|---|---|---|\n| description | Use when writing or refactoring Nushell Rust tests that use nu_test_support, NuTester, Playground, rstest, deps, plugins, CompleteResult, or when replacing old nu!/nu_with_plugins! tests. | ||||||\n| license | MIT | ||||||\n| compatibility | opencode | ||||||\n| metadata |\n|\n\nUse this skill when writing new tests or refactoring old tests in the Nushell repository. The goal is to use the modern `nu_test_support`\n\ninfrastructure directly, keep tests small and explicit, and avoid subprocess-style testing unless the behavior under test truly crosses the `nu`\n\nbinary boundary.\n\nThe most important rule: do not use `nu!`\n\nin new tests. Do not introduce `nu!`\n\n, `nu_with_plugins!`\n\n, `nu_with_std!`\n\n, or `nu_repl_code`\n\n. Some of these APIs may still exist for compatibility in older branches, but new and refactored tests should use `test()`\n\nand `NuTester`\n\ninstead.\n\nPrefer in-process tests. `test()`\n\nreturns a `NuTester`\n\nthat runs Nushell code in-process against the current crate code. This is faster, more deterministic, and avoids stale `nu`\n\nbinaries.\n\nAssert values, not text rendering. If a command returns a list, record, bool, int, table, duration, or structured error, assert that structured value directly. Do not add `to json`\n\n, `to nuon`\n\n, `to text`\n\n, `str join`\n\n, or manual output collapsing just to make assertion easier.\n\nMake required external artifacts explicit. If a test needs a compiled `nu`\n\nbinary, use `#[deps(NU)]`\n\n. If it needs a plugin, use `#[deps(NU_PLUGIN_EXAMPLE)]`\n\nor the appropriate plugin dependency. Do not call `add_nu_to_path()`\n\nin new code.\n\nKeep tests local and readable. Avoid tiny helper functions that hide the testing infra. Prefer a few explicit lines in the test body, `rstest`\n\ncases for repeated shapes, and constants only for large shared data or setup snippets.\n\nReturn `Result`\n\n. Most Nushell tests should be `fn test_name() -> Result`\n\n, where `Result`\n\ncomes from `nu_test_support::prelude::*`\n\n. Let `?`\n\npropagate `TestError`\n\nfrom `NuTester`\n\nand assertion helpers.\n\nUse the newest guidance over older examples. Older refactors may use `add_nu_to_path()`\n\n; newer test infra replaces that with `#[deps(NU)]`\n\n.\n\nMost test files should start with the prelude:\n\n```\nuse nu_test_support::prelude::*;\n```\n\nAdd specific imports when needed:\n\n```\nuse nu_protocol::Signals;\nuse pretty_assertions::assert_matches;\nuse rstest::rstest;\n```\n\nDo not import `test_value!`\n\n, `test_record!`\n\n, or `test_table!`\n\nagain; those `test_*!`\n\nmacros are already provided by `nu_test_support::prelude::*`\n\n.\n\nDo not import the old macros for new tests:\n\n```\n// Bad\nuse nu_test_support::nu;\nuse nu_test_support::{nu, nu_repl_code};\nuse nu_test_support::nu_with_plugins;\n```\n\nWhy: even when `nu`\n\nis still re-exported by compatibility modules or the prelude on some branches, that is not permission to use it in new tests. The direction is to remove these macros from tests.\n\nFor a single snippet and a single expected value, chain `test().run(...).expect_value_eq(...)`\n\n:\n\n``` php\nuse nu_test_support::prelude::*;\n\n#[test]\nfn can_average_range() -> Result {\n    test().run(\"0..5 | math avg\").expect_value_eq(2.5)\n}\n```\n\nThis is better than a `nu!`\n\ntest because it compares a real `Value`\n\nconverted through `IntoValue`\n\n, not collapsed stdout text.\n\nDo not write this:\n\n``` js\nuse nu_test_support::nu;\n\n#[test]\nfn can_average_range() {\n    let actual = nu!(\"0..5 | math avg\");\n    assert_eq!(actual.out, \"2.5\");\n}\n```\n\nUse a local variable named `code`\n\nonly for long or multiline snippets:\n\n```\nuse nu_test_support::prelude::*;\n\n#[test]\nfn format_filesize_respects_float_precision_for_fractional_values() -> Result {\n    let code = \"\n        $env.config = ($env.config | upsert float_precision 5)\n        1024B | format filesize kB\n    \";\n\n    test().run(code).expect_value_eq(\"1.02400 kB\")\n}\n```\n\nGuidelines for multiline snippets:\n\nUse `let code = \"...\"`\n\nor `let code = r#\"...\"#`\n\nwhen the snippet is long or multiline.\n\nFor short snippets, pass a one-line string directly to `run(...)`\n\nor `run_with_data(...)`\n\n.\n\nKeep Nushell code readable; do not compress everything into one line unless it is genuinely clearer.\n\nIf the expected output is a multiline Rust string, prefer `indoc::indoc!`\n\n.\n\nUse `cwd`\n\non the tester when the snippet reads files relative to a fixture directory:\n\n``` php\nuse nu_test_support::prelude::*;\n\n#[test]\nfn reads_cargo_sample() -> Result {\n    let code = r#\"\n        open cargo_sample.toml\n        | get package\n        | format pattern \"{name} has license {license}\"\n    \"#;\n\n    test()\n        .cwd(\"tests/fixtures/formats\")\n        .run(code)\n        .expect_value_eq(\"nu has license ISC\")\n}\n```\n\n`cwd`\n\naccepts repo-relative paths and sandbox paths. Prefer it over `cd`\n\nin the snippet when the test setup itself should start in a specific directory.\n\n`run`\n\nextracts the result into any type implementing `FromValue`\n\n. `expect_value_eq`\n\naccepts any type implementing `IntoValue`\n\n.\n\nNever write `run::<T>(...)`\n\nor `run_with_data::<T>(...)`\n\n. If you extract a value, let the left-hand side define the type with `let value: T = test().run(...) ?`\n\n. If you call `expect_value_eq`\n\n, `expect_shell_error`\n\n, `expect_parse_error`\n\n, or similar, the helper already fixes the needed type.\n\nSimple expected values should be raw Rust values when they are homogeneous or scalar:\n\n``` php\n#[test]\nfn filters_ints() -> Result {\n    test()\n        .run(\"[3, 1, 2] | sort\")\n        .expect_value_eq([1, 2, 3])\n}\n```\n\nUse `test_value!`\n\n, `test_record!`\n\n, and `test_table!`\n\nfor mixed, nested, record, or table-like data. Prefer `test_value!`\n\nfor nested structures instead of nesting `test_record!`\n\ninside `test_record!`\n\n:\n\n``` php\nuse nu_test_support::prelude::*;\n\n#[test]\nfn insert_at_end_of_list() -> Result {\n    test()\n        .run(\"[1, 2, 3] | insert 3 abc\")\n        .expect_value_eq(test_value!([1, 2, 3, \"abc\"]))\n}\nphp\nuse nu_test_support::prelude::*;\n\n#[test]\nfn inserts_into_nested_record() -> Result {\n    test()\n        .run(\"{a: {}} | insert a.b.c 0\")\n        .expect_value_eq(test_value!({\n            a: { b: { c: 0 } }\n        }))\n}\nphp\nuse nu_test_support::prelude::*;\n\n#[test]\nfn insert_uses_enumerate_index() -> Result {\n    let code = \"\n        [[a]; [7] [6]]\n        | enumerate\n        | insert b {|el| $el.index + 1 + $el.item.a }\n        | flatten\n    \";\n\n    test().run(code).expect_value_eq(test_table![\n        [\"index\", \"a\", \"b\"];\n        [0, 7, 8],\n        [1, 6, 8],\n    ])\n}\n```\n\nPrefer these forms over serialized output:\n\n```\n// Bad unless serialization itself is under test\n#[test]\nfn insert_at_end_of_list() -> Result {\n    test()\n        .run(\"[1, 2, 3] | insert 3 abc | to json --raw\")\n        .expect_value_eq(r#\"[1,2,3,\"abc\"]\"#)\n}\nphp\n// Good\n#[test]\nfn insert_at_end_of_list() -> Result {\n    test()\n        .run(\"[1, 2, 3] | insert 3 abc\")\n        .expect_value_eq(test_value!([1, 2, 3, \"abc\"]))\n}\n```\n\nExceptions are real rendering and serialization tests, such as `to json`\n\n, `to nuon`\n\n, `table`\n\n, or `to text`\n\n, where the string representation is the behavior under test.\n\nWhen assertions need Rust logic, extract a typed value:\n\n``` php\n#[test]\nfn splits_empty_path() -> Result {\n    let outcome: bool = test().cwd(\"tests\").run(\"echo '' | path split | is-empty\")?;\n    assert!(outcome);\n    Ok(())\n}\n```\n\nIf the test only verifies that a command succeeds and returns a value of a certain type, bind to `_`\n\n:\n\n``` php\n#[test]\nfn is_terminal_accepts_stdin_flag() -> Result {\n    let _: bool = test().run(\"is-terminal --stdin\")?;\n    Ok(())\n}\n```\n\nIf you only care that a command succeeds and returns any value:\n\n``` php\n#[test]\nfn help_does_not_error() -> Result {\n    let _: Value = test().run(\"overlay hide --help\")?;\n    Ok(())\n}\n```\n\nIf the command should return `nothing`\n\n, use `let ()`\n\nor `expect_value_eq(())`\n\n:\n\n``` php\n#[test]\nfn definition_compiles() -> Result {\n    let () = test().run(\"def helper [] { 'ok' }\")?;\n    Ok(())\n}\n```\n\nUse `run_with_data`\n\nwhen the test needs external data. This keeps the Nushell snippet stable and avoids unsafe or noisy `format!`\n\nuse:\n\n``` php\nuse nu_test_support::prelude::*;\n\n#[test]\nfn reads_input_record() -> Result {\n    let input = test_record! {\n        \"name\" => \"nu\",\n        \"license\" => \"MIT\",\n    };\n\n    test()\n        .run_with_data(\"$in | format pattern '{name} has license {license}'\", input)\n        .expect_value_eq(\"nu has license MIT\")\n}\n```\n\nPrefer this over injecting values into source with `format!`\n\n:\n\n``` php\n// Bad when only the value changes\n#[test]\nfn uppercases_name() -> Result {\n    let name = \"nu\";\n    test()\n        .run(format!(\"'{name}' | str upcase\"))\n        .expect_value_eq(\"NU\")\n}\nphp\n// Good\n#[test]\nfn uppercases_name() -> Result {\n    let name = \"nu\";\n    test()\n        .run_with_data(\"$in | str upcase\", name)\n        .expect_value_eq(\"NU\")\n}\n```\n\n`format!`\n\nis fine when you are intentionally generating Nushell syntax, such as an option flag or a large code fixture, and the generated code remains obvious.\n\nUse one mutable `NuTester`\n\nwhen later snippets depend on definitions, environment changes, overlays, or variables from earlier snippets:\n\n``` php\n#[test]\nfn use_main_def_env() -> Result {\n    let mut tester = test();\n\n    let () = tester.run(r#\"module spam { export def --env main [] { $env.SPAM = \"spam\" } }\"#)?;\n    let () = tester.run(\"use spam\")?;\n    let () = tester.run(\"spam\")?;\n\n    tester.run(\"$env.SPAM\").expect_value_eq(\"spam\")\n}\n```\n\nUse `run_multiple`\n\nwhen the test intentionally needs to run several pipelines after each other on the same tester and only the final value matters:\n\n``` php\n#[test]\nfn new_overlay_from_const_name() -> Result {\n    let commands = [\n        \"const mod = 'spam'\",\n        \"overlay new $mod\",\n        \"overlay list | last | get name\",\n    ];\n\n    test().run_multiple(commands).expect_value_eq(\"spam\")\n}\n```\n\nPrefer explicit repeated `run`\n\ncalls when intermediate results matter, when some steps are setup with `nothing`\n\noutput, or when the source-unit boundaries are important for understanding the test:\n\n``` php\n#[test]\nfn use_main_def_env() -> Result {\n    let mut tester = test();\n    let () = tester.run(r#\"module spam { export def --env main [] { $env.SPAM = \"spam\" } }\"#)?;\n    let () = tester.run(\"use spam\")?;\n    let () = tester.run(\"spam\")?;\n    tester.run(\"$env.SPAM\").expect_value_eq(\"spam\")\n}\n```\n\nDo not use `nu_repl_code`\n\n. If the test wants REPL-like separate source units, use `run_multiple`\n\nfor a simple sequence where only the final value matters, or repeated `tester.run(...)`\n\ncalls when intermediate steps matter.\n\nBe deliberate about source-unit boundaries:\n\nIf behavior must occur in one parse/source unit, put it in one `code`\n\nblock and run once.\n\nIf behavior must persist across source units, use repeated `run`\n\ncalls on one mutable tester.\n\nIf scenarios are independent, use separate `test()`\n\ncalls or `rstest`\n\ncases.\n\nExample of independent scenarios using fresh testers:\n\n``` php\n#[test]\nfn module_main_not_found() -> Result {\n    let mut tester = test();\n    let () = tester.run(\"module spam {}\")?;\n    tester\n        .run(\"use spam main\")\n        .expect_error_code_eq(\"nu::parser::export_not_found\")?;\n\n    let mut tester = test();\n    let () = tester.run(\"module spam {}\")?;\n    tester\n        .run(\"use spam [ main ]\")\n        .expect_error_code_eq(\"nu::parser::export_not_found\")\n}\n```\n\nUse `Playground::setup`\n\nwhen the test needs a sandboxed filesystem:\n\n```\nuse nu_test_support::{fs::Stub::EmptyFile, prelude::*};\n\n#[test]\nfn rm_deletes_file() -> Result {\n    Playground::setup(\"rm_deletes_file\", |dirs, sandbox| {\n        sandbox.with_files(&[EmptyFile(\"delete-me.txt\")]);\n\n        test()\n            .cwd(dirs.test())\n            .run(\"rm delete-me.txt; 'delete-me.txt' | path exists\")\n            .expect_value_eq(false)\n    })\n}\n```\n\nRules for playground tests:\n\nReturn the `Playground::setup`\n\nresult directly from the test.\n\nUse `test().cwd(dirs.test())`\n\nwhen the snippet should run in the sandbox.\n\nUse `FileWithContentToBeTrimmed`\n\nfor indented file contents.\n\nWhen the Rust side needs to read a file, do not use `file_contents(path)`\n\n. Use `std::fs::read_to_string(path)?`\n\nor `fs::read_to_string(path)?`\n\nif `std::fs`\n\nis imported. `io::Error`\n\nconverts into `TestError`\n\n, so this works naturally in tests that return `Result`\n\n:\n\n```\nuse std::fs;\nuse nu_test_support::{fs::Stub::FileWithContent, prelude::*};\n\n#[test]\nfn writes_expected_file() -> Result {\n    Playground::setup(\"writes_expected_file\", |dirs, sandbox| {\n        sandbox.with_files(&[FileWithContent(\"input.txt\", \"hello\")]);\n\n        let () = test()\n            .cwd(dirs.test())\n            .run(\"open input.txt | str upcase | save output.txt\")?;\n\n        let contents = fs::read_to_string(dirs.test().join(\"output.txt\"))?;\n        assert_eq!(contents, \"HELLO\");\n        Ok(())\n    })\n}\n```\n\nDo not create a playground when no filesystem behavior is being tested.\n\nDo not write this shape:\n\n``` php\n// Bad\n#[test]\nfn bad_playground_shape() -> Result {\n    Playground::setup(\"case\", |dirs, sandbox| {\n        sandbox.with_files(&[EmptyFile(\"x\")]);\n        test().cwd(dirs.test()).run(\"ls\")?;\n        Ok(())\n    });\n\n    Ok(())\n}\n```\n\nWrite this instead:\n\n``` php\n#[test]\nfn good_playground_shape() -> Result {\n    Playground::setup(\"case\", |dirs, sandbox| {\n        sandbox.with_files(&[EmptyFile(\"x\")]);\n        let _: Value = test().cwd(dirs.test()).run(\"ls\")?;\n        Ok(())\n    })\n}\n```\n\nUse `rstest`\n\nwhen several tests have the same shape and only data changes:\n\n```\nuse nu_test_support::prelude::*;\nuse rstest::rstest;\n\n#[rstest]\n#[case::null(\"null\", ())]\n#[case::true_(\"true\", true)]\n#[case::false_(\"false\", false)]\nfn top_level_values_from_json(#[case] json: &str, #[case] expected: impl IntoValue) -> Result {\n    test()\n        .run_with_data(\"from json\", json)\n        .expect_value_eq(expected)\n}\n```\n\nThis is better than a loop inside one test because every case is reported independently.\n\nWhen combining `rstest`\n\nwith `#[deps]`\n\n, `#[env]`\n\n, `#[exp]`\n\n, `#[serial]`\n\n, or other harness attributes, explicitly use the harness test macro:\n\n```\nuse nu_test_support::prelude::*;\nuse rstest::rstest;\n\n#[rstest]\n#[case::child_lib_dirs(\"$env.NU_LIB_DIRS | describe\", \"list<string>\")]\n#[case::child_plugin_dirs(\"$NU_PLUGIN_DIRS | describe\", \"list<string>\")]\n#[nu_test_support::test]\n#[deps(NU)]\nfn child_process_defaults(#[case] child_code: &str, #[case] expected: impl IntoValue) -> Result {\n    test()\n        .run_with_data(\"let child_code; nu -n -c $child_code\", child_code)\n        .expect_value_eq(expected)\n}\n```\n\nFor per-case dependencies, follow nearby repository style and place the dependency attribute next to the case it belongs to.\n\nPrefer structured error assertions.\n\nIf the exact error code is enough, use `expect_error_code_eq`\n\n:\n\n``` php\n#[test]\nfn insert_past_end_of_list() -> Result {\n    test()\n        .run(\"[1, 2, 3] | insert 5 abc\")\n        .expect_error_code_eq(\"nu::shell::access_beyond_end\")\n}\n```\n\nIf the variant or fields matter, extract the error and match it:\n\n```\nuse nu_test_support::prelude::*;\nuse pretty_assertions::assert_matches;\n\n#[test]\nfn insert_nested_path_into_empty_list_errors_without_underflow() -> Result {\n    let err = test().run(\"[] | insert 0.0 1\").expect_shell_error()?;\n    assert_matches!(err, ShellError::AccessEmptyContent { .. });\n    Ok(())\n}\n```\n\nParse and compile errors have dedicated helpers:\n\n```\nuse nu_protocol::Type;\nuse nu_test_support::prelude::*;\nuse pretty_assertions::assert_matches;\n\n#[test]\nfn parse_function_signature_switch_is_bool() -> Result {\n    let err = test()\n        .run(\"def foo [--bar] { let baz: int = $bar }\")\n        .expect_parse_error()?;\n\n    assert_matches!(err, ParseError::TypeMismatch(Type::Int, Type::Bool, _));\n    Ok(())\n}\n```\n\nUse `ShellErrorExt`\n\nfor generic or labeled errors when needed:\n\n``` php\n#[test]\nfn glob_without_match_reports_missing_file() -> Result {\n    let err = test().run(\"ls root3*\").expect_shell_error()?;\n    let msg = err.generic_msg()?;\n    assert_contains(\"file or folder not found\", msg);\n    Ok(())\n}\n```\n\nAvoid this old style:\n\n``` js\n// Bad\n#[test]\nfn insert_past_end_of_list() {\n    let actual = nu!(\"[1, 2, 3] | insert 5 abc\");\n    assert!(actual.err.contains(\"too large\"));\n}\n```\n\nUse `expect_value_eq`\n\nfor Nushell values.\n\nUse `assert_contains(needle, haystack)`\n\nfor positive contains checks.\n\nUse `pretty_assertions::assert_eq`\n\nor `pretty_assertions::assert_str_eq`\n\nfor large Rust-side values or rendered strings when it improves diffs.\n\nUse `assert_matches!`\n\nfor error variants and enum shapes.\n\nAvoid `assert!(haystack.contains(needle))`\n\n; it gives weaker diagnostics than `assert_contains`\n\n.\n\nAvoid `assert_eq!(actual.out, ...)`\n\nbecause new tests should not produce `actual.out`\n\nin the first place.\n\nUse `assert_contains_not(needle, haystack)`\n\nfor negative containment checks. Prefer it over hand-written `assert!(!haystack.contains(needle))`\n\nbecause it matches the positive `assert_contains`\n\nstyle and gives a consistent failure message:\n\n``` php\n#[test]\nfn overlay_list_does_not_contain_hidden_overlay() -> Result {\n    let names: String = test().run(\"overlay list | get name | str join ' '\")?;\n    assert_contains_not(\"spam\", names);\n    Ok(())\n}\n```\n\nMost tests should not spawn the `nu`\n\nbinary. Spawn it only when testing behavior that exists at the process boundary, such as CLI flags, script-file execution, process exit behavior, or child process environment defaults.\n\nWhen spawning `nu`\n\n, declare `#[deps(NU)]`\n\n:\n\n```\nuse nu_test_support::prelude::*;\n\n#[test]\n#[deps(NU)]\nfn source_file_relative_to_file() -> Result {\n    let result: CompleteResult = test()\n        .cwd(\"tests/parsing/samples\")\n        .run(\"nu -n source_file_relative.nu | complete\")?;\n\n    assert_eq!(result.exit_code, 0);\n    assert_eq!(result.stdout.trim(), \"5\");\n    assert_eq!(result.stderr, \"\");\n    Ok(())\n}\n```\n\nUse `CompleteResult`\n\nfor `complete`\n\noutput instead of indexing a record:\n\n``` php\n#[test]\n#[deps(NU)]\nfn child_nu_reports_missing_script() -> Result {\n    let result: CompleteResult = test().run(\"nu -n missing-script.nu | complete\")?;\n\n    assert_ne!(result.exit_code, 0);\n    assert_contains(\"file_not_found\", result.stderr);\n    Ok(())\n}\n```\n\nDo not use `add_nu_to_path()`\n\nin new tests:\n\n``` php\n// Bad\n#[test]\nfn child_version() -> Result {\n    test()\n        .add_nu_to_path()\n        .run(\"nu -n -c 'version | get version'\")\n        .expect_value_eq(env!(\"CARGO_PKG_VERSION\"))\n}\nphp\n// Good\n#[test]\n#[deps(NU)]\nfn child_version() -> Result {\n    test()\n        .run(\"nu -n -c 'version | get version'\")\n        .expect_value_eq(env!(\"CARGO_PKG_VERSION\"))\n}\n```\n\nDo not call test binaries through `nu --testbin ...`\n\n. That route is deprecated for tests. Declare the testbin dependency and call the binary directly:\n\n``` php\n// Bad\n#[test]\n#[deps(NU)]\nfn bad_testbin_call() -> Result {\n    test()\n        .run(\"nu --testbin cococo a b c\")\n        .expect_value_eq(\"a b c\")\n}\nphp\n// Good\n#[test]\n#[deps(TESTBIN_COCOCO)]\nfn calls_testbin_directly() -> Result {\n    test()\n        .run(\"cococo a b c\")\n        .expect_value_eq(\"a b c\")\n}\n```\n\nUse the appropriate `TESTBIN_*`\n\nconstant from the prelude, such as `TESTBIN_COCOCO`\n\n, `TESTBIN_MEOW`\n\n, or `TESTBIN_REPEATER`\n\n. Add `NU`\n\nonly if the behavior under test specifically requires a child Nushell process in addition to the testbin.\n\nPlugin tests should not use `nu_with_plugins!`\n\n. Declare plugin dependencies with `#[deps]`\n\nand call plugin commands directly in-process:\n\n```\nuse nu_test_support::prelude::*;\n\n#[test]\n#[deps(NU_PLUGIN_EXAMPLE)]\nfn call_to_json() -> Result {\n    test()\n        .run(\"[42] | example call-decl 'to json' {indent: 4}\")\n        .expect_value_eq(\"[\\n    42\\n]\")\n}\nuse nu_test_support::prelude::*;\n\n#[test]\n#[deps(NU_PLUGIN_EXAMPLE)]\nfn plugin_receives_config() -> Result {\n    let code = r#\"\n        $env.config = {\n            plugins: {\n                example: {\n                    path: \"some/path\"\n                    nested: { bool: true, string: \"Hello Example!\" }\n                }\n            }\n        }\n        example config\n    \"#;\n\n    test().run(code).expect_value_eq(test_value!({\n        path: \"some/path\",\n        nested: {\n            bool: true,\n            string: \"Hello Example!\"\n        }\n    }))\n}\n```\n\nIf a plugin test specifically needs an actual child `nu`\n\nprocess plus a plugin path, declare both dependencies and pass the plugin path as data:\n\n```\n#[test]\n#[deps(NU, NU_PLUGIN_INC)]\nfn plugin_process_exits_when_nushell_exits() -> Result {\n    let pid: u32 = test().run_with_data(\n        r#\"nu -n --plugins $in -c \"'2.0.0' | inc -m; (plugin list).0.pid\" | into int\"#,\n        NU_PLUGIN_INC.path(),\n    )?;\n\n    let mut tester = test();\n    let () = tester.run(\"sleep 500ms\")?;\n    let () = tester.run_with_data(\"let pid = $in\", pid)?;\n    tester\n        .run(\"ps | where pid == $pid | is-empty\")\n        .expect_value_eq(true)\n}\n```\n\nUse `#[serial]`\n\nfor plugin tests that check process identity, start/stop timing, or shared process state. Do not mark all plugin tests serial by default.\n\nBy default, `NuTester`\n\nkeeps `$env.PATH`\n\nunset or minimal for deterministic tests.\n\nUse `#[deps(NU)]`\n\nfor the Nushell binary.\n\nUse plugin dependencies for plugins.\n\nUse `#[deps(TESTBIN_X)]`\n\nfor test binaries and invoke them directly by name. Do not route testbins through `nu --testbin`\n\n.\n\nUse `inherit_path()`\n\nonly when the test intentionally relies on arbitrary system commands.\n\nUse `inherit_rust_toolchain_env()`\n\nfor tests that intentionally spawn `cargo`\n\n, `rustc`\n\n, or `rustup`\n\n.\n\nExample:\n\n``` php\n#[test]\nfn cargo_is_available_when_inheriting_rust_toolchain_env() -> Result {\n    test()\n        .inherit_rust_toolchain_env()\n        .run(\"cargo --version | split row ' ' | get 0\")\n        .expect_value_eq(\"cargo\")\n}\n```\n\nDo not use `inherit_path()`\n\nas a workaround for missing `#[deps]`\n\n.\n\nThe custom harness supports attributes on tests:\n\n`#[serial]`\n\nruns tests sequentially. Use it for process-wide state, heavy IO contention, plugin PID lifecycle tests, or other unavoidable shared-state cases. Do not use global locks instead.\n\n`#[env(KEY = \"value\")]`\n\nsets process environment variables for a test group. Use this when engine setup or harness grouping depends on an environment variable.\n\n`#[exp(OPTION)]`\n\nenables an experimental option. Use it instead of setting experimental options manually in a `nu!`\n\ncall.\n\n`#[deps(...)]`\n\ndeclares binaries or plugins that must be built and made available to `test()`\n\n.\n\nExample:\n\n```\nuse nu_experimental::REORDER_CELL_PATHS;\nuse nu_test_support::prelude::*;\n\n#[test]\n#[exp(REORDER_CELL_PATHS)]\nfn update_table_cell_respects_reorder_option() -> Result {\n    let code = \"\n        let a = [[foo]; [bar]]\n        let b = ($a | update foo.0 'baz')\n        $b.0.foo\n    \";\n\n    test().run(code).expect_value_eq(\"baz\")\n}\n```\n\nFor locale-sensitive tests, use `#[env(NU_TEST_LOCALE_OVERRIDE = \"en_US.UTF-8\")]`\n\nwhen the locale must affect harness/engine setup, or `test().locale_en()`\n\nwhen configuring only the tester is enough.\n\nFor commands whose output is intentionally a string rendering, assert the string directly. Use `indoc!`\n\nto keep expected output readable.\n\n```\nuse indoc::indoc;\nuse nu_test_support::prelude::*;\n\n#[test]\nfn table_padding_zero() -> Result {\n    test()\n        .run_with_data(\n            \"\n                let data = $in\n                $env.config.table.padding = {left: 0, right: 0}\n                $data | table --width=80\n            \",\n            test_table![\n                [\"a\", \"b\", \"c\"];\n                [1, 2, 3],\n                [4, 5, [1, 2, 3]],\n            ],\n        )\n        .expect_value_eq(indoc! {\"\n            ╭─┬─┬─┬──────────────╮\n            │#│a│b│      c       │\n            ├─┼─┼─┼──────────────┤\n            │0│1│2│             3│\n            │1│4│5│[list 3 items]│\n            ╰─┴─┴─┴──────────────╯\n        \"})\n}\n```\n\nNotes for rendering tests:\n\nUse `run_with_data`\n\nfor complex input so the test focuses on rendering.\n\nUse `ansi strip`\n\nwhen colors are not the behavior under test.\n\nWhen colors are the behavior under test, assert the ANSI string explicitly.\n\nUse `pretty_assertions::assert_str_eq!`\n\nwhen comparing large rendered strings extracted as `String`\n\n.\n\nUse this checklist when converting old tests.\n\n- Remove\n`use nu_test_support::nu`\n\n,`nu_with_plugins`\n\n, and`nu_repl_code`\n\nimports. - Add\n`use nu_test_support::prelude::*;`\n\nand only specific extra imports that are needed. - Change\n`fn test_name()`\n\nto`fn test_name() -> Result`\n\n. - Replace\n`let actual = nu!(...)`\n\nwith`test().run(...)`\n\n,`test().cwd(...).run(...)`\n\n, or`let mut tester = test(); tester.run(...)`\n\n. - Replace\n`assert_eq!(actual.out, \"...\")`\n\nwith`.expect_value_eq(...)`\n\nor typed extraction. - Replace\n`assert!(actual.err.contains(...))`\n\nwith`.expect_error_code_eq(...)`\n\n,`.expect_shell_error()?`\n\n,`.expect_parse_error()?`\n\n, or`.expect_compile_error()?`\n\n. - Replace plugin macro usage with\n`#[deps(NU_PLUGIN_...)]`\n\nand direct plugin commands. - Replace\n`add_nu_to_path()`\n\nwith`#[deps(NU)]`\n\n. - Replace\n`nu --testbin name ...`\n\nwith`#[deps(TESTBIN_NAME)]`\n\nand direct`name ...`\n\ncalls. - Replace\n`nu_repl_code`\n\nwith`run_multiple`\n\nor a mutable`NuTester`\n\nand repeated`run`\n\ncalls, depending on which is clearer. - Replace serialization-only assertions with structured values unless serialization is the feature under test.\n- Use\n`rstest`\n\nwhen the refactor exposes several same-shaped tests. - Keep the test body explicit rather than moving a small sequence into a helper.\n\nExample refactor:\n\n``` js\n// Old\nuse nu_test_support::nu;\n\n#[test]\nfn insert_into_list() {\n    let actual = nu!(\"[1, 2, 3] | insert 1 abc | to json -r\");\n    assert_eq!(actual.out, r#\"[1,\"abc\",2,3]\"#);\n}\nphp\n// New\nuse nu_test_support::prelude::*;\n\n#[test]\nfn insert_into_list() -> Result {\n    test()\n        .run(\"[1, 2, 3] | insert 1 abc\")\n        .expect_value_eq(test_value!([1, \"abc\", 2, 3]))\n}\n```\n\nError refactor:\n\n``` js\n// Old\n#[test]\nfn list_unknown_long_flag() {\n    let actual = nu!(\"ls --full-path\");\n    assert!(actual.err.contains(\"Did you mean: `--full-paths`?\"));\n}\n// New\nuse nu_protocol::ParseError;\nuse nu_test_support::prelude::*;\nuse pretty_assertions::assert_matches;\n\n#[test]\nfn list_unknown_long_flag() -> Result {\n    let err = test().run(\"ls --full-path\").expect_parse_error()?;\n    assert_matches!(\n        err,\n        ParseError::UnknownFlag(_, _, _, help) if help == \"Did you mean: `--full-paths`?\"\n    );\n    Ok(())\n}\n```\n\nPlugin refactor:\n\n``` js\n// Old\n#[test]\nfn call_reduce() {\n    let result = nu_with_plugins!(\n        cwd: \".\",\n        plugin: (\"nu_plugin_example\"),\n        \"[1 2 3] | example call-decl 'reduce' {fold: 10} { |it, acc| $it + $acc }\"\n    );\n    assert_eq!(\"16\", result.out);\n}\nphp\n// New\n#[test]\n#[deps(NU_PLUGIN_EXAMPLE)]\nfn call_reduce() -> Result {\n    test()\n        .run(\"[1 2 3] | example call-decl 'reduce' {fold: 10} {|it, acc| $it + $acc}\")\n        .expect_value_eq(16)\n}\n```\n\nMultiple source-unit refactor:\n\n```\n// Acceptable when only the final value matters\n#[test]\nfn new_overlay_from_const_name() -> Result {\n    let commands = [\n        \"const mod = 'spam'\",\n        \"overlay new $mod\",\n        \"overlay list | last | get name\",\n    ];\n\n    test().run_multiple(commands).expect_value_eq(\"spam\")\n}\n// Prefer this when intermediate steps or assertions matter\n#[test]\nfn new_overlay_from_const_name() -> Result {\n    let mut tester = test();\n    let () = tester.run(\"const mod = 'spam'\")?;\n    let () = tester.run(\"overlay new $mod\")?;\n    tester.run(\"overlay list | last | get name\").expect_value_eq(\"spam\")\n}\n```\n\nDo not use `nu!`\n\nfor new tests.\n\nDo not use `nu_with_plugins!`\n\n.\n\nDo not use `nu_repl_code`\n\n.\n\nDo not use `add_nu_to_path()`\n\n.\n\nDo not call testbins via `nu --testbin`\n\n; use `#[deps(TESTBIN_...)]`\n\nand call them directly.\n\nDo not assert `actual.out`\n\n, `actual.err`\n\n, or `actual.status`\n\nfrom `nu!`\n\noutput.\n\nDo not write `run::<T>(...)`\n\nor `run_with_data::<T>(...)`\n\n; annotate the left-hand binding or use the expectation helpers.\n\nDo not depend on `NU`\n\njust because a test used to spawn `nu`\n\n; first ask whether the behavior can be tested in-process.\n\nDo not depend on a plugin or binary without `#[deps(...)]`\n\n.\n\nDo not serialize values to JSON, NUON, markdown, text, or joined strings only to assert them.\n\nDo not use `Playground::setup`\n\nwhen no filesystem behavior is needed.\n\nDo not use `file_contents(path)`\n\n; use `std::fs::read_to_string(path)?`\n\ninstead.\n\nDo not emulate serialization or isolation with global mutexes; use `#[serial]`\n\nwhen serial execution is truly required.\n\nDo not create helper functions just to wrap `test().run`\n\nor hide a small command sequence.\n\nDo not manually set process environment with `std::env::set_var`\n\n; use `#[env]`\n\nor `test().env(...)`\n\n.\n\nDo not manually enable experimental options in the snippet or old macro options; use `#[exp]`\n\n.\n\nDo not import `test_value`\n\n, `test_record`\n\n, or `test_table`\n\nfrom `nu_protocol`\n\nwhen `nu_test_support::prelude::*`\n\nis already imported; the prelude provides the `test_*!`\n\nmacros.\n\nUse `test().run(code).expect_value_eq(expected)`\n\nfor one snippet and one expected value.\n\nUse `let mut tester = test(); tester.run(...)`\n\nfor shared state across source units.\n\nUse `test().run_multiple(commands)`\n\nfor a simple sequence of pipelines where only the last output is asserted.\n\nUse one multiline `code`\n\nblock for behavior that must parse or execute in one source unit.\n\nUse `run_with_data`\n\nwhen external Rust data is the input.\n\nUse `Playground::setup`\n\nplus `.cwd(dirs.test())`\n\nfor sandboxed files.\n\nUse `rstest`\n\nfor same-shaped cases.\n\nUse `expect_error_code_eq`\n\nwhen the code is the contract.\n\nUse `expect_shell_error`\n\n, `expect_parse_error`\n\n, or `expect_compile_error`\n\nwhen the error shape matters.\n\nUse `#[deps(NU)]`\n\nplus `CompleteResult`\n\nwhen testing a child `nu`\n\nprocess.\n\nUse `#[deps(TESTBIN_...)]`\n\nwhen testing commands that need a testbin, then call the testbin directly.\n\nUse `#[deps(NU_PLUGIN_...)]`\n\nwhen testing plugin commands in-process.\n\nUse `#[serial]`\n\nonly for unavoidable shared state, timing-sensitive process lifecycle, or heavy IO conflicts.\n\nAbove all, keep the test explicit, type-aware, and free of `nu!`\n\n.", "url": "https://wpnews.pro/news/an-ai-agent-skill-for-writing-tests-in-the-nushell-repo", "canonical_source": "https://gist.github.com/cptpiepmatz/2e73779a4a4493c6339d68215eb9aa83", "published_at": "2026-07-19 10:44:47+00:00", "updated_at": "2026-07-24 19:29:42.284822+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools"], "entities": ["Nushell"], "alternates": {"html": "https://wpnews.pro/news/an-ai-agent-skill-for-writing-tests-in-the-nushell-repo", "markdown": "https://wpnews.pro/news/an-ai-agent-skill-for-writing-tests-in-the-nushell-repo.md", "text": "https://wpnews.pro/news/an-ai-agent-skill-for-writing-tests-in-the-nushell-repo.txt", "jsonld": "https://wpnews.pro/news/an-ai-agent-skill-for-writing-tests-in-the-nushell-repo.jsonld"}}