mirror of
https://github.com/logos-co/logos-test-modules.git
synced 2026-08-27 10:11:12 +00:00
* probe(qt-consumer): throwaway module proving interface_dependencies works on a Qt api-style module
test-qtbind-probe-module is type: core with NO `interface` key, so the
backend picks apiStyle=qt. It declares interface_dependencies on full_api
(a .lidl copy of the shared contract) plus concrete dependencies on both
providers, forwards echoInt, and re-emits intEvent.
Phase-1 throwaway: delete once the real Qt-typed proxy lands.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(conformance): a QT-TYPED consumer of full_api — the matrix's third surface
The matrix had two providers and one consumer coordinate. The existing proxies
do not add another: `interface: "universal"` selects apiStyle=lp and
`interface: "cdylib"` selects the Rust client, so both bypass the Qt generated
wrappers. Replaying the whole case table through them moved 2 cells of 86.
test_fullapi_qtproxy is `type: core` with NO `interface` key, which selects
apiStyle=qt (mkLogosModule.nix picks lp only for universal non-ui_qml), so
`modules().bind_full_api(name)` hands back a Qt-typed wrapper — QString /
QByteArray / qlonglong / qulonglong / QVariantList / QVariantMap / LogosResult.
It forwards all 33 full_api methods and re-emits all 15 events, so the matrix
replays cases.json through it unchanged.
Two things only this surface reaches, both now measured end-to-end under
logoscore against BOTH providers:
* M3. A one-key `_bytes` map is reinterpreted as bytes in
logos_json_convert.cpp on the way to a Qt consumer. At a `{tstr: any}` slot
that is total loss: `echoMap {"_bytes":"AID_"}` answers `{}`, where the same
call at the provider round-trips and looks green.
* The generated ASYNC return table converts with `qvariant_cast<T>(v)` where
the SYNC one uses `_result.toT()`. syncProbe() and probeAsync() /
getAsyncProbe() render the same 18 calls through both tables in one format
so they are diffable; today they agree, and now a change to either is
visible instead of silent.
Also:
* check_contract_copies.py grows from 5 to 8 tracked copies and learns a third
parse kind. The Qt copy is compared by COMPATIBILITY, not equality, because
the Qt style is not 1:1 with LIDL — `QVariantList` is the one type behind
`[any]`, `[int]`, `[uint]`, `[float64]` and `[bool]`. Verified live against
four injected drifts (missing method, wrong type, wrong arity, unknown
spelling); a Qt provider has no declared event block, so its events stay
compiler-enforced rather than silently "checked".
* Subscriptions are per-provider-once and drop deliveries from a provider that
is no longer bound. Without this, re-binding stacked callbacks and every
event arrived three times — measured, and it would have multiplied every
event-position cell.
* Drops test-qtbind-probe-module, the phase-1 throwaway; every finding it
established is reproduced by this module. It stays in history at 432812a.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(conformance): sync/async as an axis, and what the Qt consumer actually changes
The Qt proxy forwarded every method through the SYNC generated wrapper only.
That left the async table — which converts with `qvariant_cast<T>(v)` and
substitutes a default on an invalid QVariant, where sync uses `_result.toT()` —
driven by nothing but an 18-call fixed probe.
`useCallMode sync|async` makes it an axis: all 33 forwarded methods route
through whichever table is selected, so the whole case table replays twice.
`lastCallStatus()` reports `ok-sync`/`ok-async` per call, because a mode switch
that silently no-ops would make the async half a duplicate of the sync half and
86 green cells would prove nothing. Async waits by polling a mutex-guarded slot
and pumping, never a cross-thread QEventLoop::quit(); a completion is delivered
on whatever thread the transport uses. Events keep no mode — a subscription is a
callback either way, and the header says so rather than leaving it a gap.
Measured over both providers:
* sync vs async: 0 deltas. The difference is real in the generated source and
is now driven; it does not currently change an answer.
* M3 is MEASURABLE and moves from `unmeasurable` to an ordinary xfail. The
collision is symmetric, so an `any` echo is green no matter what happens in
between — the observation is a TYPED slot: echoMap({"_bytes":"aGk"}) arrives
{}. Confirmed head-on by the proxy's type-tagged event rendering, which says
QByteArray where the sender put a map.
* Q1 is new and was not suspected: the generated Qt dispatch coerces a hostile
argument into the declared parameter type before the method body runs, so
echoUint(-1) answers 18446744073709551615 where every other surface answers
dispatch_failed. Nine cases. Argument validation is not something a Qt-typed
module can rely on.
Both attributed by replaying the same table through the lp proxy — a hop of the
same shape on a non-Qt client — which rejects all nine and preserves the map. So
it is the api style, not the extra hop.
Registry: `consumers` is now REQUIRED on every xfail entry, with no default. An
entry that does not say which surface it was measured on manufactures failures
on the surfaces it was not, and that already happened once on the provider axis.
M4-residual gains its two Qt consumers only after measuring them.
`skip[]` was declared and never read. Reading it found its own first bug: the
pattern `uint/boundary` matched nothing, because the case had been split in two
and the entry was never updated. A registry that is not executed decays into a
comment.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* spike(qtproxy): the Qt full_api surface as a veneer over the lp wrapper
Phase-1 spike for "make the Qt consumer a skin over the lp one". Adds, beside
the generated Qt `FullApi`, a `FullApiVeneer` with the SAME 33 sync + 33 async +
15 typed-event surface whose every body is convert-args / delegate / convert-
return against `FullApiLp` (verbatim `--api-style lp` generator output, class
renamed). `useWrapper("generated"|"veneer")` routes the whole forwarded case
table through either one against the same provider in the same process, so the
two are diffed rather than reasoned about.
The constraint under test is that the veneer writes NO conversion of its own.
It does not: the entire adapter is four function templates over
logos::qvariantToNlohmann / nlohmannToQVariant (logos-protocol
logos_json_convert.cpp:38,129) and logos::toJson / fromJson (logos_codec.h),
plus two specialisations for `result` — the one LIDL type whose canonical
converters do not exist and would have to be added to logos-protocol
(json -> LogosResult) and logos-cpp-sdk (StdLogosResult -> json).
Measured, both providers, sync and async: identical on every cell except
`result`, where the std hop turns a null error into "". `makeResultNoStdHop`
renders the same call without the std intermediate and matches the generated
wrapper exactly, so the divergence is the hop, not the veneer.
`tokenProbe` records the prerequisite the spike hit first: a Qt-style plugin has
TWO TokenManager singletons (qtTM=100889ec0 lpTM=102fb58a0 same=NO qtCap=yes
lpCap=no), because LogosAPI is constructed in the host image while
lp_client_create is compiled into the plugin. Every lp call from a Qt plugin is
therefore unauthorized and returns a default value. The cdylib backend seeds its
copy via logos_module_accept_token -> lp_token_save; the Qt backend has no such
hook. veneerTarget() does that seeding inline so the type matrix could run.
Spike only — not a proposal to merge.
* test(qtproxy): A/B the two implementations of one Qt surface, in one process
The hand-written spike veneer is replaced by verbatim
`logos-qt-generator --backend consumer` output (class FullApiVeneer, bound
mode). `useWrapper` still swaps every forwarded call between the legacy
generated Qt wrapper and the veneer, against the same provider in the same
process, so the two are diffed rather than reasoned about.
Result: identical on every cell — 40 method/probe cells and 15 events, sync and
async, against both test_fullapi_cpp and test_fullapi_rust. Including uint64
max, int64 min, an int outside double's exact range, bytes carrying
0x00/0x80/0xFF, and `any` as string/number/object.
The one cell that diverged in the spike is closed. `makeResult(true)` rendered
`e=s:` through a veneer that hopped via StdLogosResult (whose error is a
std::string and so cannot be absent) and `e=-` through the generated wrapper.
This wrapper converts Qt <-> canonical JSON and calls the lp client directly —
no std intermediate — and now renders `e=-` both ways.
Everything the spike did by hand in veneerTarget() (owning the LpClient and its
subscriptions per provider, seeding the plugin-side TokenManager) is gone: it
lives in logos-qt-sdk's LpBridge, which is where generated code can reach it.
The veneer is now constructed exactly like target() — same two arguments.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(qtproxy): the same consumer module, built against each Qt implementation
Phase 2 A/B'd the two Qt consumer implementations by compiling BOTH into the
module: the build's generated `FullApi` plus a committed copy of the veneer
emitter's output under a second class name. That proves the two agree; it does
not prove the veneer is reachable through the BUILD, and every call site had to
be made generic over the wrapper type to switch between them.
`mkQtProxy` / `mkFullapiUi` take `qtConsumerCodegen`, so each module is built
TWICE from one `src`, differing only in which generator emitted
full_api_api.{h,cpp}:
legacy logos-cpp-generator --general-only --api-style qt
veneer logos-qt-generator --backend consumer (over the legacy output)
Not one line under src/ differs between the two builds — the premise of the whole
change is that a Qt call site cannot tell. The generated trees differ in exactly
the three full_api_api files and nothing else, and both plugins link. (The
phase-2 in-process copy stays for now as a control: `useWrapper veneer` still
reaches it, and it agrees with the build's wrapper in the veneer build.)
Measured under logoscore against both providers, every call routed through
`modules().bind_full_api(provider)`:
* 212 cells (2 providers x sync/async x 53 type cells incl. every event)
identical between the two builds.
* 36 failure-path cells (bound to a module that does not implement the
contract, so the invalid-QVariant branch runs) identical between the two
builds AND between sync and async in each.
* Against a DIRECT call on the provider, 35 of 36 cells agree in both builds.
The one that does not is registry entry M3 and it is unchanged by this work:
`echoMap({"_bytes":"aGk"})` is `{}` through a Qt consumer either way.
The runtime proof that `bind_full_api` really is the lp path in the veneer build
is the token probe. After one warm-up call:
legacy qtCap=yes lpCap=no qtProv=yes Qt client minted a target token
veneer qtCap=yes lpCap=yes qtProv=no LpBridge mirrored the capability
token; no Qt target token exists
test_fullapi_ui gets the same treatment: a second, independently written Qt
consumer, binding a `.h` interface rather than a .lidl contract. Build only — it
is a UI plugin and needs the app to drive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>