223 lines
7.6 KiB
Bash
223 lines
7.6 KiB
Bash
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
SCRIPT="$ROOT_DIR/scripts/verify-aug1-release-readiness.sh"
|
|
PREFLIGHT="$ROOT_DIR/scripts/release-preflight.sh"
|
|
DEV_SETUP="$ROOT_DIR/docs/DEV_SETUP.md"
|
|
|
|
bash -n "$SCRIPT"
|
|
bash -n "$PREFLIGHT"
|
|
|
|
pending_output="$("$SCRIPT" --execute 2>&1 || true)"
|
|
grep -Fq 'releaseStatus must be APPROVED' <<<"$pending_output"
|
|
grep -Fq 'strict approval did not return exactly five scenario snapshots' <<<"$pending_output"
|
|
|
|
test_tmp_base="${TMPDIR:-/tmp}"
|
|
test_tmp="$(mktemp -d "$test_tmp_base/aug1-readiness-test.XXXXXX")"
|
|
cleanup() {
|
|
case "$test_tmp" in
|
|
"$test_tmp_base"/aug1-readiness-test.*) rm -rf -- "$test_tmp" ;;
|
|
*) echo "FAIL: unsafe test cleanup path: $test_tmp" >&2; exit 1 ;;
|
|
esac
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
cp "$ROOT_DIR/docs/content-candidates/aug1-life-advisor-content-candidates-v0.1.json" \
|
|
"$test_tmp/aug1-life-advisor-content-candidates-v0.1.json"
|
|
node - "$test_tmp/aug1-life-advisor-content-candidates-v0.1.json" \
|
|
"$test_tmp/approval.json" "$test_tmp/scenario-rows.tsv" <<'NODE'
|
|
const fs = require('node:fs')
|
|
const crypto = require('node:crypto')
|
|
const [candidatePath, approvalPath, rowsPath] = process.argv.slice(2)
|
|
const bytes = fs.readFileSync(candidatePath)
|
|
const candidate = JSON.parse(bytes)
|
|
const source = {
|
|
sourceId: 'FORMAL-OPS-001',
|
|
title: 'test-only formal operations standard',
|
|
version: 'test-v1',
|
|
effectiveDate: '2026-07-01',
|
|
scope: 'test-only',
|
|
sha256: 'a'.repeat(64),
|
|
formalPolicy: true,
|
|
}
|
|
const scenarioApprovals = {}
|
|
const rows = []
|
|
candidate.scenarios.forEach((item, index) => {
|
|
const highRisk = item.proposedRiskLevel === '高风险'
|
|
const version = `aug1-v${index + 1}`
|
|
const hash = String(index + 1).repeat(64)
|
|
scenarioApprovals[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`,
|
|
productionContentVersion: version,
|
|
productionContentHash: hash,
|
|
}
|
|
rows.push([
|
|
item.scenarioDraft.id,
|
|
version,
|
|
hash,
|
|
'1',
|
|
'已发布',
|
|
item.proposedRiskLevel,
|
|
String(100 + index),
|
|
'2026-07-30T09:00:00',
|
|
highRisk ? String(200 + index) : '0',
|
|
highRisk ? '2026-07-30T10:00:00' : '-',
|
|
'FORMAL-OPS-001:section-1',
|
|
].join('\t'))
|
|
})
|
|
const policyQuestionApprovals = {}
|
|
candidate.policyQuestionCandidates.forEach((item, index) => {
|
|
policyQuestionApprovals[item.id] = {
|
|
status: 'APPROVED',
|
|
answerDisposition: item.category === 'NO_EVIDENCE'
|
|
? 'FORMAL_NO_EVIDENCE_BOUNDARY'
|
|
: item.category === 'UNAUTHORIZED'
|
|
? 'FORMAL_UNAUTHORIZED_BOUNDARY'
|
|
: 'FORMALLY_SOURCED',
|
|
sourceRefs: ['FORMAL-OPS-001:section-2'],
|
|
expectedBehavior: item.expectedBehavior,
|
|
observedBehavior: 'test-only observed result',
|
|
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',
|
|
}]))
|
|
fs.writeFileSync(approvalPath, JSON.stringify({
|
|
schemaVersion: '1.0',
|
|
releaseTarget: '2026-08-01',
|
|
tenantId: '000000',
|
|
candidateFile: 'aug1-life-advisor-content-candidates-v0.1.json',
|
|
candidateSha256: crypto.createHash('sha256').update(bytes).digest('hex'),
|
|
releaseStatus: 'APPROVED',
|
|
formalContentVersion: 'aug1-formal-v1',
|
|
sourceRegistry: [source],
|
|
scenarioApprovals,
|
|
policyQuestionApprovals,
|
|
signoffs,
|
|
}))
|
|
fs.writeFileSync(rowsPath, rows.join('\n') + '\n')
|
|
NODE
|
|
|
|
mock_bin="$test_tmp/mock-bin"
|
|
mkdir -p "$mock_bin"
|
|
cat > "$mock_bin/systemctl" <<'MOCK_SYSTEMCTL'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
printf '%s\n' "$MOCK_SERVICE_PID"
|
|
MOCK_SYSTEMCTL
|
|
cat > "$mock_bin/mysql" <<'MOCK_MYSQL'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
query="$(cat)"
|
|
if [[ "$query" == *'COUNT(DISTINCT u.user_id)'* ]]; then
|
|
handler_count="${MOCK_HANDLER_COUNT:-2}"
|
|
printf 'direct_audit\t%s\n' "$handler_count"
|
|
printf 'direct_finance\t%s\n' "$handler_count"
|
|
printf 'direct_hr\t%s\n' "$handler_count"
|
|
printf 'direct_operations\t%s\n' "$handler_count"
|
|
printf 'direct_president\t%s\n' "$handler_count"
|
|
elif [[ "$query" == *'FROM aihr_practice_scenario'* ]]; then
|
|
cat "$MOCK_SCENARIO_ROWS"
|
|
else
|
|
echo 'FAIL: unexpected mock MySQL query' >&2
|
|
exit 90
|
|
fi
|
|
MOCK_MYSQL
|
|
cat > "$mock_bin/ssh" <<'MOCK_SSH'
|
|
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
while (($#)); do
|
|
if [[ "$1" == '--' ]]; then
|
|
shift
|
|
break
|
|
fi
|
|
shift
|
|
done
|
|
remote_script="$MOCK_REMOTE_SCRIPT"
|
|
cat > "$remote_script"
|
|
export MOCK_SERVICE_PID="$$"
|
|
PATH="$MOCK_REMOTE_BIN:$PATH" bash "$remote_script" "$@"
|
|
MOCK_SSH
|
|
chmod +x "$mock_bin/systemctl" "$mock_bin/mysql" "$mock_bin/ssh"
|
|
|
|
run_mock_readiness() {
|
|
PATH="$mock_bin:$PATH" \
|
|
MOCK_REMOTE_BIN="$mock_bin" \
|
|
MOCK_REMOTE_SCRIPT="$test_tmp/remote.sh" \
|
|
MOCK_SCENARIO_ROWS="$test_tmp/scenario-rows.tsv" \
|
|
AIHR_SMS_DEV_FIXED_CODE='test-only-fixed-code' \
|
|
AIHR_SMS_PROD_FIXED_CODE_ENABLED="${AIHR_SMS_PROD_FIXED_CODE_ENABLED:-true}" \
|
|
AIHR_AUG1_APPROVAL_PATH="$test_tmp/approval.json" \
|
|
"$SCRIPT" --execute
|
|
}
|
|
|
|
ready_output="$(run_mock_readiness)"
|
|
grep -Fq 'AUG1_FIXED_CODE_NO_REAL_SMS_MODE=PASS' <<<"$ready_output"
|
|
grep -Fq 'AUG1_FORMAL_SCENARIOS=5/5' <<<"$ready_output"
|
|
grep -Fq 'AUG1_RELEASE_READINESS=PASS' <<<"$ready_output"
|
|
|
|
handler_failure="$(
|
|
MOCK_HANDLER_COUNT=1 run_mock_readiness 2>&1 || true
|
|
)"
|
|
grep -Fq 'requires an active primary handler and backup; got 1/2' <<<"$handler_failure"
|
|
|
|
fixed_mode_failure="$(
|
|
AIHR_SMS_PROD_FIXED_CODE_ENABLED=false run_mock_readiness 2>&1 || true
|
|
)"
|
|
grep -Fq 'production fixed-code mode is not explicitly enabled' <<<"$fixed_mode_failure"
|
|
|
|
grep -Fq 'AIHR_SMS_DEV_FIXED_CODE' "$SCRIPT"
|
|
grep -Fq 'AIHR_SMS_PROD_FIXED_CODE_ENABLED' "$SCRIPT"
|
|
grep -Fq 'AUG1_FIXED_CODE_NO_REAL_SMS_MODE=PASS' "$SCRIPT"
|
|
grep -Fq 'COUNT(DISTINCT u.user_id)' "$SCRIPT"
|
|
grep -Fq 'handler_count >= 2' "$SCRIPT"
|
|
grep -Fq 'u.status = '\''0'\''' "$SCRIPT"
|
|
grep -Fq 'u.del_flag = '\''0'\''' "$SCRIPT"
|
|
grep -Fq 'review_status" == "已发布"' "$SCRIPT"
|
|
grep -Fq 'second_reviewer_user_id" != "$reviewer_user_id"' "$SCRIPT"
|
|
grep -Fq 'production content hash does not match the approved snapshot' "$SCRIPT"
|
|
grep -Fq 'AUG1_FORMAL_SCENARIOS=5/5' "$SCRIPT"
|
|
grep -Fq 'AUG1_FORMAL_POLICY_QUESTIONS=30/30' "$SCRIPT"
|
|
|
|
if grep -Eiq '(^|[[:space:]])(insert|update|delete|replace|alter|drop|truncate)[[:space:]]' "$SCRIPT"; then
|
|
echo 'FAIL: August 1 release readiness verifier contains a mutating SQL verb' >&2
|
|
exit 1
|
|
fi
|
|
if grep -Fq '/resource/sms/code' "$SCRIPT" || grep -Fq '/auth/mobile/sms-login' "$SCRIPT"; then
|
|
echo 'FAIL: final readiness verifier must inspect fixed-code state without requesting or consuming a code' >&2
|
|
exit 1
|
|
fi
|
|
if grep -Eq 'echo[[:space:]].*(fixed_code|reviewedBy|user_id)' "$SCRIPT"; then
|
|
echo 'FAIL: final readiness verifier may expose a code or identity' >&2
|
|
exit 1
|
|
fi
|
|
|
|
grep -Fq 'verify_aug1_readiness="${RELEASE_VERIFY_AUG1_READINESS:-false}"' "$PREFLIGHT"
|
|
grep -Fq 'verify-aug1-release-readiness.sh" --execute' "$PREFLIGHT"
|
|
grep -Fq 'RELEASE_VERIFY_AUG1_READINESS=true' "$DEV_SETUP"
|
|
|
|
echo 'PASS: August 1 final readiness gate is fixed-code-only, read-only, content-bound and handler-bound'
|