diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index dd44daa..b0ba416 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -1,15 +1,17 @@ @@ -72,6 +74,12 @@ ### Data Minimization & Safe Logging - Payload-heavy content belongs in immutable snapshots/backup storage, not Inventory. - Logs MUST not contain secrets/tokens; monitoring MUST rely on run records + error codes (not log parsing). +### Badge Semantics Are Centralized (BADGE-001) +- Status-like badges (status/outcome/severity/risk/availability/boolean signals) MUST render via `BadgeCatalog` / `BadgeRenderer`. +- Filament resources/pages/widgets/views MUST NOT introduce ad-hoc status-like badge mappings (use a `BadgeDomain` instead). +- Introducing or changing a status-like value MUST include updating the relevant badge mapper and adding/updating tests for the mapping. +- Tag/category chips (e.g., type/platform/environment) are not status-like and are not governed by BADGE-001. + ### Spec-First Workflow - For any feature that changes runtime behavior, include or update `specs/-/` with `spec.md`, `plan.md`, `tasks.md`, and `checklists/requirements.md`. - New work branches from `dev` using `feat/-` (spec + code in the same PR). @@ -96,4 +104,4 @@ ### Versioning Policy (SemVer) - **MINOR**: new principle/section or materially expanded guidance. - **MAJOR**: removing/redefining principles in a backward-incompatible way. -**Version**: 1.2.1 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-01-17 +**Version**: 1.3.0 | **Ratified**: 2026-01-03 | **Last Amended**: 2026-01-22 diff --git a/.specify/templates/plan-template.md b/.specify/templates/plan-template.md index 436949c..f30b962 100644 --- a/.specify/templates/plan-template.md +++ b/.specify/templates/plan-template.md @@ -39,6 +39,7 @@ ## Constitution Check - Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log - Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter - Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens +- Badge semantics (BADGE-001): status-like badges use `BadgeCatalog` / `BadgeRenderer`; no ad-hoc mappings; new values include tests ## Project Structure diff --git a/.specify/templates/spec-template.md b/.specify/templates/spec-template.md index c1867e3..d39188a 100644 --- a/.specify/templates/spec-template.md +++ b/.specify/templates/spec-template.md @@ -82,6 +82,9 @@ ## Requirements *(mandatory)* (preview/confirmation/audit), tenant isolation, run observability (`OperationRun` type/identity/visibility), and tests. If security-relevant DB-only actions intentionally skip `OperationRun`, the spec MUST describe `AuditLog` entries. +**Constitution alignment (BADGE-001):** If this feature changes status-like badges (status/outcome/severity/risk/availability/boolean), +the spec MUST describe how badge semantics stay centralized (no ad-hoc mappings) and which tests cover any new/changed values. + + +**Language/Version**: PHP 8.4.15 (Laravel 12) +**Primary Dependencies**: Filament v5 + Livewire v4 +**Storage**: PostgreSQL +**Testing**: Pest v4 (PHPUnit v12 runtime via `php artisan test`) +**Target Platform**: Web application (Filament admin panel) +**Project Type**: Web (Laravel monolith) +**Performance Goals**: Badge mapping is constant-time; no added queries or N+1; typical list pages render <2s for normal tenant sizes. +**Constraints**: Tenant-scoped; status-like badge rendering must be DB-only and must not trigger outbound HTTP or job dispatch during render/polling/hydration. +**Scale/Scope**: Suite-wide migration for status/health and severity/risk badges across tables, dashboards/KPIs, and detail views. + +## Constitution Check + +*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.* + +- Inventory-first: clarify what is “last observed” vs snapshots/backups +- Read/write separation: any writes require preview + confirmation + audit + tests +- Graph contract path: Graph calls only via `GraphClientInterface` + `config/graph_contracts.php` +- Deterministic capabilities: capability derivation is testable (snapshot/golden tests) +- Tenant isolation: all reads/writes tenant-scoped; cross-tenant views are explicit and access-checked +- Run observability: long-running/remote/queued work creates/reuses `OperationRun`; start surfaces enqueue-only; Monitoring is DB-only; DB-only <2s actions may skip runs but security-relevant ones still audit-log +- Automation: queued/scheduled ops use locks + idempotency; handle 429/503 with backoff+jitter +- Data minimization: Inventory stores metadata + whitelisted meta; logs contain no secrets/tokens + +Status: ✅ No constitution violations (UI semantics only; no new Graph calls; no new write behavior; badge mapping is pure and tenant-safe). + +## Project Structure + +### Documentation (this feature) + +```text +/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/ +├── plan.md # This file (/speckit.plan command output) +├── research.md # Phase 0 output (/speckit.plan command) +├── data-model.md # Phase 1 output (/speckit.plan command) +├── quickstart.md # Phase 1 output (/speckit.plan command) +├── contracts/ # Phase 1 output (/speckit.plan command) +└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan) +``` + +### Source Code (repository root) + +```text +/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/ +├── Filament/ +│ ├── Pages/Monitoring/Operations.php # Update: migrate status/outcome badges to central mapping +│ ├── Resources/ # Update: status-like columns/entries across resources +│ └── Widgets/ # Update: status-like + severity badges in dashboard widgets +├── Support/ +│ └── Badges/ # New: central badge semantics (status/health + severity/risk) +└── Models/ # Existing: status/severity sources (OperationRun, Finding, etc.) + +/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/ +└── filament/ # Update: replace any ad-hoc status-like badge colors + +/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/ +├── Feature/Guards/ # New: lightweight “no ad-hoc badge semantics” guard +└── Unit/ # New/updated: badge mapping tests per domain +``` + +**Structure Decision**: Laravel monolith + Filament v5 conventions. Centralize semantics in `app/Support/Badges` and consume from Filament resources/pages/widgets + Blade views. + +## Complexity Tracking + +> **Fill ONLY if Constitution Check has violations that must be justified** + +None. + +## Phase 0 — Outline & Research (complete) + +- Output: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/research.md` +- Key decisions captured: + - v1 scope: status/health + severity/risk badges suite-wide; tag/category chips deferred. + - Drift severity mapping: low = neutral, medium = warning, high = danger. + - Enforcement: mapping tests + lightweight guard to prevent reintroducing ad-hoc mappings. + +## Phase 1 — Design & Contracts (complete) + +### Data model +- Output: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/data-model.md` +- No schema changes required; badge semantics derive from existing fields (status/outcome/severity booleans). + +### Contracts +- Output: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/contracts/badge-semantics.md` +- Output: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/contracts/guardrails.md` + +### Quickstart +- Output: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/quickstart.md` + +### Provider registration (Laravel 11+) +- Panel providers remain registered in `bootstrap/providers.php` (no changes required for this feature unless adding a new provider). + +### Livewire / Filament version safety +- Livewire v4.0+ (required by Filament v5) is in use. + +### Asset strategy +- No new assets expected. If new panel assets are introduced during implementation, ensure deployment runs `php artisan filament:assets`. + +### Destructive actions +- None introduced in this feature. + +### Constitution re-check (post-design) +- ✅ Inventory-first / Snapshots-second: unaffected (UI-only semantics). +- ✅ Read/write separation: this feature is read-only. +- ✅ Graph contract path: no Graph calls added. +- ✅ Tenant isolation: badge mapping is pure and uses already-available tenant-scoped data. +- ✅ Run observability: only consumes existing run records; does not introduce new long-running work. +- ✅ Data minimization: no new payload storage. + +**Gate status (post-design)**: PASS + +## Phase 2 — Implementation Plan (next) + +### Story 1 (P1): Trustworthy status/health badges everywhere +- Introduce a central badge semantics layer for status/health domains (runs, findings status, tenant status, booleans, availability). +- Migrate all status-like badge surfaces suite-wide to the centralized mapping, prioritizing: + - Monitoring/Operations list surfaces + - Inventory sync runs and backup schedule runs + - Restore runs + - Findings status +- Ensure the invariant: success/completed is never presented as warning/attention. + +### Story 2 (P2): Readable status badges in dark mode +- Remove fragile per-page color overrides for status-like badges in Blade where present. +- Ensure status-like badges remain readable in dark mode and icons do not appear disabled unless intentionally neutral. + +### Story 3 (P3): Consistency stays enforced over time +- Add mapping tests per domain (including drift severity mapping and “success is never warning” invariants). +- Add a lightweight guard test to detect newly introduced ad-hoc status/health or severity/risk badge mappings. diff --git a/specs/059-unified-badges/quickstart.md b/specs/059-unified-badges/quickstart.md new file mode 100644 index 0000000..baad294 --- /dev/null +++ b/specs/059-unified-badges/quickstart.md @@ -0,0 +1,29 @@ +# Quickstart — Unified Badge System (v1) + +## Prereqs +- Run everything via Sail. + +## Setup +- `vendor/bin/sail up -d` +- `vendor/bin/sail composer install` + +## Run tests (targeted) +Existing safety nets to keep green: +- `vendor/bin/sail artisan test tests/Feature/Monitoring/OperationsDbOnlyTest.php` +- `vendor/bin/sail artisan test tests/Feature/Monitoring/OperationsTenantScopeTest.php` + +When the feature is implemented, add + run: +- Badge mapping tests (new). +- Status-like ad-hoc mapping guard test (new). + +## Manual QA (tenant-scoped) +- Operations/Monitoring: queued/running/completed + outcome badges are consistent and success is never warning. +- Drift findings: severity mapping is consistent (low=neutral, medium=warning, high=danger). +- Restore runs: all lifecycle statuses render consistently across list + detail. +- Dark mode: status-like badges remain readable; icons don’t appear disabled unless intentionally neutral. + +## Frontend assets +If UI changes don’t show: +- `vendor/bin/sail npm run dev` +- or `vendor/bin/sail npm run build` + diff --git a/specs/059-unified-badges/research.md b/specs/059-unified-badges/research.md new file mode 100644 index 0000000..853091b --- /dev/null +++ b/specs/059-unified-badges/research.md @@ -0,0 +1,62 @@ +# Research — Unified Badge System (Single Source of Truth) v1 + +## Goal +Standardize status/health and severity/risk badge semantics suite-wide so operators can reliably scan the admin UI without misread signals (for example, “success” never appearing as warning). + +V1 scope explicitly excludes tag/category chips (policy type/platform/environment). + +## Existing Code & Patterns (to reuse) + +### Filament badge surfaces (current) +- Tables already use `TextColumn::badge()` in many places. +- Ad-hoc status and severity mapping exists in several hotspots, for example: + - `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Pages/Monitoring/Operations.php` + - `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Widgets/Dashboard/RecentDriftFindings.php` + - `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/forms/components/restore-run-checks.blade.php` + +### Guard test pattern (current) +- The repo already uses Pest “guard” tests that scan the codebase for forbidden patterns: + - `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Guards/NoLegacyBulkOperationsTest.php` + +### Status / severity sources (current) +Status-like values already exist in models/enums and must remain the source of truth for meaning: +- Operation runs: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Models/OperationRun.php` (status) + outcome usage in UI. +- Inventory sync runs: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Models/InventorySyncRun.php` (status constants). +- Backup schedule runs: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Models/BackupScheduleRun.php` (status constants). +- Entra group sync runs: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Models/EntraGroupSyncRun.php` (status constants). +- Restore runs: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/RestoreRunStatus.php` (enum). +- Findings: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Models/Finding.php` (severity + status constants). + +## Key Decisions + +### Decision: Centralize status-like badge semantics behind domain mappers +- **Decision**: Introduce a central badge semantics layer that maps a “domain” + “value” to a single badge meaning (label + color meaning + optional icon). +- **Rationale**: Eliminates drift from per-page match blocks and keeps UI semantics testable and reviewable. +- **Alternatives considered**: + - **Inline mappings per resource/widget**: rejected (drifts quickly; hard to enforce). + - **Config-only mappings**: rejected for v1 (harder to type-check; still needs a rendering abstraction). + - **DB-stored mappings**: rejected (adds runtime dependency and migration/tenant complexity for no user value). + +### Decision: V1 scope is “status-like” only (status/health + severity/risk) +- **Decision**: V1 migrates status-like badges suite-wide; tag/category chips are deferred. +- **Rationale**: Status/health and severity/risk are the highest-risk trust killers when inconsistent; tags are valuable but less safety-critical and more domain-specific. + +### Decision: Canonical drift severity meanings +- **Decision**: Drift finding severity mapping is canonical: low = neutral, medium = warning, high = danger. +- **Rationale**: Severity is a risk/attention signal; “low” should not appear as “success”. + +### Decision: No severity taxonomy changes in v1 +- **Decision**: Do not add/rename severity levels (for example, do not introduce “critical” in v1). +- **Rationale**: This feature standardizes rendering semantics; changing underlying severity taxonomy is a separate scope and needs domain review. + +### Decision: Enforcement is tests + a lightweight guard +- **Decision**: Add: + - Mapping tests per domain (including invariants like “success is never warning”). + - A lightweight guard test that flags newly introduced ad-hoc mappings for status/health and severity/risk. +- **Rationale**: Mapping tests prove correctness; the guard prevents regressions and enforces the single-source-of-truth rule. +- **Alternatives considered**: + - **Strict guard banning any badge usage not from the central system**: rejected (too brittle; would block deferred tag/category chip work and legitimate non-status uses). + +## Open Questions +None — remaining work is implementation-time discovery of all status-like badge surfaces to migrate. + diff --git a/specs/059-unified-badges/spec.md b/specs/059-unified-badges/spec.md new file mode 100644 index 0000000..f10f0c2 --- /dev/null +++ b/specs/059-unified-badges/spec.md @@ -0,0 +1,151 @@ +# Feature Specification: Unified Badge System (Single Source of Truth) v1 + +**Feature Branch**: `059-unified-badges` +**Created**: 2026-01-22 +**Status**: Draft +**Input**: Suite-wide badge/chip standardization so the same underlying value always renders with the same meaning (label + color + optional icon) across tables, dashboards/KPIs, and detail views; tenant-safe and DB-only at render time where required. + +## Clarifications + +### Session 2026-01-22 + +- Q: What is the v1 migration coverage target? → A: Status-like badges suite-wide; tag/category chips later. +- Q: What counts as “status-like” for v1 scope? → A: Status/health plus severity/risk signals. +- Q: Should v1 introduce any new severity levels (e.g., “critical”), or standardize the existing severity values only? → A: Standardize existing severity values only (no new levels in v1). +- Q: Should v1 include an automated “no ad-hoc badge semantics” guard beyond mapping tests? → A: Yes — tests plus a lightweight automated guard that flags ad-hoc mappings. +- Q: What is the canonical meaning for drift finding severity (low | medium | high) in v1? → A: low = neutral, medium = warning, high = danger. + +## User Scenarios & Testing *(mandatory)* + + + +### User Story 1 - Trustworthy status badges everywhere (Priority: P1) + +As a tenant admin, I can trust that status/health and severity/risk badges mean the same thing everywhere in the admin panel, so I can scan lists quickly and make the right decision. + +**Why this priority**: Inconsistent badge semantics create operational mistakes (false “success”, missed failures) and erode trust in the UI. + +**Independent Test**: View a representative set of pages that contain status/health and severity/risk badges (lists + detail views) and confirm the same underlying value always uses the same label and visual meaning across pages. + +**Acceptance Scenarios**: + +1. **Given** a run is in the “queued” state, **When** I view it in any table, dashboard list, or detail view, **Then** it is clearly shown as “Queued” with a consistent visual meaning that indicates “waiting to start”. +2. **Given** a run is in the “running” state, **When** I view it in any table, dashboard list, or detail view, **Then** it is clearly shown as “Running” with a consistent visual meaning that indicates “in progress”. +3. **Given** a run is in a successful terminal state (“succeeded” / “completed”), **When** I view it anywhere, **Then** it is shown with a consistent “success” meaning and is never shown using warning/attention colors. +4. **Given** a finding has a high-severity value, **When** I view it in any table, dashboard list, or detail view, **Then** it is shown with a consistent “high severity” meaning and is never shown as a neutral or low-attention meaning. + +--- + +### User Story 2 - Readable status badges in dark mode (Priority: P2) + +As a tenant admin, I can scan status badges in both light and dark mode without readability regressions. + +**Why this priority**: Badges are a high-density UI element; readability and correct “good/bad” signaling reduce cognitive load and prevent mistakes. + +**Independent Test**: Open key list pages and dashboards in dark mode and light mode and verify status badges remain readable without relying on fragile per-page styling overrides. + +**Acceptance Scenarios**: + +1. **Given** I use dark mode, **When** I view status-like badges on common pages, **Then** badge text and any icons remain readable and do not rely on fragile per-page styling overrides. +2. **Given** a status badge includes an icon in a dense list, **When** I view it, **Then** the icon appearance matches the badge meaning and does not appear disabled unless the status is intentionally neutral. + +--- + +### User Story 3 - Consistency stays enforced over time (Priority: P3) + +As a maintainer, I can update badge semantics in one place and have the change apply everywhere, and regressions are caught before release. + +**Why this priority**: Without enforcement, ad-hoc badge mappings quickly reappear and the UI drifts back into inconsistent meanings. + +**Independent Test**: Make a small change to a centralized badge definition and confirm it affects multiple UI surfaces; introduce a deliberately inconsistent mapping and confirm automated validation fails. + +**Acceptance Scenarios**: + +1. **Given** a new status value is introduced, **When** it is not yet defined in the central badge system, **Then** it displays with a safe “unknown” meaning rather than being misrepresented as success or warning. +2. **Given** a developer attempts to reintroduce an ad-hoc badge mapping, **When** automated validation runs, **Then** it is detected and fails until the centralized definition is used. + +--- + +[Add more user stories as needed, each with an assigned priority] + +### Edge Cases + +- A record has an unrecognized/legacy status value (null/empty/unknown string). +- A record has an unrecognized/legacy severity/risk value. +- The same status appears on multiple pages (list + detail) and must remain consistent. +- A status value exists across multiple “domains” (e.g., “completed” used in different workflows) and must not be conflated if meanings differ. +- Dark mode and high-contrast settings reduce readability of badge text or icons. +- A page that must remain read-only/DB-only accidentally introduces side effects during render (for example, remote calls or background work). +- Tenant switching occurs mid-session and badges must not leak cross-tenant data. + +## Requirements *(mandatory)* + +**Constitution alignment (required):** If this feature introduces any Microsoft Graph calls, any write/change behavior, +or any long-running/queued/scheduled work, the spec MUST describe contract registry updates, safety gates +(preview/confirmation/audit), tenant isolation, run observability (`OperationRun` type/identity/visibility), and tests. +If security-relevant DB-only actions intentionally skip `OperationRun`, the spec MUST describe `AuditLog` entries. + + + +### Functional Requirements + +- **FR-001**: The system MUST define badge semantics centrally for each status badge “domain” used in the admin UI, including: label, color meaning, and (when applicable) an icon. +- **FR-002**: The system MUST apply the centralized status badge semantics consistently across all status badge surfaces (tables, dashboards/KPIs, and detail views) so the same underlying value always renders with the same meaning. +- **FR-003**: The system MUST clearly distinguish “status-like” badges (status/health and severity/risk signals) from tag/category chips so scope boundaries are unambiguous; v1 MUST standardize status-like badges suite-wide. +- **FR-004**: The system MUST standardize the canonical meanings for run-like statuses at minimum: queued, running, succeeded/completed, partial, failed. +- **FR-005**: Warning/attention colors (e.g., orange/yellow) MUST be reserved for “queued / needs attention / partial / in progress” meanings and MUST NOT represent success/completed outcomes. +- **FR-006**: Badge rendering MUST remain tenant-safe: it must not display cross-tenant data and must rely only on data already available in the current tenant context. +- **FR-007**: For designated DB-only pages (for example, Monitoring/Operations views), badge rendering MUST NOT trigger outbound network requests, background jobs, or other side effects during render or during automatic refresh/polling. +- **FR-008**: Badge rendering MUST be performant and predictable: it must not require additional data lookups at view time and must not introduce noticeable delays on high-row-count tables. +- **FR-009**: In dense tables, badges MAY include icons for scanability; when icons are shown, they MUST not appear disabled/gray unless the badge itself is intentionally neutral. +- **FR-010**: The system MUST provide a safe default for unrecognized values (neutral + clearly labeled as unknown) to avoid misleading operators. +- **FR-011**: The admin UI MUST be migrated so existing status-like badges/chips use the centralized system across the suite; tag/category chips are explicitly out of scope for v1 migration and may remain unchanged. +- **FR-012**: The delivery MUST include automated regression checks that validate canonical mappings (including “success is never warning”) and prevent reintroducing ad-hoc badge semantics. +- **FR-013**: This change MUST be limited to badge/chip rendering semantics; it MUST NOT change underlying workflow logic, status definitions, or page layouts beyond what is required to standardize badge rendering. +- **FR-014**: Severity/risk badges (for example, findings severity) MUST be standardized and rendered consistently across all in-scope pages. +- **FR-015**: The system MUST NOT introduce new severity levels as part of this feature; it MUST standardize and render existing severity values consistently. +- **FR-016**: The delivery MUST include a lightweight automated guard that detects newly introduced ad-hoc status/health or severity/risk badge semantics and blocks release until the centralized system is used. +- **FR-017**: Drift finding severity MUST have a canonical meaning: low = neutral, medium = warning, high = danger. + +### Assumptions & Dependencies + +- Existing status values and business meanings are already established; this feature standardizes how they are presented, not what they mean. +- A defined set of status-like badge domains exists across the suite (runs, findings status, tenant status, availability, enabled/disabled, severity/risk); any newly discovered status-like domains will be included in the v1 standardization scope. +- Dark mode is supported and is considered in acceptance for badge readability. +- Tag/category chip standardization (policy type/platform/environment) is deferred to a later version. +- Severity level changes (such as adding “critical”) are deferred to a later version. + +### Key Entities *(include if feature involves data)* + +- **Badge Domain**: A named category of values that share a consistent badge meaning (for example, “Operation run status”). +- **Badge Definition**: The centralized mapping for a domain’s values to label + color meaning + optional icon. +- **Status Badge**: A badge that communicates progress/outcome/health or severity/risk (for example, queued/running/succeeded). +- **Tag Badge**: A badge that communicates categorization/metadata (for example, platform/type/environment). + +## Success Criteria *(mandatory)* + + + +### Measurable Outcomes + +- **SC-001**: For each defined status badge domain, the same value renders with the same label and visual meaning across all in-scope pages in 100% of validation runs. +- **SC-002**: Across the in-scope admin UI, 0 instances exist where a success/completed outcome is presented using a warning/attention badge meaning. +- **SC-003**: Viewing designated DB-only pages triggers 0 outbound network requests and 0 background work as a side effect of badge rendering, in 100% of regression runs. +- **SC-004**: Status-badge-related UI regressions (incorrect label/color/icon meaning) decrease by at least 80% in the 30 days after release compared to the previous 30 days. diff --git a/specs/059-unified-badges/tasks.md b/specs/059-unified-badges/tasks.md new file mode 100644 index 0000000..a8c104d --- /dev/null +++ b/specs/059-unified-badges/tasks.md @@ -0,0 +1,176 @@ +--- +description: "Task list for feature implementation" +--- + +# Tasks: Unified Badge System (Single Source of Truth) v1 + +**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/` + +**Tests**: Required (Pest) — this feature changes runtime UI semantics and adds regression guardrails. + +--- + +## Phase 1: Setup (Shared Infrastructure) + +- [X] T001 Confirm feature inputs exist: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/spec.md`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/plan.md` +- [X] T002 Confirm Phase 0/1 artifacts exist: `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/research.md`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/data-model.md`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/contracts/badge-semantics.md`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/contracts/guardrails.md`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/quickstart.md` + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Establish the centralized badge semantics layer that all user stories depend on. + +- [X] T003 Create badge value object in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/BadgeSpec.php` +- [X] T004 Create badge domain + mapper contracts in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/BadgeDomain.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/BadgeMapper.php` +- [X] T005 Create central resolver/registry in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/BadgeCatalog.php` (safe unknown fallback; no side effects) +- [X] T006 Create Filament + Blade helper closures in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/BadgeRenderer.php` (table/infolist helpers for status-like badges) +- [X] T007 Add foundational unit coverage for unknown fallback + allowed color set in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/BadgeCatalogTest.php` + +**Checkpoint**: Central badge semantics infrastructure exists and is covered. + +--- + +## Phase 3: User Story 1 — Trustworthy status/health + severity/risk badges everywhere (Priority: P1) 🎯 MVP + +**Goal**: Status-like values (status/health and severity/risk) render consistently across the suite, using central semantics. + +**Independent Test**: Run badge mapper tests and verify key pages (Operations + Drift findings + Restore runs) show consistent meanings, including “success is never warning”. + +### Tests (US1) + +- [X] T008 [P] [US1] Add OperationRun badge mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/OperationRunBadgesTest.php` +- [X] T009 [P] [US1] Add Finding status + severity mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/FindingBadgesTest.php` +- [X] T010 [P] [US1] Add RestoreRun status mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RestoreRunBadgesTest.php` +- [X] T011 [P] [US1] Add InventorySyncRun + BackupScheduleRun mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RunStatusBadgesTest.php` + +### Implementation (US1) + +- [X] T012 [US1] Implement OperationRun status/outcome badge domains in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/OperationRunStatusBadge.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/OperationRunOutcomeBadge.php` +- [X] T013 [US1] Implement drift finding severity badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/FindingSeverityBadge.php` (low=neutral, medium=warning, high=danger) +- [X] T014 [US1] Implement finding status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/FindingStatusBadge.php` +- [X] T015 [US1] Implement RestoreRun status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/RestoreRunStatusBadge.php` +- [X] T016 [US1] Implement InventorySyncRun status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/InventorySyncRunStatusBadge.php` +- [X] T017 [US1] Implement BackupScheduleRun status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/BackupScheduleRunStatusBadge.php` +- [X] T018 [US1] Implement EntraGroupSyncRun status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/EntraGroupSyncRunStatusBadge.php` +- [X] T019 [US1] Implement status-like boolean badge domains in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/BooleanEnabledBadge.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/BooleanHasErrorsBadge.php` + +### Migration (US1) + +- [X] T020 [P] [US1] Migrate Operations resource badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/OperationRunResource.php` (remove ad-hoc `statusColor()` / `outcomeColor()` logic) +- [X] T021 [P] [US1] Migrate Monitoring Operations table badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Pages/Monitoring/Operations.php` (remove `->colors([...])`) +- [X] T022 [P] [US1] Migrate dashboard “Recent Operations” badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Widgets/Dashboard/RecentOperations.php` +- [X] T023 [P] [US1] Migrate dashboard “Recent Drift Findings” severity/status badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Widgets/Dashboard/RecentDriftFindings.php` +- [X] T024 [P] [US1] Migrate Finding resource status/severity badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/FindingResource.php` +- [X] T025 [P] [US1] Migrate Inventory sync run status + had_errors badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/InventorySyncRunResource.php` +- [X] T026 [P] [US1] Migrate backup schedule “last run status” and runs relation manager badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/BackupScheduleResource.php` and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/BackupScheduleResource/RelationManagers/BackupScheduleRunsRelationManager.php` (leave tag-like frequency badge unchanged in v1) +- [X] T027 [P] [US1] Migrate Entra group sync run status badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/EntraGroupSyncRunResource.php` +- [X] T028 [P] [US1] Migrate Restore run status badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/RestoreRunResource.php` +- [X] T029 [P] [US1] Migrate restore run check severity badges to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/forms/components/restore-run-checks.blade.php` +- [X] T030 [US1] Sweep + migrate remaining status-like badge semantics in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Livewire/`, and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/` (status/health + severity/risk only; tag/category chips explicitly out of scope for v1) + +**Checkpoint**: US1 is shippable as an MVP (status-like badges consistent across key surfaces). + +--- + +## Phase 4: User Story 2 — Readable status badges in dark mode (Priority: P2) + +**Goal**: Status-like badges remain readable in dark mode, without custom Tailwind chip overrides. + +**Independent Test**: Open restore preview/results and other badge-heavy views in dark mode and confirm badges remain readable and consistent. + +### Tests (US2) + +- [X] T031 [P] [US2] Add restore preview/results decision/status mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RestoreUiBadgesTest.php` + +### Implementation (US2) + +- [X] T032 [US2] Introduce restore preview decision badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/RestorePreviewDecisionBadge.php` +- [X] T033 [US2] Introduce restore results status badge domain in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Support/Badges/Domains/RestoreResultStatusBadge.php` +- [X] T034 [US2] Replace custom Tailwind decision chips with Filament badges in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/infolists/entries/restore-preview.blade.php` (status-like chips only; keep policy type/platform tags as-is) +- [X] T035 [US2] Replace custom Tailwind result/status chips with Filament badges in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/infolists/entries/restore-results.blade.php` (status-like chips only) + +**Checkpoint**: Dark mode readability is preserved on badge-heavy restore views. + +--- + +## Phase 5: User Story 3 — Consistency stays enforced over time (Priority: P3) + +**Goal**: Ad-hoc status-like badge semantics cannot be reintroduced without failing automated checks. + +**Independent Test**: Introduce an ad-hoc status/severity mapping in a Filament surface and confirm the guard test fails. + +### Tests + Guard (US3) + +- [X] T036 [US3] Add “no ad-hoc status-like badge semantics” guard test in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Guards/NoAdHocStatusBadgesTest.php` (pattern-based; allowlist tag/category chips) + +**Checkpoint**: Guardrails prevent drift from reappearing. + +--- + +## Phase 6: Polish & Cross-Cutting Concerns + +- [X] T037 [P] Run formatting on changed files via `vendor/bin/sail php /Users/ahmeddarrazi/Documents/projects/TenantAtlas/vendor/bin/pint --dirty` +- [X] T038 Run targeted tests via `vendor/bin/sail artisan test /Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/ /Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Guards/NoAdHocStatusBadgesTest.php` +- [X] T039 Run quickstart verification steps from `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/specs/059-unified-badges/quickstart.md` +- [X] T040 [P] Add BackupSet status mapping tests in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/BackupSetBadgesTest.php` (completed must be success) +- [X] T041 Migrate BackupSet status badge to central mapping in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/app/Filament/Resources/BackupSetResource.php` (completed must be green) +- [X] T042 Sweep for any remaining `completed` status-like badges rendered as warning and migrate to BadgeCatalog +- [X] T043 Fix Blade compilation for restore views (replace inline `@php(...)`) in `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/infolists/entries/restore-results.blade.php`, `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/infolists/entries/restore-preview.blade.php`, and `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/resources/views/filament/forms/components/restore-run-checks.blade.php` + +--- + +## Dependencies & Execution Order + +### Dependency Graph (high-level) + +```text +Phase 1 (Setup) + ↓ +Phase 2 (Foundational badge system) + ↓ +US1 (status/health + severity/risk migration) ──┬─→ US2 (dark mode restore views) + └─→ US3 (guard test) + ↓ +Phase 6 (Polish) +``` + +### User Story Dependencies + +- US1 (P1) depends on Foundational (Phase 2) and can ship as the MVP. +- US2 (P2) depends on Foundational (Phase 2); easiest after US1 establishes the core domains. +- US3 (P3) depends on Foundational (Phase 2) and should run after the first migrations to tune allowlists and reduce false positives. + +Suggested MVP-first order: Phase 1 → Phase 2 → US1 → Phase 6 (minimum) → US2 → US3 → Phase 6 (final pass). + +--- + +## Parallel Execution Examples (per user story) + +### US1 +- T008 [P] [US1] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/OperationRunBadgesTest.php` +- T009 [P] [US1] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/FindingBadgesTest.php` +- T010 [P] [US1] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RestoreRunBadgesTest.php` +- T011 [P] [US1] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RunStatusBadgesTest.php` +- T020–T029 [P] [US1] (migration tasks; different files) + +### US2 +- T031 [P] [US2] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/RestoreUiBadgesTest.php` + +### US3 +- T036 [US3] `/Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Feature/Guards/NoAdHocStatusBadgesTest.php` + +--- + +## Implementation Strategy + +### MVP First (US1 only) + +1. Complete Phase 1 + Phase 2 +2. Implement US1 migrations for Operations + Drift findings + Restore runs first (T020–T029) +3. Run: `vendor/bin/sail artisan test /Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/OperationRunBadgesTest.php` +4. Run: `vendor/bin/sail artisan test /Users/ahmeddarrazi/Documents/projects/TenantAtlas/tests/Unit/Badges/FindingBadgesTest.php` + +### Incremental Delivery + +- Finish US1 sweep (T030), then address restore Blade dark-mode hotspots in US2, then lock in regression prevention in US3. diff --git a/tests/Feature/Guards/NoAdHocStatusBadgesTest.php b/tests/Feature/Guards/NoAdHocStatusBadgesTest.php new file mode 100644 index 0000000..a513b79 --- /dev/null +++ b/tests/Feature/Guards/NoAdHocStatusBadgesTest.php @@ -0,0 +1,115 @@ +color\\s*\\(\\s*(?:fn|function)\\b/'; + $inlineLabelStartPattern = '/->formatStateUsing\\s*\\(\\s*(?:fn|function)\\b/'; + + $forbiddenPlainPatterns = [ + '/\\bBadgeColumn::make\\b/', + '/->colors\\s*\\(/', + ]; + + $lookaheadLines = 25; + + /** @var Collection $files */ + $files = collect($directories) + ->filter(fn (string $dir): bool => is_dir($dir)) + ->flatMap(function (string $dir): array { + $iterator = new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS) + ); + + $paths = []; + + foreach ($iterator as $file) { + if (! $file->isFile()) { + continue; + } + + $path = $file->getPathname(); + + if (! str_ends_with($path, '.php')) { + continue; + } + + $paths[] = $path; + } + + return $paths; + }) + ->filter(function (string $path) use ($excludedPaths, $self): bool { + if ($self && realpath($path) === $self) { + return false; + } + + foreach ($excludedPaths as $excluded) { + if (str_starts_with($path, $excluded)) { + return false; + } + } + + return true; + }) + ->values(); + + $hits = []; + + foreach ($files as $path) { + $contents = file_get_contents($path); + + if (! is_string($contents) || $contents === '') { + continue; + } + + $lines = preg_split('/\R/', $contents) ?: []; + + foreach ($lines as $index => $line) { + foreach ($forbiddenPlainPatterns as $pattern) { + if (preg_match($pattern, $line)) { + $relative = str_replace($root.'/', '', $path); + $hits[] = $relative.':'.($index + 1).' -> '.trim($line); + } + } + + if (preg_match($inlineColorStartPattern, $line)) { + $window = implode("\n", array_slice($lines, $index, $lookaheadLines)); + + if (preg_match($statusLikeTokenPattern, $window)) { + $relative = str_replace($root.'/', '', $path); + $hits[] = $relative.':'.($index + 1).' -> '.trim($line); + } + } + + if (preg_match($inlineLabelStartPattern, $line)) { + $window = implode("\n", array_slice($lines, $index, $lookaheadLines)); + + if (preg_match($statusLikeTokenPattern, $window)) { + $relative = str_replace($root.'/', '', $path); + $hits[] = $relative.':'.($index + 1).' -> '.trim($line); + } + } + } + } + + expect($hits)->toBeEmpty("Ad-hoc status-like badge semantics found:\n".implode("\n", $hits)); +}); diff --git a/tests/Unit/Badges/BackupSetBadgesTest.php b/tests/Unit/Badges/BackupSetBadgesTest.php new file mode 100644 index 0000000..bf1a2fa --- /dev/null +++ b/tests/Unit/Badges/BackupSetBadgesTest.php @@ -0,0 +1,24 @@ +label)->toBe('Running'); + expect($running->color)->toBe('info'); + + $completed = BadgeCatalog::spec(BadgeDomain::BackupSetStatus, 'completed'); + expect($completed->label)->toBe('Completed'); + expect($completed->color)->toBe('success'); + + $partial = BadgeCatalog::spec(BadgeDomain::BackupSetStatus, 'partial'); + expect($partial->label)->toBe('Partial'); + expect($partial->color)->toBe('warning'); + + $failed = BadgeCatalog::spec(BadgeDomain::BackupSetStatus, 'failed'); + expect($failed->label)->toBe('Failed'); + expect($failed->color)->toBe('danger'); +}); diff --git a/tests/Unit/Badges/BadgeCatalogTest.php b/tests/Unit/Badges/BadgeCatalogTest.php new file mode 100644 index 0000000..e8c9049 --- /dev/null +++ b/tests/Unit/Badges/BadgeCatalogTest.php @@ -0,0 +1,26 @@ +toBeInstanceOf(BadgeSpec::class); + expect($spec->label)->toBe('Unknown'); + expect($spec->color)->toBe('gray'); +}); + +it('defines the allowed Filament badge colors', function (): void { + expect(BadgeSpec::allowedColors())->toBe([ + 'gray', + 'info', + 'success', + 'warning', + 'danger', + 'primary', + ]); +}); diff --git a/tests/Unit/Badges/FindingBadgesTest.php b/tests/Unit/Badges/FindingBadgesTest.php new file mode 100644 index 0000000..40902b8 --- /dev/null +++ b/tests/Unit/Badges/FindingBadgesTest.php @@ -0,0 +1,30 @@ +label)->toBe('Low'); + expect($low->color)->toBe('gray'); + + $medium = BadgeCatalog::spec(BadgeDomain::FindingSeverity, 'medium'); + expect($medium->label)->toBe('Medium'); + expect($medium->color)->toBe('warning'); + + $high = BadgeCatalog::spec(BadgeDomain::FindingSeverity, 'high'); + expect($high->label)->toBe('High'); + expect($high->color)->toBe('danger'); +}); + +it('maps finding status values to canonical badge semantics', function (): void { + $new = BadgeCatalog::spec(BadgeDomain::FindingStatus, 'new'); + expect($new->label)->toBe('New'); + expect($new->color)->toBe('warning'); + + $acknowledged = BadgeCatalog::spec(BadgeDomain::FindingStatus, 'acknowledged'); + expect($acknowledged->label)->toBe('Acknowledged'); + expect($acknowledged->color)->toBe('gray'); +}); diff --git a/tests/Unit/Badges/OperationRunBadgesTest.php b/tests/Unit/Badges/OperationRunBadgesTest.php new file mode 100644 index 0000000..515b6ec --- /dev/null +++ b/tests/Unit/Badges/OperationRunBadgesTest.php @@ -0,0 +1,48 @@ +label)->toBe('Queued'); + expect($queued->color)->toBe('warning'); + + $running = BadgeCatalog::spec(BadgeDomain::OperationRunStatus, 'running'); + expect($running->label)->toBe('Running'); + expect($running->color)->toBe('info'); + + $completed = BadgeCatalog::spec(BadgeDomain::OperationRunStatus, 'completed'); + expect($completed->label)->toBe('Completed'); + expect($completed->color)->toBe('gray'); +}); + +it('maps operation run outcome values to canonical badge semantics', function (): void { + $pending = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'pending'); + expect($pending->label)->toBe('Pending'); + expect($pending->color)->toBe('gray'); + + $succeeded = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'succeeded'); + expect($succeeded->label)->toBe('Succeeded'); + expect($succeeded->color)->toBe('success'); + + $partial = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'partially_succeeded'); + expect($partial->label)->toBe('Partially succeeded'); + expect($partial->color)->toBe('warning'); + + $failed = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'failed'); + expect($failed->label)->toBe('Failed'); + expect($failed->color)->toBe('danger'); + + $cancelled = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'cancelled'); + expect($cancelled->label)->toBe('Cancelled'); + expect($cancelled->color)->toBe('gray'); +}); + +it('never represents a success outcome with warning/attention meaning', function (): void { + $succeeded = BadgeCatalog::spec(BadgeDomain::OperationRunOutcome, 'succeeded'); + + expect($succeeded->color)->not->toBe('warning'); +}); diff --git a/tests/Unit/Badges/PolicyBadgesTest.php b/tests/Unit/Badges/PolicyBadgesTest.php new file mode 100644 index 0000000..31bac36 --- /dev/null +++ b/tests/Unit/Badges/PolicyBadgesTest.php @@ -0,0 +1,46 @@ +label)->toBe('Full'); + expect($full->color)->toBe('success'); + + $metadataOnly = BadgeCatalog::spec(BadgeDomain::PolicySnapshotMode, 'metadata_only'); + expect($metadataOnly->label)->toBe('Metadata only'); + expect($metadataOnly->color)->toBe('warning'); +}); + +it('maps policy restore mode values to canonical badge semantics', function (): void { + $enabled = BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, 'enabled'); + expect($enabled->label)->toBe('Enabled'); + expect($enabled->color)->toBe('success'); + + $previewOnly = BadgeCatalog::spec(BadgeDomain::PolicyRestoreMode, 'preview-only'); + expect($previewOnly->label)->toBe('Preview only'); + expect($previewOnly->color)->toBe('warning'); +}); + +it('maps policy risk values to canonical badge semantics', function (): void { + $low = BadgeCatalog::spec(BadgeDomain::PolicyRisk, 'low'); + expect($low->label)->toBe('Low'); + expect($low->color)->toBe('gray'); + + $mediumHigh = BadgeCatalog::spec(BadgeDomain::PolicyRisk, 'medium-high'); + expect($mediumHigh->label)->toBe('Medium-high'); + expect($mediumHigh->color)->toBe('danger'); +}); + +it('maps ignored-at presence to canonical badge semantics', function (): void { + $notIgnored = BadgeCatalog::spec(BadgeDomain::IgnoredAt, null); + expect($notIgnored->label)->toBe('No'); + expect($notIgnored->color)->toBe('gray'); + + $ignored = BadgeCatalog::spec(BadgeDomain::IgnoredAt, '2026-01-01T00:00:00Z'); + expect($ignored->label)->toBe('Yes'); + expect($ignored->color)->toBe('warning'); +}); diff --git a/tests/Unit/Badges/RestoreRunBadgesTest.php b/tests/Unit/Badges/RestoreRunBadgesTest.php new file mode 100644 index 0000000..d4d3ab2 --- /dev/null +++ b/tests/Unit/Badges/RestoreRunBadgesTest.php @@ -0,0 +1,56 @@ +label)->toBe('Draft'); + expect($draft->color)->toBe('gray'); + + $previewed = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'previewed'); + expect($previewed->label)->toBe('Previewed'); + expect($previewed->color)->toBe('gray'); + + $queued = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'queued'); + expect($queued->label)->toBe('Queued'); + expect($queued->color)->toBe('warning'); + + $running = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'running'); + expect($running->label)->toBe('Running'); + expect($running->color)->toBe('info'); + + $completed = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'completed'); + expect($completed->label)->toBe('Completed'); + expect($completed->color)->toBe('success'); + + $partial = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'partial'); + expect($partial->label)->toBe('Partial'); + expect($partial->color)->toBe('warning'); + + $failed = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'failed'); + expect($failed->label)->toBe('Failed'); + expect($failed->color)->toBe('danger'); +}); + +it('never represents a completed outcome with warning/attention meaning', function (): void { + $completed = BadgeCatalog::spec(BadgeDomain::RestoreRunStatus, 'completed'); + + expect($completed->color)->not->toBe('warning'); +}); + +it('maps restore safety check severity values to canonical badge semantics', function (): void { + $blocking = BadgeCatalog::spec(BadgeDomain::RestoreCheckSeverity, 'blocking'); + expect($blocking->label)->toBe('Blocking'); + expect($blocking->color)->toBe('danger'); + + $warning = BadgeCatalog::spec(BadgeDomain::RestoreCheckSeverity, 'warning'); + expect($warning->label)->toBe('Warning'); + expect($warning->color)->toBe('warning'); + + $safe = BadgeCatalog::spec(BadgeDomain::RestoreCheckSeverity, 'safe'); + expect($safe->label)->toBe('Safe'); + expect($safe->color)->toBe('success'); +}); diff --git a/tests/Unit/Badges/RestoreUiBadgesTest.php b/tests/Unit/Badges/RestoreUiBadgesTest.php new file mode 100644 index 0000000..b34a43a --- /dev/null +++ b/tests/Unit/Badges/RestoreUiBadgesTest.php @@ -0,0 +1,38 @@ +label)->toBe('Created'); + expect($created->color)->toBe('success'); + + $mappedExisting = BadgeCatalog::spec(BadgeDomain::RestorePreviewDecision, 'mapped_existing'); + expect($mappedExisting->label)->toBe('Mapped existing'); + expect($mappedExisting->color)->toBe('info'); + + $failed = BadgeCatalog::spec(BadgeDomain::RestorePreviewDecision, 'failed'); + expect($failed->label)->toBe('Failed'); + expect($failed->color)->toBe('danger'); +}); + +it('maps restore results statuses to canonical badge semantics', function (): void { + $applied = BadgeCatalog::spec(BadgeDomain::RestoreResultStatus, 'applied'); + expect($applied->label)->toBe('Applied'); + expect($applied->color)->toBe('success'); + + $dryRun = BadgeCatalog::spec(BadgeDomain::RestoreResultStatus, 'dry_run'); + expect($dryRun->label)->toBe('Dry run'); + expect($dryRun->color)->toBe('info'); + + $manualRequired = BadgeCatalog::spec(BadgeDomain::RestoreResultStatus, 'manual_required'); + expect($manualRequired->label)->toBe('Manual required'); + expect($manualRequired->color)->toBe('warning'); + + $failed = BadgeCatalog::spec(BadgeDomain::RestoreResultStatus, 'failed'); + expect($failed->label)->toBe('Failed'); + expect($failed->color)->toBe('danger'); +}); diff --git a/tests/Unit/Badges/RunStatusBadgesTest.php b/tests/Unit/Badges/RunStatusBadgesTest.php new file mode 100644 index 0000000..57eff1a --- /dev/null +++ b/tests/Unit/Badges/RunStatusBadgesTest.php @@ -0,0 +1,58 @@ +label)->toBe('Pending'); + expect($pending->color)->toBe('gray'); + + $running = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'running'); + expect($running->label)->toBe('Running'); + expect($running->color)->toBe('info'); + + $success = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'success'); + expect($success->label)->toBe('Success'); + expect($success->color)->toBe('success'); + + $partial = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'partial'); + expect($partial->label)->toBe('Partial'); + expect($partial->color)->toBe('warning'); + + $failed = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'failed'); + expect($failed->label)->toBe('Failed'); + expect($failed->color)->toBe('danger'); + + $skipped = BadgeCatalog::spec(BadgeDomain::InventorySyncRunStatus, 'skipped'); + expect($skipped->label)->toBe('Skipped'); + expect($skipped->color)->toBe('gray'); +}); + +it('maps backup schedule run status values to canonical badge semantics', function (): void { + $running = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'running'); + expect($running->label)->toBe('Running'); + expect($running->color)->toBe('info'); + + $success = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'success'); + expect($success->label)->toBe('Success'); + expect($success->color)->toBe('success'); + + $partial = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'partial'); + expect($partial->label)->toBe('Partial'); + expect($partial->color)->toBe('warning'); + + $failed = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'failed'); + expect($failed->label)->toBe('Failed'); + expect($failed->color)->toBe('danger'); + + $canceled = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'canceled'); + expect($canceled->label)->toBe('Canceled'); + expect($canceled->color)->toBe('gray'); + + $skipped = BadgeCatalog::spec(BadgeDomain::BackupScheduleRunStatus, 'skipped'); + expect($skipped->label)->toBe('Skipped'); + expect($skipped->color)->toBe('gray'); +}); diff --git a/tests/Unit/Badges/TenantBadgesTest.php b/tests/Unit/Badges/TenantBadgesTest.php new file mode 100644 index 0000000..231ac89 --- /dev/null +++ b/tests/Unit/Badges/TenantBadgesTest.php @@ -0,0 +1,66 @@ +label)->toBe('Active'); + expect($active->color)->toBe('success'); + + $archived = BadgeCatalog::spec(BadgeDomain::TenantStatus, 'archived'); + expect($archived->label)->toBe('Archived'); + expect($archived->color)->toBe('gray'); + + $suspended = BadgeCatalog::spec(BadgeDomain::TenantStatus, 'suspended'); + expect($suspended->label)->toBe('Suspended'); + expect($suspended->color)->toBe('warning'); + + $error = BadgeCatalog::spec(BadgeDomain::TenantStatus, 'error'); + expect($error->label)->toBe('Error'); + expect($error->color)->toBe('danger'); +}); + +it('maps tenant app status values to canonical badge semantics', function (): void { + $ok = BadgeCatalog::spec(BadgeDomain::TenantAppStatus, 'ok'); + expect($ok->label)->toBe('OK'); + expect($ok->color)->toBe('success'); + + $consentRequired = BadgeCatalog::spec(BadgeDomain::TenantAppStatus, 'consent_required'); + expect($consentRequired->label)->toBe('Consent required'); + expect($consentRequired->color)->toBe('warning'); + + $error = BadgeCatalog::spec(BadgeDomain::TenantAppStatus, 'error'); + expect($error->label)->toBe('Error'); + expect($error->color)->toBe('danger'); +}); + +it('maps tenant RBAC status values to canonical badge semantics', function (): void { + $configured = BadgeCatalog::spec(BadgeDomain::TenantRbacStatus, 'configured'); + expect($configured->label)->toBe('Configured'); + expect($configured->color)->toBe('success'); + + $manual = BadgeCatalog::spec(BadgeDomain::TenantRbacStatus, 'manual_assignment_required'); + expect($manual->label)->toBe('Manual assignment required'); + expect($manual->color)->toBe('warning'); + + $notConfigured = BadgeCatalog::spec(BadgeDomain::TenantRbacStatus, 'not_configured'); + expect($notConfigured->label)->toBe('Not configured'); + expect($notConfigured->color)->toBe('gray'); +}); + +it('maps tenant permission status values to canonical badge semantics', function (): void { + $granted = BadgeCatalog::spec(BadgeDomain::TenantPermissionStatus, 'granted'); + expect($granted->label)->toBe('Granted'); + expect($granted->color)->toBe('success'); + + $missing = BadgeCatalog::spec(BadgeDomain::TenantPermissionStatus, 'missing'); + expect($missing->label)->toBe('Missing'); + expect($missing->color)->toBe('warning'); + + $error = BadgeCatalog::spec(BadgeDomain::TenantPermissionStatus, 'error'); + expect($error->label)->toBe('Error'); + expect($error->color)->toBe('danger'); +});