feat(observatory): add portable LAB V1 foundation
This commit is contained in:
@@ -5,6 +5,7 @@ import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let fetchObservatoryLaboratorySetups;
|
||||
let fetchObservatoryPortableLaboratorySetups;
|
||||
let preflightObservatoryLaboratorySetup;
|
||||
let ObservatoryLaboratorySetupContractError;
|
||||
|
||||
@@ -70,6 +71,76 @@ function setup() {
|
||||
};
|
||||
}
|
||||
|
||||
function portableSetup() {
|
||||
return {
|
||||
setup_id: "lab-v1-eomt-ddrnet-portable-v1",
|
||||
display_name: "LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39",
|
||||
description: "Проверка записанной K1-сессии моделями EoMT и DDRNet; только наблюдение.",
|
||||
origin: "portable-definition",
|
||||
source_requirements: {
|
||||
plugin_id: "nodedc.device.xgrids-lixelkity-k1",
|
||||
archive_id: "xgrids-k1.viewer-live.evidence",
|
||||
required_modalities: ["point-cloud", "trajectory", "video"],
|
||||
camera_source_id: "sensor.camera.right",
|
||||
camera_semantic_channel_id: "camera.video.recorded",
|
||||
recorded_media_type: 'video/mp4; codecs="avc1.641028"',
|
||||
recorded_media_init_sha256: "1".repeat(64),
|
||||
camera_width: 800,
|
||||
camera_height: 600,
|
||||
calibration_slot: "camera_1",
|
||||
calibration_identity_sha256: "2".repeat(64),
|
||||
exactly_one_media_epoch: true,
|
||||
seekable: true,
|
||||
},
|
||||
run_definition: {
|
||||
definition_id: "lab-v1-eomt-ddrnet-portable",
|
||||
version: 1,
|
||||
definition_sha256: "3".repeat(64),
|
||||
result_schema: "missioncore.recorded-eomt-ddrnet-review/v2",
|
||||
result_kind: "recorded-perception-qualification",
|
||||
models: [
|
||||
{
|
||||
name: "EoMT Cityscapes Large 1024",
|
||||
release_id: "eomt-cityscapes-large-1024-v1",
|
||||
model_id: "tue-mps/cityscapes_semantic_eomt_large_1024",
|
||||
architecture: "EomtForUniversalSegmentation",
|
||||
},
|
||||
{
|
||||
name: "DDRNet-39",
|
||||
release_id: "lab-v1-ddrnet-39-goose-fine-64-v1",
|
||||
model_id: "goose-ddrnet-class-512",
|
||||
architecture: "ddrnet_39",
|
||||
},
|
||||
],
|
||||
},
|
||||
source_compatibility: {
|
||||
outcome: "pass",
|
||||
compatible: true,
|
||||
reason: "Запись соответствует требованиям EoMT + DDRNet.",
|
||||
evidence: {
|
||||
frame_count: 6830,
|
||||
timeline_start_seconds: 0,
|
||||
timeline_end_seconds: 819,
|
||||
},
|
||||
},
|
||||
executor: {
|
||||
contour_id: "worker-006",
|
||||
state: "not-installed",
|
||||
ready: false,
|
||||
reason: "Immutable executor release не установлен.",
|
||||
},
|
||||
existing_results: [],
|
||||
preflight: {
|
||||
outcome: "blocked",
|
||||
action: "blocked",
|
||||
reason: "Вычислительный контур LAB V1 пока недоступен.",
|
||||
submission_allowed: false,
|
||||
existing_result_ids: [],
|
||||
},
|
||||
authority,
|
||||
};
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
@@ -78,6 +149,7 @@ before(async () => {
|
||||
});
|
||||
({
|
||||
fetchObservatoryLaboratorySetups,
|
||||
fetchObservatoryPortableLaboratorySetups,
|
||||
preflightObservatoryLaboratorySetup,
|
||||
ObservatoryLaboratorySetupContractError,
|
||||
} = await server.ssrLoadModule("/src/core/observatory/laboratorySetups.ts"));
|
||||
@@ -111,6 +183,104 @@ test("Observatory setup catalog keeps definition identity separate from executor
|
||||
assert.equal(calls[0].init.method, "GET");
|
||||
});
|
||||
|
||||
test("portable LAB V1 reports compatible source separately from unavailable Worker", async () => {
|
||||
const calls = [];
|
||||
const selected = (await fetchObservatoryPortableLaboratorySetups("source-a", {
|
||||
fetcher: async (input, init) => {
|
||||
calls.push({ input: String(input), init });
|
||||
return new Response(JSON.stringify({
|
||||
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
|
||||
source_session_id: "source-a",
|
||||
setups: [portableSetup()],
|
||||
authority,
|
||||
}), { status: 200 });
|
||||
},
|
||||
})).setups[0];
|
||||
|
||||
assert.equal(selected.origin, "portable-definition");
|
||||
assert.equal(selected.compatibility.compatible, true);
|
||||
assert.equal(selected.executor.state, "not-installed");
|
||||
assert.equal(selected.preflight.submissionAllowed, false);
|
||||
assert.deepEqual(selected.runDefinition.models.map((model) => model.name), [
|
||||
"EoMT Cityscapes Large 1024",
|
||||
"DDRNet-39",
|
||||
]);
|
||||
assert.equal(
|
||||
calls[0].input,
|
||||
"/api/v1/observatory/portable-laboratory-setups?source_session_id=source-a",
|
||||
);
|
||||
|
||||
let unexpectedNetworkCall = false;
|
||||
const preflight = await preflightObservatoryLaboratorySetup("source-a", selected, {
|
||||
fetcher: async () => {
|
||||
unexpectedNetworkCall = true;
|
||||
throw new Error("portable preflight must use its server projection");
|
||||
},
|
||||
});
|
||||
assert.equal(unexpectedNetworkCall, false);
|
||||
assert.equal(preflight.outcome, "blocked");
|
||||
assert.equal(preflight.submissionAllowed, false);
|
||||
assert.equal(preflight.checks[0].outcome, "pass");
|
||||
assert.equal(preflight.checks[1].outcome, "fail");
|
||||
});
|
||||
|
||||
test("portable LAB V1 rejects a premature enqueue projection", async () => {
|
||||
const premature = portableSetup();
|
||||
premature.executor = {
|
||||
contour_id: "worker-006",
|
||||
state: "ready",
|
||||
ready: true,
|
||||
reason: null,
|
||||
};
|
||||
premature.preflight = {
|
||||
outcome: "ready",
|
||||
action: "enqueue",
|
||||
reason: "Запись готова к постановке в очередь.",
|
||||
submission_allowed: true,
|
||||
existing_result_ids: [],
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
fetchObservatoryPortableLaboratorySetups("source-a", {
|
||||
fetcher: async () => new Response(JSON.stringify({
|
||||
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
|
||||
source_session_id: "source-a",
|
||||
setups: [premature],
|
||||
authority,
|
||||
}), { status: 200 }),
|
||||
}),
|
||||
/значение изменилось|значение не поддерживается|постановка в очередь ещё не поддерживается/,
|
||||
);
|
||||
});
|
||||
|
||||
test("portable LAB V1 rejects an unbound existing result projection", async () => {
|
||||
const spoofed = portableSetup();
|
||||
spoofed.existing_results = [{
|
||||
result_id: `legacy-vegetation-${"4".repeat(64)}`,
|
||||
result_kind: "recorded-perception-qualification",
|
||||
created_at_utc: "2026-08-30T18:16:00Z",
|
||||
}];
|
||||
spoofed.preflight = {
|
||||
outcome: "existing",
|
||||
action: "open-existing",
|
||||
reason: "Результат якобы готов.",
|
||||
submission_allowed: false,
|
||||
existing_result_ids: [spoofed.existing_results[0].result_id],
|
||||
};
|
||||
|
||||
await assert.rejects(
|
||||
fetchObservatoryPortableLaboratorySetups("source-a", {
|
||||
fetcher: async () => new Response(JSON.stringify({
|
||||
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
|
||||
source_session_id: "source-a",
|
||||
setups: [spoofed],
|
||||
authority,
|
||||
}), { status: 200 }),
|
||||
}),
|
||||
/значение изменилось|проверяемая привязка результата/,
|
||||
);
|
||||
});
|
||||
|
||||
test("Observatory preflight sends the exact selected definition and never submits a run", async () => {
|
||||
const selected = (await fetchObservatoryLaboratorySetups("source-a", {
|
||||
fetcher: async () => new Response(JSON.stringify({
|
||||
|
||||
@@ -239,7 +239,10 @@ test("Observatory keeps one compact selector axis without the obsolete setup det
|
||||
workspace,
|
||||
/"preemption-pending": \{[\s\S]*label: "Ждём подтверждения остановки"/,
|
||||
);
|
||||
assert.match(workspace, /succeeded: \{ label: "Готово"/);
|
||||
assert.doesNotMatch(setupHook, /Promise\.allSettled/);
|
||||
assert.match(setupHook, /publishSetupCatalog\(legacyCatalog/);
|
||||
assert.match(workspace, /Worker установлен, запуск закрыт/);
|
||||
assert.match(workspace, /succeeded: \{ label: "Вычислено · ждёт публикации"/);
|
||||
assert.match(workspace, /failed: \{ label: "Ошибка расчёта"/);
|
||||
assert.match(workspace, /"reconciliation-required": \{ label: "Нужна сверка"/);
|
||||
assert.match(
|
||||
|
||||
Reference in New Issue
Block a user