feat(observatory): verify exact published result reuse
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import type {
|
||||
ObservatoryLaboratoryPreservedResult,
|
||||
ObservatoryLaboratoryRunDefinition,
|
||||
ObservatoryLaboratorySetup,
|
||||
ObservatoryLaboratorySetupCatalog,
|
||||
@@ -23,13 +24,14 @@ export function decodePortableCatalog(value: unknown): ObservatoryLaboratorySetu
|
||||
);
|
||||
exact(row.schema_version, PORTABLE_CATALOG_SCHEMA, "schema_version portable-каталога");
|
||||
observationAuthority(row.authority);
|
||||
const sourceSessionId = text(row.source_session_id, "portable source_session_id");
|
||||
return {
|
||||
sourceSessionId: text(row.source_session_id, "portable source_session_id"),
|
||||
setups: array(row.setups, "portable setups").map(decodePortableSetup),
|
||||
sourceSessionId,
|
||||
setups: array(row.setups, "portable setups").map((item) => decodePortableSetup(item, sourceSessionId)),
|
||||
};
|
||||
}
|
||||
|
||||
function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
|
||||
function decodePortableSetup(value: unknown, sourceSessionId: string): ObservatoryLaboratorySetup {
|
||||
const row = record(value, "portable-сетап");
|
||||
exactKeys(row, [
|
||||
"authority", "description", "display_name", "executor", "existing_results",
|
||||
@@ -39,6 +41,8 @@ function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
|
||||
exact(row.origin, "portable-definition", "portable origin");
|
||||
observationAuthority(row.authority);
|
||||
decodePortableSourceRequirements(row.source_requirements);
|
||||
const setupId = text(row.setup_id, "portable setup_id");
|
||||
const runDefinition = decodePortableRunDefinition(row.run_definition);
|
||||
|
||||
const compatibility = record(row.source_compatibility, "portable source_compatibility");
|
||||
exactKeys(
|
||||
@@ -96,12 +100,12 @@ function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
|
||||
], "portable preflight");
|
||||
const preflightOutcome = oneOf(
|
||||
preflight.outcome,
|
||||
["ready", "blocked"] as const,
|
||||
["existing", "ready", "blocked"] as const,
|
||||
"portable preflight outcome",
|
||||
);
|
||||
const preflightAction = oneOf(
|
||||
preflight.action,
|
||||
["check", "blocked"] as const,
|
||||
["open-existing", "check", "blocked"] as const,
|
||||
"portable preflight action",
|
||||
);
|
||||
const submissionAllowed = boolean(
|
||||
@@ -112,28 +116,37 @@ function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
|
||||
submissionAllowed !== (preflightOutcome === "ready")
|
||||
|| (preflightOutcome === "ready" && preflightAction !== "check")
|
||||
|| (preflightOutcome === "blocked" && preflightAction !== "blocked")
|
||||
|| (preflightOutcome === "existing" && preflightAction !== "open-existing")
|
||||
|| (submissionAllowed && (!compatible || !executorReady))
|
||||
) {
|
||||
throw new ObservatoryPortableSetupDecodeError(
|
||||
"Portable preflight: состояние запуска противоречиво.",
|
||||
);
|
||||
}
|
||||
const existingResults = array(row.existing_results, "portable existing_results");
|
||||
const existingResults = array(row.existing_results, "portable existing_results").map(
|
||||
(item) => decodePortableResult(item, sourceSessionId, setupId, runDefinition),
|
||||
);
|
||||
const existingResultIds = array(
|
||||
preflight.existing_result_ids,
|
||||
"portable existing_result_ids",
|
||||
);
|
||||
if (existingResults.length > 0 || existingResultIds.length > 0) {
|
||||
).map((item) => text(item, "portable existing result id"));
|
||||
if (
|
||||
(existingResults.length > 0) !== (preflightOutcome === "existing")
|
||||
|| existingResultIds.length !== existingResults.length
|
||||
|| new Set(existingResultIds).size !== existingResultIds.length
|
||||
|| existingResults.some((result, index) => result.resultId !== existingResultIds[index])
|
||||
) {
|
||||
throw new ObservatoryPortableSetupDecodeError(
|
||||
"Portable result: проверяемая привязка результата к RunDefinition ещё не поддерживается.",
|
||||
"Portable result: готовность не соответствует проверенным результатам.",
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
setupId: text(row.setup_id, "portable setup_id"),
|
||||
setupId,
|
||||
displayName: text(row.display_name, "portable display_name"),
|
||||
description: text(row.description, "portable description"),
|
||||
origin: "portable-definition",
|
||||
runDefinition: decodePortableRunDefinition(row.run_definition),
|
||||
runDefinition,
|
||||
compatibility: {
|
||||
compatible,
|
||||
reasons: compatible
|
||||
@@ -146,17 +159,58 @@ function decodePortableSetup(value: unknown): ObservatoryLaboratorySetup {
|
||||
reasonCode: executorReasonCode,
|
||||
reason: executorReason,
|
||||
},
|
||||
preservedResults: [],
|
||||
preservedResults: existingResults,
|
||||
preflight: {
|
||||
outcome: preflightOutcome,
|
||||
action: preflightAction,
|
||||
reason: text(preflight.reason, "portable preflight reason"),
|
||||
submissionAllowed,
|
||||
existingResultIds: [],
|
||||
existingResultIds,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function decodePortableResult(
|
||||
value: unknown, sourceSessionId: string, setupId: string,
|
||||
definition: ObservatoryLaboratoryRunDefinition,
|
||||
): ObservatoryLaboratoryPreservedResult {
|
||||
const row = record(value, "portable result");
|
||||
exactKeys(row, [
|
||||
"result_id", "result_kind", "relation", "access", "created_at_utc",
|
||||
"observatory_projection_available", "identity",
|
||||
], "portable result");
|
||||
exact(row.result_kind, definition.resultKind, "portable result kind");
|
||||
exact(row.relation, "exact-recorded-computation", "portable result relation");
|
||||
exact(row.access, "observatory", "portable result access");
|
||||
exact(row.observatory_projection_available, true, "portable result availability");
|
||||
const identity = record(row.identity, "portable result identity");
|
||||
exactKeys(identity, [
|
||||
"job_id", "source_session_id", "source_catalog_sha256", "source_bundle_sha256",
|
||||
"source_capability_manifest_sha256", "setup_id", "definition_sha256",
|
||||
"package_sha256", "artifact_manifest_id",
|
||||
], "portable result identity");
|
||||
exact(identity.source_session_id, sourceSessionId, "portable result source");
|
||||
exact(identity.setup_id, setupId, "portable result setup");
|
||||
exact(identity.definition_sha256, definition.definitionSha256, "portable result definition");
|
||||
text(identity.job_id, "portable result job");
|
||||
for (const key of [
|
||||
"source_catalog_sha256", "source_bundle_sha256", "source_capability_manifest_sha256",
|
||||
"definition_sha256", "package_sha256", "artifact_manifest_id",
|
||||
]) {
|
||||
if (!SHA256.test(text(identity[key], `portable result ${key}`))) {
|
||||
throw new ObservatoryPortableSetupDecodeError(`Portable result: некорректный ${key}.`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
resultId: text(row.result_id, "portable result id"),
|
||||
resultKind: text(row.result_kind, "portable result kind"),
|
||||
relation: "exact-recorded-computation",
|
||||
access: "observatory",
|
||||
createdAtUtc: text(row.created_at_utc, "portable result created_at_utc"),
|
||||
observatoryProjectionAvailable: true,
|
||||
};
|
||||
}
|
||||
|
||||
function decodePortableSourceRequirements(value: unknown): void {
|
||||
const row = record(value, "portable source_requirements");
|
||||
exactKeys(row, [
|
||||
|
||||
@@ -323,7 +323,7 @@ test("portable LAB V1 rejects an unbound existing result projection", async () =
|
||||
authority,
|
||||
}), { status: 200 }),
|
||||
}),
|
||||
/значение изменилось|значение не поддерживается|проверяемая привязка результата/,
|
||||
/значение изменилось|значение не поддерживается|обнаружены неизвестные поля/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -348,6 +348,76 @@ test("portable LAB V1 rejects heavyweight compatibility evidence", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
function cachedPortableSetup() {
|
||||
const setup = portableSetup();
|
||||
setup.existing_results = [{
|
||||
result_id: "portable-result-001",
|
||||
result_kind: setup.run_definition.result_kind,
|
||||
relation: "exact-recorded-computation",
|
||||
access: "observatory",
|
||||
created_at_utc: "2026-09-03T00:00:00Z",
|
||||
observatory_projection_available: true,
|
||||
identity: {
|
||||
job_id: "observatory-run-001",
|
||||
source_session_id: "source-a",
|
||||
source_catalog_sha256: "a".repeat(64),
|
||||
source_bundle_sha256: "b".repeat(64),
|
||||
source_capability_manifest_sha256: "c".repeat(64),
|
||||
setup_id: setup.setup_id,
|
||||
definition_sha256: setup.run_definition.definition_sha256,
|
||||
package_sha256: "d".repeat(64),
|
||||
artifact_manifest_id: "e".repeat(64),
|
||||
},
|
||||
}];
|
||||
setup.preflight = {
|
||||
outcome: "existing", action: "open-existing",
|
||||
reason: "Точный результат проверен.", submission_allowed: false,
|
||||
existing_result_ids: [setup.existing_results[0].result_id],
|
||||
};
|
||||
return setup;
|
||||
}
|
||||
|
||||
function fetchCachedPortable(setup) {
|
||||
return fetchObservatoryPortableLaboratorySetups("source-a", {
|
||||
fetcher: async () => new Response(JSON.stringify({
|
||||
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
|
||||
source_session_id: "source-a", setups: [setup], authority,
|
||||
}), { status: 200 }),
|
||||
});
|
||||
}
|
||||
|
||||
test("portable exact cached result is readable without an installed executor", async () => {
|
||||
const catalog = await fetchCachedPortable(cachedPortableSetup());
|
||||
const setup = catalog.setups[0];
|
||||
assert.equal(setup.executor.state, "not-installed");
|
||||
assert.equal(setup.preflight.outcome, "existing");
|
||||
assert.equal(setup.preflight.submissionAllowed, false);
|
||||
assert.deepEqual(setup.preflight.existingResultIds, ["portable-result-001"]);
|
||||
assert.equal(setup.preservedResults[0].access, "observatory");
|
||||
});
|
||||
|
||||
for (const [label, change] of [
|
||||
["another source", (s) => { s.existing_results[0].identity.source_session_id = "source-b"; }],
|
||||
["another setup", (s) => { s.existing_results[0].identity.setup_id = "another-profile"; }],
|
||||
["another version", (s) => { s.existing_results[0].identity.definition_sha256 = "f".repeat(64); }],
|
||||
["bad package digest", (s) => { s.existing_results[0].identity.package_sha256 = "not-a-digest"; }],
|
||||
["unavailable artifact", (s) => { s.existing_results[0].observatory_projection_available = false; }],
|
||||
["missing binding", (s) => { delete s.existing_results[0].identity; }],
|
||||
["unrelated result ID", (s) => { s.preflight.existing_result_ids = ["different-result"]; }],
|
||||
["duplicate result", (s) => {
|
||||
s.existing_results.push(s.existing_results[0]);
|
||||
s.preflight.existing_result_ids.push(s.preflight.existing_result_ids[0]);
|
||||
}],
|
||||
["contradictory action", (s) => { s.preflight.action = "check"; }],
|
||||
["cached but queueable", (s) => { s.preflight.submission_allowed = true; }],
|
||||
]) {
|
||||
test(`portable cached result rejects ${label}`, async () => {
|
||||
const setup = cachedPortableSetup();
|
||||
change(setup);
|
||||
await assert.rejects(fetchCachedPortable(setup));
|
||||
});
|
||||
}
|
||||
|
||||
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({
|
||||
|
||||
Reference in New Issue
Block a user