Repository navigation
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>
|
@Hareet board still says In Progress and I wasn't tagged, so checking before I dig in: is this ready for review, or still moving? Also |
…, config paths, gates - test-env-up.sh passes COUCHDB_USER (medic) so the stack matches DEFAULT_AUTH; the local-build default is admin, which 401d every authenticated call - classifyChtConfOutput: a clean exit with no upload evidence is now skipped + warned, never assumed uploaded (unmatched form filter, missing bucket input) - applyConfig resolves the default config/default against handle.chtCorePath; cwd/bin/timeoutMs threaded per bucket - couchdb-reset reseed reuses the seed's bin/timeoutMs - human gates: runnable test-env-restart.sh, plus test-env-down.sh fixed (compose resolves COUCHDB_* for every subcommand, so both aborted without it) - gates shell-quote the cht-core path, provision rejects control chars, and the printed text is asserted instead of "resolves to undefined" - spawn failures name bin/cwd so a bad cwd is not misread as a missing cht-conf - TLS operator note relocated to surviving files (unblocks PR_66_DESCRIPTION.md removal) - comment dedupe per review (misattached resolveRealTarget JSDoc, duplicated narration) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ted cht-conf output
- provision refuses non-disposable targets: https-only (http for localhost/nginx),
unknown hosts need allowExternalTarget or CHT_TEST_ENV_ALLOW_EXTERNAL=1, and the
built-in medic/password is never sent to one; the guard runs before the poll
- reset returns ResetResult {tier, wiped, reseeded, performedBy, protectedSkipped}
for every tier and accepts {dataPath, docIds} so a reloaded handle can drive it
- reset tracking is keyed on network|url, so parallel envs on nginx no longer collide
- the wipe worklist refuses protected ids (settings, form:*, _design/*, org.couchdb.user:*)
at tracking and wipe time, so a data project cannot delete deployed config
- bulkDocs throws on a non-array body; the wipe asserts every submitted deletion was
acknowledged with ok and reports the actual tombstone count
- cht-conf output is redacted at the runner boundary, and a failed bucket now carries
cht-conf's own explanation in warnings
- an artifact-targeted apply fails only when every form bucket matched nothing, so
targeting an app form no longer trips over the contact-forms bucket
- authenticated requests use redirect: manual and reject a 3xx
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
PR_66_DESCRIPTION.md and docs/handoffs/66-phase2-applyconfig-implementation.md were working artifacts, not deliverables: the PR body carries the description, and main has no docs/handoffs convention. The self-signed-TLS note that only existed in PR_66_DESCRIPTION.md was moved into scripts/test-env-up.sh and the agent's real-path defaults comment beforehand, so no operator guidance is lost with the file. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ayer-implementation
Shell (12): the three test-env scripts are bash, so conditionals use [[ ]]. The npm ci in test-env-up.sh keeps lifecycle scripts and carries a NOSONAR note — cht-core's postinstall is patch-package, and --ignore-scripts would leave a broken install. TypeScript (9): shellQuote uses replaceAll with the POSIX escape hoisted to a constant (String.raw inline would nest template literals, itself a gate rule); hasControlChars uses codePointAt; the bulk-docs shape check throws TypeError; and the cwd assertion uses a dedicated matcher. Cognitive complexity: extracted isSchemeAllowed/isExternalTargetAllowed from the target guard, validateProvisionOptions from provision, trackSeededDocs from prepareTestDataReal, assertNoOrphanDocIds from the reset worklist, and assertNotRedirect from the cht-api request helper — the redirect check added last round had pushed it over the limit. Found four more with eslint-plugin-sonarjs run locally than CI reported, since SonarCloud only annotates changed lines; recipe in the review notes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shell (12): the three test-env scripts are bash, so conditionals use [[ ]]. The npm ci in test-env-up.sh keeps lifecycle scripts on purpose and now says why — cht-core's postinstall is patch-package, so --ignore-scripts would leave the patches unapplied and the build broken. That one is a security hotspot to review, not a defect to fix. TypeScript (9): shellQuote uses replaceAll with the POSIX escape hoisted to a constant (String.raw inline would nest template literals, itself a gate rule); hasControlChars uses codePointAt; the bulk-docs shape check throws TypeError; and the cwd assertion uses a dedicated matcher. Cognitive complexity: extracted isSchemeAllowed/isExternalTargetAllowed from the target guard, validateProvisionOptions from provision, trackSeededDocs from prepareTestDataReal, assertNoOrphanDocIds from the reset worklist, and assertNotRedirect from the cht-api request helper — the redirect check added last round had pushed it over the limit. Found four more with eslint-plugin-sonarjs run locally than CI reported, since SonarCloud only annotates changed lines; recipe in the review notes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Ready for review now — thanks for waiting, and good catch on those two files.
Since you last looked, the branch also picked up three rounds of fixes and a live run:
Two things I would keep in your mind: the scope boundary — cht-conf is here as the @witash requesting a review from you so I can give @sugat009 a break. If you do have time to pick up this review, please drop an update so @sugat009 doesn't start one in the mean time. |
|
@sugat009 Please review |
sugat009
left a comment
There was a problem hiding this comment.
I reviewed 43a834b. Gates in a clean checkout: tsc clean, 1159 tests pass, nyc thresholds
pass, eslint clean, validate-schema 110/0/3. Merge onto main is clean.
I also ran the layer for real on 2026-09-22, with your scripts, against a fresh cht-core master
and cht-conf 6.6.1. The happy path works end to end, all four bucket classifications hold, and no
credential reached a log line. I can share the run logs on request.
Three items block:
- Comment 1.
prepareTestDatadeletes files in the operator's data project. Proven live. - Comments 12 and 13. The scripts inherit Compose's default project name
local-build. On my
machine that recreated another cht-core checkout's CouchDB container.down -vwould have removed
that stack's volumes. One-pflag fixes all three scripts. - Scope. Issue #66 item 3 asks for config-driven generation from
contact_types,rolesand
the installed forms. The layer seeds caller CSVs. Please change "Closes #66" to "Part of #66" and
file the remainder, or tell me which document is wrong.
The other 26 inline comments are non-blocking. Twenty have a one-click fix. The rest carry
code blocks or are questions.
Follow-ups outside the diff:
- The bring-up publishes nginx on all interfaces at 80 and 443 with
medic/password. - CI checks none of the scripts. The layer is LLM-free, so a keyless smoke test is possible.
- The section 5 demo in the PR text cannot run as written. Step 2 mounts the config read-only.
Every default bucket writes into the--sourceproject: compile writesapp_settings.json,
convert rewritesforms/*/*.xml, uploads write.snapshots/remote.json. So each cht-conf call
exits 1 withEROFS. Also the gate printer single-quoteschtCorePath: '~/src/cht-core', so
the shell does not expand~. - No README or runbook section covers the scripts or the env seams. The PR text says 1112 tests
and 134its (head: 1159 and 169), says "rebased" over a merge commit, and promises live-run
results and a known-limitations list that are not in the body.
| config: DiscoveredConfig, | ||
| warnings: string[] | ||
| ): Promise<{ docsOk: boolean; seeded: SeededDoc[]; counts: SeededDocCounts }> => { | ||
| const staleDocs = cleanSeededDocs(dataPath); |
There was a problem hiding this comment.
issue (blocking): This deletes every *.doc.json under <dataPath>/json_docs before
csv-to-docs runs. json_docs is also the hand-authored input directory for cht-conf
upload-docs. A partner project that keeps source docs there loses them on the host, with one
info log line. The comment on cleanSeededDocs says "left behind by a previous csv-to-docs run",
but nothing checks that this agent wrote them. I proved it on a live stack on 2026-09-22. A
hand-authored json_docs/hand-authored.doc.json was gone after prepareTestData. The doc
never reached CouchDB. Delete only the files this agent tracked for this environment. Refuse when
untracked docs are present:
// prepareTestDataReal: pass what this agent seeded here before
const previous = this.seededData.get(trackingKey(handle));
const { docsOk, seeded, counts } = await prepareDocs(shared, dataPath, config, warnings, previous?.docIds ?? []);
// prepareDocs
const onDisk = readSeededDocs(dataPath).map((doc) => doc.id);
const foreign = onDisk.filter((id) => !ownedIds.includes(id));
if (foreign.length > 0) {
throw new Error(
`prepareTestData: ${dataPath}/json_docs holds ${foreign.length} doc file(s) this layer did not write ` +
`(${foreign.slice(0, 3).join(', ')}). Clear them yourself or use a fresh data project.`
);
}
const staleDocs = cleanSeededDocs(dataPath);| const isDisposableHost = (hostname: string): boolean => | ||
| DISPOSABLE_HOSTS.has(hostname) || | ||
| hostname.endsWith('.local') || | ||
| hostname.endsWith('.localhost') || | ||
| // cht-docker-helper (the published-version bring-up) serves the stack here. | ||
| hostname.endsWith('.local-ip.medicmobile.org'); |
There was a problem hiding this comment.
suggestion (non-blocking): Two suffixes admit hosts that are not disposable.
*.local-ip.medicmobile.org is a wildcard resolver. 203-0-113-5.local-ip.medicmobile.org
resolves to 203.0.113.5, a public address, so any host passes with the built-in credentials.
*.local is also the legacy Active Directory suffix. Restrict the resolver to private ranges and
drop .local:
| const isDisposableHost = (hostname: string): boolean => | |
| DISPOSABLE_HOSTS.has(hostname) || | |
| hostname.endsWith('.local') || | |
| hostname.endsWith('.localhost') || | |
| // cht-docker-helper (the published-version bring-up) serves the stack here. | |
| hostname.endsWith('.local-ip.medicmobile.org'); | |
| const PRIVATE_DASHED_IP = | |
| /^(?:10-\d{1,3}-\d{1,3}-\d{1,3}|172-(?:1[6-9]|2\d|3[01])-\d{1,3}-\d{1,3}|192-168-\d{1,3}-\d{1,3}|127-\d{1,3}-\d{1,3}-\d{1,3})\.local-ip\.medicmobile\.org$/; | |
| const isDisposableHost = (hostname: string): boolean => | |
| DISPOSABLE_HOSTS.has(hostname) || | |
| hostname.endsWith('.localhost') || | |
| // cht-docker-helper serves the stack on a dashed private IP. The resolver answers | |
| // for ANY IP, so public addresses must not pass. | |
| PRIVATE_DASHED_IP.test(hostname); |
| * instance's app settings. | ||
| */ | ||
| const PROTECTED_DOC_ID = | ||
| /^(settings|resources|branding|partners|extension-libs)$|^_design\/|^form:|^org\.couchdb\.user:|^messages-/; |
There was a problem hiding this comment.
suggestion (non-blocking): Three deployed-config docs are missing from this list:
privacy-policies (written by cht-conf upload-privacy-policies), service-worker-meta, and
zscore-charts. All three are literal doc ids in cht-core api/src on master. reset('couchdb')
wipes any of them when a data project names it.
| /^(settings|resources|branding|partners|extension-libs)$|^_design\/|^form:|^org\.couchdb\.user:|^messages-/; | |
| /^(settings|resources|branding|partners|extension-libs|privacy-policies|service-worker-meta|zscore-charts)$|^_design\/|^form:|^org\.couchdb\.user:|^messages-/; |
| // upload-docs re-uploads the whole json_docs directory, protected ids included; | ||
| // only the docs this layer owns count towards a complete reseed (a protected doc | ||
| // conflicting with deployed config must not fail every future reset). | ||
| const expected = partitionProtected(onDisk.map((doc) => doc.id)).safe.length; |
There was a problem hiding this comment.
suggestion (non-blocking): reset('couchdb') with an explicit docIds subset throws after
the wipe. upload-docs re-uploads all of json_docs. The docs not in the subset still exist
on the instance and count as skipped. So uploadedCount is below expected. Count only the
wiped ids:
| const expected = partitionProtected(onDisk.map((doc) => doc.id)).safe.length; | |
| const wipedIds = new Set(tracked.docIds); | |
| const expected = partitionProtected(onDisk.map((doc) => doc.id)).safe.filter((id) => wipedIds.has(id)).length; |
| const timeoutId = setTimeout(() => { | ||
| proc.kill('SIGTERM'); | ||
| finish({ exitCode: null, timedOut: true }); | ||
| }, timeoutMs); |
There was a problem hiding this comment.
suggestion (non-blocking): On timeout this sends one SIGTERM and resolves. A child that
ignores it keeps running after the layer reports failed. cht-conf spawns xls2xform during
form conversion. That grandchild can outlive the timeout too. Escalate:
| const timeoutId = setTimeout(() => { | |
| proc.kill('SIGTERM'); | |
| finish({ exitCode: null, timedOut: true }); | |
| }, timeoutMs); | |
| const timeoutId = setTimeout(() => { | |
| proc.kill('SIGTERM'); | |
| const hardKill = setTimeout(() => proc.kill('SIGKILL'), 5_000); | |
| proc.once('close', () => clearTimeout(hardKill)); | |
| finish({ exitCode: null, timedOut: true }); | |
| }, timeoutMs); |
| console.log(` scripts/test-env-up.sh${target} # build + start on ${network}`); | ||
| console.log(`[Test Environment Agent] Polling ${url}/api/v2/monitoring until healthy...`); | ||
|
|
||
| await waitForReady(url, { maxWaitMs: DEFAULT_PROVISION_WAIT_MS, ...options.readiness }); |
There was a problem hiding this comment.
suggestion (non-blocking): /api/v2/monitoring is unauthenticated, so this returns a
"ready" handle with unchecked credentials. In my live run, provision succeeded with a wrong
password, and discoverConfig failed minutes later with HTTP 401. One authenticated probe here
moves the failure to where the operator is looking:
| await waitForReady(url, { maxWaitMs: DEFAULT_PROVISION_WAIT_MS, ...options.readiness }); | |
| await waitForReady(url, { maxWaitMs: DEFAULT_PROVISION_WAIT_MS, ...options.readiness }); | |
| // /api/v2/monitoring is unauthenticated: probe the credentials once, so a wrong | |
| // password fails here and not on the first discover/apply call minutes later. | |
| await fetchSettings(url, auth); |
| * Defaults to cht-core's in-repo `config/default`; cht-conf tickets pass the | ||
| * mounted deployment config (CHT_CONF_PATH). `actions` selects which cht-conf |
There was a problem hiding this comment.
nitpick (non-blocking): Nothing reads CHT_CONF_PATH. It appears only here and in
types/index.ts:808. A reader will set it and see no effect.
| * Defaults to cht-core's in-repo `config/default`; cht-conf tickets pass the | |
| * mounted deployment config (CHT_CONF_PATH). `actions` selects which cht-conf | |
| * Defaults to cht-core's in-repo `config/default`. Pass `configPath` for a deployment | |
| * config; no env var selects one yet. `actions` selects which cht-conf |
| printResetGate(handle, tier); | ||
| return gated('human-gate'); |
There was a problem hiding this comment.
question (non-blocking): provision blocks until the human gate is done. reset('restart')
and reset('full') print the gate and return at once with wiped: 0, reseeded: 0. A caller such
as #64 cannot tell a gated reset from a completed one. It also cannot wait for the human. Two
options exist. Block on waitForReady here, as provision does. Or return a distinct
performedBy: 'human-gate' result that the caller must await. Which one do you intend?
| COUCHDB_USER="${COUCHDB_USER:-medic}" COUCHDB_PASSWORD="${COUCHDB_PASSWORD:-password}" docker compose \ | ||
| -f cht-couchdb.yml -f cht-core.yml -f "$OVERRIDE" down -v | ||
|
|
||
| echo "CHT environment torn down (-v removed volumes for a clean slate)." |
There was a problem hiding this comment.
nitpick (non-blocking): -v removes the named volumes only. cht-core's compose bind-mounts
CouchDB data to local-build/srv on the host, so the data survives this command. The message
promises a clean slate it does not give:
| echo "CHT environment torn down (-v removed volumes for a clean slate)." | |
| echo "CHT environment torn down (-v removed the named volumes; CouchDB data under local-build/srv is a bind mount and stays)." |
| if (!options.chtCorePath && !options.version) { | ||
| throw new Error('provision requires either chtCorePath or version'); | ||
| } |
There was a problem hiding this comment.
nitpick (non-blocking): version passes this check, but the real path has no
published-version bring-up. It prints scripts/test-env-up.sh, which builds local images.
network is printed in the gate, but the scripts and the override hardcode cht-agent-net. Both
options read as supported and are not. Either implement them or reject them in real mode.
…ks, honest seams Blocking: - prepareTestData deletes only json_docs files it generated. A manifest beside json_docs records each csv-to-docs output with its sha256; a file that is not listed, or was edited since, is the operator's, and the seed refuses rather than touching it. Listing is recursive, as upload-docs reads it. With no csv/ input, json_docs is uploaded as-is. - each cht-core checkout gets its own Compose project (cht-agent-<dir>-<path hash>, CHT_TEST_ENV_PROJECT overrides), and with it its own internal network, CouchDB bind mount and cert volume. Compose used to name every stack local-build, so down -v on one checkout hit another's. up refuses while another project's nginx is on cht-agent-net; down and restart fail on a project with no containers instead of reporting success. The three scripts share scripts/lib/test-env.sh. Correctness: - readiness asks for JSON: until cht-api is up it answers every other client with a 200 startup page, which passed as healthy. Healthy now also means monitoring reports both an app and a CouchDB version. Redirects and TLS verification errors fail at once, the reason is logged when it changes, and the last probe keeps a real budget before the deadline. - provision checks through /_session that the credentials are a CouchDB admin, since /api/v2/monitoring is unauthenticated and --force and _bulk_docs both need one - cht-conf ERROR lines are matched after stripping ANSI, case-sensitively; the old /\bERROR\b/i missed real errors and failed benign lines such as "error-report.xml uploaded" - a seed whose upload-docs summary falls short (e.g. re-seeding the same data) reports succeeded: false; csv-to-docs and upload-docs run as separate calls so protected ids are refused before anything is uploaded - the couchdb reset refuses to wipe a doc json_docs cannot restore, and reads the wiped ids back from CouchDB after the reseed instead of trusting cht-conf's summary - a relative dataPath is resolved once and tracked absolute; a subset reset counts only what it wiped; teardown keeps tracking, because the CouchDB bind mount survives down -v - the spawn timeout escalates to SIGKILL, with the timer unref'd; signals reach only the direct child, which the comment now says - buildChtConfArgs and runBucket share one lowering, so the argv specs test what spawns Guards: - the dashed-IP resolver answers for any address, so only private ranges pass; .local is gone (it is also the Active Directory suffix) - migration-log, shortcode-id-length, privacy-policies, service-worker-meta and zscore-charts join the protected doc ids - mock mode parses and strips credentials from options.url as the real path does, and an invalid URL no longer throws with the password on error.input Seams that read as supported and were not: - real mode refuses a published version: the scripts only build a working copy, so there was no bring-up to gate or poll - a custom network is refused: the scripts and the override hardcode cht-agent-net - COUCHDB_USER and COUCHDB_PASSWORD default independently, exactly as the scripts do - the stack's cert is issued for nginx (COMMON_NAME), so NODE_EXTRA_CA_CERTS works for the agent's own fetch; host ports bind to 127.0.0.1 by default - the provision wait is 30 minutes: a cold bring-up was measured at 17m49s - CHT_CONF_PATH is gone from the prose and types; nothing read it - down.sh, the runbook and the design doc no longer promise a clean slate from down -v Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| # it leaves the patches unapplied and the build broken. This is why --ignore-scripts | ||
| # is not used here. The code being installed is the same tree we are about to build | ||
| # images from and run, so npm scripts add no privilege beyond what follows. | ||
| ( cd "$TARGET" && npm ci ) |
sugat009
left a comment
There was a problem hiding this comment.
praise: This push fixes the three round-1 blockers and 25 of the other 26 round-1 comments. My inline comment on the reset docstring follows up the last one.
issue (blocking): Issue #66 item 3 asks for test data that conforms to the deployed config. This PR does not generate that data. Please change "Closes #66" to "Part of #66" and file the rest, or tell me which document is wrong.
note: The 42 inline comments do not block. All their suggestions apply together: tsc and eslint are clean, and all 1,208 tests pass. Four suggestions fix four of the 7 new SonarCloud issues. The other three flag intended code, and I will accept them. My round-1 follow-ups on CI, the PR text and the runbook are still open.
| # by hand, always pass a project name: | ||
| # docker compose -p <project> -f cht-couchdb.yml -f cht-core.yml -f cht-agent-net.override.yml up -d |
There was a problem hiding this comment.
issue (non-blocking): This line asks only for a project name for a by-hand run, and that is not enough. The cht-core templates name the internal network ${CHT_NETWORK:-cht-net} (scripts/build/cht-core.yml.template:95-97), and Compose reuses another project's network of that name with only a warning. The cht-docs dev-environment page starts the generated cht-couchdb.yml with medic/password, the same credentials as these scripts, and with no CHT_NETWORK. So that CouchDB is on cht-net, with the service name couchdb. A by-hand stack then joins that network, and its haproxy, which defaults to couchdb, can reach that CouchDB. Two by-hand projects in one checkout also share the default ./srv data directory.
test_env_compose sets CHT_NETWORK and COUCHDB_DATA for this reason (scripts/lib/test-env.sh:51). The suggestion adds both values to the by-hand command, together with the other values that test_env_compose passes. Without COUCHDB_PASSWORD, Compose stops. With the template defaults, the admin is admin, not the agent's medic. The cert CN is also not nginx, the host that the agent dials.
| # by hand, always pass a project name: | |
| # docker compose -p <project> -f cht-couchdb.yml -f cht-core.yml -f cht-agent-net.override.yml up -d | |
| # by hand, pass the same values that test_env_compose passes, not only -p. Without | |
| # CHT_NETWORK, the stack shares the default `cht-net` network and the couchdb/haproxy/api | |
| # names with any other CHT stack that also uses `cht-net`. Without COUCHDB_PASSWORD, | |
| # Compose stops. Use this command: | |
| # COUCHDB_USER=medic COUCHDB_PASSWORD=password COMMON_NAME=nginx \ | |
| # NGINX_HTTP_PORT=127.0.0.1:80 NGINX_HTTPS_PORT=127.0.0.1:443 \ | |
| # CHT_NETWORK=<project>-net COUCHDB_DATA=./srv-<project> \ | |
| # docker compose -p <project> -f cht-couchdb.yml -f cht-core.yml -f cht-agent-net.override.yml up -d |
| > `scripts/test-env-up.sh` gives each checkout its own Compose project, internal | ||
| > network and CouchDB data dir, and refuses to start while another stack's nginx | ||
| > is on `cht-agent-net`. Overrides are listed in `scripts/lib/test-env.sh`. |
There was a problem hiding this comment.
issue (non-blocking): Section 4 has the agent poll https://nginx, but it never says how the agent trusts the stack's self-signed cert. At 436d5fe, readiness treats a TLS verification error as terminal (src/utils/cht-readiness.ts:17-24, :47, :140-141). I probed a self-signed server on Node 22: fetch fails with DEPTH_ZERO_SELF_SIGNED_CERT, so provision() throws as soon as nginx answers. Node loads the NODE_EXTRA_CA_CERTS file once, no later than the first TLS call, so the cert must be in place before the run starts. docker/docker-compose.cht-agent.yml:46-53 also does not forward COUCHDB_USER or COUCHDB_PASSWORD.
No in-container command calls provision() yet, so this is a documentation gap, not a break on the main path. The suggestion adds the trust step and the credential pass-through in the order that works.
| > `scripts/test-env-up.sh` gives each checkout its own Compose project, internal | |
| > network and CouchDB data dir, and refuses to start while another stack's nginx | |
| > is on `cht-agent-net`. Overrides are listed in `scripts/lib/test-env.sh`. | |
| > `scripts/test-env-up.sh` gives each checkout its own Compose project, internal | |
| > network and CouchDB data dir, and refuses to start while another stack's nginx | |
| > is on `cht-agent-net`. Overrides are listed in `scripts/lib/test-env.sh`. | |
| > | |
| > The stack's cert is self-signed, and the agent's readiness poll stops at the first | |
| > TLS verification error. So bring the stack up before the agent run starts, not | |
| > after the agent asks: the poll then succeeds at once. Run the `docker cp` line | |
| > that `test-env-up.sh` prints at its end, then copy that file into the container | |
| > (`docker cp <file> cht-agent:/tmp/cht-cert.pem`). Start the run with | |
| > `docker exec -e NODE_EXTRA_CA_CERTS=/tmp/cht-cert.pem ...`. After | |
| > `test-env-down.sh`, the next bring-up makes a new cert. If the bring-up used | |
| > non-default `COUCHDB_USER` or `COUCHDB_PASSWORD`, pass the same values with `docker exec -e`. |
| test_env_select() { | ||
| local path="$1" base | ||
| TEST_ENV_TARGET="$(cd "$path" && pwd -P)" | ||
| base="$(basename "$TEST_ENV_TARGET" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9_-]/-/g' -e 's/^-*//')" |
There was a problem hiding this comment.
issue (non-blocking): With GNU sed and glibc, in many UTF-8 locales such as en_US.UTF-8 (but not C.UTF-8), [a-z] also matches lowercase accented letters. I sourced this lib under LANG=en_US.UTF-8 and ran test_env_select on a directory named café. It gave cht-agent-café-9aa5266e. Compose rejects any -p value that differs from its normalized form, which keeps only ASCII [a-z0-9_-] (compose-go v2.16.1 cli/options.go:120). So down and restart fail at the ps call on line 60. Up fails only at test-env-up.sh:103, after local-images and, when they run, npm ci and build-dev.
The suggestion runs tr and sed in the C locale. That gives cht-agent-caf---9aa5266e and keeps ASCII names unchanged.
| base="$(basename "$TEST_ENV_TARGET" | tr '[:upper:]' '[:lower:]' | sed -e 's/[^a-z0-9_-]/-/g' -e 's/^-*//')" | |
| base="$(basename "$TEST_ENV_TARGET" | LC_ALL=C tr '[:upper:]' '[:lower:]' | LC_ALL=C sed -e 's/[^a-z0-9_-]/-/g' -e 's/^-*//')" |
| # restart and down would otherwise report success against a project with nothing in it. | ||
| test_env_require_containers() { | ||
| local containers | ||
| containers="$(test_env_compose ps -a -q)" | ||
| if [[ -z "$containers" ]]; then | ||
| echo "error: Compose project '$TEST_ENV_PROJECT' has no containers." >&2 | ||
| echo " 'docker compose ls -a' lists the projects that exist; a stack started under" >&2 | ||
| echo " another name needs CHT_TEST_ENV_PROJECT=<name> (earlier versions of these" >&2 | ||
| echo " scripts left it to Compose, which named every stack 'local-build')." >&2 | ||
| exit 1 | ||
| fi | ||
| } |
There was a problem hiding this comment.
issue (non-blocking): My round-1 suggestion added the CHT_TEST_ENV_PROJECT override, and this hardens it. Line 36 uses the override unchanged, and test_env_require_containers checks only that the project has containers. The error text at lines 63-65 points operators to local-build, the name Compose gives a stack in local-build/ without -p. With that name, down -v (test-env-down.sh:14) deletes another checkout's containers and named volumes, and up -d (test-env-up.sh:103) recreates its containers.
The suggestion refuses a project whose com.docker.compose.project.working_dir label names another directory, and it drops the local-build hint. I tested it with a stub docker. Down and restart still refuse an empty project and pass the owner. They now refuse a foreign or mixed project.
The label holds the path that Compose saw. So the check also refuses a stack that an operator started by hand through a symlinked path. The message names that path. My comment on scripts/test-env-up.sh:54 adds the same check there, so up refuses before the build.
| # restart and down would otherwise report success against a project with nothing in it. | |
| test_env_require_containers() { | |
| local containers | |
| containers="$(test_env_compose ps -a -q)" | |
| if [[ -z "$containers" ]]; then | |
| echo "error: Compose project '$TEST_ENV_PROJECT' has no containers." >&2 | |
| echo " 'docker compose ls -a' lists the projects that exist; a stack started under" >&2 | |
| echo " another name needs CHT_TEST_ENV_PROJECT=<name> (earlier versions of these" >&2 | |
| echo " scripts left it to Compose, which named every stack 'local-build')." >&2 | |
| exit 1 | |
| fi | |
| } | |
| # Refuse a project that another checkout started. With a shared CHT_TEST_ENV_PROJECT, | |
| # up would recreate that checkout's containers, and down -v would delete them and its volumes. | |
| test_env_require_owner() { | |
| local dirs dir | |
| dirs="$(docker ps -a --filter "label=com.docker.compose.project=$TEST_ENV_PROJECT" \ | |
| --format '{{.Label "com.docker.compose.project.working_dir"}}' | sort -u)" | |
| while IFS= read -r dir; do | |
| if [[ -n "$dir" && "$dir" != "$TEST_ENV_TARGET/local-build" ]]; then | |
| echo "error: Compose project '$TEST_ENV_PROJECT' has containers from $dir, which is not $TEST_ENV_TARGET/local-build." >&2 | |
| exit 1 | |
| fi | |
| done <<< "$dirs" | |
| return 0 | |
| } | |
| # restart and down would otherwise report success against a project with nothing in it. | |
| test_env_require_containers() { | |
| local containers | |
| containers="$(test_env_compose ps -a -q)" | |
| if [[ -z "$containers" ]]; then | |
| echo "error: Compose project '$TEST_ENV_PROJECT' has no containers." >&2 | |
| echo " 'docker compose ls -a' lists the projects that exist; a stack this checkout" >&2 | |
| echo " started under another name needs CHT_TEST_ENV_PROJECT=<name>." >&2 | |
| exit 1 | |
| fi | |
| test_env_require_owner | |
| return 0 | |
| } |
| echo "error: cht-core path not found: $TARGET" >&2 | ||
| exit 1 | ||
| fi | ||
| test_env_select "$TARGET" |
There was a problem hiding this comment.
suggestion (non-blocking): This goes with my suggestion on scripts/lib/test-env.sh:57-68. up selects the project here, before the build. Without this call, a shared CHT_TEST_ENV_PROJECT still lets up -d (line 103) recreate the containers of another checkout. With it, up refuses a project that another checkout started. A new project passes, because test_env_require_owner checks only the containers that exist.
| test_env_select "$TARGET" | |
| test_env_select "$TARGET" | |
| test_env_require_owner |
| * so it is refused; checking the release tag out in a working copy reaches the same code. | ||
| */ | ||
| const assertRealModeSupported = (options: ProvisionOptions, network: string): void => { | ||
| if (options.version !== undefined) { |
There was a problem hiding this comment.
nitpick (non-blocking): When a real-mode caller passes both chtCorePath and version, provision logs Source: local code (<path>) (:802-808). This check then throws check <version> out in a cht-core working copy and pass chtCorePath, but the caller already passed one. The refusal targets a request with only a published version, so it should test for a missing working copy.
With the suggestion, real mode ignores version when the caller also passes chtCorePath, as mock mode does. validateProvisionOptions (:260-263) requires version when chtCorePath is absent, so the message stays valid. If you want both options together to stay an error, as src/types/index.ts:973-974 implies, keep the condition and fix only the message. No spec covers this case.
| if (options.version !== undefined) { | |
| if (!options.chtCorePath) { |
| /** | ||
| * Inputs to prepareTestData's real path. The data project is a cht-conf | ||
| * project folder: docs come from `<dataPath>/csv/*.csv` (csv-to-docs naming: | ||
| * place.<type>.csv, person.csv, report.<form>.csv, contact.csv, users.csv), | ||
| * and user accounts from `<dataPath>/users.csv` (hand-written, or generated | ||
| * by csv-to-docs from users.*.csv inputs). create-users only runs when that | ||
| * file exists — cht-conf throws on a missing users.csv. | ||
| */ | ||
| export interface PrepareTestDataOptions { | ||
| /** cht-conf project folder holding csv/ (required by the real path). */ | ||
| dataPath?: string; |
There was a problem hiding this comment.
nitpick (non-blocking): This type doc still says docs come from <dataPath>/csv/*.csv and that dataPath is a folder holding csv/. At 436d5fe, prepareDocs also uploads a hand-authored json_docs when csv/ is absent (agent :635-651). With csv/, it refuses while json_docs holds a doc file it did not generate (agent :591-598). It also writes <dataPath>/.cht-agent-seeded.json into the operator's project (test-data.ts:24, :112). The type doc, the runbook and the design doc mention none of this.
The type doc also omits that cht-conf csv-to-docs replaces an existing <dataPath>/users.csv when the users CSV gives at least one user. The suggestion documents all of this, and the error at agent :955 should also say csv/ or json_docs/.
| /** | |
| * Inputs to prepareTestData's real path. The data project is a cht-conf | |
| * project folder: docs come from `<dataPath>/csv/*.csv` (csv-to-docs naming: | |
| * place.<type>.csv, person.csv, report.<form>.csv, contact.csv, users.csv), | |
| * and user accounts from `<dataPath>/users.csv` (hand-written, or generated | |
| * by csv-to-docs from users.*.csv inputs). create-users only runs when that | |
| * file exists — cht-conf throws on a missing users.csv. | |
| */ | |
| export interface PrepareTestDataOptions { | |
| /** cht-conf project folder holding csv/ (required by the real path). */ | |
| dataPath?: string; | |
| /** | |
| * Inputs to prepareTestData's real path. The data project is a cht-conf | |
| * project folder. With a csv/ directory, csv-to-docs regenerates json_docs from | |
| * `<dataPath>/csv/*.csv` (naming: place.<type>.csv, person.csv, report.<form>.csv, | |
| * contact.csv, users.csv). The layer records the files it generated in | |
| * `<dataPath>/.cht-agent-seeded.json` and refuses to seed while json_docs holds any | |
| * other doc file. Without csv/, the layer uploads the hand-authored json_docs as it is. | |
| * User accounts come from `<dataPath>/users.csv`. The operator writes that file by hand, | |
| * or csv-to-docs writes it from csv/users.csv or csv/users.<name>.csv and replaces any | |
| * existing file. create-users only runs when that file exists: cht-conf throws on a | |
| * missing users.csv. | |
| */ | |
| export interface PrepareTestDataOptions { | |
| /** cht-conf project folder with csv/ or a hand-authored json_docs/ (required in real mode). */ | |
| dataPath?: string; |
| expect(result.warnings.join(' ')).to.include('only 3 of 5 docs uploaded'); | ||
| }); | ||
|
|
||
| it('should warn when json_docs ends up empty (no csv inputs)', async () => { |
There was a problem hiding this comment.
suggestion (non-blocking): uploadShortfall marks a seed as failed in two cases: a short summary, or no summary for a seed with docs. Only the first case has a test. I silenced the second branch with docCount > 0 && false, and all 113 agent specs still pass. The cht-conf 6.6.1 CLI logs the summary line (upload-docs.js:60) whenever it exits 0 with docs to upload, unless it runs with --silent (main.js:120-121). The agent never passes --silent, so a missing summary is a real failure signal. The suggested test passes at 436d5fe and fails on that change.
| it('should warn when json_docs ends up empty (no csv inputs)', async () => { | |
| it('should report succeeded:false when upload-docs exits 0 without a summary for docs it was given', async () => { | |
| runs['upload-docs'] = okRun(ansiInfo('Uploading docs')); | |
| const result = await realAgent.prepareTestData(dockerHandle, sampleConfig, { dataPath }); | |
| expect(result.succeeded).to.equal(false); | |
| expect(result.warnings.join(' ')).to.include('upload-docs printed no summary for 5 doc(s)'); | |
| }); | |
| it('should warn when json_docs ends up empty (no csv inputs)', async () => { |
| const reseedCall = runChtConfStub.firstCall.args[0]; | ||
| expect(reseedCall.verbs).to.deep.equal(['upload-docs']); | ||
| expect(reseedCall.configPath).to.equal(dataPath); | ||
| expect(reseedCall.cwd).to.equal(dataPath); |
There was a problem hiding this comment.
suggestion (non-blocking): The commit says the reset reads the wiped ids back after the reseed, but no test checks that order. This test counts two fetchDocRevs calls (:1286) but not when the second one ran, and the default stub returns the same rows for both reads. I swapped the reseed and the verify step at test-environment-agent.ts:1065-1066, and all 113 agent specs still passed. The suggestion asserts that the second read comes after the reseed. seedTracking resets the runChtConfStub history, so its first call is the reseed. The new line passes at 436d5fe and fails with the swap.
| expect(reseedCall.cwd).to.equal(dataPath); | |
| expect(reseedCall.cwd).to.equal(dataPath); | |
| expect(fetchDocRevsStub.secondCall.calledAfter(runChtConfStub.firstCall)).to.equal(true); |
| const error = await expectRejection(waitForReady('https://nginx', { ...fast, maxWaitMs: 5 })); | ||
|
|
||
| expect(error.message).to.include('ECONNREFUSED'); | ||
| }); |
There was a problem hiding this comment.
suggestion (non-blocking): Both tests that set a cause give it a message. I checked on Node 22, with localhost mapped to both ::1 and 127.0.0.1. There, fetch('https://localhost:59997/x') rejects with an AggregateError cause that has an empty message and code ECONNREFUSED. Only || cause?.code at src/utils/cht-readiness.ts:43 turns that into a useful reason. CHT_URL=https://localhost is a real override, so this case matters.
I changed || to ??, and all 12 tests still pass. The suggested test fails on that change and passes on the current code.
| }); | |
| }); | |
| it('names the cause code when its message is empty (an AggregateError for localhost)', async () => { | |
| const cause = Object.assign(new AggregateError([], ''), { code: 'ECONNREFUSED' }); | |
| fetchStub.rejects(Object.assign(new TypeError('fetch failed'), { cause })); | |
| const error = await expectRejection(waitForReady('https://localhost', { ...fast, maxWaitMs: 5 })); | |
| expect(error.message).to.include('fetch failed (ECONNREFUSED)'); | |
| }); |
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.)