Skip to content

Commit 4a594bb

Browse files
committed
feat: add integration tests for CLI commands and packaging
1 parent 53a1b50 commit 4a594bb

5 files changed

Lines changed: 705 additions & 2 deletions

File tree

cli/Cargo.toml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,18 @@ repository.workspace = true
1212
name = "tnmsc"
1313
path = "src/main.rs"
1414

15+
[[test]]
16+
name = "command_contract"
17+
path = "integration-tests/command_contract.rs"
18+
19+
[[test]]
20+
name = "packaging_smoke"
21+
path = "integration-tests/packaging_smoke.rs"
22+
1523
[dependencies]
1624
tnmsc = { workspace = true }
1725
clap = { workspace = true }
1826
serde_json = { workspace = true }
27+
28+
[dev-dependencies]
29+
testcontainers = { version = "0.27.3", features = ["blocking"] }
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
mod support;
2+
3+
use std::fs;
4+
5+
use support::{current_package_version, not_implemented_message, run_tnmsc, TestDir};
6+
7+
#[test]
8+
fn help_lists_supported_commands() {
9+
let result = run_tnmsc(&["help"], &support::workspace_root());
10+
result.assert_success("tnmsc help");
11+
12+
for expected in ["install", "dry-run", "clean", "plugins", "version", "help"] {
13+
assert!(
14+
result.stdout.contains(expected),
15+
"help output should include `{expected}`.\nstdout:\n{}",
16+
result.stdout
17+
);
18+
}
19+
}
20+
21+
#[test]
22+
fn version_matches_workspace_version() {
23+
let result = run_tnmsc(&["version"], &support::workspace_root());
24+
result.assert_success("tnmsc version");
25+
26+
assert_eq!(result.stdout.trim(), current_package_version());
27+
}
28+
29+
#[test]
30+
fn plugins_lists_core_output_adaptors() {
31+
let result = run_tnmsc(&["plugins"], &support::workspace_root());
32+
result.assert_success("tnmsc plugins");
33+
34+
for expected in [
35+
"CodexCLIOutputAdaptor",
36+
"ClaudeCodeCLIOutputAdaptor",
37+
"TraeOutputAdaptor",
38+
"OpencodeCLIOutputAdaptor",
39+
] {
40+
assert!(
41+
result.stdout.contains(expected),
42+
"plugins output should include `{expected}`.\nstdout:\n{}",
43+
result.stdout
44+
);
45+
}
46+
}
47+
48+
#[test]
49+
fn schema_output_writes_valid_json_in_integration_sandbox() {
50+
let temp_dir = TestDir::new("tnmsc-schema-contract");
51+
let schema_path = temp_dir.path().join("tnmsc.schema.json");
52+
let schema_path_arg = schema_path.to_string_lossy().into_owned();
53+
54+
let result = run_tnmsc(
55+
&["schema", "--output", &schema_path_arg],
56+
&support::workspace_root(),
57+
);
58+
result.assert_success("tnmsc schema --output");
59+
60+
let content = fs::read_to_string(&schema_path)
61+
.unwrap_or_else(|error| panic!("failed to read {}: {error}", schema_path.display()));
62+
let parsed: serde_json::Value = serde_json::from_str(&content)
63+
.unwrap_or_else(|error| panic!("schema output should be valid JSON: {error}"));
64+
65+
let object = parsed
66+
.as_object()
67+
.expect("schema output should be a top-level JSON object");
68+
69+
assert!(object.contains_key("$schema"));
70+
assert!(object.contains_key("properties"));
71+
}
72+
73+
#[test]
74+
fn install_like_commands_fail_with_not_implemented_contract() {
75+
for (args, command_name, display) in [
76+
(&[][..], "install", "tnmsc"),
77+
(&["install"][..], "install", "tnmsc install"),
78+
(&["dry-run"][..], "dry-run", "tnmsc dry-run"),
79+
(&["clean"][..], "clean", "tnmsc clean"),
80+
(
81+
&["clean", "--dry-run"][..],
82+
"clean",
83+
"tnmsc clean --dry-run",
84+
),
85+
] {
86+
let result = run_tnmsc(args, &support::workspace_root());
87+
result.assert_failure(display);
88+
89+
let expected = not_implemented_message(command_name);
90+
assert!(
91+
result.stderr.contains(&expected),
92+
"{display} stderr should contain the not-implemented contract.\nexpected:\n{expected}\nactual:\n{}",
93+
result.stderr
94+
);
95+
}
96+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
mod support;
2+
3+
use std::fs;
4+
5+
#[cfg(unix)]
6+
use std::os::unix::fs::PermissionsExt;
7+
8+
use support::{pack_cli_artifacts, quote_shell, TestContainer};
9+
10+
#[test]
11+
fn packaging_smoke_covers_release_binary_and_global_install() {
12+
if !support::is_linux_x64_host() {
13+
eprintln!("skipping packaging smoke on unsupported host");
14+
return;
15+
}
16+
17+
let staged = support::create_staged_package_root();
18+
let package_root = staged.package_root.to_string_lossy().into_owned();
19+
let workspace_root = support::workspace_root().to_string_lossy().into_owned();
20+
21+
let assemble = support::run_tnmsc_with_env(
22+
&["assemble-npm", "--profile", "release"],
23+
&support::cli_manifest_dir(),
24+
&[
25+
("TNMSC_NPM_PACKAGE_ROOT", package_root.as_str()),
26+
("TNMSC_WORKSPACE_ROOT", workspace_root.as_str()),
27+
],
28+
);
29+
assemble.assert_success("tnmsc assemble-npm --profile release");
30+
31+
assert!(
32+
staged.linux_binary.is_file(),
33+
"expected hydrated linux binary at {}",
34+
staged.linux_binary.display()
35+
);
36+
37+
#[cfg(unix)]
38+
{
39+
let mode = fs::metadata(&staged.linux_binary)
40+
.unwrap_or_else(|error| panic!("failed to stat {}: {error}", staged.linux_binary.display()))
41+
.permissions()
42+
.mode();
43+
assert!(
44+
mode & 0o111 != 0,
45+
"expected {} to be executable, mode was {:o}",
46+
staged.linux_binary.display(),
47+
mode
48+
);
49+
}
50+
51+
let artifacts = pack_cli_artifacts();
52+
let container = TestContainer::start(&artifacts);
53+
54+
let install_command = format!(
55+
"corepack enable && corepack prepare pnpm@{} --activate && pnpm add -g {} {}",
56+
quote_shell(support::pnpm_version()),
57+
quote_shell("/artifacts/cli.tgz"),
58+
quote_shell("/artifacts/linux-x64-gnu.tgz")
59+
);
60+
container.exec_success(&install_command);
61+
62+
let help = container.exec("tnmsc help");
63+
help.assert_success("global tnmsc help");
64+
for expected in ["install", "dry-run", "clean", "plugins"] {
65+
assert!(
66+
help.stdout.contains(expected),
67+
"global help output should include `{expected}`.\nstdout:\n{}",
68+
help.stdout
69+
);
70+
}
71+
72+
let plugins = container.exec("tnmsc plugins");
73+
plugins.assert_success("global tnmsc plugins");
74+
for expected in [
75+
"CodexCLIOutputAdaptor",
76+
"ClaudeCodeCLIOutputAdaptor",
77+
"TraeOutputAdaptor",
78+
"OpencodeCLIOutputAdaptor",
79+
] {
80+
assert!(
81+
plugins.stdout.contains(expected),
82+
"global plugins output should include `{expected}`.\nstdout:\n{}",
83+
plugins.stdout
84+
);
85+
}
86+
87+
container.exec_success(
88+
r#"
89+
MAIN_PACKAGE_JSON="$(find -L /pnpm/global -path '*/@truenine/memory-sync-cli/package.json' -print -quit)"
90+
PLATFORM_PACKAGE_JSON="$(find -L /pnpm/global -path '*/@truenine/memory-sync-cli-linux-x64-gnu/package.json' -print -quit)"
91+
test -n "$MAIN_PACKAGE_JSON"
92+
test -n "$PLATFORM_PACKAGE_JSON"
93+
test -x "$(dirname "$PLATFORM_PACKAGE_JSON")/bin/tnmsc"
94+
test -x "$(command -v tnmsc)"
95+
test ! -e "$(dirname "$MAIN_PACKAGE_JSON")/dist/index.mjs"
96+
"#,
97+
);
98+
}

0 commit comments

Comments
 (0)