Skip to content

Commit 61c6665

Browse files
fix(store): realign git install_meta stamped from a since-corrected source
The scanner now resolves the real source from install manifests, but the git-source backfill only writes install_meta when install_type IS NULL, so a row stamped in an earlier sync (e.g. a plugin first attributed to the enclosing dotfiles repo) keeps its stale install_url. deriveExtensionUrl prefers install_url, so the corrected source_json.url stayed shadowed and the extension lingered in the wrong group. Add refresh_stale_git_install_meta: for install_type='git' rows whose install_url disagrees with the authoritative source_json.url, realign install_url/revision and clear pack so backfill_packs re-derives it. Rows already in agreement and non-git installs are untouched. Runs in both sync paths after the symlink heal, before backfill_packs. This is the store half of the manifest source-resolution fix (parallel to the RealZST#88 self-heal).
1 parent 0cf5ca2 commit 61c6665

1 file changed

Lines changed: 119 additions & 0 deletions

File tree

crates/hk-core/src/store.rs

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1065,6 +1065,11 @@ impl Store {
10651065
// pack backfill so the cleared rows don't re-acquire a pack.
10661066
Self::heal_symlinked_git_install_meta(&tx, extensions)?;
10671067

1068+
// Realign git install_meta the backfill stamped from a since-corrected
1069+
// source (e.g. plugins re-attributed to their marketplace repo). Run
1070+
// before pack backfill so pack re-derives from the refreshed URL.
1071+
Self::refresh_stale_git_install_meta(&tx)?;
1072+
10681073
// Backfill pack from install_url or source_json URL for deployed extensions
10691074
// that lost their git context after being copied to agent directories
10701075
Self::backfill_packs(&tx)?;
@@ -1171,6 +1176,8 @@ impl Store {
11711176

11721177
Self::heal_symlinked_git_install_meta(&tx, extensions)?;
11731178

1179+
Self::refresh_stale_git_install_meta(&tx)?;
1180+
11741181
Self::backfill_packs(&tx)?;
11751182

11761183
tx.commit()?;
@@ -1214,6 +1221,30 @@ impl Store {
12141221
Ok(())
12151222
}
12161223

1224+
/// Refresh `git` install_meta that the source backfill stamped from a now-
1225+
/// corrected source. The backfill (above) only fires on `install_type IS
1226+
/// NULL`, so a row stamped in an earlier sync keeps its old `install_url`
1227+
/// even after the scanner learns the real source (e.g. a plugin first seen
1228+
/// as the enclosing dotfiles repo, now resolved to its marketplace repo via
1229+
/// the install manifest). `deriveExtensionUrl` prefers `install_url`, so the
1230+
/// stale value would keep the extension in the wrong group. Realign
1231+
/// `install_url`/revision to the authoritative `source_json.url` and clear
1232+
/// `pack` so `backfill_packs` re-derives it. Rows already in agreement, and
1233+
/// non-git installs (marketplace/manual/cli), are untouched.
1234+
fn refresh_stale_git_install_meta(conn: &rusqlite::Connection) -> Result<(), HkError> {
1235+
conn.execute_batch(
1236+
"UPDATE extensions
1237+
SET install_url = json_extract(source_json, '$.url'),
1238+
install_url_resolved = NULL,
1239+
install_revision = json_extract(source_json, '$.commit_hash'),
1240+
remote_revision = NULL, checked_at = NULL, check_error = NULL, pack = NULL
1241+
WHERE install_type = 'git'
1242+
AND json_extract(source_json, '$.url') IS NOT NULL
1243+
AND install_url IS NOT json_extract(source_json, '$.url')",
1244+
)?;
1245+
Ok(())
1246+
}
1247+
12171248
/// Backfill `pack` from install_url, source_json URL, or child extensions.
12181249
/// Deployed skills lose their git context after being copied to agent directories,
12191250
/// but install_url retains the repo URL. CLI parent extensions inherit pack from children.
@@ -2736,6 +2767,94 @@ mod tests {
27362767
);
27372768
}
27382769

2770+
#[test]
2771+
fn test_sync_refreshes_stale_git_install_meta() {
2772+
// Regression: a plugin first scanned as the enclosing dotfiles repo got
2773+
// install_type='git' + install_url/pack of that repo. After the scanner
2774+
// learns the real marketplace source, the backfill (install_type IS
2775+
// NULL only) can't update it, so the stale install_url kept the plugin
2776+
// in the dotfiles group. Sync must realign install_url + pack to the
2777+
// corrected source; a git row already in agreement stays untouched.
2778+
let (store, _dir) = test_store();
2779+
2780+
// Polluted plugin: stale dotfiles install_meta, but the scan now reports
2781+
// the real marketplace source.
2782+
let mut polluted = sample_extension();
2783+
polluted.id = "plugin-cr".into();
2784+
polluted.kind = ExtensionKind::Plugin;
2785+
polluted.name = "code-review".into();
2786+
polluted.source.origin = SourceOrigin::Git;
2787+
polluted.source.url = Some("https://github.com/anthropics/claude-plugins-official".into());
2788+
store.insert_extension(&polluted).unwrap();
2789+
store
2790+
.set_install_meta(
2791+
"plugin-cr",
2792+
&InstallMeta {
2793+
install_type: "git".into(),
2794+
url: Some("https://github.com/octo/dotfiles".into()),
2795+
url_resolved: None,
2796+
branch: None,
2797+
subpath: None,
2798+
revision: None,
2799+
remote_revision: None,
2800+
checked_at: None,
2801+
check_error: None,
2802+
},
2803+
)
2804+
.unwrap();
2805+
store.update_pack("plugin-cr", Some("octo/dotfiles")).unwrap();
2806+
2807+
// Consistent git install: install_url already matches its source.
2808+
let mut consistent = sample_extension();
2809+
consistent.id = "plugin-ok".into();
2810+
consistent.kind = ExtensionKind::Plugin;
2811+
consistent.name = "ok".into();
2812+
consistent.source.origin = SourceOrigin::Git;
2813+
consistent.source.url = Some("https://github.com/real/repo".into());
2814+
store.insert_extension(&consistent).unwrap();
2815+
store
2816+
.set_install_meta(
2817+
"plugin-ok",
2818+
&InstallMeta {
2819+
install_type: "git".into(),
2820+
url: Some("https://github.com/real/repo".into()),
2821+
url_resolved: None,
2822+
branch: None,
2823+
subpath: None,
2824+
revision: None,
2825+
remote_revision: None,
2826+
checked_at: None,
2827+
check_error: None,
2828+
},
2829+
)
2830+
.unwrap();
2831+
store.update_pack("plugin-ok", Some("real/repo")).unwrap();
2832+
2833+
// Re-sync as the scanner now reports them (install_meta carried in DB).
2834+
let mut s_polluted = polluted.clone();
2835+
s_polluted.install_meta = None;
2836+
s_polluted.pack = None;
2837+
let mut s_consistent = consistent.clone();
2838+
s_consistent.install_meta = None;
2839+
s_consistent.pack = None;
2840+
store.sync_extensions(&[s_polluted, s_consistent]).unwrap();
2841+
2842+
let fixed = store.get_extension("plugin-cr").unwrap().unwrap();
2843+
assert_eq!(
2844+
fixed.install_meta.expect("install_meta kept").url.as_deref(),
2845+
Some("https://github.com/anthropics/claude-plugins-official"),
2846+
"stale install_url must be realigned to the corrected source"
2847+
);
2848+
assert_eq!(
2849+
fixed.pack.as_deref(),
2850+
Some("anthropics/claude-plugins-official"),
2851+
"pack must re-derive from the corrected source"
2852+
);
2853+
2854+
let kept = store.get_extension("plugin-ok").unwrap().unwrap();
2855+
assert_eq!(kept.pack.as_deref(), Some("real/repo"), "consistent git row untouched");
2856+
}
2857+
27392858
#[test]
27402859
fn test_stale_row_should_prune_decision() {
27412860
let cli = ExtensionKind::Cli.as_str();

0 commit comments

Comments
 (0)