chore: replace legacy SQLite migrations with baselines
This commit is contained in:
@@ -1513,33 +1513,6 @@ mod tests {
|
||||
assert_eq!(revision, first.snapshot);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn migration_materializes_main_for_existing_workspace_without_config() {
|
||||
let conn = rusqlite::Connection::open_in_memory().unwrap();
|
||||
crate::store::configure_sqlite(&conn).unwrap();
|
||||
crate::store::apply_migrations_through(&conn, 30).unwrap();
|
||||
conn.execute(
|
||||
"INSERT INTO workspaces (
|
||||
workspace_id, display_name, state, created_at, updated_at
|
||||
) VALUES ('legacy', 'Legacy', 'active', '2026-08-06T00:00:00Z', '2026-08-06T00:00:00Z')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
crate::store::persist_workspace_config_schema_bundles(&conn).unwrap();
|
||||
crate::store::materialize_main_config_entrypoint(&conn).unwrap();
|
||||
let state = load_state(&conn, "legacy").unwrap().unwrap();
|
||||
assert!(
|
||||
state
|
||||
.snapshot
|
||||
.entries
|
||||
.contains_key(&path(MAIN_CONFIG_ENTRYPOINT))
|
||||
);
|
||||
assert_eq!(
|
||||
state.contract.entrypoints,
|
||||
vec![path(MAIN_CONFIG_ENTRYPOINT)]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exports_typescript_transport_contract() {
|
||||
use ts_rs::TS;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,6 @@ enum Command {
|
||||
Serve(ServeOptions),
|
||||
Identity(Vec<String>),
|
||||
TrustRuntime(Vec<String>),
|
||||
MigrateDryRun { database: Option<PathBuf> },
|
||||
Skills(SkillsCommand),
|
||||
Help,
|
||||
}
|
||||
@@ -71,17 +70,6 @@ async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
Command::Serve(options) => run_serve(options).await,
|
||||
Command::Identity(args) => run_identity_command(args),
|
||||
Command::TrustRuntime(args) => run_trust_runtime_command(args),
|
||||
Command::MigrateDryRun { database } => {
|
||||
let database = database.unwrap_or_else(ServerConfig::default_server_database_path);
|
||||
let plan = SqliteWorkspaceStore::migration_plan(&database).map_err(|error| {
|
||||
CliError(format!(
|
||||
"migration dry-run failed for {}: {error}",
|
||||
database.display()
|
||||
))
|
||||
})?;
|
||||
println!("{}", serde_json::to_string_pretty(&plan)?);
|
||||
Ok(())
|
||||
}
|
||||
Command::Skills(command) => run_skills(command),
|
||||
Command::Help => Ok(()),
|
||||
}
|
||||
@@ -96,7 +84,6 @@ fn parse_command(args: &[String]) -> Result<Command, CliError> {
|
||||
match command.as_str() {
|
||||
"identity" => Ok(Command::Identity(rest.to_vec())),
|
||||
"trust-runtime" => Ok(Command::TrustRuntime(rest.to_vec())),
|
||||
"migrate" => parse_migrate_command(rest),
|
||||
"skills" => parse_skills_command(rest),
|
||||
"serve" => {
|
||||
if rest.iter().any(|arg| arg == "--help" || arg == "-h") {
|
||||
@@ -625,32 +612,6 @@ fn workspace_root_from_server_data(workspace: &WorkspaceRecord) -> Result<PathBu
|
||||
))
|
||||
}
|
||||
|
||||
fn parse_migrate_command(args: &[String]) -> Result<Command, CliError> {
|
||||
let mut dry_run = false;
|
||||
let mut database = None;
|
||||
let mut index = 0;
|
||||
while index < args.len() {
|
||||
match args[index].as_str() {
|
||||
"--dry-run" => dry_run = true,
|
||||
"--database" => {
|
||||
index += 1;
|
||||
database =
|
||||
Some(PathBuf::from(args.get(index).ok_or_else(|| {
|
||||
CliError("--database requires a path".to_string())
|
||||
})?));
|
||||
}
|
||||
value => {
|
||||
return Err(CliError(format!("unknown migrate option: {value}")));
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
if !dry_run {
|
||||
return Err(CliError("migrate currently requires --dry-run".to_string()));
|
||||
}
|
||||
Ok(Command::MigrateDryRun { database })
|
||||
}
|
||||
|
||||
fn parse_skills_command(args: &[String]) -> Result<Command, CliError> {
|
||||
let Some((subcommand, rest)) = args.split_first() else {
|
||||
print_skills_help();
|
||||
@@ -770,8 +731,7 @@ fn parse_listen(value: &str) -> Result<SocketAddr, CliError> {
|
||||
|
||||
fn print_help() {
|
||||
println!(
|
||||
"yoi-server\n\nUsage:\n yoi-server identity init --server-id <SERVER_ID> [--replace]\n yoi-server identity show [--json]\n yoi-server trust-runtime add --runtime-id <RUNTIME_ID> --workspace-id <WORKSPACE_ID> --base-url <URL> --public-key <KEY> [--display-name <NAME>] [--replace]\n yoi-server trust-runtime list [--json] [--include-revoked]\n yoi-server trust-runtime revoke --runtime-id <RUNTIME_ID>\n yoi-server skills <COMMAND> [OPTIONS]\n yoi-server migrate --dry-run [--database <PATH>]
|
||||
yoi-server serve [OPTIONS]\n\nOptions:\n -h, --help Print help"
|
||||
"yoi-server\n\nUsage:\n yoi-server identity init --server-id <SERVER_ID> [--replace]\n yoi-server identity show [--json]\n yoi-server trust-runtime add --runtime-id <RUNTIME_ID> --workspace-id <WORKSPACE_ID> --base-url <URL> --public-key <KEY> [--display-name <NAME>] [--replace]\n yoi-server trust-runtime list [--json] [--include-revoked]\n yoi-server trust-runtime revoke --runtime-id <RUNTIME_ID>\n yoi-server skills <COMMAND> [OPTIONS]\n yoi-server serve [OPTIONS]\n\nOptions:\n -h, --help Print help"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -783,8 +743,7 @@ fn print_skills_help() {
|
||||
|
||||
fn print_serve_help() {
|
||||
println!(
|
||||
"yoi-server serve\n\nUsage:\n yoi-server migrate --dry-run [--database <PATH>]
|
||||
yoi-server serve [OPTIONS]\n\nDescription:\n Serves Workspaces recorded in the Yoi server DB. Host-level deployment settings are loaded from the explicit --config path or the canonical XDG yoi/server.toml path, and runtime sources are loaded from XDG runtimes.toml.\n\nOptions:\n --listen <ADDR> Listen address (default 127.0.0.1:8787)\n --config <PATH> Host-level Server config path\n -h, --help Print help"
|
||||
"yoi-server serve\n\nUsage:\n yoi-server serve [OPTIONS]\n\nDescription:\n Serves Workspaces recorded in the Yoi server DB. Host-level deployment settings are loaded from the explicit --config path or the canonical XDG yoi/server.toml path, and runtime sources are loaded from XDG runtimes.toml.\n\nOptions:\n --listen <ADDR> Listen address (default 127.0.0.1:8787)\n --config <PATH> Host-level Server config path\n -h, --help Print help"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -823,22 +782,6 @@ mod tests {
|
||||
assert_eq!(name, "debug-rust");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_migrate_requires_dry_run_and_accepts_database_path() {
|
||||
let error = parse_migrate_command(&[]).unwrap_err();
|
||||
assert_eq!(error.to_string(), "migrate currently requires --dry-run");
|
||||
let command = parse_migrate_command(&[
|
||||
"--dry-run".to_string(),
|
||||
"--database".to_string(),
|
||||
"/tmp/server.db".to_string(),
|
||||
])
|
||||
.unwrap();
|
||||
let Command::MigrateDryRun { database } = command else {
|
||||
panic!("expected migration dry-run command");
|
||||
};
|
||||
assert_eq!(database, Some(PathBuf::from("/tmp/server.db")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_serve_accepts_listen_and_host_config() {
|
||||
let args = vec![
|
||||
|
||||
@@ -163,101 +163,6 @@ pub enum WorkerRetentionError {
|
||||
Invalid(String),
|
||||
}
|
||||
|
||||
pub(crate) fn repair_worker_diagnostics_archive_table(conn: &Connection) -> crate::Result<bool> {
|
||||
let existed: bool = conn.query_row(
|
||||
"SELECT EXISTS(SELECT 1 FROM sqlite_master WHERE type='table' AND name='worker_diagnostics_archives')",
|
||||
[],
|
||||
|row| row.get(0),
|
||||
)?;
|
||||
if !existed {
|
||||
conn.execute_batch(
|
||||
"CREATE TABLE worker_diagnostics_archives (
|
||||
operation_id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL,
|
||||
worker_id TEXT NOT NULL, policy_id TEXT NOT NULL, policy_revision INTEGER NOT NULL,
|
||||
committed_at TEXT NOT NULL, expires_at TEXT NOT NULL,
|
||||
FOREIGN KEY(operation_id) REFERENCES worker_removal_operations(operation_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE);",
|
||||
)?;
|
||||
}
|
||||
Ok(!existed)
|
||||
}
|
||||
|
||||
pub(crate) fn create_worker_retention_tables(conn: &Connection) -> crate::Result<()> {
|
||||
conn.execute_batch(r#"
|
||||
CREATE TABLE workspace_worker_retention_policy_revisions (
|
||||
workspace_id TEXT NOT NULL, policy_id TEXT NOT NULL, revision INTEGER NOT NULL CHECK(revision>0),
|
||||
session_disposition TEXT NOT NULL CHECK(session_disposition IN ('archive','purge')),
|
||||
metadata_disposition TEXT NOT NULL CHECK(metadata_disposition IN ('tombstone','purge')),
|
||||
archive_retention_kind TEXT NOT NULL CHECK(archive_retention_kind IN ('forever','for_seconds')),
|
||||
archive_retention_seconds INTEGER,
|
||||
diagnostics_disposition TEXT NOT NULL CHECK(diagnostics_disposition IN ('purge','retain')),
|
||||
diagnostics_retention_seconds INTEGER, created_at TEXT NOT NULL,
|
||||
PRIMARY KEY(workspace_id,policy_id,revision),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE);
|
||||
CREATE TABLE workspace_worker_retention_policies (
|
||||
workspace_id TEXT PRIMARY KEY, policy_id TEXT NOT NULL, revision INTEGER NOT NULL, updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(workspace_id,policy_id,revision) REFERENCES workspace_worker_retention_policy_revisions(workspace_id,policy_id,revision),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE);
|
||||
CREATE TABLE worker_removal_operations (
|
||||
operation_id TEXT PRIMARY KEY, plan_id TEXT NOT NULL UNIQUE, input_fingerprint TEXT NOT NULL,
|
||||
workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL,
|
||||
worker_revision TEXT NOT NULL, run_generation INTEGER NOT NULL CHECK(run_generation>=0),
|
||||
policy_id TEXT NOT NULL, policy_revision INTEGER NOT NULL,
|
||||
session_disposition TEXT NOT NULL, metadata_disposition TEXT NOT NULL,
|
||||
archive_retention_kind TEXT NOT NULL, archive_retention_seconds INTEGER,
|
||||
diagnostics_disposition TEXT NOT NULL,
|
||||
diagnostics_retention_seconds INTEGER, archive_id TEXT UNIQUE, blockers_json TEXT NOT NULL,
|
||||
state TEXT NOT NULL CHECK(state IN ('planned','blocked','executing','failed','stale','succeeded')),
|
||||
reason TEXT NOT NULL, failure_category TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE);
|
||||
CREATE INDEX worker_removal_operations_worker_idx ON worker_removal_operations(workspace_id,runtime_id,worker_id,created_at);
|
||||
CREATE TABLE worker_session_archives (
|
||||
archive_id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL,
|
||||
session_id TEXT NOT NULL, checksum_sha256 TEXT NOT NULL, content_bytes INTEGER NOT NULL,
|
||||
policy_id TEXT NOT NULL, policy_revision INTEGER NOT NULL, operation_id TEXT NOT NULL UNIQUE,
|
||||
committed_at TEXT NOT NULL, expires_at TEXT,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(operation_id) REFERENCES worker_removal_operations(operation_id));
|
||||
CREATE TABLE worker_diagnostics_archives (
|
||||
operation_id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL,
|
||||
worker_id TEXT NOT NULL, policy_id TEXT NOT NULL, policy_revision INTEGER NOT NULL,
|
||||
committed_at TEXT NOT NULL, expires_at TEXT NOT NULL,
|
||||
FOREIGN KEY(operation_id) REFERENCES worker_removal_operations(operation_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE);
|
||||
CREATE TABLE worker_tombstones (
|
||||
workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL,
|
||||
display_name TEXT NOT NULL, profile TEXT, worker_created_at TEXT NOT NULL, removed_at TEXT NOT NULL,
|
||||
archive_id TEXT, policy_id TEXT NOT NULL, policy_revision INTEGER NOT NULL, operation_id TEXT NOT NULL UNIQUE,
|
||||
PRIMARY KEY(workspace_id,runtime_id,worker_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE,
|
||||
FOREIGN KEY(archive_id) REFERENCES worker_session_archives(archive_id),
|
||||
FOREIGN KEY(operation_id) REFERENCES worker_removal_operations(operation_id));
|
||||
CREATE TABLE worker_orphan_diagnostics (
|
||||
diagnostic_id TEXT PRIMARY KEY, workspace_id TEXT NOT NULL, runtime_id TEXT NOT NULL, worker_id TEXT NOT NULL,
|
||||
category TEXT NOT NULL, detail TEXT NOT NULL, observed_at TEXT NOT NULL,
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE);
|
||||
CREATE TABLE worker_retention_audit_events (
|
||||
event_id TEXT PRIMARY KEY, operation_id TEXT NOT NULL, workspace_id TEXT NOT NULL,
|
||||
event_kind TEXT NOT NULL, detail TEXT NOT NULL, created_at TEXT NOT NULL,
|
||||
FOREIGN KEY(operation_id) REFERENCES worker_removal_operations(operation_id),
|
||||
FOREIGN KEY(workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE);
|
||||
CREATE TRIGGER seed_worker_retention_policy_after_workspace_insert AFTER INSERT ON workspaces BEGIN
|
||||
INSERT INTO workspace_worker_retention_policy_revisions
|
||||
(workspace_id,policy_id,revision,session_disposition,metadata_disposition,archive_retention_kind,archive_retention_seconds,diagnostics_disposition,diagnostics_retention_seconds,created_at)
|
||||
VALUES(NEW.workspace_id,'workspace-default-conservative',1,'archive','tombstone','forever',NULL,'purge',NULL,NEW.created_at);
|
||||
INSERT INTO workspace_worker_retention_policies(workspace_id,policy_id,revision,updated_at)
|
||||
VALUES(NEW.workspace_id,'workspace-default-conservative',1,NEW.created_at);
|
||||
END;
|
||||
"#)?;
|
||||
let now = Utc::now().to_rfc3339();
|
||||
conn.execute("INSERT OR IGNORE INTO workspace_worker_retention_policy_revisions
|
||||
(workspace_id,policy_id,revision,session_disposition,metadata_disposition,archive_retention_kind,archive_retention_seconds,diagnostics_disposition,diagnostics_retention_seconds,created_at)
|
||||
SELECT workspace_id,?1,1,'archive','tombstone','forever',NULL,'purge',NULL,?2 FROM workspaces", params![CONSERVATIVE_POLICY_ID,now])?;
|
||||
conn.execute("INSERT OR IGNORE INTO workspace_worker_retention_policies(workspace_id,policy_id,revision,updated_at)
|
||||
SELECT workspace_id,?1,1,?2 FROM workspaces", params![CONSERVATIVE_POLICY_ID,now])?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl SqliteWorkspaceStore {
|
||||
pub fn worker_retention_policy(
|
||||
&self,
|
||||
@@ -1724,36 +1629,4 @@ mod tests {
|
||||
);
|
||||
assert_eq!(recovered.plan.state, WorkerRemovalPlanState::Failed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn old_schema_upgrade_seeds_existing_workspace() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let path = temp.path().join("server.db");
|
||||
{
|
||||
let connection = rusqlite::Connection::open(&path).unwrap();
|
||||
crate::store::configure_sqlite(&connection).unwrap();
|
||||
crate::store::apply_migrations_through(&connection, 27).unwrap();
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO accounts(
|
||||
account_id, kind, handle, display_name, created_at, updated_at
|
||||
) VALUES ('owner-account', 'user', 'owner-account', 'Owner Account', 'old', 'old')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
connection
|
||||
.execute(
|
||||
"INSERT INTO workspaces(
|
||||
workspace_id, display_name, state, created_at, updated_at, owner_account_id
|
||||
) VALUES ('legacy', 'Legacy', 'active', 'old', 'old', 'owner-account')",
|
||||
[],
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let reopened = SqliteWorkspaceStore::open(&path).unwrap();
|
||||
let p = reopened.worker_retention_policy("legacy").unwrap().unwrap();
|
||||
assert_eq!(p.policy_id, CONSERVATIVE_POLICY_ID);
|
||||
assert_eq!(p.session_disposition, SessionDisposition::Archive);
|
||||
assert_eq!(p.metadata_disposition, MetadataDisposition::Tombstone);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -91,44 +91,6 @@ pub struct WorkdirRemovalGuard {
|
||||
pub detail: &'static str,
|
||||
}
|
||||
|
||||
pub(crate) fn create_workdir_removal_operations(conn: &Connection) -> Result<()> {
|
||||
conn.execute_batch(
|
||||
r#"
|
||||
CREATE TABLE workdir_removal_operations (
|
||||
workspace_id TEXT NOT NULL,
|
||||
operation_id TEXT NOT NULL,
|
||||
request_fingerprint TEXT NOT NULL,
|
||||
workdir_id TEXT NOT NULL,
|
||||
runtime_id TEXT NOT NULL,
|
||||
repository_id TEXT NOT NULL,
|
||||
materialization_fingerprint TEXT NOT NULL,
|
||||
source_actor TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
state TEXT NOT NULL CHECK (state IN ('pending', 'failed', 'completed')),
|
||||
attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
|
||||
retryable INTEGER NOT NULL CHECK (retryable IN (0, 1)),
|
||||
disposition TEXT CHECK (disposition IN ('removed', 'retained', 'attention_required')),
|
||||
failure_category TEXT,
|
||||
attempt_owner_pid INTEGER CHECK (attempt_owner_pid > 0),
|
||||
attempt_owner_start_marker INTEGER CHECK (attempt_owner_start_marker >= 0),
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
completed_at TEXT,
|
||||
PRIMARY KEY (workspace_id, operation_id),
|
||||
FOREIGN KEY (workspace_id) REFERENCES workspaces(workspace_id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE UNIQUE INDEX idx_workdir_removal_operations_one_pending
|
||||
ON workdir_removal_operations(workspace_id, workdir_id)
|
||||
WHERE state = 'pending';
|
||||
CREATE INDEX idx_workdir_removal_operations_recovery
|
||||
ON workdir_removal_operations(workspace_id, state, retryable, updated_at);
|
||||
CREATE INDEX idx_workdir_removal_operations_workdir
|
||||
ON workdir_removal_operations(workspace_id, workdir_id, created_at DESC);
|
||||
"#,
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn workdir_materialization_fingerprint(record: &WorkdirRegistryRecord) -> String {
|
||||
let bytes = serde_json::to_vec(&serde_json::json!([
|
||||
record.workspace_id,
|
||||
|
||||
Reference in New Issue
Block a user