feat: verifyIntegrity CLI 래퍼 추가 및 문서화 (TDD)
Auto PR / open-pr (push) Successful in 16s
Auto Label PR / add-automerge-label (pull_request) Successful in 5s
CI / test (pull_request) Successful in 1m11s

- verify.cli.ts/verify.cli.test.ts 추가
- README 사용법/종료코드/CI 가이드 문서화
- 전체 테스트 그린 유지
This commit is contained in:
2025-11-16 20:24:30 +09:00
parent ba2ba588f7
commit 86155c9fd3
3 changed files with 142 additions and 0 deletions
+20
View File
@@ -80,6 +80,26 @@ The Builder can export a production-ready ZIP that contains:
### Asset paths and grouping
- AssetManager writes files to `assets/` with stable hash-based filenames.
### verifyIntegrity CLI
- Purpose: Validate exported bundle assets against `assets.integrity.index.json`.
- Input format: two JSON files mapping file path → base64 bytes
- Extras JSON: must contain `"assets.integrity.index.json"` key with base64 of the index file
- Assets JSON: keys are asset paths, values are base64 of file bytes
- Usage:
```bash
node scripts/verify.js --extras extras.json --assets assets.json
```
- Output: prints a JSON with fields of `VerifyResult` (`ok`, `errors`, `summary`)
- Exit codes:
- 0: ok
- 1: verification failed or invalid inputs
- 2: usage error (missing args)
- CI: add a step to run the CLI after build/export; fail the job if exit code != 0.
- Path strategy can be configured:
- `flat` (default): `assets/<hash>.<ext>`
- `grouped`: `assets/images/...`, `assets/fonts/...`, `assets/other/...`
+54
View File
@@ -0,0 +1,54 @@
import { describe, it, expect } from 'vitest'
import { verifyCli } from '@/lib/exporter/verify.cli'
// helpers to build base64 from string
function b64(s: string) {
if (typeof Buffer !== 'undefined') return Buffer.from(s, 'utf-8').toString('base64')
throw new Error('Buffer not available')
}
describe('verify.cli', () => {
const EMPTY_SHA256_B64 = '47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU='
it('returns 0 and logs summary when ok', async () => {
// empty entries integrity index; bundleHash = sha256("") in base64
const extras = {
'assets.integrity.index.json': b64(JSON.stringify({ entries: [], bundleHash: 'sha256-' + EMPTY_SHA256_B64 }))
}
const assets = {}
const extrasJson = JSON.stringify(extras)
const assetsJson = JSON.stringify(assets)
const paths: Record<string, string> = {
'/tmp/extras.ok.json': extrasJson,
'/tmp/assets.ok.json': assetsJson,
}
const logs: string[] = []
const readText = async (p: string) => {
const v = paths[p]
if (v == null) throw new Error('no such file: ' + p)
return v
}
const code = await verifyCli(['--extras', '/tmp/extras.ok.json', '--assets', '/tmp/assets.ok.json'], readText, (s: string) => logs.push(s))
expect(code).toBe(0)
expect(logs.join('\n')).toMatch(/"ok": true/)
expect(logs.join('\n')).toMatch(/"total": 0/)
})
it('returns 1 and logs errors when invalid JSON', async () => {
const paths: Record<string, string> = {
'/tmp/extras.bad.json': JSON.stringify({ 'assets.integrity.index.json': 'not-base64' }),
'/tmp/assets.empty.json': JSON.stringify({}),
}
const logs: string[] = []
const readText = async (p: string) => {
const v = paths[p]
if (v == null) throw new Error('no such file: ' + p)
return v
}
const code = await verifyCli(['--extras', '/tmp/extras.bad.json', '--assets', '/tmp/assets.empty.json'], readText, (s: string) => logs.push(s))
expect(code).toBe(1)
expect(logs.join('\n')).toMatch(/"ok": false/)
expect(logs.join('\n')).toMatch(/Invalid assets\.integrity\.index\.json JSON|Missing assets\.integrity\.index\.json/)
})
})
+68
View File
@@ -0,0 +1,68 @@
import { verifyIntegrity } from '@/lib/exporter/verify'
export type ReadText = (path: string) => Promise<string>
export type Logger = (line: string) => void
// base64 -> Uint8Array
function fromBase64(b64: string): Uint8Array {
if (typeof Buffer !== 'undefined') return new Uint8Array(Buffer.from(b64, 'base64'))
throw new Error('Buffer not available')
}
// parse args like: [--extras, path, --assets, path]
function parseArgs(argv: string[]): { extras?: string; assets?: string } {
const out: { extras?: string; assets?: string } = {}
for (let i = 0; i < argv.length; i++) {
const a = argv[i]
if (a === '--extras') out.extras = argv[++i]
else if (a === '--assets') out.assets = argv[++i]
}
return out
}
export async function verifyCli(argv: string[], readText: ReadText, log: Logger): Promise<number> {
try {
const { extras: extrasPath, assets: assetsPath } = parseArgs(argv)
if (!extrasPath || !assetsPath) {
log('Usage: verify --extras <extras.json> --assets <assets.json>')
return 2
}
const extrasJson = await readText(extrasPath)
const assetsJson = await readText(assetsPath)
let extrasMap: Record<string, string>
let assetsMap: Record<string, string>
try {
extrasMap = JSON.parse(extrasJson)
assetsMap = JSON.parse(assetsJson)
} catch (e) {
log('Invalid JSON input files')
return 1
}
// decode base64 into Uint8Array maps
const extrasBytes: Record<string, Uint8Array> = {}
for (const [k, v] of Object.entries(extrasMap)) {
try {
extrasBytes[k] = fromBase64(v)
} catch {
// mark invalid; let verifyIntegrity handle missing/invalid as possible
log(`Failed to decode base64 for extras entry: ${k}`)
return 1
}
}
const assetsBytes: Record<string, Uint8Array> = {}
for (const [k, v] of Object.entries(assetsMap)) {
try {
assetsBytes[k] = fromBase64(v)
} catch {
log(`Failed to decode base64 for asset entry: ${k}`)
return 1
}
}
const result = verifyIntegrity(extrasBytes, assetsBytes)
log(JSON.stringify(result, null, 2))
return result.ok ? 0 : 1
} catch (e) {
log(String(e instanceof Error ? e.message : e))
return 1
}
}