feat(aihr): complete phase two pilot readiness

This commit is contained in:
2026-07-10 19:13:31 +08:00
parent 0219de6fa8
commit 13230e6b9a
48 changed files with 7040 additions and 1647 deletions
@@ -0,0 +1,140 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
SCRIPT="$ROOT_DIR/scripts/demo-check.sh"
assert_contains() {
local text="$1"
grep -Fq "$text" "$SCRIPT" || {
echo "FAIL: demo-check.sh missing: $text" >&2
exit 1
}
}
assert_not_contains() {
local text="$1"
if grep -Fq "$text" "$SCRIPT"; then
echo "FAIL: demo-check.sh still contains unsafe match: $text" >&2
exit 1
fi
}
assert_contains 'AIHR_PILOT_START_DATE'
assert_contains 'AIHR_PILOT_END_DATE'
assert_contains 'AIHR_DEMO_CHECK_LIBRARY_ONLY'
assert_contains 'safe_org AS'
assert_contains 'HAVING SUM(s.session_count) >= 10'
assert_contains 'HAVING COUNT(DISTINCT ext_party_id) = 1'
assert_contains 'finished_time >='
assert_contains 'reviewed_time >='
assert_not_contains 'p.identity_key = o.ext_party_id OR p.identity_key = o.person_phone'
assert_not_contains 's.ext_party_id = o.ext_party_id OR s.ext_party_id = o.person_phone'
if ! command -v docker >/dev/null || ! docker ps --format '{{.Names}}' | grep -qx 'wygj-mysql'; then
echo "SKIP: behavioral pilot gate test requires wygj-mysql"
exit 0
fi
export AIHR_DEMO_CHECK_LIBRARY_ONLY=true
# shellcheck source=../demo-check.sh
source "$SCRIPT"
unset AIHR_PILOT_DB
[[ "$(pilot_sql_scalar 'SELECT 1')" == "1" ]] || {
echo "FAIL: default ry-vue pilot database is unavailable" >&2
exit 1
}
TEST_DB="wygj_demo_check_test_$$"
cleanup() {
docker exec wygj-mysql mysql -uroot -proot -e "DROP DATABASE IF EXISTS \`$TEST_DB\`" >/dev/null 2>&1 || true
}
trap cleanup EXIT
docker exec -i wygj-mysql mysql -uroot -proot --default-character-set=utf8mb4 <<SQL >/dev/null
CREATE DATABASE \`$TEST_DB\` CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
USE \`$TEST_DB\`;
CREATE TABLE aihr_practice_scenario (tenant_id varchar(20), position varchar(80), enabled tinyint);
CREATE TABLE aihr_practice_session (tenant_id varchar(20), session_id varchar(100), ext_party_id varchar(100), mode varchar(30), finished_time datetime);
CREATE TABLE aihr_practice_assignment (tenant_id varchar(20), ext_party_id varchar(100), create_time datetime);
CREATE TABLE aihr_org_snapshot (
tenant_id varchar(20), employment_status varchar(20), ext_party_id varchar(100),
project_code varchar(50), person_phone varchar(30)
);
CREATE TABLE aihr_practice_calibration (
id bigint auto_increment primary key, tenant_id varchar(20), session_id varchar(100),
original_score int, corrected_score int, create_time datetime
);
CREATE TABLE aihr_sop_answer_review (
tenant_id varchar(20), status varchar(30), usable tinyint,
reviewed_time datetime, create_time datetime
);
INSERT INTO aihr_practice_scenario
SELECT '000000','生活顾问',1 FROM (
SELECT 1 n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6
UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10 UNION ALL SELECT 11 UNION ALL SELECT 12
) n;
INSERT INTO aihr_org_snapshot VALUES
('000000','active','A','P1','13900000001'),
('000000','active','B','P1','13900000001'),
('000000','active','C','P2','D'),
('000000','active','D','P2','13900000004'),
('000000','active','E','P3','13900000005'),
('000000','active','F','P3','13900000006');
INSERT INTO aihr_practice_assignment VALUES
('000000','D','2026-07-08 08:00:00'),
('000000','13900000001','2026-07-08 08:00:00'),
('000000','E','2026-07-08 08:00:00'),
('000000','13900000005','2026-07-08 08:00:00'),
('000000','F','2026-07-08 08:00:00');
INSERT INTO aihr_practice_session
SELECT '000000', CONCAT('e-', n), CASE WHEN n <= 5 THEN 'E' ELSE '13900000005' END, 'mobile', '2026-07-08 09:00:00'
FROM (
SELECT 1 n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
) n;
INSERT INTO aihr_practice_session
SELECT '000000', CONCAT('f-', n), 'F','mobile','2026-07-08 10:00:00' FROM (
SELECT 1 n UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9 UNION ALL SELECT 10
) n;
INSERT INTO aihr_practice_session VALUES
('000000','e-out','E','mobile','2026-07-06 10:00:00'),
('000000','unsafe-inside','D','mobile','2026-07-08 10:00:00');
INSERT INTO aihr_practice_calibration (tenant_id,session_id,original_score,corrected_score,create_time) VALUES
('000000','f-1',80,82,'2026-07-08 11:00:00'),
('000000','unsafe-inside',80,40,'2026-07-08 11:00:00'),
('000000','e-out',80,40,'2026-07-06 11:00:00');
INSERT INTO aihr_sop_answer_review VALUES
('000000','已评审',1,'2026-07-08 12:00:00','2026-07-08 11:00:00'),
('000000','已评审',0,'2026-07-06 12:00:00','2026-07-06 11:00:00'),
('000000','待评审',NULL,NULL,'2026-07-08 13:00:00'),
('000000','待评审',NULL,NULL,'2026-07-06 13:00:00');
SQL
export AIHR_PILOT_DB="$TEST_DB"
pilot_output="$(AIHR_PILOT_START_DATE=2026-07-07 AIHR_PILOT_END_DATE=2026-07-10 check_pilot_samples)"
[[ "$pilot_output" == *'ten_sessions=1/5'* ]] || { echo "FAIL: 9/10 boundary or raw window count: $pilot_output" >&2; exit 1; }
[[ "$pilot_output" == *'org_login=2/6'* ]] || { echo "FAIL: unsafe org identities were not excluded: $pilot_output" >&2; exit 1; }
[[ "$pilot_output" == *'org_pilot=2/20'* ]] || { echo "FAIL: collision or dual-key identity was miscounted: $pilot_output" >&2; exit 1; }
[[ "$pilot_output" == *'projects=1/1-2'* ]] || { echo "FAIL: safe pilot project count: $pilot_output" >&2; exit 1; }
[[ "$pilot_output" == *'org_ten_sessions=1/2'* ]] || { echo "FAIL: formal 9/10 boundary: $pilot_output" >&2; exit 1; }
[[ "$pilot_output" == *'calibration=1/20 consistency=1/1'* ]] || { echo "FAIL: calibration window: $pilot_output" >&2; exit 1; }
[[ "$pilot_output" == *'sop_usable=1/1 sop_pending=1'* ]] || { echo "FAIL: SOP window: $pilot_output" >&2; exit 1; }
pilot_sql_scalar "INSERT INTO aihr_practice_session VALUES ('000000','e-10','13900000005','mobile','2026-07-09 09:00:00')" >/dev/null
pilot_output="$(AIHR_PILOT_START_DATE=2026-07-07 AIHR_PILOT_END_DATE=2026-07-10 check_pilot_samples)"
[[ "$pilot_output" == *'org_ten_sessions=2/2'* ]] || { echo "FAIL: formal 10/10 boundary: $pilot_output" >&2; exit 1; }
if (AIHR_PILOT_STRICT=true AIHR_PILOT_START_DATE= AIHR_PILOT_END_DATE= check_pilot_samples) >/tmp/wygj-pilot-strict-test.log 2>&1; then
echo "FAIL: strict mode accepted an implicit date window" >&2
exit 1
fi
grep -Fq 'strict pilot check requires AIHR_PILOT_START_DATE and AIHR_PILOT_END_DATE' /tmp/wygj-pilot-strict-test.log || {
echo "FAIL: strict mode missing-date error is unclear" >&2
exit 1
}
echo "PASS: demo pilot gates use a bounded window and unique formal identities"
@@ -0,0 +1,145 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { parseCsv, verifyPilotExportContract } from '../lib/pilot-export-contract.mjs';
const startDate = '2026-07-07';
const endDate = '2026-07-10';
function validCsv() {
const metrics = {
pilot_start_date: startDate,
pilot_end_date: endDate,
data_scope: 'active_org_unique_identity',
scenario_count: '12',
scenario_count_target: '12',
formal_org_login_count: '20',
formal_pilot_people_count: '20',
formal_pilot_people_target: '20',
formal_pilot_project_count: '1',
formal_pilot_project_target: '1-2',
formal_pilot_scope_ready: 'true',
training_count: '1',
completion_definition: 'at_least_10_sessions_per_person',
completion_people_count: '1',
completion_people_total: '20',
completion_rate: '5.0%',
completion_rate_target: '80.0%',
completion_rate_ready: 'false',
average_score: '86.0',
alert_count: '0',
review_rate: '100.0%',
calibration_sample_count: '1',
ai_human_band_matched_count: '1',
calibration_sample_target: '20',
calibration_sample_ready: 'false',
ai_human_band_consistency_rate: '100.0%',
ai_human_band_consistency_target: '70.0%',
ai_human_band_consistency_ready: 'false',
sop_review_sample_count: '2',
sop_answer_usable_count: '2',
sop_answer_usable_rate: '100.0%',
sop_answer_usable_target: '80.0%',
sop_answer_usable_ready: 'true',
pilot_metric_ready: 'false'
};
const lines = ['metric,value', ...Object.entries(metrics).map(([key, value]) => `${key},${value}`)];
lines.push('', 'formal_ext_party_id,project_codes,session_id,trainee,scene,score,status,finished_time,summary,review_advice,incentive_point');
lines.push('FORMAL-1,P1,session-1,员工甲,投诉处理,86,已复盘,2026-07-08 10:00:00,正式记录,保持回访,3');
return lines.join('\n');
}
test('accepts a complete formal export whose training summary matches its details', () => {
const result = verifyPilotExportContract({
csv: validCsv(),
startDate,
endDate,
contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv'
});
assert.equal(result.metrics.get('training_count'), '1');
assert.equal(result.detailRows.length, 1);
});
test('rejects a missing metric and a detail row outside the requested date window', () => {
assert.throws(() => verifyPilotExportContract({
csv: validCsv().replace('calibration_sample_count,1\n', ''),
startDate,
endDate,
contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv'
}), /missing metric calibration_sample_count/);
assert.throws(() => verifyPilotExportContract({
csv: validCsv().replace('2026-07-08 10:00:00', '2026-07-11 10:00:00'),
startDate,
endDate,
contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv'
}), /outside requested window/);
});
test('rejects contradictory pilot metrics and incomplete formal details', () => {
assert.throws(() => verifyPilotExportContract({
csv: validCsv().replace('completion_people_total,20', 'completion_people_total,19'),
startDate,
endDate,
contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv'
}), /completion_people_total/);
assert.throws(() => verifyPilotExportContract({
csv: validCsv().replace('training_count,1', 'training_count,2'),
startDate,
endDate,
contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv'
}), /does not match detail rows/);
assert.throws(() => verifyPilotExportContract({
csv: validCsv().replace('FORMAL-1,P1,session-1', ',P1,session-1'),
startDate,
endDate,
contentDisposition: 'attachment; filename=wrong.csv'
}), /missing formal identity/);
assert.throws(() => verifyPilotExportContract({
csv: validCsv().replace('ai_human_band_consistency_rate,100.0%', 'ai_human_band_consistency_rate,0.0%'),
startDate,
endDate,
contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv'
}), /ai_human_band_consistency_rate/);
});
test('parses valid quoted CSV fields and rejects malformed quoting', () => {
assert.deepEqual(parseCsv('metric,value\nsummary,"逗号,转义""引号""\n第二行"\n'), [
['metric', 'value'],
['summary', '逗号,转义"引号"\n第二行']
]);
assert.deepEqual(parseCsv('""'), [['']]);
assert.throws(() => parseCsv('metric,value\nsummary,"未闭合'), /unclosed quoted field/);
assert.throws(() => parseCsv('metric,value\nsummary,值"错误'), /quote must start a field/);
assert.throws(() => parseCsv('metric,value\nsummary,"值"错误'), /unexpected character after closing quote/);
assert.throws(() => verifyPilotExportContract({
csv: validCsv().replace('metric,value', '"metric,value"'),
startDate,
endDate,
contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv'
}), /missing metric,value header/);
});
test('uses raw numerators for threshold readiness instead of rounded percentages', () => {
const csv = validCsv()
.replace('calibration_sample_count,1', 'calibration_sample_count,203')
.replace('ai_human_band_matched_count,1', 'ai_human_band_matched_count,142')
.replace('ai_human_band_consistency_rate,100.0%', 'ai_human_band_consistency_rate,70.0%')
.replace('calibration_sample_ready,false', 'calibration_sample_ready,true')
.replace('sop_review_sample_count,2', 'sop_review_sample_count,2049')
.replace('sop_answer_usable_count,2', 'sop_answer_usable_count,1639')
.replace('sop_answer_usable_rate,100.0%', 'sop_answer_usable_rate,80.0%')
.replace('sop_answer_usable_ready,true', 'sop_answer_usable_ready,false');
assert.doesNotThrow(() => verifyPilotExportContract({
csv,
startDate,
endDate,
contentDisposition: 'attachment; filename=aihr-practice-pilot-2026-07-07-2026-07-10.csv'
}));
});
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$ROOT_DIR/scripts/lib/process-tree.sh"
sleep 60 &
protected_pid="$!"
signal_process_snapshot "${protected_pid}|not-the-real-start-time" KILL
if ! kill -0 "$protected_pid" >/dev/null 2>&1; then
echo "FAIL: identity mismatch killed an unrelated process" >&2
exit 1
fi
kill "$protected_pid" >/dev/null 2>&1 || true
wait "$protected_pid" 2>/dev/null || true
sh -c 'sleep 60 & wait' &
parent_pid="$!"
sleep 0.1
child_pid="$(pgrep -P "$parent_pid" | head -n 1)"
if [[ -z "$child_pid" ]]; then
echo "FAIL: dummy child process was not created" >&2
stop_process_tree "$parent_pid" KILL
exit 1
fi
stop_process_tree "$parent_pid"
wait "$parent_pid" 2>/dev/null || true
sleep 0.1
child_state="$(ps -o stat= -p "$child_pid" 2>/dev/null | xargs || true)"
if [[ -n "$child_state" && "$child_state" != Z* ]]; then
echo "FAIL: child process $child_pid is still running ($child_state)" >&2
stop_process_tree "$child_pid" KILL
exit 1
fi
echo "PASS: process tree stopped"
PROCESS_TREE_TERM_WAIT_LOOPS=2
export PROCESS_TREE_TERM_WAIT_LOOPS
sh -c 'trap "" TERM; sh -c '\''trap "" TERM; sleep 60'\'' & wait' &
stubborn_parent_pid="$!"
sleep 0.1
stubborn_child_pid="$(pgrep -P "$stubborn_parent_pid" | head -n 1)"
stop_process_tree "$stubborn_parent_pid"
wait "$stubborn_parent_pid" 2>/dev/null || true
sleep 0.1
stubborn_child_state="$(ps -o stat= -p "$stubborn_child_pid" 2>/dev/null | xargs || true)"
if [[ -n "$stubborn_child_state" && "$stubborn_child_state" != Z* ]]; then
echo "FAIL: TERM-resistant child process $stubborn_child_pid is still running ($stubborn_child_state)" >&2
kill -KILL "$stubborn_child_pid" >/dev/null 2>&1 || true
exit 1
fi
echo "PASS: TERM-resistant process tree escalated safely"
identity_marker="$(mktemp)"
rm -f "$identity_marker"
process_identity_token() {
if [[ ! -f "$identity_marker" ]]; then
touch "$identity_marker"
echo "first identity"
else
echo "reused identity"
fi
}
reused_snapshot="$(snapshot_process_tree 999999)"
rm -f "$identity_marker"
if [[ -n "$reused_snapshot" ]]; then
echo "FAIL: snapshot retained a process tree after root identity changed" >&2
exit 1
fi
echo "PASS: root identity change invalidates the snapshot"