feat(archive): complete recorded session lifecycle

This commit is contained in:
DCCONSTRUCTIONS
2026-07-19 01:07:21 +03:00
parent ffffee1879
commit 71c85e9894
22 changed files with 922 additions and 144 deletions
@@ -9,6 +9,7 @@ let decodeObservationSessionCatalog;
let decodeObservationSessionReplay;
let decodeObservationSessionPreparation;
let fetchObservationSessionCatalog;
let deleteObservationSession;
let replayObservationSession;
let fetchObservationSessionPreparation;
let cancelObservationSessionPreparation;
@@ -34,6 +35,7 @@ before(async () => {
decodeObservationSessionReplay,
decodeObservationSessionPreparation,
fetchObservationSessionCatalog,
deleteObservationSession,
replayObservationSession,
fetchObservationSessionPreparation,
cancelObservationSessionPreparation,
@@ -136,6 +138,39 @@ test("session catalog decodes canonical snake_case into a path-free camelCase mo
assert.equal("path" in catalog.items[0], false);
});
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(
new URL("../src/styles/observation-sessions.css", import.meta.url),
"utf8",
);
const spatialControls = await readFile(
new URL(
"../../../plugins/xgrids-k1/frontend/src/components/K1SpatialControls.tsx",
import.meta.url,
),
"utf8",
);
const acquisitionPipeline = await readFile(
new URL(
"../../../plugins/xgrids-k1/frontend/src/components/K1AcquisitionPipeline.tsx",
import.meta.url,
),
"utf8",
);
assert.match(appSource, /setRecordedReplayLabel\(session\.label\)/);
assert.match(appSource, /`\$\{activeDefinition\.title\}: \$\{recordedReplayLabel\}`/);
const deleteStyle = styles.slice(
styles.indexOf(".observation-session-option__delete"),
styles.indexOf(".observation-session-option__delete:disabled"),
);
assert.match(deleteStyle, /background:\s*transparent/);
assert.doesNotMatch(deleteStyle, /danger-rgb\) \/ 0\.12/);
assert.doesNotMatch(spatialControls, /Индикатор постоянно зелёный/);
assert.doesNotMatch(acquisitionPipeline, /Индикатор постоянно зелёный/);
});
test("session catalog exposes authoritative background preparation state", () => {
const catalog = decodeObservationSessionCatalog({
items: [session({
@@ -302,6 +337,37 @@ test("catalog API preserves HTTP detail without accepting a malformed success bo
);
});
test("delete API uses one opaque same-origin target and requires an empty 204", async () => {
const calls = [];
await deleteObservationSession("session-20260716T205632Z", {
fetcher: async (input, init) => {
calls.push({ input: String(input), init });
return new Response(null, { status: 204 });
},
});
assert.equal(calls.length, 1);
assert.equal(
calls[0].input,
"/api/v1/observation-sessions/session-20260716T205632Z",
);
assert.equal(calls[0].init.method, "DELETE");
await assert.rejects(
deleteObservationSession("../private", { fetcher: async () => new Response(null) }),
ObservationSessionContractError,
);
await assert.rejects(
deleteObservationSession("session-20260716T205632Z", {
fetcher: async () => new Response(JSON.stringify({ detail: "recording is open" }), {
status: 409,
headers: { "Content-Type": "application/json" },
}),
}),
(error) => error instanceof ObservationSessionApiError &&
error.status === 409 && error.message === "recording is open",
);
});
test("replay API accepts only a same-origin seekable recording descriptor", async () => {
const calls = [];
const launch = await replayObservationSession("session-20260716T205632Z", {