fix: harden subject memory consolidation
This commit is contained in:
@@ -781,6 +781,39 @@ mod tests {
|
||||
))
|
||||
}
|
||||
|
||||
fn repository_error_client(status: u16, code: &str) -> Arc<dyn WorkspaceClient> {
|
||||
use std::io::{Read, Write};
|
||||
use std::net::TcpListener;
|
||||
|
||||
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
let body = serde_json::json!({
|
||||
"error": "request rejected",
|
||||
"message": format!("{code}: retry after reread"),
|
||||
"diagnostics": [{
|
||||
"code": code,
|
||||
"severity": "error",
|
||||
"message": format!("{code}: retry after reread"),
|
||||
}],
|
||||
})
|
||||
.to_string();
|
||||
std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().unwrap();
|
||||
let mut request = [0_u8; 4096];
|
||||
let _ = stream.read(&mut request).unwrap();
|
||||
let response = format!(
|
||||
"HTTP/1.1 {status} rejected\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
stream.write_all(response.as_bytes()).unwrap();
|
||||
});
|
||||
Arc::new(crate::worker::TestWorkspaceHttpClient::new(
|
||||
"workspace",
|
||||
format!("http://{addr}"),
|
||||
))
|
||||
}
|
||||
|
||||
fn tool_names(definitions: Vec<ToolDefinition>) -> Vec<String> {
|
||||
let mut names = definitions
|
||||
.into_iter()
|
||||
@@ -991,6 +1024,39 @@ mod tests {
|
||||
assert!(names.contains(&"MemoryStagingClose".to_string()));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn subjektiv_candidate_conflicts_survive_workspace_http_transport() {
|
||||
let input = serde_json::json!({
|
||||
"request_id": "decision-request-1",
|
||||
"candidate_id": "candidate-1",
|
||||
"reason": "candidate is already covered",
|
||||
"decision": {
|
||||
"kind": "close",
|
||||
"action": "already_covered",
|
||||
"affected_memory": [],
|
||||
},
|
||||
})
|
||||
.to_string();
|
||||
for (status, code) in [
|
||||
(409, "candidate_decision_conflict"),
|
||||
(403, "subject_scope_mismatch"),
|
||||
] {
|
||||
let tool = SubjectConsolidationTool {
|
||||
client: repository_error_client(status, code),
|
||||
operation: SubjectConsolidationOperation::DecideCandidate,
|
||||
};
|
||||
let output = tool
|
||||
.execute(&input, ToolExecutionContext::direct())
|
||||
.await
|
||||
.expect("typed conflicts are model-visible retry output");
|
||||
let content: serde_json::Value =
|
||||
serde_json::from_str(output.content.as_deref().unwrap()).unwrap();
|
||||
assert_eq!(content["status"], "error");
|
||||
assert_eq!(content["error"]["code"], code);
|
||||
assert!(output.summary.contains("requires reread"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subjektiv_consolidation_has_atomic_decision_without_host_scope_fields() {
|
||||
let tools = workspace_http_subjektiv_consolidation_tools(test_client());
|
||||
|
||||
@@ -79,6 +79,15 @@ struct MemoryLifecycleTask {
|
||||
event_tx: Option<broadcast::Sender<Event>>,
|
||||
}
|
||||
|
||||
fn memory_lifecycle_requested(
|
||||
lifecycle_enabled: bool,
|
||||
config: &manifest::ResolvedMemoryFeatureConfig,
|
||||
) -> bool {
|
||||
lifecycle_enabled
|
||||
&& config.profile.enabled
|
||||
&& (config.profile.extraction.enabled || config.profile.consolidation.request_enabled)
|
||||
}
|
||||
|
||||
impl MemoryLifecycleFeature {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn from_resolved_config(
|
||||
@@ -93,10 +102,7 @@ impl MemoryLifecycleFeature {
|
||||
workspace_context: WorkerWorkspaceContext,
|
||||
event_tx: Option<broadcast::Sender<Event>>,
|
||||
) -> std::io::Result<Option<Self>> {
|
||||
if !lifecycle_enabled
|
||||
|| !config.profile.enabled
|
||||
|| (!config.profile.extraction.enabled && !config.profile.consolidation.request_enabled)
|
||||
{
|
||||
if !memory_lifecycle_requested(lifecycle_enabled, &config) {
|
||||
return Ok(None);
|
||||
}
|
||||
config
|
||||
@@ -1365,6 +1371,26 @@ permission = "write"
|
||||
config
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_subjektiv_consolidation_profile_does_not_install_global_memory_lifecycle() {
|
||||
let workspace = tempfile::tempdir().unwrap();
|
||||
let resolved = manifest::ProfileResolver::new()
|
||||
.with_workspace_base(workspace.path())
|
||||
.resolve(
|
||||
&manifest::ProfileSelector::source_named(
|
||||
manifest::ProfileRegistrySource::Builtin,
|
||||
"subjektiv-memory-consolidation",
|
||||
),
|
||||
manifest::ProfileResolveOptions::with_worker_name("subject-consolidator"),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert!(!memory_lifecycle_requested(
|
||||
true,
|
||||
&resolved.manifest.feature.memory
|
||||
));
|
||||
}
|
||||
|
||||
fn test_task(
|
||||
capture: CommittedSessionCapture,
|
||||
client: Box<dyn LlmClient>,
|
||||
|
||||
Reference in New Issue
Block a user