test: cover subjektiv lifecycle boundaries

This commit is contained in:
2026-10-02 06:18:02 +09:00
parent 9a7bfba507
commit 6e293d57cd
2 changed files with 119 additions and 15 deletions
+35 -10
View File
@@ -1141,6 +1141,23 @@ pub(crate) fn wire_event_bridges_on_engine<C, St>(
// per-item commit channel is wired at the top of this function.
}
fn validate_automatic_memory_extraction_targets(
memory: &manifest::MemoryFeatureProfileConfig,
subjektiv: &manifest::SubjektivFeatureProfileConfig,
) -> std::io::Result<()> {
if memory.enabled
&& memory.extraction.enabled
&& subjektiv.enabled
&& subjektiv.extraction.enabled
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Workspace Memory and subjektiv automatic extraction cannot both be enabled for one Worker",
));
}
Ok(())
}
/// Register the builtin file-manipulation tools, optional memory tools,
/// and the Worker-orchestration tools (SubWorkerSpawn + comm) on the Worker's
/// Engine. Returns the WorkdirSession handle used to attach a `WorkerFsView` to
@@ -1252,16 +1269,7 @@ where
}
let memory_profile = &worker.manifest().feature.memory.profile;
let subjektiv_profile = &worker.manifest().feature.subjektiv.profile;
if memory_profile.enabled
&& memory_profile.extraction.enabled
&& subjektiv_profile.enabled
&& subjektiv_profile.extraction.enabled
{
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Workspace Memory and subjektiv automatic extraction cannot both be enabled for one Worker",
));
}
validate_automatic_memory_extraction_targets(memory_profile, subjektiv_profile)?;
let memory_install_plan = crate::feature::builtin::memory::MemoryFeatureInstallPlan::prepare(
worker.manifest(),
worker.workspace_client_handle(),
@@ -3438,6 +3446,23 @@ mod tests {
use tempfile::TempDir;
use tokio::net::UnixListener;
#[test]
fn automatic_memory_extraction_destinations_are_mutually_exclusive() {
let mut memory = manifest::MemoryFeatureProfileConfig::default();
memory.enabled = true;
memory.extraction.enabled = true;
let mut subjektiv = manifest::SubjektivFeatureProfileConfig::default();
subjektiv.enabled = true;
subjektiv.extraction.enabled = true;
let error = validate_automatic_memory_extraction_targets(&memory, &subjektiv).unwrap_err();
assert_eq!(error.kind(), std::io::ErrorKind::InvalidInput);
assert!(error.to_string().contains("cannot both be enabled"));
subjektiv.extraction.enabled = false;
validate_automatic_memory_extraction_targets(&memory, &subjektiv).unwrap();
}
#[test]
fn no_controller_parent_notification_uses_durable_pending_authority() {
let temp = TempDir::new().unwrap();
@@ -1356,6 +1356,43 @@ permission = "write"
assert!(feature.is_none());
}
#[test]
fn child_host_suppresses_enabled_subjektiv_lifecycle_without_capture_side_effects() {
let capture = CommittedSessionCaptureHandle::new(|| {
panic!("a child Worker must not capture for an inherited subjektiv profile")
});
let extensions = SessionExtensionHandle::new(|_, _, _| {
panic!("a child Worker must not write a subjektiv extraction pointer")
});
let mut config = manifest::ResolvedSubjektivFeatureConfig::default();
config.profile.enabled = true;
config.profile.extraction.enabled = true;
config.profile.extraction.threshold = Some(1);
config
.bind_workspace_settings(manifest::WorkspaceMemorySettingsSnapshot {
workspace_id: "workspace-1".to_string(),
settings_revision: 1,
language: "English".to_string(),
})
.unwrap();
let feature = SubjektivLifecycleFeature::from_resolved_config(
false,
config,
capture,
extensions,
crate::worker::marker_workspace_client(None, "child-disabled"),
test_manifest(),
Box::new(ScriptClient::new(Vec::new())),
Arc::new(ArcSwap::from(PromptCatalog::builtins_only().unwrap())),
WorkerWorkspaceContext::no_workspace(),
None,
)
.unwrap();
assert!(feature.is_none());
}
#[test]
fn extraction_manifest_applies_memory_model_and_reasoning_overrides() {
use agen::llm_client::capability::{ReasoningControl, ReasoningEffort};
@@ -1595,9 +1632,11 @@ permission = "write"
1,
"the immutable partial candidate remains staged for retry/deduplication"
);
assert!(!requests
.iter()
.any(|request| request.path.ends_with("/subjektiv/sessions")));
assert!(
!requests
.iter()
.any(|request| request.path.ends_with("/subjektiv/sessions"))
);
}
#[tokio::test]
@@ -1680,6 +1719,48 @@ permission = "write"
);
}
#[tokio::test]
async fn subjektiv_rewrite_barrier_cancels_without_pointer_or_legacy_side_effects() {
let calls = Arc::new(AtomicUsize::new(0));
let client = PendingClient {
calls: Arc::clone(&calls),
};
let extension_writes = Arc::new(Mutex::new(Vec::new()));
let (event_tx, mut event_rx) = broadcast::channel(16);
let workspace_client = Arc::new(RecordingWorkspaceClient::default());
let mut task = test_task(
capture(2, 250),
Box::new(client),
Arc::clone(&extension_writes),
event_tx,
workspace_client.clone(),
);
task.target = ExtractionTarget::Subjektiv;
task.config.profile.consolidation.request_enabled = false;
let registry = start_background_task(task);
tokio::time::timeout(Duration::from_secs(5), async {
while calls.load(Ordering::SeqCst) == 0 {
tokio::task::yield_now().await;
}
})
.await
.expect("subjektiv extraction child should reach its first provider request");
let rewrite_guard = registry.begin_session_rewrite().await.unwrap();
drop(rewrite_guard);
registry.shutdown().await.unwrap();
assert!(extension_writes.lock().unwrap().is_empty());
let events = std::iter::from_fn(|| event_rx.try_recv().ok()).collect::<Vec<_>>();
assert!(events.iter().any(|event| {
matches!(event, Event::MemoryWorker(event) if event.status == "cancelled")
}));
let requests = workspace_client.requests.lock().unwrap();
assert!(
requests.is_empty(),
"cancelled subjektiv requests: {requests:?}"
);
}
#[tokio::test]
async fn interrupted_committed_run_skips_internal_worker_and_pointer_commit() {
let client = ScriptClient::new(Vec::new());
@@ -1972,9 +2053,7 @@ permission = "write"
assert!(controller_source.contains("MemoryFeatureInstallPlan::prepare"));
assert!(controller_source.contains("MemoryLifecycleFeature::from_resolved_config"));
let worker_production = worker_source.split("#[cfg(test)]").next().unwrap();
let controller_production = controller_source.split("#[cfg(test)]").next().unwrap();
assert!(!worker_production.contains(".feature.memory"));
assert!(!controller_production.contains(".feature.memory"));
let lifecycle_source = include_str!("memory_lifecycle.rs");
assert!(lifecycle_source.contains("request_memory_staging_consolidation"));
let internal_worker_source = include_str!("../../internal_worker.rs");