Skip to content

Configurable i3 emulator + per-configuration compliance CI - #43

Merged
github-actions[bot] merged 6 commits into
masterfrom
feat/i3-config-profiles
Jul 20, 2026
Merged

github-actions[bot] merged 6 commits into
masterfrom
feat/i3-config-profiles

Conversation

@Amantux

@Amantux Amantux commented Jul 20, 2026

Copy link
Copy Markdown
Owner

What & why

The in-repo Quantum i3 emulator (openblade.api.main:app, backend=mock) was hard-coded to one library shape (scalar-i3-50-3), even though the cross-repo contract advertises EMULATOR_SLOT_COUNT / EMULATOR_DRIVE_COUNT / EMULATOR_PROFILE / EMULATOR_OCCUPANCY_PERCENT — so it couldn't represent any other valid i3 configuration and was non-compliant with its own contract.

This makes the emulator honor those knobs and adds CI that boots the emulator once per supported configuration and runs the i3 suite against each, so the emulated i3 behaves like a real i3 across the range of supported shapes. The same tests/i3 suite runs against a real i3 (I3_TEST_MODE=real), so both are held to one behavioral contract.

Configuration axes (shipped, in CI)

Boot with EMULATOR_PROFILE=<name> (or scalar-i3-<slots>-<drives>, with per-field EMULATOR_SLOT_COUNT / EMULATOR_DRIVE_COUNT / EMULATOR_OCCUPANCY_PERCENT overrides). Matrix profiles:

  • Drive count (1–6) — scalar-i3-25-1, scalar-i3-50-6 (6 = documented i3 max, per docs/sharding.md; >6 fails fast).
  • LTO generation — scalar-i3-50-3-lto9 (LTO-7/8/9 = 6/12/18 TB).
  • Slot / module scaling — scalar-i3-100-3 (with correct 1,bay,slot addressing).
  • Default scalar-i3-50-3 is byte-identical to before (golden-locked).

Deferred: a multi-partition axis is not shipped — the parity-gated AML partition route serializer (routes_aml_partitions._partition_slots) assumes a single partition over the absolute slot range, so a 2-partition library can't be served faithfully until that route work lands. Documented in emulator_contract/README.md.

How it works

  • openblade/simulator/i3_config.py — a validated ScalarI3Profile builder (build_scalar_i3_config, scalar_i3_active_config, NAMED_PROFILES); fail-fast on invalid configs.
  • openblade/api/aml_state.py + scenarios.py — repointed to scalar_i3_active_config() so /aml/* responses and the mock robotics backend reflect the selected shape (unchanged when no env is set).
  • .github/workflows/i3-emulator-compliance.yml — new i3-config-<profile> matrix job.

Review (T3)

Reviewed by the adversarial reviewer subagent. It found two proven blockers — both fixed:

  • Selecting scalar-i3-50-3 by name dropped the canonical drive list → now returned verbatim (byte-identical).
  • The 2-partition profile orphaned media on the AML layer → the axis is deferred (not shipped) rather than reworking a parity-gated surface under this change.
    Plus: clear fail-fast messages naming a bad EMULATOR_* var; demo NAS seeding guards on the active backend (no phantom cartridges under low occupancy).

Verified

ruff clean on all changed files. 125 tests green (contract gates test_15/16/17, aml operations/system/diagnostics, builder + test_18); all 5 shipped profiles boot a real server and serve every /aml endpoint with no 5xx; default /aml output byte-identical to master.

Pre-existing and out of scope (unchanged by this PR): repo-wide ruff/mypy debt in untouched files, and validate_cross_repo_contract.py already red on master (missing OPENBLADE_EMULATOR_IMAGE pin in docker-compose.yml).

🤖 Generated with Claude Code

https://claude.ai/code/session_012K5Zjc2bggcyJtFbz2D4Pw

Amantux and others added 6 commits July 20, 2026 21:06
…builder

The mock i3 shape was hard-coded to scalar-i3-50-3, so the emulator could not
represent any other valid i3 configuration despite the cross-repo contract
advertising EMULATOR_SLOT_COUNT / EMULATOR_DRIVE_COUNT / EMULATOR_PROFILE /
EMULATOR_OCCUPANCY_PERCENT. This adds a ScalarI3Profile builder:

- build_scalar_i3_config(profile) generalizes slots, drive mix, LTO generation
  (incl. LTO-9=18TB), occupancy, IE slots, and partitions, keeping the 1,bay,slot
  addressing and one-cleaning-slot-per-drive rules.
- Fail-fast validation: 1<=drives<=6 (documented i3 max), known LTO gens, slot_count
  >= drives+1, occupancy 0..100, partition_count in 1..drive_count.
- scalar_i3_active_config() reads the EMULATOR_* env knobs (EMULATOR_PROFILE named or
  parsed scalar-i3-<slots>-<drives>, with per-field slot/drive/occupancy overrides),
  defaulting to the canonical default when unset.
- scalar_i3_default_config() output is byte-identical to the historical literal
  (locked by a golden unit test); the default's drive list is preserved verbatim.
- The barcode helpers stay bound to the DEFAULT config on purpose so bootstrap's
  demo NAS seeding remains deterministic across profiles.

NAMED_PROFILES ships representative valid configs (min 1-drive, default, 100-slot,
max 6-drive, LTO-9, 2-partition) for the compliance suite + CI. No consumers repointed
yet; behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012K5Zjc2bggcyJtFbz2D4Pw
…imulator

Repoint the /aml response builders (aml_state.py, 11 sites) and the mock robotics
backend (scenarios.scalar_i3_default) from scalar_i3_default_config() to
scalar_i3_active_config(), so both the control-plane responses and the simulated
library reflect the EMULATOR_*-selected shape. With no env set active == default,
so behavior is unchanged. bootstrap.py is deliberately NOT repointed (its demo NAS
seeding stays bound to the default barcodes).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012K5Zjc2bggcyJtFbz2D4Pw
test_14_emulator_profile and the physical-elements bay test derived expected
slots/drives/generations from the hard-coded 50/3 shape; derive them from
scalar_i3_active_config() instead so they hold under any emulator profile (still
assert 50/3 by default). Also generalize the media-generation check to "every data
media generation is readable by some drive" so single-generation profiles
(LTO-9-only, 1-drive) pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012K5Zjc2bggcyJtFbz2D4Pw
Parameterized over NAMED_PROFILES: asserts each config is internally consistent
(slot/drive counts, 1,bay,slot addressing uniqueness, one cleaning slot per drive
at top ids, occupancy-derived data count, capacity-by-generation incl LTO-9,
data generations readable by drives, partition bookkeeping); asserts invalid
configs fail fast; and boots the app under scalar-i3-25-1 / scalar-i3-50-6 to
confirm /aml/library + /aml/library/inventory + /aml/drives report the selected
shape end-to-end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012K5Zjc2bggcyJtFbz2D4Pw
Add an i3-config-<profile> matrix job to i3-emulator-compliance.yml that boots the
emulator once per named profile (scalar-i3-25-1, -50-3, -100-3, -50-6, -50-3-lto9,
-50-4-p2) with EMULATOR_PROFILE set (server + pytest both observe the shape) and runs
the i3 core suite + test_18 against each, failing the build if any configuration is
non-compliant. The existing suite=core/operations/system matrix is preserved.

Add test_18_config_profiles to the emulator-change-gates pre-boot gate, and document
in emulator_contract/README.md that the in-repo app now honors the EMULATOR_* shape
knobs (default scalar-i3-50-3; >6 drives fails fast) so the emulated i3 behaves like a
real i3 across configurations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012K5Zjc2bggcyJtFbz2D4Pw
…s; defer partitions

Reviewer (adversarial) found two proven blockers and three smaller issues:

- BLOCKER: EMULATOR_PROFILE=scalar-i3-50-3 dropped the canonical drive_overrides and
  regenerated drive metadata (load counts drifted) — selecting the default profile by
  name no longer reproduced the default. Fixed: a named profile with no per-field
  override is returned verbatim (drive_overrides preserved). Byte-identical again.
- BLOCKER: the 2-partition profile orphaned partition-2 media from the AML layer
  (media counts 28->25, single partition served) because the parity-gated route
  serializer (routes_aml_partitions._partition_slots) assumes one partition. Rather
  than rework a parity-gated surface under this change, the multi-partition axis is
  DEFERRED: reverted the aml_state partition builder to its original single-partition
  form (kept the scalar_i3_active_config rename) and removed all multi-partition code
  from the i3_config builder. The compliance matrix now ships the three fully-working
  axes (drive count, LTO generation, slot/module scaling); multi-partition is
  documented as awaiting AML route-serializer work.
- Clear fail-fast messages naming the offending EMULATOR_* variable on non-integer input.
- Demo NAS seeding now guards on the ACTIVE backend's barcodes (identical to default
  for every shipped profile), so a low-occupancy override can't seed phantom cartridges.
- test_18 fixture ordering fixed so the default context is restored after monkeypatch reverts.

Added regression tests: named-default reproduces the default; non-integer knob names the
variable.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012K5Zjc2bggcyJtFbz2D4Pw
Copilot AI review requested due to automatic review settings July 20, 2026 21:56

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR makes the in-repo Quantum Scalar i3 emulator configuration-driven (via contracted EMULATOR_* knobs) instead of being hard-coded to a single library shape, and adds CI coverage to ensure each supported profile boots and passes the i3 compliance suite.

Changes:

  • Introduces a validated ScalarI3Profile builder and an env-driven scalar_i3_active_config() for generating deterministic i3 shapes (slots/drives/occupancy/LTO gen).
  • Repoints AML state + simulator scenario wiring to reflect the active profile across /aml/* responses and the mock robotics backend.
  • Adds new i3 compliance tests and a GitHub Actions matrix job that boots the emulator per profile and runs the i3 suite against each configuration.

Reviewed changes

Copilot reviewed 11 out of 43 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
openblade/simulator/i3_config.py Replaces hard-coded i3 config with a profile-based builder plus env-driven active profile selection.
openblade/api/aml_state.py Switches AML default state generation to use the active i3 config so /aml/* reflects selected shape.
openblade/simulator/scenarios.py Builds the mock library backend from the active i3 config (so simulator backend shape follows env).
openblade/bootstrap.py Seeds demo catalog cartridges based on the active emulator media set to avoid phantom cartridges in small/low-occupancy profiles.
tests/unit/test_i3_config_builder.py Adds unit coverage for default determinism, env precedence, and fail-fast invalid configs.
tests/i3/test_18_config_profiles.py Adds i3 compliance coverage for profile invariants and live /aml reflection under non-default profiles.
tests/i3/test_14_emulator_profile.py Updates existing i3 tests to validate against the active profile rather than assuming 50 slots / 3 drives.
tests/integration/test_aml_diagnostics.py Makes bay/slot expectations scale with the active profile slot count.
openblade/emulator_contract/README.md Documents that the in-repo app honors EMULATOR_* shape knobs and describes the new CI coverage.
.github/workflows/i3-emulator-compliance.yml Adds an i3-config-<profile> matrix job to boot + test each shipped i3 profile.
.github/workflows/emulator-change-gates.yml Adds the new config profile test file to existing emulator change gates.

Comment on lines +23 to +33
from openblade.simulator.i3_config import (
_CAPACITY_BY_GENERATION,
NAMED_PROFILES,
ScalarI3Profile,
build_scalar_i3_config,
)

pytestmark = pytest.mark.i3

_COORD_RE = re.compile(r"\A1,[12],\d+\Z")

Comment on lines 505 to 509
def scalar_i3_mock_library_barcodes(*, include_cleaning: bool = True) -> list[str]:
# Bound to the DEFAULT config on purpose: bootstrap.py seeds the demo NAS
# catalog from fixed VOLxxxL9 barcodes via a membership guard, so this must
# stay deterministic regardless of the active emulator profile.
media = scalar_i3_default_config()["media"]
Comment on lines 66 to 68
def scalar_i3_default() -> tuple[MockLibraryBackend, MockLTFSBackend]:
config = scalar_i3_default_config()
config = scalar_i3_active_config()
partition = config["partition"]
@github-actions
github-actions Bot merged commit ec30be8 into master Jul 20, 2026
15 of 22 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants