diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 44c8afa..0d2cbb5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -38,7 +38,7 @@ jobs: uses: actions/setup-go@v7 with: # Keep CI on the patched standard library release reported by govulncheck. - go-version: '1.26.6' + go-version: '1.26.9' cache: true - name: Check formatting @@ -57,6 +57,16 @@ jobs: - name: Run go vet run: go vet ./... + - name: Locate Chrome for offline browser regressions + shell: bash + run: | + test_browser="$(command -v google-chrome || command -v chromium || command -v chromium-browser || true)" + if [[ -z "$test_browser" ]]; then + echo "Chrome or Chromium is required for the offline Ask regression tests." >&2 + exit 1 + fi + printf 'GOBOT_TEST_BROWSER=%s\n' "$test_browser" >> "$GITHUB_ENV" + - name: Run tests with race detection run: go test -race ./... diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index fb73fe1..e2e142b 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,18 +37,18 @@ jobs: - name: Set up Go uses: actions/setup-go@v7 with: - go-version: '1.26.6' + go-version: '1.26.9' cache: true - name: Initialize CodeQL - uses: github/codeql-action/init@v4.38.2 + uses: github/codeql-action/init@v4.38.3 with: languages: go - name: Autobuild - uses: github/codeql-action/autobuild@v4.38.2 + uses: github/codeql-action/autobuild@v4.38.3 - name: Perform CodeQL analysis - uses: github/codeql-action/analyze@v4.38.2 + uses: github/codeql-action/analyze@v4.38.3 with: category: /language:go diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d42864d..f8657af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -13,7 +13,8 @@ review and maintain. ## Local checks -GoBot currently targets Go `1.26.6` or newer. Before opening a pull request: +GoBot targets Go `1.26.9+` (or `1.27.2+` for the Go 1.27 series). +Before opening a pull request: ```sh go test ./... @@ -26,6 +27,16 @@ make build Use `gofmt` on changed Go files. If a change adds a plugin or configuration option, include focused tests and update the relevant guide under `docs/`. +For changes to the rendered `!ask` fallback or its browser dependencies, also +run the offline browser fixtures with your Chrome/Chromium executable: + +```sh +GOBOT_TEST_BROWSER=/path/to/chrome go test -race ./plugins -run 'TestAskRendered' -count=1 +``` + +These fixtures use synthetic pages without contacting DuckDuckGo or sending +IRC messages. CI runs them with the runner's installed browser as well. + ## Branches and pull requests - Start from an up-to-date `main` branch. diff --git a/Dockerfile b/Dockerfile index 7969e51..f0f6f6a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # syntax=docker/dockerfile:1 -FROM golang:1.26.6-alpine AS builder +FROM golang:1.26.9-alpine AS builder WORKDIR /src COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod \ diff --git a/README.md b/README.md index 479a9bd..d5e37e6 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![GoBot CI](https://github.com/variablenix/GoBot/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/variablenix/GoBot/actions/workflows/ci.yml) [![CodeQL](https://github.com/variablenix/GoBot/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/variablenix/GoBot/actions/workflows/codeql.yml) -[![Go 1.26.6+](https://img.shields.io/badge/Go-1.26.6%2B-00ADD8?logo=go&logoColor=white)](go.mod) +[![Go 1.26.9+](https://img.shields.io/badge/Go-1.26.9%2B-00ADD8?logo=go&logoColor=white)](go.mod) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) GoBot is an extensible Go IRC bot for long-running use on one or more IRC @@ -53,7 +53,7 @@ are also persisted so they survive restarts. Requirements: -- Go `1.26.6+` or a newer supported release +- Go `1.26.9+` (or `1.27.2+` when using the Go 1.27 series) - IRC network access - a writable data directory diff --git a/docs/deployment.md b/docs/deployment.md index c1fd13a..e5333de 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -5,7 +5,7 @@ systemd service, and using Docker. ## Requirements -- Go `1.26.6+` or a newer supported release +- Go `1.26.9+` (or `1.27.2+` when using the Go 1.27 series) - a writable filesystem for the BoltDB database - network access to the IRC servers - optionally, Docker and Docker Compose @@ -42,6 +42,11 @@ make test Configuration-only changes require a restart. Code changes require a new build before restarting. +Use a patched Go toolchain when rebuilding: Go 1.26.9 and 1.27.2 contain +standard-library security fixes that are not supplied by updating modules +alone. An existing binary retains its old standard library until it is rebuilt +and the service is restarted. CI and the Docker builder use Go 1.26.9. + ## First-run workflow There is no setup wizard yet. GoBot is configuration-first: diff --git a/go.mod b/go.mod index 1fd5989..86a2659 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,23 @@ module github.com/variablenix/GoBot -go 1.26.6 +go 1.26.9 require ( github.com/agnivade/levenshtein v1.2.1 - github.com/chromedp/chromedp v0.16.0 + github.com/chromedp/chromedp v0.20.1 github.com/spf13/viper v1.21.0 go.etcd.io/bbolt v1.5.0 go.uber.org/zap v1.28.0 - golang.org/x/net v0.59.0 + golang.org/x/net v0.60.0 golang.org/x/text v0.42.0 gopkg.in/irc.v3 v3.1.4 ) require ( - github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f // indirect - github.com/chromedp/sysutil v1.1.0 // indirect + github.com/chromedp/cdproto v0.157.8 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 // indirect github.com/go-viper/mapstructure/v2 v2.4.0 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/gobwas/ws v1.4.0 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/sagikazarmark/locafero v0.11.0 // indirect github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect diff --git a/go.sum b/go.sum index 0abfc78..4d1ae13 100644 --- a/go.sum +++ b/go.sum @@ -2,12 +2,10 @@ github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KO github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= -github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f h1:0Z1zcSLEmnj2c2CmJYBqewtS6pxhB39bNWUSEUAWjgk= -github.com/chromedp/cdproto v0.0.0-20260714215040-dc233986426f/go.mod h1:RwFsSODCtFExll+GhHM6R92SARHR3Z3oipaxLHj46C0= -github.com/chromedp/chromedp v0.16.0 h1:rOO4deOm4CbZgBCa8mD9g2rDyIoNs0BkgvNrlbp5ouk= -github.com/chromedp/chromedp v0.16.0/go.mod h1:rbuGKFT1vMcFcFqKfPIO1GpX/N+2s8onm2qMxZLbU5U= -github.com/chromedp/sysutil v1.1.0 h1:PUFNv5EcprjqXZD9nJb9b/c9ibAbxiYo4exNWZyipwM= -github.com/chromedp/sysutil v1.1.0/go.mod h1:WiThHUdltqCNKGc4gaU50XgYjwjYIhKWoHGPTUfWTJ8= +github.com/chromedp/cdproto v0.157.8 h1:Tkq0AjsUlC3Z4sJfHBzV/uAAxnTF/AZyuSTjfV/1gvk= +github.com/chromedp/cdproto v0.157.8/go.mod h1:h68JaIuFr9o3yiayxEYhBdvaxgYO3shDrPSYhNlyfu0= +github.com/chromedp/chromedp v0.20.1 h1:983T9wmyM09JtROWlxhLQwljKOQovqt8CVE89W3U0NA= +github.com/chromedp/chromedp v0.20.1/go.mod h1:6z1Y5FdKUxRzxx2qcC5h1xjaBJrSWrZp1mjgITfNZig= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -21,22 +19,12 @@ github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68 h1:KZaTBSy github.com/go-json-experiment/json v0.0.0-20260623181947-01eb4420fa68/go.mod h1:tphK2c80bpPhMOI4v6bIc2xWywPfbqi1Z06+RcrMkDg= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= -github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80 h1:6Yzfa6GP0rIo/kULo2bwGEkFvCePZ3qHDDTC3/J9Swo= -github.com/ledongthuc/pdf v0.0.0-20220302134840-0c2507a12d80/go.mod h1:imJHygn/1yfhB7XSJJKlFZKl/J+dCPAknuiaGOshXAs= -github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde h1:x0TT0RDC7UhAVbbWWBzr41ElhJx5tXPWkIHA2HWPRuw= -github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -71,11 +59,10 @@ go.uber.org/zap v1.28.0 h1:IZzaP1Fv73/T/pBMLk4VutPl36uNC+OSUh3JLG3FIjo= go.uber.org/zap v1.28.0/go.mod h1:rDLpOi171uODNm/mxFcuYWxDsqWSAVkFdX4XojSKg/Q= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= -golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= +golang.org/x/net v0.60.0 h1:79p50tfZlm0J9YfoDsSi639qSXNGVwEzOPLCxM2FsYU= +golang.org/x/net v0.60.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= diff --git a/plugins/ask.go b/plugins/ask.go index 098e105..90add59 100644 --- a/plugins/ask.go +++ b/plugins/ask.go @@ -1031,8 +1031,7 @@ func askDuckDuckGoRenderedSearchAssistOnce(ctx context.Context, question, fallba browserCtx, browserCancel := chromedp.NewContext(allocatorCtx) defer browserCancel() - var result askRenderedSearchAssistData - if err := chromedp.Run(browserCtx, chromedp.Navigate(duckDuckGoSearchAssistURL(question))); err != nil { + if err := chromedp.Do(browserCtx, chromedp.Navigate(duckDuckGoSearchAssistURL(question))); err != nil { return askSource{}, false } // Navigation and browser startup have already consumed some of the budget. @@ -1045,9 +1044,10 @@ func askDuckDuckGoRenderedSearchAssistOnce(ctx context.Context, question, fallba if assistTimeout <= 0 { return askSource{}, false } - if err := chromedp.Run(browserCtx, chromedp.PollFunction(askRenderedSearchAssistScript, &result, + result, err := chromedp.Run(browserCtx, chromedp.PollFunction[askRenderedSearchAssistData](askRenderedSearchAssistScript, chromedp.WithPollingInterval(100*time.Millisecond), - chromedp.WithPollingTimeout(assistTimeout))); err == nil { + chromedp.WithPollingTimeout(assistTimeout))) + if err == nil { if source, ok := parseRenderedSearchAssist(result, fallbackURL); ok { return source, true } @@ -1061,14 +1061,14 @@ func askDuckDuckGoRenderedSearchAssistOnce(ctx context.Context, question, fallba if !fetchResults { return askSource{}, false } - var results []askRenderedSearchResult resultTimeout := time.Until(deadline) if resultTimeout <= 0 { return askSource{}, false } - if err := chromedp.Run(browserCtx, chromedp.PollFunction(askRenderedSearchResultsScript, &results, + results, err := chromedp.Run(browserCtx, chromedp.PollFunction[[]askRenderedSearchResult](askRenderedSearchResultsScript, chromedp.WithPollingInterval(100*time.Millisecond), - chromedp.WithPollingTimeout(resultTimeout))); err != nil { + chromedp.WithPollingTimeout(resultTimeout))) + if err != nil { return askSource{}, false } selected, ok := selectAskSearchResult(question, results) diff --git a/plugins/ask_resilience_test.go b/plugins/ask_resilience_test.go index 7a601d4..c8e0032 100644 --- a/plugins/ask_resilience_test.go +++ b/plugins/ask_resilience_test.go @@ -2,6 +2,7 @@ package plugins import ( "context" + "errors" "net/http" "net/url" "os" @@ -246,34 +247,35 @@ func TestAskBrowserBudgetLeavesFallbackTime(t *testing.T) { // Run with GOBOT_TEST_BROWSER=/path/to/chromium. This fixture runs entirely // offline and verifies the DOM code against a delayed, expanded answer card. func TestAskRenderedCardWaitsForAnswer(t *testing.T) { - executable := os.Getenv("GOBOT_TEST_BROWSER") - if executable == "" { - t.Skip("set GOBOT_TEST_BROWSER for offline browser fixture") - } - ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second) - defer cancel() - allocator, stop := chromedp.NewExecAllocator(ctx, append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(executable))...) - defer stop() - page, closePage := chromedp.NewContext(allocator) - defer closePage() - var before *askRenderedSearchAssistData - var answer askRenderedSearchAssistData + page := askBrowserFixtureContext(t) const fixture = `document.body.innerHTML='
Search Assist

Unrelated search result
'` - if err := chromedp.Run(page, chromedp.Navigate("about:blank"), chromedp.Evaluate(fixture, nil), chromedp.Evaluate("("+askRenderedSearchAssistScript+")()", &before)); err != nil { + if err := chromedp.Do(page, chromedp.Navigate("about:blank"), chromedp.Evaluate[chromedp.Void](fixture)); err != nil { + t.Fatal(err) + } + before, err := chromedp.Run(page, chromedp.Evaluate[*askRenderedSearchAssistData]("("+askRenderedSearchAssistScript+")()")) + if err != nil { t.Fatal(err) } if before != nil { t.Fatalf("empty card accepted: %+v", before) } const delayed = `setTimeout(() => {document.getElementById('answer').textContent='Dirt bikes and go-karts have different purchase and ownership costs. Compare equivalent models before deciding.';document.getElementById('sources').innerHTML='Source';const details=document.createElement('div');details.textContent='Additional comparison details. '.repeat(180);document.getElementById('card').append(details);}, 1800)` - if err := chromedp.Run(page, chromedp.Evaluate(delayed, nil), chromedp.PollFunction(askRenderedSearchAssistScript, &answer, chromedp.WithPollingInterval(100*time.Millisecond), chromedp.WithPollingTimeout(4*time.Second))); err != nil { + if err := chromedp.Do(page, chromedp.Evaluate[chromedp.Void](delayed)); err != nil { + t.Fatal(err) + } + answer, err := chromedp.Run(page, chromedp.PollFunction[askRenderedSearchAssistData](askRenderedSearchAssistScript, chromedp.WithPollingInterval(100*time.Millisecond), chromedp.WithPollingTimeout(4*time.Second))) + if err != nil { t.Fatal(err) } if !strings.Contains(answer.Text, "purchase and ownership") || strings.Contains(answer.Text, "Unrelated") || len(answer.Links) != 1 { t.Fatalf("wrong card: %+v", answer) } // A short, cited answer is valid too: do not impose a prose-length floor. - if err := chromedp.Run(page, chromedp.Evaluate(`document.getElementById('card').innerHTML='
Search Assist

2002

Source'`, nil), chromedp.Evaluate("("+askRenderedSearchAssistScript+")()", &answer)); err != nil { + if err := chromedp.Do(page, chromedp.Evaluate[chromedp.Void](`document.getElementById('card').innerHTML='
Search Assist

2002

Source'`)); err != nil { + t.Fatal(err) + } + answer, err = chromedp.Run(page, chromedp.Evaluate[askRenderedSearchAssistData]("("+askRenderedSearchAssistScript+")()")) + if err != nil { t.Fatal(err) } if answer.Text != "2002" { @@ -281,6 +283,66 @@ func TestAskRenderedCardWaitsForAnswer(t *testing.T) { } } +func askBrowserFixtureContext(t *testing.T) context.Context { + t.Helper() + executable := os.Getenv("GOBOT_TEST_BROWSER") + if executable == "" { + t.Skip("set GOBOT_TEST_BROWSER for offline browser fixture") + } + ctx, cancel := context.WithTimeout(t.Context(), 20*time.Second) + allocator, stop := chromedp.NewExecAllocator(ctx, append(chromedp.DefaultExecAllocatorOptions[:], chromedp.ExecPath(executable))...) + page, closePage := chromedp.NewContext(allocator) + t.Cleanup(func() { + closePage() + stop() + cancel() + }) + return page +} + +func TestAskRenderedResultsPollingPreservesFallbacks(t *testing.T) { + page := askBrowserFixtureContext(t) + if err := chromedp.Do(page, chromedp.Navigate("about:blank"), chromedp.Evaluate[chromedp.Void](`document.body.innerHTML='
'`)); err != nil { + t.Fatal(err) + } + before, err := chromedp.Run(page, chromedp.Evaluate[[]askRenderedSearchResult]("("+askRenderedSearchResultsScript+")()")) + if err != nil { + t.Fatal(err) + } + if before != nil { + t.Fatalf("empty results accepted: %+v", before) + } + _, err = chromedp.Run(page, chromedp.PollFunction[[]askRenderedSearchResult](askRenderedSearchResultsScript, chromedp.WithPollingInterval(50*time.Millisecond), chromedp.WithPollingTimeout(150*time.Millisecond))) + if !errors.Is(err, chromedp.ErrPollingTimeout) || page.Err() != nil { + t.Fatalf("empty-result polling did not preserve the browser context: %v", err) + } + const delayed = `setTimeout(() => {document.getElementById('results').innerHTML='
Example comparison

Compare equivalent models.

Example guide

A public guide.

Ignore this link
';}, 200)` + if err := chromedp.Do(page, chromedp.Evaluate[chromedp.Void](delayed)); err != nil { + t.Fatal(err) + } + results, err := chromedp.Run(page, chromedp.PollFunction[[]askRenderedSearchResult](askRenderedSearchResultsScript, chromedp.WithPollingInterval(50*time.Millisecond), chromedp.WithPollingTimeout(2*time.Second))) + if err != nil { + t.Fatal(err) + } + want := []askRenderedSearchResult{ + {Title: "Example comparison", URL: "https://example.org/comparison", Snippet: "Compare equivalent models."}, + {Title: "Example guide", URL: "https://example.org/guide", Snippet: "A public guide."}, + } + if !reflect.DeepEqual(results, want) { + t.Fatalf("wrong results or duplicates/unsafe links accepted: %+v", results) + } + step, cancel := context.WithCancel(page) + cancel() + _, err = chromedp.Run(step, chromedp.PollFunction[[]askRenderedSearchResult](askRenderedSearchResultsScript)) + if !errors.Is(err, context.Canceled) || page.Err() != nil { + t.Fatalf("child cancellation did not preserve the browser context: %v", err) + } + value, err := chromedp.Run(page, chromedp.Evaluate[int]("1 + 1")) + if err != nil || value != 2 { + t.Fatalf("browser no longer usable after canceled polling: %d, %v", value, err) + } +} + func TestLiveAskCostComparison(t *testing.T) { if os.Getenv("GOBOT_LIVE_LOOKUPS") != "1" { t.Skip("opt-in live provider smoke test")