feat(observatory): ship modular AI inference labs
This commit is contained in:
@@ -125,7 +125,7 @@ test("Observatory owns a bounded explicit-open lifecycle around the shared recor
|
||||
assert.match(observatory, /data-observatory-authority="observation-only"/);
|
||||
assert.match(
|
||||
observatory,
|
||||
/data-observatory-viewer={replay\.kind === "ready" \? "attached" : "detached"}/,
|
||||
/data-observatory-viewer={replay\.kind === "ready" \|\| replay\.kind === "composition" \? "attached" : "detached"}/,
|
||||
);
|
||||
assert.doesNotMatch(
|
||||
`${observatory}\n${observatoryCore}`,
|
||||
@@ -144,8 +144,8 @@ test("Observatory owns a bounded explicit-open lifecycle around the shared recor
|
||||
);
|
||||
assert.equal(sharedReplay.match(/<RerunViewport\b/g)?.length, 1);
|
||||
assert.match(sharedReplay, /recordedSessionRerunProfile/);
|
||||
assert.match(sharedReplay, /useState<0 \| 1>\(costmap \? 1 : 0\)/);
|
||||
assert.match(sharedReplay, /costmap \? 0\.000001 : 0/);
|
||||
assert.match(sharedReplay, /useState<0 \| 1>\(hasTgs \? 1 : 0\)/);
|
||||
assert.match(sharedReplay, /hasTgs \? 0\.000001 : 0/);
|
||||
for (const adapter of ["CanonicalVegetationRerunReplay", "PortableResultReplay"]) {
|
||||
const source = await read(`components/laboratory/${adapter}.tsx`);
|
||||
assert.match(source, /<CanonicalResultRerunReplay/);
|
||||
|
||||
@@ -133,9 +133,12 @@ test("canonical LAB resolves one generation-bound merged RRD", async () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("portable TGS resolves Core cache for the exact result and base, never a legacy LAB", async () => {
|
||||
for (const [prefix, sourceKind] of [
|
||||
["m49-tgs-portable-review", "portable-tgs"],
|
||||
["lab-v1-eomt-ddrnet", "portable-semantic"],
|
||||
]) test(`${sourceKind} resolves Core cache for the exact result and base, never a legacy LAB`, async () => {
|
||||
const base = "a".repeat(64), generation = "b".repeat(64);
|
||||
const resultId = `m49-tgs-portable-review-${"c".repeat(64)}`;
|
||||
const resultId = `${prefix}-${"c".repeat(64)}`;
|
||||
const launch = { sourceUrl: "/api/v1/observation-sessions/source/recording.rrd",
|
||||
viewerSourceUrl: `/api/v1/observation-sessions/source/recording.rrd?generation=${base}`,
|
||||
sha256: base };
|
||||
@@ -147,11 +150,14 @@ test("portable TGS resolves Core cache for the exact result and base, never a le
|
||||
return new Response(null, { headers: { "Content-Type": "application/vnd.rerun.rrd",
|
||||
"Content-Length": "100", "ETag": `"${generation}"`, "X-Rerun-Format": "RRF2" } });
|
||||
};
|
||||
const options = { sourceKind: "portable-tgs", origin: "http://mission-core.test", fetcher };
|
||||
const options = { sourceKind, origin: "http://mission-core.test", fetcher };
|
||||
const replay = await resolveCanonicalLabReplay(resultId, launch, options);
|
||||
assert.equal(replay.viewerSourceUrl, `${replay.sourceUrl}?generation=${generation}`);
|
||||
assert.equal(replay.blueprintSourceUrl, "/api/v1/observation-sessions/source/blueprint.rrd");
|
||||
await assert.rejects(resolveCanonicalLabReplay(`lab-v1-vegetation-shadow-${"c".repeat(64)}`, launch, options));
|
||||
await assert.rejects(resolveCanonicalLabReplay(resultId, launch, {
|
||||
...options, sourceKind: sourceKind === "portable-tgs" ? "portable-semantic" : "portable-tgs",
|
||||
}));
|
||||
await assert.rejects(resolveCanonicalLabReplay(resultId, { ...launch, sha256: "d".repeat(64) }, options));
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server, fetchLabViewProfile, saveLabViewProfile, normalizeLabSceneSettings;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({ appType: "custom", logLevel: "silent",
|
||||
server: { middlewareMode: true } });
|
||||
({ fetchLabViewProfile, saveLabViewProfile } = await server.ssrLoadModule(
|
||||
"/src/core/observatory/labViewProfile.ts"));
|
||||
({ normalizeLabSceneSettings } = await server.ssrLoadModule(
|
||||
"/src/components/laboratory/CanonicalResultRerunReplay.tsx"));
|
||||
});
|
||||
|
||||
after(async () => { await server?.close(); });
|
||||
|
||||
const serverProfile = {
|
||||
schema_version: "missioncore.observatory-lab-view-profile/v1",
|
||||
result_id: "result-1",
|
||||
scene_settings: {
|
||||
point_size: 4.7,
|
||||
accumulation_seconds: 8,
|
||||
color_mode: "height",
|
||||
palette: "viridis",
|
||||
show_grid: false,
|
||||
show_labels: true,
|
||||
show_camera_frustums: false,
|
||||
},
|
||||
updated_at_utc: "2026-09-04T09:30:00.000Z",
|
||||
};
|
||||
|
||||
test("LAB view profile round-trips through the result-scoped server endpoint", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const requests = [];
|
||||
globalThis.fetch = async (url, init = {}) => {
|
||||
requests.push({ url, init });
|
||||
return new Response(JSON.stringify(serverProfile), {
|
||||
status: 200, headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
try {
|
||||
const settings = await fetchLabViewProfile("result-1");
|
||||
assert.equal(settings.pointSize, 4.7);
|
||||
assert.equal(settings.palette, "viridis");
|
||||
const saved = await saveLabViewProfile("result-1", settings);
|
||||
assert.equal(saved.accumulationSeconds, 8);
|
||||
assert.equal(requests[0].url, "/api/v1/observatory/lab-view-profiles/result-1");
|
||||
assert.equal(requests[1].init.method, "PUT");
|
||||
assert.deepEqual(JSON.parse(requests[1].init.body).scene_settings, serverProfile.scene_settings);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("LAB view profile fails closed on a foreign result identity", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = async () => new Response(JSON.stringify({
|
||||
...serverProfile, result_id: "foreign-result",
|
||||
}), { status: 200, headers: { "Content-Type": "application/json" } });
|
||||
try {
|
||||
await assert.rejects(fetchLabViewProfile("result-1"), /другой LAB/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("LAB scene settings normalize typed values before persistence", () => {
|
||||
assert.deepEqual(
|
||||
normalizeLabSceneSettings({
|
||||
pointSize: 28,
|
||||
accumulationSeconds: 305,
|
||||
colorMode: "height",
|
||||
palette: "viridis",
|
||||
customColor: "#ffffff",
|
||||
showGrid: false,
|
||||
showPoints: true,
|
||||
showTrajectory: true,
|
||||
showLabels: true,
|
||||
showCameraFrustums: false,
|
||||
projection: "3d",
|
||||
}),
|
||||
{
|
||||
pointSize: 28,
|
||||
accumulationSeconds: 305,
|
||||
colorMode: "height",
|
||||
palette: "viridis",
|
||||
customColor: "#ffffff",
|
||||
showGrid: false,
|
||||
showPoints: true,
|
||||
showTrajectory: true,
|
||||
showLabels: true,
|
||||
showCameraFrustums: false,
|
||||
projection: "3d",
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -886,7 +886,7 @@ test("M4.6 viewer keeps media and spatial panes on one playback clock", async ()
|
||||
assert.match(canonical, /primary=\{mediaPane\}/);
|
||||
assert.match(canonical, /secondary=\{spatialPane/);
|
||||
assert.match(canonical, /primarySize=\{splitView \? splitPrimarySize : mediaMode !== "none" \? 100 : 0\}/);
|
||||
assert.match(canonical, /resizable=\{splitView && !unifiedContent\}/);
|
||||
assert.match(canonical, /resizable=\{splitView\}/);
|
||||
assert.match(canonical, /separatorLabel="Изменить размер видео\/камеры и 3D\/плана"/);
|
||||
assert.match(canonical, /secondaryMode=\{\{/);
|
||||
assert.match(visual, /playback=\{playbackController\.playback\}/);
|
||||
|
||||
@@ -40,6 +40,7 @@ let formatAccumulationDuration;
|
||||
let resolveRerunSourceUrl;
|
||||
let resolveRecordedBlueprintUrl;
|
||||
let fetchRecordedBlueprintRrd;
|
||||
let recordedCameraJournalContract;
|
||||
let resolveRecordedPerceptionUrl;
|
||||
let fetchRecordedPerceptionRrd;
|
||||
let resolveRecordedPerceptionViewerSourceUrl;
|
||||
@@ -110,6 +111,7 @@ before(async () => {
|
||||
resolveRerunSourceUrl,
|
||||
resolveRecordedBlueprintUrl,
|
||||
fetchRecordedBlueprintRrd,
|
||||
recordedCameraJournalContract,
|
||||
resolveRecordedPerceptionUrl,
|
||||
fetchRecordedPerceptionRrd,
|
||||
resolveRecordedPerceptionViewerSourceUrl,
|
||||
@@ -118,7 +120,7 @@ before(async () => {
|
||||
fetchRecordedPointColorsRrd,
|
||||
recordedPointColorKey,
|
||||
isRecordedPlaybackFullyBuffered,
|
||||
} = await server.ssrLoadModule(
|
||||
} = await server.ssrLoadModule(
|
||||
"/src/components/RerunViewport.tsx",
|
||||
));
|
||||
({ recordedObservationSources } = await server.ssrLoadModule(
|
||||
@@ -129,6 +131,25 @@ before(async () => {
|
||||
));
|
||||
});
|
||||
|
||||
test("follow toggles retain the current recorded camera journal", () => {
|
||||
const before = recordedCameraJournalContract({
|
||||
activeView: "spatial", viewResetGeneration: 0, planView: false, followTrajectory: false,
|
||||
});
|
||||
const afterFollowToggle = recordedCameraJournalContract({
|
||||
activeView: "spatial", viewResetGeneration: 0, planView: false, followTrajectory: true,
|
||||
});
|
||||
const afterPlanToggle = recordedCameraJournalContract({
|
||||
activeView: "spatial", viewResetGeneration: 0, planView: true, followTrajectory: true,
|
||||
});
|
||||
const afterReset = recordedCameraJournalContract({
|
||||
activeView: "spatial", viewResetGeneration: 1, planView: false, followTrajectory: false,
|
||||
});
|
||||
|
||||
assert.equal(afterFollowToggle, before);
|
||||
assert.notEqual(afterPlanToggle, before);
|
||||
assert.notEqual(afterReset, before);
|
||||
});
|
||||
|
||||
test("observation camera windows tile from the bottom-right above the live timeline", () => {
|
||||
const bounds = { width: 1280, height: 720 };
|
||||
const left = initialObservationWindowRect(0, 2, bounds);
|
||||
@@ -444,6 +465,7 @@ test("recorded replay becomes ready only after the complete declared timeline is
|
||||
|
||||
test("recorded blueprint fetch is bounded, strict and sends only display settings", async () => {
|
||||
const calls = [];
|
||||
const cameraLimits = [];
|
||||
const payload = Uint8Array.from([0x52, 0x52, 0x46, 0x32, 0x01]);
|
||||
const result = await fetchRecordedBlueprintRrd(
|
||||
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
|
||||
@@ -464,6 +486,9 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
|
||||
activeView: "perception3d",
|
||||
viewResetGeneration: 1,
|
||||
followTrajectory: true,
|
||||
currentTimeNs: 39_215_263_458,
|
||||
onCameraMaxOrbitalRadius: value => cameraLimits.push(value),
|
||||
unifiedCameraShare: 0.73,
|
||||
perceptionLayers: {
|
||||
enabled: true,
|
||||
detections2d: true,
|
||||
@@ -474,7 +499,10 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
|
||||
calls.push({ input: String(input), init, body: JSON.parse(String(init.body)) });
|
||||
return new Response(payload, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/vnd.rerun.rrd" },
|
||||
headers: {
|
||||
"Content-Type": "application/vnd.rerun.rrd",
|
||||
"X-MissionCore-Camera-Max-Orbital-Radius": "686.024231",
|
||||
},
|
||||
});
|
||||
},
|
||||
},
|
||||
@@ -497,13 +525,20 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
|
||||
active_view: "perception3d",
|
||||
view_reset_generation: 1,
|
||||
follow_trajectory: true,
|
||||
current_time_ns: 39_215_263_458,
|
||||
unified_perception: true,
|
||||
unified_camera_share: 0.73,
|
||||
semantic_layer: null,
|
||||
plan_view: false,
|
||||
eye_position: null,
|
||||
eye_look_target: null,
|
||||
eye_up: null,
|
||||
show_camera_image: true,
|
||||
show_detections_2d: true,
|
||||
show_segmentation: false,
|
||||
show_cuboids_3d: true,
|
||||
});
|
||||
assert.deepEqual(cameraLimits, [686.024231]);
|
||||
|
||||
await fetchRecordedBlueprintRrd(
|
||||
"http://127.0.0.1:5174/api/v1/observation-sessions/session-1/blueprint.rrd",
|
||||
@@ -537,6 +572,7 @@ test("recorded blueprint fetch is bounded, strict and sends only display setting
|
||||
},
|
||||
);
|
||||
assert.equal(calls[1].body.unified_perception, false);
|
||||
assert.equal(calls[1].body.unified_camera_share, 0.46);
|
||||
assert.equal(calls[1].body.show_cuboids_3d, true);
|
||||
|
||||
await assert.rejects(
|
||||
|
||||
@@ -63,14 +63,31 @@ function evidence(id, sourceSessionId, publishedAtUtc) {
|
||||
labId: `LAB-${id}`,
|
||||
sourceSessionId,
|
||||
resultKind: "recorded-evidence",
|
||||
resultId: `result-${id}`,
|
||||
resultId: id,
|
||||
sourceResultId: null,
|
||||
configSha256: "a".repeat(64),
|
||||
runCreatedAtUtc: publishedAtUtc,
|
||||
publishedAtUtc,
|
||||
replayCapability: null,
|
||||
replayCapability: {
|
||||
schemaVersion: "missioncore.observation-lab-replay-capability/v2",
|
||||
kind: "portable-result-review", viewerProfile: "portable-result",
|
||||
timeline: "result-defined", activation: "explicit", commandsEnabled: false,
|
||||
},
|
||||
calculationProfile: null,
|
||||
provenance: { verdict: "must-not-be-inferred" },
|
||||
provenance: {
|
||||
schema_version: "missioncore.observatory-portable-result-publication/v1",
|
||||
authority: { commands_enabled: false },
|
||||
calculation_profile: null, calculation_profile_sha256: "1".repeat(64),
|
||||
job: {}, method: {}, storage: {},
|
||||
source: { session_id: sourceSessionId },
|
||||
run_definition: { definition_sha256: "a".repeat(64) },
|
||||
result_package: { manifest_sha256: "b".repeat(64), artifact_manifest_id: "c".repeat(64) },
|
||||
replay_capability: {
|
||||
schema_version: "missioncore.observation-lab-replay-capability/v2",
|
||||
kind: "portable-result-review", viewer_profile: "portable-result",
|
||||
timeline: "result-defined", activation: "explicit", commands_enabled: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -154,7 +171,9 @@ test("Observatory fetches disjoint read-only source and laboratory projections",
|
||||
const calls = [];
|
||||
const fetcher = async (input, init) => {
|
||||
calls.push({ input: String(input), method: init?.method });
|
||||
return new Response(JSON.stringify({ items: [] }), {
|
||||
return new Response(JSON.stringify({
|
||||
schema_version: "missioncore.observation-session-page/v1", items: [], next_cursor: null,
|
||||
}), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
@@ -175,8 +194,8 @@ test("Observatory fetches disjoint read-only source and laboratory projections",
|
||||
assert.deepEqual(
|
||||
calls.map(({ input }) => input).sort(),
|
||||
[
|
||||
"/api/v1/observation-sessions?limit=50&scope=laboratory&lab_contract=v3",
|
||||
"/api/v1/observation-sessions?limit=50&scope=source",
|
||||
"/api/v1/observation-sessions?limit=50&scope=laboratory&lab_contract=v3&pagination=cursor-v1",
|
||||
"/api/v1/observation-sessions?limit=50&scope=source&pagination=cursor-v1",
|
||||
],
|
||||
);
|
||||
assert.deepEqual(new Set(calls.map(({ method }) => method)), new Set(["GET"]));
|
||||
@@ -203,7 +222,7 @@ test("Observatory exposes bounded-window uncertainty without inventing a broken
|
||||
);
|
||||
});
|
||||
|
||||
test("Observatory projects a typed canonical run only through its exact sourceSessionId", () => {
|
||||
test("historical canonical replay stays out of Observatory without modifying its archive record", () => {
|
||||
const canonicalResultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
|
||||
const canonical = evidence(
|
||||
canonicalResultId,
|
||||
@@ -227,20 +246,144 @@ test("Observatory projects a typed canonical run only through its exact sourceSe
|
||||
commandsEnabled: false,
|
||||
},
|
||||
};
|
||||
const before = structuredClone(canonical);
|
||||
const catalog = buildObservatoryCatalog(
|
||||
[source("20260828T130511Z_viewer_live", "2026-08-28T13:05:11Z")],
|
||||
[canonical],
|
||||
);
|
||||
|
||||
assert.deepEqual(catalog.items[0].evidence[0].recordedRun, {
|
||||
kind: "canonical-recorded-rerun",
|
||||
evidenceSessionId: canonicalResultId,
|
||||
sourceSessionId: "20260828T130511Z_viewer_live",
|
||||
resultId: canonicalResultId,
|
||||
viewerProfile: "recorded-session",
|
||||
timeline: "session_time",
|
||||
activation: "explicit",
|
||||
assert.deepEqual(catalog.items[0].evidence, []);
|
||||
assert.equal(catalog.window.laboratoryCount, 0);
|
||||
assert.deepEqual(canonical, before);
|
||||
});
|
||||
|
||||
test("only admitted portable evidence is visible regardless of LAB label, age or installed version", () => {
|
||||
const legacy = evidence("legacy", "source", "2026-09-03T13:00:00Z");
|
||||
legacy.lab.labId = "LAB M4.9T5";
|
||||
legacy.lab.replayCapability = null;
|
||||
const historical = evidence("old-portable", "source", "2026-07-20T10:00:00Z");
|
||||
historical.lab.labId = "LAB E24"; // Labels must never become an allow/deny list.
|
||||
const current = evidence("new-portable", "source", "2026-09-03T12:00:00Z");
|
||||
const catalog = buildObservatoryCatalog(
|
||||
[source("source", "2026-07-19T10:00:00Z")], [legacy, historical, current],
|
||||
);
|
||||
assert.deepEqual(catalog.items[0].evidence.map((item) => item.sessionId), ["new-portable", "old-portable"]);
|
||||
assert.equal(catalog.window.laboratoryCount, 2);
|
||||
assert.equal(findObservatoryEvidence(catalog, "legacy"), null);
|
||||
});
|
||||
|
||||
test("a malformed portable binding errors instead of silently disappearing as legacy", () => {
|
||||
const broken = evidence("broken", "source", "2026-09-03T12:00:00Z");
|
||||
broken.lab.provenance.source.session_id = "another-source";
|
||||
assert.throws(() => buildObservatoryCatalog(
|
||||
[source("source", "2026-07-19T10:00:00Z")], [broken],
|
||||
), /identity/);
|
||||
});
|
||||
|
||||
function wire(item) {
|
||||
const lab = item.lab;
|
||||
return {
|
||||
id: item.id, label: item.label, started_at_utc: item.startedAtUtc,
|
||||
completed_at_utc: item.completedAtUtc, status: item.status,
|
||||
modalities: item.modalities, duration_seconds: item.durationSeconds, replayable: item.replayable,
|
||||
...(lab ? { lab: {
|
||||
lab_id: "LAB M4.9T5", source_session_id: lab.sourceSessionId, result_kind: lab.resultKind,
|
||||
result_id: lab.resultId, source_result_id: lab.sourceResultId, config_sha256: lab.configSha256,
|
||||
run_created_at_utc: lab.runCreatedAtUtc, published_at_utc: lab.publishedAtUtc,
|
||||
provenance: lab.provenance, replay_capability: lab.replayCapability ? lab.provenance.replay_capability : null,
|
||||
calculation_profile: null,
|
||||
} } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
function pageResponse(items, nextCursor = null) {
|
||||
return new Response(JSON.stringify({
|
||||
schema_version: "missioncore.observation-session-page/v1",
|
||||
items: items.map(wire), next_cursor: nextCursor,
|
||||
}), { headers: { "Content-Type": "application/json" } });
|
||||
}
|
||||
|
||||
test("500 sources and later-page results are searchable metadata, with no archive or viewer requests", async () => {
|
||||
const sources = Array.from({ length: 500 }, (_, index) => source(`source-${index}`, "2026-08-29T10:00:00Z"));
|
||||
const labs = Array.from({ length: 101 }, (_, index) => {
|
||||
const item = evidence(`legacy-${index}`, sources[0].id, "2026-08-29T11:00:00Z");
|
||||
item.lab.replayCapability = null;
|
||||
return item;
|
||||
});
|
||||
labs.push(evidence("portable-after-legacy", sources[499].id, "2026-08-29T12:00:00Z"));
|
||||
const calls = [];
|
||||
const catalog = await fetchObservatoryCatalog({ fetcher: async (input, init) => {
|
||||
const url = new URL(String(input), "http://localhost");
|
||||
calls.push(url);
|
||||
assert.equal(init.method, "GET");
|
||||
assert.equal(url.pathname, "/api/v1/observation-sessions");
|
||||
const rows = url.searchParams.get("scope") === "source" ? sources : labs;
|
||||
const cursor = url.searchParams.get("cursor");
|
||||
const start = cursor === null ? 0 : rows.findIndex((item) => item.id === cursor) + 1;
|
||||
const page = rows.slice(start, start + 100);
|
||||
return pageResponse(page, start + page.length < rows.length ? page.at(-1).id : null);
|
||||
} });
|
||||
assert.equal(calls.length, 7);
|
||||
assert.equal(catalog.items.length, 500);
|
||||
assert.equal(catalog.window.laboratoryCount, 1);
|
||||
assert.equal(catalog.window.sourceLimitReached, false);
|
||||
assert.equal(catalog.window.laboratoryLimitReached, false);
|
||||
assert.equal(catalog.items.find((item) => item.source.id === "source-499").evidence[0].sessionId, "portable-after-legacy");
|
||||
assert.deepEqual(catalog.unresolvedEvidence, []);
|
||||
});
|
||||
|
||||
test("exactly one full page with no cursor is complete, not a guessed truncated window", async () => {
|
||||
const catalog = await fetchObservatoryCatalog({ limit: 1, fetcher: async (url) =>
|
||||
String(url).includes("scope=source")
|
||||
? pageResponse([source("source", "2026-08-29T10:00:00Z")])
|
||||
: pageResponse([evidence("result", "source", "2026-08-29T11:00:00Z")]),
|
||||
});
|
||||
assert.equal(catalog.window.sourceLimitReached, false);
|
||||
assert.equal(catalog.window.laboratoryLimitReached, false);
|
||||
});
|
||||
|
||||
test("paging rejects repeated records, cursor cycles, unsafe cursors and unversioned responses", async () => {
|
||||
for (const failure of ["duplicate", "cycle", "unsafe", "unversioned", "too-large"]) {
|
||||
let index = 0;
|
||||
await assert.rejects(fetchObservatoryCatalog({ limit: 1, fetcher: async (url) => {
|
||||
if (String(url).includes("scope=laboratory")) return pageResponse([]);
|
||||
index += 1;
|
||||
if (failure === "unversioned") return new Response(JSON.stringify({ items: [] }));
|
||||
if (failure === "unsafe") return pageResponse([], "../../private");
|
||||
const id = failure === "duplicate" ? "same" : `source-${index}`;
|
||||
const rows = [source(id, "2026-08-29T10:00:00Z")];
|
||||
if (failure === "too-large") rows.push(source("extra", "2026-08-29T10:00:00Z"));
|
||||
return pageResponse(rows, failure === "cycle" ? "cycle" : id);
|
||||
} }));
|
||||
assert.ok(index <= 2);
|
||||
}
|
||||
});
|
||||
|
||||
test("the traversal cap is explicit and cannot confirm absence of a result", async () => {
|
||||
let count = 0;
|
||||
const catalog = await fetchObservatoryCatalog({ limit: 1, fetcher: async (url) => {
|
||||
if (String(url).includes("scope=source")) return pageResponse([]);
|
||||
count += 1;
|
||||
return pageResponse([], `cursor-${count}`);
|
||||
} });
|
||||
assert.equal(count, 256);
|
||||
assert.equal(catalog.window.laboratoryLimitReached, true);
|
||||
assert.equal(observatoryCatalogConfirmsEvidenceDeletion(catalog, "missing"), false);
|
||||
});
|
||||
|
||||
test("failure aborts the sibling traversal and an already aborted caller starts no requests", async () => {
|
||||
const signals = [];
|
||||
await assert.rejects(fetchObservatoryCatalog({ fetcher: async (url, init) => {
|
||||
signals.push(init.signal);
|
||||
if (String(url).includes("scope=source")) return new Response("unavailable", { status: 503 });
|
||||
return pageResponse([]);
|
||||
} }), /503/);
|
||||
assert.ok(signals.every((signal) => signal.aborted));
|
||||
const request = new AbortController();
|
||||
request.abort();
|
||||
await assert.rejects(fetchObservatoryCatalog({ signal: request.signal, fetcher: async () => {
|
||||
assert.fail("aborted catalog cannot start a request");
|
||||
} }), { name: "AbortError" });
|
||||
});
|
||||
|
||||
test("Observatory applies an exact rename locally without mutating canonical identity", () => {
|
||||
|
||||
@@ -68,6 +68,10 @@ test("missing and stale telemetry show no fabricated advancement", () => {
|
||||
assert.equal(label(job, null), "Ожидаем данные расчёта");
|
||||
assert.equal(label(job, decode({ ...view(), age_seconds: 30 }, job)), "Ожидаем обновление прогресса");
|
||||
});
|
||||
test("terminal jobs keep their real state when progress telemetry is absent", () => {
|
||||
assert.equal(label({ ...job, state: "failed" }, null), "Ошибка расчёта");
|
||||
assert.equal(label({ ...job, state: "succeeded" }, null), "Расчёт завершён");
|
||||
});
|
||||
test("progress is a read-only request bound to the active job", async () => {
|
||||
const calls = [];
|
||||
const progress = await fetchProgress(job, { fetcher: async (path, init) => {
|
||||
|
||||
@@ -35,9 +35,9 @@ test("Observatory is the third independent Polygon workspace", () => {
|
||||
id: "observatory",
|
||||
root: "polygon",
|
||||
label: "Обсерватория",
|
||||
title: "Проверка компьютерного зрения",
|
||||
title: "AI inference",
|
||||
eyebrow: "ТЕСТОВЫЙ КОНТУР / ОБСЕРВАТОРИЯ",
|
||||
description: "Сессии и квалификация компьютерного зрения без доступа к управлению.",
|
||||
description: "Сессии и квалификация без доступа к управлению.",
|
||||
icon: "eye",
|
||||
kind: "observatory",
|
||||
groups: [],
|
||||
@@ -73,9 +73,15 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
|
||||
assert.doesNotMatch(app, /\["recordings", "lab-archive", "observatory"\]/);
|
||||
assert.match(workspaceHub, /case "observatory":[\s\S]*<ObservatoryWorkspace/);
|
||||
assert.match(workspace, /useObservatoryCatalog/);
|
||||
assert.doesNotMatch(workspace, /observatory-lead/);
|
||||
assert.doesNotMatch(workspace, /Расчёт записанных маршрутов выбранными профилями/);
|
||||
assert.match(workspace, /Связанных результатов нет/);
|
||||
assert.match(workspace, /не является выводом о качестве/);
|
||||
assert.match(workspace, /presentedEvidence = selectedSession\?\.evidence \?\? \[\]/);
|
||||
assert.match(workspace, /function compositionProducedEvidence\(/);
|
||||
assert.match(workspace, /publishedAt >= compositionCreatedAt/);
|
||||
assert.match(workspace, /const presentedCompositionRuns = aiJobsController\.runs/);
|
||||
assert.match(workspace, /presentedEvidence = \(selectedSession\?\.evidence \?\? \[\]\)\.filter/);
|
||||
assert.match(workspace, /!presentedCompositionRuns\.some\([\s\S]*compositionProducedEvidence\(run, evidence\)/);
|
||||
assert.doesNotMatch(workspace, /MAX_PRESENTED_EVIDENCE|\.evidence\.slice\(/);
|
||||
assert.match(workspace, /Полнота исторических/);
|
||||
assert.match(workspace, /вне текущего загруженного среза/);
|
||||
@@ -86,12 +92,13 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
|
||||
assert.match(workspace, /observatory-evidence-card__copy/);
|
||||
assert.match(
|
||||
workspace,
|
||||
/function evidenceResultSubtitle[\s\S]*calculationProfile\?\.displayName[\s\S]*`\$\{evidence\.label\} · \$\{profileName\}`[\s\S]*: evidence\.label/,
|
||||
/function evidenceConfigurationLabel[\s\S]*calculationProfile\?\.displayName[\s\S]*return profileName \?\? evidence\.label/,
|
||||
);
|
||||
assert.match(
|
||||
workspace,
|
||||
/<span>\{evidenceResultSubtitle\(evidence\)\}<\/span>/,
|
||||
/<strong>\{evidenceConfigurationLabel\([\s\S]*evidence,[\s\S]*aiJobsController\.moduleLabelsBySetup,[\s\S]*aiJobsController\.jobs,[\s\S]*\)\}<\/strong>/,
|
||||
);
|
||||
assert.doesNotMatch(workspace, /evidenceConfigurationLabel\([^)]*,\s*selectedSession\.source\.label/);
|
||||
assert.match(
|
||||
workspace,
|
||||
/evidence\.recordedRun \? \([\s\S]*name="trash"[\s\S]*name="edit"[\s\S]*Открыть визуальный разбор:[\s\S]*name="eye"/,
|
||||
@@ -101,9 +108,15 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
|
||||
assert.match(workspace, /replay\.kind === "ready"[\s\S]*<CanonicalVegetationRerunReplay/);
|
||||
assert.match(workspace, /<PortableResultReplay review=\{replay\.review\}/);
|
||||
const portable = await read("components/laboratory/PortableResultReplay.tsx");
|
||||
assert.match(portable, /<summary>Документ результата<\/summary>/);
|
||||
assert.doesNotMatch(portable, /Документ результата|JSON.stringify|StatusBadge/);
|
||||
assert.match(portable, /missioncore.recorded-eomt-ddrnet-review\/v2/);
|
||||
assert.match(portable, /sourceKind: "portable-semantic"/);
|
||||
assert.match(portable, /sourceKind: "portable-objects"/);
|
||||
assert.match(portable, /costmap=\{tgs\} semantics=\{semantics\}/);
|
||||
assert.match(portable, /detections=\{objects\}/);
|
||||
assert.match(portable, /<CanonicalResultRerunReplay/);
|
||||
assert.doesNotMatch(workspace, /UNIVERSAL VIEWER|content-addressed artifacts/);
|
||||
assert.doesNotMatch(workspace, /evidence\.lab\.resultKind|без запуска тяжёлого/);
|
||||
assert.match(workspace, /Проверяем точную связь результата/);
|
||||
assert.match(workspace, /role="alert"/);
|
||||
assert.match(workspace, /Повторить/);
|
||||
@@ -112,7 +125,10 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
|
||||
workspace,
|
||||
/const returnToOverview[\s\S]*closeReplay\(\);[\s\S]*scrollIntoView\(\{ block: "start" \}\)/,
|
||||
);
|
||||
assert.match(workspace, /<h3>\{replayEvidence\?\.label \?\? replay\.binding\.resultId\}<\/h3>/);
|
||||
assert.match(
|
||||
workspace,
|
||||
/<h3>\{replayEvidence && selectedSession[\s\S]*evidenceConfigurationLabel\([\s\S]*replayEvidence,[\s\S]*aiJobsController\.moduleLabelsBySetup,[\s\S]*aiJobsController\.jobs,[\s\S]*\)[\s\S]*replay\.binding\.resultId\}<\/h3>/,
|
||||
);
|
||||
assert.doesNotMatch(workspace, /RAVNOVES004TREE · полный маршрут восприятия/);
|
||||
assert.match(workspace, /const selectSession[\s\S]*closeReplay\(\);[\s\S]*setSelectedSessionId/);
|
||||
assert.match(workspace, /data-observatory-authority="observation-only"/);
|
||||
@@ -186,6 +202,10 @@ test("Observatory rename and delete use admitted projection mutations and canoni
|
||||
assert.match(workspace, /<WindowFooterActions>[\s\S]*Сохранить/);
|
||||
assert.match(workspace, /<ConfirmationModal[\s\S]*title="Удалить результат из Обсерватории\?"/);
|
||||
assert.match(workspace, /Исходная сессия, запечатанный лабораторный результат и файлы доказательств/);
|
||||
assert.match(workspace, /renameAICompositionRunProjection/);
|
||||
assert.match(workspace, /deleteAICompositionRunProjection/);
|
||||
assert.match(workspace, /openCompositionRename\(run\)/);
|
||||
assert.match(workspace, /openCompositionDelete\(run\)/);
|
||||
assert.match(
|
||||
workspace,
|
||||
/const result = await renameObservatoryLabProjection\([\s\S]*controller\.applyEvidenceRename\(result\.sessionId, result\.displayName\);[\s\S]*setRenameTarget\(null\);[\s\S]*void controller\.refresh\(\);/,
|
||||
@@ -196,9 +216,12 @@ test("Observatory rename and delete use admitted projection mutations and canoni
|
||||
);
|
||||
|
||||
const deleteFlowStart = workspace.indexOf("const confirmDelete = useCallback");
|
||||
const deleteFlowEnd = workspace.indexOf("}, [closeReplay, controller", deleteFlowStart);
|
||||
const deleteFlowEnd = workspace.indexOf("}, [aiJobsController, closeReplay", deleteFlowStart);
|
||||
assert.ok(deleteFlowStart >= 0 && deleteFlowEnd > deleteFlowStart);
|
||||
const deleteFlow = workspace.slice(deleteFlowStart, deleteFlowEnd);
|
||||
const wholeDeleteFlow = workspace.slice(deleteFlowStart, deleteFlowEnd);
|
||||
const evidenceFlowStart = wholeDeleteFlow.indexOf("if (!deleteTarget?.recordedRun");
|
||||
assert.ok(evidenceFlowStart >= 0);
|
||||
const deleteFlow = wholeDeleteFlow.slice(evidenceFlowStart);
|
||||
const teardown = deleteFlow.indexOf("closeReplay();");
|
||||
const remove = deleteFlow.indexOf("await deleteObservatoryLabProjection");
|
||||
const tombstone = deleteFlow.indexOf("controller.applyEvidenceDeletion");
|
||||
@@ -219,77 +242,128 @@ test("Observatory rename and delete use admitted projection mutations and canoni
|
||||
assert.match(hook, /applyObservatoryCatalogMutationOverlay\(/);
|
||||
});
|
||||
|
||||
test("Observatory keeps one compact selector axis without the obsolete setup detail", async () => {
|
||||
const [workspace, styles, setupHook, jobsHook] = await Promise.all([
|
||||
test("Observatory configures independent AI modules and shows queued evidence progress", async () => {
|
||||
const [workspace, styles, configWindow, jobsHook, sharedReplay] = await Promise.all([
|
||||
read("workspaces/observatory/ObservatoryWorkspace.tsx"),
|
||||
read("styles/observatory.css"),
|
||||
read("core/observatory/useObservatoryLaboratorySetups.ts"),
|
||||
read("core/observatory/useObservatoryRecordedJobs.ts"),
|
||||
read("components/observatory/AIConfigurationWindow.tsx"),
|
||||
read("core/observatory/useAICompositionJobs.ts"),
|
||||
read("components/laboratory/CanonicalResultRerunReplay.tsx"),
|
||||
]);
|
||||
|
||||
assert.match(workspace, /className="observatory-catalog-bar" padding="sm"/);
|
||||
assert.match(workspace, /label="Выбрать сохранённую сессию"/);
|
||||
assert.match(workspace, /label="Выбрать сетап лаборатории"/);
|
||||
assert.match(workspace, /setupController\.error \? \(/);
|
||||
assert.match(workspace, />\s*Сконфигурировать AI-слой\s*<\/Button>/);
|
||||
assert.doesNotMatch(workspace, /label="Выбрать сетап лаборатории"/);
|
||||
assert.doesNotMatch(workspace, />\s*Обновить\s*<\/Button>/);
|
||||
assert.doesNotMatch(workspace, /Выбор меняет только читаемую карточку/);
|
||||
assert.doesNotMatch(workspace, /ObservatorySetupDetail|observatory-setup-detail/);
|
||||
assert.doesNotMatch(styles, /observatory-setup-detail|observatory-setup-results/);
|
||||
assert.match(workspace, /useObservatoryRecordedJobs/);
|
||||
assert.doesNotMatch(workspace, /useObservatoryRecordedJobs|useObservatoryLaboratorySetups/);
|
||||
assert.match(configWindow, /title="Сконфигурировать AI-слой"/);
|
||||
assert.match(configWindow, /label: "Сегментация"/);
|
||||
assert.match(configWindow, /Зависимые блоки подключаются явно/);
|
||||
assert.match(configWindow, /label: "Облако точек \/ TGS"/);
|
||||
assert.match(configWindow, /label: "Дистанция"/);
|
||||
assert.match(configWindow, /Дистанция использует рамки детектора и синхронное облако точек/);
|
||||
assert.match(configWindow, /TGS обрабатывает LiDAR независимо от сегментации и детектора/);
|
||||
assert.match(configWindow, /providers\.length === 1/);
|
||||
assert.match(
|
||||
workspace,
|
||||
/runPreflight\?\.outcome === "queueable"[\s\S]*runPreflight\.submissionAllowed/,
|
||||
configWindow,
|
||||
/<WindowFooterActions>[\s\S]*"Отправляем на Worker…" : "Рассчитать"/,
|
||||
);
|
||||
assert.match(configWindow, /className=\{state === "saving" \? "observatory-ai-config__calculate--saving"/);
|
||||
assert.match(configWindow, /aria-busy=\{state === "saving"\}/);
|
||||
assert.match(styles, /@keyframes observatory-ai-calculate-pulse/);
|
||||
assert.match(
|
||||
workspace,
|
||||
/showCalculate \? \([\s\S]*>\s*Рассчитать\s*<\/Button>/,
|
||||
styles,
|
||||
/\.observatory-ai-config__calculate--saving:disabled \{[\s\S]*animation: observatory-ai-calculate-pulse 1\.8s ease-in-out infinite;[\s\S]*opacity: 1;/,
|
||||
);
|
||||
assert.doesNotMatch(workspace, /recordedJobStatus|presentedJobStatus|Расчёт завершён|Результат опубликован|Вычислено ·/);
|
||||
assert.match(workspace, /setupController\.selectableSetups\.map/);
|
||||
assert.match(workspace, /showCalculate = setupController\.selectedSetup !== null/);
|
||||
assert.match(workspace, /disabled=\{!canSubmitRecordedJob\}/);
|
||||
assert.match(workspace, /aria-busy=\{calculationPending\}/);
|
||||
const runBar = workspace.slice(workspace.indexOf('<div className="observatory-catalog-bar__run"'), workspace.indexOf("{queueStatusError ?"));
|
||||
assert.doesNotMatch(runBar, /StatusBadge/);
|
||||
assert.match(setupHook, /fetchObservatoryPortableLaboratorySetups/);
|
||||
assert.doesNotMatch(setupHook, /fetchObservatoryLaboratorySetups|mergeSetupCatalogs|legacyResult/);
|
||||
assert.match(setupHook, /selectableObservatorySetups\(next\)/);
|
||||
assert.match(setupHook, /selectable\[0\]\?\.setupId \?\? ""/);
|
||||
assert.match(setupHook, /selectedSetupId, sourceSessionId, selectedDefinitionSha256/);
|
||||
assert.match(workspace, /useAICompositionJobs\(selectedSessionId\)/);
|
||||
assert.match(workspace, /observatory-evidence-card--progress/);
|
||||
assert.match(workspace, /const active = run\.state === "running"/);
|
||||
assert.match(workspace, /\{active \? \([\s\S]*observatory-evidence-card__progress/);
|
||||
assert.match(workspace, /run\.state === "ready" \? <Icon name="clipboard"/);
|
||||
assert.match(workspace, /function aiJobStage\(/);
|
||||
assert.match(workspace, /return "Инициализация"/);
|
||||
assert.match(workspace, /return "Передача на сервер"/);
|
||||
assert.match(workspace, /return "Просчёт"/);
|
||||
assert.match(workspace, /Собираем видеоряд для модели/);
|
||||
assert.match(workspace, /измеренная скорость Worker 006: 17–18 FPS/);
|
||||
assert.doesNotMatch(workspace, /phaseElapsedSeconds \/ progress\.completed/);
|
||||
assert.match(workspace, /AI_PROGRESS_PHASE_RANGES/);
|
||||
assert.match(workspace, /"source-preparation": \[5, 25\]/);
|
||||
assert.match(workspace, /computing: \[25, 90\]/);
|
||||
assert.match(workspace, /Подготавливаем камеру и LiDAR/);
|
||||
assert.match(workspace, /прошло.*formatDuration\(progress\.elapsedSeconds\)/s);
|
||||
assert.match(workspace, /Не удалось запустить модель/);
|
||||
assert.doesNotMatch(workspace, /job\.terminalMessage|job\.publication\.error/);
|
||||
assert.doesNotMatch(workspace, /function aiModuleLabel\(/);
|
||||
assert.match(workspace, /aiJobsController\.moduleLabelsBySetup\[job\.setupId\] \?\? job\.setupId/);
|
||||
assert.match(jobsHook, /fetchAIModuleCatalog\(request\.signal\)/);
|
||||
assert.match(jobsHook, /AI_MODULE_SETUP_IDS\[module\.moduleId\]/);
|
||||
assert.match(jobsHook, /run\.presentation\.modules\.flatMap/);
|
||||
assert.match(jobsHook, /Object\.fromEntries\(\[\.\.\.catalogLabels, \.\.\.projectedLabels\]\)/);
|
||||
assert.match(workspace, /<strong>\{moduleLabel\}<\/strong>/);
|
||||
assert.match(workspace, /<strong>\{run\.displayName \?\? run\.presentation\.configurationLabel\}<\/strong>/);
|
||||
assert.doesNotMatch(workspace, /<strong>\{selectedSession\.source\.label\} ·/);
|
||||
assert.match(workspace, /type EvidenceSortDirection = "newest" \| "oldest"/);
|
||||
assert.match(workspace, /const presentedRows = useMemo<readonly ObservatoryEvidenceRow\[]>/);
|
||||
assert.match(workspace, /direction \* \(Date\.parse\(left\.timestamp\) - Date\.parse\(right\.timestamp\)\)/);
|
||||
assert.match(workspace, /className="observatory-evidence__sort"/);
|
||||
assert.match(workspace, /data-direction=\{evidenceSortDirection\}/);
|
||||
assert.match(workspace, /formatTimestamp\(row\.timestamp\)/);
|
||||
assert.match(styles, /\.observatory-evidence-card\s*\{[\s\S]*grid-template-columns:\s*auto minmax\(0, 1fr\) auto auto/);
|
||||
assert.match(styles, /\.observatory-evidence__sort\s*\{[\s\S]*border:\s*0;[\s\S]*outline:\s*0/);
|
||||
assert.match(
|
||||
workspace,
|
||||
/preflightCandidate\.definitionSha256[\s\S]*selectedSetup\?\.runDefinition\?\.definitionSha256/,
|
||||
styles,
|
||||
/\.observatory-evidence-card__copy strong \{[\s\S]*font-size: var\(--nodedc-font-size-sm\);/,
|
||||
);
|
||||
assert.match(sharedReplay, /LAB_SCENE_SETTINGS_STORAGE_PREFIX/);
|
||||
assert.match(sharedReplay, /window\.localStorage\.getItem/);
|
||||
assert.match(sharedReplay, /window\.localStorage\.setItem/);
|
||||
assert.match(sharedReplay, /unifiedCameraShare: blueprintSplitPrimarySize \/ 100/);
|
||||
assert.match(sharedReplay, /setSettingsOpen\(false\);[\s\S]*void saveLabViewProfile/);
|
||||
assert.match(
|
||||
jobsHook,
|
||||
/OPEN_STATES[\s\S]*"preemption-pending",[\s\S]*"reconciliation-required"/,
|
||||
sharedReplay,
|
||||
/showPoints: spatialMode !== null && \(showSourcePoints \|\| showLocalSlam\)/,
|
||||
);
|
||||
assert.match(jobsHook, /return `observatory-ui:\$\{entropy\}`;/);
|
||||
assert.match(jobsHook, /JSON\.stringify\(\[sourceSessionId, setupId, definitionSha256\]\)/);
|
||||
assert.match(jobsHook, /snapshot\.selectionKey === selectionKey \? snapshot : null/);
|
||||
assert.doesNotMatch(jobsHook, /observatory-ui:[^`]*sourceSessionId|\.slice\(0, 160\)/);
|
||||
assert.match(workspace, /recordedJobsController\.refresh\(\)/);
|
||||
assert.match(jobsHook, /POLL_INTERVAL_MS = 1_500/);
|
||||
assert.match(sharedReplay, /showTrajectory: spatialMode !== null && showLocalSlam/);
|
||||
assert.match(
|
||||
sharedReplay,
|
||||
/accumulationSeconds: showLocalSlam \? sceneDraft\.accumulationSeconds : 0/,
|
||||
);
|
||||
assert.match(workspace, /<AICompositionReplay run=\{replay\.run\} \/>/);
|
||||
assert.match(workspace, /function presentAIJob\(job: ObservatoryRecordedJob\)/);
|
||||
assert.match(workspace, /if \(job\.state === "failed"\) return true/);
|
||||
assert.match(workspace, /return job\.state !== "succeeded"/);
|
||||
assert.match(workspace, /function presentLatestAIJobs/);
|
||||
assert.match(workspace, /seenSetups\.has\(job\.setupId\)/);
|
||||
assert.match(configWindow, /fetchAICompositionJobs\(sourceSessionId, request\.signal\)/);
|
||||
assert.match(
|
||||
configWindow,
|
||||
/if \(job\.state === "failed" \|\| job\.publication\.state === "failed"\) continue;/,
|
||||
);
|
||||
assert.match(configWindow, /Текущая конфигурация уже рассчитана/);
|
||||
assert.match(configWindow, /configurationAlreadyExists/);
|
||||
assert.match(configWindow, /Готовый результат будет использован без повторного расчёта/);
|
||||
assert.doesNotMatch(configWindow, /disabled: existingByModule\.has\(module\.moduleId\)/);
|
||||
assert.doesNotMatch(configWindow, /selections\.some\(\(\{ module \}\) => existingByModule/);
|
||||
assert.match(jobsHook, /const OPEN = new Set/);
|
||||
assert.match(jobsHook, /1_500/);
|
||||
assert.match(jobsHook, /globalThis\.setTimeout/);
|
||||
assert.doesNotMatch(jobsHook, /setInterval/);
|
||||
assert.match(workspace, /job\.publication\.state === "published"/);
|
||||
assert.match(workspace, /void controller\.refresh\(\);[\s\S]*setupController\.refresh\(\);/);
|
||||
assert.match(workspace, /onCalculated=\{\(\) => aiJobsController\.refresh\(\)\}/);
|
||||
assert.match(
|
||||
styles,
|
||||
/\.observatory-catalog-bar__controls \{[\s\S]*min-width: 0;[\s\S]*flex: 1 1 auto;[\s\S]*flex-wrap: nowrap;[\s\S]*justify-content: flex-end;/,
|
||||
);
|
||||
assert.match(
|
||||
styles,
|
||||
/\.observatory-catalog-bar__run \{[\s\S]*display: flex;[\s\S]*align-items: center;/,
|
||||
);
|
||||
assert.match(styles, /\.observatory-evidence-card__progress \{/);
|
||||
assert.match(styles, /\.observatory-ai-module-group > header \{/);
|
||||
assert.match(styles, /\.observatory-ai-module-group \{[\s\S]*border: 0;[\s\S]*outline: 0;/);
|
||||
assert.match(
|
||||
styles,
|
||||
/@container observatory-workspace \(max-width: 920px\)[\s\S]*\.observatory-catalog-bar,[\s\S]*\.observatory-catalog-bar__controls,[\s\S]*flex-direction: column;/,
|
||||
);
|
||||
assert.match(setupHook, /catalog\?\.sourceSessionId === sourceSessionId/);
|
||||
assert.match(setupHook, /preflightRequest\.current\?\.abort\(\)/);
|
||||
assert.match(setupHook, /preflightRequest\.current !== request/);
|
||||
assert.match(
|
||||
setupHook,
|
||||
/state !== "ready" \|\| !selectedSetup \|\| preflight\.kind !== "idle"[\s\S]*void check\(\)/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server, createRecordedRerunCameraJournal, initialEye;
|
||||
before(async () => {
|
||||
server = await createServer({ appType: "custom", logLevel: "silent",
|
||||
server: { middlewareMode: true } });
|
||||
const module = await server.ssrLoadModule(
|
||||
"/src/components/rerun/recordedRerunCameraJournal.ts");
|
||||
createRecordedRerunCameraJournal = module.createRecordedRerunCameraJournal;
|
||||
initialEye = module.RECORDED_RERUN_ORBITAL_EYE;
|
||||
});
|
||||
after(async () => { await server?.close(); });
|
||||
|
||||
class FakeEvent {
|
||||
constructor(type, init = {}) { this.type = type; Object.assign(this, init); }
|
||||
}
|
||||
|
||||
class FakeTarget {
|
||||
listeners = new Map();
|
||||
emitted = [];
|
||||
addEventListener(type, listener) {
|
||||
const listeners = this.listeners.get(type) ?? [];
|
||||
listeners.push(listener); this.listeners.set(type, listeners);
|
||||
}
|
||||
removeEventListener(type, listener) {
|
||||
this.listeners.set(type, (this.listeners.get(type) ?? []).filter(item => item !== listener));
|
||||
}
|
||||
dispatchEvent(event) {
|
||||
this.emitted.push(event);
|
||||
for (const listener of this.listeners.get(event.type) ?? []) listener(event);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const radius = (eye) => Math.hypot(
|
||||
eye.position[0] - eye.lookTarget[0],
|
||||
eye.position[1] - eye.lookTarget[1],
|
||||
eye.position[2] - eye.lookTarget[2],
|
||||
);
|
||||
|
||||
test("recorded orbital eye tracks only 3D viewport navigation", () => {
|
||||
const scope = new FakeTarget();
|
||||
const timers = [];
|
||||
Object.assign(scope, {
|
||||
WheelEvent: FakeEvent,
|
||||
requestAnimationFrame(callback) { callback(); return 1; },
|
||||
setTimeout(callback) { timers.push(callback); return timers.length; },
|
||||
});
|
||||
const canvas = new FakeTarget();
|
||||
Object.assign(canvas, {
|
||||
clientHeight: 200,
|
||||
isConnected: true,
|
||||
getBoundingClientRect: () => ({
|
||||
left: 100, right: 500, top: 50, bottom: 250, width: 400, height: 200,
|
||||
}),
|
||||
});
|
||||
const journal = createRecordedRerunCameraJournal(canvas, scope);
|
||||
journal.configure(initialEye, 0.46);
|
||||
const pointer = (type, x, y, buttons) => new FakeEvent(type, {
|
||||
clientX: x, clientY: y, button: 0, buttons, pointerId: 7, pointerType: "mouse",
|
||||
altKey: false, ctrlKey: false, metaKey: false, shiftKey: false,
|
||||
});
|
||||
|
||||
canvas.dispatchEvent(pointer("pointerdown", 200, 100, 1));
|
||||
scope.dispatchEvent(pointer("pointermove", 240, 120, 1));
|
||||
scope.dispatchEvent(pointer("pointerup", 240, 120, 0));
|
||||
assert.deepEqual(journal.current(), initialEye);
|
||||
|
||||
canvas.dispatchEvent(pointer("pointerdown", 380, 100, 1));
|
||||
scope.dispatchEvent(pointer("pointermove", 420, 120, 1));
|
||||
scope.dispatchEvent(pointer("pointerup", 420, 120, 0));
|
||||
const rotated = journal.current();
|
||||
assert.notDeepEqual(rotated.position, initialEye.position);
|
||||
assert.ok(Math.abs(radius(rotated) - radius(initialEye)) < 1e-9);
|
||||
|
||||
journal.setMaxOrbitalRadius(40);
|
||||
scope.dispatchEvent(new FakeEvent("wheel", {
|
||||
clientX: 380, clientY: 130, deltaX: 0, deltaY: 2_000, deltaMode: 0,
|
||||
altKey: false, ctrlKey: false, metaKey: false, shiftKey: false,
|
||||
}));
|
||||
assert.ok(Math.abs(radius(journal.current()) - 40) < 1e-9);
|
||||
|
||||
journal.configure(rotated, 0.46);
|
||||
|
||||
scope.dispatchEvent(new FakeEvent("wheel", {
|
||||
clientX: 0, clientY: 0, deltaX: 0, deltaY: -20, deltaMode: 0,
|
||||
altKey: false, ctrlKey: false, metaKey: false, shiftKey: false,
|
||||
}));
|
||||
const zoomed = journal.current();
|
||||
assert.ok(Math.abs(radius(zoomed) - radius(rotated) * Math.exp(-20 / 200)) < 1e-9);
|
||||
|
||||
const snapshot = journal.current();
|
||||
journal.setSpatialViewportStart(0.8);
|
||||
scope.dispatchEvent(new FakeEvent("wheel", {
|
||||
clientX: 380, clientY: 130, deltaX: 0, deltaY: -20, deltaMode: 0,
|
||||
}));
|
||||
assert.deepEqual(journal.current(), snapshot);
|
||||
journal.dispose();
|
||||
});
|
||||
@@ -0,0 +1,261 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server, createIsolatedRerunHost, keepRecordedBlueprintSession, createRecordedRerunFacade, createRecordedRerunOwner, relayIsolatedRerunInput;
|
||||
before(async () => {
|
||||
server = await createServer({ appType: "custom", logLevel: "silent",
|
||||
server: { middlewareMode: true } });
|
||||
({ createIsolatedRerunHost } = await server.ssrLoadModule(
|
||||
"/src/components/rerun/isolatedRerunHost.ts"));
|
||||
({ keepRecordedBlueprintSession } = await server.ssrLoadModule(
|
||||
"/src/core/observation/recordedBlueprintLifecycle.ts"));
|
||||
({ createRecordedRerunFacade } = await server.ssrLoadModule(
|
||||
"/src/components/rerun/recordedRerunFacade.ts"));
|
||||
({ createRecordedRerunOwner } = await server.ssrLoadModule(
|
||||
"/src/components/rerun/recordedRerunOwner.ts"));
|
||||
({ relayIsolatedRerunInput } = await server.ssrLoadModule(
|
||||
"/src/components/rerun/isolatedRerunInput.ts"));
|
||||
});
|
||||
after(async () => { await server?.close(); });
|
||||
|
||||
function fixture(t, { stopFails = false } = {}) {
|
||||
const timers = new Map();
|
||||
let timerId = 0, creates = 0, stops = 0, removals = 0;
|
||||
t.mock.method(globalThis, "setTimeout", (fn) => {
|
||||
timers.set(++timerId, fn); return timerId;
|
||||
});
|
||||
t.mock.method(globalThis, "clearTimeout", (id) => timers.delete(id));
|
||||
const listeners = new Map();
|
||||
const viewer = { stop() { stops++; if (stopFails) throw new Error("partial start"); } };
|
||||
const mount = {};
|
||||
const frame = {
|
||||
contentWindow: {
|
||||
missionCoreRerun: createRecordedRerunOwner(() => { creates++; return viewer; }, mount),
|
||||
addEventListener: (name, fn) => listeners.set(name, fn),
|
||||
removeEventListener: (name) => listeners.delete(name),
|
||||
},
|
||||
remove() { removals++; },
|
||||
};
|
||||
const host = { dataset: {}, ownerDocument: { createElement(name) {
|
||||
assert.equal(name, "iframe"); return frame;
|
||||
} }, append(child) { assert.equal(child, frame); } };
|
||||
return { host, frame, viewer, mount, timers, listeners,
|
||||
counts: () => ({ creates, stops, removals }) };
|
||||
}
|
||||
|
||||
function bridge(native, mount = {}) {
|
||||
const child = createRecordedRerunOwner(() => native, mount);
|
||||
const parent = createRecordedRerunFacade((request, bytes) => {
|
||||
assert.equal(typeof request, "string");
|
||||
const result = child.invoke(request, bytes);
|
||||
assert.equal(typeof result, "string");
|
||||
return result;
|
||||
});
|
||||
child.connect(message => {
|
||||
assert.equal(typeof message, "string");
|
||||
parent.notify(message);
|
||||
});
|
||||
return parent;
|
||||
}
|
||||
|
||||
test("recorded realm terminates on close even if upstream stop throws", async (t) => {
|
||||
const f = fixture(t, { stopFails: true });
|
||||
const scope = createIsolatedRerunHost(f.host);
|
||||
assert.equal(f.frame.src, "/rerun-runtime.html");
|
||||
f.frame.onload();
|
||||
const ready = await scope.ready;
|
||||
assert.notEqual(ready.viewer, f.viewer);
|
||||
assert.equal(ready.mount, f.host);
|
||||
assert.equal(f.timers.size, 0);
|
||||
assert.equal(f.listeners.size, 1);
|
||||
scope.dispose(); scope.dispose();
|
||||
assert.deepEqual(f.counts(), { creates: 1, stops: 1, removals: 1 });
|
||||
assert.equal(f.listeners.size, 0);
|
||||
assert.equal(f.frame.onload, null);
|
||||
assert.equal(ready.viewer.ready, false);
|
||||
});
|
||||
|
||||
test("native frame input relays Escape and leaves pointer ownership inside Rerun", () => {
|
||||
const listeners = new Map(), events = [];
|
||||
class ParentEvent { constructor(type, init) { this.type = type; Object.assign(this, init); } }
|
||||
const host = { dataset: {}, ownerDocument: { defaultView: {
|
||||
KeyboardEvent: ParentEvent, PointerEvent: ParentEvent,
|
||||
} }, dispatchEvent: event => events.push(event) };
|
||||
const child = {
|
||||
addEventListener: (name, fn) => listeners.set(name, fn),
|
||||
removeEventListener: name => listeners.delete(name),
|
||||
};
|
||||
const release = relayIsolatedRerunInput(host, child, null);
|
||||
assert.equal(listeners.has("pointerdown"), false);
|
||||
listeners.get("keydown")({ key: "Escape", defaultPrevented: false });
|
||||
assert.equal(events[0].key, "Escape");
|
||||
release(); release();
|
||||
assert.equal(listeners.size, 0); assert.equal(events.length, 1);
|
||||
});
|
||||
|
||||
test("disposed facade, channel and unsubscribe reject native use and copy event values", async () => {
|
||||
let eventCallback, unsubscribed = 0, sends = 0, starts = 0;
|
||||
const range = { min: 1, max: 2 }, mount = {};
|
||||
const native = {
|
||||
ready: true, stop() {},
|
||||
async start(source, parent) { assert.equal(parent, mount); starts++; },
|
||||
get_time_range: () => range,
|
||||
on(event, callback) { eventCallback = callback; return () => { unsubscribed++; }; },
|
||||
open_channel: () => ({ ready: true, send_rrd() { sends++; }, close() {} }),
|
||||
};
|
||||
const { facade, dispose } = bridge(native, mount);
|
||||
await facade.start(null, {}, null);
|
||||
assert.equal(starts, 1);
|
||||
let event;
|
||||
const unsubscribe = facade.on("time_update", value => { event = value; });
|
||||
eventCallback(range);
|
||||
assert.deepEqual(event, range); assert.notEqual(event, range);
|
||||
assert.notEqual(facade.get_time_range(), range);
|
||||
const channel = facade.open_channel();
|
||||
channel.send_rrd(new Uint8Array());
|
||||
dispose(); dispose(); unsubscribe();
|
||||
assert.equal(unsubscribed, 1);
|
||||
assert.equal(channel.ready, false);
|
||||
channel.send_rrd(new Uint8Array());
|
||||
eventCallback({ min: 3, max: 4 });
|
||||
assert.deepEqual(event, range);
|
||||
assert.equal(sends, 1);
|
||||
await assert.rejects(facade.start(null, {}, null), /disposed/);
|
||||
});
|
||||
|
||||
test("close before frame load cancels startup and cannot reopen", async (t) => {
|
||||
const f = fixture(t);
|
||||
const scope = createIsolatedRerunHost(f.host);
|
||||
const lateLoad = f.frame.onload;
|
||||
const rejected = assert.rejects(scope.ready, /disposed/);
|
||||
scope.dispose(); lateLoad();
|
||||
await rejected;
|
||||
assert.deepEqual(f.counts(), { creates: 0, stops: 0, removals: 1 });
|
||||
assert.equal(f.timers.size, 0);
|
||||
});
|
||||
|
||||
test("closing during SDK startup also releases a late successful start", async () => {
|
||||
let finish, stops = 0;
|
||||
const owner = bridge({
|
||||
start: () => new Promise(resolve => { finish = resolve; }),
|
||||
stop() { stops++; },
|
||||
}, {});
|
||||
const pending = owner.facade.start(null, {}, null);
|
||||
const rejected = assert.rejects(pending, /disposed/);
|
||||
owner.dispose();
|
||||
assert.equal(stops, 1);
|
||||
finish();
|
||||
await rejected;
|
||||
await Promise.resolve();
|
||||
assert.equal(stops, 2);
|
||||
assert.equal(owner.facade.ready, false);
|
||||
});
|
||||
|
||||
test("late failed startup cannot revive a disposed parent and still finishes cleanup", async () => {
|
||||
let fail, stops = 0;
|
||||
const owner = bridge({
|
||||
start: () => new Promise((_resolve, reject) => { fail = reject; }),
|
||||
stop() { stops++; if (stops > 1) throw new Error("already gone"); },
|
||||
}, {});
|
||||
const pending = owner.facade.start(null, {}, null);
|
||||
const rejected = assert.rejects(pending, /disposed/);
|
||||
owner.dispose();
|
||||
fail(new Error("startup failed"));
|
||||
await rejected;
|
||||
await Promise.resolve();
|
||||
assert.equal(stops, 2);
|
||||
assert.equal(owner.facade.ready, false);
|
||||
});
|
||||
|
||||
test("primitive bridge preserves all public clock/control arguments and errors", async () => {
|
||||
const calls = [];
|
||||
const methods = {
|
||||
open: [["one.rrd", "two.rrd"]], close: ["one.rrd"],
|
||||
override_panel_state: ["time", "hidden"],
|
||||
get_active_recording_id: [], get_active_timeline: ["recording"],
|
||||
get_current_time: ["recording", "session_time"], get_playing: ["recording"],
|
||||
get_time_range: ["recording", "session_time"],
|
||||
set_active_timeline: ["recording", "session_time"],
|
||||
set_current_time: ["recording", "session_time", 123.456],
|
||||
set_playing: ["recording", false],
|
||||
};
|
||||
const native = { stop() {}, start: async () => { throw new Error("native startup failure"); } };
|
||||
for (const name of Object.keys(methods)) native[name] = (...args) => { calls.push([name, args]); return 1; };
|
||||
const { facade, dispose } = bridge(native);
|
||||
for (const [name, args] of Object.entries(methods)) facade[name](...args);
|
||||
assert.deepEqual(calls, Object.entries(methods));
|
||||
await assert.rejects(facade.start(null, {}, null), /native startup failure/);
|
||||
dispose();
|
||||
});
|
||||
|
||||
test("sync native failures become parent-owned errors and optional channel names stay undefined", () => {
|
||||
const nativeError = new Error("native failure");
|
||||
let channelName = "unobserved";
|
||||
const { facade, dispose } = bridge({
|
||||
stop() {},
|
||||
open() { throw nativeError; },
|
||||
open_channel(name) { channelName = name; return { close() {} }; },
|
||||
});
|
||||
assert.throws(() => facade.open("rrd"), error => error !== nativeError && /native failure/.test(error.message));
|
||||
facade.open_channel();
|
||||
assert.equal(channelName, undefined);
|
||||
dispose();
|
||||
});
|
||||
|
||||
test("dispose closes every auxiliary channel even if one channel fails", () => {
|
||||
const closed = [];
|
||||
let stops = 0;
|
||||
const owner = bridge({
|
||||
stop() { stops++; },
|
||||
open_channel: name => ({ close() {
|
||||
closed.push(name);
|
||||
if (name === "first") throw new Error("already gone");
|
||||
} }),
|
||||
}, {});
|
||||
const first = owner.facade.open_channel("first");
|
||||
const second = owner.facade.open_channel("second");
|
||||
owner.dispose(); owner.dispose(); first.close(); second.close();
|
||||
assert.deepEqual(closed, ["first", "second"]);
|
||||
assert.equal(stops, 1);
|
||||
});
|
||||
|
||||
test("host load timeout removes the frame and clears its timer", async (t) => {
|
||||
const f = fixture(t);
|
||||
const scope = createIsolatedRerunHost(f.host);
|
||||
const rejected = assert.rejects(scope.ready, /timed out/);
|
||||
[...f.timers.values()][0]();
|
||||
await rejected;
|
||||
assert.equal(f.counts().removals, 1);
|
||||
assert.equal(f.timers.size, 0);
|
||||
});
|
||||
|
||||
test("blueprint termination cancels renewal and sends one bounded keepalive release", async () => {
|
||||
let tick, canceled = 0;
|
||||
const requests = [];
|
||||
const release = keepRecordedBlueprintSession({
|
||||
endpointUrl: "/api/v1/observation-sessions/source/blueprint.rrd",
|
||||
origin: "http://core.test", applicationId: "nodedc_mission_core_recorded",
|
||||
recordingId: "recording", ownerId: "a".repeat(32),
|
||||
fetcher: async (url, init) => { requests.push({ url, ...init }); return {}; },
|
||||
schedule(fn) { tick = fn; return 1; }, cancel() { canceled++; },
|
||||
});
|
||||
tick();
|
||||
const renewal = requests[0];
|
||||
release(); release(); tick();
|
||||
assert.equal(canceled, 1);
|
||||
assert.equal(requests.length, 2);
|
||||
assert.equal(renewal.signal.aborted, true);
|
||||
assert.equal(requests[1].keepalive, true);
|
||||
assert.equal(requests[1].url, "http://core.test/api/v1/observation-sessions/source/blueprint-lifecycle");
|
||||
assert.deepEqual(JSON.parse(requests[1].body), {
|
||||
action: "release", application_id: "nodedc_mission_core_recorded",
|
||||
recording_id: "recording", blueprint_session_id: "a".repeat(32),
|
||||
});
|
||||
});
|
||||
|
||||
test("blueprint lease refuses a foreign endpoint", () => {
|
||||
assert.throws(() => keepRecordedBlueprintSession({ endpointUrl: "https://other.test/blueprint.rrd",
|
||||
origin: "http://core.test", applicationId: "app", recordingId: "recording", ownerId: "owner",
|
||||
}), /Unsafe/);
|
||||
});
|
||||
@@ -90,8 +90,15 @@ test("one canonical LAB replay generation reaches the same native receiver", ()
|
||||
);
|
||||
});
|
||||
|
||||
test("portable replay uses recorded admission and the shared source blueprint", () => {
|
||||
const source = `/api/v1/observatory/portable-results/m49-tgs-portable-review-${"c".repeat(64)}/replays/${"d".repeat(64)}/recording.rrd`;
|
||||
for (const prefix of [
|
||||
"m49-tgs-portable-review",
|
||||
"lab-v1-eomt-ddrnet",
|
||||
"ai-layer-ddrnet",
|
||||
"ai-layer-eomt",
|
||||
"ai-layer-rf-detr",
|
||||
"ai-layer-object-distance",
|
||||
]) test(`${prefix} replay uses recorded admission and the shared source blueprint`, () => {
|
||||
const source = `/api/v1/observatory/portable-results/${prefix}-${"c".repeat(64)}/replays/${"d".repeat(64)}/recording.rrd`;
|
||||
const blueprint = "/api/v1/observation-sessions/source/blueprint.rrd";
|
||||
assert.equal(isRecordedRrdSource(source), true);
|
||||
assert.equal(isRecordedRrdSource(source.replace("/replays/", "/untrusted/")), false);
|
||||
|
||||
@@ -97,7 +97,7 @@ test("M4 keeps independent semantic controls in media and spatial panes", async
|
||||
assert.match(source, /aria-label="Слои 3D и плана"/);
|
||||
assert.match(canonical, /data-pane-mode="media"/);
|
||||
assert.match(canonical, /data-pane-mode="spatial"/);
|
||||
assert.match(canonical, /modeControlsVisible=\{!splitView\}/);
|
||||
assert.match(canonical, /modeControlsVisible=\{!splitView && !paneToolbarsAlwaysVisible\}/);
|
||||
assert.match(source, /semanticOverlay=\{mediaMode === "video" \? semanticOverlay : undefined\}/);
|
||||
});
|
||||
|
||||
|
||||
@@ -497,14 +497,17 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
|
||||
assert.match(rerunSource, /resolveReplay\(resultId, value/);
|
||||
assert.doesNotMatch(rerunSource, /canonical-overlay\.rrd/);
|
||||
assert.match(rerunSource, /unifiedPerception: splitView/);
|
||||
assert.match(rerunSource, /lockPerceptionCameraInteraction: mediaMode !== null/);
|
||||
assert.match(rerunSource, /!splitView[\s\S]*event\.button !== 0/);
|
||||
assert.match(rerunSource, /if \(!splitView\) stopNativeSplitTracking\(\)/);
|
||||
assert.match(rerunSource, /event\.target instanceof HTMLCanvasElement/);
|
||||
assert.match(rerunSource, /--canonical-rerun-camera-pane/);
|
||||
assert.match(rerunSource, /onPointerDownCapture=\{splitView \? trackNativeSplit : undefined\}/);
|
||||
assert.match(rerunSource, /lockPerceptionCameraInteraction: false/);
|
||||
assert.match(rerunSource, /&& !semantics[\s\S]*&& !detections[\s\S]*&& !costmap/);
|
||||
assert.doesNotMatch(rerunSource, /trackNativeSplit|rerunNativeCursor|rerunNativeInput/);
|
||||
assert.match(canonicalSource, /resizable=\{splitView\}/);
|
||||
assert.match(rerunSource, /unifiedCameraShare: blueprintSplitPrimarySize \/ 100/);
|
||||
assert.match(rerunSource, /data-split-view=\{splitView \? "true" : undefined\}/);
|
||||
assert.match(rerunSource, /mediaMode === null[\s\S]*\? 0[\s\S]*: splitView[\s\S]*\? nativeSplitPercentRef\.current[\s\S]*: 100/);
|
||||
assert.match(rerunSource, /max=\{500\}/);
|
||||
assert.match(rerunSource, /exactValueBounds=\{\{ min: 0 \}\}/);
|
||||
assert.match(replayStyles, /m4-replay-threat-visual__pane-toolbar \{[\s\S]*flex-wrap: wrap;/);
|
||||
assert.match(replayStyles, /m4-replay-threat-visual__spatial-toolbar-end \{[\s\S]*display: contents;/);
|
||||
assert.match(rerunSource, /<ToastStack items=\{toasts\}/);
|
||||
assert.match(rerunSource, /isRecordedPlaybackPresentationReady\(viewerStatus, playback\)/);
|
||||
assert.match(rerunSource, /data-presentation-state=\{presentationState\}/);
|
||||
assert.match(rerunSource, /<ActivityIndicator label="Загружаем синхронизированную запись"/);
|
||||
@@ -517,22 +520,14 @@ test("vegetation realtime LAB uses one upstream Rerun clock and keeps archival r
|
||||
replayStyles,
|
||||
/canonical-vegetation-rerun-replay__timeline \.observation-timeline__playback \{[\s\S]*grid-template-columns: auto auto minmax\(0, 1fr\) auto;/,
|
||||
);
|
||||
assert.match(
|
||||
replayStyles,
|
||||
/canonical-vegetation-rerun-replay__viewport-lock[\s\S]*rerun-viewport__camera-lock \{[\s\S]*width: var\(--canonical-rerun-camera-pane, 100%\);/,
|
||||
);
|
||||
assert.match(
|
||||
replayStyles,
|
||||
/canonical-vegetation-rerun-replay__viewport-lock\[data-split-view="true"\][\s\S]*width: calc\(var\(--canonical-rerun-camera-pane, 46%\) - 0\.75rem\);/,
|
||||
);
|
||||
assert.doesNotMatch(rerunSource, /LaboratoryRecordedClipPlayer|LaboratoryMetricEvidenceScene/);
|
||||
assert.match(resultSource, /point-aligned 3D semantics пока не запечатаны/);
|
||||
assert.match(rerunSource, /value: "tgs", label: "TGS", disabled: !costmap/);
|
||||
assert.match(rerunSource, /hasTgs \? toggle\("TGS", showTgs/);
|
||||
assert.match(canonicalSource, /primary=\{mediaPane\}/);
|
||||
assert.match(canonicalSource, /secondary=\{spatialPane/);
|
||||
assert.match(canonicalSource, /missioncore\.canonical-recorded-lab-replay\/v1/);
|
||||
assert.match(canonicalSource, /separatorLabel="Изменить размер видео\/камеры и 3D\/плана"/);
|
||||
assert.match(canonicalSource, /resizable=\{splitView && !unifiedContent\}/);
|
||||
assert.match(canonicalSource, /resizable=\{splitView\}/);
|
||||
assert.match(resultSource, /linked canonical M4\.9 TGS evidence/);
|
||||
assert.match(resultSource, /linkedTgsResultId/);
|
||||
assert.match(benchmarkSource, /M48MaskComparisonVisual/);
|
||||
|
||||
Reference in New Issue
Block a user