Skip to content

Commit 1555f78

Browse files
author
SqlRush
committed
Accept wrapped expectation collection fields
1 parent e681407 commit 1555f78

5 files changed

Lines changed: 214 additions & 14 deletions

File tree

docs/cc-100-roadmap.md

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

609609
本轮补充:interaction script 的 direct expectation string-list alias 字段现在也递归解包 wrapper;`expectStatusContains`/`NotContains``expectSnapshotContains`/`NotContains` 可从 wrapped `value`/`values`/`contains`/`items` 恢复断言列表,避免 direct expectation fixture 在 list decode 阶段失败。
610610

611+
本轮补充:interaction script 的 direct expectation collection alias 字段现在也递归解包 wrapper;`expectEvents``expectDialogResults` 可从 wrapped `events`/`results`/`items`/`nodes` 中恢复结构化断言列表,避免 wrapper collection fixture 被当成空对象断言。
612+
611613
本轮补充:interaction script action/type/kind/name/operation 动作判别字段现在也接受 compact/camel event/media aliases,包括 `focusIn``focusOut``mouseEvent``pasteImage``imagePaste`
612614

613615
本轮补充:interaction script 的 mouseEvent/pasteImage action payload 现在递归解包 JSON:API/GraphQL-style `resource`/`attributes`/`properties`/`attrs`/`edge.node` wrapper,wrapped mouse 坐标/按钮和 image filename/media/content 可直接驱动 dialog click 与 image paste。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -315,6 +315,7 @@ test/parity/ # golden tests against TS/official behavior
315315
- 本轮补充:interaction script 的 direct expectation bool alias 字段现在也递归解包 wrapper;`expectNoEvent``expectNoDialogResult(s)``expectFocused` 可从 wrapped `value`/`enabled`/`selected` 恢复断言控制。
316316
- 本轮补充:interaction script 的 direct expectation count alias 字段现在也递归解包 wrapper;`expectEventCount``expectTotalEventCount``expectDialogResultCount``expectTotalDialogResultCount` 可从 wrapped `value`/`count`/`total` 恢复计数断言。
317317
- 本轮补充:interaction script 的 direct expectation string-list alias 字段现在也递归解包 wrapper;`expectStatusContains`/`NotContains``expectSnapshotContains`/`NotContains` 可从 wrapped `value`/`values`/`contains`/`items` 恢复断言列表。
318+
- 本轮补充:interaction script 的 direct expectation collection alias 字段现在也递归解包 wrapper;`expectEvents``expectDialogResults` 可从 wrapped `events`/`results`/`items`/`nodes` 中恢复结构化断言列表。
318319
- 本轮补充:interaction script action/type/kind/name/operation 动作判别字段继续接受 compact/camel event/media aliases,包括 `focusIn``focusOut``mouseEvent``pasteImage``imagePaste`
319320
- 本轮补充:interaction script mouseEvent/pasteImage action payload 现在递归解包 JSON:API/GraphQL-style `resource`/`attributes`/`properties`/`attrs`/`edge.node` wrapper,wrapped mouse 坐标/按钮和 image filename/media/content 可直接驱动 dialog click 与 image paste。
320321
- 本轮补充:interaction script action/type/kind/name/operation 动作判别字段也可驱动 runtime/dialog mutation,支持 `requestPermission``taskStatus``showTasks``cancelTasks``removeTask``showDialog` 等动作,并从 `value`/`payload`/`data`/`body` 载荷解析对象、ID 或取消原因。

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,7 @@ M7 progress now includes:
295295
- `internal/tui`: direct scripted expectation bool aliases such as `expectNoEvent`, `expectNoDialogResult(s)`, and `expectFocused` now recursively unwrap JSON:API/GraphQL-style wrapper objects.
296296
- `internal/tui`: direct scripted expectation count aliases such as `expectEventCount`, `expectTotalEventCount`, `expectDialogResultCount`, and `expectTotalDialogResultCount` now recursively unwrap numeric JSON:API/GraphQL-style wrapper objects.
297297
- `internal/tui`: direct scripted expectation string-list aliases such as `expectStatusContains`, `expectStatusNotContains`, `expectSnapshotContains`, and `expectSnapshotNotContains` now recursively unwrap JSON:API/GraphQL-style wrapper objects.
298+
- `internal/tui`: direct scripted expectation collection aliases such as `expectEvents` and `expectDialogResults` now recursively unwrap JSON:API/GraphQL-style wrapper objects.
298299
- `internal/tui`: terminal key parsing now accepts CSI-u/kitty keyboard protocol sequences for existing ctrl/alt editing keys, shift-enter, shift-tab, and printable shift-only runes.
299300
- `internal/tui`: terminal CSI-u/kitty keyboard parsing now also accepts colon-suffixed alternate codepoint and modifier event-type fields such as `CSI 97:65;5:1u`.
300301
- `internal/tui`: terminal CSI-u/kitty keyboard parsing now also accepts base/unmodified sequences such as `CSI 97u` and `CSI 13;1u`, mapping printable runes plus Enter, Tab, Esc, and Backspace instead of treating ordinary extended-key reports as unknown.

internal/tui/script_aliases.go

Lines changed: 143 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -455,8 +455,8 @@ func (step *ScriptStep) UnmarshalJSON(data []byte) error {
455455
Blurred *json.RawMessage `json:"blurred"`
456456
ExpectEvent *ScreenEvent `json:"expect_event"`
457457
ExpectEventCamel *ScreenEvent `json:"expectEvent"`
458-
ExpectEvents []ScreenEvent `json:"expect_events"`
459-
ExpectEventsCamel []ScreenEvent `json:"expectEvents"`
458+
ExpectEvents *json.RawMessage `json:"expect_events"`
459+
ExpectEventsCamel *json.RawMessage `json:"expectEvents"`
460460
ExpectNoEvent *json.RawMessage `json:"expect_no_event"`
461461
ExpectNoEventCamel *json.RawMessage `json:"expectNoEvent"`
462462
ExpectEventCount *json.RawMessage `json:"expect_event_count"`
@@ -465,8 +465,8 @@ func (step *ScriptStep) UnmarshalJSON(data []byte) error {
465465
ExpectTotalEventCamel *json.RawMessage `json:"expectTotalEventCount"`
466466
ExpectDialogResult *DialogResultExpectation `json:"expect_dialog_result"`
467467
ExpectDialogResultCamel *DialogResultExpectation `json:"expectDialogResult"`
468-
ExpectDialogResults []DialogResultExpectation `json:"expect_dialog_results"`
469-
ExpectDialogResultsCamel []DialogResultExpectation `json:"expectDialogResults"`
468+
ExpectDialogResults *json.RawMessage `json:"expect_dialog_results"`
469+
ExpectDialogResultsCamel *json.RawMessage `json:"expectDialogResults"`
470470
ExpectNoDialogResult *json.RawMessage `json:"expect_no_dialog_result"`
471471
ExpectNoDialogResultCamel *json.RawMessage `json:"expectNoDialogResult"`
472472
ExpectNoDialogResults *json.RawMessage `json:"expect_no_dialog_results"`
@@ -835,11 +835,19 @@ func (step *ScriptStep) UnmarshalJSON(data []byte) error {
835835
if fields.ExpectEventCamel != nil {
836836
step.ExpectEvent = fields.ExpectEventCamel
837837
}
838-
if fields.ExpectEvents != nil {
839-
step.ExpectEvents = fields.ExpectEvents
840-
}
841-
if fields.ExpectEventsCamel != nil {
842-
step.ExpectEvents = fields.ExpectEventsCamel
838+
if events := scriptNamedEventListField(fieldMap,
839+
[]string{"expect_events", "expectEvents"},
840+
"events",
841+
"event",
842+
"expected_events",
843+
"expectedEvents",
844+
"items",
845+
"entries",
846+
"nodes",
847+
"results",
848+
"value",
849+
); len(events) > 0 {
850+
step.ExpectEvents = events
843851
}
844852
if value, ok := scriptRuntimeMutationBoolField(fieldMap, "expect_no_event", "expectNoEvent"); ok {
845853
step.ExpectNoEvent = value
@@ -872,11 +880,20 @@ func (step *ScriptStep) UnmarshalJSON(data []byte) error {
872880
if fields.ExpectDialogResultCamel != nil {
873881
step.ExpectDialogResult = fields.ExpectDialogResultCamel
874882
}
875-
if fields.ExpectDialogResults != nil {
876-
step.ExpectDialogResults = fields.ExpectDialogResults
877-
}
878-
if fields.ExpectDialogResultsCamel != nil {
879-
step.ExpectDialogResults = fields.ExpectDialogResultsCamel
883+
if results := scriptNamedDialogResultListField(fieldMap,
884+
[]string{"expect_dialog_results", "expectDialogResults"},
885+
"dialog_results",
886+
"dialogResults",
887+
"results",
888+
"result",
889+
"expected_results",
890+
"expectedResults",
891+
"items",
892+
"entries",
893+
"nodes",
894+
"value",
895+
); len(results) > 0 {
896+
step.ExpectDialogResults = results
880897
}
881898
if value, ok := scriptRuntimeMutationBoolField(fieldMap, "expect_no_dialog_result", "expectNoDialogResult", "expect_no_dialog_results"); ok {
882899
step.ExpectNoDialogResult = value
@@ -1813,6 +1830,118 @@ func scriptNamedIntField(fields map[string]json.RawMessage, directNames []string
18131830
return 0, false
18141831
}
18151832

1833+
func scriptNamedEventListField(fields map[string]json.RawMessage, directNames []string, nestedNames ...string) []ScreenEvent {
1834+
for _, name := range directNames {
1835+
raw, ok := fields[name]
1836+
if !ok {
1837+
continue
1838+
}
1839+
if events := scriptEventListFromJSON(raw, nestedNames, 0); len(events) > 0 {
1840+
return events
1841+
}
1842+
}
1843+
return nil
1844+
}
1845+
1846+
func scriptEventListFromJSON(raw json.RawMessage, names []string, depth int) []ScreenEvent {
1847+
raw = bytes.TrimSpace(raw)
1848+
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
1849+
return nil
1850+
}
1851+
if raw[0] == '[' {
1852+
var items []json.RawMessage
1853+
if err := json.Unmarshal(raw, &items); err != nil {
1854+
return nil
1855+
}
1856+
out := []ScreenEvent{}
1857+
for _, item := range items {
1858+
out = append(out, scriptEventListFromJSON(item, names, depth+1)...)
1859+
}
1860+
return out
1861+
}
1862+
if depth >= 8 || raw[0] != '{' {
1863+
return nil
1864+
}
1865+
var event ScreenEvent
1866+
if err := json.Unmarshal(raw, &event); err == nil && scriptScreenEventHasData(event) {
1867+
return []ScreenEvent{event}
1868+
}
1869+
fields := map[string]json.RawMessage{}
1870+
if err := json.Unmarshal(raw, &fields); err != nil {
1871+
return nil
1872+
}
1873+
for _, name := range scriptRuntimePayloadWrapperNames(names...) {
1874+
nested, ok := fields[name]
1875+
if !ok {
1876+
continue
1877+
}
1878+
if events := scriptEventListFromJSON(nested, names, depth+1); len(events) > 0 {
1879+
return events
1880+
}
1881+
}
1882+
return nil
1883+
}
1884+
1885+
func scriptScreenEventHasData(event ScreenEvent) bool {
1886+
return event.Type != "" || event.Value != "" || event.DialogID != "" || event.DialogKind != "" || event.Display != "" || len(event.PastedContents) > 0
1887+
}
1888+
1889+
func scriptNamedDialogResultListField(fields map[string]json.RawMessage, directNames []string, nestedNames ...string) []DialogResultExpectation {
1890+
for _, name := range directNames {
1891+
raw, ok := fields[name]
1892+
if !ok {
1893+
continue
1894+
}
1895+
if results := scriptDialogResultListFromJSON(raw, nestedNames, 0); len(results) > 0 {
1896+
return results
1897+
}
1898+
}
1899+
return nil
1900+
}
1901+
1902+
func scriptDialogResultListFromJSON(raw json.RawMessage, names []string, depth int) []DialogResultExpectation {
1903+
raw = bytes.TrimSpace(raw)
1904+
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {
1905+
return nil
1906+
}
1907+
if raw[0] == '[' {
1908+
var items []json.RawMessage
1909+
if err := json.Unmarshal(raw, &items); err != nil {
1910+
return nil
1911+
}
1912+
out := []DialogResultExpectation{}
1913+
for _, item := range items {
1914+
out = append(out, scriptDialogResultListFromJSON(item, names, depth+1)...)
1915+
}
1916+
return out
1917+
}
1918+
if depth >= 8 || raw[0] != '{' {
1919+
return nil
1920+
}
1921+
var result DialogResultExpectation
1922+
if err := json.Unmarshal(raw, &result); err == nil && scriptDialogResultExpectationHasData(result) {
1923+
return []DialogResultExpectation{result}
1924+
}
1925+
fields := map[string]json.RawMessage{}
1926+
if err := json.Unmarshal(raw, &fields); err != nil {
1927+
return nil
1928+
}
1929+
for _, name := range scriptRuntimePayloadWrapperNames(names...) {
1930+
nested, ok := fields[name]
1931+
if !ok {
1932+
continue
1933+
}
1934+
if results := scriptDialogResultListFromJSON(nested, names, depth+1); len(results) > 0 {
1935+
return results
1936+
}
1937+
}
1938+
return nil
1939+
}
1940+
1941+
func scriptDialogResultExpectationHasData(result DialogResultExpectation) bool {
1942+
return result.ID != "" || result.Kind != "" || result.Action != "" || result.Status != "" || result.Found != nil || result.Stale != nil
1943+
}
1944+
18161945
func scriptActionIntFromJSON(raw json.RawMessage, names []string, depth int) (int, bool) {
18171946
raw = bytes.TrimSpace(raw)
18181947
if len(raw) == 0 || bytes.Equal(raw, []byte("null")) {

internal/tui/tui_test.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7018,6 +7018,73 @@ func TestRunInteractionScriptAcceptsEventFieldAliases(t *testing.T) {
70187018
}
70197019
}
70207020

7021+
func TestRunInteractionScriptAcceptsWrappedExpectationCollectionAliasFields(t *testing.T) {
7022+
steps, err := ParseInteractionScript([]byte(`[
7023+
{"input": "go", "expectPrompt": {"text": "go"}},
7024+
{
7025+
"key": "enter",
7026+
"expectEvents": {
7027+
"resource": {
7028+
"attributes": {
7029+
"events": [
7030+
{"type": "prompt_submitted", "value": "go"}
7031+
]
7032+
}
7033+
}
7034+
}
7035+
}
7036+
]`))
7037+
if err != nil {
7038+
t.Fatal(err)
7039+
}
7040+
if len(steps) != 2 || len(steps[1].ExpectEvents) != 1 ||
7041+
steps[1].ExpectEvents[0].Type != ScreenEventPromptSubmitted ||
7042+
steps[1].ExpectEvents[0].Value != "go" {
7043+
t.Fatalf("expect events = %#v", steps[1].ExpectEvents)
7044+
}
7045+
screen := NewREPLScreen(30, 6, nil)
7046+
result, err := RunInteractionScriptChecked(&screen, steps)
7047+
if err != nil {
7048+
t.Fatal(err)
7049+
}
7050+
if len(result.Events) != 1 || result.Events[0].Type != ScreenEventPromptSubmitted || result.Events[0].Value != "go" {
7051+
t.Fatalf("events = %#v", result.Events)
7052+
}
7053+
7054+
dialogSteps, err := ParseInteractionScript([]byte(`[
7055+
{"request_permission": {"id": "perm_1", "tool_name": "Bash"}},
7056+
{
7057+
"key": "enter",
7058+
"expectDialogResults": {
7059+
"edge": {
7060+
"node": {
7061+
"attrs": {
7062+
"results": [
7063+
{"id": "perm_1", "status": "allowed", "found": true}
7064+
]
7065+
}
7066+
}
7067+
}
7068+
}
7069+
}
7070+
]`))
7071+
if err != nil {
7072+
t.Fatal(err)
7073+
}
7074+
if len(dialogSteps) != 2 || len(dialogSteps[1].ExpectDialogResults) != 1 ||
7075+
dialogSteps[1].ExpectDialogResults[0].ID != "perm_1" ||
7076+
dialogSteps[1].ExpectDialogResults[0].Status != DialogResultAllowed ||
7077+
dialogSteps[1].ExpectDialogResults[0].Found == nil ||
7078+
!*dialogSteps[1].ExpectDialogResults[0].Found {
7079+
t.Fatalf("expect dialog results = %#v", dialogSteps[1].ExpectDialogResults)
7080+
}
7081+
dialogScreen := NewREPLScreen(40, 8, nil)
7082+
runtime := NewDialogRuntime()
7083+
if _, err := RunDialogRuntimeScriptChecked(&dialogScreen, runtime, "ready", dialogSteps); err != nil {
7084+
t.Fatal(err)
7085+
}
7086+
}
7087+
70217088
func TestRunInteractionScriptAcceptsExpectationWrappers(t *testing.T) {
70227089
steps, err := ParseInteractionScript([]byte(`[
70237090
{

0 commit comments

Comments
 (0)