Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ./...

Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 12 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ./...
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -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 \
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion docs/deployment.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 4 additions & 8 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down
25 changes: 6 additions & 19 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand All @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
14 changes: 7 additions & 7 deletions plugins/ask.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
}
Expand All @@ -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)
Expand Down
92 changes: 77 additions & 15 deletions plugins/ask_resilience_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package plugins

import (
"context"
"errors"
"net/http"
"net/url"
"os"
Expand Down Expand Up @@ -246,41 +247,102 @@ 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='<nav><span>Search Assist</span></nav><section id="card"><header><span>Search Assist</span></header><p id="answer"></p><div id="sources"></div><button>More</button></section><article data-testid="result"><a data-testid="result-title-a" href="https://example.org/other">Unrelated search result</a></article>'`
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='<a href="https://example.org/comparison">Source</a>';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='<header><svg></svg>Search Assist</header><p>2002</p><a href="https://example.org/date">Source</a>'`, nil), chromedp.Evaluate("("+askRenderedSearchAssistScript+")()", &answer)); err != nil {
if err := chromedp.Do(page, chromedp.Evaluate[chromedp.Void](`document.getElementById('card').innerHTML='<header><svg></svg>Search Assist</header><p>2002</p><a href="https://example.org/date">Source</a>'`)); err != nil {
t.Fatal(err)
}
answer, err = chromedp.Run(page, chromedp.Evaluate[askRenderedSearchAssistData]("("+askRenderedSearchAssistScript+")()"))
if err != nil {
t.Fatal(err)
}
if answer.Text != "2002" {
t.Fatalf("short answer lost: %+v", answer)
}
}

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='<main id="results"></main>'`)); 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='<article data-testid="result"><a data-testid="result-title-a" href="https://duckduckgo.com/l/?uddg=https%3A%2F%2Fexample.org%2Fcomparison">Example comparison</a><p data-testid="result-snippet">Compare equivalent models.</p></article><article class="result"><a class="result__a" href="https://example.org/guide">Example guide</a><p class="result__snippet">A public guide.</p></article><article data-testid="result"><a data-testid="result-title-a" href="javascript:void(0)">Ignore this link</a></article>';}, 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")
Expand Down
Loading