feat: complete Epic 1 production foundation

This commit is contained in:
2026-08-25 01:24:11 +03:00
parent 767428436d
commit 182bde8ac0
298 changed files with 35719 additions and 5299 deletions
+149 -2
View File
@@ -28,7 +28,73 @@ pub enum OperationStatus {
Archived,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationAvailability {
Active,
Archived,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum OperationVersionState {
Draft,
Published,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum OperationLifecycleAction {
SaveDraft,
Publish,
Archive,
Delete,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
pub enum OperationLifecycleError {
#[error("operation is archived")]
Archived,
#[error("operation lifecycle transition is invalid")]
InvalidTransition,
#[error("operation deletion would destroy durable history")]
DurableHistory,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct OperationLifecycle {
pub availability: OperationAvailability,
pub current: OperationVersionState,
pub ever_published: bool,
pub has_durable_references: bool,
}
impl OperationLifecycle {
pub fn validate(self, action: OperationLifecycleAction) -> Result<(), OperationLifecycleError> {
if self.availability == OperationAvailability::Archived {
return match action {
OperationLifecycleAction::Archive => Ok(()),
_ => Err(OperationLifecycleError::Archived),
};
}
match action {
OperationLifecycleAction::SaveDraft => Ok(()),
OperationLifecycleAction::Publish if self.current == OperationVersionState::Draft => {
Ok(())
}
OperationLifecycleAction::Archive => Ok(()),
OperationLifecycleAction::Delete
if !self.ever_published && !self.has_durable_references =>
{
Ok(())
}
OperationLifecycleAction::Delete => Err(OperationLifecycleError::DurableHistory),
OperationLifecycleAction::Publish => Err(OperationLifecycleError::InvalidTransition),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct RestTarget {
pub base_url: String,
pub method: HttpMethod,
@@ -44,11 +110,13 @@ pub enum Target {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct RetryPolicy {
pub max_attempts: u32,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ResponseCachePolicy {
pub ttl_ms: u64,
}
@@ -62,6 +130,7 @@ pub enum IdempotencyMode {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct IdempotencyPolicy {
pub mode: IdempotencyMode,
pub ttl_ms: u64,
@@ -92,11 +161,13 @@ impl OperationSafetyClass {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ConfirmationPolicy {
pub ttl_ms: u64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OperationSafetyPolicy {
pub class: OperationSafetyClass,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -130,6 +201,7 @@ pub enum OperationApprovalMode {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct OperationApprovalPolicy {
pub required: bool,
#[serde(default)]
@@ -143,6 +215,7 @@ pub struct OperationApprovalPolicy {
}
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ExecutionConfig {
pub timeout_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
@@ -162,11 +235,13 @@ pub struct ExecutionConfig {
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolExample {
pub input: Value,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct ToolDescription {
pub title: String,
pub description: String,
@@ -284,6 +359,23 @@ impl<TSchema, TMapping> Operation<TSchema, TMapping> {
}
}
impl<TSchema: PartialEq, TMapping: PartialEq> Operation<TSchema, TMapping> {
pub fn portable_semantically_eq(&self, other: &Self) -> bool {
self.name == other.name
&& self.display_name == other.display_name
&& self.category == other.category
&& self.protocol == other.protocol
&& self.security_level == other.security_level
&& self.target == other.target
&& self.input_schema == other.input_schema
&& self.output_schema == other.output_schema
&& self.input_mapping == other.input_mapping
&& self.output_mapping == other.output_mapping
&& self.execution_config == other.execution_config
&& self.tool_description == other.tool_description
}
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
@@ -296,8 +388,9 @@ mod tests {
edition::OperationSecurityLevel,
ids::{AuthProfileId, OperationId},
operation::{
ConfigExport, ExecutionConfig, Operation, OperationStatus, RestTarget, Samples, Target,
ToolDescription, ToolExample,
ConfigExport, ExecutionConfig, Operation, OperationAvailability, OperationLifecycle,
OperationLifecycleAction, OperationLifecycleError, OperationStatus,
OperationVersionState, RestTarget, Samples, Target, ToolDescription, ToolExample,
},
protocol::{AuthKind, ExportMode, HttpMethod, Protocol},
};
@@ -335,6 +428,60 @@ mod tests {
);
}
#[test]
fn lifecycle_rejects_publish_rewind_and_durable_delete() {
let published = OperationLifecycle {
availability: OperationAvailability::Active,
current: OperationVersionState::Published,
ever_published: true,
has_durable_references: true,
};
assert_eq!(
published.validate(OperationLifecycleAction::Publish),
Err(OperationLifecycleError::InvalidTransition)
);
assert_eq!(
published.validate(OperationLifecycleAction::Delete),
Err(OperationLifecycleError::DurableHistory)
);
assert_eq!(
published.validate(OperationLifecycleAction::SaveDraft),
Ok(())
);
}
#[test]
fn archived_lifecycle_is_terminal_but_archive_retry_is_idempotent() {
let archived = OperationLifecycle {
availability: OperationAvailability::Archived,
current: OperationVersionState::Published,
ever_published: true,
has_durable_references: true,
};
assert_eq!(archived.validate(OperationLifecycleAction::Archive), Ok(()));
assert_eq!(
archived.validate(OperationLifecycleAction::SaveDraft),
Err(OperationLifecycleError::Archived)
);
}
#[test]
fn portable_semantic_equality_ignores_persistence_identity() {
let left = test_operation(OperationStatus::Draft);
let mut right = left.clone();
right.id = OperationId::new("op_other");
right.version = 9;
right.status = OperationStatus::Published;
right.updated_at = timestamp("2026-03-25T09:00:00Z");
right.published_at = Some(timestamp("2026-03-25T09:00:00Z"));
assert!(left.portable_semantically_eq(&right));
right.display_name = "Changed".to_owned();
assert!(!left.portable_semantically_eq(&right));
}
#[test]
fn auth_profile_serializes_secret_ids_without_secret_values() {
let profile = AuthProfile {