Skip to content

Commit 2053747

Browse files
committed
test(cli): pin RepairArgs flag surface (download-mode=file, gc alias)
Add a parser-level integration test that locks in the public clap contract for the `repair` subcommand. The key assertion is that `--download-mode` defaults to `"file"` — diverging from every other command's `"diff"` default. `repair` restores legacy per-file blobs needed to apply any patch, so a silent flip to `"diff"` would break that path with no parser-level signal. The `gc` visible alias is also exercised (both bare and with a flag) so a refactor that drops it is caught immediately. Covers every flag in the repair row of CLI_CONTRACT.md plus an unknown-flag failure case that pins the clap error kind. Assisted-by: Claude Code:claude-opus-4-7
1 parent 0bd4f50 commit 2053747

1 file changed

Lines changed: 155 additions & 0 deletions

File tree

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
//! CLI contract tests for the `repair` subcommand (and its `gc` visible alias).
2+
//!
3+
//! These tests pin the public clap parser surface for `RepairArgs`. The most
4+
//! important invariant guarded here is that `repair`'s `--download-mode`
5+
//! defaults to `"file"` — diverging from every other command (which defaults
6+
//! to `"diff"`). This is intentional: `repair` restores the legacy per-file
7+
//! blobs needed to apply any patch. A silent flip to `"diff"` would be a
8+
//! breaking behavior change with no parser-level signal, so we lock it down
9+
//! here. The `gc` visible alias is also exercised so a refactor that drops
10+
//! it is caught immediately.
11+
//!
12+
//! See `crates/socket-patch-cli/CLI_CONTRACT.md` for the full repair table.
13+
14+
use std::path::PathBuf;
15+
16+
use clap::Parser;
17+
use socket_patch_cli::commands::repair::RepairArgs;
18+
use socket_patch_cli::{Cli, Commands};
19+
20+
fn parse_repair(extra: &[&str]) -> RepairArgs {
21+
let mut argv = vec!["socket-patch", "repair"];
22+
argv.extend_from_slice(extra);
23+
let cli = Cli::try_parse_from(&argv).expect("parse");
24+
match cli.command {
25+
Commands::Repair(a) => a,
26+
_ => panic!("expected Repair"),
27+
}
28+
}
29+
30+
fn parse_gc(extra: &[&str]) -> RepairArgs {
31+
let mut argv = vec!["socket-patch", "gc"];
32+
argv.extend_from_slice(extra);
33+
let cli = Cli::try_parse_from(&argv).expect("parse");
34+
match cli.command {
35+
Commands::Repair(a) => a,
36+
_ => panic!("expected Repair via gc alias"),
37+
}
38+
}
39+
40+
#[test]
41+
fn repair_defaults_match_contract() {
42+
let args = parse_repair(&[]);
43+
44+
// CRITICAL: repair's --download-mode default is "file", not "diff".
45+
// This is the divergent default vs every other command.
46+
assert_eq!(
47+
args.download_mode, "file",
48+
"repair --download-mode default MUST be `file` (legacy per-file blobs); diverges from other commands"
49+
);
50+
51+
// Remaining defaults from CLI_CONTRACT.md repair table.
52+
assert_eq!(args.cwd, PathBuf::from("."));
53+
assert_eq!(args.manifest_path, ".socket/manifest.json");
54+
assert!(!args.dry_run);
55+
assert!(!args.offline);
56+
assert!(!args.download_only);
57+
assert!(!args.json);
58+
}
59+
60+
#[test]
61+
fn repair_dry_run_short_flag() {
62+
let args = parse_repair(&["-d"]);
63+
assert!(args.dry_run);
64+
}
65+
66+
#[test]
67+
fn repair_dry_run_long_flag() {
68+
let args = parse_repair(&["--dry-run"]);
69+
assert!(args.dry_run);
70+
}
71+
72+
#[test]
73+
fn repair_manifest_path_short_flag() {
74+
let args = parse_repair(&["-m", "custom.json"]);
75+
assert_eq!(args.manifest_path, "custom.json");
76+
}
77+
78+
#[test]
79+
fn repair_manifest_path_long_flag() {
80+
let args = parse_repair(&["--manifest-path", "custom.json"]);
81+
assert_eq!(args.manifest_path, "custom.json");
82+
}
83+
84+
#[test]
85+
fn repair_cwd_flag() {
86+
let args = parse_repair(&["--cwd", "/tmp/x"]);
87+
assert_eq!(args.cwd, PathBuf::from("/tmp/x"));
88+
}
89+
90+
#[test]
91+
fn repair_offline_flag() {
92+
let args = parse_repair(&["--offline"]);
93+
assert!(args.offline);
94+
}
95+
96+
#[test]
97+
fn repair_download_only_flag() {
98+
let args = parse_repair(&["--download-only"]);
99+
assert!(args.download_only);
100+
}
101+
102+
#[test]
103+
fn repair_json_flag() {
104+
let args = parse_repair(&["--json"]);
105+
assert!(args.json);
106+
}
107+
108+
#[test]
109+
fn repair_download_mode_file() {
110+
let args = parse_repair(&["--download-mode", "file"]);
111+
assert_eq!(args.download_mode, "file");
112+
}
113+
114+
#[test]
115+
fn repair_download_mode_diff() {
116+
let args = parse_repair(&["--download-mode", "diff"]);
117+
assert_eq!(args.download_mode, "diff");
118+
}
119+
120+
#[test]
121+
fn repair_download_mode_package() {
122+
let args = parse_repair(&["--download-mode", "package"]);
123+
assert_eq!(args.download_mode, "package");
124+
}
125+
126+
#[test]
127+
fn repair_gc_alias_defaults_match_repair() {
128+
let via_gc = parse_gc(&[]);
129+
let via_repair = parse_repair(&[]);
130+
131+
// The whole point of the alias: identical parsing.
132+
assert_eq!(via_gc.download_mode, "file");
133+
assert_eq!(via_gc.download_mode, via_repair.download_mode);
134+
assert_eq!(via_gc.cwd, via_repair.cwd);
135+
assert_eq!(via_gc.manifest_path, via_repair.manifest_path);
136+
assert_eq!(via_gc.dry_run, via_repair.dry_run);
137+
assert_eq!(via_gc.offline, via_repair.offline);
138+
assert_eq!(via_gc.download_only, via_repair.download_only);
139+
assert_eq!(via_gc.json, via_repair.json);
140+
}
141+
142+
#[test]
143+
fn repair_gc_alias_accepts_flags() {
144+
let args = parse_gc(&["--dry-run"]);
145+
assert!(args.dry_run);
146+
}
147+
148+
#[test]
149+
fn repair_unknown_flag_is_unknown_argument_error() {
150+
let err = match Cli::try_parse_from(["socket-patch", "repair", "--nope"]) {
151+
Ok(_) => panic!("unknown flag should fail to parse"),
152+
Err(e) => e,
153+
};
154+
assert_eq!(err.kind(), clap::error::ErrorKind::UnknownArgument);
155+
}

0 commit comments

Comments
 (0)