feat(observatory): add portable calculation profiles
This commit is contained in:
@@ -101,6 +101,19 @@ function canonicalLab(overrides = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function legacyCalculationProfile(overrides = {}) {
|
||||
return {
|
||||
schema_version: "missioncore.observatory-calculation-profile/v1",
|
||||
setup_id: "lab-v1-ravnoves004tree-final",
|
||||
display_name: "LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39",
|
||||
origin: "existing-result",
|
||||
definition_id: null,
|
||||
definition_version: null,
|
||||
definition_sha256: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function canonicalProjectionProvenance(resultId, replayCapability) {
|
||||
return {
|
||||
schema_version: "missioncore.canonical-recorded-lab-projection/v1",
|
||||
@@ -260,6 +273,69 @@ test("session catalog strictly decodes the explicit recorded LAB replay capabili
|
||||
);
|
||||
});
|
||||
|
||||
test("session catalog decodes a typed calculation profile without inferring unknown results", () => {
|
||||
const resultId = `lab-v1-vegetation-shadow-${"8".repeat(64)}`;
|
||||
const decoded = decodeObservationSessionCatalog({
|
||||
items: [session({
|
||||
id: resultId,
|
||||
lab: canonicalLab({ calculation_profile: legacyCalculationProfile() }),
|
||||
})],
|
||||
}).items[0].lab;
|
||||
|
||||
assert.deepEqual(decoded.calculationProfile, {
|
||||
schemaVersion: "missioncore.observatory-calculation-profile/v1",
|
||||
setupId: "lab-v1-ravnoves004tree-final",
|
||||
displayName: "LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39",
|
||||
origin: "existing-result",
|
||||
definitionId: null,
|
||||
definitionVersion: null,
|
||||
definitionSha256: null,
|
||||
});
|
||||
assert.equal(
|
||||
decodeObservationSessionCatalog({
|
||||
items: [session({ id: resultId, lab: canonicalLab() })],
|
||||
}).items[0].lab.calculationProfile,
|
||||
null,
|
||||
);
|
||||
assert.equal(
|
||||
decodeObservationSessionCatalog({
|
||||
items: [session({
|
||||
id: resultId,
|
||||
lab: canonicalLab({ calculation_profile: null }),
|
||||
})],
|
||||
}).items[0].lab.calculationProfile,
|
||||
null,
|
||||
);
|
||||
assert.throws(
|
||||
() => decodeObservationSessionCatalog({
|
||||
items: [session({
|
||||
id: resultId,
|
||||
lab: canonicalLab({
|
||||
calculation_profile: legacyCalculationProfile({
|
||||
origin: "archived-definition",
|
||||
definition_id: "lab-v1-portable",
|
||||
}),
|
||||
}),
|
||||
})],
|
||||
}),
|
||||
ObservationSessionContractError,
|
||||
);
|
||||
assert.throws(
|
||||
() => decodeObservationSessionCatalog({
|
||||
items: [session({
|
||||
id: resultId,
|
||||
lab: canonicalLab({
|
||||
calculation_profile: {
|
||||
...legacyCalculationProfile(),
|
||||
guessed_from_provenance: true,
|
||||
},
|
||||
}),
|
||||
})],
|
||||
}),
|
||||
ObservationSessionContractError,
|
||||
);
|
||||
});
|
||||
|
||||
test("opened archive is named in the scene header and trash hover has no pill", async () => {
|
||||
const appSource = await readFile(new URL("../src/App.tsx", import.meta.url), "utf8");
|
||||
const styles = await readFile(
|
||||
@@ -365,7 +441,7 @@ test("source and laboratory catalogs are requested as disjoint backend projectio
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
"/api/v1/observation-sessions?limit=100&scope=source",
|
||||
"/api/v1/observation-sessions?limit=100&scope=laboratory&lab_contract=v2",
|
||||
"/api/v1/observation-sessions?limit=100&scope=laboratory&lab_contract=v3",
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -69,6 +69,7 @@ function evidence(id, sourceSessionId, publishedAtUtc) {
|
||||
runCreatedAtUtc: publishedAtUtc,
|
||||
publishedAtUtc,
|
||||
replayCapability: null,
|
||||
calculationProfile: null,
|
||||
provenance: { verdict: "must-not-be-inferred" },
|
||||
},
|
||||
};
|
||||
@@ -174,7 +175,7 @@ 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=v2",
|
||||
"/api/v1/observation-sessions?limit=50&scope=laboratory&lab_contract=v3",
|
||||
"/api/v1/observation-sessions?limit=50&scope=source",
|
||||
],
|
||||
);
|
||||
|
||||
@@ -122,6 +122,7 @@ function portableSetup() {
|
||||
contour_id: "worker-006",
|
||||
state: "not-installed",
|
||||
ready: false,
|
||||
reason_code: "eomt-executor-release-unsealed",
|
||||
reason: "Immutable executor release не установлен.",
|
||||
},
|
||||
existing_results: [],
|
||||
@@ -136,6 +137,26 @@ function portableSetup() {
|
||||
};
|
||||
}
|
||||
|
||||
function portableM49Setup() {
|
||||
const profile = portableSetup();
|
||||
profile.setup_id = "m49-tgs-portable-v2";
|
||||
profile.display_name = "M4.9T5 · TRAVEL TGS · CPU-only, без ML";
|
||||
profile.description = "Динамический TGS-разбор записанной K1-сессии без ML; только наблюдение.";
|
||||
profile.run_definition = {
|
||||
definition_id: "m49-tgs-portable",
|
||||
version: 2,
|
||||
definition_sha256: "4".repeat(64),
|
||||
result_schema: "missioncore.recorded-travel-tgs-review/v2",
|
||||
result_kind: "recorded-source-paced-tgs-shadow",
|
||||
models: [],
|
||||
};
|
||||
profile.source_compatibility.reason = "Запись соответствует требованиям TRAVEL TGS.";
|
||||
profile.executor.reason_code = "m49-executor-release-unsealed";
|
||||
profile.executor.reason = "Immutable executor release не установлен.";
|
||||
profile.preflight.reason = "Переносимый вычислительный контур M4.9T5 пока недоступен.";
|
||||
return profile;
|
||||
}
|
||||
|
||||
before(async () => {
|
||||
server = await createServer({
|
||||
appType: "custom",
|
||||
@@ -178,7 +199,7 @@ 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 () => {
|
||||
test("portable profiles report compatible source separately from unavailable Worker", async () => {
|
||||
const calls = [];
|
||||
const selected = (await fetchObservatoryPortableLaboratorySetups("source-a", {
|
||||
fetcher: async (input, init) => {
|
||||
@@ -186,7 +207,7 @@ test("portable LAB V1 reports compatible source separately from unavailable Work
|
||||
return new Response(JSON.stringify({
|
||||
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
|
||||
source_session_id: "source-a",
|
||||
setups: [portableSetup()],
|
||||
setups: [portableSetup(), portableM49Setup()],
|
||||
authority,
|
||||
}), { status: 200 });
|
||||
},
|
||||
@@ -200,52 +221,81 @@ test("portable LAB V1 reports compatible source separately from unavailable Work
|
||||
"EoMT Cityscapes Large 1024",
|
||||
"DDRNet-39",
|
||||
]);
|
||||
const m49 = (await fetchObservatoryPortableLaboratorySetups("source-a", {
|
||||
fetcher: async () => new Response(JSON.stringify({
|
||||
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
|
||||
source_session_id: "source-a",
|
||||
setups: [portableSetup(), portableM49Setup()],
|
||||
authority,
|
||||
}), { status: 200 }),
|
||||
})).setups[1];
|
||||
assert.equal(m49.displayName, "M4.9T5 · TRAVEL TGS · CPU-only, без ML");
|
||||
assert.deepEqual(m49.runDefinition.models, []);
|
||||
assert.equal(
|
||||
calls[0].input,
|
||||
"/api/v1/observatory/portable-laboratory-setups?source_session_id=source-a",
|
||||
);
|
||||
|
||||
let unexpectedNetworkCall = false;
|
||||
let preflightRequest;
|
||||
const preflight = await preflightObservatoryLaboratorySetup("source-a", selected, {
|
||||
fetcher: async () => {
|
||||
unexpectedNetworkCall = true;
|
||||
throw new Error("portable preflight must use its server projection");
|
||||
fetcher: async (input, init) => {
|
||||
preflightRequest = { input: String(input), init };
|
||||
return new Response(JSON.stringify({
|
||||
schema_version: "missioncore.observatory-run-preflight/v1",
|
||||
source_session_id: "source-a",
|
||||
setup_id: selected.setupId,
|
||||
definition_sha256: "3".repeat(64),
|
||||
check_sha256: null,
|
||||
outcome: "blocked",
|
||||
submission_allowed: false,
|
||||
checks: [{
|
||||
check_id: "executor",
|
||||
outcome: "fail",
|
||||
reason_code: "eomt-executor-release-unsealed",
|
||||
message: "Immutable executor release не установлен.",
|
||||
}],
|
||||
existing_result_ids: [],
|
||||
executor: portableSetup().executor,
|
||||
authority,
|
||||
}), { status: 200 });
|
||||
},
|
||||
});
|
||||
assert.equal(unexpectedNetworkCall, false);
|
||||
assert.equal(preflightRequest.input, "/api/v1/observatory/run-preflights");
|
||||
assert.equal(preflightRequest.init.method, "POST");
|
||||
assert.equal(preflight.outcome, "blocked");
|
||||
assert.equal(preflight.submissionAllowed, false);
|
||||
assert.equal(preflight.checks[0].outcome, "pass");
|
||||
assert.equal(preflight.checks[1].outcome, "fail");
|
||||
assert.equal(preflight.checkSha256, null);
|
||||
assert.equal(preflight.checks[0].outcome, "fail");
|
||||
});
|
||||
|
||||
test("portable LAB V1 rejects a premature enqueue projection", async () => {
|
||||
const premature = portableSetup();
|
||||
premature.executor = {
|
||||
test("portable profile accepts a consistent ready projection", async () => {
|
||||
const ready = portableSetup();
|
||||
ready.executor = {
|
||||
contour_id: "worker-006",
|
||||
state: "ready",
|
||||
ready: true,
|
||||
reason_code: null,
|
||||
reason: null,
|
||||
};
|
||||
premature.preflight = {
|
||||
ready.preflight = {
|
||||
outcome: "ready",
|
||||
action: "enqueue",
|
||||
action: "check",
|
||||
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 }),
|
||||
}),
|
||||
/значение изменилось|значение не поддерживается|постановка в очередь ещё не поддерживается/,
|
||||
);
|
||||
const catalog = await fetchObservatoryPortableLaboratorySetups("source-a", {
|
||||
fetcher: async () => new Response(JSON.stringify({
|
||||
schema_version: "missioncore.observatory-portable-setup-catalog/v2",
|
||||
source_session_id: "source-a",
|
||||
setups: [ready],
|
||||
authority,
|
||||
}), { status: 200 }),
|
||||
});
|
||||
assert.equal(catalog.setups[0].executor.state, "ready");
|
||||
assert.equal(catalog.setups[0].preflight.outcome, "ready");
|
||||
assert.equal(catalog.setups[0].preflight.submissionAllowed, true);
|
||||
});
|
||||
|
||||
test("portable LAB V1 rejects an unbound existing result projection", async () => {
|
||||
@@ -272,7 +322,7 @@ test("portable LAB V1 rejects an unbound existing result projection", async () =
|
||||
authority,
|
||||
}), { status: 200 }),
|
||||
}),
|
||||
/значение изменилось|проверяемая привязка результата/,
|
||||
/значение изменилось|значение не поддерживается|проверяемая привязка результата/,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -315,6 +365,7 @@ test("Observatory preflight sends the exact selected definition and never submit
|
||||
source_session_id: "source-a",
|
||||
setup_id: selected.setupId,
|
||||
definition_sha256: "b".repeat(64),
|
||||
check_sha256: null,
|
||||
outcome: "blocked",
|
||||
submission_allowed: false,
|
||||
checks: [{
|
||||
@@ -340,6 +391,7 @@ test("Observatory preflight sends the exact selected definition and never submit
|
||||
});
|
||||
assert.equal(preflight.outcome, "blocked");
|
||||
assert.equal(preflight.submissionAllowed, false);
|
||||
assert.equal(preflight.checkSha256, null);
|
||||
});
|
||||
|
||||
test("Observatory dynamic preflight admits only an explicit queueable response", async () => {
|
||||
@@ -374,6 +426,7 @@ test("Observatory dynamic preflight admits only an explicit queueable response",
|
||||
|
||||
assert.equal(preflight.outcome, "queueable");
|
||||
assert.equal(preflight.submissionAllowed, true);
|
||||
assert.equal(preflight.checkSha256, null);
|
||||
});
|
||||
|
||||
test("Observatory setup contract rejects authority escalation and response drift", async () => {
|
||||
|
||||
@@ -61,6 +61,7 @@ function job(state = "queued") {
|
||||
restart_from_zero: state === "paused",
|
||||
preemption_receipt_sha256: null,
|
||||
claim_generation: 0,
|
||||
claim_lease: null,
|
||||
result: state === "succeeded"
|
||||
? { result_id: "m49-result", sha256: "d".repeat(64) }
|
||||
: null,
|
||||
@@ -132,6 +133,7 @@ test("Observatory submits only public identities and accepts every durable state
|
||||
"source-a",
|
||||
"m49-tgs",
|
||||
"observatory-ui:source-a:m49-tgs:request-a",
|
||||
null,
|
||||
{
|
||||
fetcher: async (input, init) => {
|
||||
request = { input: String(input), init };
|
||||
@@ -172,8 +174,41 @@ test("Observatory queue contract rejects authority escalation and response drift
|
||||
"source-a",
|
||||
"m49-tgs",
|
||||
"observatory-ui:source-a:m49-tgs:request-a",
|
||||
null,
|
||||
{ fetcher: async () => new Response(JSON.stringify(drifted), { status: 200 }) },
|
||||
),
|
||||
ObservatoryRecordedJobContractError,
|
||||
);
|
||||
});
|
||||
|
||||
test("portable submission carries the exact definition/check fence", async () => {
|
||||
let request;
|
||||
await submitObservatoryRecordedJob(
|
||||
"source-a",
|
||||
"lab-v1-eomt-ddrnet-portable-v1",
|
||||
"observatory-ui:source-a:lab-v1:request-a",
|
||||
{
|
||||
definitionSha256: "e".repeat(64),
|
||||
checkSha256: "f".repeat(64),
|
||||
},
|
||||
{
|
||||
fetcher: async (input, init) => {
|
||||
request = { input: String(input), init };
|
||||
const response = job("queued");
|
||||
response.setup.setup_id = "lab-v1-eomt-ddrnet-portable-v1";
|
||||
response.setup.definition_sha256 = "e".repeat(64);
|
||||
response.source.session_id = "source-a";
|
||||
return new Response(JSON.stringify(response), { status: 202 });
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
assert.deepEqual(JSON.parse(request.init.body), {
|
||||
schema_version: "missioncore.observatory-recorded-run-submit/v1",
|
||||
idempotency_key: "observatory-ui:source-a:lab-v1:request-a",
|
||||
source_session_id: "source-a",
|
||||
setup_id: "lab-v1-eomt-ddrnet-portable-v1",
|
||||
definition_sha256: "e".repeat(64),
|
||||
check_sha256: "f".repeat(64),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,6 +84,14 @@ test("Observatory mounts the one shared canonical replay only after explicit adm
|
||||
assert.match(workspace, /observatory-session-stack/);
|
||||
assert.match(workspace, /observatory-session-summary__facts/);
|
||||
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/,
|
||||
);
|
||||
assert.match(
|
||||
workspace,
|
||||
/<span>\{evidenceResultSubtitle\(evidence\)\}<\/span>/,
|
||||
);
|
||||
assert.match(
|
||||
workspace,
|
||||
/evidence\.recordedRun \? \([\s\S]*name="trash"[\s\S]*name="edit"[\s\S]*Открыть визуальный разбор:[\s\S]*name="eye"/,
|
||||
@@ -242,8 +250,12 @@ test("Observatory keeps one compact selector axis without the obsolete setup det
|
||||
assert.doesNotMatch(setupHook, /Promise\.allSettled/);
|
||||
assert.match(setupHook, /publishSetupCatalog\(\s*legacyCatalog/);
|
||||
assert.match(setupHook, /preserveUnknownSelection: true/);
|
||||
assert.match(
|
||||
setupHook,
|
||||
/portableProfileNames[\s\S]*legacy\.setups\.filter\([\s\S]*!portableProfileNames\.has\(setup\.displayName\)[\s\S]*\.\.\.portable\.setups/,
|
||||
);
|
||||
assert.match(setupHook, /selectedSetupId, sourceSessionId/);
|
||||
assert.match(workspace, /Worker установлен, запуск закрыт/);
|
||||
assert.match(workspace, /Worker готов к проверке/);
|
||||
assert.match(
|
||||
workspace,
|
||||
/preflightCandidate\.definitionSha256[\s\S]*selectedSetup\?\.runDefinition\?\.definitionSha256/,
|
||||
|
||||
Reference in New Issue
Block a user