Skip to main content

agentmux_srv/migrations/
m0037_retire_old_pane_names.rs

1// Copyright 2026, AgentMux Corp.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Rewrites the retired pane names in stored data, once, so the frontend and srv can drop the
5//! shims that translated them on every read. The Armory (and the Trust Center before it) was
6//! split into Connectors and Memory, the Knowledge pane was renamed Memory, and Remotes became a
7//! section of Connectors (`SPEC_RETIRE_ARMORY_CONNECTORS_AND_KNOWLEDGE_PANES_2026_10_05.md`
8//! Phase 3, `SPEC_RENAME_KNOWLEDGE_TO_MEMORY_2026_10_06.md`, `SPEC_REMOTES_INTO_CONNECTORS_2026_10_08.md`).
9//!
10//! For this channel:
11//! - blocks (`db_block`) saved as `view: "armory"`, `"trust"`, `"knowledge"` or `"remotes"` move to
12//!   `connectors` or `memory`, on the section they were on;
13//! - the same for every block in the client's session-restore snapshot (`session:last_topology`);
14//! - settings.json: old names in `widget:pinned` and `widget:hidden`, old `pane:colors` keys, and
15//!   keybindings naming `app:knowledge` or `app:identity` or a retired `viewType`;
16//! - widgets.json: entries under `defwidget@knowledge` or `defwidget@remotes`, and entries that open
17//!   a retired view.
18//!
19//! Layout files and `.abf` bundles live outside AgentMux and are still translated on import.
20//!
21//! Idempotent: a second run finds no old names. The settings and widget files are rewritten only
22//! when something changed. A file that can't be read or parsed is left alone (logged), since
23//! srv's own loaders already treat it as unreadable; the block and snapshot rewrite fails the
24//! migration (retried on the next boot) when the store can't be read.
25//!
26//! The mapping below is a frozen copy, as of 0.59.21, of the shims removed alongside it: the
27//! frontend's `armoryTarget` / `armoryMigrationPatch`, the Remotes legacy manifest, Memory's
28//! Knowledge alias and section-key fallback, `RENAMED_PINS`, and srv's `RENAMED_WIDGETS` /
29//! `retarget_remotes`.
30
31use std::path::Path;
32use std::sync::Arc;
33
34use serde_json::{Map, Value};
35
36#[cfg(not(test))]
37use crate::backend::config_watcher_fs::resolve_settings_dir;
38use crate::backend::storage::store::Store;
39use crate::backend::wconfig;
40
41use super::{Migration, MigrationContext, MigrationError, MigrationScope, VerifyOutcome};
42
43pub struct M0037RetireOldPaneNames;
44
45impl Migration for M0037RetireOldPaneNames {
46    fn id(&self) -> &'static str { "0037_retire_old_pane_names" }
47    fn scope(&self) -> MigrationScope { MigrationScope::Channel }
48    fn description(&self) -> &'static str {
49        "Move saved Armory, Knowledge and Remotes panes, pins, colors and keybindings to Connectors and Memory"
50    }
51
52    fn up(&self, ctx: &MigrationContext) -> Result<(), MigrationError> {
53        if ctx.channel_store_path.exists() {
54            let store = Arc::new(
55                Store::open(&ctx.channel_store_path)
56                    .map_err(|e| MigrationError(format!("retire_old_pane_names: open store: {e}")))?,
57            );
58            let blocks = migrate_blocks(&store).map_err(MigrationError)?;
59            let snapshots = migrate_client_snapshots(&store).map_err(MigrationError)?;
60            if blocks + snapshots > 0 {
61                tracing::info!(blocks, snapshots, "retire_old_pane_names: moved saved panes to Connectors and Memory");
62            }
63        }
64        if let Some(settings_dir) = settings_dir() {
65            migrate_settings_file(&settings_dir.join(wconfig::SETTINGS_FILE));
66            migrate_widgets_file(&settings_dir.join(crate::backend::user_widgets::USER_WIDGETS_FILE));
67        }
68        Ok(())
69    }
70
71    fn verify(&self, ctx: &MigrationContext) -> VerifyOutcome {
72        if !ctx.channel_store_path.exists() {
73            return VerifyOutcome::Ok("no channel store".into());
74        }
75        let store = match Store::open(&ctx.channel_store_path) {
76            Ok(s) => s,
77            Err(e) => return VerifyOutcome::Error(format!("open store: {e}")),
78        };
79        match old_view_block_count(&store) {
80            Ok(0) => VerifyOutcome::Ok("no block saved under a retired pane name".into()),
81            Ok(n) => VerifyOutcome::Mismatch(format!("{n} block(s) still saved under a retired pane name")),
82            Err(e) => VerifyOutcome::Error(e),
83        }
84    }
85}
86
87/// Where settings.json and widgets.json live. Not under test: the migration-runner tests apply
88/// every registered migration, and the real resolver would point them at the developer's own
89/// settings. The file rewrites are tested directly with explicit paths.
90#[cfg(not(test))]
91fn settings_dir() -> Option<std::path::PathBuf> {
92    Some(resolve_settings_dir())
93}
94#[cfg(test)]
95fn settings_dir() -> Option<std::path::PathBuf> {
96    None
97}
98
99const CONNECTORS: &str = "connectors";
100const MEMORY: &str = "memory";
101const CONNECTORS_SECTION: &str = "connectors:section";
102const MEMORY_SECTION: &str = "memory:section";
103const RETIRED_VIEWS: &[&str] = &["armory", "trust", "knowledge", "remotes"];
104
105/// A pane meta saved under a retired view, moved to its new pane. Returns whether it changed.
106fn migrate_meta(meta: &mut Map<String, Value>) -> bool {
107    let view = match meta.get("view").and_then(Value::as_str) {
108        Some(v) if RETIRED_VIEWS.contains(&v) => v.to_string(),
109        _ => return false,
110    };
111    match view.as_str() {
112        "armory" | "trust" => {
113            // Frozen copy of the removed armoryTarget/armoryMigrationPatch.
114            let section = meta.get("armory:section").and_then(Value::as_str).map(str::to_string);
115            let sub = meta.get("armory:memory:subsection").and_then(Value::as_str).map(str::to_string);
116            let (target_view, key, target_section) = match section.as_deref() {
117                Some("mcp") => (CONNECTORS, CONNECTORS_SECTION, "mcp"),
118                Some("memory") => (MEMORY, MEMORY_SECTION, if sub.as_deref() == Some("personal") { "personal" } else { "global" }),
119                Some("native_memory") => (MEMORY, MEMORY_SECTION, "personal"),
120                Some("skills") => (MEMORY, MEMORY_SECTION, "skills"),
121                Some("bundles") => (MEMORY, MEMORY_SECTION, "bundles"),
122                _ => (CONNECTORS, CONNECTORS_SECTION, "accounts"),
123            };
124            meta.insert("view".into(), target_view.into());
125            meta.insert(key.into(), target_section.into());
126            meta.remove("armory:section");
127            meta.remove("armory:memory:subsection");
128        }
129        "knowledge" => {
130            meta.insert("view".into(), MEMORY.into());
131            if let Some(section) = meta.remove("knowledge:section") {
132                meta.entry(MEMORY_SECTION.to_string()).or_insert(section);
133            }
134        }
135        "remotes" => {
136            // Frozen copy of the Remotes manifest's patch: Connectors on its Remotes section,
137            // keeping `remotes:expand`.
138            meta.insert("view".into(), CONNECTORS.into());
139            meta.insert(CONNECTORS_SECTION.into(), "remotes".into());
140        }
141        _ => unreachable!(),
142    }
143    true
144}
145
146/// Every object in `value` that carries a retired `view`, moved. Returns how many changed.
147fn migrate_tree(value: &mut Value) -> usize {
148    match value {
149        Value::Object(map) => {
150            let mut n = usize::from(migrate_meta(map));
151            for child in map.values_mut() {
152                n += migrate_tree(child);
153            }
154            n
155        }
156        Value::Array(items) => items.iter_mut().map(migrate_tree).sum(),
157        _ => 0,
158    }
159}
160
161fn read_rows(store: &Store, table: &str) -> Result<Vec<(String, Vec<u8>)>, String> {
162    let conn = store.conn().lock().unwrap();
163    let mut stmt = conn
164        .prepare(&format!("SELECT oid, data FROM {table}"))
165        .map_err(|e| format!("retire_old_pane_names: read {table}: {e}"))?;
166    let rows = stmt
167        .query_map([], |row| Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?)))
168        .map_err(|e| format!("retire_old_pane_names: read {table}: {e}"))?
169        .filter_map(Result::ok)
170        .collect();
171    Ok(rows)
172}
173
174fn migrate_blocks(store: &Store) -> Result<usize, String> {
175    let mut changed = 0;
176    for (oid, data) in read_rows(store, "db_block")? {
177        let Ok(mut block) = serde_json::from_slice::<Value>(&data) else { continue };
178        let moved = match block.get_mut("meta") {
179            Some(Value::Object(meta)) => migrate_meta(meta),
180            _ => false,
181        };
182        if moved {
183            store
184                .update_raw("block", &oid, &block)
185                .map_err(|e| format!("retire_old_pane_names: update block {oid}: {e}"))?;
186            changed += 1;
187        }
188    }
189    Ok(changed)
190}
191
192const SNAPSHOT_META_KEY: &str = "session:last_topology";
193
194fn migrate_client_snapshots(store: &Store) -> Result<usize, String> {
195    let mut changed = 0;
196    for (oid, data) in read_rows(store, "db_client")? {
197        let Ok(mut client) = serde_json::from_slice::<Value>(&data) else { continue };
198        let moved = client
199            .get_mut("meta")
200            .and_then(|m| m.get_mut(SNAPSHOT_META_KEY))
201            .map(migrate_tree)
202            .unwrap_or(0);
203        if moved > 0 {
204            store
205                .update_raw("client", &oid, &client)
206                .map_err(|e| format!("retire_old_pane_names: update client {oid}: {e}"))?;
207            changed += moved;
208        }
209    }
210    Ok(changed)
211}
212
213fn old_view_block_count(store: &Store) -> Result<usize, String> {
214    Ok(read_rows(store, "db_block")?
215        .into_iter()
216        .filter(|(_, data)| {
217            serde_json::from_slice::<Value>(data)
218                .ok()
219                .and_then(|b| b.get("meta")?.get("view")?.as_str().map(|v| RETIRED_VIEWS.contains(&v)))
220                .unwrap_or(false)
221        })
222        .count())
223}
224
225// ── settings.json ─────────────────────────────────────────────────
226
227/// Frozen copy of the removed RENAMED_PINS: a pin's short name, as it reads now.
228fn current_names(short: &str) -> Vec<&str> {
229    match short {
230        "armory" => vec![CONNECTORS, MEMORY],
231        "knowledge" => vec![MEMORY],
232        "remotes" => vec![CONNECTORS],
233        other => vec![other],
234    }
235}
236
237fn dedup(items: Vec<String>) -> Vec<String> {
238    let mut seen = std::collections::HashSet::new();
239    items.into_iter().filter(|s| seen.insert(s.clone())).collect()
240}
241
242/// `widget:pinned` (short names) and `widget:hidden` (full keys) with old names mapped.
243fn migrate_widget_list(value: &Value, full_keys: bool) -> Option<Value> {
244    let items = value.as_array()?;
245    let mut out = Vec::new();
246    let mut changed = false;
247    for item in items {
248        let Some(s) = item.as_str() else {
249            out.push(item.clone());
250            continue;
251        };
252        let (prefix, short) = match (full_keys, s.strip_prefix("defwidget@")) {
253            (true, Some(short)) => ("defwidget@", short),
254            (true, None) => ("", s),
255            (false, _) => ("", s),
256        };
257        let names = if !full_keys || !prefix.is_empty() { current_names(short) } else { vec![short] };
258        if names != [short] {
259            changed = true;
260        }
261        out.extend(names.into_iter().map(|n| Value::String(format!("{prefix}{n}"))));
262    }
263    if !changed {
264        return None;
265    }
266    let strings: Vec<String> = out.iter().filter_map(|v| v.as_str().map(str::to_string)).collect();
267    Some(Value::Array(dedup(strings).into_iter().map(Value::String).collect()))
268}
269
270/// `pane:colors` with a color saved under a retired view carried to its new view, unless the new
271/// view already has one. The Armory's own color (`armory`, `trust`) followed Connectors, where
272/// the Armory opened.
273fn migrate_colors(value: &Value) -> Option<Value> {
274    let colors = value.as_object()?;
275    let renames = [("knowledge", MEMORY), ("remotes", CONNECTORS), ("trust", CONNECTORS), ("armory", CONNECTORS)];
276    if !renames.iter().any(|(old, _)| colors.contains_key(*old)) {
277        return None;
278    }
279    let mut out = colors.clone();
280    for (old, new) in renames {
281        if let Some(color) = out.remove(old) {
282            out.entry(new.to_string()).or_insert(color);
283        }
284    }
285    Some(Value::Object(out))
286}
287
288fn migrate_command(command: &str) -> Option<String> {
289    let (unbind, id) = match command.strip_prefix('-') {
290        Some(id) => ("-", id),
291        None => ("", command),
292    };
293    let new = match id {
294        "app:knowledge" => "app:memory",
295        "app:identity" => "app:connectors",
296        _ => return None,
297    };
298    Some(format!("{unbind}{new}"))
299}
300
301fn migrate_when(when: &str) -> Option<String> {
302    let mut out = when.to_string();
303    for (old, new) in [("knowledge", MEMORY), ("remotes", CONNECTORS), ("armory", CONNECTORS), ("trust", CONNECTORS)] {
304        for op in ["==", "!="] {
305            out = out.replace(&format!("viewType {op} {old}"), &format!("viewType {op} {new}"));
306        }
307    }
308    (out != when).then_some(out)
309}
310
311/// Keybinding entries naming a retired command or view.
312fn migrate_keybindings(value: &Value) -> Option<Value> {
313    let entries = value.as_array()?;
314    let mut out = entries.clone();
315    let mut changed = false;
316    for entry in out.iter_mut() {
317        let Some(obj) = entry.as_object_mut() else { continue };
318        if let Some(new) = obj.get("command").and_then(Value::as_str).and_then(migrate_command) {
319            obj.insert("command".into(), new.into());
320            changed = true;
321        }
322        if let Some(new) = obj.get("when").and_then(Value::as_str).and_then(migrate_when) {
323            obj.insert("when".into(), new.into());
324            changed = true;
325        }
326    }
327    changed.then_some(Value::Array(out))
328}
329
330/// The settings keys that change, with their new values; empty when nothing does.
331fn migrated_settings(settings: &Map<String, Value>) -> Map<String, Value> {
332    let mut changed = Map::new();
333    if let Some(v) = settings.get("widget:pinned").and_then(|v| migrate_widget_list(v, false)) {
334        changed.insert("widget:pinned".into(), v);
335    }
336    if let Some(v) = settings.get("widget:hidden").and_then(|v| migrate_widget_list(v, true)) {
337        changed.insert("widget:hidden".into(), v);
338    }
339    if let Some(v) = settings.get("pane:colors").and_then(migrate_colors) {
340        changed.insert("pane:colors".into(), v);
341    }
342    if let Some(v) = settings.get("keybindings").and_then(migrate_keybindings) {
343        changed.insert("keybindings".into(), v);
344    }
345    changed
346}
347
348fn migrate_settings_file(path: &Path) {
349    if !path.exists() {
350        return;
351    }
352    let mut settings = wconfig::read_settings_raw_jsonc(path);
353    let changed = migrated_settings(&settings);
354    if changed.is_empty() {
355        return;
356    }
357    let keys: Vec<String> = changed.keys().cloned().collect();
358    settings.extend(changed);
359    let merged = wconfig::merge_into_template(wconfig::SETTINGS_TEMPLATE, &settings);
360    match std::fs::write(path, merged) {
361        Ok(()) => tracing::info!(?keys, "retire_old_pane_names: settings.json moved to Connectors and Memory"),
362        Err(e) => tracing::warn!(error = %e, "retire_old_pane_names: couldn't write settings.json; old names left"),
363    }
364}
365
366// ── widgets.json ──────────────────────────────────────────────────
367
368/// widgets.json with entries under a retired key moved to the new key (an entry the user already
369/// wrote under the new key wins), and any entry that opens a retired view retargeted.
370fn migrated_widgets(widgets: &Map<String, Value>) -> Option<Map<String, Value>> {
371    let mut out = widgets.clone();
372    let mut changed = false;
373    for (old, new) in [("defwidget@knowledge", "defwidget@memory"), ("defwidget@remotes", "defwidget@connectors")] {
374        if let Some(entry) = out.remove(old) {
375            changed = true;
376            out.entry(new.to_string()).or_insert(entry);
377        }
378    }
379    for entry in out.values_mut() {
380        if let Some(Value::Object(meta)) = entry.get_mut("blockdef").and_then(|b| b.get_mut("meta")) {
381            changed |= migrate_meta(meta);
382        }
383    }
384    changed.then_some(out)
385}
386
387fn migrate_widgets_file(path: &Path) {
388    let Ok(content) = std::fs::read_to_string(path) else { return };
389    if content.trim().is_empty() {
390        return;
391    }
392    let widgets: Map<String, Value> = match serde_json::from_str(&content) {
393        Ok(w) => w,
394        Err(e) => {
395            tracing::warn!(error = %e, "retire_old_pane_names: widgets.json doesn't parse; left alone");
396            return;
397        }
398    };
399    let Some(next) = migrated_widgets(&widgets) else { return };
400    let text = match serde_json::to_string_pretty(&next) {
401        Ok(t) => t + "\n",
402        Err(_) => return,
403    };
404    match std::fs::write(path, text) {
405        Ok(()) => tracing::info!("retire_old_pane_names: widgets.json moved to Connectors and Memory"),
406        Err(e) => tracing::warn!(error = %e, "retire_old_pane_names: couldn't write widgets.json; old names left"),
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413    use serde_json::json;
414
415    fn meta(v: Value) -> Map<String, Value> {
416        v.as_object().unwrap().clone()
417    }
418
419    #[test]
420    fn armory_and_trust_go_where_their_section_was() {
421        let mut m = meta(json!({"view": "armory", "armory:section": "skills", "term:zoom": 1.2}));
422        assert!(migrate_meta(&mut m));
423        assert_eq!(Value::Object(m), json!({"view": "memory", "memory:section": "skills", "term:zoom": 1.2}));
424
425        let mut m = meta(json!({"view": "armory", "armory:section": "memory", "armory:memory:subsection": "personal"}));
426        migrate_meta(&mut m);
427        assert_eq!(Value::Object(m), json!({"view": "memory", "memory:section": "personal"}));
428
429        let mut m = meta(json!({"view": "trust"}));
430        migrate_meta(&mut m);
431        assert_eq!(Value::Object(m), json!({"view": "connectors", "connectors:section": "accounts"}));
432
433        let mut m = meta(json!({"view": "armory", "armory:section": "mcp"}));
434        migrate_meta(&mut m);
435        assert_eq!(Value::Object(m), json!({"view": "connectors", "connectors:section": "mcp"}));
436    }
437
438    #[test]
439    fn knowledge_and_remotes_move_and_others_are_untouched() {
440        let mut m = meta(json!({"view": "knowledge", "knowledge:section": "bundles"}));
441        assert!(migrate_meta(&mut m));
442        assert_eq!(Value::Object(m), json!({"view": "memory", "memory:section": "bundles"}));
443
444        let mut m = meta(json!({"view": "remotes", "remotes:expand": "box"}));
445        migrate_meta(&mut m);
446        assert_eq!(Value::Object(m), json!({"view": "connectors", "connectors:section": "remotes", "remotes:expand": "box"}));
447
448        let mut m = meta(json!({"view": "term", "armory:section": "skills"}));
449        assert!(!migrate_meta(&mut m));
450    }
451
452    #[test]
453    fn a_snapshot_tree_is_walked() {
454        let mut snap = json!({"windows": [{"tabs": [{"blocks": [{"meta": {"view": "knowledge"}}, {"meta": {"view": "agent"}}]}]}]});
455        assert_eq!(migrate_tree(&mut snap), 1);
456        assert_eq!(snap["windows"][0]["tabs"][0]["blocks"][0]["meta"]["view"], "memory");
457    }
458
459    #[test]
460    fn settings_keys_move_and_untouched_settings_change_nothing() {
461        let settings = meta(json!({
462            "widget:pinned": ["agent", "armory", "memory", "remotes"],
463            "widget:hidden": ["defwidget@knowledge", "ext@acme.board/main"],
464            "pane:colors": {"knowledge": 120, "memory": 200, "trust": 30},
465            "keybindings": [
466                {"key": "ctrl+k", "command": "app:knowledge"},
467                {"command": "-app:identity"},
468                {"key": "ctrl+e", "command": "split:right", "when": "viewType == knowledge && !textInputFocus"}
469            ],
470            "term:fontsize": 14
471        }));
472        let changed = migrated_settings(&settings);
473        assert_eq!(changed["widget:pinned"], json!(["agent", "connectors", "memory"]));
474        assert_eq!(changed["widget:hidden"], json!(["defwidget@memory", "ext@acme.board/main"]));
475        assert_eq!(changed["pane:colors"], json!({"memory": 200, "connectors": 30}));
476        assert_eq!(changed["keybindings"][0]["command"], "app:memory");
477        assert_eq!(changed["keybindings"][1]["command"], "-app:connectors");
478        assert_eq!(changed["keybindings"][2]["when"], "viewType == memory && !textInputFocus");
479        assert!(!changed.contains_key("term:fontsize"));
480
481        let clean = meta(json!({"widget:pinned": ["agent"], "pane:colors": {"memory": 1}, "keybindings": []}));
482        assert!(migrated_settings(&clean).is_empty());
483    }
484
485    #[test]
486    fn widgets_json_entries_move_and_a_new_key_wins() {
487        let widgets = meta(json!({
488            "defwidget@remotes": {"label": "My remotes", "blockdef": {"meta": {"view": "remotes"}}},
489            "defwidget@knowledge": {"label": "Old"},
490            "defwidget@memory": {"label": "Mine"},
491            "defwidget@x": {"blockdef": {"meta": {"view": "armory", "armory:section": "bundles"}}}
492        }));
493        let next = migrated_widgets(&widgets).unwrap();
494        assert_eq!(next["defwidget@connectors"]["blockdef"]["meta"], json!({"view": "connectors", "connectors:section": "remotes"}));
495        assert_eq!(next["defwidget@memory"]["label"], "Mine");
496        assert_eq!(next["defwidget@x"]["blockdef"]["meta"], json!({"view": "memory", "memory:section": "bundles"}));
497        assert!(!next.contains_key("defwidget@remotes") && !next.contains_key("defwidget@knowledge"));
498        assert!(migrated_widgets(&meta(json!({"defwidget@agent": {"label": "A"}}))).is_none());
499    }
500
501    #[test]
502    fn files_are_rewritten_only_when_they_change() {
503        let dir = tempfile::tempdir().unwrap();
504        let settings = dir.path().join("settings.json");
505        std::fs::write(&settings, r#"{ "widget:pinned": ["knowledge"], "term:fontsize": 14 }"#).unwrap();
506        migrate_settings_file(&settings);
507        let after = wconfig::read_settings_raw_jsonc(&settings);
508        assert_eq!(after["widget:pinned"], json!(["memory"]));
509        assert_eq!(after["term:fontsize"], json!(14));
510
511        let widgets = dir.path().join("widgets.json");
512        std::fs::write(&widgets, "{\"defwidget@agent\": {\"label\": \"A\"}}").unwrap();
513        migrate_widgets_file(&widgets);
514        assert_eq!(std::fs::read_to_string(&widgets).unwrap(), "{\"defwidget@agent\": {\"label\": \"A\"}}");
515    }
516
517    #[test]
518    fn blocks_and_snapshots_in_the_store_are_moved() {
519        let dir = tempfile::tempdir().unwrap();
520        let store = Store::open(&dir.path().join("objects.db")).unwrap();
521        let mut block = crate::backend::obj::Block {
522            oid: "b1".into(),
523            meta: meta(json!({"view": "armory", "armory:section": "bundles"})).into_iter().collect(),
524            ..Default::default()
525        };
526        store.insert(&mut block).unwrap();
527        let mut other = crate::backend::obj::Block {
528            oid: "b2".into(),
529            meta: meta(json!({"view": "term"})).into_iter().collect(),
530            ..Default::default()
531        };
532        store.insert(&mut other).unwrap();
533        assert_eq!(old_view_block_count(&store).unwrap(), 1);
534        assert_eq!(migrate_blocks(&store).unwrap(), 1);
535        assert_eq!(migrate_blocks(&store).unwrap(), 0, "idempotent");
536        let b1 = store.get_raw("block", "b1").unwrap().unwrap();
537        assert_eq!(b1["meta"]["view"], "memory");
538        assert_eq!(b1["meta"]["memory:section"], "bundles");
539        assert_eq!(old_view_block_count(&store).unwrap(), 0);
540    }
541}