mirror of
https://github.com/logos-co/logos-test-modules.git
synced 2026-08-27 18:21:07 +00:00
* fix(fullapi-ui-qml): normalize event payloads to native JS Qt exposes a list-type event payload (e.g. a [tstr] event) to QML as a non-native JS *sequence* (Array.isArray === false), so the plugin's deepEqual — which uses Array.isArray — rejected received list payloads even when their contents matched (scalars and objects were unaffected). JSON round-trip the received payload to native JS arrays/objects, exactly how method results already arrive (logos.callModule returns a JSON string this plugin JSON.parses). Scalars pass through unchanged. This unblocks the events step of the basecamp-fullapi-ui-qml doctest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(conformance): the LIDL type matrix — case table + xfail registry One question per cell: does a value of LIDL type T, in position P, survive provider R -> consumer K intact? `full_api` had 31 methods and 14 events and the native check made SIX assertions against them. The rest was cardinality — the C++ proxy reports `intList=3 uintList=2`, which passes whatever the elements became — or an aggregate `ALL_OK` asserted over the COMBINED output of both providers, so one provider alone could satisfy it while the other was broken. That is not a weak test suite, it is a structural one: with per-consumer hand-written checks, covering a type costs work in every consumer, so it does not happen. conformance/cases.json is the language-neutral table every driver replays. conformance/known.json is the xfail registry. Both live here, with the providers they describe; the drivers live with the client each one uses (the py driver is in logos-logoscore-py, which already depends on this repo). What the table adds beyond what existed: the whole VALUE axis. Boundary (int64 min/max, uint64 max, 2^53+1, all 256 byte values, embedded NUL, empty string/bytes/list/map), hostile (-1 into a uint, 3.7 into an int, mixed-type array elements) and adversarial (a user map whose only key is `_bytes`; a map carrying `__logos_pending_call__`). Every expectation was MEASURED against both providers, not assumed — several claims that looked obvious were wrong. Four properties make it honest: - a red cell is a COORDINATE (`[uint]/method_arg/test_fullapi_rust/py`), never an aggregate token; - every case runs against BOTH providers and their answers are compared to each other independently of `expect` — that differential needs nobody to know the right answer in advance, and it is what surfaced the `void` divergence; - a known-broken cell is a registry entry, and one that starts passing is an `xpass` that FAILS the run, so a fix cannot land unnoticed; - coverage is computed from the .lidl: a declared (type, position) with no case fails the run. That is the guard against 31-methods-6-assertions recurring. Adding a type now costs one LIDL line, one impl method per provider and N table rows — zero per-consumer cost. Four defects are registered with their measurements (known.json): M1/M1b a uint64 above int64max degrades to a double once nested in a container; exact as a top-level scalar. The C++ provider LOOKS green for [uint] because its typed decode coerces the double back — the Rust provider takes it untyped and reports the loss faithfully. Same defect, one surface masking it. M2 `void`: the C++ provider answers JSON true, the Rust one fails the call outright. M3/M4 the `_bytes` and `__logos_pending_call__` keys are forgeable from user data — one silently reinterprets a map as bytes, the other hijacks the call. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(conformance): multi-argument arity + a check that the 5 contract copies agree Two of the six positions had ZERO coverage: an argument in slot k of n, and an event parameter in slot k of n. Every `full_api` method took 0 or 1 argument and every event exactly 1, so nothing anywhere exercised positional dispatch — and both generators emit positional code (`args.at(i)` / `args::as_*(args, i)`). A `bstr` had also never appeared anywhere but first. method echoTriple(i: int, s: tstr, b: bstr) -> tstr method fireTripleEvent(i: int, s: tstr, b: bstr) -> bool event tripleEvent(i: int, s: tstr, b: bstr) The return is a digest — `i=<decimal>|s=<utf8>|b=<lowercase hex>` — so ONE comparison pins all three values AND their order: swap two arguments and it changes. A container return would have confounded "the arguments landed in the right slots" with "the container encoding survived". Measured, since the multi-parameter EVENT path had never run at runtime anywhere and was worth checking rather than assuming: both providers answer `i=-7|s=hé|b=00ff`, and the event arrives as {arg0, arg1, arg2} with the bstr (NUL + high byte) decoded. The driver now compares the ordered argument list, so slot order is part of the assertion. Coverage now distinguishes `method_arg` from `method_arg@k`: a sole argument cannot catch a generator that mixes up positional slots, so they are different cells. A multi-argument case declares the (type, position) PAIRS it covers — `int`@0, `tstr`@1, `bstr`@2 — rather than the cross-product of its type list and position list, which would claim cells it never exercises. Second half: check_contract_copies.py. `full_api` exists in FIVE hand-maintained copies — the C++ provider's impl header (the contract is derived from it), the Rust provider's .lidl, the shared interface .lidl, the C++ consumer's .h, and the Rust proxy's re-export .lidl — and nothing enforced that they agree. Adding a method to one and forgetting another produces no error anywhere: each side compiles against its own copy and the matrix only talks to the providers, so the drift surfaces much later as "the interface does not have that method". Adding this arity surface meant editing all five by hand, which is exactly the moment to add the check. It compares method and event SIGNATURES (name, parameter types in order, return type) in LIDL spelling, mapping C++ declarations through a closed table. An unrecognised C++ spelling FAILS rather than being skipped — silently ignoring a type is how a checker like this becomes decorative. Verified it catches both a missing method and the subtler drift of two parameters silently reordered. Matrix: 79 cases x 2 providers, green. fullapi chain check and the module suite still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(conformance): the full_api_ext contract — records, bytes at depth, typed maps The composite tail of the matrix. These types cannot go in `full_api`: it is implemented by both providers, the C++ one is header-first, and the C++ cdylib backend's typeSupported() gate rejects records and [bstr] BY NAME while its impl-header parser skips `struct` entirely — a header-first C++ provider cannot even declare a record. Adding them there would break test_fullapi_cpp's build rather than test anything. So: a separate contract, Rust-first, with one provider and therefore no differential column. That is a gap, recorded as one, not a design choice. New: test-fullapi-ext-module-rust (a contract-first Rust cdylib), ext-cases.json, known-ext.json, and a `conformance-matrix-ext` check. 20 cases, full contract coverage, green with 9 registered xfails. It found three defects on its first run — all measured, none assumed: E1 A bstr NESTED in a container is UTF-8 mangled. `00 80 ff` comes back as `00 EF BF BD EF BF BD` — every byte >= 0x80 replaced by U+FFFD and re-encoded. Through a [bstr], a {tstr:bstr}, a {tstr:[bstr]}, and a record's bstr field. A top-level bstr is exact on every provider, so the loss is nesting-specific. This is precisely what the canonical {"_bytes"} tag was introduced to prevent (protocol #21/#23), defeated one level down. Also reachable through the EXISTING full_api surface — echoMap/echoList/ echoAny with a tagged value inside — so three cells and registry entry M5 were added to the main table too, where they are red on BOTH providers. The corruption is INBOUND, deducible rather than guessed: echoBlob decodes the field into Vec<u8> and re-encodes it with the same tagged encoder the scalar path uses, and returned SIX bytes for a three-byte input. It faithfully re-tagged an already-replaced value, so neither the module nor its return path is where the bytes are lost. E2 An EMPTY bstr nested in a container arrives as `null` and fails the call: `expected bytes at arg0[1].payload, got null`. Distinct from E1 — dropped rather than corrupted, which is the louder failure mode. A top-level empty bstr is exact. E3 M1 (uint64 above int64max degrading to a double once nested) seen through a record field, with the exact path: `expected integer at arg0.n, got number`. Neither E1 nor E2 is fixed here. Both are wire-representation questions in the same family as M1, and I could not localize the hop by reading — the protocol's own JSON/QVariant bridge handles bytes and unsigned correctly and documents the hazard. Guessing a site and "fixing" it would be worse than recording what was measured. The driver's provider set is now data-driven (`--modules NAME=DIR`), so a table with a different provider set runs through the same driver rather than a second one; the differential simply has nothing to compare when a table has one provider. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(conformance): a C++ ext provider — the ext table gets its differential test_fullapi_ext_cpp mirrors test_fullapi_ext_rust method for method, so the composite tail of the matrix (records, bytes at depth, typed maps, nested composites) is now checked on BOTH surfaces instead of one. It is header-first like test_fullapi_cpp: the records are C++ structs in the impl header and the contract is derived from them, which is possible only because the impl-header parser learned `struct` (logos-cpp-sdk 3fd6841). What the second provider immediately bought, in its first run: - {tstr:int}, {tstr:tstr} and [[int]] round-trip EXACTLY on C++. Those types were previously not expressible there at all — the gate rejected them by name — so this is new coverage, not a re-check. - the differential surfaced that the two providers DISAGREE on every bytes-at-depth cell, and in an informative direction: the C++ codec type-checks the byte field and REJECTS the mangled value (`expected bytes at arg0.payload, got string`), while the Rust side takes [bstr] untyped (serde_json::Value) and passes the corruption through. Same defect (E1), loud on one surface and silent on the other. Fixing E1 makes both green; until then the C++ behaviour is the better one. The registry is now built FROM the measurement rather than by hand, with per-case provider precision: 4 of E1's 9 cases fail on only one provider, and registering them against both made the passing provider report `xpass` — the registry manufacturing a failure. The driver honours `per_case_providers`. One correction worth recording. I first wrote this module contract-first (interface: "cdylib" + codegen.impl_class) and it SIGSEGV'd on the first call; a minimal one-method version crashed identically, and so did a build with the completely unmodified pinned generator — which looked like proof that the contract-first C++ cdylib flavour is broken on master, since no module in the workspace uses it. It was not. My CMakeLists came from the Rust template, which has no SOURCES because the crate is a staticlib; without it the C++ impl translation unit is never compiled, the plugin links with the impl's symbols undefined and crashes on first call instead of failing to build. The comment in the CMakeLists now says so. 20 cases x 2 providers, green with 22 registered xfails. The 5 full_api contract copies still agree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(conformance): E1/E2 are fixed by the canonical codec — verified, not merged E1 (a bstr nested in a container UTF-8 mangled), E2 (an empty nested bstr arriving as null) and M1/E3 (a uint64 above int64max degrading to a double once nested) are ALL fixed by logos-protocol `feat/canonical-codec` — the branch that folds the Qt and plain-wire copies into one canonical LIDL<->JSON codec. That branch is 3 commits, a clean fast-forward onto master, and its own 199 tests pass. Measured against it, not predicted: full_api : 11 cells flip to xpass (every M1 and M5 cell) ext : ALL 22 xfails flip to xpass, and the differential goes from 10 pass / 9 xfail to 19/19 — the two ext providers now agree completely So the answer to "fix E1 and E2" is that the fix already exists and was never landed. The entries stay registered here because the workspace pins the PRE-fix protocol, which is what CI runs; when the re-pin lands they report xpass and these entries must be deleted — the registry's own forcing function. How it was found, since the investigation was misleading in an instructive way: every layer I tested in isolation came back CLEAN — the QVariant/json bridge, the plain transport's QVariant/RpcValue conversion, QDataStream marshalling, the whole CLI-to-daemon chain. That was because I was compiling the LOCAL protocol, which already has the fix, while the running daemon used the PINNED build. Probes on both sides of the boundary settled it in one run: core_service saw the tagged value and the module received a mangled string. One consequence to expect at re-pin time, recorded in each hostile case's `why`: the Rust provider becomes STRICT. echoUint(-1), echoInt(3.7), echoBool(1), a mixed-element [tstr], a scalar for [any] and a scalar for {tstr:any} all change from a coerced value to dispatch_failed, because the host stops coercing before the module's LIDL-type validation sees the value. That is the more correct behaviour — the provider now enforces its declared types — but it IS a semantic change, and the pinned expectations will go red until updated. Not re-pinned here: protocol sits deep in the tree and that semantic change should be a deliberate, reviewed call rather than a side effect of this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(conformance): retire what the codec fixed, and register what it did not The re-pin (logos-protocol 362b03f via cpp-sdk 3d322bd, module-builder, logoscore-cli) landed the canonical codec, so the registry has to be settled. Retired, because they now pass: M1 / M1b a uint64 above int64max degrading to a double once nested M5 a bstr nested in a container UTF-8 mangled E1/E2/E3 the same two defects through the ext contract, plus M1 seen through a record field The ext table is now FULLY green — 40/40, no xfails, differential 19/19. Its cases stay exactly where they are: they were red cells, they are now regression guards, which is the whole point of pinning a defect as a case rather than a comment. Registered, because it did NOT pass — M6: echoUint(2^64-1) method -> 18446744073709551615 exact uintEvent(2^64-1) event -> 1.8446744073709552e+19 degraded Same value, same process, one hop later. The canonical codec fixed the method path; an event payload leaves the module by a different route and still loses a value whose type its container does not declare. Worth saying how M6 surfaced: `event/uint/boundary` was one of M1's cases, so deleting M1 wholesale turned it from a registered xfail into a hard failure. The registry caught a fix that covered most of an entry's cases but not all of them — exactly the thing an xfail list is supposed to prevent you from waving through. The six hostile expectations are re-measured, not adjusted to taste: the Rust provider now answers dispatch_failed where it used to coerce (-1 for a uint, 3.7 for an int, 1 for a bool, a mixed-element [tstr], a scalar for [any] or {tstr:any}), because the host stops coercing before the module's LIDL-type validation sees the value. The C++ side stays lenient, so the divergence moved rather than closed — each case's `why` records that. full_api: 156 pass / 8 xfail (M2 void, M3/M4 the forgeable reserved keys, M6). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: re-pin onto the merged protocol/cpp-sdk chain logos-module-builder -> ab0c776 (protocol 362b03f, cpp-sdk 3d322bd) logos-liblogos -> 5c8b9f0 (protocol 362b03f, after #167) logos-logoscore-cli -> f4753dd This makes test_fullapi_ext_cpp build: its records need the impl-header parser and cdylib codec from cpp-sdk 3d322bd, which reach here through module-builder. test_fullapi_ext_rust still does NOT build — its Blob/Wrapper come from the Rust provider codegen in logos-rust-sdk#29, which is still open. module-builder pins rust-sdk a55fdac (pre-records), so that module needs: rust-sdk#29 merged -> module-builder bumps rust-sdk -> re-pin here. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * chore: re-pin onto the merged rust-sdk records support logos-module-builder -> 9ac3235 (logos-rust-sdk 2eabc95, via #167) logos-logoscore-cli -> a143727 (64-bit call args, #74) This is the commit that makes test_fullapi_ext_rust build: its Blob/Wrapper structs come from the Rust provider codegen in logos-rust-sdk#29, which reaches here only through module-builder's by-rev rust-sdk pin. All four conformance providers now build against a single merged closure: test_fullapi_cpp, test_fullapi_rust, test_fullapi_ext_cpp, test_fullapi_ext_rust. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
25 lines
955 B
CMake
25 lines
955 B
CMake
cmake_minimum_required(VERSION 3.14)
|
|
project(TestFullapiExtCppPlugin LANGUAGES CXX)
|
|
|
|
if(DEFINED ENV{LOGOS_MODULE_BUILDER_ROOT})
|
|
include($ENV{LOGOS_MODULE_BUILDER_ROOT}/cmake/LogosModule.cmake)
|
|
elseif(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake/LogosModule.cmake")
|
|
include(cmake/LogosModule.cmake)
|
|
else()
|
|
message(FATAL_ERROR "LogosModule.cmake not found")
|
|
endif()
|
|
|
|
# Universal module: `interface: "universal"` makes the builder derive the
|
|
# contract from the impl header — records included — and emit the Qt plugin
|
|
# glue, the dispatch and the codec into generated_code/.
|
|
#
|
|
# SOURCES is not optional for a C++ module: without it the impl translation
|
|
# unit is never compiled, the plugin links with the impl's symbols undefined,
|
|
# and the module SIGSEGVs on the first call rather than failing to build.
|
|
logos_module(
|
|
NAME test_fullapi_ext_cpp
|
|
SOURCES
|
|
src/test_fullapi_ext_cpp_impl.h
|
|
src/test_fullapi_ext_cpp_impl.cpp
|
|
)
|