Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
1be7698
feat(harness-protocol): wire payload types for protocol v0
adbarc92 Sep 14, 2026
41b4e3a
feat(harness-protocol): JSON-RPC envelope, method names, line codec
adbarc92 Sep 15, 2026
67587dd
feat(harness-protocol): JSON Schema export and hash-pinned contract
adbarc92 Sep 15, 2026
c1f009a
feat(harness-conformance): harness-fake, a scripted harness with misb…
adbarc92 Sep 15, 2026
c024c6f
feat(harness-conformance): session, unit driver, version and happy-pa…
adbarc92 Sep 15, 2026
1760eb8
feat(harness-conformance): oracle gate approve/reject, halt and aband…
adbarc92 Sep 15, 2026
3b8ca45
test(harness-conformance): prove the kit detects each protocol violation
adbarc92 Sep 15, 2026
5bc0f92
feat(harness-conformance): CLI with PASS/SKIP/FAIL report and exit codes
adbarc92 Sep 15, 2026
c32cb70
ci: required harness conformance check; README for harness authors
adbarc92 Sep 15, 2026
04f6838
fix(harness-conformance): skip gate_approved_t2 when the gate is neve…
adbarc92 Sep 15, 2026
1fcd319
fix(harness-conformance): require an interrupt to be answered, then e…
adbarc92 Sep 15, 2026
12b18e8
fix(harness-conformance): deliver interrupts while the gate is pendin…
adbarc92 Sep 15, 2026
b1bafff
fix(harness-conformance): pass harness stderr through
adbarc92 Sep 15, 2026
01a716f
docs(harness-protocol): state what the conformance kit checks
adbarc92 Sep 15, 2026
265515c
docs(harness-protocol): make metric and method semantics explicit; re…
adbarc92 Sep 15, 2026
78b74b6
chore(harness-conformance): reject zero grace, drop unused serde_json…
adbarc92 Sep 15, 2026
8f2b715
chore(harness-conformance): update Cargo.lock for the removed serde_j…
adbarc92 Sep 15, 2026
be593cf
docs(plans): SP-1 implementation plan for the harness protocol, confo…
adbarc92 Sep 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@ name: CI
# Tauri bundles on all three target OSes. Bundles are uploaded as workflow
# artifacts so a reviewer can smoke-test a real build.
#
# TWO CARGO WORKSPACES: the root one (crates/fleet-core, crates/fleetd) and the
# standalone cockpit/ui/src-tauri crate. Nothing run from the repo root reaches
# the latter, so the fmt, clippy and test gates each run once per manifest.
# TWO CARGO WORKSPACES: the root one (crates/fleet-core, crates/fleetd,
# crates/harness-protocol, crates/harness-conformance) and the standalone
# cockpit/ui/src-tauri crate. Nothing run from the repo root reaches the latter,
# so the fmt, clippy and test gates each run once per manifest.
#
# COVERAGE GAP (intentional): the real-Docker integration tests in
# crates/fleetd/tests/ (local_docker_it.rs, preflight_it.rs, swarm_smoke_it.rs)
Expand Down Expand Up @@ -179,6 +180,29 @@ jobs:
- name: Run workspace tests (Docker ITs stay --ignored)
run: cargo test --workspace

# ---------------------------------------------------------------------------
# Harness conformance (NEXUS spec 2026-09-14-swappable-harness-design §6.2, §6.7).
# Runs the conformance kit's CLI against harness-fake. This check must be
# REQUIRED on main: an advisory check here would repeat GAP-133.
# ---------------------------------------------------------------------------
conformance:
name: harness conformance
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Cache cargo registry + target
uses: Swatinem/rust-cache@v2

- name: Build the conformance kit and harness-fake
run: cargo build -p harness-conformance --bins

- name: Run the conformance kit against harness-fake
run: target/debug/harness-conformance --wall-clock-secs 30 --grace-secs 5 -- target/debug/harness-fake

# ---------------------------------------------------------------------------
# The cockpit crate's own tests — the app-plugin state machine, manifest
# validation and discovery. `cargo test --workspace` above runs from the repo
Expand Down
115 changes: 108 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
members = ["crates/fleet-core", "crates/fleetd"]
members = ["crates/fleet-core", "crates/fleetd", "crates/harness-protocol", "crates/harness-conformance"]

[workspace.package]
edition = "2021"
Expand Down
17 changes: 17 additions & 0 deletions crates/harness-conformance/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "harness-conformance"
edition.workspace = true
version.workspace = true
license.workspace = true
description = "Conformance kit for harness-protocol, plus harness-fake, a scripted harness."

[dependencies]
harness-protocol = { path = "../harness-protocol" }

[[bin]]
name = "harness-fake"
path = "src/bin/harness-fake.rs"

[[bin]]
name = "harness-conformance"
path = "src/bin/harness-conformance.rs"
79 changes: 79 additions & 0 deletions crates/harness-conformance/src/bin/harness-conformance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//! `harness-conformance [--wall-clock-secs N] [--grace-secs N] -- <harness command> [args...]`
//!
//! Runs every conformance case against a harness and exits 0 only if none failed.

use harness_conformance::{run_all, CaseOutcome, KitConfig};
use std::process::ExitCode;
use std::time::Duration;

const USAGE: &str =
"usage: harness-conformance [--wall-clock-secs N] [--grace-secs N] -- <harness command> [args...]";

fn parse(args: Vec<String>) -> Result<KitConfig, String> {
let split = args
.iter()
.position(|a| a == "--")
.ok_or_else(|| USAGE.to_string())?;
let (flags, rest) = args.split_at(split);
let command = rest[1..].to_vec();
if command.is_empty() {
return Err(USAGE.to_string());
}
let mut cfg = KitConfig::new(command);
let mut it = flags.iter();
while let Some(flag) = it.next() {
let value = it
.next()
.ok_or_else(|| format!("{flag} needs a value\n{USAGE}"))?;
let secs: u64 = value
.parse()
.map_err(|_| format!("{flag}: `{value}` is not a whole number of seconds"))?;
match flag.as_str() {
"--wall-clock-secs" => cfg.wall_clock = Duration::from_secs(secs),
"--grace-secs" => cfg.grace = Duration::from_secs(secs),
other => return Err(format!("unknown flag `{other}`\n{USAGE}")),
}
}
if cfg.grace.is_zero() {
return Err(format!(
"--grace-secs must be at least 1: a zero grace fails every case\n{USAGE}"
));
}
Ok(cfg)
}

fn main() -> ExitCode {
let cfg = match parse(std::env::args().skip(1).collect()) {
Ok(cfg) => cfg,
Err(message) => {
eprintln!("{message}");
return ExitCode::from(2);
}
};

let reports = run_all(&cfg);
let (mut passed, mut skipped, mut failed) = (0, 0, 0);
for report in &reports {
match &report.outcome {
CaseOutcome::Pass => {
passed += 1;
println!("PASS {}", report.name);
}
CaseOutcome::Skipped(why) => {
skipped += 1;
println!("SKIP {} ({why})", report.name);
}
CaseOutcome::Fail(violation) => {
failed += 1;
println!("FAIL {} {violation:?}", report.name);
}
}
}
println!("{passed} passed, {skipped} skipped, {failed} failed");

if failed == 0 {
ExitCode::SUCCESS
} else {
ExitCode::FAILURE
}
}
Loading
Loading