chore: merge develop into hare/develop
# Conflicts: # crates/client/src/lib.rs # web/workspace/deno.json
This commit is contained in:
@@ -18,7 +18,6 @@ tokio = { workspace = true, features = ["rt", "macros", "net", "io-util", "sync"
|
||||
tokio-tungstenite = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
workspace-api.workspace = true
|
||||
workdir = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -5,7 +5,6 @@ use std::fmt;
|
||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||
use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
|
||||
pub use workdir::workspace::WorkingDirectorySummary as BackendWorkingDirectorySummary;
|
||||
pub use workspace_api::{
|
||||
Diagnostic as BackendDiagnostic, DiagnosticSeverity as BackendDiagnosticSeverity,
|
||||
ListResponse as BackendRuntimeListResponse, RuntimeSummary as BackendRuntimeSummary,
|
||||
@@ -14,6 +13,11 @@ pub use workspace_api::{
|
||||
WorkerRestoreResponse as BackendWorkerRestoreResponse,
|
||||
WorkerRestoreResult as BackendWorkerRestoreResult, WorkerSummary as BackendWorkerSummary,
|
||||
WorkerWorkspaceSummary as BackendWorkerWorkspaceSummary,
|
||||
WorkingDirectoryCreateRequest as BackendWorkingDirectoryCreateRequest,
|
||||
WorkingDirectoryCreateResponse as BackendWorkingDirectoryCreateResponse,
|
||||
WorkingDirectoryDetailResponse as BackendWorkingDirectoryDetailResponse,
|
||||
WorkingDirectoryListResponse as BackendWorkingDirectoryListResponse,
|
||||
WorkingDirectorySummary as BackendWorkingDirectorySummary,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -526,8 +530,8 @@ mod tests {
|
||||
.unwrap()
|
||||
.occupied_by
|
||||
.expect("occupied Workdir");
|
||||
assert_eq!(occupied_by.worker.runtime_id, "arcadia");
|
||||
assert_eq!(occupied_by.worker.worker_id, "worker-opaque-64");
|
||||
assert_eq!(occupied_by.runtime_id, "arcadia");
|
||||
assert_eq!(occupied_by.worker_id, "worker-opaque-64");
|
||||
|
||||
let mut stale = payload;
|
||||
stale["working_directory"]["occupied_by"]["runtime_worker_id"] = serde_json::json!(64);
|
||||
|
||||
@@ -2,19 +2,16 @@ use crate::{BackendApiClient, BackendApiClientError};
|
||||
use reqwest::Method;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use workspace_api::{RepositoryObservedStatus, RepositorySource};
|
||||
use workspace_api::{
|
||||
WorkspaceCatalogListResponse, WorkspaceCreateResponse, WorkspaceRepositoryRecord,
|
||||
WorkspaceSummary,
|
||||
};
|
||||
|
||||
const DEFAULT_WORKSPACE_LIMIT: usize = 200;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct BackendWorkspace {
|
||||
pub workspace_id: String,
|
||||
pub owner_account_id: Option<String>,
|
||||
pub display_name: String,
|
||||
pub state: String,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
}
|
||||
pub type BackendWorkspace = WorkspaceSummary;
|
||||
pub type CreateBackendWorkspaceResponse = WorkspaceCreateResponse;
|
||||
pub type CreateBackendWorkspaceRepositoryRecord = WorkspaceRepositoryRecord;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Eq)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -32,30 +29,6 @@ pub struct CreateBackendWorkspaceRepository {
|
||||
pub default_ref: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct CreateBackendWorkspaceResponse {
|
||||
pub workspace: BackendWorkspace,
|
||||
pub repository: CreateBackendWorkspaceRepositoryRecord,
|
||||
pub config_revision: u64,
|
||||
pub request_fingerprint: String,
|
||||
pub replayed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct CreateBackendWorkspaceRepositoryRecord {
|
||||
pub workspace_id: String,
|
||||
pub repository_id: String,
|
||||
pub name: String,
|
||||
pub kind: String,
|
||||
pub provider: Option<String>,
|
||||
pub source: RepositorySource,
|
||||
pub default_ref: Option<String>,
|
||||
pub source_revision: u64,
|
||||
pub source_fingerprint: String,
|
||||
pub observed_status: RepositoryObservedStatus,
|
||||
pub observed_at: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct BackendWorkspaceCatalogTarget {
|
||||
pub base_url: String,
|
||||
@@ -118,7 +91,7 @@ async fn list_backend_workspaces_with_client(
|
||||
.send()
|
||||
.await?;
|
||||
client.check_status(response.status())?;
|
||||
Ok(response.json::<Vec<BackendWorkspace>>().await?)
|
||||
Ok(response.json::<WorkspaceCatalogListResponse>().await?.0)
|
||||
}
|
||||
|
||||
pub async fn create_backend_workspace(
|
||||
|
||||
@@ -39,5 +39,10 @@ pub use target::{
|
||||
StandaloneWorkerResumeIntent, Target, TargetError, TargetKind, WorkerConnection,
|
||||
WorkerConnectionSelector, WorkerList, WorkerListRequest, WorkerSpawn,
|
||||
};
|
||||
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;
|
||||
|
||||
@@ -18,6 +18,7 @@ sha2.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["process", "rt", "sync", "time"] }
|
||||
workspace-api = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
|
||||
+11
-251
@@ -6,7 +6,11 @@
|
||||
//! [`crate::http`].
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
|
||||
pub use workspace_api::{
|
||||
WorkingDirectoryCleanupTarget, WorkingDirectoryMaterializerKind as MaterializerKind,
|
||||
WorkingDirectoryOccupancy, WorkingDirectoryStatusKind, WorkingDirectorySummary,
|
||||
};
|
||||
|
||||
/// Stable Workspace identity for a Worker hosted by a Runtime.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
|
||||
@@ -26,83 +30,6 @@ impl RuntimeWorkerRef {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum MaterializerKind {
|
||||
#[default]
|
||||
RuntimeGitCache,
|
||||
/// Legacy persisted value from the pre-cache local `git worktree` materializer.
|
||||
LocalGitWorktree,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkingDirectoryStatusKind {
|
||||
Active,
|
||||
CleanupPending,
|
||||
Corrupted,
|
||||
NotFound,
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl WorkingDirectoryStatusKind {
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Active => "active",
|
||||
Self::CleanupPending => "cleanup_pending",
|
||||
Self::Corrupted => "corrupted",
|
||||
Self::NotFound => "not_found",
|
||||
Self::Unknown => "unknown",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for WorkingDirectoryStatusKind {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(self.as_str())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkingDirectoryCleanupTarget {
|
||||
pub kind: String,
|
||||
pub working_directory_id: String,
|
||||
pub repository_id: String,
|
||||
}
|
||||
|
||||
/// Durable Workspace occupancy projection for one Workdir.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
|
||||
pub struct WorkingDirectoryOccupancy {
|
||||
#[serde(flatten)]
|
||||
pub worker: RuntimeWorkerRef,
|
||||
pub display_name: String,
|
||||
pub linked_at: String,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for WorkingDirectoryOccupancy {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct Wire {
|
||||
runtime_id: String,
|
||||
worker_id: String,
|
||||
display_name: String,
|
||||
linked_at: String,
|
||||
}
|
||||
|
||||
let wire = Wire::deserialize(deserializer)?;
|
||||
Ok(Self {
|
||||
worker: RuntimeWorkerRef::new(wire.runtime_id, wire.worker_id),
|
||||
display_name: wire.display_name,
|
||||
linked_at: wire.linked_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Immutable materialization provenance retained by Workspace inventory.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
@@ -139,100 +66,6 @@ pub struct WorkingDirectoryCurrentObservation {
|
||||
pub occupied_by: Option<WorkingDirectoryOccupancy>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkingDirectorySummary {
|
||||
pub working_directory_id: String,
|
||||
pub repository_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub creation_selector: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub creation_ref: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub creation_tree: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_selector: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_ref: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_tree: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub observed_at_epoch_seconds: Option<u64>,
|
||||
pub materializer_kind: MaterializerKind,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cleanup_target: Option<WorkingDirectoryCleanupTarget>,
|
||||
pub status: WorkingDirectoryStatusKind,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub cleanliness: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub primary_worker_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub occupied_by: Option<WorkingDirectoryOccupancy>,
|
||||
}
|
||||
|
||||
impl WorkingDirectorySummary {
|
||||
/// Workspace-managed inventory rows carry explicit cleanup authority.
|
||||
pub fn is_workspace_managed(&self) -> bool {
|
||||
self.cleanup_target.is_some()
|
||||
}
|
||||
|
||||
pub fn provenance(&self) -> WorkingDirectoryProvenance {
|
||||
WorkingDirectoryProvenance {
|
||||
creation_selector: self.creation_selector.clone(),
|
||||
creation_ref: self.creation_ref.clone(),
|
||||
creation_tree: self.creation_tree.clone(),
|
||||
materializer_kind: self.materializer_kind.clone(),
|
||||
cleanup_target: self.cleanup_target.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_observation(&self) -> WorkingDirectoryCurrentObservation {
|
||||
WorkingDirectoryCurrentObservation {
|
||||
current_selector: self.current_selector.clone(),
|
||||
current_ref: self.current_ref.clone(),
|
||||
current_tree: self.current_tree.clone(),
|
||||
observed_at_epoch_seconds: self.observed_at_epoch_seconds,
|
||||
status: self.status.clone(),
|
||||
cleanliness: self.cleanliness.clone(),
|
||||
primary_worker_id: self.primary_worker_id.clone(),
|
||||
occupied_by: self.occupied_by.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum WorkingDirectoryDiagnosticSeverity {
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkingDirectoryDiagnostic {
|
||||
pub code: String,
|
||||
pub severity: WorkingDirectoryDiagnosticSeverity,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkingDirectoryListResponse {
|
||||
pub workspace_id: String,
|
||||
pub items: Vec<WorkingDirectorySummary>,
|
||||
pub diagnostics: Vec<WorkingDirectoryDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct WorkingDirectoryDetailResponse {
|
||||
pub workspace_id: String,
|
||||
pub runtime_id: String,
|
||||
pub item: WorkingDirectorySummary,
|
||||
pub diagnostics: Vec<WorkingDirectoryDiagnostic>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -255,88 +88,15 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occupied_and_free_list_response_round_trips() {
|
||||
let response = WorkingDirectoryListResponse {
|
||||
workspace_id: "workspace".to_string(),
|
||||
items: vec![
|
||||
WorkingDirectorySummary {
|
||||
working_directory_id: "occupied".to_string(),
|
||||
repository_id: "repo".to_string(),
|
||||
creation_selector: Some("develop".to_string()),
|
||||
creation_ref: Some("abc123".to_string()),
|
||||
creation_tree: Some("tree123".to_string()),
|
||||
current_selector: Some("work/ticket".to_string()),
|
||||
current_ref: Some("def456".to_string()),
|
||||
current_tree: Some("tree456".to_string()),
|
||||
observed_at_epoch_seconds: Some(1_777_777_777),
|
||||
materializer_kind: MaterializerKind::LocalGitWorktree,
|
||||
cleanup_target: Some(WorkingDirectoryCleanupTarget {
|
||||
kind: "git_worktree".to_string(),
|
||||
working_directory_id: "occupied".to_string(),
|
||||
repository_id: "repo".to_string(),
|
||||
}),
|
||||
status: WorkingDirectoryStatusKind::Active,
|
||||
cleanliness: Some("clean".to_string()),
|
||||
primary_worker_id: None,
|
||||
occupied_by: Some(WorkingDirectoryOccupancy {
|
||||
worker: RuntimeWorkerRef::new("arcadia", "worker-opaque-64"),
|
||||
display_name: "Coder".to_string(),
|
||||
linked_at: "2026-08-12T00:00:00Z".to_string(),
|
||||
}),
|
||||
},
|
||||
WorkingDirectorySummary {
|
||||
working_directory_id: "free".to_string(),
|
||||
repository_id: "repo".to_string(),
|
||||
creation_selector: None,
|
||||
creation_ref: None,
|
||||
creation_tree: None,
|
||||
current_selector: None,
|
||||
current_ref: Some("987fed".to_string()),
|
||||
current_tree: None,
|
||||
observed_at_epoch_seconds: None,
|
||||
materializer_kind: MaterializerKind::LocalGitWorktree,
|
||||
cleanup_target: None,
|
||||
status: WorkingDirectoryStatusKind::Active,
|
||||
cleanliness: Some("unknown".to_string()),
|
||||
primary_worker_id: None,
|
||||
occupied_by: None,
|
||||
},
|
||||
],
|
||||
diagnostics: vec![WorkingDirectoryDiagnostic {
|
||||
code: "observed".to_string(),
|
||||
severity: WorkingDirectoryDiagnosticSeverity::Info,
|
||||
message: "inventory observed".to_string(),
|
||||
}],
|
||||
};
|
||||
|
||||
let encoded = serde_json::to_value(&response).unwrap();
|
||||
fn workspace_workdir_projection_reexports_workspace_api_authority() {
|
||||
assert_eq!(
|
||||
encoded["items"][0]["occupied_by"]["worker_id"],
|
||||
"worker-opaque-64"
|
||||
std::any::TypeId::of::<WorkingDirectorySummary>(),
|
||||
std::any::TypeId::of::<workspace_api::WorkingDirectorySummary>()
|
||||
);
|
||||
assert!(
|
||||
encoded["items"][0]["occupied_by"]
|
||||
.get("runtime_worker_id")
|
||||
.is_none()
|
||||
assert_eq!(
|
||||
std::any::TypeId::of::<WorkingDirectoryOccupancy>(),
|
||||
std::any::TypeId::of::<workspace_api::WorkingDirectoryOccupancy>()
|
||||
);
|
||||
assert!(encoded["items"][1].get("occupied_by").is_none());
|
||||
|
||||
let mut stale = encoded.clone();
|
||||
stale["items"][0]["occupied_by"]["runtime_worker_id"] = serde_json::json!(64);
|
||||
assert!(serde_json::from_value::<WorkingDirectoryListResponse>(stale).is_err());
|
||||
|
||||
let decoded: WorkingDirectoryListResponse = serde_json::from_value(encoded).unwrap();
|
||||
assert_eq!(decoded, response);
|
||||
|
||||
let detail = WorkingDirectoryDetailResponse {
|
||||
workspace_id: decoded.workspace_id.clone(),
|
||||
runtime_id: "arcadia".to_string(),
|
||||
item: decoded.items[0].clone(),
|
||||
diagnostics: decoded.diagnostics.clone(),
|
||||
};
|
||||
let encoded = serde_json::to_value(&detail).unwrap();
|
||||
let decoded: WorkingDirectoryDetailResponse = serde_json::from_value(encoded).unwrap();
|
||||
assert_eq!(decoded, detail);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,7 @@ use async_trait::async_trait;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use workdir::http::{WorkdirSessionOperation, WorkdirSessionOperationResult};
|
||||
use workdir::workspace::{
|
||||
WorkingDirectoryDetailResponse as WorkdirDetailResponse,
|
||||
WorkingDirectoryListResponse as WorkdirListResponse, WorkspaceWorkdirSessionFence,
|
||||
WorkspaceWorkdirSessionOperationRequest,
|
||||
};
|
||||
use workdir::workspace::{WorkspaceWorkdirSessionFence, WorkspaceWorkdirSessionOperationRequest};
|
||||
use workdir::{
|
||||
CommandHandle, CommandOutput, CommandOutputRequest, CommandRequest, CommandStatus, EditRequest,
|
||||
EditResult, GlobRequest, GlobResult, GrepRequest, GrepResult, ListRequest, ListResult,
|
||||
@@ -24,6 +20,13 @@ use workdir::{
|
||||
WorkdirSessionCapabilities, WorkdirSessionHandle, WriteRequest, WriteResult,
|
||||
};
|
||||
|
||||
use workspace_api::{
|
||||
WorkingDirectoryCreateRequest as WorkdirCreateRequest,
|
||||
WorkingDirectoryCreateResponse as WorkdirCreateResponse,
|
||||
WorkingDirectoryDetailResponse as WorkdirDetailResponse,
|
||||
WorkingDirectoryListResponse as WorkdirListResponse,
|
||||
};
|
||||
|
||||
use crate::feature::{
|
||||
FeatureDescriptor, FeatureInstallContext, FeatureInstallError, FeatureModule, ToolContribution,
|
||||
ToolDeclaration,
|
||||
@@ -426,9 +429,9 @@ impl WorkspaceHttpWorkdirBackend {
|
||||
runtime_id: runtime_id.map(str::to_string),
|
||||
repository_id: repository_id.to_string(),
|
||||
selector,
|
||||
operation_id,
|
||||
operation_id: Some(operation_id),
|
||||
};
|
||||
let response = self.execute_json::<WorkdirDetailResponse>(WorkspaceRequest::json(
|
||||
let response = self.execute_json::<WorkdirCreateResponse>(WorkspaceRequest::json(
|
||||
WorkspaceRequestMethod::Post,
|
||||
format!("/api/w/{workspace_id}/working-directories"),
|
||||
serde_json::to_string(&request).map_err(decode_error)?,
|
||||
@@ -707,16 +710,6 @@ struct WorkdirCreateInput {
|
||||
selector: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
struct WorkdirCreateRequest {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
runtime_id: Option<String>,
|
||||
repository_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
selector: Option<String>,
|
||||
operation_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct WorkdirAttachInput {
|
||||
|
||||
@@ -12,7 +12,22 @@ typescript = ["dep:ts-rs"]
|
||||
[dependencies]
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
ts-rs = { version = "12.0.1", optional = true }
|
||||
workdir.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "generate_typescript"
|
||||
required-features = ["typescript"]
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json.workspace = true
|
||||
|
||||
[[example]]
|
||||
name = "generate_workdir_api_types"
|
||||
required-features = ["typescript"]
|
||||
|
||||
[[example]]
|
||||
name = "generate_companion_api_types"
|
||||
required-features = ["typescript"]
|
||||
|
||||
[[example]]
|
||||
name = "generate_repository_access_types"
|
||||
required-features = ["typescript"]
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
print!("{}", workspace_api::companion_api_typescript());
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
print!("{}", workspace_api::repository_access_api_typescript());
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
print!("{}", workspace_api::catalog_typescript());
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
print!("{}", workspace_api::workdir_api_typescript());
|
||||
}
|
||||
+1031
-1
File diff suppressed because it is too large
Load Diff
@@ -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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,19 +42,19 @@ pub use authority::{
|
||||
pub use config::{BackendRuntimesConfigFile, ResolvedWorkspaceBackendConfig, ServerHostConfigFile};
|
||||
pub use identity::{WORKSPACE_IDENTITY_RELATIVE_PATH, WorkspaceIdentity};
|
||||
pub use records::{ObjectiveDetail, ObjectiveSummary, TicketDetail, TicketSummary};
|
||||
pub use repositories::{
|
||||
ConfiguredRepository, GitCommitSummary, GitRemoteSummary, GitRepositorySummary,
|
||||
RepositoryLogRead, RepositoryRegistryReader, RepositorySummary,
|
||||
};
|
||||
pub use repositories::{ConfiguredRepository, RepositoryLogRead, RepositoryRegistryReader};
|
||||
pub use server::{
|
||||
AuthConfig, ServerConfig, WorkspaceApi, WorkspaceServerApi, build_router,
|
||||
build_workspace_server_router, serve, serve_workspace_catalog,
|
||||
};
|
||||
pub use store::{ControlPlaneStore, SqliteWorkspaceStore, WorkspaceRecord};
|
||||
pub use workspace_catalog::{
|
||||
InitialRepositoryIntent, WorkspaceCatalogService, WorkspaceCreateRequest,
|
||||
pub use workspace_api::{
|
||||
GitCommitSummary, GitRemoteSummary, GitRepositorySummary, RepositorySummary,
|
||||
WorkspaceCreateResponse,
|
||||
};
|
||||
pub use workspace_catalog::{
|
||||
InitialRepositoryIntent, WorkspaceCatalogService, WorkspaceCreateRequest, WorkspaceCreateResult,
|
||||
};
|
||||
|
||||
use worker_runtime::identity::RuntimeWorkerRef;
|
||||
|
||||
|
||||
@@ -4,8 +4,10 @@ use std::{
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use workspace_api::{RepositoryObservedStatus, RepositorySource};
|
||||
use workspace_api::{
|
||||
Diagnostic, DiagnosticSeverity, GitCommitSummary, GitRemoteSummary, GitRepositorySummary,
|
||||
RepositoryDiagnostic, RepositoryObservedStatus, RepositorySource, RepositorySummary,
|
||||
};
|
||||
|
||||
pub type RepositoryId = String;
|
||||
pub type RepositorySelector = String;
|
||||
@@ -24,74 +26,19 @@ pub struct ConfiguredRepository {
|
||||
pub default_selector: Option<RepositorySelector>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RepositorySummary {
|
||||
pub id: RepositoryId,
|
||||
pub display_name: String,
|
||||
pub kind: String,
|
||||
pub provider: String,
|
||||
pub source: RepositorySource,
|
||||
pub source_revision: u64,
|
||||
pub source_fingerprint: String,
|
||||
pub observed_status: RepositoryObservedStatus,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub observed_at: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_selector: Option<RepositorySelector>,
|
||||
pub record_authority: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub git: Option<GitRepositorySummary>,
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub diagnostics: Vec<RepositoryDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GitRepositorySummary {
|
||||
pub status: String,
|
||||
pub head: Option<String>,
|
||||
pub branch: Option<String>,
|
||||
pub dirty: bool,
|
||||
pub remotes: Vec<GitRemoteSummary>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GitRemoteSummary {
|
||||
pub name: String,
|
||||
pub fetch_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct RepositoryDiagnostic {
|
||||
pub severity: String,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RepositoryListProjection {
|
||||
pub items: Vec<RepositorySummary>,
|
||||
pub diagnostics: Vec<RepositoryDiagnostic>,
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct RepositoryLogRead {
|
||||
pub repository_id: RepositoryId,
|
||||
pub default_selector: Option<RepositorySelector>,
|
||||
pub limit: usize,
|
||||
pub commits: Vec<GitCommitSummary>,
|
||||
pub diagnostics: Vec<RepositoryDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct GitCommitSummary {
|
||||
pub hash: String,
|
||||
pub short_hash: String,
|
||||
pub summary: String,
|
||||
pub author_name: String,
|
||||
pub author_email: String,
|
||||
pub author_date: String,
|
||||
pub parents: Vec<String>,
|
||||
pub refs: Vec<String>,
|
||||
pub diagnostics: Vec<Diagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -131,8 +78,8 @@ impl RepositoryRegistryReader {
|
||||
if self.repositories.is_empty() {
|
||||
return RepositoryListProjection {
|
||||
items: Vec::new(),
|
||||
diagnostics: vec![RepositoryDiagnostic {
|
||||
severity: "warning".to_string(),
|
||||
diagnostics: vec![Diagnostic {
|
||||
severity: DiagnosticSeverity::Warning,
|
||||
code: "repository_config_empty".to_string(),
|
||||
message: "No repositories are configured for this workspace backend."
|
||||
.to_string(),
|
||||
@@ -177,8 +124,8 @@ impl RepositoryRegistryReader {
|
||||
let commits = match self.git_log(repository, limit) {
|
||||
Ok(commits) => commits,
|
||||
Err(message) => {
|
||||
diagnostics.push(RepositoryDiagnostic {
|
||||
severity: "warning".to_string(),
|
||||
diagnostics.push(Diagnostic {
|
||||
severity: DiagnosticSeverity::Warning,
|
||||
code: "repository_git_log_unavailable".to_string(),
|
||||
message,
|
||||
});
|
||||
@@ -379,7 +326,7 @@ impl RepositoryRegistryReader {
|
||||
default_selector: repository.default_selector.clone(),
|
||||
record_authority: "workspace-control-plane".to_string(),
|
||||
git,
|
||||
diagnostics,
|
||||
diagnostics: (!diagnostics.is_empty()).then_some(diagnostics),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,7 +622,10 @@ mod tests {
|
||||
RepositoryObservedStatus::Unverified
|
||||
);
|
||||
assert!(summary.git.is_none());
|
||||
assert_eq!(summary.diagnostics[0].code, "repository_source_unverified");
|
||||
assert_eq!(
|
||||
summary.diagnostics.as_ref().unwrap()[0].code,
|
||||
"repository_source_unverified"
|
||||
);
|
||||
|
||||
let repository = reader.merge_repository("remote").unwrap();
|
||||
let error = merge_git_stdout(&repository, "inspect", &["rev-parse", "HEAD"]).unwrap_err();
|
||||
|
||||
@@ -48,10 +48,7 @@ use workdir::http::{
|
||||
WorkdirSessionOperation, WorkdirSessionOperationResult, WorkdirTransportError,
|
||||
};
|
||||
use workdir::workspace::{
|
||||
MaterializerKind, WorkingDirectoryCleanupTarget,
|
||||
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
|
||||
WorkingDirectoryDiagnostic, WorkingDirectoryDiagnosticSeverity,
|
||||
WorkingDirectoryListResponse as BrowserWorkingDirectoryListResponse, WorkingDirectoryOccupancy,
|
||||
MaterializerKind, WorkingDirectoryCleanupTarget, WorkingDirectoryOccupancy,
|
||||
WorkingDirectoryStatusKind, WorkingDirectorySummary, WorkspaceWorkdirSessionFence,
|
||||
WorkspaceWorkdirSessionOperationRequest,
|
||||
};
|
||||
@@ -65,9 +62,17 @@ use workspace_api::{
|
||||
DeleteRepositorySshCredentialRequest, DeleteRepositorySshHostTrustRequest,
|
||||
ObjectiveCreateRequest, ObjectiveEditRequest, ObjectiveLinkTicketRequest,
|
||||
ObjectiveStateRequest, PutRepositorySshHostTrustRequest, RepositoryAccessProjection,
|
||||
RepositoryDetailResponse, RepositoryListResponse, RepositoryLogResponse,
|
||||
RepositorySshCredential, RepositorySshHostTrust, RotateRepositorySshCredentialRequest,
|
||||
RuntimeConnectionTestResponse, RuntimeManagementSummary, TICKET_ORCHESTRATION_PLANS_QUERY_PATH,
|
||||
TICKET_RELATIONS_QUERY_PATH, WorkspaceRuntimeResource, WorkspaceWorkerDiscoveryItem,
|
||||
TICKET_RELATIONS_QUERY_PATH,
|
||||
WorkingDirectoryCreateRequest as BrowserWorkingDirectoryCreateRequest,
|
||||
WorkingDirectoryCreateResponse as BrowserWorkingDirectoryCreateResponse,
|
||||
WorkingDirectoryDetailResponse as BrowserWorkingDirectoryDetailResponse,
|
||||
WorkingDirectoryListResponse as BrowserWorkingDirectoryListResponse,
|
||||
WorkspaceCatalogListResponse, WorkspaceCreateResponse, WorkspaceExtensionPointState,
|
||||
WorkspaceExtensionPoints, WorkspacePermissionSummary, WorkspaceRepositoryRecord,
|
||||
WorkspaceResponse, WorkspaceRuntimeResource, WorkspaceSummary, WorkspaceWorkerDiscoveryItem,
|
||||
WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject,
|
||||
};
|
||||
|
||||
@@ -116,7 +121,7 @@ use crate::records::{
|
||||
};
|
||||
use crate::repositories::{
|
||||
ConfiguredRepository, RepositoryListProjection, RepositoryLogRead, RepositoryLookupError,
|
||||
RepositoryRegistryReader, RepositorySummary,
|
||||
RepositoryRegistryReader,
|
||||
};
|
||||
use crate::repository_access::{
|
||||
RepositoryAccessConfigSchemaProvider, RepositorySecretService,
|
||||
@@ -152,17 +157,7 @@ use worker_runtime::identity::{RuntimeWorkerRef, WorkerId};
|
||||
|
||||
const EMBEDDED_WORKER_RUNTIME_ID: &str = "embedded-worker-runtime";
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub enum AuthConfig {
|
||||
/// Browser human auth uses Passkey ceremonies and HttpOnly cookie sessions;
|
||||
/// CLI/TUI auth uses API tokens obtained through the device login flow.
|
||||
Passkey {
|
||||
rp_id: String,
|
||||
origin: String,
|
||||
public_base_url: String,
|
||||
cookie_name: String,
|
||||
},
|
||||
}
|
||||
pub use workspace_api::WorkspaceAuthConfig as AuthConfig;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ServerConfig {
|
||||
@@ -989,7 +984,9 @@ async fn list_server_workspaces(
|
||||
let owner = match resolve_server_actor(&api, &headers).await {
|
||||
Ok(Some(actor)) => Some(actor.account_id),
|
||||
Ok(None) => match api.catalog.is_empty() {
|
||||
Ok(true) => return Json(Vec::<WorkspaceRecord>::new()).into_response(),
|
||||
Ok(true) => {
|
||||
return Json(WorkspaceCatalogListResponse(Vec::new())).into_response();
|
||||
}
|
||||
Ok(false) => return StatusCode::UNAUTHORIZED.into_response(),
|
||||
Err(error) => return server_error_response(error),
|
||||
},
|
||||
@@ -999,7 +996,10 @@ async fn list_server_workspaces(
|
||||
.catalog
|
||||
.list(owner.as_deref(), query.limit.unwrap_or(100))
|
||||
{
|
||||
Ok(workspaces) => Json(workspaces).into_response(),
|
||||
Ok(workspaces) => Json(WorkspaceCatalogListResponse(
|
||||
workspaces.into_iter().map(workspace_summary).collect(),
|
||||
))
|
||||
.into_response(),
|
||||
Err(error) => server_error_response(error),
|
||||
}
|
||||
}
|
||||
@@ -1031,7 +1031,48 @@ async fn create_server_workspace(
|
||||
} else {
|
||||
StatusCode::CREATED
|
||||
};
|
||||
(status, Json(created)).into_response()
|
||||
(status, Json(workspace_create_response(created))).into_response()
|
||||
}
|
||||
|
||||
fn workspace_summary(record: WorkspaceRecord) -> WorkspaceSummary {
|
||||
WorkspaceSummary {
|
||||
workspace_id: record.workspace_id,
|
||||
owner_account_id: record.owner_account_id,
|
||||
display_name: record.display_name,
|
||||
state: record.state,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_repository_record(record: RepositoryRecord) -> WorkspaceRepositoryRecord {
|
||||
WorkspaceRepositoryRecord {
|
||||
workspace_id: record.workspace_id,
|
||||
repository_id: record.repository_id,
|
||||
name: record.name,
|
||||
kind: record.kind,
|
||||
provider: record.provider,
|
||||
source: record.source,
|
||||
default_ref: record.default_ref,
|
||||
source_revision: record.source_revision,
|
||||
source_fingerprint: record.source_fingerprint,
|
||||
observed_status: record.observed_status,
|
||||
observed_at: record.observed_at,
|
||||
created_at: record.created_at,
|
||||
updated_at: record.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn workspace_create_response(
|
||||
created: crate::workspace_catalog::WorkspaceCreateResult,
|
||||
) -> WorkspaceCreateResponse {
|
||||
WorkspaceCreateResponse {
|
||||
workspace: workspace_summary(created.workspace),
|
||||
repository: workspace_repository_record(created.repository),
|
||||
config_revision: created.config_revision,
|
||||
request_fingerprint: created.request_fingerprint,
|
||||
replayed: created.replayed,
|
||||
}
|
||||
}
|
||||
|
||||
async fn resolve_server_actor(
|
||||
@@ -2743,31 +2784,6 @@ pub async fn serve(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct WorkspaceResponse {
|
||||
pub workspace_id: String,
|
||||
pub display_name: String,
|
||||
pub record_authority: String,
|
||||
pub schema_version: i64,
|
||||
pub auth: AuthConfig,
|
||||
pub extension_points: ExtensionPoints,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ExtensionPoints {
|
||||
pub store: String,
|
||||
pub event_stream: ExtensionPointState,
|
||||
pub host_worker_bridge: ExtensionPointState,
|
||||
pub companion_console: ExtensionPointState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ExtensionPointState {
|
||||
pub status: String,
|
||||
pub note: String,
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct ListResponse<T> {
|
||||
pub workspace_id: String,
|
||||
@@ -3003,18 +3019,6 @@ pub struct WorkingDirectoryRepositoryOption {
|
||||
pub default_selector: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BrowserWorkingDirectoryCreateRequest {
|
||||
#[serde(default)]
|
||||
pub runtime_id: Option<String>,
|
||||
pub repository_id: String,
|
||||
#[serde(default)]
|
||||
pub selector: Option<String>,
|
||||
#[serde(default)]
|
||||
pub operation_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BrowserWorkerWorkingDirectorySelection {
|
||||
@@ -3071,32 +3075,6 @@ pub struct BrowserCreateWorkerResponse {
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RepositoryListResponse {
|
||||
pub workspace_id: String,
|
||||
pub items: Vec<RepositorySummary>,
|
||||
pub source: String,
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RepositoryDetailResponse {
|
||||
pub workspace_id: String,
|
||||
pub item: RepositorySummary,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct RepositoryLogResponse {
|
||||
pub workspace_id: String,
|
||||
pub repository_id: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub default_selector: Option<String>,
|
||||
pub limit: usize,
|
||||
pub items: Vec<crate::repositories::GitCommitSummary>,
|
||||
pub diagnostics: Vec<RuntimeDiagnostic>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LogQuery {
|
||||
limit: Option<usize>,
|
||||
@@ -3361,11 +3339,12 @@ async fn scoped_get_flow(
|
||||
}
|
||||
|
||||
async fn scoped_get_workspace(
|
||||
headers: HeaderMap,
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
) -> ApiResult<Json<WorkspaceResponse>> {
|
||||
validate_workspace_scope(&api, &path.workspace_id)?;
|
||||
get_workspace(State(api)).await
|
||||
get_workspace(headers, State(api)).await
|
||||
}
|
||||
|
||||
async fn scoped_get_workspace_settings(
|
||||
@@ -8947,15 +8926,15 @@ async fn scoped_get_worker_launch_options(
|
||||
|
||||
fn working_directory_diagnostics(
|
||||
diagnostics: Vec<RuntimeDiagnostic>,
|
||||
) -> Vec<WorkingDirectoryDiagnostic> {
|
||||
) -> Vec<workspace_api::Diagnostic> {
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| WorkingDirectoryDiagnostic {
|
||||
.map(|diagnostic| workspace_api::Diagnostic {
|
||||
code: diagnostic.code,
|
||||
severity: match diagnostic.severity {
|
||||
DiagnosticSeverity::Info => WorkingDirectoryDiagnosticSeverity::Info,
|
||||
DiagnosticSeverity::Warning => WorkingDirectoryDiagnosticSeverity::Warning,
|
||||
DiagnosticSeverity::Error => WorkingDirectoryDiagnosticSeverity::Error,
|
||||
DiagnosticSeverity::Info => workspace_api::DiagnosticSeverity::Info,
|
||||
DiagnosticSeverity::Warning => workspace_api::DiagnosticSeverity::Warning,
|
||||
DiagnosticSeverity::Error => workspace_api::DiagnosticSeverity::Error,
|
||||
},
|
||||
message: diagnostic.message,
|
||||
})
|
||||
@@ -8979,7 +8958,7 @@ async fn scoped_create_runtime_working_directory(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedRuntimePath>,
|
||||
Json(request): Json<BrowserWorkingDirectoryCreateRequest>,
|
||||
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryDetailResponse>)> {
|
||||
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryCreateResponse>)> {
|
||||
create_workspace_working_directory(
|
||||
&api,
|
||||
&path.workspace_id,
|
||||
@@ -9022,7 +9001,7 @@ async fn scoped_create_working_directory(
|
||||
State(api): State<WorkspaceApi>,
|
||||
AxumPath(path): AxumPath<ScopedWorkspacePath>,
|
||||
Json(request): Json<BrowserWorkingDirectoryCreateRequest>,
|
||||
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryDetailResponse>)> {
|
||||
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryCreateResponse>)> {
|
||||
create_workspace_working_directory(&api, &path.workspace_id, None, request).await
|
||||
}
|
||||
|
||||
@@ -9115,7 +9094,7 @@ async fn create_workspace_working_directory(
|
||||
workspace_id: &str,
|
||||
route_runtime_id: Option<&str>,
|
||||
request: BrowserWorkingDirectoryCreateRequest,
|
||||
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryDetailResponse>)> {
|
||||
) -> ApiResult<(StatusCode, Json<BrowserWorkingDirectoryCreateResponse>)> {
|
||||
validate_workspace_scope(api, workspace_id)?;
|
||||
if let (Some(route_runtime_id), Some(request_runtime_id)) =
|
||||
(route_runtime_id, request.runtime_id.as_deref())
|
||||
@@ -9278,7 +9257,17 @@ async fn create_workspace_working_directory(
|
||||
&reserved.resolved_runtime_id,
|
||||
&reserved.working_directory_id,
|
||||
)
|
||||
.map(|response| (StatusCode::OK, response));
|
||||
.map(|Json(response)| {
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(BrowserWorkingDirectoryCreateResponse {
|
||||
workspace_id: response.workspace_id,
|
||||
runtime_id: response.runtime_id,
|
||||
item: response.item,
|
||||
diagnostics: response.diagnostics,
|
||||
}),
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
let runtime = match api
|
||||
@@ -9457,7 +9446,7 @@ async fn create_workspace_working_directory(
|
||||
apply_workdir_occupancy_projection(api, &mut summary)?;
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
Json(BrowserWorkingDirectoryDetailResponse {
|
||||
Json(BrowserWorkingDirectoryCreateResponse {
|
||||
workspace_id: workspace_id.to_string(),
|
||||
runtime_id: reserved.resolved_runtime_id,
|
||||
item: summary,
|
||||
@@ -11171,9 +11160,20 @@ async fn require_actor(api: &ServerAuthApi, headers: &HeaderMap) -> ApiResult<Re
|
||||
})
|
||||
}
|
||||
|
||||
async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<WorkspaceResponse>> {
|
||||
async fn get_workspace(
|
||||
headers: HeaderMap,
|
||||
State(api): State<WorkspaceApi>,
|
||||
) -> ApiResult<Json<WorkspaceResponse>> {
|
||||
let cookie_name = auth_public_config(&api.config).cookie_name;
|
||||
let actor = resolve_request_actor(api.store.as_ref(), &headers, &cookie_name).await?;
|
||||
let schema_version = api.store.schema_version().await?;
|
||||
let stored = api.store.get_workspace(api.workspace_id()).await?;
|
||||
let is_owner = actor.as_ref().is_some_and(|actor| {
|
||||
stored
|
||||
.as_ref()
|
||||
.and_then(|workspace| workspace.owner_account_id.as_ref())
|
||||
== Some(&actor.account_id)
|
||||
});
|
||||
let display_name = stored
|
||||
.as_ref()
|
||||
.map(|record| record.display_name.clone())
|
||||
@@ -11186,14 +11186,18 @@ async fn get_workspace(State(api): State<WorkspaceApi>) -> ApiResult<Json<Worksp
|
||||
record_authority: "local_yoi_project_records".to_string(),
|
||||
schema_version,
|
||||
auth: api.config.auth.clone(),
|
||||
extension_points: ExtensionPoints {
|
||||
permissions: WorkspacePermissionSummary {
|
||||
manage_repositories: is_owner,
|
||||
manage_secrets: is_owner,
|
||||
},
|
||||
extension_points: WorkspaceExtensionPoints {
|
||||
store: "sqlite".to_string(),
|
||||
event_stream: ExtensionPointState {
|
||||
event_stream: WorkspaceExtensionPointState {
|
||||
status: "backend_proxy".to_string(),
|
||||
note: "Worker observation streams are exposed only through the Workspace server proxy keyed by runtime_id + worker_id; browser clients never receive raw Runtime endpoints or socket paths.".to_string(),
|
||||
diagnostics: Vec::new(),
|
||||
},
|
||||
host_worker_bridge: ExtensionPointState {
|
||||
host_worker_bridge: WorkspaceExtensionPointState {
|
||||
status: "runtime_registry".to_string(),
|
||||
note: "Hosts and Workers are projected from the Workspace RuntimeRegistry; raw Runtime endpoints, sockets, and local metadata paths are not exposed.".to_string(),
|
||||
diagnostics: Vec::new(),
|
||||
@@ -11203,32 +11207,32 @@ 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."
|
||||
),
|
||||
fn companion_console_extension_point(
|
||||
status: &CompanionStatusResponse,
|
||||
) -> WorkspaceExtensionPointState {
|
||||
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,
|
||||
WorkspaceExtensionPointState {
|
||||
status: extension_status,
|
||||
note,
|
||||
diagnostics: status.diagnostics.clone(),
|
||||
}
|
||||
@@ -11313,7 +11317,7 @@ async fn list_repositories(
|
||||
workspace_id: api.config.workspace_id,
|
||||
items,
|
||||
source: "workspace-control-plane".to_string(),
|
||||
diagnostics: repository_diagnostics(diagnostics),
|
||||
diagnostics,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -11350,7 +11354,7 @@ async fn repository_log(
|
||||
default_selector,
|
||||
limit,
|
||||
items: commits,
|
||||
diagnostics: repository_diagnostics(diagnostics),
|
||||
diagnostics,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -14261,7 +14265,8 @@ fn merge_worker_registry_projection(
|
||||
.map(|workdir| {
|
||||
let mut workdir_summary = workdir_summary_from_record(workdir);
|
||||
workdir_summary.occupied_by = Some(WorkingDirectoryOccupancy {
|
||||
worker: record.worker.clone(),
|
||||
runtime_id: record.worker.runtime_id.clone(),
|
||||
worker_id: record.worker.worker_id.clone(),
|
||||
display_name: record.display_name.clone(),
|
||||
linked_at: link.linked_at.clone(),
|
||||
});
|
||||
@@ -14610,7 +14615,8 @@ fn apply_workdir_occupancy_projection(
|
||||
})?;
|
||||
summary.primary_worker_id = None;
|
||||
summary.occupied_by = Some(WorkingDirectoryOccupancy {
|
||||
worker: link.worker.clone(),
|
||||
runtime_id: link.worker.runtime_id.clone(),
|
||||
worker_id: link.worker.worker_id.clone(),
|
||||
display_name: worker.display_name,
|
||||
linked_at: link.linked_at.clone(),
|
||||
});
|
||||
@@ -15057,23 +15063,6 @@ fn sanitize_backend_error(message: &str) -> String {
|
||||
message.to_string()
|
||||
}
|
||||
|
||||
fn repository_diagnostics(
|
||||
diagnostics: Vec<crate::repositories::RepositoryDiagnostic>,
|
||||
) -> Vec<RuntimeDiagnostic> {
|
||||
diagnostics
|
||||
.into_iter()
|
||||
.map(|diagnostic| RuntimeDiagnostic {
|
||||
code: diagnostic.code,
|
||||
severity: match diagnostic.severity.as_str() {
|
||||
"error" => DiagnosticSeverity::Error,
|
||||
"warning" => DiagnosticSeverity::Warning,
|
||||
_ => DiagnosticSeverity::Info,
|
||||
},
|
||||
message: diagnostic.message,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn repository_lookup<T>(result: std::result::Result<T, RepositoryLookupError>) -> ApiResult<T> {
|
||||
result.map_err(|error| match error {
|
||||
RepositoryLookupError::UnknownRepository { id } => {
|
||||
@@ -16110,7 +16099,8 @@ mod tests {
|
||||
assert_eq!(working_directory.current_selector, None);
|
||||
assert_eq!(working_directory.current_ref.as_deref(), Some("fedcba"));
|
||||
let occupied_by = working_directory.occupied_by.as_ref().unwrap();
|
||||
assert_eq!(occupied_by.worker, RuntimeWorkerRef::new("embedded", "1"));
|
||||
assert_eq!(occupied_by.runtime_id, "embedded");
|
||||
assert_eq!(occupied_by.worker_id, "1");
|
||||
assert!(working_directory.primary_worker_id.is_none());
|
||||
let occupancy = serde_json::to_value(occupied_by).unwrap();
|
||||
assert_eq!(occupancy["runtime_id"], "embedded");
|
||||
@@ -17315,10 +17305,8 @@ mod tests {
|
||||
.find(|summary| summary.working_directory_id == "managed")
|
||||
.unwrap();
|
||||
let occupied_by = managed.occupied_by.as_ref().unwrap();
|
||||
assert_eq!(
|
||||
occupied_by.worker,
|
||||
RuntimeWorkerRef::new(EMBEDDED_WORKER_RUNTIME_ID, "7")
|
||||
);
|
||||
assert_eq!(occupied_by.runtime_id, EMBEDDED_WORKER_RUNTIME_ID);
|
||||
assert_eq!(occupied_by.worker_id, "7");
|
||||
assert_eq!(occupied_by.display_name, "Worker Seven");
|
||||
assert_eq!(occupied_by.linked_at, "3");
|
||||
|
||||
@@ -17928,6 +17916,12 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(authenticated_legacy.status(), StatusCode::OK);
|
||||
let workspace_body = to_bytes(authenticated_legacy.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let typed_workspace: workspace_api::WorkspaceResponse =
|
||||
serde_json::from_slice(&workspace_body).unwrap();
|
||||
assert!(typed_workspace.permissions.manage_repositories);
|
||||
|
||||
let anonymous_catalog = app
|
||||
.clone()
|
||||
@@ -17952,6 +17946,15 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(authenticated_catalog.status(), StatusCode::OK);
|
||||
let catalog_body = to_bytes(authenticated_catalog.into_body(), usize::MAX)
|
||||
.await
|
||||
.unwrap();
|
||||
let typed_catalog: workspace_api::WorkspaceCatalogListResponse =
|
||||
serde_json::from_slice(&catalog_body).unwrap();
|
||||
assert_eq!(
|
||||
typed_catalog.0[0].workspace_id,
|
||||
workspace.workspace.workspace_id
|
||||
);
|
||||
|
||||
for path in ["/api/workspaces", "/api/auth/device-login/approve"] {
|
||||
let cross_site = app
|
||||
@@ -23290,6 +23293,77 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn browser_workspace_workdir_create_rejects_stale_json_before_side_effects() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
init_clean_git_workspace(dir.path());
|
||||
let api = test_api(dir.path()).await;
|
||||
let token = seed_test_api_token(api.store.as_ref(), "stale-workdir-create-json");
|
||||
|
||||
let response = request_json_authenticated(
|
||||
build_router(api.clone()),
|
||||
"POST",
|
||||
&format!("/api/w/{TEST_WORKSPACE_ID}/working-directories"),
|
||||
Some(serde_json::json!({
|
||||
"runtime_id": "missing-runtime",
|
||||
"repository_id": TEST_REPOSITORY_ID,
|
||||
"selector": "HEAD",
|
||||
"operation_id": "stale-workdir-create",
|
||||
"path": "/tmp/legacy-workdir",
|
||||
})),
|
||||
&token,
|
||||
StatusCode::UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("unknown field")
|
||||
);
|
||||
assert!(
|
||||
api.config_store
|
||||
.load_workdir_create_operation(TEST_WORKSPACE_ID, "stale-workdir-create")
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn browser_workspace_workdir_create_rejects_unconfigured_repository() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
init_clean_git_workspace(dir.path());
|
||||
let api = test_api(dir.path()).await;
|
||||
let token = seed_test_api_token(api.store.as_ref(), "missing-workdir-repository");
|
||||
|
||||
let response = request_json_authenticated(
|
||||
build_router(api.clone()),
|
||||
"POST",
|
||||
&format!("/api/w/{TEST_WORKSPACE_ID}/working-directories"),
|
||||
Some(serde_json::json!({
|
||||
"runtime_id": EMBEDDED_WORKER_RUNTIME_ID,
|
||||
"repository_id": "foreign-or-missing-repository",
|
||||
"selector": "HEAD",
|
||||
"operation_id": "missing-workdir-repository",
|
||||
})),
|
||||
&token,
|
||||
StatusCode::NOT_FOUND,
|
||||
)
|
||||
.await;
|
||||
assert!(
|
||||
response["message"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("unknown local repository")
|
||||
);
|
||||
assert!(
|
||||
api.config_store
|
||||
.load_workdir_create_operation(TEST_WORKSPACE_ID, "missing-workdir-repository")
|
||||
.unwrap()
|
||||
.is_none()
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn browser_workspace_workdir_create_delegates_and_records_default_runtime_failure() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -24188,6 +24262,9 @@ mod tests {
|
||||
let workspace = get_json(app.clone(), "/api/workspace").await;
|
||||
assert_eq!(workspace["workspace_id"], TEST_WORKSPACE_ID);
|
||||
assert_eq!(workspace["display_name"], "Test Workspace");
|
||||
let typed_workspace: workspace_api::WorkspaceResponse =
|
||||
serde_json::from_value(workspace.clone()).unwrap();
|
||||
assert!(!typed_workspace.permissions.manage_repositories);
|
||||
assert_eq!(workspace["record_authority"], "local_yoi_project_records");
|
||||
assert_eq!(
|
||||
workspace["extension_points"]["host_worker_bridge"]["status"],
|
||||
@@ -24374,6 +24451,9 @@ mod tests {
|
||||
);
|
||||
|
||||
let repositories = get_json(app.clone(), "/api/repositories").await;
|
||||
let typed_repositories: workspace_api::RepositoryListResponse =
|
||||
serde_json::from_value(repositories.clone()).unwrap();
|
||||
assert_eq!(typed_repositories.items[0].id, TEST_REPOSITORY_ID);
|
||||
assert_eq!(repositories["items"][0]["id"], TEST_REPOSITORY_ID);
|
||||
assert_eq!(repositories["items"][0]["kind"], "git");
|
||||
assert_eq!(
|
||||
@@ -24387,6 +24467,8 @@ mod tests {
|
||||
);
|
||||
|
||||
let repository_detail = get_json(app.clone(), "/api/repositories/main").await;
|
||||
let _: workspace_api::RepositoryDetailResponse =
|
||||
serde_json::from_value(repository_detail.clone()).unwrap();
|
||||
assert_eq!(repository_detail["item"]["id"], TEST_REPOSITORY_ID);
|
||||
let scoped_repository_detail = get_json(
|
||||
app.clone(),
|
||||
@@ -24396,6 +24478,8 @@ mod tests {
|
||||
assert_eq!(scoped_repository_detail["item"]["id"], TEST_REPOSITORY_ID);
|
||||
|
||||
let repository_log = get_json(app.clone(), "/api/repositories/main/log?limit=3").await;
|
||||
let _: workspace_api::RepositoryLogResponse =
|
||||
serde_json::from_value(repository_log.clone()).unwrap();
|
||||
assert_eq!(repository_log["repository_id"], TEST_REPOSITORY_ID);
|
||||
assert_eq!(repository_log["default_selector"], "HEAD");
|
||||
assert_eq!(repository_log["limit"], 3);
|
||||
@@ -24460,10 +24544,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(
|
||||
@@ -24473,16 +24557,26 @@ 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 empty_window = get_json(app.clone(), "/api/companion/transcript?start=0&limit=0").await;
|
||||
assert_eq!(
|
||||
empty_window,
|
||||
json!({
|
||||
"state": "stopped",
|
||||
"start": 0,
|
||||
"limit": 0,
|
||||
"total": 0,
|
||||
"next": null,
|
||||
"items": [],
|
||||
})
|
||||
);
|
||||
|
||||
let host_workers = get_json(app.clone(), &format!("/api/hosts/{host_id}/workers")).await;
|
||||
assert!(
|
||||
@@ -24587,7 +24681,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"
|
||||
@@ -24596,12 +24690,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(
|
||||
@@ -24611,13 +24706,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!(
|
||||
@@ -25744,7 +25840,8 @@ VALUES ('0192f0e8-4d84-7d6e-a000-000000000001', 'ticket', 3);
|
||||
cleanliness: Some("clean".to_string()),
|
||||
primary_worker_id: None,
|
||||
occupied_by: Some(WorkingDirectoryOccupancy {
|
||||
worker: RuntimeWorkerRef::new("arcadia", "worker-opaque-64"),
|
||||
runtime_id: "arcadia".to_string(),
|
||||
worker_id: "worker-opaque-64".to_string(),
|
||||
display_name: "Coder".to_string(),
|
||||
linked_at: "2026-08-12T00:00:00Z".to_string(),
|
||||
}),
|
||||
|
||||
@@ -35,8 +35,8 @@ pub struct WorkspaceCreateRequest {
|
||||
pub repository: InitialRepositoryIntent,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
||||
pub struct WorkspaceCreateResponse {
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WorkspaceCreateResult {
|
||||
pub workspace: WorkspaceRecord,
|
||||
pub repository: RepositoryRecord,
|
||||
pub config_revision: u64,
|
||||
@@ -81,7 +81,7 @@ impl WorkspaceCatalogService {
|
||||
&self,
|
||||
request: WorkspaceCreateRequest,
|
||||
owner_account_id: String,
|
||||
) -> Result<WorkspaceCreateResponse> {
|
||||
) -> Result<WorkspaceCreateResult> {
|
||||
self.create_internal(request, owner_account_id, None)
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ impl WorkspaceCatalogService {
|
||||
request: WorkspaceCreateRequest,
|
||||
owner_account_id: String,
|
||||
requested_workspace_id: Option<String>,
|
||||
) -> Result<WorkspaceCreateResponse> {
|
||||
) -> Result<WorkspaceCreateResult> {
|
||||
let operation_key = normalize_required(
|
||||
"operation_key",
|
||||
request.operation_key,
|
||||
@@ -165,7 +165,7 @@ impl WorkspaceCatalogService {
|
||||
updated_at: now,
|
||||
},
|
||||
})?;
|
||||
Ok(WorkspaceCreateResponse {
|
||||
Ok(WorkspaceCreateResult {
|
||||
workspace: result.workspace,
|
||||
repository: result.repository,
|
||||
config_revision: result.config_revision,
|
||||
|
||||
Reference in New Issue
Block a user