| /* | |
| * 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. | |
| */ | |
| #[path = "tests/plan.rs"] | |
| mod plan_tests; | |
| #[path = "tests/goal.rs"] | |
| mod goal_tests; | |
| #[path = "tests/recap.rs"] | |
| mod recap_tests; | |
| #[path = "tests/title.rs"] | |
| mod title_tests; | |
| #[path = "tests/todo.rs"] | |
| mod todo_tests; | |
| #[path = "tests/computer.rs"] | |
| mod computer; | |
| #[path = "tests/service_tools.rs"] | |
| mod service_tools; | |
| #[path = "tests/skill_preparation.rs"] | |
| mod skill_preparation; | |
| #[path = "tests/skills.rs"] | |
| mod skills; | |
| #[path = "tests/attachments.rs"] | |
| mod attachments; | |
| #[path = "tests/fork.rs"] | |
| mod fork; | |
| #[path = "tests/files.rs"] | |
| mod files; | |
| #[path = "tests/file_browsing.rs"] | |
| mod file_browsing; | |
| #[path = "tests/file_reconciliation.rs"] | |
| mod file_reconciliation; | |
| #[path = "tests/health.rs"] | |
| mod health; | |
| #[path = "tests/processes.rs"] | |
| mod processes; | |
| #[path = "tests/protocol_processes.rs"] | |
| mod protocol_processes; | |
| #[path = "tests/terminal.rs"] | |
| mod terminal; | |
| #[path = "tests/interactions.rs"] | |
| mod interactions; | |
| #[path = "tests/catalog.rs"] | |
| mod catalog; | |
| #[path = "tests/session_availability.rs"] | |
| mod session_availability; | |
| #[path = "tests/history_search.rs"] | |
| mod history_search; | |
| #[path = "tests/compaction.rs"] | |
| mod compaction; | |
| #[path = "tests/metrics.rs"] | |
| mod metrics; | |
| #[path = "tests/execution.rs"] | |
| mod execution; | |
| #[path = "tests/executor.rs"] | |
| mod executor; | |
| #[path = "tests/prompt_language.rs"] | |
| mod prompt_language; | |
| #[path = "tests/session_context.rs"] | |
| mod session_context; | |
| #[path = "tests/tool_schemas.rs"] | |
| mod tool_schemas; | |
| #[path = "tests/workhub.rs"] | |
| mod workhub; | |
| #[path = "tests/scale.rs"] | |
| #[cfg(unix)] | |
| mod scale; | |
| #[path = "tests/queue.rs"] | |
| mod queue; | |
| #[path = "tests/large_messages.rs"] | |
| mod large_messages; | |
| #[path = "tests/selection.rs"] | |
| mod selection; | |
| #[path = "tests/git.rs"] | |
| mod git; | |
| #[path = "tests/jev.rs"] | |
| mod jev_tests; | |
| #[path = "tests/web.rs"] | |
| mod web_tests; | |
| #[path = "tests/worktrees.rs"] | |
| mod worktrees; | |
| #[path = "tests/settings.rs"] | |
| mod settings; | |
| #[path = "tests/images.rs"] | |
| mod images; | |
| #[path = "tests/imports.rs"] | |
| mod imports; | |
| #[path = "tests/projects.rs"] | |
| mod projects; | |
| #[path = "tests/recall.rs"] | |
| mod recall_tests; | |
| #[path = "tests/scheduler.rs"] | |
| mod scheduler_tests; | |
| #[path = "tests/workspaces.rs"] | |
| mod workspaces; | |
| use super::*; | |
| use crate::{EXPORT, ModelSelection, contract, service::Agent}; | |
| use maka_agent_facts::configuration::*; | |
| use maka_capabilities::call::Attachments; | |
| use maka_capabilities::call::Identity; | |
| use maka_capabilities::call::Issuer; | |
| use maka_capabilities::call::Scope; | |
| use maka_event_log::root::{RootNamespaces, RootOwner}; | |
| use maka_host_runtime::application::{ | |
| Application, Binding, Invocation as HostInvocation, NativeActivation, NativeRegistry, Policy, | |
| }; | |
| use maka_packages::MANIFEST_FILE; | |
| use maka_packages::Package; | |
| use maka_plugin_core::{ | |
| composition::{ | |
| Configuration as NodeConfiguration, Environment, ExecutionLocation, Input, InstanceId, | |
| Node, PackageUse, Target, | |
| }, | |
| contract::{Availability, Cardinality, Export, Import}, | |
| operation::{OperationOutcome, Receipt}, | |
| package::{Declaration, EntryDeclaration, EntryTarget, Implementation, Realm}, | |
| }; | |
| use serde_json::json; | |
| use std::sync::Mutex as StdMutex; | |
| use tokio::io::{AsyncReadExt, AsyncWriteExt}; | |
| use tokio::net::{TcpListener, TcpStream}; | |
| use tokio::sync::Semaphore; | |
| struct CallerGrant(maka_host_runtime::application::Caller); | |
| struct Grant(Issuer, Option<Arc<maka_transport::transport::PeerPolicy>>); | |
| impl Policy for Grant { | |
| fn caller(&self, scope: &Scope) -> Option<maka_host_runtime::application::Caller> { | |
| self.0.evidence::<CallerGrant>(scope).map(|e| e.0.clone()) | |
| } | |
| fn recover( | |
| &self, | |
| invocation: maka_host_runtime::application::RecoveryInvocation<'_>, | |
| ) -> Result<(), String> { | |
| if invocation.native_origin.is_some() { | |
| return self | |
| .1 | |
| .as_ref() | |
| .ok_or("native recovery denied")? | |
| .recover(invocation); | |
| } | |
| if self.0.evidence::<bool>(invocation.scope) == Some(&true) | |
| || self.0.evidence::<CallerGrant>(invocation.scope).is_some() | |
| { | |
| Ok(()) | |
| } else { | |
| Err("denied".into()) | |
| } | |
| } | |
| fn admit(&self, invocation: HostInvocation<'_>) -> Result<(), String> { | |
| if invocation.native_origin.is_some() { | |
| return self | |
| .1 | |
| .as_ref() | |
| .ok_or("native tools denied")? | |
| .admit(invocation); | |
| } | |
| if self.0.evidence::<bool>(invocation.scope) == Some(&true) | |
| || self.0.evidence::<CallerGrant>(invocation.scope).is_some() | |
| { | |
| Ok(()) | |
| } else { | |
| Err("denied".into()) | |
| } | |
| } | |
| } | |
| fn scope(issuer: &Issuer, grant: bool) -> Scope { | |
| issuer | |
| .issue_with( | |
| Identity::Remote { | |
| request_id: Uuid::new_v4(), | |
| }, | |
| grant, | |
| CancellationToken::new(), | |
| ) | |
| .unwrap() | |
| } | |
| fn id(value: u128) -> InstanceId { | |
| InstanceId(vec![Uuid::from_u128(value)]) | |
| } | |
| fn package(provider: bool) -> Package { | |
| let entry = EntryDeclaration { | |
| scene: None, | |
| implementation: if provider { | |
| Implementation::Native { | |
| implementation: IMPLEMENTATION.into(), | |
| } | |
| } else { | |
| Implementation::JavaScript { | |
| module: "host.mjs".into(), | |
| realm: Realm::Shared, | |
| } | |
| }, | |
| configuration: if provider { | |
| configuration_schema().into() | |
| } else { | |
| json!({"type":"object","additionalProperties":false}).into() | |
| }, | |
| imports: if provider { | |
| BTreeMap::new() | |
| } else { | |
| BTreeMap::from([( | |
| EXPORT.into(), | |
| Import { | |
| contract: contract(), | |
| cardinality: Cardinality::One, | |
| }, | |
| )]) | |
| }, | |
| exports: if provider { | |
| BTreeMap::from([( | |
| EXPORT.into(), | |
| Export { | |
| contract: contract(), | |
| availability: Availability::Required, | |
| }, | |
| )]) | |
| } else { | |
| BTreeMap::new() | |
| }, | |
| }; | |
| let declaration = Declaration { | |
| entries: BTreeMap::from([(EntryTarget::Host, entry)]), | |
| composition: None, | |
| }; | |
| let mut files = BTreeMap::from([(MANIFEST_FILE.into(), serde_json::to_vec(&json!({"schemaVersion":1,"id":if provider {"agent"} else {"consumer"},"application":declaration})).unwrap())]); | |
| if !provider { | |
| files.insert( | |
| "host.mjs".into(), | |
| b"export function activate() { return {}; }".to_vec(), | |
| ); | |
| } | |
| Package::new(files).unwrap() | |
| } | |
| fn input(settings: &Configuration) -> (Input, Vec<Package>) { | |
| let provider = package(true); | |
| let consumer = package(false); | |
| let node = |key: u128, provider_node: bool| Node { | |
| key: Uuid::from_u128(key), | |
| package: if provider_node { | |
| provider.application_ref() | |
| } else { | |
| consumer.application_ref() | |
| } | |
| .into(), | |
| use_package: PackageUse::Entry, | |
| configuration: if provider_node { | |
| NodeConfiguration { | |
| values: serde_json::to_value(settings) | |
| .unwrap() | |
| .as_object() | |
| .unwrap() | |
| .iter() | |
| .map(|(k, v)| (k.clone(), v.clone())) | |
| .collect(), | |
| parameters: BTreeMap::new(), | |
| } | |
| } else { | |
| NodeConfiguration::default() | |
| }, | |
| parameters: NodeConfiguration::default(), | |
| disabled: false, | |
| overrides: vec![], | |
| bindings: if provider_node { | |
| BTreeMap::new() | |
| } else { | |
| BTreeMap::from([( | |
| EXPORT.into(), | |
| vec![Target::Instance { | |
| path: vec!["provider".into()], | |
| export: EXPORT.into(), | |
| }], | |
| )]) | |
| }, | |
| }; | |
| let receipt_contract = maka_plugin_core::operation::receipt_reader_contract(); | |
| let receipts = Package::new(BTreeMap::from([( | |
| MANIFEST_FILE.into(), | |
| serde_json::to_vec(&json!({ | |
| "schemaVersion":1,"id":"receipts","application":{"entries":{"host":{ | |
| "implementation":{"kind":"native","implementation":"maka.operations.receipts"}, | |
| "configuration":{"type":"object","additionalProperties":false},"imports":{}, | |
| "exports":{"Receipts":{"contract":receipt_contract,"availability":"required"}} | |
| }}} | |
| })) | |
| .unwrap(), | |
| )])) | |
| .unwrap(); | |
| let receipt_node = Node { | |
| key: Uuid::from_u128(4), | |
| package: receipts.application_ref().into(), | |
| use_package: PackageUse::Entry, | |
| configuration: Default::default(), | |
| parameters: Default::default(), | |
| bindings: BTreeMap::new(), | |
| disabled: false, | |
| overrides: vec![], | |
| }; | |
| let mut input = Input { | |
| environment: Environment { | |
| location: ExecutionLocation::Host, | |
| presentation: None, | |
| }, | |
| instances: BTreeMap::from([ | |
| ("provider".into(), node(1, true)), | |
| ("a".into(), node(2, false)), | |
| ("b".into(), node(3, false)), | |
| ]), | |
| recovery: None, | |
| launch: None, | |
| }; | |
| input.instances.insert("receipts".into(), receipt_node); | |
| for name in ["a", "b"] { | |
| input.instances.get_mut(name).unwrap().bindings.insert( | |
| "receipts".into(), | |
| vec![Target::Instance { | |
| path: vec!["receipts".into()], | |
| export: "Receipts".into(), | |
| }], | |
| ); | |
| } | |
| // Both consumers declare recovery independently of their execution import. | |
| let mut files = consumer.files().clone(); | |
| let mut manifest = consumer.manifest().clone(); | |
| let mut manifest_application = manifest.application.as_ref().unwrap().declaration().clone(); | |
| manifest_application | |
| .entries | |
| .get_mut(&EntryTarget::Host) | |
| .unwrap() | |
| .imports | |
| .insert( | |
| "receipts".into(), | |
| Import { | |
| contract: receipt_contract, | |
| cardinality: Cardinality::One, | |
| }, | |
| ); | |
| files.insert(MANIFEST_FILE.into(), { | |
| manifest.application = | |
| Some(maka_plugin_core::package::CheckedDeclaration::new(manifest_application).unwrap()); | |
| serde_json::to_vec(&manifest).unwrap() | |
| }); | |
| let consumer = Package::new(files).unwrap(); | |
| for name in ["a", "b"] { | |
| input.instances.get_mut(name).unwrap().package = consumer.application_ref().into(); | |
| } | |
| let settings = { | |
| let mut manifest = provider.manifest().clone(); | |
| let mut manifest_application = manifest.application.as_ref().unwrap().declaration().clone(); | |
| manifest.id = "settings".into(); | |
| let entry = manifest_application | |
| .entries | |
| .get_mut(&EntryTarget::Host) | |
| .unwrap(); | |
| entry.implementation = Implementation::Native { | |
| implementation: maka_settings_native::IMPLEMENTATION.into(), | |
| }; | |
| entry.configuration = | |
| json!({"type":"object","properties":{},"additionalProperties":false}).into(); | |
| entry.exports = BTreeMap::from([( | |
| maka_settings_native::EXPORT.into(), | |
| Export { | |
| contract: maka_settings_native::contract::contract(), | |
| availability: Availability::Required, | |
| }, | |
| )]); | |
| Package::new(BTreeMap::from([(MANIFEST_FILE.into(), { | |
| manifest.application = Some( | |
| maka_plugin_core::package::CheckedDeclaration::new(manifest_application).unwrap(), | |
| ); | |
| serde_json::to_vec(&manifest).unwrap() | |
| })])) | |
| .unwrap() | |
| }; | |
| let mut files = consumer.files().clone(); | |
| let mut manifest = consumer.manifest().clone(); | |
| let mut manifest_application = manifest.application.as_ref().unwrap().declaration().clone(); | |
| manifest_application | |
| .entries | |
| .get_mut(&EntryTarget::Host) | |
| .unwrap() | |
| .imports | |
| .insert( | |
| "settings".into(), | |
| Import { | |
| contract: maka_settings_native::contract::contract(), | |
| cardinality: Cardinality::One, | |
| }, | |
| ); | |
| files.insert(MANIFEST_FILE.into(), { | |
| manifest.application = | |
| Some(maka_plugin_core::package::CheckedDeclaration::new(manifest_application).unwrap()); | |
| serde_json::to_vec(&manifest).unwrap() | |
| }); | |
| let consumer = Package::new(files).unwrap(); | |
| input.instances.insert( | |
| "settings".into(), | |
| Node { | |
| key: Uuid::from_u128(5), | |
| package: settings.application_ref().into(), | |
| use_package: PackageUse::Entry, | |
| configuration: NodeConfiguration::default(), | |
| parameters: NodeConfiguration::default(), | |
| disabled: false, | |
| overrides: vec![], | |
| bindings: BTreeMap::new(), | |
| }, | |
| ); | |
| for name in ["a", "b"] { | |
| let node = input.instances.get_mut(name).unwrap(); | |
| node.package = consumer.application_ref().into(); | |
| node.bindings.insert( | |
| "settings".into(), | |
| vec![Target::Instance { | |
| path: vec!["settings".into()], | |
| export: maka_settings_native::EXPORT.into(), | |
| }], | |
| ); | |
| } | |
| (input, vec![provider, consumer, receipts, settings]) | |
| } | |
| // The default dispatcher is thread-local: hold the guard through current-thread | |
| // fixture shutdown so actual producer tasks remain inside the capture. | |
| fn log_capture() -> (std::fs::File, tracing::subscriber::DefaultGuard) { | |
| let file = tempfile::tempfile().unwrap(); | |
| let subscriber = tracing_subscriber::fmt() | |
| .with_writer(file.try_clone().unwrap()) | |
| .with_max_level(tracing::Level::WARN) | |
| .with_ansi(false) | |
| .without_time() | |
| .finish(); | |
| (file, tracing::subscriber::set_default(subscriber)) | |
| } | |
| fn log_text(file: &mut std::fs::File) -> String { | |
| use std::io::{Read, Seek}; | |
| file.rewind().unwrap(); | |
| let mut text = String::new(); | |
| file.read_to_string(&mut text).unwrap(); | |
| text | |
| } | |
| fn bundled_catalog() -> Arc<maka_providers::catalog::Catalog> { | |
| Arc::new(maka_providers::catalog::Catalog::new( | |
| maka_providers::catalog::Snapshot::bundled().unwrap(), | |
| )) | |
| } | |
| struct Fixture { | |
| services: maka_host_runtime::application::NativeServices, | |
| _dir: tempfile::TempDir, | |
| namespaces: RootNamespaces, | |
| root: std::path::PathBuf, | |
| workspace: std::path::PathBuf, | |
| store: Arc<EventLog>, | |
| config: Arc<ConfigurationStore>, | |
| app: Application, | |
| issuer: Issuer, | |
| owner: Arc<Owner>, | |
| } | |
| impl Fixture { | |
| async fn new(base: &str, model: Option<ModelExecutor>) -> Self { | |
| Self::with_peer(base, model, None).await | |
| } | |
| async fn with_peer( | |
| base: &str, | |
| model: Option<ModelExecutor>, | |
| peer: Option<Arc<maka_transport::transport::PeerPolicy>>, | |
| ) -> Self { | |
| Self::with_services(base, model, peer, None).await | |
| } | |
| async fn with_services( | |
| base: &str, | |
| model: Option<ModelExecutor>, | |
| peer: Option<Arc<maka_transport::transport::PeerPolicy>>, | |
| tooling: Option<service_tools::Setup>, | |
| ) -> Self { | |
| Self::configured(base, model, peer, tooling, |_| {}).await | |
| } | |
| async fn configured( | |
| base: &str, | |
| model: Option<ModelExecutor>, | |
| peer: Option<Arc<maka_transport::transport::PeerPolicy>>, | |
| tooling: Option<service_tools::Setup>, | |
| configure: impl FnOnce(&mut Configuration), | |
| ) -> Self { | |
| Self::configured_provider(base, model, peer, tooling, configure, "openai-compatible").await | |
| } | |
| async fn configured_provider( | |
| base: &str, | |
| model: Option<ModelExecutor>, | |
| peer: Option<Arc<maka_transport::transport::PeerPolicy>>, | |
| tooling: Option<service_tools::Setup>, | |
| configure: impl FnOnce(&mut Configuration), | |
| provider_name: &str, | |
| ) -> Self { | |
| let dir = tempfile::tempdir().unwrap(); | |
| let root = dir.path().join("root"); | |
| let workspace = dir.path().join("workspace"); | |
| let other_workspace = dir.path().join("other-workspace"); | |
| std::fs::create_dir(&workspace).unwrap(); | |
| std::fs::create_dir(&other_workspace).unwrap(); | |
| let namespaces = RootNamespaces { | |
| ownership: dir.path().join("owners"), | |
| control: dir.path().join("control"), | |
| }; | |
| let root_owner = Arc::new(RootOwner::create(&root, &namespaces).unwrap()); | |
| let store = Arc::new(EventLog::for_root(root_owner.clone()).await.unwrap()); | |
| let config = Arc::new(ConfigurationStore::for_root(root_owner).await.unwrap()); | |
| let provider = json!({"packageId":maka_providers::api::ID,"entryId":maka_providers::api::ID,"scope":"profile","name":provider_name}); | |
| let create = serde_json::from_value(json!({"expectedCatalogRevision":0,"connection":{"slug":"fixture","name":"Fixture","provider":provider,"configuration":{"baseUrl":base},"enabled":true,"enabledModelIds":["fixture-model"]}})).unwrap(); | |
| let CatalogMutationResult::Committed { | |
| connection: Some(basis), | |
| .. | |
| } = config.create_connection(create).await.unwrap() | |
| else { | |
| panic!("connection commit"); | |
| }; | |
| let maka_config::oauth::enrollment::LoginPreparation::Ready(ticket) = config | |
| .prepare_oauth_login(maka_agent_facts::oauth::LoginStart { | |
| expected_credential: None, | |
| attempt_id: Uuid::new_v4().to_string(), | |
| target: maka_agent_facts::oauth::Target::Existing { | |
| expected: ConnectionCredentialTarget { | |
| connection_id: basis.connection_id.clone(), | |
| revision: basis.revision, | |
| provider: serde_json::from_value(provider).unwrap(), | |
| slug: "fixture".into(), | |
| configuration: json!({"baseUrl":base}), | |
| }, | |
| configuration: json!({"baseUrl":base}), | |
| }, | |
| authentication: maka_agent_facts::provider::AuthenticationInput { | |
| method: "api-key".into(), | |
| input: json!({"apiKey":"private-fixture-key"}), | |
| }, | |
| }) | |
| .await | |
| .unwrap() | |
| else { | |
| panic!("actual login preparation"); | |
| }; | |
| assert!(ticket.claim().await.unwrap()); | |
| assert!(matches!( | |
| ticket | |
| .complete( | |
| maka_agent_facts::provider::Credential { | |
| secret: "private-fixture-key".into(), | |
| refresh_at: None | |
| }, | |
| 1 | |
| ) | |
| .await | |
| .unwrap(), | |
| maka_config::oauth::enrollment::LoginCompletion::Committed(_) | |
| )); | |
| let mut settings = Configuration { | |
| locale: String::new(), | |
| tool_discovery: false, | |
| isolated_workspaces: false, | |
| assistant: None, | |
| scheduled_tasks: false, | |
| goals: false, | |
| plans: false, | |
| todo: false, | |
| automatic_titles: false, | |
| recaps: false, | |
| recall: false, | |
| import_roots: vec![maka_host_runtime::DirectoryRootSpec { | |
| label: "Import sources".into(), | |
| path: workspace.clone(), | |
| }], | |
| executors: vec![], | |
| skill_library: tooling | |
| .as_ref() | |
| .and_then(service_tools::Setup::skill_import), | |
| permissions: WorkspacePermissions { | |
| access: WorkspaceAccess::WorkspaceWrite, | |
| request_permissions: true, | |
| }, | |
| workspace_roots: vec![ | |
| maka_host_runtime::DirectoryRootSpec { | |
| label: "Workspace".into(), | |
| path: workspace.clone(), | |
| }, | |
| maka_host_runtime::DirectoryRootSpec { | |
| label: "Other workspace".into(), | |
| path: other_workspace, | |
| }, | |
| ], | |
| tools: vec![], | |
| }; | |
| configure(&mut settings); | |
| if let Some(tooling) = &tooling { | |
| settings.tools = tooling.selections(); | |
| } | |
| let (mut input, mut packages) = input(&settings); | |
| if let Some(tooling) = &tooling { | |
| tooling.install(&mut input, &mut packages); | |
| } | |
| let deployment = Uuid::new_v4(); | |
| store | |
| .commit_application(deployment, 0, input, vec![], packages) | |
| .await | |
| .unwrap(); | |
| let captured = Arc::new(StdMutex::new(None)); | |
| let capture = captured.clone(); | |
| let replacement = Arc::new(StdMutex::new(model)); | |
| let mut native = NativeRegistry::default(); | |
| let catalog = bundled_catalog(); | |
| maka_settings_native::provide(&mut native, catalog.clone()).unwrap(); | |
| if let Some(tooling) = &tooling { | |
| tooling.provide(&mut native); | |
| } | |
| // Actual Host NativeContext and original NativeOwner; only the model | |
| // implementation is controlled in failure tests, never Root/admission. | |
| native | |
| .insert( | |
| IMPLEMENTATION.into(), | |
| Arc::new(move |context, configuration| { | |
| let capture = capture.clone(); | |
| let catalog = catalog.clone(); | |
| let replacement = replacement.lock().unwrap().take(); | |
| Box::pin(async move { | |
| let services = context.services(); | |
| let mut owner = Owner::open(context, configuration, catalog).await.unwrap(); | |
| if let Some(model) = replacement { | |
| let store = owner.store.clone(); | |
| Arc::get_mut(&mut owner).unwrap().engine = Engine::new( | |
| store, | |
| model, | |
| maka_tools::CodeExecutor::new(Default::default()).unwrap(), | |
| ); | |
| } | |
| *capture.lock().unwrap() = Some((owner.clone(), services)); | |
| let service = Arc::new(Agent(owner)); | |
| NativeActivation { | |
| owner: service.clone(), | |
| exports: Ok(BTreeMap::from([( | |
| EXPORT.into(), | |
| maka_plugin_core::runtime::Published { | |
| contract: contract(), | |
| value: service | |
| as Arc<dyn maka_host_runtime::application::Handler>, | |
| }, | |
| )])), | |
| } | |
| }) | |
| }), | |
| ) | |
| .unwrap(); | |
| let (issuer, policy): (Issuer, Arc<dyn Policy>) = match peer { | |
| Some(peer) => (peer.issuer(), peer), | |
| None => { | |
| let native = tooling.as_ref().map(service_tools::Setup::policy); | |
| let issuer = native | |
| .as_ref() | |
| .map(|policy| policy.issuer()) | |
| .unwrap_or_default(); | |
| (issuer.clone(), Arc::new(Grant(issuer, native))) | |
| } | |
| }; | |
| let app = Application::start_from_store( | |
| store.clone(), | |
| deployment, | |
| native, | |
| issuer.clone(), | |
| policy, | |
| Some(maka_host_js::factory(Default::default())), | |
| ) | |
| .await | |
| .unwrap(); | |
| app.ready().await.unwrap(); | |
| let (owner, services) = captured.lock().unwrap().take().unwrap(); | |
| Self { | |
| _dir: dir, | |
| services, | |
| namespaces, | |
| root, | |
| workspace: workspace.canonicalize().unwrap(), | |
| store, | |
| config, | |
| app, | |
| issuer, | |
| owner, | |
| } | |
| } | |
| async fn binding(&self, consumer: u128) -> Binding { | |
| let client = self.app.client(); | |
| client | |
| .bind(client.activation(&id(consumer)).unwrap(), EXPORT.into()) | |
| .await | |
| .unwrap() | |
| .remove(0) | |
| } | |
| async fn mutation( | |
| &self, | |
| consumer: u128, | |
| method: &str, | |
| input: Value, | |
| operation: Uuid, | |
| ) -> Result<Value, String> { | |
| self.mutation_with(consumer, method, input, operation, Attachments::new()) | |
| .await | |
| } | |
| async fn mutation_with( | |
| &self, | |
| consumer: u128, | |
| method: &str, | |
| input: Value, | |
| operation: Uuid, | |
| attachments: Attachments, | |
| ) -> Result<Value, String> { | |
| let client = self.app.client(); | |
| let prepared = client | |
| .prepare_mutation( | |
| self.binding(consumer).await, | |
| method.into(), | |
| input, | |
| scope(&self.issuer, true), | |
| operation, | |
| attachments.into(), | |
| ) | |
| .await?; | |
| // Exercise the actual trusted Host gateway, including its durable caller | |
| // intent and original activation evidence, rather than reloading the | |
| // complete deployment in the test driver for every mutation. | |
| let reply = client.mutate_owned(prepared).await?.result().await; | |
| match reply.receipt { | |
| Receipt::Recorded { | |
| outcome: OperationOutcome::Returned { value }, | |
| .. | |
| } => Ok(value), | |
| Receipt::Recorded { | |
| outcome: OperationOutcome::Unknown { diagnostic }, | |
| .. | |
| } => Err(diagnostic), | |
| Receipt::Unavailable { diagnostic } => Err(diagnostic), | |
| other => panic!("unexpected receipt {other:?}"), | |
| } | |
| } | |
| async fn create(&self, title: &str) -> String { | |
| self.mutation(2, "create", self.create_input(title).await, Uuid::new_v4()) | |
| .await | |
| .unwrap()["session"]["id"] | |
| .as_str() | |
| .unwrap() | |
| .into() | |
| } | |
| async fn create_input(&self, title: &str) -> Value { | |
| let roots = self | |
| .query("directories", json!({"kind":"roots"})) | |
| .await | |
| .unwrap(); | |
| json!({"title":title, "labels":[], "workspace":{"kind":"directory","selection":roots["roots"][0]["selection"]}}) | |
| } | |
| async fn selection(&self) -> ModelSelection { | |
| let row = self.config.catalog().await.unwrap().connections.remove(0); | |
| ModelSelection { | |
| connection_id: row.connection_id, | |
| revision: row.revision, | |
| model_id: "fixture-model".into(), | |
| } | |
| } | |
| async fn send(&self, session: &str) -> Result<Value, String> { | |
| self.mutation( | |
| 2, | |
| "send", | |
| json!({"sessionId":session,"message":"Hello","model":self.selection().await}), | |
| Uuid::new_v4(), | |
| ) | |
| .await | |
| } | |
| async fn query(&self, method: &str, input: Value) -> Result<Value, String> { | |
| self.app | |
| .client() | |
| .start( | |
| self.binding(2).await, | |
| method.into(), | |
| input, | |
| scope(&self.issuer, true), | |
| Attachments::new().into(), | |
| ) | |
| .await? | |
| .result() | |
| .await | |
| } | |
| async fn drained(&self) { | |
| while !self.owner.workers.is_empty() { | |
| tokio::task::yield_now().await; | |
| } | |
| } | |
| } | |
| async fn request(socket: &mut TcpStream) -> Value { | |
| request_with_key(socket, "private-fixture-key").await | |
| } | |
| async fn request_with_key(socket: &mut TcpStream, key: &str) -> Value { | |
| let mut bytes = Vec::new(); | |
| let boundary = loop { | |
| let mut buffer = [0; 4096]; | |
| let count = socket.read(&mut buffer).await.unwrap(); | |
| assert_ne!( | |
| count, | |
| 0, | |
| "EOF before complete HTTP headers: buffered_bytes={}", | |
| bytes.len() | |
| ); | |
| bytes.extend_from_slice(&buffer[..count]); | |
| assert!(bytes.len() < 512 * 1024); | |
| if let Some(at) = bytes.windows(4).position(|window| window == b"\r\n\r\n") { | |
| break at + 4; | |
| } | |
| }; | |
| let headers = String::from_utf8_lossy(&bytes[..boundary]); | |
| assert!( | |
| headers | |
| .to_ascii_lowercase() | |
| .contains(&format!("authorization: bearer {key}")) | |
| ); | |
| let length: usize = headers | |
| .lines() | |
| .find_map(|line| { | |
| let (name, value) = line.split_once(':')?; | |
| name.eq_ignore_ascii_case("content-length") | |
| .then(|| value.trim().parse().unwrap()) | |
| }) | |
| .unwrap(); | |
| while bytes.len() < boundary + length { | |
| let mut buffer = [0; 4096]; | |
| let count = socket.read(&mut buffer).await.unwrap(); | |
| assert_ne!(count, 0); | |
| bytes.extend_from_slice(&buffer[..count]); | |
| } | |
| serde_json::from_slice(&bytes[boundary..boundary + length]).unwrap() | |
| } | |
| async fn response(socket: &mut TcpStream) { | |
| response_with_text(socket, "Elegant response").await; | |
| } | |
| async fn response_with_text(socket: &mut TcpStream, text: &str) { | |
| response_with_thinking(socket, text, None).await; | |
| } | |
| async fn response_with_thinking(socket: &mut TcpStream, text: &str, thinking: Option<&str>) { | |
| let reasoning = thinking.map(|text| format!("data: {}\n\n", json!({"id":"fixture","choices":[{"index":0,"delta":{"reasoning_content":text},"finish_reason":null}]}))).unwrap_or_default(); | |
| let delta = json!({"id":"fixture","choices":[{"index":0,"delta":{"content":text},"finish_reason":null}]}); | |
| let end = json!({"id":"fixture","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":1,"completion_tokens":2,"total_tokens":3}}); | |
| let body = format!("{reasoning}data: {delta}\n\ndata: {end}\n\ndata: [DONE]\n\n"); | |
| socket.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(); | |
| } | |
| #[tokio::test] | |
| async fn canonical_history_is_bounded_utf8_and_keeps_its_original_fence() { | |
| tokio::time::timeout(Duration::from_secs(30), async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let base = format!("http://{}/v1", listener.local_addr().unwrap()); | |
| let reply = "中文👩🏽💻\u{1}".repeat(700); | |
| let response_text = reply.clone(); | |
| let server = tokio::spawn(async move { | |
| for turn in 0..2 { | |
| let (mut socket, _) = listener.accept().await.unwrap(); | |
| request(&mut socket).await; | |
| let text = if turn == 0 { &response_text } else { "Later response" }; | |
| let delta = json!({"id":"history","choices":[{"index":0,"delta":{"content":text},"finish_reason":null}]}); | |
| let body = format!("data: {delta}\n\ndata: {{\"id\":\"history\",\"choices\":[{{\"index\":0,\"delta\":{{}},\"finish_reason\":\"stop\"}}]}}\n\ndata: [DONE]\n\n"); | |
| socket.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(); | |
| } | |
| }); | |
| let fixture = Fixture::new(&base, None).await; | |
| let session = fixture.create("History").await; | |
| let message = format!("{}中文", "\u{0}".repeat(9000)); | |
| fixture.mutation(2, "send", json!({"sessionId":session,"message":message,"model":fixture.selection().await}), Uuid::new_v4()).await.unwrap(); | |
| fixture.drained().await; | |
| let mut input = json!({"sessionId":session,"position":{"kind":"start"}}); | |
| let mut user = String::new(); | |
| let mut assistant = String::new(); | |
| let mut fence = None; | |
| let mut later = false; | |
| let mut finished = false; | |
| for _ in 0..128 { | |
| let page = fixture.query("history", input.clone()).await.unwrap(); | |
| assert!(serde_json::to_vec(&page).unwrap().len() < 64 * 1024); | |
| assert!(!page.to_string().contains("private-fixture-key")); | |
| let through = page["through"].as_u64().unwrap(); | |
| assert_eq!(*fence.get_or_insert(through), through); | |
| input["through"] = json!(through); | |
| assert_eq!(page["kind"], "ready"); | |
| let chunks = page["chunks"].as_array().unwrap(); | |
| assert!(chunks.len() <= 2); | |
| assert!(chunks.iter().map(|chunk| chunk["text"].as_str().unwrap().len()).sum::<usize>() <= 32 * 1024); | |
| for chunk in chunks { | |
| let text = chunk["text"].as_str().unwrap(); | |
| assert!(text.len() <= 32 * 1024); | |
| match chunk["role"].as_str().unwrap() { | |
| "user" => user.push_str(text), | |
| "assistant" => assistant.push_str(text), | |
| role => panic!("unexpected conversation role {role}"), | |
| } | |
| assert!(chunk.get("attachments").is_none()); | |
| } | |
| if !later { | |
| fixture.send(&session).await.unwrap(); | |
| fixture.drained().await; | |
| later = true; | |
| } | |
| if page["after"].is_null() { finished = true; break; } | |
| input["position"] = json!({"kind":"after","cursor":page["after"]}); | |
| } | |
| assert!(finished); | |
| assert_eq!(user, message); | |
| assert_eq!(assistant, reply); | |
| let event_fence = fence.unwrap(); | |
| let mut input = json!({"sessionId":session,"position":{"kind":"event","sequence":event_fence + 1}}); | |
| let mut newer_fence = None; | |
| let mut newer_user = String::new(); | |
| let mut newer_assistant = String::new(); | |
| let mut finished = false; | |
| for _ in 0..128 { | |
| let page = fixture.query("history", input.clone()).await.unwrap(); | |
| let through = page["through"].as_u64().unwrap(); | |
| assert_eq!(*newer_fence.get_or_insert(through), through); | |
| input["through"] = json!(through); | |
| assert_eq!(page["kind"], "ready"); | |
| for chunk in page["chunks"].as_array().unwrap() { | |
| assert!(chunk["position"]["sourceSequence"].as_u64().unwrap() > event_fence); | |
| match chunk["role"].as_str().unwrap() { | |
| "user" => newer_user.push_str(chunk["text"].as_str().unwrap()), | |
| "assistant" => newer_assistant.push_str(chunk["text"].as_str().unwrap()), | |
| role => panic!("unexpected conversation role {role}"), | |
| } | |
| } | |
| if page["after"].is_null() { finished = true; break; } | |
| input["position"] = json!({"kind":"after","cursor":page["after"]}); | |
| } | |
| assert!(finished); | |
| assert_eq!(newer_user, "Hello"); | |
| assert_eq!(newer_assistant, "Later response"); | |
| let search = |through| json!({"sessionId":session,"through":through,"after":null,"query":"Later response","caseSensitive":true,"wholeWord":false,"includeInternal":false}); | |
| let original_search = fixture.query("search", search(event_fence)).await.unwrap(); | |
| assert_eq!(original_search["matches"], json!([])); | |
| let current_search = fixture.query("search", search(newer_fence.unwrap())).await.unwrap(); | |
| assert_eq!(current_search["matches"].as_array().unwrap().len(), 1); | |
| let match_position = current_search["matches"][0]["position"].clone(); | |
| let located = fixture.query("history", json!({"sessionId":session,"through":newer_fence.unwrap(),"position":{"kind":"row","position":match_position,"field":"body"}})).await.unwrap(); | |
| assert_eq!(located["chunks"][0]["text"], "Later response"); | |
| let tail = fixture.query("history", json!({"sessionId":session,"through":event_fence,"position":{"kind":"tail"}})).await.unwrap(); | |
| let cursor = tail["before"].clone(); | |
| assert!(cursor.is_object()); | |
| for kind in ["before", "after"] { | |
| assert!(fixture.query("history", json!({"sessionId":session,"position":{"kind":kind,"cursor":cursor}})).await.is_err(), "cursor navigation must carry its captured fence"); | |
| } | |
| let mut reverse_input = json!({"sessionId":session,"through":event_fence,"position":{"kind":"tail"}}); | |
| let (mut reverse_user, mut reverse_assistant) = (String::new(), String::new()); | |
| let mut reverse_done = false; | |
| for _ in 0..128 { | |
| let page = fixture.query("history", reverse_input.clone()).await.unwrap(); | |
| assert_eq!(page["through"], event_fence); | |
| assert!(serde_json::to_vec(&page).unwrap().len() < 64 * 1024); | |
| assert_eq!(page["kind"], "ready"); | |
| for chunk in page["chunks"].as_array().unwrap().iter().rev() { | |
| let text = chunk["text"].as_str().unwrap(); | |
| match chunk["role"].as_str().unwrap() { | |
| "user" => reverse_user.insert_str(0, text), | |
| "assistant" => reverse_assistant.insert_str(0, text), | |
| role => panic!("unexpected role {role}"), | |
| } | |
| } | |
| if page["before"].is_null() { reverse_done = true; break; } | |
| reverse_input["position"] = json!({"kind":"before","cursor":page["before"]}); | |
| } | |
| assert!(reverse_done); | |
| assert_eq!(reverse_user, message); | |
| assert_eq!(reverse_assistant, reply); | |
| // Sampled navigation uses the same canonical rows and immutable fence. | |
| let empty = fixture.query("historyLandmarks", json!({"sessionId":session,"through":0})).await.unwrap(); | |
| assert_eq!(empty, json!({"through":0,"landmarks":[]})); | |
| let landmarks = fixture.query("historyLandmarks", json!({"sessionId":session,"through":event_fence})).await.unwrap(); | |
| assert_eq!(landmarks["through"], event_fence); | |
| assert!(serde_json::to_vec(&landmarks).unwrap().len() <= 64 * 1024); | |
| let original_landmarks = landmarks["landmarks"].as_array().unwrap(); | |
| assert_eq!(original_landmarks.len(), 1); | |
| let start = fixture.query("history", json!({"sessionId":session,"through":event_fence,"position":{"kind":"start"}})).await.unwrap(); | |
| assert_eq!(original_landmarks[0]["turnId"], start["chunks"][0]["turnId"]); | |
| assert_eq!(original_landmarks[0]["position"], start["chunks"][0]["position"]); | |
| assert_eq!(original_landmarks[0]["label"], "\0".repeat(96)); | |
| let current = fixture.query("historyLandmarks", json!({"sessionId":session,"through":newer_fence.unwrap()})).await.unwrap(); | |
| assert_eq!(current["through"], newer_fence.unwrap()); | |
| assert!(serde_json::to_vec(¤t).unwrap().len() <= 64 * 1024); | |
| let current_landmarks = current["landmarks"].as_array().unwrap(); | |
| assert_eq!(current_landmarks.len(), 2); | |
| assert_eq!(current_landmarks[0], original_landmarks[0]); | |
| let raw = fixture.store.session_events(&session, 0, newer_fence.unwrap(), 512, 1024 * 1024).await.unwrap(); | |
| assert!(raw.next_after.is_none()); | |
| let endings = raw.events.iter().filter(|stored| matches!(&stored.event.fact, maka_agent_facts::event::Fact::InvocationEnded {outcome:maka_agent_facts::event::InvocationOutcome::Completed})).collect::<Vec<_>>(); | |
| assert_eq!(endings.len(), 2); | |
| // Successful settlement adds a raw turn_state row that conversation | |
| // pages omit; the returned bound must retain that canonical endpoint. | |
| for (landmark, ending) in current_landmarks.iter().zip(endings) { | |
| assert_eq!(landmark["turnId"], ending.event.invocation.turn_id); | |
| assert_eq!(landmark["lastPosition"], json!({"sourceSequence":ending.sequence,"itemIndex":0})); | |
| } | |
| assert_eq!(current_landmarks[1]["label"], "Hello"); | |
| assert_eq!(current_landmarks[1]["turnId"], located["chunks"][0]["turnId"]); | |
| assert!(current_landmarks[1]["position"]["sourceSequence"].as_u64().unwrap() > event_fence); | |
| let anchored = fixture.query("history", json!({"sessionId":session,"through":newer_fence.unwrap(),"position":{"kind":"row","position":current_landmarks[1]["position"],"field":"body"}})).await.unwrap(); | |
| assert_eq!(anchored["chunks"][0]["text"], "Hello"); | |
| assert_eq!(anchored["chunks"][0]["turnId"], current_landmarks[1]["turnId"]); | |
| let future = *fixture.store.subscribe_commits().borrow() + 1; | |
| assert!(fixture.query("historyLandmarks", json!({"sessionId":session,"through":future})).await.is_err()); | |
| assert!(fixture.query("historyLandmarks", json!({"sessionId":"foreign_session","through":event_fence})).await.is_err()); | |
| assert!(fixture.query("history", json!({"sessionId":session,"position":{"kind":"after","cursor":{"position":{"sourceSequence":0,"itemIndex":0},"offset":0}}})).await.is_err()); | |
| assert!(fixture.query("history", json!({"sessionId":"foreign_session","position":{"kind":"start"}})).await.is_err()); | |
| server.await.unwrap(); | |
| fixture.app.shutdown().await.unwrap(); | |
| fixture.config.shutdown().await.unwrap(); | |
| }).await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn actual_http_runs_overlap_and_observer_loss_does_not_cancel_execution() { | |
| tokio::time::timeout(Duration::from_secs(30),async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let base = format!("http://{}/v1",listener.local_addr().unwrap()); | |
| let entered = Arc::new(Semaphore::new(0)); let release = Arc::new(Semaphore::new(0)); | |
| let worker_entered = entered.clone(); let worker_release = release.clone(); | |
| let server = tokio::spawn(async move { | |
| let mut requests = tokio::task::JoinSet::new(); | |
| for _ in 0..3 { | |
| let (mut socket,_) = listener.accept().await.unwrap(); | |
| let entered = worker_entered.clone(); | |
| let release = worker_release.clone(); | |
| requests.spawn(async move { | |
| let body = request(&mut socket).await; | |
| assert_eq!(body["model"],"fixture-model"); | |
| entered.add_permits(1); | |
| release.acquire().await.unwrap().forget(); | |
| response(&mut socket).await; | |
| }); | |
| } | |
| while let Some(result) = requests.join_next().await { result.unwrap(); } | |
| }); | |
| let fixture = Fixture::new(&base,None).await; | |
| let first = fixture.create("First").await; let second = fixture.create("Second").await; let third = fixture.create("Third").await; | |
| let started = fixture.send(&first).await.unwrap(); entered.acquire().await.unwrap().forget(); | |
| assert!(!fixture.app.shutdown_if_idle().await.unwrap(), "completed send RPC still owns a live model run"); | |
| let active = fixture.query("observe", json!({"sessionId":first})).await.unwrap(); | |
| assert_eq!(active["session"]["id"], first); | |
| assert_eq!(active["session"]["activity"], "working"); | |
| fixture.store.establish_interaction(&catalog_interaction(&first, "working-request")).await.unwrap(); | |
| let waiting = fixture.query("observe", json!({"sessionId":first})).await.unwrap(); | |
| assert_eq!(waiting["session"]["activity"], "waiting"); | |
| fixture.store.commit_interaction_outcome("working-request", maka_agent_facts::interaction::InteractionOutcome::Closure { reason: maka_agent_facts::interaction::ClosureReason::ProducerCancelled, committed_at: 20 }).await.unwrap(); | |
| let resumed = fixture.query("observe", json!({"sessionId":first})).await.unwrap(); | |
| assert_eq!(resumed["session"]["activity"], "working"); | |
| assert_eq!(active["run"]["invocationId"], started["invocationId"]); | |
| assert_eq!(active["run"]["state"], "running"); | |
| assert!(!active.to_string().contains("private-fixture-key")); | |
| let mut watch = fixture.app.client().subscribe(fixture.binding(2).await,"watch".into(),json!({"sessionId":first,"after":0}),scope(&fixture.issuer,true),Attachments::new().into()).await.unwrap(); | |
| let page = watch.recv().await.unwrap(); assert!(!page.to_string().contains("private-fixture-key")); | |
| drop(watch); | |
| fixture.send(&second).await.unwrap(); | |
| fixture.send(&third).await.unwrap(); | |
| entered.acquire_many(2).await.unwrap().forget(); | |
| assert_eq!(fixture.owner.runs.lock().await.controls.len(),3); | |
| release.add_permits(3); server.await.unwrap(); fixture.drained().await; | |
| assert!(fixture.owner.runs.lock().await.controls.is_empty()); | |
| let completed = fixture.query("observe", json!({"sessionId":first})).await.unwrap(); | |
| let metrics = &completed["metrics"]; | |
| assert_eq!(metrics["modelId"], "fixture-model"); | |
| assert_eq!(metrics["contextTokens"], 3); | |
| assert_eq!(metrics["contextLastKnown"], false); | |
| let (basis, _) = fixture.store.context_usage(&first).await.unwrap(); | |
| let maka_event_log::context::LatestMainContext::Selected(basis) = basis else { panic!("missing accepted main request") }; | |
| let seconds = basis.finished_at.unwrap().duration_since(basis.requested_at.unwrap()).unwrap().as_secs_f64(); | |
| assert_eq!(metrics["outputTokensPerSecond"].as_f64().unwrap(), 2.0 / seconds); | |
| assert!(completed["run"].is_null()); | |
| assert_eq!(completed["session"]["activity"], "idle"); | |
| assert_eq!(completed["parts"], json!([])); | |
| assert!(completed["through"].as_u64().unwrap() >= active["through"].as_u64().unwrap()); | |
| let observation = fixture.store.session_projection::<Value>(&first).await.unwrap().unwrap(); | |
| let events = fixture.store.session_stream_events(&first,0,observation.through_sequence,32,64*1024).await.unwrap(); | |
| assert!(events.events.iter().any(|event| matches!(&event.fact,maka_event_log::observation::StreamFact::PartDelta{text,..} if text=="Elegant response"))); | |
| let prefix = fixture.store.prefix(128, 1024*1024).await.unwrap(); | |
| let model_request = prefix.events.iter().find(|row| row.event.invocation.session_id == first && matches!(row.event.fact, maka_agent_facts::event::Fact::ModelRequested { .. })).unwrap(); | |
| let composition = fixture.store.request_composition(&first, &model_request.event.id).await.unwrap().unwrap(); | |
| let adapter = composition.sources.iter().find(|source| source.kind == SourceKind::ModelAdapter).unwrap(); | |
| let mut expected = fixture.owner.revision.clone(); | |
| expected.kind = SourceKind::ModelAdapter; | |
| expected.name = "chat-completions".into(); | |
| assert_eq!(adapter, &expected, "model evidence retains the actual Agent activation"); | |
| assert!(fixture.app.shutdown_if_idle().await.unwrap()); fixture.config.shutdown().await.unwrap(); | |
| }).await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn original_operation_conflicts_and_private_namespace_never_cross_sessions() { | |
| tokio::time::timeout(Duration::from_secs(20), async { | |
| let fixture = Fixture::new("http://127.0.0.1:1/v1", None).await; | |
| let operation = Uuid::new_v4(); | |
| let first = fixture | |
| .mutation(2, "create", fixture.create_input("A").await, operation) | |
| .await | |
| .unwrap(); | |
| let conflict = fixture | |
| .mutation(3, "create", fixture.create_input("B").await, operation) | |
| .await | |
| .unwrap_err(); | |
| assert!(conflict.contains("event identity conflicts")); | |
| let second = fixture | |
| .mutation(3, "create", fixture.create_input("B").await, Uuid::new_v4()) | |
| .await | |
| .unwrap(); | |
| assert_ne!(first["session"]["id"], second["session"]["id"]); | |
| fixture | |
| .store | |
| .create_session("foreign_session", "foreign", &json!({"foreign":true}), 1) | |
| .await | |
| .unwrap(); | |
| let listed = fixture | |
| .app | |
| .client() | |
| .start( | |
| fixture.binding(2).await, | |
| "list".into(), | |
| json!({"filter":"all","position":{"kind":"start"}}), | |
| scope(&fixture.issuer, true), | |
| Attachments::new().into(), | |
| ) | |
| .await | |
| .unwrap() | |
| .result() | |
| .await | |
| .unwrap(); | |
| assert_eq!(listed["sessions"].as_array().unwrap().len(), 2); | |
| assert!(!listed.to_string().contains("foreign_session")); | |
| assert!( | |
| fixture | |
| .send("foreign_session") | |
| .await | |
| .unwrap_err() | |
| .contains("agent_session_unavailable") | |
| ); | |
| assert!( | |
| fixture | |
| .app | |
| .client() | |
| .prepare_mutation( | |
| fixture.binding(2).await, | |
| "send".into(), | |
| json!({"sessionId":first["id"],"message":"Unauthorized","model":fixture.selection().await}), | |
| scope(&fixture.issuer, false), | |
| Uuid::new_v4(), | |
| Attachments::new().into(), | |
| ) | |
| .await | |
| .is_err() | |
| ); | |
| fixture.app.shutdown().await.unwrap(); | |
| fixture.config.shutdown().await.unwrap(); | |
| }) | |
| .await | |
| .unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn exact_stop_cannot_cancel_a_successor_and_close_waits_for_original_workers() { | |
| tokio::time::timeout(Duration::from_secs(30), async { | |
| let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); | |
| let base = format!("http://{}/v1", listener.local_addr().unwrap()); | |
| let entered = Arc::new(Semaphore::new(0)); | |
| let release = Arc::new(Semaphore::new(0)); | |
| let worker_entered = entered.clone(); | |
| let worker_release = release.clone(); | |
| let server = tokio::spawn(async move { | |
| for _ in 0..2 { | |
| let (mut socket, _) = listener.accept().await.unwrap(); | |
| request(&mut socket).await; | |
| worker_entered.add_permits(1); | |
| worker_release.acquire().await.unwrap().forget(); | |
| response(&mut socket).await; | |
| } | |
| }); | |
| let fixture = Fixture::new(&base, None).await; | |
| let session = fixture.create("Conversation").await; | |
| let first = fixture.send(&session).await.unwrap(); | |
| entered.acquire().await.unwrap().forget(); | |
| release.add_permits(1); | |
| fixture.drained().await; | |
| let second = fixture.send(&session).await.unwrap(); | |
| entered.acquire().await.unwrap().forget(); | |
| let stale = fixture | |
| .mutation( | |
| 2, | |
| "stop", | |
| json!({"sessionId":session,"invocationId":first["invocationId"]}), | |
| Uuid::new_v4(), | |
| ) | |
| .await | |
| .unwrap(); | |
| assert_eq!(stale["requested"], false); | |
| let runs = fixture.owner.runs.lock().await; | |
| let control = runs.controls.get(&session).unwrap(); | |
| assert_eq!( | |
| control.invocation.invocation_id, | |
| second["invocationId"].as_str().unwrap() | |
| ); | |
| assert!(!control.cancellation.is_cancelled()); | |
| drop(runs); | |
| release.add_permits(1); | |
| server.await.unwrap(); | |
| fixture.drained().await; | |
| fixture.app.shutdown().await.unwrap(); | |
| fixture.config.shutdown().await.unwrap(); | |
| }) | |
| .await | |
| .unwrap(); | |
| } | |
| #[derive(Clone)] | |
| struct Fault { | |
| panic: bool, | |
| entered: Arc<Semaphore>, | |
| release: Arc<Semaphore>, | |
| store: Arc<StdMutex<Option<Arc<EventLog>>>>, | |
| } | |
| impl maka_agent_ports::model::ProviderAdapter for Fault { | |
| fn open( | |
| &self, | |
| _: maka_agent_ports::model::Lifetime, | |
| _: CancellationToken, | |
| ) -> BoxFuture<'_, Result<Arc<dyn maka_agent_ports::model::Session>, maka_model::ModelError>> | |
| { | |
| let session = self.clone(); | |
| Box::pin(async { Ok(Arc::new(session) as Arc<dyn maka_agent_ports::model::Session>) }) | |
| } | |
| } | |
| impl maka_agent_ports::model::Session for Fault { | |
| fn stream( | |
| &self, | |
| _: maka_agent_ports::model::Request, | |
| _: maka_agent_ports::model::Context, | |
| ) -> BoxFuture<'static, Result<(), maka_model::ModelError>> { | |
| let fault = self.clone(); | |
| Box::pin(async move { | |
| fault.entered.add_permits(1); | |
| fault.release.acquire().await.unwrap().forget(); | |
| // Real EventLog SQL owner shuts down after durable start. Later | |
| // model-interruption and terminal writes must fail independently. | |
| let store = fault.store.lock().unwrap().as_ref().unwrap().clone(); | |
| store.shutdown().await.unwrap(); | |
| assert!(!fault.panic, "controlled original model worker panic"); | |
| Err(maka_model::ModelError::Adapter( | |
| "ordinary model failure".into(), | |
| )) | |
| }) | |
| } | |
| } | |
| fn controlled_model(panic: bool) -> (ModelExecutor, Fault) { | |
| let fault = Fault { | |
| panic, | |
| entered: Arc::new(Semaphore::new(0)), | |
| release: Arc::new(Semaphore::new(0)), | |
| store: Arc::new(StdMutex::new(None)), | |
| }; | |
| let binding = maka_agent_ports::model::Binding::new( | |
| Arc::new(fault.clone()), | |
| maka_agent_facts::composition::SourceRevision { | |
| kind: maka_agent_facts::composition::SourceKind::ModelAdapter, | |
| name: "chat-completions".into(), | |
| package_id: "fixture.adapter".into(), | |
| entry_id: "fixture.adapter".into(), | |
| activation: "fixture.activation".into(), | |
| revision: "1".into(), | |
| }, | |
| ); | |
| ( | |
| ModelExecutor::new(Duration::from_secs(30)) | |
| .unwrap() | |
| .with_adapters(BTreeMap::from([("chat-completions".into(), binding)])), | |
| fault, | |
| ) | |
| } | |
| async fn cleanup_case(panic: bool) { | |
| let (mut capture, _log_guard) = log_capture(); | |
| let (model, fault) = controlled_model(panic); | |
| let fixture = Fixture::new("http://127.0.0.1:1/v1", Some(model)).await; | |
| *fault.store.lock().unwrap() = Some(fixture.store.clone()); | |
| let session = fixture.create("Physical lifetime").await; | |
| fixture.send(&session).await.unwrap(); | |
| let invocation = fixture | |
| .owner | |
| .runs | |
| .lock() | |
| .await | |
| .controls | |
| .get(&session) | |
| .unwrap() | |
| .invocation | |
| .clone(); | |
| let activation = fixture.owner.revision.activation.clone(); | |
| fault.entered.acquire().await.unwrap().forget(); | |
| let selection = fixture.selection().await; | |
| let reference = if panic { | |
| assert_eq!(fixture.send(&session).await.unwrap()["kind"], "queued"); | |
| let prepared = fixture | |
| .app | |
| .client() | |
| .prepare_mutation( | |
| fixture.binding(2).await, | |
| "send".into(), | |
| json!({"sessionId":session,"message":"after failed cleanup","model":selection}), | |
| scope(&fixture.issuer, true), | |
| Uuid::new_v4(), | |
| Attachments::new().into(), | |
| ) | |
| .await | |
| .unwrap(); | |
| Some(prepared.reference().clone()) | |
| } else { | |
| None | |
| }; | |
| fault.release.add_permits(1); | |
| fixture.drained().await; | |
| if let Some(reference) = reference { | |
| // The fault closed the SQL owner. Test the original admission gate | |
| // directly; a new Host intent would fail before reaching that gate. | |
| assert!( | |
| fixture | |
| .owner | |
| .send( | |
| Send { | |
| session_id: session.clone(), | |
| message: "after failed cleanup".into(), | |
| model: selection, | |
| thinking_level: None, | |
| attachments: vec![] | |
| }, | |
| &reference, | |
| &scope(&fixture.issuer, true) | |
| ) | |
| .await | |
| .err() | |
| .unwrap() | |
| .contains("agent_cleanup_unconfirmed") | |
| ); | |
| let raw = rusqlite::Connection::open_with_flags( | |
| fixture.root.join(maka_event_log::root::ROOT_DATABASE), | |
| rusqlite::OpenFlags::SQLITE_OPEN_READ_ONLY, | |
| ) | |
| .unwrap(); | |
| let pending: i64 = raw | |
| .query_row( | |
| "SELECT COUNT(*) FROM message_admissions WHERE session_id = ?1", | |
| [&session], | |
| |row| row.get(0), | |
| ) | |
| .unwrap(); | |
| assert_eq!(pending, 1); | |
| } | |
| assert_eq!( | |
| fixture.owner.runs.lock().await.quarantined.len(), | |
| usize::from(panic) | |
| ); | |
| assert_eq!( | |
| fixture.owner.runs.lock().await.closing, | |
| !panic, | |
| "completion uncertainty quarantines only the session; advancing after SQL shutdown closes the owner" | |
| ); | |
| assert_eq!(fixture.owner.stop.is_cancelled(), !panic); | |
| let close = fixture.app.shutdown().await; | |
| assert_eq!(close.is_err(), panic); | |
| fixture.config.shutdown().await.unwrap(); | |
| fault.store.lock().unwrap().take(); | |
| let Fixture { | |
| services, | |
| _dir, | |
| namespaces, | |
| root, | |
| workspace: _, | |
| store, | |
| config, | |
| app, | |
| issuer, | |
| owner, | |
| } = fixture; | |
| drop(services); | |
| drop(owner); | |
| drop(issuer); | |
| drop(app); | |
| drop(config); | |
| drop(store); | |
| drop(fault); | |
| if panic { | |
| assert_eq!( | |
| RootOwner::open(&root, &namespaces).err().unwrap().kind(), | |
| std::io::ErrorKind::WouldBlock | |
| ); | |
| } else { | |
| drop(RootOwner::open(&root, &namespaces).unwrap()); | |
| } | |
| let log = log_text(&mut capture); | |
| let (stage, cause, consequence) = if panic { | |
| ( | |
| "queue_cleanup_unconfirmed", | |
| "cleanup_unconfirmed", | |
| "session_quarantined", | |
| ) | |
| } else { | |
| ( | |
| "queue_advance_failed", | |
| "store_connection_closed", | |
| "owner_closing", | |
| ) | |
| }; | |
| let event = log | |
| .lines() | |
| .find(|line| line.contains(&format!("stage=\"{stage}\""))) | |
| .expect("actual fatal queue custody outcome must be logged"); | |
| assert!(event.contains("ERROR")); | |
| assert!(event.contains(&format!("cause=\"{cause}\""))); | |
| assert!(event.contains(&format!("consequence=\"{consequence}\""))); | |
| for identity in [ | |
| &invocation.session_id, | |
| &invocation.turn_id, | |
| &invocation.run_id, | |
| &invocation.invocation_id, | |
| &activation, | |
| ] { | |
| assert!( | |
| event.contains(identity), | |
| "fatal event must retain original invocation/activation: {event}" | |
| ); | |
| } | |
| for secret in [ | |
| "controlled original model worker panic", | |
| "ordinary model failure", | |
| "private-fixture-key", | |
| "after failed cleanup", | |
| "Hello", | |
| ] { | |
| assert!( | |
| !log.contains(secret), | |
| "fatal evidence must exclude panic/error payloads, credentials and submitted text" | |
| ); | |
| } | |
| } | |
| #[tokio::test] | |
| async fn model_panic_and_terminal_sql_failure_quarantine_execution_and_keep_root() { | |
| tokio::time::timeout(Duration::from_secs(20), cleanup_case(true)) | |
| .await | |
| .unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn ordinary_model_and_terminal_sql_failures_preserve_confirmed_physical_cleanup() { | |
| tokio::time::timeout(Duration::from_secs(20), cleanup_case(false)) | |
| .await | |
| .unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn public_availability_resumes_stable_sessions_and_separates_deployments() { | |
| tokio::time::timeout(Duration::from_secs(20), async { | |
| let fixture = Fixture::new("http://127.0.0.1:1/v1", None).await; | |
| let session = fixture.create("Stable conversation").await; | |
| let deployment = fixture.app.source().unwrap().deployment; | |
| fixture.app.shutdown().await.unwrap(); | |
| let mut native = NativeRegistry::default(); | |
| maka_settings_native::provide(&mut native, crate::owner::tests::bundled_catalog()).unwrap(); | |
| crate::owner::tests::provide_all(&mut native).unwrap(); | |
| let resumed = Application::start_from_store( | |
| fixture.store.clone(), | |
| deployment, | |
| native, | |
| fixture.issuer.clone(), | |
| Arc::new(Grant(fixture.issuer.clone(), None)), | |
| Some(maka_host_js::factory(Default::default())), | |
| ) | |
| .await | |
| .unwrap(); | |
| resumed.ready().await.unwrap(); | |
| let client = resumed.client(); | |
| let binding = client | |
| .bind(client.activation(&id(2)).unwrap(), EXPORT.into()) | |
| .await | |
| .unwrap() | |
| .remove(0); | |
| let listed = client | |
| .start( | |
| binding.clone(), | |
| "list".into(), | |
| json!({"filter":"all","position":{"kind":"start"}}), | |
| scope(&fixture.issuer, true), | |
| Attachments::new().into(), | |
| ) | |
| .await | |
| .unwrap() | |
| .result() | |
| .await | |
| .unwrap(); | |
| assert_eq!(listed["sessions"][0]["id"], session); | |
| let other_deployment = Uuid::new_v4(); | |
| let (input, packages) = input(&fixture.owner.settings); | |
| fixture | |
| .store | |
| .commit_application(other_deployment, 0, input, vec![], packages) | |
| .await | |
| .unwrap(); | |
| let mut native = NativeRegistry::default(); | |
| maka_settings_native::provide(&mut native, crate::owner::tests::bundled_catalog()).unwrap(); | |
| crate::owner::tests::provide_all(&mut native).unwrap(); | |
| let other = Application::start_from_store( | |
| fixture.store.clone(), | |
| other_deployment, | |
| native, | |
| fixture.issuer.clone(), | |
| Arc::new(Grant(fixture.issuer.clone(), None)), | |
| Some(maka_host_js::factory(Default::default())), | |
| ) | |
| .await | |
| .unwrap(); | |
| other.ready().await.unwrap(); | |
| let client = other.client(); | |
| let binding = client | |
| .bind(client.activation(&id(2)).unwrap(), EXPORT.into()) | |
| .await | |
| .unwrap() | |
| .remove(0); | |
| let listed = client | |
| .start( | |
| binding.clone(), | |
| "list".into(), | |
| json!({"filter":"all","position":{"kind":"start"}}), | |
| scope(&fixture.issuer, true), | |
| Attachments::new().into(), | |
| ) | |
| .await | |
| .unwrap() | |
| .result() | |
| .await | |
| .unwrap(); | |
| assert!(listed["sessions"].as_array().unwrap().is_empty()); | |
| let mut foreign = client | |
| .subscribe( | |
| binding, | |
| "watch".into(), | |
| json!({"sessionId":session,"after":0}), | |
| scope(&fixture.issuer, true), | |
| Attachments::new().into(), | |
| ) | |
| .await | |
| .unwrap(); | |
| assert!(foreign.recv().await.is_none()); | |
| assert!( | |
| foreign | |
| .completion() | |
| .await | |
| .result | |
| .unwrap_err() | |
| .contains("agent_session_unavailable") | |
| ); | |
| other.shutdown().await.unwrap(); | |
| resumed.shutdown().await.unwrap(); | |
| fixture.config.shutdown().await.unwrap(); | |
| }) | |
| .await | |
| .unwrap(); | |
| } | |
| async fn disabled_connection() { | |
| let fixture = Fixture::new("http://127.0.0.1:1/v1", None).await; | |
| let session = fixture.create("Bound model").await; | |
| let catalog = fixture.config.catalog().await.unwrap(); | |
| let row = &catalog.connections[0]; | |
| let expected: ConnectionVersionBasis = | |
| serde_json::from_value(json!({"connectionId":row.connection_id,"revision":row.revision})) | |
| .unwrap(); | |
| let updated = fixture | |
| .config | |
| .update_connection(UpdateCatalogConnectionInput { | |
| expected, | |
| changes: ConnectionCatalogEntryUpdate { | |
| name: Some(row.name.clone()), | |
| configuration: Some(row.configuration.clone()), | |
| enabled: Some(false), | |
| enabled_model_ids: Some(row.enabled_model_ids.clone()), | |
| model_overrides: Patch::Keep, | |
| request_body_overlay: Patch::Keep, | |
| }, | |
| }) | |
| .await | |
| .unwrap(); | |
| assert!(matches!(updated, CatalogMutationResult::Committed { .. })); | |
| let revision = fixture.config.catalog().await.unwrap().revision; | |
| let alternative = serde_json::from_value(json!({"expectedCatalogRevision":revision,"connection":{"slug":"available","name":"Available","provider":row.provider,"configuration":row.configuration,"enabled":true,"enabledModelIds":["fixture-model"]}})).unwrap(); | |
| assert!(matches!( | |
| fixture.config.create_connection(alternative).await.unwrap(), | |
| CatalogMutationResult::Committed { .. } | |
| )); | |
| let alternative_row = fixture | |
| .config | |
| .catalog() | |
| .await | |
| .unwrap() | |
| .connections | |
| .into_iter() | |
| .find(|row| row.slug == "available") | |
| .unwrap(); | |
| let maka_config::oauth::enrollment::LoginPreparation::Ready(ticket) = fixture | |
| .config | |
| .prepare_oauth_login(maka_agent_facts::oauth::LoginStart { | |
| expected_credential: None, | |
| attempt_id: Uuid::new_v4().to_string(), | |
| target: maka_agent_facts::oauth::Target::Existing { | |
| expected: ConnectionCredentialTarget { | |
| connection_id: alternative_row.connection_id.clone(), | |
| revision: alternative_row.revision, | |
| slug: alternative_row.slug, | |
| provider: alternative_row.provider, | |
| configuration: alternative_row.configuration.clone(), | |
| }, | |
| configuration: alternative_row.configuration, | |
| }, | |
| authentication: maka_agent_facts::provider::AuthenticationInput { | |
| method: "api-key".into(), | |
| input: json!({"apiKey":"private-alternative-key"}), | |
| }, | |
| }) | |
| .await | |
| .unwrap() | |
| else { | |
| panic!("alternative login preparation"); | |
| }; | |
| assert!(ticket.claim().await.unwrap()); | |
| assert!(matches!( | |
| ticket | |
| .complete( | |
| maka_agent_facts::provider::Credential { | |
| secret: "private-alternative-key".into(), | |
| refresh_at: None | |
| }, | |
| 2 | |
| ) | |
| .await | |
| .unwrap(), | |
| maka_config::oauth::enrollment::LoginCompletion::Committed(_) | |
| )); | |
| let result = fixture | |
| .config | |
| .set_default_target(SetDefaultConnectionTargetInput { | |
| expected_catalog_revision: fixture.config.catalog().await.unwrap().revision, | |
| target: Some(ConnectionTarget { | |
| connection_id: alternative_row.connection_id, | |
| model_id: "fixture-model".into(), | |
| }), | |
| }) | |
| .await | |
| .unwrap(); | |
| assert!(matches!(result, CatalogMutationResult::Committed { .. })); | |
| let error = fixture.send(&session).await.unwrap_err(); | |
| assert!(error.contains("agent_execution_unavailable")); | |
| assert!(!error.contains("private-fixture-key") && !error.contains("private-alternative-key")); | |
| let observation = fixture | |
| .store | |
| .session_projection::<Value>(&session) | |
| .await | |
| .unwrap() | |
| .unwrap(); | |
| assert!(observation.root_turn.is_none()); | |
| fixture.app.shutdown().await.unwrap(); | |
| fixture.config.shutdown().await.unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn disabled_selected_connection_does_not_use_an_authenticated_default_or_open_a_run() { | |
| tokio::time::timeout(Duration::from_secs(20), disabled_connection()) | |
| .await | |
| .unwrap(); | |
| } | |
| #[tokio::test] | |
| async fn tool_source_uses_original_agent_grant_and_frozen_canonical_fields() { | |
| use maka_agent_facts::import::Content; | |
| use maka_agent_facts::import::Record; | |
| use maka_agent_facts::import::Source; | |
| use maka_capabilities::storage::Namespace; | |
| use maka_event_log::sessions::PluginSession; | |
| use maka_plugin_core::scope::Scope as StorageScope; | |
| let fixture = Fixture::new("http://127.0.0.1:9/v1", None).await; | |
| let seed = fixture.create("Tool source").await; | |
| let configuration = fixture.owner.session(&seed).await.unwrap().configuration; | |
| let session = format!("{}{}", fixture.owner.prefix, Uuid::new_v4().simple()); | |
| let plugin = PluginSession { | |
| session_id: session.clone(), | |
| creator: Namespace::new("tool.source.fixture", StorageScope::Profile).unwrap(), | |
| fingerprint: "tool-source-import".into(), | |
| managed: false, | |
| authority_session_id: None, | |
| }; | |
| let input = json!({"count":42,"flag":false,"text":"中😀\0".repeat(2000)}); | |
| let records = [ | |
| Content::User { | |
| text: "Inspect tools".into(), | |
| }, | |
| Content::ToolCall { | |
| call_id: "call".into(), | |
| name: "Count".into(), | |
| input: Some(input.clone()), | |
| }, | |
| Content::ToolResult { | |
| call_id: "call".into(), | |
| output: json!(null), | |
| is_error: false, | |
| }, | |
| ] | |
| .into_iter() | |
| .enumerate() | |
| .map(|(i, content)| Record { | |
| source_message_id: format!("m{i}"), | |
| source_turn_id: "turn".into(), | |
| timestamp: Some(1), | |
| content, | |
| }) | |
| .collect(); | |
| fixture | |
| .store | |
| .import_session( | |
| &plugin, | |
| &Source { | |
| adapter: "fixture".into(), | |
| session_id: "foreign".into(), | |
| }, | |
| &configuration, | |
| records, | |
| 1, | |
| ) | |
| .await | |
| .unwrap(); | |
| let fence = *fixture.store.subscribe_commits().borrow(); | |
| let history = loop { | |
| let value = fixture | |
| .query( | |
| "history", | |
| json!({"sessionId":session,"through":fence,"position":{"kind":"start"}}), | |
| ) | |
| .await | |
| .unwrap(); | |
| if value["kind"] == "ready" { | |
| break value; | |
| } | |
| }; | |
| let sequence = history["chunks"] | |
| .as_array() | |
| .unwrap() | |
| .iter() | |
| .find(|chunk| chunk["role"] == "tool_call") | |
| .unwrap()["position"] | |
| .clone(); | |
| let mut position = json!({"kind":"start"}); | |
| let mut complete = String::new(); | |
| loop { | |
| let page = fixture.query("source", json!({"sessionId":session,"through":fence,"source":{"kind":"row","position":sequence},"field":"arguments","position":position})).await.unwrap(); | |
| assert!(serde_json::to_vec(&page).unwrap().len() <= 64 * 1024); | |
| assert_eq!(page["kind"], "ready"); | |
| assert_eq!(page["through"], fence); | |
| let source = &page["source"]; | |
| assert_eq!(source["format"], "json"); | |
| assert_eq!(source["tool"]["call"]["name"], "Count"); | |
| let text = source["text"].as_str().unwrap(); | |
| assert!(text.len() <= 8192); | |
| complete.push_str(text); | |
| let end = source["offset"].as_u64().unwrap() + text.len() as u64; | |
| if end == source["totalBytes"].as_u64().unwrap() { | |
| break; | |
| } | |
| position = json!({"kind":"after","offset":end}); | |
| } | |
| assert_eq!(serde_json::from_str::<Value>(&complete).unwrap(), input); | |
| let mut literal = String::new(); | |
| let mut offset = 0; | |
| loop { | |
| let page = fixture | |
| .query( | |
| "source", | |
| json!({"sessionId":session,"through":fence, | |
| "source":{"kind":"row","position":sequence},"field":"arguments","path":["text"], | |
| "position":{"kind":"after","offset":offset}}), | |
| ) | |
| .await | |
| .unwrap(); | |
| let source = &page["source"]; | |
| assert_eq!(source["path"], json!(["text"])); | |
| assert_eq!(source["format"], "text"); | |
| assert_eq!(source["offset"], offset); | |
| literal.push_str(source["text"].as_str().unwrap()); | |
| offset = literal.len(); | |
| if offset as u64 == source["totalBytes"].as_u64().unwrap() { | |
| break; | |
| } | |
| } | |
| assert_eq!(literal, input["text"].as_str().unwrap()); | |
| let request = json!({"sessionId":session,"through":fence,"source":{"kind":"row","position":sequence},"field":"arguments","path":["text"],"position":{"kind":"start"}}); | |
| for (key, value) in [ | |
| ("sessionId", json!("foreign_session")), | |
| ("through", json!(fence + 1)), | |
| ("through", json!(0)), | |
| ("field", json!("result")), | |
| ] { | |
| let mut invalid = request.clone(); | |
| invalid[key] = value; | |
| assert!(fixture.query("source", invalid).await.is_err()); | |
| } | |
| let denied = fixture | |
| .app | |
| .client() | |
| .start( | |
| fixture.binding(2).await, | |
| "source".into(), | |
| request, | |
| scope(&fixture.issuer, false), | |
| Attachments::new().into(), | |
| ) | |
| .await; | |
| assert!(denied.is_err()); | |
| fixture.app.shutdown().await.unwrap(); | |
| fixture.config.shutdown().await.unwrap(); | |
| } | |
| fn catalog_interaction( | |
| session_id: &str, | |
| request_id: &str, | |
| ) -> maka_agent_facts::interaction::InteractionRecord { | |
| use maka_agent_facts::interaction::GrantCapability; | |
| use maka_agent_facts::interaction::GrantScope; | |
| use maka_agent_facts::interaction::GrantTarget; | |
| use maka_agent_facts::interaction::InteractionRecord; | |
| use maka_agent_facts::interaction::InteractionRequest; | |
| InteractionRecord { | |
| session_id: session_id.into(), | |
| turn_id: "turn".into(), | |
| run_id: "run".into(), | |
| request_id: request_id.into(), | |
| created_at: 10, | |
| outcome: None, | |
| request: InteractionRequest::ClientCapability { | |
| base_revision: None, | |
| tool_use_id: "tool".into(), | |
| target: GrantTarget { | |
| provider_id: "provider".into(), | |
| contract_id: "contract".into(), | |
| server_id: "browser".into(), | |
| tool_name: "navigate".into(), | |
| capability: GrantCapability::Browser, | |
| scope: GrantScope::BrowserOrigin { | |
| origin: "https://example.com".into(), | |
| }, | |
| }, | |
| }, | |
| } | |
| } | |
| fn provide_all(registry: &mut NativeRegistry) -> Result<(), String> { | |
| crate::provide(registry, bundled_catalog())?; | |
| maka_acp_native::provide(registry)?; | |
| maka_files_native::provide(registry)?; | |
| maka_processes_native::provide(registry)?; | |
| maka_skills_native::provide(registry)?; | |
| maka_interactions_native::questions::provide(registry)?; | |
| maka_interactions_native::forms::provide(registry)?; | |
| maka_web::provide(registry)?; | |
| maka_jev_native::provide(registry)?; | |
| crate::workhub::provide(registry, bundled_catalog())?; | |
| Ok(()) | |
| } |