mirror of
https://github.com/nearai/ironclaw.git
synced 2026-09-03 08:06:01 +08:00
All three were NORMAL, none blocking; closing them so review sees the design rather than a list of known nits. **One owner for the debug-info policy** (converged finding, reported independently by both system-audit lanes). The migration deleted these env pairs from five workflow job envs on the strength of Cargo.toml's `[profile.dev] debug = 0` owning the value — but left the identical `:-0` defaults standing in scripts/ci/quality_gate.sh and reborn-local-coverage-ratchet.sh. The PR's own claim of a single owner was not true of the tree it shipped. Both removed; no behaviour change, the values already agreed, and a developer's `CARGO_PROFILE_DEV_DEBUG=2` still reaches cargo because `env` inherits what it does not override. Deleting the lines alone would just let them come back, so `validate_single_debug_policy_owner` makes it structural: no script under scripts/ may ASSIGN a CARGO_PROFILE_*_DEBUG value. Assignments only — run-hermetic-test-process.sh names the same variables in a passthrough allowlist (a `case` pattern, no `=`), which is exactly how that override survives the hermetic barrier, and matching it would break the documented escape hatch. Test files are skipped: they carry the string on purpose as fixtures, and scanning them would make the contract untestable. **No speculative escape hatch.** `ACCEPTED_RUST_BOOTSTRAPS` was a per-path override with symmetric over-use AND under-use validation, plus a test pinning it empty — built in full for a case the module's own comment says does not exist. Removed; the rule is now unconditional. If an unavoidable bootstrap ever appears, the hatch gets added then, with that lane as its first entry. The test that asserted the dict stays empty now asserts the mechanism is gone rather than merely empty. **One job-boundary walk.** `_job_blocks` reimplemented the slice-between- consecutive-headings logic that `extract_job_block` already did in the same subsystem. `job_blocks()` in workflow_text.py is now the single primitive: `extract_job_block` filters it to one named job and keeps its exactly-one refusal, and the toolchain contracts enumerate it from the `jobs:` offset. That offset is load-bearing and stays — JOB_HEADING matches any two-space key, so an unbounded walk treats `on:`'s children as jobs. Verified: ws12 141 tests OK, live gate passed, planner 90 OK, staged-paths 4 OK, classify-test-scope exit 0, check-guidance OK, both edited shell scripts pass `bash -n`. The debug-policy guard proven red-first by restoring one deleted line and watching the gate name it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2834 lines
119 KiB
Python
Executable File
2834 lines
119 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Sabotage tests for scheduled, merge, and release WS12 lanes."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import copy
|
|
import dataclasses
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent / "lib"))
|
|
|
|
import rust_toolchain_contracts # noqa: E402
|
|
import ws12_workflow_contracts # noqa: E402
|
|
from rust_toolchain_contracts import ( # noqa: E402
|
|
SETUP_RUST_ACTION,
|
|
validate_no_direct_dtolnay_usage,
|
|
validate_no_job_env_rustflags_with_setup_rust,
|
|
validate_no_unmanaged_rust_bootstrap,
|
|
validate_release_workflow_installs_rust,
|
|
validate_rust_jobs_reach_the_composite,
|
|
validate_setup_rust_action,
|
|
validate_single_debug_policy_owner,
|
|
validate_toolchain_pin_sync,
|
|
)
|
|
from workflow_text import JOB_HEADING, STEP_HEADING, job_body, step_body # noqa: E402
|
|
from ws12_workflow_contracts import ( # noqa: E402
|
|
CODE_STYLE_WORKFLOW,
|
|
CRATE_NAME_RESIDUE,
|
|
CRATE_SCOPE_FILTERS,
|
|
DOCKER_WORKFLOW,
|
|
E2E_WORKFLOW,
|
|
LIBSQL_SCRIPTED_MEMORY_JOB,
|
|
NIGHTLY_DEEP_CI_WORKFLOW,
|
|
PLATFORM_WORKFLOW,
|
|
REQUIRED_MARKERS,
|
|
STRESS_WORKFLOW,
|
|
WEBUI_FRONTEND_CRATE,
|
|
WEBUI_NESTED_LOCKFILE_PATTERN,
|
|
crate_directory,
|
|
extract_job_block,
|
|
github_glob_to_regex,
|
|
load_workflows,
|
|
validate_crate_name_residue,
|
|
validate_crate_scope_filters,
|
|
validate_e2e_scope_filters,
|
|
validate_libsql_scripted_memory_job,
|
|
validate_postgres_scripted_parity,
|
|
validate_production_lint_targets,
|
|
validate_windows_webui_install_shell,
|
|
validate_webui_frontend_sites,
|
|
validate_workflow_texts,
|
|
)
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
SCCACHE_SETUP_ACTION = (
|
|
ROOT / ".github" / "actions" / "setup-sccache-dist" / "action.yml"
|
|
)
|
|
|
|
|
|
class SingleDebugPolicyOwnerTests(unittest.TestCase):
|
|
"""Cargo.toml's [profile.dev] is the only writer of the debug-info value."""
|
|
|
|
def tree(self, body: str) -> Path:
|
|
root = Path(self.enterContext(tempfile.TemporaryDirectory()))
|
|
(root / "scripts" / "ci").mkdir(parents=True)
|
|
(root / "scripts" / "ci" / "gate.sh").write_text(body, encoding="utf-8")
|
|
return root
|
|
|
|
def test_a_second_writer_is_rejected(self) -> None:
|
|
root = self.tree('CARGO_PROFILE_TEST_DEBUG="${CARGO_PROFILE_TEST_DEBUG:-0}"\n')
|
|
errors = validate_single_debug_policy_owner(root)
|
|
self.assertEqual(1, len(errors), errors)
|
|
self.assertIn("scripts/ci/gate.sh:1", errors[0])
|
|
self.assertIn("Cargo.toml", errors[0])
|
|
|
|
def test_the_hermetic_passthrough_allowlist_still_passes(self) -> None:
|
|
"""A `case` pattern naming the vars is not a second writer.
|
|
|
|
run-hermetic-test-process.sh lists them so a developer's
|
|
`CARGO_PROFILE_DEV_DEBUG=2` override survives the hermetic barrier.
|
|
Matching that entry would break the documented escape hatch.
|
|
"""
|
|
root = self.tree(
|
|
" CARGO_INCREMENTAL|CARGO_PROFILE_DEV_DEBUG|"
|
|
"CARGO_PROFILE_TEST_DEBUG|CARGO_TEST_ARGS|\\\n"
|
|
)
|
|
self.assertEqual([], validate_single_debug_policy_owner(root))
|
|
|
|
def test_a_comment_mentioning_the_override_still_passes(self) -> None:
|
|
root = self.tree("# override per-run: CARGO_PROFILE_DEV_DEBUG=2 cargo test\n")
|
|
self.assertEqual([], validate_single_debug_policy_owner(root))
|
|
|
|
def test_the_live_tree_has_exactly_one_owner(self) -> None:
|
|
self.assertEqual([], validate_single_debug_policy_owner(ROOT))
|
|
|
|
|
|
class RustJobsReachTheCompositeTests(unittest.TestCase):
|
|
"""Every job that runs cargo must reach the composite, not just the release lane.
|
|
|
|
The release-lane guard was written after a workflow lost its Rust install
|
|
and every absence-only check called that clean — but it only covered one
|
|
file. Deleting the composite step from any other workflow reproduced the
|
|
identical bug with the suite green, because "no dtolnay, no bootstrap, no
|
|
shadowing RUSTFLAGS" is trivially true of a job that installs nothing.
|
|
"""
|
|
|
|
COMPOSITE = " - uses: ./.github/actions/setup-rust\n"
|
|
|
|
def workflow(self, *, with_composite: bool) -> str:
|
|
return (
|
|
"name: demo\n"
|
|
"on:\n"
|
|
" push:\n"
|
|
"jobs:\n"
|
|
" build:\n"
|
|
" runs-on: ubuntu-latest\n"
|
|
" steps:\n"
|
|
+ (self.COMPOSITE if with_composite else "")
|
|
+ " - run: cargo test --workspace\n"
|
|
)
|
|
|
|
def test_a_cargo_job_without_the_composite_is_rejected(self) -> None:
|
|
errors = validate_rust_jobs_reach_the_composite(
|
|
{".github/workflows/demo.yml": self.workflow(with_composite=False)}
|
|
)
|
|
self.assertEqual(1, len(errors), errors)
|
|
self.assertIn("runs cargo but never reaches", errors[0])
|
|
self.assertIn("'build'", errors[0])
|
|
|
|
def test_a_cargo_job_with_the_composite_passes(self) -> None:
|
|
self.assertEqual(
|
|
[],
|
|
validate_rust_jobs_reach_the_composite(
|
|
{".github/workflows/demo.yml": self.workflow(with_composite=True)}
|
|
),
|
|
)
|
|
|
|
def test_a_job_that_never_runs_cargo_needs_no_composite(self) -> None:
|
|
text = (
|
|
"name: docs\n"
|
|
"on:\n"
|
|
" push:\n"
|
|
"jobs:\n"
|
|
" lint-docs:\n"
|
|
" runs-on: ubuntu-latest\n"
|
|
" steps:\n"
|
|
" - run: npm run lint\n"
|
|
)
|
|
self.assertEqual(
|
|
[], validate_rust_jobs_reach_the_composite({".github/workflows/d.yml": text})
|
|
)
|
|
|
|
def test_cargo_in_a_path_filter_or_comment_is_not_an_invocation(self) -> None:
|
|
"""`Cargo.toml` in a paths filter must not demand a Rust install."""
|
|
text = (
|
|
"name: paths\n"
|
|
"on:\n"
|
|
" push:\n"
|
|
' paths:\n - "Cargo.toml"\n - "**/cargo-timings/**"\n'
|
|
"jobs:\n"
|
|
" notify:\n"
|
|
" runs-on: ubuntu-latest\n"
|
|
" steps:\n"
|
|
" # we could run cargo here one day\n"
|
|
" - run: echo hi\n"
|
|
)
|
|
self.assertEqual(
|
|
[], validate_rust_jobs_reach_the_composite({".github/workflows/p.yml": text})
|
|
)
|
|
|
|
def test_a_job_reaching_the_composite_by_yaml_alias_passes(self) -> None:
|
|
"""release-plz.yml picks the composite up through an anchor alias."""
|
|
text = (
|
|
"name: alias\n"
|
|
"on:\n"
|
|
" push:\n"
|
|
"x-steps:\n"
|
|
" - &install-rust\n"
|
|
" uses: ./.github/actions/setup-rust\n"
|
|
"jobs:\n"
|
|
" build:\n"
|
|
" runs-on: ubuntu-latest\n"
|
|
" steps:\n"
|
|
" - *install-rust\n"
|
|
" - run: cargo build\n"
|
|
)
|
|
self.assertEqual(
|
|
[], validate_rust_jobs_reach_the_composite({".github/workflows/a.yml": text})
|
|
)
|
|
|
|
def test_every_live_cargo_job_reaches_the_composite(self) -> None:
|
|
"""Ships with no allowlist: an exemption would be an unpinned lane."""
|
|
self.assertEqual(
|
|
[],
|
|
validate_rust_jobs_reach_the_composite(
|
|
ws12_workflow_contracts.load_workflows(ROOT)
|
|
),
|
|
)
|
|
|
|
|
|
class WorkflowTextHelperTests(unittest.TestCase):
|
|
"""The shared helpers must define each pattern exactly once.
|
|
|
|
ws12_workflow_contracts.py used to bind JOB_HEADING twice, the second
|
|
silently shadowing the first for every validator below it. Both patterns
|
|
happened to be equivalent, so nothing broke — but an edit to the dead one
|
|
would have had no effect and no test would have said so. Splitting the
|
|
helpers out is only a fix if the duplicate actually dies with it.
|
|
"""
|
|
|
|
def test_each_pattern_is_defined_exactly_once(self) -> None:
|
|
source = (Path(__file__).resolve().parent / "lib" / "workflow_text.py").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
for name in ("JOB_HEADING", "STEP_HEADING"):
|
|
self.assertEqual(
|
|
1,
|
|
source.count(f"{name} = re.compile"),
|
|
f"{name} must be bound exactly once; a second binding silently "
|
|
"shadows the first for every caller below it",
|
|
)
|
|
|
|
def test_job_heading_matches_a_two_space_job_key(self) -> None:
|
|
text = "jobs:\n build-Rust_1:\n runs-on: ubuntu-latest\n"
|
|
self.assertEqual(
|
|
["build-Rust_1"], [m.group("name") for m in JOB_HEADING.finditer(text)]
|
|
)
|
|
|
|
def test_job_heading_ignores_deeper_keys(self) -> None:
|
|
text = "jobs:\n build:\n steps:\n - name: x\n"
|
|
self.assertEqual(
|
|
["build"], [m.group("name") for m in JOB_HEADING.finditer(text)]
|
|
)
|
|
|
|
|
|
class SetupRustActionContractTests(unittest.TestCase):
|
|
"""The setup-rust composite must actually pin RUSTUP_TOOLCHAIN and mold."""
|
|
|
|
OK = (
|
|
"runs:\n"
|
|
" using: composite\n"
|
|
" steps:\n"
|
|
" - name: Install Rust\n"
|
|
" id: install\n"
|
|
" uses: dtolnay/rust-toolchain@abc123 # stable\n"
|
|
" with:\n"
|
|
" toolchain: ${{ inputs.toolchain }}\n"
|
|
" - name: Pin the resolved toolchain for the rest of this job\n"
|
|
" shell: bash\n"
|
|
" run: |\n"
|
|
' echo "RUSTUP_TOOLCHAIN=${{ steps.install.outputs.name }}" >> "$GITHUB_ENV"\n'
|
|
" - name: Install mold and clang\n"
|
|
" if: ${{ inputs.mold == 'true' && runner.os == 'Linux' }}\n"
|
|
" shell: bash\n"
|
|
" run: scripts/ci/install-ci-apt-packages.sh clang mold\n"
|
|
" - name: Verify mold linker is active\n"
|
|
" if: ${{ inputs.mold == 'true' && runner.os == 'Linux' }}\n"
|
|
" shell: bash\n"
|
|
" run: rustc --version\n"
|
|
" - name: Export mold RUSTFLAGS\n"
|
|
" if: ${{ inputs.mold == 'true' && runner.os == 'Linux' }}\n"
|
|
" shell: bash\n"
|
|
" run: |\n"
|
|
' echo "RUSTFLAGS=-C linker=clang -C link-arg=--ld-path=/usr/bin/mold ${RUSTFLAGS:-}" >> "$GITHUB_ENV"\n'
|
|
)
|
|
|
|
def test_missing_action_file_fails(self):
|
|
errors = validate_setup_rust_action(None)
|
|
self.assertTrue(any("could not read" in e for e in errors))
|
|
|
|
def test_missing_rustup_toolchain_pin_fails(self):
|
|
bad = self.OK.replace(
|
|
'echo "RUSTUP_TOOLCHAIN=${{ steps.install.outputs.name }}" >> "$GITHUB_ENV"\n',
|
|
"",
|
|
)
|
|
errors = validate_setup_rust_action(bad)
|
|
self.assertTrue(any("RUSTUP_TOOLCHAIN" in e for e in errors))
|
|
|
|
def test_missing_mold_linux_guard_fails(self):
|
|
bad = self.OK.replace(
|
|
"if: ${{ inputs.mold == 'true' && runner.os == 'Linux' }}\n shell: bash\n run: scripts/ci/install-ci-apt-packages.sh clang mold\n",
|
|
"shell: bash\n run: scripts/ci/install-ci-apt-packages.sh clang mold\n",
|
|
)
|
|
errors = validate_setup_rust_action(bad)
|
|
self.assertTrue(any("Linux" in e for e in errors))
|
|
|
|
def test_missing_mold_verify_linux_guard_fails(self):
|
|
"""An unguarded 'Verify mold linker is active' step runs the mold
|
|
link check on every runner OS, not just Linux — the same
|
|
mold: true-is-unsafe-elsewhere failure mode the install/export steps
|
|
are already pinned against."""
|
|
bad = self.OK.replace(
|
|
"if: ${{ inputs.mold == 'true' && runner.os == 'Linux' }}\n shell: bash\n run: rustc --version\n",
|
|
"shell: bash\n run: rustc --version\n",
|
|
)
|
|
errors = validate_setup_rust_action(bad)
|
|
self.assertTrue(any("Verify mold linker is active" in e and "Linux" in e for e in errors), errors)
|
|
|
|
def test_missing_mold_verify_step_fails(self):
|
|
bad = self.OK.replace(
|
|
" - name: Verify mold linker is active\n"
|
|
" if: ${{ inputs.mold == 'true' && runner.os == 'Linux' }}\n"
|
|
" shell: bash\n"
|
|
" run: rustc --version\n",
|
|
"",
|
|
)
|
|
errors = validate_setup_rust_action(bad)
|
|
self.assertTrue(
|
|
any("missing the 'Verify mold linker is active' step" in e for e in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_ok_passes(self):
|
|
self.assertEqual([], validate_setup_rust_action(self.OK))
|
|
|
|
|
|
class DirectDtolnayUsageForbiddenTests(unittest.TestCase):
|
|
"""Every workflow must install Rust through the setup-rust composite."""
|
|
|
|
def test_direct_dtolnay_call_fails(self):
|
|
workflows = {
|
|
".github/workflows/x.yml": (
|
|
" - uses: dtolnay/rust-toolchain@abc123 # stable\n"
|
|
)
|
|
}
|
|
errors = validate_no_direct_dtolnay_usage(workflows)
|
|
self.assertTrue(any("x.yml" in e for e in errors))
|
|
|
|
def test_composite_call_passes(self):
|
|
workflows = {
|
|
".github/workflows/x.yml": (
|
|
" - uses: ./.github/actions/setup-rust\n"
|
|
)
|
|
}
|
|
self.assertEqual([], validate_no_direct_dtolnay_usage(workflows))
|
|
|
|
def test_bare_mold_rustflags_string_outside_composite_fails(self):
|
|
workflows = {
|
|
".github/workflows/x.yml": (
|
|
'RUSTFLAGS: "-C linker=clang -C link-arg=--ld-path=/usr/bin/mold"\n'
|
|
)
|
|
}
|
|
errors = validate_no_direct_dtolnay_usage(workflows)
|
|
self.assertTrue(any("mold" in e.lower() for e in errors))
|
|
|
|
|
|
class JobEnvRustflagsShadowingTests(unittest.TestCase):
|
|
"""A setup-rust job may not declare its own RUSTFLAGS env key.
|
|
|
|
Job env is re-applied to every step on top of $GITHUB_ENV, so such a key
|
|
silently shadows the composite's mold export — slower builds, never a
|
|
red check. The flags belong in the composite's extra_rustflags input.
|
|
"""
|
|
|
|
SETUP_RUST_JOB = (
|
|
" crate-tests:\n"
|
|
" env:\n"
|
|
' RUSTC_BOOTSTRAP: "1"\n'
|
|
" steps:\n"
|
|
" - uses: ./.github/actions/setup-rust\n"
|
|
" with:\n"
|
|
" mold: true\n"
|
|
)
|
|
|
|
def test_job_level_rustflags_alongside_setup_rust_fails(self):
|
|
shadowing = self.SETUP_RUST_JOB.replace(
|
|
' RUSTC_BOOTSTRAP: "1"\n',
|
|
' RUSTC_BOOTSTRAP: "1"\n RUSTFLAGS: "-Zcrate-attr=x"\n',
|
|
)
|
|
errors = validate_no_job_env_rustflags_with_setup_rust(
|
|
{".github/workflows/x.yml": shadowing}
|
|
)
|
|
self.assertTrue(any("crate-tests" in e for e in errors), errors)
|
|
|
|
def test_extra_rustflags_input_passes(self):
|
|
passing = self.SETUP_RUST_JOB + ' extra_rustflags: "-Zcrate-attr=x"\n'
|
|
self.assertEqual(
|
|
[],
|
|
validate_no_job_env_rustflags_with_setup_rust(
|
|
{".github/workflows/x.yml": passing}
|
|
),
|
|
)
|
|
|
|
def test_rustflags_in_a_job_that_does_not_use_setup_rust_is_allowed(self):
|
|
other = (
|
|
" docs:\n"
|
|
" env:\n"
|
|
' RUSTFLAGS: "-Zcrate-attr=x"\n'
|
|
" steps:\n"
|
|
" - run: echo hi\n"
|
|
)
|
|
self.assertEqual(
|
|
[],
|
|
validate_no_job_env_rustflags_with_setup_rust(
|
|
{".github/workflows/x.yml": other}
|
|
),
|
|
)
|
|
|
|
def test_sibling_job_without_setup_rust_is_allowed_in_a_multi_job_file(self):
|
|
"""The FILE-level skip (`SETUP_RUST_USES not in text`) is already
|
|
covered above by a single-job file. This is the PER-JOB skip branch
|
|
(`SETUP_RUST_USES not in block`): a sibling job's own RUSTFLAGS must
|
|
stay allowed even when another job in the SAME file uses the
|
|
composite."""
|
|
workflow = (
|
|
" docs:\n"
|
|
" env:\n"
|
|
' RUSTFLAGS: "-Zcrate-attr=docs"\n'
|
|
" steps:\n"
|
|
" - run: echo hi\n"
|
|
) + self.SETUP_RUST_JOB
|
|
self.assertEqual(
|
|
[],
|
|
validate_no_job_env_rustflags_with_setup_rust(
|
|
{".github/workflows/x.yml": workflow}
|
|
),
|
|
)
|
|
|
|
def test_workflow_level_rustflags_alongside_setup_rust_fails(self):
|
|
"""A workflow-level top `env:` block applies to every job in the
|
|
file identically to a job-level env key, but sits before any job
|
|
heading at two-space indentation — invisible to a check that only
|
|
slices text starting at each job heading and only matches six-space
|
|
indentation."""
|
|
workflow = 'env:\n RUSTFLAGS: "-Zcrate-attr=x"\njobs:\n' + self.SETUP_RUST_JOB
|
|
errors = validate_no_job_env_rustflags_with_setup_rust(
|
|
{".github/workflows/x.yml": workflow}
|
|
)
|
|
self.assertTrue(any("workflow-level" in e for e in errors), errors)
|
|
|
|
def test_workflow_level_rustflags_is_caught_when_an_on_block_exists(self):
|
|
"""The fixture must carry an `on:` block or the check looks fine.
|
|
|
|
JOB_HEADING matches any two-space `key:` line, so `on:`'s children
|
|
(`push:`, `workflow_call:`) matched too. Taking headings[0] blindly
|
|
truncated the preamble at the first TRIGGER, making this check dead
|
|
code on every real workflow — and the original fixture passed only
|
|
because it happened to omit `on:`.
|
|
"""
|
|
workflows = {
|
|
".github/workflows/x.yml": (
|
|
"name: X\n"
|
|
"on:\n"
|
|
" push:\n"
|
|
" branches: [main]\n"
|
|
" workflow_call:\n"
|
|
"env:\n"
|
|
' RUSTFLAGS: "-Zcrate-attr=x"\n'
|
|
"jobs:\n" + self.SETUP_RUST_JOB
|
|
)
|
|
}
|
|
errors = validate_no_job_env_rustflags_with_setup_rust(workflows)
|
|
self.assertTrue(
|
|
any("workflow-level" in e for e in errors),
|
|
f"workflow-level RUSTFLAGS must be caught behind an on: block: {errors}",
|
|
)
|
|
|
|
def test_a_job_reaching_the_composite_by_yaml_alias_is_still_checked(self):
|
|
"""`- *install-rust` carries no literal `uses:` line of its own.
|
|
|
|
release-plz.yml reuses the composite step through a YAML anchor, so a
|
|
text-only per-job scan skipped that job entirely and its job-level
|
|
RUSTFLAGS would have shadowed mold silently.
|
|
"""
|
|
workflows = {
|
|
".github/workflows/x.yml": (
|
|
"jobs:\n"
|
|
" first:\n"
|
|
" steps:\n"
|
|
" - &install-rust\n"
|
|
" name: Install Rust\n"
|
|
" uses: ./.github/actions/setup-rust\n"
|
|
" second:\n"
|
|
" env:\n"
|
|
' RUSTFLAGS: "-Zcrate-attr=x"\n'
|
|
" steps:\n"
|
|
" - *install-rust\n"
|
|
)
|
|
}
|
|
errors = validate_no_job_env_rustflags_with_setup_rust(workflows)
|
|
self.assertTrue(
|
|
any("'second'" in e for e in errors),
|
|
f"alias-reached job must be checked: {errors}",
|
|
)
|
|
|
|
def test_step_level_rustflags_is_also_caught(self):
|
|
"""A step's own `env:` shadows the composite just like a job's.
|
|
|
|
The pattern pinned the exact 6-space job-env depth, so a step-level
|
|
key (10 spaces in this repo's workflows) slipped through — the same
|
|
silent mold-drop the guard exists to prevent.
|
|
"""
|
|
workflows = {
|
|
".github/workflows/x.yml": (
|
|
"jobs:\n"
|
|
" build:\n"
|
|
" steps:\n"
|
|
" - uses: ./.github/actions/setup-rust\n"
|
|
" - name: Compile\n"
|
|
" env:\n"
|
|
' RUSTFLAGS: "-Zcrate-attr=x"\n'
|
|
" run: cargo build\n"
|
|
)
|
|
}
|
|
errors = validate_no_job_env_rustflags_with_setup_rust(workflows)
|
|
self.assertTrue(
|
|
any("'build'" in e for e in errors),
|
|
f"step-level RUSTFLAGS must be caught: {errors}",
|
|
)
|
|
|
|
def test_live_workflows_are_clean(self):
|
|
workflows = ws12_workflow_contracts.load_workflows(ROOT)
|
|
self.assertEqual(
|
|
[], validate_no_job_env_rustflags_with_setup_rust(workflows)
|
|
)
|
|
|
|
|
|
|
|
class UnmanagedRustBootstrapTests(unittest.TestCase):
|
|
"""A curl bootstrap installs Rust as surely as the vendor action.
|
|
|
|
The dtolnay check greps one vendor string, so `curl sh.rustup.rs | sh`
|
|
passed it forever — unpinned, without mold, unchecked against
|
|
rust-toolchain.toml. cargo-dist regenerates ironclaw-release.yml, so its
|
|
container-only bootstrap is an accepted exception, pinned to one.
|
|
"""
|
|
|
|
def test_bootstrap_in_a_hand_written_workflow_fails(self):
|
|
workflows = {
|
|
".github/workflows/x.yml": (
|
|
" run: curl -sSf https://sh.rustup.rs | sh -s -- -y\n"
|
|
)
|
|
}
|
|
errors = validate_no_unmanaged_rust_bootstrap(workflows)
|
|
self.assertTrue(any("x.yml" in e for e in errors), errors)
|
|
|
|
def test_rustup_init_and_toolchain_install_also_count(self):
|
|
for snippet in ("rustup-init -y", "rustup toolchain install 1.98.0"):
|
|
with self.subTest(snippet=snippet):
|
|
errors = validate_no_unmanaged_rust_bootstrap(
|
|
{".github/workflows/x.yml": f" run: {snippet}\n"}
|
|
)
|
|
self.assertTrue(errors, f"{snippet} should be caught")
|
|
|
|
def test_the_release_workflow_has_no_carve_out(self):
|
|
"""The release lane is migrated, not exempted.
|
|
|
|
cargo-dist re-includes .github/dist-build-setup.yml on every
|
|
regeneration, so the release build jobs reach the composite there;
|
|
a bootstrap reappearing in the generated workflow is a regression.
|
|
"""
|
|
workflows = {
|
|
".github/workflows/ironclaw-release.yml": (
|
|
" run: curl -sSf https://sh.rustup.rs | sh -s -- -y\n"
|
|
)
|
|
}
|
|
errors = validate_no_unmanaged_rust_bootstrap(workflows)
|
|
self.assertTrue(any("ironclaw-release" in e for e in errors), errors)
|
|
|
|
def test_a_bootstrap_is_rejected_with_no_exemption_available(self) -> None:
|
|
"""There is no allowlist to add a lane to; the rule is unconditional.
|
|
|
|
The empty `ACCEPTED_RUST_BOOTSTRAPS` dict this replaces was an escape
|
|
hatch, fully built with symmetric over/under-use validation, for a
|
|
case the module's own comment said did not exist. Flagged by the
|
|
structural-discipline audit as speculative generality.
|
|
"""
|
|
errors = validate_no_unmanaged_rust_bootstrap(
|
|
{".github/workflows/demo.yml": " - run: curl https://sh.rustup.rs | sh\n"}
|
|
)
|
|
self.assertEqual(1, len(errors), errors)
|
|
self.assertIn("raw Rust bootstrap", errors[0])
|
|
self.assertNotIn("ACCEPTED", errors[0])
|
|
self.assertFalse(
|
|
hasattr(rust_toolchain_contracts, "ACCEPTED_RUST_BOOTSTRAPS"),
|
|
"the exemption mechanism should be gone, not merely empty",
|
|
)
|
|
|
|
def test_alternate_vendor_toolchain_actions_are_caught(self):
|
|
"""The composite is the only sanctioned installer.
|
|
|
|
Enumerating rustup bootstraps alone left an obvious hole: a workflow
|
|
could reach for a different vendor action and pass every gate while
|
|
running an unpinned toolchain with no mold.
|
|
"""
|
|
for action in (
|
|
"actions-rs/toolchain@v1",
|
|
"actions-rust-lang/setup-rust-toolchain@v1",
|
|
"hecrj/setup-rust-action@v1",
|
|
):
|
|
with self.subTest(action=action):
|
|
errors = validate_no_unmanaged_rust_bootstrap(
|
|
{".github/workflows/x.yml": f" - uses: {action}\n"}
|
|
)
|
|
self.assertTrue(errors, f"{action} should be caught")
|
|
|
|
def test_live_workflows_hold_the_contract(self):
|
|
workflows = ws12_workflow_contracts.load_workflows(ROOT)
|
|
self.assertEqual([], validate_no_unmanaged_rust_bootstrap(workflows))
|
|
|
|
|
|
|
|
class ReleaseWorkflowInstallsRustTests(unittest.TestCase):
|
|
"""The release lane must REACH the composite, not merely lack a bootstrap.
|
|
|
|
Every sibling check asserts an absence. That let a release workflow with
|
|
no Rust install at all pass the entire suite: the old `curl | sh` step was
|
|
deleted and its replacement went only into the cargo-dist fragment, which
|
|
reaches the generated file solely via `dist generate`. Container builds
|
|
would have died on `cargo: command not found`.
|
|
"""
|
|
|
|
STEP = "uses: ./.github/actions/setup-rust"
|
|
|
|
def test_missing_step_in_the_generated_workflow_fails(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
(root / ".github").mkdir()
|
|
(root / ".github" / "dist-build-setup.yml").write_text(
|
|
f"- {self.STEP}\n", encoding="utf-8"
|
|
)
|
|
errors = validate_release_workflow_installs_rust(
|
|
{".github/workflows/ironclaw-release.yml": "jobs:\n build:\n"},
|
|
root,
|
|
)
|
|
self.assertTrue(
|
|
any("ironclaw-release.yml" in e for e in errors), errors
|
|
)
|
|
|
|
def test_missing_step_in_the_fragment_fails(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
(root / ".github").mkdir()
|
|
(root / ".github" / "dist-build-setup.yml").write_text(
|
|
"- name: Something else\n", encoding="utf-8"
|
|
)
|
|
errors = validate_release_workflow_installs_rust(
|
|
{".github/workflows/ironclaw-release.yml": f" - {self.STEP}\n"},
|
|
root,
|
|
)
|
|
self.assertTrue(
|
|
any("dist-build-setup.yml" in e for e in errors), errors
|
|
)
|
|
|
|
def test_both_present_passes(self):
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
root = Path(tmp)
|
|
(root / ".github").mkdir()
|
|
(root / ".github" / "dist-build-setup.yml").write_text(
|
|
f"- {self.STEP}\n", encoding="utf-8"
|
|
)
|
|
self.assertEqual(
|
|
[],
|
|
validate_release_workflow_installs_rust(
|
|
{".github/workflows/ironclaw-release.yml": f" - {self.STEP}\n"},
|
|
root,
|
|
),
|
|
)
|
|
|
|
def test_the_live_release_lane_reaches_the_composite(self):
|
|
workflows = ws12_workflow_contracts.load_workflows(ROOT)
|
|
self.assertEqual([], validate_release_workflow_installs_rust(workflows, ROOT))
|
|
|
|
|
|
class ToolchainPinSyncTests(unittest.TestCase):
|
|
"""rust-toolchain.toml and the composite's default must name one version."""
|
|
|
|
def _root(self, toolchain_text: str | None, action_text: str | None) -> Path:
|
|
tmp = Path(tempfile.mkdtemp())
|
|
if toolchain_text is not None:
|
|
(tmp / "rust-toolchain.toml").write_text(toolchain_text)
|
|
action_dir = tmp / ".github" / "actions" / "setup-rust"
|
|
action_dir.mkdir(parents=True)
|
|
if action_text is not None:
|
|
(action_dir / "action.yml").write_text(action_text)
|
|
return tmp
|
|
|
|
ACTION_OK = (
|
|
"inputs:\n"
|
|
" toolchain:\n"
|
|
" default: \"1.98.0\"\n"
|
|
)
|
|
FILE_OK = '[toolchain]\nchannel = "1.98.0"\ncomponents = ["clippy", "rustfmt"]\n'
|
|
|
|
def test_missing_file_fails(self):
|
|
errors = validate_toolchain_pin_sync(self._root(None, self.ACTION_OK))
|
|
self.assertTrue(any("rust-toolchain.toml" in e for e in errors))
|
|
|
|
def test_mismatched_default_fails(self):
|
|
action = self.ACTION_OK.replace("1.98.0", "1.97.0")
|
|
errors = validate_toolchain_pin_sync(self._root(self.FILE_OK, action))
|
|
self.assertTrue(any("1.97.0" in e for e in errors))
|
|
|
|
def test_matching_default_passes(self):
|
|
errors = validate_toolchain_pin_sync(self._root(self.FILE_OK, self.ACTION_OK))
|
|
self.assertEqual([], errors)
|
|
|
|
def test_a_reordered_earlier_input_with_a_non_empty_default_cannot_hide_drift(
|
|
self,
|
|
):
|
|
"""A whole-file `re.search` for the first `default: "..."` resolves
|
|
to whichever input happens to come first — today that is `toolchain`
|
|
only because every other input's default is empty and sits after it.
|
|
Add an earlier input with a non-empty default that happens to equal
|
|
the pinned channel while `toolchain` itself has drifted: an unscoped
|
|
search would find the decoy default, see it match, and report no
|
|
drift at all — exactly the silent-pass this contract exists to rule
|
|
out."""
|
|
action = (
|
|
"inputs:\n"
|
|
' components:\n'
|
|
' default: "1.98.0"\n'
|
|
" toolchain:\n"
|
|
' default: "1.97.0"\n'
|
|
)
|
|
errors = validate_toolchain_pin_sync(self._root(self.FILE_OK, action))
|
|
self.assertTrue(any("1.97.0" in e for e in errors), errors)
|
|
|
|
|
|
class LoadWorkflowsTests(unittest.TestCase):
|
|
"""Every workflow contract runs on load_workflows()'s output — a file it
|
|
fails to discover is invisible to every one of them, silently, since
|
|
nothing iterates the directory a second way to notice the gap."""
|
|
|
|
def test_discovers_both_yml_and_yaml_extensions(self):
|
|
tmp = Path(tempfile.mkdtemp())
|
|
workflows_dir = tmp / ".github" / "workflows"
|
|
workflows_dir.mkdir(parents=True)
|
|
(workflows_dir / "a.yml").write_text("name: a\n")
|
|
(workflows_dir / "b.yaml").write_text("name: b\n")
|
|
loaded = ws12_workflow_contracts.load_workflows(tmp)
|
|
self.assertEqual(
|
|
loaded,
|
|
{
|
|
".github/workflows/a.yml": "name: a\n",
|
|
".github/workflows/b.yaml": "name: b\n",
|
|
},
|
|
)
|
|
|
|
|
|
class SccacheSetupActionContractTests(unittest.TestCase):
|
|
"""The optional compiler cache must never gate the tests it accelerates."""
|
|
|
|
COMPLIANT_ACTION = """\
|
|
runs:
|
|
using: composite
|
|
steps:
|
|
- name: Install sccache
|
|
id: install_sccache
|
|
continue-on-error: true
|
|
uses: mozilla-actions/sccache-action@example
|
|
|
|
- name: Configure OVH sccache
|
|
if: ${{ inputs.enabled == 'true' && steps.install_sccache.outcome == 'success' }}
|
|
shell: bash
|
|
run: configure-cache
|
|
|
|
- name: Fall back to local compilation
|
|
if: ${{ steps.install_sccache.outcome == 'failure' }}
|
|
shell: bash
|
|
run: echo "::warning title=sccache unavailable::Installation failed; using local compilation."
|
|
"""
|
|
|
|
def validate(self, text: str) -> list[str]:
|
|
validator = getattr(
|
|
ws12_workflow_contracts, "validate_sccache_setup_action", None
|
|
)
|
|
self.assertIsNotNone(
|
|
validator,
|
|
"ws12_workflow_contracts must validate the shared sccache action",
|
|
)
|
|
return validator(text)
|
|
|
|
def test_checked_in_action_keeps_installation_best_effort(self) -> None:
|
|
action = SCCACHE_SETUP_ACTION.read_text(encoding="utf-8")
|
|
self.assertEqual(self.validate(action), [])
|
|
|
|
def test_compliant_fixture_passes(self) -> None:
|
|
self.assertEqual(self.validate(self.COMPLIANT_ACTION), [])
|
|
|
|
def test_install_failure_must_be_tolerated(self) -> None:
|
|
sabotaged = self.COMPLIANT_ACTION.replace(
|
|
" continue-on-error: true\n", ""
|
|
)
|
|
errors = self.validate(sabotaged)
|
|
self.assertTrue(any("continue-on-error" in error for error in errors), errors)
|
|
|
|
def test_configuration_must_require_a_successful_install(self) -> None:
|
|
sabotaged = self.COMPLIANT_ACTION.replace(
|
|
" && steps.install_sccache.outcome == 'success'", ""
|
|
)
|
|
errors = self.validate(sabotaged)
|
|
self.assertTrue(
|
|
any("successful installation" in error for error in errors), errors
|
|
)
|
|
|
|
def test_install_failure_must_explain_the_local_fallback(self) -> None:
|
|
sabotaged = self.COMPLIANT_ACTION.replace(
|
|
" if: ${{ steps.install_sccache.outcome == 'failure' }}\n",
|
|
" if: ${{ steps.install_sccache.outcome == 'success' }}\n",
|
|
)
|
|
errors = self.validate(sabotaged)
|
|
self.assertTrue(any("local compilation" in error for error in errors), errors)
|
|
|
|
def test_action_failures_reach_the_top_level_contract(self) -> None:
|
|
original = ws12_workflow_contracts.validate_sccache_setup_action
|
|
sentinel = "sccache action contract sentinel"
|
|
ws12_workflow_contracts.validate_sccache_setup_action = lambda _text: [sentinel]
|
|
self.addCleanup(
|
|
setattr,
|
|
ws12_workflow_contracts,
|
|
"validate_sccache_setup_action",
|
|
original,
|
|
)
|
|
|
|
errors = validate_workflow_texts(load_workflows(ROOT), ROOT)
|
|
self.assertIn(sentinel, errors)
|
|
|
|
|
|
class WorkflowContractSabotageTests(unittest.TestCase):
|
|
def setUp(self) -> None:
|
|
self.workflows = load_workflows(ROOT)
|
|
|
|
def test_checked_in_workflows_cover_every_lane(self) -> None:
|
|
self.assertEqual(validate_workflow_texts(self.workflows), [])
|
|
|
|
def test_removing_each_lane_marker_fails_loudly(self) -> None:
|
|
for path, markers in REQUIRED_MARKERS.items():
|
|
for marker in markers:
|
|
with self.subTest(path=path, marker=marker):
|
|
sabotaged = copy.deepcopy(self.workflows)
|
|
sabotaged[path] = sabotaged[path].replace(marker, "")
|
|
errors = validate_workflow_texts(sabotaged)
|
|
self.assertTrue(
|
|
any(path in error and marker in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_missing_workflow_fails_loudly(self) -> None:
|
|
sabotaged = copy.deepcopy(self.workflows)
|
|
path = next(iter(REQUIRED_MARKERS))
|
|
del sabotaged[path]
|
|
|
|
self.assertIn(f"missing workflow: {path}", validate_workflow_texts(sabotaged))
|
|
|
|
def test_unconditional_skip_fails_loudly(self) -> None:
|
|
path = ".github/workflows/nightly-deep-ci.yml"
|
|
conditions = (
|
|
"if: false",
|
|
"if: false",
|
|
"if: 'false'",
|
|
"if: |\n false",
|
|
)
|
|
for condition in conditions:
|
|
with self.subTest(condition=condition):
|
|
sabotaged = copy.deepcopy(self.workflows)
|
|
sabotaged[path] += f"\n disabled-lane:\n {condition}\n"
|
|
|
|
self.assertTrue(
|
|
any(
|
|
"unconditionally skipped" in error
|
|
for error in validate_workflow_texts(sabotaged)
|
|
)
|
|
)
|
|
|
|
def test_reborn_e2e_scope_filters_pass_as_checked_in(self) -> None:
|
|
self.assertEqual(validate_e2e_scope_filters(self.workflows[E2E_WORKFLOW]), [])
|
|
|
|
def test_flat_tree_scope_regex_fails_loudly(self) -> None:
|
|
"""Re-narrowing the scope regex to `crates/ironclaw_*` must not pass.
|
|
|
|
This is the exact regression the WS10 rewrite exists to prevent: the
|
|
pattern still matches every path in today's flat tree, so nothing looks
|
|
broken until crates move and every E2E job silently stops running.
|
|
"""
|
|
sabotaged = self.workflows[E2E_WORKFLOW].replace(
|
|
"grep -Eq '^(crates/|", "grep -Eq '^(crates/ironclaw_[^/]+/|"
|
|
)
|
|
errors = validate_e2e_scope_filters(sabotaged)
|
|
|
|
self.assertTrue(
|
|
any("substrates/ironclaw_event_log" in error for error in errors), errors
|
|
)
|
|
|
|
def test_flat_tree_paths_glob_fails_loudly(self) -> None:
|
|
sabotaged = self.workflows[E2E_WORKFLOW].replace(
|
|
'- "crates/**"', '- "crates/ironclaw_*/**"'
|
|
)
|
|
errors = validate_e2e_scope_filters(sabotaged)
|
|
|
|
self.assertTrue(any("push `paths:` filter" in error for error in errors), errors)
|
|
|
|
def test_over_broad_scope_regex_fails_loudly(self) -> None:
|
|
"""The filter must stay a filter — matching everything is not a fix."""
|
|
sabotaged = self.workflows[E2E_WORKFLOW].replace(
|
|
"grep -Eq '^(crates/|", "grep -Eq '^(|"
|
|
)
|
|
errors = validate_e2e_scope_filters(sabotaged)
|
|
|
|
self.assertTrue(any("must NOT be in scope" in error for error in errors), errors)
|
|
|
|
def test_scope_regex_survives_an_escaped_newline_continuation(self) -> None:
|
|
"""A guard split across a line continuation is still a guard.
|
|
|
|
Regression for the one-line-only form: it reported this exact workflow
|
|
as having no scope regex at all, failing the build over a formatting
|
|
choice (.claude/rules/review-discipline.md — guardrails must handle
|
|
multiline syntax).
|
|
"""
|
|
wrapped = self.workflows[E2E_WORKFLOW].replace(
|
|
"| grep -Eq '^(crates/|", "| grep -Eq \\\n '^(crates/|"
|
|
)
|
|
self.assertNotEqual(wrapped, self.workflows[E2E_WORKFLOW])
|
|
|
|
self.assertEqual(validate_e2e_scope_filters(wrapped), [])
|
|
|
|
def test_missing_scope_regex_fails_loudly(self) -> None:
|
|
sabotaged = self.workflows[E2E_WORKFLOW].replace("grep -Eq '^(crates/|", "true #")
|
|
errors = validate_e2e_scope_filters(sabotaged)
|
|
|
|
self.assertTrue(
|
|
any("could not find the `changes` job scope regex" in e for e in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_production_lint_passes_as_checked_in_without_reading_its_neighbour(
|
|
self,
|
|
) -> None:
|
|
"""Passing here is itself the proof that the scan stays in its step.
|
|
|
|
`Check all-target lints` sits directly below and legitimately passes
|
|
the flags this contract forbids, so a scan that overran its own step
|
|
would fail on the checked-in workflow. The first assertion keeps that
|
|
proof honest: if the neighbour ever stops passing `--tests`, this test
|
|
would still pass while having stopped testing anything.
|
|
"""
|
|
neighbour = step_body(
|
|
self.workflows[CODE_STYLE_WORKFLOW], "Check all-target lints"
|
|
)
|
|
self.assertIsNotNone(neighbour)
|
|
self.assertIn("--tests", neighbour or "")
|
|
|
|
self.assertEqual(
|
|
validate_production_lint_targets(self.workflows[CODE_STYLE_WORKFLOW]), []
|
|
)
|
|
|
|
def test_windows_webui_install_step_requires_bash(self) -> None:
|
|
workflow = self.workflows[CODE_STYLE_WORKFLOW]
|
|
windows_job = job_body(workflow, "clippy-windows")
|
|
self.assertIsNotNone(windows_job)
|
|
self.assertEqual(validate_windows_webui_install_shell(workflow), [])
|
|
|
|
sabotaged_job = (windows_job or "").replace(" shell: bash\n", "")
|
|
self.assertNotEqual(sabotaged_job, windows_job)
|
|
sabotaged = dict(self.workflows)
|
|
sabotaged[CODE_STYLE_WORKFLOW] = workflow.replace(
|
|
windows_job or "", sabotaged_job, 1
|
|
)
|
|
|
|
errors = validate_workflow_texts(sabotaged)
|
|
self.assertTrue(any("must run" in error for error in errors), errors)
|
|
|
|
def test_target_filters_on_the_production_lint_fail_loudly(self) -> None:
|
|
"""Every explicit target selector, in bare and value-bearing form.
|
|
|
|
Regression for PR #6965: the lane ran `cargo clippy -p <pkg> --lib
|
|
--bins`, and the first PR whose only changed package was the bin-only
|
|
`ironclaw` died on `no library targets found in package` — exit 101,
|
|
no lint ever run. `--bins` alone is the quieter half of the same bug:
|
|
on a lib-only package cargo warns "no targets matched; this is a
|
|
no-op" and the lane reports green having linted nothing. The rest swap
|
|
the package's default production targets for a hand-picked set.
|
|
|
|
Exact-count, so `--bins` is never also reported as `--bin` — a
|
|
substring match would do exactly that.
|
|
"""
|
|
for injected, flag in (
|
|
("--lib", "--lib"),
|
|
("--bins", "--bins"),
|
|
("--bin ironclaw", "--bin"),
|
|
("--all-targets", "--all-targets"),
|
|
("--tests", "--tests"),
|
|
("--test smoke", "--test"),
|
|
("--examples", "--examples"),
|
|
("--example demo", "--example"),
|
|
("--benches", "--benches"),
|
|
("--bench=throughput", "--bench"),
|
|
):
|
|
with self.subTest(injected=injected):
|
|
sabotaged = self.workflows[CODE_STYLE_WORKFLOW].replace(
|
|
'cargo clippy "${package_args[@]}" \\',
|
|
f'cargo clippy "${{package_args[@]}}" {injected} \\',
|
|
)
|
|
self.assertNotEqual(sabotaged, self.workflows[CODE_STYLE_WORKFLOW])
|
|
|
|
errors = validate_production_lint_targets(sabotaged)
|
|
self.assertEqual(len(errors), 1, errors)
|
|
self.assertIn(f"must not pass {flag} ", errors[0])
|
|
|
|
def test_widening_the_clippy_matrix_flags_fails_loudly(self) -> None:
|
|
"""`${{ matrix.flags }}` is the lane's other flag channel."""
|
|
sabotaged = self.workflows[CODE_STYLE_WORKFLOW].replace(
|
|
'"flags":"--all-features"', '"flags":"--all-features --all-targets"'
|
|
)
|
|
self.assertNotEqual(sabotaged, self.workflows[CODE_STYLE_WORKFLOW])
|
|
|
|
errors = validate_production_lint_targets(sabotaged)
|
|
self.assertTrue(
|
|
any("clippy_matrix flags" in e and "--all-targets" in e for e in errors),
|
|
errors,
|
|
)
|
|
|
|
# …and only that matrix. Some other matrix in this workflow may
|
|
# legitimately carry `--tests`; reading it as widening *this* lane
|
|
# would be a false failure on an unrelated change.
|
|
unrelated = self.workflows[CODE_STYLE_WORKFLOW] + (
|
|
'\n echo \'doc_matrix=[{"name":"docs","flags":"--tests"}]\''
|
|
' >> "$GITHUB_OUTPUT"\n'
|
|
)
|
|
self.assertEqual(validate_production_lint_targets(unrelated), [])
|
|
|
|
def test_losing_the_step_or_its_command_fails_loudly(self) -> None:
|
|
"""A contract that cannot see the command must say so, not pass."""
|
|
renamed = self.workflows[CODE_STYLE_WORKFLOW].replace(
|
|
"name: Check production-target lints", "name: Check nothing at all"
|
|
)
|
|
self.assertTrue(
|
|
any(
|
|
"could not find the 'Check production-target lints' step" in e
|
|
for e in validate_production_lint_targets(renamed)
|
|
)
|
|
)
|
|
|
|
moved = self.workflows[CODE_STYLE_WORKFLOW].replace(
|
|
'cargo clippy "${package_args[@]}" \\\n ${{ matrix.flags }} -- -D warnings',
|
|
"bash scripts/ci/production-clippy.sh",
|
|
)
|
|
self.assertNotEqual(moved, self.workflows[CODE_STYLE_WORKFLOW])
|
|
self.assertTrue(
|
|
any(
|
|
"no longer runs `cargo clippy`" in e
|
|
for e in validate_production_lint_targets(moved)
|
|
)
|
|
)
|
|
|
|
def test_masking_the_production_lint_exit_status_fails_loudly(self) -> None:
|
|
"""A lane that runs clippy and ignores it is the silent-green case.
|
|
|
|
Distinct from a disguised command, which this contract deliberately
|
|
does not chase: each of these is a plausible edit made on purpose and
|
|
for a stated reason — unblock the queue, quiet a flaky lane — and each
|
|
leaves the lint running and its verdict discarded.
|
|
"""
|
|
for mask, injected in (
|
|
("|| true", '${{ matrix.flags }} -- -D warnings || true'),
|
|
("|| :", '${{ matrix.flags }} -- -D warnings || :'),
|
|
("set +e", 'set +e\n ${{ matrix.flags }} -- -D warnings'),
|
|
):
|
|
with self.subTest(mask=mask):
|
|
sabotaged = self.workflows[CODE_STYLE_WORKFLOW].replace(
|
|
"${{ matrix.flags }} -- -D warnings", injected
|
|
)
|
|
self.assertNotEqual(sabotaged, self.workflows[CODE_STYLE_WORKFLOW])
|
|
|
|
errors = validate_production_lint_targets(sabotaged)
|
|
self.assertTrue(
|
|
any(f"must not mask the lint's exit status with `{mask}`" in e
|
|
for e in errors),
|
|
errors,
|
|
)
|
|
|
|
# The YAML-level equivalent: the command runs, fails, and the job
|
|
# reports success anyway.
|
|
tolerated = self.workflows[CODE_STYLE_WORKFLOW].replace(
|
|
" - name: Check production-target lints\n",
|
|
" - name: Check production-target lints\n continue-on-error: true\n",
|
|
)
|
|
self.assertNotEqual(tolerated, self.workflows[CODE_STYLE_WORKFLOW])
|
|
self.assertTrue(
|
|
any(
|
|
"continue-on-error" in e
|
|
for e in validate_production_lint_targets(tolerated)
|
|
),
|
|
validate_production_lint_targets(tolerated),
|
|
)
|
|
|
|
def test_production_lint_failures_reach_the_top_level_contract(self) -> None:
|
|
"""The validator must stay wired into `validate_workflow_texts`.
|
|
|
|
Every assertion above calls it directly, so without this the guard
|
|
could be unhooked from the entry point CI runs and stay green.
|
|
"""
|
|
sabotaged = dict(self.workflows)
|
|
sabotaged[CODE_STYLE_WORKFLOW] = self.workflows[CODE_STYLE_WORKFLOW].replace(
|
|
'cargo clippy "${package_args[@]}" \\',
|
|
'cargo clippy "${package_args[@]}" --lib \\',
|
|
)
|
|
self.assertNotEqual(
|
|
sabotaged[CODE_STYLE_WORKFLOW], self.workflows[CODE_STYLE_WORKFLOW]
|
|
)
|
|
|
|
errors = validate_workflow_texts(sabotaged)
|
|
self.assertTrue(any("must not pass --lib" in e for e in errors), errors)
|
|
|
|
def test_code_style_runs_workflow_and_shard_sabotage_tests(self) -> None:
|
|
workflow = (ROOT / ".github/workflows/code_style.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
self.assertIn("python3 scripts/ci/test_ws12_suite_shards.py", workflow)
|
|
self.assertIn("python3 scripts/ci/test_ws12_workflow_contracts.py", workflow)
|
|
|
|
def test_code_style_fast_checks_share_one_runner_without_losing_gates(self) -> None:
|
|
workflow = (ROOT / ".github/workflows/code_style.yml").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
|
|
self.assertIn(" fast-checks:", workflow)
|
|
for marker in (
|
|
"cargo fmt --all -- --check",
|
|
"EmbarkStudios/cargo-deny-action@",
|
|
"git ls-files -ci --exclude-standard",
|
|
"scripts/ci/check-include-str-paths.sh",
|
|
"scripts/ci/check-hermetic-env.sh",
|
|
"scripts/check_no_panics.py --reborn-baseline",
|
|
"scripts/ci/check-composition-budget.sh",
|
|
):
|
|
with self.subTest(marker=marker):
|
|
self.assertIn(marker, workflow)
|
|
|
|
for retired_job in (
|
|
" clippy-matrix:",
|
|
" format:",
|
|
" deny-check:",
|
|
" tracked-ignored-files:",
|
|
" static-checks:",
|
|
" no-panics:",
|
|
" composition-budget:",
|
|
):
|
|
with self.subTest(retired_job=retired_job):
|
|
self.assertNotIn(retired_job, workflow)
|
|
|
|
|
|
class CrateScopeFilterSabotageTests(unittest.TestCase):
|
|
"""#6963: the crate-keyed workflow scope filters, plus has_docs.
|
|
|
|
The three crate-keyed filters go silently green under family nesting —
|
|
the dist-build lane skips, every WASM ABI check skips, the stress
|
|
workflow stops triggering. The fourth pin, `has_docs`, is deliberately
|
|
not crate-keyed: docs/ sits outside the has_code scope, so its grep is
|
|
the only trigger for the docs publication-boundary gate and narrowing it
|
|
skips that gate with nothing red anywhere. None of these filters can
|
|
assert anything about itself. These are the sabotage cases that prove
|
|
each pin binds.
|
|
"""
|
|
|
|
def setUp(self) -> None:
|
|
self.workflows = load_workflows(ROOT)
|
|
|
|
def sabotage(self, workflow: str, old: str, new: str) -> dict[str, str]:
|
|
mutated = copy.deepcopy(self.workflows)
|
|
replaced = mutated[workflow].replace(old, new)
|
|
self.assertNotEqual(replaced, mutated[workflow], f"no-op sabotage: {old!r}")
|
|
mutated[workflow] = replaced
|
|
return mutated
|
|
|
|
def test_checked_in_scope_filters_pass(self) -> None:
|
|
self.assertEqual(validate_crate_scope_filters(self.workflows, ROOT), [])
|
|
|
|
def test_has_code_covers_the_toolchain_pin_and_its_own_guard(self) -> None:
|
|
"""validate_toolchain_pin_sync() only runs inside fast-checks, gated
|
|
on has_code || has_guidance. A PR touching only rust-toolchain.toml or
|
|
the setup-rust composite — exactly the two files the sync guard
|
|
exists to police — must still set has_code=true, or the guard never
|
|
fires for its own governed diff."""
|
|
has_code = next(f for f in CRATE_SCOPE_FILTERS if f.name == "has_code")
|
|
self.assertIn("rust-toolchain.toml", has_code.in_scope)
|
|
self.assertIn(".github/actions/setup-rust/action.yml", has_code.in_scope)
|
|
|
|
def test_has_docs_trigger_is_pinned(self) -> None:
|
|
"""The docs publication gate rides its own trigger: docs-only PRs have
|
|
has_code=false, so nothing else would run the boundary job. An
|
|
unpinned grep here is the same silent-skip class as the crate
|
|
filters."""
|
|
self.assertIn("has_docs", {scope.name for scope in CRATE_SCOPE_FILTERS})
|
|
|
|
def test_narrowing_the_has_docs_trigger_fails_loudly(self) -> None:
|
|
sabotaged = self.sabotage(CODE_STYLE_WORKFLOW, "'^(docs/|", "'^(")
|
|
errors = validate_crate_scope_filters(sabotaged, ROOT)
|
|
self.assertTrue(
|
|
any("has_docs" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_code_style_docs_gate_markers_are_pinned(self) -> None:
|
|
"""Losing the boundary job, either of its two script steps, or the
|
|
roll-up guard silently unhooks the gate for docs-only PRs. (Guard
|
|
ORDERING relative to the has_code early exit is a separate pin —
|
|
presence markers cannot see order.)"""
|
|
markers = REQUIRED_MARKERS.get(CODE_STYLE_WORKFLOW, ())
|
|
for marker in (
|
|
"docs-publication-boundary:",
|
|
"python3 scripts/ci/test_docs_publication_boundary.py",
|
|
"python3 scripts/ci/docs_publication_boundary.py",
|
|
'"${{ needs.docs-publication-boundary.result }}" != "success"',
|
|
):
|
|
with self.subTest(marker=marker):
|
|
self.assertIn(marker, markers)
|
|
|
|
def test_moving_the_docs_guard_below_the_early_exit_fails_loudly(self) -> None:
|
|
"""Marker presence cannot see order: relocating the docs-gate guard
|
|
to after the has_code `exit 0` keeps every REQUIRED_MARKERS string in
|
|
the file while docs-only PRs (has_code=false) exit before the guard
|
|
ever runs — a clean pass on a fully broken gate. The ordering pin
|
|
must catch exactly this move."""
|
|
text = self.workflows[CODE_STYLE_WORKFLOW]
|
|
guard = ws12_workflow_contracts.CODE_STYLE_DOCS_GUARD_MARKER
|
|
early_exit = ws12_workflow_contracts.CODE_STYLE_HAS_CODE_EXIT_MARKER
|
|
self.assertIn(guard, text)
|
|
self.assertIn(early_exit, text)
|
|
|
|
# Relocate the guard's marker line to just after the early-exit line.
|
|
guard_line = next(
|
|
line for line in text.splitlines() if guard in line
|
|
)
|
|
exit_line = next(line for line in text.splitlines() if early_exit in line)
|
|
sabotaged = copy.deepcopy(self.workflows)
|
|
sabotaged[CODE_STYLE_WORKFLOW] = text.replace(
|
|
guard_line + "\n", ""
|
|
).replace(exit_line, exit_line + "\n" + guard_line)
|
|
self.assertIn(guard, sabotaged[CODE_STYLE_WORKFLOW])
|
|
|
|
errors = validate_workflow_texts(sabotaged, ROOT)
|
|
self.assertTrue(
|
|
any("docs" in error and "early exit" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_a_commented_decoy_guard_does_not_satisfy_the_order_pin(self) -> None:
|
|
"""A raw substring search accepts inert text: commenting out the old
|
|
guard block above the early exit while the executable guard sits
|
|
below it (a realistic refactor leftover) must still fail — only
|
|
executable occurrences count."""
|
|
text = self.workflows[CODE_STYLE_WORKFLOW]
|
|
guard = ws12_workflow_contracts.CODE_STYLE_DOCS_GUARD_MARKER
|
|
early_exit = ws12_workflow_contracts.CODE_STYLE_HAS_CODE_EXIT_MARKER
|
|
guard_line = next(line for line in text.splitlines() if guard in line)
|
|
exit_line = next(line for line in text.splitlines() if early_exit in line)
|
|
decoy = " # old guard: " + guard_line.strip()
|
|
sabotaged = copy.deepcopy(self.workflows)
|
|
sabotaged[CODE_STYLE_WORKFLOW] = (
|
|
text.replace(guard_line + "\n", "")
|
|
.replace(exit_line, decoy + "\n" + exit_line + "\n" + guard_line)
|
|
)
|
|
errors = validate_workflow_texts(sabotaged, ROOT)
|
|
self.assertTrue(
|
|
any("docs" in error and "early exit" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_checked_in_docs_guard_order_passes(self) -> None:
|
|
self.assertEqual(
|
|
ws12_workflow_contracts.validate_code_style_docs_guard_order(
|
|
self.workflows[CODE_STYLE_WORKFLOW]
|
|
),
|
|
[],
|
|
)
|
|
|
|
def test_every_filter_declares_probes(self) -> None:
|
|
"""A pin with no probes asserts nothing — the defect being fixed."""
|
|
for scope in CRATE_SCOPE_FILTERS:
|
|
with self.subTest(filter=scope.name):
|
|
self.assertTrue(
|
|
scope.crates or scope.crate_globs or scope.in_scope,
|
|
"filter declares no in-scope probe",
|
|
)
|
|
self.assertTrue(scope.out_of_scope, "filter declares no negative probe")
|
|
|
|
def test_renarrowing_each_filter_to_the_flat_tree_fails_loudly(self) -> None:
|
|
"""The exact regression this rewrite exists to prevent.
|
|
|
|
`crates/<name>/` still matches every path in today's flat tree, so
|
|
nothing looks broken until crates move and the lane silently stops.
|
|
"""
|
|
for workflow, flat, nested_probe in (
|
|
(
|
|
CODE_STYLE_WORKFLOW,
|
|
"crates/([^/]+/)*ironclaw_turn_runner/",
|
|
"crates/substrates/ironclaw_turn_runner/src/lib.rs",
|
|
),
|
|
(
|
|
PLATFORM_WORKFLOW,
|
|
"crates/([^/]+/)*ironclaw_common/",
|
|
"crates/substrates/ironclaw_common/src/lib.rs",
|
|
),
|
|
):
|
|
with self.subTest(workflow=workflow):
|
|
flattened = flat.replace("([^/]+/)*", "")
|
|
errors = validate_crate_scope_filters(
|
|
self.sabotage(workflow, flat, flattened), ROOT
|
|
)
|
|
self.assertTrue(
|
|
any(nested_probe in error for error in errors), errors
|
|
)
|
|
|
|
def test_dropping_the_nested_stress_glob_fails_loudly(self) -> None:
|
|
errors = validate_crate_scope_filters(
|
|
self.sabotage(
|
|
STRESS_WORKFLOW, ' - "crates/*/ironclaw_turns/**"\n', ""
|
|
),
|
|
ROOT,
|
|
)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
"crates/substrates/ironclaw_turns/src/lib.rs" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_a_filter_naming_a_deleted_crate_fails_loudly(self) -> None:
|
|
"""`ironclaw_wasm_product_adapters` sat in the WASM filter for releases
|
|
after the crate was deleted, matching nothing. Naming a crate the
|
|
inventory cannot resolve is now an error rather than dead weight."""
|
|
stale = dataclasses.replace(
|
|
next(f for f in CRATE_SCOPE_FILTERS if f.name == "has_direct_wasm_abi_risk"),
|
|
crates=(("ironclaw_wasm_product_adapters", "src/lib.rs"),),
|
|
)
|
|
workflows = self.sabotage(
|
|
PLATFORM_WORKFLOW,
|
|
"^(crates/([^/]+/)*ironclaw_common/|",
|
|
"^(crates/([^/]+/)*ironclaw_wasm_product_adapters/"
|
|
"|crates/([^/]+/)*ironclaw_common/|",
|
|
)
|
|
with self.patched_filters((stale,)):
|
|
errors = validate_crate_scope_filters(workflows, ROOT)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
"ironclaw_wasm_product_adapters" in error
|
|
and "cannot resolve" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_dropping_a_governed_crate_name_fails_loudly(self) -> None:
|
|
errors = validate_crate_scope_filters(
|
|
self.sabotage(
|
|
CODE_STYLE_WORKFLOW, "crates/([^/]+/)*ironclaw_config/|", ""
|
|
),
|
|
ROOT,
|
|
)
|
|
|
|
self.assertTrue(
|
|
any("ironclaw_config" in error for error in errors), errors
|
|
)
|
|
|
|
def test_over_broadening_a_filter_fails_loudly(self) -> None:
|
|
"""Matching everything is not a fix — the dist-build and stress lanes
|
|
are deliberately scoped and must stay scoped."""
|
|
for workflows in (
|
|
self.sabotage(CODE_STYLE_WORKFLOW, "^(crates/([^/]+/)*ironclaw_turn_runner/", "^(crates/"),
|
|
self.sabotage(
|
|
STRESS_WORKFLOW,
|
|
'- "crates/kernel/ironclaw_turns/**"',
|
|
'- "crates/**"',
|
|
),
|
|
):
|
|
errors = validate_crate_scope_filters(workflows, ROOT)
|
|
self.assertTrue(
|
|
any("must NOT be in scope" in error for error in errors), errors
|
|
)
|
|
|
|
def test_deleting_a_filter_entirely_fails_loudly(self) -> None:
|
|
for workflows, needle in (
|
|
(
|
|
self.sabotage(
|
|
CODE_STYLE_WORKFLOW,
|
|
"grep -Eq '^(crates/([^/]+/)*ironclaw_turn_runner/",
|
|
"true #",
|
|
),
|
|
"expected exactly one scope regex",
|
|
),
|
|
(self.sabotage(STRESS_WORKFLOW, " paths:\n", ""), "no `paths:` trigger filter"),
|
|
):
|
|
errors = validate_crate_scope_filters(workflows, ROOT)
|
|
self.assertTrue(any(needle in error for error in errors), errors)
|
|
|
|
def test_a_second_paths_block_is_an_ambiguity_not_a_silent_pick(self) -> None:
|
|
"""`extract_scope_regex` refuses zero-or-many; the globs half took the
|
|
first `paths:` block unconditionally, so a workflow that grew a second
|
|
filter would have had the wrong one pinned with nothing to say so."""
|
|
text = self.workflows[STRESS_WORKFLOW]
|
|
start = text.index(" paths:\n")
|
|
end = text.index("\n\n", start)
|
|
duplicated = (
|
|
text[:end] + "\n push:\n" + text[start:end] + text[end:]
|
|
)
|
|
errors = validate_crate_scope_filters(
|
|
{**self.workflows, STRESS_WORKFLOW: duplicated}, ROOT
|
|
)
|
|
|
|
self.assertTrue(
|
|
any("found 2 `paths:` trigger filters" in error for error in errors), errors
|
|
)
|
|
|
|
def test_a_discovered_file_probe_that_discovers_nothing_fails_loudly(self) -> None:
|
|
"""The first-party manifest probe is derived from real files. If those
|
|
files move (WS2 colocates extension packages), the probe must fail
|
|
rather than quietly pin the filter against an empty set."""
|
|
moved = dataclasses.replace(
|
|
next(f for f in CRATE_SCOPE_FILTERS if f.name == "has_direct_wasm_abi_risk"),
|
|
crate_globs=(("ironclaw_extension_support", "assets/*/gone.toml"),),
|
|
)
|
|
with self.patched_filters((moved,)):
|
|
errors = validate_crate_scope_filters(self.workflows, ROOT)
|
|
|
|
self.assertTrue(
|
|
any("discovered no files" in error for error in errors), errors
|
|
)
|
|
|
|
def test_a_broken_crate_inventory_fails_loudly(self) -> None:
|
|
"""No crate tree at all must refuse, never pass with zero probes."""
|
|
with tempfile.TemporaryDirectory() as empty:
|
|
errors = validate_crate_scope_filters(self.workflows, Path(empty))
|
|
|
|
self.assertTrue(
|
|
any("crate inventory unavailable" in error for error in errors), errors
|
|
)
|
|
|
|
def test_scope_filter_failures_reach_the_top_level_contract(self) -> None:
|
|
"""The pin is only worth anything if `main()`'s entry point sees it."""
|
|
flattened = self.sabotage(
|
|
CODE_STYLE_WORKFLOW,
|
|
"crates/([^/]+/)*ironclaw_turn_runner/",
|
|
"crates/ironclaw_turn_runner/",
|
|
)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
"crates/substrates/ironclaw_turn_runner" in error
|
|
for error in validate_workflow_texts(flattened, ROOT)
|
|
)
|
|
)
|
|
|
|
def test_github_glob_semantics(self) -> None:
|
|
"""`*` does not cross `/`; `**` does. The stress filter's two-form
|
|
enumeration depends on exactly this."""
|
|
single = github_glob_to_regex("crates/*/ironclaw_turns/**")
|
|
self.assertTrue(single.match("crates/substrates/ironclaw_turns/src/lib.rs"))
|
|
self.assertFalse(single.match("crates/ironclaw_turns/src/lib.rs"))
|
|
self.assertFalse(single.match("crates/a/b/ironclaw_turns/src/lib.rs"))
|
|
|
|
double = github_glob_to_regex("crates/ironclaw_turns/**")
|
|
self.assertTrue(double.match("crates/ironclaw_turns/src/deep/lib.rs"))
|
|
self.assertFalse(double.match("crates/substrates/ironclaw_turns/src/lib.rs"))
|
|
|
|
exact = github_glob_to_regex("Cargo.toml")
|
|
self.assertTrue(exact.match("Cargo.toml"))
|
|
self.assertFalse(exact.match("crates/x/Cargo.toml"))
|
|
self.assertFalse(exact.match("CargoXtoml"))
|
|
|
|
def patched_filters(self, filters: tuple[object, ...]):
|
|
test = self
|
|
|
|
class _Patch:
|
|
def __enter__(self) -> None:
|
|
self.saved = ws12_workflow_contracts.CRATE_SCOPE_FILTERS
|
|
ws12_workflow_contracts.CRATE_SCOPE_FILTERS = filters
|
|
|
|
def __exit__(self, *_: object) -> None:
|
|
ws12_workflow_contracts.CRATE_SCOPE_FILTERS = self.saved
|
|
|
|
test.addCleanup(
|
|
setattr,
|
|
ws12_workflow_contracts,
|
|
"CRATE_SCOPE_FILTERS",
|
|
ws12_workflow_contracts.CRATE_SCOPE_FILTERS,
|
|
)
|
|
return _Patch()
|
|
|
|
|
|
class WebuiFrontendSiteSabotageTests(unittest.TestCase):
|
|
"""#7155 WS10: the 28 `crates/ironclaw_webui/frontend` sites.
|
|
|
|
`cache-dependency-path` is a static YAML value, so its fix twins the flat
|
|
lockfile line with a nested wildcard sibling; every `cd` and
|
|
`working-directory:` site resolves dynamically through
|
|
scripts/ci/crate-dir.sh and must carry no literal trace of the flat path.
|
|
These are the sabotage cases that prove the pin catches both regressions.
|
|
"""
|
|
|
|
def setUp(self) -> None:
|
|
self.workflows = load_workflows(ROOT)
|
|
# Resolved dynamically, never hardcoded as `crates/ironclaw_webui`:
|
|
# this repo's crate tree is mid-restructure and the concurrent WS10
|
|
# physical-move work observably flips `ironclaw_webui` between its
|
|
# flat and family-nested location while this suite runs. A sabotage
|
|
# string built from a stale assumption about the current location
|
|
# would stop matching real workflow text the moment the tree moves
|
|
# again — exactly the fragility this whole pin exists to eliminate.
|
|
self.webui_dir = crate_directory(WEBUI_FRONTEND_CRATE, ROOT)
|
|
|
|
def sabotage(self, workflow: str, old: str, new: str, count: int = -1) -> dict[str, str]:
|
|
mutated = copy.deepcopy(self.workflows)
|
|
replaced = mutated[workflow].replace(old, new, count) if count >= 0 else mutated[workflow].replace(old, new)
|
|
self.assertNotEqual(replaced, mutated[workflow], f"no-op sabotage: {old!r}")
|
|
mutated[workflow] = replaced
|
|
return mutated
|
|
|
|
def test_checked_in_webui_sites_pass(self) -> None:
|
|
self.assertEqual(validate_webui_frontend_sites(self.workflows, ROOT), [])
|
|
|
|
def test_every_site_was_actually_converted(self) -> None:
|
|
"""Sanity floor: the checked-in tree must contain the expected number
|
|
of sanctioned cache-dependency-path pairings (12) — a pin that passes
|
|
vacuously because nobody scanned anything is the defect being fixed."""
|
|
flat_lockfile = f"{self.webui_dir}/frontend/pnpm-lock.yaml"
|
|
pairs = 0
|
|
for text in self.workflows.values():
|
|
lines = text.splitlines()
|
|
for index, line in enumerate(lines):
|
|
if line.strip() != flat_lockfile:
|
|
continue
|
|
following = next(
|
|
(c.strip() for c in lines[index + 1 :] if c.strip()), ""
|
|
)
|
|
if following == WEBUI_NESTED_LOCKFILE_PATTERN:
|
|
pairs += 1
|
|
self.assertEqual(pairs, 12, "expected exactly 12 cache-dependency-path sites")
|
|
|
|
def test_reintroducing_a_bare_cd_site_fails_loudly(self) -> None:
|
|
"""The exact pre-#7155 regression: a `cd` back to the flat literal."""
|
|
sabotaged = self.sabotage(
|
|
".github/workflows/coverage.yml",
|
|
'set -euo pipefail\n webui_dir="$(bash scripts/ci/crate-dir.sh '
|
|
'ironclaw_webui)"\n cd "${webui_dir}/frontend"',
|
|
f"cd {self.webui_dir}/frontend",
|
|
count=1,
|
|
)
|
|
errors = validate_webui_frontend_sites(sabotaged, ROOT)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
"coverage.yml" in error and "hardcodes" in error and "frontend'" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_reintroducing_a_bare_working_directory_site_fails_loudly(self) -> None:
|
|
sabotaged = self.sabotage(
|
|
CODE_STYLE_WORKFLOW,
|
|
"working-directory: ${{ env.WEBUI_FRONTEND_DIR }}",
|
|
f"working-directory: {self.webui_dir}/frontend",
|
|
count=1,
|
|
)
|
|
errors = validate_webui_frontend_sites(sabotaged, ROOT)
|
|
|
|
self.assertTrue(
|
|
any(CODE_STYLE_WORKFLOW in error and "hardcodes" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_dropping_the_nested_cache_glob_sibling_fails_loudly(self) -> None:
|
|
sabotaged = self.sabotage(
|
|
".github/workflows/reborn-playwright.yml",
|
|
f" {self.webui_dir}/frontend/pnpm-lock.yaml\n"
|
|
f" {WEBUI_NESTED_LOCKFILE_PATTERN}\n",
|
|
f" {self.webui_dir}/frontend/pnpm-lock.yaml\n",
|
|
)
|
|
errors = validate_webui_frontend_sites(sabotaged, ROOT)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
"reborn-playwright.yml" in error and "not twinned" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_webui_crate_unresolvable_fails_loudly(self) -> None:
|
|
"""The exact `ironclaw_webui` name must keep resolving — if renamed or
|
|
deleted from the inventory, this must refuse rather than pass with
|
|
nothing measured."""
|
|
with tempfile.TemporaryDirectory() as empty:
|
|
errors = validate_webui_frontend_sites(self.workflows, Path(empty))
|
|
|
|
self.assertTrue(
|
|
any(
|
|
"crate inventory cannot resolve" in error and WEBUI_FRONTEND_CRATE in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_empty_workflow_set_is_an_empty_probe_not_a_pass(self) -> None:
|
|
"""A probe that discovers nothing must fail closed, matching the
|
|
`crate_globs` / `CRATE_SCOPE_FILTERS` fail-closed floor."""
|
|
errors = validate_webui_frontend_sites({}, ROOT)
|
|
|
|
self.assertTrue(
|
|
any("cache-dependency-path probe set is empty" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_flat_lockfile_missing_on_disk_fails_loudly(self) -> None:
|
|
"""The real-file probe: if the lockfile this pin is measured against
|
|
stops existing, the pin must say so rather than silently matching
|
|
nothing (mirrors CRATE_SCOPE_FILTERS' `crate_globs` discovery floor)."""
|
|
with tempfile.TemporaryDirectory() as empty_str:
|
|
empty = Path(empty_str)
|
|
(empty / "crates" / WEBUI_FRONTEND_CRATE).mkdir(parents=True)
|
|
(empty / "crates" / WEBUI_FRONTEND_CRATE / "Cargo.toml").write_text(
|
|
"[package]\nname = \"ironclaw_webui\"\n"
|
|
)
|
|
# crate_directory() refuses under crate_tree.MIN_CRATE_DIRECTORIES
|
|
# (20) real crates — a deliberate fail-closed floor, not something
|
|
# this fixture should route around. Real filler directories, not
|
|
# symlinks: crate_directories() finds manifests via `rglob`, which
|
|
# does not descend into symlinked directories, so a symlink farm
|
|
# would silently under-count and trip the very floor this fixture
|
|
# exists to clear.
|
|
for n in range(25):
|
|
filler = empty / "crates" / f"ironclaw_filler_{n}"
|
|
filler.mkdir(parents=True)
|
|
(filler / "Cargo.toml").write_text(f'[package]\nname = "ironclaw_filler_{n}"\n')
|
|
|
|
errors = validate_webui_frontend_sites(self.workflows, empty)
|
|
|
|
self.assertTrue(
|
|
any("does not exist on disk" in error for error in errors), errors
|
|
)
|
|
|
|
|
|
class CrateNameResidueSabotageTests(unittest.TestCase):
|
|
"""#7155 WS10 B1/B2: docker.yml and nightly-deep-ci.yml resolve their
|
|
governed crate's PATH dynamically now, but still spell the crate NAME as a
|
|
bare token. This is the pin that catches that token going stale — a
|
|
rename or deletion the workflow text never followed.
|
|
"""
|
|
|
|
def setUp(self) -> None:
|
|
self.workflows = load_workflows(ROOT)
|
|
|
|
def sabotage(self, workflow: str, old: str, new: str) -> dict[str, str]:
|
|
mutated = copy.deepcopy(self.workflows)
|
|
replaced = mutated[workflow].replace(old, new)
|
|
self.assertNotEqual(replaced, mutated[workflow], f"no-op sabotage: {old!r}")
|
|
mutated[workflow] = replaced
|
|
return mutated
|
|
|
|
def test_checked_in_residue_passes(self) -> None:
|
|
self.assertEqual(validate_crate_name_residue(self.workflows, ROOT), [])
|
|
|
|
def test_every_governed_workflow_declares_probes(self) -> None:
|
|
self.assertEqual(
|
|
{workflow for workflow, _ in CRATE_NAME_RESIDUE},
|
|
{DOCKER_WORKFLOW, NIGHTLY_DEEP_CI_WORKFLOW},
|
|
)
|
|
|
|
def test_dropping_the_docker_crate_name_fails_loudly(self) -> None:
|
|
sabotaged = self.sabotage(
|
|
DOCKER_WORKFLOW, "ironclaw_cli", "ironclaw_renamed_cli"
|
|
)
|
|
errors = validate_crate_name_residue(sabotaged, ROOT)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
DOCKER_WORKFLOW in error
|
|
and "ironclaw_cli" in error
|
|
and "no longer names" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_dropping_the_nightly_crate_name_fails_loudly(self) -> None:
|
|
sabotaged = self.sabotage(
|
|
NIGHTLY_DEEP_CI_WORKFLOW, "ironclaw_capabilities", "ironclaw_dispatch_only"
|
|
)
|
|
errors = validate_crate_name_residue(sabotaged, ROOT)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
NIGHTLY_DEEP_CI_WORKFLOW in error
|
|
and "ironclaw_capabilities" in error
|
|
and "no longer names" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_a_residue_crate_the_inventory_cannot_resolve_fails_loudly(self) -> None:
|
|
"""A name that still appears as a token but the inventory can no
|
|
longer resolve (renamed elsewhere, deleted) must refuse."""
|
|
stale = ((DOCKER_WORKFLOW, "ironclaw_reborn_cli_renamed"),)
|
|
# Make the workflow text contain the stale name too, so the "no
|
|
# longer names" branch does not fire first and mask this one.
|
|
workflows = self.sabotage(
|
|
DOCKER_WORKFLOW, "ironclaw_cli", "ironclaw_reborn_cli_renamed"
|
|
)
|
|
with self.patched_residue(stale):
|
|
errors = validate_crate_name_residue(workflows, ROOT)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
"ironclaw_reborn_cli_renamed" in error and "cannot resolve" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_missing_workflow_fails_loudly(self) -> None:
|
|
mutated = copy.deepcopy(self.workflows)
|
|
del mutated[NIGHTLY_DEEP_CI_WORKFLOW]
|
|
|
|
errors = validate_crate_name_residue(mutated, ROOT)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
NIGHTLY_DEEP_CI_WORKFLOW in error and "not loaded" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_residue_failures_reach_the_top_level_contract(self) -> None:
|
|
sabotaged = self.sabotage(
|
|
NIGHTLY_DEEP_CI_WORKFLOW, "ironclaw_capabilities", "ironclaw_dispatch_only"
|
|
)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
"no longer names crate 'ironclaw_capabilities'" in error
|
|
for error in validate_workflow_texts(sabotaged, ROOT)
|
|
)
|
|
)
|
|
|
|
def patched_residue(self, filters: tuple[tuple[str, str], ...]):
|
|
test = self
|
|
|
|
class _Patch:
|
|
def __enter__(self) -> None:
|
|
self.saved = ws12_workflow_contracts.CRATE_NAME_RESIDUE
|
|
ws12_workflow_contracts.CRATE_NAME_RESIDUE = filters
|
|
|
|
def __exit__(self, *_: object) -> None:
|
|
ws12_workflow_contracts.CRATE_NAME_RESIDUE = self.saved
|
|
|
|
test.addCleanup(
|
|
setattr,
|
|
ws12_workflow_contracts,
|
|
"CRATE_NAME_RESIDUE",
|
|
ws12_workflow_contracts.CRATE_NAME_RESIDUE,
|
|
)
|
|
return _Patch()
|
|
|
|
|
|
class LibsqlScriptedMemoryJobSabotageTests(unittest.TestCase):
|
|
"""#7360: the libsql-scripted-memory job's lifecycle and artifact contract.
|
|
|
|
REQUIRED_MARKERS pins tokens file-wide, so it cannot tell which job a
|
|
token belongs to; this class pins them inside the ONE job block. The
|
|
checked-in workflow is GREEN under the contract. Sabotages that touch a
|
|
#7360-fixed invariant start from `fixed_workflows()`, which normalizes
|
|
EITHER source shape (the historical RED one: --operations 2, an EXIT
|
|
trap that kills without waiting, the server log embedded in every
|
|
per-script upload, final readiness curls without timeout flags; or the
|
|
checked-in GREEN one) to one canonical compliant fixture, and then break
|
|
exactly one invariant, so each test fails for its own reason rather than
|
|
piggybacking on the checked-in failures. Sabotages of invariants both
|
|
shapes already share mutate the checked-in text directly.
|
|
"""
|
|
|
|
SERVER_LOG_LINE = " target/ironclaw-stress/libsql-scripted-server.log"
|
|
SERVER_LOG_STEP = """\
|
|
- name: Upload libsql scripted server log
|
|
if: always()
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: ironclaw-stress-libsql-scripted-server-log
|
|
path: |
|
|
target/ironclaw-stress/libsql-scripted-server.log
|
|
if-no-files-found: error
|
|
"""
|
|
SERVER_LOG_STEP_NAME = "Upload libsql scripted server log"
|
|
# The canonical trap is single-quoted so the expansion is DELAYED:
|
|
# `$server_pid` is read when the trap fires, not when it is registered.
|
|
KILL_ONLY_TRAP = "trap 'kill \"$server_pid\" 2>/dev/null || true' EXIT"
|
|
CANONICAL_TRAP = (
|
|
"trap 'kill \"$server_pid\" 2>/dev/null || true; "
|
|
"wait \"$server_pid\" 2>/dev/null || true' EXIT"
|
|
)
|
|
FINAL_HEALTH_PROBE = (
|
|
" curl -fsS http://127.0.0.1:18080/api/health >/dev/null"
|
|
)
|
|
FINAL_HEALTH_PROBE_FIXED = (
|
|
" curl -fsS --connect-timeout 5 --max-time 10 "
|
|
"http://127.0.0.1:18080/api/health >/dev/null"
|
|
)
|
|
FINAL_SESSION_PROBE = (
|
|
" curl -fsS \\\n"
|
|
" -H \"Authorization: Bearer $IRONCLAW_REBORN_WEBUI_TOKEN\" \\\n"
|
|
" http://127.0.0.1:18080/api/webchat/v2/session >/dev/null"
|
|
)
|
|
FINAL_SESSION_PROBE_FIXED = (
|
|
" curl -fsS --connect-timeout 5 --max-time 10 \\\n"
|
|
" -H \"Authorization: Bearer $IRONCLAW_REBORN_WEBUI_TOKEN\" \\\n"
|
|
" http://127.0.0.1:18080/api/webchat/v2/session >/dev/null"
|
|
)
|
|
|
|
def setUp(self) -> None:
|
|
self.workflows = load_workflows(ROOT)
|
|
|
|
def sabotage(self, workflow: str, old: str, new: str) -> dict[str, str]:
|
|
mutated = copy.deepcopy(self.workflows)
|
|
replaced = mutated[workflow].replace(old, new)
|
|
self.assertNotEqual(replaced, mutated[workflow], f"no-op sabotage: {old!r}")
|
|
mutated[workflow] = replaced
|
|
return mutated
|
|
|
|
def errors_for(self, workflows: dict[str, str]) -> list[str]:
|
|
return validate_libsql_scripted_memory_job(workflows[STRESS_WORKFLOW])
|
|
|
|
def _fixed_workflow_text(self, text: str) -> str:
|
|
"""One workflow's text with the #7360 contract fixes applied.
|
|
|
|
Accepts EITHER source shape — the original RED one (--operations 2,
|
|
a kill-only EXIT trap, the server log embedded in all three
|
|
per-script uploads) or the checked-in GREEN one (--operations 4,
|
|
kill+wait trap, its own log step) — and normalizes it to one
|
|
canonical compliant shape. Every transform is conditional, so
|
|
re-applying it to an already-fixed text is a no-op (idempotent), and
|
|
an unexpected shape fails clearly instead of silently passing.
|
|
"""
|
|
block, detail = extract_job_block(text, LIBSQL_SCRIPTED_MEMORY_JOB)
|
|
self.assertIsNotNone(block, detail)
|
|
fixed = block
|
|
|
|
if "--operations 2 \\" in fixed:
|
|
fixed = fixed.replace("--operations 2 \\", "--operations 4 \\")
|
|
|
|
if self.KILL_ONLY_TRAP in fixed:
|
|
fixed = fixed.replace(self.KILL_ONLY_TRAP, self.CANONICAL_TRAP)
|
|
elif self.CANONICAL_TRAP not in fixed:
|
|
self.fail(
|
|
"unexpected EXIT trap shape: expected either the kill-only "
|
|
f"RED form or the canonical {self.CANONICAL_TRAP!r}"
|
|
)
|
|
|
|
embedded = fixed.count(self.SERVER_LOG_LINE + "\n")
|
|
if embedded == 3:
|
|
fixed = fixed.replace(self.SERVER_LOG_LINE + "\n", "")
|
|
fixed = fixed.rstrip("\n") + "\n\n" + self.SERVER_LOG_STEP
|
|
elif embedded == 1:
|
|
heading = next(
|
|
(
|
|
match
|
|
for match in STEP_HEADING.finditer(fixed)
|
|
if match.group("name").strip() == self.SERVER_LOG_STEP_NAME
|
|
),
|
|
None,
|
|
)
|
|
if heading is None:
|
|
self.fail(
|
|
"checked-in GREEN shape must contain the server-log "
|
|
"upload step"
|
|
)
|
|
following = STEP_HEADING.search(fixed, heading.end())
|
|
end = following.start() if following else len(fixed)
|
|
fixed = fixed[: heading.start()] + self.SERVER_LOG_STEP + fixed[end:]
|
|
else:
|
|
self.fail(
|
|
f"unexpected server-log upload shape: {embedded} occurrences "
|
|
f"of {self.SERVER_LOG_LINE!r} (expected 3 embedded in the "
|
|
"per-script uploads or 1 in its own step)"
|
|
)
|
|
|
|
if self.FINAL_HEALTH_PROBE in fixed:
|
|
fixed = fixed.replace(self.FINAL_HEALTH_PROBE, self.FINAL_HEALTH_PROBE_FIXED)
|
|
if self.FINAL_SESSION_PROBE in fixed:
|
|
fixed = fixed.replace(
|
|
self.FINAL_SESSION_PROBE, self.FINAL_SESSION_PROBE_FIXED
|
|
)
|
|
|
|
self.assertIn(self.SERVER_LOG_STEP.rstrip("\n"), fixed)
|
|
start = text.find(block)
|
|
self.assertNotEqual(start, -1, "extracted job block must be in the text")
|
|
return text[:start] + fixed + text[start + len(block) :]
|
|
|
|
def fixed_workflows(self) -> dict[str, str]:
|
|
"""Checked-in text plus the #7360 contract fixes (see
|
|
`_fixed_workflow_text`). Every sabotage starts from this fixture so
|
|
it fails for exactly the invariant it breaks."""
|
|
mutated = copy.deepcopy(self.workflows)
|
|
mutated[STRESS_WORKFLOW] = self._fixed_workflow_text(
|
|
self.workflows[STRESS_WORKFLOW]
|
|
)
|
|
return mutated
|
|
|
|
def red_shape_text(self) -> str:
|
|
"""The original RED job shape, reconstructed from the checked-in text
|
|
by undoing the #7360 fixes: --operations 2, a kill-only EXIT trap,
|
|
the server log embedded in all three per-script uploads (no separate
|
|
artifact), and final probes without timeout flags."""
|
|
text = self.workflows[STRESS_WORKFLOW]
|
|
block, detail = extract_job_block(text, LIBSQL_SCRIPTED_MEMORY_JOB)
|
|
self.assertIsNotNone(block, detail)
|
|
red = block
|
|
red = red.replace("--operations 4 \\", "--operations 2 \\")
|
|
red = red.replace(self.CANONICAL_TRAP, self.KILL_ONLY_TRAP)
|
|
red = red.replace(self.FINAL_HEALTH_PROBE_FIXED, self.FINAL_HEALTH_PROBE)
|
|
red = red.replace(self.FINAL_SESSION_PROBE_FIXED, self.FINAL_SESSION_PROBE)
|
|
heading = next(
|
|
match
|
|
for match in STEP_HEADING.finditer(red)
|
|
if match.group("name").strip() == self.SERVER_LOG_STEP_NAME
|
|
)
|
|
following = STEP_HEADING.search(red, heading.end())
|
|
end = following.start() if following else len(red)
|
|
red = red[: heading.start()] + red[end:]
|
|
red = red.rstrip("\n") + "\n"
|
|
for script in ("memory_roundtrip", "memory_grow", "memory_mixed"):
|
|
red = red.replace(
|
|
f"ironclaw-stress-libsql-scripted-{script}/report.txt\n",
|
|
f"ironclaw-stress-libsql-scripted-{script}/report.txt\n"
|
|
+ self.SERVER_LOG_LINE
|
|
+ "\n",
|
|
)
|
|
start = text.find(block)
|
|
self.assertNotEqual(start, -1, "extracted job block must be in the text")
|
|
return text[:start] + red + text[start + len(block) :]
|
|
|
|
def test_checked_in_libsql_job_meets_the_contract(self) -> None:
|
|
"""The checked-in workflow satisfies the full scoped contract —
|
|
lifecycle, exact runner flags, and artifact upload policy."""
|
|
self.assertEqual(self.errors_for(self.workflows), [])
|
|
|
|
def test_fixed_workflow_meets_the_contract(self) -> None:
|
|
"""Guard the fixture: if the fixed shape stopped satisfying the
|
|
checker, every sabotage below would fail on fixture construction
|
|
rather than on the invariant it breaks."""
|
|
self.assertEqual(self.errors_for(self.fixed_workflows()), [])
|
|
|
|
def test_fixture_is_idempotent_across_red_and_green_shapes(self) -> None:
|
|
"""`_fixed_workflow_text` must normalize EITHER source shape — the
|
|
original RED job or the checked-in GREEN one — to one canonical
|
|
compliant fixture, and re-applying it must be a no-op. The RED shape
|
|
must stay RED before normalization and pass after it."""
|
|
green_text = self._fixed_workflow_text(self.workflows[STRESS_WORKFLOW])
|
|
self.assertEqual(self._fixed_workflow_text(green_text), green_text)
|
|
green = copy.deepcopy(self.workflows)
|
|
green[STRESS_WORKFLOW] = green_text
|
|
self.assertEqual(self.errors_for(green), [])
|
|
self.assertIn("--operations 4 \\", green_text)
|
|
self.assertIn(self.CANONICAL_TRAP, green_text)
|
|
self.assertIn(self.SERVER_LOG_STEP.rstrip("\n"), green_text)
|
|
self.assertIn(self.FINAL_HEALTH_PROBE_FIXED, green_text)
|
|
self.assertIn(self.FINAL_SESSION_PROBE_FIXED, green_text)
|
|
|
|
red_text = self.red_shape_text()
|
|
self.assertNotEqual(red_text, self.workflows[STRESS_WORKFLOW])
|
|
red = copy.deepcopy(self.workflows)
|
|
red[STRESS_WORKFLOW] = red_text
|
|
self.assertNotEqual(self.errors_for(red), [])
|
|
fixed_red = self._fixed_workflow_text(red_text)
|
|
self.assertEqual(self._fixed_workflow_text(fixed_red), fixed_red)
|
|
red_fixed = copy.deepcopy(self.workflows)
|
|
red_fixed[STRESS_WORKFLOW] = fixed_red
|
|
self.assertEqual(self.errors_for(red_fixed), [])
|
|
|
|
def test_operations_count_is_exactly_four(self) -> None:
|
|
"""The fix lands at exactly 4 — fewer under-samples, and 14 / 4.0
|
|
are not a fuzzy '4'. Dropping the flag entirely must fail too."""
|
|
fixed = self.fixed_workflows()
|
|
for old, new in (
|
|
("--operations 4 \\", "--operations 3 \\"),
|
|
("--operations 4 \\", "--operations 14 \\"),
|
|
("--operations 4 \\", "--operations 4.0 \\"),
|
|
):
|
|
with self.subTest(new=new):
|
|
mutated = copy.deepcopy(fixed)
|
|
mutated[STRESS_WORKFLOW] = mutated[STRESS_WORKFLOW].replace(old, new)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any(
|
|
"--operations 4" in error
|
|
and "libsql-scripted-memory" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
dropped = copy.deepcopy(fixed)
|
|
dropped[STRESS_WORKFLOW] = dropped[STRESS_WORKFLOW].replace(
|
|
" --operations 4 \\\n", ""
|
|
)
|
|
errors = self.errors_for(dropped)
|
|
self.assertTrue(any("--operations 4" in error for error in errors), errors)
|
|
|
|
def test_the_script_loop_is_fixed_and_complete(self) -> None:
|
|
"""The loop must enumerate exactly the three scripts in order — a
|
|
parametrized list, a dropped scenario, or a reordered one all
|
|
silently change what the matrix measures."""
|
|
for old, new in (
|
|
(
|
|
"for script in memory_roundtrip memory_grow memory_mixed; do",
|
|
"for script in memory_roundtrip memory_grow; do",
|
|
),
|
|
(
|
|
"for script in memory_roundtrip memory_grow memory_mixed; do",
|
|
"for script in ${scripts}; do",
|
|
),
|
|
(
|
|
"for script in memory_roundtrip memory_grow memory_mixed; do",
|
|
"for script in memory_roundtrip memory_mixed memory_grow; do",
|
|
),
|
|
):
|
|
with self.subTest(new=new):
|
|
mutated = self.sabotage(STRESS_WORKFLOW, old, new)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any("fixed sequential loop" in error for error in errors), errors
|
|
)
|
|
|
|
def test_the_zero_tolerance_gate_is_pinned(self) -> None:
|
|
"""--max-failure-rate must stay exactly 0 — 0.5 or 1 accepts failed
|
|
or leaked scripted verdicts and the job still reports green."""
|
|
for old, new in (
|
|
("--max-failure-rate 0 \\", "--max-failure-rate 0.5 \\"),
|
|
("--max-failure-rate 0 \\", "--max-failure-rate 1 \\"),
|
|
):
|
|
with self.subTest(new=new):
|
|
mutated = self.sabotage(STRESS_WORKFLOW, old, new)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any("--max-failure-rate 0" in error for error in errors), errors
|
|
)
|
|
|
|
dropped = self.sabotage(
|
|
STRESS_WORKFLOW, " --max-failure-rate 0 \\\n", ""
|
|
)
|
|
errors = self.errors_for(dropped)
|
|
self.assertTrue(any("--max-failure-rate 0" in error for error in errors), errors)
|
|
|
|
def test_runner_flags_cannot_be_satisfied_by_same_job_decoys(self) -> None:
|
|
"""Every matrix pin belongs to the guarded runner command itself.
|
|
Relocating a flag before the loop must fail even when an exact decoy
|
|
remains elsewhere in the same job."""
|
|
text = self.workflows[STRESS_WORKFLOW]
|
|
block, detail = extract_job_block(text, LIBSQL_SCRIPTED_MEMORY_JOB)
|
|
self.assertIsNotNone(block, detail)
|
|
start = text.find(block)
|
|
self.assertNotEqual(start, -1)
|
|
for required, replacement in (
|
|
("--operations 4", "--operations 3"),
|
|
(
|
|
"--api-scripted-doc-sizes 4096,32768,131072,1048576",
|
|
"--api-scripted-doc-sizes 4096,32768",
|
|
),
|
|
("--max-failure-rate 0", "--max-failure-rate 0.5"),
|
|
("--api-hot-writers 2", "--api-hot-writers 3"),
|
|
("--mock-llm-bind 127.0.0.1:19090", "--mock-llm-bind 127.0.0.1:19091"),
|
|
("--api-poll-interval-ms 10000", "--api-poll-interval-ms 2000"),
|
|
("--api-terminal-timeout-ms 120000", "--api-terminal-timeout-ms 60000"),
|
|
("--max-p95-ms 120000", "--max-p95-ms 60000"),
|
|
):
|
|
with self.subTest(required=required):
|
|
loop = ws12_workflow_contracts.LIBSQL_SCRIPTED_LOOP_BODY.search(block)
|
|
self.assertIsNotNone(loop)
|
|
commands = ws12_workflow_contracts.extract_continued_commands(
|
|
loop.group("body"), "target/release/ironclaw_stress"
|
|
)
|
|
self.assertEqual(len(commands), 1)
|
|
runner = commands[0]
|
|
mutated_runner = runner.replace(required, replacement, 1)
|
|
self.assertNotEqual(mutated_runner, runner)
|
|
mutated_block = block.replace(runner, mutated_runner, 1)
|
|
loop_start = mutated_block.find(
|
|
"for script in memory_roundtrip memory_grow memory_mixed; do"
|
|
)
|
|
self.assertNotEqual(loop_start, -1)
|
|
mutated_block = (
|
|
mutated_block[:loop_start]
|
|
+ f" {required} \\\n"
|
|
+ mutated_block[loop_start:]
|
|
)
|
|
mutated = text[:start] + mutated_block + text[start + len(block) :]
|
|
errors = validate_libsql_scripted_memory_job(mutated)
|
|
self.assertTrue(
|
|
any(required in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
|
|
def test_the_four_doc_sizes_are_exact(self) -> None:
|
|
"""Adding a fifth size or changing any value breaks the pinned
|
|
small-to-large latency curve."""
|
|
for old, new in (
|
|
(
|
|
"--api-scripted-doc-sizes 4096,32768,131072,1048576 \\",
|
|
"--api-scripted-doc-sizes 4096,32768,131072,1048576,8388608 \\",
|
|
),
|
|
(
|
|
"--api-scripted-doc-sizes 4096,32768,131072,1048576 \\",
|
|
"--api-scripted-doc-sizes 4096,32768,131072,2097152 \\",
|
|
),
|
|
):
|
|
with self.subTest(new=new):
|
|
mutated = self.sabotage(STRESS_WORKFLOW, old, new)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any("4096,32768,131072,1048576" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_hot_writers_is_exactly_two(self) -> None:
|
|
"""--api-hot-writers must stay exactly 2 — 2.0/20 are not fuzzy
|
|
'2', and a dropped flag is a regression too."""
|
|
for old, new in (
|
|
(" --api-hot-writers 2 \\", " --api-hot-writers 3 \\"),
|
|
(" --api-hot-writers 2 \\", " --api-hot-writers 20 \\"),
|
|
(" --api-hot-writers 2 \\", " --api-hot-writers 2.0 \\"),
|
|
):
|
|
with self.subTest(new=new):
|
|
mutated = self.sabotage(STRESS_WORKFLOW, old, new)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any("--api-hot-writers 2" in error for error in errors), errors
|
|
)
|
|
|
|
dropped = self.sabotage(
|
|
STRESS_WORKFLOW, " --api-hot-writers 2 \\\n", ""
|
|
)
|
|
errors = self.errors_for(dropped)
|
|
self.assertTrue(
|
|
any("--api-hot-writers 2" in error for error in errors), errors
|
|
)
|
|
|
|
def test_mock_llm_bind_is_pinned_to_19090(self) -> None:
|
|
"""The mock sidecar must bind where the server's LLM base_url
|
|
points: 127.0.0.1:19090."""
|
|
for old, new in (
|
|
(
|
|
" --mock-llm-bind 127.0.0.1:19090 \\",
|
|
" --mock-llm-bind 127.0.0.1:19091 \\",
|
|
),
|
|
(
|
|
" --mock-llm-bind 127.0.0.1:19090 \\",
|
|
" --mock-llm-bind 127.0.0.1:18080 \\",
|
|
),
|
|
):
|
|
with self.subTest(new=new):
|
|
mutated = self.sabotage(STRESS_WORKFLOW, old, new)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any(
|
|
"--mock-llm-bind" in error and "19090" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_poll_interval_is_exactly_10000ms(self) -> None:
|
|
"""--api-poll-interval-ms must stay 10000 — the README's 2000ms was
|
|
the regression this pin exists to catch."""
|
|
for old, new in (
|
|
(
|
|
" --api-poll-interval-ms 10000 \\",
|
|
" --api-poll-interval-ms 2000 \\",
|
|
),
|
|
(
|
|
" --api-poll-interval-ms 10000 \\",
|
|
" --api-poll-interval-ms 5000 \\",
|
|
),
|
|
):
|
|
with self.subTest(new=new):
|
|
mutated = self.sabotage(STRESS_WORKFLOW, old, new)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any("--api-poll-interval-ms 10000" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_terminal_timeout_is_exactly_120000ms(self) -> None:
|
|
"""--api-terminal-timeout-ms must stay 120000 — a shorter cap turns
|
|
slow-but-healthy scripted terminals into false failures."""
|
|
for old, new in (
|
|
(
|
|
" --api-terminal-timeout-ms 120000 \\",
|
|
" --api-terminal-timeout-ms 60000 \\",
|
|
),
|
|
(
|
|
" --api-terminal-timeout-ms 120000 \\",
|
|
" --api-terminal-timeout-ms 30000 \\",
|
|
),
|
|
):
|
|
with self.subTest(new=new):
|
|
mutated = self.sabotage(STRESS_WORKFLOW, old, new)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any("--api-terminal-timeout-ms 120000" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_p95_ceiling_is_exactly_120000ms(self) -> None:
|
|
"""--max-p95-ms must stay 120000 — both a tighter and a looser
|
|
ceiling change what the matrix enforces."""
|
|
for old, new in (
|
|
(" --max-p95-ms 120000 \\", " --max-p95-ms 60000 \\"),
|
|
(" --max-p95-ms 120000 \\", " --max-p95-ms 1200000 \\"),
|
|
):
|
|
with self.subTest(new=new):
|
|
mutated = self.sabotage(STRESS_WORKFLOW, old, new)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any("--max-p95-ms 120000" in error for error in errors), errors
|
|
)
|
|
|
|
def test_loop_continues_after_a_failed_invocation(self) -> None:
|
|
"""A failed script must not abort the loop under set -e: the runner
|
|
call's `|| failed=1` tail is what lets the later scripts still run
|
|
and upload their evidence. `failed=0` must initialize the
|
|
accumulator (set -u would abort on an unset variable) and the step
|
|
must `exit "$failed"` after the loop."""
|
|
fixed = self.fixed_workflows()
|
|
|
|
bare = copy.deepcopy(fixed)
|
|
bare[STRESS_WORKFLOW] = bare[STRESS_WORKFLOW].replace(
|
|
'2> "${outdir}/report.txt" || failed=1',
|
|
'2> "${outdir}/report.txt"',
|
|
)
|
|
errors = self.errors_for(bare)
|
|
self.assertTrue(
|
|
any("must not abort the loop" in error for error in errors), errors
|
|
)
|
|
|
|
no_init = copy.deepcopy(fixed)
|
|
no_init[STRESS_WORKFLOW] = no_init[STRESS_WORKFLOW].replace(
|
|
" failed=0\n", ""
|
|
)
|
|
errors = self.errors_for(no_init)
|
|
self.assertTrue(any("failed=0" in error for error in errors), errors)
|
|
|
|
no_exit = copy.deepcopy(fixed)
|
|
no_exit[STRESS_WORKFLOW] = no_exit[STRESS_WORKFLOW].replace(
|
|
' exit "$failed"\n', ""
|
|
)
|
|
errors = self.errors_for(no_exit)
|
|
self.assertTrue(any('exit "$failed"' in error for error in errors), errors)
|
|
|
|
def test_each_script_outdir_is_created_inside_the_loop(self) -> None:
|
|
"""`mkdir -p "${outdir}"` must run per script, before the
|
|
invocation — the per-script upload paths must exist even when the
|
|
invocation fails and the step continues."""
|
|
fixed = self.fixed_workflows()
|
|
no_outdir = copy.deepcopy(fixed)
|
|
no_outdir[STRESS_WORKFLOW] = no_outdir[STRESS_WORKFLOW].replace(
|
|
' mkdir -p "${outdir}"\n', ""
|
|
)
|
|
errors = self.errors_for(no_outdir)
|
|
self.assertTrue(
|
|
any("mkdir -p" in error and "outdir" in error for error in errors), errors
|
|
)
|
|
|
|
def test_relocating_mkdir_breaks_the_loop_structure(self) -> None:
|
|
"""`mkdir -p "${outdir}"` must stay inside the loop, after the
|
|
outdir= assignment and before the guarded invocation. Moved outside
|
|
the loop (before `failed=0`) it no longer runs per script — the
|
|
upload paths are missing exactly when the invocation failed; moved
|
|
before the assignment or after the invocation it runs at the wrong
|
|
moment and the runner writes into a directory that does not exist
|
|
yet (or creates it too late for the redirects)."""
|
|
fixed = self.fixed_workflows()
|
|
|
|
outside = copy.deepcopy(fixed)
|
|
outside[STRESS_WORKFLOW] = outside[STRESS_WORKFLOW].replace(
|
|
" failed=0\n"
|
|
" for script in memory_roundtrip memory_grow memory_mixed; do\n"
|
|
' outdir="target/ironclaw-stress/'
|
|
'ironclaw-stress-libsql-scripted-${script}"\n'
|
|
' mkdir -p "${outdir}"\n',
|
|
' mkdir -p "${outdir}"\n'
|
|
" failed=0\n"
|
|
" for script in memory_roundtrip memory_grow memory_mixed; do\n"
|
|
' outdir="target/ironclaw-stress/'
|
|
'ironclaw-stress-libsql-scripted-${script}"\n',
|
|
)
|
|
errors = self.errors_for(outside)
|
|
self.assertTrue(
|
|
any(
|
|
"mkdir -p" in error and "outdir" in error and "inside the loop" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
before_assign = copy.deepcopy(fixed)
|
|
before_assign[STRESS_WORKFLOW] = before_assign[STRESS_WORKFLOW].replace(
|
|
' outdir="target/ironclaw-stress/'
|
|
'ironclaw-stress-libsql-scripted-${script}"\n'
|
|
' mkdir -p "${outdir}"\n',
|
|
' mkdir -p "${outdir}"\n'
|
|
' outdir="target/ironclaw-stress/'
|
|
'ironclaw-stress-libsql-scripted-${script}"\n',
|
|
)
|
|
errors = self.errors_for(before_assign)
|
|
self.assertTrue(
|
|
any(
|
|
"in order" in error and "mkdir -p" in error and "outdir" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
after_guard = copy.deepcopy(fixed)
|
|
text = after_guard[STRESS_WORKFLOW].replace(
|
|
' outdir="target/ironclaw-stress/'
|
|
'ironclaw-stress-libsql-scripted-${script}"\n'
|
|
' mkdir -p "${outdir}"\n',
|
|
' outdir="target/ironclaw-stress/'
|
|
'ironclaw-stress-libsql-scripted-${script}"\n',
|
|
)
|
|
after_guard[STRESS_WORKFLOW] = text.replace(
|
|
' 2> "${outdir}/report.txt" || failed=1\n'
|
|
" done\n",
|
|
' 2> "${outdir}/report.txt" || failed=1\n'
|
|
' mkdir -p "${outdir}"\n'
|
|
" done\n",
|
|
)
|
|
errors = self.errors_for(after_guard)
|
|
self.assertTrue(
|
|
any(
|
|
"in order" in error and "mkdir -p" in error and "outdir" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_exit_before_done_fails_loudly(self) -> None:
|
|
"""`exit "$failed"` must come after the loop's `done` — moved inside
|
|
the loop it fails the job right after the first failed script,
|
|
skipping the later scripts' evidence uploads."""
|
|
fixed = self.fixed_workflows()
|
|
mutated = copy.deepcopy(fixed)
|
|
mutated[STRESS_WORKFLOW] = mutated[STRESS_WORKFLOW].replace(
|
|
' 2> "${outdir}/report.txt" || failed=1\n'
|
|
" done\n"
|
|
' exit "$failed"\n',
|
|
' 2> "${outdir}/report.txt" || failed=1\n'
|
|
' exit "$failed"\n'
|
|
" done\n",
|
|
)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any(
|
|
'exit "$failed"' in error and "after the loop" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_upload_steps_always_run_and_fail_on_missing_evidence(self) -> None:
|
|
"""Every upload step needs `if: always()` (a failed matrix step must
|
|
still upload the evidence produced before it failed) and
|
|
`if-no-files-found: error` (an upload that silently carries nothing
|
|
hides a lost run behind a green job)."""
|
|
fixed = self.fixed_workflows()
|
|
|
|
no_always = copy.deepcopy(fixed)
|
|
no_always[STRESS_WORKFLOW] = no_always[STRESS_WORKFLOW].replace(
|
|
" - name: Upload libsql scripted memory_roundtrip artifacts\n"
|
|
" if: always()\n",
|
|
" - name: Upload libsql scripted memory_roundtrip artifacts\n",
|
|
)
|
|
errors = self.errors_for(no_always)
|
|
self.assertTrue(
|
|
any(
|
|
"if: always()" in error and "memory-roundtrip" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
no_error_on_missing = copy.deepcopy(fixed)
|
|
no_error_on_missing[STRESS_WORKFLOW] = no_error_on_missing[
|
|
STRESS_WORKFLOW
|
|
].replace(
|
|
"target/ironclaw-stress/ironclaw-stress-libsql-scripted-"
|
|
"memory_roundtrip/report.txt\n"
|
|
" if-no-files-found: error\n",
|
|
"target/ironclaw-stress/ironclaw-stress-libsql-scripted-"
|
|
"memory_roundtrip/report.txt\n"
|
|
" if-no-files-found: ignore\n",
|
|
)
|
|
errors = self.errors_for(no_error_on_missing)
|
|
self.assertTrue(
|
|
any(
|
|
"if-no-files-found: error" in error and "memory-roundtrip" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_per_script_artifacts_carry_exactly_their_three_paths(self) -> None:
|
|
"""Each per-script artifact name must map to exactly its own
|
|
outdir's summary.jsonl, summary.json, and report.txt — a swapped
|
|
path points one script's upload at another script's evidence, and a
|
|
missing path makes that evidence unrecoverable after a failure."""
|
|
fixed = self.fixed_workflows()
|
|
|
|
swapped = copy.deepcopy(fixed)
|
|
swapped[STRESS_WORKFLOW] = swapped[STRESS_WORKFLOW].replace(
|
|
"target/ironclaw-stress/ironclaw-stress-libsql-scripted-"
|
|
"memory_roundtrip/report.txt",
|
|
"target/ironclaw-stress/ironclaw-stress-libsql-scripted-"
|
|
"memory_grow/report.txt",
|
|
)
|
|
errors = self.errors_for(swapped)
|
|
self.assertTrue(
|
|
any(
|
|
"exactly the three" in error and "memory-roundtrip" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
missing = copy.deepcopy(fixed)
|
|
missing[STRESS_WORKFLOW] = missing[STRESS_WORKFLOW].replace(
|
|
"target/ironclaw-stress/ironclaw-stress-libsql-scripted-"
|
|
"memory_grow/summary.json\n",
|
|
"",
|
|
)
|
|
errors = self.errors_for(missing)
|
|
self.assertTrue(
|
|
any(
|
|
"exactly the three" in error and "memory-grow" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_readiness_probes_are_bounded(self) -> None:
|
|
"""An unbounded probe (`while true`, or a seq bound past the job
|
|
timeout) hangs the job on a dead server instead of failing fast."""
|
|
for old, new in (
|
|
("for _ in $(seq 1 120); do", "for _ in $(seq 1 999999999); do"),
|
|
("for _ in $(seq 1 120); do", "while true; do"),
|
|
):
|
|
with self.subTest(new=new):
|
|
mutated = self.sabotage(STRESS_WORKFLOW, old, new)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any(
|
|
"bounded" in error
|
|
and ("probe" in error or "retry bound" in error)
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
no_sleep = self.sabotage(STRESS_WORKFLOW, " sleep 1\n", "")
|
|
errors = self.errors_for(no_sleep)
|
|
self.assertTrue(
|
|
any("curl the endpoint, sleep, and break" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_giving_up_after_the_retries_is_a_failure(self) -> None:
|
|
"""The final unconditional curl after each bounded loop is what makes
|
|
'bounded' fail fast: without it, 120 failed tries degrade into a
|
|
matrix run against a dead server. Removing either final probe from
|
|
the compliant fixture must fail."""
|
|
fixed = self.fixed_workflows()
|
|
for name, probe in (
|
|
("health", self.FINAL_HEALTH_PROBE_FIXED),
|
|
("webchat session", self.FINAL_SESSION_PROBE_FIXED),
|
|
):
|
|
with self.subTest(probe=name):
|
|
mutated = copy.deepcopy(fixed)
|
|
mutated[STRESS_WORKFLOW] = mutated[STRESS_WORKFLOW].replace(
|
|
probe + "\n", ""
|
|
)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any(
|
|
"final unconditional curl" in error and name in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_final_probes_require_explicit_timeouts(self) -> None:
|
|
"""The final curls must carry explicit --connect-timeout and
|
|
--max-time flags: without them a wedged server hangs the job past the
|
|
bounded loop instead of failing fast. Removing the flags from either
|
|
final probe must fail — this is exactly the checked-in gap — and the
|
|
values are pinned, so a dead-letter 50/100 is a regression, not a
|
|
fix."""
|
|
fixed = self.fixed_workflows()
|
|
for name, fixed_probe, bare_probe in (
|
|
("health", self.FINAL_HEALTH_PROBE_FIXED, self.FINAL_HEALTH_PROBE),
|
|
(
|
|
"webchat session",
|
|
self.FINAL_SESSION_PROBE_FIXED,
|
|
self.FINAL_SESSION_PROBE,
|
|
),
|
|
):
|
|
with self.subTest(probe=name):
|
|
mutated = copy.deepcopy(fixed)
|
|
self.assertIn(fixed_probe, mutated[STRESS_WORKFLOW])
|
|
mutated[STRESS_WORKFLOW] = mutated[STRESS_WORKFLOW].replace(
|
|
fixed_probe, bare_probe
|
|
)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any(
|
|
"final unconditional curl" in error
|
|
and name in error
|
|
and "--connect-timeout" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_cleanup_kills_and_waits_for_the_server(self) -> None:
|
|
"""Kill without wait can leave the port bound when the step ends —
|
|
the next run then collides with a zombie server. The trap must do
|
|
both, and must exist at all. Sabotages mutate the canonical fixture
|
|
(the raw checked-in text still carries a kill-only trap in a
|
|
neighbouring job, so it cannot stand in for this job's trap)."""
|
|
fixed = self.fixed_workflows()
|
|
no_wait = copy.deepcopy(fixed)
|
|
no_wait[STRESS_WORKFLOW] = no_wait[STRESS_WORKFLOW].replace(
|
|
'; wait "$server_pid" 2>/dev/null || true', ""
|
|
)
|
|
errors = self.errors_for(no_wait)
|
|
self.assertTrue(
|
|
any("kill the server AND wait" in error for error in errors), errors
|
|
)
|
|
|
|
no_trap = copy.deepcopy(fixed)
|
|
no_trap[STRESS_WORKFLOW] = no_trap[STRESS_WORKFLOW].replace(
|
|
self.CANONICAL_TRAP, "# cleanup handled by the runner\n"
|
|
)
|
|
errors = self.errors_for(no_trap)
|
|
self.assertTrue(
|
|
any("register an EXIT trap" in error for error in errors), errors
|
|
)
|
|
|
|
def test_server_log_has_its_own_artifact(self) -> None:
|
|
"""A failed run's log must live in ONE upload: embedded in every
|
|
per-script upload it is split three ways and each copy is incomplete
|
|
on its own (the RED shape this fixture normalizes away)."""
|
|
fixed = self.fixed_workflows()
|
|
self.assertIn(self.SERVER_LOG_STEP.rstrip("\n"), fixed[STRESS_WORKFLOW])
|
|
|
|
removed = copy.deepcopy(fixed)
|
|
removed[STRESS_WORKFLOW] = removed[STRESS_WORKFLOW].replace(
|
|
self.SERVER_LOG_STEP.rstrip("\n") + "\n", ""
|
|
)
|
|
errors = self.errors_for(removed)
|
|
self.assertTrue(
|
|
any(
|
|
"own artifact" in error
|
|
and "ironclaw-stress-libsql-scripted-server-log" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
renamed = copy.deepcopy(fixed)
|
|
renamed[STRESS_WORKFLOW] = renamed[STRESS_WORKFLOW].replace(
|
|
"name: ironclaw-stress-libsql-scripted-server-log",
|
|
"name: ironclaw-stress-libsql-scripted-serverlog",
|
|
)
|
|
errors = self.errors_for(renamed)
|
|
self.assertTrue(any("own artifact" in error for error in errors), errors)
|
|
|
|
def test_per_script_artifacts_are_unique_and_exclude_the_server_log(self) -> None:
|
|
"""Three scripts must map to three distinct artifact identities, and
|
|
the server log must live in its own upload, not in each of them."""
|
|
fixed = self.fixed_workflows()
|
|
|
|
duplicated = copy.deepcopy(fixed)
|
|
duplicated[STRESS_WORKFLOW] = duplicated[STRESS_WORKFLOW].replace(
|
|
"name: ironclaw-stress-libsql-scripted-memory-grow",
|
|
"name: ironclaw-stress-libsql-scripted-memory-roundtrip",
|
|
)
|
|
errors = self.errors_for(duplicated)
|
|
self.assertTrue(
|
|
any("exactly three distinct per-script artifacts" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
leaked = copy.deepcopy(fixed)
|
|
leaked[STRESS_WORKFLOW] = leaked[STRESS_WORKFLOW].replace(
|
|
"ironclaw-stress-libsql-scripted-memory_roundtrip/report.txt\n",
|
|
"ironclaw-stress-libsql-scripted-memory_roundtrip/report.txt\n"
|
|
+ self.SERVER_LOG_LINE
|
|
+ "\n",
|
|
)
|
|
errors = self.errors_for(leaked)
|
|
self.assertTrue(
|
|
any(
|
|
"must not include" in error and "libsql-scripted-server.log" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_the_job_stays_scheduled_and_manually_triggerable(self) -> None:
|
|
"""The gate must keep both trigger paths — dropping
|
|
workflow_dispatch strands the daily scan unreachable by hand."""
|
|
mutated = self.sabotage(
|
|
STRESS_WORKFLOW,
|
|
"if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'",
|
|
"if: github.event_name == 'schedule'",
|
|
)
|
|
errors = self.errors_for(mutated)
|
|
self.assertTrue(
|
|
any("schedule and workflow_dispatch" in error for error in errors), errors
|
|
)
|
|
|
|
def test_the_libsql_volume_profile_is_pinned_inside_the_job(self) -> None:
|
|
text = self.workflows[STRESS_WORKFLOW]
|
|
block, detail = extract_job_block(text, LIBSQL_SCRIPTED_MEMORY_JOB)
|
|
self.assertIsNotNone(block, detail)
|
|
start = text.find(block)
|
|
self.assertNotEqual(start, -1)
|
|
expected = 'profile = "hosted-single-tenant-volume"'
|
|
replacement = 'profile = "hosted-single-tenant-provisioned"'
|
|
|
|
inside = copy.deepcopy(self.workflows)
|
|
inside[STRESS_WORKFLOW] = (
|
|
f"{expected}\n"
|
|
+ text[:start]
|
|
+ block.replace(expected, replacement, 1)
|
|
+ text[start + len(block) :]
|
|
)
|
|
errors = self.errors_for(inside)
|
|
self.assertTrue(any(expected in error for error in errors), errors)
|
|
|
|
outside = copy.deepcopy(self.workflows)
|
|
outside[STRESS_WORKFLOW] = f'{replacement}\n{text}'
|
|
self.assertEqual(self.errors_for(outside), [])
|
|
|
|
|
|
def postgres_errors_for(self, workflows: dict[str, str]) -> list[str]:
|
|
return validate_postgres_scripted_parity(workflows[STRESS_WORKFLOW])
|
|
|
|
# The runner picks each operation's document size by cycling the
|
|
# --api-scripted-doc-sizes list (doc_size_for), so N operations exercise
|
|
# only the first N sizes. The historical `--operations 2` ran just the
|
|
# 4096/32768 buckets; parity requires at least one operation per
|
|
# configured size.
|
|
POSTGRES_SCRIPTED_FLAGS = (
|
|
"--operations 4 \\\n"
|
|
" --api-scripted-tool memory_roundtrip \\"
|
|
)
|
|
|
|
def test_checked_in_postgres_scripted_leg_reaches_every_size(self) -> None:
|
|
"""The Postgres parity leg must run at least as many operations as
|
|
configured doc sizes — with fewer, doc_size_for() cycles only the
|
|
first buckets and the largest sizes are never exercised."""
|
|
self.assertEqual(self.postgres_errors_for(self.workflows), [])
|
|
self.assertEqual(self.postgres_errors_for(self.fixed_workflows()), [])
|
|
|
|
def test_postgres_scripted_leg_has_zero_failure_tolerance(self) -> None:
|
|
"""One failed operation out of 32 is below five percent, so only an
|
|
exact zero gate fails closed on leaks and lost durable writes."""
|
|
for bad in ("0.05", "0.5"):
|
|
with self.subTest(value=bad):
|
|
mutated = self.sabotage(
|
|
STRESS_WORKFLOW,
|
|
" --max-failure-rate 0 \\\n",
|
|
f" --max-failure-rate {bad} \\\n",
|
|
)
|
|
errors = self.postgres_errors_for(mutated)
|
|
self.assertTrue(
|
|
any("--max-failure-rate 0" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
dropped = self.sabotage(
|
|
STRESS_WORKFLOW, " --max-failure-rate 0 \\\n", ""
|
|
)
|
|
errors = self.postgres_errors_for(dropped)
|
|
self.assertTrue(
|
|
any("--max-failure-rate 0" in error for error in errors), errors
|
|
)
|
|
|
|
def test_postgres_failure_gate_rejects_same_job_decoy(self) -> None:
|
|
text = self.workflows[STRESS_WORKFLOW]
|
|
block, detail = extract_job_block(text, "postgres-api-capacity")
|
|
self.assertIsNotNone(block, detail)
|
|
start = text.find(block)
|
|
commands = [
|
|
command
|
|
for command in ws12_workflow_contracts.extract_continued_commands(
|
|
block, "target/release/ironclaw_stress"
|
|
)
|
|
if "--api-scripted-tool memory_roundtrip" in command
|
|
]
|
|
self.assertEqual(len(commands), 1)
|
|
runner = commands[0]
|
|
mutated_runner = runner.replace(
|
|
"--max-failure-rate 0", "--max-failure-rate 0.05", 1
|
|
)
|
|
self.assertNotEqual(mutated_runner, runner)
|
|
mutated_block = block.replace(
|
|
runner,
|
|
' echo "--max-failure-rate 0"\n' + mutated_runner,
|
|
1,
|
|
)
|
|
self.assertNotEqual(mutated_block, block)
|
|
mutated = text[:start] + mutated_block + text[start + len(block) :]
|
|
errors = validate_postgres_scripted_parity(mutated)
|
|
self.assertTrue(
|
|
any("--max-failure-rate 0" in error for error in errors), errors
|
|
)
|
|
|
|
|
|
def test_postgres_operations_below_size_count_fail_loudly(self) -> None:
|
|
"""`--operations 2`/`3` leave configured doc sizes unexercised —
|
|
both must fail. Values at or above the size count stay legal (the
|
|
cycling then covers every bucket), pinning the >= contract rather
|
|
than an exact value."""
|
|
for ops in ("2", "3"):
|
|
with self.subTest(operations=ops):
|
|
mutated = self.sabotage(
|
|
STRESS_WORKFLOW,
|
|
self.POSTGRES_SCRIPTED_FLAGS,
|
|
f"--operations {ops} \\\n"
|
|
" --api-scripted-tool memory_roundtrip \\",
|
|
)
|
|
errors = self.postgres_errors_for(mutated)
|
|
self.assertTrue(
|
|
any(
|
|
"postgres-api-capacity" in error
|
|
and "--operations" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
generous = self.sabotage(
|
|
STRESS_WORKFLOW,
|
|
self.POSTGRES_SCRIPTED_FLAGS,
|
|
"--operations 14 \\\n"
|
|
" --api-scripted-tool memory_roundtrip \\",
|
|
)
|
|
self.assertEqual(self.postgres_errors_for(generous), [])
|
|
|
|
def test_dropping_the_postgres_doc_sizes_fails_loudly(self) -> None:
|
|
"""Without the exact four-size list the operations count has nothing
|
|
to reach — the parity leg must configure the same small-to-large
|
|
curve as the libsql matrix."""
|
|
mutated = self.sabotage(
|
|
STRESS_WORKFLOW,
|
|
" --api-scripted-doc-sizes 4096,32768,131072,1048576 \\\n"
|
|
" --api-hot-writers 2 \\",
|
|
" --api-hot-writers 2 \\",
|
|
)
|
|
errors = self.postgres_errors_for(mutated)
|
|
self.assertTrue(
|
|
any(
|
|
"postgres-api-capacity" in error
|
|
and "4096,32768,131072,1048576" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_postgres_parity_failures_reach_the_top_level_contract(self) -> None:
|
|
"""The parity validator must stay wired into `validate_workflow_texts`
|
|
— main()'s entry point is the only consumer CI actually runs."""
|
|
mutated = self.sabotage(
|
|
STRESS_WORKFLOW,
|
|
self.POSTGRES_SCRIPTED_FLAGS,
|
|
"--operations 2 \\\n"
|
|
" --api-scripted-tool memory_roundtrip \\",
|
|
)
|
|
errors = validate_workflow_texts(mutated, ROOT)
|
|
self.assertTrue(
|
|
any(
|
|
"postgres-api-capacity" in error and "--operations" in error
|
|
for error in errors
|
|
),
|
|
errors,
|
|
)
|
|
|
|
def test_zero_or_two_matching_job_blocks_refuse(self) -> None:
|
|
"""The scoped contract must resolve exactly one job block — a deleted
|
|
job and a duplicated job key both make the pins unassertable."""
|
|
text = self.workflows[STRESS_WORKFLOW]
|
|
self.assertIn(" libsql-scripted-memory:\n", text)
|
|
|
|
deleted = text.replace(" libsql-scripted-memory:\n", "")
|
|
errors = validate_libsql_scripted_memory_job(deleted)
|
|
self.assertTrue(any("expected exactly one" in error for error in errors), errors)
|
|
|
|
duplicated = text.replace(
|
|
" libsql-scripted-memory:\n",
|
|
" libsql-scripted-memory:\n libsql-scripted-memory:\n",
|
|
1,
|
|
)
|
|
errors = validate_libsql_scripted_memory_job(duplicated)
|
|
self.assertTrue(
|
|
any("expected exactly one" in error and "found 2" in error for error in errors),
|
|
errors,
|
|
)
|
|
|
|
def test_libsql_job_failures_reach_the_top_level_contract(self) -> None:
|
|
"""The validator must stay wired into `validate_workflow_texts` —
|
|
main()'s entry point is the only consumer CI actually runs."""
|
|
mutated = self.sabotage(
|
|
STRESS_WORKFLOW,
|
|
"for script in memory_roundtrip memory_grow memory_mixed; do",
|
|
"for script in memory_roundtrip memory_grow; do",
|
|
)
|
|
errors = validate_workflow_texts(mutated, ROOT)
|
|
self.assertTrue(any("fixed sequential loop" in error for error in errors), errors)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|