feat(observatory): add bounded M5.1 session review
This commit is contained in:
@@ -104,3 +104,27 @@ test("laboratory UI is a bounded feature slice, not a central workspace branch",
|
||||
assert.doesNotMatch(laboratoryCss, /\.e30-human-review/);
|
||||
assert.match(e30HumanReviewCss, /\.e30-human-review/);
|
||||
});
|
||||
|
||||
test("Observatory is a bounded read-only slice outside legacy LAB and viewer lifecycles", async () => {
|
||||
const workspaceHub = await read("workspaces/Workspaces.tsx");
|
||||
const observatory = await read("workspaces/observatory/ObservatoryWorkspace.tsx");
|
||||
const observatoryCore = await read("core/observatory/catalog.ts");
|
||||
const workspaceCss = await read("styles/workspaces.css");
|
||||
const observatoryCss = await read("styles/observatory.css");
|
||||
|
||||
assert.match(
|
||||
workspaceHub,
|
||||
/case "observatory":[\s\S]*<ObservatoryWorkspace definition=\{props\.definition\} \/>/,
|
||||
);
|
||||
assert.match(observatory, /export function ObservatoryWorkspace/);
|
||||
assert.match(observatory, /useObservatoryCatalog/);
|
||||
assert.match(observatory, /data-observatory-authority="observation-only"/);
|
||||
assert.match(observatory, /data-observatory-viewer="detached"/);
|
||||
assert.doesNotMatch(
|
||||
`${observatory}\n${observatoryCore}`,
|
||||
/(?:core|components|workspaces)\/laboratory|\/api\/v1\/laboratory|RerunViewport|ObservationSessionSelect/,
|
||||
);
|
||||
assert.doesNotMatch(observatory, /replayObservationSession|deleteObservationSession/);
|
||||
assert.doesNotMatch(workspaceCss, /\.observatory-/);
|
||||
assert.match(observatoryCss, /\.observatory-workspace/);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let buildObservatoryCatalog;
|
||||
let fetchObservatoryCatalog;
|
||||
let ObservatoryCatalogContractError;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
({
|
||||
buildObservatoryCatalog,
|
||||
fetchObservatoryCatalog,
|
||||
ObservatoryCatalogContractError,
|
||||
} = await server.ssrLoadModule("/src/core/observatory/catalog.ts"));
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
function source(id, startedAtUtc) {
|
||||
return {
|
||||
id,
|
||||
label: `Источник ${id}`,
|
||||
startedAtUtc,
|
||||
completedAtUtc: startedAtUtc,
|
||||
status: "ready",
|
||||
modalities: ["point-cloud", "video"],
|
||||
durationSeconds: 42,
|
||||
replayable: true,
|
||||
preparation: null,
|
||||
lab: null,
|
||||
};
|
||||
}
|
||||
|
||||
function evidence(id, sourceSessionId, publishedAtUtc) {
|
||||
return {
|
||||
id,
|
||||
label: `Результат ${id}`,
|
||||
startedAtUtc: publishedAtUtc,
|
||||
completedAtUtc: publishedAtUtc,
|
||||
status: "ready",
|
||||
modalities: ["video"],
|
||||
durationSeconds: 12,
|
||||
replayable: true,
|
||||
preparation: null,
|
||||
lab: {
|
||||
labId: `LAB-${id}`,
|
||||
sourceSessionId,
|
||||
resultKind: "recorded-evidence",
|
||||
resultId: `result-${id}`,
|
||||
sourceResultId: null,
|
||||
configSha256: "a".repeat(64),
|
||||
runCreatedAtUtc: publishedAtUtc,
|
||||
publishedAtUtc,
|
||||
provenance: { verdict: "must-not-be-inferred" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test("Observatory joins evidence only by sourceSessionId and keeps deterministic order", () => {
|
||||
const catalog = buildObservatoryCatalog(
|
||||
[
|
||||
source("older", "2026-08-28T10:00:00Z"),
|
||||
source("newer", "2026-08-29T10:00:00Z"),
|
||||
],
|
||||
[
|
||||
evidence("old-result", "newer", "2026-08-29T11:00:00Z"),
|
||||
evidence("new-result", "newer", "2026-08-29T12:00:00Z"),
|
||||
evidence("orphan", "missing", "2026-08-29T13:00:00Z"),
|
||||
],
|
||||
);
|
||||
|
||||
assert.deepEqual(catalog.items.map(({ source: item }) => item.id), ["newer", "older"]);
|
||||
assert.deepEqual(
|
||||
catalog.items[0].evidence.map((item) => item.sessionId),
|
||||
["new-result", "old-result"],
|
||||
);
|
||||
assert.deepEqual(catalog.unresolvedEvidence.map((item) => item.sessionId), ["orphan"]);
|
||||
assert.deepEqual(catalog.window, {
|
||||
limit: 100,
|
||||
sourceCount: 2,
|
||||
laboratoryCount: 3,
|
||||
sourceLimitReached: false,
|
||||
laboratoryLimitReached: false,
|
||||
});
|
||||
assert.equal("verdict" in catalog.items[0].evidence[0], false);
|
||||
assert.equal(catalog.items[1].evidence.length, 0);
|
||||
});
|
||||
|
||||
test("Observatory fails visibly when the laboratory projection loses its typed link", () => {
|
||||
assert.throws(
|
||||
() => buildObservatoryCatalog(
|
||||
[source("source", "2026-08-29T10:00:00Z")],
|
||||
[source("not-lab", "2026-08-29T11:00:00Z")],
|
||||
),
|
||||
ObservatoryCatalogContractError,
|
||||
);
|
||||
});
|
||||
|
||||
test("Observatory fetches disjoint read-only source and laboratory projections", async () => {
|
||||
const calls = [];
|
||||
const fetcher = async (input, init) => {
|
||||
calls.push({ input: String(input), method: init?.method });
|
||||
return new Response(JSON.stringify({ items: [] }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
};
|
||||
|
||||
const catalog = await fetchObservatoryCatalog({ fetcher, limit: 50 });
|
||||
assert.deepEqual(catalog, {
|
||||
items: [],
|
||||
unresolvedEvidence: [],
|
||||
window: {
|
||||
limit: 50,
|
||||
sourceCount: 0,
|
||||
laboratoryCount: 0,
|
||||
sourceLimitReached: false,
|
||||
laboratoryLimitReached: false,
|
||||
},
|
||||
});
|
||||
assert.deepEqual(
|
||||
calls.map(({ input }) => input).sort(),
|
||||
[
|
||||
"/api/v1/observation-sessions?limit=50&scope=laboratory",
|
||||
"/api/v1/observation-sessions?limit=50&scope=source",
|
||||
],
|
||||
);
|
||||
assert.deepEqual(new Set(calls.map(({ method }) => method)), new Set(["GET"]));
|
||||
});
|
||||
|
||||
test("Observatory exposes bounded-window uncertainty without inventing a broken link", () => {
|
||||
const catalog = buildObservatoryCatalog(
|
||||
[
|
||||
source("newer", "2026-08-29T10:00:00Z"),
|
||||
source("older", "2026-08-28T10:00:00Z"),
|
||||
],
|
||||
[
|
||||
evidence("linked", "newer", "2026-08-29T11:00:00Z"),
|
||||
evidence("outside-window", "older-than-window", "2026-08-29T12:00:00Z"),
|
||||
],
|
||||
2,
|
||||
);
|
||||
|
||||
assert.equal(catalog.window.sourceLimitReached, true);
|
||||
assert.equal(catalog.window.laboratoryLimitReached, true);
|
||||
assert.deepEqual(
|
||||
catalog.unresolvedEvidence.map((item) => item.sessionId),
|
||||
["outside-window"],
|
||||
);
|
||||
});
|
||||
@@ -0,0 +1,84 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { after, before, test } from "node:test";
|
||||
|
||||
import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let productModel;
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
logLevel: "silent",
|
||||
server: { middlewareMode: true },
|
||||
});
|
||||
productModel = await server.ssrLoadModule("/src/productModel.ts");
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await server?.close();
|
||||
});
|
||||
|
||||
async function read(relativePath) {
|
||||
return readFile(new URL(`../src/${relativePath}`, import.meta.url), "utf8");
|
||||
}
|
||||
|
||||
test("Observatory is the third independent Polygon workspace", () => {
|
||||
assert.deepEqual(
|
||||
productModel.workspacesForRoot("polygon").map(({ id }) => id),
|
||||
["lab-archive", "simulations", "observatory"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
productModel.workspaceById("observatory"),
|
||||
{
|
||||
id: "observatory",
|
||||
root: "polygon",
|
||||
label: "Обсерватория",
|
||||
title: "Обсерватория восприятия",
|
||||
eyebrow: "ТЕСТОВЫЙ КОНТУР / ОБСЕРВАТОРИЯ",
|
||||
description: "Сессии и квалификация компьютерного зрения без доступа к управлению.",
|
||||
icon: "eye",
|
||||
kind: "observatory",
|
||||
groups: [],
|
||||
},
|
||||
);
|
||||
assert.equal(productModel.workspaceById("lab-archive").kind, "lab-archive");
|
||||
});
|
||||
|
||||
test("Observatory reads catalog evidence without inheriting replay or LAB composition", async () => {
|
||||
const [app, workspaceHub, workspace, hook, viewerProfiles, styles] = await Promise.all([
|
||||
read("App.tsx"),
|
||||
read("workspaces/Workspaces.tsx"),
|
||||
read("workspaces/observatory/ObservatoryWorkspace.tsx"),
|
||||
read("core/observatory/useObservatoryCatalog.ts"),
|
||||
read("core/observation/viewerProfile.ts"),
|
||||
read("styles/observatory.css"),
|
||||
]);
|
||||
|
||||
assert.match(app, /activeDefinition\.kind === "observatory"[\s\S]*Только наблюдение/);
|
||||
assert.doesNotMatch(app, /\["recordings", "lab-archive", "observatory"\]/);
|
||||
assert.match(workspaceHub, /case "observatory":[\s\S]*<ObservatoryWorkspace/);
|
||||
assert.match(workspace, /useObservatoryCatalog/);
|
||||
assert.match(workspace, /Связанных результатов нет/);
|
||||
assert.match(workspace, /не является выводом о качестве/);
|
||||
assert.match(workspace, /\.evidence\.slice\([\s\S]*MAX_PRESENTED_EVIDENCE/);
|
||||
assert.match(workspace, /Полный архив остаётся в legacy LAB/);
|
||||
assert.match(workspace, /Полнота исторических/);
|
||||
assert.match(workspace, /вне текущего загруженного среза/);
|
||||
assert.match(workspace, /observatory-notice__copy/);
|
||||
assert.match(workspace, /observatory-evidence-card/);
|
||||
assert.doesNotMatch(workspace, /Нарушена связь|compactIdentity/);
|
||||
assert.doesNotMatch(
|
||||
`${workspace}\n${hook}`,
|
||||
/RerunViewport|ObservationSessionSelect|resolveObservationSessionReplay|deleteObservationSession|setInterval|setTimeout/,
|
||||
);
|
||||
assert.match(viewerProfiles, /kind: "live-acquisition"/);
|
||||
assert.match(viewerProfiles, /kind: "recorded-session"/);
|
||||
assert.match(viewerProfiles, /kind: "lab-recorded-evidence"/);
|
||||
assert.match(
|
||||
styles,
|
||||
/@media \(max-width: 920px\)[\s\S]*\.observatory-catalog-bar__controls \.nodedc-select-anchor \{[\s\S]*flex: 0 0 auto;/,
|
||||
);
|
||||
assert.doesNotMatch(styles, /nodedc-glass-surface|nodedc-status-badge/);
|
||||
});
|
||||
@@ -324,7 +324,7 @@ test("Polygon exposes one dataset surface and keeps legacy links compatible", ()
|
||||
assert.equal(workspaceById("datasets").kind, "datasets");
|
||||
assert.deepEqual(
|
||||
workspacesForRoot("polygon").map(({ id }) => id),
|
||||
["lab-archive", "simulations"],
|
||||
["lab-archive", "simulations", "observatory"],
|
||||
);
|
||||
assert.equal(
|
||||
workspacesForRoot("system").some(({ id }) => id === "polygon-run"),
|
||||
|
||||
@@ -27,6 +27,19 @@ test("top navigation has no Center and Park owns contour health first", () => {
|
||||
);
|
||||
assert.equal(productModel.workspacesForRoot("fleet")[0]?.id, "contour-health");
|
||||
assert.equal(productModel.workspaceById("contour-health")?.root, "fleet");
|
||||
assert.deepEqual(
|
||||
productModel.workspacesForRoot("polygon").map(({ id }) => id),
|
||||
["lab-archive", "simulations", "observatory"],
|
||||
);
|
||||
assert.equal(productModel.workspaceById("lab-archive")?.kind, "lab-archive");
|
||||
assert.deepEqual(
|
||||
{
|
||||
root: productModel.workspaceById("observatory")?.root,
|
||||
kind: productModel.workspaceById("observatory")?.kind,
|
||||
icon: productModel.workspaceById("observatory")?.icon,
|
||||
},
|
||||
{ root: "polygon", kind: "observatory", icon: "eye" },
|
||||
);
|
||||
});
|
||||
|
||||
test("every laboratory result uses the shared evidence template", async () => {
|
||||
|
||||
Reference in New Issue
Block a user