fix: restore pending workspace prompt catalog
This commit is contained in:
@@ -412,6 +412,48 @@ impl ProfileRuntimeWorkerFactory {
|
|||||||
.map_err(|err| format!("failed to build restore fallback manifest: {err}"))?;
|
.map_err(|err| format!("failed to build restore fallback manifest: {err}"))?;
|
||||||
Ok((manifest, PromptCatalogSource::builtins_only()))
|
Ok((manifest, PromptCatalogSource::builtins_only()))
|
||||||
}
|
}
|
||||||
|
fn observe_bundle_prompt_projection(
|
||||||
|
&self,
|
||||||
|
bundle: &crate::config_bundle::ConfigBundle,
|
||||||
|
expected_workspace_id: Option<&str>,
|
||||||
|
) -> Result<Option<Arc<worker::WorkspacePromptCatalogResolution>>, String> {
|
||||||
|
let Some(prompt_catalog) = bundle.prompt_catalog.clone() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
if let Some(expected_workspace_id) = expected_workspace_id
|
||||||
|
&& bundle.metadata.workspace_id != expected_workspace_id
|
||||||
|
{
|
||||||
|
return Err(format!(
|
||||||
|
"Workspace Prompt projection scope mismatch: expected {expected_workspace_id}, got {}",
|
||||||
|
bundle.metadata.workspace_id
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let source_digest = if prompt_catalog.source_digest.is_empty() {
|
||||||
|
bundle
|
||||||
|
.metadata
|
||||||
|
.provenance
|
||||||
|
.detail
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|detail| {
|
||||||
|
detail
|
||||||
|
.split(';')
|
||||||
|
.find_map(|part| part.strip_prefix("source_tree_digest="))
|
||||||
|
})
|
||||||
|
.unwrap_or(&prompt_catalog.catalog_digest)
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
prompt_catalog.source_digest.clone()
|
||||||
|
};
|
||||||
|
let projection = worker::WorkspacePromptProjection::new(
|
||||||
|
bundle.metadata.workspace_id.clone(),
|
||||||
|
source_digest,
|
||||||
|
prompt_catalog.catalog_digest.clone(),
|
||||||
|
prompt_catalog,
|
||||||
|
)
|
||||||
|
.map_err(|error| error.to_string())?;
|
||||||
|
self.prompt_projection_cache.observe(projection).map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
async fn resolve_profile_source_archive(
|
async fn resolve_profile_source_archive(
|
||||||
&self,
|
&self,
|
||||||
source: &ProfileSourceArchiveSource,
|
source: &ProfileSourceArchiveSource,
|
||||||
@@ -672,34 +714,11 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
|||||||
)?
|
)?
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if let Some(bundle) = request.config_bundle.as_ref() {
|
if let Some(bundle) = request.config_bundle.as_ref()
|
||||||
if let Some(prompt_catalog) = bundle.prompt_catalog.clone() {
|
&& let Some(resolution) =
|
||||||
let source_digest = if prompt_catalog.source_digest.is_empty() {
|
self.observe_bundle_prompt_projection(bundle, observation_workspace_id.as_deref())?
|
||||||
bundle
|
{
|
||||||
.metadata
|
loader = loader.with_effective_catalog(resolution.projection.catalog.clone());
|
||||||
.provenance
|
|
||||||
.detail
|
|
||||||
.as_deref()
|
|
||||||
.and_then(|detail| {
|
|
||||||
detail
|
|
||||||
.split(';')
|
|
||||||
.find_map(|part| part.strip_prefix("source_tree_digest="))
|
|
||||||
})
|
|
||||||
.unwrap_or(&prompt_catalog.catalog_digest)
|
|
||||||
.to_string()
|
|
||||||
} else {
|
|
||||||
prompt_catalog.source_digest.clone()
|
|
||||||
};
|
|
||||||
let projection = worker::WorkspacePromptProjection::new(
|
|
||||||
bundle.metadata.workspace_id.clone(),
|
|
||||||
source_digest,
|
|
||||||
prompt_catalog.catalog_digest.clone(),
|
|
||||||
prompt_catalog,
|
|
||||||
)
|
|
||||||
.map_err(|error| error.to_string())?;
|
|
||||||
let projection = self.prompt_projection_cache.observe(projection)?;
|
|
||||||
loader = loader.with_effective_catalog(projection.projection.catalog.clone());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let flow_transition_enabled = manifest.feature.flow.enabled;
|
let flow_transition_enabled = manifest.feature.flow.enabled;
|
||||||
|
|
||||||
@@ -869,23 +888,34 @@ impl RuntimeWorkerFactory for ProfileRuntimeWorkerFactory {
|
|||||||
.await
|
.await
|
||||||
{
|
{
|
||||||
Ok(worker) => worker,
|
Ok(worker) => worker,
|
||||||
Err(WorkerError::WorkerMetadataPending { .. })
|
|
||||||
if workspace_context.workspace_id().is_some()
|
|
||||||
&& request.config_bundle.is_none() =>
|
|
||||||
{
|
|
||||||
return Err(
|
|
||||||
"pending Workspace Worker restore requires operation-owned launch material; generic restore must not reconstruct it from current Workspace config"
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Err(WorkerError::WorkerMetadataPending { .. })
|
Err(WorkerError::WorkerMetadataPending { .. })
|
||||||
if request.request.initial_input.is_none() =>
|
if request.request.initial_input.is_none() =>
|
||||||
{
|
{
|
||||||
|
let pending_loader = if workspace_context.workspace_id().is_some() {
|
||||||
|
let bundle = request.config_bundle.as_ref().ok_or_else(|| {
|
||||||
|
"pending Workspace Worker restore requires operation-owned launch material; generic restore must not reconstruct it from current Workspace config"
|
||||||
|
.to_string()
|
||||||
|
})?;
|
||||||
|
let resolution = self
|
||||||
|
.observe_bundle_prompt_projection(
|
||||||
|
bundle,
|
||||||
|
observation_workspace_id.as_deref(),
|
||||||
|
)?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
"pending Workspace Worker restore requires a saved Workspace Prompt projection"
|
||||||
|
.to_string()
|
||||||
|
})?;
|
||||||
|
loader
|
||||||
|
.clone()
|
||||||
|
.with_effective_catalog(resolution.projection.catalog.clone())
|
||||||
|
} else {
|
||||||
|
loader.clone()
|
||||||
|
};
|
||||||
Worker::restore_pending_from_worker_metadata_with_context(
|
Worker::restore_pending_from_worker_metadata_with_context(
|
||||||
&worker_name,
|
&worker_name,
|
||||||
manifest.clone(),
|
manifest.clone(),
|
||||||
store,
|
store,
|
||||||
loader,
|
pending_loader,
|
||||||
workspace_context,
|
workspace_context,
|
||||||
filesystem_authority,
|
filesystem_authority,
|
||||||
)
|
)
|
||||||
@@ -2694,6 +2724,42 @@ mod tests {
|
|||||||
.expect("embedded archive should resolve without Backend resource client");
|
.expect("embedded archive should resolve without Backend resource client");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pending_restore_launch_material_preserves_workspace_prompt_catalog() {
|
||||||
|
let root = tempfile::tempdir().unwrap();
|
||||||
|
let factory = ProfileRuntimeWorkerFactory::new(root.path());
|
||||||
|
let builtins = worker::PromptCatalog::builtins_only().unwrap();
|
||||||
|
let projection = builtins.projection();
|
||||||
|
let mut templates = projection.templates.clone();
|
||||||
|
templates.insert(
|
||||||
|
"internal.notify_wrapper".to_string(),
|
||||||
|
"PENDING-LAUNCH {{ message }}".to_string(),
|
||||||
|
);
|
||||||
|
let mut effective = worker::EffectivePromptCatalog::new(
|
||||||
|
templates,
|
||||||
|
7,
|
||||||
|
projection.schema_fingerprint.clone(),
|
||||||
|
projection.toolchain_fingerprint.clone(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
effective.source_digest = "source-7".to_string();
|
||||||
|
let mut bundle = test_bundle();
|
||||||
|
bundle.metadata.workspace_id = "workspace-restore".to_string();
|
||||||
|
bundle.prompt_catalog = Some(effective);
|
||||||
|
bundle = bundle.with_computed_digest();
|
||||||
|
|
||||||
|
let resolution = factory
|
||||||
|
.observe_bundle_prompt_projection(&bundle, Some("workspace-restore"))
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(resolution.projection.config_revision, 7);
|
||||||
|
assert_eq!(
|
||||||
|
resolution.catalog.notify_wrapper("restored").unwrap(),
|
||||||
|
"PENDING-LAUNCH restored"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
#[serial_test::serial(worker_allocation)]
|
#[serial_test::serial(worker_allocation)]
|
||||||
async fn restore_pending_workspace_worker_without_system_prompt_fails_closed() {
|
async fn restore_pending_workspace_worker_without_system_prompt_fails_closed() {
|
||||||
|
|||||||
Reference in New Issue
Block a user