@feltdb/core 0.4.12 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands.js +5 -2
- package/dist/cli/index.js +1 -1
- package/dist/create/cli.js +16 -1
- package/dist/create/create.js +30 -16
- package/dist/create/docker-compose-generator.js +48 -9
- package/dist/create/package-versions.js +1 -1
- package/dist/create/server-source/Cargo.lock +2238 -0
- package/dist/create/server-source/Cargo.toml +7 -0
- package/dist/create/server-source/crates/feltdb/Cargo.lock +175 -0
- package/dist/create/server-source/crates/feltdb/Cargo.toml +79 -0
- package/dist/create/server-source/crates/feltdb/benches/baselines/gate-13-redux.json +370 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_baseline.rs +589 -0
- package/dist/create/server-source/crates/feltdb/benches/gate13_phase_7_1_release_economics.rs +259 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_redux.rs +446 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_13_regression_runner.rs +272 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_concurrent_writer_scaling.rs +378 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_production_admission_revalidation.rs +414 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc2_admission_contract.rs +486 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_rc_root_cause.rs +273 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync1_queued_prototype.rs +587 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14a_sync_economics.rs +513 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14b_causal_backlog_scaling.rs +395 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_contract_test.rs +469 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14c_replication_scaling.rs +409 -0
- package/dist/create/server-source/crates/feltdb/benches/gate_14d_combined_dimension_scaling.rs +627 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_2_optimization_benchmark.rs +383 -0
- package/dist/create/server-source/crates/feltdb/benches/phase_7_1_3_crossover_analysis.rs +298 -0
- package/dist/create/server-source/crates/feltdb/src/acceptance_tests.rs +1698 -0
- package/dist/create/server-source/crates/feltdb/src/acquisition.rs +286 -0
- package/dist/create/server-source/crates/feltdb/src/admission.rs +192 -0
- package/dist/create/server-source/crates/feltdb/src/admission_contract_tests.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/adversarial_transport.rs +566 -0
- package/dist/create/server-source/crates/feltdb/src/analytics.rs +475 -0
- package/dist/create/server-source/crates/feltdb/src/application.rs +2244 -0
- package/dist/create/server-source/crates/feltdb/src/application_runtime.rs +1070 -0
- package/dist/create/server-source/crates/feltdb/src/authorization.rs +1030 -0
- package/dist/create/server-source/crates/feltdb/src/bin/feltdb_node.rs +266 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/mod.rs +8 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/search.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/capabilities/vector.rs +482 -0
- package/dist/create/server-source/crates/feltdb/src/capability.rs +903 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_diagnostics.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/cardinality_endpoint.rs +78 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier.rs +2214 -0
- package/dist/create/server-source/crates/feltdb/src/causal_dependency_barrier_phase_7_1.rs +194 -0
- package/dist/create/server-source/crates/feltdb/src/concurrency_fuzzing.rs +427 -0
- package/dist/create/server-source/crates/feltdb/src/consistency_contract.rs +453 -0
- package/dist/create/server-source/crates/feltdb/src/content_distribution.rs +465 -0
- package/dist/create/server-source/crates/feltdb/src/convergence.rs +618 -0
- package/dist/create/server-source/crates/feltdb/src/crash_atomic_boundary.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/crash_injection.rs +380 -0
- package/dist/create/server-source/crates/feltdb/src/crash_recovery_tests.rs +362 -0
- package/dist/create/server-source/crates/feltdb/src/cron.rs +294 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_indexing.rs +474 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_tests.rs +1003 -0
- package/dist/create/server-source/crates/feltdb/src/distributed_transactions.rs +536 -0
- package/dist/create/server-source/crates/feltdb/src/durability_guarantees.rs +364 -0
- package/dist/create/server-source/crates/feltdb/src/durable_dedup_set.rs +235 -0
- package/dist/create/server-source/crates/feltdb/src/durable_operation_log.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/durable_sync.rs +316 -0
- package/dist/create/server-source/crates/feltdb/src/execution.rs +426 -0
- package/dist/create/server-source/crates/feltdb/src/in_process_transport.rs +219 -0
- package/dist/create/server-source/crates/feltdb/src/indexing.rs +779 -0
- package/dist/create/server-source/crates/feltdb/src/lib.rs +2838 -0
- package/dist/create/server-source/crates/feltdb/src/materialization.rs +184 -0
- package/dist/create/server-source/crates/feltdb/src/metrics.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/multi_node_convergence.rs +311 -0
- package/dist/create/server-source/crates/feltdb/src/observability.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/operation.rs +251 -0
- package/dist/create/server-source/crates/feltdb/src/operation_algebra.rs +438 -0
- package/dist/create/server-source/crates/feltdb/src/operation_log.rs +344 -0
- package/dist/create/server-source/crates/feltdb/src/partition_reconciliation.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/peer_registry.rs +166 -0
- package/dist/create/server-source/crates/feltdb/src/permutation_scheduler.rs +261 -0
- package/dist/create/server-source/crates/feltdb/src/persistence_reality.rs +560 -0
- package/dist/create/server-source/crates/feltdb/src/phase1b_acceptance.rs +3226 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c1_acceptance.rs +201 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c2_acceptance.rs +263 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c3_acceptance.rs +484 -0
- package/dist/create/server-source/crates/feltdb/src/phase1c_atomicity_proof.rs +216 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_integration.rs +281 -0
- package/dist/create/server-source/crates/feltdb/src/phase5_scenarios.rs +323 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_adversarial_scenarios.rs +573 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_convergence_validator.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/phase6_persistence.rs +418 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_real_tcp.rs +381 -0
- package/dist/create/server-source/crates/feltdb/src/phase_1c_three_node.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2a_failures.rs +334 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2b_network.rs +306 -0
- package/dist/create/server-source/crates/feltdb/src/phase_2c_cascading.rs +355 -0
- package/dist/create/server-source/crates/feltdb/src/phase_3_durability.rs +395 -0
- package/dist/create/server-source/crates/feltdb/src/phase_4_baseline.rs +346 -0
- package/dist/create/server-source/crates/feltdb/src/phase_5_soak.rs +430 -0
- package/dist/create/server-source/crates/feltdb/src/production_api.rs +400 -0
- package/dist/create/server-source/crates/feltdb/src/provenance.rs +207 -0
- package/dist/create/server-source/crates/feltdb/src/query_performance.rs +217 -0
- package/dist/create/server-source/crates/feltdb/src/references.rs +404 -0
- package/dist/create/server-source/crates/feltdb/src/replay_fuzzing.rs +401 -0
- package/dist/create/server-source/crates/feltdb/src/replication_manager.rs +160 -0
- package/dist/create/server-source/crates/feltdb/src/replication_protocol.rs +132 -0
- package/dist/create/server-source/crates/feltdb/src/routing.rs +409 -0
- package/dist/create/server-source/crates/feltdb/src/sharding.rs +523 -0
- package/dist/create/server-source/crates/feltdb/src/state_contract.rs +3118 -0
- package/dist/create/server-source/crates/feltdb/src/state_hash.rs +291 -0
- package/dist/create/server-source/crates/feltdb/src/state_transition_store.rs +376 -0
- package/dist/create/server-source/crates/feltdb/src/storage.rs +267 -0
- package/dist/create/server-source/crates/feltdb/src/submission.rs +477 -0
- package/dist/create/server-source/crates/feltdb/src/sync.rs +483 -0
- package/dist/create/server-source/crates/feltdb/src/sync_contract.rs +822 -0
- package/dist/create/server-source/crates/feltdb/src/tcp_transport.rs +366 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_api.rs +473 -0
- package/dist/create/server-source/crates/feltdb/src/transaction_invariants.rs +949 -0
- package/dist/create/server-source/crates/feltdb/src/transactions.rs +646 -0
- package/dist/create/server-source/crates/feltdb/src/trigger.rs +390 -0
- package/dist/create/server-source/crates/feltdb/src/worker_mesh.rs +928 -0
- package/dist/create/server-source/crates/feltdb/src/workflow.rs +713 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_acceptance_tests.rs +510 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_integration.rs +354 -0
- package/dist/create/server-source/crates/feltdb/src/workflow_runtime.rs +594 -0
- package/dist/create/server-source/crates/feltdb/src/workload.rs +1310 -0
- package/dist/create/server-source/crates/feltdb-server/Cargo.toml +23 -0
- package/dist/create/server-source/crates/feltdb-server/README.md +79 -0
- package/dist/create/server-source/crates/feltdb-server/src/app_state.rs +73 -0
- package/dist/create/server-source/crates/feltdb-server/src/application_contract.rs +425 -0
- package/dist/create/server-source/crates/feltdb-server/src/artifacts.rs +449 -0
- package/dist/create/server-source/crates/feltdb-server/src/audit.rs +59 -0
- package/dist/create/server-source/crates/feltdb-server/src/auth.rs +308 -0
- package/dist/create/server-source/crates/feltdb-server/src/authorization.rs +228 -0
- package/dist/create/server-source/crates/feltdb-server/src/backup.rs +416 -0
- package/dist/create/server-source/crates/feltdb-server/src/causal.rs +218 -0
- package/dist/create/server-source/crates/feltdb-server/src/certification.rs +223 -0
- package/dist/create/server-source/crates/feltdb-server/src/clock.rs +132 -0
- package/dist/create/server-source/crates/feltdb-server/src/cluster.rs +165 -0
- package/dist/create/server-source/crates/feltdb-server/src/connections.rs +1044 -0
- package/dist/create/server-source/crates/feltdb-server/src/content.rs +111 -0
- package/dist/create/server-source/crates/feltdb-server/src/identity.rs +250 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_management.rs +78 -0
- package/dist/create/server-source/crates/feltdb-server/src/key_provider.rs +247 -0
- package/dist/create/server-source/crates/feltdb-server/src/leases.rs +123 -0
- package/dist/create/server-source/crates/feltdb-server/src/lib.rs +25 -0
- package/dist/create/server-source/crates/feltdb-server/src/main.rs +8715 -0
- package/dist/create/server-source/crates/feltdb-server/src/metrics.rs +97 -0
- package/dist/create/server-source/crates/feltdb-server/src/portable_bundle.rs +350 -0
- package/dist/create/server-source/crates/feltdb-server/src/principals.rs +510 -0
- package/dist/create/server-source/crates/feltdb-server/src/providers.rs +269 -0
- package/dist/create/server-source/crates/feltdb-server/src/releases.rs +2563 -0
- package/dist/create/server-source/crates/feltdb-server/src/sessions.rs +156 -0
- package/dist/create/server-source/crates/feltdb-server/src/tenancy.rs +1097 -0
- package/dist/create/server-source/crates/feltdb-server/src/versions.rs +264 -0
- package/dist/create/server-source/crates/feltdb-wasm/Cargo.toml +31 -0
- package/dist/create/server-source/crates/feltdb-wasm/src/lib.rs +1086 -0
- package/dist/create/server-source/crates/feltdb-wasm/test.db +0 -0
- package/dist/flowspec.d.ts +2 -0
- package/dist/flowspec.d.ts.map +1 -1
- package/dist/flowspec.js +28 -4
- package/dist/state-contract.d.ts +7 -1
- package/dist/state-contract.d.ts.map +1 -1
- package/dist/studio/components/ApplicationDesigner.d.ts.map +1 -1
- package/dist/studio/components/index.js +1 -1
- package/dist/studio/{components-Q0Nx6gLE.js → components-bHTARBin.js} +156 -46
- package/dist/studio/index.js +31 -28
- package/dist/studio-app/assets/{index-BHOmLZF-.js → index-BqKquLuU.js} +8 -8
- package/dist/studio-app/index.html +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,713 @@
|
|
|
1
|
+
use crate::FlowRef;
|
|
2
|
+
/// Workflow module for durable, reactive distributed workflows
|
|
3
|
+
///
|
|
4
|
+
/// A workflow is a durable graph of intents and references that can execute
|
|
5
|
+
/// wherever the required capabilities exist, without the workflow owning execution location.
|
|
6
|
+
use serde::{Deserialize, Serialize};
|
|
7
|
+
use std::collections::{HashMap, HashSet};
|
|
8
|
+
use std::fmt::{Display, Formatter};
|
|
9
|
+
|
|
10
|
+
/// Reference to a workflow - addressable object in the fabric
|
|
11
|
+
///
|
|
12
|
+
/// WorkflowRef {
|
|
13
|
+
/// namespace: "workflows",
|
|
14
|
+
/// workflow_id: "order-processing",
|
|
15
|
+
/// version: 1,
|
|
16
|
+
/// }
|
|
17
|
+
///
|
|
18
|
+
/// For example:
|
|
19
|
+
/// - flow://workflow/order-processing@1
|
|
20
|
+
/// - flow://workflow/document-ingestion@2
|
|
21
|
+
/// - flow://workflow/research@1
|
|
22
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
|
23
|
+
pub struct WorkflowRef {
|
|
24
|
+
/// Namespace for workflows (typically "workflow")
|
|
25
|
+
pub namespace: String,
|
|
26
|
+
/// Unique identifier for the workflow
|
|
27
|
+
pub workflow_id: String,
|
|
28
|
+
/// Version of the workflow
|
|
29
|
+
pub version: u64,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl WorkflowRef {
|
|
33
|
+
/// Create a new WorkflowRef
|
|
34
|
+
pub fn new(namespace: impl Into<String>, workflow_id: impl Into<String>, version: u64) -> Self {
|
|
35
|
+
Self {
|
|
36
|
+
namespace: namespace.into(),
|
|
37
|
+
workflow_id: workflow_id.into(),
|
|
38
|
+
version,
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// Get canonical string representation
|
|
43
|
+
pub fn canonical(&self) -> String {
|
|
44
|
+
format!(
|
|
45
|
+
"flow://{}/{}@{}",
|
|
46
|
+
self.namespace, self.workflow_id, self.version
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/// Parse from canonical form
|
|
51
|
+
pub fn from_canonical(s: &str) -> Option<Self> {
|
|
52
|
+
if !s.starts_with("flow://") {
|
|
53
|
+
return None;
|
|
54
|
+
}
|
|
55
|
+
let s = &s[7..]; // Remove "flow://"
|
|
56
|
+
|
|
57
|
+
let (namespace, rest) = s.split_once('/')?;
|
|
58
|
+
let (workflow_id, version_str) = rest.split_once('@')?;
|
|
59
|
+
let version = version_str.parse().ok()?;
|
|
60
|
+
|
|
61
|
+
Some(Self {
|
|
62
|
+
namespace: namespace.to_string(),
|
|
63
|
+
workflow_id: workflow_id.to_string(),
|
|
64
|
+
version,
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
impl Display for WorkflowRef {
|
|
70
|
+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
71
|
+
write!(f, "{}", self.canonical())
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// Reference to a step within a workflow
|
|
76
|
+
///
|
|
77
|
+
/// Provides stable identity for workflow steps across peer failures and migrations.
|
|
78
|
+
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
|
79
|
+
pub struct WorkflowStepRef {
|
|
80
|
+
/// Reference to the parent workflow
|
|
81
|
+
pub workflow_ref: WorkflowRef,
|
|
82
|
+
/// Step identifier within the workflow (e.g., "step-1", "extract", "join-results")
|
|
83
|
+
pub step_id: String,
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
impl WorkflowStepRef {
|
|
87
|
+
/// Create a new WorkflowStepRef
|
|
88
|
+
pub fn new(workflow_ref: WorkflowRef, step_id: impl Into<String>) -> Self {
|
|
89
|
+
Self {
|
|
90
|
+
workflow_ref,
|
|
91
|
+
step_id: step_id.into(),
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/// Get canonical string representation
|
|
96
|
+
pub fn canonical(&self) -> String {
|
|
97
|
+
format!("{}/{}", self.workflow_ref.canonical(), self.step_id)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Parse from canonical form
|
|
101
|
+
pub fn from_canonical(s: &str) -> Option<Self> {
|
|
102
|
+
// Expected format: flow://workflow/workflow-id@version/step-id
|
|
103
|
+
if !s.starts_with("flow://") {
|
|
104
|
+
return None;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let s = &s[7..]; // Remove "flow://"
|
|
108
|
+
|
|
109
|
+
// Find the last '/' to separate workflow part from step-id
|
|
110
|
+
let last_slash_idx = s.rfind('/')?;
|
|
111
|
+
let workflow_str = &s[..last_slash_idx];
|
|
112
|
+
let step_id = &s[last_slash_idx + 1..];
|
|
113
|
+
|
|
114
|
+
// Parse the workflow part
|
|
115
|
+
let (namespace, rest) = workflow_str.split_once('/')?;
|
|
116
|
+
let (workflow_id, version_str) = rest.split_once('@')?;
|
|
117
|
+
let version = version_str.parse().ok()?;
|
|
118
|
+
|
|
119
|
+
Some(Self {
|
|
120
|
+
workflow_ref: WorkflowRef {
|
|
121
|
+
namespace: namespace.to_string(),
|
|
122
|
+
workflow_id: workflow_id.to_string(),
|
|
123
|
+
version,
|
|
124
|
+
},
|
|
125
|
+
step_id: step_id.to_string(),
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
impl Display for WorkflowStepRef {
|
|
131
|
+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
132
|
+
write!(f, "{}", self.canonical())
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/// State of a workflow step in the durable execution model
|
|
137
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
138
|
+
pub enum WorkflowStepState {
|
|
139
|
+
/// Waiting for dependencies to be satisfied
|
|
140
|
+
Pending,
|
|
141
|
+
/// Dependencies satisfied, ready to execute
|
|
142
|
+
Ready,
|
|
143
|
+
/// Step has been claimed by a peer for execution
|
|
144
|
+
Claimed,
|
|
145
|
+
/// Step is currently executing
|
|
146
|
+
Running,
|
|
147
|
+
/// Step completed successfully
|
|
148
|
+
Completed,
|
|
149
|
+
/// Step failed during execution
|
|
150
|
+
Failed,
|
|
151
|
+
/// Step scheduled for retry after failure
|
|
152
|
+
Retrying,
|
|
153
|
+
/// Step is blocked, waiting for external input
|
|
154
|
+
Blocked,
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
impl Display for WorkflowStepState {
|
|
158
|
+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
159
|
+
match self {
|
|
160
|
+
Self::Pending => write!(f, "Pending"),
|
|
161
|
+
Self::Ready => write!(f, "Ready"),
|
|
162
|
+
Self::Claimed => write!(f, "Claimed"),
|
|
163
|
+
Self::Running => write!(f, "Running"),
|
|
164
|
+
Self::Completed => write!(f, "Completed"),
|
|
165
|
+
Self::Failed => write!(f, "Failed"),
|
|
166
|
+
Self::Retrying => write!(f, "Retrying"),
|
|
167
|
+
Self::Blocked => write!(f, "Blocked"),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/// Reason a workflow step is blocked
|
|
173
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
174
|
+
pub enum BlockedReason {
|
|
175
|
+
/// Awaiting human approval
|
|
176
|
+
AwaitingApproval(String),
|
|
177
|
+
/// Awaiting human input
|
|
178
|
+
AwaitingInput(String),
|
|
179
|
+
/// Required capability not available
|
|
180
|
+
AwaitingCapability(String),
|
|
181
|
+
/// Insufficient resources
|
|
182
|
+
AwaitingResource(String),
|
|
183
|
+
/// Custom blocking reason
|
|
184
|
+
Custom(String),
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
impl Display for BlockedReason {
|
|
188
|
+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
189
|
+
match self {
|
|
190
|
+
Self::AwaitingApproval(msg) => write!(f, "AwaitingApproval: {}", msg),
|
|
191
|
+
Self::AwaitingInput(msg) => write!(f, "AwaitingInput: {}", msg),
|
|
192
|
+
Self::AwaitingCapability(cap) => write!(f, "AwaitingCapability: {}", cap),
|
|
193
|
+
Self::AwaitingResource(res) => write!(f, "AwaitingResource: {}", res),
|
|
194
|
+
Self::Custom(msg) => write!(f, "Custom: {}", msg),
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// State of a workflow
|
|
200
|
+
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
201
|
+
pub enum WorkflowState {
|
|
202
|
+
/// Workflow has been created but not started
|
|
203
|
+
Created,
|
|
204
|
+
/// Workflow is currently executing
|
|
205
|
+
Running,
|
|
206
|
+
/// Workflow completed successfully
|
|
207
|
+
Completed,
|
|
208
|
+
/// Workflow failed
|
|
209
|
+
Failed,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
impl Display for WorkflowState {
|
|
213
|
+
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
|
214
|
+
match self {
|
|
215
|
+
Self::Created => write!(f, "Created"),
|
|
216
|
+
Self::Running => write!(f, "Running"),
|
|
217
|
+
Self::Completed => write!(f, "Completed"),
|
|
218
|
+
Self::Failed => write!(f, "Failed"),
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/// Workflow lifecycle event - becomes a durable operation
|
|
224
|
+
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
225
|
+
pub enum WorkflowOperation {
|
|
226
|
+
/// Workflow was created
|
|
227
|
+
Created { workflow_ref: WorkflowRef },
|
|
228
|
+
/// Workflow started execution
|
|
229
|
+
Started { workflow_ref: WorkflowRef },
|
|
230
|
+
/// A step became ready to execute
|
|
231
|
+
StepReady { step_ref: WorkflowStepRef },
|
|
232
|
+
/// A step was claimed by a peer
|
|
233
|
+
StepClaimed {
|
|
234
|
+
step_ref: WorkflowStepRef,
|
|
235
|
+
claimed_by: String,
|
|
236
|
+
},
|
|
237
|
+
/// A step started executing
|
|
238
|
+
StepExecuting {
|
|
239
|
+
step_ref: WorkflowStepRef,
|
|
240
|
+
claimed_by: String,
|
|
241
|
+
},
|
|
242
|
+
/// A step completed successfully
|
|
243
|
+
StepCompleted {
|
|
244
|
+
step_ref: WorkflowStepRef,
|
|
245
|
+
result_ref: Option<FlowRef>,
|
|
246
|
+
},
|
|
247
|
+
/// A step failed
|
|
248
|
+
StepFailed {
|
|
249
|
+
step_ref: WorkflowStepRef,
|
|
250
|
+
error: String,
|
|
251
|
+
attempt: u32,
|
|
252
|
+
},
|
|
253
|
+
/// A step is blocked pending external input
|
|
254
|
+
StepBlocked {
|
|
255
|
+
step_ref: WorkflowStepRef,
|
|
256
|
+
reason: BlockedReason,
|
|
257
|
+
},
|
|
258
|
+
/// A step retry was scheduled
|
|
259
|
+
StepRetrying {
|
|
260
|
+
step_ref: WorkflowStepRef,
|
|
261
|
+
attempt: u32,
|
|
262
|
+
},
|
|
263
|
+
/// Workflow completed
|
|
264
|
+
Completed { workflow_ref: WorkflowRef },
|
|
265
|
+
/// Workflow failed
|
|
266
|
+
Failed {
|
|
267
|
+
workflow_ref: WorkflowRef,
|
|
268
|
+
error: String,
|
|
269
|
+
},
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
impl WorkflowOperation {
|
|
273
|
+
/// Get the affected workflow reference
|
|
274
|
+
pub fn workflow_ref(&self) -> Option<&WorkflowRef> {
|
|
275
|
+
match self {
|
|
276
|
+
Self::Created { workflow_ref } => Some(workflow_ref),
|
|
277
|
+
Self::Started { workflow_ref } => Some(workflow_ref),
|
|
278
|
+
Self::Completed { workflow_ref } => Some(workflow_ref),
|
|
279
|
+
Self::Failed { workflow_ref, .. } => Some(workflow_ref),
|
|
280
|
+
_ => None,
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/// Get the affected step reference
|
|
285
|
+
pub fn step_ref(&self) -> Option<&WorkflowStepRef> {
|
|
286
|
+
match self {
|
|
287
|
+
Self::StepReady { step_ref } => Some(step_ref),
|
|
288
|
+
Self::StepClaimed { step_ref, .. } => Some(step_ref),
|
|
289
|
+
Self::StepExecuting { step_ref, .. } => Some(step_ref),
|
|
290
|
+
Self::StepCompleted { step_ref, .. } => Some(step_ref),
|
|
291
|
+
Self::StepFailed { step_ref, .. } => Some(step_ref),
|
|
292
|
+
Self::StepBlocked { step_ref, .. } => Some(step_ref),
|
|
293
|
+
Self::StepRetrying { step_ref, .. } => Some(step_ref),
|
|
294
|
+
_ => None,
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/// Workflow graph node - represents a capability to execute
|
|
300
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
301
|
+
pub struct WorkflowNode {
|
|
302
|
+
/// Step identifier (e.g., "extract", "embed", "classify")
|
|
303
|
+
pub step_id: String,
|
|
304
|
+
/// Capability to execute (reference to a CapabilityRef)
|
|
305
|
+
pub capability: String,
|
|
306
|
+
/// Input references required for this step
|
|
307
|
+
pub inputs: Vec<String>, // Step IDs or input slot names
|
|
308
|
+
/// Output slot names produced by this step
|
|
309
|
+
pub outputs: Vec<String>,
|
|
310
|
+
/// State requirements for this step
|
|
311
|
+
pub state_requirements: HashMap<String, String>,
|
|
312
|
+
/// Metadata about this node
|
|
313
|
+
pub metadata: HashMap<String, serde_json::Value>,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
impl WorkflowNode {
|
|
317
|
+
/// Create a new workflow node
|
|
318
|
+
pub fn new(step_id: impl Into<String>, capability: impl Into<String>) -> Self {
|
|
319
|
+
Self {
|
|
320
|
+
step_id: step_id.into(),
|
|
321
|
+
capability: capability.into(),
|
|
322
|
+
inputs: Vec::new(),
|
|
323
|
+
outputs: Vec::new(),
|
|
324
|
+
state_requirements: HashMap::new(),
|
|
325
|
+
metadata: HashMap::new(),
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/// Add an input dependency
|
|
330
|
+
pub fn with_input(mut self, input: impl Into<String>) -> Self {
|
|
331
|
+
self.inputs.push(input.into());
|
|
332
|
+
self
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/// Add an output slot
|
|
336
|
+
pub fn with_output(mut self, output: impl Into<String>) -> Self {
|
|
337
|
+
self.outputs.push(output.into());
|
|
338
|
+
self
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// Add a state requirement
|
|
342
|
+
pub fn with_state_requirement(
|
|
343
|
+
mut self,
|
|
344
|
+
key: impl Into<String>,
|
|
345
|
+
value: impl Into<String>,
|
|
346
|
+
) -> Self {
|
|
347
|
+
self.state_requirements.insert(key.into(), value.into());
|
|
348
|
+
self
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/// Workflow graph definition - durable DAG structure
|
|
353
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
354
|
+
pub struct WorkflowGraph {
|
|
355
|
+
/// Workflow reference
|
|
356
|
+
pub workflow_ref: WorkflowRef,
|
|
357
|
+
/// Nodes in the workflow
|
|
358
|
+
pub nodes: HashMap<String, WorkflowNode>,
|
|
359
|
+
/// Edges: from_step_id -> to_step_id
|
|
360
|
+
pub edges: HashMap<String, Vec<String>>,
|
|
361
|
+
/// Input references for the entire workflow
|
|
362
|
+
pub inputs: Vec<FlowRef>,
|
|
363
|
+
/// Output slot definitions
|
|
364
|
+
pub outputs: Vec<String>,
|
|
365
|
+
/// Metadata
|
|
366
|
+
pub metadata: HashMap<String, serde_json::Value>,
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
impl WorkflowGraph {
|
|
370
|
+
/// Create a new workflow graph
|
|
371
|
+
pub fn new(workflow_ref: WorkflowRef) -> Self {
|
|
372
|
+
Self {
|
|
373
|
+
workflow_ref,
|
|
374
|
+
nodes: HashMap::new(),
|
|
375
|
+
edges: HashMap::new(),
|
|
376
|
+
inputs: Vec::new(),
|
|
377
|
+
outputs: Vec::new(),
|
|
378
|
+
metadata: HashMap::new(),
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/// Add a node to the graph
|
|
383
|
+
pub fn add_node(mut self, node: WorkflowNode) -> Self {
|
|
384
|
+
self.nodes.insert(node.step_id.clone(), node);
|
|
385
|
+
self
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/// Add an edge from one step to another
|
|
389
|
+
pub fn add_edge(mut self, from_step: impl Into<String>, to_step: impl Into<String>) -> Self {
|
|
390
|
+
let from = from_step.into();
|
|
391
|
+
let to = to_step.into();
|
|
392
|
+
self.edges.entry(from).or_insert_with(Vec::new).push(to);
|
|
393
|
+
self
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/// Add an input reference
|
|
397
|
+
pub fn with_input(mut self, input: FlowRef) -> Self {
|
|
398
|
+
self.inputs.push(input);
|
|
399
|
+
self
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/// Validate the graph structure (basic DAG check)
|
|
403
|
+
pub fn validate(&self) -> Result<(), String> {
|
|
404
|
+
// Check all edges reference existing nodes
|
|
405
|
+
for (from, tos) in &self.edges {
|
|
406
|
+
if !self.nodes.contains_key(from) {
|
|
407
|
+
return Err(format!("Edge source '{}' does not exist", from));
|
|
408
|
+
}
|
|
409
|
+
for to in tos {
|
|
410
|
+
if !self.nodes.contains_key(to) {
|
|
411
|
+
return Err(format!("Edge target '{}' does not exist", to));
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Check input dependencies reference existing nodes or inputs
|
|
417
|
+
for node in self.nodes.values() {
|
|
418
|
+
for input in &node.inputs {
|
|
419
|
+
if !self.nodes.contains_key(input) && !input.starts_with("input:") {
|
|
420
|
+
return Err(format!(
|
|
421
|
+
"Node '{}' has input dependency '{}' that doesn't exist",
|
|
422
|
+
node.step_id, input
|
|
423
|
+
));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Simple cycle detection using DFS
|
|
429
|
+
self.detect_cycles()?;
|
|
430
|
+
|
|
431
|
+
Ok(())
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/// Detect cycles in the graph using DFS
|
|
435
|
+
fn detect_cycles(&self) -> Result<(), String> {
|
|
436
|
+
let mut visited: HashSet<String> = HashSet::new();
|
|
437
|
+
let mut rec_stack: HashSet<String> = HashSet::new();
|
|
438
|
+
|
|
439
|
+
for node_id in self.nodes.keys() {
|
|
440
|
+
if !visited.contains(node_id) {
|
|
441
|
+
self.dfs(node_id, &mut visited, &mut rec_stack)?;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
Ok(())
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/// DFS helper for cycle detection
|
|
449
|
+
fn dfs(
|
|
450
|
+
&self,
|
|
451
|
+
node_id: &str,
|
|
452
|
+
visited: &mut HashSet<String>,
|
|
453
|
+
rec_stack: &mut HashSet<String>,
|
|
454
|
+
) -> Result<(), String> {
|
|
455
|
+
visited.insert(node_id.to_string());
|
|
456
|
+
rec_stack.insert(node_id.to_string());
|
|
457
|
+
|
|
458
|
+
if let Some(neighbors) = self.edges.get(node_id) {
|
|
459
|
+
for neighbor in neighbors {
|
|
460
|
+
if !visited.contains(neighbor) {
|
|
461
|
+
self.dfs(neighbor, visited, rec_stack)?;
|
|
462
|
+
} else if rec_stack.contains(neighbor) {
|
|
463
|
+
return Err(format!("Cycle detected: {} -> {}", node_id, neighbor));
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
rec_stack.remove(node_id);
|
|
469
|
+
Ok(())
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/// Get nodes with no dependencies (entry points)
|
|
473
|
+
pub fn entry_nodes(&self) -> Vec<String> {
|
|
474
|
+
self.nodes
|
|
475
|
+
.keys()
|
|
476
|
+
.filter(|node_id| {
|
|
477
|
+
if let Some(node) = self.nodes.get(*node_id) {
|
|
478
|
+
// A node is an entry point if all its inputs come from workflow inputs
|
|
479
|
+
node.inputs.is_empty()
|
|
480
|
+
|| node.inputs.iter().all(|inp| inp.starts_with("input:"))
|
|
481
|
+
} else {
|
|
482
|
+
false
|
|
483
|
+
}
|
|
484
|
+
})
|
|
485
|
+
.cloned()
|
|
486
|
+
.collect()
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
/// Get nodes that depend on a given step
|
|
490
|
+
pub fn dependents(&self, step_id: &str) -> Vec<String> {
|
|
491
|
+
self.edges
|
|
492
|
+
.get(step_id)
|
|
493
|
+
.map(|deps| deps.clone())
|
|
494
|
+
.unwrap_or_default()
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/// Get nodes that a given step depends on
|
|
498
|
+
pub fn dependencies(&self, step_id: &str) -> Vec<String> {
|
|
499
|
+
if let Some(node) = self.nodes.get(step_id) {
|
|
500
|
+
node.inputs
|
|
501
|
+
.iter()
|
|
502
|
+
.filter(|inp| !inp.starts_with("input:"))
|
|
503
|
+
.cloned()
|
|
504
|
+
.collect()
|
|
505
|
+
} else {
|
|
506
|
+
Vec::new()
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/// Runtime state for a workflow instance
|
|
512
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
513
|
+
pub struct WorkflowInstance {
|
|
514
|
+
/// Reference to the workflow definition
|
|
515
|
+
pub workflow_ref: WorkflowRef,
|
|
516
|
+
/// Unique instance ID for this execution
|
|
517
|
+
pub instance_id: String,
|
|
518
|
+
/// Overall workflow state
|
|
519
|
+
pub state: WorkflowState,
|
|
520
|
+
/// Current state of each step
|
|
521
|
+
pub step_states: HashMap<String, WorkflowStepState>,
|
|
522
|
+
/// Which peer claimed each step
|
|
523
|
+
pub step_claims: HashMap<String, String>,
|
|
524
|
+
/// Results produced by each step
|
|
525
|
+
pub step_results: HashMap<String, FlowRef>,
|
|
526
|
+
/// Failed attempts for each step
|
|
527
|
+
pub step_failures: HashMap<String, Vec<String>>,
|
|
528
|
+
/// Blocked reason for each step
|
|
529
|
+
pub step_blocked_reasons: HashMap<String, BlockedReason>,
|
|
530
|
+
/// Timestamp created (ms since UNIX epoch)
|
|
531
|
+
pub created_ms: u128,
|
|
532
|
+
/// Timestamp started (ms since UNIX epoch)
|
|
533
|
+
pub started_ms: Option<u128>,
|
|
534
|
+
/// Timestamp completed (ms since UNIX epoch)
|
|
535
|
+
pub completed_ms: Option<u128>,
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
impl WorkflowInstance {
|
|
539
|
+
/// Create a new workflow instance
|
|
540
|
+
pub fn new(workflow_ref: WorkflowRef, instance_id: impl Into<String>) -> Self {
|
|
541
|
+
Self {
|
|
542
|
+
workflow_ref,
|
|
543
|
+
instance_id: instance_id.into(),
|
|
544
|
+
state: WorkflowState::Created,
|
|
545
|
+
step_states: HashMap::new(),
|
|
546
|
+
step_claims: HashMap::new(),
|
|
547
|
+
step_results: HashMap::new(),
|
|
548
|
+
step_failures: HashMap::new(),
|
|
549
|
+
step_blocked_reasons: HashMap::new(),
|
|
550
|
+
created_ms: crate::now_ms(),
|
|
551
|
+
started_ms: None,
|
|
552
|
+
completed_ms: None,
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/// Get the state of a step
|
|
557
|
+
pub fn get_step_state(&self, step_id: &str) -> Option<WorkflowStepState> {
|
|
558
|
+
self.step_states.get(step_id).copied()
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
/// Set the state of a step
|
|
562
|
+
pub fn set_step_state(&mut self, step_id: impl Into<String>, state: WorkflowStepState) {
|
|
563
|
+
self.step_states.insert(step_id.into(), state);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/// Mark a step as claimed
|
|
567
|
+
pub fn claim_step(
|
|
568
|
+
&mut self,
|
|
569
|
+
step_id: impl Into<String>,
|
|
570
|
+
claimed_by: impl Into<String>,
|
|
571
|
+
) -> Result<(), String> {
|
|
572
|
+
let step_id = step_id.into();
|
|
573
|
+
let claimed_by = claimed_by.into();
|
|
574
|
+
|
|
575
|
+
match self.get_step_state(&step_id) {
|
|
576
|
+
Some(WorkflowStepState::Ready) => {
|
|
577
|
+
self.set_step_state(step_id.clone(), WorkflowStepState::Claimed);
|
|
578
|
+
self.step_claims.insert(step_id, claimed_by);
|
|
579
|
+
Ok(())
|
|
580
|
+
}
|
|
581
|
+
Some(state) => Err(format!("Cannot claim step in state {}", state)),
|
|
582
|
+
None => Err(format!("Step not found: {}", step_id)),
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/// Record a step completion
|
|
587
|
+
pub fn complete_step(
|
|
588
|
+
&mut self,
|
|
589
|
+
step_id: impl Into<String>,
|
|
590
|
+
result_ref: FlowRef,
|
|
591
|
+
) -> Result<(), String> {
|
|
592
|
+
let step_id = step_id.into();
|
|
593
|
+
self.set_step_state(step_id.clone(), WorkflowStepState::Completed);
|
|
594
|
+
self.step_results.insert(step_id, result_ref);
|
|
595
|
+
Ok(())
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/// Record a step failure
|
|
599
|
+
pub fn fail_step(&mut self, step_id: impl Into<String>, error: String) -> Result<(), String> {
|
|
600
|
+
let step_id = step_id.into();
|
|
601
|
+
self.set_step_state(step_id.clone(), WorkflowStepState::Failed);
|
|
602
|
+
self.step_failures
|
|
603
|
+
.entry(step_id)
|
|
604
|
+
.or_insert_with(Vec::new)
|
|
605
|
+
.push(error);
|
|
606
|
+
Ok(())
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/// Check if workflow is terminal (won't change state)
|
|
610
|
+
pub fn is_terminal(&self) -> bool {
|
|
611
|
+
matches!(self.state, WorkflowState::Completed | WorkflowState::Failed)
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
#[cfg(test)]
|
|
616
|
+
mod tests {
|
|
617
|
+
use super::*;
|
|
618
|
+
|
|
619
|
+
#[test]
|
|
620
|
+
fn test_workflow_ref_canonical() {
|
|
621
|
+
let r = WorkflowRef::new("workflow", "order-processing", 1);
|
|
622
|
+
assert_eq!(r.canonical(), "flow://workflow/order-processing@1");
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
#[test]
|
|
626
|
+
fn test_workflow_ref_parse() {
|
|
627
|
+
let s = "flow://workflow/order-processing@1";
|
|
628
|
+
let r = WorkflowRef::from_canonical(s).unwrap();
|
|
629
|
+
assert_eq!(r.namespace, "workflow");
|
|
630
|
+
assert_eq!(r.workflow_id, "order-processing");
|
|
631
|
+
assert_eq!(r.version, 1);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
#[test]
|
|
635
|
+
fn test_workflow_step_ref_canonical() {
|
|
636
|
+
let wf = WorkflowRef::new("workflow", "test", 1);
|
|
637
|
+
let step = WorkflowStepRef::new(wf, "step-1");
|
|
638
|
+
assert_eq!(step.canonical(), "flow://workflow/test@1/step-1");
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
#[test]
|
|
642
|
+
fn test_workflow_step_ref_parse() {
|
|
643
|
+
let s = "flow://workflow/test@1/step-1";
|
|
644
|
+
let step = WorkflowStepRef::from_canonical(s).unwrap();
|
|
645
|
+
assert_eq!(step.step_id, "step-1");
|
|
646
|
+
assert_eq!(step.workflow_ref.workflow_id, "test");
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
#[test]
|
|
650
|
+
fn test_workflow_graph_validation() {
|
|
651
|
+
let wf = WorkflowRef::new("workflow", "test", 1);
|
|
652
|
+
let graph = WorkflowGraph::new(wf)
|
|
653
|
+
.add_node(WorkflowNode::new("step-1", "capability-a"))
|
|
654
|
+
.add_node(WorkflowNode::new("step-2", "capability-b").with_input("step-1"))
|
|
655
|
+
.add_edge("step-1", "step-2");
|
|
656
|
+
|
|
657
|
+
assert!(graph.validate().is_ok());
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
#[test]
|
|
661
|
+
fn test_workflow_graph_cycle_detection() {
|
|
662
|
+
let wf = WorkflowRef::new("workflow", "test", 1);
|
|
663
|
+
let graph = WorkflowGraph::new(wf)
|
|
664
|
+
.add_node(WorkflowNode::new("step-1", "capability-a"))
|
|
665
|
+
.add_node(WorkflowNode::new("step-2", "capability-b"))
|
|
666
|
+
.add_edge("step-1", "step-2")
|
|
667
|
+
.add_edge("step-2", "step-1");
|
|
668
|
+
|
|
669
|
+
assert!(graph.validate().is_err());
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
#[test]
|
|
673
|
+
fn test_workflow_graph_entry_nodes() {
|
|
674
|
+
let wf = WorkflowRef::new("workflow", "test", 1);
|
|
675
|
+
let graph = WorkflowGraph::new(wf)
|
|
676
|
+
.add_node(WorkflowNode::new("step-1", "capability-a"))
|
|
677
|
+
.add_node(WorkflowNode::new("step-2", "capability-b").with_input("step-1"))
|
|
678
|
+
.add_edge("step-1", "step-2");
|
|
679
|
+
|
|
680
|
+
let entries = graph.entry_nodes();
|
|
681
|
+
assert_eq!(entries.len(), 1);
|
|
682
|
+
assert!(entries.contains(&"step-1".to_string()));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
#[test]
|
|
686
|
+
fn test_workflow_instance_lifecycle() {
|
|
687
|
+
let wf = WorkflowRef::new("workflow", "test", 1);
|
|
688
|
+
let mut instance = WorkflowInstance::new(wf, "instance-1");
|
|
689
|
+
|
|
690
|
+
instance.set_step_state("step-1", WorkflowStepState::Pending);
|
|
691
|
+
assert_eq!(
|
|
692
|
+
instance.get_step_state("step-1"),
|
|
693
|
+
Some(WorkflowStepState::Pending)
|
|
694
|
+
);
|
|
695
|
+
|
|
696
|
+
instance.set_step_state("step-1", WorkflowStepState::Ready);
|
|
697
|
+
let claim_result = instance.claim_step("step-1", "peer-a");
|
|
698
|
+
assert!(claim_result.is_ok());
|
|
699
|
+
assert_eq!(
|
|
700
|
+
instance.get_step_state("step-1"),
|
|
701
|
+
Some(WorkflowStepState::Claimed)
|
|
702
|
+
);
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
#[test]
|
|
706
|
+
fn test_workflow_blocked_reason() {
|
|
707
|
+
let reason = BlockedReason::AwaitingApproval("Needs manager approval".to_string());
|
|
708
|
+
assert_eq!(
|
|
709
|
+
reason.to_string(),
|
|
710
|
+
"AwaitingApproval: Needs manager approval"
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
}
|