| /* | |
| * Licensed to the Apache Software Foundation (ASF) under one | |
| * or more contributor license agreements. See the NOTICE file | |
| * distributed with this work for additional information | |
| * regarding copyright ownership. The ASF licenses this file | |
| * to you under the Apache License, Version 2.0 (the | |
| * "License"); you may not use this file except in compliance | |
| * with the License. You may obtain a copy of the License at | |
| * | |
| * http://www.apache.org/licenses/LICENSE-2.0 | |
| * | |
| * Unless required by applicable law or agreed to in writing, | |
| * software distributed under the License is distributed on an | |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | |
| * KIND, either express or implied. See the License for the | |
| * specific language governing permissions and limitations | |
| * under the License. | |
| */ | |
| use super::*; | |
| use maka_event_log::usage::{AuxiliarySource, Origin, Outcome, Query}; | |
| async fn fixture(base: &str) -> Fixture { | |
| Fixture::configured(base, None, None, None, |settings| { | |
| settings.automatic_titles = true | |
| }) | |
| .await | |
| } | |
| async fn automatic(f: &Fixture) -> String { | |
| let mut input = f.create_input("Untitled").await; | |
| input["automaticTitle"] = json!(true); | |
| f.mutation(2, "create", input, Uuid::new_v4()) | |
| .await | |
| .unwrap()["session"]["id"] | |
| .as_str() | |
| .unwrap() | |
| .into() | |
| } | |
| fn is_title(sent: &Value) -> bool { | |
| sent["messages"].as_array().unwrap().iter().any(|message| { | |
| message["role"] == "system" | |
| && message["content"] | |
| .as_str() | |
| .is_some_and(|text| text.starts_with("Generate a short session title")) | |
| }) | |
| } | |
| async fn pair(listener: &TcpListener) -> ((Value, TcpStream), (Value, TcpStream)) { | |
| let (mut first, _) = listener.accept().await.unwrap(); | |
| let a = request(&mut first).await; | |
| // Both physical requests are admitted while neither response has finished. | |
| let (mut second, _) = listener.accept().await.unwrap(); | |
| let b = request(&mut second).await; | |
| assert_ne!(is_title(&a), is_title(&b)); | |
| if is_title(&a) { | |
| ((b, second), (a, first)) | |
| } else { | |
| ((a, first), (b, second)) | |
| } | |
| } | |
| async fn attempts(f: &Fixture, session: &str) -> Vec<maka_agent_facts::accounting::ModelAttempt> { | |
| f.store | |
| .model_attempts( | |
| Query { | |
| from: 0.0, | |
| to: 253402300799999.0, | |
| session_id: Some(session.into()), | |
| through: None, | |
| }, | |
| 0, | |
| 100, | |
| ) | |
| .await | |
| .unwrap() | |
| .attempts | |
| } | |
| #[tokio::test] | |
| async fn kimi_automatic_title_uses_low_without_defaulting_main_thinking() { | |
| tokio::time::timeout(Duration::from_secs(60), async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let f = Fixture::configured_provider( | |
| &format!("http://{}/v1", listener.local_addr().unwrap()), | |
| None, | |
| None, | |
| None, | |
| |settings| settings.automatic_titles = true, | |
| "kimi-coding-plan", | |
| ) | |
| .await; | |
| selection::edit(&f, |changes| { | |
| changes.model_overrides = Patch::Set(BTreeMap::from([( | |
| "fixture-model".into(), | |
| ModelOverride { | |
| thinking_levels: Some(vec![ | |
| ThinkingLevel::Low, | |
| ThinkingLevel::High, | |
| ThinkingLevel::Max, | |
| ]), | |
| ..Default::default() | |
| }, | |
| )])); | |
| }) | |
| .await; | |
| let session = automatic(&f).await; | |
| let mut commits = f.store.subscribe_commits(); | |
| let sent = f.send(&session).await.unwrap(); | |
| let mut main_request = None; | |
| let mut title_request = None; | |
| // Baseline rejects auxiliary Low before HTTP. Observe that durable failure | |
| // instead of waiting for a second request which cannot arrive. | |
| loop { | |
| commits.borrow_and_update(); | |
| let diagnostics = f | |
| .query( | |
| "diagnostics", | |
| json!({"sessionId":session,"position":{"kind":"latest"}}), | |
| ) | |
| .await | |
| .unwrap(); | |
| let failed = diagnostics["entries"] | |
| .as_array() | |
| .unwrap() | |
| .iter() | |
| .any(|entry| entry["detail"]["kind"] == "session_title"); | |
| if main_request.is_some() && (title_request.is_some() || failed) { | |
| break; | |
| } | |
| tokio::select! { | |
| accepted = listener.accept() => { | |
| let (mut socket, _) = accepted.unwrap(); | |
| let body = tool_schemas::read_wire(&mut socket).await; | |
| if body["system"].to_string().contains("Generate a short session title") { | |
| tool_schemas::anthropic_text_reply(&mut socket, "Kimi auxiliary title").await; | |
| title_request = Some(body); | |
| } else { | |
| tool_schemas::anthropic_text_reply(&mut socket, "Kimi main completed").await; | |
| main_request = Some(body); | |
| } | |
| } | |
| changed = commits.changed() => { changed.unwrap(); } | |
| } | |
| } | |
| f.drained().await; | |
| let record = f.owner.session(&session).await.unwrap(); | |
| let physical = attempts(&f, &session).await; | |
| let diagnostics = f | |
| .query( | |
| "diagnostics", | |
| json!({"sessionId":session,"position":{"kind":"latest"}}), | |
| ) | |
| .await | |
| .unwrap(); | |
| let boundary = f | |
| .store | |
| .invocation_boundary(&session, sent["invocationId"].as_str().unwrap()) | |
| .await | |
| .unwrap() | |
| .unwrap() | |
| .0; | |
| f.app.shutdown().await.unwrap(); | |
| f.config.shutdown().await.unwrap(); | |
| assert!(main_request.unwrap()["output_config"]["effort"].is_null()); | |
| assert_eq!( | |
| record.configuration.title, "Kimi auxiliary title", | |
| "auxiliary preparation must succeed: {diagnostics}" | |
| ); | |
| assert!(!record.configuration.title_is_manual); | |
| let title_request = title_request.expect("supported Kimi Low must reach actual Anthropic HTTP"); | |
| assert_eq!(title_request["output_config"]["effort"], "low"); | |
| assert_eq!(title_request["max_tokens"], 2048); | |
| assert!(title_request["tools"].is_null() || title_request["tools"] == json!([])); | |
| assert_eq!(diagnostics["entries"], json!([])); | |
| assert!(matches!( | |
| boundary.state, | |
| maka_event_log::turns::InvocationState::Ended { | |
| outcome: maka_agent_facts::event::InvocationOutcome::Completed, | |
| .. | |
| } | |
| )); | |
| assert_eq!(physical.len(), 2); | |
| let auxiliary = physical | |
| .iter() | |
| .find(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. })) | |
| .unwrap(); | |
| assert_eq!(auxiliary.session_id.as_deref(), Some(session.as_str())); | |
| assert!(matches!(&auxiliary.origin, Origin::Auxiliary { | |
| source: AuxiliarySource::SessionTitle { invocation } | |
| } if invocation == &boundary.invocation)); | |
| assert_eq!(auxiliary.outcome, Outcome::Success); | |
| assert_eq!(auxiliary.usage.input_tokens, Some(1)); | |
| assert_eq!(auxiliary.usage.output_tokens, Some(2)); | |
| }).await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn title_survives_main_completion_between_metadata_read_and_write() { | |
| verify_title_metadata_conflict(false).await; | |
| } | |
| #[tokio::test] | |
| async fn title_retry_preserves_manual_placeholder_after_metadata_read() { | |
| verify_title_metadata_conflict(true).await; | |
| } | |
| async fn verify_title_metadata_conflict(manual_rename: bool) { | |
| tokio::time::timeout(Duration::from_secs(30), async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await; | |
| let session = automatic(&f).await; | |
| let sent = f.send(&session).await.unwrap(); | |
| let ((_, mut main), (_, mut title)) = pair(&listener).await; | |
| let gate = Arc::new(super::super::title::WriteGate { | |
| entered: tokio::sync::Semaphore::new(0), | |
| release: tokio::sync::Semaphore::new(0), | |
| }); | |
| *f.owner.title_write_gate.lock().unwrap() = Some(gate.clone()); | |
| response_with_text(&mut title, "A title preserved across main completion").await; | |
| gate.entered.acquire().await.unwrap().forget(); | |
| let before = f.owner.session(&session).await.unwrap(); | |
| let observed_revision = before.revision; | |
| let mut expected = before.configuration; | |
| let mut commits = f.store.subscribe_commits(); | |
| response(&mut main).await; | |
| let completed = loop { | |
| commits.borrow_and_update(); | |
| let boundary = f | |
| .store | |
| .invocation_boundary(&session, sent["invocationId"].as_str().unwrap()) | |
| .await | |
| .unwrap() | |
| .unwrap() | |
| .0; | |
| if matches!( | |
| boundary.state, | |
| maka_event_log::turns::InvocationState::Ended { .. } | |
| ) { | |
| break boundary; | |
| } | |
| commits.changed().await.unwrap(); | |
| }; | |
| let completed_revision = f.owner.session(&session).await.unwrap().revision; | |
| if manual_rename { | |
| // An external metadata writer can commit while this Owner holds its gate. | |
| let changed = f | |
| .store | |
| .update_session_metadata( | |
| &session, | |
| completed_revision, | |
| |configuration: &mut SessionConfiguration| { | |
| configuration.title_is_manual = true; | |
| configuration.labels = vec!["Preserved label".into()]; | |
| Ok(()) | |
| }, | |
| ) | |
| .await | |
| .unwrap(); | |
| assert!(matches!( | |
| changed, | |
| maka_event_log::sessions::SessionMutation::Committed(_) | |
| )); | |
| expected.title_is_manual = true; | |
| expected.labels = vec!["Preserved label".into()]; | |
| } else { | |
| expected.title = "A title preserved across main completion".into(); | |
| } | |
| gate.release.add_permits(1); | |
| f.drained().await; | |
| let record = f.owner.session(&session).await.unwrap(); | |
| let attempts = attempts(&f, &session).await; | |
| f.app.shutdown().await.unwrap(); | |
| f.config.shutdown().await.unwrap(); | |
| assert!(completed_revision > observed_revision); | |
| assert!(matches!( | |
| completed.state, | |
| maka_event_log::turns::InvocationState::Ended { | |
| outcome: maka_agent_facts::event::InvocationOutcome::Completed, | |
| .. | |
| } | |
| )); | |
| assert!( | |
| record.configuration == expected, | |
| "title update must preserve the expected session configuration" | |
| ); | |
| let auxiliary: Vec<_> = attempts | |
| .iter() | |
| .filter(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. })) | |
| .collect(); | |
| assert_eq!(auxiliary.len(), 1); | |
| assert_eq!(auxiliary[0].outcome, Outcome::Success); | |
| assert_eq!(auxiliary[0].usage.input_tokens, Some(1)); | |
| assert_eq!(auxiliary[0].usage.output_tokens, Some(2)); | |
| }) | |
| .await | |
| .unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn title_first_message_is_bounded_accounted_and_not_reissued_after_retry_or_restart() { | |
| tokio::time::timeout(Duration::from_secs(60), async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let mut f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await; | |
| selection::thinking(&f, vec![ThinkingLevel::Off, ThinkingLevel::High], ThinkingLevel::High).await; | |
| let session = automatic(&f).await; | |
| let text = "中".repeat(5000); | |
| let input = json!({"sessionId":session,"message":text,"model":f.selection().await}); | |
| let operation = Uuid::new_v4(); | |
| let sent = f.mutation(2, "send", input.clone(), operation).await.unwrap(); | |
| let ((main, mut main_socket), (title, mut title_socket)) = pair(&listener).await; | |
| assert_eq!(main["reasoning_effort"], "high"); | |
| assert_eq!(title["reasoning_effort"], "none"); | |
| assert_eq!(title["messages"][1]["content"], "中".repeat(4096)); | |
| assert!(title["tools"].is_null() || title["tools"] == json!([])); | |
| assert!(title["max_tokens"] == 2048 || title["max_completion_tokens"] == 2048, "{title}"); | |
| assert_eq!(f.mutation(2, "send", input, operation).await.unwrap(), sent); | |
| response(&mut main_socket).await; | |
| // An auxiliary title is owned by the Host, so main-turn completion does not cancel it. | |
| loop { | |
| let boundary = f.store.invocation_boundary(&session, sent["invocationId"].as_str().unwrap()).await.unwrap().unwrap().0; | |
| if matches!(boundary.state, maka_event_log::turns::InvocationState::Ended { .. }) { break; } | |
| tokio::task::yield_now().await; | |
| } | |
| response_with_text(&mut title_socket, " cafe\u{301}\n\u{202e}任务😀 ").await; | |
| f.drained().await; | |
| let record = f.owner.session(&session).await.unwrap(); | |
| assert_eq!(record.configuration.title, "café 任务😀"); | |
| assert!(!record.configuration.title_is_manual); | |
| let original = attempts(&f, &session).await; | |
| assert_eq!(original.len(), 2); | |
| let auxiliary = original.iter().find(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. })).unwrap(); | |
| assert!(matches!(&auxiliary.origin, Origin::Auxiliary { source: AuxiliarySource::SessionTitle { invocation } } if invocation.invocation_id == sent["invocationId"].as_str().unwrap())); | |
| assert_eq!(auxiliary.outcome, Outcome::Success); | |
| assert_eq!(auxiliary.usage.input_tokens, Some(1)); | |
| assert_eq!(auxiliary.usage.output_tokens, Some(2)); | |
| assert!(auxiliary.quote.is_some()); | |
| todo_tests::reopen(&mut f).await; | |
| assert_eq!(attempts(&f, &session).await.len(), 2); | |
| f.send(&session).await.unwrap(); | |
| let (mut socket, _) = listener.accept().await.unwrap(); | |
| assert!(!is_title(&request(&mut socket).await)); | |
| response(&mut socket).await; | |
| loop { | |
| let observation = f.query("observe", json!({"sessionId":session})).await.unwrap(); | |
| if observation["run"].is_null() { break; } | |
| tokio::task::yield_now().await; | |
| } | |
| f.app.shutdown().await.unwrap(); | |
| let current = attempts(&f, &session).await; | |
| assert_eq!(current.len(), 3); | |
| assert_eq!(current.iter().filter(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. })).count(), 1); | |
| assert_eq!(f.store.get_session::<SessionConfiguration>(&session).await.unwrap().unwrap().configuration.title, "café 任务😀"); | |
| f.config.shutdown().await.unwrap(); | |
| }).await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn title_manual_rename_even_to_placeholder_and_archive_win_in_flight() { | |
| tokio::time::timeout(Duration::from_secs(30), async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await; | |
| for archive in [false, true] { | |
| let session = automatic(&f).await; | |
| let sent = f.send(&session).await.unwrap(); | |
| let ((_, mut main), (_, mut title)) = pair(&listener).await; | |
| response(&mut main).await; | |
| loop { | |
| let boundary = f.store.invocation_boundary(&session, sent["invocationId"].as_str().unwrap()).await.unwrap().unwrap().0; | |
| if matches!(boundary.state, maka_event_log::turns::InvocationState::Ended { .. }) { break; } | |
| tokio::task::yield_now().await; | |
| } | |
| if archive { | |
| f.store.set_session_archived::<SessionConfiguration>(&session, true, now()).await.unwrap(); | |
| } else { | |
| let revision = f.owner.session(&session).await.unwrap().revision; | |
| let result = f.mutation(2, "metadata", json!({"sessionId":session,"expectedRevision":revision,"title":"Untitled","labels":[]}), Uuid::new_v4()).await.unwrap(); | |
| assert_eq!(result["kind"], "committed", "{result}"); | |
| } | |
| response_with_text(&mut title, "Must not replace the user's title").await; | |
| f.drained().await; | |
| let record = f.owner.session(&session).await.unwrap(); | |
| assert_eq!(record.configuration.title, "Untitled"); | |
| assert_eq!(record.archived, archive); | |
| assert_eq!(record.configuration.title_is_manual, !archive); | |
| assert_eq!(attempts(&f, &session).await.len(), 2); | |
| } | |
| f.app.shutdown().await.unwrap(); | |
| f.config.shutdown().await.unwrap(); | |
| }).await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn title_skips_manual_and_managed_sessions_before_model_dispatch() { | |
| tokio::time::timeout(Duration::from_secs(30), async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await; | |
| let manual = f.create("My chosen title").await; | |
| let mut configuration = f.owner.session(&manual).await.unwrap().configuration; | |
| configuration.title_is_manual = false; | |
| let managed = format!("{}{}", f.owner.prefix, Uuid::new_v4().simple()); | |
| f.store | |
| .create_plugin_session( | |
| &maka_event_log::sessions::PluginSession { | |
| session_id: managed.clone(), | |
| creator: maka_capabilities::storage::Namespace::new( | |
| "title.fixture", | |
| maka_plugin_core::scope::Scope::Profile, | |
| ) | |
| .unwrap(), | |
| fingerprint: "managed-title".into(), | |
| managed: true, | |
| authority_session_id: None, | |
| }, | |
| &configuration, | |
| now(), | |
| ) | |
| .await | |
| .unwrap(); | |
| for session in [&manual, &managed] { | |
| f.send(session).await.unwrap(); | |
| let (mut socket, _) = listener.accept().await.unwrap(); | |
| assert!(!is_title(&request(&mut socket).await)); | |
| response(&mut socket).await; | |
| f.drained().await; | |
| assert_eq!(attempts(&f, session).await.len(), 1); | |
| assert_eq!( | |
| f.owner.session(session).await.unwrap().configuration.title, | |
| "My chosen title" | |
| ); | |
| } | |
| f.app.shutdown().await.unwrap(); | |
| f.config.shutdown().await.unwrap(); | |
| }) | |
| .await | |
| .unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn title_invalid_finish_keeps_observed_usage_and_never_retries() { | |
| tokio::time::timeout(Duration::from_secs(30), async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await; | |
| let session = automatic(&f).await; | |
| f.send(&session).await.unwrap(); | |
| let ((_, mut main), (_, mut title)) = pair(&listener).await; | |
| response(&mut main).await; | |
| let end = json!({"id":"fixture","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":7,"completion_tokens":9,"total_tokens":16}}); | |
| let body = format!("data: {end}\n\ndata: [DONE]\n\n"); | |
| title.write_all(format!("HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}", body.len()).as_bytes()).await.unwrap(); | |
| f.drained().await; | |
| let attempts = attempts(&f, &session).await; | |
| let auxiliary = attempts.iter().find(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. })).unwrap(); | |
| assert_eq!(auxiliary.outcome, Outcome::Error); | |
| assert_eq!(auxiliary.usage.input_tokens, Some(7)); | |
| assert_eq!(auxiliary.usage.output_tokens, Some(9)); | |
| assert_eq!(f.owner.session(&session).await.unwrap().configuration.title, "Untitled"); | |
| f.send(&session).await.unwrap(); | |
| let (mut socket, _) = listener.accept().await.unwrap(); | |
| assert!(!is_title(&request(&mut socket).await)); | |
| response(&mut socket).await; | |
| f.drained().await; | |
| f.app.shutdown().await.unwrap(); | |
| f.config.shutdown().await.unwrap(); | |
| }).await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn title_host_shutdown_awaits_socket_cleanup_and_restart_does_not_replay() { | |
| tokio::time::timeout(Duration::from_secs(30), async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let mut f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await; | |
| let session = automatic(&f).await; | |
| f.send(&session).await.unwrap(); | |
| let ((_, mut main), (_, mut title)) = pair(&listener).await; | |
| response(&mut main).await; | |
| f.app.shutdown().await.unwrap(); | |
| let mut buffer = [0; 1]; | |
| assert_eq!(title.read(&mut buffer).await.unwrap(), 0); | |
| assert!(f.owner.auxiliary.resources.is_idle()); | |
| let original = attempts(&f, &session).await; | |
| let auxiliary = original | |
| .iter() | |
| .find(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. })) | |
| .unwrap(); | |
| assert_eq!(auxiliary.outcome, Outcome::Aborted); | |
| todo_tests::reopen(&mut f).await; | |
| f.send(&session).await.unwrap(); | |
| let (mut socket, _) = listener.accept().await.unwrap(); | |
| assert!(!is_title(&request(&mut socket).await)); | |
| response(&mut socket).await; | |
| loop { | |
| if f.query("observe", json!({"sessionId":session})) | |
| .await | |
| .unwrap()["run"] | |
| .is_null() | |
| { | |
| break; | |
| } | |
| tokio::task::yield_now().await; | |
| } | |
| f.app.shutdown().await.unwrap(); | |
| assert_eq!( | |
| attempts(&f, &session) | |
| .await | |
| .iter() | |
| .filter(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. })) | |
| .count(), | |
| 1 | |
| ); | |
| assert_eq!( | |
| f.store | |
| .get_session::<SessionConfiguration>(&session) | |
| .await | |
| .unwrap() | |
| .unwrap() | |
| .configuration | |
| .title, | |
| "Untitled" | |
| ); | |
| f.config.shutdown().await.unwrap(); | |
| }) | |
| .await | |
| .unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn title_settlement_fault_fences_owner_and_preserves_original_usage_for_recovery() { | |
| tokio::time::timeout(Duration::from_secs(30), async { | |
| const SQL_SECRET: &str = "SECRET_TITLE_SETTLEMENT_SQL_PAYLOAD"; | |
| let (mut capture, _log_guard) = log_capture(); | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let base = format!("http://{}/v1", listener.local_addr().unwrap()); | |
| let f = fixture(&base).await; | |
| let activation = f.owner.revision.activation.clone(); | |
| let session = automatic(&f).await; | |
| let sent = f.send(&session).await.unwrap(); | |
| let ((_, mut main), (_, mut title)) = pair(&listener).await; | |
| let raw = rusqlite::Connection::open(f.root.join(maka_event_log::root::ROOT_DATABASE)).unwrap(); | |
| raw.execute_batch(&format!("CREATE TRIGGER hold_title_settlement BEFORE INSERT ON event_log WHEN NEW.kind='auxiliary_model_settled' BEGIN SELECT RAISE(ABORT, '{SQL_SECRET}'); END;")).unwrap(); | |
| response(&mut main).await; | |
| let completed = loop { | |
| let boundary = f.store.invocation_boundary(&session, sent["invocationId"].as_str().unwrap()).await.unwrap().unwrap().0; | |
| if matches!(boundary.state, maka_event_log::turns::InvocationState::Ended { .. }) { break boundary; } | |
| tokio::task::yield_now().await; | |
| }; | |
| response_with_text(&mut title, "Uncommitted title").await; | |
| f.drained().await; | |
| assert!(f.owner.runs.lock().await.closing); | |
| assert!(f.owner.stop.is_cancelled()); | |
| assert_eq!(f.send(&session).await.unwrap_err(), "agent_closed"); | |
| assert_eq!(f.owner.session(&session).await.unwrap().configuration.title, "Untitled"); | |
| let count: i64 = raw.query_row("SELECT COUNT(*) FROM event_log WHERE kind='auxiliary_model_settled'", [], |row| row.get(0)).unwrap(); | |
| assert_eq!(count, 0); | |
| raw.execute_batch("DROP TRIGGER hold_title_settlement").unwrap(); | |
| f.app.shutdown().await.unwrap(); | |
| // The exclusive startup recovery owns this transition, never plugin activation. | |
| f.store.recover_auxiliary_models().await.unwrap(); | |
| let original = attempts(&f, &session).await; | |
| let auxiliary = original.iter().find(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. })).unwrap(); | |
| assert_eq!(auxiliary.outcome, Outcome::Unknown); | |
| assert_eq!(auxiliary.usage.input_tokens, Some(1)); | |
| assert_eq!(auxiliary.usage.output_tokens, Some(2)); | |
| f.config.shutdown().await.unwrap(); | |
| assert!(matches!(completed.state, maka_event_log::turns::InvocationState::Ended { | |
| outcome: maka_agent_facts::event::InvocationOutcome::Completed, .. | |
| })); | |
| assert!(matches!(&auxiliary.origin, Origin::Auxiliary { | |
| source: AuxiliarySource::SessionTitle { invocation } | |
| } if invocation == &completed.invocation)); | |
| let log = log_text(&mut capture); | |
| let settlement = log.lines().find(|line| line.contains("stage=\"auxiliary_model_settlement_failed\"")) | |
| .expect("typed SQL settlement failure must be logged before it becomes a persistence string"); | |
| assert!(settlement.contains("ERROR")); | |
| assert!(settlement.contains("cause=\"store_sqlx\"")); | |
| assert!(settlement.contains("sql_code=1811")); | |
| assert!(settlement.contains("request_id=") && settlement.contains(&auxiliary.request_id)); | |
| let fatal = log.lines().find(|line| line.contains("stage=\"title_worker_persistence_failed\"")) | |
| .expect("title worker must log its actual owner-closing consequence"); | |
| assert!(fatal.contains("ERROR")); | |
| assert!(fatal.contains("consequence=\"owner_closing\"")); | |
| for identity in [&completed.invocation.session_id, &completed.invocation.turn_id, &completed.invocation.run_id, &completed.invocation.invocation_id, &activation] { | |
| assert!(fatal.contains(identity), "title fatal event must retain its triggering invocation/activation: {fatal}"); | |
| } | |
| for secret in [SQL_SECRET, "private-fixture-key", "Uncommitted title", "Hello", base.as_str()] { | |
| assert!(!log.contains(secret), "typed cause and custody events must exclude SQL messages, credentials, input, title and URLs"); | |
| } | |
| }).await.unwrap(); | |
| } | |
| #[derive(Clone, Copy)] | |
| enum VisibleTitleFailure { | |
| Http, | |
| OutputLimit, | |
| EmptyOutput, | |
| } | |
| #[tokio::test] | |
| async fn title_http_failure_is_public_sanitized_and_survives_restart() { | |
| verify_visible_title_failure(VisibleTitleFailure::Http).await; | |
| } | |
| #[tokio::test] | |
| async fn title_output_limit_is_public_without_changing_successful_physical_accounting() { | |
| verify_visible_title_failure(VisibleTitleFailure::OutputLimit).await; | |
| } | |
| #[tokio::test] | |
| async fn title_empty_output_is_public_without_changing_successful_physical_accounting() { | |
| verify_visible_title_failure(VisibleTitleFailure::EmptyOutput).await; | |
| } | |
| async fn verify_visible_title_failure(case: VisibleTitleFailure) { | |
| tokio::time::timeout(Duration::from_secs(30), async { | |
| const SECRET: &str = "SECRET_TITLE_PROVIDER_BODY_SENTINEL"; | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let mut f = fixture(&format!("http://{}/v1", listener.local_addr().unwrap())).await; | |
| let session = automatic(&f).await; | |
| let other = f.create("Unaffected session").await; | |
| let sent = f.send(&session).await.unwrap(); | |
| let ((_,mut main),(_,mut title)) = pair(&listener).await; | |
| response(&mut main).await; | |
| let completed = loop { | |
| let boundary = f.store.invocation_boundary(&session,sent["invocationId"].as_str().unwrap()).await.unwrap().unwrap().0; | |
| if matches!(boundary.state,maka_event_log::turns::InvocationState::Ended {..}) {break boundary;} | |
| tokio::task::yield_now().await; | |
| }; | |
| let diagnostic_watch = if matches!(case,VisibleTitleFailure::Http) { | |
| let mut watch=f.app.client().subscribe(f.binding(2).await,"watchDiagnostics".into(),json!({"sessionId":session}), | |
| scope(&f.issuer,true),Attachments::default().into()).await.unwrap(); | |
| let initial=watch.recv().await.unwrap(); | |
| Some((watch,initial)) | |
| } else {None}; | |
| let before_failure = now(); | |
| let (status,content_type,body,cause,physical_outcome) = match case { | |
| VisibleTitleFailure::Http => ( | |
| "400 Bad Request", "application/json", | |
| json!({"error":{"type":"invalid_request_error","message":SECRET}}).to_string(), | |
| json!({"kind":"model","cause":{"kind":"http","status":400}}), Outcome::Error, | |
| ), | |
| VisibleTitleFailure::OutputLimit | VisibleTitleFailure::EmptyOutput => { | |
| let limited = matches!(case,VisibleTitleFailure::OutputLimit); | |
| let text = if limited {"A partial title"} else {" \n\t\u{0000}\u{202e} "}; | |
| let delta = json!({"id":"title-failure","choices":[{"index":0,"delta":{"content":text},"finish_reason":null}]}); | |
| let end = json!({"id":"title-failure","choices":[{"index":0,"delta":{},"finish_reason":if limited {"length"} else {"stop"}}],"usage":{"prompt_tokens":7,"completion_tokens":9,"total_tokens":16}}); | |
| ("200 OK","text/event-stream",format!("data: {delta}\n\ndata: {end}\n\ndata: [DONE]\n\n"), | |
| json!({"kind":if limited {"output_limit"} else {"empty_output"}}),Outcome::Success) | |
| } | |
| }; | |
| title.write_all(format!("HTTP/1.1 {status}\r\nContent-Type: {content_type}\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",body.len()).as_bytes()).await.unwrap(); | |
| f.drained().await; | |
| let after_failure = now(); | |
| let watched = if let Some((mut watch,initial))=diagnostic_watch { | |
| let late=watch.recv().await.unwrap(); | |
| let revision=f.owner.session(&other).await.unwrap().revision; | |
| f.mutation(2,"metadata",json!({"sessionId":other,"expectedRevision":revision,"title":"Unrelated metadata commit","labels":[]}),Uuid::new_v4()).await.unwrap(); | |
| let unrelated_update=tokio::time::timeout(Duration::from_millis(30),watch.recv()).await.is_ok(); | |
| drop(watch); | |
| Some((initial,late,unrelated_update)) | |
| } else {None}; | |
| let observed = f.query("observe",json!({"sessionId":session})).await.unwrap(); | |
| let record = f.owner.session(&session).await.unwrap(); | |
| let original_attempts = attempts(&f,&session).await; | |
| let diagnostics = f.query("diagnostics",json!({"sessionId":session,"position":{"kind":"latest"}})).await; | |
| let scoped = f.query("diagnostics",json!({"sessionId":other,"position":{"kind":"latest"}})).await; | |
| let foreign = f.query("diagnostics",json!({"sessionId":"foreign_session","position":{"kind":"latest"}})).await; | |
| todo_tests::reopen(&mut f).await; | |
| let restored = f.query("diagnostics",json!({"sessionId":session,"position":{"kind":"latest"}})).await; | |
| let restored_attempts = attempts(&f,&session).await; | |
| let reconnected = if matches!(case,VisibleTitleFailure::Http) { | |
| let mut watch=f.app.client().subscribe(f.binding(2).await,"watchDiagnostics".into(),json!({"sessionId":session}), | |
| scope(&f.issuer,true),Attachments::default().into()).await.unwrap(); | |
| let initial=watch.recv().await.unwrap(); | |
| drop(watch); | |
| Some(initial) | |
| } else {None}; | |
| let redispatched = tokio::time::timeout(Duration::from_millis(30),listener.accept()).await.is_ok(); | |
| f.app.shutdown().await.unwrap(); | |
| f.config.shutdown().await.unwrap(); | |
| assert!(matches!(completed.state,maka_event_log::turns::InvocationState::Ended { | |
| outcome:maka_agent_facts::event::InvocationOutcome::Completed,..})); | |
| assert!(observed["run"].is_null()); | |
| assert_eq!(record.configuration.title,"Untitled"); | |
| assert!(!record.configuration.title_is_manual); | |
| assert_eq!(original_attempts.len(),2); | |
| let auxiliary = original_attempts.iter().find(|attempt|matches!(attempt.origin,Origin::Auxiliary {..})).unwrap(); | |
| assert_eq!(auxiliary.outcome,physical_outcome); | |
| if !matches!(case,VisibleTitleFailure::Http) { | |
| assert_eq!(auxiliary.usage.input_tokens,Some(7)); | |
| assert_eq!(auxiliary.usage.output_tokens,Some(9)); | |
| } | |
| assert_eq!(restored_attempts,original_attempts); | |
| assert!(!redispatched); | |
| let diagnostics = diagnostics.expect("public diagnostics must expose the actual title failure"); | |
| let restored = restored.expect("public diagnostics must persist across restart"); | |
| let scoped = scoped.expect("owned-session diagnostics must be readable"); | |
| assert_eq!(diagnostics["entries"].as_array().unwrap().len(),1); | |
| let failure = &diagnostics["entries"][0]; | |
| let sequence = failure["sequence"].as_u64().expect("diagnostic must identify its durable sequence"); | |
| assert!(sequence > 0); | |
| let event_id = failure["eventId"].as_str().expect("diagnostic must identify its actual event"); | |
| assert!(!event_id.is_empty()); | |
| let recorded_at = failure["recordedAt"].as_f64().expect("diagnostic must include its Host capture time"); | |
| assert!((before_failure as f64..=(after_failure+1) as f64).contains(&recorded_at)); | |
| assert_eq!(*failure,json!({"sequence":sequence,"eventId":event_id,"recordedAt":recorded_at, | |
| "detail":{"kind":"session_title","invocation":completed.invocation,"cause":cause}})); | |
| assert_eq!(restored["entries"],diagnostics["entries"]); | |
| if let Some((initial,late,unrelated_update))=watched { | |
| assert_eq!(initial,json!({"revision":0})); | |
| assert_eq!(late,json!({"revision":sequence}),"late title completion must notify after the main invocation ends"); | |
| assert!(!unrelated_update,"another session's metadata commit must not emit a diagnostic revision"); | |
| assert_eq!(reconnected,Some(json!({"revision":sequence})),"reconnect must begin with the latest diagnostic independent of transcript progress"); | |
| } | |
| assert_eq!(scoped["entries"],json!([]),"failure must not leak into another owned session"); | |
| assert!(foreign.is_err(),"foreign-session diagnostics must remain unauthorized"); | |
| assert!(!diagnostics.to_string().contains(SECRET)); | |
| assert!(!restored.to_string().contains(SECRET)); | |
| }).await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn title_preparation_failure_is_public_without_a_physical_title_attempt() { | |
| tokio::time::timeout(Duration::from_secs(30),async { | |
| let (mut capture, _log_guard) = log_capture(); | |
| let listener=TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| // Automatic dispatch is disabled; this test calls generation once from | |
| // the actual first opening's frozen source after its connection changes. | |
| let base = format!("http://{}/v1", listener.local_addr().unwrap()); | |
| let f=Fixture::new(&base,None).await; | |
| let connection = f.selection().await.connection_id; | |
| let session=automatic(&f).await; | |
| let sent=f.send(&session).await.unwrap(); | |
| let (mut socket,_)=listener.accept().await.unwrap(); | |
| request(&mut socket).await; | |
| let accepted=f.owner.runs.lock().await.controls.get(&session).unwrap().basis.model.clone().unwrap(); | |
| response(&mut socket).await; | |
| f.drained().await; | |
| let opening=f.store.invocation_boundary(&session,sent["invocationId"].as_str().unwrap()).await.unwrap().unwrap().0; | |
| selection::edit(&f,|changes| changes.enabled=Some(false)).await; | |
| f.owner.generate_title(opening.invocation.clone(),accepted).await.unwrap(); | |
| let diagnostics=f.query("diagnostics",json!({"sessionId":session,"position":{"kind":"latest"}})).await; | |
| let actual_attempts=attempts(&f,&session).await; | |
| let record=f.owner.session(&session).await.unwrap(); | |
| let dispatched=tokio::time::timeout(Duration::from_millis(30),listener.accept()).await.is_ok(); | |
| f.app.shutdown().await.unwrap(); | |
| f.config.shutdown().await.unwrap(); | |
| assert!(matches!(opening.state,maka_event_log::turns::InvocationState::Ended { | |
| outcome:maka_agent_facts::event::InvocationOutcome::Completed,..})); | |
| assert_eq!(actual_attempts.len(),1); | |
| assert!(matches!(actual_attempts[0].origin,Origin::Agent {..})); | |
| assert_eq!(actual_attempts[0].outcome,Outcome::Success); | |
| assert!(!dispatched,"capture failure must not invent a physical request or title attempt"); | |
| assert_eq!(record.configuration.title,"Untitled"); | |
| assert!(!record.configuration.title_is_manual); | |
| let diagnostics=diagnostics.expect("actual pre-capture title failure must be public"); | |
| let entries=diagnostics["entries"].as_array().unwrap(); | |
| assert_eq!(entries.len(),1); | |
| assert_eq!(entries[0]["detail"],json!({"kind":"session_title","invocation":opening.invocation,"cause":{"kind":"preparation"}})); | |
| assert!(entries[0]["sequence"].as_u64().is_some_and(|sequence|sequence>0)); | |
| assert!(entries[0]["eventId"].as_str().is_some_and(|id|!id.is_empty())); | |
| assert!(entries[0]["recordedAt"].as_f64().is_some()); | |
| let log = log_text(&mut capture); | |
| let cause = log.lines().find(|line| line.contains("stage=\"accepted_model_observation\"") | |
| && line.contains("cause=\"accepted_source_unavailable\"")) | |
| .expect("title preparation must retain its unavailable accepted-source cause"); | |
| assert!(cause.contains("session_id=") && cause.contains(&session)); | |
| assert!(cause.contains("connection_id=") && cause.contains(&connection)); | |
| for secret in ["private-fixture-key", base.as_str(), "Hello"] { | |
| assert!(!log.contains(secret), "title preparation warning must omit credentials, URL and original input"); | |
| } | |
| }).await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn independently_admitted_title_uses_current_language_after_english_main_capture() { | |
| tokio::time::timeout(Duration::from_secs(45), async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| // Reuse the explicit generation path from title_preparation_failure: | |
| // automatic dispatch is disabled so Settings can change after Main capture. | |
| let f = Fixture::new(&format!("http://{}/v1", listener.local_addr().unwrap()), None).await; | |
| let english = prompt_language::select_language(&f, "en").await; | |
| let session = automatic(&f).await; | |
| let original = "Please investigate title capture 🦀 /keep-original-bytes"; | |
| let sent = f.mutation(2, "send", json!({ | |
| "sessionId":session,"message":original,"model":f.selection().await, | |
| }), Uuid::new_v4()).await.unwrap(); | |
| let (mut main_socket, _) = listener.accept().await.unwrap(); | |
| let main_request = request(&mut main_socket).await; | |
| let accepted = f.owner.runs.lock().await.controls.get(&session).unwrap().basis.model.clone().unwrap(); | |
| let opening = f.store.invocation_boundary(&session, sent["invocationId"].as_str().unwrap()) | |
| .await.unwrap().unwrap().0; | |
| let configuration = f.store.invocation_configuration(&opening.invocation).await.unwrap().unwrap(); | |
| let chinese = prompt_language::select_language(&f, "zh-CN").await; | |
| let (generated, title_request) = tokio::join!( | |
| f.owner.generate_title(opening.invocation, accepted), | |
| async { | |
| let (mut title_socket, _) = listener.accept().await.unwrap(); | |
| let outgoing = request(&mut title_socket).await; | |
| response_with_text(&mut title_socket, "Investigate title capture").await; | |
| outgoing | |
| } | |
| ); | |
| response(&mut main_socket).await; | |
| f.drained().await; | |
| let title = f.owner.session(&session).await.unwrap().configuration.title; | |
| let physical_attempts = attempts(&f, &session).await; | |
| let raw = rusqlite::Connection::open_with_flags( | |
| f.root.join(maka_event_log::root::ROOT_DATABASE), | |
| rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, | |
| ).unwrap(); | |
| let recorded_language: String = raw.query_row( | |
| "SELECT json_extract(event_json, '$.language') FROM event_log WHERE kind='auxiliary_model_started' AND json_extract(event_json, '$.source.kind')='session_title' AND json_extract(event_json, '$.source.invocation.session_id')=?1", | |
| [&session], |row| row.get(0), | |
| ).unwrap(); | |
| drop(raw); | |
| f.app.shutdown().await.unwrap(); | |
| f.config.shutdown().await.unwrap(); | |
| assert_eq!(english["kind"], "committed"); | |
| assert_eq!(chinese["kind"], "committed"); | |
| assert!(generated.is_ok(), "title generation failed: {generated:?}"); | |
| assert_eq!(configuration.language, maka_agent_facts::language::Language::En); | |
| assert_eq!(main_request["messages"].as_array().unwrap().iter() | |
| .find(|message| message["role"] == "user").unwrap()["content"], original); | |
| assert_eq!(title_request["messages"][1]["content"], original); | |
| let instruction = title_request["messages"][0]["content"].as_str().unwrap(); | |
| assert!(instruction.contains("会话标题"), "new title capture must use current Chinese builtin copy: {title_request}"); | |
| assert!(instruction.contains("与消息语言相同"), "the title's output language still follows original user text: {title_request}"); | |
| assert_eq!(recorded_language, "zh-CN"); | |
| assert_eq!(title, "Investigate title capture"); | |
| assert_eq!(physical_attempts.len(), 2); | |
| assert_eq!(physical_attempts.iter().filter(|attempt| matches!(attempt.origin, Origin::Auxiliary { .. })).count(), 1); | |
| }).await.unwrap(); | |
| } |