diff --git a/crates/worker/src/controller.rs b/crates/worker/src/controller.rs index 4834abd0..03a98e5e 100644 --- a/crates/worker/src/controller.rs +++ b/crates/worker/src/controller.rs @@ -1020,11 +1020,10 @@ where worker.manifest(), worker.workspace_client_handle(), worker.prompts().load_full(), - ) - .await?; + )?; let memory_prompt_contribution = memory_install_plan.as_ref().map(|plan| { ( - plan.resident_summary.clone(), + plan.resident_summary_source.clone(), plan.system_prompt_override.clone(), ) }); diff --git a/crates/worker/src/feature/builtin/memory.rs b/crates/worker/src/feature/builtin/memory.rs index 33c5eff3..ea4c68b1 100644 --- a/crates/worker/src/feature/builtin/memory.rs +++ b/crates/worker/src/feature/builtin/memory.rs @@ -23,7 +23,8 @@ use crate::feature::{ ToolDeclaration, }; use crate::worker::{ - WorkspaceClient, WorkspaceClientError, WorkspaceRequest, WorkspaceRequestMethod, + SystemPromptContributionSource, WorkspaceClient, WorkspaceClientError, WorkspaceRequest, + WorkspaceRequestMethod, }; #[derive(Clone, Debug)] @@ -342,15 +343,44 @@ fn query_schema() -> serde_json::Value { }) } -pub struct MemoryFeatureInstallPlan { - pub module: MemoryToolsFeature, - pub resident_summary: Option, - pub system_prompt_override: Option, +struct WorkspaceResidentSummarySource { + client: Arc, +} + +#[async_trait] +impl SystemPromptContributionSource for WorkspaceResidentSummarySource { + async fn load(&self) -> Option { + match self + .client + .execute_memory_backend_operation( + memory::backend::MemoryBackendOperation::ResidentSummary( + memory::backend::MemoryResidentSummaryOperation::default(), + ), + ) + .await + { + Ok(memory::backend::MemoryBackendOperationResult::ToolOutput(output)) => output.content, + Ok(other) => { + tracing::debug!(?other, "unexpected resident Memory Backend result"); + None + } + Err(error) => { + tracing::debug!(%error, "resident Memory summary unavailable"); + None + } + } + } +} + +pub(crate) struct MemoryFeatureInstallPlan { + pub(crate) module: MemoryToolsFeature, + pub(crate) resident_summary_source: Option>, + pub(crate) system_prompt_override: Option, pub(crate) resolved_config: manifest::ResolvedMemoryFeatureConfig, } impl MemoryFeatureInstallPlan { - pub async fn prepare( + pub fn prepare( manifest: &manifest::WorkerManifest, client: Arc, prompts: Arc, @@ -361,10 +391,9 @@ impl MemoryFeatureInstallPlan { prompts, manifest.profile.clone(), ) - .await } - async fn prepare_resolved( + fn prepare_resolved( config: manifest::ResolvedMemoryFeatureConfig, client: Arc, prompts: Arc, @@ -405,30 +434,11 @@ impl MemoryFeatureInstallPlan { )); } - let resident_summary = if config.profile.resident.inject_summary { - match client - .execute_memory_backend_operation( - memory::backend::MemoryBackendOperation::ResidentSummary( - memory::backend::MemoryResidentSummaryOperation::default(), - ), - ) - .await - { - Ok(memory::backend::MemoryBackendOperationResult::ToolOutput(output)) => { - output.content - } - Ok(other) => { - tracing::debug!(?other, "unexpected resident Memory Backend result"); - None - } - Err(error) => { - tracing::debug!(%error, "resident Memory summary unavailable"); - None - } - } - } else { - None - }; + let resident_summary_source = config.profile.resident.inject_summary.then(|| { + Arc::new(WorkspaceResidentSummarySource { + client: Arc::clone(&client), + }) as Arc + }); let system_prompt_override = if memory_consolidation_worker { let language = settings.language; Some( @@ -442,7 +452,7 @@ impl MemoryFeatureInstallPlan { Ok(Some(Self { module: MemoryToolsFeature::new(client, config.profile.staging_tools), - resident_summary, + resident_summary_source, system_prompt_override, resolved_config: config, })) @@ -559,7 +569,6 @@ mod tests { prompts.clone(), None, ) - .await .unwrap(); assert!(disabled.is_none()); @@ -573,7 +582,6 @@ mod tests { prompts.clone(), None, ) - .await .is_err() ); enabled @@ -592,7 +600,6 @@ mod tests { prompts.clone(), None, ) - .await .is_err() ); let plan = MemoryFeatureInstallPlan::prepare_resolved( @@ -601,10 +608,9 @@ mod tests { prompts.clone(), None, ) - .await .unwrap() .unwrap(); - assert!(plan.resident_summary.is_none()); + assert!(plan.resident_summary_source.is_none()); assert!(plan.system_prompt_override.is_none()); enabled.profile.resident.inject_summary = true; @@ -614,14 +620,20 @@ mod tests { prompts, None, ) - .await .unwrap() .unwrap(); - assert_eq!(plan.resident_summary.as_deref(), Some("# Durable Memory")); + assert_eq!( + plan.resident_summary_source + .unwrap() + .load() + .await + .as_deref(), + Some("# Durable Memory") + ); } #[tokio::test] - async fn memory_prompt_contribution_rereads_resident_summary_for_each_install() { + async fn memory_prompt_contribution_defers_resident_summary_until_loaded() { let prompts = crate::prompt::catalog::PromptCatalog::builtins_only().unwrap(); let mut config = manifest::ResolvedMemoryFeatureConfig::default(); config.profile.enabled = true; @@ -639,7 +651,6 @@ mod tests { prompts.clone(), None, ) - .await .unwrap() .unwrap(); let restored = MemoryFeatureInstallPlan::prepare_resolved( @@ -648,16 +659,25 @@ mod tests { prompts, None, ) - .await .unwrap() .unwrap(); assert_eq!( - first.resident_summary.as_deref(), + first + .resident_summary_source + .unwrap() + .load() + .await + .as_deref(), Some("first resident summary") ); assert_eq!( - restored.resident_summary.as_deref(), + restored + .resident_summary_source + .unwrap() + .load() + .await + .as_deref(), Some("updated resident summary") ); } diff --git a/crates/worker/src/feature/builtin/resource_projection.rs b/crates/worker/src/feature/builtin/resource_projection.rs index 56fb9a53..199837f9 100644 --- a/crates/worker/src/feature/builtin/resource_projection.rs +++ b/crates/worker/src/feature/builtin/resource_projection.rs @@ -124,7 +124,6 @@ struct ModelBlocker { ticket: String, kind: String, state: Option, - resolved: bool, } #[derive(Debug, Serialize)] @@ -436,7 +435,6 @@ fn project_blocker(value: &Value) -> Result { ticket: resource_ref(blocker, "blocking_resource_key", "T-")?, kind: string_field(blocker, "relation_kind")?, state: optional_string(blocker, "blocking_state")?, - resolved: bool_field(blocker, "resolved")?, }) } @@ -760,6 +758,70 @@ mod tests { assert!(!objective_json.contains("00001TICKETINTERNAL")); } + #[test] + fn ticket_detail_projection_accepts_current_blocker_shape() { + let projected = project_ticket_detail(json!({ + "id": "internal-ticket", + "resource_key": "T-588", + "title": "Queued Submit", + "body": "Body", + "state": "planning", + "readiness": null, + "priority": "P2", + "created_at": "2026-09-03T00:00:00Z", + "updated_at": "2026-09-03T00:00:00Z", + "events": [], + "relations": { + "outgoing": [], + "incoming": [], + "blockers": [{ + "blocking_ticket": "internal-blocker", + "blocking_resource_key": "T-584", + "reason_kind": "depends_on", + "relation_kind": "depends_on", + "note": "required foundation", + "blocking_state": "planning" + }], + "notices": [] + }, + "linked_objectives": [], + "implementation_reports": [], + "assignments": [], + "current_coder": null, + "merge_request": null, + "evidence": { + "has_merge_request": false, + "has_current_subject_ref": false, + "has_review_request": false, + "has_commit": false, + "review_status": null, + "approved_current_subject": false, + "unresolved_request_changes": false, + "complete_for_integration": false, + "missing": ["merge_request"] + }, + "action_eligibility": { + "can_assign_orchestrator": true, + "can_unassign_orchestrator": false, + "can_queue": false, + "can_start_manual_coder": false + }, + "event_page": {"next_cursor": null, "has_more": false} + })) + .expect("current Ticket blocker shape must project"); + + let projected = serde_json::to_value(projected).expect("serialize Ticket detail"); + assert_eq!( + projected["relations"]["blockers"], + json!([{ + "ticket": "T-584", + "kind": "depends_on", + "state": "planning" + }]) + ); + assert!(!projected.to_string().contains("internal-blocker")); + } + #[test] fn relation_projection_accepts_current_workspace_api_shapes() { let outgoing = project_relation( diff --git a/crates/worker/src/spawn/tool.rs b/crates/worker/src/spawn/tool.rs index 7fab828e..b071ee29 100644 --- a/crates/worker/src/spawn/tool.rs +++ b/crates/worker/src/spawn/tool.rs @@ -384,14 +384,21 @@ impl Tool for SubWorkerSpawnTool { child_bash_output_dir.display() )) })?; - workdir_rules.push(WorkdirDelegationRule { - target: WorkdirPath::new_scoped(child_bash_output_dir.to_string_lossy()) - .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?, - permission: WorkdirDelegationPermission::Read, - recursive: true, - }); let source_workdir_session = require_active_workdir_session(self.source_workdir_session.as_ref())?; + let transports_delegation_context = source_workdir_session.transports_delegation_context(); + // Provider-transported sessions resolve every delegation rule in the + // receiving Workdir namespace. The Bash spill directory instead belongs + // to this Worker host, so forwarding it would widen the request with a + // foreign absolute path and fail the provider's existing scope check. + if !transports_delegation_context { + workdir_rules.push(WorkdirDelegationRule { + target: WorkdirPath::new_scoped(child_bash_output_dir.to_string_lossy()) + .map_err(|error| ToolError::ExecutionFailed(error.to_string()))?, + permission: WorkdirDelegationPermission::Read, + recursive: true, + }); + } let delegation_request = workdir_delegation_request(input.cwd.as_deref(), workdir_rules)?; let workdir_delegation = source_workdir_session .delegate(delegation_request) @@ -1016,10 +1023,12 @@ mod tests { use super::*; use manifest::{DelegationScope, Permission, Scope, SharedScope}; use std::pin::Pin; + use std::sync::Mutex; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::time::Duration; use crate::WorkspaceId; + use crate::feature::builtin::manage_workdir::WorkspaceAttachedWorkdirSession; use agen::llm_client::event::{Event as LlmEvent, ResponseStatus, StatusEvent}; use agen::llm_client::{ClientError, LlmClient, Request}; use async_trait::async_trait; @@ -1242,6 +1251,15 @@ enabled = false let record = registry .get_internal("reviewer-child") .expect("Internal reviewer registry record"); + let child_bash_output_dir = bash_output_dir.join("sub-workers").join("reviewer-child"); + record + .workdir_delegation + .scoped_session + .stat(workdir::StatRequest { + path: WorkdirPath::new_scoped(child_bash_output_dir.to_string_lossy()).unwrap(), + }) + .await + .expect("local child retains read scope for its Bash output directory"); for required in ["Read", "Write", "Edit", "Glob", "Grep", "Bash"] { assert!( record.installed_tools.iter().any(|name| name == required), @@ -1389,6 +1407,130 @@ enabled = false assert!(spawner_scope.snapshot().is_writable(&workspace_root)); } + #[tokio::test] + async fn remote_subworker_spawn_keeps_worker_host_output_path_out_of_workdir_scope() { + let runtime = TempDir::new().unwrap(); + let workspace_root = runtime.path().join("worker-host/project"); + let bash_output_dir = runtime.path().join("worker-host/bash-output"); + std::fs::create_dir_all(&workspace_root).unwrap(); + std::fs::create_dir_all(&bash_output_dir).unwrap(); + + let mut manifest = parent_manifest(&workspace_root, None); + manifest + .scope + .allow + .push(abs_rule(&bash_output_dir, Permission::Read)); + manifest.delegation_scope = ScopeConfig { + allow: vec![abs_rule(&workspace_root, Permission::Write)], + deny: Vec::new(), + }; + let spawner_scope = SharedScope::new(Scope::from_config(&manifest.scope).unwrap()); + let registry = SpawnedWorkerRegistry::new_internal("parent".into(), spawner_scope.clone()); + let workspace_context = crate::worker::WorkerWorkspaceContext::with_client( + Some(WorkspaceId::new("workspace-test").unwrap()), + Arc::new(AvailableWorkspaceClient), + ); + let remote_client = Arc::new(StrictRemoteWorkdirWorkspaceClient::default()); + let source_workdir_session = workdir::delegation_capable_session( + WorkspaceAttachedWorkdirSession::handle(remote_client.clone()), + ); + let calls = Arc::new(AtomicUsize::new(0)); + let (parent_method_tx, _parent_method_rx) = mpsc::channel(8); + let tool = SubWorkerSpawnTool::new( + "parent".into(), + workspace_context, + ParentNotificationTarget::Controller(parent_method_tx.downgrade()), + runtime.path().to_path_buf(), + bash_output_dir.clone(), + workspace_root.clone(), + Some(source_workdir_session), + registry.clone(), + manifest, + PromptCatalogSource::builtins_only(), + AvailableProfiles::discover(&workspace_root), + ) + .with_internal_client(Box::new(ScriptedInternalClient { + calls: calls.clone(), + parent_scope: spawner_scope, + delegated_path: workspace_root.clone(), + observed_parent_write_revoked: Arc::new(AtomicBool::new(false)), + observed_instruction_override: Arc::new(AtomicBool::new(false)), + fail_requests: Arc::new(AtomicBool::new(false)), + })); + + tool.execute( + &serde_json::json!({ + "name": "remote-child", + "profile": "inherit", + "instruction": "role.reviewer", + "task": "inspect the remote Workdir", + "scope": [{ + "target": ".", + "permission": "write", + "recursive": true + }] + }) + .to_string(), + agen::tool::ToolExecutionContext::direct(), + ) + .await + .expect("remote Workdir delegation must not receive Worker-host paths"); + + let record = registry + .get_internal("remote-child") + .expect("remote Internal Worker registry record"); + assert_eq!( + record.session.wait_until_idle().await, + crate::internal_worker::InternalWorkerSessionStatus::Idle + ); + assert_eq!(calls.load(Ordering::SeqCst), 1); + assert_eq!( + remote_client + .foreign_scope_rejections + .load(Ordering::SeqCst), + 0 + ); + let child_bash_output_dir = bash_output_dir.join("sub-workers").join("remote-child"); + assert!(child_bash_output_dir.is_dir()); + for required in ["Read", "Write", "Edit", "Glob", "Grep", "Bash"] { + assert!( + record.installed_tools.iter().any(|name| name == required), + "remote write-scoped child is missing {required}: {:?}", + record.installed_tools + ); + } + + let remote_requests = remote_client.requests(); + let operate_requests = remote_requests + .iter() + .filter(|request| request.body.is_some()) + .collect::>(); + assert_eq!( + operate_requests.len(), + 1, + "remote requests: {remote_requests:?}" + ); + let operation_body: serde_json::Value = serde_json::from_str( + operate_requests[0] + .body + .as_deref() + .expect("remote operation body"), + ) + .unwrap(); + let rules = operation_body["delegations"][0]["rules"] + .as_array() + .expect("delegation rules"); + assert_eq!(rules.len(), 1, "remote operation body: {operation_body}"); + assert_eq!(rules[0]["target"], ""); + assert!( + !operation_body.to_string().contains( + child_bash_output_dir + .to_str() + .expect("UTF-8 test output directory") + ) + ); + } + #[test] fn spawn_worker_input_schema_includes_optional_cwd() { let schema = serde_json::to_value(schemars::schema_for!(SubWorkerSpawnInput)).unwrap(); @@ -1523,6 +1665,97 @@ enabled = false } } + #[derive(Debug, Default)] + struct StrictRemoteWorkdirWorkspaceClient { + requests: Mutex>, + foreign_scope_rejections: AtomicUsize, + } + + impl StrictRemoteWorkdirWorkspaceClient { + fn requests(&self) -> Vec { + self.requests + .lock() + .expect("remote Workdir request lock") + .clone() + } + } + + impl WorkspaceClient for StrictRemoteWorkdirWorkspaceClient { + fn workspace_id(&self) -> Option<&str> { + Some("workspace-test") + } + + fn kind(&self) -> &str { + "strict-remote-workdir-test" + } + + fn is_available(&self) -> bool { + true + } + + fn execute( + &self, + request: WorkspaceRequest, + ) -> Result { + self.requests + .lock() + .expect("remote Workdir request lock") + .push(request.clone()); + if request.path.ends_with("/fence") { + return Ok(WorkspaceResponse { + status: 200, + body: serde_json::json!({ "value": "remote-fence-1" }).to_string(), + }); + } + + let body: serde_json::Value = serde_json::from_str( + request + .body + .as_deref() + .ok_or_else(|| WorkspaceClientError::Request("missing request body".into()))?, + ) + .map_err(|error| WorkspaceClientError::Request(error.to_string()))?; + let has_foreign_scope = body + .get("delegations") + .and_then(serde_json::Value::as_array) + .into_iter() + .flatten() + .flat_map(|delegation| { + delegation + .get("rules") + .and_then(serde_json::Value::as_array) + .into_iter() + .flatten() + }) + .filter_map(|rule| rule.get("target").and_then(serde_json::Value::as_str)) + .any(|target| Path::new(target).is_absolute()); + if has_foreign_scope { + self.foreign_scope_rejections.fetch_add(1, Ordering::SeqCst); + return Ok(WorkspaceResponse { + status: 403, + body: serde_json::json!({ + "code": "out_of_scope", + "message": "Worker-host path is outside the remote Workdir namespace" + }) + .to_string(), + }); + } + + Ok(WorkspaceResponse { + status: 200, + body: serde_json::json!({ + "operation": "stat", + "result": { + "path": "", + "kind": "directory", + "size": 0 + } + }) + .to_string(), + }) + } + } + fn parent_manifest(root: &Path, deny: Option<&Path>) -> WorkerManifest { WorkerManifestConfig { worker: WorkerMetaConfig { diff --git a/crates/worker/src/worker.rs b/crates/worker/src/worker.rs index 3bfb9230..3275aab0 100644 --- a/crates/worker/src/worker.rs +++ b/crates/worker/src/worker.rs @@ -1765,6 +1765,11 @@ impl WorkerSession { } } +#[async_trait::async_trait] +pub(crate) trait SystemPromptContributionSource: Send + Sync { + async fn load(&self) -> Option; +} + /// An independent agent execution unit. /// /// Holds a [`Engine`] directly and persists session state via @@ -1908,8 +1913,8 @@ pub struct Worker { prompts: Arc>, /// Test/internal policy gate for installed resident prompt contributions. inject_resident_summary: bool, - /// Materialized resident prompt context installed by an enabled Feature. - feature_resident_summary: Option, + /// Deferred resident prompt source installed by an enabled Feature. + feature_resident_summary_source: Option>, /// Complete system prompt replacement installed by an enabled Feature. feature_system_prompt_override: Option, /// Typed user submissions in submit order. K-th entry corresponds to @@ -2145,7 +2150,7 @@ impl Worker { runtime_ticket_role: None, prompts, inject_resident_summary: true, - feature_resident_summary: None, + feature_resident_summary_source: None, feature_system_prompt_override: None, user_segments: Vec::new(), sink: SegmentLogSink::new(), @@ -2191,10 +2196,10 @@ impl Worker { pub(crate) fn install_system_prompt_contribution( &mut self, - resident_summary: Option, + resident_summary_source: Option>, system_prompt_override: Option, ) { - self.feature_resident_summary = resident_summary; + self.feature_resident_summary_source = resident_summary_source; self.feature_system_prompt_override = system_prompt_override; } @@ -3206,10 +3211,14 @@ impl Worker { } } } - let resident_summary = self - .inject_resident_summary - .then(|| self.feature_resident_summary.clone()) - .flatten(); + let resident_summary = if self.inject_resident_summary { + match &self.feature_resident_summary_source { + Some(source) => source.load().await, + None => None, + } + } else { + None + }; let worker_language = worker_language(&self.manifest.engine); let scope_snapshot = self.scope.snapshot(); let cwd_for_prompt = self @@ -5394,7 +5403,7 @@ where runtime_ticket_role: None, prompts: common.prompts, inject_resident_summary: true, - feature_resident_summary: None, + feature_resident_summary_source: None, feature_system_prompt_override: None, user_segments: Vec::new(), sink: SegmentLogSink::new(), @@ -5478,7 +5487,7 @@ where runtime_ticket_role: None, prompts: common.prompts, inject_resident_summary: true, - feature_resident_summary: None, + feature_resident_summary_source: None, feature_system_prompt_override: None, user_segments: Vec::new(), sink: SegmentLogSink::new(), @@ -5596,7 +5605,7 @@ where runtime_ticket_role: None, prompts: common.prompts, inject_resident_summary: true, - feature_resident_summary: None, + feature_resident_summary_source: None, feature_system_prompt_override: None, user_segments: Vec::new(), sink: SegmentLogSink::new(), @@ -5971,7 +5980,7 @@ where runtime_ticket_role: None, prompts: common.prompts, inject_resident_summary: true, - feature_resident_summary: None, + feature_resident_summary_source: None, feature_system_prompt_override: None, user_segments: state.user_segments, // Seed the mirror with the entries we just replayed so a @@ -7345,6 +7354,32 @@ permission = "read" mod build_summary_prompt_tests { use super::*; + struct TestSystemPromptContributionSource { + value: Option, + load_count: Arc, + } + + #[async_trait::async_trait] + impl SystemPromptContributionSource for TestSystemPromptContributionSource { + async fn load(&self) -> Option { + self.load_count.fetch_add(1, Ordering::SeqCst); + self.value.clone() + } + } + + fn test_system_prompt_contribution_source( + value: Option, + ) -> (Arc, Arc) { + let load_count = Arc::new(AtomicUsize::new(0)); + ( + Arc::new(TestSystemPromptContributionSource { + value, + load_count: Arc::clone(&load_count), + }), + load_count, + ) + } + fn test_summary_input(items: &[Item]) -> String { build_summary_input( items, @@ -8801,6 +8836,32 @@ mod build_summary_prompt_tests { } } + #[tokio::test] + async fn worker_without_initial_system_prompt_does_not_load_feature_contribution() { + let dir = tempfile::tempdir().unwrap(); + let cwd = dir.path().join("workspace"); + std::fs::create_dir_all(&cwd).unwrap(); + let store = session_store::FsStore::new(dir.path().join("sessions")).unwrap(); + let mut worker = Worker::new( + minimal_manifest(), + Engine::<_, Mutable, SessionHistoryMetadata>::new_annotated(NoopClient), + store, + WorkerWorkspaceContext::no_workspace(), + WorkerFilesystemAuthority::local(cwd.clone(), cwd.clone()), + Scope::writable(&cwd).unwrap(), + ) + .await + .unwrap(); + let (source, load_count) = + test_system_prompt_contribution_source(Some("# Durable Memory".to_string())); + worker.install_system_prompt_contribution(Some(source), None); + + worker.ensure_system_prompt_materialized().await.unwrap(); + + assert_eq!(load_count.load(Ordering::SeqCst), 0); + assert!(worker.history().is_empty()); + } + #[tokio::test] async fn memory_consolidation_prompt_uses_bound_workspace_language() { let dir = tempfile::tempdir().unwrap(); @@ -8899,16 +8960,17 @@ mod build_summary_prompt_tests { .await .unwrap(); worker.set_resident_memory_injection(gates.summary); - let resident_summary = if memory_config + let resident_summary_source = if memory_config .as_ref() .is_some_and(|cfg| cfg.profile.resident.inject_summary) && gates.summary { - summary_doc.and_then(summary_content_for_backend) + let summary = summary_doc.and_then(summary_content_for_backend); + Some(test_system_prompt_contribution_source(summary).0) } else { None }; - worker.install_system_prompt_contribution(resident_summary, None); + worker.install_system_prompt_contribution(resident_summary_source, None); let template = SystemPromptTemplate::parse( "default", crate::prompt::source::PromptCatalogSource::builtins_only(), diff --git a/crates/worker/tests/controller_test.rs b/crates/worker/tests/controller_test.rs index 28fc96e2..3f6438fe 100644 --- a/crates/worker/tests/controller_test.rs +++ b/crates/worker/tests/controller_test.rs @@ -813,13 +813,25 @@ permission = "write" #[tokio::test] async fn builtin_orchestrator_exposes_worker_remove_and_workdir_delete() { let workspace = tempfile::tempdir().unwrap(); - let resolved = ProfileResolver::new() + let mut resolved = ProfileResolver::new() .with_workspace_base(workspace.path()) .resolve( &ProfileSelector::source_named(ProfileRegistrySource::Builtin, "orchestrator"), ProfileResolveOptions::with_worker_name("orchestrator-worker"), ) .unwrap(); + if resolved.manifest.feature.memory.enabled() { + resolved + .manifest + .feature + .memory + .bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot { + workspace_id: "workspace-test".to_string(), + settings_revision: 1, + language: "English".to_string(), + }) + .unwrap(); + } let workspace_context = WorkerWorkspaceContext::with_client(None, Arc::new(NoopWorkspaceClient)); let client = MockClient::new(simple_text_events()); diff --git a/crates/workspace-server/src/config_source.rs b/crates/workspace-server/src/config_source.rs index d1dbc986..3d3ec807 100644 --- a/crates/workspace-server/src/config_source.rs +++ b/crates/workspace-server/src/config_source.rs @@ -101,7 +101,6 @@ fn main_config_contract_with_schema( } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)] -#[ts(export)] pub struct WorkspaceConfigState { pub snapshot: ConfigTreeSnapshot, pub contract: ToolchainContract, @@ -118,7 +117,6 @@ pub struct EvaluatedConfigCandidate { } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ts_rs::TS)] -#[ts(export)] pub struct ConfigCommitRequest { #[ts(type = "number")] pub base_revision: u64, diff --git a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts index 92d9531e..30e64ccd 100644 --- a/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts +++ b/web/workspace/src/lib/workspace/console/worker-console.ui.test.ts @@ -567,7 +567,7 @@ Deno.test("Worker Console composer keeps a compact bounded chip editor", async ( ); assert( consolePage.includes("') && + consolePage.includes('class="composer-input-shell"') && !consolePage.includes("handleComposerShellClick") && consolePage.includes("bind:this={composerInputElement}") && consolePage.includes("onchange={handleComposerChange}") && @@ -616,7 +616,7 @@ Deno.test("Worker Console paste chips preserve typed draft and target authority" consolePage.includes("if (!composerEditable) return") && composerInput.includes('chip.setAttribute("aria-label", label)') && composerInput.includes("preserveExactText = false") && - consolePage.includes("buildComposerSegmentsRequest(value.segments, {") && + consolePage.includes("const command = buildComposerSegmentsRequest(") && consolePage.includes("preserveExactText: value.textPastes.length > 0") && consolePage.includes("composerDrafts.set(activeComposerTargetKey") && consolePage.includes("switchComposerTarget(target)") &&