fix: reject conflicting repository import intent

This commit is contained in:
2026-09-03 02:44:24 +09:00
parent 63ad590262
commit f74f3cd133
2 changed files with 116 additions and 32 deletions
+41
View File
@@ -18175,6 +18175,47 @@ mod tests {
SqliteWorkspaceStore::open(&config.database_path).unwrap()
}
#[tokio::test]
async fn configured_repository_import_rejects_changed_source_intent() {
let dir = tempfile::tempdir().unwrap();
let mut config = test_server_config(dir.path());
let store = test_control_store(&config);
seed_test_registered_workspace(&store, &config)
.await
.unwrap();
import_configured_repositories(&store, &config).unwrap();
let persisted = store
.get_repository_by_key(TEST_WORKSPACE_ID, "test-repository")
.unwrap()
.unwrap();
assert_eq!(persisted.source, config.repositories[0].source);
let changed_source = workspace_api::RepositorySource {
kind: workspace_api::RepositorySourceKind::LocalPath,
uri: dir.path().join("different-source").display().to_string(),
};
config.repositories[0].source_fingerprint =
crate::repository_source::repository_source_fingerprint(&changed_source);
config.repositories[0].source = changed_source;
let error = import_configured_repositories(&store, &config).unwrap_err();
assert!(
matches!(error, crate::Error::WorkspaceConfigConflict(_)),
"unexpected error: {error}"
);
let after_rejected_import = store
.get_repository_by_key(TEST_WORKSPACE_ID, "test-repository")
.unwrap()
.unwrap();
assert_eq!(after_rejected_import.repository_id, persisted.repository_id);
assert_eq!(after_rejected_import.source, persisted.source);
assert_eq!(
after_rejected_import.source_fingerprint,
persisted.source_fingerprint
);
}
#[tokio::test]
async fn server_router_serves_workspace_chooser_before_first_workspace_exists() {
let dir = tempfile::tempdir().unwrap();
+75 -32
View File
@@ -2197,38 +2197,67 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()> {
validate_repository_record_identity(record)?;
self.with_conn(|conn| {
conn.execute(
r#"INSERT INTO repositories (
workspace_id, repository_id, repository_key, kind, provider, uri,
source_kind, source_uri, default_ref, source_revision,
source_fingerprint, observed_status, observed_at, created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)
ON CONFLICT(workspace_id, repository_key) DO UPDATE SET
kind = excluded.kind,
provider = excluded.provider,
default_ref = excluded.default_ref,
observed_status = excluded.observed_status,
observed_at = excluded.observed_at,
updated_at = excluded.updated_at"#,
params![
record.workspace_id,
record.repository_id,
record.repository_key,
record.kind,
record.provider,
record.source.uri,
record.source.kind.as_str(),
record.source.uri,
record.default_ref,
record.source_revision,
record.source_fingerprint,
record.observed_status.as_str(),
record.observed_at,
record.created_at,
record.updated_at,
],
)?;
self.with_conn_mut(|conn| {
let tx = conn.transaction_with_behavior(TransactionBehavior::Immediate)?;
let existing = tx
.query_row(
r#"SELECT workspace_id, repository_id, repository_key, kind, provider,
source_kind, source_uri, default_ref, source_revision,
source_fingerprint, observed_status, observed_at, created_at, updated_at
FROM repositories
WHERE workspace_id = ?1 AND repository_key = ?2"#,
params![record.workspace_id, record.repository_key],
read_repository_record,
)
.optional()?;
if let Some(existing) = existing {
if !repository_registration_intent_matches(&existing, record) {
return Err(Error::WorkspaceConfigConflict(format!(
"Repository key `{}` already exists with different registration intent",
record.repository_key
)));
}
tx.execute(
r#"UPDATE repositories
SET observed_status = ?3,
observed_at = ?4,
updated_at = ?5
WHERE workspace_id = ?1 AND repository_id = ?2"#,
params![
record.workspace_id,
existing.repository_id,
record.observed_status.as_str(),
record.observed_at,
record.updated_at,
],
)?;
} else {
tx.execute(
r#"INSERT INTO repositories (
workspace_id, repository_id, repository_key, kind, provider, uri,
source_kind, source_uri, default_ref, source_revision,
source_fingerprint, observed_status, observed_at, created_at, updated_at
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12, ?13, ?14, ?15)"#,
params![
record.workspace_id,
record.repository_id,
record.repository_key,
record.kind,
record.provider,
record.source.uri,
record.source.kind.as_str(),
record.source.uri,
record.default_ref,
record.source_revision,
record.source_fingerprint,
record.observed_status.as_str(),
record.observed_at,
record.created_at,
record.updated_at,
],
)?;
}
tx.commit()?;
Ok(())
})
}
@@ -5358,6 +5387,20 @@ fn read_workspace_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<WorkspaceR
})
}
fn repository_registration_intent_matches(
existing: &RepositoryRecord,
requested: &RepositoryRecord,
) -> bool {
existing.workspace_id == requested.workspace_id
&& existing.repository_key == requested.repository_key
&& existing.kind == requested.kind
&& existing.provider == requested.provider
&& existing.source == requested.source
&& existing.default_ref == requested.default_ref
&& existing.source_revision == requested.source_revision
&& existing.source_fingerprint == requested.source_fingerprint
}
fn validate_repository_record_identity(record: &RepositoryRecord) -> Result<()> {
workspace_api::validate_repository_key(&record.repository_key)
.map_err(|error| Error::InvalidInput(format!("invalid Repository key: {error}")))?;