One product per machine: the CLI and the desktop app share one version, the store accounts and the app folder list
- one version: package.json's version is the desktop app's too (desktopVersion removed); tools/desktop-version.ts keeps the plugin manifests and the app's copies equal, desktop-release publish refuses another version, release order app first then npm publish; the unpublished desktop 0.1.0 candidate is noted in the docs and CHANGELOG - storebound desktop installs (per user, no admin: macOS DMG -> /Applications or ~/Applications, Windows NSIS /S, Linux AppImage + .desktop with the storebound:// handler), updates an older app, notes a newer one (downgrade only with --force), asks once ([Y/n], --yes, exit 3 without a terminal), opens on the folder; --dry / --json / --download; the install steps are pure plans (cli/lib/desktop-install.ts); the version's own manifest dl/<version>/latest.json - credentials: one store, the CLI's credential profiles in ~/.storebound/credentials; the desktop writes and reads them (profiles.rs, files 0600), moves old keychain accounts into profiles on first start, shows profiles the CLI made; precedence env > named profile > project file > default profile; config `account`, "account" in the project's credentials.json; storebound accounts list | import (dedup by key id / service account e-mail, key files copied 0600, project file keeps keystore and review contact, backup, --dry, never prints values) | use - app folders: one list ~/.storebound/projects.json shared by the desktop (registry.rs), storebound desktop, apps folders | add | remove, ws init and ws without a workspace file - desktop: notice when a folder pins another storebound version; storebound-desktop --version - tests: version drift, desktop decisions and per-OS plans, credential precedence and import, registry (TS + Rust), keychain migration and profile round trip (Rust), boot check with old keychain state and the new shared state
This commit is contained in:
parent
bba46b5c7f
commit
d2ce185441
60 changed files with 2443 additions and 246 deletions
|
|
@ -4,11 +4,39 @@ use serde_json::{json, Value};
|
|||
use storebound_desktop::commands::project_view;
|
||||
use storebound_desktop::logs;
|
||||
use storebound_desktop::store::{self, State, VERSION};
|
||||
use storebound_desktop::accounts::{AccountKind, Secret, SecretStore};
|
||||
use storebound_desktop::profiles::{self, ProfileStore};
|
||||
use storebound_desktop::registry;
|
||||
use storebound_desktop::workspace::{Overrides, Settings};
|
||||
|
||||
const D1: &str = include_str!("../../tests/fixtures/state/d1-missing-folders.json");
|
||||
const D3: &str = include_str!("../../tests/fixtures/state/d3-full.json");
|
||||
const EMPTY: &str = include_str!("../../tests/fixtures/state/empty.json");
|
||||
const SHARED: &str = include_str!("../../tests/fixtures/state/shared-profiles.json");
|
||||
|
||||
/// Keychain entries of versions before 0.8, for the migration into credential profiles.
|
||||
#[derive(Default)]
|
||||
struct Keychain(std::sync::Mutex<std::collections::HashMap<String, Secret>>);
|
||||
impl SecretStore for Keychain {
|
||||
fn set(&self, id: &str, s: &Secret) -> Result<(), String> {
|
||||
self.0.lock().unwrap().insert(id.into(), s.clone());
|
||||
Ok(())
|
||||
}
|
||||
fn get(&self, id: &str) -> Result<Secret, String> {
|
||||
self.0.lock().unwrap().get(id).cloned().ok_or_else(|| "no entry".into())
|
||||
}
|
||||
fn delete(&self, id: &str) -> Result<(), String> {
|
||||
self.0.lock().unwrap().remove(id);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
const FAKE_PEM: &str = "-----BEGIN PRIVATE KEY-----\nnot-a-real-key\n-----END PRIVATE KEY-----\n";
|
||||
fn fake_secret(kind: AccountKind, n: usize) -> Secret {
|
||||
match kind {
|
||||
AccountKind::Apple => Secret::Apple { key_id: format!("FAKEKEY{n:03}"), issuer_id: "00000000-0000-0000-0000-000000000000".into(), p8: FAKE_PEM.into() },
|
||||
AccountKind::Play => Secret::Play { service_account: json!({ "client_email": format!("bot{n}@example.iam.gserviceaccount.com"), "private_key": FAKE_PEM }).to_string() },
|
||||
}
|
||||
}
|
||||
|
||||
fn write(dir: &std::path::Path, text: &str) {
|
||||
std::fs::write(dir.join(store::FILE), text).unwrap();
|
||||
|
|
@ -177,16 +205,43 @@ fn log_entries_are_one_block_and_rotate() {
|
|||
}
|
||||
|
||||
/// What the host answers for each fixture (projects_list without `missing`, accounts_list, workspace_get), written
|
||||
/// next to it as `<name>.host.json`. tools/desktop-boot-check.ts boots the built window on exactly these answers.
|
||||
/// next to it as `<name>.host.json`, after the start-up sync with the machine's shared stores: D1 keeps its accounts in a
|
||||
/// keychain that has no entries (the keys were never added: the accounts stay without a profile), D3's keychain has
|
||||
/// them (they move into credential profiles), and shared-profiles is the 0.8 format whose apple account comes from a
|
||||
/// profile the CLI wrote, with a folder that pins storebound 0.6.0. tools/desktop-boot-check.ts boots the built window on exactly these answers.
|
||||
/// After a change to the state format: STOREBOUND_UPDATE_FIXTURES=1 cargo test --test state
|
||||
#[test]
|
||||
fn host_answers_for_the_fixtures_are_current() {
|
||||
let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../tests/fixtures/state");
|
||||
for (name, text) in [("empty", EMPTY), ("d1-missing-folders", D1), ("d3-full", D3)] {
|
||||
let (st, _, notes) = store::migrate(serde_json::from_str(text).unwrap());
|
||||
for (name, text, keychain) in [("empty", EMPTY, false), ("d1-missing-folders", D1, false), ("d3-full", D3, true), ("shared-profiles", SHARED, false)] {
|
||||
let (mut st, _, notes) = store::migrate(serde_json::from_str(text).unwrap());
|
||||
assert!(notes.is_empty(), "{name}: {notes:?}");
|
||||
let machine = tempfile::tempdir().unwrap();
|
||||
let profiles = ProfileStore::new(machine.path().join("credentials"), None);
|
||||
let legacy = Keychain::default();
|
||||
if keychain {
|
||||
for (i, a) in st.accounts.iter().enumerate() {
|
||||
legacy.set(&a.id, &fake_secret(a.kind, i)).unwrap();
|
||||
}
|
||||
}
|
||||
if name == "shared-profiles" {
|
||||
profiles.save("default", "Acme Games", &fake_secret(AccountKind::Play, 0)).unwrap();
|
||||
profiles.save("default", "Acme Games", &fake_secret(AccountKind::Apple, 0)).unwrap();
|
||||
}
|
||||
profiles::sync(&mut st.accounts, &profiles, &legacy, "2026-10-13T00:00:00Z");
|
||||
registry::sync(&mut st, &machine.path().join("projects.json")).unwrap();
|
||||
let mut projects = serde_json::to_value(&st.projects).unwrap();
|
||||
if name == "shared-profiles" {
|
||||
// what project_view says for a folder whose package.json pins storebound 0.6.0
|
||||
let pinned = tempfile::tempdir().unwrap();
|
||||
std::fs::write(pinned.path().join("package.json"), r#"{"devDependencies":{"storebound":"0.6.0"}}"#).unwrap();
|
||||
let mut p = st.projects[0].clone();
|
||||
p.path = pinned.path().to_string_lossy().to_string();
|
||||
let pin = project_view(&p).storebound_pin.expect("0.6.0 is not this app");
|
||||
projects[0]["storeboundPin"] = json!(pin);
|
||||
}
|
||||
let host = json!({
|
||||
"projects_list": st.projects,
|
||||
"projects_list": projects,
|
||||
"accounts_list": st.accounts,
|
||||
"workspace_get": { "studios": st.studios, "overrides": st.overrides, "settings": st.settings },
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue