wsb-daemon 0.248.6265

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
//! Where the workbench layout is kept, and what survives being written.
//!
//! The layout lived in `.wsb/workbench.json`, written with a bare `fs::write`
//! beside the database that holds everything else about the workspace. These
//! are about the two things that made that wrong: a write that could destroy
//! the layout it was saving, and a second place for a workspace's state to
//! live.

use super::*;
use std::sync::Arc;

fn database() -> (tempfile::TempDir, Arc<wsb::persist::global::service::DatabaseService>) {
    let dir = tempfile::tempdir().expect("a scratch directory");
    let config = wsb::persist::global::config::DatabaseConfig {
        db_path: dir.path().join("wsb.db").display().to_string(),
        db_max_connections: 2,
        ..Default::default()
    };
    let service = Arc::new(
        wsb::persist::global::service::DatabaseService::new(&config).expect("a database"),
    );
    service.run_migrations(wsb::persist::global::Schema::Worktree).expect("the schema is current");
    (dir, service)
}

/// Save a layout the way the handler does, so the test exercises the statement
/// that actually runs rather than a second spelling of it.
fn save(
    service: &Arc<wsb::persist::global::service::DatabaseService>,
    root: &str,
    json: &str,
) {
    let conn = service.get_connection().expect("a connection");
    conn.execute(
        UPSERT_LAYOUT,
        rusqlite::params![root, json],
    )
    .expect("the layout is saved");
}

fn read(
    service: &Arc<wsb::persist::global::service::DatabaseService>,
    root: &str,
) -> Option<String> {
    use rusqlite::OptionalExtension;
    let conn = service.get_connection().expect("a connection");
    conn.query_row(
        SELECT_LAYOUT,
        [root],
        |row| row.get::<_, String>(0),
    )
    .optional()
    .expect("the read succeeds")
}

/// TEST12026: the layout round-trips through the database.
///
/// The table exists, the upsert replaces rather than duplicates, and what
/// comes back is what went in. A `PRIMARY KEY` that did not conflict-update
/// would make every save fail after the first, and the layout would freeze at
/// whatever was open the first time the daemon ran.
#[test]
fn test12026_a_layout_is_saved_and_read_back() {
    let (_dir, service) = database();
    let root = "/Users/somebody/ws/prj/machinefabric/main-worktree";

    assert_eq!(read(&service, root), None, "a fresh workspace has no layout");

    save(&service, root, r#"{"groups":[],"active_group_id":"group:1"}"#);
    assert_eq!(
        read(&service, root).as_deref(),
        Some(r#"{"groups":[],"active_group_id":"group:1"}"#)
    );

    // Saved again: replaced, not a second row.
    save(&service, root, r#"{"groups":[],"active_group_id":"group:2"}"#);
    assert_eq!(
        read(&service, root).as_deref(),
        Some(r#"{"groups":[],"active_group_id":"group:2"}"#),
        "a second save did not replace the first"
    );

    let conn = service.get_connection().expect("a connection");
    let rows: i64 = conn
        .query_row("SELECT count(*) FROM workbench_state", [], |row| row.get(0))
        .expect("the count reads");
    assert_eq!(rows, 1, "saving twice left {rows} rows");
}

/// TEST12027: two workspaces keep two layouts.
///
/// One daemon serves several, and the tabs open in one are about its files. A
/// single-row table โ€” or a key that was not the workspace โ€” would have the
/// second workspace's layout overwrite the first's, so opening one workspace
/// would silently rearrange another.
#[test]
fn test12027_each_workspace_keeps_its_own_layout() {
    let (_dir, service) = database();

    save(&service, "/ws/one", r#"{"which":"one"}"#);
    save(&service, "/ws/two", r#"{"which":"two"}"#);

    assert_eq!(read(&service, "/ws/one").as_deref(), Some(r#"{"which":"one"}"#));
    assert_eq!(read(&service, "/ws/two").as_deref(), Some(r#"{"which":"two"}"#));
}

/// TEST12028: the layout the frontend sends survives the round trip whole.
///
/// A tab's `input` is opaque JSON the backend never interprets โ€” that is the
/// one documented decision about this state, and it is why a column holds it
/// as well as a file did. Parsed into the wire type and back so the check is
/// on what the frontend would actually get.
#[test]
fn test12028_a_tabs_opaque_input_is_not_reshaped() {
    let (_dir, service) = database();
    let root = "/ws/one";

    let sent = serde_json::json!({
        "groups": [{
            "id": "group:1",
            "tabs": [{
                "id": "op-output:b54a0de8-9784-48a5-9a3d-735b3257ba54",
                "label": "suite ยท default",
                "description": "Operation output",
                // Whatever the frontend puts here comes back unchanged.
                "input": {"kind": "op-output", "opId": "b54a0de8", "nested": {"deep": [1, 2]}},
                "pinned": false
            }],
            "active_tab_id": "op-output:b54a0de8-9784-48a5-9a3d-735b3257ba54"
        }],
        "active_group_id": "group:1",
        "sidebar_open": true,
        "sidebar_active_container_id": "explorer",
        "activity_bar_order": ["explorer"],
        "bottom_panel_open": false,
        "bottom_panel_height": 200,
        "bottom_panel_active_id": "output",
        "bottom_panel_order": ["output"],
        "expanded_paths": ["src"]
    });

    let parsed: PersistedWorkbenchState =
        serde_json::from_value(sent.clone()).expect("the wire type parses what the frontend sends");
    save(
        &service,
        root,
        &serde_json::to_string(&parsed).expect("it serializes"),
    );

    let back: PersistedWorkbenchState =
        serde_json::from_str(&read(&service, root).expect("it was saved")).expect("it reads back");

    assert_eq!(back.groups.len(), 1);
    let tab = &back.groups[0].tabs[0];
    assert_eq!(tab.id, "op-output:b54a0de8-9784-48a5-9a3d-735b3257ba54");
    assert_eq!(
        tab.input,
        serde_json::json!({"kind": "op-output", "opId": "b54a0de8", "nested": {"deep": [1, 2]}}),
        "the tab's opaque input was reshaped on the way through"
    );
    assert_eq!(back.expanded_paths, vec!["src".to_string()]);
}