def load(p):
    raw = open(p, encoding='utf-8', newline='').read()
    return raw.replace('\r\n', '\n'), '\r\n' in raw

def save(p, s, crlf):
    open(p, 'w', encoding='utf-8', newline='').write(s.replace('\n', '\r\n') if crlf else s)

def once(s, old, new):
    assert s.count(old) == 1, (old[:80], s.count(old))
    return s.replace(old, new)

p = 'crates/spt-net/src/net/update.rs'
s, c = load(p)
s = once(s, 'mod tests {\n    use super::*;\n\n', '''mod tests {
    use super::*;

    /// The roster row EXACTLY as it was before `hostable_types` (main at
    /// 11a2f373): the decoder every peer still on an older core runs.
    #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
    struct PreW8Row {
        name: String,
        kind: String,
        version: String,
        sha256: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        signing_key: Option<String>,
    }

    // [unit->REQ-ADAPTER-LIST-SURFACE] the new field crosses the version line
    // BOTH ways. A new row decodes on the old type: serde's default for a
    // struct without deny_unknown_fields IGNORES the extra key, so the old
    // peer reads every field it knows unchanged. An old row decodes on the new
    // type with an empty list, and an empty list is never written.
    #[test]
    fn roster_row_hostable_types_round_trip_across_versions() {
        let new = AdapterRosterRow {
            name: "lecturn".into(),
            kind: "harness".into(),
            version: "0.3.0".into(),
            sha256: "ab".repeat(32),
            signing_key: None,
            hostable_types: vec!["LiveAgent".into(), "Gateway".into()],
        };
        let wire = serde_json::to_string(&new).unwrap();
        assert!(wire.contains(r#""hostable_types":["LiveAgent","Gateway"]"#), "{wire}");
        let old: PreW8Row = serde_json::from_str(&wire).expect("an old peer decodes a new row");
        assert_eq!(
            old,
            PreW8Row {
                name: "lecturn".into(),
                kind: "harness".into(),
                version: "0.3.0".into(),
                sha256: "ab".repeat(32),
                signing_key: None,
            }
        );

        let old_wire = serde_json::to_string(&old).unwrap();
        let upgraded: AdapterRosterRow = serde_json::from_str(&old_wire).expect("a new peer decodes an old row");
        assert!(upgraded.hostable_types.is_empty(), "{upgraded:?}");
        assert!(!serde_json::to_string(&upgraded).unwrap().contains("hostable_types"), "an empty list is not written");

        // And inside the record an old peer really receives: an AdapterOffer
        // line whose row carries the field still decodes whole.
        let line = UpdRecord::AdapterOffer { upd_id: "u".into(), row: new, signature_hex: None }.encode_line();
        let text = String::from_utf8(line).unwrap();
        let v: serde_json::Value = serde_json::from_str(text.trim_end()).unwrap();
        let row = v.pointer("/row").or_else(|| v.pointer("/AdapterOffer/row")).expect("row in the offer line");
        let _: PreW8Row = serde_json::from_value(row.clone()).expect("the old row type decodes the offer's row");
    }

''')
save(p, s, c)

p = 'crates/spt-daemon/src/relcache.rs'
s, c = load(p)
anchor = '''    // [unit->REQ-UPDATE-SOURCES-STORE] learn-on-serve writes a learned row'''
s = once(s, anchor, '''    // [unit->REQ-ADAPTER-LIST-SURFACE] the roster row carries the RUNNING
    // manifest's hostable types, which the retained sidecar does not hold.
    #[test]
    fn roster_rows_carry_the_manifests_hostable_types() {
        let dir = tempfile::tempdir().unwrap();
        let cache = ReleaseCache::open(dir.path());
        cache.retain_adapter("gw", b"SPT", &retained("1.0.0")).unwrap();
        let (r, mut m) = registered_at("gw", "1.0.0", true);
        m.adapter.hostable_types = vec!["Gateway".into()];
        let roster = cache.adapter_roster(&[(r, m)]);
        assert_eq!(roster.len(), 1);
        assert_eq!(roster[0].hostable_types, vec!["Gateway".to_string()]);
        assert!(cache.retained_adapter("gw").unwrap().row("gw").hostable_types.is_empty(), "the offer row stays empty");
    }

''' + anchor)
save(p, s, c)
print('ok')
