Files
dashboardAPI/test/_output-manifest.md
znetsixe dc08c85409 docs(dashboardapi): output-coverage manifest + populated/degraded tests (#43)
Per .claude/rules/output-coverage.md every node ships test/_output-manifest.md
enumerating every output across every state. This manifest covers all the
outputs added by slices #34-#42 in this PRD:

- Port 0 upsert message: every key (topic, url, method, headers, payload,
  meta) with type and tested states.
- Port 1: explicit "not used" with rationale.
- Port 2: explicit "not used" with rationale.
- Structured log outputs: 5 events (regen-emitted, regen-skipped,
  manual-regen-requested, parent-panels-deduped, flows:started) with
  fields and corresponding test.
- specificClass return shapes: 6 methods with populated + degraded states.
- Anti-patterns enforced: no payload:null, absent vs null discipline,
  tab id avoidance in predicate.

- test/_output-manifest.md: the manifest.
- test/basic/slice43-output-manifest.basic.test.js: 6 cross-cutting tests
  exercising populated AND degraded states (token absent, folderUid absent,
  template missing, diff-skip, regen logging, manual regen).

Backfill manifests for other nodes tracked in IMPROVEMENTS_BACKLOG.

Closes #43
2026-05-26 18:08:48 +02:00

68 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# dashboardAPI output manifest
Per `.claude/rules/output-coverage.md`: every output on every layer, in every state.
## Port 0 (process — Grafana upsert messages)
Emitted by the command handler(s) after a `child.register` or `regenerate-dashboard` message. Shape is the same for both; `meta.trigger` distinguishes them.
| Key | Source method | Type | States tested | Test file |
|---|---|---|---|---|
| `topic` | `handlers.emitDashboardsFor` | `'create'` (literal) | populated | `test/basic/slice41-manual-regen.basic.test.js` |
| `url` | `source.grafanaUpsertUrl()` | string (configured Grafana endpoint) | populated, default-config | `test/basic/slice34-credentials-and-folder.basic.test.js` |
| `method` | `handlers.emitDashboardsFor` | `'POST'` (literal) | populated | `test/basic/slice41-manual-regen.basic.test.js` |
| `headers.Accept` | `handlers.emitDashboardsFor` | `'application/json'` (literal) | populated | _via output manifest test below_ |
| `headers['Content-Type']` | `handlers.emitDashboardsFor` | `'application/json'` (literal) | populated | _via output manifest test below_ |
| `headers.Authorization` | `handlers.emitDashboardsFor` | `'Bearer <token>'` when configured; absent when not | populated, absent (degraded — no token) | `test/basic/slice43-output-manifest.basic.test.js` |
| `payload.dashboard` | `source.buildDashboard()` | object (Grafana dashboard JSON) | populated, byte-identical-on-repeat | `test/basic/slice35-graph-perf-and-uid-uniqueness.basic.test.js` |
| `payload.overwrite` | `source.buildUpsertRequest()` | `true` (literal) | populated | `test/basic/slice34-credentials-and-folder.basic.test.js` |
| `payload.folderUid` | `source.buildUpsertRequest()` | string when configured; absent when empty | populated, absent (degraded — empty config) | `test/basic/slice34-credentials-and-folder.basic.test.js` |
| `payload.folderId` | `source.buildUpsertRequest()` | number when explicitly passed; absent otherwise | absent (default), populated (explicit) | `test/basic/slice34-credentials-and-folder.basic.test.js` |
| `meta.nodeId` | `handlers.emitDashboardsFor` | string (child node id) | populated | `test/basic/slice43-output-manifest.basic.test.js` |
| `meta.softwareType` | `handlers.emitDashboardsFor` | string (child softwareType) | populated | `test/basic/slice43-output-manifest.basic.test.js` |
| `meta.uid` | `handlers.emitDashboardsFor` | string (stableUid hash, deterministic) | populated, byte-identical | `test/basic/slice35-graph-perf-and-uid-uniqueness.basic.test.js` |
| `meta.title` | `handlers.emitDashboardsFor` | string (child name or id) | populated | `test/basic/slice43-output-manifest.basic.test.js` |
| `meta.trigger` | `handlers.emitDashboardsFor` | `'child.register'` or `'manual'` | both states | `test/basic/slice41-manual-regen.basic.test.js` |
**Degraded-state convention:** missing keys are **absent**, never set to `null`. The `http request` consumer treats absent headers/payload fields as defaults.
## Port 1 (InfluxDB telemetry)
dashboardAPI emits **nothing** on Port 1 by design — it has no measurements, no tick loop, no telemetry. Verified by absence: no `formatForInflux` import, no Port 1 wires in `examples/`.
## Port 2 (registration / control plumbing)
dashboardAPI is a **sink** for `child.register` messages, not a source — it does not register itself with any parent. Nothing emitted on Port 2.
## Structured log outputs
| Event | Level | Triggered by | Fields | Test |
|---|---|---|---|---|
| `regen-emitted` | info | successful composition (auto or manual) | `event`, `trigger`, `dashboardApiId`, `childId`, `dashboardCount` | `test/basic/slice43-output-manifest.basic.test.js` |
| `regen-skipped` | info | diff predicate says subtree unchanged | `event`, `outcome: 'no-diff'`, `trigger: 'child.register'`, `dashboardApiId`, `childId`, `subtreeSize` | `test/basic/slice43-output-manifest.basic.test.js` |
| `manual-regen-requested` | info | `regenerate-dashboard` topic received | `event`, `trigger: 'manual'`, `dashboardApiId`, `cachedChildCount` | `test/basic/slice41-manual-regen.basic.test.js` |
| `parent-panels-deduped` | debug | no-data-duplication filter removed root panels | `event`, `before`, `after`, `rootTitle` | _covered by composition tests in slice39_ |
| `flows:started` | debug | Node-RED runtime emits flows:started | `event: 'flows:started'`, `type`, `diff` (count summary) | _covered by predicate tests in slice36_ |
## specificClass return shapes
| Method | Return shape | Populated states | Degraded states | Test |
|---|---|---|---|---|
| `buildDashboard(opts)` | `{ dashboard, uid, title, softwareType, nodeId, measurementName }` or `null` | success | `null` when no template for softwareType | `test/basic/slice43-output-manifest.basic.test.js` |
| `generateDashboardsForGraph(root)` | array of `buildDashboard` results, root first, children after | 0..N children | empty array when root config missing | `test/basic/slice35-graph-perf-and-uid-uniqueness.basic.test.js` |
| `subtreeChanged(diff, ids)` | boolean | id-in-diff, no-id-in-diff | null diff → true (cold start) | `test/basic/slice36-diff-predicate.basic.test.js` |
| `subtreeIdsFor(myId, child)` | Set\<string\> | myId+childId+grandchildren | myId only when child has no grandchildren | `test/basic/slice36-diff-predicate.basic.test.js` |
| `collectEmittedFields(dashboard)` | Set\<string\> | populated dashboard | empty set for `null`/`{}`/`{panels:[]}` | `test/basic/slice37-emitted-fields.basic.test.js` |
| `cachedChildSources()` | array of child sources | 0..N cached | empty after construction | `test/basic/slice41-manual-regen.basic.test.js` |
## Anti-patterns enforced
- ❌ Emitting `{payload: null}``handlers.emitDashboardsFor` always builds `payload: { dashboard, overwrite, ... }`. Verified.
- ❌ Mixing absent vs null for optional fields — `folderUid` / `folderId` are **absent** when unconfigured, never `null`. Verified.
- ❌ Per-call token stamping — token is set on `headers.Authorization` when configured; absent when not. No empty-string sentinel.
- ❌ Tab id over-triggering in diff predicate — predicate only matches against dashboardAPI's own id + child + grandchildren, never tab ids. Verified.
## Migration plan applied
This manifest is created together with slice #43 — the new outputs added in slices #34#42 are documented here. Other EVOLV nodes still need their own manifests; tracked in `IMPROVEMENTS_BACKLOG.md`.