mirror of
https://github.com/Matysh/houseplan-card
synced 2026-09-29 03:09:36 +00:00
The Voluptuous schema is now dumped into a deterministic committed manifest (265 leaf paths); a pytest fails on drift. A parity test compares manifest enums with the exported frontend const lists through a machine-readable allow-list that also refuses to rot. The field registry gains passports (allow-extra / lovelace-card), enforcedBy citations for mechanisms that already shipped, and passports for the v1.68-v1.69 fields; a completeness test bans dead decisions. Lifecycle fixtures (oldest / current / future) prove lossless loading, and the config auditor gains the 0/3/2 exit-code contract. User-Visible: yes Issue: #33
88 lines
3.6 KiB
JavaScript
88 lines
3.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { resolve } from 'node:path';
|
|
import test from 'node:test';
|
|
import { auditConfig } from '../scripts/config-audit.mjs';
|
|
|
|
test('config audit finds registered compatibility fields without changing input', () => {
|
|
const config = {
|
|
settings: { show_all: true, weather_entity: 'weather.home' },
|
|
markers: [{
|
|
id: 'm1', binding: 'virtual', display: 'ripple',
|
|
vacuum: { trail: true, room_highlight: false },
|
|
}],
|
|
spaces: [{
|
|
id: 's1', plan_scale: 1.2, segments: [],
|
|
rooms: [{ id: 'r1', name: 'R', area: null, open_to: ['r2'] }],
|
|
decor: [
|
|
{ id: 'd1', kind: 'line', width: 2 },
|
|
{ id: 'd2', kind: 'text', text: '{}', size: 'm', entity: 'sensor.demo' },
|
|
],
|
|
}],
|
|
};
|
|
const before = JSON.stringify(config);
|
|
const findings = auditConfig({ config });
|
|
const ids = new Set(findings.map((finding) => finding.id));
|
|
|
|
for (const id of [
|
|
'settings.show_all', 'settings.weather_entity', 'markers[].display=ripple',
|
|
'markers[].vacuum.trail', 'markers[].vacuum.room_highlight', 'spaces[].segments',
|
|
'spaces[].plan_scale', 'spaces[].rooms[].open_to', 'spaces[].decor[].width',
|
|
'spaces[].decor[text].size', 'spaces[].decor[text].entity',
|
|
]) assert.equal(ids.has(id), true, id);
|
|
assert.equal(JSON.stringify(config), before);
|
|
});
|
|
|
|
test('config audit is silent for a canonical minimal config', () => {
|
|
assert.deepEqual(auditConfig({ spaces: [], markers: [], settings: {} }), []);
|
|
});
|
|
|
|
test('config audit rejects --json without an input file concisely', () => {
|
|
const result = spawnSync(process.execPath, ['scripts/config-audit.mjs', '--json'], {
|
|
cwd: resolve('.'), encoding: 'utf8',
|
|
});
|
|
assert.equal(result.status, 2);
|
|
assert.match(result.stderr, /--json requires/);
|
|
assert.doesNotMatch(result.stderr, /\n\s+at /);
|
|
});
|
|
|
|
test('config audit reports malformed JSON without a raw stack trace', () => {
|
|
const directory = mkdtempSync(resolve(tmpdir(), 'houseplan-config-audit-'));
|
|
const file = resolve(directory, 'broken.json');
|
|
try {
|
|
writeFileSync(file, '{broken', 'utf8');
|
|
const result = spawnSync(process.execPath, ['scripts/config-audit.mjs', file], {
|
|
cwd: resolve('.'), encoding: 'utf8',
|
|
});
|
|
assert.equal(result.status, 2);
|
|
assert.match(result.stderr, /^config-audit:/);
|
|
assert.doesNotMatch(result.stderr, /\n\s+at /);
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
// #33 AC6: the CLI exit-code contract — 0 clean, 3 migration available,
|
|
// 2 invalid input — proven on the lifecycle fixtures.
|
|
test('#33 config audit exit codes distinguish clean, migration and invalid', () => {
|
|
const run = (file) => spawnSync(process.execPath, ['scripts/config-audit.mjs', file], {
|
|
cwd: resolve('.'), encoding: 'utf8',
|
|
});
|
|
const clean = run('test/fixtures/config-lifecycle/current.json');
|
|
assert.equal(clean.status, 0,
|
|
`the current fixture must be clean, got ${clean.status}: ${clean.stdout}`);
|
|
const legacy = run('test/fixtures/config-lifecycle/oldest-supported.json');
|
|
assert.equal(legacy.status, 3,
|
|
'show_all/weather_entity/ripple in the oldest fixture must report "migration available"');
|
|
const directory = mkdtempSync(resolve(tmpdir(), 'houseplan-audit-codes-'));
|
|
const broken = resolve(directory, 'broken.json');
|
|
try {
|
|
writeFileSync(broken, '{nope', 'utf8');
|
|
assert.equal(run(broken).status, 2, 'invalid input keeps the existing code 2');
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|