Single-file Node.js CLI that orchestrates the full setup lifecycle: - Interactive env var configuration with validation and smart defaults - Postgres provisioning via Docker Compose with readiness polling - Schema initialization (psql with docker exec fallback) - npm dependency installation with 120s timeout - Optional SQLite-to-Postgres data migration with retry logic - Frontend build with smart skip on reconfiguration Includes 84 tests: 50 property-based (fast-check) covering 19 correctness properties, and 34 integration tests for filesystem and parsing flows.
121 lines
3.8 KiB
JavaScript
121 lines
3.8 KiB
JavaScript
/**
|
|
* Property-Based Tests: Config Wizard Registry Invariants
|
|
*
|
|
* Feature: config-wizard
|
|
*
|
|
* Tests the structural invariants of the VARIABLE_DESCRIPTORS registry
|
|
* from `configure.js`.
|
|
*
|
|
* Validates: Requirements 2.1, 2.4, 2.5
|
|
*/
|
|
|
|
const {
|
|
VARIABLE_DESCRIPTORS,
|
|
GROUP_ORDER
|
|
} = require('../../configure.js');
|
|
|
|
// --- Property 1: Descriptor registry uniqueness ---
|
|
describe('Property 1: Descriptor registry uniqueness', () => {
|
|
/**
|
|
* **Validates: Requirements 2.5**
|
|
*
|
|
* Every variable name appears exactly once across all groups in the
|
|
* VARIABLE_DESCRIPTORS registry.
|
|
*/
|
|
test('every variable name appears exactly once in the registry', () => {
|
|
const names = VARIABLE_DESCRIPTORS.map(d => d.name);
|
|
const nameSet = new Set(names);
|
|
|
|
// No duplicates: set size equals array length
|
|
expect(nameSet.size).toBe(names.length);
|
|
|
|
// Each name appears exactly once
|
|
const nameCounts = {};
|
|
for (const name of names) {
|
|
nameCounts[name] = (nameCounts[name] || 0) + 1;
|
|
}
|
|
for (const [name, count] of Object.entries(nameCounts)) {
|
|
expect(count).toBe(1);
|
|
}
|
|
});
|
|
|
|
test('no variable is assigned to multiple groups', () => {
|
|
const nameToGroups = {};
|
|
for (const desc of VARIABLE_DESCRIPTORS) {
|
|
if (!nameToGroups[desc.name]) {
|
|
nameToGroups[desc.name] = [];
|
|
}
|
|
nameToGroups[desc.name].push(desc.group);
|
|
}
|
|
|
|
for (const [name, groups] of Object.entries(nameToGroups)) {
|
|
expect(groups.length).toBe(1);
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- Property 2: Group presentation order ---
|
|
describe('Property 2: Group presentation order', () => {
|
|
/**
|
|
* **Validates: Requirements 2.1**
|
|
*
|
|
* Consecutive descriptors have non-decreasing group index in GROUP_ORDER,
|
|
* ensuring variables are presented in group order.
|
|
*/
|
|
test('consecutive descriptors have non-decreasing group index', () => {
|
|
for (let i = 1; i < VARIABLE_DESCRIPTORS.length; i++) {
|
|
const prevGroupIndex = GROUP_ORDER.indexOf(VARIABLE_DESCRIPTORS[i - 1].group);
|
|
const currGroupIndex = GROUP_ORDER.indexOf(VARIABLE_DESCRIPTORS[i].group);
|
|
|
|
// Both groups must exist in GROUP_ORDER
|
|
expect(prevGroupIndex).toBeGreaterThanOrEqual(0);
|
|
expect(currGroupIndex).toBeGreaterThanOrEqual(0);
|
|
|
|
// Current group index must be >= previous group index
|
|
expect(currGroupIndex).toBeGreaterThanOrEqual(prevGroupIndex);
|
|
}
|
|
});
|
|
|
|
test('all descriptor groups are present in GROUP_ORDER', () => {
|
|
const descriptorGroups = new Set(VARIABLE_DESCRIPTORS.map(d => d.group));
|
|
for (const group of descriptorGroups) {
|
|
expect(GROUP_ORDER).toContain(group);
|
|
}
|
|
});
|
|
});
|
|
|
|
// --- Property 3: Required-before-optional ordering ---
|
|
describe('Property 3: Required-before-optional ordering', () => {
|
|
/**
|
|
* **Validates: Requirements 2.4**
|
|
*
|
|
* Within each group, all required descriptors precede optional ones
|
|
* in the registry ordering.
|
|
*/
|
|
test('within each group, all required descriptors precede optional ones', () => {
|
|
for (const group of GROUP_ORDER) {
|
|
const groupDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.group === group);
|
|
|
|
let seenOptional = false;
|
|
for (const desc of groupDescriptors) {
|
|
if (desc.required) {
|
|
// Once we've seen an optional, no more required should appear
|
|
expect(seenOptional).toBe(false);
|
|
} else {
|
|
seenOptional = true;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
test('required count + optional count equals total for each group', () => {
|
|
for (const group of GROUP_ORDER) {
|
|
const groupDescriptors = VARIABLE_DESCRIPTORS.filter(d => d.group === group);
|
|
const requiredCount = groupDescriptors.filter(d => d.required).length;
|
|
const optionalCount = groupDescriptors.filter(d => !d.required).length;
|
|
|
|
expect(requiredCount + optionalCount).toBe(groupDescriptors.length);
|
|
}
|
|
});
|
|
});
|