fix: align Companion REST DTO contracts
This commit is contained in:
@@ -38,5 +38,10 @@ pub use target::{
|
||||
WorkerConnection, WorkerConnectionSelector, WorkerList, WorkerListRequest, WorkerSpawn,
|
||||
};
|
||||
pub use worker_client::WorkerClient;
|
||||
pub use workspace_api::{ObjectiveDetail, ObjectiveSummary};
|
||||
pub use workspace_api::{
|
||||
CompanionCancelRequest, CompanionLifecycleState, CompanionMessageDisposition,
|
||||
CompanionMessageRequest, CompanionMessageResponse, CompanionStatusResponse,
|
||||
CompanionTranscriptItem, CompanionTranscriptProjection, CompanionTranscriptRole,
|
||||
CompanionTransportSummary, ObjectiveDetail, ObjectiveSummary,
|
||||
};
|
||||
pub use workspace_product::BackendWorkspaceProductClient;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
print!("{}", workspace_api::companion_api_typescript());
|
||||
}
|
||||
@@ -93,6 +93,7 @@ pub const TICKET_RELATIONS_QUERY_PATH: &str = "/tickets/relations/search";
|
||||
pub const TICKET_ORCHESTRATION_PLANS_QUERY_PATH: &str = "/tickets/orchestration-plans/search";
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DiagnosticSeverity {
|
||||
Info,
|
||||
@@ -101,6 +102,7 @@ pub enum DiagnosticSeverity {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct Diagnostic {
|
||||
pub code: String,
|
||||
pub severity: DiagnosticSeverity,
|
||||
@@ -325,6 +327,7 @@ pub struct WorkerCapabilitySummary {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||
pub enum WorkspaceWorkerSubject {
|
||||
RuntimeWorker {
|
||||
@@ -337,6 +340,7 @@ pub enum WorkspaceWorkerSubject {
|
||||
/// Runtime placement appears only in the typed subject required by Worker
|
||||
/// control operations; provider and launch internals are intentionally omitted.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
pub struct WorkspaceWorkerDiscoveryItem {
|
||||
pub subject: WorkspaceWorkerSubject,
|
||||
pub resource_key: String,
|
||||
@@ -346,6 +350,137 @@ pub struct WorkspaceWorkerDiscoveryItem {
|
||||
pub status: Option<String>,
|
||||
}
|
||||
|
||||
/// Public lifecycle projection for the Workspace Companion endpoint.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompanionLifecycleState {
|
||||
Idle,
|
||||
Running,
|
||||
Stopped,
|
||||
}
|
||||
|
||||
/// Public outcome of a Companion message submission.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompanionMessageDisposition {
|
||||
Accepted,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
/// Public, bounded transport metadata for Companion status.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CompanionTransportSummary {
|
||||
pub mode: String,
|
||||
pub available: bool,
|
||||
}
|
||||
|
||||
/// Public Workspace Companion status response.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CompanionStatusResponse {
|
||||
pub state: CompanionLifecycleState,
|
||||
pub worker: Option<WorkspaceWorkerDiscoveryItem>,
|
||||
pub transport: CompanionTransportSummary,
|
||||
#[serde(default)]
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
/// Public Workspace Companion message request.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CompanionMessageRequest {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
/// Public Workspace Companion cancellation request.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CompanionCancelRequest {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Public Workspace Companion message response.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CompanionMessageResponse {
|
||||
pub state: CompanionMessageDisposition,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// User-visible role accepted in the public Companion transcript.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompanionTranscriptRole {
|
||||
User,
|
||||
Assistant,
|
||||
}
|
||||
|
||||
/// One allowlisted, user-visible Companion transcript item.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CompanionTranscriptItem {
|
||||
pub sequence: usize,
|
||||
pub role: CompanionTranscriptRole,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
/// Bounded public Companion transcript projection.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[cfg_attr(feature = "typescript", derive(ts_rs::TS))]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CompanionTranscriptProjection {
|
||||
pub state: CompanionLifecycleState,
|
||||
pub start: usize,
|
||||
pub limit: usize,
|
||||
pub total: usize,
|
||||
pub next: Option<usize>,
|
||||
pub items: Vec<CompanionTranscriptItem>,
|
||||
}
|
||||
|
||||
#[cfg(feature = "typescript")]
|
||||
pub fn companion_api_typescript() -> String {
|
||||
use ts_rs::TS;
|
||||
|
||||
let config = ts_rs::Config::default();
|
||||
let declarations = [
|
||||
DiagnosticSeverity::decl(&config),
|
||||
Diagnostic::decl(&config),
|
||||
WorkspaceWorkerSubject::decl(&config),
|
||||
WorkspaceWorkerDiscoveryItem::decl(&config),
|
||||
CompanionLifecycleState::decl(&config),
|
||||
CompanionMessageDisposition::decl(&config),
|
||||
CompanionTransportSummary::decl(&config),
|
||||
CompanionStatusResponse::decl(&config),
|
||||
CompanionMessageRequest::decl(&config),
|
||||
CompanionCancelRequest::decl(&config),
|
||||
CompanionMessageResponse::decl(&config),
|
||||
CompanionTranscriptRole::decl(&config),
|
||||
CompanionTranscriptItem::decl(&config),
|
||||
CompanionTranscriptProjection::decl(&config),
|
||||
];
|
||||
|
||||
format!(
|
||||
"// Generated by `cargo run -p workspace-api --features typescript --example generate_companion_api_types`.\n// Do not edit manually.\n\n{}\n",
|
||||
declarations
|
||||
.into_iter()
|
||||
.map(|declaration| format!("export {declaration}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n\n")
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct WorkspaceWorkerDiscoveryPage {
|
||||
pub workers: Vec<WorkspaceWorkerDiscoveryItem>,
|
||||
@@ -560,4 +695,159 @@ mod tests {
|
||||
|
||||
assert!(serde_json::from_value::<WorkerSummary>(payload).is_err());
|
||||
}
|
||||
|
||||
fn round_trip<T>(value: T)
|
||||
where
|
||||
T: std::fmt::Debug + PartialEq + Serialize + for<'de> Deserialize<'de>,
|
||||
{
|
||||
let encoded = serde_json::to_vec(&value).expect("fixture should serialize");
|
||||
let decoded: T = serde_json::from_slice(&encoded).expect("fixture should deserialize");
|
||||
assert_eq!(decoded, value);
|
||||
}
|
||||
|
||||
fn companion_worker() -> WorkspaceWorkerDiscoveryItem {
|
||||
WorkspaceWorkerDiscoveryItem {
|
||||
subject: WorkspaceWorkerSubject::RuntimeWorker {
|
||||
runtime_id: "arcadia".to_string(),
|
||||
worker_id: "worker-7".to_string(),
|
||||
},
|
||||
resource_key: "W-7".to_string(),
|
||||
display_name: "Companion".to_string(),
|
||||
profile: Some("builtin:companion".to_string()),
|
||||
status: Some("idle".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn companion_status_fixtures_round_trip() {
|
||||
for state in [
|
||||
CompanionLifecycleState::Idle,
|
||||
CompanionLifecycleState::Running,
|
||||
CompanionLifecycleState::Stopped,
|
||||
] {
|
||||
round_trip(CompanionStatusResponse {
|
||||
state,
|
||||
worker: Some(companion_worker()),
|
||||
transport: CompanionTransportSummary {
|
||||
mode: "worker_runtime".to_string(),
|
||||
available: state != CompanionLifecycleState::Stopped,
|
||||
},
|
||||
diagnostics: Vec::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn companion_message_fixtures_round_trip() {
|
||||
for state in [
|
||||
CompanionMessageDisposition::Accepted,
|
||||
CompanionMessageDisposition::Rejected,
|
||||
] {
|
||||
round_trip(CompanionMessageResponse {
|
||||
state,
|
||||
message: if state == CompanionMessageDisposition::Accepted {
|
||||
"accepted"
|
||||
} else {
|
||||
"rejected"
|
||||
}
|
||||
.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn companion_transcript_fixture_round_trips() {
|
||||
round_trip(CompanionTranscriptProjection {
|
||||
state: CompanionLifecycleState::Idle,
|
||||
start: 0,
|
||||
limit: 2,
|
||||
total: 2,
|
||||
next: None,
|
||||
items: vec![
|
||||
CompanionTranscriptItem {
|
||||
sequence: 1,
|
||||
role: CompanionTranscriptRole::User,
|
||||
content: "hello".to_string(),
|
||||
created_at: "2026-08-31T00:00:00Z".to_string(),
|
||||
},
|
||||
CompanionTranscriptItem {
|
||||
sequence: 2,
|
||||
role: CompanionTranscriptRole::Assistant,
|
||||
content: "hi".to_string(),
|
||||
created_at: "2026-08-31T00:00:01Z".to_string(),
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn companion_transcript_rejects_system_and_private_fields() {
|
||||
let public_item = CompanionTranscriptItem {
|
||||
sequence: 1,
|
||||
role: CompanionTranscriptRole::Assistant,
|
||||
content: "visible".to_string(),
|
||||
created_at: "2026-08-31T00:00:00Z".to_string(),
|
||||
};
|
||||
let public_fields = serde_json::to_value(public_item)
|
||||
.expect("public transcript item should serialize")
|
||||
.as_object()
|
||||
.expect("public transcript item should be an object")
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
public_fields,
|
||||
["content", "created_at", "role", "sequence"]
|
||||
.into_iter()
|
||||
.map(str::to_string)
|
||||
.collect()
|
||||
);
|
||||
|
||||
let system_item = serde_json::json!({
|
||||
"sequence": 1,
|
||||
"role": "system",
|
||||
"content": "raw system prompt",
|
||||
"created_at": "2026-08-31T00:00:00Z"
|
||||
});
|
||||
assert!(serde_json::from_value::<CompanionTranscriptItem>(system_item).is_err());
|
||||
|
||||
let private_item = serde_json::json!({
|
||||
"sequence": 1,
|
||||
"role": "assistant",
|
||||
"content": "visible",
|
||||
"created_at": "2026-08-31T00:00:00Z",
|
||||
"reasoning": "hidden",
|
||||
"credential": "secret",
|
||||
"provider_session_id": "session-private"
|
||||
});
|
||||
assert!(serde_json::from_value::<CompanionTranscriptItem>(private_item).is_err());
|
||||
}
|
||||
|
||||
#[cfg(feature = "typescript")]
|
||||
#[test]
|
||||
fn generated_companion_api_contract_is_current() {
|
||||
let expected = companion_api_typescript();
|
||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("../../web/workspace/src/lib/generated/companion-api.ts");
|
||||
let actual = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("failed to read {}: {error}", path.display()));
|
||||
assert_eq!(
|
||||
normalize_typescript(&actual),
|
||||
normalize_typescript(&expected),
|
||||
"regenerate Companion API TypeScript types with `cargo run -q -p workspace-api --features typescript --example generate_companion_api_types > web/workspace/src/lib/generated/companion-api.ts` and format the generated file",
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(feature = "typescript")]
|
||||
fn normalize_typescript(value: &str) -> String {
|
||||
value
|
||||
.chars()
|
||||
.filter_map(|character| match character {
|
||||
character if character.is_whitespace() => None,
|
||||
',' => Some(';'),
|
||||
character => Some(character),
|
||||
})
|
||||
.collect::<String>()
|
||||
.replace(";}", "}")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,77 +1,14 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace_api::{
|
||||
CompanionLifecycleState, CompanionMessageDisposition, CompanionTransportSummary, Diagnostic,
|
||||
DiagnosticSeverity,
|
||||
};
|
||||
|
||||
use crate::hosts::{DiagnosticSeverity, RuntimeDiagnostic, WorkerSummary};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CompanionState {
|
||||
Disabled,
|
||||
Rejected,
|
||||
Cancelled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct CompanionStatusResponse {
|
||||
pub state: CompanionState,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub worker: Option<WorkerSummary>,
|
||||
pub transport: CompanionTransportSummary,
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct CompanionTransportSummary {
|
||||
pub kind: String,
|
||||
pub completion: String,
|
||||
pub limitation: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct CompanionMessageRequest {
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Default)]
|
||||
pub struct CompanionCancelRequest {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub reason: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct CompanionMessageResponse {
|
||||
pub state: CompanionState,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub worker: Option<WorkerSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub user_item: Option<CompanionTranscriptItem>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub assistant_item: Option<CompanionTranscriptItem>,
|
||||
pub transcript: CompanionTranscriptProjection,
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct CompanionTranscriptProjection {
|
||||
pub state: CompanionState,
|
||||
pub start: usize,
|
||||
pub limit: usize,
|
||||
pub total_items: usize,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub next_start: Option<usize>,
|
||||
pub items: Vec<CompanionTranscriptItem>,
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct CompanionTranscriptItem {
|
||||
pub sequence: u64,
|
||||
pub role: String,
|
||||
pub content: String,
|
||||
pub created_at: String,
|
||||
pub source: String,
|
||||
pub status: String,
|
||||
}
|
||||
pub use workspace_api::{
|
||||
CompanionCancelRequest, CompanionMessageRequest, CompanionMessageResponse,
|
||||
CompanionStatusResponse, CompanionTranscriptProjection,
|
||||
};
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct CompanionConsole;
|
||||
|
||||
impl CompanionConsole {
|
||||
@@ -81,68 +18,50 @@ impl CompanionConsole {
|
||||
|
||||
pub fn status(&self) -> CompanionStatusResponse {
|
||||
CompanionStatusResponse {
|
||||
state: CompanionState::Disabled,
|
||||
state: CompanionLifecycleState::Stopped,
|
||||
worker: None,
|
||||
transport: disabled_transport(),
|
||||
transport: CompanionTransportSummary {
|
||||
mode: "disabled".to_string(),
|
||||
available: false,
|
||||
},
|
||||
diagnostics: vec![disabled_diagnostic()],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn transcript(&self, start: usize, limit: usize) -> CompanionTranscriptProjection {
|
||||
CompanionTranscriptProjection {
|
||||
state: CompanionState::Disabled,
|
||||
state: CompanionLifecycleState::Stopped,
|
||||
start,
|
||||
limit,
|
||||
total_items: 0,
|
||||
next_start: None,
|
||||
total: 0,
|
||||
next: None,
|
||||
items: Vec::new(),
|
||||
diagnostics: vec![disabled_diagnostic()],
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_message(&self, _request: CompanionMessageRequest) -> CompanionMessageResponse {
|
||||
disabled_message_response(CompanionState::Rejected)
|
||||
disabled_message_response()
|
||||
}
|
||||
|
||||
pub fn cancel(&self, _request: CompanionCancelRequest) -> CompanionMessageResponse {
|
||||
disabled_message_response(CompanionState::Cancelled)
|
||||
disabled_message_response()
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled_message_response(state: CompanionState) -> CompanionMessageResponse {
|
||||
fn disabled_message_response() -> CompanionMessageResponse {
|
||||
CompanionMessageResponse {
|
||||
state,
|
||||
worker: None,
|
||||
user_item: None,
|
||||
assistant_item: None,
|
||||
transcript: CompanionTranscriptProjection {
|
||||
state: CompanionState::Disabled,
|
||||
start: 0,
|
||||
limit: 200,
|
||||
total_items: 0,
|
||||
next_start: None,
|
||||
items: Vec::new(),
|
||||
diagnostics: vec![disabled_diagnostic()],
|
||||
},
|
||||
diagnostics: vec![disabled_diagnostic()],
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled_transport() -> CompanionTransportSummary {
|
||||
CompanionTransportSummary {
|
||||
kind: "none".to_string(),
|
||||
completion: "disabled".to_string(),
|
||||
limitation:
|
||||
"Workspace Companion auto-start has been removed; create an explicit Worker instead."
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled_diagnostic() -> RuntimeDiagnostic {
|
||||
RuntimeDiagnostic {
|
||||
code: "companion_disabled".to_string(),
|
||||
severity: DiagnosticSeverity::Info,
|
||||
message: "Workspace Companion auto-start is disabled; create an explicit Worker instead."
|
||||
state: CompanionMessageDisposition::Rejected,
|
||||
message: "Workspace Companion auto-start is disabled; create or select an explicit Worker instead."
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn disabled_diagnostic() -> Diagnostic {
|
||||
Diagnostic {
|
||||
code: "companion_disabled".to_string(),
|
||||
severity: DiagnosticSeverity::Info,
|
||||
message:
|
||||
"Workspace Companion auto-start was removed; use the explicit Worker lifecycle instead."
|
||||
.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11084,33 +11084,43 @@ async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<Worksp
|
||||
}
|
||||
|
||||
fn companion_console_extension_point(status: &CompanionStatusResponse) -> ExtensionPointState {
|
||||
let completion = status.transport.completion.clone();
|
||||
let note = match completion.as_str() {
|
||||
"connected" => "Workspace Companion is input-capable and browser input is dispatched through the normal Worker runtime path.".to_string(),
|
||||
"not_input_capable" => {
|
||||
let diagnostic_codes = status
|
||||
.diagnostics
|
||||
.iter()
|
||||
.map(|diagnostic| diagnostic.code.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
if diagnostic_codes.is_empty() {
|
||||
"Workspace Companion is not input-capable; check provider, config, profile, secret, and authority diagnostics.".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"Workspace Companion is not input-capable; check typed diagnostics: {diagnostic_codes}."
|
||||
)
|
||||
}
|
||||
}
|
||||
"disabled" => "Workspace Companion auto-start has been removed; create an explicit Worker instead.".to_string(),
|
||||
other => format!(
|
||||
"Workspace Companion transport reports {other}; browser input follows the Companion Worker runtime capability state."
|
||||
),
|
||||
let extension_status = match status.state {
|
||||
workspace_api::CompanionLifecycleState::Idle => "idle",
|
||||
workspace_api::CompanionLifecycleState::Running => "running",
|
||||
workspace_api::CompanionLifecycleState::Stopped => "stopped",
|
||||
}
|
||||
.to_string();
|
||||
let diagnostic_codes = status
|
||||
.diagnostics
|
||||
.iter()
|
||||
.map(|diagnostic| diagnostic.code.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let note = if status.transport.available {
|
||||
"Workspace Companion is input-capable and browser input is dispatched through the normal Worker runtime path."
|
||||
.to_string()
|
||||
} else if diagnostic_codes.is_empty() {
|
||||
"Workspace Companion is unavailable; create or select an explicit Worker instead."
|
||||
.to_string()
|
||||
} else {
|
||||
format!("Workspace Companion is unavailable; check typed diagnostics: {diagnostic_codes}.")
|
||||
};
|
||||
ExtensionPointState {
|
||||
status: completion,
|
||||
status: extension_status,
|
||||
note,
|
||||
diagnostics: status.diagnostics.clone(),
|
||||
diagnostics: status
|
||||
.diagnostics
|
||||
.iter()
|
||||
.map(|diagnostic| RuntimeDiagnostic {
|
||||
code: diagnostic.code.clone(),
|
||||
severity: match diagnostic.severity {
|
||||
workspace_api::DiagnosticSeverity::Info => DiagnosticSeverity::Info,
|
||||
workspace_api::DiagnosticSeverity::Warning => DiagnosticSeverity::Warning,
|
||||
workspace_api::DiagnosticSeverity::Error => DiagnosticSeverity::Error,
|
||||
},
|
||||
message: diagnostic.message.clone(),
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24137,10 +24147,10 @@ mod tests {
|
||||
);
|
||||
|
||||
let companion_status = get_json(app.clone(), "/api/companion/status").await;
|
||||
assert_eq!(companion_status["state"], "disabled");
|
||||
assert_eq!(companion_status["state"], "stopped");
|
||||
assert!(companion_status["worker"].is_null());
|
||||
assert_eq!(companion_status["transport"]["kind"], "none");
|
||||
assert_eq!(companion_status["transport"]["completion"], "disabled");
|
||||
assert_eq!(companion_status["transport"]["mode"], "disabled");
|
||||
assert_eq!(companion_status["transport"]["available"], false);
|
||||
assert!(!companion_status.to_string().contains("/workspace/demo"));
|
||||
|
||||
let companion_message = post_json(
|
||||
@@ -24150,16 +24160,14 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
assert_eq!(companion_message["state"], "rejected");
|
||||
assert_eq!(
|
||||
companion_message["diagnostics"][0]["code"],
|
||||
"companion_disabled"
|
||||
);
|
||||
assert!(companion_message["user_item"].is_null());
|
||||
assert!(companion_message["assistant_item"].is_null());
|
||||
assert!(companion_message.get("accepted").is_none());
|
||||
assert!(companion_message.get("diagnostics").is_none());
|
||||
assert!(companion_message.get("user_item").is_none());
|
||||
assert!(companion_message.get("assistant_item").is_none());
|
||||
assert!(!companion_message.to_string().contains("/workspace/demo"));
|
||||
|
||||
let companion_transcript = get_json(app.clone(), "/api/companion/transcript").await;
|
||||
assert_eq!(companion_transcript["total_items"], 0);
|
||||
assert_eq!(companion_transcript["total"], 0);
|
||||
|
||||
let host_workers = get_json(app.clone(), &format!("/api/hosts/{host_id}/workers")).await;
|
||||
assert!(
|
||||
@@ -24264,7 +24272,7 @@ mod tests {
|
||||
|
||||
let workspace = get_json(app.clone(), "/api/workspace").await;
|
||||
let workspace_companion = &workspace["extension_points"]["companion_console"];
|
||||
assert_eq!(workspace_companion["status"], "disabled");
|
||||
assert_eq!(workspace_companion["status"], "stopped");
|
||||
assert_eq!(
|
||||
workspace_companion["diagnostics"][0]["code"],
|
||||
"companion_disabled"
|
||||
@@ -24273,12 +24281,13 @@ mod tests {
|
||||
workspace_companion["note"]
|
||||
.as_str()
|
||||
.unwrap()
|
||||
.contains("auto-start has been removed")
|
||||
.contains("typed diagnostics")
|
||||
);
|
||||
|
||||
let status = get_json(app.clone(), "/api/companion/status").await;
|
||||
assert_eq!(status["state"], "disabled");
|
||||
assert_eq!(status["transport"]["completion"], "disabled");
|
||||
assert_eq!(status["state"], "stopped");
|
||||
assert_eq!(status["transport"]["mode"], "disabled");
|
||||
assert_eq!(status["transport"]["available"], false);
|
||||
assert!(status["worker"].is_null());
|
||||
|
||||
let response = post_json(
|
||||
@@ -24288,13 +24297,14 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
assert_eq!(response["state"], "rejected");
|
||||
assert_eq!(response["diagnostics"][0]["code"], "companion_disabled");
|
||||
assert!(response["user_item"].is_null());
|
||||
assert!(response["assistant_item"].is_null());
|
||||
assert!(response.get("accepted").is_none());
|
||||
assert!(response.get("diagnostics").is_none());
|
||||
assert!(response.get("user_item").is_none());
|
||||
assert!(response.get("assistant_item").is_none());
|
||||
|
||||
let transcript = get_json(app.clone(), "/api/companion/transcript").await;
|
||||
assert_eq!(transcript["state"], "disabled");
|
||||
assert_eq!(transcript["total_items"], 0);
|
||||
assert_eq!(transcript["state"], "stopped");
|
||||
assert_eq!(transcript["total"], 0);
|
||||
|
||||
let workers = get_json(app, "/api/workers").await;
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user