server: use single XDG server database
This commit is contained in:
@@ -378,7 +378,7 @@ impl WorkspaceBackendConfigFile {
|
||||
.workspace_database_path
|
||||
.as_ref()
|
||||
.map(|path| resolve_workspace_path(workspace_root, path))
|
||||
.unwrap_or_else(|| data_root.join("workspace.db"));
|
||||
.unwrap_or_else(ServerConfig::default_server_database_path);
|
||||
let embedded_runtime_store_root = self
|
||||
.data
|
||||
.embedded_runtime_store_root
|
||||
@@ -570,7 +570,7 @@ mod tests {
|
||||
assert_eq!(resolved.listen, "127.0.0.1:8787".parse().unwrap());
|
||||
assert_eq!(resolved.server.frontend_url, DEFAULT_FRONTEND_URL);
|
||||
assert_eq!(resolved.server.max_records, DEFAULT_MAX_RECORDS);
|
||||
assert!(resolved.database_path.ends_with("workspace.db"));
|
||||
assert!(resolved.database_path.ends_with("server.db"));
|
||||
assert!(
|
||||
resolved
|
||||
.server
|
||||
@@ -643,7 +643,7 @@ embedded_runtime_store_root = "/tmp/yoi-runtime"
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_root_derives_database_and_runtime_store_paths() {
|
||||
fn data_root_derives_runtime_store_path_only() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let config = WorkspaceBackendConfigFile::parse_str(
|
||||
r#"
|
||||
@@ -655,10 +655,7 @@ root = ".local-data"
|
||||
.unwrap();
|
||||
let resolved = config.resolve(dir.path(), identity()).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
resolved.database_path,
|
||||
dir.path().join(".local-data/workspace.db")
|
||||
);
|
||||
assert!(resolved.database_path.ends_with("server.db"));
|
||||
assert_eq!(
|
||||
resolved.server.embedded_runtime_store_root,
|
||||
dir.path().join(".local-data/embedded-runtime")
|
||||
|
||||
+159
-103
@@ -4,9 +4,11 @@ use std::process::ExitCode;
|
||||
use std::sync::Arc;
|
||||
|
||||
use tokio::net::TcpListener;
|
||||
use yoi_workspace_server::store::RepositoryRecord;
|
||||
use yoi_workspace_server::{
|
||||
BackendRuntimesConfigFile, SqliteWorkspaceStore, WORKSPACE_BACKEND_CONFIG_TEMPLATE,
|
||||
WorkspaceBackendConfigFile, WorkspaceIdentity, serve,
|
||||
BackendRuntimesConfigFile, ControlPlaneStore, ServerConfig, SqliteWorkspaceStore,
|
||||
WORKSPACE_BACKEND_CONFIG_TEMPLATE, WorkspaceBackendConfigFile, WorkspaceIdentity,
|
||||
WorkspaceRecord, serve,
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -21,9 +23,6 @@ enum Command {
|
||||
|
||||
#[derive(Debug)]
|
||||
struct ServeOptions {
|
||||
workspace: PathBuf,
|
||||
db: Option<PathBuf>,
|
||||
frontend: Option<PathBuf>,
|
||||
listen: Option<SocketAddr>,
|
||||
}
|
||||
|
||||
@@ -70,7 +69,7 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = std::env::args().skip(1).collect::<Vec<_>>();
|
||||
match parse_command(&args)? {
|
||||
Command::Serve(options) => run_serve(options).await,
|
||||
Command::Init(options) => run_init(options),
|
||||
Command::Init(options) => run_init(options).await,
|
||||
Command::ConfigDefault => run_config_default(),
|
||||
Command::ConfigDiff(options) => run_config_diff(options),
|
||||
Command::Skills(command) => run_skills(command),
|
||||
@@ -111,13 +110,50 @@ fn parse_command(args: &[String]) -> Result<Command, CliError> {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_init(options: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
||||
async fn run_init(options: InitOptions) -> Result<(), Box<dyn std::error::Error>> {
|
||||
run_init_with_database_path(options, ServerConfig::default_server_database_path()).await
|
||||
}
|
||||
|
||||
async fn run_init_with_database_path(
|
||||
options: InitOptions,
|
||||
database_path: PathBuf,
|
||||
) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let identity = WorkspaceIdentity::load_or_init(&options.workspace)?;
|
||||
WorkspaceBackendConfigFile::ensure_local_config_for_workspace(&options.workspace)?;
|
||||
|
||||
if let Some(parent) = database_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
let store = SqliteWorkspaceStore::open(&database_path)?;
|
||||
store
|
||||
.upsert_workspace(&WorkspaceRecord {
|
||||
workspace_id: identity.workspace_id.clone(),
|
||||
owner_account_id: None,
|
||||
display_name: identity.display_name.clone(),
|
||||
state: "active".to_string(),
|
||||
created_at: identity.created_at.clone(),
|
||||
updated_at: identity.created_at.clone(),
|
||||
})
|
||||
.await?;
|
||||
store.upsert_repository(&RepositoryRecord {
|
||||
workspace_id: identity.workspace_id.clone(),
|
||||
repository_id: "main".to_string(),
|
||||
name: "Main repository".to_string(),
|
||||
kind: "git".to_string(),
|
||||
provider: Some("git".to_string()),
|
||||
uri: options.workspace.display().to_string(),
|
||||
default_ref: Some("HEAD".to_string()),
|
||||
auth_ref_kind: None,
|
||||
auth_ref_key: None,
|
||||
created_at: identity.created_at.clone(),
|
||||
updated_at: identity.created_at.clone(),
|
||||
})?;
|
||||
|
||||
eprintln!(
|
||||
"yoi-workspace-server: initialized workspace `{}` ({})",
|
||||
"yoi-workspace-server: initialized workspace `{}` ({}) in server DB `{}`",
|
||||
options.workspace.display(),
|
||||
identity.workspace_id
|
||||
identity.workspace_id,
|
||||
database_path.display()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -167,36 +203,90 @@ fn run_skills(command: SkillsCommand) -> Result<(), Box<dyn std::error::Error>>
|
||||
}
|
||||
|
||||
async fn run_serve(options: ServeOptions) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let identity = WorkspaceIdentity::load_required(&options.workspace)?;
|
||||
let config_file = WorkspaceBackendConfigFile::load_for_workspace(&options.workspace)?;
|
||||
let database_path = ServerConfig::default_server_database_path();
|
||||
if let Some(parent) = database_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let store = Arc::new(SqliteWorkspaceStore::open(&database_path)?);
|
||||
let workspace = select_serve_workspace(store.as_ref())?;
|
||||
let workspace_root = infer_workspace_root_from_repositories(store.as_ref(), &workspace)?;
|
||||
let identity = WorkspaceIdentity {
|
||||
workspace_id: workspace.workspace_id.clone(),
|
||||
created_at: workspace.created_at.clone(),
|
||||
display_name: workspace.display_name.clone(),
|
||||
};
|
||||
let runtime_config = BackendRuntimesConfigFile::load_default()?;
|
||||
let mut resolved =
|
||||
config_file.resolve_with_runtime_config(&options.workspace, identity, &runtime_config)?;
|
||||
if let Some(db) = options.db {
|
||||
resolved = resolved.with_database_path(db);
|
||||
}
|
||||
if let Some(frontend) = options.frontend {
|
||||
resolved = resolved.with_static_assets_dir(Some(frontend));
|
||||
}
|
||||
let mut resolved = WorkspaceBackendConfigFile::default().resolve_with_runtime_config(
|
||||
&workspace_root,
|
||||
identity,
|
||||
&runtime_config,
|
||||
)?;
|
||||
resolved.database_path = database_path.clone();
|
||||
resolved.server.database_path = database_path.clone();
|
||||
if let Some(listen) = options.listen {
|
||||
resolved = resolved.with_listen(listen);
|
||||
}
|
||||
|
||||
if let Some(parent) = resolved.database_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let store = Arc::new(SqliteWorkspaceStore::open(&resolved.database_path)?);
|
||||
let listener = TcpListener::bind(resolved.listen).await?;
|
||||
eprintln!(
|
||||
"yoi-workspace-server: serving workspace `{}` on http://{}",
|
||||
options.workspace.display(),
|
||||
"yoi-workspace-server: serving workspace `{}` from server DB `{}` on http://{}",
|
||||
workspace.workspace_id,
|
||||
database_path.display(),
|
||||
listener.local_addr()?
|
||||
);
|
||||
serve(resolved.server, store, listener).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn select_serve_workspace(store: &SqliteWorkspaceStore) -> Result<WorkspaceRecord, CliError> {
|
||||
let workspaces = store
|
||||
.list_workspaces()
|
||||
.map_err(|error| CliError(format!("failed to list workspaces from server DB: {error}")))?;
|
||||
match workspaces.as_slice() {
|
||||
[] => Err(CliError(
|
||||
"server DB has no workspace records; run `yoi-workspace-server init --workspace <PATH>`".to_string(),
|
||||
)),
|
||||
[workspace] => Ok(workspace.clone()),
|
||||
_ => Err(CliError(format!(
|
||||
"server DB contains {} workspaces; serve workspace selection is not implemented yet",
|
||||
workspaces.len()
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_workspace_root_from_repositories(
|
||||
store: &SqliteWorkspaceStore,
|
||||
workspace: &WorkspaceRecord,
|
||||
) -> Result<PathBuf, CliError> {
|
||||
let repositories = store
|
||||
.list_repositories(&workspace.workspace_id)
|
||||
.map_err(|error| {
|
||||
CliError(format!(
|
||||
"failed to list repositories from server DB: {error}"
|
||||
))
|
||||
})?;
|
||||
let Some(repository) = repositories
|
||||
.iter()
|
||||
.find(|repository| repository.repository_id == "main")
|
||||
.or_else(|| repositories.first())
|
||||
else {
|
||||
return Err(CliError(format!(
|
||||
"workspace `{}` has no repository records; cannot derive a workspace root",
|
||||
workspace.workspace_id
|
||||
)));
|
||||
};
|
||||
|
||||
let repository_path = PathBuf::from(&repository.uri);
|
||||
if !repository_path.is_absolute() {
|
||||
return Err(CliError(format!(
|
||||
"repository `{}` has relative URI `{}`; repository records used by serve must be absolute paths",
|
||||
repository.repository_id, repository.uri
|
||||
)));
|
||||
}
|
||||
Ok(repository_path)
|
||||
}
|
||||
|
||||
fn parse_config_command(args: &[String]) -> Result<Command, CliError> {
|
||||
let Some((subcommand, rest)) = args.split_first() else {
|
||||
print_config_help();
|
||||
@@ -313,36 +403,12 @@ fn parse_init_options(args: &[String]) -> Result<InitOptions, CliError> {
|
||||
}
|
||||
|
||||
fn parse_serve_options(args: &[String]) -> Result<ServeOptions, CliError> {
|
||||
let mut workspace = None;
|
||||
let mut db = None;
|
||||
let mut frontend = None;
|
||||
let mut listen = None;
|
||||
|
||||
let mut index = 0;
|
||||
while index < args.len() {
|
||||
let arg = &args[index];
|
||||
match arg.as_str() {
|
||||
"--workspace" => {
|
||||
index += 1;
|
||||
let value = args
|
||||
.get(index)
|
||||
.ok_or_else(|| CliError("--workspace requires a value".to_string()))?;
|
||||
workspace = Some(PathBuf::from(value));
|
||||
}
|
||||
"--db" => {
|
||||
index += 1;
|
||||
let value = args
|
||||
.get(index)
|
||||
.ok_or_else(|| CliError("--db requires a value".to_string()))?;
|
||||
db = Some(PathBuf::from(value));
|
||||
}
|
||||
"--frontend" => {
|
||||
index += 1;
|
||||
let value = args
|
||||
.get(index)
|
||||
.ok_or_else(|| CliError("--frontend requires a value".to_string()))?;
|
||||
frontend = Some(PathBuf::from(value));
|
||||
}
|
||||
"--listen" => {
|
||||
index += 1;
|
||||
let value = args
|
||||
@@ -350,15 +416,6 @@ fn parse_serve_options(args: &[String]) -> Result<ServeOptions, CliError> {
|
||||
.ok_or_else(|| CliError("--listen requires a value".to_string()))?;
|
||||
listen = Some(parse_listen(value)?);
|
||||
}
|
||||
_ if arg.starts_with("--workspace=") => {
|
||||
workspace = Some(PathBuf::from(value_after_equals(arg, "--workspace")?));
|
||||
}
|
||||
_ if arg.starts_with("--db=") => {
|
||||
db = Some(PathBuf::from(value_after_equals(arg, "--db")?));
|
||||
}
|
||||
_ if arg.starts_with("--frontend=") => {
|
||||
frontend = Some(PathBuf::from(value_after_equals(arg, "--frontend")?));
|
||||
}
|
||||
_ if arg.starts_with("--listen=") => {
|
||||
listen = Some(parse_listen(value_after_equals(arg, "--listen")?)?);
|
||||
}
|
||||
@@ -367,28 +424,14 @@ fn parse_serve_options(args: &[String]) -> Result<ServeOptions, CliError> {
|
||||
}
|
||||
_ => {
|
||||
return Err(CliError(format!(
|
||||
"unexpected positional argument `{arg}`; use --workspace <PATH>"
|
||||
"unexpected positional argument `{arg}`; serve reads the workspace from the server DB"
|
||||
)));
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
let workspace = workspace
|
||||
.ok_or_else(|| CliError("serve requires --workspace <path>; the current directory is no longer used as an implicit workspace".to_string()))?;
|
||||
let workspace = workspace.canonicalize().map_err(|error| {
|
||||
CliError(format!(
|
||||
"failed to canonicalize workspace `{}`: {error}",
|
||||
workspace.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
Ok(ServeOptions {
|
||||
workspace,
|
||||
db,
|
||||
frontend,
|
||||
listen,
|
||||
})
|
||||
Ok(ServeOptions { listen })
|
||||
}
|
||||
|
||||
fn value_after_equals<'a>(arg: &'a str, flag: &str) -> Result<&'a str, CliError> {
|
||||
@@ -416,7 +459,7 @@ fn print_help() {
|
||||
|
||||
fn print_init_help() {
|
||||
println!(
|
||||
"yoi-workspace-server init\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n\nDescription:\n Initializes a Workspace identity and copies the packaged Backend config template to .yoi/workspace-backend.local.toml. Does not create Backend data stores.\n\nOptions:\n --workspace <PATH> Workspace root to initialize (defaults to cwd)\n -h, --help Print help"
|
||||
"yoi-workspace-server init\n\nUsage:\n yoi-workspace-server init [OPTIONS]\n\nDescription:\n Initializes a Workspace identity, copies the packaged Backend config template to .yoi/workspace-backend.local.toml, and registers the Workspace in the Yoi server DB.\n\nOptions:\n --workspace <PATH> Workspace root to initialize (defaults to cwd)\n -h, --help Print help"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -434,7 +477,7 @@ fn print_skills_help() {
|
||||
|
||||
fn print_serve_help() {
|
||||
println!(
|
||||
"yoi-workspace-server serve\n\nUsage:\n yoi-workspace-server serve --workspace <PATH> [OPTIONS]\n\nDescription:\n Serves an already initialized Workspace. Run `yoi workspace init --workspace <PATH>` first. Backend serve no longer treats the process current directory as an implicit Workspace.\n\nOptions:\n --workspace <PATH> Workspace root containing .yoi project records (required)\n --db <PATH> SQLite database path (legacy dev override)\n --frontend <PATH> Static SPA build directory to serve (legacy dev override)\n --listen <ADDR> Listen address (legacy dev override; default 127.0.0.1:8787)\n -h, --help Print help"
|
||||
"yoi-workspace-server serve\n\nUsage:\n yoi-workspace-server serve [OPTIONS]\n\nDescription:\n Serves the Workspace recorded in the Yoi server DB. Workspace records are stored in the XDG/Yoi data directory, and runtime sources are loaded from XDG runtimes.toml.\n\nOptions:\n --listen <ADDR> Listen address (default 127.0.0.1:8787)\n -h, --help Print help"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -455,43 +498,42 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_serve_rejects_missing_workspace() {
|
||||
fn parse_serve_accepts_listen_only() {
|
||||
let args = vec!["--listen".to_string(), "127.0.0.1:0".to_string()];
|
||||
let error = parse_serve_options(&args).unwrap_err();
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"serve requires --workspace <path>; the current directory is no longer used as an implicit workspace"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_serve_requires_explicit_workspace() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let args = vec![
|
||||
"--workspace".to_string(),
|
||||
temp.path().display().to_string(),
|
||||
"--listen".to_string(),
|
||||
"127.0.0.1:0".to_string(),
|
||||
];
|
||||
let options = parse_serve_options(&args).unwrap();
|
||||
assert_eq!(options.workspace, temp.path().canonicalize().unwrap());
|
||||
assert_eq!(options.listen.unwrap(), "127.0.0.1:0".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_serve_accepts_equals_workspace() {
|
||||
fn parse_serve_rejects_legacy_workspace_flag() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let args = vec![format!("--workspace={}", temp.path().display())];
|
||||
let options = parse_serve_options(&args).unwrap();
|
||||
assert_eq!(options.workspace, temp.path().canonicalize().unwrap());
|
||||
let args = vec!["--workspace".to_string(), temp.path().display().to_string()];
|
||||
let error = parse_serve_options(&args).unwrap_err();
|
||||
assert_eq!(error.to_string(), "unknown serve option `--workspace`");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn init_creates_identity_and_local_config_only() {
|
||||
fn parse_serve_rejects_legacy_db_and_frontend_flags() {
|
||||
let error = parse_serve_options(&["--db=/tmp/yoi.db".to_string()]).unwrap_err();
|
||||
assert_eq!(error.to_string(), "unknown serve option `--db=/tmp/yoi.db`");
|
||||
let error = parse_serve_options(&["--frontend=/tmp/web".to_string()]).unwrap_err();
|
||||
assert_eq!(
|
||||
error.to_string(),
|
||||
"unknown serve option `--frontend=/tmp/web`"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn init_creates_identity_local_config_and_server_records() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
run_init(InitOptions {
|
||||
workspace: temp.path().canonicalize().unwrap(),
|
||||
})
|
||||
let database_path = temp.path().join("data").join("server").join("server.db");
|
||||
run_init_with_database_path(
|
||||
InitOptions {
|
||||
workspace: temp.path().canonicalize().unwrap(),
|
||||
},
|
||||
database_path.clone(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(temp.path().join(WORKSPACE_IDENTITY_RELATIVE_PATH).exists());
|
||||
@@ -509,5 +551,19 @@ mod tests {
|
||||
);
|
||||
assert!(!temp.path().join(".yoi/workspace.db").exists());
|
||||
assert!(!temp.path().join(".yoi/embedded-runtime").exists());
|
||||
assert!(database_path.exists());
|
||||
|
||||
let store = SqliteWorkspaceStore::open(&database_path).unwrap();
|
||||
let workspaces = store.list_workspaces().unwrap();
|
||||
assert_eq!(workspaces.len(), 1);
|
||||
let repositories = store
|
||||
.list_repositories(&workspaces[0].workspace_id)
|
||||
.unwrap();
|
||||
assert_eq!(repositories.len(), 1);
|
||||
assert_eq!(repositories[0].repository_id, "main");
|
||||
assert_eq!(
|
||||
repositories[0].uri,
|
||||
temp.path().canonicalize().unwrap().display().to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -138,8 +138,7 @@ impl ServerConfig {
|
||||
let workspace_root = workspace_root.into();
|
||||
let workspace_id = identity.workspace_id;
|
||||
let embedded_runtime_store_root = Self::default_embedded_runtime_store_root(&workspace_id);
|
||||
let database_path =
|
||||
Self::default_workspace_backend_data_root(&workspace_id).join("workspace.db");
|
||||
let database_path = Self::default_server_database_path();
|
||||
Self {
|
||||
workspace_id,
|
||||
workspace_display_name: identity.display_name,
|
||||
@@ -164,13 +163,31 @@ impl ServerConfig {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_data_root_for_data_dir(data_dir: impl Into<PathBuf>) -> PathBuf {
|
||||
data_dir.into().join("server")
|
||||
}
|
||||
|
||||
pub fn default_server_data_root() -> PathBuf {
|
||||
match manifest::paths::data_dir() {
|
||||
Some(data_dir) => Self::server_data_root_for_data_dir(data_dir),
|
||||
None => std::env::temp_dir().join("yoi").join("server"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn server_database_path_for_data_dir(data_dir: impl Into<PathBuf>) -> PathBuf {
|
||||
Self::server_data_root_for_data_dir(data_dir).join("server.db")
|
||||
}
|
||||
|
||||
pub fn default_server_database_path() -> PathBuf {
|
||||
Self::default_server_data_root().join("server.db")
|
||||
}
|
||||
|
||||
pub fn workspace_backend_data_root_for_data_dir(
|
||||
data_dir: impl Into<PathBuf>,
|
||||
workspace_id: impl AsRef<str>,
|
||||
) -> PathBuf {
|
||||
data_dir
|
||||
.into()
|
||||
.join("workspace-server")
|
||||
Self::server_data_root_for_data_dir(data_dir)
|
||||
.join("workspaces")
|
||||
.join(workspace_id.as_ref())
|
||||
}
|
||||
|
||||
@@ -181,7 +198,8 @@ impl ServerConfig {
|
||||
}
|
||||
None => std::env::temp_dir()
|
||||
.join("yoi")
|
||||
.join("workspace-server")
|
||||
.join("server")
|
||||
.join("workspaces")
|
||||
.join(workspace_id.as_ref()),
|
||||
}
|
||||
}
|
||||
@@ -6846,6 +6864,7 @@ mod tests {
|
||||
let store_root = workspace_root.join(".test-embedded-runtime-store");
|
||||
let mut config = ServerConfig::local_dev(workspace_root.clone(), test_identity())
|
||||
.with_embedded_runtime_store_root(store_root);
|
||||
config.database_path = workspace_root.join(".test-yoi-server.db");
|
||||
config.runtime_config_path = Some(workspace_root.join(".test-config/runtimes.toml"));
|
||||
config.repositories = vec![ConfiguredRepository {
|
||||
id: TEST_REPOSITORY_ID.to_string(),
|
||||
@@ -7952,7 +7971,6 @@ mod tests {
|
||||
#[tokio::test]
|
||||
async fn serves_bounded_read_apis_and_static_spa_separately() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
write_ticket(dir.path(), "00000000001J2", "API Ticket", "ready");
|
||||
write_objective(dir.path(), "00000000001J3", "API Objective", "active");
|
||||
let static_dir = dir.path().join("static");
|
||||
std::fs::create_dir_all(static_dir.join("assets")).unwrap();
|
||||
@@ -7961,6 +7979,12 @@ mod tests {
|
||||
|
||||
let store = SqliteWorkspaceStore::in_memory().unwrap();
|
||||
let mut config = test_server_config(dir.path());
|
||||
write_ticket(
|
||||
&config.database_path,
|
||||
TEST_WORKSPACE_ID,
|
||||
"API Ticket",
|
||||
ticket::TicketWorkflowState::Ready,
|
||||
);
|
||||
config.static_assets_dir = Some(static_dir);
|
||||
let api = WorkspaceApi::new_with_execution_backend(
|
||||
config,
|
||||
@@ -8044,7 +8068,7 @@ mod tests {
|
||||
);
|
||||
|
||||
let tickets = get_json(app.clone(), "/api/tickets").await;
|
||||
assert_eq!(tickets["items"][0]["id"], "00000000001J2");
|
||||
assert_eq!(tickets["items"][0]["title"], "API Ticket");
|
||||
assert_eq!(tickets["items"][0]["state"], "ready");
|
||||
|
||||
let objectives = get_json(app.clone(), "/api/objectives").await;
|
||||
@@ -8098,7 +8122,7 @@ mod tests {
|
||||
.iter()
|
||||
.find(|column| column["state"] == "ready")
|
||||
.unwrap();
|
||||
assert_eq!(ready_column["items"][0]["id"], "00000000001J2");
|
||||
assert_eq!(ready_column["items"][0]["title"], "API Ticket");
|
||||
assert_eq!(
|
||||
repository_tickets["diagnostics"][0]["code"],
|
||||
"repository_ticket_target_metadata_absent"
|
||||
@@ -8456,7 +8480,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
default_root,
|
||||
data_dir
|
||||
.join("workspace-server")
|
||||
.join("server")
|
||||
.join("workspaces")
|
||||
.join(TEST_WORKSPACE_ID)
|
||||
.join("embedded-runtime")
|
||||
);
|
||||
@@ -8941,25 +8966,18 @@ mod tests {
|
||||
serde_json::from_slice(&bytes).unwrap()
|
||||
}
|
||||
|
||||
fn write_ticket(root: &Path, id: &str, title: &str, state: &str) {
|
||||
let ticket_dir = root.join(".yoi/tickets").join(id);
|
||||
std::fs::create_dir_all(&ticket_dir).unwrap();
|
||||
std::fs::write(
|
||||
ticket_dir.join("item.md"),
|
||||
format!(
|
||||
r#"---
|
||||
title: "{title}"
|
||||
state: "{state}"
|
||||
created_at: "2026-01-01T00:00:00Z"
|
||||
updated_at: "2026-01-02T00:00:00Z"
|
||||
---
|
||||
fn write_ticket(
|
||||
database_path: &Path,
|
||||
workspace_id: &str,
|
||||
title: &str,
|
||||
state: ticket::TicketWorkflowState,
|
||||
) {
|
||||
use ticket::TicketBackend as _;
|
||||
|
||||
Ticket body.
|
||||
"#,
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
std::fs::write(ticket_dir.join("thread.md"), "").unwrap();
|
||||
let backend = ticket::SqliteTicketBackend::new(database_path, workspace_id);
|
||||
let mut input = ticket::NewTicket::new(title);
|
||||
input.workflow_state = Some(state);
|
||||
backend.create(input).unwrap();
|
||||
}
|
||||
|
||||
fn write_objective(root: &Path, id: &str, title: &str, state: &str) {
|
||||
|
||||
@@ -226,6 +226,7 @@ pub trait ControlPlaneStore: Send + Sync {
|
||||
async fn schema_version(&self) -> Result<i64>;
|
||||
async fn upsert_workspace(&self, record: &WorkspaceRecord) -> Result<()>;
|
||||
async fn get_workspace(&self, workspace_id: &str) -> Result<Option<WorkspaceRecord>>;
|
||||
fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>>;
|
||||
fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()>;
|
||||
fn list_repositories(&self, workspace_id: &str) -> Result<Vec<RepositoryRecord>>;
|
||||
|
||||
@@ -416,6 +417,19 @@ impl ControlPlaneStore for SqliteWorkspaceStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>> {
|
||||
self.with_conn(|conn| {
|
||||
let mut stmt = conn.prepare(
|
||||
r#"SELECT workspace_id, owner_account_id, display_name, state, created_at, updated_at
|
||||
FROM workspaces
|
||||
ORDER BY workspace_id ASC"#,
|
||||
)?;
|
||||
let rows = stmt.query_map([], read_workspace_record)?;
|
||||
rows.collect::<std::result::Result<Vec<_>, _>>()
|
||||
.map_err(Error::from)
|
||||
})
|
||||
}
|
||||
|
||||
fn upsert_repository(&self, record: &RepositoryRecord) -> Result<()> {
|
||||
self.with_conn(|conn| {
|
||||
conn.execute(
|
||||
|
||||
Reference in New Issue
Block a user