refactor: own Worker and Ticket service contracts outside providers
Move shared control/lifecycle/observation/Ticket contracts into feature::contracts. Consumers and host bindings import the neutral owner; providers retain current authorization, explicit combined routing and persisted composition. Document stable semantic IDs and exact compatibility epochs; connect consumer requirements to the existing planner without a new resolver. Exercise consumer installation/dispatch with contract-only test providers, atomic rejection paths, and parsed-source architecture guards including negative fixtures. Validation: worker and worker-runtime normal tests and doctests passed; root cargo check, rustdoc with warnings denied, fmt, diff and state-contract names passed. Dependent Server authorization tests require separate stale native-v1 fixture updates captured next.
This commit is contained in:
Generated
+1
@@ -6604,6 +6604,7 @@ dependencies = [
|
|||||||
"session-store",
|
"session-store",
|
||||||
"sha2 0.11.0",
|
"sha2 0.11.0",
|
||||||
"subjektiv",
|
"subjektiv",
|
||||||
|
"syn 2.0.117",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"ticket",
|
"ticket",
|
||||||
|
|||||||
@@ -144,9 +144,11 @@ use workdir::{
|
|||||||
use worker::WorkerController;
|
use worker::WorkerController;
|
||||||
use worker::feature::builtin::manage_workdir::WorkspaceAttachedWorkdirSession;
|
use worker::feature::builtin::manage_workdir::WorkspaceAttachedWorkdirSession;
|
||||||
use worker::feature::builtin::{
|
use worker::feature::builtin::{
|
||||||
CompositeWorkerObservationProvider, WorkerObservationError, WorkerObservationProvider,
|
CompositeWorkerObservationProvider, WorkspaceClientWorkerObservationProvider,
|
||||||
WorkerObservationSubject, WorkerObservationSubjectRef, WorkerSessionCapture,
|
};
|
||||||
WorkspaceClientWorkerObservationProvider,
|
use worker::feature::contracts::{
|
||||||
|
WorkerObservationError, WorkerObservationProvider, WorkerObservationSubject,
|
||||||
|
WorkerObservationSubjectRef, WorkerSessionCapture,
|
||||||
};
|
};
|
||||||
#[cfg(feature = "ws-server")]
|
#[cfg(feature = "ws-server")]
|
||||||
use worker::ipc::protocol_session::{WorkerProtocolSessionStreams, live_log_entry_event};
|
use worker::ipc::protocol_session::{WorkerProtocolSessionStreams, live_log_entry_event};
|
||||||
|
|||||||
@@ -64,3 +64,4 @@ dotenv = "0.15.0"
|
|||||||
futures = { workspace = true }
|
futures = { workspace = true }
|
||||||
serial_test = "3.4.0"
|
serial_test = "3.4.0"
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
|
syn = { version = "2", features = ["full", "visit"] }
|
||||||
|
|||||||
@@ -311,7 +311,7 @@ pub struct RestoredControllerBindings {
|
|||||||
pub backend_job: Option<crate::BackendJobExecutionBinding>,
|
pub backend_job: Option<crate::BackendJobExecutionBinding>,
|
||||||
pub workdir_sessions: std::sync::Arc<workdir::WorkdirSessionRouter>,
|
pub workdir_sessions: std::sync::Arc<workdir::WorkdirSessionRouter>,
|
||||||
pub observation_provider:
|
pub observation_provider:
|
||||||
Option<std::sync::Arc<dyn crate::feature::builtin::WorkerObservationProvider>>,
|
Option<std::sync::Arc<dyn crate::feature::contracts::WorkerObservationProvider>>,
|
||||||
pub subjektiv_attached: bool,
|
pub subjektiv_attached: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2083,7 +2083,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
let mut observation_providers: Vec<
|
let mut observation_providers: Vec<
|
||||||
Arc<dyn crate::feature::builtin::worker_observation::WorkerObservationProvider>,
|
Arc<dyn crate::feature::contracts::WorkerObservationProvider>,
|
||||||
> = Vec::new();
|
> = Vec::new();
|
||||||
|
|
||||||
// Worker-orchestration tools derive child filesystem authority from the
|
// Worker-orchestration tools derive child filesystem authority from the
|
||||||
|
|||||||
@@ -514,6 +514,11 @@ impl ServiceRequirement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_version(mut self, version: ServiceVersionReq) -> Self {
|
||||||
|
self.version = version;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn optional(id: ServiceId, reason: impl Into<String>) -> Self {
|
pub fn optional(id: ServiceId, reason: impl Into<String>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id,
|
id,
|
||||||
@@ -2372,6 +2377,7 @@ pub use invocation::{
|
|||||||
FeatureInvocationHandlerError, FeatureInvocationRegistry, InvocationContributionRegistrar,
|
FeatureInvocationHandlerError, FeatureInvocationRegistry, InvocationContributionRegistrar,
|
||||||
};
|
};
|
||||||
pub mod builtin;
|
pub mod builtin;
|
||||||
|
pub mod contracts;
|
||||||
pub mod mcp;
|
pub mod mcp;
|
||||||
pub(crate) mod session;
|
pub(crate) mod session;
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ pub mod workspace_worker_discovery;
|
|||||||
pub use task::{TaskFeature, task_tools_feature};
|
pub use task::{TaskFeature, task_tools_feature};
|
||||||
pub use ticket::{TicketFeature, TicketFeatureAccess, ticket_tools_feature};
|
pub use ticket::{TicketFeature, TicketFeatureAccess, ticket_tools_feature};
|
||||||
pub use worker_observation::{
|
pub use worker_observation::{
|
||||||
CompositeWorkerObservationProvider, WorkerObservationError, WorkerObservationFeature,
|
CompositeWorkerObservationProvider, WorkerObservationFeature,
|
||||||
WorkerObservationProvider, WorkerObservationSubject, WorkerObservationSubjectRef,
|
WorkspaceClientWorkerObservationProvider,
|
||||||
WorkerSessionCapture, WorkspaceClientWorkerObservationProvider,
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ use server_api::{
|
|||||||
CreateWorkspaceWorkerTicketAssignmentRequest,
|
CreateWorkspaceWorkerTicketAssignmentRequest,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use crate::feature::contracts::{
|
||||||
|
WORKER_CONTROL_SERVICE_ID, WORKER_CONTROL_SERVICE_VERSION, WORKER_LIFECYCLE_SERVICE_ID,
|
||||||
|
WORKER_LIFECYCLE_SERVICE_VERSION, WorkerControlInputKind, WorkerControlService,
|
||||||
|
WorkerControlStopKind, WorkerControlSubject, WorkerLifecycleService,
|
||||||
|
WorkerLifecycleSpawnRequest, WorkerLifecycleWorkdirAttachment, WorkerObservationSubjectRef,
|
||||||
|
};
|
||||||
use crate::feature::{
|
use crate::feature::{
|
||||||
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule,
|
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule,
|
||||||
ServiceDeclaration, ServiceId, ToolContribution, ToolDeclaration,
|
ServiceDeclaration, ServiceId, ToolContribution, ToolDeclaration,
|
||||||
@@ -25,78 +31,8 @@ const FEATURE_ID: &str = "worker";
|
|||||||
const FEATURE_NAME: &str = "Worker";
|
const FEATURE_NAME: &str = "Worker";
|
||||||
const FEATURE_DESCRIPTION: &str =
|
const FEATURE_DESCRIPTION: &str =
|
||||||
"Workspace-authority tools for managing Workdir-bound Backend/Runtime Worker sessions.";
|
"Workspace-authority tools for managing Workdir-bound Backend/Runtime Worker sessions.";
|
||||||
pub const WORKER_LIFECYCLE_SERVICE_ID: &str = "worker.lifecycle";
|
|
||||||
pub const WORKER_CONTROL_SERVICE_ID: &str = "worker.control";
|
|
||||||
const WORKER_LIFECYCLE_SERVICE_VERSION: &str = "1";
|
|
||||||
const INITIAL_WORKDIR_ALIAS: &str = "workdir";
|
const INITIAL_WORKDIR_ALIAS: &str = "workdir";
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub enum WorkerControlSubject {
|
|
||||||
RuntimeWorker {
|
|
||||||
runtime_id: String,
|
|
||||||
worker_id: String,
|
|
||||||
},
|
|
||||||
SubWorker {
|
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum WorkerControlInputKind {
|
|
||||||
User,
|
|
||||||
Notify,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum WorkerControlStopKind {
|
|
||||||
Cancel,
|
|
||||||
Stop,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait WorkerControlService: Send + Sync {
|
|
||||||
fn workspace_id(&self) -> &str;
|
|
||||||
async fn list_workers(&self) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
|
||||||
async fn send_input(
|
|
||||||
&self,
|
|
||||||
subject: WorkerControlSubject,
|
|
||||||
content: String,
|
|
||||||
kind: WorkerControlInputKind,
|
|
||||||
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
|
||||||
async fn stop_worker(
|
|
||||||
&self,
|
|
||||||
subject: WorkerControlSubject,
|
|
||||||
kind: WorkerControlStopKind,
|
|
||||||
reason: Option<String>,
|
|
||||||
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
|
||||||
async fn spawn_worker(
|
|
||||||
&self,
|
|
||||||
request: WorkerLifecycleSpawnRequest,
|
|
||||||
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
|
||||||
async fn restore_worker(
|
|
||||||
&self,
|
|
||||||
runtime_id: String,
|
|
||||||
worker_id: String,
|
|
||||||
request: server_api::WorkerRestoreRequest,
|
|
||||||
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
|
||||||
let _ = (runtime_id, worker_id, request);
|
|
||||||
Err(WorkspaceClientError::Unavailable(
|
|
||||||
"Runtime Worker restore is unavailable".to_string(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
fn remove_runtime_worker(
|
|
||||||
&self,
|
|
||||||
runtime_id: &str,
|
|
||||||
worker_id: &str,
|
|
||||||
reason: &str,
|
|
||||||
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
|
||||||
async fn ensure_permission(
|
|
||||||
&self,
|
|
||||||
subject: &super::worker_observation::WorkerObservationSubjectRef,
|
|
||||||
permission: &str,
|
|
||||||
) -> Result<(), WorkspaceClientError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WorkspaceWorkerControlService {
|
struct WorkspaceWorkerControlService {
|
||||||
client: Arc<dyn WorkspaceClient>,
|
client: Arc<dyn WorkspaceClient>,
|
||||||
workspace_id: String,
|
workspace_id: String,
|
||||||
@@ -296,9 +232,7 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
self.ensure_permission(
|
self.ensure_permission(
|
||||||
&super::worker_observation::WorkerObservationSubjectRef::SubWorker {
|
&WorkerObservationSubjectRef::SubWorker { name: name.clone() },
|
||||||
name: name.clone(),
|
|
||||||
},
|
|
||||||
"send_input",
|
"send_input",
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -377,9 +311,7 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
self.ensure_permission(
|
self.ensure_permission(
|
||||||
&super::worker_observation::WorkerObservationSubjectRef::SubWorker {
|
&WorkerObservationSubjectRef::SubWorker { name: name.clone() },
|
||||||
name: name.clone(),
|
|
||||||
},
|
|
||||||
"stop",
|
"stop",
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
@@ -456,11 +388,11 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
|||||||
|
|
||||||
async fn ensure_permission(
|
async fn ensure_permission(
|
||||||
&self,
|
&self,
|
||||||
subject: &super::worker_observation::WorkerObservationSubjectRef,
|
subject: &WorkerObservationSubjectRef,
|
||||||
permission: &str,
|
permission: &str,
|
||||||
) -> Result<(), WorkspaceClientError> {
|
) -> Result<(), WorkspaceClientError> {
|
||||||
match subject {
|
match subject {
|
||||||
super::worker_observation::WorkerObservationSubjectRef::SubWorker { name } => {
|
WorkerObservationSubjectRef::SubWorker { name } => {
|
||||||
let known = self
|
let known = self
|
||||||
.registry
|
.registry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -474,7 +406,7 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
super::worker_observation::WorkerObservationSubjectRef::RuntimeWorker {
|
WorkerObservationSubjectRef::RuntimeWorker {
|
||||||
runtime_id,
|
runtime_id,
|
||||||
worker_id,
|
worker_id,
|
||||||
} => {
|
} => {
|
||||||
@@ -533,43 +465,6 @@ impl WorkerControlService for WorkspaceWorkerControlService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait WorkerLifecycleService: Send + Sync {
|
|
||||||
async fn spawn(
|
|
||||||
&self,
|
|
||||||
request: WorkerLifecycleSpawnRequest,
|
|
||||||
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct WorkerLifecycleWorkdirAttachment {
|
|
||||||
/// Stable Worker-local routing alias preserved into the canonical create request.
|
|
||||||
pub alias: String,
|
|
||||||
/// Workspace-authoritative Workdir id preserved into the canonical create request.
|
|
||||||
pub working_directory_id: String,
|
|
||||||
/// Optional normalized initial cwd relative to this Workdir.
|
|
||||||
pub relative_cwd: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct WorkerLifecycleSpawnRequest {
|
|
||||||
/// Observed delegation required for Orchestrator Ticket starts.
|
|
||||||
pub orchestrator_assignment_id: Option<String>,
|
|
||||||
|
|
||||||
pub runtime_id: String,
|
|
||||||
/// Ordered alias-keyed selections forwarded as canonical Workdir attachments.
|
|
||||||
pub workdir_attachments: Vec<WorkerLifecycleWorkdirAttachment>,
|
|
||||||
pub profile: String,
|
|
||||||
pub singleton_key: Option<String>,
|
|
||||||
pub ticket_id: Option<String>,
|
|
||||||
/// Required for ordinary and Ticket spawn; exact retries reuse this ID and full input.
|
|
||||||
pub operation_id: Option<String>,
|
|
||||||
/// Explicit requested authority. Profile selection alone grants nothing.
|
|
||||||
pub execution_grant: Option<server_api::authorization::WorkerDelegationRequest>,
|
|
||||||
pub display_name: String,
|
|
||||||
pub initial_submit: Vec<Segment>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct WorkspaceWorkerLifecycleService {
|
struct WorkspaceWorkerLifecycleService {
|
||||||
client: Arc<dyn WorkspaceClient>,
|
client: Arc<dyn WorkspaceClient>,
|
||||||
}
|
}
|
||||||
@@ -702,7 +597,7 @@ impl FeatureModule for SubWorkerControlFeature {
|
|||||||
)
|
)
|
||||||
.with_provided_service(ServiceDeclaration::new(
|
.with_provided_service(ServiceDeclaration::new(
|
||||||
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
||||||
WORKER_LIFECYCLE_SERVICE_VERSION,
|
WORKER_CONTROL_SERVICE_VERSION,
|
||||||
"Parent-owned SubWorker discovery and control operations",
|
"Parent-owned SubWorker discovery and control operations",
|
||||||
));
|
));
|
||||||
for operation in SUB_WORKER_CONTROL_OPERATIONS {
|
for operation in SUB_WORKER_CONTROL_OPERATIONS {
|
||||||
@@ -721,7 +616,7 @@ impl FeatureModule for SubWorkerControlFeature {
|
|||||||
context.services().provide(
|
context.services().provide(
|
||||||
ServiceDeclaration::new(
|
ServiceDeclaration::new(
|
||||||
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
||||||
WORKER_LIFECYCLE_SERVICE_VERSION,
|
WORKER_CONTROL_SERVICE_VERSION,
|
||||||
"Parent-owned SubWorker discovery and control operations",
|
"Parent-owned SubWorker discovery and control operations",
|
||||||
),
|
),
|
||||||
control.clone(),
|
control.clone(),
|
||||||
@@ -755,7 +650,7 @@ impl FeatureModule for ManageWorkerFeature {
|
|||||||
))
|
))
|
||||||
.with_provided_service(ServiceDeclaration::new(
|
.with_provided_service(ServiceDeclaration::new(
|
||||||
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
||||||
WORKER_LIFECYCLE_SERVICE_VERSION,
|
WORKER_CONTROL_SERVICE_VERSION,
|
||||||
"Known-Worker discovery and permission-fenced control operations",
|
"Known-Worker discovery and permission-fenced control operations",
|
||||||
));
|
));
|
||||||
for operation in WorkerOperation::ALL {
|
for operation in WorkerOperation::ALL {
|
||||||
@@ -790,7 +685,7 @@ impl FeatureModule for ManageWorkerFeature {
|
|||||||
context.services().provide(
|
context.services().provide(
|
||||||
ServiceDeclaration::new(
|
ServiceDeclaration::new(
|
||||||
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
||||||
WORKER_LIFECYCLE_SERVICE_VERSION,
|
WORKER_CONTROL_SERVICE_VERSION,
|
||||||
"Known-Worker discovery and permission-fenced control operations",
|
"Known-Worker discovery and permission-fenced control operations",
|
||||||
),
|
),
|
||||||
self.control.clone(),
|
self.control.clone(),
|
||||||
@@ -1570,7 +1465,7 @@ mod tests {
|
|||||||
|
|
||||||
async fn ensure_permission(
|
async fn ensure_permission(
|
||||||
&self,
|
&self,
|
||||||
_subject: &crate::feature::builtin::WorkerObservationSubjectRef,
|
_subject: &crate::feature::contracts::WorkerObservationSubjectRef,
|
||||||
_permission: &str,
|
_permission: &str,
|
||||||
) -> Result<(), WorkspaceClientError> {
|
) -> Result<(), WorkspaceClientError> {
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1942,7 +1837,7 @@ mod tests {
|
|||||||
});
|
});
|
||||||
let permission_error = control
|
let permission_error = control
|
||||||
.ensure_permission(
|
.ensure_permission(
|
||||||
&crate::feature::builtin::WorkerObservationSubjectRef::RuntimeWorker {
|
&crate::feature::contracts::WorkerObservationSubjectRef::RuntimeWorker {
|
||||||
runtime_id: "runtime-1".to_string(),
|
runtime_id: "runtime-1".to_string(),
|
||||||
worker_id: "worker-1".to_string(),
|
worker_id: "worker-1".to_string(),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,14 +9,14 @@ use protocol::Segment;
|
|||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::manage_worker::{
|
use crate::feature::contracts::{
|
||||||
WORKER_LIFECYCLE_SERVICE_ID, WorkerLifecycleService, WorkerLifecycleSpawnRequest,
|
TICKET_SERVICE_ID, TICKET_SERVICE_VERSION, TicketService, WORKER_LIFECYCLE_SERVICE_ID,
|
||||||
|
WORKER_LIFECYCLE_SERVICE_VERSION, WorkerLifecycleService, WorkerLifecycleSpawnRequest,
|
||||||
WorkerLifecycleWorkdirAttachment,
|
WorkerLifecycleWorkdirAttachment,
|
||||||
};
|
};
|
||||||
use super::ticket::{TICKET_SERVICE_ID, TicketService};
|
|
||||||
use crate::feature::{
|
use crate::feature::{
|
||||||
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ServiceId,
|
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ServiceId,
|
||||||
ServiceRequirement, ToolContribution, ToolDeclaration,
|
ServiceRequirement, ServiceVersionReq, ToolContribution, ToolDeclaration,
|
||||||
};
|
};
|
||||||
|
|
||||||
const FEATURE_ID: &str = "orchestration";
|
const FEATURE_ID: &str = "orchestration";
|
||||||
@@ -34,14 +34,20 @@ impl FeatureModule for OrchestrationFeature {
|
|||||||
fn descriptor(&self) -> FeatureDescriptor {
|
fn descriptor(&self) -> FeatureDescriptor {
|
||||||
FeatureDescriptor::builtin(FEATURE_ID, "Orchestration")
|
FeatureDescriptor::builtin(FEATURE_ID, "Orchestration")
|
||||||
.with_description("Semantic Ticket orchestration operations.")
|
.with_description("Semantic Ticket orchestration operations.")
|
||||||
.with_service_requirement(ServiceRequirement::required(
|
.with_service_requirement(
|
||||||
ServiceId::builtin(TICKET_SERVICE_ID),
|
ServiceRequirement::required(
|
||||||
"SpawnTicketWorker requires current typed Ticket authority",
|
ServiceId::builtin(TICKET_SERVICE_ID),
|
||||||
))
|
"SpawnTicketWorker requires current typed Ticket authority",
|
||||||
.with_service_requirement(ServiceRequirement::required(
|
)
|
||||||
ServiceId::builtin(WORKER_LIFECYCLE_SERVICE_ID),
|
.with_version(ServiceVersionReq::exact(TICKET_SERVICE_VERSION)),
|
||||||
"SpawnTicketWorker requires Workspace Worker lifecycle authority",
|
)
|
||||||
))
|
.with_service_requirement(
|
||||||
|
ServiceRequirement::required(
|
||||||
|
ServiceId::builtin(WORKER_LIFECYCLE_SERVICE_ID),
|
||||||
|
"SpawnTicketWorker requires Workspace Worker lifecycle authority",
|
||||||
|
)
|
||||||
|
.with_version(ServiceVersionReq::exact(WORKER_LIFECYCLE_SERVICE_VERSION)),
|
||||||
|
)
|
||||||
.with_tool(ToolDeclaration::new(TOOL_NAME, TOOL_DESCRIPTION))
|
.with_tool(ToolDeclaration::new(TOOL_NAME, TOOL_DESCRIPTION))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +281,7 @@ mod tests {
|
|||||||
use ticket::{TicketError, TicketState};
|
use ticket::{TicketError, TicketState};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::feature::builtin::ticket::TicketHandoff;
|
use crate::feature::contracts::TicketHandoff;
|
||||||
use crate::worker::{WorkspaceClientError, WorkspaceResponse};
|
use crate::worker::{WorkspaceClientError, WorkspaceResponse};
|
||||||
|
|
||||||
struct FixedTicketService(TicketState);
|
struct FixedTicketService(TicketState);
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ mod native;
|
|||||||
mod notification_diagnostics;
|
mod notification_diagnostics;
|
||||||
mod tags;
|
mod tags;
|
||||||
|
|
||||||
|
use crate::feature::contracts::{
|
||||||
|
TICKET_SERVICE_ID, TICKET_SERVICE_VERSION, TicketHandoff, TicketService,
|
||||||
|
};
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
@@ -539,29 +543,6 @@ const FEATURE_NAME: &str = "Ticket tools";
|
|||||||
const FEATURE_DESCRIPTION: &str = "Intent-led Ticket reads and authorized decisions through the Workspace API; no universal MR/approval gate, and no Backend judgment of natural-language satisfaction.";
|
const FEATURE_DESCRIPTION: &str = "Intent-led Ticket reads and authorized decisions through the Workspace API; no universal MR/approval gate, and no Backend judgment of natural-language satisfaction.";
|
||||||
const TICKET_WORKFLOW_INSTRUCTION_ID: &str = "ticket.workflow";
|
const TICKET_WORKFLOW_INSTRUCTION_ID: &str = "ticket.workflow";
|
||||||
const TICKET_WORKFLOW_PROMPT_REF: &str = "common.tickets";
|
const TICKET_WORKFLOW_PROMPT_REF: &str = "common.tickets";
|
||||||
pub const TICKET_SERVICE_ID: &str = "ticket.authority";
|
|
||||||
const TICKET_SERVICE_VERSION: &str = "1";
|
|
||||||
|
|
||||||
pub trait TicketService: Send + Sync {
|
|
||||||
fn ticket_handoff(&self, ticket_ref: &str) -> Result<TicketHandoff, TicketError>;
|
|
||||||
|
|
||||||
/// Typed creation without a synthetic Tool context. The default keeps legacy
|
|
||||||
/// handoff-only fixture implementations fail-closed; production installs
|
|
||||||
/// provide creation through the shared authorized backend dispatch.
|
|
||||||
fn create(&self, _input: NewTicket) -> TicketResult<TicketRef> {
|
|
||||||
Err(TicketError::Conflict(
|
|
||||||
"Ticket creation service is unavailable".into(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct TicketHandoff {
|
|
||||||
pub id: String,
|
|
||||||
pub resource_key: String,
|
|
||||||
pub workflow_state: TicketState,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_canonical_ticket_resource_key(resource_key: &str) -> bool {
|
fn is_canonical_ticket_resource_key(resource_key: &str) -> bool {
|
||||||
resource_key.strip_prefix("T-").is_some_and(|sequence| {
|
resource_key.strip_prefix("T-").is_some_and(|sequence| {
|
||||||
!sequence.is_empty() && sequence.bytes().all(|byte| byte.is_ascii_digit())
|
!sequence.is_empty() && sequence.bytes().all(|byte| byte.is_ascii_digit())
|
||||||
|
|||||||
@@ -18,14 +18,17 @@ use agen::Item;
|
|||||||
use agen::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
use agen::tool::{Tool, ToolDefinition, ToolError, ToolMeta, ToolOutput};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Deserialize;
|
||||||
use session_store::LogEntry;
|
|
||||||
|
|
||||||
use super::manage_worker::{WORKER_CONTROL_SERVICE_ID, WorkerControlService};
|
use crate::feature::contracts::{
|
||||||
|
WORKER_CONTROL_SERVICE_ID, WORKER_CONTROL_SERVICE_VERSION, WorkerControlService,
|
||||||
|
WorkerObservationError, WorkerObservationProvider, WorkerObservationSubject,
|
||||||
|
WorkerObservationSubjectRef, WorkerSessionCapture,
|
||||||
|
};
|
||||||
use crate::feature::{
|
use crate::feature::{
|
||||||
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureInstructionContribution,
|
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureInstructionContribution,
|
||||||
FeatureInstructionDeclaration, FeatureInstructionId, FeatureModule, ServiceId,
|
FeatureInstructionDeclaration, FeatureInstructionId, FeatureModule, ServiceId,
|
||||||
ServiceRequirement, ToolContribution, ToolDeclaration,
|
ServiceRequirement, ServiceVersionReq, ToolContribution, ToolDeclaration,
|
||||||
};
|
};
|
||||||
use crate::session_capture::{
|
use crate::session_capture::{
|
||||||
ReadDetail, ReadOptions, ReadSelector, ReferenceKind, SearchOptions, SessionCapture,
|
ReadDetail, ReadOptions, ReadSelector, ReferenceKind, SearchOptions, SessionCapture,
|
||||||
@@ -49,69 +52,6 @@ fn observation_instruction() -> FeatureInstructionDeclaration {
|
|||||||
.expect("static worker-observation instruction declaration is valid")
|
.expect("static worker-observation instruction declaration is valid")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(
|
|
||||||
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
|
|
||||||
)]
|
|
||||||
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
|
||||||
pub enum WorkerObservationSubjectRef {
|
|
||||||
RuntimeWorker {
|
|
||||||
runtime_id: String,
|
|
||||||
worker_id: String,
|
|
||||||
},
|
|
||||||
SubWorker {
|
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct WorkerObservationSubject {
|
|
||||||
pub subject: WorkerObservationSubjectRef,
|
|
||||||
pub display_name: String,
|
|
||||||
pub relation: String,
|
|
||||||
pub status: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct WorkerSessionCapture {
|
|
||||||
pub segment_id: String,
|
|
||||||
pub session: protocol::SessionSnapshot,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl WorkerSessionCapture {
|
|
||||||
pub fn from_log_entries(
|
|
||||||
segment_id: impl Into<String>,
|
|
||||||
log_entries: &[LogEntry],
|
|
||||||
) -> Result<Self, String> {
|
|
||||||
Ok(Self {
|
|
||||||
segment_id: segment_id.into(),
|
|
||||||
session: session_store::public_snapshot::project_current_session_snapshot(log_entries),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, thiserror::Error)]
|
|
||||||
pub enum WorkerObservationError {
|
|
||||||
#[error("worker session was not found or is not accessible")]
|
|
||||||
NotFound,
|
|
||||||
#[error("worker session observation failed: {0}")]
|
|
||||||
Unavailable(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[async_trait]
|
|
||||||
pub trait WorkerObservationProvider: Send + Sync {
|
|
||||||
/// Returns only subjects already authorized for the current Worker.
|
|
||||||
async fn list_worker_sessions(
|
|
||||||
&self,
|
|
||||||
) -> Result<Vec<WorkerObservationSubject>, WorkerObservationError>;
|
|
||||||
|
|
||||||
/// Reauthorizes and captures the latest committed session for one subject.
|
|
||||||
/// Unauthorized and missing subjects must both return `NotFound`.
|
|
||||||
async fn capture_worker_session(
|
|
||||||
&self,
|
|
||||||
subject: &WorkerObservationSubjectRef,
|
|
||||||
) -> Result<WorkerSessionCapture, WorkerObservationError>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct WorkspaceWorkerObservationListResponse {
|
struct WorkspaceWorkerObservationListResponse {
|
||||||
sessions: Vec<WorkerObservationSubject>,
|
sessions: Vec<WorkerObservationSubject>,
|
||||||
@@ -290,7 +230,7 @@ impl FeatureModule for WorkerObservationFeature {
|
|||||||
.with_service_requirement(ServiceRequirement::required(
|
.with_service_requirement(ServiceRequirement::required(
|
||||||
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
||||||
"Worker observation extends the known-Worker control authority",
|
"Worker observation extends the known-Worker control authority",
|
||||||
))
|
).with_version(ServiceVersionReq::exact(WORKER_CONTROL_SERVICE_VERSION)))
|
||||||
.with_instruction(observation_instruction())
|
.with_instruction(observation_instruction())
|
||||||
.with_tool(ToolDeclaration::new(
|
.with_tool(ToolDeclaration::new(
|
||||||
"ViewSessionOverview",
|
"ViewSessionOverview",
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
//! Provider-independent contracts used by Worker Features and trusted host composition.
|
||||||
|
//!
|
||||||
|
//! Service IDs identify a capability's semantics, not a Feature, backend, Rust type name, or
|
||||||
|
//! TypeId. Versions are exact compatibility epochs checked by the existing install planner;
|
||||||
|
//! they are not crate versions or semver ranges. Compatible implementation changes retain the
|
||||||
|
//! ID/epoch. Changes to required methods, request/result meaning, authorization guarantees, or
|
||||||
|
//! subject routing that break consumers increment the affected epoch and update providers and
|
||||||
|
//! requirements together. A different capability needs a different ID, not provider priority.
|
||||||
|
//! Rust downcasts only validate an in-process binding and are not a public compatibility ABI.
|
||||||
|
//!
|
||||||
|
//! Trusted composition selects the exact-one control provider, including the explicit combined
|
||||||
|
//! Workspace/SubWorker implementation. Acquiring a handle does not grant authority: providers
|
||||||
|
//! must revalidate current authority, Workspace ownership and direct-child constraints at each
|
||||||
|
//! operation. Observation tools bound public projections; providers reauthorize every capture.
|
||||||
|
//! These Rust contracts are not a Plugin/guest ABI; an external boundary needs an explicit adapter.
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use protocol::Segment;
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use session_store::LogEntry;
|
||||||
|
use ticket::{NewTicket, Result as TicketResult, TicketError, TicketRef, TicketState};
|
||||||
|
|
||||||
|
// Reuse the Host-neutral response/error vocabulary rather than redesigning transport DTOs.
|
||||||
|
use crate::worker::{WorkspaceClientError, WorkspaceResponse};
|
||||||
|
|
||||||
|
pub const WORKER_CONTROL_SERVICE_ID: &str = "worker.control";
|
||||||
|
pub const WORKER_CONTROL_SERVICE_VERSION: &str = "1";
|
||||||
|
pub const WORKER_LIFECYCLE_SERVICE_ID: &str = "worker.lifecycle";
|
||||||
|
pub const WORKER_LIFECYCLE_SERVICE_VERSION: &str = "1";
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub enum WorkerControlSubject {
|
||||||
|
RuntimeWorker {
|
||||||
|
runtime_id: String,
|
||||||
|
worker_id: String,
|
||||||
|
},
|
||||||
|
SubWorker {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum WorkerControlInputKind {
|
||||||
|
User,
|
||||||
|
Notify,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum WorkerControlStopKind {
|
||||||
|
Cancel,
|
||||||
|
Stop,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait WorkerControlService: Send + Sync {
|
||||||
|
fn workspace_id(&self) -> &str;
|
||||||
|
async fn list_workers(&self) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
||||||
|
async fn send_input(
|
||||||
|
&self,
|
||||||
|
subject: WorkerControlSubject,
|
||||||
|
content: String,
|
||||||
|
kind: WorkerControlInputKind,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
||||||
|
async fn stop_worker(
|
||||||
|
&self,
|
||||||
|
subject: WorkerControlSubject,
|
||||||
|
kind: WorkerControlStopKind,
|
||||||
|
reason: Option<String>,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
||||||
|
async fn spawn_worker(
|
||||||
|
&self,
|
||||||
|
request: WorkerLifecycleSpawnRequest,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
||||||
|
async fn restore_worker(
|
||||||
|
&self,
|
||||||
|
runtime_id: String,
|
||||||
|
worker_id: String,
|
||||||
|
request: server_api::WorkerRestoreRequest,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||||
|
let _ = (runtime_id, worker_id, request);
|
||||||
|
Err(WorkspaceClientError::Unavailable(
|
||||||
|
"Runtime Worker restore is unavailable".to_string(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn remove_runtime_worker(
|
||||||
|
&self,
|
||||||
|
runtime_id: &str,
|
||||||
|
worker_id: &str,
|
||||||
|
reason: &str,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
||||||
|
async fn ensure_permission(
|
||||||
|
&self,
|
||||||
|
subject: &WorkerObservationSubjectRef,
|
||||||
|
permission: &str,
|
||||||
|
) -> Result<(), WorkspaceClientError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait WorkerLifecycleService: Send + Sync {
|
||||||
|
async fn spawn(
|
||||||
|
&self,
|
||||||
|
request: WorkerLifecycleSpawnRequest,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct WorkerLifecycleWorkdirAttachment {
|
||||||
|
/// Stable Worker-local routing alias preserved into the canonical create request.
|
||||||
|
pub alias: String,
|
||||||
|
/// Workspace-authoritative Workdir id preserved into the canonical create request.
|
||||||
|
pub working_directory_id: String,
|
||||||
|
/// Optional normalized initial cwd relative to this Workdir.
|
||||||
|
pub relative_cwd: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WorkerLifecycleSpawnRequest {
|
||||||
|
/// Observed delegation required for Orchestrator Ticket starts.
|
||||||
|
pub orchestrator_assignment_id: Option<String>,
|
||||||
|
|
||||||
|
pub runtime_id: String,
|
||||||
|
/// Ordered alias-keyed selections forwarded as canonical Workdir attachments.
|
||||||
|
pub workdir_attachments: Vec<WorkerLifecycleWorkdirAttachment>,
|
||||||
|
pub profile: String,
|
||||||
|
pub singleton_key: Option<String>,
|
||||||
|
pub ticket_id: Option<String>,
|
||||||
|
/// Required for ordinary and Ticket spawn; exact retries reuse this ID and full input.
|
||||||
|
pub operation_id: Option<String>,
|
||||||
|
/// Explicit requested authority. Profile selection alone grants nothing.
|
||||||
|
pub execution_grant: Option<server_api::authorization::WorkerDelegationRequest>,
|
||||||
|
pub display_name: String,
|
||||||
|
pub initial_submit: Vec<Segment>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(
|
||||||
|
Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, JsonSchema,
|
||||||
|
)]
|
||||||
|
#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
|
||||||
|
pub enum WorkerObservationSubjectRef {
|
||||||
|
RuntimeWorker {
|
||||||
|
runtime_id: String,
|
||||||
|
worker_id: String,
|
||||||
|
},
|
||||||
|
SubWorker {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct WorkerObservationSubject {
|
||||||
|
pub subject: WorkerObservationSubjectRef,
|
||||||
|
pub display_name: String,
|
||||||
|
pub relation: String,
|
||||||
|
pub status: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct WorkerSessionCapture {
|
||||||
|
pub segment_id: String,
|
||||||
|
pub session: protocol::SessionSnapshot,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WorkerSessionCapture {
|
||||||
|
pub fn from_log_entries(
|
||||||
|
segment_id: impl Into<String>,
|
||||||
|
log_entries: &[LogEntry],
|
||||||
|
) -> Result<Self, String> {
|
||||||
|
Ok(Self {
|
||||||
|
segment_id: segment_id.into(),
|
||||||
|
session: session_store::public_snapshot::project_current_session_snapshot(log_entries),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, thiserror::Error)]
|
||||||
|
pub enum WorkerObservationError {
|
||||||
|
#[error("worker session was not found or is not accessible")]
|
||||||
|
NotFound,
|
||||||
|
#[error("worker session observation failed: {0}")]
|
||||||
|
Unavailable(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait WorkerObservationProvider: Send + Sync {
|
||||||
|
/// Returns only subjects already authorized for the current Worker.
|
||||||
|
async fn list_worker_sessions(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<WorkerObservationSubject>, WorkerObservationError>;
|
||||||
|
|
||||||
|
/// Reauthorizes and captures the latest committed session for one subject.
|
||||||
|
/// Unauthorized and missing subjects must both return `NotFound`.
|
||||||
|
async fn capture_worker_session(
|
||||||
|
&self,
|
||||||
|
subject: &WorkerObservationSubjectRef,
|
||||||
|
) -> Result<WorkerSessionCapture, WorkerObservationError>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const TICKET_SERVICE_ID: &str = "ticket.authority";
|
||||||
|
pub const TICKET_SERVICE_VERSION: &str = "1";
|
||||||
|
|
||||||
|
pub trait TicketService: Send + Sync {
|
||||||
|
fn ticket_handoff(&self, ticket_ref: &str) -> Result<TicketHandoff, TicketError>;
|
||||||
|
|
||||||
|
/// Typed creation without a synthetic Tool context. The default keeps legacy
|
||||||
|
/// handoff-only fixture implementations fail-closed; production installs
|
||||||
|
/// provide creation through the shared authorized backend dispatch.
|
||||||
|
fn create(&self, _input: NewTicket) -> TicketResult<TicketRef> {
|
||||||
|
Err(TicketError::Conflict(
|
||||||
|
"Ticket creation service is unavailable".into(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct TicketHandoff {
|
||||||
|
pub id: String,
|
||||||
|
pub resource_key: String,
|
||||||
|
pub workflow_state: TicketState,
|
||||||
|
}
|
||||||
|
#[cfg(test)]
|
||||||
|
#[path = "contracts_tests.rs"]
|
||||||
|
mod tests;
|
||||||
@@ -0,0 +1,378 @@
|
|||||||
|
//! Component coverage: real consumer installation, planner and Tool dispatch with contract-only
|
||||||
|
//! test providers. No concrete provider constructor, Workspace client or spawned-worker registry is
|
||||||
|
//! passed to a consumer. Production authority/routing is covered by the providers' own suites.
|
||||||
|
|
||||||
|
use std::sync::{
|
||||||
|
Arc, Mutex,
|
||||||
|
atomic::{AtomicBool, AtomicUsize, Ordering},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
use crate::feature::builtin::{
|
||||||
|
orchestration::orchestration_feature, worker_observation::WorkerObservationFeature,
|
||||||
|
};
|
||||||
|
use crate::feature::{
|
||||||
|
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, FeaturePlanError,
|
||||||
|
FeatureRegistryBuilder, ServiceDeclaration, ServiceId, ServiceRequirement,
|
||||||
|
};
|
||||||
|
use crate::hook::HookRegistryBuilder;
|
||||||
|
use agen::tool::{Tool, ToolDefinition, ToolExecutionContext};
|
||||||
|
|
||||||
|
struct TestServices {
|
||||||
|
revoked: AtomicBool,
|
||||||
|
captures: AtomicUsize,
|
||||||
|
spawns: Mutex<Vec<WorkerLifecycleSpawnRequest>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TestServices {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
revoked: AtomicBool::new(false),
|
||||||
|
captures: AtomicUsize::new(0),
|
||||||
|
spawns: Mutex::new(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response() -> WorkspaceResponse {
|
||||||
|
WorkspaceResponse {
|
||||||
|
status: 200,
|
||||||
|
body: r#"{"worker_id":"test-worker"}"#.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl WorkerLifecycleService for TestServices {
|
||||||
|
async fn spawn(
|
||||||
|
&self,
|
||||||
|
request: WorkerLifecycleSpawnRequest,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||||
|
self.spawns.lock().unwrap().push(request);
|
||||||
|
Ok(response())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TicketService for TestServices {
|
||||||
|
fn ticket_handoff(&self, ticket_ref: &str) -> Result<TicketHandoff, TicketError> {
|
||||||
|
assert_eq!(ticket_ref, "T-788");
|
||||||
|
Ok(TicketHandoff {
|
||||||
|
id: "ticket-internal-id".into(),
|
||||||
|
resource_key: ticket_ref.into(),
|
||||||
|
workflow_state: TicketState::Open,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl WorkerControlService for TestServices {
|
||||||
|
fn workspace_id(&self) -> &str {
|
||||||
|
"test-workspace"
|
||||||
|
}
|
||||||
|
async fn list_workers(&self) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||||
|
unreachable!("not an observation operation")
|
||||||
|
}
|
||||||
|
async fn send_input(
|
||||||
|
&self,
|
||||||
|
_: WorkerControlSubject,
|
||||||
|
_: String,
|
||||||
|
_: WorkerControlInputKind,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||||
|
unreachable!("not a consumer operation")
|
||||||
|
}
|
||||||
|
async fn stop_worker(
|
||||||
|
&self,
|
||||||
|
_: WorkerControlSubject,
|
||||||
|
_: WorkerControlStopKind,
|
||||||
|
_: Option<String>,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||||
|
unreachable!("not a consumer operation")
|
||||||
|
}
|
||||||
|
async fn spawn_worker(
|
||||||
|
&self,
|
||||||
|
_: WorkerLifecycleSpawnRequest,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||||
|
unreachable!("orchestration must use lifecycle")
|
||||||
|
}
|
||||||
|
fn remove_runtime_worker(
|
||||||
|
&self,
|
||||||
|
_: &str,
|
||||||
|
_: &str,
|
||||||
|
_: &str,
|
||||||
|
) -> Result<WorkspaceResponse, WorkspaceClientError> {
|
||||||
|
unreachable!("not a consumer operation")
|
||||||
|
}
|
||||||
|
async fn ensure_permission(
|
||||||
|
&self,
|
||||||
|
subject: &WorkerObservationSubjectRef,
|
||||||
|
permission: &str,
|
||||||
|
) -> Result<(), WorkspaceClientError> {
|
||||||
|
assert_eq!(permission, "observe");
|
||||||
|
if !self.revoked.load(Ordering::SeqCst)
|
||||||
|
&& matches!(subject, WorkerObservationSubjectRef::SubWorker { name } if name == "child")
|
||||||
|
{
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(WorkspaceClientError::Request("not granted".into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl WorkerObservationProvider for TestServices {
|
||||||
|
async fn list_worker_sessions(
|
||||||
|
&self,
|
||||||
|
) -> Result<Vec<WorkerObservationSubject>, WorkerObservationError> {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
|
async fn capture_worker_session(
|
||||||
|
&self,
|
||||||
|
_: &WorkerObservationSubjectRef,
|
||||||
|
) -> Result<WorkerSessionCapture, WorkerObservationError> {
|
||||||
|
self.captures.fetch_add(1, Ordering::SeqCst);
|
||||||
|
WorkerSessionCapture::from_log_entries("test-segment", &[])
|
||||||
|
.map_err(WorkerObservationError::Unavailable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TestProvider {
|
||||||
|
id: &'static str,
|
||||||
|
services: Arc<TestServices>,
|
||||||
|
declarations: Vec<ServiceDeclaration>,
|
||||||
|
requirements: Vec<ServiceRequirement>,
|
||||||
|
undeclared: bool,
|
||||||
|
installs: Arc<AtomicUsize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TestProvider {
|
||||||
|
fn new(id: &'static str, services: Arc<TestServices>) -> Self {
|
||||||
|
Self {
|
||||||
|
id,
|
||||||
|
services,
|
||||||
|
declarations: vec![
|
||||||
|
ServiceDeclaration::new(
|
||||||
|
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
||||||
|
WORKER_CONTROL_SERVICE_VERSION,
|
||||||
|
"test control",
|
||||||
|
),
|
||||||
|
ServiceDeclaration::new(
|
||||||
|
ServiceId::builtin(WORKER_LIFECYCLE_SERVICE_ID),
|
||||||
|
WORKER_LIFECYCLE_SERVICE_VERSION,
|
||||||
|
"test lifecycle",
|
||||||
|
),
|
||||||
|
ServiceDeclaration::new(
|
||||||
|
ServiceId::builtin(TICKET_SERVICE_ID),
|
||||||
|
TICKET_SERVICE_VERSION,
|
||||||
|
"test Ticket",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
requirements: Vec::new(),
|
||||||
|
undeclared: false,
|
||||||
|
installs: Arc::new(AtomicUsize::new(0)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FeatureModule for TestProvider {
|
||||||
|
fn descriptor(&self) -> FeatureDescriptor {
|
||||||
|
let mut descriptor = FeatureDescriptor::builtin(self.id, "Contract test provider");
|
||||||
|
descriptor.provides_services = self.declarations.clone();
|
||||||
|
descriptor.requires_services = self.requirements.clone();
|
||||||
|
descriptor
|
||||||
|
}
|
||||||
|
fn install(&self, context: &mut FeatureInstallContext<'_>) -> Result<(), FeatureInstallError> {
|
||||||
|
self.installs.fetch_add(1, Ordering::SeqCst);
|
||||||
|
for declaration in &self.declarations {
|
||||||
|
match declaration.id.as_str() {
|
||||||
|
"builtin:worker.control" => context.services().provide(
|
||||||
|
declaration.clone(),
|
||||||
|
self.services.clone() as Arc<dyn WorkerControlService>,
|
||||||
|
)?,
|
||||||
|
"builtin:worker.lifecycle" => context.services().provide(
|
||||||
|
declaration.clone(),
|
||||||
|
self.services.clone() as Arc<dyn WorkerLifecycleService>,
|
||||||
|
)?,
|
||||||
|
"builtin:ticket.authority" => context.services().provide(
|
||||||
|
declaration.clone(),
|
||||||
|
self.services.clone() as Arc<dyn TicketService>,
|
||||||
|
)?,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if self.undeclared {
|
||||||
|
context.services().provide(
|
||||||
|
ServiceDeclaration::new(ServiceId::builtin("hidden"), "1", "undeclared"),
|
||||||
|
Arc::new(()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn consumers(services: Arc<TestServices>) -> FeatureRegistryBuilder {
|
||||||
|
FeatureRegistryBuilder::new()
|
||||||
|
.with_module(orchestration_feature())
|
||||||
|
.with_module(WorkerObservationFeature::new(services))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tool(tools: &[ToolDefinition], name: &str) -> Arc<dyn Tool> {
|
||||||
|
tools
|
||||||
|
.iter()
|
||||||
|
.map(|factory| factory())
|
||||||
|
.find(|(meta, _)| meta.name == name)
|
||||||
|
.unwrap()
|
||||||
|
.1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn consumers_install_and_execute_with_contract_only_test_provider() {
|
||||||
|
let services = Arc::new(TestServices::default());
|
||||||
|
let mut tools = Vec::new();
|
||||||
|
let report = consumers(services.clone())
|
||||||
|
.with_module(TestProvider::new("test-provider", services.clone()))
|
||||||
|
.install_into_pending(&mut tools, &mut HookRegistryBuilder::default());
|
||||||
|
assert!(!report.has_errors(), "{report:?}");
|
||||||
|
let result = tool(&tools, "SpawnTicketWorker").execute(
|
||||||
|
r#"{"ticket_id":"T-788","runtime_id":"runtime-test","profile":"builtin:coder","initial_request":"implement","orchestrator_assignment_id":"delegation-test"}"#,
|
||||||
|
ToolExecutionContext::new("spawn-call", "batch", 0),
|
||||||
|
).await.unwrap();
|
||||||
|
assert_eq!(result.summary, "Spawned Worker for Ticket T-788");
|
||||||
|
{
|
||||||
|
let requests = services.spawns.lock().unwrap();
|
||||||
|
assert_eq!(requests.len(), 1);
|
||||||
|
assert_eq!(requests[0].ticket_id.as_deref(), Some("ticket-internal-id"));
|
||||||
|
assert_eq!(
|
||||||
|
requests[0].orchestrator_assignment_id.as_deref(),
|
||||||
|
Some("delegation-test")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
requests[0].operation_id.as_deref(),
|
||||||
|
Some("spawn-ticket-worker:ticket-internal-id:spawn-call")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
requests[0].initial_submit,
|
||||||
|
vec![
|
||||||
|
Segment::Flow {
|
||||||
|
selector: "builtin:coder-review".into()
|
||||||
|
},
|
||||||
|
Segment::text("implement")
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let overview = tool(&tools, "ViewSessionOverview");
|
||||||
|
let granted = r#"{"subject":{"kind":"sub_worker","name":"child"}}"#;
|
||||||
|
overview
|
||||||
|
.execute(granted, ToolExecutionContext::direct())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(services.captures.load(Ordering::SeqCst), 1);
|
||||||
|
services.revoked.store(true, Ordering::SeqCst);
|
||||||
|
for denied in [
|
||||||
|
granted,
|
||||||
|
r#"{"subject":{"kind":"sub_worker","name":"non-child"}}"#,
|
||||||
|
] {
|
||||||
|
let error = overview
|
||||||
|
.execute(denied, ToolExecutionContext::direct())
|
||||||
|
.await
|
||||||
|
.unwrap_err();
|
||||||
|
assert!(error.to_string().contains("not found or is not accessible"));
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
services.captures.load(Ordering::SeqCst),
|
||||||
|
1,
|
||||||
|
"revocation must fence the existing handle before capture"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn consumer_plan_rejections_leave_no_partial_installation() {
|
||||||
|
for condition in [
|
||||||
|
"missing",
|
||||||
|
"duplicate",
|
||||||
|
"control-version",
|
||||||
|
"lifecycle-version",
|
||||||
|
"ticket-version",
|
||||||
|
"cycle",
|
||||||
|
"undeclared",
|
||||||
|
] {
|
||||||
|
let services = Arc::new(TestServices::default());
|
||||||
|
let mut provider = TestProvider::new("test-provider", services.clone());
|
||||||
|
let installs = provider.installs.clone();
|
||||||
|
let mut builder = consumers(services.clone());
|
||||||
|
match condition {
|
||||||
|
"missing" => {}
|
||||||
|
"duplicate" => {
|
||||||
|
builder.add_module(TestProvider::new("duplicate-provider", services.clone()));
|
||||||
|
}
|
||||||
|
"control-version" => provider.declarations[0].version = "incompatible".into(),
|
||||||
|
"lifecycle-version" => provider.declarations[1].version = "incompatible".into(),
|
||||||
|
"ticket-version" => provider.declarations[2].version = "incompatible".into(),
|
||||||
|
"cycle" => {
|
||||||
|
provider.requirements.push(ServiceRequirement::required(
|
||||||
|
ServiceId::builtin("consumer-output"),
|
||||||
|
"cycle",
|
||||||
|
));
|
||||||
|
struct CyclicConsumer;
|
||||||
|
impl FeatureModule for CyclicConsumer {
|
||||||
|
fn descriptor(&self) -> FeatureDescriptor {
|
||||||
|
FeatureDescriptor::builtin("cyclic-consumer", "Cycle")
|
||||||
|
.with_service_requirement(ServiceRequirement::required(
|
||||||
|
ServiceId::builtin(WORKER_CONTROL_SERVICE_ID),
|
||||||
|
"control",
|
||||||
|
))
|
||||||
|
.with_provided_service(ServiceDeclaration::new(
|
||||||
|
ServiceId::builtin("consumer-output"),
|
||||||
|
"1",
|
||||||
|
"output",
|
||||||
|
))
|
||||||
|
}
|
||||||
|
fn install(
|
||||||
|
&self,
|
||||||
|
_: &mut FeatureInstallContext<'_>,
|
||||||
|
) -> Result<(), FeatureInstallError> {
|
||||||
|
panic!("cycle must fail preflight")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
builder.add_module(CyclicConsumer);
|
||||||
|
}
|
||||||
|
"undeclared" => provider.undeclared = true,
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
if condition != "missing" {
|
||||||
|
builder.add_module(provider);
|
||||||
|
}
|
||||||
|
let mut tools = Vec::new();
|
||||||
|
let report = builder.install_into_pending(&mut tools, &mut HookRegistryBuilder::default());
|
||||||
|
assert!(report.has_errors(), "{condition}: {report:?}");
|
||||||
|
assert!(tools.is_empty(), "{condition}");
|
||||||
|
assert!(report.services.providers().is_empty(), "{condition}");
|
||||||
|
assert!(
|
||||||
|
report.reports.iter().all(|entry| !entry.installed),
|
||||||
|
"{condition}"
|
||||||
|
);
|
||||||
|
match (condition, report.plan_error) {
|
||||||
|
("missing", Some(FeaturePlanError::MissingServiceProvider { .. }))
|
||||||
|
| ("duplicate", Some(FeaturePlanError::AmbiguousServiceProvider { .. }))
|
||||||
|
| (
|
||||||
|
"control-version" | "lifecycle-version" | "ticket-version",
|
||||||
|
Some(FeaturePlanError::ServiceVersionMismatch { .. }),
|
||||||
|
)
|
||||||
|
| ("cycle", Some(FeaturePlanError::ServiceDependencyCycle { .. })) => {
|
||||||
|
assert_eq!(installs.load(Ordering::SeqCst), 0)
|
||||||
|
}
|
||||||
|
("undeclared", None) => {
|
||||||
|
assert_eq!(installs.load(Ordering::SeqCst), 1);
|
||||||
|
assert!(
|
||||||
|
report
|
||||||
|
.reports
|
||||||
|
.iter()
|
||||||
|
.flat_map(|entry| &entry.diagnostics)
|
||||||
|
.any(|diagnostic| diagnostic
|
||||||
|
.message
|
||||||
|
.contains("undeclared Service contribution"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
(_, error) => panic!("{condition}: unexpected plan error {error:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1528,16 +1528,16 @@ enabled = false
|
|||||||
crate::feature::builtin::worker_observation::SpawnedSubWorkerObservationProvider::new(
|
crate::feature::builtin::worker_observation::SpawnedSubWorkerObservationProvider::new(
|
||||||
registry.clone(),
|
registry.clone(),
|
||||||
);
|
);
|
||||||
let observed_child =
|
let observed_child = crate::feature::contracts::WorkerObservationSubjectRef::SubWorker {
|
||||||
crate::feature::builtin::worker_observation::WorkerObservationSubjectRef::SubWorker {
|
name: "reviewer-child".to_string(),
|
||||||
name: "reviewer-child".to_string(),
|
};
|
||||||
};
|
let first_capture =
|
||||||
let first_capture = crate::feature::builtin::worker_observation::WorkerObservationProvider::capture_worker_session(
|
crate::feature::contracts::WorkerObservationProvider::capture_worker_session(
|
||||||
&observation,
|
&observation,
|
||||||
&observed_child,
|
&observed_child,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert!(
|
assert!(
|
||||||
serde_json::to_string(&first_capture.session)
|
serde_json::to_string(&first_capture.session)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1554,12 +1554,13 @@ enabled = false
|
|||||||
crate::internal_worker::InternalWorkerSessionStatus::Idle
|
crate::internal_worker::InternalWorkerSessionStatus::Idle
|
||||||
);
|
);
|
||||||
assert_eq!(calls.load(Ordering::SeqCst), 2);
|
assert_eq!(calls.load(Ordering::SeqCst), 2);
|
||||||
let latest_capture = crate::feature::builtin::worker_observation::WorkerObservationProvider::capture_worker_session(
|
let latest_capture =
|
||||||
&observation,
|
crate::feature::contracts::WorkerObservationProvider::capture_worker_session(
|
||||||
&observed_child,
|
&observation,
|
||||||
)
|
&observed_child,
|
||||||
.await
|
)
|
||||||
.unwrap();
|
.await
|
||||||
|
.unwrap();
|
||||||
assert!(latest_capture.session.entries.len() > first_capture.session.entries.len());
|
assert!(latest_capture.session.entries.len() > first_capture.session.entries.len());
|
||||||
|
|
||||||
fail_requests.store(true, Ordering::SeqCst);
|
fail_requests.store(true, Ordering::SeqCst);
|
||||||
|
|||||||
@@ -53,8 +53,9 @@ use crate::compact::telemetry::{
|
|||||||
};
|
};
|
||||||
use crate::compact::usage_tracker::{UsageTracker, persist_pending_usage};
|
use crate::compact::usage_tracker::{UsageTracker, persist_pending_usage};
|
||||||
use crate::feature::background::{BackgroundTaskRewriteGuard, FeatureBackgroundTaskRegistry};
|
use crate::feature::background::{BackgroundTaskRewriteGuard, FeatureBackgroundTaskRegistry};
|
||||||
|
use crate::feature::builtin::TaskFeature;
|
||||||
use crate::feature::builtin::memory::WorkspaceMemoryBackendError;
|
use crate::feature::builtin::memory::WorkspaceMemoryBackendError;
|
||||||
use crate::feature::builtin::{TaskFeature, WorkerObservationProvider};
|
use crate::feature::contracts::WorkerObservationProvider;
|
||||||
use crate::feature::session::{
|
use crate::feature::session::{
|
||||||
CommittedRunExit, CommittedSessionCapture, CommittedSessionCaptureHandle, FeatureSessionError,
|
CommittedRunExit, CommittedSessionCapture, CommittedSessionCaptureHandle, FeatureSessionError,
|
||||||
SessionExtensionHandle,
|
SessionExtensionHandle,
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
//! Source dependency regression for the neutral contracts and representative consumers. Parse Rust
|
||||||
|
//! syntax so grouped/renamed imports and fully qualified type/expression paths are checked alike.
|
||||||
|
|
||||||
|
use syn::visit::Visit;
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct ProviderDependencies(Vec<String>);
|
||||||
|
|
||||||
|
impl ProviderDependencies {
|
||||||
|
fn check(&mut self, path: &[String]) {
|
||||||
|
// These modules are provider/composition namespaces. `super::*` inside a consumer's own
|
||||||
|
// unit tests is local access, but importing/aliasing the parent namespace is not.
|
||||||
|
if path.windows(2).any(|pair| pair == ["feature", "builtin"])
|
||||||
|
|| path.first().is_some_and(|segment| segment == "builtin")
|
||||||
|
|| (path.first().is_some_and(|segment| segment == "super")
|
||||||
|
&& path.get(1).is_none_or(|segment| segment != "*"))
|
||||||
|
{
|
||||||
|
self.0.push(path.join("::"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn use_tree(&mut self, prefix: &mut Vec<String>, tree: &syn::UseTree) {
|
||||||
|
match tree {
|
||||||
|
syn::UseTree::Path(path) => {
|
||||||
|
prefix.push(path.ident.to_string());
|
||||||
|
self.use_tree(prefix, &path.tree);
|
||||||
|
prefix.pop();
|
||||||
|
}
|
||||||
|
syn::UseTree::Group(group) => {
|
||||||
|
for item in &group.items {
|
||||||
|
self.use_tree(prefix, item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
syn::UseTree::Name(name) => {
|
||||||
|
prefix.push(name.ident.to_string());
|
||||||
|
self.check(prefix);
|
||||||
|
prefix.pop();
|
||||||
|
}
|
||||||
|
syn::UseTree::Rename(rename) => {
|
||||||
|
prefix.push(rename.ident.to_string());
|
||||||
|
self.check(prefix);
|
||||||
|
prefix.pop();
|
||||||
|
}
|
||||||
|
syn::UseTree::Glob(_) => {
|
||||||
|
prefix.push("*".into());
|
||||||
|
self.check(prefix);
|
||||||
|
prefix.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ast> Visit<'ast> for ProviderDependencies {
|
||||||
|
fn visit_item_use(&mut self, item: &'ast syn::ItemUse) {
|
||||||
|
self.use_tree(&mut Vec::new(), &item.tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn visit_path(&mut self, path: &'ast syn::Path) {
|
||||||
|
self.check(
|
||||||
|
&path
|
||||||
|
.segments
|
||||||
|
.iter()
|
||||||
|
.map(|segment| segment.ident.to_string())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
);
|
||||||
|
syn::visit::visit_path(self, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependencies(source: &str) -> Vec<String> {
|
||||||
|
let file = syn::parse_file(source).expect("valid Rust source");
|
||||||
|
let mut visitor = ProviderDependencies::default();
|
||||||
|
visitor.visit_file(&file);
|
||||||
|
visitor.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn contracts_and_consumers_do_not_depend_on_provider_namespaces() {
|
||||||
|
for (name, source) in [
|
||||||
|
("contracts", include_str!("../src/feature/contracts.rs")),
|
||||||
|
(
|
||||||
|
"orchestration",
|
||||||
|
include_str!("../src/feature/builtin/orchestration.rs"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"worker_observation",
|
||||||
|
include_str!("../src/feature/builtin/worker_observation.rs"),
|
||||||
|
),
|
||||||
|
] {
|
||||||
|
assert_eq!(dependencies(source), Vec::<String>::new(), "{name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn architecture_guard_detects_imports_reexports_aliases_and_qualified_provider_paths() {
|
||||||
|
for source in [
|
||||||
|
"use super::manage_worker::WorkerControlService;",
|
||||||
|
"pub use super::ticket::TicketService;",
|
||||||
|
"use crate::feature::{builtin::{manage_worker as provider, ticket::*}};",
|
||||||
|
"use crate::feature::builtin as providers;",
|
||||||
|
"use super as providers;",
|
||||||
|
"type Contract = dyn crate::feature::builtin::manage_worker::WorkerControlService;",
|
||||||
|
"fn consumer() { crate::feature::builtin::manage_worker::manage_worker_feature(); }",
|
||||||
|
] {
|
||||||
|
assert!(!dependencies(source).is_empty(), "guard missed {source}");
|
||||||
|
}
|
||||||
|
assert!(dependencies("use ticket::TicketError; use crate::feature::contracts::TicketService; mod tests { use super::*; }").is_empty());
|
||||||
|
}
|
||||||
@@ -1015,7 +1015,7 @@ async fn ticket_worker_host_with_service(
|
|||||||
) -> (
|
) -> (
|
||||||
worker::Worker<UnusedModel, session_store::FsStore>,
|
worker::Worker<UnusedModel, session_store::FsStore>,
|
||||||
bool,
|
bool,
|
||||||
Option<Arc<dyn worker::feature::builtin::ticket::TicketService>>,
|
Option<Arc<dyn worker::feature::contracts::TicketService>>,
|
||||||
) {
|
) {
|
||||||
let manifest = worker::WorkerManifest::from_toml(
|
let manifest = worker::WorkerManifest::from_toml(
|
||||||
r#"
|
r#"
|
||||||
@@ -1060,9 +1060,9 @@ async fn ticket_worker_host_with_service(
|
|||||||
Some(
|
Some(
|
||||||
report
|
report
|
||||||
.services
|
.services
|
||||||
.service::<dyn worker::feature::builtin::ticket::TicketService>(
|
.service::<dyn worker::feature::contracts::TicketService>(
|
||||||
&worker::feature::ServiceId::builtin(
|
&worker::feature::ServiceId::builtin(
|
||||||
worker::feature::builtin::ticket::TICKET_SERVICE_ID,
|
worker::feature::contracts::TICKET_SERVICE_ID,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.unwrap(),
|
.unwrap(),
|
||||||
|
|||||||
Reference in New Issue
Block a user