diff --git a/src/v1/compiler_tests_rust.dag b/src/v1/compiler_tests_rust.dag index a563a76a381..939a39d59b6 100644 --- a/src/v1/compiler_tests_rust.dag +++ b/src/v1/compiler_tests_rust.dag @@ -3143,6 +3143,22 @@ fn ct_shell_service_output_projection_test() -> String { // no modeled channel must REFUSE, typed and located, rather than being answered // with stdout. The refusal is minted at the BINDING, before a target is chosen, // so there is no renderer default arm left for it to fall through. +// +// ASSERT THROUGH THE WALL, NOT ABOUT THE BYTES. This test first asked for the +// refusal TEXT inside an emitted src/probe.rs, and it was RED on main for exactly +// as long as that shape stood, because emit_artifact in v1.compiler.compile pairs +// the refusal EXPRESSION with a BLOCKING diagnostic and returns `files: []` -- +// the compile itself stops, so there is no src/probe.rs to read. The weaker claim +// was not merely unsatisfiable here; it was satisfiable by precisely the class the +// wall exists to close, refusal_deferred_to_emitted_runtime: a compile that reports +// success while shipping bytes that carry the refusal to a runtime nobody reads. +// So the assertion is now the compile's own verdict -- a located diagnostic naming +// the key, and the ABSENCE of any emitted body for the refused module. +// +// ITS POSITIVE CONTROL IS THE SIBLING ABOVE. shell_service_output_projection_binds +// _each_declared_channel compiles the same fixture with every key modeled and gets +// src/probe.rs with zero diagnostics, so "no file" here is the refusal firing and +// not an emitter that emits nothing for services. fn ct_shell_service_unmodeled_output_key_refusal_test() -> String { concat( " #[test]\n", @@ -3158,23 +3174,51 @@ fn ct_shell_service_unmodeled_output_key_refusal_test() -> String { " std::rc::Rc::new(im::vector![source]),\n", " crate::v1_compiler_artifact::RenderTarget::Rust,\n", " );\n", - " let emitted = r\n", - " .files\n", + " let errors: Vec<_> = r\n", + " .diagnostics\n", " .iter()\n", - " .find(|f| f.path == \"src/probe.rs\")\n", - " .map(|f| f.content.clone())\n", - " .expect(\"service module must emit src/probe.rs\");\n", + " .filter(|d| crate::v1_std_core::is_error_diagnostic(d.diagnostic.clone()))\n", + " .collect();\n", + " // The line stops, and it stops with the TYPED cause -- not merely\n", + " // with some error that happens to be present.\n", " assert!(\n", - " emitted.contains(\"not_a_channel\")\n", - " && emitted.contains(\"has no modeled channel\"),\n", - " \"an unmodeled output key must refuse and name the key. Got:\\n{}\",\n", - " emitted\n", + " errors.iter().any(|d| matches!(\n", + " *d.diagnostic,\n", + " crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled { .. }\n", + " )),\n", + " \"an unmodeled output key must refuse with TransportEmissionNotModeled. \\\n", + " Got: {:?}\",\n", + " r.diagnostics\n", + " );\n", + " // ...and it is LOCATED at the field: the message names the key the\n", + " // author wrote, so an operation with one bad key among several says\n", + " // which one.\n", + " let messages: Vec = errors\n", + " .iter()\n", + " .map(|d| crate::v1_std_core::diagnostic_to_message(d.diagnostic.clone()))\n", + " .collect();\n", + " assert!(\n", + " messages.iter().any(|m| m.contains(\"not_a_channel\")\n", + " && m.contains(\"has no modeled channel\")),\n", + " \"the refusal must name the unmodeled key. Got: {:?}\",\n", + " messages\n", + " );\n", + " // THE DISCRIMINATING HALF. A refusal deferred into emitted bytes\n", + " // would still satisfy both assertions above while leaving a\n", + " // src/probe.rs behind; the wall is that emit returns no files at all.\n", + " let paths: Vec = r.files.iter().map(|f| f.path.clone()).collect();\n", + " assert!(\n", + " !paths.iter().any(|p| p == \"src/probe.rs\"),\n", + " \"a refused operation must not be emitted at all -- the refusal may \\\n", + " not be deferred into a body. Got files: {:?}\",\n", + " paths\n", " );\n", + " // And nothing anywhere fell through to the stdout channel.\n", " assert!(\n", - " !emitted.contains(\"stdout.clone()\"),\n", + " !r.files.iter().any(|f| f.content.contains(\"stdout.clone()\")),\n", " \"a refused operation must not fall through to the stdout channel. \\\n", - " Got:\\n{}\",\n", - " emitted\n", + " Got files: {:?}\",\n", + " paths\n", " );\n", " })\n", " .expect(\"failed to spawn thread\")\n", diff --git a/src/v1/stage0/src/compiler_tests.rs b/src/v1/stage0/src/compiler_tests.rs index 3a56a28bb83..4e4a54e3833 100644 --- a/src/v1/stage0/src/compiler_tests.rs +++ b/src/v1/stage0/src/compiler_tests.rs @@ -1037,23 +1037,51 @@ mod compiler_tests { std::rc::Rc::new(im::vector![source]), crate::v1_compiler_artifact::RenderTarget::Rust, ); - let emitted = r - .files + let errors: Vec<_> = r + .diagnostics .iter() - .find(|f| f.path == "src/probe.rs") - .map(|f| f.content.clone()) - .expect("service module must emit src/probe.rs"); + .filter(|d| crate::v1_std_core::is_error_diagnostic(d.diagnostic.clone())) + .collect(); + // The line stops, and it stops with the TYPED cause -- not merely + // with some error that happens to be present. assert!( - emitted.contains("not_a_channel") - && emitted.contains("has no modeled channel"), - "an unmodeled output key must refuse and name the key. Got:\n{}", - emitted + errors.iter().any(|d| matches!( + *d.diagnostic, + crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled { .. } + )), + "an unmodeled output key must refuse with TransportEmissionNotModeled. \ + Got: {:?}", + r.diagnostics + ); + // ...and it is LOCATED at the field: the message names the key the + // author wrote, so an operation with one bad key among several says + // which one. + let messages: Vec = errors + .iter() + .map(|d| crate::v1_std_core::diagnostic_to_message(d.diagnostic.clone())) + .collect(); + assert!( + messages.iter().any(|m| m.contains("not_a_channel") + && m.contains("has no modeled channel")), + "the refusal must name the unmodeled key. Got: {:?}", + messages ); + // THE DISCRIMINATING HALF. A refusal deferred into emitted bytes + // would still satisfy both assertions above while leaving a + // src/probe.rs behind; the wall is that emit returns no files at all. + let paths: Vec = r.files.iter().map(|f| f.path.clone()).collect(); assert!( - !emitted.contains("stdout.clone()"), + !paths.iter().any(|p| p == "src/probe.rs"), + "a refused operation must not be emitted at all -- the refusal may \ + not be deferred into a body. Got files: {:?}", + paths + ); + // And nothing anywhere fell through to the stdout channel. + assert!( + !r.files.iter().any(|f| f.content.contains("stdout.clone()")), "a refused operation must not fall through to the stdout channel. \ - Got:\n{}", - emitted + Got files: {:?}", + paths ); }) .expect("failed to spawn thread") diff --git a/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs b/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs index 44c37931bd0..37dd22fad44 100644 --- a/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs +++ b/src/v1/stage0/src/v1_compiler_compiler_tests_rust.rs @@ -385,7 +385,7 @@ pub fn ct_shell_service_output_projection_test() -> String { } pub fn ct_shell_service_unmodeled_output_key_refusal_test() -> String { - v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" #[test]\n".to_string(), " fn shell_service_unmodeled_output_key_refuses() {\n".to_string()), " let result = std::thread::Builder::new()\n".to_string()), " .stack_size(32 * 1024 * 1024)\n".to_string()), " .spawn(|| {\n".to_string()), " let source = std::rc::Rc::new(crate::v1_compiler_compile::SourceFile {\n".to_string()), " path: \"probe.dag\".to_string(),\n".to_string()), " content: \"module probe\\nservice Probe {\\n operation Version {\\n input {}\\n output {\\n first: String from \\\"not_a_channel\\\"\\n out: String from \\\"stdout\\\"\\n }\\n transport shell { argv: [\\\"git\\\", \\\"--version\\\"] }\\n }\\n}\\n\".to_string(),\n".to_string()), " });\n".to_string()), " let r = crate::v1_compiler_compile::compile_sources(\n".to_string()), " std::rc::Rc::new(im::vector![source]),\n".to_string()), " crate::v1_compiler_artifact::RenderTarget::Rust,\n".to_string()), " );\n".to_string()), " let emitted = r\n".to_string()), " .files\n".to_string()), " .iter()\n".to_string()), " .find(|f| f.path == \"src/probe.rs\")\n".to_string()), " .map(|f| f.content.clone())\n".to_string()), " .expect(\"service module must emit src/probe.rs\");\n".to_string()), " assert!(\n".to_string()), " emitted.contains(\"not_a_channel\")\n".to_string()), " && emitted.contains(\"has no modeled channel\"),\n".to_string()), " \"an unmodeled output key must refuse and name the key. Got:\\n{}\",\n".to_string()), " emitted\n".to_string()), " );\n".to_string()), " assert!(\n".to_string()), " !emitted.contains(\"stdout.clone()\"),\n".to_string()), " \"a refused operation must not fall through to the stdout channel. \\\n".to_string()), " Got:\\n{}\",\n".to_string()), " emitted\n".to_string()), " );\n".to_string()), " })\n".to_string()), " .expect(\"failed to spawn thread\")\n".to_string()), " .join();\n".to_string()), " result.expect(\"shell_service_unmodeled_output_key_refuses panicked\");\n".to_string()), " }\n".to_string()), "\n".to_string()) + v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(v1_rt::concat(" #[test]\n".to_string(), " fn shell_service_unmodeled_output_key_refuses() {\n".to_string()), " let result = std::thread::Builder::new()\n".to_string()), " .stack_size(32 * 1024 * 1024)\n".to_string()), " .spawn(|| {\n".to_string()), " let source = std::rc::Rc::new(crate::v1_compiler_compile::SourceFile {\n".to_string()), " path: \"probe.dag\".to_string(),\n".to_string()), " content: \"module probe\\nservice Probe {\\n operation Version {\\n input {}\\n output {\\n first: String from \\\"not_a_channel\\\"\\n out: String from \\\"stdout\\\"\\n }\\n transport shell { argv: [\\\"git\\\", \\\"--version\\\"] }\\n }\\n}\\n\".to_string(),\n".to_string()), " });\n".to_string()), " let r = crate::v1_compiler_compile::compile_sources(\n".to_string()), " std::rc::Rc::new(im::vector![source]),\n".to_string()), " crate::v1_compiler_artifact::RenderTarget::Rust,\n".to_string()), " );\n".to_string()), " let errors: Vec<_> = r\n".to_string()), " .diagnostics\n".to_string()), " .iter()\n".to_string()), " .filter(|d| crate::v1_std_core::is_error_diagnostic(d.diagnostic.clone()))\n".to_string()), " .collect();\n".to_string()), " // The line stops, and it stops with the TYPED cause -- not merely\n".to_string()), " // with some error that happens to be present.\n".to_string()), " assert!(\n".to_string()), " errors.iter().any(|d| matches!(\n".to_string()), " *d.diagnostic,\n".to_string()), " crate::v1_std_core::CompilerDiagnostic::TransportEmissionNotModeled { .. }\n".to_string()), " )),\n".to_string()), " \"an unmodeled output key must refuse with TransportEmissionNotModeled. \\\n".to_string()), " Got: {:?}\",\n".to_string()), " r.diagnostics\n".to_string()), " );\n".to_string()), " // ...and it is LOCATED at the field: the message names the key the\n".to_string()), " // author wrote, so an operation with one bad key among several says\n".to_string()), " // which one.\n".to_string()), " let messages: Vec = errors\n".to_string()), " .iter()\n".to_string()), " .map(|d| crate::v1_std_core::diagnostic_to_message(d.diagnostic.clone()))\n".to_string()), " .collect();\n".to_string()), " assert!(\n".to_string()), " messages.iter().any(|m| m.contains(\"not_a_channel\")\n".to_string()), " && m.contains(\"has no modeled channel\")),\n".to_string()), " \"the refusal must name the unmodeled key. Got: {:?}\",\n".to_string()), " messages\n".to_string()), " );\n".to_string()), " // THE DISCRIMINATING HALF. A refusal deferred into emitted bytes\n".to_string()), " // would still satisfy both assertions above while leaving a\n".to_string()), " // src/probe.rs behind; the wall is that emit returns no files at all.\n".to_string()), " let paths: Vec = r.files.iter().map(|f| f.path.clone()).collect();\n".to_string()), " assert!(\n".to_string()), " !paths.iter().any(|p| p == \"src/probe.rs\"),\n".to_string()), " \"a refused operation must not be emitted at all -- the refusal may \\\n".to_string()), " not be deferred into a body. Got files: {:?}\",\n".to_string()), " paths\n".to_string()), " );\n".to_string()), " // And nothing anywhere fell through to the stdout channel.\n".to_string()), " assert!(\n".to_string()), " !r.files.iter().any(|f| f.content.contains(\"stdout.clone()\")),\n".to_string()), " \"a refused operation must not fall through to the stdout channel. \\\n".to_string()), " Got files: {:?}\",\n".to_string()), " paths\n".to_string()), " );\n".to_string()), " })\n".to_string()), " .expect(\"failed to spawn thread\")\n".to_string()), " .join();\n".to_string()), " result.expect(\"shell_service_unmodeled_output_key_refuses panicked\");\n".to_string()), " }\n".to_string()), "\n".to_string()) } pub fn ct_shell_service_exit_error_arm_boxed_test() -> String {