v0.3.9-pre.002

This commit is contained in:
2026-09-04 14:21:05 +02:00
parent 5c797827f7
commit 7d0710a1e6
17 changed files with 1235 additions and 22 deletions

View File

@@ -0,0 +1,14 @@
# file: crates/ksp-worker-api/Cargo.toml
# version: 1
[package]
name = "ksp-worker-api"
version.workspace = true
edition.workspace = true
repository.workspace = true
[dependencies]
ksp-core-lib = { path = "../ksp-core-lib" }
[lints]
workspace = true

View File

@@ -0,0 +1,12 @@
// file: crates/ksp-worker-api/src/error.rs
// version: 1
/// Error code used when a Worker identifier violates its bounded safe-code contract.
pub const ERROR_CODE_WORKER_ID_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("worker_api", "worker_id_invalid");
/// Error code used when a Worker kind code violates its bounded safe-code contract.
pub const ERROR_CODE_WORKER_KIND_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("worker_api", "worker_kind_invalid");
/// Error code used when a Worker snapshot sequence cannot advance without wrapping.
pub const ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED: ksp_core_lib::ErrorCode =
ksp_core_lib::ErrorCode::new("worker_api", "worker_snapshot_sequence_exhausted");
/// Error code used when a requested Worker lifecycle transition is not allowed.
pub const ERROR_CODE_WORKER_TRANSITION_INVALID: ksp_core_lib::ErrorCode = ksp_core_lib::ErrorCode::new("worker_api", "worker_transition_invalid");

View File

@@ -0,0 +1,76 @@
// file: crates/ksp-worker-api/src/identity.rs
// version: 1
/// Maximum UTF-8 byte length admitted for one Worker identifier.
pub const MAX_WORKER_ID_BYTES: usize = 128;
/// Maximum UTF-8 byte length admitted for one Worker kind code.
pub const MAX_WORKER_KIND_CODE_BYTES: usize = 128;
/// Bounded caller-supplied identity of one logical Worker instance.
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WorkerId(std::string::String);
impl WorkerId {
/// Creates one non-empty Worker identifier using the KSP safe-code alphabet.
pub fn new(value: impl std::convert::Into<std::string::String>) -> crate::Result<Self> {
let value = value.into();
if !valid_worker_code(value.as_str(), crate::MAX_WORKER_ID_BYTES) {
return std::result::Result::Err(identity_error(crate::ERROR_CODE_WORKER_ID_INVALID, "worker_id"));
}
return std::result::Result::Ok(Self(value));
}
/// Returns the validated Worker identifier.
#[must_use]
pub fn as_str(&self) -> &str {
return self.0.as_str();
}
}
impl std::fmt::Debug for WorkerId {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.write_str("WorkerId(..)");
}
}
/// Bounded stable code identifying one concrete family of Workers.
#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WorkerKindCode(std::string::String);
impl WorkerKindCode {
/// Creates one non-empty Worker kind using the KSP safe-code alphabet.
pub fn new(value: impl std::convert::Into<std::string::String>) -> crate::Result<Self> {
let value = value.into();
if !valid_worker_code(value.as_str(), crate::MAX_WORKER_KIND_CODE_BYTES) {
return std::result::Result::Err(identity_error(crate::ERROR_CODE_WORKER_KIND_INVALID, "worker_kind"));
}
return std::result::Result::Ok(Self(value));
}
/// Returns the validated stable Worker kind code.
#[must_use]
pub fn as_str(&self) -> &str {
return self.0.as_str();
}
}
impl std::fmt::Debug for WorkerKindCode {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_tuple("WorkerKindCode").field(&self.0).finish();
}
}
fn identity_error(code: crate::ErrorCode, field: &'static str) -> crate::Error {
return crate::Error::new(code, "invalid bounded Worker identity").with_context("field", field);
}
fn valid_worker_code(value: &str, maximum_len: usize) -> bool {
if value.is_empty() || value.len() > maximum_len {
return false;
}
return value.bytes().all(|byte| return byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b':'));
}
#[cfg(test)]
#[path = "../unit_tests/identity.rs"]
mod tests;

View File

@@ -0,0 +1,62 @@
// file: crates/ksp-worker-api/src/lib.rs
// version: 1
#![warn(missing_docs)]
#![deny(unreachable_pub)]
#![forbid(unsafe_code)]
//! Passive runtime-neutral lifecycle contracts for continuous KSP Workers.
//!
//! This foundation owns bounded Worker identity, explicit continuous lifecycle
//! transitions, cooperative stop intent and a fixed latest-value observation
//! contract. Concrete runtimes, restart policy, Transport, Store, Config, Jobs
//! and domain-specific Worker behavior remain outside this crate.
mod error;
mod identity;
mod lifecycle;
mod snapshot;
mod stop;
/// Error code used when a Worker identifier violates its bounded safe-code contract.
pub use self::error::ERROR_CODE_WORKER_ID_INVALID;
/// Error code used when a Worker kind code violates its bounded safe-code contract.
pub use self::error::ERROR_CODE_WORKER_KIND_INVALID;
/// Error code used when a Worker snapshot sequence cannot advance without wrapping.
pub use self::error::ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED;
/// Error code used when a requested Worker lifecycle transition is not allowed.
pub use self::error::ERROR_CODE_WORKER_TRANSITION_INVALID;
/// Maximum UTF-8 byte length admitted for one Worker identifier.
pub use self::identity::MAX_WORKER_ID_BYTES;
/// Maximum UTF-8 byte length admitted for one Worker kind code.
pub use self::identity::MAX_WORKER_KIND_CODE_BYTES;
/// Bounded caller-supplied identity of one logical Worker instance.
pub use self::identity::WorkerId;
/// Bounded stable code identifying one concrete family of Workers.
pub use self::identity::WorkerKindCode;
/// Minimal generic activity classification for a continuous Worker.
pub use self::lifecycle::WorkerActivity;
/// Operational health classification independent from Worker lifecycle phase.
pub use self::lifecycle::WorkerHealth;
/// Passive owner of one Worker identity and its validated lifecycle state.
pub use self::lifecycle::WorkerLifecycle;
/// Current lifecycle state of one continuous Worker.
pub use self::lifecycle::WorkerState;
/// Fixed common latest-value snapshot exposed by every Worker implementation.
pub use self::snapshot::WorkerSnapshot;
/// Runtime-neutral future returned while observing a latest-value Worker snapshot source.
pub use self::snapshot::WorkerSnapshotFuture;
/// Monotone sequence attached to one latest-value Worker snapshot stream.
pub use self::snapshot::WorkerSnapshotSequence;
/// Runtime-neutral read and change-wait contract for one latest-value Worker snapshot stream.
pub use self::snapshot::WorkerSnapshotSource;
/// Runtime-neutral cloneable token carrying cooperative Worker stop intent.
pub use self::stop::WorkerStopToken;
/// Common KSP error type used by Worker-facing contracts.
pub use ksp_core_lib::Error;
/// Stable structured code identifying a KSP error category and condition.
pub use ksp_core_lib::ErrorCode;
/// Structured contextual field attached to a KSP error.
pub use ksp_core_lib::ErrorContext;
/// Common KSP result alias using [`Error`].
pub use ksp_core_lib::Result;

View File

@@ -0,0 +1,199 @@
// file: crates/ksp-worker-api/src/lifecycle.rs
// version: 1
/// Current lifecycle state of one continuous Worker.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum WorkerState {
/// The Worker exists but has not started initialization.
Created,
/// The Worker is initializing resources before entering steady service.
Starting,
/// The Worker is actively providing its continuous service.
Running,
/// The Worker observed stop intent and is draining or releasing resources.
Stopping,
/// The Worker reached its normal terminal stopped state.
Stopped,
/// The Worker reached a terminal fault classified by one stable KSP error code.
Faulted(crate::ErrorCode),
}
impl WorkerState {
/// Returns the stable safe lifecycle code without rendering Worker data.
#[must_use]
pub const fn code(&self) -> &'static str {
return match self {
Self::Created => "created",
Self::Starting => "starting",
Self::Running => "running",
Self::Stopping => "stopping",
Self::Stopped => "stopped",
Self::Faulted(_) => "faulted",
};
}
/// Returns the terminal fault code when the state is [`Self::Faulted`].
#[must_use]
pub const fn fault_code(&self) -> std::option::Option<crate::ErrorCode> {
return match self {
Self::Faulted(code) => std::option::Option::Some(*code),
_ => std::option::Option::None,
};
}
/// Reports whether no later lifecycle transition is permitted.
#[must_use]
pub const fn is_terminal(&self) -> bool {
return matches!(self, Self::Stopped | Self::Faulted(_));
}
}
/// Operational health classification independent from Worker lifecycle phase.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum WorkerHealth {
/// Health has not yet been established or cannot currently be classified.
Unknown,
/// The Worker is operating within its expected healthy envelope.
Healthy,
/// The Worker is operating with a known degradation while service remains available.
Degraded,
/// The Worker is currently unable to satisfy its expected service health contract.
Unhealthy,
}
impl WorkerHealth {
/// Returns the stable safe code for this health classification.
#[must_use]
pub const fn code(&self) -> &'static str {
return match self {
Self::Unknown => "unknown",
Self::Healthy => "healthy",
Self::Degraded => "degraded",
Self::Unhealthy => "unhealthy",
};
}
}
/// Minimal generic activity classification for a continuous Worker.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
#[non_exhaustive]
pub enum WorkerActivity {
/// Activity has not yet been established or cannot currently be classified.
Unknown,
/// The Worker is alive but not currently processing concrete work.
Idle,
/// The Worker is currently processing concrete work.
Active,
}
impl WorkerActivity {
/// Returns the stable safe code for this activity classification.
#[must_use]
pub const fn code(&self) -> &'static str {
return match self {
Self::Unknown => "unknown",
Self::Idle => "idle",
Self::Active => "active",
};
}
}
/// Passive owner of one Worker identity and its validated lifecycle state.
#[derive(Eq, PartialEq)]
pub struct WorkerLifecycle {
id: crate::WorkerId,
kind: crate::WorkerKindCode,
state: crate::WorkerState,
}
impl WorkerLifecycle {
/// Creates one lifecycle in [`WorkerState::Created`] state.
#[must_use]
pub const fn new(id: crate::WorkerId, kind: crate::WorkerKindCode) -> Self {
return Self { id, kind, state: crate::WorkerState::Created };
}
/// Returns the logical Worker identity.
#[must_use]
pub const fn id(&self) -> &crate::WorkerId {
return &self.id;
}
/// Returns the stable Worker family code.
#[must_use]
pub const fn kind(&self) -> &crate::WorkerKindCode {
return &self.kind;
}
/// Returns the current lifecycle state.
#[must_use]
pub const fn state(&self) -> crate::WorkerState {
return self.state;
}
/// Begins initialization of a newly created Worker.
pub fn start(&mut self) -> crate::Result<()> {
return self.transition(crate::WorkerState::Starting);
}
/// Records that initialization completed and steady service is running.
pub fn mark_running(&mut self) -> crate::Result<()> {
return self.transition(crate::WorkerState::Running);
}
/// Records that a starting or running Worker observed cooperative stop intent.
pub fn mark_stopping(&mut self) -> crate::Result<()> {
return self.transition(crate::WorkerState::Stopping);
}
/// Records normal terminal stop after draining, or before initialization began.
pub fn mark_stopped(&mut self) -> crate::Result<()> {
return self.transition(crate::WorkerState::Stopped);
}
/// Records a terminal fault using only a stable KSP error code.
pub fn fault(&mut self, code: crate::ErrorCode) -> crate::Result<()> {
return self.transition(crate::WorkerState::Faulted(code));
}
fn transition(&mut self, target: crate::WorkerState) -> crate::Result<()> {
if !allowed_transition(self.state, target) {
return std::result::Result::Err(transition_error(self.state, target));
}
self.state = target;
return std::result::Result::Ok(());
}
}
impl std::fmt::Debug for WorkerLifecycle {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("WorkerLifecycle").field("id", &self.id).field("kind", &self.kind).field("state", &self.state).finish();
}
}
fn allowed_transition(source: crate::WorkerState, target: crate::WorkerState) -> bool {
return matches!(
(source, target),
(crate::WorkerState::Created, crate::WorkerState::Starting)
| (crate::WorkerState::Created, crate::WorkerState::Stopped)
| (crate::WorkerState::Starting, crate::WorkerState::Running)
| (crate::WorkerState::Starting, crate::WorkerState::Stopping)
| (crate::WorkerState::Starting, crate::WorkerState::Faulted(_))
| (crate::WorkerState::Running, crate::WorkerState::Stopping)
| (crate::WorkerState::Running, crate::WorkerState::Faulted(_))
| (crate::WorkerState::Stopping, crate::WorkerState::Stopped)
| (crate::WorkerState::Stopping, crate::WorkerState::Faulted(_))
);
}
fn transition_error(source: crate::WorkerState, target: crate::WorkerState) -> crate::Error {
return crate::Error::new(crate::ERROR_CODE_WORKER_TRANSITION_INVALID, "invalid Worker lifecycle transition")
.with_context("source_state", source.code())
.with_context("target_state", target.code());
}
#[cfg(test)]
#[path = "../unit_tests/lifecycle.rs"]
mod tests;

View File

@@ -0,0 +1,138 @@
// file: crates/ksp-worker-api/src/snapshot.rs
// version: 1
/// Monotone sequence attached to one latest-value Worker snapshot stream.
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct WorkerSnapshotSequence(u64);
impl WorkerSnapshotSequence {
/// Creates the initial sequence position for one Worker snapshot stream.
#[must_use]
pub const fn initial() -> Self {
return Self(0);
}
/// Returns the opaque numeric position carried by this sequence.
#[must_use]
pub const fn value(&self) -> u64 {
return self.0;
}
/// Advances the sequence exactly once or reports exhaustion without wrapping.
pub fn next(&self) -> crate::Result<Self> {
let next = match self.0.checked_add(1) {
std::option::Option::Some(value) => value,
std::option::Option::None => {
return std::result::Result::Err(
crate::Error::new(crate::ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED, "Worker snapshot sequence exhausted")
.with_context("sequence", self.0.to_string()),
);
},
};
return std::result::Result::Ok(Self(next));
}
/// Reports whether this sequence is strictly newer than an observed sequence.
#[must_use]
pub const fn is_after(&self, observed: Self) -> bool {
return self.0 > observed.0;
}
}
/// Fixed common latest-value snapshot exposed by every Worker implementation.
#[derive(Clone, Eq, PartialEq)]
pub struct WorkerSnapshot {
id: crate::WorkerId,
kind: crate::WorkerKindCode,
sequence: crate::WorkerSnapshotSequence,
state: crate::WorkerState,
health: crate::WorkerHealth,
activity: crate::WorkerActivity,
}
impl WorkerSnapshot {
/// Creates one immutable common Worker snapshot from already validated values.
#[must_use]
pub const fn new(
id: crate::WorkerId,
kind: crate::WorkerKindCode,
sequence: crate::WorkerSnapshotSequence,
state: crate::WorkerState,
health: crate::WorkerHealth,
activity: crate::WorkerActivity,
) -> Self {
return Self { id, kind, sequence, state, health, activity };
}
/// Returns the logical Worker identity.
#[must_use]
pub const fn id(&self) -> &crate::WorkerId {
return &self.id;
}
/// Returns the stable Worker family code.
#[must_use]
pub const fn kind(&self) -> &crate::WorkerKindCode {
return &self.kind;
}
/// Returns the monotone sequence of this latest value.
#[must_use]
pub const fn sequence(&self) -> crate::WorkerSnapshotSequence {
return self.sequence;
}
/// Returns the lifecycle state represented by this snapshot.
#[must_use]
pub const fn state(&self) -> crate::WorkerState {
return self.state;
}
/// Returns the operational health represented by this snapshot.
#[must_use]
pub const fn health(&self) -> crate::WorkerHealth {
return self.health;
}
/// Returns the generic activity represented by this snapshot.
#[must_use]
pub const fn activity(&self) -> crate::WorkerActivity {
return self.activity;
}
}
impl std::fmt::Debug for WorkerSnapshot {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter
.debug_struct("WorkerSnapshot")
.field("id", &self.id)
.field("kind", &self.kind)
.field("sequence", &self.sequence)
.field("state", &self.state)
.field("health", &self.health)
.field("activity", &self.activity)
.finish();
}
}
/// Runtime-neutral future returned while observing a latest-value Worker snapshot source.
pub type WorkerSnapshotFuture<'a> = std::pin::Pin<std::boxed::Box<dyn std::future::Future<Output = crate::WorkerSnapshot> + std::marker::Send + 'a>>;
/// Runtime-neutral read and change-wait contract for one latest-value Worker snapshot stream.
pub trait WorkerSnapshotSource: std::marker::Send + std::marker::Sync {
/// Returns the complete current common Worker snapshot without replaying prior updates.
#[must_use]
fn current(&self) -> crate::WorkerSnapshot;
/// Waits for a snapshot newer than `observed`, returning the complete current value after coalescing intermediate updates.
fn wait_for_change(&self, observed: crate::WorkerSnapshotSequence) -> crate::WorkerSnapshotFuture<'_>;
}
#[cfg(test)]
fn exhausted_snapshot_sequence() -> crate::WorkerSnapshotSequence {
return WorkerSnapshotSequence(u64::MAX);
}
#[cfg(test)]
#[path = "../unit_tests/snapshot.rs"]
mod tests;

View File

@@ -0,0 +1,44 @@
// file: crates/ksp-worker-api/src/stop.rs
// version: 1
/// Runtime-neutral cloneable token carrying cooperative Worker stop intent.
#[derive(Clone)]
pub struct WorkerStopToken {
requested: std::sync::Arc<std::sync::atomic::AtomicBool>,
}
impl WorkerStopToken {
/// Creates a token with no stop request.
#[must_use]
pub fn new() -> Self {
return Self { requested: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)) };
}
/// Requests stop and returns `true` only for the first request shared by all clones.
#[must_use]
pub fn request_stop(&self) -> bool {
return !self.requested.swap(true, std::sync::atomic::Ordering::AcqRel);
}
/// Reports whether stop has been requested through any clone.
#[must_use]
pub fn is_stop_requested(&self) -> bool {
return self.requested.load(std::sync::atomic::Ordering::Acquire);
}
}
impl std::default::Default for WorkerStopToken {
fn default() -> Self {
return Self::new();
}
}
impl std::fmt::Debug for WorkerStopToken {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
return formatter.debug_struct("WorkerStopToken").field("stop_requested", &self.is_stop_requested()).finish();
}
}
#[cfg(test)]
#[path = "../unit_tests/stop.rs"]
mod tests;

View File

@@ -0,0 +1,82 @@
// file: crates/ksp-worker-api/tests/dependency_boundary.rs
// version: 1
//! Dependency and runtime-neutrality canaries for the Worker API foundation.
#[test]
fn pre_002_manifest_has_exact_core_only_dependency_graph() {
let manifest = include_str!("../Cargo.toml");
assert!(!manifest.contains("[features]"));
assert!(!manifest.contains("[dev-dependencies]"));
assert!(!manifest.contains("[build-dependencies]"));
assert_eq!(manifest.matches("[dependencies]").count(), 1);
let dependencies_tail = manifest.split("[dependencies]").nth(1);
assert!(dependencies_tail.is_some());
let dependencies_tail = match dependencies_tail {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let dependencies = match dependencies_tail.split("[lints]").next() {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert_eq!(manifest_dependency_names(dependencies), std::vec!["ksp-core-lib"]);
return;
}
#[test]
fn pre_002_production_sources_forbid_job_runtime_domain_and_wire_dependencies() {
let sources = [
include_str!("../src/error.rs"),
include_str!("../src/identity.rs"),
include_str!("../src/lib.rs"),
include_str!("../src/lifecycle.rs"),
include_str!("../src/snapshot.rs"),
include_str!("../src/stop.rs"),
];
for source in sources {
for forbidden in [
"ksp_config_lib::",
"ksp_interface_lib::",
"ksp_job_api::",
"ksp_logging_lib::",
"ksp_offchain_transport_lib::",
"ksp_onchain_transport_lib::",
"ksp_store_api::",
"ksp_store_lib::",
"reqwest::",
"serde::",
"serde_json::",
"solana_",
"tauri::",
"tokio::",
"tonic::",
concat!("tracing", "::"),
] {
assert!(!source.contains(forbidden), "forbidden Worker API dependency path detected: {forbidden}");
}
}
return;
}
fn manifest_dependency_names(section: &str) -> std::vec::Vec<&str> {
let mut names = std::vec::Vec::new();
for line in section.lines() {
let content = match line.split('#').next() {
std::option::Option::Some(value) => value.trim(),
std::option::Option::None => continue,
};
if content.is_empty() {
continue;
}
let name = match content.split('=').next() {
std::option::Option::Some(value) => value.trim().trim_end_matches(".workspace"),
std::option::Option::None => continue,
};
if !name.is_empty() {
names.push(name);
}
}
names.sort_unstable();
return names;
}

View File

@@ -0,0 +1,52 @@
// file: crates/ksp-worker-api/tests/public_api.rs
// version: 1
//! External-consumer canaries for the Worker API crate-root facade.
#[test]
fn pre_002_identity_lifecycle_stop_and_snapshot_are_consumable_from_crate_root() {
let id = ksp_worker_api::WorkerId::new("worker-public-001");
let kind = ksp_worker_api::WorkerKindCode::new("example_worker");
assert!(id.is_ok());
assert!(kind.is_ok());
let id = match id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let kind = match kind {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let mut lifecycle = ksp_worker_api::WorkerLifecycle::new(id.clone(), kind.clone());
assert_eq!(lifecycle.state(), ksp_worker_api::WorkerState::Created);
assert!(lifecycle.start().is_ok());
assert!(lifecycle.mark_running().is_ok());
let stop = ksp_worker_api::WorkerStopToken::new();
assert!(stop.request_stop());
assert!(lifecycle.mark_stopping().is_ok());
let snapshot = ksp_worker_api::WorkerSnapshot::new(
id,
kind,
ksp_worker_api::WorkerSnapshotSequence::initial(),
lifecycle.state(),
ksp_worker_api::WorkerHealth::Healthy,
ksp_worker_api::WorkerActivity::Idle,
);
assert_eq!(snapshot.state(), ksp_worker_api::WorkerState::Stopping);
assert_eq!(snapshot.health(), ksp_worker_api::WorkerHealth::Healthy);
assert_eq!(snapshot.activity(), ksp_worker_api::WorkerActivity::Idle);
return;
}
#[test]
fn pre_002_public_bounds_and_error_codes_are_exact() {
assert_eq!(ksp_worker_api::MAX_WORKER_ID_BYTES, 128);
assert_eq!(ksp_worker_api::MAX_WORKER_KIND_CODE_BYTES, 128);
assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_ID_INVALID.domain(), "worker_api");
assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_ID_INVALID.code(), "worker_id_invalid");
assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_KIND_INVALID.domain(), "worker_api");
assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_KIND_INVALID.code(), "worker_kind_invalid");
assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_TRANSITION_INVALID.code(), "worker_transition_invalid");
assert_eq!(ksp_worker_api::ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED.code(), "worker_snapshot_sequence_exhausted");
return;
}

View File

@@ -0,0 +1,64 @@
// file: crates/ksp-worker-api/unit_tests/identity.rs
// version: 1
#[test]
fn pre_002_identity_accepts_safe_codes_at_exact_bounds() {
let worker_id = crate::WorkerId::new("a".repeat(crate::MAX_WORKER_ID_BYTES));
let kind = crate::WorkerKindCode::new("continuous.raw:example-1");
assert!(worker_id.is_ok());
assert!(kind.is_ok());
let worker_id = match worker_id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let kind = match kind {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(worker_id.as_str().len(), crate::MAX_WORKER_ID_BYTES);
assert_eq!(kind.as_str(), "continuous.raw:example-1");
return;
}
#[test]
fn pre_002_identity_rejects_empty_oversized_and_unsafe_values() {
for value in [
std::string::String::new(),
"a".repeat(crate::MAX_WORKER_ID_BYTES + 1),
"unsafe/value".to_string(),
"unsafe\\value".to_string(),
"space value".to_string(),
"unicode-é".to_string(),
] {
let rejected = crate::WorkerId::new(value);
assert!(rejected.is_err());
let error = match rejected {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => continue,
};
assert_eq!(error.code(), crate::ERROR_CODE_WORKER_ID_INVALID);
assert_eq!(error.context().len(), 1);
assert_eq!(error.context()[0].key(), "field");
assert_eq!(error.context()[0].value(), "worker_id");
}
for value in [std::string::String::new(), "b".repeat(crate::MAX_WORKER_KIND_CODE_BYTES + 1), "worker kind".to_string()] {
let rejected = crate::WorkerKindCode::new(value);
assert!(rejected.is_err());
let error = match rejected {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => continue,
};
assert_eq!(error.code(), crate::ERROR_CODE_WORKER_KIND_INVALID);
assert_eq!(error.context()[0].value(), "worker_kind");
}
return;
}
#[test]
fn pre_002_private_validator_uses_exact_safe_code_alphabet() {
assert!(super::valid_worker_code("AZaz09_.:-", 10));
for rejected in ["slash/value", "back\\slash", "space value", "line\nbreak", "é"] {
assert!(!super::valid_worker_code(rejected, crate::MAX_WORKER_ID_BYTES));
}
return;
}

View File

@@ -0,0 +1,151 @@
// file: crates/ksp-worker-api/unit_tests/lifecycle.rs
// version: 1
const TEST_FAULT: crate::ErrorCode = crate::ErrorCode::new("worker_test", "fault");
fn new_lifecycle() -> std::option::Option<crate::WorkerLifecycle> {
let id = match crate::WorkerId::new("worker-001") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
let kind = match crate::WorkerKindCode::new("continuous_worker") {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return std::option::Option::None,
};
return std::option::Option::Some(crate::WorkerLifecycle::new(id, kind));
}
#[test]
fn pre_002_lifecycle_accepts_every_planned_terminal_path() {
let lifecycle = new_lifecycle();
assert!(lifecycle.is_some());
let mut lifecycle = match lifecycle {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert!(lifecycle.mark_stopped().is_ok());
assert_eq!(lifecycle.state(), crate::WorkerState::Stopped);
assert!(lifecycle.state().is_terminal());
let lifecycle = new_lifecycle();
assert!(lifecycle.is_some());
let mut lifecycle = match lifecycle {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert!(lifecycle.start().is_ok());
assert!(lifecycle.mark_running().is_ok());
assert!(lifecycle.mark_stopping().is_ok());
assert!(lifecycle.mark_stopped().is_ok());
assert_eq!(lifecycle.state(), crate::WorkerState::Stopped);
for fault_from_running in [false, true] {
let lifecycle = new_lifecycle();
assert!(lifecycle.is_some());
let mut lifecycle = match lifecycle {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert!(lifecycle.start().is_ok());
if fault_from_running {
assert!(lifecycle.mark_running().is_ok());
}
assert!(lifecycle.fault(TEST_FAULT).is_ok());
assert_eq!(lifecycle.state(), crate::WorkerState::Faulted(TEST_FAULT));
assert_eq!(lifecycle.state().fault_code(), std::option::Option::Some(TEST_FAULT));
}
return;
}
#[test]
fn pre_002_stopping_allows_normal_stop_or_fault() {
for terminal in [crate::WorkerState::Stopped, crate::WorkerState::Faulted(TEST_FAULT)] {
let lifecycle = new_lifecycle();
assert!(lifecycle.is_some());
let mut lifecycle = match lifecycle {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
assert!(lifecycle.start().is_ok());
assert!(lifecycle.mark_running().is_ok());
assert!(lifecycle.mark_stopping().is_ok());
let result = match terminal {
crate::WorkerState::Stopped => lifecycle.mark_stopped(),
crate::WorkerState::Faulted(code) => lifecycle.fault(code),
_ => return,
};
assert!(result.is_ok());
assert_eq!(lifecycle.state(), terminal);
}
return;
}
#[test]
fn pre_002_invalid_transition_preserves_source_state_and_reports_safe_context() {
let lifecycle = new_lifecycle();
assert!(lifecycle.is_some());
let mut lifecycle = match lifecycle {
std::option::Option::Some(value) => value,
std::option::Option::None => return,
};
let rejected = lifecycle.mark_running();
assert!(rejected.is_err());
assert_eq!(lifecycle.state(), crate::WorkerState::Created);
let error = match rejected {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => return,
};
assert_eq!(error.code(), crate::ERROR_CODE_WORKER_TRANSITION_INVALID);
assert_eq!(error.context().len(), 2);
assert_eq!(error.context()[0].key(), "source_state");
assert_eq!(error.context()[0].value(), "created");
assert_eq!(error.context()[1].key(), "target_state");
assert_eq!(error.context()[1].value(), "running");
return;
}
#[test]
fn pre_002_transition_matrix_is_exact() {
let states = [
crate::WorkerState::Created,
crate::WorkerState::Starting,
crate::WorkerState::Running,
crate::WorkerState::Stopping,
crate::WorkerState::Stopped,
crate::WorkerState::Faulted(TEST_FAULT),
];
for source in states {
for target in states {
let expected = matches!(
(source, target),
(crate::WorkerState::Created, crate::WorkerState::Starting)
| (crate::WorkerState::Created, crate::WorkerState::Stopped)
| (crate::WorkerState::Starting, crate::WorkerState::Running)
| (crate::WorkerState::Starting, crate::WorkerState::Stopping)
| (crate::WorkerState::Starting, crate::WorkerState::Faulted(_))
| (crate::WorkerState::Running, crate::WorkerState::Stopping)
| (crate::WorkerState::Running, crate::WorkerState::Faulted(_))
| (crate::WorkerState::Stopping, crate::WorkerState::Stopped)
| (crate::WorkerState::Stopping, crate::WorkerState::Faulted(_))
);
assert_eq!(super::allowed_transition(source, target), expected, "unexpected transition matrix cell: {source:?} -> {target:?}");
}
}
return;
}
#[test]
fn pre_002_health_activity_and_state_codes_are_stable() {
assert_eq!(crate::WorkerState::Created.code(), "created");
assert_eq!(crate::WorkerState::Starting.code(), "starting");
assert_eq!(crate::WorkerState::Running.code(), "running");
assert_eq!(crate::WorkerState::Stopping.code(), "stopping");
assert_eq!(crate::WorkerState::Stopped.code(), "stopped");
assert_eq!(crate::WorkerState::Faulted(TEST_FAULT).code(), "faulted");
assert_eq!(crate::WorkerHealth::Unknown.code(), "unknown");
assert_eq!(crate::WorkerHealth::Healthy.code(), "healthy");
assert_eq!(crate::WorkerHealth::Degraded.code(), "degraded");
assert_eq!(crate::WorkerHealth::Unhealthy.code(), "unhealthy");
assert_eq!(crate::WorkerActivity::Unknown.code(), "unknown");
assert_eq!(crate::WorkerActivity::Idle.code(), "idle");
assert_eq!(crate::WorkerActivity::Active.code(), "active");
return;
}

View File

@@ -0,0 +1,66 @@
// file: crates/ksp-worker-api/unit_tests/snapshot.rs
// version: 1
#[test]
fn pre_002_snapshot_sequence_advances_strictly_and_orders_positions() {
let first = crate::WorkerSnapshotSequence::initial();
assert_eq!(first.value(), 0);
let second = first.next();
assert!(second.is_ok());
let second = match second {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
assert_eq!(second.value(), 1);
assert!(second.is_after(first));
assert!(!first.is_after(second));
assert!(!first.is_after(first));
return;
}
#[test]
fn pre_002_snapshot_sequence_exhaustion_is_explicit_and_non_wrapping() {
let exhausted = super::exhausted_snapshot_sequence();
let result = exhausted.next();
assert!(result.is_err());
let error = match result {
std::result::Result::Err(value) => value,
std::result::Result::Ok(_) => return,
};
assert_eq!(error.code(), crate::ERROR_CODE_WORKER_SNAPSHOT_SEQUENCE_EXHAUSTED);
assert_eq!(error.context().len(), 1);
assert_eq!(error.context()[0].key(), "sequence");
assert_eq!(error.context()[0].value(), u64::MAX.to_string());
return;
}
#[test]
fn pre_002_snapshot_preserves_exact_common_dimensions() {
let id = crate::WorkerId::new("worker-001");
let kind = crate::WorkerKindCode::new("example_worker");
assert!(id.is_ok());
assert!(kind.is_ok());
let id = match id {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let kind = match kind {
std::result::Result::Ok(value) => value,
std::result::Result::Err(_) => return,
};
let snapshot = crate::WorkerSnapshot::new(
id,
kind,
crate::WorkerSnapshotSequence::initial(),
crate::WorkerState::Running,
crate::WorkerHealth::Healthy,
crate::WorkerActivity::Active,
);
assert_eq!(snapshot.id().as_str(), "worker-001");
assert_eq!(snapshot.kind().as_str(), "example_worker");
assert_eq!(snapshot.sequence().value(), 0);
assert_eq!(snapshot.state(), crate::WorkerState::Running);
assert_eq!(snapshot.health(), crate::WorkerHealth::Healthy);
assert_eq!(snapshot.activity(), crate::WorkerActivity::Active);
return;
}

View File

@@ -0,0 +1,22 @@
// file: crates/ksp-worker-api/unit_tests/stop.rs
// version: 1
#[test]
fn pre_002_stop_token_is_shared_and_idempotent() {
let token = crate::WorkerStopToken::new();
let clone = token.clone();
assert!(!token.is_stop_requested());
assert!(clone.request_stop());
assert!(token.is_stop_requested());
assert!(!token.request_stop());
assert!(!clone.request_stop());
return;
}
#[test]
fn pre_002_default_stop_token_starts_without_stop_request() {
let token = crate::WorkerStopToken::default();
assert!(!token.is_stop_requested());
assert_eq!(std::format!("{token:?}"), "WorkerStopToken { stop_requested: false }");
return;
}