You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
feat: Declarative integrations with real/fake parity (#610)
* docs(adr): propose ADR-0055 declarative integrations with real/fake parity
Introduces a framework-level Integration typeclass where every outbound
and inbound integration ships both a real and fake implementation, with
Arbitrary-based defaults, hash-keyed fixture fallback, and auto-generated
contract tests for schema drift detection. Supersedes the ToAction adapter
layer from ADR-0008 and the adapter portion of ADR-0049.
* docs(adr): revise ADR-0055 with security and performance review fixes
Address blockers from Phase 2 (security) and Phase 3 (performance) pipeline
reviews:
- Gate fake-mode CLI flag behind NEOHASKELL_ALLOW_FAKE_INTEGRATIONS env var
- Require Redacted type for secret-bearing fields; no separate mechanism
- Move Arbitrary from superclass to constrained default (no QuickCheck in
production dispatch dictionary)
- Resolve dispatcher shim at wire-up time (pre-bound closure per instance,
INLINE pragma); no per-call selection check
- Move simulate/record/promote/fakeProperty to Test.Integration namespace
(nhcore:testing library stanza, test-suite-only at compile time)
- Fixture recorder defaults to gitignored tests/fixtures/local/ with
explicit promote step, entropy scan, redaction hooks
- Specify fixture path construction: rooted at project root, NameOf
validated to [A-Za-z0-9_]+, makeAbsolute + prefix check
- First-party RFC 8785 canonical-JSON implementation with committed
test-vector conformance suite (no unmaintained external dep)
- Add section 11 on error discipline: IntegrationError payloads are
public-safe; IntegrationDebug carries Redacted diagnostic context
- Contract tests use Json.encode (toEncoding path) to avoid Value-tree
allocation
- Specify bounded inbound channel (default capacity 1024) backed by
Channel.newBounded for backpressure parity with real inbounds
* docs(adr): specify env-var gate error message in ADR-0055
DevEx review flagged that a vague error would send Jess to search engines.
Pin the error text to name the offending flag, the exact env var name and
value, and a docs link.
* docs(adr): add Phase 5 architecture design for ADR-0055
Drop-in implementation blueprint. 14 sections covering module layout,
full type definitions, public API signatures, import maps, cabal diffs
for the new nhcore:testing library stanza, Application.run wire-up,
ADR-0049/ADR-0008 integration plans, fixture path-validation algorithm,
RFC 8785 canonical-JSON implementation outline, TypeRep-keyed
DispatchRegistry for runtime dispatch selection, test layout, and
migration impact across existing ToAction / Integration.Inbound call
sites. No open questions.
* docs(adr): move Phase 5 architecture doc out of committed decisions tree
Architecture design is an in-flight working document, not a durable
decision. It now lives under docs/architecture/ (gitignored) so it
stays local to the branch without cluttering the committed ADR trail.
* test(integration): add Phase 7 pending test skeletons for ADR-0055
Outside-in TDD: 140 pending test placeholders across 9 new Spec files,
one test per row of the Phase 6 test spec. Each test body is
Test.pending so the suites compile clean but nothing passes until
Phase 8 fills in the production implementation and real assertions.
Wired into the three existing test-suite stanzas (nhcore-test-core,
nhcore-test-service, nhcore-test-integration) and their respective
Main.hs runners. No production code added — only test placeholders.
* feat(integration): implement ADR-0055 production + testing library
Phase 8 — Implementation. All 11 production modules and 6 test-library
modules from the architecture doc now compile and link clean.
Production (core/service/Service/Integration/):
Adapter, Canonical, Canonical/Version, Debug, DispatchRegistry,
Fixture, FixtureKey, Inbound, IntegrationError, Selection, ShimEmit
Test-only (core/testing-integration/Test/Integration/):
top-level Integration, Simulate, Fixture (record/promote),
EntropyScan, Property (fakeProperty), Contract (contractTests)
New cabal stanza: library testing (public visibility) exposing the
Test.Integration.* surface, wired as a build-depend into all three
affected test-suite stanzas.
Test bodies partially filled: 55 of 140 rows converted from pending
to real assertions against the new production surface. Remaining 85
stay pending with per-row explanations (await test fixture modules,
filesystem/concurrency harness, RFC 8785 vector tree).
Known architectural deviations documented in the report:
- InboundIntegration methods renamed runRealInbound/runFakeInbound
(Haskell's class-method namespace is flat).
- IntegrationError is a NEW module; old Integration.hs variant
left untouched per Phase 8 scope.
- testing library source dir is core/testing-integration/ (not
core/service/) to avoid duplicate-symbol linker errors.
- ADR-0008/0049 adapter migration explicitly deferred.
* style(testing): rename 'pattern' identifier to avoid hlint false positive
'pattern' is a reserved word with the PatternSynonyms extension, which
hlint assumes is enabled. The rename sidesteps the false-positive
parse-error hint without changing semantics.
* fix(integration): address Phase 10/11 review findings for ADR-0055
Security (Phase 10):
- Sec-H1: Test.Integration.Fixture.record/promote now call
Service.Integration.Fixture.validateIntegrationName at entry and
canonicalise+prefix-check every write target, blocking path-traversal
via attacker-influenced integration names.
- Sec-H2: resolveProjectRoot no longer falls back to getCurrentDirectory
when NEOHASKELL_PROJECT_ROOT is unset — throws ValidationFailure,
matching ADR-0055 §4.
- Sec-H3: added **/tests/fixtures/local/ to the repo .gitignore as a
backstop for the neohaskell-new-template gitignore that doesn't
exist yet.
- Sec-M4: Service.Integration.Fixture now uses canonicalizePath (which
resolves intermediate symlinks) instead of makeAbsolute + per-leaf
symlink probe, closing the symlinked-parent-component escape.
- Sec-M5: EntropyScan now scans for secret markers anywhere in the
string (Text.contains) rather than only as a leading prefix, catching
secrets embedded in free-text fields like 'auth: Bearer sk_...'.
Performance (Phase 11):
- Perf-F6: DispatchRegistry.empty/register/lookup/size marked
{-# INLINE #-} so the {-# INLINE emit #-} in ShimEmit.hs can fuse the
'case Maybe' across module boundaries, preserving the
one-direct-function-call hot-path property from ADR-0055 §3.
- Perf-F7: ShimEmit.emit now throws PermanentFailure 'Integration not
registered' when the registry has no entry, per ADR-0055 §6. Silent
fallback to runReal was hiding wire-up bugs and bypassing the startup
selection-flag decision.
- Perf-F1: Canonical.encode/hash marked INLINE and the ToJSON->Value
tradeoff is now documented in-source. A fully toEncoding-streaming
canonical JSON encoder would require buffering object keys mid-stream
(RFC 8785 requires sort), so remains out of scope; the only production
caller is IntegrationDebug.log which is Debug-gated.
* refactor(integrations): drop RFC 8785 canonical JSON — fixtures are Haskell-only
Fixture files are only ever read and written by Haskell test binaries in
this repository, so the cross-language byte-equivalence guarantees that
RFC 8785 provides were buying us nothing. Replace the 300-line
`Service.Integration.Canonical` encoder (ECMA-262 number formatting,
UTF-16 code-unit sort, manual string escape rules, NaN/Infinity
rejection, RFC 8785 test-vector conformance gate) with a ~15-line
sorted-key `Aeson.encode` + SHA-256 hash in
`Service.Integration.FixtureKey`.
Net: -463 lines of production code + tests, same fixture-lookup
determinism, no public API surface worth maintaining.
Also strip ADR-0055 §12 and related consequences/mitigations/risks
entries that were specific to the RFC 8785 choice.
* website
* refactor(integrations): simplify integration layer — remove fixtures, inbound, contract, testing-integration
Drops the fixture/inbound/contract/recorder/property abstractions in favour of a leaner dispatch-registry + adapter model aligned with the revised ADR-0055. Removes the testing-integration library entirely and consolidates helpers into TestFixtures. Updates docs and website guide accordingly.
* refactor(integrations): remove GlobalRegistry; thread DispatchRegistry explicitly
Replace the implicit GlobalRegistry module with explicit DispatchRegistry
threading through Integration.ActionContext.outboundDispatch. Extract
buildIntegrationContext helper from Application.run so runWith/runWithAsync
share the same registry construction path.
All ActionContext call sites in tests and integrations now pass
DispatchRegistry.empty explicitly.
* docs(adr-0055): log fake-mode at critical level; /health omits block in real mode
Reviewer feedback on PR #610 flagged two issues:
1. The ADR claimed the startup banner was logged at ERROR level, but
NeoHaskell's Log module has no ERROR level — the intended level is
critical. Fix the ADR and testing guide to match.
2. The /health shape in real mode was ambiguous. Clarify that the
integrations block is omitted entirely in real mode (absence of key =
production signal), so deploy-checks assert absence rather than
matching a string.
Add three HealthCheckSpec tests covering: real mode → key absent, fake
mode → mode=fake, hybrid mode → mode=hybrid + fakes array.
* test(integrations): un-pend writable tests; add end-to-end wire-up spec
Convert the remaining actionable pending entries in AdapterSpec and
DispatchRegistrySpec into real tests; drop the entries that could not be
written (mismatched-type negatives, thread-safety tests — the registry
is immutable, and the 1024-types stress test).
Added:
- AdapterSpec: overridden runFake compiles without Arbitrary (Response r);
runFake returns within 2 s (non-blocking sanity check)
- DispatchRegistrySpec: stored closure preserves its Response type
bit-for-bit (round-trip via ChargeIntent)
New WireupSpec covers the Application.withIntegration →
buildIntegrationContext → DispatchRegistry → ShimEmit.emit chain end to
end, across real/fake/hybrid modes and the unregistered-integration
error path.
* fix(integrations): address CodeRabbit review comments
- Remove Test.QuickCheck from production Adapter.hs; the default runFake
was coupling production code to test-fuzzing semantics (major issue).
Move the QuickCheck-backed runFake to TestFixtures.hs as an explicit
implementation on Integration SendEmail.
- Fix FakeNameRegistry.fromSelection Fake latent bug: Fake mode was
mapping to an empty set so fakeNameMember would always return False.
Replace newtype with a 3-constructor ADT (NoFakeNames | AllFakeNames |
SomeFakeNames) so Fake => AllFakeNames => fakeNameMember _ = True.
- Improve AdapterSpec exhaustiveness test: replace the count-based foldl
(which silently passes when new constructors are added) with a
total case expression that GHC warns about on constructor changes.
* ci: disable Claude Code Review workflow pending app installation
* fix(integrations): address CodeRabbit comments and fix build
- Remove unused 'import Task qualified' from Adapter.hs (build fix)
- Fix validateOrThrow docstring to honestly state it is a no-op stub
- Simplify lastList to use listToMaybe . reverse instead of manual recursion
- Remove unnecessary AllowAmbiguousTypes pragma from TestFixtures
* fix(integrations): address CodeRabbit review comments
- Remove docsUrl export; inline URL into error message
- Simplify validateFakeNames: remove redundant empty-list branch
- Delegate isFakeByName to fakeNameMember/fromSelection
- Remove do-notation from pure mkDispatcher (use let..in)
- Fix vacuous selection test: use 'nonesuch' not 'unknown'
- Remove redundant Task.yield in TestFixtures.runFake
- Fix buildHealthResponse to use Aeson.encode (proper JSON escaping)
- Consolidate duplicate hybrid tests; add fakes assertion to fake-mode test
- Add JSON injection guard regression test (Bad"Name)
- Register HealthCheckSpec in nhcore-test-service
- Add tracking comment to disabled CI job
@@ -984,14 +1010,16 @@ runWith eventStore app = do
984
1010
caseArray.isEmpty app.deferredInboundRegs of
985
1011
False->Task.throw "runWith does not support config-dependent inbound integrations. Use Application.run instead, or use: withInbound @() (\\_ -> yourInbound)"
986
1012
True-> pass
1013
+
-- Build ADR-0055 DispatchRegistry and integration status (same helper as run)
0 commit comments