Skip to content

Commit e0b0284

Browse files
author
SqlRush
committed
Flatten script suite case arrays
1 parent a97701b commit e0b0284

5 files changed

Lines changed: 182 additions & 2 deletions

File tree

docs/cc-100-roadmap.md

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

597597
本轮补充:interaction script key input 现在接受 press-style aliases,包括 `press``keyPress``keypress``shortcutKey``presses``keyPresses``shortcuts`
598598

599+
本轮补充:interaction script loader 现在会扁平化 `cases`/`tests`/`testCases`/`scenarios`/`fixtures` 等 suite array,每个 case 内的 `steps`/`timeline`/`scriptSteps` 会按顺序展开,顶层数组也可直接混入 case object。
600+
599601
本轮补充:interaction script 的 key/keySequence action payload 现在递归解包 JSON:API/GraphQL-style wrapper,wrapped single key 和 key sequence array 可直接驱动按键输入与组合键事件。
600602

601603
本轮补充:interaction script 的 direct key alias 字段现在也接受 wrapped object,`key``keyPress``keyPresses` 等字段可从 `resource.attributes``edge.node.attrs` 中恢复单键和 key sequence,避免官方 fixture 直接字段形态在 string/list decode 阶段失败。

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,7 @@ test/parity/ # golden tests against TS/official behavior
324324
- 本轮补充:interaction script 的 per-step keybinding mutation 复用同一套 collection alias、object-map 和 resource wrapper 解析,脚本步骤可直接使用 `keymap``keyboardShortcuts``hotkeys``keyboard``preferences``keybindingConfig` 临时改键位。
325325
- 本轮补充:interaction script 的 `keys` 字段支持 printable text chunk 和空格分隔 named-key sequence,例如 `ctrl-x ctrl-k`,减少官方脚本把连续输入拆成数组的改写成本。
326326
- 本轮补充:interaction script key input 接受 press-style aliases,包括 `press``keyPress``keypress``shortcutKey``presses``keyPresses``shortcuts`
327+
- 本轮补充:interaction script loader 现在会扁平化 `cases`/`tests`/`testCases`/`scenarios`/`fixtures` 等 suite array,每个 case 内的 `steps`/`timeline`/`scriptSteps` 会按顺序展开,顶层数组也可直接混入 case object。
327328
- 本轮补充:interaction script key/keySequence action payload 现在递归解包 JSON:API/GraphQL-style wrapper,`payload.resource.attributes.key``edge.node.attrs.sequence` 可直接驱动按键与组合键序列。
328329
- 本轮补充:interaction script 的直接字符串 alias 字段现在也接受 wrapped object;`text``pasteText``setStatus``snapshotName` 等字段可从 `resource.attributes``edge.node.attrs` 中恢复正文、paste、status 和 snapshot 名称,避免 direct field fixture 在 scalar decode 阶段失败。
329330
- 本轮补充:interaction script action/type/kind/name/operation 动作判别字段接受 compact/camel fixture aliases,包括 `typeText``inputText``insertText``keyPress``pressKey``keySequence``pasteText``pastedText``clipboardText``setStatus``statusLine``terminalSize``screenSize`

docs/first-second-parity-audit.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ M7 progress now includes:
305305
- `internal/tui`: keybinding config and named-key script input now accept Shift-Tab terminfo aliases such as `backtab`, `back-tab`, and `btab`, mapping them to the existing focus-previous key surface.
306306
- `internal/tui`: scripted interaction `keys` entries now accept printable text chunks and whitespace-separated named key sequences such as `ctrl-x ctrl-k`.
307307
- `internal/tui`: scripted interaction key input now accepts press-style aliases such as `press`, `keyPress`, `keypress`, `shortcutKey`, `presses`, `keyPresses`, and `shortcuts`.
308+
- `internal/tui`: interaction script loading now flattens suite/case arrays such as `cases`, `tests`, `testCases`, `scenarios`, and `fixtures`, expanding nested `steps`, `timeline`, or `scriptSteps` arrays in order while still allowing top-level arrays to mix direct steps and case objects.
308309
- `internal/tui`: scripted key and key-sequence action payloads now recursively unwrap JSON:API/GraphQL-style wrappers, preserving wrapped single keys and key sequence arrays from API-shaped interaction fixtures.
309310
- `internal/tui`: direct scripted key alias fields such as `key`, `keyPress`, and `keyPresses` now accept JSON:API/GraphQL-style wrapper objects without failing strong string/list decoding.
310311
- `internal/tui`: direct scripted string alias fields such as `text`, `pasteText`, `setStatus`, and `snapshotName` now accept JSON:API/GraphQL-style wrapper objects without failing strong scalar decoding.

internal/tui/script_loader.go

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,8 @@ func ParseInteractionScript(data []byte) ([]ScriptStep, error) {
2626
return nil, nil
2727
}
2828
if data[0] == '[' {
29-
var steps []ScriptStep
30-
if err := json.Unmarshal(data, &steps); err != nil {
29+
steps, err := parseInteractionScriptStepsValue(data)
30+
if err != nil {
3131
return nil, fmt.Errorf("parse interaction script array: %w", err)
3232
}
3333
return steps, nil
@@ -84,6 +84,17 @@ func parseInteractionScriptObject(data []byte) ([]ScriptStep, bool, error) {
8484
"included",
8585
"actions",
8686
"timeline",
87+
"tests",
88+
"test_cases",
89+
"testCases",
90+
"cases",
91+
"scenarios",
92+
"fixtures",
93+
"recordings",
94+
"runs",
95+
"operations",
96+
"commands",
97+
"plays",
8798
"collection",
8899
"collections",
89100
"list",
@@ -183,13 +194,131 @@ func parseInteractionScriptStepsValue(value json.RawMessage) ([]ScriptStep, erro
183194
return steps, nil
184195
}
185196
}
197+
if len(value) > 0 && value[0] == '[' {
198+
return parseInteractionScriptArrayValue(value)
199+
}
186200
var steps []ScriptStep
187201
if err := json.Unmarshal(value, &steps); err != nil {
188202
return nil, err
189203
}
190204
return steps, nil
191205
}
192206

207+
func parseInteractionScriptArrayValue(value json.RawMessage) ([]ScriptStep, error) {
208+
var items []json.RawMessage
209+
if err := json.Unmarshal(value, &items); err != nil {
210+
return nil, err
211+
}
212+
steps := make([]ScriptStep, 0, len(items))
213+
for index, item := range items {
214+
item = bytes.TrimSpace(item)
215+
if len(item) == 0 || bytes.Equal(item, []byte("null")) {
216+
continue
217+
}
218+
if item[0] == '{' && interactionScriptObjectHasContainer(item) {
219+
nested, ok, err := parseInteractionScriptObject(item)
220+
if err != nil {
221+
return nil, fmt.Errorf("item %d: %w", index, err)
222+
}
223+
if ok {
224+
steps = append(steps, nested...)
225+
continue
226+
}
227+
}
228+
var step ScriptStep
229+
if err := json.Unmarshal(item, &step); err != nil {
230+
return nil, fmt.Errorf("item %d: %w", index, err)
231+
}
232+
steps = append(steps, step)
233+
}
234+
return steps, nil
235+
}
236+
237+
func interactionScriptObjectHasContainer(data []byte) bool {
238+
fields := map[string]json.RawMessage{}
239+
if err := json.Unmarshal(data, &fields); err != nil {
240+
return false
241+
}
242+
for _, name := range []string{
243+
"steps",
244+
"script",
245+
"script_steps",
246+
"scriptSteps",
247+
"interaction_script",
248+
"interactionScript",
249+
"interaction_steps",
250+
"interactionSteps",
251+
"records",
252+
"recorded_steps",
253+
"recordedSteps",
254+
"events",
255+
"entries",
256+
"items",
257+
"included",
258+
"actions",
259+
"timeline",
260+
"tests",
261+
"test_cases",
262+
"testCases",
263+
"cases",
264+
"scenarios",
265+
"fixtures",
266+
"recordings",
267+
"runs",
268+
"operations",
269+
"commands",
270+
"plays",
271+
} {
272+
raw, ok := fields[name]
273+
if !ok {
274+
continue
275+
}
276+
raw = bytes.TrimSpace(raw)
277+
if len(raw) > 0 && (raw[0] == '{' || raw[0] == '[') {
278+
return true
279+
}
280+
}
281+
if scriptStepJSONHasDirectFields(fields) {
282+
return false
283+
}
284+
for _, name := range []string{
285+
"scenario",
286+
"test",
287+
"case",
288+
"fixture",
289+
"interaction",
290+
"viewer",
291+
"connection",
292+
"stepConnection",
293+
"stepsConnection",
294+
"interactionConnection",
295+
"recordingConnection",
296+
"recording",
297+
"session",
298+
"run",
299+
} {
300+
raw, ok := fields[name]
301+
if !ok {
302+
continue
303+
}
304+
raw = bytes.TrimSpace(raw)
305+
if len(raw) > 0 && raw[0] == '{' {
306+
return true
307+
}
308+
}
309+
for _, name := range []string{"data", "payload", "body", "result", "response"} {
310+
raw, ok := fields[name]
311+
if !ok {
312+
continue
313+
}
314+
raw = bytes.TrimSpace(raw)
315+
if len(raw) > 0 && raw[0] == '{' && interactionScriptObjectHasContainer(raw) {
316+
return true
317+
}
318+
}
319+
return false
320+
}
321+
193322
func parseInteractionScriptOptionalStepsValue(value json.RawMessage) ([]ScriptStep, bool, error) {
194323
value = bytes.TrimSpace(value)
195324
if len(value) == 0 || bytes.Equal(value, []byte("null")) {

internal/tui/tui_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8614,6 +8614,53 @@ func TestParseInteractionScriptAcceptsWrapperObjects(t *testing.T) {
86148614
}
86158615
}
86168616

8617+
func TestParseInteractionScriptAcceptsSuiteCaseArrays(t *testing.T) {
8618+
for name, script := range map[string]string{
8619+
"cases": `{"cases":[
8620+
{"name":"case-one","steps":[
8621+
{"action":"type","value":"go"},
8622+
{"type":"press","value":"enter","expectEvent":{"type":"prompt_submitted","value":"go"}}
8623+
]}
8624+
]}`,
8625+
"data_test_cases": `{"data":{"testCases":[
8626+
{"id":"test-1","scenario":{"timeline":[
8627+
{"action":"type","value":"ship"},
8628+
{"type":"press","value":"enter","expectEvent":{"type":"prompt_submitted","value":"ship"}}
8629+
]}}
8630+
]}}`,
8631+
"scenarios": `{"scenarios":[
8632+
{"fixture":{"scriptSteps":[
8633+
{"action":"type","value":"audit"},
8634+
{"type":"press","value":"enter","expectEvent":{"type":"prompt_submitted","value":"audit"}}
8635+
]}}
8636+
]}`,
8637+
"top_level_case_array": `[
8638+
{"name":"array-case","steps":[
8639+
{"action":"type","value":"run"},
8640+
{"type":"press","value":"enter","expectEvent":{"type":"prompt_submitted","value":"run"}}
8641+
]}
8642+
]`,
8643+
} {
8644+
t.Run(name, func(t *testing.T) {
8645+
steps, err := ParseInteractionScript([]byte(script))
8646+
if err != nil {
8647+
t.Fatal(err)
8648+
}
8649+
if len(steps) != 2 {
8650+
t.Fatalf("steps = %#v", steps)
8651+
}
8652+
screen := NewREPLScreen(30, 6, nil)
8653+
result, err := RunInteractionScriptChecked(&screen, steps)
8654+
if err != nil {
8655+
t.Fatal(err)
8656+
}
8657+
if len(result.Events) != 1 || result.Events[0].Type != ScreenEventPromptSubmitted {
8658+
t.Fatalf("events = %#v", result.Events)
8659+
}
8660+
})
8661+
}
8662+
}
8663+
86178664
func TestParseInteractionScriptAcceptsStepRecordWrappers(t *testing.T) {
86188665
for name, script := range map[string]string{
86198666
"array": `[

0 commit comments

Comments
 (0)