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

6.3 KiB
Raw Blame History

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.