feat(#66): test environment layer — provisioning, config discovery, test data, config apply - #144
Open
Hareet wants to merge 14 commits into
Open
feat(#66): test environment layer — provisioning, config discovery, test data, config apply#144Hareet wants to merge 14 commits into
Hareet wants to merge 14 commits into
Conversation
…, provision real path (h-gated)
…, doc + test polish
…elector + result) Bake the Phase-2 API shape into applyConfig so #134 PR5's cht-conf diagnose→fix→validate loop plugs in without reshaping the type later. Real Docker path still throws NOT_IMPLEMENTED (Phase 2); only the type shape and mock change here. - ConfigUploadAction buckets (app-settings/app-forms/contact-forms/ resources) map to their cht-conf verbs; `actions` narrows which run so the loop re-uploads only the artifact it changed. - `artifact` targets a single form (cht-conf --forms=<name>). - ConfigActionStatus uploaded|skipped|failed — skipped is cht-conf's hash-check no-op, which a boolean couldn't express and verify needs. - applyConfig returns ConfigApplyResult (was void); accepts a bare configPath string for back-compat. 573 tests pass, build + lint clean (Node 22). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…t spawn, status parsing) Implement applyConfig's !useMockDocker path. A new cht-conf-runner isolates child_process: it spawns `cht` per upload bucket (no shell, explicit argv), classifies the result as uploaded/skipped/failed, and never logs credentials. - runBucket() spawns cht-conf with the autonomous-safe flags (--force, --skip-*-check, --accept-self-signed-certs); creds ride the --url arg. - classifyChtConfOutput() parses status per line, skip-detected first — real cht-conf "no changes" lines contain the word "uploaded", so naive matching misclassifies them; verified against the cht-conf source strings. - minimalEnv() passes a PATH/HOME allow-list, not the agent's full env, so the child (which runs config-project code) can't read LLM provider keys. - agent loops buckets sequentially; one failure flips succeeded:false without aborting the rest. Mock path unchanged; both paths share toApplyResult(). 591 tests pass, build + lint clean, coverage 96.66%/86.1% (Node 22). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y minus excisions Bring the Phase-2 cht-conf runner up to the workbench lineage's shape (the port source of truth), excising the cht-conf-extension-only pieces so this PR stays test-environment-layer scoped. Parity uplift (from the workbench src/utils/cht-conf-runner.ts): - Split the spawn wrapper into two layers: runChtConf() runs one `cht` process for an ordered verb list and returns the raw ChtConfExecResult; runBucket() wraps it for the config-upload buckets and classifies the status. The generic layer is what the Phase-3 test-data verbs reuse. - resolveChtConfBin() adds the CHT_CONF_BIN seam so the agent can run a deployment-pinned cht-conf instead of the image's global `cht`. - Add the `app-settings-only` bucket (upload-app-settings, no compile) for a pre-compiled, deployment-recovered app_settings.json. - Artifact form-filter now rides after a literal `--` separator (cht-conf reads only cmdArgs['--'] into args-form-filter; a bare positional throws "Unsupported action(s)"). - minimalEnv() env allow-list and per-line skip-first classification preserved. Excisions (cht-conf-extension, deferred to that PR): - No offline-convert block (runOfflineConvert, createConvertSandbox, CONVERT_VERBS, SANDBOX_EXCLUDES, OfflineConvertOptions). - No skipValidate handling; ChtConfExecOptions.instanceUrl is REQUIRED (offline URL-less convert was the only caller that omitted it). Also fix the stale agent header comment (applyConfig is implemented now). Types add ChtConfExecOptions/ChtConfExecResult and the app-settings-only union member. Runner spec ported to parity minus the offline-convert describes. 1045 tests pass, build + lint clean (Node 22). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-data, couchdb-tier reset
Land the Phase-3 real paths of the Test Environment Agent, ported from the
workbench lineage (the port source of truth), minus the cht-conf-extension
pieces that belong to that PR.
New HTTP/fs isolation modules (mirroring how cht-readiness.ts isolates fetch):
- src/utils/cht-api.ts — bounded, authed CouchDB/CHT helpers: fetchSettings,
fetchFormRevs, fetchDocRevs, bulkDocs. Credentials ride the Authorization
header only, never a URL or log line.
- src/utils/test-data.ts — json_docs reading, seeded-doc classification against
the discovered config, and cht-conf stdout parsers (upload-docs summary,
create-users progress; ANSI-stripped).
Agent real paths (mock paths unchanged):
- discoverConfig: GET /api/v1/settings + the form-rev _all_docs range, parsed
into DiscoveredConfig. formVersions carries each installed form's rev — the
change-detection hash for the apply -> verify loop.
- prepareTestData: cht-conf csv-to-docs + upload-docs seed the instance, then
create-users when users.csv exists. Seeded doc ids are tracked per env for
the reset. Takes a PrepareTestDataOptions (dataPath/bin/timeoutMs).
- reset('couchdb'): the one reset the agent does itself — wipe the tracked docs
at their CURRENT revs, then reseed pristine copies. The reseed source is
pre-flighted before the destructive wipe so a vanished data project fails
closed. restart/full stay human-gated. teardown clears the tracking.
Excisions (cht-conf-extension, deferred): verifyArtifact + fetchDeployedFormXml
and their imports (fetchFormXml from cht-api, verifyFormBinds from xform-inspect
which is not ported); mockVerifyArtifactResult.
Types: DiscoveredConfig.formVersions, TestDataResult.succeeded/seededDocIds,
PrepareTestDataOptions. Mock fixture gains formVersions + seededDocIds. The
three obsolete "throws not-implemented in real mode" agent assertions are
removed here (real-path specs land in the following test commit).
1042 tests pass, build + lint clean (Node 22).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ata)
Complete the Test Environment Layer's real-path spec coverage, ported from
the workbench lineage minus the excised cht-conf-extension describes. All
real-path specs mock fetch / child_process, so the suite stays green with no
running instance and no Docker.
New unit specs:
- test/utils/cht-api.spec.ts — fetchSettings / fetchFormRevs / fetchDocRevs /
bulkDocs against a stubbed global fetch (auth header, abort signal, path+status
error text with no password, non-object/non-array normalisation). The
fetchFormXml describe is excised with its import.
- test/utils/test-data.spec.ts — the upload-docs/create-users stdout parsers
(ANSI-stripped), json_docs fixtures on a real tmp dir, and classifySeededDocs
against a discovered config.
Agent real-mode describes (stubbing cht-api / cht-conf-runner / test-data):
- discoverConfig: fetches with the handle url+auth, parses contact types / roles /
permissions / transitions dropping junk, lists forms with their revs, propagates
a fetch failure, and warns on an instance with no contact_types.
- prepareTestData: runs csv-to-docs+upload-docs then create-users, classifies the
seeded docs, counts users, skips create-users without users.csv, warns on
partial/empty uploads, and reports succeeded:false on a failed run.
- reset('couchdb'): no-op when nothing tracked; wipes tracked docs at CURRENT revs
and reseeds; skips tombstones; throws on rejected deletion / failed / partial
reseed; fails closed before the wipe when the reseed source is gone; refreshes
tracking; teardown clears tracking. Plus mock formVersions + succeeded/seededDocIds
parity assertions.
The runner spec landed with the C1 parity uplift; the excised describes
(verifyArtifact, fetchDeployedFormXml, fetchFormXml, offline-convert) are not
ported. Excision grep over src/ and test/ is empty.
1104 tests pass, build + lint clean (Node 22).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…n map Add PR_66_DESCRIPTION.md (the reviewer-facing writeup) and mark the phase-2 handoff DONE now that phases 2 + 3 have landed. PR_66_DESCRIPTION.md covers, per the completion plan §7 / §6b: - what the layer delivers + the implementation map; - the test surface (1104 passing; 126 in the layer) and the standalone, Docker-free / instance-free guarantee + independent cht-core demo recipe; - the env seams (CHT_CONF_BIN, ProvisionOptions url/auth, useMockDocker, human-gated test-env-up.sh) with an explicit scope note on provision; - the deferred cht-conf-extension boundary map (verbatim); - the excision-grep proof (empty over src/ + test/); - per-function parity proofs vs the workbench port source (identical modulo the excisions; test-data byte-identical; the one provision divergence called out); - the sonar sweep result (four house rules zero; cognitive-complexity note). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ng, COUCHDB_* auth seam
Port the workbench provision() env-seam resolver (and the decodeUserinfo helper)
into the real-mode path verbatim. This is test-environment-layer code — nothing
cht-conf about it — and closes a handle-contract gap: the ported phase-2/3
consumers (credentialedUrl in applyConfig/prepareTestData/resetCouchdbTier,
discoverConfig's auth) assume provision's handle invariants — creds stripped off
handle.url, auth resolved into the handle, url canonical (it keys the seeded-doc
tracking map). Leaving provision at the Phase-1 shape risked that drift.
Six behaviors, exactly as the workbench:
- URL fallback order: options.url -> process.env.CHT_URL (trimmed; blank ignored)
-> the on-network default (https://nginx).
- Resolved URL canonicalized: trailing slashes stripped.
- Embedded basic-auth creds stripped OUT of handle.url (it is logged and passed
to undici fetch(), which rejects credentialed URLs); they survive only as an
auth fallback, decoded via decodeUserinfo (tolerates a raw '%' without throwing).
- COUCHDB_USER/COUCHDB_PASSWORD auth seam (the same seam scripts/test-env-up.sh
uses); COUCHDB_USER absent -> default user; gated on COUCHDB_PASSWORD presence.
- Auth precedence: options.auth -> embedded-URL creds -> COUCHDB_* env -> default.
- Handle shape otherwise unchanged (url, auth copy, network, chtCorePath, source).
Specs: port the workbench describe('CHT_URL fallback') block (8 its) and add the
CHT_URL/COUCHDB_* save/clear/restore to the provision real-mode describe so the
suite stays order-independent. The provision describe is now byte-identical to
the workbench.
Docs: PR_66_DESCRIPTION.md — provision is now at full workbench parity (the only
agent divergence is the excised verifyArtifact/fetchDeployedFormXml); updated the
env-seam list (CHT_URL/COUCHDB_*), the parity table, and the test counts.
1112 tests pass (+8), build + lint clean, excision grep still empty (Node 22).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…d-style agent spec titles Pre-push code-review cleanup. Comment / spec-title changes only — zero behavior change (build clean, 1112 tests unchanged, lint clean, excision grep still empty). - Drop See:-block refs to files that exist in no repo (designs/cht-conf-agent- extension.md §7.2) and the transient handoff citation, from the cht-conf-runner and cht-api headers and the ConfigUploadAction docblock — keep the durable designs/layer_recommendations/test-environment-layer.md reference. - Agent header: replace the stale "LangGraph node + CLI" roadmap line with "The pipeline wiring that invokes this layer lands with #64." - Compress the provision cred-stripping narration to two lines. - Spec comment: "workbench env" -> "ambient env". - docker/cht-agent-net.override.yml: drop the non-ascii warning glyph. - Agent spec: rename the real-mode it() titles to should-style so the file is uniformly should-style (matching the repo's other agent specs / CHT convention); the four utils specs stay imperative per the repo's utils convention. - Agent spec: hoist the shared discoverConfig / reset realAgent into a let + beforeEach, matching the couchdb-reset suite's pattern. The workbench parity source receives these identical edits operator-side, so the per-function parity claim in PR_66_DESCRIPTION.md §8 stays true (left unreworded). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…chers, api polish Behavior-preserving SonarCloud round for PR #144. No public API changes, no spec-title changes, no NOSONAR — only helper extraction and assertion polish. A. S3776 cognitive complexity — every function in the five touched src files now lands at <=5 (the repo threshold), via top-level helpers / private methods (never inner closures, which count toward the parent's nesting): - prepareTestData -> prepareTestDataReal + prepareDocs / seedUsers / noteUploadShortfall (the phase steps); warning + log order preserved. - classifySeededDocs -> classifyOneDoc / resolveContactType / noteUnknownType. - resetCouchdbTier -> wipeTrackedDocs / reseedTrackedDocs / buildTombstones. - waitForReady -> probeOnce (single-attempt probe). - provision -> resolveRealTarget / resolveRealAuth / buildMockHandle; the six documented env-seam behaviors are kept bit-for-bit. - parseRoles -> parseRole; runBucket -> deriveBucketStatus; applyConfig -> applyConfigReal; reset flattened to a mock-first / tier dispatch. B. Warnings: - FORM_BUCKETS is a Set<ConfigUploadAction>, .has() at both call sites. - cht-api request(): 5 params -> 4 (the {method, body} init folded into the one options object; request is module-private, callers are in-file). - provision trailing-slash strip: the /\/+$/ regex replaced with a non-backtracking while (endsWith('/')) slice. - Chai dedicated matchers (.to.be.undefined / .to.be.null) at the flagged spec sites; test-data.spec asserts the concrete SyntaxError on a malformed doc instead of a bare throw. Verified locally with a one-off eslint-plugin-sonarjs cognitive-complexity ["error", 5] pass over the five src files (all functions pass); the package.json / package-lock.json change from that install was reverted. 1112 tests pass (unchanged), build + lint clean, excision grep still empty (Node 22). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The complexity round introduced a 5-parameter helper (noteUnknownType in src/utils/test-data.ts), which SonarCloud's S107 (max 4 parameters) flagged. Bundle the two related sets — the config-known types and the already-warned types — into one UnknownTypeTracker object, dropping the signature to 4 params. Behavior-identical: the once-per-type unknown-contact-type warning is unchanged, and classifySeededDocs stays at cognitive complexity 3. Re-verified locally with a one-off eslint pass — sonarjs/cognitive-complexity ["error", 5] AND max-params ["error", 4] over the five src files all pass; the package.json / package-lock.json change from that install was reverted. 1112 tests pass (unchanged), build + lint clean, excision grep still empty (Node 22). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
feat(#66): Test Environment Layer — provisioning, config discovery, test data, config apply
Closes #66. Enables #64 (QA Supervisor pipeline invocation).
1. What this PR delivers
The Test Environment Layer for the cht-agent QA Supervisor: a deterministic
(no-LLM) orchestrator that stands up a live CHT instance, applies a cht-conf
config, reads the deployed config back, seeds conforming test data, and resets
between runs — all human-gated for Docker (the agent runs no Docker; a human
brings the environment up/down) and credential-safe (creds ride the cht-conf
--urlarg / the HTTPAuthorizationheader, never a log line).It is the layer that drove the closed-loop demo's QA phase live; this PR ships it
at workbench parity minus the cht-conf-extension pieces (which land in their
own PR — see the deferred map in §7). The pipeline/LangGraph wiring that invokes
this layer arrives with #64; this PR ships the layer + its direct-use surface.
Real paths implemented
provision— human-gated bring-up (scripts/test-env-up.sh <cht-core>) +a bounded, backing-off readiness poll of
/api/v2/monitoring.applyConfig— onechtinvocation per upload bucket (app-settings /app-forms / contact-forms / resources), status parsed from stdout
(
uploaded/skipped/failed), buckets independent so one failure doesnot abort the rest.
discoverConfig—GET /api/v1/settings+ theform:_all_docsrange,parsed into
DiscoveredConfig(contact types, roles, permissions, transitions,forms, and
formVersions— each installed form's rev, the change-detectionhash for the apply → verify loop).
prepareTestData— cht-confcsv-to-docs+upload-docsseed theinstance, then
create-userswhenusers.csvexists; seeded doc ids aretracked per environment.
reset('couchdb')— the one reset the agent performs itself over theCouchDB HTTP API: wipe the tracked docs at their current revs, then reseed
pristine copies (reseed source pre-flighted before the destructive wipe, so a
vanished data project fails closed).
restart/fullstay human-gated.teardown— prints the humandocker compose down -vgate and clears theper-env tracking.
2. Implementation map
src/utils/cht-conf-runner.tschild_processisolation for cht-conf:runChtConf(generic, ordered verb list) +runBucket(config-upload buckets),classifyChtConfOutput,minimalEnv(secret-free child env),resolveChtConfBin(CHT_CONF_BINseam).src/utils/cht-api.tsfetchisolation for CHT/CouchDB:fetchSettings,fetchFormRevs,fetchDocRevs,bulkDocs(bounded, authed, cred-safe).src/utils/test-data.tsreadSeededDocs,cleanSeededDocs,classifySeededDocs,parseUploadDocsSummary,countCreatedUsers,hasUsersCsv.src/agents/test-environment-agent.tssrc/agents/test-environment-agent.mock-data.tssrc/types/index.tsapp-settings-only;instanceUrlREQUIRED).src/utils/cht-readiness.tsscripts/test-env-up.sh,scripts/test-env-down.sh,docker/cht-agent-net.override.yml3. Test surface
build+test+lintare green after every commit (Node 22). Full suite:1112 passing. Real-path specs mock
fetch/child_process/fs, so thesuite is green with no instance and no Docker.
itstest/utils/cht-conf-runner.spec.tstest/utils/cht-api.spec.tstest/utils/test-data.spec.tstest/agents/test-environment-agent.spec.tstest/utils/cht-readiness.spec.ts4. Environment seams
CHT_CONF_BIN— override the cht-conf binary (defaultcht); lets theagent run a deployment-pinned cht-conf and lets specs stub a fake script.
CHT_URL—provision()falls back to it for the instance URL(
options.url→CHT_URL(trimmed; blank ignored) → the on-network defaulthttps://nginx). The resolvedhandle.urlis canonicalized (trailing slashstripped) and stripped of any embedded basic-auth creds (which survive only as
an auth fallback,
decodeUserinfo-decoded, tolerating a raw%).COUCHDB_USER/COUCHDB_PASSWORD— the instance-auth seam, the same onescripts/test-env-up.shuses for the bring-up, so a non-default password needsno code change. Auth precedence:
options.auth→ URL-embedded creds →COUCHDB_*env → the defaultmedic/password.ProvisionOptions.url/ProvisionOptions.auth— the highest-precedencetarget + creds the handle carries; every downstream call reads
handle.url/handle.auth.useMockDocker(constructor) — mock mode is the default;falseselectsthe real paths. This is what keeps CI Docker-free.
scripts/test-env-up.sh <cht-core>— the human-gated bring-up seam.5. Standalone guarantee + independent cht-core demo
Zero imports from workflow / cht-conf-extension code. Real-path specs mock
fetch/child_process→ the suite is green with no instance and no Docker.The demo below exercises the whole layer in real mode
(
useMockDocker: false) against a real dockerized CHT, entirely in Docker:the CHT stack comes up human-gated on
cht-agent-net, and the layer runs ina disposable container on the same network, reaching the instance at the
layer's native
https://nginxdefault through theCHT_URLseam (§4).1. Bring CHT up (human-gated — the layer itself never runs Docker):
2. Start a disposable runner on the same network (from this repo's root):
(
NODE_TLS_REJECT_UNAUTHORIZED=0covers the dev stack's self-signed cert forthe layer's own
fetch; the cht-conf child already runs with--accept-self-signed-certs.)3. Inside the container — build, then drive every layer method:
No URL or credentials are passed to any call:
provision()resolves theinstance from the
CHT_URLenv seam and the default auth, strips/canonicalizesit onto the handle, and every downstream method reads the handle — the §4
seams doing their job. Expected: the readiness poll returns, discovery reports
the deployed forms, all four upload buckets run, the demo person seeds, the
couchdb-tier reset wipes and reseeds it, and teardown prints the human
down-gate.
Mock mode (
useMockDocker, the default) mirrors the same call sequenceCI-safe — it is what the spec suite drives. Ticket-driven invocation of this
sequence is #64's scope; this PR ships the layer and the direct-use surface.
6. Excision proof (no cht-conf-extension code)
7. Deferred cht-conf-extension map (verbatim boundary)
Ported with excisions (the stripped items are the cht-conf extension):
src/agents/test-environment-agent.tsverifyArtifact,fetchDeployedFormXml, and their imports (fetchFormXml,verifyFormBinds)src/utils/cht-api.tsfetchFormXmlsrc/utils/cht-conf-runner.tsrunOfflineConvert,createConvertSandbox,CONVERT_VERBS,SANDBOX_EXCLUDES,OfflineConvertOptions),skipValidate;instanceUrlrestored to REQUIREDsrc/types/index.tsVerifyArtifact*,QaInput/QaResult/QaTier2Result,XlsformBindDiff, offline-convert optionalsfetchFormXml,verifyArtifact,runOfflineConvert,createConvertSandbox, F2/F5 xform describes)Not ported at all (whole files → cht-conf-extension PR):
xform-inspect.ts,config-type.ts,cht-conf-tier2.ts,cht-conf-test-spec.ts,qa-workflow.ts,orchestrator.tswiring, CLI flags.8. Commits (on
66-test-environment-layer-implementation, rebased ontoorigin/main@fdf4af2)feat(#66): phase-2 parity uplift — cht-conf runner to workbench parity minus excisionsfeat(#66): phase 3 — discoverConfig + cht-api, prepareTestData + test-data, couchdb-tier resettest(#66): layer spec suite ported (agent real paths, cht-api, test-data)docs(#66): handoff status, PR description, deferred cht-conf-extension mapfeat(#66): provision env-seam parity — CHT_URL fallback, cred stripping, COUCHDB_* auth seamchore(#66): review hygiene — dead doc refs, stale roadmap note, should-style agent spec titles(Phases 1–2 groundwork — provision/readiness/scripts/apply shape — is the branch's
pre-existing history, replayed unchanged by the rebase.)