@kkeve/suite 0.0.19 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,7 +9,7 @@
9
9
  * 分层:DB 装载 / 内嵌场景展开 / 元素库 source 跳解析 / 变量表查询全部留在
10
10
  * 调用方(见 eve-backend 的 dslLoader)。这里只做纯函数投影。
11
11
  */
12
- export { toDecorator, type DslHop, type DslValue, type DslExpectation, type DslStep, type DslVariableRow, type ToDecoratorOptions, } from './toDecorator.js';
12
+ export { toDecorator, stepExpectedText, type DslHop, type DslValue, type DslExpectation, type DslStep, type DslVariableRow, type ExpectedRenderContext, type ToDecoratorOptions, } from './toDecorator.js';
13
13
  /** 语义别名:调用方按「生成一份装饰器 spec」的语义引用时更直观 */
14
14
  export { toDecorator as generateDecoratorSpec } from './toDecorator.js';
15
15
  export { STEP_OPERATIONS, ASSERT_OPERATIONS, TEXT_ONLY_ASSERT_OPERATIONS, EXPECTED_VALUE_CTYPES, COMPARISON_EXPRESSIONS, isKnownOperation, isKnownAssert, isTextOnlyAssert, parseComparison, type StepOperation, } from './ops.js';
package/dist/dsl/index.js CHANGED
@@ -11,9 +11,10 @@
11
11
  * 调用方(见 eve-backend 的 dslLoader)。这里只做纯函数投影。
12
12
  */
13
13
  Object.defineProperty(exports, "__esModule", { value: true });
14
- exports.parseComparison = exports.isTextOnlyAssert = exports.isKnownAssert = exports.isKnownOperation = exports.COMPARISON_EXPRESSIONS = exports.EXPECTED_VALUE_CTYPES = exports.TEXT_ONLY_ASSERT_OPERATIONS = exports.ASSERT_OPERATIONS = exports.STEP_OPERATIONS = exports.generateDecoratorSpec = exports.toDecorator = void 0;
14
+ exports.parseComparison = exports.isTextOnlyAssert = exports.isKnownAssert = exports.isKnownOperation = exports.COMPARISON_EXPRESSIONS = exports.EXPECTED_VALUE_CTYPES = exports.TEXT_ONLY_ASSERT_OPERATIONS = exports.ASSERT_OPERATIONS = exports.STEP_OPERATIONS = exports.generateDecoratorSpec = exports.stepExpectedText = exports.toDecorator = void 0;
15
15
  var toDecorator_js_1 = require("./toDecorator.js");
16
16
  Object.defineProperty(exports, "toDecorator", { enumerable: true, get: function () { return toDecorator_js_1.toDecorator; } });
17
+ Object.defineProperty(exports, "stepExpectedText", { enumerable: true, get: function () { return toDecorator_js_1.stepExpectedText; } });
17
18
  /** 语义别名:调用方按「生成一份装饰器 spec」的语义引用时更直观 */
18
19
  var toDecorator_js_2 = require("./toDecorator.js");
19
20
  Object.defineProperty(exports, "generateDecoratorSpec", { enumerable: true, get: function () { return toDecorator_js_2.toDecorator; } });
@@ -96,3 +96,20 @@ export interface ToDecoratorOptions {
96
96
  }
97
97
  /** 生成装饰器脚本源码(UTF-8 文本) */
98
98
  export declare function toDecorator(options: ToDecoratorOptions): string;
99
+ /** 计算 expected 文案所需的最小上下文(与脚本投影共用,保证报告与脚本同文案) */
100
+ export interface ExpectedRenderContext {
101
+ env: string;
102
+ variableList?: DslVariableRow[];
103
+ account?: {
104
+ name: string;
105
+ password: string;
106
+ };
107
+ }
108
+ /**
109
+ * 步骤 expected 文案的**公开**入口。
110
+ *
111
+ * 投影脚本(toDecorator)与报告 outline(run-dsl-task / report-data)必须产出
112
+ * 同一份文案,否则会出现「报告的预期」与「评测 Agent 看到的预期」不一致 ——
113
+ * 那正是「报错4」这类标签被当成字面量去页面搜索的根因。调用方统一走这里。
114
+ */
115
+ export declare function stepExpectedText(step: DslStep, ctx: ExpectedRenderContext): string;
@@ -21,6 +21,7 @@
21
21
  */
22
22
  Object.defineProperty(exports, "__esModule", { value: true });
23
23
  exports.toDecorator = toDecorator;
24
+ exports.stepExpectedText = stepExpectedText;
24
25
  const ops_js_1 = require("./ops.js");
25
26
  // ─── 生成入口 ────────────────────────────────────────────────────────
26
27
  /** 生成装饰器脚本源码(UTF-8 文本) */
@@ -96,8 +97,7 @@ function renderStep(d, ctx) {
96
97
  }
97
98
  const out = [];
98
99
  const stepText = String(d.text || op);
99
- const expTexts = (d.expectation || []).map((e) => String(e.text || '')).filter(Boolean);
100
- const expected = expTexts.length ? expTexts.join(';') : '操作完成,页面正常响应';
100
+ const expected = expectedTextOf(d.expectation || [], (e) => resolveExpectationValue(e, ctx));
101
101
  const chain = chainExpr(d.context);
102
102
  const expLines = [];
103
103
  for (const e of d.expectation || []) {
@@ -106,7 +106,7 @@ function renderStep(d, ctx) {
106
106
  expLines.push(a);
107
107
  }
108
108
  const emit = (body) => {
109
- out.push(`await keveGoal({ step: ${q(stepText)}, expected: ${q(expected)}${reactOption(d)} }, async () => {`);
109
+ out.push(`await keveGoal({ step: ${q(stepText)}, expected: ${q(expected)} }, async () => {`);
110
110
  for (const b of [...body, ...expLines])
111
111
  out.push(` ${b}`);
112
112
  out.push(`});`);
@@ -232,7 +232,7 @@ function renderStep(d, ctx) {
232
232
  // 区域截图容错:元素不在当前页面(权限阻断等)时降级为整页截图,
233
233
  // 是否一致由基线比对决定,截图本身不做权限硬门。
234
234
  if (shotChain) {
235
- out.push(`await keveGoal({ step: ${q(stepText)}, expected: ${q(expected)}${reactOption(d)} }, async () => {`);
235
+ out.push(`await keveGoal({ step: ${q(stepText)}, expected: ${q(expected)} }, async () => {`);
236
236
  out.push(` try {`);
237
237
  out.push(` await engine.screenshot(${q(shotName)}, ${shotChain});`);
238
238
  out.push(` } catch (_shotErr: any) {`);
@@ -275,7 +275,8 @@ function renderOpenPageWithGates(d, gates, ctx) {
275
275
  const stepText = String(d.text || 'OPEN_PAGE');
276
276
  const gateTexts = gates.map((g) => String(g.text || '')).filter(Boolean);
277
277
  const gateExpects = gates
278
- .flatMap((g) => (g.expectation || []).map((e) => String(e.text || '')))
278
+ .filter((g) => (g.expectation || []).length > 0)
279
+ .map((g) => expectedTextOf(g.expectation || [], (e) => resolveExpectationValue(e, ctx)))
279
280
  .filter(Boolean);
280
281
  const expected = [...gateTexts, ...gateExpects].filter(Boolean).join(';') || '打开目标页面并等待就绪响应';
281
282
  const resolved = resolveOpenPage(d, ctx);
@@ -299,51 +300,143 @@ function renderOpenPageWithGates(d, gates, ctx) {
299
300
  body.push(`await __gate${gi}; // 等 ${String(g.text || '就绪响应')}`);
300
301
  });
301
302
  }
302
- out.push(`await keveGoal({ step: ${q(stepText)}, expected: ${q(expected)}${reactOption(d)} }, async () => {`);
303
+ out.push(`await keveGoal({ step: ${q(stepText)}, expected: ${q(expected)} }, async () => {`);
303
304
  for (const b of body)
304
305
  out.push(` ${b}`);
305
306
  out.push(`});`);
306
307
  return out;
307
308
  }
308
- // ─── 断言转译 ────────────────────────────────────────────────────────
309
309
  /**
310
- * 步骤是否只有「纯文案期望」,必须依赖 LLM 才能判定。
310
+ * 步骤 expected 文案的**公开**入口。
311
311
  *
312
- * 平台 DSL 里有两类 expectation:
313
- * - 可执行断言(ELEMENT_NOT_EXIST / TEXT_EXIST …)→ 转译成 engine 断言,
314
- * 成败在脚本里已经确定,无需再看页面;
315
- * - 纯文案载体(TEXT / 无 type)→ 不产生任何断言,只以文字描述预期,
316
- * 脚本无法自证,只能交给 Agent 观察页面后判定。
312
+ * 投影脚本(toDecorator)与报告 outline(run-dsl-task / report-data)必须产出
313
+ * 同一份文案,否则会出现「报告的预期」与「评测 Agent 看到的预期」不一致 ——
314
+ * 那正是「报错4」这类标签被当成字面量去页面搜索的根因。调用方统一走这里。
315
+ */
316
+ function stepExpectedText(step, ctx) {
317
+ const renderCtx = {
318
+ env: ctx.env,
319
+ variableList: ctx.variableList || [],
320
+ account: ctx.account,
321
+ };
322
+ return expectedTextOf(step.expectation || [], (e) => resolveExpectationValue(e, renderCtx));
323
+ }
324
+ /**
325
+ * 步骤的 expected 文案 —— 喂给评测 Agent 判断「预期是否真的达成」。
326
+ *
327
+ * 平台 expectation[].text 常常只是录制期标签(「报错1」「报错4」),不携带
328
+ * 任何可判定语义;真实预期藏在同一条 expectation 的 type + value + context 里
329
+ * (如 ELEMENT_NOT_EXIST + includes「业务逻辑错误」= 页面不应出现该错误)。
330
+ * 只把标签交给评测 Agent,它会去页面上找字面量「报错4」并据此误判失败。
317
331
  *
318
- * 只有第二类(且该步骤没有可执行断言)才需要保留 Agent Re-Act。
332
+ * 因此按「可执行断言语义优先、标签作为备注」组装:
333
+ * - 有可执行断言 → 渲染成人话(页面不存在包含「业务逻辑错误」的内容);
334
+ * - 纯文案载体(TEXT / 无 type)→ 标签本身就是人话,直接采用;
335
+ * - 一个断言都渲染不出来时,退回平台标签,最后兜底为通用文案。
319
336
  */
320
- function needsAgentJudge(d) {
321
- const expects = d.expectation || [];
322
- if (!expects.length)
323
- return false;
324
- const hasExecutable = expects.some((e) => {
337
+ function expectedTextOf(expectations, resolveValue) {
338
+ const semantic = [];
339
+ const labelOnly = [];
340
+ for (const e of expectations) {
341
+ const label = String(e.text || '').trim();
325
342
  const type = String(e.type || '');
326
- return type && !(0, ops_js_1.isTextOnlyAssert)(type);
327
- });
328
- if (hasExecutable)
329
- return false;
330
- return expects.some((e) => {
331
- const type = String(e.type || '');
332
- return !type || (0, ops_js_1.isTextOnlyAssert)(type);
343
+ if (!type || (0, ops_js_1.isTextOnlyAssert)(type)) {
344
+ if (label)
345
+ labelOnly.push(label);
346
+ continue;
347
+ }
348
+ const phrase = describeExpectation(e, resolveValue(e));
349
+ // 未知断言在 renderExpectation 里会确定性报错;这里不能让 expected 抢先崩,
350
+ // 退化成标签即可,真正的失败由断言渲染抛出。
351
+ if (phrase)
352
+ semantic.push(label && !phrase.includes(label) ? `${phrase}(${label})` : phrase);
353
+ else if (label)
354
+ labelOnly.push(label);
355
+ }
356
+ const parts = [...semantic, ...labelOnly].filter(Boolean);
357
+ return parts.length ? parts.join(';') : '操作完成,页面正常响应';
358
+ }
359
+ /** 单条可执行断言 → 人类可读预期;返回 null 表示无法给出语义(由调用方兜底) */
360
+ function describeExpectation(e, val) {
361
+ const type = String(e.type || '');
362
+ const target = describeChain(e.context);
363
+ const value = String(val ?? '');
364
+ switch (type) {
365
+ case 'ELEMENT_EXIST':
366
+ return `页面存在${target}`;
367
+ case 'ELEMENT_NOT_EXIST':
368
+ return `页面不存在${target}`;
369
+ case 'ELEMENT_VISIBLE':
370
+ return `${target}可见`;
371
+ case 'ELEMENT_NOT_VISIBLE':
372
+ return `${target}不可见`;
373
+ case 'TEXT_EXIST':
374
+ if (isPageTitleAssertion(e))
375
+ return `页面标题包含「${value}」`;
376
+ return `页面存在包含「${value}」的内容${target}`;
377
+ case 'TEXT_NOT_EXIST':
378
+ if (!value.trim())
379
+ return null;
380
+ return `页面不存在包含「${value}」的内容${target}`;
381
+ case 'TEXT_EQUAL':
382
+ return `元素${target}文本等于「${value}」`;
383
+ case 'EXPECTED_VALUE': {
384
+ const ctype = String(e.ctype || 'text').toLowerCase();
385
+ const { mode, negated } = (0, ops_js_1.parseComparison)(String(e.cexpression || 'include'));
386
+ const dim = { text: '文本', class: '类名', placeholder: '占位提示', style: '样式', index: '元素个数' }[ctype] || ctype;
387
+ if (ctype === 'index')
388
+ return `元素${target}个数等于 ${value}`;
389
+ const op = mode === 'eq' ? (negated ? '不等于' : '等于') : (negated ? '不包含' : '包含');
390
+ return `元素${target}的${dim}${op}「${value}」`;
391
+ }
392
+ case 'EXPECTED__PAGET_VALUE': {
393
+ const ctype = String(e.ctype || 'url').toLowerCase();
394
+ const { mode, negated } = (0, ops_js_1.parseComparison)(String(e.cexpression || 'eq'));
395
+ const dim = ctype === 'title' ? '标题' : '地址';
396
+ const op = mode === 'eq' ? (negated ? '不等于' : '等于') : (negated ? '不包含' : '包含');
397
+ return `页面${dim}${op}「${value}」`;
398
+ }
399
+ case 'SCREEN_SHOT_COMPARE':
400
+ case 'SCREENSHOT_COMPARE':
401
+ case 'IMAGE_COMPARE':
402
+ return `页面截图与基线「${value || e.text || ''}」一致`;
403
+ default:
404
+ return null;
405
+ }
406
+ }
407
+ /** 定位链 → 人类可读描述(评测 Agent 据此知道「在哪个区域」观察) */
408
+ function describeChain(context) {
409
+ let hops;
410
+ try {
411
+ hops = collectHops(context);
412
+ }
413
+ catch {
414
+ return '';
415
+ }
416
+ if (!hops.length)
417
+ return '';
418
+ // 单跳文本包含:平台最常见的「页面不应出现某文案」录制形态
419
+ if (hops.length === 1 && (hops[0].type === 'includes' || hops[0].type === 'text')) {
420
+ return `包含「${hops[0].value.trim()}」的元素`;
421
+ }
422
+ const parts = hops.map((h) => {
423
+ const text = h.value.trim();
424
+ if (h.type === 'includes' || h.type === 'text')
425
+ return `包含「${text}」`;
426
+ if (h.type === 'selector')
427
+ return `选择器「${text}」`;
428
+ return `${h.type}=「${text}」`;
333
429
  });
430
+ return `(${parts.join(' → ')})`;
334
431
  }
335
432
  /**
336
- * keveGoal 的 react 参数片段。
433
+ * 渲染一条 expectation 为 engine 断言;返回 null 表示「纯文案载体,不产生断言」。
337
434
  *
338
- * 平台 DSL 是确定性录制产物:凡能自证的步骤都显式 react: false,跳过 Agent
339
- * Re-Act —— 否则每个步骤都会额外发起一轮 LLM 探索,既拖长整体耗时至超出
340
- * Cypress 用例超时(表现为「任务卡住」),也会让「报错1」这类仅作平台标签的
341
- * expected 文案参与最终结论。
435
+ * 注意投影脚本**不输出 `react: false`**:可执行断言只证明预期的一个子集,
436
+ * fn 执行成功不等于 expected 已达成,所以每个 goal 都保留评测阶段 —— Agent
437
+ * 先观察真实页面评测,达成即放行,未达成再继续探索;断言失败由引擎侧统一
438
+ * 参与终局覆盖,不允许被评审判成 pass。
342
439
  */
343
- function reactOption(d) {
344
- return needsAgentJudge(d) ? '' : ', react: false';
345
- }
346
- /** 渲染一条 expectation;返回 null 表示「纯文案载体,不产生断言」 */
347
440
  function renderExpectation(e, ctx) {
348
441
  const type = String(e.type || '');
349
442
  // 无 type / TEXT:纯「期望文案」载体,只喂 keveGoal.expected,不是断言
@@ -17,16 +17,6 @@ export interface KeveGoalCallOptions {
17
17
  precondition?: string;
18
18
  step: string;
19
19
  expected: string;
20
- /**
21
- * 是否在 fn 之后追加一轮 Agent Re-Act 做最终判定。
22
- *
23
- * 默认 true(AI 场景 / 手写用例):fn 只是前置动作与确定性校验,预期是否真正
24
- * 达成由 Agent 观察真实页面后判定。
25
- *
26
- * DSL 投影脚本会显式传 false:整条链路的判定已由确定性断言给出,Re-Act 只会
27
- * 额外消耗 LLM 预算,并让「报错1」这类平台标签参与结论。
28
- */
29
- react?: boolean;
30
20
  }
31
21
  /** agent Re-Act 的结果(P0-3 接入;未接入时由 fn 结果直接判定) */
32
22
  export interface CyGoalExploreResult {
@@ -75,7 +75,6 @@ function createCyKeveGoal(engine) {
75
75
  let fnErrorRaw;
76
76
  let fnResult;
77
77
  let fnBlocked = false;
78
- const reactEnabled = options.react !== false;
79
78
  const fnSource = fn?.toString() || undefined;
80
79
  const owned = !(0, shotBuffer_js_1.isGoalShotDiffCaptureActive)();
81
80
  if (owned)
@@ -140,31 +139,12 @@ function createCyKeveGoal(engine) {
140
139
  };
141
140
  console.log(`[keveGoal] 📊 "${options.step}" 截图差异超阈值 — 确定性失败`);
142
141
  }
143
- else if (!reactEnabled) {
144
- // 确定性模式:断言是否通过就是结论,不再发起 LLM 探索。
145
- reactResult = {
146
- expectedMet: !fnError,
147
- conclusion: fnError ? 'fail' : 'pass',
148
- actions: [{
149
- action: {
150
- tool: 'done',
151
- verdict: fnError ? 'fail' : 'pass',
152
- text: fnError ? `Assertion failed: ${fnError}` : '确定性断言全部通过',
153
- },
154
- toolOutput: '',
155
- result: 'ok',
156
- }],
157
- agentScreenshots: [],
158
- };
159
- logLine(fnError
160
- ? `[keveGoal] ⚙️ "${options.step}" 确定性模式:fn 失败,跳过 Agent Re-Act`
161
- : `[keveGoal] ⚙️ "${options.step}" 确定性模式:fn 通过,跳过 Agent Re-Act`);
162
- }
163
142
  else {
164
- // fn 成功 / 失败都交给探索器(若引擎入口注册了 AI Re-Act)。
165
- // fn 只是前置确定性校验,@keveGoal 的预期是否真正达成必须由 Agent
166
- // 观察真实页面后给出最终结论 —— 即便 fn 已通过,也要产出推理轨迹与
167
- // 执行后截图,否则报告里会缺少 AI 推理日志。
143
+ // fn 成功 / 失败都交给评测器。fn 只是前置动作与确定性校验:
144
+ // - fn 成功 ≠ expected 达成(fn 可能只覆盖预期的一个子集);
145
+ // - fn 失败 ≠ 不可恢复(普通运行时错误下页面可能仍可探索自愈)。
146
+ // 「先评测、再决定放行还是探索」由这一轮承担:Agent 先观察真实页面评测
147
+ // expected,达成即 done(pass) 放行,未达成再继续探索。
168
148
  let explored;
169
149
  if (explorer) {
170
150
  try {
@@ -32,13 +32,6 @@ export interface KeveGoalCallOptions {
32
32
  precondition?: string;
33
33
  step: string;
34
34
  expected: string;
35
- /**
36
- * 是否在 fn 之后追加一轮 Agent Re-Act 做最终判定。
37
- *
38
- * 默认 true(AI 场景 / 手写用例);DSL 投影脚本传 false,判定完全由确定性
39
- * 断言给出,避免为每个步骤额外消耗一轮 LLM。
40
- */
41
- react?: boolean;
42
35
  }
43
36
  type KeveFixture = {
44
37
  keveGoal: (options: KeveGoalCallOptions, fn?: () => Promise<void>) => Promise<void>;
@@ -189,7 +189,6 @@ exports.test = test_1.test.extend({
189
189
  let fnErrorRaw; // 原始错误对象(ScreenshotDiffError 等带结构的错误需要保留结构)
190
190
  let fnResult; // fn 返回值透传(flow 产物,如 createDashboard 的看板名)
191
191
  let fnBlocked = false; // true when fn error is environment-blocked (skip agent Re-Act)
192
- const reactEnabled = options.react !== false;
193
192
  // 截图对比:fn 前开缓冲、fn 后收集 —— engine.expectScreenshot 的结果经此进 attachment
194
193
  (0, screenshotDiff_js_1.beginGoalShotDiffCapture)();
195
194
  if (fn) {
@@ -298,27 +297,6 @@ exports.test = test_1.test.extend({
298
297
  conclusion: 'fail',
299
298
  };
300
299
  }
301
- else if (!reactEnabled) {
302
- // 确定性模式(DSL 投影脚本):断言结果即结论,不再发起 LLM 探索。
303
- console.log(fnError
304
- ? `[keveGoal] ⚙️ "${options.step}" deterministic mode: fn failed — skipping agent Re-Act`
305
- : `[keveGoal] ⚙️ "${options.step}" deterministic mode: fn passed — skipping agent Re-Act`);
306
- reactResult = {
307
- expectedMet: !fnError,
308
- actions: [{
309
- action: {
310
- tool: 'done',
311
- verdict: fnError ? 'fail' : 'pass',
312
- text: fnError ? `Assertion failed: ${fnError}` : '确定性断言全部通过',
313
- },
314
- toolOutput: '',
315
- result: 'ok',
316
- }],
317
- finalSnapshot: '',
318
- agentScreenshots: [],
319
- conclusion: fnError ? 'fail' : 'pass',
320
- };
321
- }
322
300
  else {
323
301
  // ── Per-goal timeout: prevent one slow goal from exhausting the test timeout ──
324
302
  // Default 300s per goal; total test timeout should be ≥ (goals × 300s + overhead)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kkeve/suite",
3
- "version": "0.0.19",
3
+ "version": "0.0.20",
4
4
  "description": "AI-powered full-stack test agent CLI - static analysis, unit test, component test, E2E test, execute, evaluate, and report",
5
5
  "license": "MIT",
6
6
  "bin": {
@@ -134,8 +134,12 @@ function normalizeStep(step) {
134
134
  * 投影 DSL 步骤为报告 outline,语义与 toDecorator 的 keveGoal 参数保持一致。
135
135
  * OPEN_PAGE 后紧跟的 WAIT_RESPONSE 会合并进同一个导航步骤,避免报告步骤数
136
136
  * 与脚本实际 keveGoal 数不一致。
137
+ *
138
+ * expected 文案统一走 stepExpectedText(与脚本投影同一个函数):报告里展示的
139
+ * 预期与评测 Agent 读到的预期必须逐字一致,否则平台标签(「报错4」)会在报告
140
+ * 与评测两侧产生不同解读。
137
141
  */
138
- function buildCaseSteps(dslList) {
142
+ function buildCaseSteps(dslList, ctx, stepExpectedText) {
139
143
  const steps = [];
140
144
  for (let i = 0; i < dslList.length; i++) {
141
145
  const d = dslList[i];
@@ -149,7 +153,8 @@ function buildCaseSteps(dslList) {
149
153
  if (gates.length) {
150
154
  const gateTexts = gates.map((g) => String(g?.text || '')).filter(Boolean);
151
155
  const gateExpects = gates
152
- .flatMap((g) => (g?.expectation || []).map((e) => String(e?.text || '')))
156
+ .filter((g) => (g?.expectation || []).length > 0)
157
+ .map((g) => stepExpectedText(g, ctx))
153
158
  .filter(Boolean);
154
159
  steps.push({
155
160
  step: String(d?.text || 'OPEN_PAGE'),
@@ -160,8 +165,7 @@ function buildCaseSteps(dslList) {
160
165
  }
161
166
  }
162
167
  const stepText = String(d?.text || op || '');
163
- const expTexts = (d?.expectation || []).map((e) => String(e?.text || '')).filter(Boolean);
164
- const expected = expTexts.length ? expTexts.join(';') : '操作完成,页面正常响应';
168
+ const expected = stepExpectedText(d, ctx);
165
169
  if (!stepText) continue;
166
170
  steps.push({ step: stepText, expected });
167
171
  }
@@ -217,7 +221,7 @@ async function main() {
217
221
  fs.rmSync(resultFile, { force: true });
218
222
  fs.rmSync(manifestFile, { force: true });
219
223
 
220
- const { toDecorator } = await import(
224
+ const { toDecorator, stepExpectedText } = await import(
221
225
  url.pathToFileURL(path.join(suiteRoot, 'dist/dsl/index.js')).href
222
226
  );
223
227
 
@@ -233,20 +237,26 @@ async function main() {
233
237
  const importFrom = engine === 'cypress'
234
238
  ? '@kkeve/suite/engine-cypress'
235
239
  : '@kkeve/suite/keve-test';
240
+ const account = resolveAccount(item.accountInfo || input.accountInfo);
241
+ const renderCtx = {
242
+ env: input.env || 'test',
243
+ variableList: Array.isArray(item.variableList) ? item.variableList : [],
244
+ account,
245
+ };
236
246
  const code = toDecorator({
237
247
  stepGroupId,
238
248
  caseName: item.caseName || `用例 ${stepGroupId}`,
239
249
  dslList,
240
- env: input.env || 'test',
241
- variableList: Array.isArray(item.variableList) ? item.variableList : [],
242
- account: resolveAccount(item.accountInfo || input.accountInfo),
250
+ env: renderCtx.env,
251
+ variableList: renderCtx.variableList,
252
+ account: renderCtx.account,
243
253
  ssoInject: input.ssoInject !== false,
244
254
  importFrom,
245
255
  });
246
256
 
247
257
  const fileName = `sg${stepGroupId}.spec.ts`;
248
258
  fs.writeFileSync(path.join(specDir, fileName), code, 'utf-8');
249
- const dslSteps = buildCaseSteps(dslList);
259
+ const dslSteps = buildCaseSteps(dslList, renderCtx, stepExpectedText);
250
260
  caseEntries.push({
251
261
  caseId: `S${stepGroupId}`,
252
262
  stepGroupId,