Archived
fix(w05-e2e): 登录优先级倒过来(服务账号优先)+ 候选缺库案例改两阶段填表 —— 五案例仍未跑成
Round 2 逼出的两处修: ①`login()`/`DIRECT_BEARER` 从「旁路无条件优先」改成「服务账号三变量全齐才是正路, JCAI_E2E_BEARER 只在缺席时兜底」——实测踩过:.stable/env 留着一枚已过期的真人 token 时,脚本会悄悄绕开新配好的服务账号,五案例全部倒在起单前的 401(未派发,SQL 核过 0 个 testbed 任务被创建)。 ②案例 0(候选缺库·新建类型)随 Task 7b 的 parts.chip_type.type_id 从 integer 改 text/auto 重写成两阶段 customFill:先铸自增 type_id、读回来再引用它写 type_param/ profile/parameter,与真人在浏览器里「先建类型保存再选它」同一个顺序。 装包已核实:part-substitution-assessment@2.1.1,六列 ALTER 到 text 全部成功(种子 数据完好)。跑前清理过一批与本次改动无关的旧模型(九步版,08-27)测试残渣 (XL2596S-5.0 在 parts__profile 里的假数据 + 连带 78 行)。 Step 4(五案例)仍未跑成——login() 修好之后卡在新的一处:e2e 服务账号 service-account-task-dispatcher 只有 workbench 端,没有 design 端(试跑起单 挂在设计端),ends=["workbench"],token 里没有 admin 角色。三轮试跑全部倒在 login() 里,没有一个任务被创建。需要人给这个服务账号加能开出 design 端的 realm 角色。 pnpm gates 退出码 0(2422 passed | 34 skipped)。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LKMZDwMbyKzjw6Hembwvet
This commit is contained in:
+107
-52
@@ -37,9 +37,14 @@ const E2E_CLIENT_SECRET = process.env.KEYCLOAK_E2E_CLIENT_SECRET ?? '';
|
||||
* 两处代价说在前头,别不知不觉踩上:
|
||||
* ①**卡会落到那个人的真工作台上**(服务账号那条路才是「绝不派到用户工作台」的);
|
||||
* ②**不会自动续期** —— 过期了整套当场 401,得换一枚重跑。
|
||||
* **给了就用它** —— 显式设了这个变量就是要走旁路,哪怕环境里还留着一对
|
||||
* `KEYCLOAK_E2E_CLIENT_ID/SECRET`(常见于 `.stable/env` 里的历史残留,还未必有效);
|
||||
* 不设它才走服务账号那条正路(ADR-012)。
|
||||
*
|
||||
* **2026-08-30 取信优先级改成服务账号优先**(实测踩过一次:`.stable/env` 里留着一枚
|
||||
* chaihao 的旧 token,`KEYCLOAK_E2E_CLIENT_ID/SECRET` 那时也已经配好能用,而脚本原来
|
||||
* 「给了就用它」的写法让旁路无条件盖过正路 —— 每次 e2e 都会悄悄绕开服务账号,把人类卡
|
||||
* 派到那个人的真实工作台上,而日志只有一行不起眼的提示,容易被漏看)。
|
||||
* **服务账号三个变量(`KEYCLOAK_URL`/`KEYCLOAK_E2E_CLIENT_ID`/`KEYCLOAK_E2E_CLIENT_SECRET`)
|
||||
* 全齐时走正路**;`JCAI_E2E_BEARER` 只在**任意一个缺席**时才顶上来兜底,兜底那一刻把
|
||||
* 代价喊得更响一点(见 `login()`)。
|
||||
*/
|
||||
const DIRECT_BEARER = (process.env.JCAI_E2E_BEARER ?? '').replace(/^Bearer\s+/i, '').trim();
|
||||
/**
|
||||
@@ -612,58 +617,85 @@ const SUITES = {
|
||||
cases: [
|
||||
{
|
||||
sample: 0,
|
||||
name: '候选缺库 · XL2596S-5.0 新建芯片类型(type_id=3,字段清单沿用原型口径)入库,'
|
||||
name: '候选缺库 · XL2596S-5.0 新建芯片类型(自增 type_id,字段清单沿用原型口径)入库,'
|
||||
+ '原 PN 一并转挂新类型才够得上 distinct=1 · 审核先点名意见驳回再通过',
|
||||
decideSeq: ['custom:使能阈值那条 UNKNOWN 的去向请写明验证任务号与测试条件', 'approve'],
|
||||
expectDecision: true,
|
||||
expectPort: 'assessment-released',
|
||||
// 全套只该有一次模型调用(S2 唯一一次 analyze)—— S3 审核循环走的是纯投影重算,不重问模型
|
||||
expectAnalyzeCalls: 1,
|
||||
stageEdits: {
|
||||
'parts.chip_type': [
|
||||
{
|
||||
type_id: '3',
|
||||
type_name: 'DC-DC 降压稳压器(评估自定义)',
|
||||
description: '本次评估自定义的降压转换器类型,核心参数字段清单沿用原类型口径,供原 PN 与候选料共用比对。',
|
||||
},
|
||||
],
|
||||
// 字段名 / 说明 / 约束 / 单位逐字照抄 type_id=1 的字典行(见 seed-mvp-data.ts)——
|
||||
// 换个类型号但比对维度不变,原 PN 既有的参数值才能按同名继续 lookup 到。
|
||||
'parts.type_param': [
|
||||
{ type_id: '3', param_name: '输入电压范围', param_desc: '芯片允许的输入直流电压区间。候选料的上限低于应用输入上限就是硬伤,烧的是芯片。', constraint_type: 'hard', param_unit: 'V' },
|
||||
{ type_id: '3', param_name: '输出电压', param_desc: '固定版给定值;可调版给可调区间,需外围反馈电阻网络设定。固定换可调要动外围。', constraint_type: 'hard', param_unit: 'V' },
|
||||
{ type_id: '3', param_name: '输出电流', param_desc: '满载持续输出能力。低于原料即降额,负载重时会掉压或过热保护。', constraint_type: 'hard', param_unit: 'A' },
|
||||
{ type_id: '3', param_name: '开关频率', param_desc: '决定外围电感与输出电容的取值。频率变了外围多半要跟着换,不是换颗料就完。', constraint_type: 'soft', param_unit: 'kHz' },
|
||||
{ type_id: '3', param_name: '静态电流', param_desc: '空载待机时自身消耗。电池供电场景敏感,市电场景通常不敏感。', constraint_type: 'soft', param_unit: 'mA' },
|
||||
{ type_id: '3', param_name: '工作温度', param_desc: '保证参数指标的环境温度区间。工业温域窄于应用要求就是硬伤。', constraint_type: 'hard', param_unit: '℃' },
|
||||
{ type_id: '3', param_name: '使能阈值', param_desc: 'ON/OFF 引脚的开关门限。上游用 MCU 电平直驱时,阈值对不上会关不掉或开不起来。', constraint_type: 'hard', param_unit: 'V' },
|
||||
],
|
||||
'parts.profile': [
|
||||
// 原 PN 转挂新类型 —— distinct(chip_type_id) 要求两颗归同一个值,新建类型时
|
||||
// 两颗都得落在它上面,不是只给候选料一颗挂新号。
|
||||
{ internal_pn: 'LM2596S-5.0', chip_type_id: '3' },
|
||||
{
|
||||
internal_pn: 'XL2596S-5.0',
|
||||
chip_type_id: '3',
|
||||
brand_name: 'XLSEMI',
|
||||
manufacturer_name: 'XLSEMI',
|
||||
specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz',
|
||||
lifecycle_status: 'active',
|
||||
datasheet_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf',
|
||||
datasheet_version: 'Rev C',
|
||||
},
|
||||
],
|
||||
// LM2596S-5.0 的既有参数值按同名 lookup 自动带过来,不必在这里重填;
|
||||
// 只需交代候选料 XL2596S-5.0 那一份。
|
||||
'parts.parameter': paramRows('XL2596S-5.0', [
|
||||
['输入电压范围', '4.5~40', 'V', '客户上限 40V'],
|
||||
['输出电压', '5.0', 'V', '固定版'],
|
||||
['输出电流', '3', 'A', '满载能力'],
|
||||
['开关频率', '150', 'kHz', '影响外围'],
|
||||
['静态电流', '4.2', 'mA', '待机功耗'],
|
||||
['工作温度', '-40~+125', '℃', '工业温域'],
|
||||
['使能阈值', '未披露', 'V', '原厂未给'],
|
||||
]),
|
||||
/*
|
||||
* **真走「新建芯片类型」这条路,两步走**(Task 7b,2026-08-29 补齐件①的编辑面之后):
|
||||
* `parts.chip_type.type_id` 是自增列(`auto:true`,声明 `text`)—— 件上那颗静态种子行
|
||||
* 只收 `type_name`/`description`,`type_id` 由 `autoKeys()` 续号铸出来,写之前**不知道**
|
||||
* 铸出来的号是什么;`parts.type_param`/`parts.profile` 引用的正是这个号(`refersTo`
|
||||
* 挡着,写一个编造的号会被当场拒)。与真人在浏览器里「先建类型 → 保存 → 新类型出现在
|
||||
* 下拉里 → 再选它」同一个两步顺序,不是抄近道 —— 只是脚本自己读回来,不用眼睛看下拉。
|
||||
*/
|
||||
customFill: async (taskId) => {
|
||||
// ① 新建芯片类型:表尾静态种子行只填名称与说明,type_id 留空走 autoKeys() 续号
|
||||
await api(`/api/desk/tasks/${taskId}/table`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
tables: {
|
||||
'parts.chip_type': [
|
||||
{
|
||||
type_name: 'DC-DC 降压稳压器(评估自定义)',
|
||||
description: '本次评估自定义的降压转换器类型,核心参数字段清单沿用原类型口径,供原 PN 与候选料共用比对。',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
// ② 读回基座续出的 type_id —— 与真人「保存后新类型出现在下拉里」看到的是同一份数据
|
||||
const after = await api(`/api/gov/testbed/activity/${taskId}`);
|
||||
const minted = draftRows(after.body.drafts ?? [], 'parts.chip_type').find(
|
||||
(r) => String(r['type_name'] ?? '') === 'DC-DC 降压稳压器(评估自定义)',
|
||||
);
|
||||
if (!minted) throw new Error('候选缺库·新建类型:parts.chip_type 新行没落下 —— 新建类型这条路没打通');
|
||||
const typeId = String(minted['type_id'] ?? '');
|
||||
console.log(` 新建类型已保存,基座续号 type_id=${typeId}`);
|
||||
// ③ 核心参数字段清单(逐字照抄 type_id=1 的字典行,见 seed-mvp-data.ts)+
|
||||
// 两颗料都转挂新类型(distinct 要求同一个值)+ 候选料的参数值。
|
||||
// LM2596S-5.0 的既有参数值按同名 lookup 自动带过来,不必在这里重填。
|
||||
await api(`/api/desk/tasks/${taskId}/table`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
tables: {
|
||||
'parts.type_param': [
|
||||
{ type_id: typeId, param_name: '输入电压范围', param_desc: '芯片允许的输入直流电压区间。候选料的上限低于应用输入上限就是硬伤,烧的是芯片。', constraint_type: 'hard', param_unit: 'V' },
|
||||
{ type_id: typeId, param_name: '输出电压', param_desc: '固定版给定值;可调版给可调区间,需外围反馈电阻网络设定。固定换可调要动外围。', constraint_type: 'hard', param_unit: 'V' },
|
||||
{ type_id: typeId, param_name: '输出电流', param_desc: '满载持续输出能力。低于原料即降额,负载重时会掉压或过热保护。', constraint_type: 'hard', param_unit: 'A' },
|
||||
{ type_id: typeId, param_name: '开关频率', param_desc: '决定外围电感与输出电容的取值。频率变了外围多半要跟着换,不是换颗料就完。', constraint_type: 'soft', param_unit: 'kHz' },
|
||||
{ type_id: typeId, param_name: '静态电流', param_desc: '空载待机时自身消耗。电池供电场景敏感,市电场景通常不敏感。', constraint_type: 'soft', param_unit: 'mA' },
|
||||
{ type_id: typeId, param_name: '工作温度', param_desc: '保证参数指标的环境温度区间。工业温域窄于应用要求就是硬伤。', constraint_type: 'hard', param_unit: '℃' },
|
||||
{ type_id: typeId, param_name: '使能阈值', param_desc: 'ON/OFF 引脚的开关门限。上游用 MCU 电平直驱时,阈值对不上会关不掉或开不起来。', constraint_type: 'hard', param_unit: 'V' },
|
||||
],
|
||||
'parts.profile': [
|
||||
{ internal_pn: 'LM2596S-5.0', chip_type_id: typeId },
|
||||
{
|
||||
internal_pn: 'XL2596S-5.0',
|
||||
chip_type_id: typeId,
|
||||
brand_name: 'XLSEMI',
|
||||
manufacturer_name: 'XLSEMI',
|
||||
specification_description: '降压型 DCDC 稳压器,3A,固定 5.0V,150kHz',
|
||||
lifecycle_status: 'active',
|
||||
datasheet_ref: 'https://www.xlsemi.com/datasheet/XL2596S.pdf',
|
||||
datasheet_version: 'Rev C',
|
||||
},
|
||||
],
|
||||
'parts.parameter': paramRows('XL2596S-5.0', [
|
||||
['输入电压范围', '4.5~40', 'V', '客户上限 40V'],
|
||||
['输出电压', '5.0', 'V', '固定版'],
|
||||
['输出电流', '3', 'A', '满载能力'],
|
||||
['开关频率', '150', 'kHz', '影响外围'],
|
||||
['静态电流', '4.2', 'mA', '待机功耗'],
|
||||
['工作温度', '-40~+125', '℃', '工业温域'],
|
||||
['使能阈值', '未披露', 'V', '原厂未给'],
|
||||
]),
|
||||
},
|
||||
}),
|
||||
});
|
||||
},
|
||||
// 使能阈值未披露 → UNKNOWN → 有条件推荐(规则算的,不是模型说的)
|
||||
expectRevised: { address: 'substitution.relation', where: 'candidate_internal_pn', equals: 'XL2596S-5.0', column: 'recommendation_level', contains: 'conditional' },
|
||||
@@ -809,13 +841,22 @@ async function api(path, init = {}) {
|
||||
}
|
||||
|
||||
async function login() {
|
||||
if (DIRECT_BEARER !== '') {
|
||||
if (bearer === '') console.log(' 身份:JCAI_E2E_BEARER 直供的 token(不续期;卡会落到这枚 token 主人的工作台)');
|
||||
const svcReady = KC_URL !== '' && E2E_CLIENT_ID !== '' && E2E_CLIENT_SECRET !== '';
|
||||
/*
|
||||
* **服务账号三个变量全齐时走正路,`JCAI_E2E_BEARER` 只在缺任意一个时才兜底**
|
||||
* (2026-08-30 改序,见 `DIRECT_BEARER` 声明处的大注释)—— 从前 `DIRECT_BEARER !== ''`
|
||||
* 无条件优先,环境里留着一枚旧 token 就会悄悄绕开服务账号一整轮,还只有一行不起眼的日志。
|
||||
*/
|
||||
if (!svcReady && DIRECT_BEARER !== '') {
|
||||
if (bearer === '') {
|
||||
console.log(' ⚠️ 身份:JCAI_E2E_BEARER 直供的 token(服务账号三个变量缺了至少一个,降级用它)——');
|
||||
console.log(' 不续期;卡会落到这枚 token 主人的真实工作台上,不是「绝不派到用户工作台」那条正路。');
|
||||
}
|
||||
bearer = `Bearer ${DIRECT_BEARER}`;
|
||||
bearerAt = Date.now();
|
||||
return;
|
||||
}
|
||||
if (KC_URL === '' || E2E_CLIENT_ID === '' || E2E_CLIENT_SECRET === '') {
|
||||
if (!svcReady) {
|
||||
throw new Error('缺 KEYCLOAK_URL / KEYCLOAK_E2E_CLIENT_ID / KEYCLOAK_E2E_CLIENT_SECRET —— e2e 靠服务账号拿 token(ADR-012);临时可用 JCAI_E2E_BEARER 直供一枚');
|
||||
}
|
||||
const res = await fetch(`${KC_URL}/realms/${KC_REALM}/protocol/openid-connect/token`, {
|
||||
@@ -1230,6 +1271,8 @@ async function runCase(suite, kase) {
|
||||
let dataCardSeen = false;
|
||||
/** w05「类型不一致·硬拦」:`sub_eval.case.gate_reason` 命中过 `kase.expectGateReason` 那个词 */
|
||||
let gateReasonMatched = false;
|
||||
/** `kase.customFill` 只跑一次(w05「候选缺库·新建类型」:两阶段写 —— 先铸 type_id,读回来再引用它) */
|
||||
let customFilled = false;
|
||||
const deadline = Date.now() + 20 * 60 * 1000;
|
||||
let last;
|
||||
|
||||
@@ -1356,6 +1399,18 @@ async function runCase(suite, kase) {
|
||||
: ` ⚠️ 缺着必填还放过去了:提交闸失效`,
|
||||
);
|
||||
}
|
||||
/*
|
||||
* **两阶段自定义填表**(w05「候选缺库·新建类型」专用)—— 在空手交卷的闸测过**之后**、
|
||||
* 常规 `buildEdits` 之前跑一次:先铸 `parts.chip_type` 的自增 `type_id`,读回来才知道
|
||||
* 引用它的 `parts.type_param`/`parts.profile` 该填哪个值(与真人在浏览器里「先建类型 →
|
||||
* 保存 → 新类型出现在下拉里 → 再选它」的两步顺序同构,不是脚本抄近道)。跑过一次就不再跑,
|
||||
* 跑完之后 `buildEdits` 对这张卡照常算(该案例没有别的 `stageEdits`,算出来是空,
|
||||
* 只会去交卷)。
|
||||
*/
|
||||
if (kase.customFill && !customFilled) {
|
||||
customFilled = true;
|
||||
await kase.customFill(taskId);
|
||||
}
|
||||
const lines = buildEdits(stage, body.drafts ?? [], kase);
|
||||
// 人加行:与真人在件上「加一行」同一条链 —— 新主键的行落进保存载荷即是新增
|
||||
if (kase.adds && kase.adds.address === stage.address && !addedRows) {
|
||||
|
||||
Reference in New Issue
Block a user