merge: sqlite memory authority
This commit is contained in:
commit
386152749a
|
|
@ -1,5 +1,7 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use chrono::Utc;
|
||||||
|
|
||||||
use ticket::{
|
use ticket::{
|
||||||
SqliteTicketBackend, TicketBackend, TicketIdOrSlug, TicketListQuery,
|
SqliteTicketBackend, TicketBackend, TicketIdOrSlug, TicketListQuery,
|
||||||
TicketWorkspaceActionPriority, project_ticket_workspace_item,
|
TicketWorkspaceActionPriority, project_ticket_workspace_item,
|
||||||
|
|
@ -9,22 +11,24 @@ use crate::records::{
|
||||||
ObjectiveDetail, ObjectiveResourceSummary, ObjectiveSummary, ProjectRecordList, TicketDetail,
|
ObjectiveDetail, ObjectiveResourceSummary, ObjectiveSummary, ProjectRecordList, TicketDetail,
|
||||||
TicketSummary, summarize_body, truncate_body, validate_project_id,
|
TicketSummary, summarize_body, truncate_body, validate_project_id,
|
||||||
};
|
};
|
||||||
use crate::store::{ControlPlaneStore, SqliteWorkspaceStore};
|
use crate::store::{
|
||||||
|
ControlPlaneStore, MemoryDocumentRecord, MemoryStagingRecord, MemoryStagingResolutionRecord,
|
||||||
|
SqliteWorkspaceStore,
|
||||||
|
};
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
|
|
||||||
const DETAIL_BODY_LIMIT: usize = 64 * 1024;
|
const DETAIL_BODY_LIMIT: usize = 64 * 1024;
|
||||||
|
const DEFAULT_MEMORY_DOCUMENT_BODY: &str = "# Memory\n\n";
|
||||||
|
const RECORD_SOURCE_WORKSPACE_SQLITE: &str = "workspace-sqlite";
|
||||||
|
|
||||||
/// Workspace-scoped runtime authority for project resources.
|
/// Workspace-scoped runtime authority for project resources.
|
||||||
///
|
///
|
||||||
/// Normal Backend API handlers should depend on this authority abstraction, not
|
/// Normal Backend API handlers should depend on this authority abstraction, not
|
||||||
/// on legacy filesystem layouts. Filesystem readers belong in explicitly
|
/// on legacy filesystem layouts. Filesystem readers belong in explicitly
|
||||||
/// temporary migration/repair tools and tests, not normal runtime authority paths.
|
/// temporary migration/repair tools and tests, not normal runtime authority paths.
|
||||||
pub trait WorkspaceAuthority: ObjectiveAuthority + TicketAuthority + MemoryAuthorityMarker {}
|
pub trait WorkspaceAuthority: ObjectiveAuthority + TicketAuthority + MemoryAuthority {}
|
||||||
|
|
||||||
impl<T> WorkspaceAuthority for T where
|
impl<T> WorkspaceAuthority for T where T: ObjectiveAuthority + TicketAuthority + MemoryAuthority {}
|
||||||
T: ObjectiveAuthority + TicketAuthority + MemoryAuthorityMarker
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait TicketAuthority {
|
pub trait TicketAuthority {
|
||||||
fn list_tickets(&self, limit: usize) -> Result<ProjectRecordList<TicketSummary>>;
|
fn list_tickets(&self, limit: usize) -> Result<ProjectRecordList<TicketSummary>>;
|
||||||
|
|
@ -36,19 +40,57 @@ pub trait ObjectiveAuthority {
|
||||||
fn objective(&self, id: &str) -> Result<ObjectiveDetail>;
|
fn objective(&self, id: &str) -> Result<ObjectiveDetail>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Marker for the pending Memory authority split.
|
pub trait MemoryAuthority {
|
||||||
///
|
fn ensure_memory_document(&self) -> Result<MemoryDocument>;
|
||||||
/// Memory still routes through the legacy `memory` crate backend in this codebase.
|
fn memory_document(&self) -> Result<MemoryDocument>;
|
||||||
/// This marker makes the missing SQL-backed Memory authority explicit so callers
|
fn update_memory_document(&self, body_md: &str) -> Result<MemoryDocument>;
|
||||||
/// do not mistake the current Backend API boundary for a completed storage
|
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<MemoryStagingEntry>>;
|
||||||
/// authority migration.
|
fn memory_staging_record(&self, candidate_id: &str) -> Result<MemoryStagingEntry>;
|
||||||
pub trait MemoryAuthorityMarker {
|
fn upsert_memory_staging_record(
|
||||||
fn memory_authority_status(&self) -> MemoryAuthorityStatus;
|
&self,
|
||||||
|
candidate_id: &str,
|
||||||
|
raw_json: &str,
|
||||||
|
source_path: Option<&str>,
|
||||||
|
) -> Result<MemoryStagingEntry>;
|
||||||
|
fn close_memory_staging_record(
|
||||||
|
&self,
|
||||||
|
candidate_id: &str,
|
||||||
|
action: &str,
|
||||||
|
reason: &str,
|
||||||
|
affected_refs_json: &str,
|
||||||
|
) -> Result<MemoryStagingResolution>;
|
||||||
|
fn list_memory_staging_resolutions(&self, limit: usize)
|
||||||
|
-> Result<Vec<MemoryStagingResolution>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum MemoryAuthorityStatus {
|
pub struct MemoryDocument {
|
||||||
LegacyFilesystemBackendPendingSqliteAuthority,
|
pub body_md: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
pub record_source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct MemoryStagingEntry {
|
||||||
|
pub candidate_id: String,
|
||||||
|
pub raw_json: String,
|
||||||
|
pub source_path: Option<String>,
|
||||||
|
pub imported_at: String,
|
||||||
|
pub record_source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct MemoryStagingResolution {
|
||||||
|
pub candidate_id: String,
|
||||||
|
pub action: String,
|
||||||
|
pub reason: String,
|
||||||
|
pub affected_refs_json: String,
|
||||||
|
pub staging_raw_json: String,
|
||||||
|
pub source_path: Option<String>,
|
||||||
|
pub imported_at: String,
|
||||||
|
pub resolved_at: String,
|
||||||
|
pub record_source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -196,9 +238,192 @@ impl ObjectiveAuthority for SqliteWorkspaceAuthority {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MemoryAuthorityMarker for SqliteWorkspaceAuthority {
|
impl MemoryAuthority for SqliteWorkspaceAuthority {
|
||||||
fn memory_authority_status(&self) -> MemoryAuthorityStatus {
|
fn ensure_memory_document(&self) -> Result<MemoryDocument> {
|
||||||
MemoryAuthorityStatus::LegacyFilesystemBackendPendingSqliteAuthority
|
let now = now_rfc3339();
|
||||||
|
self.store
|
||||||
|
.ensure_memory_document(&self.workspace_id, DEFAULT_MEMORY_DOCUMENT_BODY, &now)
|
||||||
|
.map(memory_document_from_record)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_document(&self) -> Result<MemoryDocument> {
|
||||||
|
self.store
|
||||||
|
.get_memory_document(&self.workspace_id)?
|
||||||
|
.map(memory_document_from_record)
|
||||||
|
.ok_or_else(|| Error::Store("workspace memory document is not initialized".to_string()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_memory_document(&self, body_md: &str) -> Result<MemoryDocument> {
|
||||||
|
let existing = self.ensure_memory_document()?;
|
||||||
|
let updated_at = now_rfc3339();
|
||||||
|
let record = MemoryDocumentRecord {
|
||||||
|
workspace_id: self.workspace_id.clone(),
|
||||||
|
body_md: body_md.to_string(),
|
||||||
|
created_at: existing.created_at,
|
||||||
|
updated_at,
|
||||||
|
};
|
||||||
|
self.store.upsert_memory_document(&record)?;
|
||||||
|
Ok(memory_document_from_record(record))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_memory_staging_records(&self, limit: usize) -> Result<Vec<MemoryStagingEntry>> {
|
||||||
|
self.store
|
||||||
|
.list_memory_staging_records(&self.workspace_id, limit)
|
||||||
|
.map(|records| {
|
||||||
|
records
|
||||||
|
.into_iter()
|
||||||
|
.map(memory_staging_from_record)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_staging_record(&self, candidate_id: &str) -> Result<MemoryStagingEntry> {
|
||||||
|
validate_memory_candidate_id(candidate_id)?;
|
||||||
|
self.store
|
||||||
|
.get_memory_staging_record(&self.workspace_id, candidate_id)?
|
||||||
|
.map(memory_staging_from_record)
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::Store(format!("unknown memory staging candidate '{candidate_id}'"))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upsert_memory_staging_record(
|
||||||
|
&self,
|
||||||
|
candidate_id: &str,
|
||||||
|
raw_json: &str,
|
||||||
|
source_path: Option<&str>,
|
||||||
|
) -> Result<MemoryStagingEntry> {
|
||||||
|
validate_memory_candidate_id(candidate_id)?;
|
||||||
|
validate_json_object(raw_json, "raw_json")?;
|
||||||
|
let imported_at = now_rfc3339();
|
||||||
|
let record = MemoryStagingRecord {
|
||||||
|
workspace_id: self.workspace_id.clone(),
|
||||||
|
candidate_id: candidate_id.to_string(),
|
||||||
|
raw_json: raw_json.to_string(),
|
||||||
|
source_path: source_path.map(str::to_string),
|
||||||
|
imported_at,
|
||||||
|
};
|
||||||
|
self.store.upsert_memory_staging_record(&record)?;
|
||||||
|
Ok(memory_staging_from_record(record))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn close_memory_staging_record(
|
||||||
|
&self,
|
||||||
|
candidate_id: &str,
|
||||||
|
action: &str,
|
||||||
|
reason: &str,
|
||||||
|
affected_refs_json: &str,
|
||||||
|
) -> Result<MemoryStagingResolution> {
|
||||||
|
validate_memory_candidate_id(candidate_id)?;
|
||||||
|
validate_non_empty(action, "action")?;
|
||||||
|
validate_non_empty(reason, "reason")?;
|
||||||
|
validate_json_array(affected_refs_json, "affected_refs_json")?;
|
||||||
|
let staging = self
|
||||||
|
.store
|
||||||
|
.get_memory_staging_record(&self.workspace_id, candidate_id)?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
Error::Store(format!("unknown memory staging candidate '{candidate_id}'"))
|
||||||
|
})?;
|
||||||
|
let record = MemoryStagingResolutionRecord {
|
||||||
|
workspace_id: self.workspace_id.clone(),
|
||||||
|
candidate_id: staging.candidate_id,
|
||||||
|
action: action.trim().to_string(),
|
||||||
|
reason: reason.trim().to_string(),
|
||||||
|
affected_refs_json: affected_refs_json.to_string(),
|
||||||
|
staging_raw_json: staging.raw_json,
|
||||||
|
source_path: staging.source_path,
|
||||||
|
imported_at: staging.imported_at,
|
||||||
|
resolved_at: now_rfc3339(),
|
||||||
|
};
|
||||||
|
self.store.insert_memory_staging_resolution(&record)?;
|
||||||
|
self.store
|
||||||
|
.delete_memory_staging_record(&self.workspace_id, candidate_id)?;
|
||||||
|
Ok(memory_resolution_from_record(record))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_memory_staging_resolutions(
|
||||||
|
&self,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<MemoryStagingResolution>> {
|
||||||
|
self.store
|
||||||
|
.list_memory_staging_resolutions(&self.workspace_id, limit)
|
||||||
|
.map(|records| {
|
||||||
|
records
|
||||||
|
.into_iter()
|
||||||
|
.map(memory_resolution_from_record)
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn now_rfc3339() -> String {
|
||||||
|
Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_memory_candidate_id(candidate_id: &str) -> Result<()> {
|
||||||
|
validate_non_empty(candidate_id, "candidate_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_non_empty(value: &str, field: &str) -> Result<()> {
|
||||||
|
if value.trim().is_empty() {
|
||||||
|
Err(Error::Store(format!("memory {field} must not be empty")))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_json_object(raw_json: &str, field: &str) -> Result<()> {
|
||||||
|
let value: serde_json::Value = serde_json::from_str(raw_json)
|
||||||
|
.map_err(|err| Error::Store(format!("memory {field} must be valid JSON: {err}")))?;
|
||||||
|
if value.is_object() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::Store(format!(
|
||||||
|
"memory {field} must be a JSON object"
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_json_array(raw_json: &str, field: &str) -> Result<()> {
|
||||||
|
let value: serde_json::Value = serde_json::from_str(raw_json)
|
||||||
|
.map_err(|err| Error::Store(format!("memory {field} must be valid JSON: {err}")))?;
|
||||||
|
if value.is_array() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(Error::Store(format!("memory {field} must be a JSON array")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_document_from_record(record: MemoryDocumentRecord) -> MemoryDocument {
|
||||||
|
MemoryDocument {
|
||||||
|
body_md: record.body_md,
|
||||||
|
created_at: record.created_at,
|
||||||
|
updated_at: record.updated_at,
|
||||||
|
record_source: RECORD_SOURCE_WORKSPACE_SQLITE.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_staging_from_record(record: MemoryStagingRecord) -> MemoryStagingEntry {
|
||||||
|
MemoryStagingEntry {
|
||||||
|
candidate_id: record.candidate_id,
|
||||||
|
raw_json: record.raw_json,
|
||||||
|
source_path: record.source_path,
|
||||||
|
imported_at: record.imported_at,
|
||||||
|
record_source: RECORD_SOURCE_WORKSPACE_SQLITE.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn memory_resolution_from_record(record: MemoryStagingResolutionRecord) -> MemoryStagingResolution {
|
||||||
|
MemoryStagingResolution {
|
||||||
|
candidate_id: record.candidate_id,
|
||||||
|
action: record.action,
|
||||||
|
reason: record.reason,
|
||||||
|
affected_refs_json: record.affected_refs_json,
|
||||||
|
staging_raw_json: record.staging_raw_json,
|
||||||
|
source_path: record.source_path,
|
||||||
|
imported_at: record.imported_at,
|
||||||
|
resolved_at: record.resolved_at,
|
||||||
|
record_source: RECORD_SOURCE_WORKSPACE_SQLITE.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -293,9 +518,36 @@ mod tests {
|
||||||
|
|
||||||
let objective = authority.objective("00000000001J3").unwrap();
|
let objective = authority.objective("00000000001J3").unwrap();
|
||||||
assert!(objective.body.contains("Objective body"));
|
assert!(objective.body.contains("Objective body"));
|
||||||
|
let memory = authority.ensure_memory_document().unwrap();
|
||||||
|
assert_eq!(memory.body_md, DEFAULT_MEMORY_DOCUMENT_BODY);
|
||||||
|
let updated = authority
|
||||||
|
.update_memory_document("# Memory\n\n- Durable fact.\n")
|
||||||
|
.unwrap();
|
||||||
|
assert!(updated.body_md.contains("Durable fact"));
|
||||||
|
|
||||||
|
let staging = authority
|
||||||
|
.upsert_memory_staging_record(
|
||||||
|
"candidate-1",
|
||||||
|
r#"{"kind":"summary","body_md":"Candidate"}"#,
|
||||||
|
Some("memory/_staging/candidate-1.json"),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(staging.record_source, "workspace-sqlite");
|
||||||
|
assert_eq!(authority.list_memory_staging_records(20).unwrap().len(), 1);
|
||||||
|
let resolution = authority
|
||||||
|
.close_memory_staging_record("candidate-1", "apply", "accepted", r#"["memory"]"#)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(resolution.action, "apply");
|
||||||
|
assert_eq!(resolution.reason, "accepted");
|
||||||
|
assert!(
|
||||||
|
authority
|
||||||
|
.list_memory_staging_records(20)
|
||||||
|
.unwrap()
|
||||||
|
.is_empty()
|
||||||
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
authority.memory_authority_status(),
|
authority.list_memory_staging_resolutions(20).unwrap().len(),
|
||||||
MemoryAuthorityStatus::LegacyFilesystemBackendPendingSqliteAuthority
|
1
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ pub mod skills;
|
||||||
pub mod store;
|
pub mod store;
|
||||||
|
|
||||||
pub use authority::{
|
pub use authority::{
|
||||||
MemoryAuthorityMarker, MemoryAuthorityStatus, ObjectiveAuthority, SqliteWorkspaceAuthority,
|
MemoryAuthority, MemoryDocument, MemoryStagingEntry, MemoryStagingResolution,
|
||||||
TicketAuthority, WorkspaceAuthority,
|
ObjectiveAuthority, SqliteWorkspaceAuthority, TicketAuthority, WorkspaceAuthority,
|
||||||
};
|
};
|
||||||
pub use config::{
|
pub use config::{
|
||||||
BackendRuntimesConfigFile, ConfigDiff, ResolvedWorkspaceBackendConfig,
|
BackendRuntimesConfigFile, ConfigDiff, ResolvedWorkspaceBackendConfig,
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,11 @@ const MIGRATIONS: &[Migration] = &[
|
||||||
name: "sqlite objective authority and memory staging import",
|
name: "sqlite objective authority and memory staging import",
|
||||||
apply: create_objective_sqlite_authority_tables,
|
apply: create_objective_sqlite_authority_tables,
|
||||||
},
|
},
|
||||||
|
Migration {
|
||||||
|
version: 11,
|
||||||
|
name: "sqlite memory authority documents and staging resolutions",
|
||||||
|
apply: create_memory_authority_tables,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
struct Migration {
|
struct Migration {
|
||||||
|
|
@ -257,6 +262,14 @@ pub struct ObjectiveResourceRecord {
|
||||||
pub updated_at: String,
|
pub updated_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct MemoryDocumentRecord {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub body_md: String,
|
||||||
|
pub created_at: String,
|
||||||
|
pub updated_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
pub struct MemoryStagingRecord {
|
pub struct MemoryStagingRecord {
|
||||||
pub workspace_id: String,
|
pub workspace_id: String,
|
||||||
|
|
@ -266,6 +279,19 @@ pub struct MemoryStagingRecord {
|
||||||
pub imported_at: String,
|
pub imported_at: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||||
|
pub struct MemoryStagingResolutionRecord {
|
||||||
|
pub workspace_id: String,
|
||||||
|
pub candidate_id: String,
|
||||||
|
pub action: String,
|
||||||
|
pub reason: String,
|
||||||
|
pub affected_refs_json: String,
|
||||||
|
pub staging_raw_json: String,
|
||||||
|
pub source_path: Option<String>,
|
||||||
|
pub imported_at: String,
|
||||||
|
pub resolved_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
pub trait ControlPlaneStore: Send + Sync {
|
pub trait ControlPlaneStore: Send + Sync {
|
||||||
async fn schema_version(&self) -> Result<i64>;
|
async fn schema_version(&self) -> Result<i64>;
|
||||||
|
|
@ -299,8 +325,36 @@ pub trait ControlPlaneStore: Send + Sync {
|
||||||
workspace_id: &str,
|
workspace_id: &str,
|
||||||
objective_id: &str,
|
objective_id: &str,
|
||||||
) -> Result<Vec<ObjectiveResourceRecord>>;
|
) -> Result<Vec<ObjectiveResourceRecord>>;
|
||||||
|
fn ensure_memory_document(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
default_body_md: &str,
|
||||||
|
now: &str,
|
||||||
|
) -> Result<MemoryDocumentRecord>;
|
||||||
|
fn get_memory_document(&self, workspace_id: &str) -> Result<Option<MemoryDocumentRecord>>;
|
||||||
|
fn upsert_memory_document(&self, record: &MemoryDocumentRecord) -> Result<()>;
|
||||||
fn upsert_memory_staging_record(&self, record: &MemoryStagingRecord) -> Result<()>;
|
fn upsert_memory_staging_record(&self, record: &MemoryStagingRecord) -> Result<()>;
|
||||||
|
fn list_memory_staging_records(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<MemoryStagingRecord>>;
|
||||||
|
fn get_memory_staging_record(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
candidate_id: &str,
|
||||||
|
) -> Result<Option<MemoryStagingRecord>>;
|
||||||
|
fn delete_memory_staging_record(&self, workspace_id: &str, candidate_id: &str) -> Result<bool>;
|
||||||
fn count_memory_staging_records(&self, workspace_id: &str) -> Result<usize>;
|
fn count_memory_staging_records(&self, workspace_id: &str) -> Result<usize>;
|
||||||
|
fn insert_memory_staging_resolution(
|
||||||
|
&self,
|
||||||
|
record: &MemoryStagingResolutionRecord,
|
||||||
|
) -> Result<()>;
|
||||||
|
fn list_memory_staging_resolutions(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<MemoryStagingResolutionRecord>>;
|
||||||
|
|
||||||
fn upsert_account(&self, record: &AccountRecord) -> Result<()>;
|
fn upsert_account(&self, record: &AccountRecord) -> Result<()>;
|
||||||
fn get_account(&self, account_id: &str) -> Result<Option<AccountRecord>>;
|
fn get_account(&self, account_id: &str) -> Result<Option<AccountRecord>>;
|
||||||
|
|
@ -712,6 +766,64 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn ensure_memory_document(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
default_body_md: &str,
|
||||||
|
now: &str,
|
||||||
|
) -> Result<MemoryDocumentRecord> {
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
conn.execute(
|
||||||
|
r#"INSERT OR IGNORE INTO workspace_memory_documents (
|
||||||
|
workspace_id, body_md, created_at, updated_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?3)"#,
|
||||||
|
params![workspace_id, default_body_md, now],
|
||||||
|
)?;
|
||||||
|
conn.query_row(
|
||||||
|
r#"SELECT workspace_id, body_md, created_at, updated_at
|
||||||
|
FROM workspace_memory_documents
|
||||||
|
WHERE workspace_id = ?1"#,
|
||||||
|
params![workspace_id],
|
||||||
|
read_memory_document_record,
|
||||||
|
)
|
||||||
|
.map_err(Error::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_memory_document(&self, workspace_id: &str) -> Result<Option<MemoryDocumentRecord>> {
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
conn.query_row(
|
||||||
|
r#"SELECT workspace_id, body_md, created_at, updated_at
|
||||||
|
FROM workspace_memory_documents
|
||||||
|
WHERE workspace_id = ?1"#,
|
||||||
|
params![workspace_id],
|
||||||
|
read_memory_document_record,
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.map_err(Error::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn upsert_memory_document(&self, record: &MemoryDocumentRecord) -> Result<()> {
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
conn.execute(
|
||||||
|
r#"INSERT INTO workspace_memory_documents (
|
||||||
|
workspace_id, body_md, created_at, updated_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4)
|
||||||
|
ON CONFLICT(workspace_id) DO UPDATE SET
|
||||||
|
body_md = excluded.body_md,
|
||||||
|
updated_at = excluded.updated_at"#,
|
||||||
|
params![
|
||||||
|
record.workspace_id,
|
||||||
|
record.body_md,
|
||||||
|
record.created_at,
|
||||||
|
record.updated_at,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn upsert_memory_staging_record(&self, record: &MemoryStagingRecord) -> Result<()> {
|
fn upsert_memory_staging_record(&self, record: &MemoryStagingRecord) -> Result<()> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
@ -734,6 +846,56 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn list_memory_staging_records(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<MemoryStagingRecord>> {
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
r#"SELECT workspace_id, candidate_id, raw_json, source_path, imported_at
|
||||||
|
FROM memory_staging_records
|
||||||
|
WHERE workspace_id = ?1
|
||||||
|
ORDER BY imported_at DESC, candidate_id ASC
|
||||||
|
LIMIT ?2"#,
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map(
|
||||||
|
params![workspace_id, limit as i64],
|
||||||
|
read_memory_staging_record,
|
||||||
|
)?;
|
||||||
|
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.map_err(Error::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_memory_staging_record(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
candidate_id: &str,
|
||||||
|
) -> Result<Option<MemoryStagingRecord>> {
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
conn.query_row(
|
||||||
|
r#"SELECT workspace_id, candidate_id, raw_json, source_path, imported_at
|
||||||
|
FROM memory_staging_records
|
||||||
|
WHERE workspace_id = ?1 AND candidate_id = ?2"#,
|
||||||
|
params![workspace_id, candidate_id],
|
||||||
|
read_memory_staging_record,
|
||||||
|
)
|
||||||
|
.optional()
|
||||||
|
.map_err(Error::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delete_memory_staging_record(&self, workspace_id: &str, candidate_id: &str) -> Result<bool> {
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
let changed = conn.execute(
|
||||||
|
"DELETE FROM memory_staging_records WHERE workspace_id = ?1 AND candidate_id = ?2",
|
||||||
|
params![workspace_id, candidate_id],
|
||||||
|
)?;
|
||||||
|
Ok(changed > 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn count_memory_staging_records(&self, workspace_id: &str) -> Result<usize> {
|
fn count_memory_staging_records(&self, workspace_id: &str) -> Result<usize> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.query_row(
|
conn.query_row(
|
||||||
|
|
@ -746,6 +908,55 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn insert_memory_staging_resolution(
|
||||||
|
&self,
|
||||||
|
record: &MemoryStagingResolutionRecord,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
conn.execute(
|
||||||
|
r#"INSERT INTO memory_staging_resolutions (
|
||||||
|
workspace_id, candidate_id, action, reason, affected_refs_json,
|
||||||
|
staging_raw_json, source_path, imported_at, resolved_at
|
||||||
|
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)"#,
|
||||||
|
params![
|
||||||
|
record.workspace_id,
|
||||||
|
record.candidate_id,
|
||||||
|
record.action,
|
||||||
|
record.reason,
|
||||||
|
record.affected_refs_json,
|
||||||
|
record.staging_raw_json,
|
||||||
|
record.source_path,
|
||||||
|
record.imported_at,
|
||||||
|
record.resolved_at,
|
||||||
|
],
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn list_memory_staging_resolutions(
|
||||||
|
&self,
|
||||||
|
workspace_id: &str,
|
||||||
|
limit: usize,
|
||||||
|
) -> Result<Vec<MemoryStagingResolutionRecord>> {
|
||||||
|
self.with_conn(|conn| {
|
||||||
|
let mut stmt = conn.prepare(
|
||||||
|
r#"SELECT workspace_id, candidate_id, action, reason, affected_refs_json,
|
||||||
|
staging_raw_json, source_path, imported_at, resolved_at
|
||||||
|
FROM memory_staging_resolutions
|
||||||
|
WHERE workspace_id = ?1
|
||||||
|
ORDER BY resolved_at DESC, candidate_id ASC
|
||||||
|
LIMIT ?2"#,
|
||||||
|
)?;
|
||||||
|
let rows = stmt.query_map(
|
||||||
|
params![workspace_id, limit as i64],
|
||||||
|
read_memory_staging_resolution_record,
|
||||||
|
)?;
|
||||||
|
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||||
|
.map_err(Error::from)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn upsert_account(&self, record: &AccountRecord) -> Result<()> {
|
fn upsert_account(&self, record: &AccountRecord) -> Result<()> {
|
||||||
self.with_conn(|conn| {
|
self.with_conn(|conn| {
|
||||||
conn.execute(
|
conn.execute(
|
||||||
|
|
@ -1569,6 +1780,41 @@ fn read_objective_resource_record(
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn read_memory_document_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<MemoryDocumentRecord> {
|
||||||
|
Ok(MemoryDocumentRecord {
|
||||||
|
workspace_id: row.get(0)?,
|
||||||
|
body_md: row.get(1)?,
|
||||||
|
created_at: row.get(2)?,
|
||||||
|
updated_at: row.get(3)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_memory_staging_record(row: &rusqlite::Row<'_>) -> rusqlite::Result<MemoryStagingRecord> {
|
||||||
|
Ok(MemoryStagingRecord {
|
||||||
|
workspace_id: row.get(0)?,
|
||||||
|
candidate_id: row.get(1)?,
|
||||||
|
raw_json: row.get(2)?,
|
||||||
|
source_path: row.get(3)?,
|
||||||
|
imported_at: row.get(4)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_memory_staging_resolution_record(
|
||||||
|
row: &rusqlite::Row<'_>,
|
||||||
|
) -> rusqlite::Result<MemoryStagingResolutionRecord> {
|
||||||
|
Ok(MemoryStagingResolutionRecord {
|
||||||
|
workspace_id: row.get(0)?,
|
||||||
|
candidate_id: row.get(1)?,
|
||||||
|
action: row.get(2)?,
|
||||||
|
reason: row.get(3)?,
|
||||||
|
affected_refs_json: row.get(4)?,
|
||||||
|
staging_raw_json: row.get(5)?,
|
||||||
|
source_path: row.get(6)?,
|
||||||
|
imported_at: row.get(7)?,
|
||||||
|
resolved_at: row.get(8)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
fn worker_registry_select_sql(where_clause: &str) -> String {
|
fn worker_registry_select_sql(where_clause: &str) -> String {
|
||||||
format!(
|
format!(
|
||||||
"SELECT workspace_id, runtime_id, runtime_worker_id, display_name, profile, \
|
"SELECT workspace_id, runtime_id, runtime_worker_id, display_name, profile, \
|
||||||
|
|
@ -1732,6 +1978,33 @@ CREATE TABLE IF NOT EXISTS memory_staging_records (
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_memory_authority_tables(conn: &Connection) -> Result<()> {
|
||||||
|
conn.execute_batch(
|
||||||
|
r#"
|
||||||
|
CREATE TABLE IF NOT EXISTS workspace_memory_documents (
|
||||||
|
workspace_id TEXT PRIMARY KEY REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
body_md TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS memory_staging_resolutions (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
candidate_id TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
affected_refs_json TEXT NOT NULL,
|
||||||
|
staging_raw_json TEXT NOT NULL,
|
||||||
|
source_path TEXT,
|
||||||
|
imported_at TEXT NOT NULL,
|
||||||
|
resolved_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (workspace_id, candidate_id, resolved_at)
|
||||||
|
);
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn create_account_identity_tables(conn: &Connection) -> Result<()> {
|
fn create_account_identity_tables(conn: &Connection) -> Result<()> {
|
||||||
conn.execute_batch(
|
conn.execute_batch(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -2293,6 +2566,26 @@ CREATE TABLE IF NOT EXISTS memory_staging_records (
|
||||||
PRIMARY KEY (workspace_id, candidate_id)
|
PRIMARY KEY (workspace_id, candidate_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS workspace_memory_documents (
|
||||||
|
workspace_id TEXT PRIMARY KEY REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
body_md TEXT NOT NULL,
|
||||||
|
created_at TEXT NOT NULL,
|
||||||
|
updated_at TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS memory_staging_resolutions (
|
||||||
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
|
candidate_id TEXT NOT NULL,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
reason TEXT NOT NULL,
|
||||||
|
affected_refs_json TEXT NOT NULL,
|
||||||
|
staging_raw_json TEXT NOT NULL,
|
||||||
|
source_path TEXT,
|
||||||
|
imported_at TEXT NOT NULL,
|
||||||
|
resolved_at TEXT NOT NULL,
|
||||||
|
PRIMARY KEY (workspace_id, candidate_id, resolved_at)
|
||||||
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS repositories (
|
CREATE TABLE IF NOT EXISTS repositories (
|
||||||
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||||
repository_id TEXT PRIMARY KEY,
|
repository_id TEXT PRIMARY KEY,
|
||||||
|
|
@ -2424,7 +2717,7 @@ mod tests {
|
||||||
let db = dir.path().join("control-plane.sqlite");
|
let db = dir.path().join("control-plane.sqlite");
|
||||||
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
let store = SqliteWorkspaceStore::open(&db).unwrap();
|
||||||
|
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 10);
|
assert_eq!(store.schema_version().await.unwrap(), 11);
|
||||||
|
|
||||||
let record = WorkspaceRecord {
|
let record = WorkspaceRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
|
|
@ -2437,7 +2730,7 @@ mod tests {
|
||||||
store.upsert_workspace(&record).await.unwrap();
|
store.upsert_workspace(&record).await.unwrap();
|
||||||
|
|
||||||
let reopened = SqliteWorkspaceStore::open(&db).unwrap();
|
let reopened = SqliteWorkspaceStore::open(&db).unwrap();
|
||||||
assert_eq!(reopened.schema_version().await.unwrap(), 10);
|
assert_eq!(reopened.schema_version().await.unwrap(), 11);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
reopened.get_workspace("local-dev").await.unwrap(),
|
reopened.get_workspace("local-dev").await.unwrap(),
|
||||||
Some(record)
|
Some(record)
|
||||||
|
|
@ -2458,6 +2751,10 @@ mod tests {
|
||||||
"ticket_relations",
|
"ticket_relations",
|
||||||
"objectives",
|
"objectives",
|
||||||
"objective_ticket_links",
|
"objective_ticket_links",
|
||||||
|
"objective_resources",
|
||||||
|
"memory_staging_records",
|
||||||
|
"workspace_memory_documents",
|
||||||
|
"memory_staging_resolutions",
|
||||||
"repositories",
|
"repositories",
|
||||||
"ticket_targets",
|
"ticket_targets",
|
||||||
"ticket_target_paths",
|
"ticket_target_paths",
|
||||||
|
|
@ -2654,7 +2951,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let store = SqliteWorkspaceStore::from_connection(conn).unwrap();
|
let store = SqliteWorkspaceStore::from_connection(conn).unwrap();
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 10);
|
assert_eq!(store.schema_version().await.unwrap(), 11);
|
||||||
|
|
||||||
store
|
store
|
||||||
.with_conn(|conn| {
|
.with_conn(|conn| {
|
||||||
|
|
@ -2667,6 +2964,9 @@ mod tests {
|
||||||
"ticket_worker_links",
|
"ticket_worker_links",
|
||||||
"artifacts",
|
"artifacts",
|
||||||
"audit_events",
|
"audit_events",
|
||||||
|
"workspace_memory_documents",
|
||||||
|
"memory_staging_records",
|
||||||
|
"memory_staging_resolutions",
|
||||||
"legacy_workspaces",
|
"legacy_workspaces",
|
||||||
"legacy_repositories",
|
"legacy_repositories",
|
||||||
"legacy_runs",
|
"legacy_runs",
|
||||||
|
|
@ -2745,7 +3045,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn repository_records_round_trip() {
|
async fn repository_records_round_trip() {
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 10);
|
assert_eq!(store.schema_version().await.unwrap(), 11);
|
||||||
let workspace = WorkspaceRecord {
|
let workspace = WorkspaceRecord {
|
||||||
workspace_id: "local-dev".to_string(),
|
workspace_id: "local-dev".to_string(),
|
||||||
owner_account_id: None,
|
owner_account_id: None,
|
||||||
|
|
@ -2780,6 +3080,81 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn memory_authority_records_round_trip_and_close_staging() {
|
||||||
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
|
assert_eq!(store.schema_version().await.unwrap(), 11);
|
||||||
|
let workspace = WorkspaceRecord {
|
||||||
|
workspace_id: "local-dev".to_string(),
|
||||||
|
owner_account_id: None,
|
||||||
|
display_name: "Local Dev".to_string(),
|
||||||
|
state: "active".to_string(),
|
||||||
|
created_at: "1".to_string(),
|
||||||
|
updated_at: "1".to_string(),
|
||||||
|
};
|
||||||
|
store.upsert_workspace(&workspace).await.unwrap();
|
||||||
|
|
||||||
|
let document = store
|
||||||
|
.ensure_memory_document("local-dev", "# Memory\n", "2")
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(document.body_md, "# Memory\n");
|
||||||
|
let updated = MemoryDocumentRecord {
|
||||||
|
workspace_id: "local-dev".to_string(),
|
||||||
|
body_md: "# Memory\n\n- fact\n".to_string(),
|
||||||
|
created_at: document.created_at.clone(),
|
||||||
|
updated_at: "3".to_string(),
|
||||||
|
};
|
||||||
|
store.upsert_memory_document(&updated).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
store.get_memory_document("local-dev").unwrap(),
|
||||||
|
Some(updated)
|
||||||
|
);
|
||||||
|
|
||||||
|
let staging = MemoryStagingRecord {
|
||||||
|
workspace_id: "local-dev".to_string(),
|
||||||
|
candidate_id: "candidate-a".to_string(),
|
||||||
|
raw_json: r#"{"kind":"summary","body":"candidate"}"#.to_string(),
|
||||||
|
source_path: Some("memory/_staging/candidate-a.json".to_string()),
|
||||||
|
imported_at: "4".to_string(),
|
||||||
|
};
|
||||||
|
store.upsert_memory_staging_record(&staging).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.get_memory_staging_record("local-dev", "candidate-a")
|
||||||
|
.unwrap(),
|
||||||
|
Some(staging.clone())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
store.list_memory_staging_records("local-dev", 10).unwrap(),
|
||||||
|
vec![staging.clone()]
|
||||||
|
);
|
||||||
|
|
||||||
|
let resolution = MemoryStagingResolutionRecord {
|
||||||
|
workspace_id: "local-dev".to_string(),
|
||||||
|
candidate_id: staging.candidate_id.clone(),
|
||||||
|
action: "apply".to_string(),
|
||||||
|
reason: "accepted".to_string(),
|
||||||
|
affected_refs_json: r#"["summary"]"#.to_string(),
|
||||||
|
staging_raw_json: staging.raw_json.clone(),
|
||||||
|
source_path: staging.source_path.clone(),
|
||||||
|
imported_at: staging.imported_at.clone(),
|
||||||
|
resolved_at: "5".to_string(),
|
||||||
|
};
|
||||||
|
store.insert_memory_staging_resolution(&resolution).unwrap();
|
||||||
|
assert!(
|
||||||
|
store
|
||||||
|
.delete_memory_staging_record("local-dev", "candidate-a")
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
assert_eq!(store.count_memory_staging_records("local-dev").unwrap(), 0);
|
||||||
|
assert_eq!(
|
||||||
|
store
|
||||||
|
.list_memory_staging_resolutions("local-dev", 10)
|
||||||
|
.unwrap(),
|
||||||
|
vec![resolution]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn worker_workdir_registry_round_trips_and_preserves_pinned_retention() {
|
async fn worker_workdir_registry_round_trips_and_preserves_pinned_retention() {
|
||||||
let temp = tempfile::tempdir().unwrap();
|
let temp = tempfile::tempdir().unwrap();
|
||||||
|
|
@ -2882,7 +3257,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn account_and_login_records_round_trip() {
|
async fn account_and_login_records_round_trip() {
|
||||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||||
assert_eq!(store.schema_version().await.unwrap(), 10);
|
assert_eq!(store.schema_version().await.unwrap(), 11);
|
||||||
let now = "2026-07-22T00:00:00Z".to_string();
|
let now = "2026-07-22T00:00:00Z".to_string();
|
||||||
let account = AccountRecord {
|
let account = AccountRecord {
|
||||||
account_id: "acct-user-alice".to_string(),
|
account_id: "acct-user-alice".to_string(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user