Volume 2 — Distributed Automation — contents
  1. Chapter 1 — Why Distributed Automation Is Genuinely Hard
  2. Chapter 2 — Single Actor, Multiple Observers — The Determinism Model
  3. Chapter 3 — The Pod Model — Where Parallelism Actually Lives
  4. Chapter 4 — The TestJob Inheritance Chain — The Customisation Cascade
  5. Chapter 5 — Tandem Testing in Depth
  6. Chapter 6 — Landscape Activation
  7. Chapter 7 — The CI → Job Hand-off as a Verified Contract
  8. Chapter 8 — The DistributedTestSequencer
  9. Chapter 9 — job_seed and Reproducibility
  10. Chapter 10 — The Remote-Execution Wire
  11. Chapter 11 — Credentials at Scale
  12. Chapter 12 — Data Profiles in Remote Workers
  13. Chapter 13 — Result Aggregation
  14. Chapter 14 — Partial-Run Semantics
  15. Chapter 15 — Debugging a Distributed Failure
  16. Chapter 16 — Case Study: A 5-Host Integration Test for a Sysman Feature

Chapter 15 — Debugging a Distributed Failure

Distributed automation produces failures that don't reproduce on a laptop. The framework's debugging story is therefore not "step through the code"; it's "extract enough structured data from the failed run to reason about what happened across hosts and produce a hypothesis you can test." This chapter walks the audit-diary pattern, with the six-pass landscaping audit as the reference example.

A distributed test failed in CI. The summary says one test errored on host C, two validators caught steady-state violations, and host F's log fetch was truncated. The CI runner has been torn down. Re-running may or may not reproduce. How does an SDET diagnose what happened?

This chapter is the framework's answer. It is not a universal-debugging chapter — the framework can't replace domain knowledge of the system under test. It is a chapter on what the framework's structured data lets you do and the audit-diary pattern for working through it.

The full production-grade post-mortem story — replay-driven debugging, time-travel reconstruction, pod instrumentation — lives in Volume 8. This chapter establishes the workflow at the framework level; Volume 8 provides the tooling that makes the workflow easier in production.

The structured data the framework provides

The structured-data debugging story in this chapter is the story of an integration run — a run driven by IntegratedTestJob (in codecraftai-integration-runtime). Plain testbase runs standalone and records only to stdout (its default recorder is StdoutResultRecorder); it has no presence folder, no RuntimeVariables, and no on-disk results-directory layout to debug from. The presence-folder results directory, the JSOS-to-disk stream, the rendered HTML summary, and the CI/build metadata are all the responsibility of IntegratedTestJob, which overrides three TestJob provider hooks: _create_output_context() returns RuntimeOutputContext (artefacts land under the activated app's well-known presence folder via ccai.thisapp.paths — activation-gated), _create_run_metadata() returns RuntimeRunMetadata (build / pipeline / job from RuntimeVariables), and _create_recorder() returns JsonResultRecorder (JSOS streaming plus the rendered HTML summary). The audit-diary and evidence discipline below therefore applies to IntegratedTestJob runs — that's where the files exist to reason about.

An integration run that failed leaves a results directory shaped roughly:

results/20260501-153012-abc123/
├── testsummary.json                # high-level
├── testresults_stream.jsos         # streaming records
├── testresults.json                # consolidated
├── startup-configuration.json      # run inputs (Vol 1 ch.6)
├── host-logs/
│   ├── casey-01/
│   │   ├── syslog.log
│   │   ├── auth.log
│   │   └── application.log
│   ├── casey-02/
│   │   └── ...
│   └── casey-12/                   # may be partial — host went down
│       └── pre_only.log
├── diagnostics/
│   ├── pre_testrun/
│   │   ├── package-versions.json
│   │   ├── environment.json
│   │   └── per-host/
│   │       ├── casey-01.json       # host metadata snapshot
│   │       └── ...
│   └── post_testrun/
│       └── ...
└── tc-by-products/
    ├── test_replication_in_majority/
    │   ├── stdout.log
    │   ├── stderr.log
    │   └── per-host-output/
    │       ├── casey-01.log        # commands run on casey-01
    │       └── ...
    └── ...

That is the framework's gift to the debugger. Every structured fact about the run is recoverable from these files. The debugger's job is to reason about what combination of facts produced the failure.

The audit-diary pattern

The portfolio's debugging discipline — applied across multiple audits in real history — is the audit diary. An audit diary is a structured note an SDET keeps while diagnosing a failure. It has six sections:

  1. Symptom. What the run reported. Quote the JSOS record.
  2. Hypothesis-1. The first plausible explanation.
  3. Evidence for / against H1. What the structured data says.
  4. Hypothesis-2 (if H1 ruled out). Repeat.
  5. Diagnosis. The root cause once a hypothesis fits all the evidence.
  6. Remediation. The fix and how it's verified.

The discipline is not unique to this framework — it's a generic engineering pattern. The framework's contribution is making each section answerable through the structured data. Symptoms come from JSOS records; evidence comes from per-test logs and host logs; remediation is verified by re-running the suite with the same job_seed (chapter 9).

A worked audit diary

A real-shape example. Suppose a run failed with:

{
  "run_id": "20260501-153012-abc123",
  "outcome": "partial",
  "tests": {"passed": 47, "failed": 1, "errored": 1, "skipped": 3},
  "phases": {
    "operational": "partial",
    "verification": {"landscape": "partial", "topology": "success"}
  },
  "missing_devices": ["casey-12"]
}

And the failure record:

{
  "kind": "test_finished",
  "test_name": "test_quorum_after_partition",
  "outcome": "errored",
  "exception": {
    "type": "TimeoutError",
    "message": "Timeout waiting for cluster to converge",
    "traceback": [
      "  File \"tests/test_quorum.py\", line 87",
      "    wait_for_quorum(cluster, timeout=60)",
      "  File \"ccai/agentkit/cluster.py\", line 142, in wait_for_quorum",
      "    raise TimeoutError(...)"
    ]
  },
  "host_log_status": {
    "casey-01": "complete", "casey-02": "complete",
    "casey-03": "complete", "casey-04": "complete",
    "casey-05": "complete", "casey-06": "complete",
    "casey-07": "complete", "casey-08": "complete",
    "casey-09": "complete", "casey-10": "complete",
    "casey-11": "complete", "casey-12": "unreachable_at_activation"
  }
}

Symptom

test_quorum_after_partition errored with TimeoutError — "Timeout waiting for cluster to converge." The Pod was 11/12 hosts (casey-12 unreachable from activation).

Hypothesis-1

The test depends on casey-12 for quorum. With casey-12 down, the cluster cannot reach quorum and the wait_for_quorum times out. Root cause: the test should have been skipped (like test_failover_to_casey_12 was) but wasn't.

Evidence for H1

  • The test has no @skip_if(host_unavailable("casey-12")) decoration. (Read the test source.)
  • Tests that were skipped have explicit declarations. (Read the JSOS skip records: each carries reason: "Required host casey-12 was not in operational landscape" — the framework's deactivation chain caught them.)
  • The test's host requirements are implicit — it walks the full cluster expecting all 12 hosts.

Diagnosis

The test's host requirements are implicit; the framework couldn't deactivate it because the framework didn't know what the test required. The test ran against an 11-host cluster expecting a 12-host quorum.

Remediation

Add explicit host requirements to the test. Either through a @skip_if(host_unavailable(...)) decoration matching the ones on the explicitly-host-bound tests, or by reading the integrated landscape and skipping when the configured majority cannot be reached. Re-run with the same job_seed against an 11-host Pod (recreating the failure deterministically); the test should now skip rather than error.

The audit-diary takes 15-30 minutes for a clear case like this. The framework's structured data is what makes it possible to answer each section without re-running the suite.

The cross-host log-correlation discipline

When a failure spans multiple hosts (a network partition test, a replication test, a leader-election test), the audit diary needs to correlate events across host logs. The framework's discipline:

  • Use the JSOS record's timestamps as the controller's reference clock. Volume 1 chapter 5 said the controller's clock is the JSOS clock.
  • Identify each host's offset from the controller at the pre-test diagnostic capture. The capture includes per-host time queries; the offset is host_time - controller_time at that moment.
  • Apply the offset (with caveats about drift) when reading host logs. A controller-side JSOS record at T_controller = 15:32:00.500 corresponds to roughly T_host_C = T_controller + offset_C in casey-03's syslog.

The framework does not automatically do this correlation — chapter 13 explained the design choice to keep timelines side-by-side rather than forced-merged. But the diagnostic capture provides the offset values, and a debugging script (or AI agent) can compute the correlation when needed.

The bisection pattern for reproducing flakes

A test that fails 5% of the time is a flake. The audit diary for a flake has a different shape than for a deterministic failure.

Three phases:

Phase 1 — Reproduce the failure deterministically

Run the test repeatedly with random seeds until you have a seed that triggers the failure. Record the seed. Now you have a reproducible failure (same seed → same input → same test behaviour, modulo Pod-state non-determinism).

Phase 2 — Bisect on Pod state

Re-run the failure-inducing seed against a fresh Pod. If the failure reproduces, the failure is Pod-state-independent (it's about the test or the system under test, not the specific Pod state). If it doesn't, the failure is Pod-state-dependent — re-running won't help; you need Volume 8's time-travel tooling to reconstruct the original Pod state.

Phase 3 — Bisect on inputs

For Pod-state-independent failures, the failure-inducing seed gives you a specific input. Use the framework's shrinking machinery (Volume 1 chapter 4) to find the smallest input that still triggers. Add that input as a regression test case.

The pattern is universal but the tools differ between Pod-state-dependent and Pod-state-independent flakes. Most distributed flakes turn out to be Pod-state-dependent, which is why Volume 8's tooling is so important for production debugging.

Reference: the six-pass landscaping audit

The portfolio's history includes a six-pass audit of codecraftai-landscaping that fixed 15+ silent runtime bugs. The audits are real production-grade examples of the audit-diary pattern at scale. The commits:

  • Pass 1 (42f9c86) — six refactor-leftover bugs, including Liskov-substitution violations (three protocol extension subclasses lacked **kwargs forwarding), an uninitialized ControlExtension._control_id, three string-concatenation bugs, a mutable default argument in factories.create_landscape(), and duplicate dataclass definitions of DatacenterSummary and FolderSummary.
  • Pass 2 (a92e5d2) — two attribute-name mismatches: a LandscapeService.__init__ declaring _primary_ssh_credential but the search loop and property using _ssh_credential (silent AttributeError on credential lookup); an OrchestrationJob.begin() writing to _orch_results_dir but reading the nonexistent _test_results_dir.
  • Pass 3 (1679fe7) — two more refactor-leftover bugs: LandscapeIntegrationLayer.__init__ initialising pool attributes to None instead of {} (breaking @property accessors that called .copy() unconditionally); a ClientSourcePackager leaking file handles in zip-file digest computation.
  • Pass 4 (1545f67) — five logic / behavioural bugs: exclude-filter "duplicate-add" bug at four call sites (missing break, wrong semantics — items that passed N filters were added N times); FriendlyIdentifier.__eq__ always returning True (compared self._full_identifier to itself rather than other._full_identifier); FriendlyIdentifier.identity property dereferencing regex groups without null checks; wrong return-type annotations on get_devices() and get_services() (annotated to return single items instead of lists); inconsistent exception type in ClientSourcePackager (Exception instead of RuntimeError).
  • Passes 5 and 6 — incremental, integrated into specific test fixtures.

Three lessons from the six-pass arc:

1. Audits surface progressively

Each audit pass caught bugs the previous passes missed. The first pass caught structural bugs (Liskov violations, uninitialised attributes, mutable defaults). Later passes caught semantic bugs (filter logic, equality semantics, type annotations). The progression reflects the reviewer's lens shift — each pass looks for a different class of issue.

2. Silent failures are common

Every bug fixed across the six passes was silent at import time. None of them caused an ImportError; none of them broke the package's smoke tests. They surfaced only when specific code paths exercised the bug — and the code paths were sometimes exercised only in distributed scenarios that were rare in single-host tests.

The lesson: the framework's structured-data discipline is critical for catching silent failures. An audit diary working from JSOS records can spot inconsistencies (a test that should have been skipped wasn't; a credential that should have resolved didn't) that no individual test exposed.

3. Tests added per fix

Every audit pass added regression tests for the bugs it caught. The Volume 1 chapter 10 case study mentioned that testbase's own test suite runs around 3,700 tests; the landscaping audit contributed +101 tests across 11 test files to that count. The tests are now permanent regressions ensuring the same classes of bugs can't recur.

What's not in this chapter — Volume 8

Production-grade post-mortem debugging — replaying a failed run, walking the Pod's state at a specific moment, reconstructing what happened with full instrumentation — is Volume 8 (Time-Travel Debugging in Production). Volume 8's tools assume:

  • The Pod was instrumented during the run.
  • The instrumentation produced a replay-able event log.
  • A debugger can replay the event log against a reconstructed Pod state.

For Pod-state-dependent flakes (the hard ones), Volume 8 is the right tool. This chapter walks what an SDET can do with just the framework's structured data; that's enough for ~70% of distributed failures and a starting point for the remaining 30% that need Volume 8's tooling.

What the next chapter does

Chapter 16 is the volume's case study: a 5-host integration test for a sysman feature. The chapter walks the entire volume's machinery in one worked example — Pod provisioning, inheritance chain, landscape activation, verification, tandem validators, the wire, credentials, partial-run semantics, audit diary.

By the end of the case study, a reader who started Volume 2 cold should be able to read a real-world distributed test job's source and predict its behaviour. That fluency is the volume's contract.