1#![deny(missing_docs)]
2#![deny(unreachable_pub, rust_2018_idioms)]
3#![forbid(unsafe_code)]
4
5use alloc::string::{String, ToString};
9use alloc::vec::Vec;
10
11use byteor_pipeline_lower::{
12 lower_v2 as lower_plan, LowerDiagnosticCategoryV2, LowerDiagnosticV2, LowerError,
13 LowerOptionsV1, LowerReportV2,
14};
15use byteor_pipeline_plan::{
16 validate_plan_v2 as validate_plan_contract, ExecutionTargetV1, PipelinePlanV2,
17};
18use byteor_pipeline_spec::{
19 canonical_encode_kv_v1, canonical_spec_sha256_hex_v1, decode_kv_v1, describe_v1, dot_v1,
20 validate_v1, OnFullV1, PipelineSpecV1,
21};
22use serde::{Deserialize, Serialize};
23use wasm_bindgen::prelude::*;
24
25extern crate alloc;
26
27#[derive(Deserialize, Serialize)]
29pub struct CompileResult {
30 ok: bool,
31 spec_json: Option<String>,
32 report_json: Option<String>,
33 diagnostics: Vec<LowerDiagnosticV2>,
34}
35
36#[derive(Deserialize, Serialize)]
38pub struct ValidateResult {
39 ok: bool,
40 errors: Vec<String>,
41}
42
43#[derive(Deserialize, Serialize)]
45pub struct ValidatePlanResult {
46 ok: bool,
47 diagnostics: Vec<String>,
48}
49
50#[derive(Deserialize, Serialize)]
52pub struct DiffResult {
53 equal: bool,
54 spec_hash_a: String,
55 spec_hash_b: String,
56 canonical_kv_a: String,
57 canonical_kv_b: String,
58}
59
60#[wasm_bindgen]
62pub fn validate_plan(plan_json: &str) -> Result<JsValue, JsValue> {
63 to_js_value(&validate_plan_impl(plan_json)?)
64}
65
66#[wasm_bindgen]
68pub fn lower(plan_json: &str, target: &str) -> Result<JsValue, JsValue> {
69 to_js_value(&lower_impl(plan_json, parse_target(target)?)?)
70}
71
72#[wasm_bindgen]
74pub fn lower_single_ring(plan_json: &str) -> Result<JsValue, JsValue> {
75 to_js_value(&lower_impl(plan_json, ExecutionTargetV1::SingleRing)?)
76}
77
78#[wasm_bindgen]
80pub fn lower_lane_graph(plan_json: &str) -> Result<JsValue, JsValue> {
81 to_js_value(&lower_impl(plan_json, ExecutionTargetV1::LaneGraph)?)
82}
83
84#[wasm_bindgen]
86pub fn validate(spec_json: &str) -> Result<JsValue, JsValue> {
87 to_js_value(&validate_impl(spec_json)?)
88}
89
90#[wasm_bindgen]
92pub fn encode_kv(spec_json: &str) -> Result<Vec<u8>, JsValue> {
93 let spec: PipelineSpecV1 = parse_json(spec_json)?;
94 let encoded = canonical_encode_kv_v1(&spec).map_err(to_js_error)?;
95 Ok(encoded.into_bytes())
96}
97
98#[wasm_bindgen]
100pub fn decode_kv(bytes: &[u8]) -> Result<String, JsValue> {
101 let input = core::str::from_utf8(bytes).map_err(|e| JsValue::from_str(&e.to_string()))?;
102 let spec = decode_kv_v1(input).map_err(to_js_error)?;
103 to_json_string(&spec)
104}
105
106#[wasm_bindgen]
108pub fn describe(spec_json: &str) -> Result<String, JsValue> {
109 let spec: PipelineSpecV1 = parse_json(spec_json)?;
110 Ok(describe_v1(&spec))
111}
112
113#[wasm_bindgen]
115pub fn dot(spec_json: &str) -> Result<String, JsValue> {
116 let spec: PipelineSpecV1 = parse_json(spec_json)?;
117 dot_v1(&spec)
118 .map(|dot| dot.to_string())
119 .map_err(to_js_error)
120}
121
122#[wasm_bindgen]
124pub fn diff(spec_a_json: &str, spec_b_json: &str) -> Result<JsValue, JsValue> {
125 to_js_value(&diff_impl(spec_a_json, spec_b_json)?)
126}
127
128fn validate_plan_impl(plan_json: &str) -> Result<ValidatePlanResult, JsValue> {
129 let plan: PipelinePlanV2 = parse_json(plan_json)?;
130 let diagnostics = validate_plan_contract(&plan)
131 .into_iter()
132 .map(|diagnostic| diagnostic.message)
133 .collect::<Vec<_>>();
134 Ok(ValidatePlanResult {
135 ok: diagnostics.is_empty(),
136 diagnostics,
137 })
138}
139
140fn lower_impl(plan_json: &str, target: ExecutionTargetV1) -> Result<CompileResult, JsValue> {
141 let plan: PipelinePlanV2 = parse_json(plan_json)?;
142 match lower_plan(&plan, target, &default_lower_options()) {
143 Ok(report) => compile_result_from_lower_report(report),
144 Err(err) => Ok(compile_result_from_lower_error(target, err)),
145 }
146}
147
148fn validate_impl(spec_json: &str) -> Result<ValidateResult, JsValue> {
149 let spec: PipelineSpecV1 = parse_json(spec_json)?;
150 Ok(match validate_v1(&spec) {
151 Ok(()) => ValidateResult {
152 ok: true,
153 errors: Vec::new(),
154 },
155 Err(err) => ValidateResult {
156 ok: false,
157 errors: Vec::from([err.to_string()]),
158 },
159 })
160}
161
162fn diff_impl(spec_a_json: &str, spec_b_json: &str) -> Result<DiffResult, JsValue> {
163 let spec_a: PipelineSpecV1 = parse_json(spec_a_json)?;
164 let spec_b: PipelineSpecV1 = parse_json(spec_b_json)?;
165
166 let canonical_kv_a = canonical_encode_kv_v1(&spec_a).map_err(to_js_error)?;
167 let canonical_kv_b = canonical_encode_kv_v1(&spec_b).map_err(to_js_error)?;
168 Ok(DiffResult {
169 equal: canonical_kv_a == canonical_kv_b,
170 spec_hash_a: canonical_spec_sha256_hex_v1(&spec_a).map_err(to_js_error)?,
171 spec_hash_b: canonical_spec_sha256_hex_v1(&spec_b).map_err(to_js_error)?,
172 canonical_kv_a,
173 canonical_kv_b,
174 })
175}
176
177fn compile_result_from_lower_report(report: LowerReportV2) -> Result<CompileResult, JsValue> {
178 let diagnostics = report.lower_diagnostics.clone();
179 Ok(CompileResult {
180 ok: true,
181 spec_json: Some(to_json_string(&report.spec)?),
182 report_json: Some(to_json_string(&report)?),
183 diagnostics,
184 })
185}
186
187fn compile_result_from_lower_error(
188 target: ExecutionTargetV1,
189 err: LowerError,
190) -> CompileResult {
191 let diagnostics = match err {
192 LowerError::DiagnosticsV2(diagnostics) => diagnostics,
193 LowerError::Invalid(message) => Vec::from([LowerDiagnosticV2::Rejected {
194 target,
195 category: LowerDiagnosticCategoryV2::InternalInvariant,
196 message: message.to_string(),
197 }]),
198 LowerError::Unsupported(message) => Vec::from([LowerDiagnosticV2::Rejected {
199 target,
200 category: LowerDiagnosticCategoryV2::UnsupportedIntent,
201 message: message.to_string(),
202 }]),
203 _ => Vec::from([LowerDiagnosticV2::Rejected {
204 target,
205 category: LowerDiagnosticCategoryV2::InternalInvariant,
206 message: "unclassified lowering failure".to_string(),
207 }]),
208 };
209
210 CompileResult {
211 ok: false,
212 spec_json: None,
213 report_json: None,
214 diagnostics,
215 }
216}
217
218fn default_lower_options() -> LowerOptionsV1 {
219 LowerOptionsV1 {
220 backing_prefix: "wasm".to_string(),
221 on_full: OnFullV1::Block,
222 }
223}
224
225fn parse_target(target: &str) -> Result<ExecutionTargetV1, JsValue> {
226 match target.trim().to_ascii_lowercase().as_str() {
227 "single_ring" | "singlering" => Ok(ExecutionTargetV1::SingleRing),
228 "lane_graph" | "lanegraph" => Ok(ExecutionTargetV1::LaneGraph),
229 _ => Err(JsValue::from_str(
230 "target must be one of: single_ring, lane_graph",
231 )),
232 }
233}
234
235fn parse_json<T>(json: &str) -> Result<T, JsValue>
236where
237 T: serde::de::DeserializeOwned,
238{
239 serde_json::from_str(json).map_err(|e| JsValue::from_str(&e.to_string()))
240}
241
242fn to_json_string<T>(value: &T) -> Result<String, JsValue>
243where
244 T: Serialize,
245{
246 serde_json::to_string(value).map_err(|e| JsValue::from_str(&e.to_string()))
247}
248
249fn to_js_value<T>(value: &T) -> Result<JsValue, JsValue>
250where
251 T: Serialize,
252{
253 serde_wasm_bindgen::to_value(value).map_err(|e| JsValue::from_str(&e.to_string()))
254}
255
256fn to_js_error<E>(err: E) -> JsValue
257where
258 E: core::fmt::Display,
259{
260 JsValue::from_str(&err.to_string())
261}
262
263#[cfg(test)]
264mod tests {
265 use super::*;
266
267 use byteor_pipeline_plan::{
268 LaneGraphAuthoringV2, PipelinePlanSourceV2, PipelinePlanV2, PlanBoundary,
269 PlanBoundaryKind, PlanConnection, PlanConnectionDestination, PlanConnectionSource,
270 PlanStage, SingleRingAuthoringV2, SingleRingConstraintSetV2,
271 SingleRingStagePlanV2,
272 };
273 use byteor_pipeline_spec::{
274 EndpointCfgV1, EndpointKindV1, LaneCfgV1, LaneGraphV1, LaneKindV1, RoleCfgV1,
275 StageRoleCfgV1,
276 };
277
278 #[test]
279 fn encode_decode_round_trip_matches_canonical_spec() {
280 let spec = PipelineSpecV1::LaneGraph(LaneGraphV1 {
281 on_full: OnFullV1::Block,
282 endpoints: alloc::vec::Vec::from([
283 EndpointCfgV1 {
284 name: "in".into(),
285 kind: EndpointKindV1::Ingress,
286 lane: "a".into(),
287 },
288 EndpointCfgV1 {
289 name: "out".into(),
290 kind: EndpointKindV1::Egress,
291 lane: "b".into(),
292 },
293 ]),
294 lanes: alloc::vec::Vec::from([
295 LaneCfgV1 {
296 name: "a".into(),
297 kind: LaneKindV1::Events,
298 },
299 LaneCfgV1 {
300 name: "b".into(),
301 kind: LaneKindV1::Events,
302 },
303 ]),
304 roles: alloc::vec::Vec::from([RoleCfgV1::Stage(StageRoleCfgV1 {
305 name: "s0".into(),
306 stage: "identity".into(),
307 rx: "a".into(),
308 tx: "b".into(),
309 })]),
310 });
311
312 let spec_json = serde_json::to_string(&spec).unwrap();
313 let encoded = encode_kv(&spec_json).unwrap();
314 let decoded = decode_kv(&encoded).unwrap();
315
316 assert_eq!(decoded, spec_json);
317 }
318
319 #[test]
320 fn validate_plan_returns_ok_for_valid_plan() {
321 let plan = PipelinePlanV2 {
322 name: "wasm-plan-v2".into(),
323 target: ExecutionTargetV1::SingleRing,
324 stages: alloc::vec![
325 PlanStage {
326 name: "s0".into(),
327 stage: "identity".into(),
328 },
329 PlanStage {
330 name: "s1".into(),
331 stage: "add_u8".into(),
332 },
333 ],
334 boundaries: alloc::vec![
335 PlanBoundary {
336 name: "in".into(),
337 kind: PlanBoundaryKind::Ingress,
338 },
339 PlanBoundary {
340 name: "out".into(),
341 kind: PlanBoundaryKind::Egress,
342 },
343 ],
344 source: PipelinePlanSourceV2::SingleRing(SingleRingAuthoringV2 {
345 ingress_boundary: "in".into(),
346 egress_boundary: "out".into(),
347 stages: alloc::vec![
348 SingleRingStagePlanV2 {
349 stage: "s0".into(),
350 depends_on: alloc::vec![],
351 },
352 SingleRingStagePlanV2 {
353 stage: "s1".into(),
354 depends_on: alloc::vec!["s0".into()],
355 },
356 ],
357 constraints: SingleRingConstraintSetV2::default(),
358 }),
359 };
360
361 let result = validate_plan_impl(&serde_json::to_string(&plan).unwrap()).unwrap();
362 assert!(result.ok);
363 assert!(result.diagnostics.is_empty());
364 }
365
366 #[test]
367 fn lower_returns_spec_and_report_json() {
368 let plan = PipelinePlanV2 {
369 name: "wasm-smoke-v2".into(),
370 target: ExecutionTargetV1::LaneGraph,
371 stages: alloc::vec![PlanStage {
372 name: "s0".into(),
373 stage: "identity".into(),
374 }],
375 boundaries: alloc::vec![
376 PlanBoundary {
377 name: "in".into(),
378 kind: PlanBoundaryKind::Ingress,
379 },
380 PlanBoundary {
381 name: "out".into(),
382 kind: PlanBoundaryKind::Egress,
383 },
384 ],
385 source: PipelinePlanSourceV2::LaneGraph(LaneGraphAuthoringV2 {
386 boundary_lane: byteor_pipeline_plan::LaneIntent::Events,
387 connections: alloc::vec![
388 PlanConnection {
389 from: PlanConnectionSource::BoundaryIngress {
390 boundary: "in".into(),
391 },
392 to: PlanConnectionDestination::StageIn { stage: "s0".into() },
393 intent: byteor_pipeline_plan::LaneIntent::Events,
394 },
395 PlanConnection {
396 from: PlanConnectionSource::StageOut { stage: "s0".into() },
397 to: PlanConnectionDestination::BoundaryEgress {
398 boundary: "out".into(),
399 },
400 intent: byteor_pipeline_plan::LaneIntent::Events,
401 },
402 ],
403 on_full: OnFullV1::Drop,
404 }),
405 };
406
407 let result = lower_impl(&serde_json::to_string(&plan).unwrap(), ExecutionTargetV1::LaneGraph)
408 .unwrap();
409 assert!(result.ok);
410 let spec: PipelineSpecV1 = serde_json::from_str(result.spec_json.as_ref().unwrap()).unwrap();
411 match spec {
412 PipelineSpecV1::LaneGraph(lg) => assert_eq!(lg.on_full, OnFullV1::Drop),
413 PipelineSpecV1::SingleRing(_) => panic!("expected lane_graph spec"),
414 }
415 assert!(result.report_json.as_ref().unwrap().contains("diagnostics"));
416 assert!(result
417 .diagnostics
418 .iter()
419 .all(|diagnostic| !matches!(diagnostic, LowerDiagnosticV2::Rejected { .. })));
420 }
421
422 #[test]
423 fn lower_returns_structured_diagnostics_on_rejected_input() {
424 let plan = PipelinePlanV2 {
425 name: "wasm-bad-v2".into(),
426 target: ExecutionTargetV1::SingleRing,
427 stages: alloc::vec![
428 PlanStage {
429 name: "s0".into(),
430 stage: "identity".into(),
431 },
432 PlanStage {
433 name: "s1".into(),
434 stage: "add_u8".into(),
435 },
436 ],
437 boundaries: alloc::vec![
438 PlanBoundary {
439 name: "in".into(),
440 kind: PlanBoundaryKind::Ingress,
441 },
442 PlanBoundary {
443 name: "out".into(),
444 kind: PlanBoundaryKind::Egress,
445 },
446 ],
447 source: PipelinePlanSourceV2::SingleRing(SingleRingAuthoringV2 {
448 ingress_boundary: "in".into(),
449 egress_boundary: "out".into(),
450 stages: alloc::vec![
451 SingleRingStagePlanV2 {
452 stage: "s0".into(),
453 depends_on: alloc::vec!["s1".into()],
454 },
455 SingleRingStagePlanV2 {
456 stage: "s1".into(),
457 depends_on: alloc::vec![],
458 },
459 ],
460 constraints: SingleRingConstraintSetV2::default(),
461 }),
462 };
463
464 let result = lower_impl(
465 &serde_json::to_string(&plan).unwrap(),
466 ExecutionTargetV1::SingleRing,
467 )
468 .unwrap();
469
470 assert!(!result.ok);
471 assert!(result.spec_json.is_none());
472 assert!(result.report_json.is_none());
473 assert_eq!(
474 result.diagnostics,
475 alloc::vec![LowerDiagnosticV2::Rejected {
476 target: ExecutionTargetV1::SingleRing,
477 category: LowerDiagnosticCategoryV2::PlanValidation,
478 message: "single_ring dependencies must reference earlier authored stages".into(),
479 }]
480 );
481 }
482
483 #[test]
484 fn lower_json_shape_is_locked_for_success_and_failure() {
485 let ok_json = serde_json::to_string(&CompileResult {
486 ok: true,
487 spec_json: Some("{\"kind\":\"lane_graph\"}".into()),
488 report_json: Some("{\"lower_diagnostics\":[]}".into()),
489 diagnostics: alloc::vec![],
490 })
491 .unwrap();
492 let err_json = serde_json::to_string(&CompileResult {
493 ok: false,
494 spec_json: None,
495 report_json: None,
496 diagnostics: alloc::vec![LowerDiagnosticV2::Rejected {
497 target: ExecutionTargetV1::LaneGraph,
498 category: LowerDiagnosticCategoryV2::PolicyConflict,
499 message: "out-degree > 1 requires fanout intent on all outgoing edges".into(),
500 }],
501 })
502 .unwrap();
503
504 assert!(ok_json.contains("\"ok\""));
505 assert!(ok_json.contains("\"spec_json\""));
506 assert!(ok_json.contains("\"report_json\""));
507 assert!(ok_json.contains("\"diagnostics\""));
508 assert!(err_json.contains("\"category\":\"policy_conflict\""));
509 assert!(err_json.contains("\"target\":\"lane_graph\""));
510 assert!(err_json.contains("\"message\""));
511 }
512
513 #[test]
514 fn lower_single_ring_returns_single_ring_spec() {
515 let plan = PipelinePlanV2 {
516 name: "ring-plan".into(),
517 target: ExecutionTargetV1::SingleRing,
518 stages: alloc::vec![
519 PlanStage {
520 name: "s0".into(),
521 stage: "identity".into(),
522 },
523 PlanStage {
524 name: "s1".into(),
525 stage: "add_u8".into(),
526 },
527 ],
528 boundaries: alloc::vec![
529 PlanBoundary {
530 name: "in".into(),
531 kind: PlanBoundaryKind::Ingress,
532 },
533 PlanBoundary {
534 name: "out".into(),
535 kind: PlanBoundaryKind::Egress,
536 },
537 ],
538 source: PipelinePlanSourceV2::SingleRing(SingleRingAuthoringV2 {
539 ingress_boundary: "in".into(),
540 egress_boundary: "out".into(),
541 stages: alloc::vec![
542 SingleRingStagePlanV2 {
543 stage: "s0".into(),
544 depends_on: alloc::vec![],
545 },
546 SingleRingStagePlanV2 {
547 stage: "s1".into(),
548 depends_on: alloc::vec!["s0".into()],
549 },
550 ],
551 constraints: SingleRingConstraintSetV2::default(),
552 }),
553 };
554
555 let result = lower_impl(&serde_json::to_string(&plan).unwrap(), ExecutionTargetV1::SingleRing)
556 .unwrap();
557 let spec: PipelineSpecV1 = serde_json::from_str(result.spec_json.as_ref().unwrap()).unwrap();
558 assert!(result.ok);
559 assert!(matches!(spec, PipelineSpecV1::SingleRing(_)));
560 }
561
562 #[test]
563 fn dot_returns_graphviz_for_valid_spec() {
564 let spec = PipelineSpecV1::LaneGraph(LaneGraphV1 {
565 on_full: OnFullV1::Block,
566 endpoints: alloc::vec::Vec::from([
567 EndpointCfgV1 {
568 name: "in".into(),
569 kind: EndpointKindV1::Ingress,
570 lane: "a".into(),
571 },
572 EndpointCfgV1 {
573 name: "out".into(),
574 kind: EndpointKindV1::Egress,
575 lane: "b".into(),
576 },
577 ]),
578 lanes: alloc::vec::Vec::from([
579 LaneCfgV1 {
580 name: "a".into(),
581 kind: LaneKindV1::Events,
582 },
583 LaneCfgV1 {
584 name: "b".into(),
585 kind: LaneKindV1::Events,
586 },
587 ]),
588 roles: alloc::vec::Vec::from([RoleCfgV1::Stage(StageRoleCfgV1 {
589 name: "s0".into(),
590 stage: "identity".into(),
591 rx: "a".into(),
592 tx: "b".into(),
593 })]),
594 });
595
596 let dot_text = dot(&serde_json::to_string(&spec).unwrap()).unwrap();
597 assert!(dot_text.contains("digraph"));
598 assert!(dot_text.contains("role_s0"));
599 assert!(dot_text.contains("endpoint_in"));
600 assert!(dot_text.contains("endpoint_out"));
601 }
602}
603
604#[cfg(all(test, target_arch = "wasm32"))]
605mod wasm_tests {
606 use super::*;
607
608 use byteor_pipeline_plan::{
609 LaneGraphAuthoringV2, PipelinePlanSourceV2, PipelinePlanV2, PlanBoundary,
610 PlanBoundaryKind, PlanConnection, PlanConnectionDestination, PlanConnectionSource,
611 PlanStage,
612 };
613 use byteor_pipeline_spec::{
614 EndpointCfgV1, EndpointKindV1, LaneCfgV1, LaneGraphV1, LaneKindV1, RoleCfgV1,
615 StageRoleCfgV1,
616 };
617 use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
618
619 wasm_bindgen_test_configure!(run_in_browser);
620
621 fn sample_plan() -> PipelinePlanV2 {
622 PipelinePlanV2 {
623 name: "browser-smoke".into(),
624 target: ExecutionTargetV1::LaneGraph,
625 stages: alloc::vec![PlanStage {
626 name: "s0".into(),
627 stage: "identity".into(),
628 }],
629 boundaries: alloc::vec![
630 PlanBoundary {
631 name: "in".into(),
632 kind: PlanBoundaryKind::Ingress,
633 },
634 PlanBoundary {
635 name: "out".into(),
636 kind: PlanBoundaryKind::Egress,
637 },
638 ],
639 source: PipelinePlanSourceV2::LaneGraph(LaneGraphAuthoringV2 {
640 boundary_lane: byteor_pipeline_plan::LaneIntent::Events,
641 connections: alloc::vec![
642 PlanConnection {
643 from: PlanConnectionSource::BoundaryIngress {
644 boundary: "in".into(),
645 },
646 to: PlanConnectionDestination::StageIn { stage: "s0".into() },
647 intent: byteor_pipeline_plan::LaneIntent::Events,
648 },
649 PlanConnection {
650 from: PlanConnectionSource::StageOut { stage: "s0".into() },
651 to: PlanConnectionDestination::BoundaryEgress {
652 boundary: "out".into(),
653 },
654 intent: byteor_pipeline_plan::LaneIntent::Events,
655 },
656 ],
657 on_full: OnFullV1::Block,
658 }),
659 }
660 }
661
662 fn sample_spec_json() -> String {
663 let spec = PipelineSpecV1::LaneGraph(LaneGraphV1 {
664 on_full: OnFullV1::Block,
665 endpoints: alloc::vec::Vec::from([
666 EndpointCfgV1 {
667 name: "in".into(),
668 kind: EndpointKindV1::Ingress,
669 lane: "a".into(),
670 },
671 EndpointCfgV1 {
672 name: "out".into(),
673 kind: EndpointKindV1::Egress,
674 lane: "b".into(),
675 },
676 ]),
677 lanes: alloc::vec::Vec::from([
678 LaneCfgV1 {
679 name: "a".into(),
680 kind: LaneKindV1::Events,
681 },
682 LaneCfgV1 {
683 name: "b".into(),
684 kind: LaneKindV1::Events,
685 },
686 ]),
687 roles: alloc::vec::Vec::from([RoleCfgV1::Stage(StageRoleCfgV1 {
688 name: "s0".into(),
689 stage: "identity".into(),
690 rx: "a".into(),
691 tx: "b".into(),
692 })]),
693 });
694
695 serde_json::to_string(&spec).unwrap()
696 }
697
698 #[wasm_bindgen_test]
699 fn lower_preview_flow_runs_in_browser() {
700 let plan_json = serde_json::to_string(&sample_plan()).unwrap();
701 let compile_result: CompileResult =
702 serde_wasm_bindgen::from_value(lower_lane_graph(&plan_json).unwrap()).unwrap();
703
704 let spec: PipelineSpecV1 = serde_json::from_str(compile_result.spec_json.as_ref().unwrap()).unwrap();
705 let report: LowerReportV2 = serde_json::from_str(compile_result.report_json.as_ref().unwrap()).unwrap();
706 let validate_result: ValidateResult =
707 serde_wasm_bindgen::from_value(validate(compile_result.spec_json.as_ref().unwrap()).unwrap()).unwrap();
708
709 assert!(matches!(spec, PipelineSpecV1::LaneGraph(_)));
710 assert_eq!(report.spec, spec);
711 assert!(compile_result.ok);
712 assert!(validate_result.ok);
713 assert!(validate_result.errors.is_empty());
714 assert!(!describe(compile_result.spec_json.as_ref().unwrap()).unwrap().is_empty());
715 }
716
717 #[wasm_bindgen_test]
718 fn import_export_flow_preserves_canonical_identity() {
719 let spec_json = sample_spec_json();
720
721 let encoded = encode_kv(&spec_json).unwrap();
722 let decoded_json = decode_kv(&encoded).unwrap();
723 let reencoded = encode_kv(&decoded_json).unwrap();
724
725 let validate_result: ValidateResult =
726 serde_wasm_bindgen::from_value(validate(&decoded_json).unwrap()).unwrap();
727 let diff_result: DiffResult =
728 serde_wasm_bindgen::from_value(diff(&spec_json, &decoded_json).unwrap()).unwrap();
729
730 assert!(validate_result.ok);
731 assert!(validate_result.errors.is_empty());
732 assert_eq!(encoded, reencoded);
733 assert!(diff_result.equal);
734 assert_eq!(diff_result.spec_hash_a, diff_result.spec_hash_b);
735 assert_eq!(diff_result.canonical_kv_a, diff_result.canonical_kv_b);
736 }
737}