feat(workstate): read-model live-state minimal des conversations/délégations (Lot A backend)
Introduit le module application `workstate` (modèle de read-model live + snapshots des conversations/délégations en cours) et l'expose via la couche terminal (exports mod/registry). Câble la surface Tauri : DTO, commande et state pour exposer le live-state au frontend (lib + state + commands), avec tests. QA verte. Réserve environnementale non bloquante : tests loopback socket Unix réels non exécutables en sandbox (UnixListener::bind PermissionDenied), alternatives avec skips vertes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -12,13 +12,13 @@ use application::{
|
|||||||
AppError, AssignSkillToAgentInput, ChangeAgentProfileInput, CloseProjectInput,
|
AppError, AssignSkillToAgentInput, ChangeAgentProfileInput, CloseProjectInput,
|
||||||
CreateAgentInput, CreateLayoutInput, CreateMemoryInput, CreateSkillInput, DeleteAgentInput,
|
CreateAgentInput, CreateLayoutInput, CreateMemoryInput, CreateSkillInput, DeleteAgentInput,
|
||||||
DeleteEmbedderProfileInput, DeleteLayoutInput, DeleteMemoryInput, DeleteSkillInput,
|
DeleteEmbedderProfileInput, DeleteLayoutInput, DeleteMemoryInput, DeleteSkillInput,
|
||||||
DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput, GitBranchesInput, GitCheckoutInput,
|
DeleteTemplateInput, DetectAgentDriftInput, GetMemoryInput, GetProjectWorkStateInput,
|
||||||
GitCommitInput, GitGraphInput, GitInitInput, GitLogInput, GitStagePathInput, GitStatusInput,
|
GitBranchesInput, GitCheckoutInput, GitCommitInput, GitGraphInput, GitInitInput, GitLogInput,
|
||||||
InspectConversationInput, LaunchAgentInput, ListAgentsInput, ListLayoutsInput,
|
GitStagePathInput, GitStatusInput, InspectConversationInput, LaunchAgentInput, ListAgentsInput,
|
||||||
ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, LiveSessions, LoadLayoutInput,
|
ListLayoutsInput, ListMemoriesInput, ListResumableAgentsInput, ListSkillsInput, LiveSessions,
|
||||||
McpRuntime, MutateLayoutInput, OpenProjectInput, ReadAgentContextInput, ReadMemoryIndexInput,
|
LoadLayoutInput, McpRuntime, MutateLayoutInput, OpenProjectInput, ReadAgentContextInput,
|
||||||
ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput, RenameLayoutInput,
|
ReadMemoryIndexInput, ReadProjectContextInput, RecallMemoryInput, ReconcileLayoutsInput,
|
||||||
ResolveAgentPermissionsInput, ResolveMemoryLinksInput, SetActiveLayoutInput,
|
RenameLayoutInput, ResolveAgentPermissionsInput, ResolveMemoryLinksInput, SetActiveLayoutInput,
|
||||||
SnapshotRunningAgentsInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
|
SnapshotRunningAgentsInput, SyncAgentWithTemplateInput, UnassignSkillFromAgentInput,
|
||||||
UpdateAgentContextInput, UpdateAgentPermissionsInput, UpdateMemoryInput,
|
UpdateAgentContextInput, UpdateAgentPermissionsInput, UpdateMemoryInput,
|
||||||
UpdateProjectContextInput, UpdateProjectPermissionsInput, UpdateSkillInput,
|
UpdateProjectContextInput, UpdateProjectPermissionsInput, UpdateSkillInput,
|
||||||
@ -42,11 +42,11 @@ use crate::dto::{
|
|||||||
InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto,
|
InterruptAgentRequestDto, LaunchAgentRequestDto, LayoutDto, LayoutOperationDto, ListLayoutsDto,
|
||||||
LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto,
|
LiveAgentListDto, MemoryDto, MemoryIndexDto, MemoryLinksDto, MemoryListDto,
|
||||||
OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
|
OpenTerminalRequestDto, ProfileDto, ProfileListDto, ProjectDto, ProjectListDto,
|
||||||
ProjectPermissionsDto, ReadAgentContextResponseDto, ReattachChatDto, ReattachResultDto,
|
ProjectPermissionsDto, ProjectWorkStateDto, ReadAgentContextResponseDto, ReattachChatDto,
|
||||||
RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk, ResizeTerminalRequestDto,
|
ReattachResultDto, RecallMemoryRequestDto, RenameLayoutRequestDto, ReplyChunk,
|
||||||
ResolveAgentPermissionsRequestDto, ResumableAgentListDto, SaveEmbedderProfileRequestDto,
|
ResizeTerminalRequestDto, ResolveAgentPermissionsRequestDto, ResumableAgentListDto,
|
||||||
SaveProfileRequestDto, SetActiveLayoutRequestDto, SkillDto, SkillListDto,
|
SaveEmbedderProfileRequestDto, SaveProfileRequestDto, SetActiveLayoutRequestDto, SkillDto,
|
||||||
SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
|
SkillListDto, SyncAgentWithTemplateRequestDto, SyncResultDto, TemplateDto, TemplateListDto,
|
||||||
TerminalClosedDto, TerminalSessionDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
|
TerminalClosedDto, TerminalSessionDto, UnassignSkillRequestDto, UpdateAgentContextRequestDto,
|
||||||
UpdateAgentPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
|
UpdateAgentPermissionsRequestDto, UpdateMemoryRequestDto, UpdateProjectContextRequestDto,
|
||||||
UpdateProjectPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
|
UpdateProjectPermissionsRequestDto, UpdateSkillRequestDto, UpdateTemplateRequestDto,
|
||||||
@ -968,6 +968,25 @@ pub async fn list_agents(
|
|||||||
.map_err(ErrorDto::from)
|
.map_err(ErrorDto::from)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `get_project_work_state` — read-only live/busy state for manifest agents.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Returns an [`ErrorDto`] (`INVALID` for a malformed id, `NOT_FOUND` if the
|
||||||
|
/// project is unknown, `STORE` on manifest I/O failure).
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_project_work_state(
|
||||||
|
project_id: String,
|
||||||
|
state: State<'_, AppState>,
|
||||||
|
) -> Result<ProjectWorkStateDto, ErrorDto> {
|
||||||
|
let project = resolve_project(&project_id, &state).await?;
|
||||||
|
state
|
||||||
|
.get_project_work_state
|
||||||
|
.execute(GetProjectWorkStateInput { project })
|
||||||
|
.await
|
||||||
|
.map(ProjectWorkStateDto::from)
|
||||||
|
.map_err(ErrorDto::from)
|
||||||
|
}
|
||||||
|
|
||||||
/// `list_live_agents` — list every agent that currently owns a live session
|
/// `list_live_agents` — list every agent that currently owns a live session
|
||||||
/// (raw PTY **or** structured/chat) and the cell hosting each, so the UI can
|
/// (raw PTY **or** structured/chat) and the cell hosting each, so the UI can
|
||||||
/// disable an agent already running in another cell (the "one live session per
|
/// disable an agent already running in another cell (the "one live session per
|
||||||
|
|||||||
@ -10,9 +10,9 @@ use serde::{Deserialize, Serialize};
|
|||||||
|
|
||||||
use application::{
|
use application::{
|
||||||
AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput, HealthReport,
|
AppError, CreateProjectInput, CreateProjectOutput, GitGraphOutput, HealthInput, HealthReport,
|
||||||
LayoutKind, ListProjectsOutput, OpenProjectOutput,
|
LayoutKind, ListProjectsOutput, LiveSessionKind, OpenProjectOutput, ProjectWorkState,
|
||||||
};
|
};
|
||||||
use domain::{Project, ProjectId};
|
use domain::{AgentBusyState, Project, ProjectId};
|
||||||
|
|
||||||
/// Request DTO for the `health` command.
|
/// Request DTO for the `health` command.
|
||||||
#[derive(Debug, Clone, Default, Deserialize)]
|
#[derive(Debug, Clone, Default, Deserialize)]
|
||||||
@ -1472,6 +1472,83 @@ impl LiveAgentListDto {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runtime family of a live work-state session.
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub enum LiveWorkSessionKindDto {
|
||||||
|
/// Raw PTY-backed CLI session.
|
||||||
|
Pty,
|
||||||
|
/// Structured agent-session backend.
|
||||||
|
Structured,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<LiveSessionKind> for LiveWorkSessionKindDto {
|
||||||
|
fn from(kind: LiveSessionKind) -> Self {
|
||||||
|
match kind {
|
||||||
|
LiveSessionKind::Pty => Self::Pty,
|
||||||
|
LiveSessionKind::Structured => Self::Structured,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Live session coordinates in the project work-state read model.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct LiveWorkSessionDto {
|
||||||
|
/// The layout node currently hosting the session view.
|
||||||
|
pub node_id: String,
|
||||||
|
/// The live session id.
|
||||||
|
pub session_id: String,
|
||||||
|
/// Runtime family that owns the session.
|
||||||
|
pub kind: LiveWorkSessionKindDto,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One manifest agent's current live/busy state.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct AgentWorkStateDto {
|
||||||
|
/// Agent id.
|
||||||
|
pub agent_id: String,
|
||||||
|
/// Agent display name.
|
||||||
|
pub name: String,
|
||||||
|
/// Runtime profile id assigned to the agent.
|
||||||
|
pub profile_id: String,
|
||||||
|
/// Live session, if any.
|
||||||
|
pub live: Option<LiveWorkSessionDto>,
|
||||||
|
/// Current mediated-input busy state.
|
||||||
|
pub busy: AgentBusyState,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Project-level read model for conversation/delegation UX.
|
||||||
|
#[derive(Debug, Clone, Serialize)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct ProjectWorkStateDto {
|
||||||
|
/// Manifest agents in manifest order, enriched with live/busy state.
|
||||||
|
pub agents: Vec<AgentWorkStateDto>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ProjectWorkState> for ProjectWorkStateDto {
|
||||||
|
fn from(state: ProjectWorkState) -> Self {
|
||||||
|
Self {
|
||||||
|
agents: state
|
||||||
|
.agents
|
||||||
|
.into_iter()
|
||||||
|
.map(|agent| AgentWorkStateDto {
|
||||||
|
agent_id: agent.agent_id.to_string(),
|
||||||
|
name: agent.name,
|
||||||
|
profile_id: agent.profile_id.to_string(),
|
||||||
|
live: agent.live.map(|live| LiveWorkSessionDto {
|
||||||
|
node_id: live.node_id.to_string(),
|
||||||
|
session_id: live.session_id.to_string(),
|
||||||
|
kind: live.kind.into(),
|
||||||
|
}),
|
||||||
|
busy: agent.busy,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Request DTO for `attach_live_agent`: bind an already-running agent session to
|
/// Request DTO for `attach_live_agent`: bind an already-running agent session to
|
||||||
/// a visible layout cell without spawning a new process.
|
/// a visible layout cell without spawning a new process.
|
||||||
#[derive(Debug, Clone, Deserialize)]
|
#[derive(Debug, Clone, Deserialize)]
|
||||||
|
|||||||
@ -163,6 +163,7 @@ pub fn run() {
|
|||||||
commands::dismiss_embedder_suggestion,
|
commands::dismiss_embedder_suggestion,
|
||||||
commands::create_agent,
|
commands::create_agent,
|
||||||
commands::list_agents,
|
commands::list_agents,
|
||||||
|
commands::get_project_work_state,
|
||||||
commands::list_live_agents,
|
commands::list_live_agents,
|
||||||
commands::attach_live_agent,
|
commands::attach_live_agent,
|
||||||
commands::read_agent_context,
|
commands::read_agent_context,
|
||||||
|
|||||||
@ -18,15 +18,15 @@ use application::{
|
|||||||
CreateSkill, CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteLayout, DeleteMemory,
|
CreateSkill, CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteLayout, DeleteMemory,
|
||||||
DeleteProfile, DeleteSkill, DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift,
|
DeleteProfile, DeleteSkill, DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift,
|
||||||
DetectProfiles, DismissEmbedderSuggestion, FirstRunState, GetMemory, GetProjectPermissions,
|
DetectProfiles, DismissEmbedderSuggestion, FirstRunState, GetMemory, GetProjectPermissions,
|
||||||
GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage, GitStatus,
|
GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph, GitInit, GitLog, GitStage,
|
||||||
GitUnstage, HealthUseCase, InspectConversation, LaunchAgent, LaunchAgentInput, ListAgents,
|
GitStatus, GitUnstage, HealthUseCase, InspectConversation, LaunchAgent, LaunchAgentInput,
|
||||||
ListAgentsInput, ListEmbedderProfiles, ListLayouts, ListMemories, ListProfiles, ListProjects,
|
ListAgents, ListAgentsInput, ListEmbedderProfiles, ListLayouts, ListMemories, ListProfiles,
|
||||||
ListResumableAgents, ListSkills, ListTemplates, LiveAgentRegistry, LoadLayout, McpRuntime,
|
ListProjects, ListResumableAgents, ListSkills, ListTemplates, LiveAgentRegistry, LiveSessions,
|
||||||
MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
|
LoadLayout, McpRuntime, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject,
|
||||||
OrchestratorService, PermissionProjectorRegistry, ProposeContext, ReadAgentContext,
|
OpenTerminal, OrchestratorService, PermissionProjectorRegistry, ProposeContext,
|
||||||
ReadContext, ReadMemory, ReadMemoryIndex, ReadProjectContext, RecallMemory, ReconcileLayouts,
|
ReadAgentContext, ReadContext, ReadMemory, ReadMemoryIndex, ReadProjectContext, RecallMemory,
|
||||||
RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameLayout, ResizeTerminal,
|
ReconcileLayouts, RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameLayout,
|
||||||
ResolveAgentPermissions, ResolveMemoryLinks, SaveEmbedderProfile, SaveProfile,
|
ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks, SaveEmbedderProfile, SaveProfile,
|
||||||
SessionLimitService, SetActiveLayout, SnapshotRunningAgents, StructuredSessions,
|
SessionLimitService, SetActiveLayout, SnapshotRunningAgents, StructuredSessions,
|
||||||
SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent,
|
SuggestedThisSession, SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent,
|
||||||
UpdateAgentContext, UpdateAgentPermissions, UpdateMemory, UpdateProjectContext,
|
UpdateAgentContext, UpdateAgentPermissions, UpdateMemory, UpdateProjectContext,
|
||||||
@ -330,6 +330,8 @@ pub struct AppState {
|
|||||||
/// Read-only inventory of a project's resumable agent cells, for the reopen
|
/// Read-only inventory of a project's resumable agent cells, for the reopen
|
||||||
/// panel (§15.2).
|
/// panel (§15.2).
|
||||||
pub list_resumable_agents: Arc<ListResumableAgents>,
|
pub list_resumable_agents: Arc<ListResumableAgents>,
|
||||||
|
/// Read-only live/busy state for the project's manifest agents.
|
||||||
|
pub get_project_work_state: Arc<GetProjectWorkState>,
|
||||||
/// Best-effort inspection of a conversation (last topic + token indicator)
|
/// Best-effort inspection of a conversation (last topic + token indicator)
|
||||||
/// for the resume popup (T7). Optional/extensible: backed by a `Vec` of
|
/// for the resume popup (T7). Optional/extensible: backed by a `Vec` of
|
||||||
/// [`domain::ports::SessionInspector`]s; an empty/missing match yields empty
|
/// [`domain::ports::SessionInspector`]s; an empty/missing match yields empty
|
||||||
@ -1021,6 +1023,15 @@ impl AppState {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
let input_mediator = Arc::clone(&mediated_inbox) as Arc<dyn domain::input::InputMediator>;
|
let input_mediator = Arc::clone(&mediated_inbox) as Arc<dyn domain::input::InputMediator>;
|
||||||
|
let live_sessions = Arc::new(LiveSessions::new(
|
||||||
|
Arc::clone(&terminal_sessions),
|
||||||
|
Arc::clone(&structured_sessions),
|
||||||
|
));
|
||||||
|
let get_project_work_state = Arc::new(GetProjectWorkState::new(
|
||||||
|
Arc::clone(&contexts_port),
|
||||||
|
Arc::clone(&live_sessions),
|
||||||
|
Arc::clone(&input_mediator),
|
||||||
|
));
|
||||||
|
|
||||||
// --- Limites de session des agents (ARCHITECTURE §21, LS7) ---
|
// --- Limites de session des agents (ARCHITECTURE §21, LS7) ---
|
||||||
// Service pur-ports « détecter → planifier → reprendre » câblé sur l'existant :
|
// Service pur-ports « détecter → planifier → reprendre » câblé sur l'existant :
|
||||||
@ -1187,6 +1198,7 @@ impl AppState {
|
|||||||
launch_agent,
|
launch_agent,
|
||||||
change_agent_profile,
|
change_agent_profile,
|
||||||
list_resumable_agents,
|
list_resumable_agents,
|
||||||
|
get_project_work_state,
|
||||||
inspect_conversation,
|
inspect_conversation,
|
||||||
project_store,
|
project_store,
|
||||||
get_project_permissions,
|
get_project_permissions,
|
||||||
|
|||||||
@ -9,7 +9,8 @@ use app_tauri_lib::dto::{
|
|||||||
};
|
};
|
||||||
use application::AppError;
|
use application::AppError;
|
||||||
use application::{
|
use application::{
|
||||||
CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput, ListAgentsOutput,
|
AgentWorkState, CreateAgentOutput, InspectConversationOutput, LaunchAgentOutput,
|
||||||
|
ListAgentsOutput, LiveSessionKind, LiveWorkSession, ProjectWorkState,
|
||||||
};
|
};
|
||||||
use domain::ids::{AgentId, NodeId, ProfileId, SessionId};
|
use domain::ids::{AgentId, NodeId, ProfileId, SessionId};
|
||||||
use domain::ports::ConversationDetails;
|
use domain::ports::ConversationDetails;
|
||||||
@ -197,6 +198,44 @@ fn live_agent_list_dto_serialises_camelcase_array() {
|
|||||||
assert!(arr[0].get("node_id").is_none());
|
assert!(arr[0].get("node_id").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn project_work_state_dto_serialises_live_and_busy_camelcase() {
|
||||||
|
let agent = AgentId::from_uuid(Uuid::from_u128(11));
|
||||||
|
let profile = ProfileId::from_uuid(Uuid::from_u128(12));
|
||||||
|
let node = NodeId::from_uuid(Uuid::from_u128(13));
|
||||||
|
let session = SessionId::from_uuid(Uuid::from_u128(14));
|
||||||
|
let ticket = domain::TicketId::from_uuid(Uuid::from_u128(15));
|
||||||
|
let dto = app_tauri_lib::dto::ProjectWorkStateDto::from(ProjectWorkState {
|
||||||
|
agents: vec![AgentWorkState {
|
||||||
|
agent_id: agent,
|
||||||
|
name: "Worker".to_owned(),
|
||||||
|
profile_id: profile,
|
||||||
|
live: Some(LiveWorkSession {
|
||||||
|
node_id: node,
|
||||||
|
session_id: session,
|
||||||
|
kind: LiveSessionKind::Structured,
|
||||||
|
}),
|
||||||
|
busy: domain::AgentBusyState::Busy {
|
||||||
|
ticket,
|
||||||
|
since_ms: 1_234,
|
||||||
|
},
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let v = serde_json::to_value(&dto).unwrap();
|
||||||
|
assert_eq!(v["agents"][0]["agentId"], agent.to_string());
|
||||||
|
assert_eq!(v["agents"][0]["profileId"], profile.to_string());
|
||||||
|
assert_eq!(v["agents"][0]["live"]["nodeId"], node.to_string());
|
||||||
|
assert_eq!(v["agents"][0]["live"]["sessionId"], session.to_string());
|
||||||
|
assert_eq!(v["agents"][0]["live"]["kind"], "structured");
|
||||||
|
assert_eq!(v["agents"][0]["busy"]["state"], "busy");
|
||||||
|
assert_eq!(v["agents"][0]["busy"]["ticket"], ticket.to_string());
|
||||||
|
assert_eq!(v["agents"][0]["busy"]["sinceMs"], 1_234);
|
||||||
|
assert!(v["agents"][0].get("agent_id").is_none());
|
||||||
|
assert!(v["agents"][0]["live"].get("session_id").is_none());
|
||||||
|
assert!(v["agents"][0]["busy"].get("since_ms").is_none());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn launch_agent_request_carries_conversation_id_for_resume() {
|
fn launch_agent_request_carries_conversation_id_for_resume() {
|
||||||
let raw = json!({
|
let raw = json!({
|
||||||
|
|||||||
@ -28,6 +28,7 @@ pub mod skill;
|
|||||||
pub mod template;
|
pub mod template;
|
||||||
pub mod terminal;
|
pub mod terminal;
|
||||||
pub mod window;
|
pub mod window;
|
||||||
|
pub mod workstate;
|
||||||
|
|
||||||
pub use agent::{
|
pub use agent::{
|
||||||
drain_with_readiness, drain_with_readiness_outcome, reference_profile_id, reference_profiles,
|
drain_with_readiness, drain_with_readiness_outcome, reference_profile_id, reference_profiles,
|
||||||
@ -111,8 +112,13 @@ pub use template::{
|
|||||||
UpdateTemplateOutput,
|
UpdateTemplateOutput,
|
||||||
};
|
};
|
||||||
pub use terminal::{
|
pub use terminal::{
|
||||||
CloseTerminal, CloseTerminalInput, CloseTerminalOutput, LiveAgentRegistry, LiveSessions,
|
CloseTerminal, CloseTerminalInput, CloseTerminalOutput, LiveAgentRegistry, LiveSessionKind,
|
||||||
OpenTerminal, OpenTerminalInput, OpenTerminalOutput, ResizeTerminal, ResizeTerminalInput,
|
LiveSessionSnapshot, LiveSessions, OpenTerminal, OpenTerminalInput, OpenTerminalOutput,
|
||||||
StructuredSessions, TerminalSessions, WriteToTerminal, WriteToTerminalInput,
|
ResizeTerminal, ResizeTerminalInput, StructuredSessions, TerminalSessions, WriteToTerminal,
|
||||||
|
WriteToTerminalInput,
|
||||||
};
|
};
|
||||||
pub use window::{MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput};
|
pub use window::{MoveTabToNewWindow, MoveTabToNewWindowInput, MoveTabToNewWindowOutput};
|
||||||
|
pub use workstate::{
|
||||||
|
AgentWorkState, GetProjectWorkState, GetProjectWorkStateInput, LiveWorkSession,
|
||||||
|
ProjectWorkState,
|
||||||
|
};
|
||||||
|
|||||||
@ -28,7 +28,10 @@
|
|||||||
mod registry;
|
mod registry;
|
||||||
mod usecases;
|
mod usecases;
|
||||||
|
|
||||||
pub use registry::{LiveAgentRegistry, LiveSessions, StructuredSessions, TerminalSessions};
|
pub use registry::{
|
||||||
|
LiveAgentRegistry, LiveSessionKind, LiveSessionSnapshot, LiveSessions, StructuredSessions,
|
||||||
|
TerminalSessions,
|
||||||
|
};
|
||||||
pub use usecases::{
|
pub use usecases::{
|
||||||
CloseTerminal, CloseTerminalInput, CloseTerminalOutput, OpenTerminal, OpenTerminalInput,
|
CloseTerminal, CloseTerminalInput, CloseTerminalOutput, OpenTerminal, OpenTerminalInput,
|
||||||
OpenTerminalOutput, ResizeTerminal, ResizeTerminalInput, WriteToTerminal, WriteToTerminalInput,
|
OpenTerminalOutput, ResizeTerminal, ResizeTerminalInput, WriteToTerminal, WriteToTerminalInput,
|
||||||
|
|||||||
@ -13,6 +13,28 @@ use domain::conversation::ConversationId;
|
|||||||
use domain::ports::{AgentSession, PtyHandle};
|
use domain::ports::{AgentSession, PtyHandle};
|
||||||
use domain::{AgentId, NodeId, SessionId, SessionKind, TerminalSession};
|
use domain::{AgentId, NodeId, SessionId, SessionKind, TerminalSession};
|
||||||
|
|
||||||
|
/// Runtime family of a live agent session.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum LiveSessionKind {
|
||||||
|
/// Raw PTY-backed CLI session.
|
||||||
|
Pty,
|
||||||
|
/// Structured agent-session backend.
|
||||||
|
Structured,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only coordinates of one live agent session.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct LiveSessionSnapshot {
|
||||||
|
/// The agent owning the live session.
|
||||||
|
pub agent_id: AgentId,
|
||||||
|
/// The layout node currently hosting the session view.
|
||||||
|
pub node_id: NodeId,
|
||||||
|
/// The live session id.
|
||||||
|
pub session_id: SessionId,
|
||||||
|
/// Which runtime registry owns the session.
|
||||||
|
pub kind: LiveSessionKind,
|
||||||
|
}
|
||||||
|
|
||||||
/// A registered, live terminal: its PTY handle plus the domain snapshot.
|
/// A registered, live terminal: its PTY handle plus the domain snapshot.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
struct Entry {
|
struct Entry {
|
||||||
@ -516,6 +538,31 @@ impl LiveSessions {
|
|||||||
all.extend(self.structured.live_agents());
|
all.extend(self.structured.live_agents());
|
||||||
all
|
all
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tous les agents vivants avec le type de registre source (PTY puis structuré).
|
||||||
|
#[must_use]
|
||||||
|
pub fn live_agent_snapshots(&self) -> Vec<LiveSessionSnapshot> {
|
||||||
|
let mut all: Vec<LiveSessionSnapshot> = self
|
||||||
|
.pty
|
||||||
|
.live_agents()
|
||||||
|
.into_iter()
|
||||||
|
.map(|(agent_id, node_id, session_id)| LiveSessionSnapshot {
|
||||||
|
agent_id,
|
||||||
|
node_id,
|
||||||
|
session_id,
|
||||||
|
kind: LiveSessionKind::Pty,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
all.extend(self.structured.live_agents().into_iter().map(
|
||||||
|
|(agent_id, node_id, session_id)| LiveSessionSnapshot {
|
||||||
|
agent_id,
|
||||||
|
node_id,
|
||||||
|
session_id,
|
||||||
|
kind: LiveSessionKind::Structured,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
all
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LiveAgentRegistry for LiveSessions {
|
impl LiveAgentRegistry for LiveSessions {
|
||||||
|
|||||||
124
crates/application/src/workstate/mod.rs
Normal file
124
crates/application/src/workstate/mod.rs
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
//! Read-only project work-state read model.
|
||||||
|
//!
|
||||||
|
//! This module composes existing runtime state only: the project agent manifest,
|
||||||
|
//! live session registries and the input mediator busy state. It performs no I/O
|
||||||
|
//! beyond loading the manifest through the existing context store and creates no
|
||||||
|
//! durable projection.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use domain::input::AgentBusyState;
|
||||||
|
use domain::ports::AgentContextStore;
|
||||||
|
use domain::{AgentId, InputMediator, NodeId, ProfileId, Project, SessionId};
|
||||||
|
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::terminal::{LiveSessionKind, LiveSessionSnapshot, LiveSessions};
|
||||||
|
|
||||||
|
/// Input for [`GetProjectWorkState::execute`].
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct GetProjectWorkStateInput {
|
||||||
|
/// Project whose manifest provides the agent order and boundary.
|
||||||
|
pub project: Project,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read model for a project's current agent work state.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ProjectWorkState {
|
||||||
|
/// Agents in manifest order.
|
||||||
|
pub agents: Vec<AgentWorkState>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read model for one manifest agent.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct AgentWorkState {
|
||||||
|
/// Agent id.
|
||||||
|
pub agent_id: AgentId,
|
||||||
|
/// Agent display name.
|
||||||
|
pub name: String,
|
||||||
|
/// Runtime profile id currently assigned to the agent.
|
||||||
|
pub profile_id: ProfileId,
|
||||||
|
/// Current live session, if the agent is live.
|
||||||
|
pub live: Option<LiveWorkSession>,
|
||||||
|
/// Current FIFO/busy state.
|
||||||
|
pub busy: AgentBusyState,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Live session coordinates exposed by the work-state read model.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct LiveWorkSession {
|
||||||
|
/// The layout node currently hosting the session view.
|
||||||
|
pub node_id: NodeId,
|
||||||
|
/// The live session id.
|
||||||
|
pub session_id: SessionId,
|
||||||
|
/// Runtime family that owns the session.
|
||||||
|
pub kind: LiveSessionKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Read-only use case aggregating manifest agents with live and busy state.
|
||||||
|
pub struct GetProjectWorkState {
|
||||||
|
contexts: Arc<dyn AgentContextStore>,
|
||||||
|
live: Arc<LiveSessions>,
|
||||||
|
input: Arc<dyn InputMediator>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GetProjectWorkState {
|
||||||
|
/// Builds the read-model use case from existing stores/registries.
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(
|
||||||
|
contexts: Arc<dyn AgentContextStore>,
|
||||||
|
live: Arc<LiveSessions>,
|
||||||
|
input: Arc<dyn InputMediator>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
contexts,
|
||||||
|
live,
|
||||||
|
input,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes the read-only aggregation.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// - [`AppError::Store`] when the manifest cannot be loaded,
|
||||||
|
/// - [`AppError::Invalid`] if a manifest entry violates agent invariants.
|
||||||
|
pub async fn execute(
|
||||||
|
&self,
|
||||||
|
input: GetProjectWorkStateInput,
|
||||||
|
) -> Result<ProjectWorkState, AppError> {
|
||||||
|
let manifest = self.contexts.load_manifest(&input.project).await?;
|
||||||
|
let live_by_agent = live_by_agent(self.live.live_agent_snapshots());
|
||||||
|
let agents = manifest
|
||||||
|
.entries
|
||||||
|
.iter()
|
||||||
|
.map(|entry| {
|
||||||
|
let agent = entry
|
||||||
|
.to_agent()
|
||||||
|
.map_err(|err| AppError::Invalid(err.to_string()))?;
|
||||||
|
let live = live_by_agent
|
||||||
|
.get(&agent.id)
|
||||||
|
.map(|snapshot| LiveWorkSession {
|
||||||
|
node_id: snapshot.node_id,
|
||||||
|
session_id: snapshot.session_id,
|
||||||
|
kind: snapshot.kind,
|
||||||
|
});
|
||||||
|
Ok(AgentWorkState {
|
||||||
|
agent_id: agent.id,
|
||||||
|
name: agent.name,
|
||||||
|
profile_id: agent.profile_id,
|
||||||
|
live,
|
||||||
|
busy: self.input.busy_state(agent.id),
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, AppError>>()?;
|
||||||
|
Ok(ProjectWorkState { agents })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn live_by_agent(snapshots: Vec<LiveSessionSnapshot>) -> HashMap<AgentId, LiveSessionSnapshot> {
|
||||||
|
let mut out = HashMap::new();
|
||||||
|
for snapshot in snapshots {
|
||||||
|
out.entry(snapshot.agent_id).or_insert(snapshot);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
307
crates/application/tests/workstate.rs
Normal file
307
crates/application/tests/workstate.rs
Normal file
@ -0,0 +1,307 @@
|
|||||||
|
//! Tests for the project work-state read model.
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
|
||||||
|
use async_trait::async_trait;
|
||||||
|
|
||||||
|
use application::{
|
||||||
|
GetProjectWorkState, GetProjectWorkStateInput, LiveSessionKind, LiveSessions,
|
||||||
|
StructuredSessions, TerminalSessions,
|
||||||
|
};
|
||||||
|
use domain::mailbox::{MailboxError, PendingReply, Ticket};
|
||||||
|
use domain::ports::{
|
||||||
|
AgentContextStore, AgentSession, AgentSessionError, PtyHandle, ReplyStream, StoreError,
|
||||||
|
};
|
||||||
|
use domain::{
|
||||||
|
Agent, AgentBusyState, AgentId, AgentManifest, AgentOrigin, InputMediator, ManifestEntry,
|
||||||
|
MarkdownDoc, NodeId, ProfileId, Project, ProjectId, ProjectPath, PtySize, RemoteRef, SessionId,
|
||||||
|
SessionKind, TerminalSession,
|
||||||
|
};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn aid(n: u128) -> AgentId {
|
||||||
|
AgentId::from_uuid(Uuid::from_u128(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pid(n: u128) -> ProfileId {
|
||||||
|
ProfileId::from_uuid(Uuid::from_u128(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn sid(n: u128) -> SessionId {
|
||||||
|
SessionId::from_uuid(Uuid::from_u128(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn nid(n: u128) -> NodeId {
|
||||||
|
NodeId::from_uuid(Uuid::from_u128(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ticket_id(n: u128) -> domain::TicketId {
|
||||||
|
domain::TicketId::from_uuid(Uuid::from_u128(n))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn project() -> Project {
|
||||||
|
Project::new(
|
||||||
|
ProjectId::from_uuid(Uuid::from_u128(1)),
|
||||||
|
"demo",
|
||||||
|
ProjectPath::new("/tmp/idea-workstate-test").unwrap(),
|
||||||
|
RemoteRef::local(),
|
||||||
|
1_700_000_000_000,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn agent(n: u128, name: &str) -> Agent {
|
||||||
|
Agent::new(
|
||||||
|
aid(n),
|
||||||
|
name,
|
||||||
|
format!("agents/{name}.md"),
|
||||||
|
pid(100 + n),
|
||||||
|
AgentOrigin::Scratch,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn manifest(agents: &[Agent]) -> AgentManifest {
|
||||||
|
AgentManifest::new(1, agents.iter().map(ManifestEntry::from_agent).collect()).unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct FakeContexts {
|
||||||
|
manifest: AgentManifest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AgentContextStore for FakeContexts {
|
||||||
|
async fn read_context(
|
||||||
|
&self,
|
||||||
|
_project: &Project,
|
||||||
|
_agent: &AgentId,
|
||||||
|
) -> Result<MarkdownDoc, StoreError> {
|
||||||
|
Err(StoreError::NotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn write_context(
|
||||||
|
&self,
|
||||||
|
_project: &Project,
|
||||||
|
_agent: &AgentId,
|
||||||
|
_md: &MarkdownDoc,
|
||||||
|
) -> Result<(), StoreError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn load_manifest(&self, _project: &Project) -> Result<AgentManifest, StoreError> {
|
||||||
|
Ok(self.manifest.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_manifest(
|
||||||
|
&self,
|
||||||
|
_project: &Project,
|
||||||
|
_manifest: &AgentManifest,
|
||||||
|
) -> Result<(), StoreError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
struct FakeInput {
|
||||||
|
busy: Mutex<HashMap<AgentId, AgentBusyState>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FakeInput {
|
||||||
|
fn set_busy(&self, agent: AgentId, busy: AgentBusyState) {
|
||||||
|
self.busy.lock().unwrap().insert(agent, busy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputMediator for FakeInput {
|
||||||
|
fn enqueue(&self, _agent: AgentId, _ticket: Ticket) -> PendingReply {
|
||||||
|
let fut: Pin<Box<dyn Future<Output = Result<String, MailboxError>> + Send>> =
|
||||||
|
Box::pin(async { Err(MailboxError::Cancelled) });
|
||||||
|
PendingReply::new(fut)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn preempt(&self, _agent: AgentId) {}
|
||||||
|
|
||||||
|
fn mark_idle(&self, _agent: AgentId) {}
|
||||||
|
|
||||||
|
fn busy_state(&self, agent: AgentId) -> AgentBusyState {
|
||||||
|
self.busy
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.get(&agent)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(AgentBusyState::Idle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct FakeSession {
|
||||||
|
id: SessionId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl AgentSession for FakeSession {
|
||||||
|
fn id(&self) -> SessionId {
|
||||||
|
self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn conversation_id(&self) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn send(&self, _prompt: &str) -> Result<ReplyStream, AgentSessionError> {
|
||||||
|
Ok(Box::new(std::iter::empty()))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn shutdown(&self) -> Result<(), AgentSessionError> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fake_session(id: SessionId) -> Arc<dyn AgentSession> {
|
||||||
|
Arc::new(FakeSession { id })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_pty(
|
||||||
|
sessions: &TerminalSessions,
|
||||||
|
session_id: SessionId,
|
||||||
|
agent_id: AgentId,
|
||||||
|
node_id: NodeId,
|
||||||
|
) {
|
||||||
|
sessions.insert(
|
||||||
|
PtyHandle { session_id },
|
||||||
|
TerminalSession::starting(
|
||||||
|
session_id,
|
||||||
|
node_id,
|
||||||
|
ProjectPath::new("/tmp/idea-workstate-test").unwrap(),
|
||||||
|
SessionKind::Agent { agent_id },
|
||||||
|
PtySize { rows: 24, cols: 80 },
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Fixture {
|
||||||
|
usecase: GetProjectWorkState,
|
||||||
|
pty: Arc<TerminalSessions>,
|
||||||
|
structured: Arc<StructuredSessions>,
|
||||||
|
input: Arc<FakeInput>,
|
||||||
|
project: Project,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fixture(agents: &[Agent]) -> Fixture {
|
||||||
|
let pty = Arc::new(TerminalSessions::new());
|
||||||
|
let structured = Arc::new(StructuredSessions::new());
|
||||||
|
let live = Arc::new(LiveSessions::new(Arc::clone(&pty), Arc::clone(&structured)));
|
||||||
|
let input = Arc::new(FakeInput::default());
|
||||||
|
let input_port = Arc::clone(&input) as Arc<dyn InputMediator>;
|
||||||
|
let usecase = GetProjectWorkState::new(
|
||||||
|
Arc::new(FakeContexts {
|
||||||
|
manifest: manifest(agents),
|
||||||
|
}),
|
||||||
|
live,
|
||||||
|
input_port,
|
||||||
|
);
|
||||||
|
Fixture {
|
||||||
|
usecase,
|
||||||
|
pty,
|
||||||
|
structured,
|
||||||
|
input,
|
||||||
|
project: project(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workstate_lists_manifest_agents_idle_without_live_sessions() {
|
||||||
|
let a = agent(10, "alpha");
|
||||||
|
let b = agent(20, "beta");
|
||||||
|
let f = fixture(&[a.clone(), b.clone()]);
|
||||||
|
|
||||||
|
let out = f
|
||||||
|
.usecase
|
||||||
|
.execute(GetProjectWorkStateInput { project: f.project })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(out.agents.len(), 2);
|
||||||
|
assert_eq!(out.agents[0].agent_id, a.id);
|
||||||
|
assert_eq!(out.agents[0].name, "alpha");
|
||||||
|
assert_eq!(out.agents[0].profile_id, a.profile_id);
|
||||||
|
assert_eq!(out.agents[0].live, None);
|
||||||
|
assert_eq!(out.agents[0].busy, AgentBusyState::Idle);
|
||||||
|
assert_eq!(out.agents[1].agent_id, b.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workstate_attaches_live_pty_session_to_manifest_agent() {
|
||||||
|
let a = agent(10, "alpha");
|
||||||
|
let f = fixture(std::slice::from_ref(&a));
|
||||||
|
insert_pty(&f.pty, sid(1), a.id, nid(100));
|
||||||
|
|
||||||
|
let out = f
|
||||||
|
.usecase
|
||||||
|
.execute(GetProjectWorkStateInput { project: f.project })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let live = out.agents[0].live.unwrap();
|
||||||
|
assert_eq!(live.session_id, sid(1));
|
||||||
|
assert_eq!(live.node_id, nid(100));
|
||||||
|
assert_eq!(live.kind, LiveSessionKind::Pty);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workstate_attaches_live_structured_session_to_manifest_agent() {
|
||||||
|
let a = agent(10, "alpha");
|
||||||
|
let f = fixture(std::slice::from_ref(&a));
|
||||||
|
f.structured.insert(fake_session(sid(2)), a.id, nid(200));
|
||||||
|
|
||||||
|
let out = f
|
||||||
|
.usecase
|
||||||
|
.execute(GetProjectWorkStateInput { project: f.project })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let live = out.agents[0].live.unwrap();
|
||||||
|
assert_eq!(live.session_id, sid(2));
|
||||||
|
assert_eq!(live.node_id, nid(200));
|
||||||
|
assert_eq!(live.kind, LiveSessionKind::Structured);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workstate_includes_busy_state_from_input_mediator() {
|
||||||
|
let a = agent(10, "alpha");
|
||||||
|
let f = fixture(std::slice::from_ref(&a));
|
||||||
|
let busy = AgentBusyState::Busy {
|
||||||
|
ticket: ticket_id(7),
|
||||||
|
since_ms: 1_234,
|
||||||
|
};
|
||||||
|
f.input.set_busy(a.id, busy);
|
||||||
|
|
||||||
|
let out = f
|
||||||
|
.usecase
|
||||||
|
.execute(GetProjectWorkStateInput { project: f.project })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(out.agents[0].busy, busy);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn workstate_ignores_live_agents_absent_from_manifest() {
|
||||||
|
let a = agent(10, "alpha");
|
||||||
|
let f = fixture(std::slice::from_ref(&a));
|
||||||
|
insert_pty(&f.pty, sid(99), aid(999), nid(999));
|
||||||
|
|
||||||
|
let out = f
|
||||||
|
.usecase
|
||||||
|
.execute(GetProjectWorkStateInput { project: f.project })
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(out.agents.len(), 1);
|
||||||
|
assert_eq!(out.agents[0].agent_id, a.id);
|
||||||
|
assert_eq!(out.agents[0].live, None);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user