feat(terminal): wait_for needle 支持 regex,found 返回实际匹配文本
needle 以 JavaScript 正则优先(编译失败回退字面匹配,旧字面 needle 完全兼容);found 结果新增 match 字段回报实际匹配文本——单个 needle 可覆盖多结局场景(如 (BUILD_OK|BUILD_FAIL) 同步构建成功/失败,match 区分命中分支);工具 description / needle 参数 / output schema / render 同步更新并写明多重匹配场景;新增 3 个 pty 级测试(alternation 命中分支、非法正则回退、元字符匹配);全量测试失败集合与干净 main 基线完全一致(27 个失败均为预存在 win32 非回归)
This commit is contained in:
@@ -281,6 +281,57 @@ describe('AgentPtyRegistry', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('waitFor supports regex alternation and reports which alternative matched', async () => {
|
||||
const registry = new AgentPtyRegistry(testShell())
|
||||
try {
|
||||
// Multi-outcome pattern (build success/failure): the terminal prints
|
||||
// only the FAILURE marker; the alternation must match immediately and
|
||||
// the `match` field must tell WHICH alternative hit.
|
||||
const uuid = registry.create('s1', 'multi-outcome', 'echo BUILD_FAIL', process.cwd(), 80, 24)
|
||||
await waitForTranscript(registry, uuid, 'BUILD_FAIL')
|
||||
const result = await registry.waitFor(uuid, 'BUILD_(OK|FAIL)', 2000)
|
||||
expect(result.kind).toBe('found')
|
||||
if (result.kind === 'found') {
|
||||
expect(result.needle).toBe('BUILD_(OK|FAIL)')
|
||||
expect(result.match).toBe('BUILD_FAIL')
|
||||
}
|
||||
} finally {
|
||||
registry.disposeAll()
|
||||
}
|
||||
})
|
||||
|
||||
it('waitFor falls back to verbatim matching when the pattern is not a valid regex', async () => {
|
||||
const registry = new AgentPtyRegistry(testShell())
|
||||
try {
|
||||
// `BUILD_(OK` does not compile as a regex (unbalanced group); the wait
|
||||
// must degrade to literal substring matching and still find it.
|
||||
const uuid = registry.create('s1', 'bad-regex', 'echo "BUILD_(OK literal"', process.cwd(), 80, 24)
|
||||
await waitForTranscript(registry, uuid, 'BUILD_(OK')
|
||||
const result = await registry.waitFor(uuid, 'BUILD_(OK', 2000)
|
||||
expect(result.kind).toBe('found')
|
||||
if (result.kind === 'found') {
|
||||
expect(result.match).toBe('BUILD_(OK')
|
||||
}
|
||||
} finally {
|
||||
registry.disposeAll()
|
||||
}
|
||||
})
|
||||
|
||||
it('waitFor regex matches text a plain substring could not express', async () => {
|
||||
const registry = new AgentPtyRegistry(testShell())
|
||||
try {
|
||||
const uuid = registry.create('s1', 'regex-version', 'echo version v1.2.3 ready', process.cwd(), 80, 24)
|
||||
await waitForTranscript(registry, uuid, 'v1.2.3')
|
||||
const result = await registry.waitFor(uuid, String.raw`v\d+\.\d+\.\d+`, 2000)
|
||||
expect(result.kind).toBe('found')
|
||||
if (result.kind === 'found') {
|
||||
expect(result.match).toBe('v1.2.3')
|
||||
}
|
||||
} finally {
|
||||
registry.disposeAll()
|
||||
}
|
||||
})
|
||||
|
||||
it('waitFor returns found after the needle appears (async output)', async () => {
|
||||
const registry = new AgentPtyRegistry(testShell())
|
||||
try {
|
||||
|
||||
+3
-3
@@ -58,11 +58,11 @@ class FakeRegistry {
|
||||
return true
|
||||
}
|
||||
|
||||
waitFor(_uuid: string, needle: string): Promise<{ kind: 'found'; needle: string; line: number; column: number; elapsedMs: number }> {
|
||||
waitFor(_uuid: string, needle: string): Promise<{ kind: 'found'; needle: string; line: number; column: number; match: string; elapsedMs: number }> {
|
||||
// Mirrors the registry's empty-needle rejection (the tool layer no
|
||||
// longer duplicates this check).
|
||||
if (needle === '') return Promise.reject(new Error('needle must be a non-empty string'))
|
||||
return Promise.resolve({ kind: 'found', needle, line: 0, column: 0, elapsedMs: 1 })
|
||||
return Promise.resolve({ kind: 'found', needle, line: 0, column: 0, match: needle, elapsedMs: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,7 +189,7 @@ describe('agent terminal tools', () => {
|
||||
const tool = toolOf(captured, 'terminal_wait_for')
|
||||
const uuid = registry.create('s1', 'waiter', '')
|
||||
const value = await tool.execute({ uuid, needle: 'done' }, exec('s1'))
|
||||
expect(value).toEqual({ kind: 'found', needle: 'done', line: 0, column: 0, elapsedMs: 1 })
|
||||
expect(value).toEqual({ kind: 'found', needle: 'done', line: 0, column: 0, match: 'done', elapsedMs: 1 })
|
||||
expect(validateJsonSchemaValue(tool.output.schema, value, 'value')).toEqual([])
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user