diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 4f781495c..180a90061 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -422,6 +422,28 @@ jobs:
- name: Run ARSAS application regression tests
run: dotnet test .\ArIED61850Tester\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-build --no-restore --logger "trx;LogFileName=arsas-tests.trx" --results-directory .\ArIED61850Tester\TestResults
+ # Consolidated legacy SV/IO domain evidence verification: the full exact-SHA
+ # ARSAS test run above is the producer; do not rebuild/test two more times.
+ - name: Assert SV and IO domain regression coverage in canonical TRX
+ shell: powershell
+ run: |
+ python .\ArIED61850Tester\scripts\test-ci-canonical-reuse.py
+ if ($LASTEXITCODE -ne 0) { throw 'Canonical CI reuse regression failed.' }
+ $path = '.\ArIED61850Tester\TestResults\arsas-tests.trx'
+ if (-not (Test-Path -LiteralPath $path)) { throw 'Missing canonical ARSAS TRX.' }
+ [xml]$trx = Get-Content -LiteralPath $path -Raw
+ $names = @($trx.TestRun.TestDefinitions.UnitTest | ForEach-Object { [string]$_.name })
+ foreach ($family in @('SmvSnapshotEvidenceExporterTests','IoTest','IoList','IoFat','FatDataSet','FatScl')) {
+ if (-not (@($names | Where-Object { $_.Contains($family) }).Count -gt 0)) {
+ throw "Canonical full regression omitted mandatory domain family: $family"
+ }
+ }
+ $counters = $trx.TestRun.ResultSummary.Counters
+ if ([int]$counters.failed -ne 0 -or [int]$counters.error -ne 0 -or [int]$counters.total -le 0) {
+ throw 'Canonical full regression has failed/error/empty results.'
+ }
+ Write-Host 'SV exporter and IO domain test families verified in exact-SHA full-run TRX.'
+
- name: Write exact-SHA canonical test attestation
shell: powershell
run: |
diff --git a/.github/workflows/smart-discovery-golden-budget-lock.yml b/.github/workflows/smart-discovery-golden-budget-lock.yml
index 083a05b26..7d2aa9298 100644
--- a/.github/workflows/smart-discovery-golden-budget-lock.yml
+++ b/.github/workflows/smart-discovery-golden-budget-lock.yml
@@ -1,5 +1,7 @@
name: Smart Discovery Golden Budget Lock
+# P0-5e provenance guard consolidated below as the same exact-SHA Windows job.
+
on:
pull_request:
paths:
@@ -230,3 +232,144 @@ jobs:
path: ArIED61850Tester\TestResults\P0-5E-*.json
if-no-files-found: warn
retention-days: 14
+
+ - name: Execute P0-5e provenance fixtures
+ shell: powershell
+ run: |
+ $writer = '.\ArIED61850Tester\scripts\new-smart-discovery-golden-lock.ps1'
+ $verifier = '.\ArIED61850Tester\scripts\verify-smart-discovery-golden-lock.ps1'
+ $target = '.\ArIED61850Tester\evidence\smart-discovery-golden-target.json'
+ foreach ($required in @($writer, $verifier, $target)) {
+ if (-not (Test-Path $required -PathType Leaf)) { throw "Missing P0-5e provenance source: $required" }
+ }
+
+ foreach ($script in @($writer, $verifier)) {
+ $tokens = $null
+ $errors = $null
+ [System.Management.Automation.Language.Parser]::ParseFile($script, [ref]$tokens, [ref]$errors) | Out-Null
+ if ($errors.Count -ne 0) { throw "PowerShell parse failure in $script" }
+ }
+
+ $results = '.\ArIED61850Tester\TestResults'
+ New-Item -ItemType Directory -Force $results | Out-Null
+ $capture = Join-Path $results 'p0-5e-provenance-fixture.pcapng'
+ [IO.File]::WriteAllBytes($capture, [Text.Encoding]::UTF8.GetBytes('CI fixture only - never physical authority'))
+
+ $arsasCommit = (git -C .\ArIED61850Tester rev-parse HEAD).Trim().ToLowerInvariant()
+ $engineCommit = '4467124775d8d9d76f3db194f9fbfd97144767a8'
+ $manifest = Join-Path $results 'SMART-CAPTURE-BUILD.txt'
+ @(
+ 'ARSAS smart discovery field-capture build',
+ "ARSAS commit: $arsasCommit",
+ "ARIEC61850 commit: $engineCommit",
+ 'Engine PR: 134'
+ ) | Set-Content $manifest -Encoding utf8
+
+ $proofPath = Join-Path $results 'P0-5D-provenance-source.json'
+ $proof = [ordered]@{
+ SchemaVersion = 1
+ Phase = 'P0-5d'
+ Verdict = 'PASS'
+ AcceptanceFailures = @()
+ ArsasCapture = [ordered]@{
+ Capture = 'fixture'
+ ClientIp = '192.0.2.10'
+ ServerIp = '192.0.2.20'
+ RequestTcpStreams = @('0')
+ ConfirmedRequests = 4
+ ConfirmedResponsesOrErrors = 4
+ ServiceCounts = [ordered]@{
+ GetNameList = 2
+ GetVariableAccessAttributes = 2
+ }
+ DuplicateSemanticRequests = 0
+ DuplicateGetNameListRequests = 0
+ DuplicateGvaRequests = 0
+ DuplicateDetails = @()
+ SecondGetNameListSweepDetected = $false
+ PeakOutstandingRequests = 3
+ NegotiatedMaxOutstandingCalling = 10
+ InvokeIdReuseWhileOutstanding = 0
+ OrphanResponses = 0
+ UnansweredRequestsAtCaptureEnd = 0
+ }
+ }
+ $proof | ConvertTo-Json -Depth 12 | Set-Content $proofPath -Encoding utf8
+
+ $lockPath = Join-Path $results 'P0-5E-provenance.lock.json'
+ & $writer `
+ -ProofJson $proofPath `
+ -CapturePath $capture `
+ -DeviceIdentity 'AA1E1F06R4' `
+ -ArsasCommit $arsasCommit `
+ -EngineCommit $engineCommit `
+ -TargetPath $target `
+ -BuildManifestPath $manifest `
+ -OutputPath $lockPath `
+ -AllowFixtureEvidence
+
+ $lock = Get-Content $lockPath -Raw | ConvertFrom-Json
+ if ($lock.SchemaVersion -ne 2 -or
+ $lock.GoldenSource.BuildManifestSha256 -notmatch '^[0-9a-f]{64}$' -or
+ $lock.GoldenSource.SemanticTargetSha256 -notmatch '^[0-9a-f]{64}$' -or
+ $lock.GoldenSource.RawCaptureReverified) {
+ throw 'Fixture lock did not carry expected P0-5e provenance metadata.'
+ }
+
+ $acceptPath = Join-Path $results 'P0-5E-provenance-pass.json'
+ & $verifier `
+ -LockPath $lockPath `
+ -ProofJson $proofPath `
+ -DeviceIdentity 'AA1E1F06R4' `
+ -CandidateArsasCommit $arsasCommit `
+ -CandidateEngineCommit $engineCommit `
+ -TargetPath $target `
+ -OutputJson $acceptPath
+ $accept = Get-Content $acceptPath -Raw | ConvertFrom-Json
+ if ($accept.Verdict -ne 'PASS') { throw 'Exact build/target fixture should pass the golden provenance lock.' }
+
+ $wrongArsas = '1111111111111111111111111111111111111111'
+ $mismatchPath = Join-Path $results 'P0-5E-provenance-arsas-mismatch.json'
+ & $verifier `
+ -LockPath $lockPath `
+ -ProofJson $proofPath `
+ -DeviceIdentity 'AA1E1F06R4' `
+ -CandidateArsasCommit $wrongArsas `
+ -CandidateEngineCommit $engineCommit `
+ -TargetPath $target `
+ -OutputJson $mismatchPath `
+ -NoFailExit
+ $mismatch = Get-Content $mismatchPath -Raw | ConvertFrom-Json
+ if ($mismatch.Verdict -ne 'FAIL' -or
+ -not (@($mismatch.AcceptanceFailures) -match 'Candidate ARSAS commit differs')) {
+ throw 'P0-5e did not reject ARSAS build identity drift.'
+ }
+
+ $alteredTarget = Join-Path $results 'altered-target.json'
+ $changed = Get-Content $target -Raw | ConvertFrom-Json
+ $changed.SemanticTarget.LogicalNodes = 120
+ $changed | ConvertTo-Json -Depth 12 | Set-Content $alteredTarget -Encoding utf8
+ $targetMismatchPath = Join-Path $results 'P0-5E-provenance-target-mismatch.json'
+ & $verifier `
+ -LockPath $lockPath `
+ -ProofJson $proofPath `
+ -DeviceIdentity 'AA1E1F06R4' `
+ -CandidateArsasCommit $arsasCommit `
+ -CandidateEngineCommit $engineCommit `
+ -TargetPath $alteredTarget `
+ -OutputJson $targetMismatchPath `
+ -NoFailExit
+ $targetMismatch = Get-Content $targetMismatchPath -Raw | ConvertFrom-Json
+ if ($targetMismatch.Verdict -ne 'FAIL' -or
+ -not (@($targetMismatch.AcceptanceFailures) -match 'Semantic target hash differs')) {
+ throw 'P0-5e did not reject semantic-target authority drift.'
+ }
+
+ - name: Upload P0-5e provenance evidence
+ if: always()
+ uses: actions/upload-artifact@v7
+ with:
+ name: ARSAS-p0-5e-golden-provenance-fixtures
+ path: ArIED61850Tester\TestResults\P0-5E-*.json
+ if-no-files-found: warn
+ retention-days: 14
diff --git a/.github/workflows/smart-discovery-golden-provenance.yml b/.github/workflows/smart-discovery-golden-provenance.yml
deleted file mode 100644
index 0b5d62cd0..000000000
--- a/.github/workflows/smart-discovery-golden-provenance.yml
+++ /dev/null
@@ -1,201 +0,0 @@
-name: Smart Discovery Golden Provenance
-
-on:
- pull_request:
- paths:
- - "*.cs"
- - "**/*.cs"
- - "*.csproj"
- - "**/*.csproj"
- - "*.sln"
- - "**/*.sln"
- - "*.props"
- - "**/*.props"
- - "*.targets"
- - "**/*.targets"
- - "global.json"
- - "*.xaml"
- - "**/*.xaml"
- - "app.manifest"
- - "VERSION"
- - ".release/**"
- - ".github/workflows/**"
- - "Assets/**"
- - "Resources/**"
- - "installer/**"
- - "engine-patches/**"
- - "landing/release-notes*.json"
- - "landing/latest.json"
- - "engines/**"
- - "scripts/**"
- - "tests/**"
- - "evidence/**"
- - "docs/INTEROPERABILITY_REFERENCE_CONTRACT.md"
- - ".github/workflows/smart-discovery-*.yml"
- workflow_dispatch:
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
- cancel-in-progress: true
-
-jobs:
- verify-provenance:
- name: Verify P0-5e capture build and target provenance
- runs-on: windows-latest
- steps:
- - name: Checkout exact ARSAS candidate
- uses: actions/checkout@v7
- with:
- path: ArIED61850Tester
- fetch-depth: 1
- persist-credentials: false
-
- - name: Verify exact candidate revision
- shell: powershell
- run: |
- $actual = (git -C .\ArIED61850Tester rev-parse HEAD).Trim().ToLowerInvariant()
- $expected = $env:GITHUB_SHA.Trim().ToLowerInvariant()
- if ($actual -ne $expected) {
- throw "ARSAS source revision mismatch. Expected $expected, checked out $actual."
- }
-
- - name: Execute P0-5e provenance fixtures
- shell: powershell
- run: |
- $writer = '.\ArIED61850Tester\scripts\new-smart-discovery-golden-lock.ps1'
- $verifier = '.\ArIED61850Tester\scripts\verify-smart-discovery-golden-lock.ps1'
- $target = '.\ArIED61850Tester\evidence\smart-discovery-golden-target.json'
- foreach ($required in @($writer, $verifier, $target)) {
- if (-not (Test-Path $required -PathType Leaf)) { throw "Missing P0-5e provenance source: $required" }
- }
-
- foreach ($script in @($writer, $verifier)) {
- $tokens = $null
- $errors = $null
- [System.Management.Automation.Language.Parser]::ParseFile($script, [ref]$tokens, [ref]$errors) | Out-Null
- if ($errors.Count -ne 0) { throw "PowerShell parse failure in $script" }
- }
-
- $results = '.\ArIED61850Tester\TestResults'
- New-Item -ItemType Directory -Force $results | Out-Null
- $capture = Join-Path $results 'p0-5e-provenance-fixture.pcapng'
- [IO.File]::WriteAllBytes($capture, [Text.Encoding]::UTF8.GetBytes('CI fixture only - never physical authority'))
-
- $arsasCommit = (git -C .\ArIED61850Tester rev-parse HEAD).Trim().ToLowerInvariant()
- $engineCommit = '4467124775d8d9d76f3db194f9fbfd97144767a8'
- $manifest = Join-Path $results 'SMART-CAPTURE-BUILD.txt'
- @(
- 'ARSAS smart discovery field-capture build',
- "ARSAS commit: $arsasCommit",
- "ARIEC61850 commit: $engineCommit",
- 'Engine PR: 134'
- ) | Set-Content $manifest -Encoding utf8
-
- $proofPath = Join-Path $results 'P0-5D-provenance-source.json'
- $proof = [ordered]@{
- SchemaVersion = 1
- Phase = 'P0-5d'
- Verdict = 'PASS'
- AcceptanceFailures = @()
- ArsasCapture = [ordered]@{
- Capture = 'fixture'
- ClientIp = '192.0.2.10'
- ServerIp = '192.0.2.20'
- RequestTcpStreams = @('0')
- ConfirmedRequests = 4
- ConfirmedResponsesOrErrors = 4
- ServiceCounts = [ordered]@{
- GetNameList = 2
- GetVariableAccessAttributes = 2
- }
- DuplicateSemanticRequests = 0
- DuplicateGetNameListRequests = 0
- DuplicateGvaRequests = 0
- DuplicateDetails = @()
- SecondGetNameListSweepDetected = $false
- PeakOutstandingRequests = 3
- NegotiatedMaxOutstandingCalling = 10
- InvokeIdReuseWhileOutstanding = 0
- OrphanResponses = 0
- UnansweredRequestsAtCaptureEnd = 0
- }
- }
- $proof | ConvertTo-Json -Depth 12 | Set-Content $proofPath -Encoding utf8
-
- $lockPath = Join-Path $results 'P0-5E-provenance.lock.json'
- & $writer `
- -ProofJson $proofPath `
- -CapturePath $capture `
- -DeviceIdentity 'AA1E1F06R4' `
- -ArsasCommit $arsasCommit `
- -EngineCommit $engineCommit `
- -TargetPath $target `
- -BuildManifestPath $manifest `
- -OutputPath $lockPath `
- -AllowFixtureEvidence
-
- $lock = Get-Content $lockPath -Raw | ConvertFrom-Json
- if ($lock.SchemaVersion -ne 2 -or
- $lock.GoldenSource.BuildManifestSha256 -notmatch '^[0-9a-f]{64}$' -or
- $lock.GoldenSource.SemanticTargetSha256 -notmatch '^[0-9a-f]{64}$' -or
- $lock.GoldenSource.RawCaptureReverified) {
- throw 'Fixture lock did not carry expected P0-5e provenance metadata.'
- }
-
- $acceptPath = Join-Path $results 'P0-5E-provenance-pass.json'
- & $verifier `
- -LockPath $lockPath `
- -ProofJson $proofPath `
- -DeviceIdentity 'AA1E1F06R4' `
- -CandidateArsasCommit $arsasCommit `
- -CandidateEngineCommit $engineCommit `
- -TargetPath $target `
- -OutputJson $acceptPath
- $accept = Get-Content $acceptPath -Raw | ConvertFrom-Json
- if ($accept.Verdict -ne 'PASS') { throw 'Exact build/target fixture should pass the golden provenance lock.' }
-
- $wrongArsas = '1111111111111111111111111111111111111111'
- $mismatchPath = Join-Path $results 'P0-5E-provenance-arsas-mismatch.json'
- & $verifier `
- -LockPath $lockPath `
- -ProofJson $proofPath `
- -DeviceIdentity 'AA1E1F06R4' `
- -CandidateArsasCommit $wrongArsas `
- -CandidateEngineCommit $engineCommit `
- -TargetPath $target `
- -OutputJson $mismatchPath `
- -NoFailExit
- $mismatch = Get-Content $mismatchPath -Raw | ConvertFrom-Json
- if ($mismatch.Verdict -ne 'FAIL' -or
- -not (@($mismatch.AcceptanceFailures) -match 'Candidate ARSAS commit differs')) {
- throw 'P0-5e did not reject ARSAS build identity drift.'
- }
-
- $alteredTarget = Join-Path $results 'altered-target.json'
- $changed = Get-Content $target -Raw | ConvertFrom-Json
- $changed.SemanticTarget.LogicalNodes = 120
- $changed | ConvertTo-Json -Depth 12 | Set-Content $alteredTarget -Encoding utf8
- $targetMismatchPath = Join-Path $results 'P0-5E-provenance-target-mismatch.json'
- & $verifier `
- -LockPath $lockPath `
- -ProofJson $proofPath `
- -DeviceIdentity 'AA1E1F06R4' `
- -CandidateArsasCommit $arsasCommit `
- -CandidateEngineCommit $engineCommit `
- -TargetPath $alteredTarget `
- -OutputJson $targetMismatchPath `
- -NoFailExit
- $targetMismatch = Get-Content $targetMismatchPath -Raw | ConvertFrom-Json
- if ($targetMismatch.Verdict -ne 'FAIL' -or
- -not (@($targetMismatch.AcceptanceFailures) -match 'Semantic target hash differs')) {
- throw 'P0-5e did not reject semantic-target authority drift.'
- }
-
- - name: Upload P0-5e provenance evidence
- if: always()
- uses: actions/upload-artifact@v7
- with:
- name: ARSAS-p0-5e-golden-provenance-fixtures
- path: ArIED61850Tester\TestResults\P0-5E-*.json
- if-no-files-found: warn
- retention-days: 14
diff --git a/.github/workflows/smv-direct-inno-preview.yml b/.github/workflows/smv-direct-inno-preview.yml
index 5b6e30fcd..57e97d2c8 100644
--- a/.github/workflows/smv-direct-inno-preview.yml
+++ b/.github/workflows/smv-direct-inno-preview.yml
@@ -4,7 +4,7 @@ name: Direct Inno SMV Preview EXE
# Trusted, narrowly scoped branch only. Stable release is never modified.
on:
push:
- branches: [feat/smv-sharp-latest-p0-20261022]
+ branches: [feat/smv-sharp-latest-p0-20261022, fix/smv-arstack-p0-integrated-20261022]
permissions:
contents: write
@@ -15,7 +15,7 @@ concurrency:
jobs:
direct-preview:
- if: github.repository == 'masarray/arsas' && github.ref == 'refs/heads/feat/smv-sharp-latest-p0-20261022'
+ if: github.repository == 'masarray/arsas' && (github.ref == 'refs/heads/feat/smv-sharp-latest-p0-20261022' || github.ref == 'refs/heads/fix/smv-arstack-p0-integrated-20261022')
runs-on: windows-latest
timeout-minutes: 65
defaults:
diff --git a/.github/workflows/validate-io-testing.yml b/.github/workflows/validate-io-testing.yml
deleted file mode 100644
index c63609462..000000000
--- a/.github/workflows/validate-io-testing.yml
+++ /dev/null
@@ -1,142 +0,0 @@
-name: Validate IO List Testing
-
-on:
- pull_request:
- paths:
- - "MainWindow.IoTesting.cs"
- - "IoListTestingWindow.xaml"
- - "IoListTestingWindow.xaml.cs"
- - "Models/IoTesting/**"
- - "Services/IoTesting/**"
- - "tests/ARSAS.Tests/IoTest*"
- - "tests/ARSAS.Tests/IoList*"
- - "tests/ARSAS.Tests/IoFat*"
- - "tests/ARSAS.Tests/FatDataSet*"
- - "tests/ARSAS.Tests/FatScl*"
- - "engines/ARIEC61850.lock.json"
- - ".github/workflows/validate-io-testing.yml"
- - "scripts/verify-ci-canonical-reuse.py"
- - "scripts/test-ci-canonical-reuse.py"
- workflow_dispatch:
-
-permissions:
- contents: read
- actions: read
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
- cancel-in-progress: true
-
-jobs:
- validate-io-testing:
- name: Build and test IO List Testing
- runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'windows-latest' }}
-
- steps:
- - name: Checkout exact ARSAS application revision
- uses: actions/checkout@v7
- with:
- path: ARSAS
- ref: ${{ github.sha }}
- persist-credentials: false
- fetch-depth: 1
-
- - id: engine
- name: Resolve exact immutable ARIEC61850 engine identity
- uses: ./ARSAS/.github/actions/resolve-ariec-engine
- with:
- lock-path: ARSAS/engines/ARIEC61850.lock.json
-
- - name: Run offline canonical evidence safety fixtures
- if: github.event_name == 'pull_request'
- run: python3 ARSAS/scripts/test-ci-canonical-reuse.py
-
- - name: Prove exact canonical IO domain tests passed
- if: github.event_name == 'pull_request'
- shell: bash
- env:
- GITHUB_TOKEN: ${{ github.token }}
- PR_HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
- PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
- PR_MERGE_SHA: ${{ github.sha }}
- ENGINE_SHA: ${{ steps.engine.outputs.commit }}
- run: |
- set -euo pipefail
- mkdir -p ARSAS/TestResults
- python3 ARSAS/scripts/verify-ci-canonical-reuse.py \
- --repository "$GITHUB_REPOSITORY" \
- --head-branch "$PR_HEAD_BRANCH" \
- --head-sha "$PR_HEAD_SHA" \
- --merge-sha "$PR_MERGE_SHA" \
- --engine-sha "$ENGINE_SHA" \
- --wait-seconds 960 \
- --allow-in-progress-artifact \
- --require-test-substring IoTest \
- --require-test-substring IoList \
- --require-test-substring IoFat \
- --require-test-substring FatDataSet \
- --require-test-substring FatScl \
- --output ARSAS/TestResults/io-canonical-domain-proof.json
-
- - name: Checkout immutable ARIEC61850 engine revision for manual diagnostics
- if: github.event_name == 'workflow_dispatch'
- shell: powershell
- env:
- ENGINE_REPOSITORY: ${{ steps.engine.outputs.repository }}
- ENGINE_COMMIT: ${{ steps.engine.outputs.commit }}
- run: |
- git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:ENGINE_REPOSITORY.git" ARIEC61850
- if ($LASTEXITCODE -ne 0) { throw 'Engine clone failed.' }
- git -C .\ARIEC61850 fetch --quiet --depth 1 origin $env:ENGINE_COMMIT
- if ($LASTEXITCODE -ne 0) { throw 'Pinned engine fetch failed.' }
- git -C .\ARIEC61850 checkout --quiet --detach $env:ENGINE_COMMIT
- if ($LASTEXITCODE -ne 0) { throw 'Pinned engine checkout failed.' }
- $actual = (git -C .\ARIEC61850 rev-parse HEAD).Trim().ToLowerInvariant()
- if ($actual -cne $env:ENGINE_COMMIT) {
- throw "ARIEC61850 pin mismatch. Expected $env:ENGINE_COMMIT, got $actual."
- }
-
- - name: Setup .NET 8 for manual diagnostics
- if: github.event_name == 'workflow_dispatch'
- uses: actions/setup-dotnet@v6
- with:
- dotnet-version: 8.0.x
-
- - name: Restore focused test project
- if: github.event_name == 'workflow_dispatch'
- run: dotnet restore .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj
-
- - name: Build focused IO testing project
- if: github.event_name == 'workflow_dispatch'
- shell: powershell
- run: |
- $log = ".\ARSAS\io-testing-build.log"
- dotnet build .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-restore *> $log
- $buildExitCode = $LASTEXITCODE
- if ($buildExitCode -ne 0) {
- Write-Host "=== IO Testing compiler errors ===" -ForegroundColor Red
- $errors = @(Select-String -Path $log -Pattern ': error ')
- if ($errors.Count -eq 0) {
- Get-Content $log -Tail 100 | ForEach-Object { Write-Host $_ -ForegroundColor Red }
- }
- else {
- $errors | ForEach-Object { Write-Host $_.Line -ForegroundColor Red }
- }
- exit $buildExitCode
- }
- Get-Content $log -Tail 12
-
- - name: Run IO testing regression tests
- if: github.event_name == 'workflow_dispatch'
- run: dotnet test .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-build --no-restore --filter "FullyQualifiedName~IoTest|FullyQualifiedName~IoList|FullyQualifiedName~IoFat|FullyQualifiedName~FatDataSet|FullyQualifiedName~FatScl" --logger "trx;LogFileName=io-testing-tests.trx" --results-directory .\ARSAS\TestResults
-
- - name: Upload IO validation evidence
- if: always()
- uses: actions/upload-artifact@v7
- with:
- name: ARSAS-io-testing-test-results
- path: |
- ARSAS/TestResults/io-canonical-domain-proof.json
- ARSAS/TestResults/*.trx
- ARSAS/io-testing-build.log
- if-no-files-found: warn
diff --git a/.github/workflows/validate-sv-evidence.yml b/.github/workflows/validate-sv-evidence.yml
deleted file mode 100644
index 4e4311c44..000000000
--- a/.github/workflows/validate-sv-evidence.yml
+++ /dev/null
@@ -1,133 +0,0 @@
-name: Validate SV evidence bundles
-
-on:
- pull_request:
- paths:
- - "SmvViewerWindow.xaml"
- - "Services/SmvViewerWindow.P1Evidence.cs"
- - "Services/SmvSnapshotEvidenceExporter.cs"
- - "Services/SmvSnapshotCaptureService.cs"
- - "tests/ARSAS.Tests/**"
- - "engines/ARIEC61850.lock.json"
- - ".github/workflows/validate-sv-evidence.yml"
- - "scripts/verify-ci-canonical-reuse.py"
- - "scripts/test-ci-canonical-reuse.py"
- workflow_dispatch:
-
-permissions:
- contents: read
- actions: read
-
-concurrency:
- group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
- cancel-in-progress: true
-
-jobs:
- validate-evidence:
- name: Build and test deterministic SV evidence
- runs-on: ${{ github.event_name == 'pull_request' && 'ubuntu-latest' || 'windows-latest' }}
-
- steps:
- - name: Checkout exact ARSAS application revision
- uses: actions/checkout@v7
- with:
- path: ARSAS
- ref: ${{ github.sha }}
- persist-credentials: false
- fetch-depth: 1
-
- - id: engine
- name: Resolve exact immutable ARIEC61850 engine identity
- uses: ./ARSAS/.github/actions/resolve-ariec-engine
- with:
- lock-path: ARSAS/engines/ARIEC61850.lock.json
-
- - name: Run offline canonical evidence safety fixtures
- if: github.event_name == 'pull_request'
- run: python3 ARSAS/scripts/test-ci-canonical-reuse.py
-
- - name: Prove exact canonical SV evidence tests passed
- if: github.event_name == 'pull_request'
- shell: bash
- env:
- GITHUB_TOKEN: ${{ github.token }}
- PR_HEAD_BRANCH: ${{ github.event.pull_request.head.ref }}
- PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
- PR_MERGE_SHA: ${{ github.sha }}
- ENGINE_SHA: ${{ steps.engine.outputs.commit }}
- run: |
- set -euo pipefail
- mkdir -p ARSAS/TestResults
- python3 ARSAS/scripts/verify-ci-canonical-reuse.py \
- --repository "$GITHUB_REPOSITORY" \
- --head-branch "$PR_HEAD_BRANCH" \
- --head-sha "$PR_HEAD_SHA" \
- --merge-sha "$PR_MERGE_SHA" \
- --engine-sha "$ENGINE_SHA" \
- --wait-seconds 960 \
- --allow-in-progress-artifact \
- --require-test-substring SmvSnapshotEvidenceExporterTests \
- --output ARSAS/TestResults/sv-canonical-domain-proof.json
-
- - name: Checkout immutable ARIEC61850 engine revision for manual diagnostics
- if: github.event_name == 'workflow_dispatch'
- shell: powershell
- env:
- ENGINE_REPOSITORY: ${{ steps.engine.outputs.repository }}
- ENGINE_COMMIT: ${{ steps.engine.outputs.commit }}
- run: |
- git clone --quiet --filter=blob:none --no-checkout "https://github.com/$env:ENGINE_REPOSITORY.git" ARIEC61850
- if ($LASTEXITCODE -ne 0) { throw 'Engine clone failed.' }
- git -C .\ARIEC61850 fetch --quiet --depth 1 origin $env:ENGINE_COMMIT
- if ($LASTEXITCODE -ne 0) { throw 'Pinned engine fetch failed.' }
- git -C .\ARIEC61850 checkout --quiet --detach $env:ENGINE_COMMIT
- if ($LASTEXITCODE -ne 0) { throw 'Pinned engine checkout failed.' }
- $actual = (git -C .\ARIEC61850 rev-parse HEAD).Trim().ToLowerInvariant()
- if ($actual -cne $env:ENGINE_COMMIT) {
- throw "ARIEC61850 pin mismatch. Expected $env:ENGINE_COMMIT, got $actual."
- }
-
- - name: Setup .NET 8 for manual diagnostics
- if: github.event_name == 'workflow_dispatch'
- uses: actions/setup-dotnet@v6
- with:
- dotnet-version: 8.0.x
-
- - name: Restore focused test project
- if: github.event_name == 'workflow_dispatch'
- run: dotnet restore .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj
-
- - name: Build focused test project
- if: github.event_name == 'workflow_dispatch'
- shell: powershell
- run: |
- $log = ".\ARSAS\sv-evidence-build.log"
- dotnet build .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-restore *> $log
- $buildExitCode = $LASTEXITCODE
- if ($buildExitCode -ne 0) {
- Write-Host "=== Focused compiler errors ===" -ForegroundColor Red
- $errors = @(Select-String -Path $log -Pattern ': error ')
- if ($errors.Count -eq 0) {
- Get-Content $log -Tail 80 | ForEach-Object { Write-Host $_ -ForegroundColor Red }
- }
- else {
- $errors | ForEach-Object { Write-Host $_.Line -ForegroundColor Red }
- }
- exit $buildExitCode
- }
- Get-Content $log -Tail 12
-
- - name: Run SV evidence regression tests
- if: github.event_name == 'workflow_dispatch'
- run: dotnet test .\ARSAS\tests\ARSAS.Tests\ARSAS.Tests.csproj -c Release --no-build --no-restore --filter "FullyQualifiedName~SmvSnapshotEvidenceExporterTests" --logger "trx;LogFileName=sv-evidence-tests.trx" --results-directory .\ARSAS\TestResults
-
- - name: Upload SV validation evidence
- if: always()
- uses: actions/upload-artifact@v7
- with:
- name: ARSAS-sv-evidence-test-results
- path: |
- ARSAS/TestResults/sv-canonical-domain-proof.json
- ARSAS/TestResults/*.trx
- ARSAS/sv-evidence-build.log
- if-no-files-found: warn
diff --git a/Services/SmvLiveCaptureService.cs b/Services/SmvLiveCaptureService.cs
index 4aaa0b19f..103e85c10 100644
--- a/Services/SmvLiveCaptureService.cs
+++ b/Services/SmvLiveCaptureService.cs
@@ -236,12 +236,15 @@ public void Receive(SampledValuesFrame frame, SampledValueAsdu asdu,
}
if (_wordIndexes.Length == 0 || _payloadLength != payload.Length ||
- _confRev != asdu.ConfigurationRevision || _smpSynch != asdu.SampleSynchronization ||
+ _confRev != asdu.ConfigurationRevision ||
!DataSetReference.Equals(asdu.DataSetReference, StringComparison.Ordinal))
{
if (_wordIndexes.Length > 0) _restarts++;
Configure(asdu, payload.Length);
}
+ // Synchronization is ASDU status, not an SV stream schema transition.
+ // AUTO 0->2 must preserve its measured timebase, counter and waveform ring.
+ _smpSynch = asdu.SampleSynchronization;
if (_timebase?.IsResolved != true || _length == 0)
{
diff --git a/scripts/test-ci-workflows.py b/scripts/test-ci-workflows.py
index 1cb6bf9d9..b8acc4773 100644
--- a/scripts/test-ci-workflows.py
+++ b/scripts/test-ci-workflows.py
@@ -170,10 +170,7 @@ def test_p3h_windows_pr_workflows_cancel_only_superseded_pr_runs(self):
".github/workflows/installer-windows.yml",
".github/workflows/smart-discovery-mainline-readiness.yml",
".github/workflows/rcb-export-guard.yml",
- ".github/workflows/validate-sv-evidence.yml",
- ".github/workflows/validate-io-testing.yml",
".github/workflows/comtrade-viewer-integration.yml",
- ".github/workflows/smart-discovery-golden-provenance.yml",
".github/workflows/smart-discovery-golden-budget-lock.yml",
".github/workflows/smart-discovery-repeat-run-stability.yml",
]
@@ -188,6 +185,26 @@ def test_p3h_windows_pr_workflows_cancel_only_superseded_pr_runs(self):
self.assertIn(expected_group, source, relative)
self.assertIn(expected_cancel, source, relative)
+ # Removed specialized SV/IO workflows are consolidated into the
+ # exact-SHA full regression, not simply dropped from acceptance.
+ canonical = (repo / ".github/workflows/build.yml").read_text(
+ encoding="utf-8"
+ )
+ for family in (
+ "SmvSnapshotEvidenceExporterTests", "IoTest", "IoList",
+ "IoFat", "FatDataSet", "FatScl",
+ ):
+ self.assertIn(family, canonical)
+ self.assertIn("Assert SV and IO domain regression coverage", canonical)
+
+ # The merged P0-5e job keeps BOTH formerly independent fixture lanes.
+ golden = (repo / ".github/workflows/smart-discovery-golden-budget-lock.yml").read_text(
+ encoding="utf-8"
+ )
+ self.assertIn("Execute P0-5e golden budget fixtures", golden)
+ self.assertIn("Execute P0-5e provenance fixtures", golden)
+ self.assertIn("ARSAS-p0-5e-golden-provenance-fixtures", golden)
+
release = (repo / ".github/workflows/release-windows.yml").read_text(
encoding="utf-8"
)
diff --git a/tests/ARSAS.Tests/SmvArstackCapturedEthernetInteropTests.cs b/tests/ARSAS.Tests/SmvArstackCapturedEthernetInteropTests.cs
new file mode 100644
index 000000000..6fdc27888
--- /dev/null
+++ b/tests/ARSAS.Tests/SmvArstackCapturedEthernetInteropTests.cs
@@ -0,0 +1,124 @@
+using System.Buffers.Binary;
+using AR.Iec61850.SampledValues;
+using ArIED61850Tester.Services;
+
+namespace ARSAS.Tests;
+
+///
+/// Packet prefixes, SV BER, payload and trailing tag 0x89 come from real
+/// ARStack ESP32-P4 Ethernet PCAP ARStack_SV_20261022_020528_781.pcap.
+/// The live replay below varies smpCnt/timestamps/values deterministically
+/// while preserving exactly the captured 802.1Q + 9-2 BER wire shape.
+/// This is not an ARSVIN/KM loopback encoder test.
+///
+public sealed class SmvArstackCapturedEthernetInteropTests
+{
+ // Exact 138-byte real frame, smpCnt=0, initial smpSynch=0.
+ private const string CapturedSourceAcquisitionFrame =
+ "010ccd040001e8f60ae0c53f8100800088ba4000007800000000606e800101a2693067800c4152535441434b5f53563031820200008304000000018501008740ffffff7c00000000fffffa2600000000000005850000000000000000000000000000000000000000ffffe3510000000000000e5a0000000000000000000000008908e8f60afffee0c53f";
+
+ // Exact 138-byte real frame after SOURCE phase ACQUIRED, smpSynch=2.
+ private const string CapturedGloballySynchronizedFrame =
+ "010ccd040001e8f60ae0c53f8100800088ba4000007800000000606e800101a2693067800c4152535441434b5f53563031820200008304000000018501028740ffffff7c00000000fffffa260000000000000585000000000000000000000000fffffffa00000000ffffe3510000000000000e5a0000000000000000000000008908e8f60afffee0c53f";
+
+ private static readonly SmvLiveCaptureOptions Settings = new()
+ {
+ AdapterSelector = "captured-arstack-8021q-pcp4-vid0",
+ NominalFrequencyHz = 50,
+ Cycles = 2,
+ MaximumChannels = 8
+ };
+
+ [Theory]
+ [InlineData(false, (byte)0)]
+ [InlineData(true, (byte)2)]
+ public void CapturedArstackRawEthernet_DecodesPriorityVlanGmIdAndOptionalMetadata(
+ bool acquired, byte expectedSync)
+ {
+ var bytes = Convert.FromHexString(acquired
+ ? CapturedGloballySynchronizedFrame : CapturedSourceAcquisitionFrame);
+ Assert.Equal(138, bytes.Length);
+ Assert.Equal(0x8100, BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(12, 2)));
+ Assert.Equal(0x8000, BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(14, 2)));
+ Assert.Equal(0x88BA, BinaryPrimitives.ReadUInt16BigEndian(bytes.AsSpan(16, 2)));
+ Assert.Equal("8908e8f60afffee0c53f",
+ Convert.ToHexString(bytes.AsSpan(bytes.Length - 10)).ToLowerInvariant());
+
+ Assert.True(SampledValuesFrameParser.TryParseEthernetFrame(bytes, out var frame));
+ Assert.Equal(0x4000, frame.AppId);
+ Assert.Equal("0", frame.Vlan?.VlanId.ToString());
+ var asdu = Assert.Single(frame.Pdu.Asdus);
+ Assert.Equal("ARSTACK_SV01", asdu.SvId);
+ Assert.Equal((ushort)0, asdu.SampleCount);
+ Assert.Equal(expectedSync, asdu.SampleSynchronization);
+ Assert.Equal(64, asdu.SamplePayload.Length);
+ Assert.Null(asdu.SampleRate);
+ Assert.Null(asdu.SampleMode);
+ }
+
+ [Fact]
+ public void CapturedArstackWireShape_AutoSync0To2_DoesNotEraseWaveformOrTimebase()
+ {
+ var raw = Convert.FromHexString(CapturedSourceAcquisitionFrame);
+ var catalog = new SmvLiveStreamCatalog(Settings);
+ var start = DateTimeOffset.FromUnixTimeSeconds(1792634731);
+ string? streamKey = null;
+ long snapshotsBeforeLock = 0;
+
+ for (var n = 0; n < 1200; n++)
+ {
+ // Modify only the captured smpCnt, smpSynch, and payload instMag.
+ // Every other byte, including actual 802.1Q, BER and gmIdentity,
+ // is preserved from the captured ARStack ESP32-P4 frame.
+ BinaryPrimitives.WriteUInt16BigEndian(raw.AsSpan(51, 2),
+ (ushort)(n % 4000));
+ raw[61] = n >= 600 ? (byte)2 : (byte)0;
+ for (var channel = 0; channel < 8; channel++)
+ BinaryPrimitives.WriteInt32BigEndian(
+ raw.AsSpan(64 + 8 * channel, 4),
+ (int)(2000 * Math.Sin(2 * Math.PI * n / 80.0 +
+ channel * Math.PI / 4)));
+
+ Assert.True(SampledValuesFrameParser.TryParseEthernetFrame(raw, out var frame));
+ // 4 kHz observed capture with bounded +-60us NIC arrival jitter.
+ // Never assume the counter rate merely from operator frequency.
+ var jitter = ((n * 37) % 1200) - 600;
+ var timestamp = start.AddTicks(n * 2500L + jitter);
+ catalog.Receive(frame, timestamp);
+ if (n == 599)
+ {
+ var before = catalog.Publish(null, timestamp);
+ streamKey = Assert.Single(before.Streams).Key;
+ Assert.True(Assert.Single(before.Streams).HasCoherentWindow);
+ Assert.NotNull(before.SelectedSnapshot);
+ snapshotsBeforeLock++;
+ }
+ }
+
+ Assert.Equal(1, snapshotsBeforeLock);
+ var result = catalog.Publish(streamKey, start.AddMilliseconds(300));
+ var summary = Assert.Single(result.Streams);
+ Assert.Equal(1200, summary.Frames);
+ Assert.Equal(1200, summary.Asdus);
+ Assert.Equal(0, summary.Gaps);
+ Assert.Equal(0, summary.Missing);
+ Assert.Equal(0, summary.Duplicates);
+ Assert.Equal(0, summary.OutOfOrder);
+ Assert.Equal(0, summary.Restarts); // smpSynch transition is NOT restart
+ Assert.True(summary.HasCoherentWindow);
+ Assert.Equal("0", summary.Vlan);
+
+ var proof = Assert.IsType(result.SelectedSnapshot);
+ Assert.Equal("ARSTACK_SV01", proof.StreamId);
+ Assert.Equal(0x4000, proof.AppId);
+ Assert.Equal(80, proof.SamplesPerCycle);
+ Assert.Equal(160, proof.CapturedSamples);
+ Assert.Equal(8, proof.Channels.Count);
+ Assert.Equal((byte)2, proof.SampleSynchronization);
+ Assert.Null(proof.DeclaredSampleRate);
+ Assert.Null(proof.DeclaredSampleMode);
+ Assert.True(proof.IsCleanProof);
+ Assert.Contains("capture rate", proof.TimebaseReason);
+ Assert.True(proof.Channels[0].Maximum > proof.Channels[0].Minimum);
+ }
+}