refactor(api): unify secret metadata types

This commit is contained in:
Bryan Helmkamp 2026-04-29 20:26:13 -04:00
parent 25cd80c072
commit 3e97cae0ae
No known key found for this signature in database
11 changed files with 128 additions and 26 deletions

1
Cargo.lock generated
View file

@ -1605,6 +1605,7 @@ dependencies = [
"fabro-oauth",
"fabro-redact",
"fabro-static",
"fabro-types",
"fabro-vault",
"httpmock",
"serde",

View file

@ -314,6 +314,8 @@ fn main() {
("QuestionType", "fabro_types::QuestionType", &[]),
("NodeStatusRecord", "fabro_types::NodeStatusRecord", &[]),
("InternalStageStatus", "fabro_types::StageStatus", &[]),
("NodeState", "fabro_types::NodeState", &[]),
("SecretMetadata", "fabro_types::SecretMetadata", &[]),
("PullRequestRecord", "fabro_types::PullRequestRecord", &[]),
("PullRequestDetail", "fabro_types::PullRequestDetail", &[]),
("PullRequestUser", "fabro_types::PullRequestUser", &[]),

View file

@ -28,9 +28,9 @@ pub mod types {
BlockedReason, FailureReason, RunControlAction, RunStatus, SuccessReason, TerminalStatus,
};
pub use fabro_types::{
ActorKind, ActorRef, DiffStats, DirtyStatus, GitContext, NodeStatusRecord,
PreRunPushOutcome, QuestionType, RepositoryReference, RunSummary, SecretType,
ServerSettings, StageStatus as InternalStageStatus, WorkflowSettings,
ActorKind, ActorRef, DiffStats, DirtyStatus, GitContext, NodeState, NodeStatusRecord,
PreRunPushOutcome, QuestionType, RepositoryReference, RunSummary, SecretMetadata,
SecretType, ServerSettings, StageStatus as InternalStageStatus, WorkflowSettings,
};
pub use crate::generated::types::*;

View file

@ -0,0 +1,44 @@
use std::any::{TypeId, type_name};
use fabro_api::types::NodeState as ApiNodeState;
use fabro_types::NodeState;
use serde_json::json;
#[test]
fn node_state_reuses_canonical_type() {
assert_same_type::<ApiNodeState, NodeState>();
}
#[test]
fn node_state_round_trips_representative_json() {
let value = json!({
"prompt": "build it",
"response": "done",
"status": {
"status": "success",
"notes": null,
"failure_reason": null,
"timestamp": "2026-04-29T12:34:56Z"
},
"provider_used": { "provider": "openai", "model": "gpt-5.2" },
"diff": "diff --git a/file b/file",
"script_invocation": { "command": "cargo test" },
"script_timing": { "duration_ms": 42 },
"parallel_results": [{ "branch": 0, "status": "success" }],
"stdout": "ok",
"stderr": ""
});
let state: NodeState = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(state).unwrap(), value);
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
);
}

View file

@ -0,0 +1,50 @@
use std::any::{TypeId, type_name};
use fabro_api::types::SecretMetadata as ApiSecretMetadata;
use fabro_types::{SecretMetadata, SecretType};
use serde_json::json;
#[test]
fn secret_metadata_reuses_canonical_type() {
assert_same_type::<ApiSecretMetadata, SecretMetadata>();
}
#[test]
fn secret_metadata_round_trips_representative_json() {
let value = json!({
"name": "ANTHROPIC_API_KEY",
"type": "environment",
"description": "Anthropic API key",
"created_at": "2026-04-29T12:34:56Z",
"updated_at": "2026-04-29T12:40:00Z"
});
let metadata: SecretMetadata = serde_json::from_value(value.clone()).unwrap();
assert_eq!(metadata.name, "ANTHROPIC_API_KEY");
assert_eq!(metadata.secret_type, SecretType::Environment);
assert_eq!(metadata.description, Some("Anthropic API key".to_string()));
assert_eq!(serde_json::to_value(metadata).unwrap(), value);
}
#[test]
fn secret_metadata_omits_absent_description() {
let value = json!({
"name": "/run/secrets/key.pem",
"type": "file",
"created_at": "2026-04-29T12:34:56Z",
"updated_at": "2026-04-29T12:40:00Z"
});
let metadata: SecretMetadata = serde_json::from_value(value.clone()).unwrap();
assert_eq!(serde_json::to_value(metadata).unwrap(), value);
}
fn assert_same_type<T: 'static, U: 'static>() {
assert_eq!(
TypeId::of::<T>(),
TypeId::of::<U>(),
"{} should be the same type as {}",
type_name::<T>(),
type_name::<U>()
);
}

View file

@ -19,6 +19,7 @@ fabro-model = { path = "../fabro-model" }
fabro-oauth = { path = "../fabro-oauth" }
fabro-redact.workspace = true
fabro-static.workspace = true
fabro-types = { path = "../fabro-types" }
fabro-vault = { path = "../fabro-vault" }
serde.workspace = true
serde_json.workspace = true

View file

@ -1,5 +1,6 @@
use fabro_model::Provider;
use fabro_vault::{SecretMetadata, SecretType, Vault};
use fabro_types::SecretMetadata;
use fabro_vault::{SecretType, Vault};
use crate::credential::AuthCredential;

View file

@ -48,7 +48,7 @@ pub(super) async fn list_command(_args: &SecretListArgs, ctx: &CommandContext) -
.map(|secret| {
vec![
secret.name.clone().cell().bold(use_color),
secret.type_.to_string().cell(),
secret.secret_type.to_string().cell(),
format_age(secret.updated_at, now).cell(),
]
})

View file

@ -69,7 +69,7 @@ pub use run_id::{RunId, fixtures};
pub use run_projection::{NodeState, PendingInterviewRecord, RunProjection};
pub use run_summary::RunSummary;
pub use sandbox_record::SandboxRecord;
pub use secret::SecretType;
pub use secret::{SecretMetadata, SecretType};
pub use stage_id::{ParallelBranchId, StageId};
pub use start::StartRecord;
pub use status::{

View file

@ -1,3 +1,4 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use strum::Display;
@ -10,3 +11,14 @@ pub enum SecretType {
File,
Credential,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecretMetadata {
pub name: String,
#[serde(rename = "type")]
pub secret_type: SecretType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}

View file

@ -7,6 +7,8 @@ use std::collections::HashMap;
use std::path::{Component, Path, PathBuf};
use std::{fmt, io};
use chrono::{DateTime, Utc};
use fabro_types::SecretMetadata;
pub use fabro_types::SecretType;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
@ -16,19 +18,8 @@ pub struct SecretEntry {
pub secret_type: SecretType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct SecretMetadata {
pub name: String,
#[serde(rename = "type")]
pub secret_type: SecretType,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub created_at: String,
pub updated_at: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug)]
@ -90,12 +81,12 @@ impl Vault {
) -> Result<SecretMetadata, Error> {
Self::validate_name(name, secret_type)?;
let now = chrono::Utc::now().to_rfc3339();
let now = Utc::now();
let (created_at, description) = self.entries.get(name).map_or_else(
|| (now.clone(), description.map(str::to_string)),
|| (now, description.map(str::to_string)),
|entry| {
(
entry.created_at.clone(),
entry.created_at,
description
.map(str::to_string)
.or_else(|| entry.description.clone()),
@ -106,8 +97,8 @@ impl Vault {
value: value.to_string(),
secret_type,
description: description.clone(),
created_at: created_at.clone(),
updated_at: now.clone(),
created_at,
updated_at: now,
};
self.entries.insert(name.to_string(), entry);
self.write_atomic()?;
@ -137,8 +128,8 @@ impl Vault {
name: name.clone(),
secret_type: entry.secret_type,
description: entry.description.clone(),
created_at: entry.created_at.clone(),
updated_at: entry.updated_at.clone(),
created_at: entry.created_at,
updated_at: entry.updated_at,
})
.collect::<Vec<_>>();
data.sort_by(|a, b| a.name.cmp(&b.name));