dsh-scout 0.9.3 → 0.9.4
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.
- package/CHANGELOG.md +39 -1
- package/README.en.md +18 -14
- package/README.md +18 -14
- package/dist/i18n.d.ts +157 -1
- package/dist/i18n.js +354 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +339 -88
- package/dist/model.d.ts +62 -9
- package/dist/model.js +655 -134
- package/docs/dsh-scout-product-contract.md +5 -3
- package/package.json +4 -1
- package/scripts/acceptance-report.mjs +622 -0
- package/scripts/profile-smoke.mjs +12 -15
- package/scripts/smoke-tool-call.mjs +43 -0
- package/tests/acceptance.test.mjs +57 -41
- package/tests/model.test.mjs +301 -69
- package/tests/package.test.mjs +6 -6
- package/tests/tools.test.mjs +382 -16
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* (launcher service) and the `fs` seam are available, and the plugin
|
|
13
13
|
* disposer runs on unload (`DSH_SCOUT_UNLOAD`);
|
|
14
14
|
* 3. REAL TOOL CALLS through the public runtime API
|
|
15
|
-
* `ctx.tools.execute(
|
|
15
|
+
* `ctx.tools.execute()` (legacy single call or executor batch), in a SIX-STEP
|
|
16
16
|
* scenario chain run across up to six real host processes:
|
|
17
17
|
* STEP 1 create: `scout_start` + `scout_add_source` (E3 registry origin)
|
|
18
18
|
* + `scout_add_claim` → each `isError === false`; `scout_start`
|
|
@@ -57,7 +57,7 @@
|
|
|
57
57
|
* exported `case.json` equals the decision reported before the restart and
|
|
58
58
|
* after rehydration and after the cross-language import.
|
|
59
59
|
*
|
|
60
|
-
* RUNTIME NOTE — failures arrive as RESULTS
|
|
60
|
+
* RUNTIME NOTE — tool failures arrive as RESULTS on the legacy API (measured on
|
|
61
61
|
* dsh 0.2.0-rc.2): a tool body that violates its declared `output.schema` does
|
|
62
62
|
* not reject `execute()`. The registry converts it to
|
|
63
63
|
* `{ isError: true, error: { message, info: { name: 'ToolOutputError',
|
|
@@ -65,7 +65,8 @@
|
|
|
65
65
|
* `toolErrorResult`, `lib/index.js:3541-3544` / `:3616-3622`). An unknown tool
|
|
66
66
|
* name behaves the same way with `ToolNotFoundError` / `UNKNOWN_TOOL`. Assertions
|
|
67
67
|
* must therefore inspect `result.isError` + `result.error.info`, and must not
|
|
68
|
-
* rely on `try`/`catch` around `execute()` to detect a rejected call.
|
|
68
|
+
* rely on `try`/`catch` around `execute()` to detect a rejected call. Batch
|
|
69
|
+
* infrastructure errors can reject; their bounded cause and partial call IDs are logged.
|
|
69
70
|
*
|
|
70
71
|
* WHAT THIS GATE DOES NOT COVER — read before trusting a green run:
|
|
71
72
|
* - no LLM and no agent loop: `execute()` runs with `agent` unset, so
|
|
@@ -125,6 +126,7 @@
|
|
|
125
126
|
* directory, and the temporary DSH home is removed in `finally`.
|
|
126
127
|
*/
|
|
127
128
|
import assert from 'node:assert/strict'
|
|
129
|
+
import { executeSmokeTool, describeSmokeError } from './smoke-tool-call.mjs'
|
|
128
130
|
import { spawnSync } from 'node:child_process'
|
|
129
131
|
import { mkdir, mkdtemp, readdir, readFile, realpath, rm, writeFile } from 'node:fs/promises'
|
|
130
132
|
import { tmpdir } from 'node:os'
|
|
@@ -287,6 +289,8 @@ function describeCall(entry, extra = []) {
|
|
|
287
289
|
if (entry?.skipped) lines.push(`skipped: ${entry.skipped}`)
|
|
288
290
|
if (result?.outcome === 'threw') {
|
|
289
291
|
lines.push(`threw: ${result.error?.name ?? 'Error'}: ${result.error?.message ?? '(no message)'}`)
|
|
292
|
+
if (result.error?.cause) lines.push(`cause: ${JSON.stringify(result.error.cause)}`)
|
|
293
|
+
if (result.error?.partial) lines.push(`partial: ${JSON.stringify(result.error.partial)}`)
|
|
290
294
|
if (result.error?.violations) lines.push(`violations: ${JSON.stringify(result.error.violations)}`)
|
|
291
295
|
if (result.error?.code) lines.push(`code: ${result.error.code}`)
|
|
292
296
|
} else if (result) {
|
|
@@ -366,12 +370,8 @@ const emit = (kind, payload) => {
|
|
|
366
370
|
process.stdout.write('${GATE_PREFIX}' + JSON.stringify(Object.assign({ kind: kind }, payload)) + '\\n')
|
|
367
371
|
}
|
|
368
372
|
|
|
369
|
-
const describeError =
|
|
370
|
-
|
|
371
|
-
message: error && error.message ? error.message : String(error),
|
|
372
|
-
code: error && error.code ? error.code : null,
|
|
373
|
-
violations: error && Array.isArray(error.violations) ? error.violations : null,
|
|
374
|
-
})
|
|
373
|
+
const describeError = ${describeSmokeError.toString()}
|
|
374
|
+
const executeSmokeTool = ${executeSmokeTool.toString()}
|
|
375
375
|
|
|
376
376
|
const waitForTool = async (ctx, name, deadline) => {
|
|
377
377
|
for (;;) {
|
|
@@ -385,12 +385,9 @@ const callTool = async (ctx, name, args) => {
|
|
|
385
385
|
callSequence += 1
|
|
386
386
|
const callId = 'smoke-' + name + '-' + callSequence
|
|
387
387
|
try {
|
|
388
|
-
const result = await
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
arguments: args,
|
|
392
|
-
signal: new AbortController().signal,
|
|
393
|
-
})
|
|
388
|
+
const result = await executeSmokeTool(
|
|
389
|
+
ctx.tools, callId, name, args, new AbortController().signal,
|
|
390
|
+
)
|
|
394
391
|
const content = result.content === undefined ? null : result.content
|
|
395
392
|
return {
|
|
396
393
|
outcome: 'returned',
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** Standalone helpers also embedded verbatim in the real-host verifier. */
|
|
2
|
+
export async function executeSmokeTool(tools, callId, name, args, signal) {
|
|
3
|
+
// setExecutor is the public provider seam in the batch API (0.2.2-alpha.2).
|
|
4
|
+
// Select before dispatch: retrying a failed mutation with another shape is unsafe.
|
|
5
|
+
const batchApi = typeof tools.setExecutor === 'function'
|
|
6
|
+
let result
|
|
7
|
+
if (batchApi) {
|
|
8
|
+
const batch = await tools.execute({
|
|
9
|
+
calls: [{ type: 'tool-call', id: callId, name, arguments: JSON.stringify(args) }],
|
|
10
|
+
signal,
|
|
11
|
+
})
|
|
12
|
+
if (!Array.isArray(batch?.results) || batch.results.length !== 1) {
|
|
13
|
+
throw new Error('Smoke batch must return exactly one result')
|
|
14
|
+
}
|
|
15
|
+
if (batch.results[0]?.callId !== callId) {
|
|
16
|
+
throw new Error('Smoke batch returned a mismatched callId')
|
|
17
|
+
}
|
|
18
|
+
result = batch.results[0].result
|
|
19
|
+
} else {
|
|
20
|
+
result = await tools.execute({ callId, name, arguments: args, signal })
|
|
21
|
+
}
|
|
22
|
+
if (!result || typeof result !== 'object' || typeof result.isError !== 'boolean') {
|
|
23
|
+
throw new Error('Smoke call must return a ToolExecutionResult with boolean isError')
|
|
24
|
+
}
|
|
25
|
+
return result
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function describeSmokeError(error, depth = 0) {
|
|
29
|
+
const summary = {
|
|
30
|
+
name: error?.name ?? typeof error,
|
|
31
|
+
message: String(error?.message ?? error).slice(0, 500),
|
|
32
|
+
code: error?.code ?? null,
|
|
33
|
+
violations: Array.isArray(error?.violations) ? error.violations : null,
|
|
34
|
+
}
|
|
35
|
+
if (error?.cause !== undefined && depth < 2) {
|
|
36
|
+
summary.cause = describeSmokeError(error.cause, depth + 1)
|
|
37
|
+
}
|
|
38
|
+
if (Array.isArray(error?.partial?.results)) {
|
|
39
|
+
// Preserve settlement identities, without dumping tool payloads or context.
|
|
40
|
+
summary.partial = { callIds: error.partial.results.map(item => item.callId) }
|
|
41
|
+
}
|
|
42
|
+
return summary
|
|
43
|
+
}
|
|
@@ -403,16 +403,19 @@ test('mutating tools render a compact summary while report and compare stay the
|
|
|
403
403
|
const reportBlocks = renderBlocks(harness, 'scout_report', await call(harness, 'scout_report', { caseId: 'budget' }, exec))
|
|
404
404
|
assert.ok(renderBytes(reportBlocks) > RENDER_BUDGET_BYTES, 'scout_report carries the full report')
|
|
405
405
|
const reportText = renderText(reportBlocks)
|
|
406
|
-
assert.match(reportText, /^# Example Co
|
|
406
|
+
assert.match(reportText, /^# Example Co$/m)
|
|
407
407
|
for (const heading of [
|
|
408
|
-
'##
|
|
409
|
-
'##
|
|
410
|
-
'##
|
|
411
|
-
'##
|
|
412
|
-
'##
|
|
413
|
-
'##
|
|
414
|
-
'##
|
|
415
|
-
'##
|
|
408
|
+
'## 30 秒结论',
|
|
409
|
+
'## 1. 公司与主体(已确认)',
|
|
410
|
+
'## 2. 已确认的事实(阶段:可直接使用)',
|
|
411
|
+
'## 3. 线索与待确认(阶段:先别当事实)',
|
|
412
|
+
'## 4. 风险与红旗(阶段:已识别)',
|
|
413
|
+
'## 5. 面试问题(按主题,最多 12 条)',
|
|
414
|
+
'## 6. 距离下一步还差什么',
|
|
415
|
+
'## 附录 A · 来源表',
|
|
416
|
+
'## 附录 B · 主张台账(技术可追溯)',
|
|
417
|
+
'## 附录 C · 调查覆盖度',
|
|
418
|
+
'## 附录 D · 术语表',
|
|
416
419
|
]) {
|
|
417
420
|
assert.ok(reportText.includes(heading), `report is missing ${heading}`)
|
|
418
421
|
}
|
|
@@ -994,7 +997,7 @@ test('rehydration requires a completion marker asserting complete:true and refus
|
|
|
994
997
|
assert.equal(JSON.parse(fs.read('/scout/silent/export.complete.json')).complete, true)
|
|
995
998
|
const healed = createHarness({ config: { scoutDir: '/scout' }, services: { fs } })
|
|
996
999
|
const report = await call(healed, 'scout_report', { caseId: 'silent' }, execFor())
|
|
997
|
-
assert.match(report.report, /^# Silent Co
|
|
1000
|
+
assert.match(report.report, /^# Silent Co$/m)
|
|
998
1001
|
await call(healed, 'scout_export', { caseId: 'silent', targetDir: '/scout/silent-2' }, execFor())
|
|
999
1002
|
assert.deepEqual(eventTypes(fs.read('/scout/silent-2/events.jsonl')), [
|
|
1000
1003
|
'case_started', 'case_exported', 'case_rehydrated', 'case_exported',
|
|
@@ -1087,7 +1090,7 @@ test('a cache miss rehydrates from <scoutDir>/<caseId>/, records it, and refuses
|
|
|
1087
1090
|
|
|
1088
1091
|
const reader = createHarness({ config: { scoutDir: '/scout' }, services: { fs } })
|
|
1089
1092
|
const report = await call(reader, 'scout_report', { caseId: 'restored' }, exec)
|
|
1090
|
-
assert.match(report.report, /^# Restored Co
|
|
1093
|
+
assert.match(report.report, /^# Restored Co$/m)
|
|
1091
1094
|
await call(reader, 'scout_export', { caseId: 'restored', targetDir: '/scout/restored-2' }, exec)
|
|
1092
1095
|
assert.deepEqual(eventTypes(fs.read('/scout/restored-2/events.jsonl')), [
|
|
1093
1096
|
'case_started', 'case_exported', 'case_rehydrated', 'case_exported',
|
|
@@ -1125,9 +1128,8 @@ test('language: en renders every human surface, including the persisted report.m
|
|
|
1125
1128
|
await call(harness, 'scout_start', { caseId: 'en-b', companyName: 'Second Co', roleTitle: 'CTO', location: '' }, exec)
|
|
1126
1129
|
|
|
1127
1130
|
const report = await call(harness, 'scout_report', { caseId: 'en-a' }, exec)
|
|
1128
|
-
assert.match(report.report,
|
|
1129
|
-
assert.match(report.report,
|
|
1130
|
-
assert.match(report.report, /- Objective: /)
|
|
1131
|
+
assert.match(report.report, /^> Role: HR Head \| Location: unspecified \| Decision: verify \(VERIFY\)$/m)
|
|
1132
|
+
assert.match(report.report, /## 30-second verdict/)
|
|
1131
1133
|
assert.match(report.report, /- No sources yet/)
|
|
1132
1134
|
assert.equal(CJK.test(report.report), false, 'an English report must not contain CJK characters')
|
|
1133
1135
|
|
|
@@ -1154,7 +1156,7 @@ test('language: en renders every human surface, including the persisted report.m
|
|
|
1154
1156
|
const reader = createHarness({ config: { language: 'en', scoutDir: '/export' }, services: { fs } })
|
|
1155
1157
|
await call(reader, 'scout_import', { caseId: 'en-a', sourceDir: '/export/en' }, execFor())
|
|
1156
1158
|
const reread = await call(reader, 'scout_report', { caseId: 'en-a' }, execFor())
|
|
1157
|
-
assert.match(reread.report,
|
|
1159
|
+
assert.match(reread.report, /^> Role: HR Head \| Location: unspecified \| Decision: verify \(VERIFY\)$/m)
|
|
1158
1160
|
})
|
|
1159
1161
|
|
|
1160
1162
|
test('scout_ingest drafts its claim notes in the deployment language', async () => {
|
|
@@ -1188,20 +1190,20 @@ test('scout_ingest drafts its claim notes in the deployment language', async ()
|
|
|
1188
1190
|
assert.equal(CJK.test(english.questions.join('')), false, 'and so must the derived interview questions')
|
|
1189
1191
|
assert.match(
|
|
1190
1192
|
english.report,
|
|
1191
|
-
/
|
|
1193
|
+
/next step: Verify this claim and add an independent source/,
|
|
1192
1194
|
)
|
|
1193
1195
|
assert.equal(english.persisted, english.report, 'the persisted snapshot stays byte-equal to the report')
|
|
1194
1196
|
|
|
1195
1197
|
const chinese = await ingestOne('zh-CN', 'zh-ingest')
|
|
1196
1198
|
assert.match(
|
|
1197
1199
|
chinese.report,
|
|
1198
|
-
|
|
1200
|
+
/下一步:核验该主张并补充独立来源/,
|
|
1199
1201
|
'the zh-CN draft wording is unchanged, so the 0.9.0 byte-compat baseline still holds',
|
|
1200
1202
|
)
|
|
1201
1203
|
assert.equal(CJK.test(chinese.report), true)
|
|
1202
1204
|
})
|
|
1203
1205
|
|
|
1204
|
-
test('the default zh-CN report keeps its
|
|
1206
|
+
test('the default zh-CN report keeps its shipped bytes and adds the 0.9.4 blocks', async () => {
|
|
1205
1207
|
const defaults = new Config({})
|
|
1206
1208
|
assert.equal(defaults.language, 'zh-CN')
|
|
1207
1209
|
assert.equal(defaults.scoutDir, 'dsh-scout')
|
|
@@ -1214,22 +1216,25 @@ test('the default zh-CN report keeps its pre-0.9.1 bytes and only adds the Cover
|
|
|
1214
1216
|
|
|
1215
1217
|
// Section headings are English in both languages — that is the shipped 0.9.0
|
|
1216
1218
|
// shape, verified against `git show HEAD:src/model.ts`, not a translation gap.
|
|
1219
|
+
// 0.9.4 inserts the verified-subject block before Coverage.
|
|
1217
1220
|
assert.deepEqual(
|
|
1218
1221
|
report.report.split('\n').filter(line => line.startsWith('## ')),
|
|
1219
1222
|
[
|
|
1220
|
-
'##
|
|
1221
|
-
'##
|
|
1222
|
-
'##
|
|
1223
|
-
'##
|
|
1224
|
-
'##
|
|
1225
|
-
'##
|
|
1226
|
-
'##
|
|
1227
|
-
'##
|
|
1223
|
+
'## 30 秒结论',
|
|
1224
|
+
'## 1. 公司与主体(已确认)',
|
|
1225
|
+
'## 2. 已确认的事实(阶段:可直接使用)',
|
|
1226
|
+
'## 3. 线索与待确认(阶段:先别当事实)',
|
|
1227
|
+
'## 4. 风险与红旗(阶段:已识别)',
|
|
1228
|
+
'## 5. 面试问题(按主题,最多 12 条)',
|
|
1229
|
+
'## 6. 距离下一步还差什么',
|
|
1230
|
+
'## 附录 A · 来源表',
|
|
1231
|
+
'## 附录 B · 主张台账(技术可追溯)',
|
|
1232
|
+
'## 附录 C · 调查覆盖度',
|
|
1233
|
+
'## 附录 D · 术语表',
|
|
1228
1234
|
],
|
|
1229
1235
|
)
|
|
1230
|
-
assert.match(report.report,
|
|
1231
|
-
assert.match(report.report,
|
|
1232
|
-
assert.match(report.report, /^- 目标:判断是否值得进入下一轮,并准备面试反问$/m)
|
|
1236
|
+
assert.match(report.report, /^> 岗位:HR Head | 地点:未指定 | 判断:继续核验(VERIFY)$/m)
|
|
1237
|
+
assert.match(report.report, /^> 目标:判断是否值得进入下一轮,并准备面试反问$/m)
|
|
1233
1238
|
assert.match(report.report, /^- 暂无来源$/m)
|
|
1234
1239
|
assert.equal(CJK.test(report.report), true)
|
|
1235
1240
|
|
|
@@ -1239,13 +1244,25 @@ test('the default zh-CN report keeps its pre-0.9.1 bytes and only adds the Cover
|
|
|
1239
1244
|
assert.deepEqual(report.coverage.map(entry => entry.dimension), [...CLAIM_DIMENSIONS])
|
|
1240
1245
|
assert.ok(report.coverage.every(entry => entry.untouched === true))
|
|
1241
1246
|
const coverageBlock = report.report.slice(
|
|
1242
|
-
report.report.indexOf('##
|
|
1243
|
-
report.report.indexOf('##
|
|
1247
|
+
report.report.indexOf('## 附录 C · 调查覆盖度'),
|
|
1248
|
+
report.report.indexOf('## 附录 D · 术语表'),
|
|
1244
1249
|
)
|
|
1250
|
+
const DIMENSION_ZH = {
|
|
1251
|
+
identity: '主体身份',
|
|
1252
|
+
role_existence: '岗位真实性',
|
|
1253
|
+
reporting_line: '汇报关系',
|
|
1254
|
+
mandate: '授权与职责',
|
|
1255
|
+
company_and_founder: '公司与创始人',
|
|
1256
|
+
product_and_technology: '产品与技术',
|
|
1257
|
+
market_and_competition: '市场与竞争',
|
|
1258
|
+
financing_and_commercialization: '融资与商业化',
|
|
1259
|
+
organization_and_hr: '组织与人力',
|
|
1260
|
+
risk: '风险',
|
|
1261
|
+
role_fit: '岗位匹配度',
|
|
1262
|
+
}
|
|
1245
1263
|
for (const dimension of CLAIM_DIMENSIONS) {
|
|
1246
|
-
assert.ok(coverageBlock.includes(`- ${dimension}
|
|
1264
|
+
assert.ok(coverageBlock.includes(`- ${DIMENSION_ZH[dimension]}`), `coverage block is missing ${dimension}`)
|
|
1247
1265
|
}
|
|
1248
|
-
assert.match(coverageBlock, /未调查维度(11):/)
|
|
1249
1266
|
})
|
|
1250
1267
|
|
|
1251
1268
|
test('a case crossing a language boundary renders every surface, report.md included, in the deployment language', async () => {
|
|
@@ -1264,8 +1281,7 @@ test('a case crossing a language boundary renders every surface, report.md inclu
|
|
|
1264
1281
|
const report = await call(en, 'scout_report', { caseId: 'mixed' }, exec)
|
|
1265
1282
|
await call(en, 'scout_export', { caseId: 'mixed', targetDir: '/export/en-from-zh' }, exec)
|
|
1266
1283
|
|
|
1267
|
-
assert.match(report.report,
|
|
1268
|
-
assert.match(report.report, /^- Reason: /m)
|
|
1284
|
+
assert.match(report.report, /^> Role: HR Head \| Location: unspecified \| Decision: verify \(VERIFY\)$/m, 'scout_report follows the configured language')
|
|
1269
1285
|
assert.match(report.report, /^- No sources yet$/m)
|
|
1270
1286
|
const persisted = fs.read('/export/en-from-zh/report.md')
|
|
1271
1287
|
assert.equal(persisted, report.report, 'the persisted snapshot and the rendered report are the same artifact')
|
|
@@ -1284,7 +1300,7 @@ test('a case crossing a language boundary renders every surface, report.md inclu
|
|
|
1284
1300
|
await call(zhReader, 'scout_import', { caseId: 'rev', sourceDir: '/export/rev-en' }, execFor())
|
|
1285
1301
|
const zhReport = await call(zhReader, 'scout_report', { caseId: 'rev' }, execFor())
|
|
1286
1302
|
await call(zhReader, 'scout_export', { caseId: 'rev', targetDir: '/export/rev-zh' }, execFor())
|
|
1287
|
-
assert.match(zhReport.report,
|
|
1303
|
+
assert.match(zhReport.report, /^> 岗位:HR Head .*判断:继续核验(VERIFY)$/m)
|
|
1288
1304
|
assert.equal(fs2.read('/export/rev-zh/report.md'), zhReport.report)
|
|
1289
1305
|
assert.equal(JSON.parse(fs2.read('/export/rev-zh/case.json')).language, 'zh-CN')
|
|
1290
1306
|
})
|
|
@@ -1318,7 +1334,7 @@ test('a case crossing a language boundary is single-language in both directions,
|
|
|
1318
1334
|
|
|
1319
1335
|
assert.equal(CJK.test(report.report), false, 'the English report must contain no CJK at all')
|
|
1320
1336
|
assert.equal(CJK.test(questions.questions.join('')), false, 'nor may the question list')
|
|
1321
|
-
assert.match(report.report,
|
|
1337
|
+
assert.match(report.report, /^> Role: /m)
|
|
1322
1338
|
assert.equal(caseJson.language, 'en')
|
|
1323
1339
|
assert.equal(caseJson.decisionObjective, enFactory.decisionObjective)
|
|
1324
1340
|
assert.deepEqual(caseJson.interviewQuestions, enFactory.interviewQuestions)
|
|
@@ -1355,7 +1371,7 @@ test('a case crossing a language boundary is single-language in both directions,
|
|
|
1355
1371
|
const enReader = createHarness({ config: { language: 'en', scoutDir: '/scout' }, services: { fs: fs3 } })
|
|
1356
1372
|
const rehydrated = await call(enReader, 'scout_report', { caseId: 'rehy' }, execFor())
|
|
1357
1373
|
assert.equal(CJK.test(rehydrated.report), false, 'a rehydrated zh case must be aligned before rendering')
|
|
1358
|
-
assert.match(rehydrated.report,
|
|
1374
|
+
assert.match(rehydrated.report, /> (目标:|Objective: )/m)
|
|
1359
1375
|
|
|
1360
1376
|
// --- round trip zh -> en -> zh restores the original factory prose ---------
|
|
1361
1377
|
const fs4 = new MemoryFs()
|
|
@@ -1400,7 +1416,7 @@ test('user-edited case prose survives a language change while untouched fields s
|
|
|
1400
1416
|
const customObjective = '用户自定义目标(勿改)'
|
|
1401
1417
|
const objectiveEdited = await editAndMove(caseJson => { caseJson.decisionObjective = customObjective }, 'obj')
|
|
1402
1418
|
assert.equal(objectiveEdited.caseJson.decisionObjective, customObjective, 'the edited objective is preserved verbatim')
|
|
1403
|
-
assert.match(objectiveEdited.reportText,
|
|
1419
|
+
assert.match(objectiveEdited.reportText, /> (岗位:|Role: )HR Head/m)
|
|
1404
1420
|
assert.deepEqual(
|
|
1405
1421
|
objectiveEdited.caseJson.interviewQuestions,
|
|
1406
1422
|
enFactory.interviewQuestions,
|
|
@@ -1674,7 +1690,7 @@ test('the /scout command exposes four working sub-actions and the scout Skill ca
|
|
|
1674
1690
|
assert.match((await invoke('help')).text, /^usage: \/scout status/)
|
|
1675
1691
|
assert.match((await invoke('status cmd')).text, /^\[cmd\] VERIFY · identity needs_verification/)
|
|
1676
1692
|
assert.match((await invoke('questions cmd')).text, /^1\. /)
|
|
1677
|
-
assert.match((await invoke('report cmd')).text, /^# Command Co
|
|
1693
|
+
assert.match((await invoke('report cmd')).text, /^# Command Co$/m)
|
|
1678
1694
|
const exported = await invoke('export cmd')
|
|
1679
1695
|
assert.equal(exported.kind, 'success')
|
|
1680
1696
|
assert.match(exported.text, /^exported to dsh-scout\/cmd$/)
|
|
@@ -1710,7 +1726,7 @@ test('T-C · one release literal across the exported constant, package.json and
|
|
|
1710
1726
|
// would assert nothing. This literal is the third, independent statement, so a
|
|
1711
1727
|
// bump that updates the manifest but forgets the constant (or vice versa)
|
|
1712
1728
|
// fails here instead of shipping a mislabelled build.
|
|
1713
|
-
const RELEASE_VERSION = '0.9.
|
|
1729
|
+
const RELEASE_VERSION = '0.9.4'
|
|
1714
1730
|
|
|
1715
1731
|
assert.equal(PLUGIN_VERSION, RELEASE_VERSION, 'the exported constant must be the release literal')
|
|
1716
1732
|
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|