Add negative Android provenance checks and short tap exercise
This commit is contained in:
@@ -17,6 +17,8 @@ pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidSmoke -Milestone FirstSlice
|
|||||||
|
|
||||||
`compare-audio.ps1`은 승인 원본과 실제 PCM을 mono PCM16 24kHz로 맞추고 가장 강한 구간의 위치를 찾은 후 전체 발화의 정규화 상관을 평가합니다. 기준은 상관 ≥0.95, RMS >32, 비무음 샘플 >100입니다. 세 발화의 시작/끝이 차례로 분리되어야 합니다. APK 생성 출처는 원본 `androidbuild-evidence.json`과 `apk-metadata.json`을 유지하고, Smoke의 재검사는 `apk-inspection.json`에만 씁니다. 시작/종료 dirty=false와 소스 SHA·APK hash를 대조하므로 이전 APK를 후속 커밋 산출물로 다시 이름 붙일 수 없습니다. 잘못된 SHA/hash 검사 복사본은 실제 기기 조작 전 실패해야 합니다.
|
`compare-audio.ps1`은 승인 원본과 실제 PCM을 mono PCM16 24kHz로 맞추고 가장 강한 구간의 위치를 찾은 후 전체 발화의 정규화 상관을 평가합니다. 기준은 상관 ≥0.95, RMS >32, 비무음 샘플 >100입니다. 세 발화의 시작/끝이 차례로 분리되어야 합니다. APK 생성 출처는 원본 `androidbuild-evidence.json`과 `apk-metadata.json`을 유지하고, Smoke의 재검사는 `apk-inspection.json`에만 씁니다. 시작/종료 dirty=false와 소스 SHA·APK hash를 대조하므로 이전 APK를 후속 커밋 산출물로 다시 이름 붙일 수 없습니다. 잘못된 SHA/hash 검사 복사본은 실제 기기 조작 전 실패해야 합니다.
|
||||||
|
|
||||||
|
그 거부 동작은 실제 Build 뒤 `pwsh -NoProfile -File tools/android/test-provenance.ps1`로 검사합니다. 원본 근거를 손상시키지 않고 별도 복사본 두 개의 소스 SHA/APK hash만 바꿔 Smoke의 실제 종료 1 및 기기 명령 0개를 확인합니다. 음성은 raw WAV뿐 아니라 수집 당시 소스 SHA/dirty·시각 메타데이터도 최종 Build와 맞아야 합니다. 입력 수정 전 ADB 짧은 `input tap` 두 번은 구 탭을 놓쳤고 같은 좌표 200ms 접촉은 정상 반응했습니다. 수정 전 원시 자료는 `pre-fix/`에 보존하고 수정 후 최종 절차는 짧은 ADB tap으로 다시 확인합니다. 물리 기기의 실제 손가락 동작이나 성능 합격으로 확대하지 않습니다.
|
||||||
|
|
||||||
APK는 `artifacts/android/FirstSlice/MagnetismHeroes.apk`, 원본 경로를 기록한 Unity 실제 packed-assets 목록은 APK 옆 `.packed-assets.txt`입니다. 근거는 `artifacts/verification/FirstSlice/`에 보존합니다. Build/Smoke의 명령·실제 종료 코드·소스 SHA/dirty·APK SHA-256, aapt 실제 badging/ZIP entries, 기기 ABI/번역기/graphics, ADB 명령별 종료 코드·화면·관찰 결과를 별도로 기록합니다. 설치·프로세스 생존만으로 게임 검사를 통과시키지 않습니다.
|
APK는 `artifacts/android/FirstSlice/MagnetismHeroes.apk`, 원본 경로를 기록한 Unity 실제 packed-assets 목록은 APK 옆 `.packed-assets.txt`입니다. 근거는 `artifacts/verification/FirstSlice/`에 보존합니다. Build/Smoke의 명령·실제 종료 코드·소스 SHA/dirty·APK SHA-256, aapt 실제 badging/ZIP entries, 기기 ABI/번역기/graphics, ADB 명령별 종료 코드·화면·관찰 결과를 별도로 기록합니다. 설치·프로세스 생존만으로 게임 검사를 통과시키지 않습니다.
|
||||||
|
|
||||||
`tools/android/observe.ps1`은 실제 ADB 조작 후 screencap을 가져오고 각 명령을 `<step>-commands.json`에 기록합니다. Unity uGUI 문자는 Android native hierarchy에 노출되지 않으므로 PNG를 직접 열어 한국어와 화면 의미를 검사합니다. `android-observations.json`에는 아래 번호별 절차·기대와 실제 결과·근거 파일을 작성하며 APK hash·소스 SHA·기기 serial이 맞아야 합니다. 미실행·빈 파일·관찰 누락은 Smoke 실패입니다. 원본 상태는 앱 전용 저장 JSON을 ADB로 읽을 수 있을 때 추가 검증하며 release APK에 진단 서버·단계 우회·테스트 제어 기능을 넣지 않습니다.
|
`tools/android/observe.ps1`은 실제 ADB 조작 후 screencap을 가져오고 각 명령을 `<step>-commands.json`에 기록합니다. Unity uGUI 문자는 Android native hierarchy에 노출되지 않으므로 PNG를 직접 열어 한국어와 화면 의미를 검사합니다. `android-observations.json`에는 아래 번호별 절차·기대와 실제 결과·근거 파일을 작성하며 APK hash·소스 SHA·기기 serial이 맞아야 합니다. 미실행·빈 파일·관찰 누락은 Smoke 실패입니다. 원본 상태는 앱 전용 저장 JSON을 ADB로 읽을 수 있을 때 추가 검증하며 release APK에 진단 서버·단계 우회·테스트 제어 기능을 넣지 않습니다.
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ try {
|
|||||||
Step 'final-02-selection' @('shell','input','tap','1200','616')
|
Step 'final-02-selection' @('shell','input','tap','1200','616')
|
||||||
Start-Audio 'audio-online-guidecollect' 8
|
Start-Audio 'audio-online-guidecollect' 8
|
||||||
Step 'final-03-stage' @('shell','input','tap','1200','560') 2500
|
Step 'final-03-stage' @('shell','input','tap','1200','560') 2500
|
||||||
Step 'final-04-precollect-tap' @('shell','input','swipe','930','510','930','510','200')
|
Step 'final-04-precollect-tap' @('shell','input','tap','930','510')
|
||||||
$uncollected=Save-State 'final-04-uncollected'
|
$uncollected=Save-State 'final-04-uncollected'
|
||||||
Assert-State 'direct-collection-required' ($uncollected.puzzleStates[0].collectedBallIds.Count -eq 0 -and $uncollected.puzzleStates[0].countedBallIds.Count -eq 0 -and $uncollected.completedStageIds.Count -eq 0) ($uncollected | ConvertTo-Json -Depth 8 -Compress)
|
Assert-State 'direct-collection-required' ($uncollected.puzzleStates[0].collectedBallIds.Count -eq 0 -and $uncollected.puzzleStates[0].countedBallIds.Count -eq 0 -and $uncollected.completedStageIds.Count -eq 0) ($uncollected | ConvertTo-Json -Depth 8 -Compress)
|
||||||
Start-Audio 'audio-online-guidecount' 8
|
Start-Audio 'audio-online-guidecount' 8
|
||||||
@@ -53,7 +53,7 @@ try {
|
|||||||
$after=Save-State 'final-07-incomplete-after'
|
$after=Save-State 'final-07-incomplete-after'
|
||||||
Assert-State 'incomplete-json-restored' (($before | ConvertTo-Json -Depth 8 -Compress) -eq ($after | ConvertTo-Json -Depth 8 -Compress)) '배치/카메라/그룹/수집/빈 계수 전체 JSON 동일'
|
Assert-State 'incomplete-json-restored' (($before | ConvertTo-Json -Depth 8 -Compress) -eq ($after | ConvertTo-Json -Depth 8 -Compress)) '배치/카메라/그룹/수집/빈 계수 전체 JSON 동일'
|
||||||
Start-Audio 'audio-online-count01' 8
|
Start-Audio 'audio-online-count01' 8
|
||||||
Step 'final-08-success-same-board' @('shell','input','swipe','1182','514','1182','514','200') 1500
|
Step 'final-08-success-same-board' @('shell','input','tap','1182','514') 1500
|
||||||
$success=Save-State 'final-08-success'
|
$success=Save-State 'final-08-success'
|
||||||
Assert-State 'completion-atomic-state' ($success.completedStageIds.Count -eq 1 -and $success.completedStageIds[0] -eq 1 -and $success.puzzleStates.Count -eq 0 -and $success.latestFreePlay.sourceStageId -eq 1) ($success | ConvertTo-Json -Depth 8 -Compress)
|
Assert-State 'completion-atomic-state' ($success.completedStageIds.Count -eq 1 -and $success.completedStageIds[0] -eq 1 -and $success.puzzleStates.Count -eq 0 -and $success.latestFreePlay.sourceStageId -eq 1) ($success | ConvertTo-Json -Depth 8 -Compress)
|
||||||
Assert-State 'success-preserves-board-camera' (($before.puzzleStates[0].balls | ConvertTo-Json -Depth 6 -Compress) -eq ($success.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress) -and ($before.puzzleStates[0].camera | ConvertTo-Json -Depth 6 -Compress) -eq ($success.latestFreePlay.camera | ConvertTo-Json -Depth 6 -Compress)) '성공 전후 balls/camera 동일'
|
Assert-State 'success-preserves-board-camera' (($before.puzzleStates[0].balls | ConvertTo-Json -Depth 6 -Compress) -eq ($success.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress) -and ($before.puzzleStates[0].camera | ConvertTo-Json -Depth 6 -Compress) -eq ($success.latestFreePlay.camera | ConvertTo-Json -Depth 6 -Compress)) '성공 전후 balls/camera 동일'
|
||||||
@@ -88,7 +88,7 @@ try {
|
|||||||
Step 'final-12-offline-collect' @('shell','input','swipe','930','510','1200','510','800') 4500
|
Step 'final-12-offline-collect' @('shell','input','swipe','930','510','1200','510','800') 4500
|
||||||
$offlineCollected=Save-State 'final-12-offline-collected'
|
$offlineCollected=Save-State 'final-12-offline-collected'
|
||||||
Assert-State 'offline-collected-unfinished' ($offlineCollected.puzzleStates.Count -eq 1 -and $offlineCollected.puzzleStates[0].collectedBallIds.Count -eq 1 -and $offlineCollected.puzzleStates[0].countedBallIds.Count -eq 0) '오프라인 실제 수집됨/계수 전 0'
|
Assert-State 'offline-collected-unfinished' ($offlineCollected.puzzleStates.Count -eq 1 -and $offlineCollected.puzzleStates[0].collectedBallIds.Count -eq 1 -and $offlineCollected.puzzleStates[0].countedBallIds.Count -eq 0) '오프라인 실제 수집됨/계수 전 0'
|
||||||
Step 'final-12-offline-count-success' @('shell','input','swipe','1182','510','1182','510','200') 1500
|
Step 'final-12-offline-count-success' @('shell','input','tap','1182','510') 1500
|
||||||
$offlineSuccess=Save-State 'final-12-offline-success'
|
$offlineSuccess=Save-State 'final-12-offline-success'
|
||||||
Assert-State 'offline-collection-count-actually-success' ($offlineSuccess.completedStageIds.Count -eq 1 -and $offlineSuccess.completedStageIds[0] -eq 1 -and $offlineSuccess.puzzleStates.Count -eq 0 -and $offlineSuccess.latestFreePlay.sourceStageId -eq 1 -and ($offlineCollected.puzzleStates[0].balls | ConvertTo-Json -Depth 6 -Compress) -eq ($offlineSuccess.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress)) '완료1/미완료없음/수집 전후 같은 IDs와 배치 자유 전이'
|
Assert-State 'offline-collection-count-actually-success' ($offlineSuccess.completedStageIds.Count -eq 1 -and $offlineSuccess.completedStageIds[0] -eq 1 -and $offlineSuccess.puzzleStates.Count -eq 0 -and $offlineSuccess.latestFreePlay.sourceStageId -eq 1 -and ($offlineCollected.puzzleStates[0].balls | ConvertTo-Json -Depth 6 -Compress) -eq ($offlineSuccess.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress)) '완료1/미완료없음/수집 전후 같은 IDs와 배치 자유 전이'
|
||||||
Step 'final-13-offline-free-move' @('shell','input','swipe','1182','510','1350','450','800')
|
Step 'final-13-offline-free-move' @('shell','input','swipe','1182','510','1350','450','800')
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
param([string]$EvidenceDir='artifacts/verification/FirstSlice')
|
||||||
|
$ErrorActionPreference='Stop'
|
||||||
|
$EvidenceDir=(Resolve-Path -LiteralPath $EvidenceDir).Path
|
||||||
|
$apkPath=(Resolve-Path -LiteralPath 'artifacts/android/FirstSlice/MagnetismHeroes.apk').Path
|
||||||
|
$source=& git rev-parse HEAD
|
||||||
|
$build=Get-Content -LiteralPath (Join-Path $EvidenceDir 'androidbuild-evidence.json') -Raw | ConvertFrom-Json
|
||||||
|
if ($build.sourceCommit -ne $source -or $build.sourceDirty -ne $false -or $build.sourceDirtyEnd -ne $false -or $build.apkSha256 -ne (Get-FileHash -LiteralPath $apkPath).Hash) { throw '현재 clean 소스의 실제 Build 근거가 있어야 부정 검사를 실행할 수 있습니다.' }
|
||||||
|
$results=@()
|
||||||
|
foreach ($field in @('sourceCommit','apkSha256')) {
|
||||||
|
$caseDir=Join-Path $EvidenceDir "red/provenance-$field"
|
||||||
|
New-Item -ItemType Directory -Path $caseDir -Force | Out-Null
|
||||||
|
$copy=Get-Content -LiteralPath (Join-Path $EvidenceDir 'androidbuild-evidence.json') -Raw | ConvertFrom-Json
|
||||||
|
$copy.$field='deliberately-invalid-verification-copy'
|
||||||
|
$copy | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $caseDir 'androidbuild-evidence.json') -Encoding utf8
|
||||||
|
Copy-Item -LiteralPath (Join-Path $EvidenceDir 'apk-metadata.json') -Destination (Join-Path $caseDir 'apk-metadata.json')
|
||||||
|
$log=Join-Path $caseDir 'negative-smoke.log'
|
||||||
|
$smoke=Join-Path $PSScriptRoot 'smoke.ps1'
|
||||||
|
& pwsh -NoProfile -File $smoke -ApkPath $apkPath -Milestone FirstSlice -EvidenceDir $caseDir *> $log
|
||||||
|
$code=$LASTEXITCODE
|
||||||
|
$outcome=Get-Content -LiteralPath (Join-Path $caseDir 'android-device-evidence.json') -Raw | ConvertFrom-Json
|
||||||
|
$passed=$code -eq 1 -and $outcome.status -eq 'failed' -and @($outcome.commands).Count -eq 0 -and $outcome.checks[-1].criterion -eq 'verified-build-provenance' -and $outcome.checks[-1].passed -eq $false
|
||||||
|
$results += [ordered]@{ field=$field; command="pwsh -NoProfile -File $smoke -ApkPath $apkPath -Milestone FirstSlice -EvidenceDir $caseDir"; actualExitCode=$code; deviceCommandCount=@($outcome.commands).Count; passed=$passed; log=$log }
|
||||||
|
}
|
||||||
|
[ordered]@{ sourceCommit=$source; apkSha256=$build.apkSha256; cases=$results } | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $EvidenceDir 'negative-provenance-results.json') -Encoding utf8
|
||||||
|
if (@($results | Where-Object passed -ne $true).Count) { throw '출처가 틀린 검사 복사본을 기기 조작 전에 거부하지 못했습니다.' }
|
||||||
|
Write-Host '소스 SHA/APK hash 손상 복사본 2개를 실제 종료 코드 1로 기기 조작 전 거부했습니다.'
|
||||||
Reference in New Issue
Block a user