diff --git a/.yoi/workspace.toml b/.yoi/workspace.toml new file mode 100644 index 00000000..6cfcc713 --- /dev/null +++ b/.yoi/workspace.toml @@ -0,0 +1,3 @@ +workspace_id = "0197a949-4b6b-7f2a-9d9a-1f87e3a4c5b6" +created_at = "2026-06-23T00:00:00Z" +display_name = "yoi" diff --git a/Cargo.lock b/Cargo.lock index b9102512..1909b286 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6050,6 +6050,7 @@ version = "0.1.0" dependencies = [ "async-trait", "axum", + "chrono", "manifest", "pod-store", "project-record", @@ -6061,8 +6062,10 @@ dependencies = [ "thiserror 2.0.18", "ticket", "tokio", + "toml", "tower", "tracing", + "uuid", ] [[package]] diff --git a/crates/workspace-server/Cargo.toml b/crates/workspace-server/Cargo.toml index 515bd2a4..cd147fe7 100644 --- a/crates/workspace-server/Cargo.toml +++ b/crates/workspace-server/Cargo.toml @@ -8,6 +8,7 @@ publish = false [dependencies] async-trait.workspace = true axum.workspace = true +chrono = { version = "0.4", default-features = false, features = ["clock"] } manifest = { workspace = true } pod-store = { workspace = true } project-record.workspace = true @@ -18,7 +19,9 @@ serde_yaml.workspace = true thiserror.workspace = true ticket.workspace = true tokio = { workspace = true, features = ["fs", "macros", "net", "rt-multi-thread", "sync"] } +toml.workspace = true tracing.workspace = true +uuid = { workspace = true, features = ["v7"] } [dev-dependencies] tempfile.workspace = true diff --git a/crates/workspace-server/src/identity.rs b/crates/workspace-server/src/identity.rs new file mode 100644 index 00000000..777e3536 --- /dev/null +++ b/crates/workspace-server/src/identity.rs @@ -0,0 +1,301 @@ +use std::fs; +use std::io::ErrorKind; +use std::path::{Path, PathBuf}; + +use chrono::{SecondsFormat, Utc}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use crate::{Error, Result}; + +pub const WORKSPACE_IDENTITY_RELATIVE_PATH: &str = ".yoi/workspace.toml"; + +/// Stable local Workspace identity persisted as a tracked, safe project record. +/// +/// The v0 TOML schema intentionally contains identity metadata only: +/// `workspace_id`, `created_at`, and `display_name`. Unknown fields are rejected +/// instead of preserved because this loader cannot safely round-trip future local +/// runtime settings without risking accidental path or secret persistence. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WorkspaceIdentity { + pub workspace_id: String, + pub created_at: String, + pub display_name: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +struct WorkspaceIdentityFile { + workspace_id: String, + created_at: String, + display_name: String, +} + +impl WorkspaceIdentity { + pub fn load_or_init(workspace_root: impl AsRef) -> Result { + Self::load_or_init_with_clock(workspace_root.as_ref(), || { + Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true) + }) + } + + pub fn path(workspace_root: impl AsRef) -> PathBuf { + workspace_root + .as_ref() + .join(WORKSPACE_IDENTITY_RELATIVE_PATH) + } + + pub fn parse_str(raw: &str, path: impl AsRef) -> Result { + let path = path.as_ref(); + let parsed: WorkspaceIdentityFile = toml::from_str(raw).map_err(|error| { + workspace_identity_error(path, format!("failed to parse TOML: {error}")) + })?; + Self::from_file(parsed, path) + } + + fn load_or_init_with_clock( + workspace_root: &Path, + now_utc_rfc3339: impl FnOnce() -> String, + ) -> Result { + let path = Self::path(workspace_root); + match fs::read_to_string(&path) { + Ok(raw) => Self::parse_str(&raw, &path), + Err(error) if error.kind() == ErrorKind::NotFound => { + Self::init(workspace_root, &path, now_utc_rfc3339()) + } + Err(error) => Err(Error::Io(error)), + } + } + + fn init(workspace_root: &Path, path: &Path, created_at: String) -> Result { + if path.exists() { + let raw = fs::read_to_string(path)?; + return Self::parse_str(&raw, path); + } + validate_created_at(&created_at, path)?; + let display_name = workspace_display_name_from_root(workspace_root, path)?; + let workspace_id = Uuid::now_v7().to_string(); + let identity = Self { + workspace_id, + created_at, + display_name, + }; + identity.write_new(path)?; + Ok(identity) + } + + fn from_file(parsed: WorkspaceIdentityFile, path: &Path) -> Result { + let workspace_id = validate_workspace_id(&parsed.workspace_id, path)?; + validate_created_at(&parsed.created_at, path)?; + validate_display_name(&parsed.display_name, path)?; + Ok(Self { + workspace_id, + created_at: parsed.created_at, + display_name: parsed.display_name, + }) + } + + fn write_new(&self, path: &Path) -> Result<()> { + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + let raw = toml::to_string_pretty(&WorkspaceIdentityFile { + workspace_id: self.workspace_id.clone(), + created_at: self.created_at.clone(), + display_name: self.display_name.clone(), + }) + .map_err(|error| { + workspace_identity_error(path, format!("failed to encode TOML: {error}")) + })?; + let tmp = path.with_extension("toml.tmp"); + fs::write(&tmp, raw)?; + if path.exists() { + let _ = fs::remove_file(&tmp); + let raw = fs::read_to_string(path)?; + return Self::parse_str(&raw, path).map(|_| ()); + } + fs::rename(tmp, path)?; + Ok(()) + } +} + +fn validate_workspace_id(value: &str, path: &Path) -> Result { + let uuid = Uuid::parse_str(value).map_err(|error| { + workspace_identity_error(path, format!("workspace_id is not a UUID: {error}")) + })?; + if uuid.get_version_num() != 7 { + return Err(workspace_identity_error( + path, + "workspace_id must be a UUIDv7 canonical string".to_string(), + )); + } + let canonical = uuid.to_string(); + if value != canonical { + return Err(workspace_identity_error( + path, + "workspace_id must use lowercase hyphenated UUID canonical form".to_string(), + )); + } + Ok(canonical) +} + +fn validate_created_at(value: &str, path: &Path) -> Result<()> { + let parsed = chrono::DateTime::parse_from_rfc3339(value).map_err(|error| { + workspace_identity_error(path, format!("created_at is not RFC3339: {error}")) + })?; + if parsed.offset().local_minus_utc() != 0 || !value.ends_with('Z') { + return Err(workspace_identity_error( + path, + "created_at must be a UTC RFC3339 timestamp ending in Z".to_string(), + )); + } + Ok(()) +} + +fn validate_display_name(value: &str, path: &Path) -> Result<()> { + if value.trim().is_empty() { + return Err(workspace_identity_error( + path, + "display_name must not be empty".to_string(), + )); + } + if value.contains('\0') || value.chars().any(|ch| ch.is_control()) { + return Err(workspace_identity_error( + path, + "display_name must not contain control characters".to_string(), + )); + } + Ok(()) +} + +fn workspace_display_name_from_root(workspace_root: &Path, path: &Path) -> Result { + let display_name = workspace_root + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| { + workspace_identity_error( + path, + "workspace root must have a UTF-8 final path component".to_string(), + ) + })? + .to_string(); + validate_display_name(&display_name, path)?; + Ok(display_name) +} + +fn workspace_identity_error(path: &Path, message: String) -> Error { + Error::WorkspaceIdentity(format!("{}: {message}", path.display())) +} + +#[cfg(test)] +mod tests { + use super::*; + + const FIXED_WORKSPACE_ID: &str = "0192f0e8-4d84-7d6e-a000-000000000001"; + const FIXED_CREATED_AT: &str = "2026-06-23T06:43:28Z"; + + #[test] + fn missing_identity_file_is_created_with_safe_fields() { + let temp = tempfile::tempdir().unwrap(); + let workspace_root = temp.path().join("example-workspace"); + fs::create_dir_all(&workspace_root).unwrap(); + + let identity = WorkspaceIdentity::load_or_init_with_clock(&workspace_root, || { + FIXED_CREATED_AT.to_string() + }) + .unwrap(); + + assert_eq!(identity.display_name, "example-workspace"); + assert_eq!(identity.created_at, FIXED_CREATED_AT); + validate_workspace_id( + &identity.workspace_id, + &WorkspaceIdentity::path(&workspace_root), + ) + .unwrap(); + + let raw = fs::read_to_string(WorkspaceIdentity::path(&workspace_root)).unwrap(); + assert!(raw.contains("workspace_id")); + assert!(raw.contains("display_name")); + assert!(raw.contains("created_at")); + assert!(!raw.contains(&workspace_root.to_string_lossy().to_string())); + + let reloaded = WorkspaceIdentity::load_or_init_with_clock(&workspace_root, || { + "2026-06-24T00:00:00Z".to_string() + }) + .unwrap(); + assert_eq!(reloaded, identity); + } + + #[test] + fn existing_identity_file_is_stable() { + let temp = tempfile::tempdir().unwrap(); + let workspace_root = temp.path().join("moved-workspace"); + let yoi_dir = workspace_root.join(".yoi"); + fs::create_dir_all(&yoi_dir).unwrap(); + let path = yoi_dir.join("workspace.toml"); + let raw = format!( + "workspace_id = \"{FIXED_WORKSPACE_ID}\"\ncreated_at = \"{FIXED_CREATED_AT}\"\ndisplay_name = \"Stable Project\"\n" + ); + fs::write(&path, &raw).unwrap(); + + let identity = WorkspaceIdentity::load_or_init_with_clock(&workspace_root, || { + "2026-06-24T00:00:00Z".to_string() + }) + .unwrap(); + + assert_eq!(identity.workspace_id, FIXED_WORKSPACE_ID); + assert_eq!(identity.created_at, FIXED_CREATED_AT); + assert_eq!(identity.display_name, "Stable Project"); + assert_eq!(fs::read_to_string(path).unwrap(), raw); + } + + #[test] + fn invalid_identity_file_fails_closed_without_rewriting() { + let temp = tempfile::tempdir().unwrap(); + let workspace_root = temp.path().join("invalid-workspace"); + let yoi_dir = workspace_root.join(".yoi"); + fs::create_dir_all(&yoi_dir).unwrap(); + let path = yoi_dir.join("workspace.toml"); + let raw = "workspace_id = \"not-a-uuid\"\ncreated_at = \"2026-06-23T06:43:28Z\"\ndisplay_name = \"Invalid\"\n"; + fs::write(&path, raw).unwrap(); + + let error = WorkspaceIdentity::load_or_init_with_clock(&workspace_root, || { + FIXED_CREATED_AT.to_string() + }) + .unwrap_err(); + + assert!(error.to_string().contains("workspace_id is not a UUID")); + assert_eq!(fs::read_to_string(path).unwrap(), raw); + } + + #[test] + fn generated_identity_does_not_leak_parent_paths() { + let temp = tempfile::tempdir().unwrap(); + let secret_parent = temp.path().join("user-secret-parent"); + let workspace_root = secret_parent.join("public-project-name"); + fs::create_dir_all(&workspace_root).unwrap(); + + WorkspaceIdentity::load_or_init_with_clock(&workspace_root, || { + FIXED_CREATED_AT.to_string() + }) + .unwrap(); + let raw = fs::read_to_string(WorkspaceIdentity::path(&workspace_root)).unwrap(); + + assert!(raw.contains("public-project-name")); + assert!(!raw.contains(&secret_parent.to_string_lossy().to_string())); + assert!(!raw.contains("user-secret-parent")); + assert!(!raw.contains("/")); + } + + #[test] + fn unknown_fields_are_rejected() { + let temp = tempfile::tempdir().unwrap(); + let path = temp.path().join("workspace.toml"); + let raw = format!( + "workspace_id = \"{FIXED_WORKSPACE_ID}\"\ncreated_at = \"{FIXED_CREATED_AT}\"\ndisplay_name = \"Stable Project\"\nlocal_root = \"/tmp/secret\"\n" + ); + + let error = WorkspaceIdentity::parse_str(&raw, &path).unwrap_err(); + + assert!(error.to_string().contains("unknown field")); + } +} diff --git a/crates/workspace-server/src/lib.rs b/crates/workspace-server/src/lib.rs index 1e538396..8d3b9f69 100644 --- a/crates/workspace-server/src/lib.rs +++ b/crates/workspace-server/src/lib.rs @@ -5,11 +5,13 @@ //! remain the canonical project records and are read through bounded bridge APIs. pub mod hosts; +pub mod identity; pub mod records; pub mod repositories; pub mod server; pub mod store; +pub use identity::{WORKSPACE_IDENTITY_RELATIVE_PATH, WorkspaceIdentity}; pub use records::{ LocalProjectRecordReader, ObjectiveDetail, ObjectiveSummary, TicketDetail, TicketSummary, }; @@ -40,6 +42,8 @@ pub enum Error { UnknownHost(String), #[error("unknown local repository `{0}`")] UnknownRepository(String), + #[error("workspace identity error: {0}")] + WorkspaceIdentity(String), #[error("store error: {0}")] Store(String), } diff --git a/crates/workspace-server/src/main.rs b/crates/workspace-server/src/main.rs index f1591014..4f9fc546 100644 --- a/crates/workspace-server/src/main.rs +++ b/crates/workspace-server/src/main.rs @@ -4,7 +4,7 @@ use std::process::ExitCode; use std::sync::Arc; use tokio::net::TcpListener; -use yoi_workspace_server::{ServerConfig, SqliteWorkspaceStore, serve}; +use yoi_workspace_server::{ServerConfig, SqliteWorkspaceStore, WorkspaceIdentity, serve}; #[derive(Debug)] struct ServeOptions { @@ -64,6 +64,7 @@ async fn run() -> Result<(), Box> { } async fn run_serve(options: ServeOptions) -> Result<(), Box> { + let identity = WorkspaceIdentity::load_or_init(&options.workspace)?; let db = options .db .unwrap_or_else(|| options.workspace.join(".yoi/workspace.db")); @@ -72,7 +73,7 @@ async fn run_serve(options: ServeOptions) -> Result<(), Box) -> Self { + pub fn new(workspace_root: impl Into, workspace_id: impl Into) -> Self { Self { workspace_root: workspace_root.into(), + workspace_id: workspace_id.into(), } } @@ -30,7 +33,7 @@ impl LocalRepositoryReader { pub fn summary(&self, workspace_display_name: &str) -> RepositorySummary { let git = inspect_git(&self.workspace_root); RepositorySummary { - id: LOCAL_REPOSITORY_ID.to_string(), + id: Self::repository_id_for_workspace(&self.workspace_id), display_name: workspace_display_name.to_string(), kind: "local".to_string(), workspace_root: self.workspace_root.clone(), @@ -46,8 +49,42 @@ impl LocalRepositoryReader { git_log(&self.workspace_root, limit) } - pub fn is_local_repository_id(id: &str) -> bool { - id == LOCAL_REPOSITORY_ID + pub fn repository_id_for_workspace(workspace_id: &str) -> String { + format!( + "{LOCAL_REPOSITORY_PREFIX}{}", + sanitize_identifier_fragment(workspace_id) + ) + } + + pub fn is_local_repository_id(id: &str, workspace_id: &str) -> bool { + id == LEGACY_LOCAL_REPOSITORY_ID || id == Self::repository_id_for_workspace(workspace_id) + } +} + +fn sanitize_identifier_fragment(value: &str) -> String { + let mut output = String::with_capacity(value.len()); + let mut previous_dash = false; + for ch in value.chars() { + let mapped = if ch.is_ascii_alphanumeric() { + ch.to_ascii_lowercase() + } else { + '-' + }; + if mapped == '-' { + if !previous_dash { + output.push(mapped); + } + previous_dash = true; + } else { + output.push(mapped); + previous_dash = false; + } + } + let output = output.trim_matches('-').to_string(); + if output.is_empty() { + "workspace".to_string() + } else { + output } } diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index 6aada3ab..7c9e786d 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -11,6 +11,7 @@ use serde::{Deserialize, Serialize}; use tokio::net::TcpListener; use crate::hosts::{HostSummary, LocalRuntimeBridge, RuntimeDiagnostic, WorkerSummary}; +use crate::identity::WorkspaceIdentity; use crate::records::{ LocalProjectRecordReader, ObjectiveDetail, ProjectRecordList, TicketDetail, TicketSummary, }; @@ -28,6 +29,8 @@ pub enum AuthConfig { #[derive(Clone)] pub struct ServerConfig { pub workspace_id: String, + pub workspace_display_name: String, + pub workspace_created_at: String, pub workspace_root: PathBuf, pub static_assets_dir: Option, pub auth: AuthConfig, @@ -36,11 +39,12 @@ pub struct ServerConfig { } impl ServerConfig { - pub fn local_dev(workspace_root: impl Into) -> Self { + pub fn local_dev(workspace_root: impl Into, identity: WorkspaceIdentity) -> Self { let workspace_root = workspace_root.into(); - let display = workspace_display_name_from_root(&workspace_root); Self { - workspace_id: format!("local:{display}"), + workspace_id: identity.workspace_id, + workspace_display_name: identity.display_name, + workspace_created_at: identity.created_at, workspace_root, static_assets_dir: None, auth: AuthConfig::LocalDevToken { @@ -61,14 +65,13 @@ pub struct WorkspaceApi { impl WorkspaceApi { pub async fn new(config: ServerConfig, store: Arc) -> Result { - let display_name = workspace_display_name_from_root(&config.workspace_root); store .upsert_workspace(&WorkspaceRecord { workspace_id: config.workspace_id.clone(), - display_name, + display_name: config.workspace_display_name.clone(), state: "active".to_string(), - created_at: "1970-01-01T00:00:00Z".to_string(), - updated_at: "1970-01-01T00:00:00Z".to_string(), + created_at: config.workspace_created_at.clone(), + updated_at: config.workspace_created_at.clone(), }) .await?; Ok(Self { @@ -91,20 +94,19 @@ impl WorkspaceApi { } fn local_repository_reader(&self) -> LocalRepositoryReader { - LocalRepositoryReader::new(self.config.workspace_root.clone()) + LocalRepositoryReader::new( + self.config.workspace_root.clone(), + self.config.workspace_id.clone(), + ) } - fn workspace_display_name(&self) -> String { - workspace_display_name_from_root(&self.config.workspace_root) + fn local_repository_id(&self) -> String { + LocalRepositoryReader::repository_id_for_workspace(self.workspace_id()) } -} -fn workspace_display_name_from_root(workspace_root: &std::path::Path) -> String { - workspace_root - .file_name() - .and_then(|name| name.to_str()) - .expect("workspace root must have a final path component") - .to_string() + fn workspace_display_name(&self) -> &str { + self.config.workspace_display_name.as_str() + } } pub fn build_router(api: WorkspaceApi) -> Router { @@ -238,7 +240,7 @@ async fn get_workspace(State(api): State) -> ApiResult, ) -> ApiResult> { let reader = api.local_repository_reader(); - let items = reader.list(&api.workspace_display_name()); + let items = reader.list(api.workspace_display_name()); Ok(Json(RepositoryListResponse { workspace_id: api.config.workspace_id, items, @@ -327,11 +329,11 @@ async fn repository_detail( State(api): State, AxumPath(repository_id): AxumPath, ) -> ApiResult> { - ensure_local_repository(&repository_id)?; + let _canonical_repository_id = ensure_local_repository(&api, &repository_id)?; let reader = api.local_repository_reader(); Ok(Json(RepositoryDetailResponse { workspace_id: api.config.workspace_id.clone(), - item: reader.summary(&api.workspace_display_name()), + item: reader.summary(api.workspace_display_name()), source: "local_workspace_root".to_string(), })) } @@ -341,7 +343,7 @@ async fn repository_log( AxumPath(repository_id): AxumPath, Query(query): Query, ) -> ApiResult> { - ensure_local_repository(&repository_id)?; + let canonical_repository_id = ensure_local_repository(&api, &repository_id)?; let RepositoryLogRead { limit, items, @@ -349,7 +351,7 @@ async fn repository_log( } = api.local_repository_reader().recent_log(query.limit); Ok(Json(RepositoryLogResponse { workspace_id: api.config.workspace_id, - repository_id, + repository_id: canonical_repository_id, limit, items, diagnostics, @@ -361,7 +363,7 @@ async fn repository_tickets( AxumPath(repository_id): AxumPath, Query(query): Query, ) -> ApiResult> { - ensure_local_repository(&repository_id)?; + let canonical_repository_id = ensure_local_repository(&api, &repository_id)?; let limit = query.limit.unwrap_or(api.config.max_records).min(200); let ProjectRecordList { items, @@ -370,7 +372,7 @@ async fn repository_tickets( } = api.records.list_tickets(limit)?; Ok(Json(RepositoryTicketsResponse { workspace_id: api.config.workspace_id, - repository_id, + repository_id: canonical_repository_id, limit, columns: ticket_kanban_columns(items), invalid_records, @@ -429,9 +431,10 @@ fn workers_response(api: WorkspaceApi) -> ApiResult Result<()> { - if LocalRepositoryReader::is_local_repository_id(repository_id) { - Ok(()) +fn ensure_local_repository(api: &WorkspaceApi, repository_id: &str) -> Result { + let canonical_repository_id = api.local_repository_id(); + if LocalRepositoryReader::is_local_repository_id(repository_id, api.workspace_id()) { + Ok(canonical_repository_id) } else { Err(Error::UnknownRepository(repository_id.to_string())) } @@ -612,6 +615,18 @@ mod tests { use crate::store::SqliteWorkspaceStore; + const TEST_WORKSPACE_ID: &str = "0192f0e8-4d84-7d6e-a000-000000000001"; + const TEST_REPOSITORY_ID: &str = "local-0192f0e8-4d84-7d6e-a000-000000000001"; + const TEST_CREATED_AT: &str = "2026-06-23T06:43:28Z"; + + fn test_identity() -> WorkspaceIdentity { + WorkspaceIdentity { + workspace_id: TEST_WORKSPACE_ID.to_string(), + display_name: "Test Workspace".to_string(), + created_at: TEST_CREATED_AT.to_string(), + } + } + #[tokio::test] async fn serves_bounded_read_apis_and_static_spa_separately() { let dir = tempfile::tempdir().unwrap(); @@ -623,15 +638,15 @@ mod tests { std::fs::write(static_dir.join("assets/app.js"), "console.log('yoi');").unwrap(); let store = SqliteWorkspaceStore::in_memory().unwrap(); - let mut config = ServerConfig::local_dev(dir.path()); - config.workspace_id = "local:test".to_string(); + let mut config = ServerConfig::local_dev(dir.path(), test_identity()); config.static_assets_dir = Some(static_dir); config.local_runtime_data_dir = Some(dir.path().join("data")); let api = WorkspaceApi::new(config, Arc::new(store)).await.unwrap(); let app = build_router(api); let workspace = get_json(app.clone(), "/api/workspace").await; - assert_eq!(workspace["workspace_id"], "local:test"); + assert_eq!(workspace["workspace_id"], TEST_WORKSPACE_ID); + assert_eq!(workspace["display_name"], "Test Workspace"); assert_eq!(workspace["record_authority"], "local_yoi_project_records"); assert_eq!( workspace["extension_points"]["host_worker_bridge"]["status"], @@ -647,18 +662,18 @@ mod tests { assert_eq!(objectives["items"][0]["summary"], "Objective body."); let repositories = get_json(app.clone(), "/api/repositories").await; - assert_eq!(repositories["items"][0]["id"], "local"); + assert_eq!(repositories["items"][0]["id"], TEST_REPOSITORY_ID); assert_eq!(repositories["items"][0]["kind"], "local"); let repository_detail = get_json(app.clone(), "/api/repositories/local").await; - assert_eq!(repository_detail["item"]["id"], "local"); + assert_eq!(repository_detail["item"]["id"], TEST_REPOSITORY_ID); let repository_log = get_json(app.clone(), "/api/repositories/local/log?limit=3").await; - assert_eq!(repository_log["repository_id"], "local"); + assert_eq!(repository_log["repository_id"], TEST_REPOSITORY_ID); assert_eq!(repository_log["limit"], 3); let repository_tickets = get_json(app.clone(), "/api/repositories/local/tickets").await; - assert_eq!(repository_tickets["repository_id"], "local"); + assert_eq!(repository_tickets["repository_id"], TEST_REPOSITORY_ID); let ready_column = repository_tickets["columns"] .as_array() .unwrap() @@ -684,7 +699,7 @@ mod tests { assert_eq!(unknown_repository_response.status(), StatusCode::NOT_FOUND); let hosts = get_json(app.clone(), "/api/hosts").await; - assert_eq!(hosts["items"][0]["host_id"], "local-local-test"); + assert_eq!(hosts["items"][0]["host_id"], TEST_REPOSITORY_ID); assert_eq!(hosts["items"][0]["kind"], "local_host"); assert_eq!( hosts["items"][0]["capabilities"]["local_pod_inspection"], @@ -698,7 +713,11 @@ mod tests { "local_pod_metadata_root_missing" ); - let host_workers = get_json(app.clone(), "/api/hosts/local-local-test/workers").await; + let host_workers = get_json( + app.clone(), + &format!("/api/hosts/{TEST_REPOSITORY_ID}/workers"), + ) + .await; assert!(host_workers["items"].as_array().unwrap().is_empty()); let runs_response = app diff --git a/package.nix b/package.nix index f7f10655..59e02b00 100644 --- a/package.nix +++ b/package.nix @@ -43,7 +43,7 @@ rustPlatform.buildRustPackage rec { filter = sourceFilter; }; - cargoHash = "sha256-M8cGY+eskFXSRjq3kBbRusflghvVKrWc1Pj50uKAlg8="; + cargoHash = "sha256-XZxqEKKDU42fFjFnCCcRRFTA0jkkiaSn3eQ8QwXRYPk="; depsExtraArgs = { # Older fetchCargoVendor utilities used crates.io's API download endpoint,