Files
prop-ai-hr/scripts/tests/aug1-formal-content.test.mjs

263 lines
11 KiB
JavaScript

import assert from 'node:assert/strict'
import { createHash } from 'node:crypto'
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { spawnSync } from 'node:child_process'
import test from 'node:test'
const testDir = path.dirname(fileURLToPath(import.meta.url))
const projectRoot = path.resolve(testDir, '..', '..')
const verifierPath = path.join(projectRoot, 'scripts', 'verify-aug1-formal-content.mjs')
const approvalPath = path.join(
projectRoot,
'docs',
'content-candidates',
'aug1-release-approval.json',
)
const candidatePath = path.join(
projectRoot,
'docs',
'content-candidates',
'aug1-life-advisor-content-candidates-v0.1.json',
)
const candidateBytes = await readFile(candidatePath)
const candidate = JSON.parse(candidateBytes.toString('utf8'))
const candidateSha256 = createHash('sha256').update(candidateBytes).digest('hex')
const source = {
sourceId: 'FORMAL-OPS-001',
title: '现行生活顾问服务作业标准',
version: '2026.07',
effectiveDate: '2026-07-01',
scope: '受控内测项目',
sha256: 'a'.repeat(64),
formalPolicy: true,
}
function validApproval() {
const scenarioApprovals = Object.fromEntries(candidate.scenarios.map((item, index) => {
const highRisk = item.proposedRiskLevel === '高风险'
return [item.candidateId, {
status: 'APPROVED',
scenarioCode: item.scenarioDraft.id,
riskLevel: item.proposedRiskLevel,
sourceRefs: ['FORMAL-OPS-001:section-1'],
reviewedBy: `reviewer-${index + 1}`,
reviewedAt: '2026-07-30T09:00:00+08:00',
...(highRisk ? {
secondReviewedBy: `second-reviewer-${index + 1}`,
secondReviewedAt: '2026-07-30T10:00:00+08:00',
} : {}),
businessEvidenceRef: `evidence/scenario-${index + 1}.pdf`,
businessEvidenceSha256: 'c'.repeat(64),
productionContentVersion: `aug1-v${index + 1}`,
productionContentHash: String(index + 1).repeat(64),
}]
}))
const policyQuestionApprovals = Object.fromEntries(candidate.policyQuestionCandidates.map((item, index) => {
const answerDisposition = item.category === 'CONFLICT'
? 'FORMAL_CONFLICT_BOUNDARY'
: item.category === 'NO_EVIDENCE'
? 'FORMAL_NO_EVIDENCE_BOUNDARY'
: item.category === 'UNAUTHORIZED'
? 'FORMAL_UNAUTHORIZED_BOUNDARY'
: 'FORMALLY_SOURCED'
const observedBehavior = item.category === 'CONFLICT'
? 'CONFLICT_BOUNDARY_DETECTED'
: item.category === 'NO_EVIDENCE'
? 'NO_EVIDENCE_BOUNDARY_DETECTED'
: item.category === 'UNAUTHORIZED'
? 'UNAUTHORIZED_BOUNDARY_DETECTED'
: '验收结果与预期边界一致'
return [item.id, {
status: 'APPROVED',
answerDisposition,
sourceRefs: answerDisposition === 'FORMALLY_SOURCED' ? ['FORMAL-OPS-001:section-2'] : [],
expectedBehavior: item.expectedBehavior,
observedBehavior,
evidenceRef: `evidence/question-${index + 1}.json`,
evidenceSha256: 'b'.repeat(64),
reviewedBy: `question-reviewer-${index + 1}`,
reviewedAt: '2026-07-30T11:00:00+08:00',
}]
}))
const signoffs = Object.fromEntries([
'businessOwner',
'knowledgeOwner',
'trainingOwner',
'channelOwner',
'releaseOwner',
].map((role) => [role, {
status: 'APPROVED',
reviewedBy: `${role}-reviewer`,
reviewedAt: '2026-07-30T12:00:00+08:00',
}]))
const channelApprovals = Object.fromEntries([
'direct_president',
'direct_finance',
'direct_hr',
'direct_audit',
'direct_operations',
].map((role) => [role, {
status: 'APPROVED',
minimumActiveHandlers: 2,
bindingEvidenceRef: `evidence/${role}-binding.json`,
bindingEvidenceSha256: 'd'.repeat(64),
positiveAccessEvidenceRef: `evidence/${role}-positive.json`,
positiveAccessEvidenceSha256: 'e'.repeat(64),
crossChannelDenialEvidenceRef: `evidence/${role}-denial.json`,
crossChannelDenialEvidenceSha256: 'f'.repeat(64),
singleReplyEvidenceRef: `evidence/${role}-reply.json`,
singleReplyEvidenceSha256: '1'.repeat(64),
reviewedBy: `${role}-reviewer`,
reviewedAt: '2026-07-30T11:30:00+08:00',
}]))
const expectedApkSignerSha256 = '2'.repeat(64)
return {
schemaVersion: '1.0',
releaseTarget: '2026-08-01',
tenantId: '000000',
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
candidateSha256,
expectedApkSignerSha256,
releaseStatus: 'APPROVED',
formalContentVersion: 'aug1-formal-v1',
sourceRegistry: [source],
scenarioApprovals,
policyQuestionApprovals,
channelApprovals,
distributionApproval: {
status: 'APPROVED',
scope: 'CONTROLLED_INTERNAL_TEST',
legalDecision: 'CONTROLLED_INTERNAL_TEST_EXCEPTION_ACCEPTED',
signerDecision: 'DCLOUD_TEST_SIGNER_ACCEPTED_FOR_CONTROLLED_INTERNAL_TEST',
signerSha256: expectedApkSignerSha256,
distributionEvidenceRef: 'evidence/controlled-distribution.json',
distributionEvidenceSha256: '3'.repeat(64),
reviewedBy: 'distribution-reviewer',
reviewedAt: '2026-07-30T12:30:00+08:00',
},
signoffs,
}
}
async function verifyApproval(approval, ...options) {
const tempDir = await mkdtemp(path.join(os.tmpdir(), 'aug1-formal-'))
const tempCandidatePath = path.join(tempDir, 'aug1-life-advisor-content-candidates-v0.1.json')
const tempApprovalPath = path.join(tempDir, 'approval.json')
try {
await writeFile(tempCandidatePath, candidateBytes)
await writeFile(tempApprovalPath, JSON.stringify(approval), 'utf8')
return spawnSync(process.execPath, [verifierPath, ...options, tempApprovalPath], {
cwd: projectRoot,
encoding: 'utf8',
})
} finally {
await rm(tempDir, { recursive: true, force: true })
}
}
test('audits the checked-in controlled-test approval and passes strict verification', () => {
const audit = spawnSync(process.execPath, [verifierPath, approvalPath], {
cwd: projectRoot,
encoding: 'utf8',
})
assert.equal(audit.status, 0, audit.stderr)
assert.match(audit.stdout, /approved scenarios: 5\/5/)
assert.match(audit.stdout, /approved policy questions: 30\/30/)
assert.match(audit.stdout, /approved direct channels: 5\/5/)
assert.match(audit.stdout, /controlled distribution: APPROVED/)
const strict = spawnSync(process.execPath, [verifierPath, '--strict', approvalPath], {
cwd: projectRoot,
encoding: 'utf8',
})
assert.equal(strict.status, 0, strict.stderr)
assert.match(strict.stdout, /strict release ready: true/)
})
test('accepts complete formal evidence and emits five immutable production snapshots', async () => {
const strict = await verifyApproval(validApproval(), '--strict')
assert.equal(strict.status, 0, strict.stderr)
assert.match(strict.stdout, /strict release ready: true/)
const snapshots = await verifyApproval(validApproval(), '--scenario-snapshots')
assert.equal(snapshots.status, 0, snapshots.stderr)
const lines = snapshots.stdout.trim().split(/\r?\n/)
assert.equal(lines.length, 5)
assert.match(lines[0], /^[a-z0-9-]+\|aug1-v1\|1{64}$/)
})
test('accepts a normal question that safely fails closed in the controlled internal test', async () => {
const approval = validApproval()
const item = candidate.policyQuestionCandidates.find((question) => question.category === 'NORMAL')
approval.policyQuestionApprovals[item.id].answerDisposition = 'FORMAL_NO_EVIDENCE_BOUNDARY'
approval.policyQuestionApprovals[item.id].sourceRefs = []
approval.policyQuestionApprovals[item.id].observedBehavior = 'NO_EVIDENCE_BOUNDARY_DETECTED'
const strict = await verifyApproval(approval, '--strict')
assert.equal(strict.status, 0, strict.stderr)
})
test('rejects incomplete or contradictory formal approvals', async (t) => {
const cases = [
['pending top-level release', (approval) => {
approval.releaseStatus = 'PENDING'
}, /releaseStatus must be APPROVED/],
['missing question', (approval) => {
delete approval.policyQuestionApprovals[candidate.policyQuestionCandidates[0].id]
}, /question approvals must cover exactly 30\/30/],
['wrong no-evidence behavior', (approval) => {
const item = candidate.policyQuestionCandidates.find((question) => question.category === 'NO_EVIDENCE')
approval.policyQuestionApprovals[item.id].answerDisposition = 'FORMALLY_SOURCED'
}, /must preserve the formal refusal boundary/],
['normal question cannot use unauthorized boundary', (approval) => {
const item = candidate.policyQuestionCandidates.find((question) => question.category === 'NORMAL')
approval.policyQuestionApprovals[item.id].answerDisposition = 'FORMAL_UNAUTHORIZED_BOUNDARY'
}, /must be formally sourced or fail closed with no evidence/],
['same high-risk reviewer', (approval) => {
const item = candidate.scenarios.find((scenario) => scenario.proposedRiskLevel === '高风险')
approval.scenarioApprovals[item.candidateId].secondReviewedBy =
approval.scenarioApprovals[item.candidateId].reviewedBy
}, /high-risk reviewers must be different people/],
['unknown source', (approval) => {
const item = candidate.scenarios[0]
approval.scenarioApprovals[item.candidateId].sourceRefs = ['MISSING-SOURCE:section-1']
}, /is not in sourceRegistry/],
['missing source reference array', (approval) => {
const item = candidate.scenarios[0]
delete approval.scenarioApprovals[item.candidateId].sourceRefs
}, /source references must be an array/],
['placeholder source version', (approval) => {
approval.sourceRegistry[0].version = '待填写'
}, /formal source version is required/],
['missing owner sign-off', (approval) => {
approval.signoffs.releaseOwner.status = 'PENDING'
}, /releaseOwner: sign-off must be APPROVED/],
['missing channel acceptance', (approval) => {
delete approval.channelApprovals.direct_audit
}, /channel approvals must cover exactly 5\/5 roles/],
['channel without backup requirement', (approval) => {
approval.channelApprovals.direct_finance.minimumActiveHandlers = 1
}, /primary and backup requirement must remain 2/],
['unaccepted distribution', (approval) => {
approval.distributionApproval.status = 'PENDING'
}, /distribution approval must be APPROVED/],
['mismatched APK signer', (approval) => {
approval.distributionApproval.signerSha256 = '4'.repeat(64)
}, /distribution signer SHA-256 must match the expected APK signer/],
]
for (const [name, mutate, expected] of cases) {
await t.test(name, async () => {
const approval = validApproval()
mutate(approval)
const result = await verifyApproval(approval, '--strict')
assert.notEqual(result.status, 0)
assert.match(result.stderr, expected)
})
}
})