fix(glue): a void method's REFUSAL is not a successful void call

The void arm discarded the dispatch reply. `value = QVariant(true)` was
unconditional: the JSON was parsed, then thrown away, so a provider answering
{"code":"invalid_args", ...} reached the caller as a successful void call.

Invisible until the providers gained something to refuse with. With the arity
upper bound in place (logos-cpp-sdk #150, logos-rust-sdk #50) the provider DOES
refuse `doVoid("junk")` -- the built module carries the `expected 0 arguments,
got ` literal, checked in the exact store path the failing run consumed -- and
the conformance cell still reported `true`, on both providers and all three
consumers. The twelve cells of the non-void arity cases went green in the same
run; these six did not, because their refusal died here rather than at the
provider.

Which is the same defect one layer down from the one the arity work started
from: an answer the caller cannot distinguish from a real one. There the
provider dropped an argument and answered anyway; here the provider refused
correctly and the glue reported success.

The closed set is the one logos-qt-sdk's consumer detector uses --
dispatch_failed / invalid_args / unknown_method -- matched only on an OBJECT
carrying a string `code`. A void method whose provider legitimately answers an
object is unaffected, and there is no such method: the contract says it returns
nothing. An ordinary reply still answers `true`; this is a branch, not a
replacement, and the test pins both sides.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Dario Gabriel Lipicar
2026-08-24 10:53:25 -03:00
co-authored by Claude Opus 5
parent c459e47787
commit 156e73f23e
2 changed files with 47 additions and 1 deletions
+29 -1
View File
@@ -328,8 +328,36 @@ QString lidlMakeCdylibGlueSource(const ModuleDecl& module, bool multi)
s << " logos_module_string_free(result);\n";
s << " if (!jResult.is_discarded()) {\n";
if (!voidMethods.isEmpty()) {
// A void method's reply is not "nothing to read" -- it is the only
// place the provider can REFUSE the call, and this branch used to
// discard it. `value = QVariant(true)` was unconditional: the
// dispatch JSON was parsed, then thrown away, so a provider that
// answered {"code":"invalid_args", ...} was reported to the caller
// as a successful void call.
//
// Measured on failure/B/arity/too-many-zero-parameter: with the
// arity upper bound in place (logos-cpp-sdk #150, logos-rust-sdk
// #50) the provider DOES refuse `doVoid("junk")` -- the built
// module carries the `expected 0 arguments, got ` literal -- and
// the cell still reported `true` on both providers and all three
// consumers, because the refusal died here rather than at the
// provider. The same six cells stayed red while the twelve cells
// of the non-void arity cases went green.
//
// The closed set is the one logos-qt-sdk's consumer detector uses
// (dispatch_failed / invalid_args / unknown_method), matched only
// on an OBJECT carrying a string `code`, so a void method whose
// provider legitimately answers an object is unaffected -- and
// there is no such method, since the contract says it returns
// nothing.
s << " if (isVoidMethod) {\n";
s << " value = QVariant(true);\n";
s << " const bool __rejected = jResult.is_object()\n";
s << " && jResult.contains(\"code\") && jResult[\"code\"].is_string()\n";
s << " && (jResult[\"code\"] == \"dispatch_failed\"\n";
s << " || jResult[\"code\"] == \"invalid_args\"\n";
s << " || jResult[\"code\"] == \"unknown_method\");\n";
s << " value = __rejected ? logos::nlohmannToQVariant(jResult)\n";
s << " : QVariant(true);\n";
s << " } else\n";
}
if (!resultMethods.isEmpty()) {
+18
View File
@@ -150,6 +150,24 @@ pkgs.runCommand "logos-qt-host-generator-test" {
|| { echo "multi worker lambda reads $flag but does not capture it: $worker_capture"; exit 1; }
done
# A void method's REFUSAL must survive. This branch used to be an
# unconditional `value = QVariant(true)`: the dispatch JSON was parsed and
# then discarded, so a provider answering {"code":"invalid_args", ...} was
# reported to the caller as a successful void call. With the arity upper
# bound in the generated dispatches (logos-cpp-sdk #150, logos-rust-sdk #50)
# the provider genuinely refuses `doVoid("junk")` -- and the conformance cell
# still went green-on-`true` until this branch stopped throwing it away.
grep -q '__rejected' $cm \
|| { echo "the void arm does not test the reply for a rejection"; exit 1; }
for code in dispatch_failed invalid_args unknown_method; do
grep -q "\"$code\"" $cm \
|| { echo "the void arm's rejection set omits $code"; exit 1; }
done
# It must still answer `true` for an ordinary void reply -- the fix is a
# branch, not a replacement.
grep -q 'QVariant(true)' $cm \
|| { echo "the void arm no longer answers true for a normal reply"; exit 1; }
# void WITHOUT result the fourth combination, and the one neither
# hand-written capture list could express: it captured neither flag while the
# body still named isVoidMethod, so `concurrency: "multi"` on any module with