const fs = require("fs"); const path = require("path"); const vm = require("vm"); const assert = require("assert"); const rootDir = path.resolve(__dirname, "..", ".."); const loadMapAppealTypeModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "mapAppealType.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/export const\s+/g, "const "); source = source.replace( /export function\s+mapAppealType/, "function mapAppealType" ); source += ` module.exports = { caseTypeAliases, caseTypeKeyByAppealTypeId, normaliseCaseType, mapAppealType }; `; const context = { module: { exports: {} }, exports: {}, require }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadMapSpecialistProcessStageTypeModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "mapSpecialistProcessStageType.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/export const\s+/g, "const "); source = source.replace( /export function\s+mapSpecialistProcessStageType/, "function mapSpecialistProcessStageType" ); source += ` module.exports = { specialistProcessStageTypeByCaseType, mapSpecialistProcessStageType }; `; const context = { module: { exports: {} }, exports: {}, require }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadGetStageCaseTypeKeyModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "getStageCaseTypeKey.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace( /export function\s+getStageCaseTypeKey/, "function getStageCaseTypeKey" ); source += ` module.exports = { getStageCaseTypeKey }; `; const context = { module: { exports: {} }, exports: {}, require, caseTypeAliases: mapAppealTypeModule.caseTypeAliases, caseTypeKeyByAppealTypeId: mapAppealTypeModule.caseTypeKeyByAppealTypeId, mapAppealType: mapAppealTypeModule.mapAppealType, normaliseCaseType: mapAppealTypeModule.normaliseCaseType, mapSpecialistProcessStageType: mapSpecialistProcessStageTypeModule.mapSpecialistProcessStageType }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadClosedCaseStatusModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "isClosedCaseStatus.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace( /export function\s+isClosedCaseStatus/, "function isClosedCaseStatus" ); source += ` module.exports = { isClosedCaseStatus }; `; const context = { module: { exports: {} }, exports: {}, require }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadGetLifecycleStageIndexModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "getLifecycleStageIndex.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace( /export function\s+getLifecycleStageIndex/, "function getLifecycleStageIndex" ); source += ` module.exports = { getLifecycleStageIndex }; `; const context = { module: { exports: {} }, exports: {}, require, isClosedCaseStatus: isClosedCaseStatusModule.isClosedCaseStatus }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadGetLifecycleStageStatusModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "getLifecycleStageStatus.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace( /export function\s+getLifecycleStageStatus/, "function getLifecycleStageStatus" ); source += "\nmodule.exports = { getLifecycleStageStatus };\n"; const context = { module: { exports: {} }, exports: {}, require }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadStageCatalogueModule = () => { const filePath = path.join( rootDir, "components", "case", "summary", "utils", "caseStagesByAppealType.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace(/export const\s+/g, "const "); source += "\nmodule.exports = { stagesByCaseType };\n"; const context = { module: { exports: {} }, exports: {}, require, getStageCaseTypeKey: getStageCaseTypeKeyModule.getStageCaseTypeKey, getStagesForCaseTypeKey: () => [], isClosedCaseStatus: isClosedCaseStatusModule.isClosedCaseStatus, getLifecycleStageIndex: getLifecycleStageIndexModule.getLifecycleStageIndex, getLifecycleStageStatus: getLifecycleStageStatusModule.getLifecycleStageStatus }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadGetStagesForCaseTypeKeyModule = () => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "getStagesForCaseTypeKey.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace( /export function\s+getStagesForCaseTypeKey/, "function getStagesForCaseTypeKey" ); source += ` module.exports = { getStagesForCaseTypeKey }; `; const context = { module: { exports: {} }, exports: {}, require: (modulePath) => { if ( modulePath === "../../../components/case/summary/utils/caseStagesByAppealType" ) { return stageCatalogueModule; } return require(modulePath); } }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadStageHelperModule = () => { const filePath = path.join( rootDir, "components", "case", "summary", "utils", "caseStagesByAppealType.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace(/import[\s\S]*?from\s+"[^"]+";\n?/g, ""); source = source.replace(/export const\s+/g, "const "); source += "\nmodule.exports = { getStagesForAppealType, stagesByCaseType };\n"; const context = { module: { exports: {} }, exports: {}, require, getStageCaseTypeKey: getStageCaseTypeKeyModule.getStageCaseTypeKey, getStagesForCaseTypeKey: getStagesForCaseTypeKeyModule.getStagesForCaseTypeKey, isClosedCaseStatus: isClosedCaseStatusModule.isClosedCaseStatus, getLifecycleStageIndex: getLifecycleStageIndexModule.getLifecycleStageIndex, getLifecycleStageStatus: getLifecycleStageStatusModule.getLifecycleStageStatus }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const loadWrapperModule = (stageHelperModule) => { const filePath = path.join( rootDir, "lib", "domain", "case-lifecycle", "getLifecycleStagesForCase.js" ); let source = fs.readFileSync(filePath, "utf8"); source = source.replace( /import\s+\{\s*getStagesForAppealType\s*\}\s+from\s+"[^"]+";\n?/, "" ); source = source.replace( /export function\s+getLifecycleStagesForCase/, "function getLifecycleStagesForCase" ); source += "\nmodule.exports = { getLifecycleStagesForCase };\n"; const context = { module: { exports: {} }, exports: {}, require, getStagesForAppealType: stageHelperModule.getStagesForAppealType }; vm.runInNewContext(source, context, { filename: filePath }); return context.module.exports; }; const tests = []; const test = (name, fn) => tests.push({ name, fn }); const normalizeForAssertion = (value) => JSON.parse(JSON.stringify(value ?? null)); const getStatusSummary = (stages) => stages.map(({ stageId, titleKey, status }) => ({ stageId, titleKey, status })); const mapAppealTypeModule = loadMapAppealTypeModule(); const mapSpecialistProcessStageTypeModule = loadMapSpecialistProcessStageTypeModule(); const getStageCaseTypeKeyModule = loadGetStageCaseTypeKeyModule(); const isClosedCaseStatusModule = loadClosedCaseStatusModule(); const getLifecycleStageIndexModule = loadGetLifecycleStageIndexModule(); const getLifecycleStageStatusModule = loadGetLifecycleStageStatusModule(); const stageCatalogueModule = loadStageCatalogueModule(); const getStagesForCaseTypeKeyModule = loadGetStagesForCaseTypeKeyModule(); const stageHelperModule = loadStageHelperModule(); const wrapperModule = loadWrapperModule(stageHelperModule); const { getStagesForAppealType } = stageHelperModule; const { getLifecycleStagesForCase } = wrapperModule; test("S78 progression preserves complete/current/future status assignment", () => { const result = normalizeForAssertion( getStagesForAppealType(846040000, 846040018) ); assert.deepStrictEqual(getStatusSummary(result).slice(0, 7), [ { stageId: 846040014, titleKey: "appeal-submitted", status: "complete" }, { stageId: 846040015, titleKey: "appeal-registered", status: "complete" }, { stageId: 846040016, titleKey: "appeal-validated", status: "complete" }, { stageId: 1, titleKey: "case-in-progress", status: "complete" }, { stageId: 846040017, titleKey: "questionnaire-lpa-to-notify", status: "complete" }, { stageId: 846040018, titleKey: "representations-period", status: "in-progress" }, { stageId: 846040019, titleKey: "final-comments", status: "not-started" } ]); }); test("DNS progression preserves current stage and surrounding status split", () => { const result = normalizeForAssertion( getStagesForAppealType(846040011, 846040006) ); assert.deepStrictEqual(getStatusSummary(result).slice(3, 8), [ { stageId: 846040004, titleKey: "acceptance-checking", status: "complete" }, { stageId: 846040005, titleKey: "invalid-submission", status: "complete" }, { stageId: 846040006, titleKey: "consultation", status: "in-progress" }, { stageId: 846040007, titleKey: "re-consultation", status: "not-started" }, { stageId: 846040008, titleKey: "reporting", status: "not-started" } ]); }); test("Householder progression preserves shortened lifecycle status behaviour", () => { const result = normalizeForAssertion( getStagesForAppealType(846040004, 846040021) ); assert.deepStrictEqual(getStatusSummary(result), [ { stageId: 846040014, titleKey: "appeal-submitted", status: "complete" }, { stageId: 846040015, titleKey: "appeal-registered", status: "complete" }, { stageId: 846040016, titleKey: "appeal-validated", status: "complete" }, { stageId: 1, titleKey: "case-in-progress", status: "complete" }, { stageId: 846040021, titleKey: "site-visit", status: "in-progress" }, { stageId: 846040022, titleKey: "decision-issued", status: "not-started" }, { stageId: 846040013, titleKey: "case-closed", status: "not-started" } ]); }); test("Call-In progression preserves current event-stage behaviour", () => { const result = normalizeForAssertion( getStagesForAppealType(846040010, 846040049) ); const currentStage = result.find((stage) => stage.stageId === 846040049); const completedCount = result.filter( (stage) => stage.status === "complete" ).length; assert.ok(currentStage); assert.strictEqual(currentStage.titleKey, "event"); assert.strictEqual(currentStage.status, "in-progress"); assert.strictEqual(completedCount, 7); assert.ok( result .slice(result.indexOf(currentStage) + 1) .every((stage) => stage.status === "not-started") ); }); test("ROW progression preserves current all-not-started behaviour for representative input", () => { const result = normalizeForAssertion( getStagesForAppealType(846040015, 846040032, 846040000) ); assert.ok(result.every((stage) => stage.status === "not-started")); assert.deepStrictEqual( result.map((stage) => stage.stageId), [ 846040033, 846040034, 846040035, 846040036, 846040062, 846040055, 846040037, 846040022, 846040013 ] ); }); test("closed-case fallback preserves current case-closed mapping for all recognised closed statuses", () => { const closedStatuses = [1000, 5, 6, 846040013, 846040059, 846040060]; for (const closedStatus of closedStatuses) { const result = normalizeForAssertion( getStagesForAppealType(846040000, closedStatus) ); const closedStage = result.find( (stage) => stage.titleKey === "case-closed" ); assert.ok( closedStage, `Expected case-closed stage for ${closedStatus}` ); assert.strictEqual(closedStage.status, "in-progress"); assert.ok( result .slice( 0, result.findIndex( (stage) => stage.titleKey === "case-closed" ) ) .every((stage) => stage.status === "complete") ); } }); test("unknown status handling preserves all-not-started fallback", () => { const result = normalizeForAssertion( getStagesForAppealType(846040000, 999999999) ); assert.ok(result.every((stage) => stage.status === "not-started")); }); test("status assignment preserves complete, in-progress and not-started semantics from index position", () => { const result = normalizeForAssertion( getStagesForAppealType(846040000, 846040020) ); const statusSequence = result.map((stage) => stage.status); assert.deepStrictEqual(statusSequence, [ "complete", "complete", "complete", "complete", "complete", "complete", "complete", "in-progress", "not-started", "not-started", "not-started" ]); }); test("statuscode ambiguity is preserved when statuscode-like values are passed into stage logic", () => { const exactStageMatch = normalizeForAssertion( getStagesForAppealType(846040000, 846040013) ); const closedStatusFallback = normalizeForAssertion( getStagesForAppealType(846040000, 846040059) ); assert.deepStrictEqual( getStatusSummary(exactStageMatch), getStatusSummary(closedStatusFallback) ); }); test("wrapper parity preserves current runtime outputs for representative progress scenarios", () => { const scenarios = [ [846040000, 846040018, undefined], [846040011, 846040006, undefined], [846040004, 846040021, undefined], [846040010, 846040049, undefined], [846040015, 846040032, 846040000], [846040000, 846040059, undefined], [846040000, 999999999, undefined] ]; for (const [caseType, currentStageId, specialistProcess] of scenarios) { assert.deepStrictEqual( normalizeForAssertion( getLifecycleStagesForCase( caseType, currentStageId, specialistProcess ) ), normalizeForAssertion( getStagesForAppealType( caseType, currentStageId, specialistProcess ) ) ); } }); const run = async () => { let passed = 0; for (const currentTest of tests) { await currentTest.fn(); passed += 1; } console.log( `Phase 22 case-lifecycle-progress-behaviour tests passed (${passed}/${tests.length}).` ); }; module.exports = run; if (require.main === module) { run().catch((error) => { console.error(error); process.exit(1); }); }