diff --git a/docs/eve-migration-roadmap.md b/docs/eve-migration-roadmap.md index ac3bd33..f8a2271 100644 --- a/docs/eve-migration-roadmap.md +++ b/docs/eve-migration-roadmap.md @@ -416,10 +416,10 @@ Recently cut: `runtimes/incubating/eve-electron/eveelectron-split-handoff.json`. It does claim a provider-agnostic Electron shell surface-tree projection and command surface, but not packaged rendering, embedded document rendering, plugin - projection, or capture. The handoff draws the line between the generic shell - and Aetheria's Starbridge RTS product client, and the split-target consumer - smoke keeps release, packaged rendering, embedded document rendering, and - capture proofs visible as blockers. + projection, or captured window artifacts. The handoff draws the line between + the generic shell and Aetheria's Starbridge RTS product client, and the + split-target consumer smoke keeps release, packaged rendering, embedded + document rendering, and capture artifact proofs visible as blockers. - EveElectron now has a direct split handoff smoke at `scripts/run-eveelectron-split-handoff-smoke.ps1`. The smoke verifies the current generic shell, command, and world-surface lowering paths, the observed @@ -431,8 +431,11 @@ Recently cut: lifecycle smoke at `scripts/run-eveelectron-lifecycle-smoke.ps1`. The manifest declares provider advertisement consumption, command transport, and surface-tree projection, claims `electron-shell` world/editor lowering, and - still keeps package release, packaged window rendering, plugin projection, - and Electron window capture as pending EveElectron work. + now carries an executable capture request contract through + `tools/eveelectron/eveelectron-capture-contract.mjs` and + `scripts/run-eveelectron-capture-contract-smoke.ps1`. Package release, + packaged window rendering, plugin projection, and actual Electron window + capture artifacts remain pending EveElectron work. - Electron and TUI pending lifecycle details now live in their runtime capability manifests rather than being duplicated in `tools/parity/parity-manifest.json`. The parity ledger points at the @@ -493,6 +496,16 @@ Recently cut: `scripts/run-eveunity-capture-contract-smoke.ps1` proves that request path without pretending a Unity PNG has been captured. The actual capture artifact remains a split blocker for EveUnity. +- EveElectron capture lifecycle now carries the same kind of structured pending + capture contract. `captureContract` names the Electron shell runtime, capture + kind, PNG artifact pattern, conformance attachment point, required Aetheria + provider surface, request schema, request builder, advertisement input, and + authority rule. `tools/eveelectron/eveelectron-capture-contract.mjs` builds a + `gamecult.eve.runtime_capture_request.v1` request from the runtime capability + manifest and provider advertisement, and + `scripts/run-eveelectron-capture-contract-smoke.ps1` proves that request path + without pretending an Electron window PNG has been captured. The actual + capture artifact remains a split blocker for EveElectron. - Runtime-owner conformance consumption now asserts EveUnity lifecycle stage evidence directly. The generic consumer supports `--expect-runtime-lifecycle-status` and diff --git a/docs/parity-testing-harness.md b/docs/parity-testing-harness.md index 43075eb..4ba2421 100644 --- a/docs/parity-testing-harness.md +++ b/docs/parity-testing-harness.md @@ -303,9 +303,10 @@ provider-agnostic shell exists. The same smoke asserts that the Aetheria Electron source paths named by the handoff currently exist as `observed-provider` sources, so the extraction pressure is visible without turning those paths into EveElectron authority. The Electron runtime now claims -a narrow provider-advertised surface-tree command surface, while packaged -rendering, embedded document rendering, capture, and plugin projection remain -blockers. EveTui is also intentionally pending: it now claims a lossy +a narrow provider-advertised surface-tree command surface and exposes a typed +capture request contract, while packaged rendering, embedded document +rendering, actual window capture, and plugin projection remain blockers. +EveTui is also intentionally pending: it now claims a lossy provider-advertised terminal-grid command surface, while transcript/cell-grid capture, embedded document rendering, plugin projection, and repo graduation remain blockers. EveUnity includes both the active `unity-uitoolkit` package proof diff --git a/runtimes/incubating/eve-electron/eve-runtime-capability.json b/runtimes/incubating/eve-electron/eve-runtime-capability.json index c8279c3..0f161a4 100644 --- a/runtimes/incubating/eve-electron/eve-runtime-capability.json +++ b/runtimes/incubating/eve-electron/eve-runtime-capability.json @@ -110,6 +110,10 @@ "captureContract": { "ownerRepo": "EveElectron", "runtimeId": "electron-shell", + "targetId": "electron-shell", + "requestSchema": "gamecult.eve.runtime_capture_request.v1", + "requestBuilder": "tools/eveelectron/eveelectron-capture-contract.mjs", + "advertisementPath": "web/fixtures/aetheria.provider-advertisement.json", "captureKind": "electron-window-png", "artifactKind": "png", "artifactPattern": "artifacts/eveelectron-capture/{stamp}/electron-shell.png", @@ -119,7 +123,11 @@ "authority": "Capture observes Electron projection only; provider state, command acceptance, and receipts stay provider-owned.", "publishProof": "Electron window capture artifact is attached to the runtime conformance report from EveElectron." }, - "evidencePaths": [], + "evidencePaths": [ + "tools/eveelectron/eveelectron-capture-contract.mjs", + "tools/eveelectron/eveelectron-capture-contract.test.mjs", + "scripts/run-eveelectron-capture-contract-smoke.ps1" + ], "pendingProofs": [ "Electron window capture artifact is attached to the runtime conformance report." ] diff --git a/runtimes/incubating/eve-electron/eveelectron-split-handoff.json b/runtimes/incubating/eve-electron/eveelectron-split-handoff.json index c0ae8b6..eff8b6f 100644 --- a/runtimes/incubating/eve-electron/eveelectron-split-handoff.json +++ b/runtimes/incubating/eve-electron/eveelectron-split-handoff.json @@ -57,9 +57,13 @@ { "id": "electron-capture-lifecycle", "stage": "capture", - "currentPaths": [], + "currentPaths": [ + "tools/eveelectron/eveelectron-capture-contract.mjs", + "tools/eveelectron/eveelectron-capture-contract.test.mjs", + "scripts/run-eveelectron-capture-contract-smoke.ps1" + ], "destinationOwner": "EveElectron", - "replacementProof": "EveElectron attaches desktop-window capture artifacts to conformance output for at least one non-Aetheria provider and one Aetheria scenario." + "replacementProof": "EveElectron builds a provider-advertisement-backed capture request now; the split is not complete until the EveElectron repo attaches desktop-window capture artifacts to conformance output for at least one non-Aetheria provider and one Aetheria scenario." } ], "contractInputs": [ diff --git a/scripts/run-eveelectron-capture-contract-smoke.ps1 b/scripts/run-eveelectron-capture-contract-smoke.ps1 new file mode 100644 index 0000000..2bc2f71 --- /dev/null +++ b/scripts/run-eveelectron-capture-contract-smoke.ps1 @@ -0,0 +1,99 @@ +param( + [string] $CapabilityManifestPath = "runtimes\incubating\eve-electron\eve-runtime-capability.json", + [string] $AdvertisementPath = "web\fixtures\aetheria.provider-advertisement.json", + [string] $OutputPath = "artifacts\eveelectron-capture-contract\latest\capture-request.json", + [string] $Stamp = "smoke" +) + +$ErrorActionPreference = "Stop" + +$projectRoot = Split-Path -Parent $PSScriptRoot +$absoluteCapabilityPath = if ([System.IO.Path]::IsPathRooted($CapabilityManifestPath)) { + $CapabilityManifestPath +} else { + Join-Path $projectRoot $CapabilityManifestPath +} +$absoluteAdvertisementPath = if ([System.IO.Path]::IsPathRooted($AdvertisementPath)) { + $AdvertisementPath +} else { + Join-Path $projectRoot $AdvertisementPath +} +$absoluteOutputPath = if ([System.IO.Path]::IsPathRooted($OutputPath)) { + $OutputPath +} else { + Join-Path $projectRoot $OutputPath +} + +foreach ($path in @($absoluteCapabilityPath, $absoluteAdvertisementPath)) { + if (-not (Test-Path -LiteralPath $path)) { + throw "EveElectron capture contract input not found: $path" + } +} + +$manifest = Get-Content -LiteralPath $absoluteCapabilityPath -Raw | ConvertFrom-Json +$captureContract = $manifest.lifecycle.capture.captureContract +if ($null -eq $captureContract) { + throw "EveElectron capture contract missing lifecycle.capture.captureContract" +} +foreach ($field in @("ownerRepo", "runtimeId", "targetId", "requestSchema", "requestBuilder", "advertisementPath", "captureKind", "artifactKind", "artifactPattern", "conformanceAttachment", "requiredProvider", "requiredSurface", "authority", "publishProof")) { + if (-not $captureContract.$field) { + throw "EveElectron capture contract missing $field" + } +} +if ($captureContract.ownerRepo -ne "EveElectron") { + throw "Unexpected EveElectron capture owner: $($captureContract.ownerRepo)" +} +if ($captureContract.runtimeId -ne "electron-shell") { + throw "Unexpected EveElectron capture runtime: $($captureContract.runtimeId)" +} +if ($captureContract.targetId -ne "electron-shell") { + throw "Unexpected EveElectron capture target: $($captureContract.targetId)" +} +if ($captureContract.requestSchema -ne "gamecult.eve.runtime_capture_request.v1") { + throw "Unexpected EveElectron capture request schema: $($captureContract.requestSchema)" +} + +$builderPath = Join-Path $projectRoot $captureContract.requestBuilder +if (-not (Test-Path -LiteralPath $builderPath)) { + throw "EveElectron capture request builder not found: $($captureContract.requestBuilder)" +} +if ((Join-Path $projectRoot $captureContract.advertisementPath) -ne $absoluteAdvertisementPath) { + throw "EveElectron capture advertisement path mismatch: $($captureContract.advertisementPath) vs $AdvertisementPath" +} + +node --test (Join-Path $projectRoot "tools\eveelectron\eveelectron-capture-contract.test.mjs") +if ($LASTEXITCODE -ne 0) { + throw "EveElectron capture contract tests failed with exit code $LASTEXITCODE" +} + +node $builderPath --capability $absoluteCapabilityPath --advertisement $absoluteAdvertisementPath --output $absoluteOutputPath --stamp $Stamp +if ($LASTEXITCODE -ne 0) { + throw "EveElectron capture request builder failed with exit code $LASTEXITCODE" +} +if (-not (Test-Path -LiteralPath $absoluteOutputPath)) { + throw "EveElectron capture request was not written: $absoluteOutputPath" +} + +$request = Get-Content -LiteralPath $absoluteOutputPath -Raw | ConvertFrom-Json +foreach ($expectation in @( + @{ field = "schema"; value = "gamecult.eve.runtime_capture_request.v1" }, + @{ field = "runtimeId"; value = "electron-shell" }, + @{ field = "ownerRepo"; value = "EveElectron" }, + @{ field = "providerId"; value = "aetheria" }, + @{ field = "surfaceId"; value = "aetheria.daemon.game" }, + @{ field = "targetId"; value = "electron-shell" }, + @{ field = "commandBoundary"; value = "aetheria.daemon.commands" }, + @{ field = "receiptSchema"; value = "aetheria.eve_command_acceptance_status.v1" }, + @{ field = "captureKind"; value = "electron-window-png" }, + @{ field = "artifactKind"; value = "png" }, + @{ field = "conformanceAttachment"; value = "runtime.captureArtifacts[]" } +)) { + if ($request.($expectation.field) -ne $expectation.value) { + throw "EveElectron capture request $($expectation.field) expected $($expectation.value) got $($request.($expectation.field))" + } +} +if ($request.artifactPath -ne "artifacts/eveelectron-capture/$Stamp/electron-shell.png") { + throw "Unexpected EveElectron capture artifact path: $($request.artifactPath)" +} + +Write-Host "EveElectron capture contract smoke passed: $absoluteOutputPath" diff --git a/scripts/run-eveelectron-lifecycle-smoke.ps1 b/scripts/run-eveelectron-lifecycle-smoke.ps1 index 318d11d..530ba90 100644 --- a/scripts/run-eveelectron-lifecycle-smoke.ps1 +++ b/scripts/run-eveelectron-lifecycle-smoke.ps1 @@ -138,13 +138,33 @@ foreach ($pathProperty in @("runnerScript", "consumerProject")) { } $captureContract = $manifest.lifecycle.capture.captureContract -foreach ($field in @("ownerRepo", "runtimeId", "captureKind", "artifactKind", "artifactPattern", "conformanceAttachment", "requiredProvider", "requiredSurface", "authority", "publishProof")) { +foreach ($field in @("ownerRepo", "runtimeId", "targetId", "requestSchema", "requestBuilder", "advertisementPath", "captureKind", "artifactKind", "artifactPattern", "conformanceAttachment", "requiredProvider", "requiredSurface", "authority", "publishProof")) { if (-not $captureContract.$field) { throw "EveElectron capture contract missing $field" } } +if ($captureContract.runtimeId -ne "electron-shell") { + throw "EveElectron capture contract has unexpected runtime: $($captureContract.runtimeId)" +} +if ($captureContract.targetId -ne "electron-shell") { + throw "EveElectron capture contract has unexpected target: $($captureContract.targetId)" +} +if ($captureContract.requestSchema -ne "gamecult.eve.runtime_capture_request.v1") { + throw "EveElectron capture contract has unexpected request schema: $($captureContract.requestSchema)" +} +if ($captureContract.captureKind -ne "electron-window-png") { + throw "EveElectron capture contract has unexpected capture kind: $($captureContract.captureKind)" +} +foreach ($pathProperty in @("requestBuilder", "advertisementPath")) { + $relativePath = $captureContract.$pathProperty + $absolutePath = Join-Path $projectRoot $relativePath + if (-not (Test-Path -LiteralPath $absolutePath)) { + throw "EveElectron capture contract $pathProperty does not exist: $relativePath" + } +} & (Join-Path $projectRoot "scripts\run-eveelectron-split-handoff-smoke.ps1") & (Join-Path $projectRoot "scripts\run-eveelectron-provider-shell-smoke.ps1") +& (Join-Path $projectRoot "scripts\run-eveelectron-capture-contract-smoke.ps1") Write-Host "EveElectron lifecycle smoke passed: $absoluteManifestPath" diff --git a/scripts/run-eveelectron-provider-shell-smoke.ps1 b/scripts/run-eveelectron-provider-shell-smoke.ps1 index c757034..fd9379b 100644 --- a/scripts/run-eveelectron-provider-shell-smoke.ps1 +++ b/scripts/run-eveelectron-provider-shell-smoke.ps1 @@ -67,7 +67,9 @@ foreach ($evidencePath in @($worldSurfaceLoweringClaim.evidencePaths)) { $expectedFiles = @( "runtimes\incubating\eve-electron\package.json", "runtimes\incubating\eve-electron\src\eve-electron-shell.mjs", - "runtimes\incubating\eve-electron\test\eve-electron-shell.test.mjs" + "runtimes\incubating\eve-electron\test\eve-electron-shell.test.mjs", + "scripts\run-eveelectron-capture-contract-smoke.ps1", + "tools\eveelectron\eveelectron-capture-contract.mjs" ) foreach ($relativePath in $expectedFiles) { diff --git a/scripts/run-eveelectron-split-handoff-smoke.ps1 b/scripts/run-eveelectron-split-handoff-smoke.ps1 index b955be9..2b98714 100644 --- a/scripts/run-eveelectron-split-handoff-smoke.ps1 +++ b/scripts/run-eveelectron-split-handoff-smoke.ps1 @@ -69,6 +69,14 @@ foreach ($id in @("electron-shell-runtime", "electron-command-transport", "elect } } +$captureMoveSet = $moveSets | Where-Object { $_.id -eq "electron-capture-lifecycle" } | Select-Object -First 1 +if (-not $captureMoveSet) { + throw "EveElectron split handoff missing move set: electron-capture-lifecycle" +} +if (@($captureMoveSet.currentPaths).Count -eq 0) { + throw "EveElectron capture lifecycle move set must declare currentPaths" +} + foreach ($contract in @( "gamecult.eve.surface.v1", "gamecult.eve.command.v1", diff --git a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 index 9317f92..4f30488 100644 --- a/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 +++ b/scripts/run-runtime-owner-conformance-consumer-smoke.ps1 @@ -167,6 +167,10 @@ node $consumerScript $consumerExport ` --expect-runtime-lifecycle-field "electron-shell:test:testContract.runnerKind:electron-provider-shell-smoke" ` --expect-runtime-lifecycle-field "electron-shell:test:testContract.runnerScript:scripts/run-eveelectron-provider-shell-smoke.ps1" ` --expect-runtime-lifecycle-field "electron-shell:capture:captureContract.captureKind:electron-window-png" ` + --expect-runtime-lifecycle-field "electron-shell:capture:captureContract.targetId:electron-shell" ` + --expect-runtime-lifecycle-field "electron-shell:capture:captureContract.requestSchema:gamecult.eve.runtime_capture_request.v1" ` + --expect-runtime-lifecycle-field "electron-shell:capture:captureContract.requestBuilder:tools/eveelectron/eveelectron-capture-contract.mjs" ` + --expect-runtime-lifecycle-field "electron-shell:capture:captureContract.advertisementPath:web/fixtures/aetheria.provider-advertisement.json" ` --expect-runtime-lifecycle-field "electron-shell:capture:captureContract.requiredSurface:aetheria.daemon.game" ` --expect-runtime-lifecycle-status tui:release:pending-tui-package ` --expect-runtime-lifecycle-status tui:test:provider-shell-contract-skeleton ` diff --git a/scripts/run-split-target-conformance-consumer-smoke.ps1 b/scripts/run-split-target-conformance-consumer-smoke.ps1 index 8902893..ec61f37 100644 --- a/scripts/run-split-target-conformance-consumer-smoke.ps1 +++ b/scripts/run-split-target-conformance-consumer-smoke.ps1 @@ -64,6 +64,7 @@ try { --expect-split-handoff-move EveElectron:electron-shell:electron-shell-runtime:current:exists:eve-electron-shell.mjs ` --expect-split-handoff-move EveElectron:electron-shell:electron-command-transport:current:exists:eve-electron-shell.mjs ` --expect-split-handoff-move EveElectron:electron-shell:electron-world-surface-lowering:current:exists:eve-electron-shell.mjs ` + --expect-split-handoff-move EveElectron:electron-shell:electron-capture-lifecycle:current:exists:eveelectron-capture-contract.mjs ` --expect-split-handoff-move EveElectron:electron-shell:electron-shell-runtime:observed-provider:exists:Aetheria.Rts.Web/Electron ` --expect-split-handoff-move EveElectron:electron-shell:electron-command-transport:observed-provider:exists:Aetheria.Rts.Web/Client/app.ts ` --expect-split-handoff-move EveElectron:electron-shell:electron-world-surface-lowering:observed-provider:exists:Aetheria.Rts.Web/Client ` diff --git a/tools/eveelectron/eveelectron-capture-contract.mjs b/tools/eveelectron/eveelectron-capture-contract.mjs new file mode 100644 index 0000000..559d97d --- /dev/null +++ b/tools/eveelectron/eveelectron-capture-contract.mjs @@ -0,0 +1,122 @@ +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +export function buildElectronCaptureRequest({ + advertisement, + capabilityManifest, + advertisementPath = "", + capabilityManifestPath = "", + stamp = new Date().toISOString().replace(/[:.]/g, "-"), +} = {}) { + if (!advertisement || typeof advertisement !== "object") { + throw new Error("advertisement is required"); + } + if (!capabilityManifest || typeof capabilityManifest !== "object") { + throw new Error("capabilityManifest is required"); + } + + const captureContract = capabilityManifest.lifecycle?.capture?.captureContract; + if (!captureContract) { + throw new Error("capability manifest is missing lifecycle.capture.captureContract"); + } + + if (advertisement.providerId !== captureContract.requiredProvider) { + throw new Error( + `capture provider mismatch: expected ${captureContract.requiredProvider} got ${advertisement.providerId || "missing"}`, + ); + } + + const targetId = captureContract.targetId || captureContract.runtimeId; + const surface = (advertisement.surfaces || []).find(candidate => candidate.surfaceId === captureContract.requiredSurface); + if (!surface) { + throw new Error(`capture surface not advertised: ${captureContract.requiredSurface}`); + } + + const loweringTargets = surface.worldInteraction?.loweringTargets || []; + if (!loweringTargets.includes(targetId)) { + throw new Error(`surface ${surface.surfaceId} does not advertise lowering target ${targetId}`); + } + + const artifactPath = replaceStamp(captureContract.artifactPattern, stamp); + + return { + schema: captureContract.requestSchema || "gamecult.eve.runtime_capture_request.v1", + runtimeId: capabilityManifest.runtimeId, + ownerRepo: captureContract.ownerRepo, + providerId: advertisement.providerId, + surfaceId: surface.surfaceId, + surfaceKind: surface.surfaceKind || "", + targetId, + projectionKind: surface.worldInteraction?.projectionKind || "", + commandBoundary: surface.worldInteraction?.commandBoundary || "", + receiptSchema: surface.worldInteraction?.receiptSchema || "", + captureKind: captureContract.captureKind, + artifactKind: captureContract.artifactKind, + artifactPath, + conformanceAttachment: captureContract.conformanceAttachment, + authority: captureContract.authority, + sourceAdvertisementPath: advertisementPath, + sourceCapabilityManifestPath: capabilityManifestPath, + issuedAtUtc: stamp, + }; +} + +export function loadJsonFile(filePath) { + return JSON.parse(readFileSync(filePath, "utf8")); +} + +export function writeCaptureRequest(request, outputPath) { + mkdirSync(path.dirname(outputPath), { recursive: true }); + writeFileSync(outputPath, `${JSON.stringify(request, null, 2)}\n`, "utf8"); +} + +function replaceStamp(pattern, stamp) { + if (!pattern || !pattern.includes("{stamp}")) { + throw new Error(`capture artifact pattern must include {stamp}: ${pattern || "missing"}`); + } + return pattern.replace("{stamp}", stamp); +} + +function parseArgs(argv) { + const args = {}; + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]; + if (!value.startsWith("--")) continue; + const key = value.slice(2); + const next = argv[index + 1]; + if (!next || next.startsWith("--")) { + args[key] = true; + } else { + args[key] = next; + index += 1; + } + } + return args; +} + +function runCli() { + const args = parseArgs(process.argv.slice(2)); + for (const required of ["advertisement", "capability", "output"]) { + if (!args[required]) { + throw new Error(`missing required --${required}`); + } + } + + const stamp = args.stamp || new Date().toISOString().replace(/[:.]/g, "-"); + const request = buildElectronCaptureRequest({ + advertisement: loadJsonFile(args.advertisement), + capabilityManifest: loadJsonFile(args.capability), + advertisementPath: args.advertisement, + capabilityManifestPath: args.capability, + stamp, + }); + writeCaptureRequest(request, args.output); + process.stdout.write(`${args.output}\n`); +} + +const invokedPath = process.argv[1] ? path.resolve(process.argv[1]) : ""; +const modulePath = fileURLToPath(import.meta.url); +if (invokedPath === modulePath) { + runCli(); +} diff --git a/tools/eveelectron/eveelectron-capture-contract.test.mjs b/tools/eveelectron/eveelectron-capture-contract.test.mjs new file mode 100644 index 0000000..3587ceb --- /dev/null +++ b/tools/eveelectron/eveelectron-capture-contract.test.mjs @@ -0,0 +1,40 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { buildElectronCaptureRequest } from "./eveelectron-capture-contract.mjs"; + +const advertisement = JSON.parse(readFileSync(new URL("../../web/fixtures/aetheria.provider-advertisement.json", import.meta.url), "utf8")); +const capabilityManifest = JSON.parse(readFileSync(new URL("../../runtimes/incubating/eve-electron/eve-runtime-capability.json", import.meta.url), "utf8")); + +test("builds Electron shell capture request from provider advertisement", () => { + const request = buildElectronCaptureRequest({ + advertisement, + capabilityManifest, + advertisementPath: "web/fixtures/aetheria.provider-advertisement.json", + capabilityManifestPath: "runtimes/incubating/eve-electron/eve-runtime-capability.json", + stamp: "smoke", + }); + + assert.equal(request.schema, "gamecult.eve.runtime_capture_request.v1"); + assert.equal(request.runtimeId, "electron-shell"); + assert.equal(request.ownerRepo, "EveElectron"); + assert.equal(request.providerId, "aetheria"); + assert.equal(request.surfaceId, "aetheria.daemon.game"); + assert.equal(request.targetId, "electron-shell"); + assert.equal(request.projectionKind, "provider-authored-world-surface"); + assert.equal(request.commandBoundary, "aetheria.daemon.commands"); + assert.equal(request.receiptSchema, "aetheria.eve_command_acceptance_status.v1"); + assert.equal(request.artifactPath, "artifacts/eveelectron-capture/smoke/electron-shell.png"); + assert.match(request.authority, /provider state/); +}); + +test("rejects capture request when the surface does not advertise Electron shell", () => { + const withoutTarget = structuredClone(advertisement); + const surface = withoutTarget.surfaces.find(candidate => candidate.surfaceId === "aetheria.daemon.game"); + surface.worldInteraction.loweringTargets = surface.worldInteraction.loweringTargets.filter(target => target !== "electron-shell"); + + assert.throws( + () => buildElectronCaptureRequest({ advertisement: withoutTarget, capabilityManifest, stamp: "smoke" }), + /does not advertise lowering target electron-shell/, + ); +}); diff --git a/tools/parity/parity-manifest.json b/tools/parity/parity-manifest.json index cbc8382..58d2492 100644 --- a/tools/parity/parity-manifest.json +++ b/tools/parity/parity-manifest.json @@ -758,7 +758,10 @@ "runtimes/incubating/eve-electron/eveelectron-split-handoff.json", "runtimes/incubating/eve-electron/eve-runtime-capability.json", "scripts/run-eveelectron-lifecycle-smoke.ps1", - "scripts/run-eveelectron-provider-shell-smoke.ps1" + "scripts/run-eveelectron-provider-shell-smoke.ps1", + "scripts/run-eveelectron-capture-contract-smoke.ps1", + "tools/eveelectron/eveelectron-capture-contract.mjs", + "tools/eveelectron/eveelectron-capture-contract.test.mjs" ], "expectedSourceSymbols": [ { @@ -768,6 +771,14 @@ { "path": "runtimes/incubating/eve-electron/test/eve-electron-shell.test.mjs", "contains": ["selects the active advertised provider surface", "lowers provider surface trees into an Electron shell projection", "rejects surface documents that do not match the advertised target", "command intents carry provider-advertised boundaries", "aetheria.daemon.commands", "aetheria.eve_command_acceptance_status.v1"] + }, + { + "path": "tools/eveelectron/eveelectron-capture-contract.mjs", + "contains": ["buildElectronCaptureRequest", "gamecult.eve.runtime_capture_request.v1", "loweringTargets", "artifactPattern", "writeCaptureRequest"] + }, + { + "path": "tools/eveelectron/eveelectron-capture-contract.test.mjs", + "contains": ["builds Electron shell capture request from provider advertisement", "rejects capture request when the surface does not advertise Electron shell", "electron-shell", "aetheria.daemon.commands"] } ], "capture": {