Skip to content

test: add core component acceptance infrastructure - #841

Open
kvinwang wants to merge 31 commits into
masterfrom
codex/core-components-test-infrastructure
Open

test: add core component acceptance infrastructure#841
kvinwang wants to merge 31 commits into
masterfrom
codex/core-components-test-infrastructure

Conversation

@kvinwang

@kvinwang kvinwang commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary

This PR is based directly on master and contains only core-component acceptance test infrastructure, evidence, and product-PR accounting.

Independence and separation invariant

This PR targets master; it has no dependency on product PR #840 or any split product PR.

Its changed paths are limited to:

  • REUSE.toml
  • docs/testing/**
  • tools/dstack-test/**
  • docs/test-plans/core-components-full/**

Verification

The remaining 8 BLOCKED cases require unavailable hardware-backed evidence or GPU capability; they are not test-framework, fixture, documentation, or product-code failures.

Copilot AI review requested due to automatic review settings July 31, 2026 02:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of files (300). Try reducing the number of changed files and requesting a review from Copilot again.

@kvinwang kvinwang changed the title test: add core component acceptance infrastructure [STACKED on #840] test: add core component acceptance infrastructure Jul 31, 2026
@kvinwang
kvinwang force-pushed the codex/core-components-test-infrastructure branch from 7a21a97 to 313efb9 Compare July 31, 2026 03:34
@kvinwang kvinwang changed the title [STACKED on #840] test: add core component acceptance infrastructure test: add core component acceptance infrastructure Jul 31, 2026
@kvinwang
kvinwang changed the base branch from codex/core-components-product-fixes to master July 31, 2026 03:34
kvinwang added 19 commits July 31, 2026 08:49
Use the main listener's existing /health endpoint for liveness and the authenticated Admin root for the dashboard. Do not require a product-only /health/dashboard alias invented by the acceptance fixture.
TC-GOS-OBSERVABIL-003 already asserted that a peer with no handshake
reaches a forced refresh, but it drove the checker with an uneven clock
(1000 1010 1191 ...). That 181s jump lets the staleness deadline and the
periodic refresh land on separate ticks, which is not how the service
runs: the loop sleeps 10s and HANDSHAKE_TIMEOUT equals REFRESH_INTERVAL,
so the two deadlines collide every cycle and the periodic refresh clears
the staleness timer 10s before it can expire.

Add two scenarios on a uniform 10s clock across 45 ticks:

- steady_no_handshake: a peer that never handshakes must still reach
  --force. Only --force rebuilds a tunnel whose config is unchanged,
  since gateway setup returns early otherwise.
- steady_force_rate_limit: with the gateway unreachable, --force must be
  capped at one attempt per 180s window rather than issued every tick,
  which would bounce the interface and re-request certificates 45 times.

Both were replayed against three checker revisions. The uneven-clock
scenario cannot separate the first two; the new ones can:

  revision                        uneven no_handshake   steady   rate limit
  master                          0 forced (fails)      0        44 forced
  #945 before review              1 forced (passes)     0        45 forced
  #945 after review               1 forced (passes)     2         3 forced

Make the sleep stub's tick budget a parameter so a scenario can run long
enough to cross several refresh windows, and require callers to supply
more clock values than ticks: when the file drains the stub returns a
far-future sentinel whose jump forces a refresh no real deployment would
perform, which is what made the checker look correct here.

case.md described the old behavior as intended ("A refresh resets the
stale timer whether the command succeeds or fails"), which is the defect
itself. Restate the contract: only an observed handshake clears the
timer, and forced refresh is rate limited because it is the expensive
path.
@kvinwang

kvinwang commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Requested coverage: gateway ACME credential rotation (PR #935)

PR #935 reworked RotateAcmeCredentials around one invariant that currently has no test backing: credentials are published before CAA re-pinning, so every partial failure converges by rerunning SetCaa without registering another rate-limited ACME account. The unit tests in the crate only cover lock and precondition paths; the convergence semantics need the acceptance harness (mock DNS provider + a Pebble-style ACME server that can count account registrations).

Proposed cases for the gateway component:

  1. rotate-happy-path — multi-domain cluster; rotation succeeds; assert: new account registered at CA, credentials in WaveKV, every domain's CAA issue/issuewild pins the new accounturi, AcmeInfo.account_uri reports the new account.
  2. rotate-precheck-aborts-cleanly — one domain has an invalid/missing DNS credential; assert: RPC fails, zero accounts registered at CA, no CAA record touched, old credentials intact.
  3. rotate-partial-caa-failure-converges — DNS provider fails for domain k of N mid-rotation; assert: RPC error names the failed domains and the SetCaa recovery path, new credentials already published, then a SetCaa rerun converges all domains without a second account registration (CA account count unchanged).
  4. rotate-cross-node-lock — node B calls rotation while node A holds the WaveKV rotation lock; assert: B is rejected with "another node is rotating"; after TTL expiry a retry proceeds.
  5. acme-url-mismatch-refuses — change acme_url (staging→prod flow); assert: renewal fails pointing at RotateAcmeCredentials instead of silently registering a fresh account; rotation then switches directory and re-pins CAA.
  6. corrupt-credentials-fail-closed — corrupt the credential JSON in WaveKV; assert: renewals fail with the remediation message, TLS serving of existing certs is unaffected, rotation repairs the state.
  7. rotate-attestation-lag — rotation with the guest agent unavailable; assert: rotation succeeds and AcmeInfo.account_uri reflects the new account with an empty quote (no stale-URI regression).
  8. caa-guard-record-recovery — interrupt set_caa_records between guard installation and cleanup; assert: issuance for that domain is blocked, and a SetCaa rerun removes the guards and restores issuance.

Cases 3 and 8 also serve as the regression tests for #1010 once an automatic CAA reconciliation loop exists (the loop should make the manual SetCaa rerun in both cases unnecessary).

Context: #935 (rework), #1008 (best-effort lock), #1009 (old-account deactivation), #1010 (CAA reconciliation loop).

@kvinwang

kvinwang commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Updated the Gateway upgrade fixture to stop passing the retired PROXY_BASE_DOMAIN setting. Candidate Gateway tests now register gateway-candidate.test through Admin.AddZtDomain; PR #947 changes SNI classification to use registered ZT domains, so TLS-passthrough routing no longer depends on a statically injected wildcard certificate.

@kvinwang

kvinwang commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Replaced the incomplete ZT-domain setup with the production certificate flow. Candidate Gateway fixtures now run isolated Pebble and mock Cloudflare DNS services, configure Certbot through the Admin API, create the DNS credential, register the ZT domain, force certificate issuance, and wait until the certificate is loaded before exercising SNI routing. This no longer depends on PROXY_BASE_DOMAIN or closed PR #947.

Checks run:

  • python3 -m py_compile docs/test-plans/core-components-full/automation/kms_upgrade_matrix_case.py
  • git diff --check
  • repository search confirms no PROXY_BASE_DOMAIN occurrence

The checker is no longer wg-checker.sh; it is dstack-util's
`gateway-checker` subcommand behind dstack-gateway-checker.service. This
case installed the shell script into the guest and pinned its sha256, so
it could not run at all against the new image.

The old driver derived the refresh matrix (periodic interval, handshake
staleness, forced-refresh rate limiting) by injecting a fake clock and
fake wg/dstack-util onto PATH. That matrix is now a pure decision
function with unit tests in dstack/dstack-util/src/gateway_checker.rs, so
reproducing it here would only restate those tests more slowly and less
reliably. Dropped.

What unit tests cannot reach is the process/systemd boundary, which is
what the case now covers:

- an app that never enabled dstack-gateway makes the checker exit 0, so
  Restart=on-failure leaves it alone instead of respawning it every
  RestartSec on every gateway-less CVM;
- a missing gateway app id and a missing gateway URL each exit with
  EXIT_MISCONFIGURED, read from the product source at run time rather
  than restated here;
- the installed unit is loaded, uses Restart=on-failure, inhibits restart
  for exactly that code, and runs the subcommand rather than the removed
  script.

The real namespace-isolated WireGuard topology is kept: it exercises the
`wg show ... latest-handshakes` shape the checker parses and is unaffected
by the rewrite.

Also renames the unit in tc-gos-platform-006's graph assertions and in
the source inventory and coverage map.

Registry: 352 scripted / 352 promoted / 0 findings. Runner unit tests: 35
passed. validate-contracts.py findings are unchanged from the branch
point.
@kvinwang

kvinwang commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator Author

Added acceptance coverage for merged PR #935 in commit e9c1086f6. The existing three-node ACME account case now verifies authenticated RotateAcmeCredentials, unauthorized rejection, account replacement, account-bound issue/issuewild CAA re-pinning, WaveKV convergence across nodes, ACME-directory mismatch fail-closed behavior, renewal recovery, and restart persistence.

Validation: runner unit tests 35 passed; Python/JSON syntax and git diff --check passed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants