refactor(provider): establish typed write ownership

Replace the generic provider upsert surface with strict typed create, update, rename, endpoint, and compensation operations. This app-independent prerequisite owns provider writes without depending on Pi runtime or canonical restore.

The v17 migration also reserves dormant device-local ledger tables and the Pi skill bit alongside endpoint normalization. Keeping that reservation here prevents a later stacked feature from rewriting a published migration; no Pi runtime behavior is activated by this commit.

Old save_provider callsite classification
==========================================

Inventory authority: abandoned 5a385fc8 tree. The old definition at
src-tauri/src/database/dao/providers.rs:180 is deleted and is not a callsite.

Production callsites:

- src-tauri/src/commands/provider.rs:253 [create] Claude Desktop import creates
  one absent aggregate; it now strict-inserts the row and initial endpoints in
  one transaction.
- src-tauri/src/database/dao/providers.rs:638 [create] official seed first
  proves absence, then strict-creates; a racing insert is a conflict.
- src-tauri/src/database/dao/providers.rs:704 [create] on-demand seed first
  proves absence, then strict-creates; it cannot overwrite an existing row.
- src-tauri/src/services/omo.rs:291 [create] OMO import constructs a new
  aggregate and strict-creates it; OMO is not eligible for rename.
- src-tauri/src/services/provider/endpoints.rs:85 [update] endpoint last-used
  is not a Provider-row save; it now calls the exact touch endpoint operation.
- src-tauri/src/services/provider/live.rs:1567 [create/update] default live
  import is reconciliation: read first, then strict create or strict update.
- src-tauri/src/services/provider/live.rs:1743 [update] an existing OpenCode
  live provider follows the strict row-update branch.
- src-tauri/src/services/provider/live.rs:1770 [create] a new OpenCode live
  provider follows the strict aggregate-create branch.
- src-tauri/src/services/provider/live.rs:1825 [update] an existing OpenClaw
  live provider follows the strict row-update branch.
- src-tauri/src/services/provider/live.rs:1858 [create] a new OpenClaw live
  provider follows the strict aggregate-create branch.
- src-tauri/src/services/provider/live.rs:1900 [update] an existing Hermes live
  provider follows the strict row-update branch.
- src-tauri/src/services/provider/live.rs:1926 [create] a new Hermes live
  provider follows the strict aggregate-create branch.
- src-tauri/src/services/provider/mod.rs:2568 [create] ProviderService::add owns
  strict aggregate creation and all initial endpoints.
- src-tauri/src/services/provider/mod.rs:2680 [rename] an additive DB-only key
  change now uses the dedicated transactional rename after eligibility checks.
- src-tauri/src/services/provider/mod.rs:2711 [update] OMO edit updates exactly
  the existing main row after its live-file coordination.
- src-tauri/src/services/provider/mod.rs:2740 [update] additive-provider edit
  updates exactly the existing main row after resolving live ownership.
- src-tauri/src/services/provider/mod.rs:2750 [update] switch-mode edit updates
  exactly the existing main row and never inserts.
- src-tauri/src/services/provider/mod.rs:2948 [update] remove-from-live changes
  only the existing provider's live-managed marker.
- src-tauri/src/services/provider/mod.rs:3120 [update] switch backfill updates
  only the existing current provider row.
- src-tauri/src/services/provider/mod.rs:3174 [update] successful additive
  switch changes only the existing live-managed marker.
- src-tauri/src/services/provider/mod.rs:3315 [update] common-config migration
  updates only each already-read existing row.
- src-tauri/src/services/provider/mod.rs:3895 [update] Gemini credential scrub
  updates only each already-read existing row.
- src-tauri/src/services/provider/mod.rs:4082 [update] sort ordering is routed
  to the dedicated sort-index state operation, not row replacement.
- src-tauri/src/services/provider/mod.rs:4636 [create/update] universal-to-
  Claude reconciliation reads the target and selects strict create or update.
- src-tauri/src/services/provider/mod.rs:4651 [create/update] universal-to-
  Codex reconciliation reads the target and selects strict create or update.
- src-tauri/src/services/provider/mod.rs:4665 [create/update] universal-to-
  Gemini reconciliation reads the target and selects strict create or update.

Required indirect ownership paths:

- src-tauri/src/deeplink/provider.rs [create] the old indirect flow called
  ProviderService::add and then appended endpoints one by one. It now supplies
  every non-primary endpoint to one strict aggregate create, so hydration is
  complete atomically and a duplicate is zero-side-effect.
- [restore] no old generic-save callsite is reclassified as restore. Exact
  aggregate replacement exists only as the sealed
  restore_provider_aggregate_on_tx compensation primitive.

Test-only callsites:

Every item below is classified [test]. Each is fixture setup, not a production
write authority, and is migrated to a real ProviderService entry where the
behavior is under test or to the cfg(test)-only typed fixture reconciler where
the test merely needs pre-existing rows.

- src-tauri/src/codex_history_migration.rs:1442 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:1452 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2174 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2176 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2199 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2219 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2247 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2267 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2288 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2320 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2393 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2449 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2498 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2555 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2604 [test] migration fixture setup.
- src-tauri/src/codex_history_migration.rs:2625 [test] migration fixture setup.
- src-tauri/src/database/dao/providers.rs:754 [test] DAO fixture setup.
- src-tauri/src/proxy/provider_router.rs:351 [test] router fixture setup.
- src-tauri/src/proxy/provider_router.rs:352 [test] router fixture setup.
- src-tauri/src/proxy/provider_router.rs:377 [test] router fixture setup.
- src-tauri/src/proxy/provider_router.rs:378 [test] router fixture setup.
- src-tauri/src/proxy/provider_router.rs:410 [test] router fixture setup.
- src-tauri/src/proxy/provider_router.rs:411 [test] router fixture setup.
- src-tauri/src/proxy/provider_router.rs:447 [test] router fixture setup.
- src-tauri/src/proxy/provider_router.rs:448 [test] router fixture setup.
- src-tauri/src/proxy/provider_router.rs:488 [test] router fixture setup.
- src-tauri/src/services/provider/mod.rs:485 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:586 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:813 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:825 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:1472 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:1607 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:1737 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:1945 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:1952 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:1978 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:2006 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:2056 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:2130 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:2167 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:2207 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:2235 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:2270 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:2320 [test] service fixture setup.
- src-tauri/src/services/provider/mod.rs:2362 [test] service fixture setup.
- src-tauri/src/services/proxy.rs:3762 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:3948 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:4034 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:4095 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:4114 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:4263 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:4341 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:4421 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:4533 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:4651 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:4787 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5264 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5320 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5385 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5387 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5460 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5462 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5611 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5613 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5615 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5698 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5700 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:5998 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:6000 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:6173 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:6175 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:6417 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:6419 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:6553 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:6555 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:6635 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:6637 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:6919 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:7173 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:7175 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:7240 [test] proxy fixture setup.
- src-tauri/src/services/proxy.rs:7242 [test] proxy fixture setup.
- src-tauri/tests/profile_roundtrip.rs:112 [test] profile fixture create.
- src-tauri/tests/profile_roundtrip.rs:116 [test] profile fixture create.
- src-tauri/tests/profile_roundtrip.rs:126 [test] profile fixture create.
- src-tauri/tests/profile_roundtrip.rs:133 [test] profile fixture create.
- src-tauri/tests/profile_roundtrip.rs:292 [test] profile fixture create.
- src-tauri/tests/profile_roundtrip.rs:501 [test] profile fixture create.
- src-tauri/tests/profile_roundtrip.rs:505 [test] profile fixture create.
- src-tauri/tests/profile_roundtrip.rs:670 [test] profile fixture create.
- src-tauri/tests/profile_roundtrip.rs:677 [test] profile fixture create.
- src-tauri/tests/profile_roundtrip.rs:762 [test] Linux Desktop fixture create.
- src-tauri/tests/profile_roundtrip.rs:769 [test] Linux Desktop fixture create.
- src-tauri/tests/provider_commands.rs:69 [test] command fixture create.
- src-tauri/tests/provider_service.rs:2927 [test] service fixture create.
This commit is contained in:
SaladDay
2026-08-03 10:01:45 +00:00
parent 13ea497ab0
commit ac764b4818
35 changed files with 5748 additions and 906 deletions
+72 -59
View File
@@ -7,13 +7,14 @@ use std::fs;
use serde_json::json;
use cc_switch_lib::{
AppType, InstalledSkill, McpServer, McpService, ProfilePayload, ProfileScope, ProfileService,
Prompt, PromptService, Provider, ProviderService, SkillApps, SkillService,
AppType, InstalledSkill, McpServer, McpService, NewProviderAggregate, ProfilePayload,
ProfileScope, ProfileService, Prompt, PromptService, Provider, ProviderService, SkillApps,
SkillService,
};
#[path = "support.rs"]
mod support;
use support::{create_test_state, ensure_test_home, reset_test_fs, test_mutex};
use support::{create_test_state, ensure_test_home, new_provider_input, reset_test_fs, test_mutex};
fn claude_provider(id: &str, token: &str) -> Provider {
Provider::with_id(
@@ -107,34 +108,41 @@ fn profile_snapshot_apply_roundtrip_restores_configuration() {
let state = create_test_state().expect("create test state");
// ---- 种子数据:2 个 Claude 供应商(p1 为当前)+ 2 个 MCP + 1 个 Skill + 2 个 Prompt ----
state
.db
.save_provider(AppType::Claude.as_str(), &claude_provider("p1", "key-1"))
.expect("save provider p1");
state
.db
.save_provider(AppType::Claude.as_str(), &claude_provider("p2", "key-2"))
.expect("save provider p2");
ProviderService::add(
&state,
AppType::Claude,
new_provider_input(claude_provider("p1", "key-1")),
false,
)
.expect("create provider p1");
ProviderService::add(
&state,
AppType::Claude,
new_provider_input(claude_provider("p2", "key-2")),
false,
)
.expect("create provider p2");
state
.db
.set_current_provider(AppType::Claude.as_str(), "p1")
.expect("set current provider p1");
// Claude Desktop 只有供应商一个活跃维度(MCP/Skills/Prompt 对它不适用)
state
.db
.save_provider(
AppType::ClaudeDesktop.as_str(),
&desktop_provider("d1", "dk-1"),
)
.expect("save desktop provider d1");
state
.db
.save_provider(
AppType::ClaudeDesktop.as_str(),
&desktop_provider("d2", "dk-2"),
)
.expect("save desktop provider d2");
for provider in [
desktop_provider("d1", "dk-1"),
desktop_provider("d2", "dk-2"),
] {
state
.db
.create_provider(
NewProviderAggregate::from_input(
AppType::ClaudeDesktop.as_str(),
new_provider_input(provider),
)
.expect("build typed desktop create"),
)
.expect("create desktop provider");
}
state
.db
.set_current_provider(AppType::ClaudeDesktop.as_str(), "d1")
@@ -287,10 +295,13 @@ fn shared_profile_sides_are_isolated_and_mergeable() {
let state = create_test_state().expect("create test state");
// 种子:Claude 侧有当前供应商 + 启用的 MCP
state
.db
.save_provider(AppType::Claude.as_str(), &claude_provider("p1", "key-1"))
.expect("save provider p1");
ProviderService::add(
&state,
AppType::Claude,
new_provider_input(claude_provider("p1", "key-1")),
false,
)
.expect("create provider p1");
state
.db
.set_current_provider(AppType::Claude.as_str(), "p1")
@@ -496,14 +507,20 @@ fn switching_profile_autosaves_previous_profile_state() {
let state = create_test_state().expect("create test state");
// ---- 种子:Claude 侧两套供应商 / MCP / Prompt ----
state
.db
.save_provider(AppType::Claude.as_str(), &claude_provider("p1", "key-1"))
.expect("save provider p1");
state
.db
.save_provider(AppType::Claude.as_str(), &claude_provider("p2", "key-2"))
.expect("save provider p2");
ProviderService::add(
&state,
AppType::Claude,
new_provider_input(claude_provider("p1", "key-1")),
false,
)
.expect("create provider p1");
ProviderService::add(
&state,
AppType::Claude,
new_provider_input(claude_provider("p2", "key-2")),
false,
)
.expect("create provider p2");
state
.db
.set_current_provider(AppType::Claude.as_str(), "p1")
@@ -665,17 +682,13 @@ fn profile_switch_auto_disables_takeover_before_apply() {
// ---- 两个 Claude 供应商:custom1 与 custom2 ----
let mut custom1 = claude_provider("custom1", "custom-key-1");
custom1.category = Some("custom".to_string());
state
.db
.save_provider(AppType::Claude.as_str(), &custom1)
.expect("save custom1 provider");
ProviderService::add(&state, AppType::Claude, new_provider_input(custom1), false)
.expect("create custom1 provider");
let mut custom2 = claude_provider("custom2", "custom-key-2");
custom2.category = Some("custom".to_string());
state
.db
.save_provider(AppType::Claude.as_str(), &custom2)
.expect("save custom2 provider");
ProviderService::add(&state, AppType::Claude, new_provider_input(custom2), false)
.expect("create custom2 provider");
// 初始状态:custom1 + 代理接管
ProviderService::switch(&state, AppType::Claude, "custom1").expect("switch to custom1");
@@ -757,20 +770,20 @@ fn claude_desktop_profile_scope_is_independent() {
let state = create_test_state().expect("create test state");
state
.db
.save_provider(
AppType::ClaudeDesktop.as_str(),
&desktop_provider("d1", "dk-1"),
)
.expect("save desktop provider d1");
state
.db
.save_provider(
AppType::ClaudeDesktop.as_str(),
&desktop_provider("d2", "dk-2"),
)
.expect("save desktop provider d2");
ProviderService::add(
&state,
AppType::ClaudeDesktop,
new_provider_input(desktop_provider("d1", "dk-1")),
false,
)
.expect("create desktop provider d1");
ProviderService::add(
&state,
AppType::ClaudeDesktop,
new_provider_input(desktop_provider("d2", "dk-2")),
false,
)
.expect("create desktop provider d2");
state
.db
.set_current_provider(AppType::ClaudeDesktop.as_str(), "d1")
+13 -13
View File
@@ -12,7 +12,7 @@ mod support;
use std::collections::HashMap;
use support::{
create_test_state, create_test_state_with_config, enable_codex_official_auth_preservation,
ensure_test_home, reset_test_fs, test_mutex,
ensure_test_home, new_provider_input, reset_test_fs, test_mutex,
};
fn settings_path(home: &Path) -> PathBuf {
@@ -64,18 +64,18 @@ fn grokbuild_import_and_switch_write_live_config() {
);
let next_config = grokbuild_config("Relay", "https://new.example/v1", "new-key");
state
.db
.save_provider(
AppType::GrokBuild.as_str(),
&Provider::with_id(
"relay".to_string(),
"Relay".to_string(),
json!({ "config": next_config }),
None,
),
)
.expect("save second Grok Build provider");
ProviderService::add(
&state,
AppType::GrokBuild,
new_provider_input(Provider::with_id(
"relay".to_string(),
"Relay".to_string(),
json!({ "config": next_config }),
None,
)),
false,
)
.expect("create second Grok Build provider");
switch_provider_test_hook(&state, AppType::GrokBuild, "relay")
.expect("switch Grok Build provider");
+3 -5
View File
@@ -9,7 +9,7 @@ use cc_switch_lib::{
mod support;
use support::{
create_test_state, create_test_state_with_config, enable_codex_official_auth_preservation,
ensure_test_home, reset_test_fs, test_mutex,
ensure_test_home, new_provider_input, reset_test_fs, test_mutex,
};
fn sanitize_provider_name(name: &str) -> String {
@@ -3084,10 +3084,8 @@ fn recover_from_crash_without_backup_cleans_placeholder_instead_of_writing_it_ba
taken_over_live.clone(),
None,
);
state
.db
.save_provider(AppType::Claude.as_str(), &provider)
.expect("save placeholder provider");
ProviderService::add(&state, AppType::Claude, new_provider_input(provider), false)
.expect("create placeholder provider");
state
.db
.set_current_provider(AppType::Claude.as_str(), "default")
+25 -1
View File
@@ -1,7 +1,31 @@
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use cc_switch_lib::{update_settings, AppSettings, AppState, Database, MultiAppConfig};
use cc_switch_lib::{
update_settings, AppSettings, AppState, Database, MultiAppConfig, Provider,
ProviderMutationInput,
};
/// Build the public write DTO explicitly for integration tests. Keeping this
/// conversion test-only avoids reintroducing a production `From<Provider>`
/// path from hydrated read projections to provider mutations.
#[allow(dead_code)]
pub fn new_provider_input(provider: Provider) -> ProviderMutationInput {
ProviderMutationInput {
id: provider.id,
name: provider.name,
settings_config: provider.settings_config,
website_url: provider.website_url,
category: provider.category,
created_at: provider.created_at,
sort_index: provider.sort_index,
notes: provider.notes,
meta: provider.meta,
icon: provider.icon,
icon_color: provider.icon_color,
in_failover_queue: provider.in_failover_queue,
}
}
/// 为测试设置隔离的 HOME 目录,避免污染真实用户数据。
pub fn ensure_test_home() -> &'static Path {