docs(openspec): add SDD artifact trails for bt-device-identity and alarm-volume-ramp-restore
In-progress artifact sets from the current SDD cycles: exploration, proposal, spec, design, tasks, and verify reports as produced so far. Also drops a leftover working copy of eq-device-disconnect-revert whose contents were already committed under changes/archive/.
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
# Verification Report
|
||||
|
||||
**Change**: bt-device-identity
|
||||
**Version**: N/A (spec has no version field)
|
||||
**Mode**: Strict TDD (Dart layers, cross-referenced against real `flutter test` runs) / code-inspection only for Kotlin (no Android/Kotlin test harness exists in this repo - confirmed, `test/` tree is Dart-only)
|
||||
**Commits verified**: `aef4e02` (Kotlin plumbing) + `b17c582` (Dart state/display/migration). Working tree clean of `lib/`/`android/` changes at verification time.
|
||||
|
||||
> **Scenario count correction**: the verification brief cited "24 scenarios." Independent counts from both the engram spec artifact (`sdd/bt-device-identity/spec`, obs #2304) and the file `openspec/changes/bt-device-identity/spec.md` (`grep -c "^##### Scenario:"`) agree: **21 scenarios across 7 requirement groups**. This report verifies against the confirmed 21.
|
||||
|
||||
---
|
||||
|
||||
## Completeness
|
||||
|
||||
| Metric | Value |
|
||||
|--------|-------|
|
||||
| Tasks total (tasks.md, grep-verified) | 56 |
|
||||
| Tasks complete `[x]` (Phase 1-5) | 40 |
|
||||
| Tasks N/A with inline justification (Phase 6, l10n) | 6 |
|
||||
| Tasks pending - human sign-off gate (Phase 7, manual/on-device QA) | 10 |
|
||||
| Tasks incomplete/unaccounted | 0 |
|
||||
|
||||
Independently re-counted via `grep -c` against the live `tasks.md` file - matches apply-progress self-reported totals exactly (40 checked, 16 unmarked = 6 N/A + 10 pending).
|
||||
|
||||
---
|
||||
|
||||
## Build & Tests Execution
|
||||
|
||||
**Build**: not run (per verification scope - `flutter build` explicitly excluded).
|
||||
|
||||
**Tests**: independently re-executed in this pass, not taken on the apply-progress report alone.
|
||||
|
||||
```text
|
||||
flutter test test/estado/estado_ecualizador_test.dart test/servicios/servicio_ecualizador_test.dart \
|
||||
test/pantallas/pantalla_ajustes_test.dart test/servicios/servicio_dispositivo_audio_test.dart \
|
||||
test/servicios/servicio_dispositivo_audio_real_test.dart test/servicios/servicio_dispositivo_audio_toggle_test.dart
|
||||
-> 00:11 +101: All tests passed!
|
||||
```
|
||||
|
||||
Per-file breakdown (re-run individually to cross-check against apply-progress claimed counts):
|
||||
|
||||
| File | Actual | Claimed (apply-progress) | Match |
|
||||
|------|--------|---------------------------|-------|
|
||||
| servicio_dispositivo_audio_real_test.dart | 10/10 | 10/10 | Yes |
|
||||
| servicio_dispositivo_audio_test.dart | 7/7 | 7/7 | Yes |
|
||||
| servicio_dispositivo_audio_toggle_test.dart | 5/5 | 5/5 | Yes |
|
||||
| estado_ecualizador_test.dart | 48/48 | 48/48 | Yes |
|
||||
| servicio_ecualizador_test.dart | 20/20 | 20/20 | Yes |
|
||||
| pantalla_ajustes_test.dart | 11/11 | 11/11 | Yes |
|
||||
|
||||
**flutter analyze**: 0 issues, whole project ("No issues found!").
|
||||
|
||||
**Coverage**: not run this pass (line-coverage percent was not part of the verification checklist; scenario-level mapping below is more precise for this purpose).
|
||||
|
||||
---
|
||||
|
||||
## Spec Compliance Matrix
|
||||
|
||||
| # | Requirement | Scenario | Test / Evidence | Result |
|
||||
|---|-------------|----------|------------------|--------|
|
||||
| 1 | BLUETOOTH_CONNECT at point-of-intent | permission requested on UI open (manual QA) | Deferred - tasks.md 7.2 | DEFERRED |
|
||||
| 2 | BLUETOOTH_CONNECT at point-of-intent | permission not requested at app launch (manual QA) | Deferred - tasks.md 7.1 | DEFERRED |
|
||||
| 3 | Placeholder MAC guarded | permission granted yields real MAC id (Dart-testable) | Covered implicitly by extensive real-MAC-id tests (3.1, D.4, 5.5a/b, etc.) - all pass | COMPLIANT |
|
||||
| 4 | Placeholder MAC guarded | permission granted yields real MAC id (manual QA, Kotlin) | Deferred - tasks.md 7.3; code-inspected: guard present, MAC-present path unchanged | DEFERRED (code OK) |
|
||||
| 5 | Placeholder MAC guarded | permission denied yields composite fallback, no crash (Dart-testable) | estado_ecualizador_test.dart 3.4 - PASS | COMPLIANT |
|
||||
| 6 | Placeholder MAC guarded | multiple denied-permission devices do not collide (Dart-testable) | estado_ecualizador_test.dart 3.6 - PASS (see SUGGESTION-1 re: wording nuance) | COMPLIANT |
|
||||
| 7 | Placeholder MAC guarded | placeholder never surfaces as a stored id (Dart-testable) | Structural: Kotlin guard (code-inspected) prevents emission + migration tests 5.1/5.9 purge legacy residue. No single dedicated processing test, but cross-layer enforced | COMPLIANT |
|
||||
| 8 | Platform-name cache feeds display | platform name displays, no custom rename (Dart-testable) | pantalla_ajustes_test.dart 4.1 - PASS | COMPLIANT |
|
||||
| 9 | Platform-name cache feeds display | custom rename overrides platform name (Dart-testable) | pantalla_ajustes_test.dart 4.4 - PASS (self-documented as triangulation, not fail-before RED; still a valid passing assertion) | COMPLIANT |
|
||||
| 10 | Platform-name cache feeds display | no platform name yet falls back to raw id (Dart-testable) | pantalla_ajustes_test.dart 4.5 - PASS | COMPLIANT |
|
||||
| 11 | Rename survives re-pair | rename persists after re-pair (Dart-testable via fake device stream) | No covering test found anywhere in the repo (searched both diffs and full test/ tree) | UNTESTED - CRITICAL |
|
||||
| 12 | Rename survives re-pair | rename persists after re-pair (manual QA) | Deferred - tasks.md 7.5 (correctly scoped, unchecked) | DEFERRED |
|
||||
| 13 | No duplicate entries on transient reports | repeated event for known id is a no-op (Dart-testable) | estado_ecualizador_test.dart 3.7 - PASS | COMPLIANT |
|
||||
| 14 | No duplicate entries on transient reports | transient non-BT id does not corrupt BT entry (Dart-testable) | estado_ecualizador_test.dart 3.8 - PASS | COMPLIANT |
|
||||
| 15 | One-time guarded migration | migration removes only exact placeholder entries (Dart-testable) | servicio_ecualizador_test.dart 5.1 - PASS | COMPLIANT |
|
||||
| 16 | One-time guarded migration | matrix keys purge only placeholder segment (Dart-testable) | servicio_ecualizador_test.dart 5.2 - PASS | COMPLIANT |
|
||||
| 17 | One-time guarded migration | near-miss keys preserved (Dart-testable) | servicio_ecualizador_test.dart 5.3 - PASS | COMPLIANT |
|
||||
| 18 | One-time guarded migration | migration runs once (Dart-testable) | servicio_ecualizador_test.dart 5.4 - PASS (strengthened: re-seed-between-calls proves true flag short-circuit) | COMPLIANT |
|
||||
| 19 | One-time guarded migration | no-op when BLUETOOTH_CONNECT never requested (Dart-testable) | servicio_ecualizador_test.dart 5.5 - PASS | COMPLIANT |
|
||||
| 20 | l10n: rationale/notice strings in all locales | new keys present in every locale (arb parity, Dart-testable) | N/A - verified via diff grep: zero Text()/SnackBar/Dialog/l10n./AppLocalizations additions in either commit; zero .arb files touched | N/A (justified) |
|
||||
| 21 | l10n: rationale/notice strings in all locales | rationale copy renders before OS dialog (manual QA) | N/A - same reason; tasks.md 7.9 correctly marked effectively N/A | N/A (justified) |
|
||||
|
||||
**Compliance summary**: 15/21 COMPLIANT, 4/21 correctly DEFERRED (manual/on-device QA, Phase 7 human gate), 2/21 correctly N/A (l10n, justified), 1/21 UNTESTED (CRITICAL).
|
||||
|
||||
---
|
||||
|
||||
## Correctness (Static Evidence) - Kotlin, code-inspection only
|
||||
|
||||
| Item | Status | Notes |
|
||||
|------|--------|-------|
|
||||
| AndroidManifest.xml declares BLUETOOTH_CONNECT | Implemented | L17, verified live file |
|
||||
| requestBluetoothConnect() mirrors requestPostNotificationsPermission() | Implemented | Structural match: SDK-version gate, checkSelfPermission, requestPermissions, return true |
|
||||
| Placeholder MAC guarded via named constant | Implemented | bluetoothMacPlaceholder = "02:00:00:00:00:00" (L41), used in guard condition, no bare string literal in the check itself |
|
||||
| Composite fallback colon-sanitizes productName | Implemented | .replace(":", "-"), blank becomes "unknown" |
|
||||
| Re-emit after grant (ADR-2) | Implemented | onRequestPermissionsResult branch for bluetoothConnectPermissionRequestCode calls getActiveAudioDevice() + audioDevicesSink success on PERMISSION_GRANTED; no-op on denial |
|
||||
| Old dead "00:00:00:00:00:00" fallback removed | Confirmed removed | Zero occurrences anywhere in MainActivity.kt (grep-verified) - replaced, not extended, as required |
|
||||
| Request codes distinct | Confirmed | 4821 (visualizer) / 4822 (notifications) / 4823 (bluetooth) - no collision |
|
||||
|
||||
---
|
||||
|
||||
## Coherence (Design) - ADR 1-6
|
||||
|
||||
| Decision | Followed? | Notes |
|
||||
|----------|-----------|-------|
|
||||
| ADR-1: request BLUETOOTH_CONNECT on UI open via new channel call | Yes, with a signed-off trigger-point deviation | Fires on toggle-turn-ON tap (_alternarMultiDevice), not a StatefulWidget on-build hook. Explicitly flagged in Batch 1 and re-confirmed in Batch 2; satisfies both spec MUSTs without an unrequested widget-type conversion |
|
||||
| ADR-2: re-emit device list after grant | Yes | Verified in onRequestPermissionsResult (Kotlin) |
|
||||
| ADR-3: placeholder guard + composite fallback bt_a2dp:name:$safeProductName | Yes | Verified in deviceToMap(); Dart's _prefijoPlaceholderCompuesto = 'bt_a2dp:name:' matches exactly, cross-layer consistent. Note: this ADR itself deviates from the spec requirement literal wording, see WARNING-1 |
|
||||
| ADR-4: in-memory-only platform-name cache, not persisted | Yes | _nombresPlataforma is a plain Map, no SharedPreferences key added |
|
||||
| ADR-5: migration location (ServicioEcualizador.cargar()) + guard flag eq_placeholder_purge_done_v1 | Yes | Verified; purges exact literal from all 3 maps as specified |
|
||||
| ADR-6: transient duplicate-entry guard, combined with pre-existing esBase guard | Yes | Verified: !esBase && !esPlaceholderCompuesto && !_presetsDispositivo.containsKey(...) - correctly reads and extends the CURRENT code (post-eq-device-disconnect-revert), not a stale design-time snapshot |
|
||||
|
||||
---
|
||||
|
||||
## TDD Compliance (Strict TDD Mode active)
|
||||
|
||||
| Check | Result | Details |
|
||||
|-------|--------|---------|
|
||||
| TDD Evidence reported | Yes | Full TDD Cycle Evidence table present in apply-progress for all Phase 2-5 Dart tasks. Phase 1 (Kotlin) correctly has none, no test harness exists, explicitly scoped as code-inspection-only in both design and tasks |
|
||||
| All tasks have tests | Yes (Dart scope) | 20 genuinely new tests across 5 files, matching every RED/GREEN task pair in tasks.md Phase 2-5 |
|
||||
| RED confirmed (tests exist) | Yes | All claimed test files and test names verified present in the actual diff/live files |
|
||||
| GREEN confirmed (tests pass) | Yes | 101/101 independently re-executed this pass, not taken on trust |
|
||||
| Triangulation adequate | Yes, with 4 explicitly self-labeled approval/regression-lock tests (3.7, 3.8, 4.4, 4.5) | Correctly and transparently distinguished from true fail-before/pass-after RED in both tasks.md and apply-progress, no overclaiming found |
|
||||
| Safety Net for modified files | Yes, with 2 numeric discrepancies found | See below |
|
||||
|
||||
**TDD Compliance**: 5/6 checks clean, 1 check (Safety Net) has minor reporting-accuracy issues, see WARNING-2.
|
||||
|
||||
**Safety-net cross-check** (pre-existing test counts, verified via `git show <parent>:<file> | grep -c "test("`):
|
||||
|
||||
| File | Apply-progress claim | Actual (git-verified) | Match |
|
||||
|------|----------------------|------------------------|-------|
|
||||
| servicio_dispositivo_audio_test.dart | 6/6 pre-existing | 6 | Yes |
|
||||
| pantalla_ajustes_test.dart | 7/7 pre-existing | 7 | Yes |
|
||||
| servicio_ecualizador_test.dart | 13/13 pre-existing | 13 | Yes |
|
||||
| estado_ecualizador_test.dart | 40/40 pre-existing | 43 | No, off by 3 |
|
||||
| servicio_dispositivo_audio_toggle_test.dart (ripple-fix file) | "7 pre-existing tests" (Test Summary prose) | 5 | No, off by 2 |
|
||||
|
||||
Both discrepancies are narrative/arithmetic only. Actual runtime results (48/48 and 5/5 respectively) were independently re-verified as correct in this pass. See WARNING-2.
|
||||
|
||||
---
|
||||
|
||||
## Test Layer Distribution
|
||||
|
||||
| Layer | New Tests | Files | Tool |
|
||||
|-------|-----------|-------|------|
|
||||
| Unit | 16 | servicio_dispositivo_audio_real_test.dart (3), servicio_dispositivo_audio_test.dart (1), estado_ecualizador_test.dart (5), servicio_ecualizador_test.dart (7) | flutter_test |
|
||||
| Widget | 4 | pantalla_ajustes_test.dart (4) | flutter_test (testWidgets) |
|
||||
| E2E | 0 | - | - |
|
||||
| Total new | 20 | 5 files (+1 file with a 1-method ripple-fix, 0 new tests) | |
|
||||
|
||||
---
|
||||
|
||||
## Assertion Quality
|
||||
|
||||
Reviewed all 20 new tests plus the 1 ripple-fix. No tautologies, no ghost loops, no assertion-free tests found; every test exercises real production code (emitirDispositivo -> _onDispositivoCambiado, migrarClavesPlaceholder(), cargar(), real widget pumps, mocked MethodChannel) and asserts concrete, non-trivial values.
|
||||
|
||||
| File | Line(s) | Assertion | Issue | Severity |
|
||||
|------|---------|-----------|-------|----------|
|
||||
| pantalla_ajustes_test.dart | 4.6 (solicitarPermisoBluetoothCalls counter) | expect(fakeDispositivo.solicitarPermisoBluetoothCalls, equals(0/1/1)) | Mock-call-count pattern (skill's explicit watch-list item) | SUGGESTION (justified, see notes) |
|
||||
|
||||
**Assertion quality**: 0 CRITICAL, 0 WARNING, 1 SUGGESTION (noted for transparency, not blocking, see SUGGESTION-2 for rationale).
|
||||
|
||||
---
|
||||
|
||||
## Localization (Phase 6)
|
||||
|
||||
Confirmed N/A, independently verified (not just taken from apply-progress claim):
|
||||
- `git diff aef4e02~1..b17c582 --stat -- lib/l10n/` returns empty (zero .arb files touched across both commits)
|
||||
- `git diff aef4e02~1..b17c582 -- lib/` piped through a grep for Text(, Snackbar, SnackBar, Dialog, l10n., AppLocalizations additions returns zero matches
|
||||
- tasks.md Phase 6 (6.1-6.6) correctly left as unchecked with inline "N/A" justification, not deleted, not silently marked done
|
||||
|
||||
---
|
||||
|
||||
## Phase 7 (Manual/On-Device QA)
|
||||
|
||||
All 10 tasks (7.1-7.10) correctly unchecked in tasks.md - human sign-off gate, explicitly out of scope for this code-correctness verify pass (no instrumented/emulator harness exists in this repo). Task 7.5 specifically covers the manual-QA half of the CRITICAL finding below (rename survives re-pair, on-device) and is appropriately still pending.
|
||||
|
||||
---
|
||||
|
||||
## Issues Found
|
||||
|
||||
### CRITICAL (1)
|
||||
|
||||
**CRITICAL-1 - Untested Dart-testable spec scenario: "rename persists after re-pair"**
|
||||
|
||||
Requirement group "Rename overlay survives re-pair under canonical id" (spec.md L91-106) has two scenarios: one Dart-testable, one manual/on-device QA. The manual scenario is correctly deferred (tasks.md 7.5). The Dart-testable scenario has no covering test anywhere in the repository. Confirmed by:
|
||||
- Full diff review of test/estado/estado_ecualizador_test.dart and test/pantallas/pantalla_ajustes_test.dart (only 5 + 4 new tests added this batch, none combining rename-set + disconnect + reconnect-same-MAC)
|
||||
- Full-tree search for renombrarDispositivo( call sites in tests - none is followed by a sequence of emitirDispositivo calls simulating disconnect/reconnect
|
||||
- tasks.md Phase 3/4 itself never planned a task for this composed scenario (traces back to design's own Testing Strategy section, which also never named it) - this is a planning-stage gap inherited faithfully through apply, not a deviation introduced during implementation
|
||||
- Mitigating context: the two constituent behaviors are each independently tested. Reconnect-does-not-duplicate-or-corrupt-an-entry is proven generically by the pre-existing D.4 regression test (eq-device-disconnect-revert), and custom-rename-wins-over-platform-name is proven by new test 4.4. Composing them carries low residual risk, but per verification rules a scenario is compliant only when an actual runtime test covers it; code-review confidence is not a substitute.
|
||||
|
||||
**Recommendation**: add one composed test to estado_ecualizador_test.dart (store a custom rename for a real-MAC id, emit disconnect, emit reconnect with the same id, assert the rename is still returned by nombreVisible/obtenerNombreDispositivo AND presetsDispositivo.length did not grow). Estimated effort: small, single test, no production-code changes expected (the underlying mechanism already looks correct by inspection). Route through sdd-apply for this one addition, then re-run sdd-verify before archiving.
|
||||
|
||||
### WARNING (2)
|
||||
|
||||
**WARNING-1 - Composite fallback id shape deviates from the spec requirement literal wording**
|
||||
|
||||
Spec requirement prose (spec.md L31) states the fallback id "MUST fall back to a composite bt_a2dp:<type>:<productName>-shaped id," with its own scenario example showing "bt_a2dp:8:AirPods Pro". The actual design (ADR-3) and implementation instead use "bt_a2dp:name:$safeProductName" (literal "name" segment, not the numeric type). This is a documented, deliberate design choice (ADR-3 rationale: preserves the single-leading-bt_a2dp:-segment invariant for eq_presets_matriz_v1's split-on-first-colon parsing), consistently implemented cross-layer (Kotlin's literal prefix matches Dart's _prefijoPlaceholderCompuesto exactly), and does not violate any scenario's actual testable MUST conditions (deterministic, not equal to the placeholder literal, no crash, all verified passing). Recommend a quick sign-off from the spec owner that this wording gap is accepted, since a literal re-read of the requirement text alone would suggest non-compliance.
|
||||
|
||||
**WARNING-2 - Minor arithmetic/count inaccuracies in apply-progress narrative (self-correcting pattern, no functional impact)**
|
||||
|
||||
Two small discrepancies found between apply-progress prose claims and git-verified reality (in addition to the "63 vs 56 total tasks" discrepancy apply-progress already self-corrected transparently):
|
||||
1. TDD Cycle Evidence table states "40/40 pre-existing" for the estado_ecualizador_test.dart Phase-3 safety-net row; git-verified actual pre-existing count is 43.
|
||||
2. Test Summary prose states "7 pre-existing tests in servicio_dispositivo_audio_toggle_test.dart"; the file actually contains 5.
|
||||
|
||||
Neither affects functional correctness - actual runtime results (48/48 and 5/5 respectively) were independently re-verified as accurate in this pass. Recommend correcting these two numbers in the artifact for future-reader accuracy, no code action needed.
|
||||
|
||||
### SUGGESTION (3)
|
||||
|
||||
**SUGGESTION-1 - "Does not collapse into one entry" wording is satisfied trivially, not by design**
|
||||
|
||||
Scenario "multiple denied-permission devices do not collide" (spec.md L52-57) says presetsDispositivo "MUST NOT collapse them into one entry." ADR-6's actual behavior is to create zero presetsDispositivo entries for ANY composite-placeholder id (not just to avoid collisions between them), confirmed by test 3.6 asserting presetsDispositivo.length == baseline (unchanged) after both devices connect. This technically satisfies the literal wording (zero entries is not "one collapsed entry") but is a broader suppression than a literal reading might imply (some readers might expect 2 distinct persisted entries). Recommend confirming this matches product intent, no code change implied, purely a documentation/interpretation checkpoint.
|
||||
|
||||
**SUGGESTION-2 - Mock-call-count assertion in test 4.6 (pantalla_ajustes_test.dart)**
|
||||
|
||||
expect(fakeDispositivo.solicitarPermisoBluetoothCalls, equals(...)) matches the Strict-TDD skill's explicit "mock call count" watch-list pattern. In this specific case it is justified: solicitarPermisoBluetooth() is a fire-and-forget side effect with no other externally observable consequence inside a widget test (the real effect is an OS permission dialog, unobservable in-process), so the call counter is the most direct available proxy for "did the trigger fire exactly once, and not on toggle-OFF." Flagged for transparency per protocol, not recommended for rework.
|
||||
|
||||
**SUGGESTION-3 - Pending Engram conflict markers on artifact observations**
|
||||
|
||||
Both sdd/bt-device-identity/tasks (obs #2314) and sdd/bt-device-identity/apply-progress (obs #2328) show a pending "contested by #obs-..." marker in mem_search results. Both resolved to their latest, highest-revision content when retrieved via mem_get_observation (Revisions: 3 and 2, respectively) and were used as authoritative for this report. Recommend resolving/judging these pending conflicts before sdd-archive to keep the artifact trail clean - housekeeping only, not a content-accuracy concern for this verification.
|
||||
|
||||
---
|
||||
|
||||
## Verdict
|
||||
|
||||
**FAIL** - blocked by exactly one CRITICAL finding (CRITICAL-1: untested Dart-testable spec scenario for rename-survives-re-pair). All other 20/21 scenarios are verified COMPLIANT or correctly DEFERRED/N/A; all 101 targeted tests independently re-executed and green; flutter analyze clean; all 6 design ADRs followed; l10n N/A correctly justified and independently confirmed; Phase 6/7 task states correctly reflect their true completion status. This is a narrow, well-scoped gap with a small, clearly-specified remediation, not a systemic implementation problem. Recommend one additional composed test via sdd-apply, then re-run sdd-verify before sdd-archive.
|
||||
Reference in New Issue
Block a user