use super::*;
use crate::db::{
CreateEvidenceSpanInput, CreateSessionInput, CreateWorkspaceInput, EvidenceProducerKind,
WorkspaceScopeFields,
};
const WORKSPACE: &str = "wsp_00000000000000000000000000";
const OTHER_WORKSPACE: &str = "wsp_00000000000000000000000001";
const SESSION: &str = "sess_00000000000000000000000000";
const EVIDENCE: &str = "ev_00000000000000000000000000";
fn destination(path: &str) -> WorkspaceRebindDestination {
let scope = derive_workspace_scope(Path::new(path));
WorkspaceRebindDestination {
path: path.to_owned(),
scope_kind: scope.kind.as_str().to_owned(),
repository_root: scope.repository_root.map(|p| p.display().to_string()),
repository_fingerprint: scope.repository_fingerprint,
subproject_path: scope.subproject_path.map(|p| p.display().to_string()),
}
}
fn bind(db: &DbConnection, id: &str, path: &str) {
let target = destination(path);
db.upsert_workspace_with_scope(
id,
&CreateWorkspaceInput {
path: path.to_owned(),
name: Some(format!("alias-{id}")),
},
&WorkspaceScopeFields {
scope_kind: target.scope_kind,
repository_root: target.repository_root,
repository_fingerprint: target.repository_fingerprint,
subproject_path: target.subproject_path,
},
)
.expect("insert fixture workspace");
}
fn fixture() -> (
DbConnection,
WorkspaceRebindOptions,
WorkspaceRebindDestination,
) {
let db = DbConnection::open_memory().expect("open database");
db.migrate().expect("migrate database");
bind(&db, WORKSPACE, "/original/project");
let options = WorkspaceRebindOptions {
workspace_path: PathBuf::from("/relocated/project"),
expected_workspace_id: WORKSPACE.to_owned(),
expected_source_path: "/original/project".to_owned(),
apply_plan: None,
};
(db, options, destination("/relocated/project"))
}
fn preview(
db: &DbConnection,
options: &WorkspaceRebindOptions,
target: &WorkspaceRebindDestination,
) -> WorkspaceRebindPlan {
plan_for_connection(db, "/relocated/project/.ee/ee.db", target, options)
.expect("preview exact single-workspace binding")
}
fn apply(
db: &DbConnection,
options: &WorkspaceRebindOptions,
target: &WorkspaceRebindDestination,
plan: &WorkspaceRebindPlan,
audit: &str,
) -> Result<WorkspaceRebindPlan, DbError> {
db.with_transaction(|| {
apply_on_connection(
db,
&plan.database_path,
target,
options,
&plan.plan_hash,
audit,
)
})
}
#[test]
fn preview_is_repeatable_and_does_not_change_the_binding() {
let (db, options, target) = fixture();
let before = single_workspace(&db).expect("binding");
let first = preview(&db, &options, &target);
let second = preview(&db, &options, &target);
assert_eq!(first, second);
assert_eq!(
before,
single_workspace(&db).expect("binding after preview")
);
let result = report(first, None);
assert!(result.dry_run);
assert!(!result.persisted);
assert!(result.audit_id.is_none());
assert!(!result.derived_assets_modified);
}
#[test]
fn rebind_preserves_durable_session_and_evidence_identity_and_normal_resolution() {
let (db, options, target) = fixture();
let excerpt = "Run the complete release checks before publishing.";
db.insert_session(
SESSION,
&CreateSessionInput {
workspace_id: WORKSPACE.to_owned(),
cass_session_id: "/original/transcript.jsonl".to_owned(),
source_path: Some("/original/transcript.jsonl".to_owned()),
agent_name: Some("codex".to_owned()),
model: None,
started_at: None,
ended_at: None,
message_count: 1,
token_count: None,
content_hash: "session-source-hash".to_owned(),
metadata_json: None,
},
)
.expect("insert durable session");
db.insert_evidence_span(
EVIDENCE,
&CreateEvidenceSpanInput {
workspace_id: WORKSPACE.to_owned(),
session_id: SESSION.to_owned(),
memory_id: None,
producer_kind: EvidenceProducerKind::CassImport,
cass_span_id: "/original/transcript.jsonl:1".to_owned(),
span_kind: "message".to_owned(),
start_line: 1,
end_line: 1,
start_byte: None,
end_byte: None,
role: Some("assistant".to_owned()),
excerpt: excerpt.to_owned(),
content_hash: format!("blake3:{}", blake3::hash(excerpt.as_bytes()).to_hex()),
metadata_json: None,
inherited_redaction_classes: Vec::new(),
},
)
.expect("insert durable evidence");
let before = preview(&db, &options, &target);
let audit = generate_audit_id();
apply(&db, &options, &target, &before, &audit).expect("apply rebind");
let after = single_workspace(&db).expect("rebound workspace");
assert_eq!(after.workspace_id, WORKSPACE);
assert_eq!(after.path, target.path);
assert_eq!(after.alias, before.previous.alias);
assert_eq!(after.created_at, before.previous.created_at);
let session = db
.get_session_by_cass_id(WORKSPACE, "/original/transcript.jsonl")
.expect("read retained session")
.expect("session still exists");
assert_eq!(session.id, SESSION);
assert_eq!(
session.source_path.as_deref(),
Some("/original/transcript.jsonl")
);
assert_eq!(session.content_hash, "session-source-hash");
let spans = db
.list_evidence_spans_for_session(SESSION)
.expect("retained evidence");
assert_eq!(spans.len(), 1);
let span = spans.first().expect("one evidence row");
assert_eq!(span.id, EVIDENCE);
assert_eq!(span.workspace_id, WORKSPACE);
assert_eq!(span.session_id, SESSION);
assert_eq!(span.cass_span_id, "/original/transcript.jsonl:1");
assert_eq!(span.excerpt, excerpt);
assert_eq!(
span.content_hash,
format!("blake3:{}", blake3::hash(excerpt.as_bytes()).to_hex())
);
let entry = db
.get_audit(&audit)
.expect("read audit")
.expect("audit committed");
assert_eq!(entry.action, REBIND_ACTION);
assert_eq!(entry.target_id.as_deref(), Some(WORKSPACE));
let path = Path::new(&target.path);
let requested = crate::core::workspace::stable_workspace_id(path);
let resolved = crate::core::workspace::ensure_bound_workspace(&db, &requested, &[path])
.expect("normal resolution must reuse the rebound identity");
assert_eq!(resolved, WORKSPACE);
assert_eq!(db.list_workspaces().expect("workspace count").len(), 1);
}
#[test]
fn refusal_never_guesses_a_source_or_merges_multiple_workspaces() {
let (db, mut options, target) = fixture();
options.expected_workspace_id = OTHER_WORKSPACE.to_owned();
assert!(plan_for_connection(&db, "store", &target, &options).is_err());
options.expected_workspace_id = WORKSPACE.to_owned();
options.expected_source_path = "/different/source".to_owned();
assert!(plan_for_connection(&db, "store", &target, &options).is_err());
options.expected_source_path = "/original/project".to_owned();
bind(&db, OTHER_WORKSPACE, "/second/project");
assert!(plan_for_connection(&db, "store", &target, &options).is_err());
let empty = DbConnection::open_memory().expect("empty database");
empty.migrate().expect("migrate empty database");
assert!(plan_for_connection(&empty, "store", &target, &options).is_err());
}
#[test]
fn a_stale_preview_cannot_overwrite_new_alias_or_scope_metadata() {
let (db, options, target) = fixture();
let original = preview(&db, &options, &target);
db.update_workspace_name(WORKSPACE, Some("changed-after-preview"))
.expect("concurrent metadata edit");
let changed = single_workspace(&db).expect("changed binding");
assert!(apply(&db, &options, &target, &original, &generate_audit_id()).is_err());
assert_eq!(
single_workspace(&db).expect("binding after refusal"),
changed
);
assert_ne!(
preview(&db, &options, &target).plan_hash,
original.plan_hash
);
}
#[test]
fn a_second_workspace_created_after_preview_blocks_apply() {
let (db, options, target) = fixture();
let plan = preview(&db, &options, &target);
bind(&db, OTHER_WORKSPACE, "/second/project");
assert!(apply(&db, &options, &target, &plan, &generate_audit_id()).is_err());
assert_eq!(db.list_workspaces().expect("workspaces").len(), 2);
}
#[test]
fn commitments_are_bound_to_the_store_destination_and_source_metadata() {
let (db, options, target) = fixture();
let original = preview(&db, &options, &target);
let other_database = plan_for_connection(&db, "/other/.ee/ee.db", &target, &options)
.expect("other addressed store");
assert_ne!(original.plan_hash, other_database.plan_hash);
let mut changed_target = target.clone();
changed_target.path = "/another/project".to_owned();
let other_target = preview(&db, &options, &changed_target);
assert_ne!(original.plan_hash, other_target.plan_hash);
assert!(
apply(
&db,
&options,
&changed_target,
&original,
&generate_audit_id()
)
.is_err()
);
changed_target = target.clone();
changed_target.repository_fingerprint = Some("changed-repository-scope".to_owned());
assert_ne!(
preview(&db, &options, &changed_target).plan_hash,
original.plan_hash
);
}
#[test]
fn an_audit_failure_rolls_back_the_location_update() {
let (db, options, target) = fixture();
let plan = preview(&db, &options, &target);
let audit = generate_audit_id();
db.insert_audit(
&audit,
&CreateAuditInput {
workspace_id: Some(WORKSPACE.to_owned()),
actor: Some("fixture".to_owned()),
action: "fixture.existing_audit".to_owned(),
target_type: Some("workspace".to_owned()),
target_id: Some(WORKSPACE.to_owned()),
details: None,
},
)
.expect("occupy audit identity");
assert!(apply(&db, &options, &target, &plan, &audit).is_err());
assert_eq!(
single_workspace(&db).expect("rolled back binding"),
plan.previous
);
let existing = db
.get_audit(&audit)
.expect("read existing audit")
.expect("audit retained");
assert_eq!(existing.action, "fixture.existing_audit");
}
#[test]
fn replaying_a_successful_plan_is_refused_without_a_second_rebind() {
let (db, options, target) = fixture();
let plan = preview(&db, &options, &target);
apply(&db, &options, &target, &plan, &generate_audit_id()).expect("first apply");
let after = single_workspace(&db).expect("rebound workspace");
assert!(apply(&db, &options, &target, &plan, &generate_audit_id()).is_err());
assert_eq!(single_workspace(&db).expect("binding after replay"), after);
}
fn test_root() -> PathBuf {
let root = std::env::temp_dir().join(format!("ee-rebind-{}", uuid::Uuid::now_v7()));
std::fs::create_dir_all(&root).expect("create test root");
root.canonicalize().expect("physical test root")
}
#[test]
fn previewing_missing_or_non_regular_stores_never_creates_a_database() {
let root = test_root();
let options = WorkspaceRebindOptions {
workspace_path: root.clone(),
expected_workspace_id: WORKSPACE.to_owned(),
expected_source_path: "/original/project".to_owned(),
apply_plan: None,
};
assert!(rebind_workspace(&options).is_err());
assert!(!root.join(".ee").exists());
std::fs::create_dir(root.join(".ee")).expect("create marker only");
assert!(rebind_workspace(&options).is_err());
assert!(!root.join(".ee/ee.db").exists());
std::fs::create_dir(root.join(".ee/ee.db")).expect("non-regular database fixture");
assert!(rebind_workspace(&options).is_err());
assert!(root.join(".ee/ee.db").is_dir());
}
#[cfg(unix)]
#[test]
fn preview_refuses_symlinked_store_files_without_following_them() {
let root = test_root();
let outside = root.join("outside.db");
std::fs::write(&outside, b"not a database").expect("outside fixture");
std::fs::create_dir(root.join(".ee")).expect("marker");
std::os::unix::fs::symlink(&outside, root.join(".ee/ee.db")).expect("symlink fixture");
let options = WorkspaceRebindOptions {
workspace_path: root,
expected_workspace_id: WORKSPACE.to_owned(),
expected_source_path: "/original/project".to_owned(),
apply_plan: None,
};
assert!(rebind_workspace(&options).is_err());
assert_eq!(
std::fs::read(outside).expect("outside remains unchanged"),
b"not a database"
);
}
#[test]
fn location_update_quotes_apostrophes_and_sql_shaped_path_text() {
let (db, options, mut target) = fixture();
target.path = "/relocated/it's a project'; DELETE FROM workspaces; --".to_owned();
let plan = preview(&db, &options, &target);
apply(&db, &options, &target, &plan, &generate_audit_id()).expect("quoted location update");
let after = single_workspace(&db).expect("workspace was not deleted");
assert_eq!(after.path, target.path);
assert_eq!(after.repository_root, target.repository_root);
assert_eq!(after.workspace_id, WORKSPACE);
assert!(sql_text("bad\0binding").is_err());
assert_eq!(sql_optional_text(None).expect("SQL NULL"), "NULL");
assert_eq!(
sql_optional_text(Some("NULL")).expect("literal NULL"),
"'NULL'"
);
}