feat: add Nepal flood incident scene

This commit is contained in:
manjunath22466 committed 2026-09-15 19:48:11 +05:30
1 parent 76c20be277
commit 60768ccc0b
48 files changed
+15431 -161

No files matched your search

+701 -2
View File
@@ -17,7 +17,550 @@ import test from 'node:test';
import { SceneDirector } from './director.js';
import { SCENE_TRACKING_PARAM_KEYS } from './scenePolicy.js';
import { SCENE_RECIPES } from './recipes.js';
import { SCENE_RECIPES, getSceneAppendRecipeById } from './recipes.js';
const NEPAL_ORIGINAL_SHOT_TITLES = [
'Global Incident Context',
'Nepal-Focused Globe Rotation',
'Bhote Koshi Regional Approach',
'Bhote Koshi Nearby Cities',
'Bhote Koshi Incident Corridor',
'Bhote Koshi Flood Path',
'Bhote Koshi Corridor Overview',
'Bhote Koshi Upper Valley',
];
function nepalProjectFixture() {
const project = structuredClone(PROJECT_FIXTURE);
project.scenes[0].title = 'Nepal Flood Incident';
project.scenes[0].shots = NEPAL_ORIGINAL_SHOT_TITLES.map((title, index) => ({
...structuredClone(PROJECT_FIXTURE.scenes[0].shots[index % 2]),
id: `nepal-original-${index + 1}`,
title,
camera: {
lat: 27 + index / 10,
lon: 85 + index / 10,
alt: 1000 + index * 100,
heading: index * 10,
pitch: -35 - index,
roll: 0,
},
visual: {
...structuredClone(PROJECT_FIXTURE.scenes[0].shots[index % 2].visual),
mapStack: 'photoreal',
},
layers: {
...structuredClone(PROJECT_FIXTURE.scenes[0].shots[index % 2].layers),
'bhote-koshi-locator': {
enabled: index > 0,
params: { presentation: `fixture-${index + 1}` },
},
},
}));
project.scenes[0].shots[7].layers['bhote-koshi-locator'] = {
enabled: true,
params: { presentation: 'bhote-koshi-trigger-record' },
};
project.scenes[0].shots[7].holdSec = 8;
return project;
}
function legacyThreeShotNepalProjectFixture() {
const project = structuredClone(PROJECT_FIXTURE);
project.scenes[0].title = 'Nepal Flood Incident';
project.scenes[0].shots = [0, 1, 2].map((index) => ({
...structuredClone(PROJECT_FIXTURE.scenes[0].shots[index % 2]),
id: `legacy-nepal-${index + 1}`,
title: `Shot ${index + 1}`,
camera: {
lat: 20 + index,
lon: 80 + index,
alt: 1000000 + index * 100000,
heading: index * 15,
pitch: -50 - index,
roll: 0,
},
}));
return project;
}
function legacyDefaultProjectWithoutNepalFixture() {
const project = structuredClone(PROJECT_FIXTURE);
project.scenes[0].id = 'bhote-koshi-flood';
project.scenes[0].title = 'Bhote Koshi Flood Reconstruction';
project.scenes[0].shots = [{
...structuredClone(PROJECT_FIXTURE.scenes[0].shots[0]),
id: 'standalone-shot',
title: 'Shot 1',
}];
return project;
}
test('Mailung clip trim estimates seven seconds and its exit, including older saved holds', () => {
const { director, restore } = makeDirector();
try {
const scene = director._project.scenes[0];
const shot = scene.shots[0];
shot.sourcePackId = 'bhote-koshi-nepal-evidence-pack';
shot.durationSec = 4.2;
shot.holdSec = 15;
shot.layers = { 'bhote-koshi-2026': { enabled: true, params: {
presentation: 'scene-beat', beatId: 'mailung-bazzar',
} } };
assert.equal(director._effectiveShotHoldSec(scene, shot), 7.65);
assert.equal(director._shotRuntimeDurationSec(scene, shot), 4.2 + 7.65);
const state = director._layerStatesForShot(scene, shot)['bhote-koshi-2026'];
assert.equal(state.params.sceneContext.holdSec, 7.65);
assert.equal(state.params.sceneControls.mediaPlaybackHoldSec, 7);
assert.equal(state.params.sceneControls.minimumHoldSec, 7.65);
assert.equal(shot.holdSec, 15, 'runtime correction does not rewrite saved shots');
shot.layers['bhote-koshi-2026'].params.beatId = 'dandagaun';
assert.equal(director._effectiveShotHoldSec(scene, shot), 15, 'other shots keep their authored hold');
} finally { restore(); }
});
test('Incident Corridor gives all overview pins time to reveal without rewriting saved shots', () => {
const { director, restore } = makeDirector();
try {
const scene = director._project.scenes[0];
const shot = scene.shots[0];
shot.holdSec = 0.9;
shot.layers = { 'bhote-koshi-locator': {
enabled: true, params: { presentation: 'bhote-koshi-incident-places' },
} };
assert.equal(director._effectiveShotHoldSec(scene, shot), 11);
assert.equal(shot.holdSec, 0.9);
shot.holdSec = 15;
assert.equal(director._effectiveShotHoldSec(scene, shot), 15);
shot.holdSec = 0.9;
shot.layers['bhote-koshi-locator'].params.presentation = 'bhote-koshi-flood-path';
assert.equal(director._effectiveShotHoldSec(scene, shot), 0.9);
shot.layers['bhote-koshi-locator'].params.presentation = 'bhote-koshi-incident-places';
shot.layers['bhote-koshi-locator'].enabled = false;
assert.equal(director._effectiveShotHoldSec(scene, shot), 0.9);
} finally {
restore();
}
});
test('scene clock seek resolves the exact shot phase and camera in both directions', async () => {
const { director, restore } = makeDirector();
const loads = [];
director._loadShot = async (sceneId, shotId, options) => {
loads.push({ sceneId, shotId, options });
return { started: true, shotId };
};
try {
assert.equal(await director.seekScene('scene-1', 0), true);
assert.equal(await director.seekScene('scene-1', 0.99), true);
assert.equal(await director.seekScene('scene-1', 0.25), true);
assert.equal(await director.seekScene('missing', 0.5), false);
assert.deepEqual(loads.map(({ sceneId, shotId }) => ({ sceneId, shotId })), [
{ sceneId: 'scene-1', shotId: 'shot-a' },
{ sceneId: 'scene-1', shotId: 'shot-b' },
{ sceneId: 'scene-1', shotId: 'shot-a' },
]);
assert.equal(loads[0].options.sceneSeek.sceneProgress, 0);
assert.equal(loads[0].options.sceneSeek.cameraProgress, 0);
assert.ok(loads[1].options.sceneSeek.cameraProgress > 0.9);
assert.equal(loads[1].options.sceneSeek.shotIndex, 1);
assert.equal(loads[2].options.sceneSeek.sceneProgress, 0.25);
assert.equal(loads[2].options.sceneSeek.cameraProgress, 0.5);
assert.equal(loads[2].options.sceneSeek.camera.lat, 10);
assert.equal(loads[2].options.sceneSeek.camera.lon, 20);
} finally {
restore();
}
});
test('scene clock subscribers receive authoritative forward playback snapshots', () => {
const { director, restore } = makeDirector();
const snapshots = [];
const unsubscribe = director.subscribeSceneClock((snapshot) => snapshots.push(snapshot));
try {
const scene = director._project.scenes[0];
director._publishSceneClock(scene, scene.shots[0], 0.1, { running: true });
director._publishSceneClock(scene, scene.shots[1], 0.3, { running: true });
unsubscribe();
director._publishSceneClock(scene, scene.shots[1], 0.4, { running: false });
assert.deepEqual(snapshots.map(({ shotId, sceneElapsedSec, running }) => ({
shotId, sceneElapsedSec, running,
})), [
{ shotId: 'shot-a', sceneElapsedSec: 0.1, running: true },
{ shotId: 'shot-b', sceneElapsedSec: 0.3, running: true },
]);
} finally {
restore();
}
});
test('the Nepal evidence pack appends once and applies the approved corridor framing', () => {
const project = nepalProjectFixture();
const originalShots = structuredClone(project.scenes[0].shots);
const { director, restore } = makeDirector({ project });
try {
const scene = director._project.scenes[0];
const originalShotIds = scene.shots.map(({ id }) => id);
const first = director.appendShotPack('scene-1', 'bhote-koshi-nepal-evidence-pack');
assert.deepEqual(first, {
appended: true,
updated: false,
shotCount: 17,
patchedShotCount: 14,
firstShotId: scene.shots[8].id,
});
assert.deepEqual(scene.shots.slice(0, 8).map(({ id }) => id), originalShotIds);
assert.equal(scene.shots.length, 25);
assert.ok(scene.releaseLayerIds.includes('bhote-koshi-2026'));
assert.ok(scene.releaseLayerIds.includes('bhote-koshi-locator'));
assert.equal(scene.appliedShotPacks[0].id, 'bhote-koshi-nepal-evidence-pack');
assert.equal(scene.appliedShotPacks[0].version, 18);
assert.deepEqual(
Object.keys(scene.appliedShotPacks[0].shotBindings),
[...NEPAL_ORIGINAL_SHOT_TITLES, ...scene.shots.slice(8).map(({ title }) => title)],
);
assert.deepEqual(
scene.shots.map((shot) => shot.visual.mapStack),
[
...Array(25).fill('photoreal'),
],
);
assert.deepEqual(
scene.shots.map((shot) => shot.layers['bhote-koshi-2026'].params.beatId),
[
'immediate-collapse-viewpoint',
'debris-dammed-lake',
'gyirong-border-gate',
'timure-cluster',
'syabru-besi',
'bidur-trishuli-bridge',
'gyirong-border-gate',
'immediate-collapse-viewpoint',
'immediate-collapse-viewpoint', 'debris-dammed-lake', 'second-landslide',
'gyirong-border-gate', 'timure-cluster', 'syabru-besi', 'dhunche',
'mailung-upper-trishuli', 'mailung-bazzar', 'dandagaun', 'dandagaun-viewpoint',
'betrawati-bazaar', 'bhainse', 'bidur-trishuli-bridge',
'devighat-taadi-khola-bridge', 'charaudi', 'final-view',
],
);
assert.deepEqual(
scene.shots[8].layers['bhote-koshi-2026'].params.sceneControls,
{ evidenceMediaAutoplay: true },
);
assert.deepEqual(
scene.shots[9].layers['bhote-koshi-2026'].params.sceneControls,
{ evidenceMediaAutoplay: true },
);
assert.deepEqual(
scene.shots[11].layers['bhote-koshi-2026'].params.sceneControls,
{
imageryComparison: true,
evidenceSequence: true,
evidenceSequenceDurationSec: 2.5,
deferEvidenceUntilCameraSettled: true,
evidenceRevealDurationSec: 2.5,
},
);
assert.deepEqual(
scene.shots.slice(12, 14).map(
(shot) => shot.layers['bhote-koshi-2026'].params.sceneControls,
),
[
{
imageryComparison: true,
evidenceSequence: true,
evidenceSequenceDurationSec: 4,
},
{
imageryComparison: true,
evidenceSequence: true,
evidenceSequenceDurationSec: 4,
},
],
);
assert.deepEqual(
scene.shots[21].layers['bhote-koshi-2026'].params.sceneControls,
{
evidenceSequence: true,
evidenceSequenceDurationSec: 4,
},
);
for (const shot of scene.shots) {
assert.equal(shot.layers['bhote-koshi-2026'].enabled, true);
assert.equal(shot.layers['bhote-koshi-2026'].params.presentation, 'scene-beat');
}
assert.deepEqual(
scene.shots.slice(0, 8).filter((_, index) => index !== 4).map(({ camera }) => camera),
originalShots.filter((_, index) => index !== 4).map(({ camera }) => camera),
);
assert.deepEqual(scene.shots[4].camera, {
lat: 28.0529,
lon: 85.2189,
alt: 126931,
heading: 0,
pitch: -90,
roll: 0,
});
assert.equal(scene.shots[4].holdSec, 11);
assert.deepEqual(
scene.shots.slice(0, 8).map((shot) => shot.layers['bhote-koshi-locator']),
originalShots.map((shot) => shot.layers['bhote-koshi-locator']),
);
assert.equal(new Set(scene.shots.map(({ id }) => id)).size, 25);
assert.deepEqual(
director.appendShotPack('scene-1', 'bhote-koshi-nepal-evidence-pack'),
{ appended: false, reason: 'already-appended' },
);
assert.equal(scene.shots.length, 25);
} finally {
restore();
}
});
test('installed v12 Nepal pack inserts ten points without replacing renamed cameras', () => {
const first = makeDirector({ project: nepalProjectFixture() });
let legacy;
const recipe = getSceneAppendRecipeById('bhote-koshi-nepal-evidence-pack');
try {
first.director.appendShotPack('scene-1', recipe.id);
legacy = structuredClone(first.director._project);
const scene = legacy.scenes[0];
scene.shots = scene.shots.filter((shot) => !shot.sourcePackId
|| recipe.previousRequiredSourcePackBeatIds.includes(shot.layers['bhote-koshi-2026'].params.beatId));
scene.appliedShotPacks[0].version = 12;
const ids = new Set(scene.shots.map(({ id }) => id));
scene.appliedShotPacks[0].shotBindings = Object.fromEntries(
Object.entries(scene.appliedShotPacks[0].shotBindings).filter(([, id]) => ids.has(id)));
scene.shots[8].title = 'My collapse camera';
scene.shots[8].camera.heading = 149;
} finally { first.restore(); }
const originalShots = structuredClone(legacy.scenes[0].shots);
const upgraded = makeDirector({ project: legacy });
try {
const scene = upgraded.director._project.scenes[0];
assert.equal(scene.shots.length, 25);
for (const old of originalShots) {
const current = scene.shots.find(({ id }) => id === old.id);
assert.ok(current);
assert.deepEqual(current.camera, old.camera);
assert.equal(current.title, old.title);
}
assert.deepEqual(scene.shots.filter((shot) => shot.sourcePackId).map((shot) =>
shot.layers['bhote-koshi-2026'].params.beatId), recipe.requiredSourcePackBeatIds);
assert.equal(scene.appliedShotPacks[0].version, 18);
assert.equal(upgraded.director.appendShotPack(scene.id, recipe.id).reason, 'already-appended');
} finally { upgraded.restore(); }
});
test('a legacy three-shot Nepal browser project bootstraps to the current 25-shot sequence', () => {
const project = legacyThreeShotNepalProjectFixture();
const originalIds = project.scenes[0].shots.map(({ id }) => id);
const originalCameras = project.scenes[0].shots.map(({ camera }) => structuredClone(camera));
const { director, restore } = makeDirector({ project });
try {
const scene = director._project.scenes[0];
assert.equal(scene.shots.length, 25);
assert.deepEqual(scene.shots.map(({ title }) => title),
getSceneAppendRecipeById('bhote-koshi-nepal-evidence-pack').requiredShotTitles);
assert.deepEqual(scene.shots.slice(0, 3).map(({ id }) => id), originalIds);
assert.deepEqual(scene.shots.slice(0, 3).map(({ camera }) => camera), originalCameras);
assert.equal(scene.appliedShotPacks[0].id, 'bhote-koshi-nepal-evidence-pack');
assert.equal(scene.appliedShotPacks[0].version, 18);
assert.equal(director._selectedSceneId, scene.id);
assert.equal(director._selectedShotId, scene.shots[0].id);
} finally {
restore();
}
});
test('the Nepal evidence pack refuses a partial inventory without mutating the scene', () => {
const project = nepalProjectFixture();
project.scenes[0].shots[3].title = 'Renamed Nearby Cities';
const { director, restore } = makeDirector({ project });
try {
const sceneBefore = structuredClone(director._project.scenes[0]);
assert.deepEqual(
director.appendShotPack('scene-1', 'bhote-koshi-nepal-evidence-pack'),
{
appended: false,
updated: false,
reason: 'shot-inventory-mismatch',
},
);
assert.deepEqual(director._project.scenes[0], sceneBefore);
} finally {
restore();
}
});
test('the Nepal pack upgrades the upper-valley shots without duplicating evidence beats', () => {
const initial = makeDirector({ project: nepalProjectFixture() });
initial.director.appendShotPack('scene-1', 'bhote-koshi-nepal-evidence-pack');
const project = structuredClone(initial.director._project);
initial.restore();
project.scenes[0].appliedShotPacks = [{
id: 'bhote-koshi-nepal-evidence-pack',
version: 4,
}];
for (const shot of project.scenes[0].shots.slice(0, 8)) {
delete shot.layers['bhote-koshi-2026'];
}
const originalLength = project.scenes[0].shots.length;
const originalIds = project.scenes[0].shots.map(({ id }) => id);
const originalCameras = project.scenes[0].shots.map(({ camera }) => structuredClone(camera));
const originalLocators = project.scenes[0].shots.map(
(shot) => structuredClone(shot.layers['bhote-koshi-locator']),
);
const { director, restore } = makeDirector({ project });
try {
const scene = director._project.scenes[0];
assert.deepEqual(
director.appendShotPack('scene-1', 'bhote-koshi-nepal-evidence-pack'),
{ appended: false, reason: 'already-appended' },
);
assert.equal(scene.shots.length, originalLength);
assert.deepEqual(scene.shots.map(({ id }) => id), originalIds);
assert.deepEqual(scene.shots.map(({ camera }) => camera), originalCameras);
assert.deepEqual(
scene.shots.map((shot) => shot.layers['bhote-koshi-locator']),
originalLocators,
);
assert.deepEqual(scene.shots[8].camera, {
lat: 28.417,
lon: 85.3937,
alt: 9850,
heading: 146,
pitch: -34,
roll: 0,
});
assert.equal(scene.appliedShotPacks[0].version, 18);
assert.equal(Object.keys(scene.appliedShotPacks[0].shotBindings).length, 25);
assert.ok(scene.shots.every((shot) => (
shot.layers['bhote-koshi-2026']?.enabled === true
&& shot.layers['bhote-koshi-2026']?.params?.presentation === 'scene-beat'
)));
const afterUpgrade = structuredClone(scene);
assert.deepEqual(
director.appendShotPack('scene-1', 'bhote-koshi-nepal-evidence-pack'),
{ appended: false, reason: 'already-appended' },
);
assert.deepEqual(scene, afterUpgrade);
} finally {
restore();
}
});
test('an older default project gains the complete selectable Nepal scene once', () => {
const project = legacyDefaultProjectWithoutNepalFixture();
const { director, restore } = makeDirector({
project,
data: { registered: [...REGISTERED, 'bhote-koshi-2026', 'bhote-koshi-locator'] },
});
try {
const nepal = director._project.scenes.find(({ title }) => title === 'Nepal Flood Incident');
assert.ok(nepal);
assert.equal(nepal.shots.length, 25);
assert.equal(nepal.shots[0].title, 'Global Incident Context');
assert.equal(nepal.shots.at(-1).title, 'Final view');
assert.deepEqual(director._project.installedBuiltInSceneIds, ['bhote-koshi-nepal-scene']);
} finally {
restore();
}
});
test('a previously installed Nepal scene stays deleted when its marker remains', () => {
const project = legacyDefaultProjectWithoutNepalFixture();
project.installedBuiltInSceneIds = ['bhote-koshi-nepal-scene'];
const { director, restore } = makeDirector({ project });
try {
assert.equal(
director._project.scenes.some(({ title }) => title === 'Nepal Flood Incident'),
false,
);
} finally {
restore();
}
});
test('public defaults include Nepal without an extra standalone flood recipe', () => {
assert.equal(SCENE_RECIPES.some((item) => item.id === 'bhote-koshi-flood'), false);
assert.equal(SCENE_RECIPES.filter((item) => item.id === 'bhote-koshi-nepal-scene').length, 1);
});
test('an existing public default project gains Nepal without replacing authored shots', () => {
const project = structuredClone(PROJECT_FIXTURE);
project.scenes[0].id = 'flights-radar';
const original = structuredClone(project.scenes[0].shots);
const { director, restore } = makeDirector({ project });
try {
assert.deepEqual(director._project.scenes[0].shots.map(({ id, camera }) => ({ id, camera })),
original.map(({ id, camera }) => ({ id, camera })));
assert.equal(director._project.scenes[1].title, 'Nepal Flood Incident');
assert.equal(director._project.scenes[1].shots.length, 25);
assert.deepEqual(director._project.installedBuiltInSceneIds, ['bhote-koshi-nepal-scene']);
} finally { restore(); }
});
test('Nepal comparison shots load Esri beneath Vantor even from a saved OSM or photoreal shot', async () => {
const { director, styleManager, restore } = makeDirector({
project: nepalProjectFixture(),
data: { registered: [...REGISTERED, 'bhote-koshi-2026', 'bhote-koshi-locator'] },
});
try {
director.appendShotPack('scene-1', 'bhote-koshi-nepal-evidence-pack');
const scene = director._project.scenes[0];
const comparisonShots = scene.shots.filter((shot) =>
director._layerStatesForShot(scene, shot)['bhote-koshi-2026']?.params?.sceneControls?.imageryComparison);
assert.ok(comparisonShots.length >= 2, 'the real comparison beats must be exercised');
for (const shot of comparisonShots) {
for (const prior of ['osm', 'photoreal']) {
shot.visual.mapStack = prior;
await director.loadShot(scene.id, shot.id, { flyDuration: 0.2 });
assert.equal(styleManager.visualStates.at(-1).mapStack, 'esri-imagery', shot.title);
}
}
} finally {
await director.destroy();
restore();
}
});
test('all saved Nepal shots choose a usable map in keyed and keyless runtimes without rewriting the project', async () => {
let photorealAvailable = false;
const { director, styleManager, restore } = makeDirector({
project: nepalProjectFixture(),
isMapStackAvailable: (id) => id === 'photoreal' && photorealAvailable,
data: { registered: [...REGISTERED, 'bhote-koshi-2026', 'bhote-koshi-locator'] },
});
try {
director.appendShotPack('scene-1', 'bhote-koshi-nepal-evidence-pack');
const scene = director._project.scenes[0];
const savedShots = structuredClone(scene.shots);
for (const available of [false, true, false]) {
photorealAvailable = available;
for (const shot of scene.shots) {
const comparison = director._layerStatesForShot(scene, shot)
['bhote-koshi-2026']?.params?.sceneControls?.imageryComparison;
const visual = director._visualStateForShot(shot);
assert.equal(visual.mapStack, available && !comparison ? 'photoreal' : 'esri-imagery', shot.title);
}
await director.loadShot(scene.id, scene.shots[0].id, { flyDuration: 0.2 });
assert.equal(styleManager.visualStates.at(-1).mapStack, available ? 'photoreal' : 'esri-imagery');
}
assert.deepEqual(scene.shots, savedShots);
const unrelated = { visual: { mapStack: 'photoreal' }, layers: { flights: { enabled: true } } };
assert.equal(director._visualStateForShot(unrelated), unrelated.visual, 'other scenes keep their provider policy');
} finally {
await director.destroy();
restore();
}
});
/** The layer registry as main.js builds it (src/main.js dataManager.register calls). */
const REGISTERED = [
@@ -98,6 +641,10 @@ function fakeDataManager({ registered = REGISTERED, refuse = () => false } = {})
const committed = [];
return {
setEnabledCalls,
subscribeVisibilityRequests(listener) {
this.visibilityListener = listener;
return () => { this.visibilityListener = null; };
},
setParamsCalls,
committed,
getAll: () => registered.map((id) => ({ id, enabled: !!enabled.get(id) })),
@@ -174,13 +721,67 @@ function makeDirector(options = {}) {
const viewer = fakeViewer();
const styleManager = fakeStyleManager(options.style);
const dataManager = fakeDataManager(options.data);
const director = new SceneDirector(viewer, styleManager, dataManager);
const director = new SceneDirector(viewer, styleManager, dataManager, {
isMapStackAvailable: options.isMapStackAvailable,
});
// Telemetry is only accumulated during a run; observable-failure assertions
// need the accumulator without driving a whole run.
director._activeRun = { events: [] };
return { director, viewer, styleManager, dataManager, restore };
}
test('scene camera waits for provider completion and fade rather than the saved media hold', async () => {
const { director, viewer, dataManager, restore } = makeDirector();
let release;
let pending = true;
const gate = new Promise((resolve) => { release = resolve; });
try {
const scene = director._project.scenes[0];
scene.shots[0].layers.flights.params = { beatId: 'trimmed' };
dataManager.layers = new Map([['flights', { module: {
getSceneShotMediaHold: (id) => id === 'trimmed' ? { pending, maxWaitMs: 20000 } : null,
} }]]);
const waits = [];
director._sleep = async (ms) => { waits.push(ms); if (ms === 70) await gate; };
const run = director.startScene('scene-1', { single: true, preview: false });
await settle(40);
assert.equal(viewer.flights.length, 1, 'no next flight while source is starting, playing or fading');
assert.deepEqual(waits, [70]);
pending = false; release();
await run;
assert.equal(viewer.flights.length, 2, 'the next shot starts immediately after owned media completion');
} finally { release(); await director.destroy(); restore(); }
});
test('Stop cancels a provider-owned shot hold and late completion cannot fly the next camera', async () => {
const { director, viewer, dataManager, restore } = makeDirector();
let release;
const gate = new Promise((resolve) => { release = resolve; });
try {
dataManager.layers = new Map([['flights', { module: {
getSceneShotMediaHold: () => ({ pending: true, maxWaitMs: 20000 }),
} }]]);
director._sleep = () => gate;
const run = director.startScene('scene-1', { single: true, preview: false });
await settle(40);
assert.equal(viewer.flights.length, 1);
director.stopScene('test stop'); release(); await run;
assert.equal(viewer.flights.length, 1);
assert.equal(director.running, false);
} finally { release(); await director.destroy(); restore(); }
});
test('a provider-owned hold fails boundedly if its owner never settles', async () => {
const { director, restore } = makeDirector();
try {
director.dataManager.layers = new Map([['flights', { module: {
getSceneShotMediaHold: () => ({ pending: true, maxWaitMs: 0 }),
} }]]);
const scene = director._project.scenes[0];
await assert.rejects(director._holdShot(scene, scene.shots[0], { cancelled: false }), /bounded playback window/);
} finally { await director.destroy(); restore(); }
});
/** The layer map a shipped recipe declares, in normalized form. */
function recipeLayers(recipeId) {
const recipe = SCENE_RECIPES.find((item) => item.id === recipeId);
@@ -189,6 +790,104 @@ function recipeLayers(recipeId) {
);
}
test('destroyed directors refuse seek, replay, adjacent and continuation without writes', async () => {
const { director, viewer, dataManager, restore } = makeDirector();
try {
await director.destroy();
director._loadedSceneId = 'scene-1';
assert.equal(await director.seekScene('scene-1', 0), false);
assert.equal((await director.replayShot('scene-1', 'shot-a')).started, false);
assert.equal(await director.loadAdjacentShot('scene-1', 'shot-a', 1), false);
assert.equal((await director.continueScene('scene-1', 'shot-a')).started, false);
assert.equal(viewer.flights.length, 0);
assert.equal(dataManager.setParamsCalls.length, 0);
} finally { restore(); }
});
test('replay, adjacent and seek report layer refusal rather than success', async () => {
const { director, restore } = makeDirector({ data: { refuse: () => true } });
try {
assert.equal((await director.replayShot('scene-1', 'shot-a')).started, false);
assert.equal(await director.loadAdjacentShot('scene-1', 'shot-a', 1), false);
assert.equal(await director.seekScene('scene-1', 0), false);
} finally { await director.destroy(); restore(); }
});
test('a seek waiting for run teardown is revoked by newer Stop, Load, Start or destroy', async () => {
for (const action of ['stop', 'load', 'start', 'destroy']) {
const { director, restore } = makeDirector();
try {
let release;
director._waitForRunIdle = () => new Promise((resolve) => { release = resolve; });
director._running = true;
director._runToken = { cancelled: false };
const seek = director.seekScene('scene-1', 0);
assert.equal(director._pendingWork.has(seek), true);
let newer;
if (action === 'stop') director.stopScene();
if (action === 'load') newer = director.loadShot('scene-1', 'shot-b');
if (action === 'start') newer = director.startScene('scene-1');
if (action === 'destroy') newer = director.destroy();
director._running = false;
release();
assert.equal(await seek, false, action);
await newer;
} finally { await director.destroy(); restore(); }
}
});
test('explicit scene-layer OFF revokes continuation during enable, flight and hold; internal OFF does not', async () => {
for (const phase of ['enable', 'flight', 'hold']) {
const { director, dataManager, restore } = makeDirector();
try {
director._project.scenes[0].releaseLayerIds = ['flights'];
let release;
const gate = new Promise((resolve) => { release = resolve; });
if (phase === 'enable') {
const enable = dataManager.setEnabled.bind(dataManager);
dataManager.setEnabled = async (...args) => { await gate; return enable(...args); };
} else if (phase === 'flight') director._flyCamera = () => gate;
else director._sleep = () => gate;
director._project.scenes[0].shots[0].holdSec = 1;
const run = director.startScene('scene-1', { single: true, preview: false });
await settle(40);
const token = director._runToken;
dataManager.visibilityListener({ layerId: 'flights', enabled: false, origin: 'scene' });
assert.equal(token.cancelled, false);
dataManager.visibilityListener({ layerId: 'flights', enabled: false, origin: 'user' });
assert.equal(token.cancelled, true, phase);
dataManager.visibilityListener({ layerId: 'flights', enabled: false, origin: 'tool' });
release();
await run;
assert.equal(dataManager.committed.some(({ id, enabled }) => id === 'traffic' && enabled), false);
} finally { await director.destroy(); restore(); }
}
});
test('scene preview owns recording chrome while panel playback leaves it available', async () => {
for (const preview of [true, false]) {
const { director, styleManager, restore } = makeDirector();
const recording = [];
const playback = [];
let release;
const flight = new Promise((resolve) => { release = resolve; });
director._flyCamera = () => flight;
styleManager.setRecordingMode = (active) => recording.push(active);
director._setPlaybackActive = (active) => playback.push(active);
try {
const run = director.startScene('scene-1', { single: true, preview });
await settle(40);
assert.deepEqual(recording, preview ? [true] : []);
assert.deepEqual(playback, preview ? [true] : []);
director.stopScene();
release();
await run;
assert.deepEqual(recording, preview ? [true, false] : []);
assert.deepEqual(playback, preview ? [true, false] : []);
} finally { release(); await director.destroy(); restore(); }
}
});
test('the director reconciles only the layers a shot declares', async () => {
// Regression: _applyLayerStates walked the LIVE registry and forced every
// undeclared layer off, tearing down CCTV/vessels/fires with no restore pass.