feat: resolve review comments through the CLI
This commit is contained in:
@@ -21,6 +21,7 @@ vamp --review list --repo . --json
|
||||
vamp --review show 1 --repo . --json
|
||||
vamp --review show 1 --repo . --all
|
||||
vamp --review delete 1 3 --repo . # Delete comment 3 from review 1
|
||||
vamp --review resolve 1 3 --repo . # Mark comment 3 resolved, preserving its feedback
|
||||
```
|
||||
|
||||
The review CLI discovers the Git/Jujutsu checkout from `--repo` (default: current directory). It uses the same default configuration, `--config PATH`, `--no-config`, and `review_directory` as the editor, but starts no editor workers or terminal UI. Missing storage lists no reviews without creating a database; incompatible storage is rejected and preserved.
|
||||
@@ -29,7 +30,7 @@ Use the persistent **Review ID** printed by `list`, not a list position. IDs are
|
||||
|
||||
`show` includes unresolved comments by default; `--all` includes resolved comments too. Human-readable output is the default. JSON includes the canonical repository root, storage review ID/revision, summary/verdict, total/resolved/unresolved counts, and comments with exact zero-based, end-exclusive grapheme anchors and selected source excerpts—not whole source files. `list --json` returns the repository and review metadata/counts without comments. Explicit editor JSON exports still contain complete anchor source text.
|
||||
|
||||
`list` and `show` are read-only. `delete REVIEW_ID COMMENT_ID` immediately commits deletion of one resolved or unresolved comment, then prints the updated review with its remaining unresolved comments (`--json` is supported). Both IDs are required; missing reviews/comments and repository mismatches fail without changing stored reviews or creating missing storage. Deleting the last comment keeps the review, summary, and verdict. Concurrent saves use the same revision-conflict protection as the editor; an already-open editor must reload before saving after a CLI deletion. Existing JSON exports are not modified.
|
||||
`list` and `show` are read-only. `delete REVIEW_ID COMMENT_ID` immediately commits deletion of one resolved or unresolved comment. `resolve REVIEW_ID COMMENT_ID` instead marks that comment resolved, preserving its body and original anchor. Repeating resolution succeeds without writing or advancing the review revision. Both commands print the updated review with its remaining unresolved comments (`--json` is supported); use `show REVIEW_ID --all` to verify resolved feedback. Both IDs are required; missing reviews/comments and repository mismatches fail without changing stored reviews or creating missing storage. Deleting the last comment keeps the review, summary, and verdict. Concurrent saves use the same revision-conflict protection as the editor; an already-open editor must reload before saving after a CLI change. Existing JSON exports are not modified.
|
||||
|
||||
`vamp review` still opens a file named `review`; `vamp -- --review` opens a file named `--review`.
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ One editor crate; the workspace also includes its patched Crossterm dependency.
|
||||
- `repository`: discovers jj/Git repositories and local/advertised remote revisions, builds disk snapshots and changed-file hunks, and owns isolated temporary comparison repositories with bounded, cancellable subprocesses. It never saves editor buffers or modifies active refs/checkouts.
|
||||
- `review`: owns the validated local review model, exact immutable anchors, persisted file checkmarks, SQLite storage through bundled rusqlite, and atomic JSON import/export with external-change protection. Schema 3 stores review identity, repository, revision, summary, and verdict in `reviews`; `comments` stores ordered comment IDs, bodies, resolution states, and explicit immutable anchor fields. `reviewed_files` stores revision pairs, paths, optional old paths, and optional exact live After text. Foreign keys, unique keys, and checks enforce relationships and scalar invariants. Full `u64` comment IDs use checked decimal text because SQLite integers are signed. No JSON documents are stored in SQLite.
|
||||
`Store` retains compare-and-swap review revisions and atomic whole-review saves: metadata and replacement comment/checkmark rows commit together. Read transactions reconstruct validated reviews from one consistent snapshot, including when listing in descending ID order. Stored text is bounded to 64 MiB per review, including a fixed per-comment/checkmark overhead. JSON version 4 is an independent import/export format. Incompatible databases, including schemas 1 and 2, are rejected and preserved without migration or reset. CLI metadata, sole-review restoration, and selector labels reuse loaded reviews; selector choices live only in the displayed buffer.
|
||||
The review CLI keeps `list`/`show` read-only. Comment deletion validates the explicit review/comment IDs through an existing read-only store before opening it for a compare-and-swap save; absent storage and invalid targets do not create reviews or a database.
|
||||
The review CLI keeps `list`/`show` read-only. Comment deletion and resolution validate the explicit review/comment IDs through an existing read-only store before opening it for a compare-and-swap save; absent storage and invalid targets do not create reviews or a database. Resolution preserves comment bodies and immutable anchors; already-resolved comments succeed without writing or advancing the revision.
|
||||
`editor_review` owns tab-local review state independently of comparisons. Ordinary repository source buffers and diff panes share the comment editor, immutable anchors, storage, and review commands. Comment/summary text saves explicitly; dismissing discards that editor's unsaved text, while switching may retain hidden drafts. Verdicts/deletions/resolution changes commit immediately without saving drafts. It restores a sole stored review or opens the floating selector for multiple reviews. Exports retain their own file baseline and do not become automatic persistence targets; imports get fresh database identities. Review editors remain tab-owned native panes outside the workspace `PaneTree`. The session supplies the configured or XDG default directory; direct `Editor` callers inject a directory with `set_review_directory` to enable persistence.
|
||||
Successful SQLite autosaves and JSON-only saves share one acknowledgement operation: clear review dirtiness and advance the owning tab's native draft saved baselines only after persistence succeeds. Failed writes retain unsaved state; JSON exports do not acknowledge a pending SQLite save.
|
||||
Review replacement and comment deletion are private editor-owned operations. Replacement loads and validates its Stored/JSON/New source before retiring old bindings, installing the document, and resetting selection/navigation. Deletion retires and acknowledges only the matching comment drafts, preventing stale text from overwriting reused IDs. Both persist before opening the overview; a post-mutation save failure retains the applied, unsaved identity change. Native text/undo and general transition guards remain unchanged.
|
||||
|
||||
@@ -107,6 +107,7 @@ SQLite review checks cover exact anchors across restarts and revision changes, r
|
||||
Manual-resolution checks cover immediate persistence, reopen/export/import, independent stale status, conflict-retained drafts, and incompatible version preservation. CLI checks cover filename-safe dispatch, Unicode excerpts, unresolved filtering/counts, absent storage, read-only access and permissions, symlink rejection, and incompatible schema preservation.
|
||||
Live PTY/CLI smoke composes and explicitly saves a comment, resolves it in the editor, observes an empty default CLI comment list with explicit counts, retrieves the resolved comment and exact excerpt with `--all`, and unresolves it. Each transition advances the same stored review revision; listing absent storage creates no data directory.
|
||||
CLI deletion checks exercise explicit review/comment IDs, resolved-comment deletion, repository isolation, preserved summaries/verdicts/other comments, last-comment deletion, missing targets/storage, and stale-save conflicts through the executable. CLI smoke verifies persisted deletion and rejects a repeated deletion without advancing the review revision.
|
||||
CLI resolution checks exercise positive review/comment IDs, preserved bodies/immutable anchors and other review metadata, unresolved filtering/full counts, `show --all`, repository isolation, missing/invalid targets and absent storage, idempotent repeats without revision changes, and stale-save rejection through the executable. CLI smoke: show a review as JSON, resolve one comment with `--repo PATH --json`, confirm counts and default filtering, show it again with `--all` to compare the original body/anchor/excerpt, and repeat resolution to confirm an unchanged storage revision.
|
||||
Relational-storage smoke inspects typed review/comment columns, round-trips exact Unicode anchors through the store and JSON export/import, deletes through the real CLI, and confirms the stored review and existing export remain intact.
|
||||
Reviewed-file checks cover SQLite/JSON round-trips, unmarking, exact revision pairs and rename paths across catalog reordering, lazy loading, live text/final-newline mismatch, edit-triggered invalidation, selected-review isolation, hidden-draft isolation, conflicts, and transactional rollback. Live PTY smoke marks a file in a real Git comparison, verifies the SQLite row, restarts the editor, observes the restored checkmark, unmarks it, and verifies row removal with unchanged source bytes.
|
||||
Per-file persistence also exercises a real Git Working comparison: mark two lazy files, save/reload the review, edit one file, refresh the snapshot, and retain only the unchanged file's mark through detail loading.
|
||||
|
||||
+3
-1
@@ -515,6 +515,8 @@ Comment and summary text is committed only by **`:w`** or **`:review.save`**, wh
|
||||
|
||||
From the shell, use `vamp --review delete REVIEW_ID COMMENT_ID [--repo PATH] [--json]` to delete a stored comment immediately without opening the editor. Find IDs with `vamp --review list` and `vamp --review show REVIEW_ID --all`. The repository defaults to the current checkout. Both resolved and unresolved comments can be deleted; the review and any JSON exports remain intact. An editor holding the old review revision must reload before saving.
|
||||
|
||||
Use `vamp --review resolve REVIEW_ID COMMENT_ID [--repo PATH] [--json]` after addressing and verifying feedback to mark it resolved without changing its body or original anchor. The command prints the updated review's remaining unresolved comments and full counts; `show REVIEW_ID --all` still includes the resolved original. Repeating resolution succeeds without a write or revision bump. Both IDs must be positive and refer to the selected repository; invalid targets and absent storage fail without creating or changing stored data. Like deletion, resolution rejects stale saves from an editor holding the previous review revision.
|
||||
|
||||
Storage defaults to `$XDG_DATA_HOME/vamp/reviews/reviews.sqlite3`, or `~/.local/share/vamp/reviews/reviews.sqlite3` when XDG_DATA_HOME is unset or relative. Configure `review_directory` to override its directory; relative configuration paths resolve against the config file. Repository identity is the canonical checkout root. Each review has a persistent database ID independent of comparison revisions, so reopening changed revisions retains stale comments. Opening a comparison automatically restores the repository's sole review. Multiple reviews open a floating selector: move to an entry and press Enter, or use `:review.open id`. Use `:review.new` for another independent review.
|
||||
|
||||
Write errors, locked databases, incompatible schemas, and conflicting edits from another editor instance retain dirty drafts and report the failure. Failed text saves leave the committed review unchanged; unrelated metadata autosaves cannot commit those drafts. Normal destructive closes remain guarded until drafts are explicitly saved or discarded. `:w!` does not bypass database revision conflicts; export your draft before reopening the current stored review. JSON exports retain external-file-change protection. Only the current database schema is accepted; existing incompatible databases are preserved rather than reset or migrated.
|
||||
@@ -527,7 +529,7 @@ Reopening requires the same repository root. Unchanged comment ranges follow edi
|
||||
|
||||
Ordinary source comments use an immutable content snapshot identity and capture the current buffer, including unsaved text; they do not require a commit or a diff. They retain the same SQLite/JSON formats. Opening a source comment navigates to its file and mapped selection; if that text is stale or the file is unavailable, it opens the preserved original text read-only instead of attaching feedback to different code.
|
||||
|
||||
Resolution is a manual human decision, independent of anchor freshness: a comment can be both resolved and stale. Overview entries and comment titles label resolved/unresolved status. Resolved comments remain in the overview and next/previous navigation, and can still be edited or reopened. Resolving never changes an anchor or deletes feedback.
|
||||
Resolution is an explicit reviewer or agent decision, independent of anchor freshness: a comment can be both resolved and stale. Overview entries and comment titles label resolved/unresolved status. Resolved comments remain in the overview and next/previous navigation, and can still be edited or reopened. Resolving never changes an anchor or deletes feedback.
|
||||
|
||||
|
||||
Text panes show change excerpts with **10 unchanged lines above and below** each change, preserving original file line numbers. Overlapping or touching excerpts merge; omitted stretches show an unchanged-line count in both panes. Files with only metadata changes show their notice or “No text changes” rather than unrelated source.
|
||||
|
||||
+2
-1
@@ -13,8 +13,9 @@ Usage: vamp [--config PATH | --no-config] [FILE]
|
||||
vamp [--config PATH | --no-config] --review list [--repo PATH] [--json]
|
||||
vamp [--config PATH | --no-config] --review show ID [--repo PATH] [--json] [--all]
|
||||
vamp [--config PATH | --no-config] --review delete REVIEW_ID COMMENT_ID [--repo PATH] [--json]
|
||||
vamp [--config PATH | --no-config] --review resolve REVIEW_ID COMMENT_ID [--repo PATH] [--json]
|
||||
|
||||
Review CLI: list/show are read-only; delete commits one comment deletion immediately.
|
||||
Review CLI: list/show are read-only; delete/resolve commit one comment change immediately.
|
||||
Show defaults to unresolved comments; --all includes resolved.
|
||||
Use vamp -- --review to edit a file named --review.
|
||||
|
||||
|
||||
+49
-19
@@ -9,9 +9,14 @@ use vamp::{
|
||||
ruby,
|
||||
};
|
||||
|
||||
enum CommentAction {
|
||||
Delete,
|
||||
Resolve,
|
||||
}
|
||||
|
||||
pub struct Request {
|
||||
id: Option<i64>,
|
||||
delete_comment: Option<u64>,
|
||||
comment: Option<(CommentAction, u64)>,
|
||||
repo: Option<PathBuf>,
|
||||
json: bool,
|
||||
all: bool,
|
||||
@@ -19,15 +24,17 @@ pub struct Request {
|
||||
|
||||
impl Request {
|
||||
pub fn parse(args: &mut impl Iterator<Item = OsString>) -> Result<Self, String> {
|
||||
let usage = "--review requires list, show ID, or delete REVIEW_ID COMMENT_ID";
|
||||
let usage = "--review requires list, show ID, delete REVIEW_ID COMMENT_ID, or resolve REVIEW_ID COMMENT_ID";
|
||||
let command = args.next().ok_or(usage)?;
|
||||
let id = if command == "list" {
|
||||
None
|
||||
} else if command == "show" || command == "delete" {
|
||||
} else if command == "show" || command == "delete" || command == "resolve" {
|
||||
let error = if command == "show" {
|
||||
"`show` requires a positive review ID.\nUsage: vamp --review show ID [--all]\n`--all` includes resolved comments; it does not select all reviews."
|
||||
} else {
|
||||
} else if command == "delete" {
|
||||
"`delete` requires a positive review ID.\nUsage: vamp --review delete REVIEW_ID COMMENT_ID"
|
||||
} else {
|
||||
"`resolve` requires a positive review ID.\nUsage: vamp --review resolve REVIEW_ID COMMENT_ID"
|
||||
};
|
||||
let id = args.next().ok_or(error)?;
|
||||
Some(
|
||||
@@ -39,21 +46,32 @@ impl Request {
|
||||
} else {
|
||||
return Err(usage.into());
|
||||
};
|
||||
let delete_comment = if command == "delete" {
|
||||
let error = "delete requires a positive comment ID";
|
||||
let comment = if command == "delete" || command == "resolve" {
|
||||
let error = if command == "delete" {
|
||||
"delete requires a positive comment ID"
|
||||
} else {
|
||||
"resolve requires a positive comment ID"
|
||||
};
|
||||
let id = args.next().ok_or(error)?;
|
||||
Some(
|
||||
id.to_str()
|
||||
.and_then(|id| id.parse::<u64>().ok())
|
||||
.filter(|id| *id > 0)
|
||||
.ok_or(error)?,
|
||||
)
|
||||
let id = id
|
||||
.to_str()
|
||||
.and_then(|id| id.parse::<u64>().ok())
|
||||
.filter(|id| *id > 0)
|
||||
.ok_or(error)?;
|
||||
Some((
|
||||
if command == "delete" {
|
||||
CommentAction::Delete
|
||||
} else {
|
||||
CommentAction::Resolve
|
||||
},
|
||||
id,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(Self {
|
||||
id,
|
||||
delete_comment,
|
||||
comment,
|
||||
repo: None,
|
||||
json: false,
|
||||
all: false,
|
||||
@@ -69,8 +87,7 @@ impl Request {
|
||||
self.repo = Some(PathBuf::from(args.next().ok_or("--repo requires a path")?));
|
||||
} else if arg == "--json" && !self.json {
|
||||
self.json = true;
|
||||
} else if arg == "--all" && self.id.is_some() && self.delete_comment.is_none() && !self.all
|
||||
{
|
||||
} else if arg == "--all" && self.id.is_some() && self.comment.is_none() && !self.all {
|
||||
self.all = true;
|
||||
} else {
|
||||
return Err(format!(
|
||||
@@ -124,14 +141,27 @@ pub fn run(request: &Request, config: Option<&Path>) -> Result<(), String> {
|
||||
root.display()
|
||||
))?
|
||||
.load(&root, id)?;
|
||||
if let Some(comment_id) = request.delete_comment {
|
||||
if let Some((action, comment_id)) = &request.comment {
|
||||
let index = review
|
||||
.comments
|
||||
.iter()
|
||||
.position(|comment| comment.id == comment_id)
|
||||
.position(|comment| comment.id == *comment_id)
|
||||
.ok_or_else(|| format!("Comment {comment_id} does not exist in review {id}"))?;
|
||||
review.comments.remove(index);
|
||||
Store::open(&directory)?.save(&mut review)?;
|
||||
let changed = match action {
|
||||
CommentAction::Delete => {
|
||||
review.comments.remove(index);
|
||||
true
|
||||
}
|
||||
CommentAction::Resolve => {
|
||||
let comment = &mut review.comments[index];
|
||||
let changed = !comment.resolved;
|
||||
comment.resolved = true;
|
||||
changed
|
||||
}
|
||||
};
|
||||
if changed {
|
||||
Store::open(&directory)?.save(&mut review)?;
|
||||
}
|
||||
}
|
||||
vec![review]
|
||||
} else {
|
||||
|
||||
@@ -128,3 +128,161 @@ fn cli_deletes_only_the_requested_comment_and_preserves_failed_targets() {
|
||||
assert!(!output.status.success());
|
||||
assert!(!storage.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_resolves_only_the_requested_comment_and_preserves_original_feedback() {
|
||||
let directory = Directory(
|
||||
std::env::temp_dir().join(format!("vamp-review-cli-resolve-{}", std::process::id())),
|
||||
);
|
||||
fs::create_dir(&directory.0).unwrap();
|
||||
let root = directory.0.canonicalize().unwrap();
|
||||
for name in ["repo", "other"] {
|
||||
let output = Command::new("jj")
|
||||
.args(["git", "init"])
|
||||
.arg(root.join(name))
|
||||
.env("JJ_CONFIG", "/dev/null")
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
}
|
||||
let config = root.join("config.rb");
|
||||
fs::write(
|
||||
&config,
|
||||
"Vamp.configure { |config| config.review_directory = 'reviews' }",
|
||||
)
|
||||
.unwrap();
|
||||
let storage = root.join("reviews");
|
||||
let mut store = Store::open(&storage).unwrap();
|
||||
let mut review = Review::new(root.join("repo"));
|
||||
review.summary = "Keep summary".into();
|
||||
review.verdict = Some(Verdict::ChangesRequested);
|
||||
review.comments = [u64::MAX, 2, 3]
|
||||
.into_iter()
|
||||
.map(|id| Comment {
|
||||
id,
|
||||
body: format!("Original feedback {id}\nKeep this history."),
|
||||
resolved: id == 3,
|
||||
anchor: Anchor {
|
||||
revision: "original-snapshot".into(),
|
||||
path: "old/file.rs".into(),
|
||||
side: Side::Before,
|
||||
start: TextPosition { row: 0, column: 1 },
|
||||
end: TextPosition { row: 1, column: 1 },
|
||||
content: "xe\u{301}👩💻\n界z\n".into(),
|
||||
},
|
||||
})
|
||||
.collect();
|
||||
store.save(&mut review).unwrap();
|
||||
let mut other = Review::new(root.join("other"));
|
||||
other.comments = review.comments.clone();
|
||||
store.save(&mut other).unwrap();
|
||||
let id = review.storage_id.unwrap();
|
||||
let id_text = id.to_string();
|
||||
let cli = |repo: &str, args: &[&str]| {
|
||||
Command::new(env!("CARGO_BIN_EXE_vamp"))
|
||||
.arg("--config")
|
||||
.arg(&config)
|
||||
.arg("--review")
|
||||
.args(args)
|
||||
.arg("--repo")
|
||||
.arg(root.join(repo))
|
||||
.current_dir(&root)
|
||||
.output()
|
||||
.unwrap()
|
||||
};
|
||||
let path = storage.join("reviews.sqlite3");
|
||||
let before = fs::read(&path).unwrap();
|
||||
for (repo, args) in [
|
||||
("repo", vec!["resolve"]),
|
||||
("repo", vec!["resolve", &id_text]),
|
||||
("repo", vec!["resolve", "0", "1"]),
|
||||
("repo", vec!["resolve", &id_text, "0"]),
|
||||
("repo", vec!["resolve", "999", "1"]),
|
||||
("repo", vec!["resolve", &id_text, "1"]),
|
||||
("other", vec!["resolve", &id_text, "2"]),
|
||||
("repo", vec!["resolve", &id_text, "2", "--all"]),
|
||||
] {
|
||||
let output = cli(repo, &args);
|
||||
assert!(!output.status.success(), "{:?}", output);
|
||||
assert_eq!(fs::read(&path).unwrap(), before);
|
||||
}
|
||||
let output = cli(
|
||||
"repo",
|
||||
&["resolve", &id_text, "18446744073709551615", "--json"],
|
||||
);
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
let json: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||
assert_eq!(json["id"], id);
|
||||
assert_eq!(json["revision"], review.storage_revision + 1);
|
||||
assert_eq!(
|
||||
json["counts"],
|
||||
serde_json::json!({"total": 3, "resolved": 2, "unresolved": 1})
|
||||
);
|
||||
assert_eq!(json["comments"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(json["comments"][0]["id"], 2);
|
||||
let loaded = store.load(&review.root, id).unwrap();
|
||||
let mut expected = review.comments.clone();
|
||||
expected[0].resolved = true;
|
||||
assert_eq!(loaded.comments, expected);
|
||||
assert_eq!(loaded.summary, review.summary);
|
||||
assert_eq!(loaded.verdict, review.verdict);
|
||||
assert_eq!(loaded.reviewed_files, review.reviewed_files);
|
||||
assert!(store.save(&mut review).is_err()); // Stale editors cannot undo resolution.
|
||||
assert_eq!(store.load(&review.root, id).unwrap().comments, expected);
|
||||
|
||||
let output = cli("repo", &["show", &id_text, "--all", "--json"]);
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
let all: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||
assert_eq!(all["comments"].as_array().unwrap().len(), 3);
|
||||
let original = &all["comments"][0];
|
||||
assert_eq!(original["id"], u64::MAX);
|
||||
assert_eq!(original["body"], review.comments[0].body);
|
||||
assert_eq!(original["resolved"], true);
|
||||
assert_eq!(original["anchor"]["revision"], "original-snapshot");
|
||||
assert_eq!(original["anchor"]["path"], "old/file.rs");
|
||||
assert_eq!(
|
||||
original["anchor"]["start"],
|
||||
serde_json::json!({"row": 0, "column": 1})
|
||||
);
|
||||
assert_eq!(original["excerpt"], "e\u{301}👩💻\n界");
|
||||
let output = cli("repo", &["show", &id_text, "--json"]);
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap(),
|
||||
json
|
||||
);
|
||||
let before = fs::read(&path).unwrap();
|
||||
for comment in ["18446744073709551615", "3"] {
|
||||
let output = cli("repo", &["resolve", &id_text, comment, "--json"]);
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
assert_eq!(
|
||||
serde_json::from_slice::<serde_json::Value>(&output.stdout).unwrap(),
|
||||
json
|
||||
);
|
||||
assert_eq!(fs::read(&path).unwrap(), before);
|
||||
}
|
||||
let output = cli("repo", &["list", "--json"]);
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
let list: serde_json::Value = serde_json::from_slice(&output.stdout).unwrap();
|
||||
assert_eq!(list["reviews"].as_array().unwrap().len(), 1);
|
||||
assert_eq!(list["reviews"][0]["counts"], json["counts"]);
|
||||
let output = cli("repo", &["resolve", &id_text, "2"]);
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
let text = String::from_utf8(output.stdout).unwrap();
|
||||
assert!(!text.contains("Original feedback"));
|
||||
let loaded = store.load(&review.root, id).unwrap();
|
||||
assert_eq!(loaded.storage_revision, review.storage_revision + 2);
|
||||
assert!(loaded.comments.iter().all(|comment| comment.resolved));
|
||||
assert_eq!(
|
||||
store
|
||||
.load(&other.root, other.storage_id.unwrap())
|
||||
.unwrap()
|
||||
.comments,
|
||||
other.comments
|
||||
);
|
||||
drop(store);
|
||||
fs::remove_dir_all(&storage).unwrap();
|
||||
let output = cli("repo", &["resolve", &id_text, "2"]);
|
||||
assert!(!output.status.success(), "{:?}", output);
|
||||
assert!(!storage.exists());
|
||||
}
|
||||
|
||||
@@ -105,3 +105,41 @@ fn existing_storage_is_read_only_and_unchanged() {
|
||||
}
|
||||
std::fs::remove_dir_all(directory).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comment_mutations_require_positive_explicit_ids_and_reject_all() {
|
||||
for command in ["delete", "resolve"] {
|
||||
for ids in [
|
||||
vec![],
|
||||
vec!["1"],
|
||||
vec!["0", "1"],
|
||||
vec!["-1", "1"],
|
||||
vec!["9223372036854775808", "1"],
|
||||
vec!["all", "1"],
|
||||
vec!["1", "0"],
|
||||
vec!["1", "-1"],
|
||||
vec!["1", "18446744073709551616"],
|
||||
vec!["1", "--json"],
|
||||
] {
|
||||
let mut args = std::iter::once(command).chain(ids).map(OsString::from);
|
||||
assert!(Request::parse(&mut args).is_err());
|
||||
}
|
||||
let mut args = [command, "9223372036854775807", "18446744073709551615"]
|
||||
.into_iter()
|
||||
.map(OsString::from);
|
||||
let mut request = Request::parse(&mut args).unwrap();
|
||||
assert_eq!(request.id, Some(i64::MAX));
|
||||
let (action, id) = request.comment.as_ref().unwrap();
|
||||
assert_eq!(*id, u64::MAX);
|
||||
assert!(matches!(
|
||||
(command, action),
|
||||
("delete", CommentAction::Delete) | ("resolve", CommentAction::Resolve)
|
||||
));
|
||||
assert!(request.option(OsStr::new("--all"), &mut args).is_err());
|
||||
request.option(OsStr::new("--json"), &mut args).unwrap();
|
||||
let mut repo = [OsString::from("checkout")].into_iter();
|
||||
request.option(OsStr::new("--repo"), &mut repo).unwrap();
|
||||
assert!(request.json);
|
||||
assert_eq!(request.repo, Some(PathBuf::from("checkout")));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user