feat(observatory-ui): review portable results and retry publication
Checkpoint the existing generic result-viewing and publication lifecycle UI. Focused architecture and Observatory tests: 62 passed; no new visual changes or rebuild in this checkpoint.
This commit is contained in:
@@ -5,6 +5,7 @@ import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let fetchObservatoryRecordedJobs;
|
||||
let retryObservatoryRecordedJobPublication;
|
||||
let submitObservatoryRecordedJob;
|
||||
let ObservatoryRecordedJobContractError;
|
||||
|
||||
@@ -65,6 +66,12 @@ function job(state = "queued") {
|
||||
result: state === "succeeded"
|
||||
? { result_id: "m49-result", sha256: "d".repeat(64) }
|
||||
: null,
|
||||
publication: {
|
||||
state: "not-required",
|
||||
attempts: 0,
|
||||
error: null,
|
||||
published_at_utc: null,
|
||||
},
|
||||
terminal: state === "failed"
|
||||
? { code: "worker-failed", message: "Worker завершил расчёт с ошибкой." }
|
||||
: null,
|
||||
@@ -82,6 +89,7 @@ before(async () => {
|
||||
});
|
||||
({
|
||||
fetchObservatoryRecordedJobs,
|
||||
retryObservatoryRecordedJobPublication,
|
||||
submitObservatoryRecordedJob,
|
||||
ObservatoryRecordedJobContractError,
|
||||
} = await server.ssrLoadModule("/src/core/observatory/recordedJobs.ts"));
|
||||
@@ -212,3 +220,28 @@ test("portable submission carries the exact definition/check fence", async () =>
|
||||
check_sha256: "f".repeat(64),
|
||||
});
|
||||
});
|
||||
|
||||
test("publication retry never submits a second compute request", async () => {
|
||||
let request;
|
||||
const response = job("succeeded");
|
||||
response.publication = {
|
||||
state: "published",
|
||||
attempts: 2,
|
||||
error: null,
|
||||
published_at_utc: "2026-09-01T12:00:00Z",
|
||||
};
|
||||
const retried = await retryObservatoryRecordedJobPublication(response.job_id, {
|
||||
fetcher: async (input, init) => {
|
||||
request = { input: String(input), init };
|
||||
return new Response(JSON.stringify(response), { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
request.input,
|
||||
`/api/v1/observatory/runs/${response.job_id}/publication/retry`,
|
||||
);
|
||||
assert.equal(request.init.method, "POST");
|
||||
assert.equal(request.init.body, undefined);
|
||||
assert.equal(retried.publication.state, "published");
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import { createServer } from "vite";
|
||||
|
||||
let server;
|
||||
let admitObservatoryRecordedRunReview;
|
||||
let fetchObservatoryRecordedRunReview;
|
||||
let observatoryRecordedRunBinding;
|
||||
let ObservatoryRecordedRunContractError;
|
||||
|
||||
@@ -17,6 +18,7 @@ before(async () => {
|
||||
});
|
||||
({
|
||||
admitObservatoryRecordedRunReview,
|
||||
fetchObservatoryRecordedRunReview,
|
||||
observatoryRecordedRunBinding,
|
||||
ObservatoryRecordedRunContractError,
|
||||
} = await server.ssrLoadModule("/src/core/observatory/recordedRun.ts"));
|
||||
@@ -87,6 +89,55 @@ function lab(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function portableCapability() {
|
||||
return {
|
||||
schemaVersion: "missioncore.observation-lab-replay-capability/v2",
|
||||
kind: "portable-result-review",
|
||||
viewerProfile: "portable-result",
|
||||
timeline: "result-defined",
|
||||
activation: "explicit",
|
||||
commandsEnabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
function portableCapabilityDocument() {
|
||||
return {
|
||||
schema_version: "missioncore.observation-lab-replay-capability/v2",
|
||||
kind: "portable-result-review",
|
||||
viewer_profile: "portable-result",
|
||||
timeline: "result-defined",
|
||||
activation: "explicit",
|
||||
commands_enabled: false,
|
||||
};
|
||||
}
|
||||
|
||||
function portableLab(portableResultId, definitionSha256) {
|
||||
return lab({
|
||||
labId: "M4.9",
|
||||
resultId: portableResultId,
|
||||
resultKind: "recorded-route-analysis",
|
||||
sourceResultId: null,
|
||||
configSha256: definitionSha256,
|
||||
replayCapability: portableCapability(),
|
||||
provenance: {
|
||||
schema_version: "missioncore.observatory-portable-result-publication/v1",
|
||||
authority: {},
|
||||
calculation_profile: {},
|
||||
calculation_profile_sha256: "b".repeat(64),
|
||||
job: {},
|
||||
source: { session_id: sourceSessionId },
|
||||
run_definition: { definition_sha256: definitionSha256 },
|
||||
result_package: {
|
||||
manifest_sha256: "c".repeat(64),
|
||||
artifact_manifest_id: "d".repeat(64),
|
||||
},
|
||||
replay_capability: portableCapabilityDocument(),
|
||||
storage: {},
|
||||
method: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test("Observatory admits only the exact typed canonical recorded run", () => {
|
||||
const binding = observatoryRecordedRunBinding(resultId, lab());
|
||||
assert.deepEqual(binding, {
|
||||
@@ -167,6 +218,67 @@ test("Observatory rechecks the selected source and sealed review before mounting
|
||||
);
|
||||
});
|
||||
|
||||
test("Observatory selects and strictly decodes a portable result viewer by capability", async () => {
|
||||
const portableResultId = "portable-result-a";
|
||||
const definitionSha256 = "e".repeat(64);
|
||||
const binding = observatoryRecordedRunBinding(
|
||||
portableResultId,
|
||||
portableLab(portableResultId, definitionSha256),
|
||||
);
|
||||
assert.deepEqual(binding, {
|
||||
kind: "portable-result-review",
|
||||
evidenceSessionId: portableResultId,
|
||||
sourceSessionId,
|
||||
resultId: portableResultId,
|
||||
viewerProfile: "portable-result",
|
||||
timeline: "result-defined",
|
||||
activation: "explicit",
|
||||
definitionSha256,
|
||||
});
|
||||
|
||||
let requestedUrl = null;
|
||||
const review = await fetchObservatoryRecordedRunReview(binding, {
|
||||
selectedSourceSessionId: sourceSessionId,
|
||||
fetcher: async (url) => {
|
||||
requestedUrl = url;
|
||||
return {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({
|
||||
schema_version: "missioncore.observatory-portable-result-view/v1",
|
||||
result_id: portableResultId,
|
||||
source_session_id: sourceSessionId,
|
||||
result_kind: "recorded-route-analysis",
|
||||
definition_sha256: definitionSha256,
|
||||
viewer_capability: portableCapabilityDocument(),
|
||||
calculation_profile: { name: "M4.9" },
|
||||
artifact_manifest_id: "d".repeat(64),
|
||||
artifacts: [{
|
||||
role: "result-document",
|
||||
media_type: "application/json",
|
||||
sha256: "f".repeat(64),
|
||||
byte_length: 42,
|
||||
}],
|
||||
result_document: { verdict: "reviewed" },
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
requestedUrl,
|
||||
`/api/v1/observatory/portable-results/${portableResultId}`,
|
||||
);
|
||||
assert.equal(review.kind, "portable-result");
|
||||
assert.deepEqual(review.resultDocument, { verdict: "reviewed" });
|
||||
assert.deepEqual(review.artifacts, [{
|
||||
role: "result-document",
|
||||
mediaType: "application/json",
|
||||
sha256: "f".repeat(64),
|
||||
byteLength: 42,
|
||||
}]);
|
||||
});
|
||||
|
||||
test("Observatory run admission remains metadata-only until explicit UI activation", async () => {
|
||||
const source = await readFile(
|
||||
new URL("../src/core/observatory/recordedRun.ts", import.meta.url),
|
||||
|
||||
@@ -99,6 +99,10 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
|
||||
assert.doesNotMatch(workspace, /observatory-evidence-card[^>]*tone="soft"/);
|
||||
assert.match(workspace, /Открыть визуальный разбор/);
|
||||
assert.match(workspace, /replay\.kind === "ready"[\s\S]*<CanonicalVegetationRerunReplay/);
|
||||
assert.match(workspace, /replay\.review\.kind === "canonical-recorded-rerun"[\s\S]*РЕЗУЛЬТАТ \/ ПРОВЕРЕННЫЙ ДОКУМЕНТ/);
|
||||
assert.match(workspace, /Связанных артефактов: \{replay\.review\.artifacts\.length\}/);
|
||||
assert.match(workspace, /<summary>Документ результата<\/summary>/);
|
||||
assert.doesNotMatch(workspace, /UNIVERSAL VIEWER|content-addressed artifacts/);
|
||||
assert.match(workspace, /Проверяем точную связь результата/);
|
||||
assert.match(workspace, /role="alert"/);
|
||||
assert.match(workspace, /Повторить/);
|
||||
@@ -247,12 +251,18 @@ test("Observatory keeps one compact selector axis without the obsolete setup det
|
||||
workspace,
|
||||
/"preemption-pending": \{[\s\S]*label: "Ждём подтверждения остановки"/,
|
||||
);
|
||||
assert.doesNotMatch(setupHook, /Promise\.allSettled/);
|
||||
assert.match(setupHook, /publishSetupCatalog\(\s*legacyCatalog/);
|
||||
assert.match(setupHook, /preserveUnknownSelection: true/);
|
||||
assert.match(setupHook, /Promise\.all\(\[legacyResult, portableResult\]\)/);
|
||||
assert.match(
|
||||
setupHook,
|
||||
/portableProfileNames[\s\S]*legacy\.setups\.filter\([\s\S]*!portableProfileNames\.has\(setup\.displayName\)[\s\S]*\.\.\.portable\.setups/,
|
||||
/legacy\.status === "rejected"[\s\S]*portable\.status === "fulfilled"[\s\S]*publishSetupCatalog\(portable\.value/,
|
||||
);
|
||||
assert.match(
|
||||
setupHook,
|
||||
/legacy\.status === "fulfilled"[\s\S]*portable\.status === "rejected"[\s\S]*publishSetupCatalog\(legacy\.value/,
|
||||
);
|
||||
assert.match(
|
||||
setupHook,
|
||||
/portableSetupIds[\s\S]*legacy\.setups\.filter\([\s\S]*!portableSetupIds\.has\(setup\.setupId\)[\s\S]*\.\.\.portable\.setups/,
|
||||
);
|
||||
assert.match(setupHook, /selectedSetupId, sourceSessionId/);
|
||||
assert.match(workspace, /Worker готов к проверке/);
|
||||
@@ -270,7 +280,11 @@ test("Observatory keeps one compact selector axis without the obsolete setup det
|
||||
assert.match(jobsHook, /return `observatory-ui:\$\{entropy\}`;/);
|
||||
assert.doesNotMatch(jobsHook, /observatory-ui:[^`]*sourceSessionId|\.slice\(0, 160\)/);
|
||||
assert.match(workspace, /recordedJobsController\.refresh\(\)/);
|
||||
assert.doesNotMatch(`${workspace}\n${jobsHook}`, /setInterval|setTimeout/);
|
||||
assert.match(jobsHook, /POLL_INTERVAL_MS = 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(
|
||||
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;/,
|
||||
|
||||
Reference in New Issue
Block a user