Skip to content

Commit 4144150

Browse files
author
SqlRush
committed
Accept scripted expectation wrappers
1 parent 4258802 commit 4144150

5 files changed

Lines changed: 166 additions & 0 deletions

File tree

docs/cc-100-roadmap.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -514,6 +514,8 @@ M7 补充:prompt history `LogEntry` 读取现在接受 `sessionID`/`session`/`
514514

515515
本轮补充:scripted interaction 的布尔字段现在接受非严格 bool payload,包括 `"true"`/`"false"``yes`/`no``on`/`off` 和数字 `1`/`0`;覆盖 mouse release、dialog visible/result、prompt empty、vim state、reverse-search,以及 focus/cancel/openTasks/expectNoEvent/expectFocused 等顶层 step 控制字段,减少官方/外部交互脚本 fixture 因 bool 表示差异失败的情况。
516516

517+
本轮补充:scripted interaction step 现在接受 `expect`/`expected`/`assertions`/`checks`/`verify`/`then`/`after` 等 expectation wrapper object,可把嵌套的 prompt/event/dialog/snapshot/screen/task/vim/viewport 断言映射到已有 `expect*` 字段。
518+
517519
本轮补充:keybinding config、keymap 解析和 interaction script named-key 输入接受 `ctrl-h`/`ctrl-i`/`ctrl-j`/`ctrl-m`、`ctrl-[`、`ctrl-?`、对应 `control-*` 以及 compact/camel 形式;terminal parser 支持 CSI-u/kitty keyboard protocol 的 ctrl/alt/shift-enter/shift-tab 序列;image hint parser 支持 OSC ST terminator 和 base64 `name=` filename;keybinding JSON loader 支持 wrapper object-map、`shortcuts`、object action 字段、string-array key sequence/chord 和 `null`/`false` unbind;mouse parser 支持 legacy X10/normal tracking 序列;interaction script 支持结构化 mouse/mouse_event 步骤,button 接受 `buttonMask`/`button_mask`/`btn`/`code`/`mask`,坐标接受 `mouseX`/`mouse_x`/`clientX`/`screenX`/`pageX`/`offsetX`/`viewportX` 和对应 Y/row/line 别名,release 接受 `mouseUp`/`isRelease`/`mouseRelease`/`releaseEvent` 等字段别名;interaction script 还支持字符串 `keys` 和 `input`/`input_text`/`keys_text`/`raw_key`/`paste_text` 字段别名,并允许 status/snapshot/viewport/pasted-content contains 断言使用单字符串或字符串数组,`keybindings`、`expectEvents`、`expectDialogResults`、`expectPrompt.pastedContents` 和 `expectTasks.contains` 使用单对象或对象数组。
518520

519521
本轮补充:keybinding config 和脚本 named-key 输入现在接受短修饰符别名,包括 `c-`/`m-`/`a-`/`opt-`/`s-` 以及 compact/camel 形式,可覆盖 control、meta、alt、option 和 shift key names。

docs/claude-code-go-rewrite-plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,7 @@ test/parity/ # golden tests against TS/official behavior
271271
- 本轮补充:permission runtime 会把 `Reject`/`deny`/`decline`/`disallow`/`no` 等 permission action 归一为 denied 结果,把 `Cancel`/`abort` 归一为 cancelled 结果,并让 scripted dialog-result status 断言接受 `rejected`/`approved` 等状态别名。
272272
- 本轮补充:scripted permission payload、dialog expectation、event、cancel-permission 和 dialog-result expectation 现在接受 `permissionID``requestID``toolUseID``operationID``operation``commandName``resourcePath``body``reasonText``allowedActions``buttons` 等相邻字段,并支持数字 request ID。
273273
- 本轮补充:scripted interaction 的布尔字段现在接受非严格 bool payload,包括 `"true"`/`"false"``yes`/`no``on`/`off` 和数字 `1`/`0`;覆盖 mouse release、dialog visible/result、prompt empty、vim state、reverse-search,以及 focus/cancel/openTasks/expectNoEvent/expectFocused 等顶层 step 控制字段。
274+
- 本轮补充:scripted interaction step 现在接受 `expect`/`expected`/`assertions`/`checks`/`verify`/`then`/`after` 等 expectation wrapper object,可把嵌套的 prompt/event/dialog/snapshot/screen/task/vim/viewport 断言映射到已有 `expect*` 字段。
274275
- 本轮补充:keybinding config、keymap 解析和 interaction script named-key 输入接受 `ctrl-h`/`ctrl-i`/`ctrl-j`/`ctrl-m`、`ctrl-[`、`ctrl-?`、对应 `control-*` 以及 compact/camel 形式;terminal parser 支持 CSI-u/kitty keyboard protocol 的 ctrl/alt/shift-enter/shift-tab 序列;image hint parser 支持 OSC ST terminator 和 base64 `name=` filename;keybinding JSON loader 支持 wrapper object-map、`shortcuts`、object action 字段、string-array key sequence/chord 和 `null`/`false` unbind;mouse parser 支持 legacy X10/normal tracking 序列;interaction script 支持结构化 mouse/mouse_event 步骤,button 接受 `buttonMask`/`button_mask`/`btn`/`code`/`mask`,坐标接受 `mouseX`/`mouse_x`/`clientX`/`screenX`/`pageX`/`offsetX`/`viewportX` 和对应 Y/row/line 别名,release 接受 `mouseUp`/`isRelease`/`mouseRelease`/`releaseEvent` 等字段别名;interaction script 还支持字符串 `keys` 和 `input`/`input_text`/`keys_text`/`raw_key`/`paste_text` 字段别名,并允许 status/snapshot/viewport/pasted-content contains 断言使用单字符串或字符串数组,`keybindings`、`expectEvents`、`expectDialogResults`、`expectPrompt.pastedContents` 和 `expectTasks.contains` 使用单对象或对象数组。
275276
- 本轮补充:keybinding config 的 page navigation key name 现在接受 `pgup`/`pg-up`/`prior``pgdn`/`pg-down`/`next`,覆盖常见终端键名/配置别名。
276277
- 本轮补充:keybinding config 和脚本 named-key 输入接受 DOM-style arrow key aliases,包括 `arrowLeft`/`arrowRight`/`arrowUp`/`arrowDown` 以及 ctrl/alt/meta/option arrow variants。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ M7 progress now includes:
210210
- `internal/tui`: permission runtime now treats action aliases such as `Reject`, `deny`, `decline`, `disallow`, and `no` as denied results, `Cancel`/`abort` as cancelled results, and keeps allowed aliases canonical for scripted dialog-result status checks.
211211
- `internal/tui`: scripted permission payloads, dialog expectations, events, cancellation, and dialog-result expectations now accept adjacent ID and field aliases such as `permissionID`, `requestID`, `toolUseID`, `operationID`, `operation`, `commandName`, `resourcePath`, `body`, `reasonText`, `allowedActions`, `buttons`, and numeric request IDs.
212212
- `internal/tui`: scripted interaction boolean aliases now accept non-strict bool payloads (`"true"`/`"false"`, `yes`/`no`, `on`/`off`, and numeric `1`/`0`) for mouse release, dialog visibility/result checks, prompt empty checks, vim state checks, reverse-search checks, and top-level step controls such as focus, cancellation, task dialog open, and negative event assertions.
213+
- `internal/tui`: scripted interaction steps now accept expectation wrapper objects such as `expect`, `expected`, `assertions`, `checks`, `verify`, `then`, and `after`, mapping nested prompt/event/dialog/snapshot/screen/task/vim/viewport assertions onto the existing `expect*` fields.
213214
- `internal/tui`: interaction scripts can now set status/baseStatus via `status`, `setStatus`, `statusLine`, or `baseStatus`, and runtime-aware scripts retain that base while layering permission/task status counts.
214215
- `internal/tui`: terminal lifecycle can now manage extended-key reporting with Kitty keyboard protocol plus xterm modifyOtherKeys, including disable ordering and pop-before-push reassertion to avoid Kitty stack leaks.
215216
- `internal/tui`: renderer and ANSI snapshots now have an opt-in DEC 2026 synchronized-output wrapper for official BSU/ESU frame fixtures without changing default rendering.

internal/tui/script_aliases.go

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,7 @@ func isBindingSpecObject(data json.RawMessage) bool {
268268

269269
func (step *ScriptStep) UnmarshalJSON(data []byte) error {
270270
data = unwrapScriptStepJSON(data)
271+
data = mergeScriptStepExpectationJSON(data)
271272
rawStepData := data
272273
data = normalizeScriptStepJSON(data)
273274
type alias ScriptStep
@@ -930,6 +931,125 @@ func (step *ScriptStep) UnmarshalJSON(data []byte) error {
930931
return nil
931932
}
932933

934+
func mergeScriptStepExpectationJSON(data []byte) []byte {
935+
var fields map[string]json.RawMessage
936+
if err := json.Unmarshal(data, &fields); err != nil {
937+
return data
938+
}
939+
changed := false
940+
for _, name := range []string{
941+
"expect",
942+
"expected",
943+
"expectation",
944+
"expectations",
945+
"assert",
946+
"asserts",
947+
"assertion",
948+
"assertions",
949+
"check",
950+
"checks",
951+
"verify",
952+
"verification",
953+
"then",
954+
"after",
955+
} {
956+
raw, ok := fields[name]
957+
if !ok {
958+
continue
959+
}
960+
raw = bytes.TrimSpace(raw)
961+
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) || raw[0] != '{' {
962+
continue
963+
}
964+
nested := map[string]json.RawMessage{}
965+
if err := json.Unmarshal(raw, &nested); err != nil {
966+
continue
967+
}
968+
if mergeScriptStepExpectationFields(fields, nested) {
969+
changed = true
970+
}
971+
}
972+
if !changed {
973+
return data
974+
}
975+
merged, err := json.Marshal(fields)
976+
if err != nil {
977+
return data
978+
}
979+
return merged
980+
}
981+
982+
func mergeScriptStepExpectationFields(fields map[string]json.RawMessage, nested map[string]json.RawMessage) bool {
983+
changed := false
984+
for name, raw := range nested {
985+
if target := scriptStepExpectationTargetField(name); target != "" {
986+
if _, exists := fields[target]; !exists {
987+
fields[target] = raw
988+
changed = true
989+
}
990+
continue
991+
}
992+
if strings.HasPrefix(name, "expect") || strings.HasPrefix(name, "Expect") {
993+
if _, exists := fields[name]; !exists {
994+
fields[name] = raw
995+
changed = true
996+
}
997+
}
998+
}
999+
return changed
1000+
}
1001+
1002+
func scriptStepExpectationTargetField(name string) string {
1003+
switch canonicalScriptStepAction(name) {
1004+
case "event":
1005+
return "expectEvent"
1006+
case "events":
1007+
return "expectEvents"
1008+
case "noevent", "no-event":
1009+
return "expectNoEvent"
1010+
case "eventcount", "event-count":
1011+
return "expectEventCount"
1012+
case "totaleventcount", "total-event-count":
1013+
return "expectTotalEventCount"
1014+
case "dialog":
1015+
return "expectDialog"
1016+
case "dialogresult", "dialog-result":
1017+
return "expectDialogResult"
1018+
case "dialogresults", "dialog-results":
1019+
return "expectDialogResults"
1020+
case "nodialogresult", "no-dialog-result", "nodialogresults", "no-dialog-results":
1021+
return "expectNoDialogResult"
1022+
case "dialogresultcount", "dialog-result-count":
1023+
return "expectDialogResultCount"
1024+
case "totaldialogresultcount", "total-dialog-result-count":
1025+
return "expectTotalDialogResultCount"
1026+
case "prompt", "input":
1027+
return "expectPrompt"
1028+
case "vim":
1029+
return "expectVim"
1030+
case "tasks":
1031+
return "expectTasks"
1032+
case "reversesearch", "reverse-search":
1033+
return "expectReverseSearch"
1034+
case "viewport":
1035+
return "expectViewport"
1036+
case "screen", "terminal":
1037+
return "expectScreen"
1038+
case "focused", "focus":
1039+
return "expectFocused"
1040+
case "statuscontains", "status-contains":
1041+
return "expectStatusContains"
1042+
case "statusnotcontains", "status-not-contains":
1043+
return "expectStatusNotContains"
1044+
case "snapshotcontains", "snapshot-contains":
1045+
return "expectSnapshotContains"
1046+
case "snapshotnotcontains", "snapshot-not-contains":
1047+
return "expectSnapshotNotContains"
1048+
default:
1049+
return ""
1050+
}
1051+
}
1052+
9331053
func unwrapScriptStepJSON(data []byte) []byte {
9341054
var fields map[string]json.RawMessage
9351055
if err := json.Unmarshal(data, &fields); err != nil {

internal/tui/tui_test.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6059,6 +6059,48 @@ func TestRunInteractionScriptAcceptsEventFieldAliases(t *testing.T) {
60596059
}
60606060
}
60616061

6062+
func TestRunInteractionScriptAcceptsExpectationWrappers(t *testing.T) {
6063+
steps, err := ParseInteractionScript([]byte(`[
6064+
{
6065+
"action": "typeText",
6066+
"value": "go",
6067+
"expect": {
6068+
"prompt": {"text": "go"},
6069+
"noEvent": "true"
6070+
}
6071+
},
6072+
{
6073+
"type": "press",
6074+
"value": "enter",
6075+
"assertions": {
6076+
"event": {"type": "prompt_submitted", "value": "go"},
6077+
"eventCount": 1,
6078+
"totalEventCount": 1,
6079+
"prompt": {"empty": "yes"}
6080+
}
6081+
},
6082+
{
6083+
"message": {"role": "assistant", "text": "ready"},
6084+
"snapshot": "ready-state",
6085+
"checks": {
6086+
"snapshotContains": "assistant: ready",
6087+
"screen": {"columns": 30, "rows": 6}
6088+
}
6089+
}
6090+
]`))
6091+
if err != nil {
6092+
t.Fatal(err)
6093+
}
6094+
screen := NewREPLScreen(30, 6, nil)
6095+
result, err := RunInteractionScriptChecked(&screen, steps)
6096+
if err != nil {
6097+
t.Fatal(err)
6098+
}
6099+
if len(result.Events) != 1 || result.Events[0].Type != ScreenEventPromptSubmitted || len(result.Snapshots) != 1 || result.Snapshots[0].Name != "ready-state" {
6100+
t.Fatalf("result = %#v", result)
6101+
}
6102+
}
6103+
60626104
func TestRunDialogRuntimeScriptAcceptsEventAndResultAliases(t *testing.T) {
60636105
steps, err := ParseInteractionScript([]byte(`[
60646106
{

0 commit comments

Comments
 (0)