Marketplace sync: keep installs the host could not build (final review M3)
Secret Scan / scan (push) Successful in 6s
Build App (Preview) / compute-version (pull_request) Successful in 5s
Secret Scan / scan (pull_request) Successful in 6s
Build App (Preview) / create-release (pull_request) Successful in 2s
Build App (Preview) / build-macos (pull_request) Successful in 3m49s
Build App (Preview) / test (pull_request) Successful in 5m37s
Build App (Preview) / build-windows (pull_request) Successful in 7m20s
Build App (Preview) / build-linux (pull_request) Successful in 8m7s
Build App (Preview) / prune-previews (pull_request) Successful in 1s

An install the host skipped (pinned commit missing from the cache, cache
unreadable, item failing a tightened validation rule) never reached the
manifest, so sync.sh treated it as deselected and deleted it from the
container. The manifest now carries `held`: the state ids of such
installs ("plugin:<slug>/<key>" for plugins). The script counts them as
still selected and carries their records forward, as it already does
for items that fail inside the container. A removed marketplace is the
one skip that still removes; a malformed `held` list changes nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-27 10:16:27 -07:00
co-authored by Claude Opus 5.5
parent 5829c42f0f
commit f2bb092586
4 changed files with 155 additions and 11 deletions
@@ -1057,3 +1057,90 @@ fn a_slug_change_reinstalls_under_the_new_name_and_retires_the_old() {
let r = run(&env);
assert!(nothing_reported(&r), "{r:?}");
}
// ── Host-side holds (final review M3) ────────────────────────────────────────
/// Everything `install_all` installed, plus plugin `p` from SLUG_A.
fn install_all_and_a_plugin(env: &Env) {
let (base, mut manifest) = all_kinds(C1);
let mut files: Vec<(String, String, bool)> = base
.iter()
.map(|(p, t, e)| (p.to_string(), t.to_string(), *e))
.collect();
files.push((
format!("plugins/{SLUG_A}/.claude-plugin/marketplace.json"),
format!(
r#"{{"name":"triple-c-{SLUG_A}","owner":{{"name":"Triple-C"}},"plugins":[{{"name":"p","source":"./p"}}]}}"#
),
false,
));
files.push((
format!("plugins/{SLUG_A}/p/.claude-plugin/plugin.json"),
r#"{"name":"p"}"#.to_string(),
false,
));
manifest["items"].as_array_mut().unwrap().push(
json!({ "kind": "plugin", "key": "p", "marketplace": "m1", "commit": C1, "slug": SLUG_A }),
);
manifest["plugin_marketplaces"] =
json!([{ "slug": SLUG_A, "dir": format!("plugins/{SLUG_A}"), "plugins": ["p"] }]);
let refs: Vec<(&str, &str, bool)> = files
.iter()
.map(|(p, t, e)| (p.as_str(), t.as_str(), *e))
.collect();
payload(env, &refs, manifest);
let r = run(env);
assert_eq!(r.installed.len(), 5, "{r:?}");
}
#[test]
fn held_items_are_kept_not_removed() {
let Some(env) = env() else { return };
install_all_and_a_plugin(&env);
let settings_path = env.home.join(".claude/settings.json");
let state_path = env.home.join(".claude/triple-c/marketplace/state.json");
let state_before = read_json(&state_path)["items"].clone();
// The host could build none of them this time (cache gone, say).
fs::remove_file(&env.log).unwrap();
let mut manifest = empty_manifest();
manifest["held"] = json!([
"agent:code-reviewer",
"skill:example-skill",
"command:example-command",
"hook:notify-on-stop",
format!("plugin:{SLUG_A}/p"),
]);
payload(&env, &[], manifest);
let r = run(&env);
assert!(nothing_reported(&r), "{r:?}");
assert_all_installed(&env);
assert!(claude_log(&env).is_empty(), "{:?}", claude_log(&env));
assert_eq!(read_json(&settings_path)["hooks"], hook_settings());
assert_eq!(read_json(&state_path)["items"], state_before);
assert!(env
.home
.join(format!(".claude/triple-c/plugins/{SLUG_A}"))
.is_dir());
// A real deselection afterwards still removes everything.
payload(&env, &[], empty_manifest());
let r = run(&env);
assert_eq!(r.removed.len(), 5, "{r:?}");
}
#[test]
fn a_malformed_held_list_changes_nothing() {
let Some(env) = env() else { return };
install_all(&env);
for bad in [json!("agent:code-reviewer"), json!([1]), json!({})] {
let mut manifest = empty_manifest();
manifest["held"] = bad.clone();
payload(&env, &[], manifest);
let r = run(&env);
assert!(r.removed.is_empty(), "{bad}: {r:?}");
assert_eq!(r.errors.len(), 1, "{bad}: {r:?}");
assert_all_installed(&env);
}
}