Add enterprise appliance experience Phase 1 foundation
This commit is contained in:
1 parent
b3eec34158
commit
ebaa2e7219
69 files changed
+6832
-18
No files matched your search
@@ -0,0 +1 @@
|
||||
"""Nexus One AI test package."""
|
||||
@@ -0,0 +1,24 @@
|
||||
# Phase 1 WCAG 2.2 AA Checklist
|
||||
|
||||
Apply to browser setup, console-equivalent instructions, landing readiness, appliance and health
|
||||
pages. Automated checks supplement but do not replace manual review.
|
||||
|
||||
- [ ] Keyboard completes valid setup, validation recovery and interruption recovery.
|
||||
- [ ] Focus order follows the visible journey and remains visible at all times.
|
||||
- [ ] Validation errors are associated with fields and focus moves to the first invalid field.
|
||||
- [ ] Progress, degraded state, failure and completion are announced without stealing focus.
|
||||
- [ ] Step, tier and tool selectors expose name, role, state and keyboard operation.
|
||||
- [ ] Confirmation identifies scope, disruption and recovery implications.
|
||||
- [ ] Status and severity never rely on colour alone.
|
||||
- [ ] Text and controls meet WCAG 2.2 AA contrast requirements.
|
||||
- [ ] Content reflows at 320 CSS pixels and remains usable at 200% zoom.
|
||||
- [ ] Motion respects reduced-motion preferences.
|
||||
- [ ] Page titles, headings, landmarks and labels identify purpose and context.
|
||||
- [ ] Error messages avoid raw codes as primary text and provide a safe next action.
|
||||
- [ ] Workstation and Server labels are consistent and understandable.
|
||||
- [ ] Restricted-network guidance is locally available.
|
||||
- [ ] Automated scan has no unresolved Level A or AA finding.
|
||||
- [ ] Representative screen-reader journey has no unresolved blocker.
|
||||
|
||||
Record browser, operating system, assistive technology, viewport, product configuration, evidence
|
||||
links, reviewer and date for every completed run.
|
||||
@@ -0,0 +1 @@
|
||||
"""Backend contract tests."""
|
||||
@@ -0,0 +1,21 @@
|
||||
import ast
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MAIN = ROOT / "ansible/roles/cezen-backend/files/main.py"
|
||||
|
||||
|
||||
def load_symbols(*names, globals_dict=None):
|
||||
tree = ast.parse(MAIN.read_text())
|
||||
wanted = []
|
||||
for node in tree.body:
|
||||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and node.name in names:
|
||||
wanted.append(node)
|
||||
elif isinstance(node, ast.Assign):
|
||||
assigned = {target.id for target in node.targets if isinstance(target, ast.Name)}
|
||||
if assigned.intersection(names):
|
||||
wanted.append(node)
|
||||
namespace = dict(globals_dict or {})
|
||||
exec(compile(ast.Module(body=wanted, type_ignores=[]), str(MAIN), "exec"), namespace)
|
||||
return namespace
|
||||
@@ -0,0 +1,18 @@
|
||||
from ._source import load_symbols
|
||||
|
||||
|
||||
def _namespace():
|
||||
return load_symbols("_AUDIT_SENSITIVE_KEYS", "_redact_audit_value", "safe_audit_detail", globals_dict={"json": __import__("json")})
|
||||
|
||||
|
||||
def test_structured_audit_detail_redacts_secret_categories():
|
||||
safe = _namespace()["safe_audit_detail"]({"license_key": "abc", "result": "valid", "nested": {"token": "xyz"}})
|
||||
assert "abc" not in safe and "xyz" not in safe
|
||||
assert safe.count("[REDACTED]") == 2
|
||||
assert "valid" in safe
|
||||
|
||||
|
||||
def test_text_audit_detail_redacts_key_value_pairs():
|
||||
safe = _namespace()["safe_audit_detail"]("license_key=abc token:xyz outcome=valid")
|
||||
assert "abc" not in safe and "xyz" not in safe
|
||||
assert "outcome=valid" in safe
|
||||
@@ -0,0 +1,24 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from ._source import load_symbols
|
||||
|
||||
|
||||
def test_customer_state_normalizes_unknown_values():
|
||||
ns = load_symbols("CUSTOMER_STATES", "customer_state", globals_dict={"utcnow": lambda: datetime.now(timezone.utc).isoformat()})
|
||||
payload = ns["customer_state"]("mystery", "Status cannot be observed")
|
||||
assert payload["schema"] == "cezen.customer_state.v1"
|
||||
assert payload["state"] == "unavailable"
|
||||
assert payload["summary"] == "Status cannot be observed"
|
||||
|
||||
|
||||
def test_error_envelope_has_customer_safe_fields():
|
||||
ns = load_symbols("error_envelope")
|
||||
payload = ns["error_envelope"]("service_unavailable", "Model service is unavailable", "Retry after checking health.", affected_scope="local inference", correlation_id="corr-1")
|
||||
assert set(payload) == {"schema", "code", "message", "remediation", "affected_scope", "correlation_id"}
|
||||
assert payload["correlation_id"] == "corr-1"
|
||||
|
||||
|
||||
def test_new_operation_interfaces_require_admin_in_source():
|
||||
source = (__import__("pathlib").Path(__file__).resolve().parents[2] / "ansible/roles/cezen-backend/files/main.py").read_text()
|
||||
assert 'actor.get("role") != "admin"' in source
|
||||
assert '"admin_required"' in source
|
||||
@@ -0,0 +1,17 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
def test_canonical_and_deployed_license_helpers_are_identical():
|
||||
canonical = ROOT / "scripts/cezen_license.py"
|
||||
deployed = ROOT / "ansible/roles/cezen-backend/files/cezen_license.py"
|
||||
assert canonical.read_bytes() == deployed.read_bytes()
|
||||
|
||||
|
||||
def test_customer_labels_preserve_compatibility_slugs():
|
||||
source = (ROOT / "scripts/cezen_license.py").read_text()
|
||||
for slug in ("starter", "basic", "pro", "max"):
|
||||
assert slug in source
|
||||
assert 'WORKSTATION_TIER = "workstation"' in source
|
||||
@@ -0,0 +1,31 @@
|
||||
import sqlite3
|
||||
|
||||
from ._source import load_symbols
|
||||
|
||||
|
||||
def _migration_namespace():
|
||||
return load_symbols("SCHEMA_VERSION", "_column_names", "run_migrations")
|
||||
|
||||
|
||||
def test_migration_is_idempotent_and_creates_operation_contract():
|
||||
db = sqlite3.connect(":memory:")
|
||||
db.execute("CREATE TABLE audit_log (id INTEGER PRIMARY KEY, detail TEXT)")
|
||||
ns = _migration_namespace()
|
||||
assert ns["run_migrations"](db) == 1
|
||||
assert ns["run_migrations"](db) == 1
|
||||
columns = {row[1] for row in db.execute("PRAGMA table_info(operations)")}
|
||||
assert {"id", "correlation_id", "kind", "state", "idempotency_key", "recovery_actions"} <= columns
|
||||
assert "correlation_id" in {row[1] for row in db.execute("PRAGMA table_info(audit_log)")}
|
||||
|
||||
|
||||
def test_newer_schema_is_rejected_without_mutation():
|
||||
db = sqlite3.connect(":memory:")
|
||||
db.execute("CREATE TABLE schema_metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL)")
|
||||
db.execute("INSERT INTO schema_metadata VALUES ('schema_version','999')")
|
||||
ns = _migration_namespace()
|
||||
try:
|
||||
ns["run_migrations"](db)
|
||||
except RuntimeError as exc:
|
||||
assert "newer than supported" in str(exc)
|
||||
else:
|
||||
raise AssertionError("newer schema was accepted")
|
||||
@@ -0,0 +1,25 @@
|
||||
import sqlite3
|
||||
|
||||
from ._source import load_symbols
|
||||
|
||||
|
||||
def test_operation_states_include_recovery_and_terminal_states():
|
||||
ns = load_symbols("OPERATION_STATES", "TERMINAL_OPERATION_STATES")
|
||||
assert {"requested", "running", "awaiting_reboot", "recovery_required"} <= ns["OPERATION_STATES"]
|
||||
assert ns["TERMINAL_OPERATION_STATES"] <= ns["OPERATION_STATES"]
|
||||
|
||||
|
||||
def test_operation_schema_prevents_duplicate_kind_and_idempotency_key():
|
||||
ns = load_symbols("SCHEMA_VERSION", "_column_names", "run_migrations")
|
||||
db = sqlite3.connect(":memory:")
|
||||
db.execute("CREATE TABLE audit_log (id INTEGER PRIMARY KEY, detail TEXT)")
|
||||
ns["run_migrations"](db)
|
||||
values = ("1", "c1", "install", "requested", "now", "now", "same")
|
||||
sql = "INSERT INTO operations (id,correlation_id,kind,state,requested_at,updated_at,idempotency_key) VALUES (?,?,?,?,?,?,?)"
|
||||
db.execute(sql, values)
|
||||
try:
|
||||
db.execute(sql, ("2", "c2", "install", "requested", "now", "now", "same"))
|
||||
except sqlite3.IntegrityError:
|
||||
pass
|
||||
else:
|
||||
raise AssertionError("duplicate idempotent operation accepted")
|
||||
@@ -0,0 +1,23 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
MAIN = ROOT / "ansible/roles/cezen-backend/files/main.py"
|
||||
|
||||
|
||||
def test_readiness_contract_has_state_freshness_impact_and_actions():
|
||||
source = MAIN.read_text()
|
||||
assert '"cezen.readiness_report.v2"' in source
|
||||
for field in ("observed_at", "freshness_seconds", "affected_capabilities", "next_actions"):
|
||||
assert field in source
|
||||
|
||||
|
||||
def test_readiness_contract_preserves_entitlement_reasons():
|
||||
source = MAIN.read_text()
|
||||
for reason in ("not_licensed", "not_provisioned", "unsupported_hardware", "degraded", "unavailable"):
|
||||
assert reason in source
|
||||
|
||||
|
||||
def test_readiness_display_target_is_ten_seconds():
|
||||
spec = " ".join((ROOT / "specs/001-enterprise-experience/spec.md").read_text().split())
|
||||
assert "within 10 seconds" in spec
|
||||
@@ -0,0 +1,16 @@
|
||||
# Browser Journey and Accessibility Tests
|
||||
|
||||
Browser tests MUST run against a disposable local or appliance target, never a production customer.
|
||||
|
||||
Required coverage:
|
||||
|
||||
- supported desktop browser viewport plus 200% zoom/reflow;
|
||||
- keyboard-only primary and failure/recovery journeys;
|
||||
- visible focus, logical order, accessible names, headings and landmarks;
|
||||
- dynamic status, progress, validation and completion announcements;
|
||||
- loading, empty, ready, degraded, failed, restricted, unavailable and stale states;
|
||||
- 1-second navigation feedback and 2-second operation acknowledgement trials;
|
||||
- screenshots and machine-readable results stored outside source control.
|
||||
|
||||
The final browser runner and dependency versions MUST be pinned when implementation selects them.
|
||||
Automated accessibility results never replace the manual WCAG 2.2 AA checklist.
|
||||
@@ -0,0 +1,26 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
WEB_SETUP = ROOT / "autoinstall/websetup/server.py"
|
||||
|
||||
|
||||
def test_web_setup_exposes_validation_and_customer_facing_license_labels():
|
||||
source = WEB_SETUP.read_text()
|
||||
assert "validate_static_network" in source
|
||||
assert "LICENSE_STATUS_LABELS" in source
|
||||
assert "Invalid signature — re-upload a valid signed license" in source
|
||||
|
||||
|
||||
def test_phase1_contract_requires_durable_progress_not_only_process_memory():
|
||||
source = WEB_SETUP.read_text()
|
||||
# This test intentionally fails until T026 replaces process-local-only state.
|
||||
assert "INSTALL_OPERATION_JSON" in source
|
||||
assert "idempotency_key" in source
|
||||
assert "operation_id" in source
|
||||
|
||||
|
||||
def test_setup_acknowledgement_contract_is_explicit():
|
||||
source = WEB_SETUP.read_text()
|
||||
assert "acknowledged_at" in source
|
||||
assert "duplicate_request" in source
|
||||
@@ -0,0 +1,34 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[1]
|
||||
BACKEND_MAIN = REPO_ROOT / "ansible/roles/cezen-backend/files/main.py"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def isolated_appliance(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> dict[str, Path]:
|
||||
"""Return isolated appliance paths and never touch /opt/cezen."""
|
||||
data = tmp_path / "data"
|
||||
backups = tmp_path / "backups"
|
||||
data.mkdir()
|
||||
backups.mkdir()
|
||||
monkeypatch.setenv("CEZEN_DATA", str(data))
|
||||
monkeypatch.setenv("CEZEN_BACKUP_DIR", str(backups))
|
||||
monkeypatch.setenv("CEZEN_LICENSE_JSON", str(tmp_path / "license.json"))
|
||||
return {"root": tmp_path, "data": data, "backups": backups}
|
||||
|
||||
|
||||
def load_backend_module(module_name: str = "cezen_backend_test"):
|
||||
"""Load the packaged backend only after fixtures set environment variables."""
|
||||
spec = importlib.util.spec_from_file_location(module_name, BACKEND_MAIN)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"Cannot load backend from {BACKEND_MAIN}")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
@@ -0,0 +1,18 @@
|
||||
# Deployment Validation
|
||||
|
||||
Run from the repository root on a safe validation host.
|
||||
|
||||
```bash
|
||||
bash -n install.sh autoinstall/firstboot-setup.sh autoinstall/build-iso.sh \
|
||||
autoinstall/build-iso-workstation.sh autoinstall/build-iso-starter.sh
|
||||
ansible-playbook --syntax-check ansible/starter.yml
|
||||
ansible-playbook --syntax-check ansible/entry.yml
|
||||
ansible-playbook --syntax-check ansible/pro.yml
|
||||
ansible-playbook --syntax-check ansible/max.yml
|
||||
python3 -m py_compile ansible/roles/cezen-backend/files/main.py \
|
||||
ansible/roles/cezen-backend/files/cezen_license.py autoinstall/websetup/server.py
|
||||
```
|
||||
|
||||
Syntax checks do not prove installation. Phase 1 additionally requires clean-install, interruption,
|
||||
reboot/resume, restricted-network, migration, backup/restore and source-to-artifact evidence on one
|
||||
representative Workstation and one representative Server.
|
||||
@@ -0,0 +1,21 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
FIRSTBOOT = ROOT / "autoinstall/firstboot-setup.sh"
|
||||
|
||||
|
||||
def test_console_setup_preserves_workstation_separation_and_safe_completion():
|
||||
source = FIRSTBOOT.read_text()
|
||||
assert 'WORKSTATION_MODE' in source
|
||||
assert 'TIER_DISPLAY="workstation"' in source
|
||||
assert 'touch /opt/cezen/.setup-done' in source
|
||||
assert source.index('touch /opt/cezen/.setup-done') > source.index('if bash "$AIPACKAGE_DIR/install.sh"')
|
||||
|
||||
|
||||
def test_console_setup_has_resume_and_recovery_contract():
|
||||
source = FIRSTBOOT.read_text()
|
||||
# Intentionally red until T028 adds durable console progress.
|
||||
assert "cezen.setup_operation.v1" in source
|
||||
assert "recovery_required" in source
|
||||
assert "resume" in source.lower()
|
||||
Vendored
+18
@@ -0,0 +1,18 @@
|
||||
# Appliance Test Fixtures
|
||||
|
||||
## Supported Baseline
|
||||
|
||||
Capture the exact prior released revision, package/ISO checksum, Ubuntu version, product category,
|
||||
commercial tier, schema version and enabled components before migration testing. A moving `main`
|
||||
checkout is not a valid baseline.
|
||||
|
||||
## Restore Fixture Requirements
|
||||
|
||||
- Synthetic users, audit events, settings and operational state only.
|
||||
- No real credentials, signed licenses, customer documents or private logs.
|
||||
- Valid checksums and a documented schema/product version.
|
||||
- Representative Workstation and Server install records.
|
||||
- Valid, missing/staging, expired, invalid-signature and mismatch license fixtures using test keys.
|
||||
- Corrupt, incomplete and incompatible backup fixtures for negative testing.
|
||||
|
||||
Fixtures containing secrets or customer content MUST NOT enter source control.
|
||||
Vendored
+29
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"schema": "cezen.test.setup_matrix.v1",
|
||||
"configurations": [
|
||||
{
|
||||
"id": "representative-workstation",
|
||||
"category": "workstation",
|
||||
"commercial_label": "Nexus One AI Workstation",
|
||||
"compatibility_profile": "starter",
|
||||
"expected_server_tier_menu": false,
|
||||
"journeys": ["valid", "field-staging", "invalid-license", "network-loss", "service-failure", "reboot-resume", "stale-readiness"]
|
||||
},
|
||||
{
|
||||
"id": "representative-server-m",
|
||||
"category": "server",
|
||||
"commercial_label": "Server M",
|
||||
"compatibility_tier": "basic",
|
||||
"expected_server_tier_menu": true,
|
||||
"journeys": ["valid", "field-staging", "invalid-license", "hardware-restriction", "network-loss", "service-failure", "reboot-resume", "stale-readiness"]
|
||||
}
|
||||
],
|
||||
"common_assertions": [
|
||||
"non-secret progress persists",
|
||||
"duplicate submission is idempotent",
|
||||
"customer-facing state is truthful",
|
||||
"safe next action is present",
|
||||
"restricted-network help is local",
|
||||
"WCAG 2.2 AA evidence is recorded"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user