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 14 — Partial-Run Semantics

A distributed run that completed every test against every host with every validator passing is the easy case. The hard case is the run where one of five hosts crashed mid-suite, where verification caught a Topology mismatch on one cluster but not another, where the framework activated only some of the declared services. The framework's commitment is honesty about scope: report what ran, what didn't, and why, without overstating success.

Single-host test frameworks have a clean answer to "did the suite pass?" — every test ran; some passed, some failed; count the failures. Distributed runs make the question more complicated. A test that didn't run because its host was unreachable is not a passing test, but it's also not a failing test in the same sense as a test whose assertions failed. A test whose validator caught a steady-state violation while the test's own assertions passed is a complicated outcome. A verification step that failed for one cluster but not another means part of the Pod was usable.

This chapter walks the framework's vocabulary for these outcomes and the discipline for reporting them honestly.

The four outcome categories per test

The base testbase outcome enum (ResultCode from codecraftai-testbase/source/packages/ccai/testbase/results/model/resultcode.py) distinguishes the per-test states. The four that matter for the per-test verdict are:

Outcome Meaning
Passed The test ran and all its assertions succeeded
Failed The test ran and an assertion failed
Errored The test ran but raised an unexpected exception
Skipped The test did not run (e.g., @skip_if, deactivation)

The four categories cover the single-host case. A distributed run adds a fifth implicit category that the framework represents through test runs that never started:

| Implicit | Not attempted | The test was discovered but the framework never reached it (run aborted, host unreachable for activation, verification failed) |

The framework's reporting acknowledges all five states explicitly.

The validator outcome — separate from the test outcome

Chapter 5 walked tandem validators: a test with three validators is one test with four recorded outcomes (the test plus three validators). Each validator has its own outcome category — passed, failed, errored. Validators don't get a "skipped" category because validators only run when their test runs.

The aggregate outcome of a test-with-validators is:

test outcome = "Passed" if test.outcome == Passed AND all validators passed
test outcome = "Failed" if test.outcome == Failed OR any validator failed
test outcome = "Errored" if test.outcome == Errored OR any validator errored
test outcome = "Skipped" if test.outcome == Skipped

A test passes its own assertions while a validator catches a violation — the aggregate is Failed. This is the framework's commitment: validators are not advisory. Their failures count.

Phase outcomes — coarser-grained than test outcomes

Volume 1 chapter 2 walked the sequencer phases (Initial, Discovery, Collection, Graph, Traversal). In a distributed run, the activation phases (Configuration, Integration, Operational from chapter 6 of this volume) and the verification step (chapter 7) also have outcomes:

Phase Possible outcomes
Discovery success / partial (some test files failed to import) / failed
Collection success / failed (originator resolution failure)
Graph success / failed (cycle detected)
Activation: Configuration success / failed (file load / parse error)
Activation: Integration success / partial (some devices failed init) / failed
Activation: Operational success / partial (some devices unreachable) / failed
Verification: Landscape success / failed (reachability check raised)
Verification: Topology success / partial (some clusters mismatched) / failed
Traversal success / partial (some tests skipped or not attempted) / failed

Each phase gets its own JSOS phase_started / phase_finished record (Volume 1 chapter 5). The aggregate run outcome is derived from the phase outcomes:

  • If any phase failed (no recovery), the run is aborted.
  • If any phase is partial, the run is partial.
  • If all phases succeeded, the run is complete.

Combined with the test outcomes, the run report has:

  • A run-level outcome (aborted / partial / complete).
  • A phase-by-phase breakdown.
  • A test-by-test breakdown.

When a partial run is honest

A partial run is honest when the report acknowledges what was not tested. The framework's reporting is structured to prevent the reader from accidentally treating a partial run as a clean run.

Who writes these files. The outcome model — the Passed/Failed/Errored/Skipped categories and the not-attempted/partial-run honesty — lives in testbase's results model (ResultCode and the recorder serialisation). But the on-disk summary and per-host log layout shown below is an integration-runtime artefact. A plain testbase run uses StdoutResultRecorder, which prints the run summary to stdout and (when an output directory is in play) writes the JSOS stream and JSON summary, but skips HTML rendering. The presence-folder results directory — the well-known location these files land in, the per-host logs, and the rendered HTML summary — is produced by IntegratedTestJob (codecraftai-integration-runtime), whose _create_recorder returns the HTML-rendering JsonResultRecorder and whose _create_output_context returns RuntimeOutputContext (results under the activated app's presence folder via ccai.thisapp.paths). The honest summary shape below is the same regardless of recorder; only its persistence is the integration runtime's job.

A representative partial-run summary (the JSON summary written by IntegratedTestJob's recorder):

{
  "run_id": "20260501-153012-abc123",
  "outcome": "partial",
  "phases": {
    "configuration": "success",
    "integration": "success",
    "operational": "partial",
    "verification": {
      "landscape": "partial",
      "topology": "success"
    },
    "traversal": "partial"
  },
  "tests": {
    "passed": 78,
    "failed": 3,
    "errored": 1,
    "skipped": 5,
    "not_attempted": 0
  },
  "operational_partial_reason": "1 of 12 devices unreachable",
  "landscape_partial_reason": "1 of 12 devices unreachable (same)",
  "traversal_partial_reason": "5 tests skipped because their declared host was unreachable",
  "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"
  },
  "missing_devices": ["casey-12"]
}

The summary tells the reader:

  • 78 tests passed, 3 failed, 1 errored, 5 skipped because of the missing device.
  • The Pod was 11/12 hosts.
  • Verification caught the missing device at the operational phase; the run was allowed to continue because allow_missing_devices=True was in effect.

A consumer reading this can decide:

  • "78 / 87 declared tests passed, but only 87 of 92 declared tests were attempted, and 1 of 12 hosts was unreachable" — is that acceptable for the use case?
  • A nightly full-sweep run treats this as a failure (we expected 12 hosts; we got 11).
  • A smoke run treats it as a success (the framework worked against 11 hosts; the failures are real and shippable signal).

The framework's job is not to decide which. The framework's job is to report honestly so the orchestrator (Volume 3) or the human can decide.

When a partial run is dishonest

A partial run is dishonest when the report:

  • Reports "passed" without acknowledging that some tests weren't attempted.
  • Aggregates outcomes such that a not_attempted count is hidden.
  • Omits the phase outcomes, presenting only test outcomes.
  • Reports test outcomes without the validator outcomes that failed.

The framework prevents these by structurally including the honest fields in every summary. A consumer that wants only a boolean "did all the declared tests pass" must compute it from the explicit fields:

def did_all_declared_tests_pass(summary):
    rtnval = (
        summary["tests"]["passed"]
        == summary["tests"]["passed"]
            + summary["tests"]["failed"]
            + summary["tests"]["errored"]
            + summary["tests"]["skipped"]
            + summary["tests"]["not_attempted"]
    ) and summary["outcome"] == "complete"
    return rtnval

The boolean is computable but the consumer has to ask explicitly. No accidental "passed."

The allow_missing_devices honesty trade-off

LandscapeActivationParams.allow_missing_devices=True (chapter 6) lets activation proceed with unreachable devices. The trade-off:

  • Benefit: smoke runs can complete against partial Pods, catching framework bugs without requiring a perfect lab.
  • Cost: a partial run is harder to interpret than an aborted run. A reader must look at the report to know what was tested.

The framework's honesty discipline is what makes the trade-off acceptable. Without honest reporting, partial runs would be misleading; with honest reporting, the trade-off is the project's to make per use case.

What partial-run failures look like in the JSOS stream

Volume 1 chapter 5's JSOS stream from the recorder carries the phase events with their outcomes. The JSOS serialisation is shared: both testbase's default StdoutResultRecorder and integration-runtime's JsonResultRecorder emit the same record shape (so the testbase agents machine CLI reads either one). What differs is where the stream lands — a plain testbase run echoes its summary to stdout and writes the stream only when an output directory is configured, while IntegratedTestJob persists it to the activated app's presence-folder results directory alongside the rendered HTML summary:

{kind: phase_started, name: configuration, ...}
{kind: phase_finished, name: configuration, outcome: success, ...}
{kind: phase_started, name: integration, ...}
{kind: phase_finished, name: integration, outcome: success, ...}
{kind: phase_started, name: operational, ...}
{kind: phase_finished, name: operational, outcome: partial,
 reason: "1 of 12 devices unreachable",
 missing_devices: ["casey-12"], ...}
{kind: phase_started, name: traversal, ...}
{kind: test_started, name: test_replication_in_majority, ...}
{kind: test_finished, name: test_replication_in_majority, outcome: passed, ...}
... (more tests) ...
{kind: test_skipped, name: test_failover_to_casey_12,
 reason: "Required host casey-12 was not in operational landscape", ...}
... (more tests, including the four other skipped ones) ...
{kind: phase_finished, name: traversal, outcome: partial,
 skipped_tests: [
   "test_failover_to_casey_12",
   "test_drift_includes_casey_12",
   "test_quorum_with_casey_12",
   "test_membership_query_casey_12",
   "test_health_check_all_hosts"
 ], ...}
{kind: run_finished, outcome: partial, ...}

A consumer parsing the stream sees the partial outcome at each layer. An AI agent triaging the run can ask "why was the run partial?" and trace the answer through the phase events. Volume 1 chapter 6's agents testresult failures extracts the failure subset; a similar agents testresult notattempted (a project addition, not shipped) would extract the not-attempted subset for review.

The run report — three audiences

Different consumers of a run report want different things:

CI dashboards

Want a green / red summary. The right boolean for a CI dashboard is strict: the run is "green" only if outcome == "complete" AND failed + errored == 0. A partial run is "yellow" or "red" depending on the project's stance.

Triaging engineers

Want the failure list and the not-attempted list. The agents CLI's testresult failures and (project-supplied) testresult notattempted are the surfaces. The host log references in the JSOS records lead to the per-host logs in the results directory — which, for an integration run, is the presence-folder directory IntegratedTestJob resolves through RuntimeOutputContext.

Replay tools

Want enough information to re-run the suite. The agents config extract (Volume 1 chapter 6) produces the exact invocation that triggered the run, including the job_seed. Combined with the partial run's missing_devices list, a replay tool can decide whether to re-attempt against the same Pod or escalate for re-provisioning.

AI agents

Want all of the above as structured data. The framework's JSOS stream and agents CLI shape are designed for this — same shape, same fields, every agent reads the same way.

What the next chapter does

Chapter 15 walks debugging a distributed failure. Where this chapter walked the vocabulary for partial outcomes, chapter 15 walks the workflow for diagnosing them: the audit-diary pattern from the six-pass landscaping audit (the real production-grade audit pattern from the portfolio's history), the cross-host log-correlation discipline, the bisection pattern for reproducing flakes, and the forward-reference to Volume 8's time-travel debugging for the production-grade post-mortem.