Skip to content

Commit 104f2fc

Browse files
authored
Create plugin_contract.rs
1 parent 8a1b214 commit 104f2fc

1 file changed

Lines changed: 102 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//! Global plugin contract tests.
2+
//!
3+
//! These tests validate guarantees that apply to ALL plugins (builtin or external):
4+
//! - deterministic execution
5+
//! - stable IDs
6+
//! - canonical JSON behavior
7+
//! - no implicit ordering dependence
8+
//!
9+
//! This test module is compiled without feature flags and represents the
10+
//! minimum contract required for any Signia-compatible plugin.
11+
12+
use serde_json::json;
13+
14+
use signia_core::determinism::canonical_json::canonicalize_json;
15+
use signia_core::pipeline::context::{PipelineConfig, PipelineContext};
16+
17+
use signia_plugins::registry::PluginRegistry;
18+
use signia_plugins::plugin::{Plugin, PluginInput};
19+
20+
#[test]
21+
fn empty_registry_is_stable() {
22+
let r1 = PluginRegistry::default();
23+
let r2 = PluginRegistry::default();
24+
25+
assert_eq!(r1.list().len(), 0);
26+
assert_eq!(r1.list(), r2.list());
27+
}
28+
29+
#[test]
30+
fn registry_order_is_deterministic() {
31+
let mut r = PluginRegistry::default();
32+
33+
let ids: Vec<String> = r.list().into_iter().map(|s| s.id).collect();
34+
let mut sorted = ids.clone();
35+
sorted.sort();
36+
assert_eq!(ids, sorted);
37+
}
38+
39+
#[test]
40+
fn pipeline_context_is_deterministic() {
41+
let cfg = PipelineConfig::default();
42+
let ctx1 = PipelineContext::new(cfg.clone());
43+
let ctx2 = PipelineContext::new(cfg);
44+
45+
assert_eq!(ctx1.config, ctx2.config);
46+
assert_eq!(ctx1.inputs.len(), ctx2.inputs.len());
47+
}
48+
49+
#[test]
50+
fn canonical_json_roundtrip_is_stable() {
51+
let input = json!({
52+
"b": [3,2,1],
53+
"a": { "y":2, "x":1 }
54+
});
55+
56+
let c1 = canonicalize_json(&input).unwrap();
57+
let c2 = canonicalize_json(&input).unwrap();
58+
59+
assert_eq!(c1, c2);
60+
}
61+
62+
#[test]
63+
fn plugin_execute_contract_allows_no_side_effects() {
64+
// This test is intentionally generic.
65+
// It asserts that executing a plugin with an empty registry
66+
// does not panic or mutate global state.
67+
let mut registry = PluginRegistry::default();
68+
69+
let ctx = PipelineContext::new(PipelineConfig::default());
70+
let input = PluginInput::None;
71+
72+
for spec in registry.list() {
73+
let plugin = registry.get(&spec.id).unwrap();
74+
let mut ctx_clone = ctx.clone();
75+
let _ = plugin.execute(&input);
76+
assert_eq!(ctx_clone.inputs, ctx.inputs);
77+
}
78+
}
79+
80+
#[test]
81+
fn repeated_execution_produces_identical_results() {
82+
let mut registry = PluginRegistry::default();
83+
let ctx_base = PipelineContext::new(PipelineConfig::default());
84+
85+
for spec in registry.list() {
86+
let plugin = registry.get(&spec.id).unwrap();
87+
88+
let mut ctx1 = ctx_base.clone();
89+
let mut ctx2 = ctx_base.clone();
90+
91+
let input = PluginInput::Pipeline(&mut ctx1);
92+
plugin.execute(&input).ok();
93+
94+
let input = PluginInput::Pipeline(&mut ctx2);
95+
plugin.execute(&input).ok();
96+
97+
assert_eq!(
98+
serde_json::to_value(&ctx1).unwrap(),
99+
serde_json::to_value(&ctx2).unwrap()
100+
);
101+
}
102+
}

0 commit comments

Comments
 (0)