fix: rebind imported experiments to active inputs

This commit is contained in:
2026-08-29 08:35:39 +09:00
parent 62971e07ec
commit 447f77dfa7
7 changed files with 130 additions and 47 deletions
+81 -20
View File
@@ -1,4 +1,4 @@
import { validateResult } from "/packages/contracts/validation.js";
import { validateExperiment, validateResult } from "/packages/contracts/validation.js";
async function waitFor(predicate, label, timeoutMs = 30000) {
const deadline = performance.now() + timeoutMs;
@@ -344,7 +344,7 @@ test("Acoustic Camera가 8×8 CPU 실행·결과 저장·stale·재실행 흐름
}
});
test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서 다중 분석 대역을 보존한다", async () => {
test("Acoustic Camera가 Experiment를 현재 입력에 연결하고 Scene·Array 재import에서도 다중 대역을 보존한다", async () => {
const frame = document.createElement("iframe");
frame.src = "/apps/acoustic-camera/?integration-test=1&band-preservation=1";
frame.style.cssText = "position:fixed;left:-20000px;top:0;width:1400px;height:1000px;border:0";
@@ -393,7 +393,8 @@ test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서
assert(link.relList.contains("noopener"), "Builder tab cannot replace Camera through opener");
}
const scene = await loadJson("/assets/sample-scenes/single-tone.scene.json");
const openScene = await loadJson("/assets/sample-scenes/single-tone.scene.json");
const scene = structuredClone(openScene);
scene.id = "two-tone-band-preservation";
scene.name = "Two-tone band preservation";
const secondSource = structuredClone(scene.sources[0]);
@@ -404,7 +405,10 @@ test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서
secondSource.signal.phaseDeg = 45;
scene.sources.push(secondSource);
const arrayDefinition = await loadJson("/assets/array-geometries/rectangular-16x12.array.json");
const openArrayDefinition = await loadJson(
"/assets/array-geometries/rectangular-16x12.array.json",
);
const arrayDefinition = structuredClone(openArrayDefinition);
arrayDefinition.id = "custom-array-band-preservation";
arrayDefinition.name = "Custom array band preservation";
@@ -427,13 +431,15 @@ test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서
);
dispatchFiles(windowObject, documentObject.querySelector("#experiment-input"), [experimentFile]);
await waitFor(
() => documentObject.documentElement.dataset.inputBusy === "false",
"clean-state Experiment-first import",
() => documentObject.documentElement.dataset.inputBusy === "false" &&
documentObject.documentElement.dataset.cameraValid === "true" &&
!documentObject.querySelector("#run-button").disabled,
"Experiment import and automatic reference binding",
);
assertEqual(
documentObject.querySelector("#experiment-id").value,
experiment.id,
"Experiment loads before its referenced Scene",
"Experiment document loads before its originally referenced Scene",
);
assertEqual(readBandEditors().length, 2, "Experiment-first import renders every analysis band");
assertEqual(
@@ -441,22 +447,77 @@ test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서
JSON.stringify(expectedBands),
"Experiment-first import preserves band IDs, ranges, and colors",
);
await waitFor(
() => documentObject.documentElement.dataset.cameraValid === "false" &&
documentObject.querySelector("#validation-message").textContent.includes("sceneId") &&
documentObject.querySelector("#validation-message").textContent.includes("arrayId"),
"pending Scene and Array linkage validation",
);
assert(
documentObject.querySelector("#validation-message").textContent.includes("다릅니다"),
"Experiment-first import explains the pending document ID mismatch",
!documentObject.querySelector("#validation-message").textContent.includes("sceneId") &&
!documentObject.querySelector("#validation-message").textContent.includes("arrayId") &&
!documentObject.querySelector("#run-status").textContent.includes("연결 대기"),
"automatic binding does not leave a reference mismatch warning",
);
assert(
documentObject.querySelector("#run-status").textContent.includes("분석 대역 2개") &&
documentObject.querySelector("#run-status").textContent.includes(scene.id) &&
documentObject.querySelector("#run-status").textContent.includes(arrayDefinition.id) &&
documentObject.querySelector("#run-status").textContent.includes("연결 대기"),
"Experiment-first status reports every band and pending reference",
documentObject.querySelector("#run-status").textContent.includes("실행 준비 완료"),
"Experiment import reaches a ready state with every band",
);
documentObject.querySelector("#export-experiment-button").click();
const reboundDownload = await waitFor(
() => downloads.find((download) => download.fileName.endsWith(".experiment.json")),
"automatically rebound Experiment export",
);
const reboundExperiment = await fetch(reboundDownload.url).then((response) => response.json());
assertEqual(reboundExperiment.sceneId, openScene.id, "import binds the open Scene ID");
assertEqual(
reboundExperiment.arrayId,
openArrayDefinition.id,
"import binds the open Array ID",
);
assertEqual(
JSON.stringify(reboundExperiment.frequencyBands),
JSON.stringify(expectedBands),
"automatic reference binding preserves every imported band",
);
assertEqual(
(await validateExperiment(reboundExperiment, {
scene: openScene,
arrayDefinition: openArrayDefinition,
})).length,
0,
"rebound Experiment passes relationship validation",
);
downloads.length = 0;
const contextInvalidExperiment = structuredClone(experiment);
contextInvalidExperiment.id = "nyquist-invalid-after-rebind";
contextInvalidExperiment.frequencyBands[0].maxHz = openScene.sampleRate / 2;
assertEqual(
(await validateExperiment(contextInvalidExperiment)).length,
0,
"Nyquist fixture is valid without a Scene context",
);
const contextInvalidFile = new windowObject.File(
[JSON.stringify(contextInvalidExperiment)],
"nyquist-invalid-after-rebind.experiment.json",
{ type: "application/json" },
);
dispatchFiles(
windowObject,
documentObject.querySelector("#experiment-input"),
[contextInvalidFile],
);
await waitFor(
() => documentObject.documentElement.dataset.inputBusy === "false" &&
documentObject.documentElement.dataset.cameraValid === "true",
"relationship-invalid Experiment rejection",
);
assertEqual(
documentObject.querySelector("#experiment-id").value,
experiment.id,
"context relationship validation rejects the imported Experiment before assignment",
);
assertEqual(
JSON.stringify(readBandEditors()),
JSON.stringify(expectedBands),
"failed relationship validation leaves the current bands unchanged",
);
let replacementConfirmCount = 0;
@@ -487,7 +548,7 @@ test("Acoustic Camera가 Experiment 선로드와 Scene·Array 재import에서
() => documentObject.documentElement.dataset.inputBusy === "false" &&
documentObject.querySelectorAll("#source-summary .source-line").length === 2 &&
documentObject.querySelectorAll("#band-list .band-editor").length === 2 &&
documentObject.documentElement.dataset.cameraValid === "false",
documentObject.documentElement.dataset.cameraValid === "true",
"source-added Scene reimport",
);
assertEqual(