From e3e71af6d44ee97a403f1ece75d10b387d1544ab Mon Sep 17 00:00:00 2001 From: Dibbayajyoti Date: Tue, 27 Oct 2026 12:01:29 +0530 Subject: [PATCH] cli: empty lists print to stderr only; scrape --extract keeps the API's field order Human mode: batch list and batch results printed their "no ..." message on stdout while sessions list and logs printed it on stderr. One rule now (DESIGN.md: stdout is data only): an empty list leaves stdout empty and says so on stderr, so `cmd | wc -l` is 0. scrape --extract in human mode (and the file -o writes for it) decoded the envelope into a map and printed the fields sorted. data is now re-read from the response body in the order the API sent it. --json and --jsonl are unchanged. --- CHANGELOG.md | 1 + DESIGN.md | 5 ++ internal/cmd/batch.go | 2 +- internal/cmd/batch_golden_test.go | 4 +- internal/cmd/batch_results.go | 2 +- internal/cmd/batch_test.go | 16 ++-- internal/cmd/empty_results_test.go | 43 ++++++++++ internal/cmd/ops_golden_test.go | 2 +- internal/cmd/scrape.go | 21 ++++- internal/cmd/scrape_golden_test.go | 2 + internal/cmd/scrape_ordered.go | 80 +++++++++++++++++++ internal/cmd/scrape_ordered_test.go | 66 +++++++++++++++ internal/cmd/sessions_golden_test.go | 2 +- .../golden/batch_human/list_empty.stderr | 1 + .../golden/batch_human/list_empty.stdout | 1 - .../golden/batch_human/results_empty.stderr | 1 + .../golden/batch_human/results_empty.stdout | 1 - .../testdata/golden/logs_human/empty.stderr | 1 + .../golden/scrape_human/extract_data.stdout | 4 +- .../scrape_human/extract_data_nested.exit | 1 + .../scrape_human/extract_data_nested.stdout | 19 +++++ .../golden/sessions_human/list_empty.stderr | 1 + 22 files changed, 257 insertions(+), 19 deletions(-) create mode 100644 internal/cmd/empty_results_test.go create mode 100644 internal/cmd/scrape_ordered.go create mode 100644 internal/cmd/scrape_ordered_test.go create mode 100644 internal/cmd/testdata/golden/batch_human/list_empty.stderr create mode 100644 internal/cmd/testdata/golden/batch_human/results_empty.stderr create mode 100644 internal/cmd/testdata/golden/logs_human/empty.stderr create mode 100644 internal/cmd/testdata/golden/scrape_human/extract_data_nested.exit create mode 100644 internal/cmd/testdata/golden/scrape_human/extract_data_nested.stdout create mode 100644 internal/cmd/testdata/golden/sessions_human/list_empty.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index 21d9cf3..c05f349 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Bug fixes from a review of 0.1.3, and live progress on a terminal. No new comman - **Ctrl-C exits 130** with "interrupted", including while `scrape -` waits on stdin. A mistyped subcommand (`spicrawl batch wiat`) is a usage error with a suggestion, not help and exit 0. - **Keys:** a trailing CR/LF or space on the key (flag, env or file) is trimmed. The config file is written via a private temp file, short keys are fully masked, and commands that get the key and base URL from flags work without `$HOME`. - **scrape:** `--body null` is a usage error, not a panic (also `batch submit`). Big integers survive `--json` (**key order now follows the API**, as `--jsonl` already did). Stdout write errors stop the run instead of exiting 0. `-o` writes atomically and **a target error page no longer overwrites the file**. `-o -` means stdout. Screenshot labels never overwrite each other. `--body` merges deeply. A BOM on stdin is stripped. Human output no longer HTML-escapes `<>&`. The multi-URL summary counts correctly. `--impersonate` help says it is on by default. +- **Empty results and `--extract` order** (**behaviour change**, human mode only). `batch list` ("no batch jobs") and `batch results` ("no finished items yet") now print that message on stderr, as `sessions list` and `logs` already did, so stdout is empty and `spicrawl batch list | wc -l` is 0; `-q` silences it. `scrape --extract` (and the file `-o` writes for it) lists the extracted fields in the API's order instead of sorted alphabetically. `--json` and `--jsonl` are unchanged. - **batch:** `wait` and `submit --wait` exit 4 when the job failed or was cancelled. Polling survives transient 5xx and network errors. `results -o` no longer empties an existing file on failure. - **browser:** a server without the cloud browser says so (exit 8) instead of `NOT_FOUND`. `$(spicrawl browser url -q)` yields the bare URL. - **usage summary** shows the enforced monthly allowance. **status** fails when the key check cannot reach the API. diff --git a/DESIGN.md b/DESIGN.md index a766d89..ccd3372 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -16,6 +16,11 @@ never invent a field the API does not accept (the API rejects unknown fields). `TERM=dumb` or `CI`. Switched off, the output is the plain text it always was. It never prints an engine or browser name; it says what the request asked for ("rendering JavaScript"), not which tier the API picked. + An empty result is not data either: in human mode an empty list + (`sessions list`, `logs`, `batch list`, `batch results`) prints nothing on + stdout and its one-line message ("no sessions") on stderr, so + `spicrawl sessions list | wc -l` is 0 and `-q` silences the message. JSON + mode prints the empty collection on stdout. 2. **JSON when not a terminal.** `--json` forces it. Human mode prints tables or the document itself. 3. **Never prompts without a TTY.** Anything interactive has a flag form. diff --git a/internal/cmd/batch.go b/internal/cmd/batch.go index 1eda27c..b2ce5a0 100644 --- a/internal/cmd/batch.go +++ b/internal/cmd/batch.go @@ -388,7 +388,7 @@ func batchRunList(cmd *cobra.Command, _ []string) error { fmt.Sprintf("%d/%d", j.Progress.Completed, j.Progress.Total), j.SubmittedAt}) } if len(rows) == 0 { - fmt.Fprintln(w, "no batch jobs") + p.Info("no batch jobs") // not data: stderr, so stdout stays empty } else { p.Table([]string{"ID", "STATUS", "NAME", "DONE", "SUBMITTED"}, rows) } diff --git a/internal/cmd/batch_golden_test.go b/internal/cmd/batch_golden_test.go index 3ef11e9..c5e3279 100644 --- a/internal/cmd/batch_golden_test.go +++ b/internal/cmd/batch_golden_test.go @@ -153,7 +153,7 @@ func init() { gJSON(200, `{"batches":[`+open.JSON()+`]}`)(w, r) }})}, goldenCase{name: "batch_human/list", human: true, args: []string{"batch", "list"}, server: list(two)}, - goldenCase{name: "batch_human/list_empty", human: true, args: []string{"batch", "list"}, server: list(`{"batches":[]}`)}, + goldenCase{name: "batch_human/list_empty", human: true, stderr: true, args: []string{"batch", "list"}, server: list(`{"batches":[]}`)}, goldenCase{name: "batch_human/list_next_cursor", human: true, args: []string{"batch", "list"}, server: list(`{"batches":[` + completed.JSON() + `],"next_cursor":"1790000000000000000.ABC"}`)}, // results and content @@ -166,7 +166,7 @@ func init() { goldenCase{name: "batch/results_expired", args: []string{"batch", "results", gBatchID}, stderr: true, server: goldenServe(map[string]http.HandlerFunc{"GET /v1/batch/{id}/results": gProblem(410, "ERR::REQUEST::BEYOND_RETENTION", "results expired")})}, goldenCase{name: "batch_human/results", human: true, args: []string{"batch", "results", gBatchID}, server: results("")}, - goldenCase{name: "batch_human/results_empty", human: true, args: []string{"batch", "results", gBatchID}, + goldenCase{name: "batch_human/results_empty", human: true, stderr: true, args: []string{"batch", "results", gBatchID}, server: goldenServe(map[string]http.HandlerFunc{"GET /v1/batch/{id}/results": func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(200) }})}, goldenCase{name: "batch/content", args: []string{"batch", "content", gBatchID, "0"}, server: goldenServe(map[string]http.HandlerFunc{"GET /v1/batch/{id}/tasks/{seq}/content": gJSON(200, `"

A

\n"`)})}, diff --git a/internal/cmd/batch_results.go b/internal/cmd/batch_results.go index 4222ed0..f1581d9 100644 --- a/internal/cmd/batch_results.go +++ b/internal/cmd/batch_results.go @@ -138,7 +138,7 @@ func batchRunResults(cmd *cobra.Command, args []string) error { if !raw { if len(rows) == 0 { - fmt.Fprintln(p.Out, "no finished items yet") + p.Info("no finished items yet") // not data: stderr, so stdout stays empty } else { p.Table([]string{"SEQ", "STATUS", "HTTP", "URL", "BYTES"}, rows) } diff --git a/internal/cmd/batch_test.go b/internal/cmd/batch_test.go index bab1261..ee9507d 100644 --- a/internal/cmd/batch_test.go +++ b/internal/cmd/batch_test.go @@ -17,17 +17,17 @@ import ( // and cobra only hands a fresh one to commands whose ctx is nil. func batchRunCLI(t *testing.T, baseURL string, args ...string) (string, string, int) { t.Helper() - var clear func(c *cobra.Command) - clear = func(c *cobra.Command) { - c.SetContext(nil) //nolint:staticcheck // nil lets cobra propagate the new root context - for _, sub := range c.Commands() { - clear(sub) - } - } - clear(rootCmd) + clearContexts(rootCmd) return runCLI(t, baseURL, args...) } +func clearContexts(c *cobra.Command) { + c.SetContext(nil) //nolint:staticcheck // nil lets cobra propagate the new root context + for _, sub := range c.Commands() { + clearContexts(sub) + } +} + func batchWriteFile(t *testing.T, name, content string) string { t.Helper() p := filepath.Join(t.TempDir(), name) diff --git a/internal/cmd/empty_results_test.go b/internal/cmd/empty_results_test.go new file mode 100644 index 0000000..927ad8f --- /dev/null +++ b/internal/cmd/empty_results_test.go @@ -0,0 +1,43 @@ +package cmd + +import ( + "net/http" + "strings" + "testing" +) + +// An empty list in human mode is not data (DESIGN.md: stdout is data only): +// stdout stays empty, so `cmd | wc -l` is 0, and the message goes to stderr. +// JSON mode is unchanged: it prints the empty collection. +func TestHumanEmptyResultsAreStderrOnly(t *testing.T) { + cases := []struct { + name string + args []string + body string + want string + }{ + {"sessions list", []string{"sessions", "list"}, `{"sessions":[]}`, "no sessions"}, + {"logs", []string{"logs"}, `{"data":[],"page":{"has_more":false,"retention_hours":24}}`, "no requests in the last 24 hours"}, + {"batch list", []string{"batch", "list"}, `{"batches":[]}`, "no batch jobs"}, + {"batch results", []string{"batch", "results", "B1"}, ``, "no finished items yet"}, + } + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + srv, _ := newRecordingServer(t, func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(c.body)) + }) + clearContexts(rootCmd) + out, errb, code := runCLIHuman(t, srv.URL, c.args...) + if code != 0 || out != "" || strings.TrimSpace(errb) != c.want { + t.Errorf("exit %d, stdout %q, stderr %q; want 0, empty, %q", code, out, errb, c.want) + } + + clearContexts(rootCmd) + out, errb, code = runCLIHuman(t, srv.URL, append([]string{"-q"}, c.args...)...) + if code != 0 || out != "" || errb != "" { + t.Errorf("-q: exit %d, stdout %q, stderr %q; want 0, empty, empty", code, out, errb) + } + }) + } +} diff --git a/internal/cmd/ops_golden_test.go b/internal/cmd/ops_golden_test.go index 8356baf..35b50e1 100644 --- a/internal/cmd/ops_golden_test.go +++ b/internal/cmd/ops_golden_test.go @@ -120,7 +120,7 @@ func init() { goldenCase{name: "logs_human/list", human: true, args: []string{"logs"}, server: logs(both)}, goldenCase{name: "logs_human/all_projects", human: true, args: []string{"logs", "--all-projects", "--errors"}, server: logs(both)}, goldenCase{name: "logs_human/has_more", human: true, args: []string{"logs"}, server: logs(`{"data":[` + gLogOK + `],` + gLogPageMore + `}`)}, - goldenCase{name: "logs_human/empty", human: true, args: []string{"logs"}, server: logs(`{"data":[],` + gLogPageEnd + `}`)}, + goldenCase{name: "logs_human/empty", human: true, stderr: true, args: []string{"logs"}, server: logs(`{"data":[],` + gLogPageEnd + `}`)}, goldenCase{name: "logs_human/get_full", human: true, args: []string{"logs", "get", "x"}, server: logGet(gLogOK)}, goldenCase{name: "logs_human/get_blocked", human: true, args: []string{"logs", "get", "x"}, server: logGet(gLogBlocked)}, diff --git a/internal/cmd/scrape.go b/internal/cmd/scrape.go index 282f547..fa61017 100644 --- a/internal/cmd/scrape.go +++ b/internal/cmd/scrape.go @@ -456,7 +456,7 @@ func (r *scrapeResult) document(body map[string]any) ([]byte, string) { return r.resp.Body, "content_base64" } if scrapeWantsData(body) { - if data, ok := r.obj["data"]; ok && data != nil { + if data := r.orderedData(); data != nil { var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetEscapeHTML(false) // a human reads <, > and & as they are @@ -470,6 +470,25 @@ func (r *scrapeResult) document(body map[string]any) ([]byte, string) { return []byte(s), "content" } +// orderedData is the envelope's "data" with its object members in the order +// the API sent them. r.obj is a map, which would print them sorted. nil when +// there is no data (absent or null). +func (r *scrapeResult) orderedData() any { + var env struct { + Data json.RawMessage `json:"data"` + } + if json.Unmarshal(r.resp.Body, &env) != nil || len(env.Data) == 0 { + return nil + } + dec := json.NewDecoder(bytes.NewReader(env.Data)) + dec.UseNumber() + v, err := decodeOrdered(dec) + if err != nil { + return nil + } + return v +} + func scrapeWantsData(body map[string]any) bool { for _, k := range []string{"extract", "ai_extract", "autoparse"} { if v, ok := body[k]; ok && v != nil && v != false { diff --git a/internal/cmd/scrape_golden_test.go b/internal/cmd/scrape_golden_test.go index 1a78c87..91457f0 100644 --- a/internal/cmd/scrape_golden_test.go +++ b/internal/cmd/scrape_golden_test.go @@ -107,6 +107,8 @@ func init() { goldenCase{name: "scrape_human/markdown_meta", human: true, args: []string{"scrape", u, "--format", "markdown", "--meta"}, server: gScrape(md)}, goldenCase{name: "scrape_human/text_no_trailing_newline", human: true, args: []string{"scrape", u, "--format", "text"}, server: gScrape(gScrapeRaw("text/plain", "Example Domain", 200))}, goldenCase{name: "scrape_human/extract_data", human: true, args: []string{"scrape", u, "--extract", `{"title":"h1"}`}, server: gScrape(gScrapeEnv(`,"data":{"title":"Anvil","price":"$19.99 & free shipping"}`))}, + goldenCase{name: "scrape_human/extract_data_nested", human: true, args: []string{"scrape", u, "--extract", `{"title":"h1"}`}, + server: gScrape(gScrapeEnv(`,"data":{"title":"Anvil","sku":"A-1","specs":{"weight":"50kg","color":"black","dims":{"w":1.0,"h":2e3}},"tags":["z","a"],"price":19.99,"empty":{},"none":null}`))}, goldenCase{name: "scrape_human/links", human: true, args: []string{"scrape", u, "--links", "--format", "markdown"}, server: gScrape(gScrapeEnv(`,"links":["https://example.com/a","https://example.com/b?x=1&y=2"]`))}, goldenCase{name: "scrape_human/network", human: true, args: []string{"scrape", u, "--render", "--network-capture", `{}`}, server: gScrape(gScrapeEnv(`,"network":[{"url":"https://example.com/api/price","status":200,"resource_type":"xhr","content_type":"application/json"},{"url":"https://example.com/x.png","resource_type":"image"}]`))}, diff --git a/internal/cmd/scrape_ordered.go b/internal/cmd/scrape_ordered.go new file mode 100644 index 0000000..a2c98bc --- /dev/null +++ b/internal/cmd/scrape_ordered.go @@ -0,0 +1,80 @@ +package cmd + +import ( + "bytes" + "encoding/json" +) + +// ordObj is a JSON object that keeps its members in the order they arrived. +// Decoding into map[string]any loses that: encoding/json writes a map's keys +// sorted, so a human reading `--extract` output saw the fields shuffled. +type ordObj []ordMember + +type ordMember struct { + key string + val any +} + +// MarshalJSON writes the members in order, without HTML escaping (a person +// reads <, > and & as they are). Nested ordObj values come back through here. +func (o ordObj) MarshalJSON() ([]byte, error) { + var b bytes.Buffer + enc := json.NewEncoder(&b) + enc.SetEscapeHTML(false) + b.WriteByte('{') + for i, m := range o { + if i > 0 { + b.WriteByte(',') + } + for j, v := range []any{m.key, m.val} { + if j == 1 { + b.WriteByte(':') + } + if err := enc.Encode(v); err != nil { + return nil, err + } + b.Truncate(b.Len() - 1) // Encode ends with a newline + } + } + b.WriteByte('}') + return b.Bytes(), nil +} + +// decodeOrdered reads the next JSON value from dec: objects as ordObj, arrays +// as []any, scalars as Token returns them (json.Number if dec.UseNumber). +func decodeOrdered(dec *json.Decoder) (any, error) { + t, err := dec.Token() + if err != nil { + return nil, err + } + d, ok := t.(json.Delim) + if !ok { + return t, nil + } + if d == '{' { + obj := ordObj{} + for dec.More() { + k, err := dec.Token() + if err != nil { + return nil, err + } + v, err := decodeOrdered(dec) + if err != nil { + return nil, err + } + obj = append(obj, ordMember{k.(string), v}) + } + _, err = dec.Token() // } + return obj, err + } + arr := []any{} + for dec.More() { + v, err := decodeOrdered(dec) + if err != nil { + return nil, err + } + arr = append(arr, v) + } + _, err = dec.Token() // ] + return arr, err +} diff --git a/internal/cmd/scrape_ordered_test.go b/internal/cmd/scrape_ordered_test.go new file mode 100644 index 0000000..c8256c1 --- /dev/null +++ b/internal/cmd/scrape_ordered_test.go @@ -0,0 +1,66 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "net/http" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestDecodeOrderedKeepsMemberOrderAndNumbers(t *testing.T) { + in := `{"z":1.0,"a":{"y":[{"b":1e3,"a":null}],"x":{}},"m":[],"s":"a&c"}` + dec := json.NewDecoder(strings.NewReader(in)) + dec.UseNumber() + v, err := decodeOrdered(dec) + if err != nil { + t.Fatal(err) + } + var buf bytes.Buffer + enc := json.NewEncoder(&buf) + enc.SetEscapeHTML(false) + if err := enc.Encode(v); err != nil { + t.Fatal(err) + } + if got := strings.TrimSpace(buf.String()); got != in { + t.Errorf("round trip changed it:\n got %s\nwant %s", got, in) + } +} + +// Human mode prints --extract data in the API's order, and so does the file +// -o writes (both come from document()). +func TestScrapeHumanDataKeepsAPIOrder(t *testing.T) { + scrapeHumanMode(t) + srv, _ := newRecordingServer(t, func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"url":"u","status":200,"content":"","data":{"title":"T","price":9,"list":[{"z":1,"a":2}]}}`)) + }) + want := "{\n \"title\": \"T\",\n \"price\": 9,\n \"list\": [\n {\n \"z\": 1,\n \"a\": 2\n }\n ]\n}\n" + out, stderr, code := scrapeRunCLI(t, srv.URL, "scrape", "https://example.com", "--extract", `{"title":"h1"}`) + if code != 0 || out != want { + t.Fatalf("exit %d, stderr %s\n got %q\nwant %q", code, stderr, out, want) + } + + file := filepath.Join(t.TempDir(), "data.json") + if _, stderr, code := scrapeRunCLI(t, srv.URL, "scrape", "https://example.com", "--extract", `{"title":"h1"}`, "-o", file); code != 0 { + t.Fatalf("exit %d, stderr %s", code, stderr) + } + if b, _ := os.ReadFile(file); strings.TrimSpace(string(b)) != strings.TrimSpace(want) { + t.Errorf("-o file = %q, want %q", b, want) + } +} + +// JSON mode prints the API's bytes (re-indented) when the envelope is +// untouched, so the order was always the API's there. +func TestScrapeJSONDataKeepsAPIOrder(t *testing.T) { + srv, _ := newRecordingServer(t, func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + _, _ = w.Write([]byte(`{"url":"u","status":200,"content":"","data":{"title":"T","price":9}}`)) + }) + out, _, code := scrapeRunCLI(t, srv.URL, "scrape", "https://example.com", "--extract", `{"title":"h1"}`) + if i, j := strings.Index(out, `"title"`), strings.Index(out, `"price"`); code != 0 || i < 0 || i > j { + t.Errorf("exit %d, data out of order:\n%s", code, out) + } +} diff --git a/internal/cmd/sessions_golden_test.go b/internal/cmd/sessions_golden_test.go index eb33ac9..9ea7236 100644 --- a/internal/cmd/sessions_golden_test.go +++ b/internal/cmd/sessions_golden_test.go @@ -58,7 +58,7 @@ func init() { goldenCase{name: "sessions_human/create", human: true, args: []string{"sessions", "create"}, server: one("POST", "/v1/sessions", gSessionNew, 201)}, goldenCase{name: "sessions_human/list", human: true, args: []string{"sessions", "list"}, server: list(page)}, - goldenCase{name: "sessions_human/list_empty", human: true, args: []string{"sessions", "list"}, server: list(`{"sessions":[]}`)}, + goldenCase{name: "sessions_human/list_empty", human: true, stderr: true, args: []string{"sessions", "list"}, server: list(`{"sessions":[]}`)}, goldenCase{name: "sessions_human/list_next_cursor", human: true, args: []string{"sessions", "list"}, server: list(more)}, goldenCase{name: "sessions_human/get", human: true, args: []string{"sessions", "get", gSessionID}, server: one("GET", "/v1/sessions/{id}", gSession, 200)}, goldenCase{name: "sessions_human/get_released", human: true, args: []string{"sessions", "get", gSessionID}, server: one("GET", "/v1/sessions/{id}", gSessionReleased, 200)}, diff --git a/internal/cmd/testdata/golden/batch_human/list_empty.stderr b/internal/cmd/testdata/golden/batch_human/list_empty.stderr new file mode 100644 index 0000000..ddc789b --- /dev/null +++ b/internal/cmd/testdata/golden/batch_human/list_empty.stderr @@ -0,0 +1 @@ +no batch jobs diff --git a/internal/cmd/testdata/golden/batch_human/list_empty.stdout b/internal/cmd/testdata/golden/batch_human/list_empty.stdout index ddc789b..e69de29 100644 --- a/internal/cmd/testdata/golden/batch_human/list_empty.stdout +++ b/internal/cmd/testdata/golden/batch_human/list_empty.stdout @@ -1 +0,0 @@ -no batch jobs diff --git a/internal/cmd/testdata/golden/batch_human/results_empty.stderr b/internal/cmd/testdata/golden/batch_human/results_empty.stderr new file mode 100644 index 0000000..22cb73b --- /dev/null +++ b/internal/cmd/testdata/golden/batch_human/results_empty.stderr @@ -0,0 +1 @@ +no finished items yet diff --git a/internal/cmd/testdata/golden/batch_human/results_empty.stdout b/internal/cmd/testdata/golden/batch_human/results_empty.stdout index 22cb73b..e69de29 100644 --- a/internal/cmd/testdata/golden/batch_human/results_empty.stdout +++ b/internal/cmd/testdata/golden/batch_human/results_empty.stdout @@ -1 +0,0 @@ -no finished items yet diff --git a/internal/cmd/testdata/golden/logs_human/empty.stderr b/internal/cmd/testdata/golden/logs_human/empty.stderr new file mode 100644 index 0000000..0d0014d --- /dev/null +++ b/internal/cmd/testdata/golden/logs_human/empty.stderr @@ -0,0 +1 @@ +no requests in the last 24 hours diff --git a/internal/cmd/testdata/golden/scrape_human/extract_data.stdout b/internal/cmd/testdata/golden/scrape_human/extract_data.stdout index a2ead9e..ef7b75c 100644 --- a/internal/cmd/testdata/golden/scrape_human/extract_data.stdout +++ b/internal/cmd/testdata/golden/scrape_human/extract_data.stdout @@ -1,4 +1,4 @@ { - "price": "$19.99 & free shipping", - "title": "Anvil" + "title": "Anvil", + "price": "$19.99 & free shipping" } diff --git a/internal/cmd/testdata/golden/scrape_human/extract_data_nested.exit b/internal/cmd/testdata/golden/scrape_human/extract_data_nested.exit new file mode 100644 index 0000000..573541a --- /dev/null +++ b/internal/cmd/testdata/golden/scrape_human/extract_data_nested.exit @@ -0,0 +1 @@ +0 diff --git a/internal/cmd/testdata/golden/scrape_human/extract_data_nested.stdout b/internal/cmd/testdata/golden/scrape_human/extract_data_nested.stdout new file mode 100644 index 0000000..0d73925 --- /dev/null +++ b/internal/cmd/testdata/golden/scrape_human/extract_data_nested.stdout @@ -0,0 +1,19 @@ +{ + "title": "Anvil", + "sku": "A-1", + "specs": { + "weight": "50kg", + "color": "black", + "dims": { + "w": 1.0, + "h": 2e3 + } + }, + "tags": [ + "z", + "a" + ], + "price": 19.99, + "empty": {}, + "none": null +} diff --git a/internal/cmd/testdata/golden/sessions_human/list_empty.stderr b/internal/cmd/testdata/golden/sessions_human/list_empty.stderr new file mode 100644 index 0000000..693540e --- /dev/null +++ b/internal/cmd/testdata/golden/sessions_human/list_empty.stderr @@ -0,0 +1 @@ +no sessions