fix: sync backend worker registry projections
This commit is contained in:
parent
3e5546ea33
commit
8695089fba
|
|
@ -2,7 +2,7 @@
|
|||
title: 'Add Backend Worker/Workdir registry and link model'
|
||||
state: 'inprogress'
|
||||
created_at: '2026-07-10T15:53:02Z'
|
||||
updated_at: '2026-07-10T16:38:10Z'
|
||||
updated_at: '2026-07-10T17:12:46Z'
|
||||
assignee: null
|
||||
queued_by: 'workspace-panel'
|
||||
queued_at: '2026-07-10T16:10:57Z'
|
||||
|
|
|
|||
|
|
@ -169,4 +169,30 @@ Validation run:
|
|||
- nix build .#yoi --no-link
|
||||
|
||||
|
||||
---
|
||||
|
||||
<!-- event: implementation_report author: hare at: 2026-07-10T17:12:46Z -->
|
||||
|
||||
## Implementation report
|
||||
|
||||
Follow-up fix for external review blockers.
|
||||
|
||||
Fix summary:
|
||||
- Browser Worker list now projects from Backend worker_registry rows after Runtime observation sync, preserving Backend-only stopped/archive rows and using worker_workdir_links as Workdir relationship authority.
|
||||
- Runtime-scoped Worker spawn now writes pending Backend-managed Workdir registry rows, records spawned Workers, and links Workers to materialized or selected Workdirs.
|
||||
- Worker stop now updates Backend Worker lifecycle and syncs linked Workdir records to present/removed/missing without deleting canonical rows.
|
||||
- Runtime Workdir listing now syncs Runtime observations into Backend registry and projects typed management_kind (`backend_managed` / `runtime_unmanaged`) instead of returning raw Runtime summaries directly.
|
||||
- Existing `pinned` Worker retention is preserved when Runtime observation/upsert sync supplies ordinary `normal` retention.
|
||||
- Strengthened tests for archive/link projection, unmanaged Workdir projection, raw path redaction, removed Workdir preservation, and pinned retention preservation.
|
||||
|
||||
Validation run:
|
||||
- git diff --check
|
||||
- cargo test -p yoi-workspace-server --lib
|
||||
- cargo test -p worker-runtime --features ws-server,fs-store
|
||||
- cargo check -p yoi
|
||||
- cd web/workspace && deno task check && deno task test
|
||||
- yoi ticket doctor
|
||||
- nix build .#yoi --no-link
|
||||
|
||||
|
||||
---
|
||||
|
|
|
|||
|
|
@ -162,6 +162,10 @@ pub struct WorkingDirectorySummary {
|
|||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cleanup_policy: Option<String>,
|
||||
pub status: WorkingDirectoryStatusKind,
|
||||
/// Backend projection metadata. Runtimes leave this absent; Workspace Browser
|
||||
/// APIs fill it with `backend_managed` or `runtime_unmanaged`.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub management_kind: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ impl WorkingDirectory {
|
|||
cleanup_target: Some(self.cleanup_target.clone()),
|
||||
cleanup_policy: Some(self.cleanup_policy.clone()),
|
||||
status: self.status.clone(),
|
||||
management_kind: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,10 +24,11 @@ use crate::config::{RemoteRuntimeConfigFile, WorkspaceBackendConfigFile, resolve
|
|||
use crate::hosts::{
|
||||
ConfigBundleCheckResult, ConfigBundleSyncResult, DiagnosticSeverity, EmbeddedWorkerRuntime,
|
||||
HostSummary, RemoteRuntimeConfig, RemoteWorkerRuntime, RuntimeDiagnostic, RuntimeRegistry,
|
||||
RuntimeRegistryUnregisterResult, RuntimeSummary, WorkerInputRequest, WorkerInputResult,
|
||||
WorkerLifecycleRequest, WorkerLifecycleResult, WorkerOperationState,
|
||||
WorkerSpawnAcceptanceRequirement, WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult,
|
||||
WorkerSpawnWorkingDirectoryRequest, WorkerSummary, WorkerTranscriptProjection,
|
||||
RuntimeRegistryUnregisterResult, RuntimeSummary, WorkerCapabilitySummary,
|
||||
WorkerImplementationSummary, WorkerInputRequest, WorkerInputResult, WorkerLifecycleRequest,
|
||||
WorkerLifecycleResult, WorkerOperationState, WorkerSpawnAcceptanceRequirement,
|
||||
WorkerSpawnIntent, WorkerSpawnRequest, WorkerSpawnResult, WorkerSpawnWorkingDirectoryRequest,
|
||||
WorkerSummary, WorkerTranscriptProjection, WorkerWorkspaceSummary,
|
||||
};
|
||||
use crate::identity::WorkspaceIdentity;
|
||||
use crate::observation::{
|
||||
|
|
@ -1186,18 +1187,11 @@ async fn scoped_list_runtime_working_directories(
|
|||
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
||||
) -> ApiResult<Json<BrowserWorkingDirectoryListResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
let list = api
|
||||
.runtime
|
||||
.list_working_directories(&path.runtime_id)
|
||||
.map_err(|err| err.into_error())?;
|
||||
let (items, diagnostics) = runtime_working_directory_summaries(&api, &path.runtime_id)?;
|
||||
Ok(Json(BrowserWorkingDirectoryListResponse {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
items: list
|
||||
.items
|
||||
.into_iter()
|
||||
.map(|status| status.summary)
|
||||
.collect(),
|
||||
diagnostics: list.diagnostics,
|
||||
items,
|
||||
diagnostics,
|
||||
}))
|
||||
}
|
||||
|
||||
|
|
@ -2138,8 +2132,7 @@ async fn create_workspace_worker(
|
|||
code: "workspace_worker_create_missing_summary".to_string(),
|
||||
message: "Runtime completed worker creation without returning a Worker summary".to_string(),
|
||||
})?;
|
||||
let worker_record =
|
||||
record_worker_summary(&api, &worker, &display_name, Some(request.profile.clone()))?;
|
||||
let worker_record = sync_worker_observation(&api, &worker)?;
|
||||
if let Some(workdir_id) = selected_working_directory_id.as_deref() {
|
||||
if api
|
||||
.store
|
||||
|
|
@ -2278,10 +2271,47 @@ async fn create_runtime_worker(
|
|||
configured_working_directory_request(&api.config, working_directory)
|
||||
})
|
||||
.transpose()?;
|
||||
let prepared_workdir_id = if let Some(working_directory_request) =
|
||||
request.resolved_working_directory_request.as_mut()
|
||||
{
|
||||
Some(upsert_pending_backend_workdir(
|
||||
&api,
|
||||
&runtime_id,
|
||||
working_directory_request,
|
||||
)?)
|
||||
} else {
|
||||
request
|
||||
.resolved_working_directory
|
||||
.as_ref()
|
||||
.map(|claim| claim.working_directory_id.clone())
|
||||
};
|
||||
let result = api
|
||||
.runtime
|
||||
.spawn_worker(&runtime_id, request)
|
||||
.map_err(|err| err.into_error())?;
|
||||
if let Some(worker) = result.worker.as_ref() {
|
||||
let record = sync_worker_observation(&api, worker)?;
|
||||
if worker.working_directory.is_none() {
|
||||
if let Some(workdir_id) = prepared_workdir_id.as_deref() {
|
||||
if api
|
||||
.store
|
||||
.get_workdir_registry(&api.config.workspace_id, workdir_id)?
|
||||
.is_some()
|
||||
{
|
||||
link_worker_to_workdir(&api, &record, workdir_id)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(workdir_id) = prepared_workdir_id.as_deref() {
|
||||
if let Some(mut record) = api
|
||||
.store
|
||||
.get_workdir_registry(&api.config.workspace_id, workdir_id)?
|
||||
{
|
||||
record.materialization_status = "failed".to_string();
|
||||
record.updated_at = now_registry_timestamp();
|
||||
api.store.upsert_workdir_registry(&record)?;
|
||||
}
|
||||
}
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
|
|
@ -2336,6 +2366,16 @@ async fn stop_runtime_worker(
|
|||
.runtime
|
||||
.stop_worker(&runtime_id, &worker_id, request)
|
||||
.map_err(|err| err.into_error())?;
|
||||
let backend_id = backend_worker_id(&runtime_id, &worker_id);
|
||||
if let Some(mut record) = api
|
||||
.store
|
||||
.get_worker_registry(&api.config.workspace_id, backend_id.as_str())?
|
||||
{
|
||||
record.lifecycle_state = "stopped".to_string();
|
||||
record.updated_at = now_registry_timestamp();
|
||||
api.store.upsert_worker_registry(&record)?;
|
||||
sync_linked_workdir_after_worker_stop(&api, &runtime_id, &record)?;
|
||||
}
|
||||
Ok(Json(result))
|
||||
}
|
||||
|
||||
|
|
@ -2529,19 +2569,37 @@ async fn list_host_workers(
|
|||
fn workers_response(api: WorkspaceApi) -> ApiResult<RuntimeListResponse<WorkerSummary>> {
|
||||
let limit = api.config.max_records.min(200);
|
||||
let runtime_workers = api.runtime.list_workers(limit);
|
||||
let mut observed = std::collections::BTreeMap::new();
|
||||
for worker in &runtime_workers.items {
|
||||
let _ = record_worker_summary(
|
||||
&api,
|
||||
worker,
|
||||
worker.worker_id.as_str(),
|
||||
worker.profile.clone(),
|
||||
let _ = sync_worker_observation(&api, worker);
|
||||
observed.insert(
|
||||
backend_worker_id(worker.runtime_id.as_str(), worker.worker_id.as_str()),
|
||||
worker.clone(),
|
||||
);
|
||||
}
|
||||
let worker_records = api
|
||||
.store
|
||||
.list_worker_registry(&api.config.workspace_id, limit)?;
|
||||
let workdir_records = api
|
||||
.store
|
||||
.list_workdir_registry(&api.config.workspace_id, 500)?;
|
||||
let mut items = Vec::new();
|
||||
for record in worker_records {
|
||||
let links = api
|
||||
.store
|
||||
.list_worker_workdir_links(&api.config.workspace_id, record.worker_id.as_str())?;
|
||||
items.push(merge_worker_registry_projection(
|
||||
observed.get(record.worker_id.as_str()),
|
||||
&record,
|
||||
links,
|
||||
&workdir_records,
|
||||
));
|
||||
}
|
||||
Ok(RuntimeListResponse {
|
||||
workspace_id: api.config.workspace_id,
|
||||
limit,
|
||||
items: runtime_workers.items,
|
||||
source: "backend_worker_registry_synced".to_string(),
|
||||
items,
|
||||
source: "backend_worker_registry".to_string(),
|
||||
diagnostics: runtime_workers.diagnostics,
|
||||
})
|
||||
}
|
||||
|
|
@ -3240,15 +3298,32 @@ fn working_directory_repository_options(
|
|||
}
|
||||
|
||||
fn working_directory_summaries(api: &WorkspaceApi) -> ApiResult<Vec<WorkingDirectorySummary>> {
|
||||
let _ = sync_all_runtime_workdir_observations(api);
|
||||
let records = api
|
||||
.store
|
||||
.list_managed_workdir_registry(&api.config.workspace_id, 200)?;
|
||||
.list_workdir_registry(&api.config.workspace_id, 200)?;
|
||||
Ok(records
|
||||
.iter()
|
||||
.map(workdir_summary_from_record)
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
fn runtime_working_directory_summaries(
|
||||
api: &WorkspaceApi,
|
||||
runtime_id: &str,
|
||||
) -> ApiResult<(Vec<WorkingDirectorySummary>, Vec<RuntimeDiagnostic>)> {
|
||||
let diagnostics = sync_runtime_workdir_observations(api, runtime_id)?;
|
||||
let records = api
|
||||
.store
|
||||
.list_workdir_registry(&api.config.workspace_id, 200)?;
|
||||
let items = records
|
||||
.iter()
|
||||
.filter(|record| record.runtime_id == runtime_id)
|
||||
.map(workdir_summary_from_record)
|
||||
.collect::<Vec<_>>();
|
||||
Ok((items, diagnostics))
|
||||
}
|
||||
|
||||
fn backend_worker_id(runtime_id: &str, runtime_worker_id: &str) -> String {
|
||||
format!("{runtime_id}/{runtime_worker_id}")
|
||||
}
|
||||
|
|
@ -3297,7 +3372,7 @@ fn record_worker_summary(
|
|||
let worker_id = backend_worker_id(worker.runtime_id.as_str(), worker.worker_id.as_str());
|
||||
let record = WorkerRegistryRecord {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
worker_id,
|
||||
worker_id: worker_id.clone(),
|
||||
runtime_id: worker.runtime_id.as_str().to_string(),
|
||||
runtime_worker_id: worker.worker_id.as_str().to_string(),
|
||||
display_name: display_name.to_string(),
|
||||
|
|
@ -3316,9 +3391,216 @@ fn record_worker_summary(
|
|||
updated_at: timestamp,
|
||||
};
|
||||
api.store.upsert_worker_registry(&record)?;
|
||||
Ok(api
|
||||
.store
|
||||
.get_worker_registry(&api.config.workspace_id, worker_id.as_str())?
|
||||
.unwrap_or(record))
|
||||
}
|
||||
|
||||
fn worker_summary_from_registry(record: &WorkerRegistryRecord) -> WorkerSummary {
|
||||
WorkerSummary {
|
||||
worker_id: record.runtime_worker_id.clone(),
|
||||
runtime_id: record.runtime_id.clone(),
|
||||
host_id: "backend-registry".to_string(),
|
||||
role: None,
|
||||
label: record.display_name.clone(),
|
||||
status: record.lifecycle_state.clone(),
|
||||
state: record.lifecycle_state.clone(),
|
||||
last_seen_at: Some(record.updated_at.clone()),
|
||||
capabilities: WorkerCapabilitySummary {
|
||||
can_accept_input: false,
|
||||
can_stop: false,
|
||||
can_spawn_followup: false,
|
||||
},
|
||||
workspace: WorkerWorkspaceSummary {
|
||||
visibility: "backend_registry".to_string(),
|
||||
identity: record.workspace_id.clone(),
|
||||
},
|
||||
profile: record.profile.clone(),
|
||||
implementation: WorkerImplementationSummary {
|
||||
kind: "backend_worker_registry".to_string(),
|
||||
display_hint: "Archived Worker".to_string(),
|
||||
},
|
||||
working_directory: None,
|
||||
diagnostics: vec![RuntimeDiagnostic {
|
||||
code: "backend_worker_registry_only".to_string(),
|
||||
severity: DiagnosticSeverity::Info,
|
||||
message:
|
||||
"Worker is preserved in the Backend registry without a live Runtime observation"
|
||||
.to_string(),
|
||||
}],
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_worker_registry_projection(
|
||||
live: Option<&WorkerSummary>,
|
||||
record: &WorkerRegistryRecord,
|
||||
links: Vec<WorkerWorkdirLinkRecord>,
|
||||
workdirs: &[WorkdirRegistryRecord],
|
||||
) -> WorkerSummary {
|
||||
let mut summary = live
|
||||
.cloned()
|
||||
.unwrap_or_else(|| worker_summary_from_registry(record));
|
||||
summary.label = record.display_name.clone();
|
||||
summary.status = record.lifecycle_state.clone();
|
||||
summary.state = record.lifecycle_state.clone();
|
||||
summary.profile = record.profile.clone();
|
||||
summary.working_directory = links.iter().find_map(|link| {
|
||||
workdirs
|
||||
.iter()
|
||||
.find(|workdir| workdir.workdir_id == link.workdir_id)
|
||||
.map(|workdir| workdir_summary_from_record(workdir))
|
||||
});
|
||||
summary
|
||||
}
|
||||
|
||||
fn sync_worker_observation(
|
||||
api: &WorkspaceApi,
|
||||
worker: &WorkerSummary,
|
||||
) -> ApiResult<WorkerRegistryRecord> {
|
||||
let record = record_worker_summary(api, worker, worker.label.as_str(), worker.profile.clone())?;
|
||||
if let Some(working_directory) = worker.working_directory.as_ref() {
|
||||
let management_kind = api
|
||||
.store
|
||||
.get_workdir_registry(
|
||||
&api.config.workspace_id,
|
||||
&working_directory.working_directory_id,
|
||||
)?
|
||||
.map(|existing| existing.management_kind)
|
||||
.unwrap_or_else(|| "runtime_unmanaged".to_string());
|
||||
let workdir_record = workdir_record_from_summary(
|
||||
api,
|
||||
worker.runtime_id.as_str(),
|
||||
working_directory,
|
||||
management_kind.as_str(),
|
||||
);
|
||||
api.store.upsert_workdir_registry(&workdir_record)?;
|
||||
link_worker_to_workdir(api, &record, &working_directory.working_directory_id)?;
|
||||
}
|
||||
Ok(record)
|
||||
}
|
||||
|
||||
fn upsert_pending_backend_workdir(
|
||||
api: &WorkspaceApi,
|
||||
runtime_id: &str,
|
||||
request: &mut WorkingDirectoryRequest,
|
||||
) -> ApiResult<String> {
|
||||
let workdir_id = request
|
||||
.backend_workdir_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| next_backend_workdir_id(&request.repository.id));
|
||||
request.backend_workdir_id = Some(workdir_id.clone());
|
||||
let timestamp = now_registry_timestamp();
|
||||
api.store.upsert_workdir_registry(&WorkdirRegistryRecord {
|
||||
workspace_id: api.config.workspace_id.clone(),
|
||||
workdir_id: workdir_id.clone(),
|
||||
runtime_id: runtime_id.to_string(),
|
||||
repository_id: request.repository.id.clone(),
|
||||
selector: request
|
||||
.repository
|
||||
.selector
|
||||
.as_ref()
|
||||
.map(|selector| selector.as_ref().to_string()),
|
||||
resolved_commit: None,
|
||||
materialization_status: "pending".to_string(),
|
||||
cleanliness: "clean".to_string(),
|
||||
management_kind: "backend_managed".to_string(),
|
||||
created_at: timestamp.clone(),
|
||||
updated_at: timestamp,
|
||||
})?;
|
||||
Ok(workdir_id)
|
||||
}
|
||||
|
||||
fn sync_runtime_workdir_observations(
|
||||
api: &WorkspaceApi,
|
||||
runtime_id: &str,
|
||||
) -> ApiResult<Vec<RuntimeDiagnostic>> {
|
||||
let response = api
|
||||
.runtime
|
||||
.list_working_directories(runtime_id)
|
||||
.map_err(|err| err.into_error())?;
|
||||
let mut observed = std::collections::BTreeSet::new();
|
||||
for status in &response.items {
|
||||
observed.insert(status.summary.working_directory_id.clone());
|
||||
let management_kind = api
|
||||
.store
|
||||
.get_workdir_registry(
|
||||
&api.config.workspace_id,
|
||||
&status.summary.working_directory_id,
|
||||
)?
|
||||
.map(|existing| existing.management_kind)
|
||||
.unwrap_or_else(|| "runtime_unmanaged".to_string());
|
||||
let record =
|
||||
workdir_record_from_summary(api, runtime_id, &status.summary, management_kind.as_str());
|
||||
api.store.upsert_workdir_registry(&record)?;
|
||||
}
|
||||
for mut record in api
|
||||
.store
|
||||
.list_workdir_registry(&api.config.workspace_id, 500)?
|
||||
.into_iter()
|
||||
.filter(|record| record.runtime_id == runtime_id && !observed.contains(&record.workdir_id))
|
||||
{
|
||||
if record.materialization_status == "present" {
|
||||
record.materialization_status = "missing".to_string();
|
||||
record.updated_at = now_registry_timestamp();
|
||||
api.store.upsert_workdir_registry(&record)?;
|
||||
}
|
||||
}
|
||||
Ok(response.diagnostics)
|
||||
}
|
||||
|
||||
fn sync_all_runtime_workdir_observations(api: &WorkspaceApi) -> Vec<RuntimeDiagnostic> {
|
||||
let mut diagnostics = Vec::new();
|
||||
let runtimes = api.runtime.list_runtimes(api.config.max_records.min(200));
|
||||
for runtime in runtimes.items {
|
||||
if runtime.capabilities.supports_worktrees {
|
||||
match sync_runtime_workdir_observations(api, runtime.runtime_id.as_str()) {
|
||||
Ok(mut runtime_diagnostics) => diagnostics.append(&mut runtime_diagnostics),
|
||||
Err(err) => diagnostics.extend(err.diagnostics),
|
||||
}
|
||||
}
|
||||
}
|
||||
diagnostics
|
||||
}
|
||||
|
||||
fn sync_linked_workdir_after_worker_stop(
|
||||
api: &WorkspaceApi,
|
||||
runtime_id: &str,
|
||||
worker_record: &WorkerRegistryRecord,
|
||||
) -> ApiResult<()> {
|
||||
let links = api
|
||||
.store
|
||||
.list_worker_workdir_links(&api.config.workspace_id, worker_record.worker_id.as_str())?;
|
||||
for link in links {
|
||||
let result = api
|
||||
.runtime
|
||||
.working_directory(runtime_id, link.workdir_id.as_str())
|
||||
.map_err(|err| err.into_error())?;
|
||||
if let Some(status) = result.working_directory {
|
||||
let management_kind = api
|
||||
.store
|
||||
.get_workdir_registry(&api.config.workspace_id, link.workdir_id.as_str())?
|
||||
.map(|record| record.management_kind)
|
||||
.unwrap_or_else(|| "runtime_unmanaged".to_string());
|
||||
let record = workdir_record_from_summary(
|
||||
api,
|
||||
runtime_id,
|
||||
&status.summary,
|
||||
management_kind.as_str(),
|
||||
);
|
||||
api.store.upsert_workdir_registry(&record)?;
|
||||
} else if let Some(mut record) = api
|
||||
.store
|
||||
.get_workdir_registry(&api.config.workspace_id, link.workdir_id.as_str())?
|
||||
{
|
||||
record.materialization_status = "missing".to_string();
|
||||
record.updated_at = now_registry_timestamp();
|
||||
api.store.upsert_workdir_registry(&record)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn workdir_record_from_summary(
|
||||
api: &WorkspaceApi,
|
||||
runtime_id: &str,
|
||||
|
|
@ -3367,6 +3649,7 @@ fn workdir_summary_from_record(record: &WorkdirRegistryRecord) -> WorkingDirecto
|
|||
}),
|
||||
cleanup_policy: Some("manual_or_worker_stop".to_string()),
|
||||
status,
|
||||
management_kind: Some(record.management_kind.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -3953,6 +4236,95 @@ mod tests {
|
|||
const TEST_REPOSITORY_ID: &str = "main";
|
||||
const TEST_CREATED_AT: &str = "2026-06-23T06:43:28Z";
|
||||
|
||||
#[test]
|
||||
fn backend_worker_projection_preserves_archive_rows_links_and_redacts_paths() {
|
||||
let worker = WorkerRegistryRecord {
|
||||
workspace_id: "workspace-1".to_string(),
|
||||
worker_id: "embedded/worker-1".to_string(),
|
||||
runtime_id: "embedded".to_string(),
|
||||
runtime_worker_id: "worker-1".to_string(),
|
||||
display_name: "Archived Worker".to_string(),
|
||||
profile: Some("builtin:coder".to_string()),
|
||||
lifecycle_state: "stopped".to_string(),
|
||||
retention_state: "pinned".to_string(),
|
||||
transcript_ref: Some("runtime://embedded/workers/worker-1/transcript".to_string()),
|
||||
session_ref: None,
|
||||
summary_ref: None,
|
||||
diagnostics_ref: None,
|
||||
created_at: "1".to_string(),
|
||||
updated_at: "2".to_string(),
|
||||
};
|
||||
let workdir = WorkdirRegistryRecord {
|
||||
workspace_id: "workspace-1".to_string(),
|
||||
workdir_id: "backend-1-repo".to_string(),
|
||||
runtime_id: "embedded".to_string(),
|
||||
repository_id: "repo".to_string(),
|
||||
selector: Some("develop".to_string()),
|
||||
resolved_commit: Some("abcdef".to_string()),
|
||||
materialization_status: "missing".to_string(),
|
||||
cleanliness: "clean".to_string(),
|
||||
management_kind: "backend_managed".to_string(),
|
||||
created_at: "1".to_string(),
|
||||
updated_at: "3".to_string(),
|
||||
};
|
||||
let link = WorkerWorkdirLinkRecord {
|
||||
workspace_id: "workspace-1".to_string(),
|
||||
worker_id: worker.worker_id.clone(),
|
||||
workdir_id: workdir.workdir_id.clone(),
|
||||
role: "primary_cwd".to_string(),
|
||||
linked_at: "4".to_string(),
|
||||
unlinked_at: None,
|
||||
};
|
||||
|
||||
let projected = merge_worker_registry_projection(None, &worker, vec![link], &[workdir]);
|
||||
|
||||
assert_eq!(projected.status, "stopped");
|
||||
assert_eq!(
|
||||
projected.working_directory.as_ref().unwrap().status,
|
||||
WorkingDirectoryStatusKind::Removed
|
||||
);
|
||||
assert_eq!(
|
||||
projected
|
||||
.working_directory
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.management_kind
|
||||
.as_deref(),
|
||||
Some("backend_managed")
|
||||
);
|
||||
let serialized = serde_json::to_string(&projected).unwrap();
|
||||
assert!(!serialized.contains("/tmp/"));
|
||||
assert!(!serialized.contains("materialized_path"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmanaged_runtime_workdir_projection_is_typed_and_diagnostic_safe() {
|
||||
let workdir = WorkdirRegistryRecord {
|
||||
workspace_id: "workspace-1".to_string(),
|
||||
workdir_id: "runtime-direct".to_string(),
|
||||
runtime_id: "embedded".to_string(),
|
||||
repository_id: "repo".to_string(),
|
||||
selector: None,
|
||||
resolved_commit: None,
|
||||
materialization_status: "present".to_string(),
|
||||
cleanliness: "unknown".to_string(),
|
||||
management_kind: "runtime_unmanaged".to_string(),
|
||||
created_at: "1".to_string(),
|
||||
updated_at: "2".to_string(),
|
||||
};
|
||||
|
||||
let projected = workdir_summary_from_record(&workdir);
|
||||
|
||||
assert_eq!(projected.status, WorkingDirectoryStatusKind::Active);
|
||||
assert_eq!(
|
||||
projected.management_kind.as_deref(),
|
||||
Some("runtime_unmanaged")
|
||||
);
|
||||
let serialized = serde_json::to_string(&projected).unwrap();
|
||||
assert!(!serialized.contains("/tmp/"));
|
||||
assert!(!serialized.contains("materialized_path"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn worker_profile_candidates_are_backend_published_and_mapped() {
|
||||
let candidates = worker_profile_candidates();
|
||||
|
|
|
|||
|
|
@ -125,6 +125,11 @@ pub trait ControlPlaneStore: Send + Sync {
|
|||
workspace_id: &str,
|
||||
workdir_id: &str,
|
||||
) -> Result<Option<WorkdirRegistryRecord>>;
|
||||
fn list_workdir_registry(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<WorkdirRegistryRecord>>;
|
||||
fn list_managed_workdir_registry(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
|
|
@ -234,7 +239,11 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||
display_name = excluded.display_name,
|
||||
profile = excluded.profile,
|
||||
lifecycle_state = excluded.lifecycle_state,
|
||||
retention_state = excluded.retention_state,
|
||||
retention_state = CASE
|
||||
WHEN worker_registry.retention_state = 'pinned' AND excluded.retention_state = 'normal'
|
||||
THEN worker_registry.retention_state
|
||||
ELSE excluded.retention_state
|
||||
END,
|
||||
transcript_ref = excluded.transcript_ref,
|
||||
session_ref = excluded.session_ref,
|
||||
summary_ref = excluded.summary_ref,
|
||||
|
|
@ -366,6 +375,25 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
|||
})
|
||||
}
|
||||
|
||||
fn list_workdir_registry(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
limit: usize,
|
||||
) -> Result<Vec<WorkdirRegistryRecord>> {
|
||||
self.with_conn(|conn| {
|
||||
let sql = workdir_registry_select_sql(
|
||||
"WHERE workspace_id = ?1 ORDER BY updated_at DESC LIMIT ?2",
|
||||
);
|
||||
let mut stmt = conn.prepare(sql.as_str())?;
|
||||
let rows = stmt.query_map(
|
||||
params![workspace_id, limit as i64],
|
||||
read_workdir_registry_record,
|
||||
)?;
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Error::from)
|
||||
})
|
||||
}
|
||||
|
||||
fn list_managed_workdir_registry(
|
||||
&self,
|
||||
workspace_id: &str,
|
||||
|
|
@ -1221,6 +1249,14 @@ mod tests {
|
|||
updated_at: "2".to_string(),
|
||||
};
|
||||
store.upsert_worker_registry(&worker).unwrap();
|
||||
let mut runtime_sync_worker = worker.clone();
|
||||
runtime_sync_worker.lifecycle_state = "running".to_string();
|
||||
runtime_sync_worker.retention_state = "normal".to_string();
|
||||
runtime_sync_worker.updated_at = "5".to_string();
|
||||
store.upsert_worker_registry(&runtime_sync_worker).unwrap();
|
||||
let mut expected_worker = worker.clone();
|
||||
expected_worker.lifecycle_state = "running".to_string();
|
||||
expected_worker.updated_at = "5".to_string();
|
||||
|
||||
let workdir = WorkdirRegistryRecord {
|
||||
workspace_id: "local-dev".to_string(),
|
||||
|
|
@ -1251,7 +1287,7 @@ mod tests {
|
|||
store
|
||||
.get_worker_registry_by_runtime("local-dev", "embedded", "browser-1")
|
||||
.unwrap(),
|
||||
Some(worker.clone())
|
||||
Some(expected_worker.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
store
|
||||
|
|
|
|||
|
|
@ -124,6 +124,7 @@ export type WorkingDirectorySummary = {
|
|||
resolved_commit: string;
|
||||
resolved_tree?: string | null;
|
||||
status: string;
|
||||
management_kind?: "backend_managed" | "runtime_unmanaged" | string | null;
|
||||
cleanup_policy: string;
|
||||
cleanup_target: {
|
||||
kind: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user