import test from 'node:test'; import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { workflowView } from './netplan-v4.js'; const now = Date.parse('2026-10-08T12:00:00Z'); const report = (state, overrides = {}) => ({ state, learningEnabled: state !== 'disabled', controlRequested: state === 'active', controlActive: state === 'active', planningReady: ['ready', 'active'].includes(state), reportFresh: true, checkedAt: new Date(now).toISOString(), sourceReportAt: new Date(now - 10000).toISOString(), sourceFreshUntil: new Date(now + 110000).toISOString(), reason: '', ...overrides }); test('workflow reports all six states without deriving control from a plan', () => { for (const [state, label] of Object.entries({ disabled: 'Ausgeschaltet', collecting: 'Sammelt Daten', learning: 'Lernt', ready: 'Bereit', active: 'Aktiv', interrupted: 'Unterbrochen' })) { assert.equal(workflowView(report(state), now).label, label); } assert.equal(workflowView(undefined, now).label, 'Betriebsstatus nicht bestaetigt'); assert.equal(workflowView({ plan: { status: 'optimal', runMode: 'shadow' }, acknowledgement: { status: 'shadow_seen' } }, now).state, 'unknown'); }); test('missing, malformed or contradictory reports never claim active control', () => { const malformed = [null, {}, report('other'), report('active', { checkedAt: 'invalid' }), report('active', { checkedAt: 123 }), report('active', { controlActive: false }), report('active', { controlRequested: false }), report('active', { planningReady: false }), report('ready', { controlActive: true }), report('ready', { planningReady: false }), report('active', { learningEnabled: 'true' })]; for (const input of malformed) assert.equal(workflowView(input, now).state, 'unknown'); }); test('active requires a fresh manager report and expires at its exact boundary', () => { for (const overrides of [{ reportFresh: false }, { reportFresh: undefined }, { learningEnabled: false }, { sourceReportAt: null }, { sourceReportAt: new Date(now + 1).toISOString() }, { sourceFreshUntil: null }, { sourceFreshUntil: new Date(now).toISOString() }, { sourceFreshUntil: new Date(now - 1).toISOString() }]) { const view = workflowView(report('active', overrides), now); assert.equal(view.state, 'interrupted'); assert.equal(view.controlActive, false); } assert.equal(workflowView(report('active'), now + 109999).state, 'active'); assert.equal(workflowView(report('active'), now + 110000).state, 'interrupted'); }); test('known reasons are understandable and unknown reasons remain visible as text', () => { assert.match(workflowView(report('collecting', { reason: 'collecting_measurements' }), now).reason, /Messdaten werden gesammelt/); assert.match(workflowView(report('active', { reason: 'manager_control_active' }), now).reason, /Manager bestaetigt/); const reason = ''; assert.equal(workflowView(report('interrupted', { reason }), now).reason, reason); }); test('portal release assets match the canonical planner sources', async () => { for (const file of ['netplan-v4.js', 'netplan-v4.css', 'netplan-v4.html']) { const canonical = await readFile(new URL(file, import.meta.url), 'utf8'); const release = await readFile(new URL('../../license/changes/20261006-forecast-integration/public/' + file, import.meta.url), 'utf8'); assert.equal(release, canonical, file); } });