refactor(platform): harden LAB evidence and telemetry
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e31-source-binding",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e31/source-qualifications",
|
||||
"result_id_prefix": "e31-source-qualification",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e31-source-qualification/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e32-track-geometry",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e32/results",
|
||||
"result_id_prefix": "e32-track-geometry",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e32-track-geometry-replay/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e33-worker-shadow",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e33/results",
|
||||
"result_id_prefix": "e33-worker-shadow",
|
||||
"document_name": "result.json",
|
||||
"schema_version": "missioncore.e33-worker-shadow-result/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e34-temporal-layer",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e34/results",
|
||||
"result_id_prefix": "e34-temporal-occupied",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e34-temporal-occupied-result/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e35-degradation-recovery",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e35/results",
|
||||
"result_id_prefix": "e35-degradation-recovery",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e35-degradation-result/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e37-ravnoves-acceptance",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e37/results",
|
||||
"result_id_prefix": "e37-ravnoves-acceptance",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e37-acceptance-contract/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e38-perception-baseline",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e38/results",
|
||||
"result_id_prefix": "e38-perception-baseline",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e38-perception-baseline/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e39-perception-refinement",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e39/results",
|
||||
"result_id_prefix": "e39-perception-refinement",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e39-perception-refinement/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e40-perception-product-gate",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e40/results",
|
||||
"result_id_prefix": "e40-perception-product-gate",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e40-perception-product-gate/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46-detector-truth-island",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46/results",
|
||||
"result_id_prefix": "e46-detector-truth-island",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46-detector-truth-island/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46a-ai-engineering-preannotation",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46a/ai-engineering-preannotations",
|
||||
"result_id_prefix": "e46a-ai-engineering-preannotation",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46a-ai-engineering-preannotation/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46b-temporal-motion",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46b/temporal-motion",
|
||||
"result_id_prefix": "e46b-temporal-motion",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46b-temporal-motion/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46c-full-replay-world-tracks",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46c/full-replay-world-tracks",
|
||||
"result_id_prefix": "e46c-full-replay-world-tracks",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46c-full-replay-world-tracks/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46d-temporal-failure-audit",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46d/temporal-failure-audits",
|
||||
"result_id_prefix": "e46d-temporal-failure-audit",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46d-temporal-failure-audit/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46e-ready-stack",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46e/ready-stack-results",
|
||||
"result_id_prefix": "e46e-ready-stack",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46e-ready-stack-result/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46f-dashcam-bakeoff",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46f/dashcam-bakeoff-results",
|
||||
"result_id_prefix": "e46f-dashcam-bakeoff",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46f-dashcam-bakeoff-result/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46g-rectified-detector-bakeoff",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46g/results",
|
||||
"result_id_prefix": "e46g-rectified-detector-bakeoff",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46g-rectified-detector-bakeoff-result/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46h-full-rectified-front-replay",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46h/results",
|
||||
"result_id_prefix": "e46h-full-rectified-front-replay",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46h-full-rectified-front-replay-result/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46i-grounding-dino-full-replay",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46i/results",
|
||||
"result_id_prefix": "e46i-grounding-dino-full-replay",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46i-grounding-dino-full-replay-result/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "e46j-raw-fisheye-realtime",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "e46j/results",
|
||||
"result_id_prefix": "e46j-raw-fisheye-realtime",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.e46j-raw-fisheye-realtime-result/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "l34-right-yolox-truth-island-freeze",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "l3/right-yolox-truth-island-freeze",
|
||||
"result_id_prefix": "l34-right-yolox-truth-island-freeze",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.l34-right-yolox-truth-island-freeze/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "l34a-assisted-yolox-error-audit",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "l3/assisted-yolox-error-audits",
|
||||
"result_id_prefix": "l34a-assisted-yolox-error-audit",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.l34a-assisted-yolox-error-audit/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "l34b-nested-box-consolidation-shadow",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "l3/nested-box-consolidation-shadows",
|
||||
"result_id_prefix": "l34b-nested-box-consolidation-shadow",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.l34b-nested-box-consolidation-shadow/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "l34c-tile-seam-stitch-shadow",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "l3/tile-seam-stitch-shadows",
|
||||
"result_id_prefix": "l34c-tile-seam-stitch-shadow",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.l34c-tile-seam-stitch-shadow/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "l34d-cumulative-postprocessing-candidate",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "l3/cumulative-postprocessing-candidates",
|
||||
"result_id_prefix": "l34d-cumulative-postprocessing-candidate",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.l34d-cumulative-postprocessing-candidate/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "l34e-self-review-diagnostic",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "l3/self-review-diagnostics",
|
||||
"result_id_prefix": "l34e-self-review-diagnostic",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.l34e-self-review-diagnostic/v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": "l34f-adjudicated-reference",
|
||||
"evidence": {
|
||||
"runtime_relative_root": "l3/adjudicated-references",
|
||||
"result_id_prefix": "l34f-adjudicated-reference",
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": "missioncore.l34f-adjudicated-reference/v1"
|
||||
}
|
||||
}
|
||||
@@ -115,6 +115,12 @@ configuration and rolls back if the service does not return to `Running`. MQTT
|
||||
credentials are scoped to the service environment and must not be passed on a command
|
||||
line or stored in the repository.
|
||||
|
||||
Both install and update configure Windows Service Control Manager recovery for
|
||||
Telegraf: restart after 5 seconds, then 30 seconds, then 60 seconds, with the failure
|
||||
counter reset after one day. The compute-contour worker network transition applies
|
||||
the same policy to already-provisioned agents and fails closed unless the worker
|
||||
confirms it.
|
||||
|
||||
The same host service reads the existing perception worker's loopback `/health`
|
||||
contract with `Get-NdcMissionCorePipelineTelemetry.ps1` and publishes nine
|
||||
stage-keyed snapshots to the contour's `pipeline` topic. The perception container
|
||||
|
||||
@@ -128,6 +128,15 @@ try {
|
||||
throw "Failed to apply the NDC service display name"
|
||||
}
|
||||
Set-Service -Name $serviceName -StartupType Automatic
|
||||
& sc.exe failure $serviceName reset= 86400 `
|
||||
actions= restart/5000/restart/30000/restart/60000 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to configure Telegraf service recovery actions"
|
||||
}
|
||||
& sc.exe failureflag $serviceName 1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to enable Telegraf recovery for non-crash failures"
|
||||
}
|
||||
Start-Service -Name $serviceName
|
||||
$service = Get-Service -Name $serviceName
|
||||
$service.WaitForStatus([ServiceProcess.ServiceControllerStatus]::Running, [TimeSpan]::FromSeconds(20))
|
||||
@@ -140,6 +149,7 @@ try {
|
||||
DisplayName = $service.DisplayName
|
||||
Status = $service.Status.ToString()
|
||||
StartType = $service.StartType.ToString()
|
||||
RecoveryConfigured = $true
|
||||
Configuration = $configurationPath
|
||||
PipelineCollector = $collectorPath
|
||||
PipelineJournal = $PipelineJournal
|
||||
|
||||
@@ -118,8 +118,27 @@ try {
|
||||
[ServiceProcess.ServiceControllerStatus]::Running,
|
||||
[TimeSpan]::FromSeconds(20)
|
||||
)
|
||||
& sc.exe failure $serviceName reset= 86400 `
|
||||
actions= restart/5000/restart/30000/restart/60000 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to configure Telegraf service recovery actions"
|
||||
}
|
||||
& sc.exe failureflag $serviceName 1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to enable Telegraf recovery for non-crash failures"
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Stop-Service -Name $serviceName -Force -ErrorAction SilentlyContinue
|
||||
$service = Get-Service -Name $serviceName
|
||||
if ($service.Status -ne [ServiceProcess.ServiceControllerStatus]::Stopped) {
|
||||
$service.WaitForStatus(
|
||||
[ServiceProcess.ServiceControllerStatus]::Stopped,
|
||||
[TimeSpan]::FromSeconds(20)
|
||||
)
|
||||
}
|
||||
Set-ItemProperty -Path $serviceRegistryPath -Name Environment `
|
||||
-Type MultiString -Value $serviceEnvironmentBefore
|
||||
Copy-Item -LiteralPath $backupPath -Destination $configurationPath
|
||||
if ($hadCollector) {
|
||||
Copy-Item -LiteralPath $backupCollectorPath -Destination $collectorPath
|
||||
@@ -135,6 +154,7 @@ try {
|
||||
SchemaVersion = "missioncore.telemetry-agent-config-result/v1"
|
||||
ServiceName = $serviceName
|
||||
Status = (Get-Service -Name $serviceName).Status.ToString()
|
||||
RecoveryConfigured = $true
|
||||
Configuration = $configurationPath
|
||||
Backup = $backupPath
|
||||
PipelineCollector = $collectorPath
|
||||
|
||||
@@ -16,10 +16,9 @@ The first local contour is active:
|
||||
- Mission Core on canonical port `8000` uses this adapter for live System
|
||||
telemetry and reports the source as `agent-mqtt`;
|
||||
- SSH is retained only for explicit profile diagnostics and bootstrap checks.
|
||||
- the selected contour owns a managed network profile: Worker 006 uses
|
||||
`desktop-opj8j04.local`, resolves the broker through the operator Mac's stable
|
||||
`.local` name, and keeps the current private Docker bind address as a separate
|
||||
reviewed value;
|
||||
- the selected contour owns a managed network profile: Worker 006 keeps its node
|
||||
identity separate from its SSH address and from the broker endpoint; every saved
|
||||
endpoint and private Docker bind address is probed before an explicit transition;
|
||||
- the existing contour settings can probe DNS/TCP from both Mac and Worker, apply the
|
||||
listener address to the local broker, and apply the endpoint to the Telegraf Windows
|
||||
service without exposing or replacing its scoped credential.
|
||||
@@ -81,11 +80,27 @@ The direct SSH/PowerShell probe remains a bounded bootstrap and diagnostic fallb
|
||||
the existing Worker 006 profile. It is not the target live telemetry transport.
|
||||
|
||||
Mesh access points belonging to one LAN are not modeled as separate broker addresses.
|
||||
Roaming keeps the stable hostname contract. A move to another LAN changes the reviewed
|
||||
private bind address, not the agent identity or MQTT topic identity. Applying a saved
|
||||
profile is explicit, verifies DNS and TCP reachability, and rolls back the affected
|
||||
configuration when verification fails. MQTT without TLS cannot be configured for a
|
||||
public endpoint or wildcard listener.
|
||||
The broker endpoint may be a hostname only when the Worker can resolve it; Mission Core
|
||||
does not assume that macOS `.local` discovery is portable to Windows. A move to another
|
||||
LAN changes the reviewed endpoint and private bind address, not the agent identity or
|
||||
MQTT topic identity. Applying a saved profile is explicit, verifies DNS and TCP
|
||||
reachability, and rolls back the affected configuration when verification fails. MQTT
|
||||
without TLS cannot be configured for a public endpoint or wildcard listener.
|
||||
|
||||
## 2026-08-05 recovery correction
|
||||
|
||||
Worker 006 stopped publishing because its Telegraf MQTT output still referenced the
|
||||
operator Mac through a `.local` name that Windows no longer resolved. Telegraf exited
|
||||
on the output connection error, and the Windows service had no configured recovery
|
||||
actions. The reviewed compute-contour transition rebound the broker and worker profile
|
||||
to the currently verified private LAN endpoint without replacing the scoped MQTT
|
||||
credential; fresh agent, hardware and pipeline telemetry then returned.
|
||||
|
||||
Install, update and existing-agent network transitions now configure the same bounded
|
||||
Windows recovery policy: restart after 5 seconds, 30 seconds and 60 seconds, resetting
|
||||
the failure counter after one day and applying recovery to non-crash failures. This
|
||||
does not make an invalid endpoint valid; it keeps transient broker or network loss from
|
||||
silently leaving the telemetry agent stopped after connectivity returns.
|
||||
|
||||
## Why these components
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# ADR 0036: Configurative laboratory evidence registry
|
||||
|
||||
Date: 2026-08-05
|
||||
Status: accepted and implemented as the first migration slice
|
||||
|
||||
## Context
|
||||
|
||||
The advanced LAB index grew by adding one result-id regular expression, one root
|
||||
provider argument, one application wiring block and one tuple for every experiment.
|
||||
That made the Mission Core control plane structurally dependent on an expanding list
|
||||
of experiment names. It also made a new frozen evidence source look like a new product
|
||||
runtime pipeline even when the UI only needed read-only result discovery.
|
||||
|
||||
The target product architecture has a stricter dependency direction:
|
||||
|
||||
```text
|
||||
contracts -> providers -> graphs -> experiments
|
||||
```
|
||||
|
||||
Mission Core owns configurations, runs, evidence, health, telemetry and operator
|
||||
control. Experiment code may invoke production graphs and freeze evidence, but the
|
||||
core must not import an experiment to discover that evidence.
|
||||
|
||||
## Decision
|
||||
|
||||
Advanced LAB evidence sources are declared as one versioned JSON definition per
|
||||
`work_id` under `config/laboratories/`. Each definition contains only:
|
||||
|
||||
- a repository-runtime-relative evidence root;
|
||||
- a digest-bound result-id prefix;
|
||||
- the bounded identity document name;
|
||||
- the exact identity-document schema version.
|
||||
|
||||
`LaboratoryEvidenceRegistry` discovers these files without importing experiment code.
|
||||
It rejects unknown keys, unsafe or non-normalized paths, symlinked definitions,
|
||||
oversized files, invalid identifiers and duplicate work IDs, roots or result prefixes.
|
||||
The existing advanced index keeps its previous fail-closed evidence validation:
|
||||
identity digest, schema, creation time and read-only/no-command authority must still
|
||||
match before a result is published.
|
||||
|
||||
The production advanced index now obtains all 27 standard evidence sources through
|
||||
this registry. Adding another standard source therefore adds data, not another router
|
||||
parameter or application wiring branch.
|
||||
|
||||
## Explicit non-goals
|
||||
|
||||
This first slice configures evidence discovery; it does not claim that the historical
|
||||
experiment implementations are already reusable production graphs. E31-E40 retain
|
||||
specialized read-model projectors behind their current adapters while those payloads
|
||||
are converted to canonical contracts. L3/L31/L32/L33 retain bounded legacy identity
|
||||
adapters because their stored evidence does not yet use the common identity document.
|
||||
|
||||
The registry does not select detectors, trackers, planners or command paths and cannot
|
||||
grant navigation or safety authority. Future executable LAB definitions must reference
|
||||
versioned provider/graph contracts; they must not contain arbitrary Python imports or
|
||||
shell commands.
|
||||
|
||||
## Consequences
|
||||
|
||||
- LAB growth no longer expands the product composition root for standard evidence.
|
||||
- Evidence location and identity contracts are reviewable, diffable configuration.
|
||||
- A malformed or ambiguous definition prevents startup instead of silently exposing
|
||||
the wrong evidence.
|
||||
- The remaining manual frontend fetch/render map and specialized backend projectors
|
||||
are visible migration debt, not the intended extension mechanism.
|
||||
- The next architecture slice can add provider and graph registries against the
|
||||
canonical perception contracts without making experiments dependencies of core.
|
||||
@@ -0,0 +1,15 @@
|
||||
"""Configuration contracts for Mission Core laboratory evidence."""
|
||||
|
||||
from k1link.laboratory.evidence_registry import (
|
||||
LABORATORY_EVIDENCE_DEFINITION_SCHEMA,
|
||||
LaboratoryEvidenceDefinition,
|
||||
LaboratoryEvidenceRegistry,
|
||||
LaboratoryRegistryError,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"LABORATORY_EVIDENCE_DEFINITION_SCHEMA",
|
||||
"LaboratoryEvidenceDefinition",
|
||||
"LaboratoryEvidenceRegistry",
|
||||
"LaboratoryRegistryError",
|
||||
]
|
||||
@@ -0,0 +1,187 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path, PurePosixPath
|
||||
from typing import Final
|
||||
|
||||
LABORATORY_EVIDENCE_DEFINITION_SCHEMA: Final = "missioncore.laboratory-evidence-definition/v1"
|
||||
_DEFINITION_MAX_BYTES: Final = 16 * 1024
|
||||
_IDENTIFIER = re.compile(r"^[a-z][a-z0-9-]{2,95}$")
|
||||
_SCHEMA_VERSION = re.compile(r"^missioncore\.[a-z0-9.-]+/v[1-9][0-9]*$")
|
||||
_TOP_LEVEL_KEYS: Final = frozenset({"schema_version", "work_id", "evidence"})
|
||||
_EVIDENCE_KEYS: Final = frozenset(
|
||||
{"runtime_relative_root", "result_id_prefix", "document_name", "schema_version"}
|
||||
)
|
||||
|
||||
|
||||
class LaboratoryRegistryError(ValueError):
|
||||
"""Raised when a LAB evidence definition is unsafe or ambiguous."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaboratoryEvidenceDefinition:
|
||||
work_id: str
|
||||
runtime_relative_root: PurePosixPath
|
||||
result_id_prefix: str
|
||||
document_name: str
|
||||
result_schema_version: str
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
_identifier(self.work_id, "work_id")
|
||||
_identifier(self.result_id_prefix, "result_id_prefix")
|
||||
_document_name(self.document_name)
|
||||
_schema_version(self.result_schema_version)
|
||||
if not isinstance(self.runtime_relative_root, PurePosixPath):
|
||||
raise LaboratoryRegistryError("runtime_relative_root must be a POSIX path")
|
||||
_relative_root(str(self.runtime_relative_root))
|
||||
|
||||
@property
|
||||
def result_id_pattern(self) -> re.Pattern[str]:
|
||||
return re.compile(rf"^{re.escape(self.result_id_prefix)}-[a-f0-9]{{64}}$")
|
||||
|
||||
def result_root(self, runtime_root: Path) -> Path:
|
||||
return runtime_root.joinpath(*self.runtime_relative_root.parts)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class LaboratoryEvidenceRegistry:
|
||||
definitions: tuple[LaboratoryEvidenceDefinition, ...]
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if not isinstance(self.definitions, tuple) or not all(
|
||||
isinstance(definition, LaboratoryEvidenceDefinition) for definition in self.definitions
|
||||
):
|
||||
raise LaboratoryRegistryError("LAB definitions must be an immutable tuple")
|
||||
_reject_duplicates(self.definitions)
|
||||
|
||||
@classmethod
|
||||
def from_directory(cls, root: Path) -> LaboratoryEvidenceRegistry:
|
||||
definition_root = _real_directory(root, "LAB definition root")
|
||||
definitions = tuple(
|
||||
_read_definition(path)
|
||||
for path in sorted(definition_root.glob("*.json"), key=lambda item: item.name)
|
||||
)
|
||||
return cls(definitions=definitions)
|
||||
|
||||
|
||||
def _real_directory(path: Path, label: str) -> Path:
|
||||
candidate = path.expanduser().absolute()
|
||||
if candidate.is_symlink():
|
||||
raise LaboratoryRegistryError(f"{label} must not be a symlink")
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError as exc:
|
||||
raise LaboratoryRegistryError(f"{label} does not exist") from exc
|
||||
if not resolved.is_dir():
|
||||
raise LaboratoryRegistryError(f"{label} must be a directory")
|
||||
return resolved
|
||||
|
||||
|
||||
def _read_definition(path: Path) -> LaboratoryEvidenceDefinition:
|
||||
if path.is_symlink() or not path.is_file():
|
||||
raise LaboratoryRegistryError(f"LAB definition must be a regular file: {path.name}")
|
||||
if path.stat().st_size > _DEFINITION_MAX_BYTES:
|
||||
raise LaboratoryRegistryError(f"LAB definition is too large: {path.name}")
|
||||
try:
|
||||
payload: object = json.loads(path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
raise LaboratoryRegistryError(f"LAB definition is unreadable: {path.name}") from exc
|
||||
document = _object(payload, f"LAB definition {path.name}")
|
||||
_exact_keys(document, _TOP_LEVEL_KEYS, f"LAB definition {path.name}")
|
||||
if document["schema_version"] != LABORATORY_EVIDENCE_DEFINITION_SCHEMA:
|
||||
raise LaboratoryRegistryError(f"LAB definition schema is invalid: {path.name}")
|
||||
work_id = _identifier(document["work_id"], "work_id")
|
||||
if path.name != f"{work_id}.json":
|
||||
raise LaboratoryRegistryError(f"LAB definition filename must match work_id: {path.name}")
|
||||
evidence = _object(document["evidence"], f"LAB evidence {work_id}")
|
||||
_exact_keys(evidence, _EVIDENCE_KEYS, f"LAB evidence {work_id}")
|
||||
result_id_prefix = _identifier(evidence["result_id_prefix"], "result_id_prefix")
|
||||
document_name = _document_name(evidence["document_name"])
|
||||
result_schema_version = _schema_version(evidence["schema_version"])
|
||||
return LaboratoryEvidenceDefinition(
|
||||
work_id=work_id,
|
||||
runtime_relative_root=_relative_root(evidence["runtime_relative_root"]),
|
||||
result_id_prefix=result_id_prefix,
|
||||
document_name=document_name,
|
||||
result_schema_version=result_schema_version,
|
||||
)
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, object]:
|
||||
if not isinstance(value, dict) or not all(isinstance(key, str) for key in value):
|
||||
raise LaboratoryRegistryError(f"{label} must be an object")
|
||||
return value
|
||||
|
||||
|
||||
def _exact_keys(document: dict[str, object], expected: frozenset[str], label: str) -> None:
|
||||
actual = frozenset(document)
|
||||
if actual != expected:
|
||||
missing = sorted(expected - actual)
|
||||
unexpected = sorted(actual - expected)
|
||||
raise LaboratoryRegistryError(
|
||||
f"{label} keys are invalid; missing={missing}, unexpected={unexpected}"
|
||||
)
|
||||
|
||||
|
||||
def _text(value: object, label: str) -> str:
|
||||
if not isinstance(value, str) or not value.strip() or value != value.strip():
|
||||
raise LaboratoryRegistryError(f"{label} must be a non-empty trimmed string")
|
||||
return value
|
||||
|
||||
|
||||
def _identifier(value: object, label: str) -> str:
|
||||
text = _text(value, label)
|
||||
if _IDENTIFIER.fullmatch(text) is None:
|
||||
raise LaboratoryRegistryError(f"{label} is invalid")
|
||||
return text
|
||||
|
||||
|
||||
def _schema_version(value: object) -> str:
|
||||
text = _text(value, "evidence schema_version")
|
||||
if _SCHEMA_VERSION.fullmatch(text) is None:
|
||||
raise LaboratoryRegistryError("evidence schema_version is invalid")
|
||||
return text
|
||||
|
||||
|
||||
def _document_name(value: object) -> str:
|
||||
text = _text(value, "document_name")
|
||||
candidate = PurePosixPath(text)
|
||||
if (
|
||||
candidate.is_absolute()
|
||||
or len(candidate.parts) != 1
|
||||
or candidate.name != text
|
||||
or candidate.suffix != ".json"
|
||||
):
|
||||
raise LaboratoryRegistryError("document_name must be one JSON filename")
|
||||
return text
|
||||
|
||||
|
||||
def _relative_root(value: object) -> PurePosixPath:
|
||||
text = _text(value, "runtime_relative_root")
|
||||
if "\\" in text:
|
||||
raise LaboratoryRegistryError("runtime_relative_root must use POSIX separators")
|
||||
candidate = PurePosixPath(text)
|
||||
if (
|
||||
candidate.is_absolute()
|
||||
or not candidate.parts
|
||||
or text == "."
|
||||
or str(candidate) != text
|
||||
or any(part in {"", ".", ".."} for part in candidate.parts)
|
||||
):
|
||||
raise LaboratoryRegistryError("runtime_relative_root must be a normalized relative path")
|
||||
return candidate
|
||||
|
||||
|
||||
def _reject_duplicates(definitions: tuple[LaboratoryEvidenceDefinition, ...]) -> None:
|
||||
dimensions = {
|
||||
"work_id": [definition.work_id for definition in definitions],
|
||||
"result_id_prefix": [definition.result_id_prefix for definition in definitions],
|
||||
"runtime_relative_root": [
|
||||
str(definition.runtime_relative_root) for definition in definitions
|
||||
],
|
||||
}
|
||||
for label, values in dimensions.items():
|
||||
if len(values) != len(set(values)):
|
||||
raise LaboratoryRegistryError(f"duplicate LAB {label}")
|
||||
@@ -55,6 +55,7 @@ from k1link.compute.e40_perception_product_gate import (
|
||||
E40PerceptionProductGateError,
|
||||
read_e40_perception_product_gate,
|
||||
)
|
||||
from k1link.laboratory import LaboratoryEvidenceDefinition, LaboratoryEvidenceRegistry
|
||||
from k1link.web.l3_pointpillars_visual_api import latest_l3_visual_identity
|
||||
from k1link.web.l32_pointpillars_camera_review_api import latest_l32_identity
|
||||
from k1link.web.l33_camera_first_detector_review_api import latest_l33_identity
|
||||
@@ -72,24 +73,6 @@ _E37_RESULT_ID = re.compile(r"^e37-ravnoves-acceptance-[a-f0-9]{64}$")
|
||||
_E38_RESULT_ID = re.compile(r"^e38-perception-baseline-[a-f0-9]{64}$")
|
||||
_E39_RESULT_ID = re.compile(r"^e39-perception-refinement-[a-f0-9]{64}$")
|
||||
_E40_RESULT_ID = re.compile(r"^e40-perception-product-gate-[a-f0-9]{64}$")
|
||||
_E46_RESULT_ID = re.compile(r"^e46-detector-truth-island-[a-f0-9]{64}$")
|
||||
_E46A_RESULT_ID = re.compile(r"^e46a-ai-engineering-preannotation-[a-f0-9]{64}$")
|
||||
_E46B_RESULT_ID = re.compile(r"^e46b-temporal-motion-[a-f0-9]{64}$")
|
||||
_E46C_RESULT_ID = re.compile(r"^e46c-full-replay-world-tracks-[a-f0-9]{64}$")
|
||||
_E46D_RESULT_ID = re.compile(r"^e46d-temporal-failure-audit-[a-f0-9]{64}$")
|
||||
_E46E_RESULT_ID = re.compile(r"^e46e-ready-stack-[a-f0-9]{64}$")
|
||||
_E46F_RESULT_ID = re.compile(r"^e46f-dashcam-bakeoff-[a-f0-9]{64}$")
|
||||
_E46G_RESULT_ID = re.compile(r"^e46g-rectified-detector-bakeoff-[a-f0-9]{64}$")
|
||||
_E46H_RESULT_ID = re.compile(r"^e46h-full-rectified-front-replay-[a-f0-9]{64}$")
|
||||
_E46I_RESULT_ID = re.compile(r"^e46i-grounding-dino-full-replay-[a-f0-9]{64}$")
|
||||
_E46J_RESULT_ID = re.compile(r"^e46j-raw-fisheye-realtime-[a-f0-9]{64}$")
|
||||
_L34_RESULT_ID = re.compile(r"^l34-right-yolox-truth-island-freeze-[a-f0-9]{64}$")
|
||||
_L34A_RESULT_ID = re.compile(r"^l34a-assisted-yolox-error-audit-[a-f0-9]{64}$")
|
||||
_L34B_RESULT_ID = re.compile(r"^l34b-nested-box-consolidation-shadow-[a-f0-9]{64}$")
|
||||
_L34C_RESULT_ID = re.compile(r"^l34c-tile-seam-stitch-shadow-[a-f0-9]{64}$")
|
||||
_L34D_RESULT_ID = re.compile(r"^l34d-cumulative-postprocessing-candidate-[a-f0-9]{64}$")
|
||||
_L34E_RESULT_ID = re.compile(r"^l34e-self-review-diagnostic-[a-f0-9]{64}$")
|
||||
_L34F_RESULT_ID = re.compile(r"^l34f-adjudicated-reference-[a-f0-9]{64}$")
|
||||
|
||||
RootProvider = Callable[[], Path | None]
|
||||
|
||||
@@ -299,6 +282,50 @@ def _advanced_index(
|
||||
}
|
||||
|
||||
|
||||
def _registry_index_specs(
|
||||
registry: LaboratoryEvidenceRegistry,
|
||||
runtime_root_provider: RootProvider,
|
||||
) -> tuple[_AdvancedIndexSpec, ...]:
|
||||
return tuple(
|
||||
(
|
||||
definition.work_id,
|
||||
_evidence_root_provider(definition, runtime_root_provider),
|
||||
definition.result_id_pattern,
|
||||
definition.document_name,
|
||||
definition.result_schema_version,
|
||||
)
|
||||
for definition in registry.definitions
|
||||
)
|
||||
|
||||
|
||||
def _evidence_root_provider(
|
||||
definition: LaboratoryEvidenceDefinition,
|
||||
runtime_root_provider: RootProvider,
|
||||
) -> RootProvider:
|
||||
def result_root_provider() -> Path | None:
|
||||
configured_runtime_root = runtime_root_provider()
|
||||
if configured_runtime_root is None:
|
||||
return None
|
||||
runtime_candidate = configured_runtime_root.expanduser().absolute()
|
||||
if runtime_candidate.is_symlink():
|
||||
return None
|
||||
try:
|
||||
runtime_root = runtime_candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
candidate = definition.result_root(runtime_root)
|
||||
if candidate.exists():
|
||||
try:
|
||||
resolved = candidate.resolve(strict=True)
|
||||
except OSError:
|
||||
return None
|
||||
if resolved != candidate.absolute() or not resolved.is_relative_to(runtime_root):
|
||||
return None
|
||||
return candidate
|
||||
|
||||
return result_root_provider
|
||||
|
||||
|
||||
def _object(value: object, label: str) -> dict[str, Any]:
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError(f"{label} is invalid")
|
||||
@@ -786,6 +813,8 @@ def _empty_catalog(configured: bool) -> dict[str, object]:
|
||||
|
||||
def build_advanced_laboratory_router(
|
||||
*,
|
||||
evidence_registry: LaboratoryEvidenceRegistry | None = None,
|
||||
evidence_runtime_root_provider: RootProvider = lambda: None,
|
||||
e31_root_provider: RootProvider = lambda: None,
|
||||
e32_root_provider: RootProvider = lambda: None,
|
||||
e33_root_provider: RootProvider = lambda: None,
|
||||
@@ -795,24 +824,6 @@ def build_advanced_laboratory_router(
|
||||
e38_root_provider: RootProvider = lambda: None,
|
||||
e39_root_provider: RootProvider = lambda: None,
|
||||
e40_root_provider: RootProvider = lambda: None,
|
||||
e46_root_provider: RootProvider = lambda: None,
|
||||
e46a_root_provider: RootProvider = lambda: None,
|
||||
e46b_root_provider: RootProvider = lambda: None,
|
||||
e46c_root_provider: RootProvider = lambda: None,
|
||||
e46d_root_provider: RootProvider = lambda: None,
|
||||
e46e_root_provider: RootProvider = lambda: None,
|
||||
e46f_root_provider: RootProvider = lambda: None,
|
||||
e46g_root_provider: RootProvider = lambda: None,
|
||||
e46h_root_provider: RootProvider = lambda: None,
|
||||
e46i_root_provider: RootProvider = lambda: None,
|
||||
e46j_root_provider: RootProvider = lambda: None,
|
||||
l34_root_provider: RootProvider = lambda: None,
|
||||
l34a_root_provider: RootProvider = lambda: None,
|
||||
l34b_root_provider: RootProvider = lambda: None,
|
||||
l34c_root_provider: RootProvider = lambda: None,
|
||||
l34d_root_provider: RootProvider = lambda: None,
|
||||
l34e_root_provider: RootProvider = lambda: None,
|
||||
l34f_root_provider: RootProvider = lambda: None,
|
||||
l3_visual_root_provider: RootProvider = lambda: None,
|
||||
l31_ravnoves_root_provider: RootProvider = lambda: None,
|
||||
l32_camera_review_root_provider: RootProvider = lambda: None,
|
||||
@@ -822,8 +833,8 @@ def build_advanced_laboratory_router(
|
||||
|
||||
@router.get("/advanced-index")
|
||||
def list_advanced_results() -> dict[str, object]:
|
||||
result = _advanced_index(
|
||||
(
|
||||
if evidence_registry is None:
|
||||
specs: tuple[_AdvancedIndexSpec, ...] = (
|
||||
(
|
||||
"e31-source-binding",
|
||||
e31_root_provider,
|
||||
@@ -887,134 +898,13 @@ def build_advanced_laboratory_router(
|
||||
"manifest.json",
|
||||
"missioncore.e40-perception-product-gate/v1",
|
||||
),
|
||||
(
|
||||
"e46-detector-truth-island",
|
||||
e46_root_provider,
|
||||
_E46_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46-detector-truth-island/v1",
|
||||
),
|
||||
(
|
||||
"e46a-ai-engineering-preannotation",
|
||||
e46a_root_provider,
|
||||
_E46A_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46a-ai-engineering-preannotation/v1",
|
||||
),
|
||||
(
|
||||
"e46b-temporal-motion",
|
||||
e46b_root_provider,
|
||||
_E46B_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46b-temporal-motion/v1",
|
||||
),
|
||||
(
|
||||
"e46c-full-replay-world-tracks",
|
||||
e46c_root_provider,
|
||||
_E46C_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46c-full-replay-world-tracks/v1",
|
||||
),
|
||||
(
|
||||
"e46d-temporal-failure-audit",
|
||||
e46d_root_provider,
|
||||
_E46D_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46d-temporal-failure-audit/v1",
|
||||
),
|
||||
(
|
||||
"e46e-ready-stack",
|
||||
e46e_root_provider,
|
||||
_E46E_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46e-ready-stack-result/v1",
|
||||
),
|
||||
(
|
||||
"e46f-dashcam-bakeoff",
|
||||
e46f_root_provider,
|
||||
_E46F_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46f-dashcam-bakeoff-result/v1",
|
||||
),
|
||||
(
|
||||
"e46g-rectified-detector-bakeoff",
|
||||
e46g_root_provider,
|
||||
_E46G_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46g-rectified-detector-bakeoff-result/v1",
|
||||
),
|
||||
(
|
||||
"e46h-full-rectified-front-replay",
|
||||
e46h_root_provider,
|
||||
_E46H_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46h-full-rectified-front-replay-result/v1",
|
||||
),
|
||||
(
|
||||
"e46i-grounding-dino-full-replay",
|
||||
e46i_root_provider,
|
||||
_E46I_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46i-grounding-dino-full-replay-result/v1",
|
||||
),
|
||||
(
|
||||
"e46j-raw-fisheye-realtime",
|
||||
e46j_root_provider,
|
||||
_E46J_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.e46j-raw-fisheye-realtime-result/v1",
|
||||
),
|
||||
(
|
||||
"l34-right-yolox-truth-island-freeze",
|
||||
l34_root_provider,
|
||||
_L34_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34-right-yolox-truth-island-freeze/v1",
|
||||
),
|
||||
(
|
||||
"l34a-assisted-yolox-error-audit",
|
||||
l34a_root_provider,
|
||||
_L34A_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34a-assisted-yolox-error-audit/v1",
|
||||
),
|
||||
(
|
||||
"l34b-nested-box-consolidation-shadow",
|
||||
l34b_root_provider,
|
||||
_L34B_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34b-nested-box-consolidation-shadow/v1",
|
||||
),
|
||||
(
|
||||
"l34c-tile-seam-stitch-shadow",
|
||||
l34c_root_provider,
|
||||
_L34C_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34c-tile-seam-stitch-shadow/v1",
|
||||
),
|
||||
(
|
||||
"l34d-cumulative-postprocessing-candidate",
|
||||
l34d_root_provider,
|
||||
_L34D_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34d-cumulative-postprocessing-candidate/v1",
|
||||
),
|
||||
(
|
||||
"l34e-self-review-diagnostic",
|
||||
l34e_root_provider,
|
||||
_L34E_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34e-self-review-diagnostic/v1",
|
||||
),
|
||||
(
|
||||
"l34f-adjudicated-reference",
|
||||
l34f_root_provider,
|
||||
_L34F_RESULT_ID,
|
||||
"manifest.json",
|
||||
"missioncore.l34f-adjudicated-reference/v1",
|
||||
),
|
||||
)
|
||||
)
|
||||
else:
|
||||
specs = _registry_index_specs(
|
||||
evidence_registry,
|
||||
evidence_runtime_root_provider,
|
||||
)
|
||||
result = _advanced_index(specs)
|
||||
items = result["items"]
|
||||
if not isinstance(items, list):
|
||||
raise RuntimeError("advanced LAB index items are invalid")
|
||||
|
||||
+8
-90
@@ -23,6 +23,7 @@ from k1link.compute import (
|
||||
RecordedPerceptionOverlayMux,
|
||||
RecordedPerceptionOverlayStore,
|
||||
)
|
||||
from k1link.laboratory import LaboratoryEvidenceRegistry
|
||||
from k1link.sessions import (
|
||||
MaterializedRecording,
|
||||
RecordedMediaInspector,
|
||||
@@ -133,6 +134,9 @@ from k1link.web.viewer_diagnostics_api import build_viewer_diagnostics_router
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[3]
|
||||
INVALID_REQUEST_DETAIL = "Некорректные параметры запроса."
|
||||
LABORATORY_EVIDENCE_REGISTRY = LaboratoryEvidenceRegistry.from_directory(
|
||||
REPOSITORY_ROOT / "config" / "laboratories"
|
||||
)
|
||||
|
||||
|
||||
def _resolve_media_tool(name: str) -> Path | None:
|
||||
@@ -557,6 +561,10 @@ app.include_router(
|
||||
)
|
||||
app.include_router(
|
||||
build_advanced_laboratory_router(
|
||||
evidence_registry=LABORATORY_EVIDENCE_REGISTRY,
|
||||
evidence_runtime_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments"
|
||||
),
|
||||
e31_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e31" / "source-qualifications"
|
||||
),
|
||||
@@ -584,96 +592,6 @@ app.include_router(
|
||||
e40_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e40" / "results"
|
||||
),
|
||||
e46_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46" / "results"
|
||||
),
|
||||
e46a_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46a"
|
||||
/ "ai-engineering-preannotations"
|
||||
),
|
||||
e46b_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46b" / "temporal-motion"
|
||||
),
|
||||
e46c_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46c"
|
||||
/ "full-replay-world-tracks"
|
||||
),
|
||||
e46d_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46d"
|
||||
/ "temporal-failure-audits"
|
||||
),
|
||||
e46e_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46e"
|
||||
/ "ready-stack-results"
|
||||
),
|
||||
e46f_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "e46f"
|
||||
/ "dashcam-bakeoff-results"
|
||||
),
|
||||
e46g_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46g" / "results"
|
||||
),
|
||||
e46h_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46h" / "results"
|
||||
),
|
||||
e46i_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46i" / "results"
|
||||
),
|
||||
e46j_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "e46j" / "results"
|
||||
),
|
||||
l34_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "right-yolox-truth-island-freeze"
|
||||
),
|
||||
l34a_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "assisted-yolox-error-audits"
|
||||
),
|
||||
l34b_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "nested-box-consolidation-shadows"
|
||||
),
|
||||
l34c_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "tile-seam-stitch-shadows"
|
||||
),
|
||||
l34d_root_provider=lambda: (
|
||||
REPOSITORY_ROOT
|
||||
/ ".runtime"
|
||||
/ "compute-experiments"
|
||||
/ "l3"
|
||||
/ "cumulative-postprocessing-candidates"
|
||||
),
|
||||
l34e_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "self-review-diagnostics"
|
||||
),
|
||||
l34f_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "adjudicated-references"
|
||||
),
|
||||
l3_visual_root_provider=lambda: (
|
||||
REPOSITORY_ROOT / ".runtime" / "compute-experiments" / "l3" / "visual-audits"
|
||||
),
|
||||
|
||||
@@ -313,6 +313,15 @@ elseif ((Get-Service -Name "telegraf").Status -ne "Running") {
|
||||
[TimeSpan]::FromSeconds(20)
|
||||
)
|
||||
}
|
||||
& sc.exe failure telegraf reset= 86400 `
|
||||
actions= restart/5000/restart/30000/restart/60000 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to configure Telegraf service recovery actions"
|
||||
}
|
||||
& sc.exe failureflag telegraf 1 | Out-Null
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Failed to enable Telegraf recovery for non-crash failures"
|
||||
}
|
||||
$resolved = @(
|
||||
[Net.Dns]::GetHostAddresses([string]$payload.mqtt_host) |
|
||||
ForEach-Object { $_.IPAddressToString } |
|
||||
@@ -326,6 +335,7 @@ $resolved = @(
|
||||
mqtt_port = [int]$payload.mqtt_port
|
||||
resolved_addresses = $resolved
|
||||
broker_reachable = $true
|
||||
recovery_configured = $true
|
||||
changed = $changed
|
||||
} | ConvertTo-Json -Depth 5 -Compress
|
||||
""".strip()
|
||||
@@ -587,6 +597,7 @@ def apply_worker_network(
|
||||
or document.get("mqtt_port") != target.mqtt_port
|
||||
or document.get("service_status") != "Running"
|
||||
or document.get("broker_reachable") is not True
|
||||
or document.get("recovery_configured") is not True
|
||||
):
|
||||
raise NetworkOperationError(
|
||||
"Worker не подтвердил применённый сетевой профиль."
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePosixPath
|
||||
from types import SimpleNamespace
|
||||
|
||||
from fastapi import APIRouter
|
||||
@@ -9,6 +9,7 @@ from fastapi.routing import APIRoute
|
||||
from pytest import MonkeyPatch
|
||||
|
||||
import k1link.web.advanced_laboratory_api as advanced_api
|
||||
from k1link.laboratory import LaboratoryEvidenceDefinition, LaboratoryEvidenceRegistry
|
||||
from k1link.web.advanced_laboratory_api import build_advanced_laboratory_router
|
||||
|
||||
|
||||
@@ -24,6 +25,26 @@ def _endpoint(router: APIRouter, path: str) -> object:
|
||||
raise AssertionError(f"GET {path} route is missing")
|
||||
|
||||
|
||||
def _evidence_registry(
|
||||
root: Path,
|
||||
*,
|
||||
work_id: str,
|
||||
result_id_prefix: str,
|
||||
schema_version: str,
|
||||
) -> LaboratoryEvidenceRegistry:
|
||||
return LaboratoryEvidenceRegistry(
|
||||
definitions=(
|
||||
LaboratoryEvidenceDefinition(
|
||||
work_id=work_id,
|
||||
runtime_relative_root=PurePosixPath(root.name),
|
||||
result_id_prefix=result_id_prefix,
|
||||
document_name="manifest.json",
|
||||
result_schema_version=schema_version,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def test_advanced_catalogs_are_empty_when_not_configured() -> None:
|
||||
router = build_advanced_laboratory_router()
|
||||
|
||||
@@ -53,6 +74,26 @@ def test_advanced_index_is_empty_when_not_configured() -> None:
|
||||
}
|
||||
|
||||
|
||||
def test_registry_index_does_not_follow_a_runtime_symlink(tmp_path: Path) -> None:
|
||||
actual = tmp_path / "actual"
|
||||
actual.mkdir()
|
||||
linked = tmp_path / "linked"
|
||||
linked.symlink_to(actual, target_is_directory=True)
|
||||
registry = _evidence_registry(
|
||||
linked,
|
||||
work_id="linked-lab",
|
||||
result_id_prefix="linked-result",
|
||||
schema_version="missioncore.linked-result/v1",
|
||||
)
|
||||
router = build_advanced_laboratory_router(
|
||||
evidence_registry=registry,
|
||||
evidence_runtime_root_provider=lambda: tmp_path,
|
||||
)
|
||||
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
|
||||
|
||||
assert route()["items"] == [] # type: ignore[index,operator]
|
||||
|
||||
|
||||
def test_advanced_index_publishes_l34b_shadow_identity(tmp_path: Path) -> None:
|
||||
root = tmp_path / "l34b"
|
||||
digest = "d" * 64
|
||||
@@ -82,7 +123,13 @@ def test_advanced_index_publishes_l34b_shadow_identity(tmp_path: Path) -> None:
|
||||
encoding="utf-8",
|
||||
)
|
||||
router = build_advanced_laboratory_router(
|
||||
l34b_root_provider=lambda: root,
|
||||
evidence_registry=_evidence_registry(
|
||||
root,
|
||||
work_id="l34b-nested-box-consolidation-shadow",
|
||||
result_id_prefix="l34b-nested-box-consolidation-shadow",
|
||||
schema_version="missioncore.l34b-nested-box-consolidation-shadow/v1",
|
||||
),
|
||||
evidence_runtime_root_provider=lambda: root.parent,
|
||||
)
|
||||
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
|
||||
|
||||
@@ -123,7 +170,13 @@ def test_advanced_index_publishes_l34c_shadow_identity(tmp_path: Path) -> None:
|
||||
encoding="utf-8",
|
||||
)
|
||||
router = build_advanced_laboratory_router(
|
||||
l34c_root_provider=lambda: root,
|
||||
evidence_registry=_evidence_registry(
|
||||
root,
|
||||
work_id="l34c-tile-seam-stitch-shadow",
|
||||
result_id_prefix="l34c-tile-seam-stitch-shadow",
|
||||
schema_version="missioncore.l34c-tile-seam-stitch-shadow/v1",
|
||||
),
|
||||
evidence_runtime_root_provider=lambda: root.parent,
|
||||
)
|
||||
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
|
||||
|
||||
@@ -166,7 +219,13 @@ def test_advanced_index_publishes_l34d_candidate_identity(tmp_path: Path) -> Non
|
||||
encoding="utf-8",
|
||||
)
|
||||
router = build_advanced_laboratory_router(
|
||||
l34d_root_provider=lambda: root,
|
||||
evidence_registry=_evidence_registry(
|
||||
root,
|
||||
work_id="l34d-cumulative-postprocessing-candidate",
|
||||
result_id_prefix="l34d-cumulative-postprocessing-candidate",
|
||||
schema_version="missioncore.l34d-cumulative-postprocessing-candidate/v1",
|
||||
),
|
||||
evidence_runtime_root_provider=lambda: root.parent,
|
||||
)
|
||||
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
|
||||
|
||||
@@ -207,7 +266,13 @@ def test_advanced_index_publishes_l34e_diagnostic_identity(tmp_path: Path) -> No
|
||||
encoding="utf-8",
|
||||
)
|
||||
router = build_advanced_laboratory_router(
|
||||
l34e_root_provider=lambda: root,
|
||||
evidence_registry=_evidence_registry(
|
||||
root,
|
||||
work_id="l34e-self-review-diagnostic",
|
||||
result_id_prefix="l34e-self-review-diagnostic",
|
||||
schema_version="missioncore.l34e-self-review-diagnostic/v1",
|
||||
),
|
||||
evidence_runtime_root_provider=lambda: root.parent,
|
||||
)
|
||||
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
|
||||
|
||||
@@ -250,7 +315,13 @@ def test_advanced_index_publishes_l34f_adjudicated_reference_identity(
|
||||
encoding="utf-8",
|
||||
)
|
||||
router = build_advanced_laboratory_router(
|
||||
l34f_root_provider=lambda: root,
|
||||
evidence_registry=_evidence_registry(
|
||||
root,
|
||||
work_id="l34f-adjudicated-reference",
|
||||
result_id_prefix="l34f-adjudicated-reference",
|
||||
schema_version="missioncore.l34f-adjudicated-reference/v1",
|
||||
),
|
||||
evidence_runtime_root_provider=lambda: root.parent,
|
||||
)
|
||||
route = _endpoint(router, "/api/v1/laboratory/advanced-index")
|
||||
|
||||
|
||||
@@ -111,6 +111,7 @@ def test_worker_apply_uses_private_resolved_endpoint_and_confirms_service(
|
||||
"mqtt_port": 1883,
|
||||
"resolved_addresses": ["192.168.68.56"],
|
||||
"broker_reachable": True,
|
||||
"recovery_configured": True,
|
||||
}
|
||||
|
||||
monkeypatch.setattr(network, "_run_worker_powershell", run)
|
||||
@@ -121,6 +122,8 @@ def test_worker_apply_uses_private_resolved_endpoint_and_confirms_service(
|
||||
assert result["ready"] is True
|
||||
assert "__PAYLOAD_BASE64__" not in captured["script"]
|
||||
assert "MISSIONCORE_MQTT_PASSWORD" not in captured["script"]
|
||||
assert "restart/5000/restart/30000/restart/60000" in captured["script"]
|
||||
assert "failureflag telegraf 1" in captured["script"]
|
||||
assert captured["timeout"] == 35
|
||||
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import hashlib
|
||||
import json
|
||||
import struct
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
import pytest
|
||||
from fastapi import APIRouter, HTTPException
|
||||
@@ -22,6 +22,7 @@ from k1link.compute.l34_right_yolox_truth_island_freeze import (
|
||||
L34_PREDICTIONS_NAME,
|
||||
L34_REPORT_NAME,
|
||||
)
|
||||
from k1link.laboratory import LaboratoryEvidenceDefinition, LaboratoryEvidenceRegistry
|
||||
from k1link.web.advanced_laboratory_api import build_advanced_laboratory_router
|
||||
from k1link.web.l34_annotation_api import (
|
||||
L34AnnotationCreateRequest,
|
||||
@@ -579,7 +580,20 @@ def test_l34_annotation_sessions_are_revisioned_and_not_truth(
|
||||
def test_advanced_index_includes_l34_by_real_creation_time(tmp_path: Path) -> None:
|
||||
result = _write_result(tmp_path)
|
||||
router = build_advanced_laboratory_router(
|
||||
l34_root_provider=lambda: tmp_path,
|
||||
evidence_registry=LaboratoryEvidenceRegistry(
|
||||
definitions=(
|
||||
LaboratoryEvidenceDefinition(
|
||||
work_id="l34-right-yolox-truth-island-freeze",
|
||||
runtime_relative_root=PurePosixPath(tmp_path.name),
|
||||
result_id_prefix="l34-right-yolox-truth-island-freeze",
|
||||
document_name="manifest.json",
|
||||
result_schema_version=(
|
||||
"missioncore.l34-right-yolox-truth-island-freeze/v1"
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
evidence_runtime_root_provider=lambda: tmp_path.parent,
|
||||
)
|
||||
index = _endpoint(router, "/api/v1/laboratory/advanced-index")()
|
||||
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path, PurePosixPath
|
||||
|
||||
import pytest
|
||||
|
||||
from k1link.laboratory import (
|
||||
LaboratoryEvidenceDefinition,
|
||||
LaboratoryEvidenceRegistry,
|
||||
LaboratoryRegistryError,
|
||||
)
|
||||
|
||||
|
||||
def _definition(
|
||||
work_id: str,
|
||||
*,
|
||||
root: str | None = None,
|
||||
prefix: str | None = None,
|
||||
extra: dict[str, object] | None = None,
|
||||
) -> dict[str, object]:
|
||||
document: dict[str, object] = {
|
||||
"schema_version": "missioncore.laboratory-evidence-definition/v1",
|
||||
"work_id": work_id,
|
||||
"evidence": {
|
||||
"runtime_relative_root": root or f"{work_id}/results",
|
||||
"result_id_prefix": prefix or work_id,
|
||||
"document_name": "manifest.json",
|
||||
"schema_version": f"missioncore.{work_id}/v1",
|
||||
},
|
||||
}
|
||||
if extra is not None:
|
||||
document.update(extra)
|
||||
return document
|
||||
|
||||
|
||||
def _write(root: Path, work_id: str, document: dict[str, object]) -> Path:
|
||||
path = root / f"{work_id}.json"
|
||||
path.write_text(json.dumps(document), encoding="utf-8")
|
||||
return path
|
||||
|
||||
|
||||
def test_registry_loads_sorted_definitions_and_resolves_beneath_runtime_root(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
definitions = tmp_path / "definitions"
|
||||
definitions.mkdir()
|
||||
_write(definitions, "z-last", _definition("z-last"))
|
||||
_write(
|
||||
definitions,
|
||||
"a-first",
|
||||
_definition("a-first", root="family/a-first/results"),
|
||||
)
|
||||
|
||||
registry = LaboratoryEvidenceRegistry.from_directory(definitions)
|
||||
|
||||
assert [item.work_id for item in registry.definitions] == ["a-first", "z-last"]
|
||||
first = registry.definitions[0]
|
||||
assert first.result_root(tmp_path / "runtime") == (
|
||||
tmp_path / "runtime" / "family" / "a-first" / "results"
|
||||
)
|
||||
assert first.result_id_pattern.fullmatch(f"a-first-{'a' * 64}") is not None
|
||||
assert first.result_id_pattern.fullmatch("a-first-not-a-digest") is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize("unsafe_root", ["../escape", "/absolute", "a\\b", "."])
|
||||
def test_registry_rejects_unsafe_runtime_roots(
|
||||
tmp_path: Path,
|
||||
unsafe_root: str,
|
||||
) -> None:
|
||||
definitions = tmp_path / "definitions"
|
||||
definitions.mkdir()
|
||||
_write(definitions, "unsafe-lab", _definition("unsafe-lab", root=unsafe_root))
|
||||
|
||||
with pytest.raises(LaboratoryRegistryError, match="runtime_relative_root"):
|
||||
LaboratoryEvidenceRegistry.from_directory(definitions)
|
||||
|
||||
|
||||
def test_registry_rejects_unknown_keys(tmp_path: Path) -> None:
|
||||
definitions = tmp_path / "definitions"
|
||||
definitions.mkdir()
|
||||
_write(
|
||||
definitions,
|
||||
"strict-lab",
|
||||
_definition("strict-lab", extra={"renderer": "hard-coded"}),
|
||||
)
|
||||
|
||||
with pytest.raises(LaboratoryRegistryError, match=r"unexpected=\['renderer'\]"):
|
||||
LaboratoryEvidenceRegistry.from_directory(definitions)
|
||||
|
||||
|
||||
def test_registry_rejects_symlinked_definitions(tmp_path: Path) -> None:
|
||||
definitions = tmp_path / "definitions"
|
||||
definitions.mkdir()
|
||||
outside = tmp_path / "outside.json"
|
||||
outside.write_text(json.dumps(_definition("linked-lab")), encoding="utf-8")
|
||||
(definitions / "linked-lab.json").symlink_to(outside)
|
||||
|
||||
with pytest.raises(LaboratoryRegistryError, match="regular file"):
|
||||
LaboratoryEvidenceRegistry.from_directory(definitions)
|
||||
|
||||
|
||||
def test_registry_rejects_ambiguous_result_prefixes(tmp_path: Path) -> None:
|
||||
definitions = tmp_path / "definitions"
|
||||
definitions.mkdir()
|
||||
_write(definitions, "first-lab", _definition("first-lab", prefix="shared-result"))
|
||||
_write(definitions, "second-lab", _definition("second-lab", prefix="shared-result"))
|
||||
|
||||
with pytest.raises(LaboratoryRegistryError, match="duplicate LAB result_id_prefix"):
|
||||
LaboratoryEvidenceRegistry.from_directory(definitions)
|
||||
|
||||
|
||||
def test_definition_constructor_cannot_bypass_path_validation() -> None:
|
||||
with pytest.raises(LaboratoryRegistryError, match="runtime_relative_root"):
|
||||
LaboratoryEvidenceDefinition(
|
||||
work_id="unsafe-lab",
|
||||
runtime_relative_root=PurePosixPath("../escape"),
|
||||
result_id_prefix="unsafe-result",
|
||||
document_name="manifest.json",
|
||||
result_schema_version="missioncore.unsafe-result/v1",
|
||||
)
|
||||
|
||||
|
||||
def test_product_registry_declares_every_advanced_evidence_source() -> None:
|
||||
repository_root = Path(__file__).resolve().parents[1]
|
||||
registry = LaboratoryEvidenceRegistry.from_directory(
|
||||
repository_root / "config" / "laboratories"
|
||||
)
|
||||
|
||||
assert len(registry.definitions) == 27
|
||||
assert {item.work_id for item in registry.definitions} >= {
|
||||
"e31-source-binding",
|
||||
"e46j-raw-fisheye-realtime",
|
||||
"l34f-adjudicated-reference",
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
|
||||
TELEGRAF_ROOT = REPOSITORY_ROOT / "deploy" / "telemetry-plane" / "telegraf"
|
||||
RECOVERY_ACTIONS = "restart/5000/restart/30000/restart/60000"
|
||||
|
||||
|
||||
def test_install_configures_bounded_service_recovery() -> None:
|
||||
script = (TELEGRAF_ROOT / "Install-NdcMissionCoreTelegraf.ps1").read_text(encoding="utf-8")
|
||||
|
||||
assert "sc.exe failure $serviceName reset= 86400" in script
|
||||
assert RECOVERY_ACTIONS in script
|
||||
assert "sc.exe failureflag $serviceName 1" in script
|
||||
assert "RecoveryConfigured = $true" in script
|
||||
|
||||
|
||||
def test_update_configures_recovery_inside_rollback_boundary() -> None:
|
||||
script = (TELEGRAF_ROOT / "Update-NdcMissionCoreTelegraf.ps1").read_text(encoding="utf-8")
|
||||
|
||||
recovery = script.index("sc.exe failure $serviceName reset= 86400")
|
||||
rollback = script.index("Stop-Service -Name $serviceName -Force", recovery)
|
||||
restore_environment = script.index(
|
||||
"-Type MultiString -Value $serviceEnvironmentBefore",
|
||||
rollback,
|
||||
)
|
||||
restart = script.index("Start-Service -Name $serviceName", restore_environment)
|
||||
assert recovery < rollback < restore_environment < restart
|
||||
assert RECOVERY_ACTIONS in script
|
||||
assert "sc.exe failureflag $serviceName 1" in script
|
||||
assert "RecoveryConfigured = $true" in script
|
||||
Reference in New Issue
Block a user