boxprobe-scout 0.1.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- boxprobe_scout-0.1.3/.forgejo/workflows/publish.yml +31 -0
- boxprobe_scout-0.1.3/.forgejo/workflows/test.yml +22 -0
- boxprobe_scout-0.1.3/.github/ISSUE_TEMPLATE/bug_report.yml +74 -0
- boxprobe_scout-0.1.3/.github/ISSUE_TEMPLATE/config.yml +8 -0
- boxprobe_scout-0.1.3/.github/ISSUE_TEMPLATE/feature_request.yml +42 -0
- boxprobe_scout-0.1.3/.github/PULL_REQUEST_TEMPLATE.md +29 -0
- boxprobe_scout-0.1.3/.github/workflows/ci.yml +85 -0
- boxprobe_scout-0.1.3/.github/workflows/release.yml +101 -0
- boxprobe_scout-0.1.3/.gitignore +12 -0
- boxprobe_scout-0.1.3/AGENTS.md +7 -0
- boxprobe_scout-0.1.3/CHANGELOG.md +34 -0
- boxprobe_scout-0.1.3/CLAUDE.md +181 -0
- boxprobe_scout-0.1.3/CODE_OF_CONDUCT.md +85 -0
- boxprobe_scout-0.1.3/CONTRIBUTING.md +109 -0
- boxprobe_scout-0.1.3/LICENSE +21 -0
- boxprobe_scout-0.1.3/PKG-INFO +258 -0
- boxprobe_scout-0.1.3/README.md +202 -0
- boxprobe_scout-0.1.3/RELEASING.md +113 -0
- boxprobe_scout-0.1.3/pyproject.toml +134 -0
- boxprobe_scout-0.1.3/scout/__init__.py +1 -0
- boxprobe_scout-0.1.3/scout/bridge/__init__.py +0 -0
- boxprobe_scout-0.1.3/scout/cli.py +783 -0
- boxprobe_scout-0.1.3/scout/collector/__init__.py +0 -0
- boxprobe_scout-0.1.3/scout/collector/control.py +114 -0
- boxprobe_scout-0.1.3/scout/collector/db.py +187 -0
- boxprobe_scout-0.1.3/scout/collector/proxy.py +80 -0
- boxprobe_scout-0.1.3/scout/collector/subprocess.py +123 -0
- boxprobe_scout-0.1.3/scout/config.py +58 -0
- boxprobe_scout-0.1.3/scout/git.py +40 -0
- boxprobe_scout-0.1.3/scout/index.py +134 -0
- boxprobe_scout-0.1.3/scout/matcher/__init__.py +0 -0
- boxprobe_scout-0.1.3/scout/matcher/align.py +145 -0
- boxprobe_scout-0.1.3/scout/matcher/compare.py +360 -0
- boxprobe_scout-0.1.3/scout/matcher/diff_db.py +264 -0
- boxprobe_scout-0.1.3/scout/matcher/diff_report.py +1382 -0
- boxprobe_scout-0.1.3/scout/matcher/noise.py +506 -0
- boxprobe_scout-0.1.3/scout/matcher/normalize.py +160 -0
- boxprobe_scout-0.1.3/scout/mcp/__init__.py +0 -0
- boxprobe_scout-0.1.3/scout/mock_vars.py +150 -0
- boxprobe_scout-0.1.3/scout/report/__init__.py +0 -0
- boxprobe_scout-0.1.3/scout/report/html.py +77 -0
- boxprobe_scout-0.1.3/scout/report/junit.py +55 -0
- boxprobe_scout-0.1.3/scout/report/verify_html.py +303 -0
- boxprobe_scout-0.1.3/scout/run_metadata.py +62 -0
- boxprobe_scout-0.1.3/scout/runner/__init__.py +13 -0
- boxprobe_scout-0.1.3/scout/runner/executor.py +296 -0
- boxprobe_scout-0.1.3/scout/runner/locator.py +383 -0
- boxprobe_scout-0.1.3/scout/runner/page.py +178 -0
- boxprobe_scout-0.1.3/scout/runner/scenario.py +58 -0
- boxprobe_scout-0.1.3/scout/secrets/__init__.py +0 -0
- boxprobe_scout-0.1.3/scout/server/__init__.py +0 -0
- boxprobe_scout-0.1.3/tests/__init__.py +0 -0
- boxprobe_scout-0.1.3/tests/collector/__init__.py +0 -0
- boxprobe_scout-0.1.3/tests/collector/test_control.py +89 -0
- boxprobe_scout-0.1.3/tests/collector/test_db.py +64 -0
- boxprobe_scout-0.1.3/tests/matcher/__init__.py +0 -0
- boxprobe_scout-0.1.3/tests/matcher/test_align.py +230 -0
- boxprobe_scout-0.1.3/tests/matcher/test_compare.py +94 -0
- boxprobe_scout-0.1.3/tests/matcher/test_diff_db.py +103 -0
- boxprobe_scout-0.1.3/tests/matcher/test_diff_report.py +86 -0
- boxprobe_scout-0.1.3/tests/matcher/test_noise.py +650 -0
- boxprobe_scout-0.1.3/tests/matcher/test_normalize.py +218 -0
- boxprobe_scout-0.1.3/tests/report/__init__.py +0 -0
- boxprobe_scout-0.1.3/tests/report/test_html.py +29 -0
- boxprobe_scout-0.1.3/tests/report/test_junit.py +37 -0
- boxprobe_scout-0.1.3/tests/runner/__init__.py +0 -0
- boxprobe_scout-0.1.3/tests/runner/test_batch.py +132 -0
- boxprobe_scout-0.1.3/tests/runner/test_executor.py +70 -0
- boxprobe_scout-0.1.3/tests/runner/test_executor_hooks.py +46 -0
- boxprobe_scout-0.1.3/tests/runner/test_locator.py +130 -0
- boxprobe_scout-0.1.3/tests/runner/test_page.py +89 -0
- boxprobe_scout-0.1.3/tests/runner/test_scenario.py +43 -0
- boxprobe_scout-0.1.3/tests/test_cli.py +70 -0
- boxprobe_scout-0.1.3/tests/test_config.py +53 -0
- boxprobe_scout-0.1.3/tests/test_git.py +71 -0
- boxprobe_scout-0.1.3/tests/test_index.py +134 -0
- boxprobe_scout-0.1.3/tests/test_run_metadata.py +128 -0
- boxprobe_scout-0.1.3/uv.lock +3738 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
name: Publish Scout
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
paths:
|
|
7
|
+
- "pyproject.toml"
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
publish:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Build and publish to Forgejo Package Registry
|
|
14
|
+
run: |
|
|
15
|
+
FORGEJO_TOKEN=$(grep -E '^FORGEJO_TOKEN=' /takumi/qa/.env.qa | cut -d= -f2-)
|
|
16
|
+
cd /takumi/qa/scout || { git clone "http://oauth2:${FORGEJO_TOKEN}@forgejo:3000/core/scout.git" /takumi/qa/scout && cd /takumi/qa/scout; }
|
|
17
|
+
git fetch "http://oauth2:${FORGEJO_TOKEN}@forgejo:3000/core/scout.git" main
|
|
18
|
+
git reset --hard FETCH_HEAD
|
|
19
|
+
|
|
20
|
+
# Build
|
|
21
|
+
UV_URL="https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-unknown-linux-musl.tar.gz"
|
|
22
|
+
wget -qO- "$UV_URL" | tar xz -C /usr/local/bin --strip-components=1
|
|
23
|
+
rm -rf dist/
|
|
24
|
+
uv build
|
|
25
|
+
|
|
26
|
+
# Publish
|
|
27
|
+
uv publish \
|
|
28
|
+
--publish-url http://forgejo:3000/api/packages/core/pypi \
|
|
29
|
+
--username __token__ \
|
|
30
|
+
--password "${FORGEJO_TOKEN}" \
|
|
31
|
+
dist/*
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [main]
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
env:
|
|
11
|
+
UV_LINK_MODE: copy
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout and run tests
|
|
14
|
+
run: |
|
|
15
|
+
UV_URL="https://github.com/astral-sh/uv/releases/latest/download/uv-x86_64-unknown-linux-musl.tar.gz"
|
|
16
|
+
wget -qO- "$UV_URL" | tar xz -C /usr/local/bin --strip-components=1
|
|
17
|
+
FORGEJO_TOKEN=$(grep -E '^FORGEJO_TOKEN=' /takumi/qa/.env.qa | cut -d= -f2-)
|
|
18
|
+
cd /takumi/qa/scout || { git clone "http://oauth2:${FORGEJO_TOKEN}@forgejo:3000/core/scout.git" /takumi/qa/scout && cd /takumi/qa/scout; }
|
|
19
|
+
git fetch "http://oauth2:${FORGEJO_TOKEN}@forgejo:3000/core/scout.git" main
|
|
20
|
+
git reset --hard FETCH_HEAD
|
|
21
|
+
uv sync --extra dev
|
|
22
|
+
uv run pytest tests/ -x --tb=short -m "not e2e"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
name: Bug report
|
|
2
|
+
description: Report something that doesn't work as expected
|
|
3
|
+
labels: ["bug", "triage"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
Thanks for filing a bug. Please complete every field — incomplete
|
|
9
|
+
reports usually take a round-trip to triage.
|
|
10
|
+
|
|
11
|
+
- type: input
|
|
12
|
+
id: scout-version
|
|
13
|
+
attributes:
|
|
14
|
+
label: scout version
|
|
15
|
+
description: Output of `scout --version` or `pip show boxprobe-scout`
|
|
16
|
+
placeholder: "0.1.3"
|
|
17
|
+
validations:
|
|
18
|
+
required: true
|
|
19
|
+
|
|
20
|
+
- type: input
|
|
21
|
+
id: python-version
|
|
22
|
+
attributes:
|
|
23
|
+
label: Python version
|
|
24
|
+
description: Output of `python --version`
|
|
25
|
+
placeholder: "3.12.7"
|
|
26
|
+
validations:
|
|
27
|
+
required: true
|
|
28
|
+
|
|
29
|
+
- type: dropdown
|
|
30
|
+
id: os
|
|
31
|
+
attributes:
|
|
32
|
+
label: Operating system
|
|
33
|
+
options:
|
|
34
|
+
- Linux
|
|
35
|
+
- macOS
|
|
36
|
+
- Windows
|
|
37
|
+
- Other (describe in reproduction)
|
|
38
|
+
validations:
|
|
39
|
+
required: true
|
|
40
|
+
|
|
41
|
+
- type: textarea
|
|
42
|
+
id: reproduction
|
|
43
|
+
attributes:
|
|
44
|
+
label: Reproduction
|
|
45
|
+
description: |
|
|
46
|
+
Minimal scenario file or CLI invocation that triggers the issue.
|
|
47
|
+
Trim to the smallest case that still fails.
|
|
48
|
+
render: shell
|
|
49
|
+
validations:
|
|
50
|
+
required: true
|
|
51
|
+
|
|
52
|
+
- type: textarea
|
|
53
|
+
id: expected
|
|
54
|
+
attributes:
|
|
55
|
+
label: What you expected
|
|
56
|
+
validations:
|
|
57
|
+
required: true
|
|
58
|
+
|
|
59
|
+
- type: textarea
|
|
60
|
+
id: actual
|
|
61
|
+
attributes:
|
|
62
|
+
label: What actually happened
|
|
63
|
+
description: Include the full traceback if there is one.
|
|
64
|
+
render: shell
|
|
65
|
+
validations:
|
|
66
|
+
required: true
|
|
67
|
+
|
|
68
|
+
- type: textarea
|
|
69
|
+
id: extra
|
|
70
|
+
attributes:
|
|
71
|
+
label: Anything else
|
|
72
|
+
description: |
|
|
73
|
+
Logs, screenshots, `result.json` snippets, diff report HTML — whatever
|
|
74
|
+
helps narrow it down.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
blank_issues_enabled: false
|
|
2
|
+
contact_links:
|
|
3
|
+
- name: Question / general discussion
|
|
4
|
+
url: https://github.com/boxprobe/scout/discussions
|
|
5
|
+
about: For questions about usage, design decisions, or open-ended discussion — please use Discussions instead of Issues.
|
|
6
|
+
- name: Security issues
|
|
7
|
+
url: mailto:contact@boxprobe.com
|
|
8
|
+
about: Please report security issues privately by email, not via public Issues.
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Feature request
|
|
2
|
+
description: Suggest a new capability or behavior change
|
|
3
|
+
labels: ["enhancement", "triage"]
|
|
4
|
+
body:
|
|
5
|
+
- type: markdown
|
|
6
|
+
attributes:
|
|
7
|
+
value: |
|
|
8
|
+
scout's scope is deliberately narrow: UI-driven API regression
|
|
9
|
+
testing. Features that broaden the scope need a strong case. Please
|
|
10
|
+
explain the use case before proposing a solution.
|
|
11
|
+
|
|
12
|
+
- type: textarea
|
|
13
|
+
id: problem
|
|
14
|
+
attributes:
|
|
15
|
+
label: Problem
|
|
16
|
+
description: What can't you do today, or what's painful?
|
|
17
|
+
validations:
|
|
18
|
+
required: true
|
|
19
|
+
|
|
20
|
+
- type: textarea
|
|
21
|
+
id: proposal
|
|
22
|
+
attributes:
|
|
23
|
+
label: Proposed solution
|
|
24
|
+
description: |
|
|
25
|
+
How would you want this to work? Concrete API sketches or CLI
|
|
26
|
+
examples are more useful than abstract descriptions.
|
|
27
|
+
validations:
|
|
28
|
+
required: true
|
|
29
|
+
|
|
30
|
+
- type: textarea
|
|
31
|
+
id: alternatives
|
|
32
|
+
attributes:
|
|
33
|
+
label: Alternatives considered
|
|
34
|
+
description: Workarounds you've tried, or other tools that solve this.
|
|
35
|
+
|
|
36
|
+
- type: checkboxes
|
|
37
|
+
id: scope
|
|
38
|
+
attributes:
|
|
39
|
+
label: Scope check
|
|
40
|
+
options:
|
|
41
|
+
- label: This is about UI-driven API regression testing (not a generic test runner / load tester / contract testing feature)
|
|
42
|
+
required: true
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Thanks for the PR! A few quick checks before submitting:
|
|
3
|
+
-->
|
|
4
|
+
|
|
5
|
+
## What this changes
|
|
6
|
+
|
|
7
|
+
<!-- One-paragraph summary of the change and its motivation. -->
|
|
8
|
+
|
|
9
|
+
## Type
|
|
10
|
+
|
|
11
|
+
- [ ] Bug fix
|
|
12
|
+
- [ ] New feature
|
|
13
|
+
- [ ] Refactor (no behavior change)
|
|
14
|
+
- [ ] Docs / formatting
|
|
15
|
+
- [ ] Test only
|
|
16
|
+
|
|
17
|
+
## Checklist
|
|
18
|
+
|
|
19
|
+
- [ ] Linked issue (or this is a trivial doc/typo PR)
|
|
20
|
+
- [ ] Tests added or updated for behavior changes
|
|
21
|
+
- [ ] `uv run pytest tests/ -x --tb=short -m "not e2e"` passes locally
|
|
22
|
+
- [ ] `uv run ruff check scout/ tests/` passes
|
|
23
|
+
- [ ] `uv run pyright scout/` passes
|
|
24
|
+
- [ ] Externally-observable surfaces flagged (scenario file format, diff report HTML, CLI flags)
|
|
25
|
+
|
|
26
|
+
## Notes for reviewers
|
|
27
|
+
|
|
28
|
+
<!-- Anything reviewers should know: tricky parts, why a specific approach
|
|
29
|
+
was chosen, follow-ups deliberately deferred. -->
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
concurrency:
|
|
9
|
+
group: ci-${{ github.ref }}
|
|
10
|
+
cancel-in-progress: true
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
lint:
|
|
14
|
+
name: Lint and type-check
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Install uv
|
|
20
|
+
uses: astral-sh/setup-uv@v6
|
|
21
|
+
with:
|
|
22
|
+
enable-cache: true
|
|
23
|
+
|
|
24
|
+
- name: Set up Python
|
|
25
|
+
run: uv python install 3.14
|
|
26
|
+
|
|
27
|
+
- name: Sync dependencies
|
|
28
|
+
run: uv sync --extra dev
|
|
29
|
+
|
|
30
|
+
- name: ruff check
|
|
31
|
+
run: uv run ruff check scout/ tests/
|
|
32
|
+
|
|
33
|
+
- name: ruff format check
|
|
34
|
+
run: uv run ruff format --check scout/ tests/
|
|
35
|
+
|
|
36
|
+
- name: pyright
|
|
37
|
+
run: uv run pyright scout/
|
|
38
|
+
|
|
39
|
+
test:
|
|
40
|
+
name: Test on Python ${{ matrix.python }}
|
|
41
|
+
runs-on: ubuntu-latest
|
|
42
|
+
strategy:
|
|
43
|
+
fail-fast: false
|
|
44
|
+
matrix:
|
|
45
|
+
python: ["3.11", "3.12", "3.13", "3.14"]
|
|
46
|
+
steps:
|
|
47
|
+
- uses: actions/checkout@v4
|
|
48
|
+
|
|
49
|
+
- name: Install uv
|
|
50
|
+
uses: astral-sh/setup-uv@v6
|
|
51
|
+
with:
|
|
52
|
+
enable-cache: true
|
|
53
|
+
|
|
54
|
+
- name: Set up Python ${{ matrix.python }}
|
|
55
|
+
run: uv python install ${{ matrix.python }}
|
|
56
|
+
|
|
57
|
+
- name: Sync dependencies
|
|
58
|
+
run: uv sync --extra dev --python ${{ matrix.python }}
|
|
59
|
+
|
|
60
|
+
- name: Run tests (no e2e)
|
|
61
|
+
run: uv run --python ${{ matrix.python }} pytest tests/ -x --tb=short -m "not e2e"
|
|
62
|
+
|
|
63
|
+
build:
|
|
64
|
+
name: Build wheel and sdist
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
needs: [lint, test]
|
|
67
|
+
steps:
|
|
68
|
+
- uses: actions/checkout@v4
|
|
69
|
+
|
|
70
|
+
- name: Install uv
|
|
71
|
+
uses: astral-sh/setup-uv@v6
|
|
72
|
+
|
|
73
|
+
- name: Build
|
|
74
|
+
run: uv build
|
|
75
|
+
|
|
76
|
+
- name: Inspect wheel metadata
|
|
77
|
+
run: |
|
|
78
|
+
unzip -p dist/boxprobe_scout-*.whl '*/METADATA' | head -30
|
|
79
|
+
|
|
80
|
+
- name: Upload artifacts
|
|
81
|
+
uses: actions/upload-artifact@v4
|
|
82
|
+
with:
|
|
83
|
+
name: dist
|
|
84
|
+
path: dist/
|
|
85
|
+
retention-days: 14
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*.*.*"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
name: Build sdist and wheel
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- uses: actions/checkout@v4
|
|
14
|
+
|
|
15
|
+
- name: Install uv
|
|
16
|
+
uses: astral-sh/setup-uv@v6
|
|
17
|
+
with:
|
|
18
|
+
enable-cache: true
|
|
19
|
+
|
|
20
|
+
- name: Set up Python
|
|
21
|
+
run: uv python install 3.14
|
|
22
|
+
|
|
23
|
+
- name: Build distributions
|
|
24
|
+
run: uv build
|
|
25
|
+
|
|
26
|
+
- name: Verify wheel metadata version matches tag
|
|
27
|
+
run: |
|
|
28
|
+
tag="${GITHUB_REF_NAME}"
|
|
29
|
+
expected="${tag#v}"
|
|
30
|
+
actual=$(unzip -p dist/boxprobe_scout-*.whl '*/METADATA' | awk '/^Version:/ {print $2; exit}')
|
|
31
|
+
if [ "$actual" != "$expected" ]; then
|
|
32
|
+
echo "Tag $tag implies version $expected, but wheel built version $actual"
|
|
33
|
+
exit 1
|
|
34
|
+
fi
|
|
35
|
+
echo "Tag and wheel agree on version $actual"
|
|
36
|
+
|
|
37
|
+
- name: Upload distributions
|
|
38
|
+
uses: actions/upload-artifact@v4
|
|
39
|
+
with:
|
|
40
|
+
name: dist
|
|
41
|
+
path: dist/
|
|
42
|
+
|
|
43
|
+
publish-pypi:
|
|
44
|
+
name: Publish to PyPI
|
|
45
|
+
needs: [build]
|
|
46
|
+
runs-on: ubuntu-latest
|
|
47
|
+
environment:
|
|
48
|
+
name: pypi
|
|
49
|
+
url: https://pypi.org/p/boxprobe-scout
|
|
50
|
+
permissions:
|
|
51
|
+
id-token: write
|
|
52
|
+
steps:
|
|
53
|
+
- name: Download distributions
|
|
54
|
+
uses: actions/download-artifact@v4
|
|
55
|
+
with:
|
|
56
|
+
name: dist
|
|
57
|
+
path: dist/
|
|
58
|
+
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
61
|
+
|
|
62
|
+
github-release:
|
|
63
|
+
name: Create GitHub Release
|
|
64
|
+
needs: [publish-pypi]
|
|
65
|
+
runs-on: ubuntu-latest
|
|
66
|
+
permissions:
|
|
67
|
+
contents: write
|
|
68
|
+
steps:
|
|
69
|
+
- uses: actions/checkout@v4
|
|
70
|
+
with:
|
|
71
|
+
fetch-depth: 0
|
|
72
|
+
|
|
73
|
+
- name: Download distributions
|
|
74
|
+
uses: actions/download-artifact@v4
|
|
75
|
+
with:
|
|
76
|
+
name: dist
|
|
77
|
+
path: dist/
|
|
78
|
+
|
|
79
|
+
- name: Extract changelog section
|
|
80
|
+
id: changelog
|
|
81
|
+
run: |
|
|
82
|
+
version="${GITHUB_REF_NAME#v}"
|
|
83
|
+
# Pull lines between [version] header and the next ## header
|
|
84
|
+
awk -v v="$version" '
|
|
85
|
+
$0 ~ "^## \\["v"\\]" {flag=1; next}
|
|
86
|
+
flag && /^## \[/ {exit}
|
|
87
|
+
flag {print}
|
|
88
|
+
' CHANGELOG.md > /tmp/release-notes.md
|
|
89
|
+
if [ ! -s /tmp/release-notes.md ]; then
|
|
90
|
+
echo "No changelog entry for $version" > /tmp/release-notes.md
|
|
91
|
+
fi
|
|
92
|
+
echo "notes_path=/tmp/release-notes.md" >> "$GITHUB_OUTPUT"
|
|
93
|
+
|
|
94
|
+
- name: Create release
|
|
95
|
+
env:
|
|
96
|
+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
97
|
+
run: |
|
|
98
|
+
gh release create "$GITHUB_REF_NAME" \
|
|
99
|
+
--title "$GITHUB_REF_NAME" \
|
|
100
|
+
--notes-file "${{ steps.changelog.outputs.notes_path }}" \
|
|
101
|
+
dist/*
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Agent Constraints
|
|
2
|
+
|
|
3
|
+
1. **Run tests before committing**: `uv run pytest tests/ -x --tb=short`
|
|
4
|
+
2. **Don't touch unrelated code** — scope changes to the task at hand
|
|
5
|
+
3. **Conventional commits** — `feat:`, `fix:`, `test:`, `refactor:`, `docs:`
|
|
6
|
+
4. **Don't add dependencies** without explicit approval
|
|
7
|
+
5. **All Python commands via `uv run`** — never bare `python` or `pip`
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to scout are documented here.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html)
|
|
7
|
+
once it reaches `1.0.0`. While in `0.x`, breaking changes may occur between
|
|
8
|
+
minor versions; patch versions remain backward-compatible bug fixes.
|
|
9
|
+
|
|
10
|
+
## [Unreleased]
|
|
11
|
+
|
|
12
|
+
## [0.1.3] - 2026-05-16
|
|
13
|
+
|
|
14
|
+
Initial public release on PyPI. Prior `0.1.x` versions were distributed
|
|
15
|
+
internally via a private package registry; this is the first cut intended
|
|
16
|
+
for external consumers.
|
|
17
|
+
|
|
18
|
+
### Features at this version
|
|
19
|
+
|
|
20
|
+
- `scout run` — execute pre-recorded Python scenarios via Playwright,
|
|
21
|
+
capture API traffic through a recording proxy
|
|
22
|
+
- `scout verify` — debug-mode scenario execution with screenshots, no proxy
|
|
23
|
+
- `scout diff` — compare two runs' API recordings, produce HTML diff report
|
|
24
|
+
with structural, value, and known-change classification
|
|
25
|
+
- `scout runs` — list local run history
|
|
26
|
+
- Pixel-anchored `Locator` API with `abs` / `rel` / `dxy` positioning
|
|
27
|
+
- `diff_ignore.json` rule format for field, value-type, endpoint, and
|
|
28
|
+
status-only noise suppression
|
|
29
|
+
- HTML diff report with filterable endpoint table, popup body diffs, and
|
|
30
|
+
known-change badges
|
|
31
|
+
- JUnit XML report alongside HTML for CI integration
|
|
32
|
+
|
|
33
|
+
[Unreleased]: https://github.com/boxprobe/scout/compare/v0.1.3...HEAD
|
|
34
|
+
[0.1.3]: https://github.com/boxprobe/scout/releases/tag/v0.1.3
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# scout
|
|
2
|
+
|
|
3
|
+
UI-driven API regression testing — a CLI that executes pre-recorded
|
|
4
|
+
scenarios, captures API traffic via a proxy, and produces cross-version
|
|
5
|
+
diff reports.
|
|
6
|
+
|
|
7
|
+
**Install:**
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install boxprobe-scout
|
|
11
|
+
playwright install chromium
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Code structure
|
|
15
|
+
|
|
16
|
+
```
|
|
17
|
+
scout/
|
|
18
|
+
├── cli.py # Click CLI entry
|
|
19
|
+
│ # scout run — record API traffic during execution
|
|
20
|
+
│ # scout verify — screenshot mode for scenario debugging
|
|
21
|
+
│ # scout diff — compare two runs' API recordings
|
|
22
|
+
│ # scout runs — list run history
|
|
23
|
+
├── config.py # app.json loader + URL overrides
|
|
24
|
+
├── git.py # git commit/branch lookup for run metadata
|
|
25
|
+
├── index.py # local run index (SQLite, .scout/index.db)
|
|
26
|
+
├── run_metadata.py # run metadata assembly
|
|
27
|
+
├── runner/
|
|
28
|
+
│ ├── executor.py # Playwright orchestration, batch execution
|
|
29
|
+
│ ├── scenario.py # Scenario DSL (base_url, @setup, @test)
|
|
30
|
+
│ ├── page.py # Page wrapper (goto/click/fill/wait via Locator)
|
|
31
|
+
│ └── locator.py # Pixel-anchored Locator with abs/rel/dxy positioning
|
|
32
|
+
├── collector/
|
|
33
|
+
│ ├── subprocess.py # recording proxy subprocess manager
|
|
34
|
+
│ ├── proxy.py # mitmproxy addon
|
|
35
|
+
│ ├── control.py # proxy control API (session start/stop)
|
|
36
|
+
│ └── db.py # recording DB (SQLite: scenarios + api_records)
|
|
37
|
+
├── matcher/
|
|
38
|
+
│ ├── align.py # endpoint pairing (path-only + 2-stage query match)
|
|
39
|
+
│ ├── compare.py # JSON structure + value comparison
|
|
40
|
+
│ ├── normalize.py # URL path normalization (dynamic ID inference)
|
|
41
|
+
│ ├── noise.py # diff_ignore.json rules + known-change suppression
|
|
42
|
+
│ ├── diff_db.py # diff result DB
|
|
43
|
+
│ └── diff_report.py # HTML diff report generation
|
|
44
|
+
├── report/
|
|
45
|
+
│ ├── html.py # per-run HTML report
|
|
46
|
+
│ └── junit.py # JUnit XML report
|
|
47
|
+
├── secrets/ # credential injection (pyrage encryption — planned)
|
|
48
|
+
├── bridge/ # browser bridge over CDP — planned
|
|
49
|
+
├── mcp/ # MCP server for AI agent integration — planned
|
|
50
|
+
└── server/ # planned
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## CLI commands
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Recording run: launches proxy, records API traffic to
|
|
57
|
+
# .scout/runs/<run_id>/record.db
|
|
58
|
+
scout run scenarios/auth/login-success --web-version 1.0.0
|
|
59
|
+
scout run scenarios/ --web-version 1.0.0 # recurse: find any test.py
|
|
60
|
+
|
|
61
|
+
# Debug verify: screenshot mode, no proxy
|
|
62
|
+
scout verify scenarios/auth/login-success --headed
|
|
63
|
+
|
|
64
|
+
# Diff: compare two recordings
|
|
65
|
+
scout runs # list run IDs
|
|
66
|
+
scout diff <baseline-id> <target-id>
|
|
67
|
+
scout diff <baseline-id> <target-id> --no-detail # skip body popup data
|
|
68
|
+
|
|
69
|
+
# URL override (for staging/local environments)
|
|
70
|
+
scout run scenarios/ --web-base-url http://localhost:9000 \
|
|
71
|
+
--api-base-url http://localhost:9000 \
|
|
72
|
+
--web-version dev
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`--web-version` is required on `scout run` — it tags the recording so
|
|
76
|
+
diff reports can label each side and so "added in <ver>" known-change
|
|
77
|
+
buttons in the report work correctly.
|
|
78
|
+
|
|
79
|
+
## Data flow
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
scout run
|
|
83
|
+
├── Launch recording proxy (separate process)
|
|
84
|
+
├── Playwright browser → proxy → target app
|
|
85
|
+
├── Notify proxy of session boundaries per scenario
|
|
86
|
+
├── API traffic → .scout/runs/<run_id>/record.db
|
|
87
|
+
├── Per-scenario → .scout/runs/<run_id>/<scenario>/result.json
|
|
88
|
+
├── HTML+JUnit → .scout/runs/<run_id>/report.html, junit.xml
|
|
89
|
+
└── Run index → .scout/index.db
|
|
90
|
+
|
|
91
|
+
scout diff baseline target
|
|
92
|
+
├── Read both runs' record.db
|
|
93
|
+
├── Pair endpoints by path; resolve query differences (2-stage)
|
|
94
|
+
├── Compare status code + JSON structure + values
|
|
95
|
+
├── Apply diff_ignore.json rules (field/value-type/endpoint ignores)
|
|
96
|
+
├── Diff DB → .scout/diffs/<baseline>_vs_<target>/diff.db
|
|
97
|
+
└── HTML report → .scout/diffs/<baseline>_vs_<target>/report.html
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Scenario file format
|
|
101
|
+
|
|
102
|
+
A scenario lives in a directory next to an `app.json`:
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
my-app/
|
|
106
|
+
├── app.json
|
|
107
|
+
├── diff_ignore.json # optional: noise rules
|
|
108
|
+
└── scenarios/
|
|
109
|
+
└── login/
|
|
110
|
+
└── test.py
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
// app.json
|
|
115
|
+
{
|
|
116
|
+
"name": "my-app",
|
|
117
|
+
"web_base_url": "https://app.example.com",
|
|
118
|
+
"api_base_url": "https://api.example.com",
|
|
119
|
+
"viewport_width": 1280,
|
|
120
|
+
"viewport_height": 800
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
# scenarios/login/test.py
|
|
126
|
+
from scout.runner import Locator, Page, Scenario
|
|
127
|
+
|
|
128
|
+
scenario = Scenario(
|
|
129
|
+
name="login",
|
|
130
|
+
base_url="https://app.example.com",
|
|
131
|
+
viewport_width=1280,
|
|
132
|
+
viewport_height=800,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
email = Locator(name="email", tag="input", bbox=(640, 320, 280, 32))
|
|
136
|
+
password = Locator(name="password", tag="input", bbox=(640, 372, 280, 32))
|
|
137
|
+
submit = Locator(name="submit", tag="button", bbox=(640, 428, 280, 40))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@scenario.test
|
|
141
|
+
async def test(page: Page) -> None:
|
|
142
|
+
await page.goto("/login")
|
|
143
|
+
await page.fill(email, "user@example.com")
|
|
144
|
+
await page.fill(password, "password123")
|
|
145
|
+
await page.click(submit)
|
|
146
|
+
await page.wait(2000)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
scenario.run()
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Tech stack
|
|
154
|
+
|
|
155
|
+
| Layer | Choice |
|
|
156
|
+
|--------------------|-------------------------------------------|
|
|
157
|
+
| CLI | Python + Click |
|
|
158
|
+
| Browser automation | Playwright (Python) |
|
|
159
|
+
| HTTP client | httpx |
|
|
160
|
+
| Recording proxy | mitmproxy (separate process) |
|
|
161
|
+
| Data store | SQLite (recordings, diffs, run index) |
|
|
162
|
+
| Reports | HTML + JUnit XML |
|
|
163
|
+
| Lint + format | ruff |
|
|
164
|
+
| Type checking | pyright |
|
|
165
|
+
| Tests | pytest + pytest-asyncio + pytest-playwright |
|
|
166
|
+
|
|
167
|
+
## Python environment
|
|
168
|
+
|
|
169
|
+
- Python >= 3.11
|
|
170
|
+
- Uses [uv](https://docs.astral.sh/uv/) for environment management
|
|
171
|
+
- **All Python commands must use `uv run`** — never bare `python` or `pip`.
|
|
172
|
+
`uv run` guarantees the venv interpreter matches `pyproject.toml`'s
|
|
173
|
+
`requires-python`; system Python can silently drift.
|
|
174
|
+
|
|
175
|
+
## Conventions
|
|
176
|
+
|
|
177
|
+
- Code, comments, commit messages, issues, PRs: English
|
|
178
|
+
- [Conventional Commits](https://www.conventionalcommits.org/) — `feat:`,
|
|
179
|
+
`fix:`, `test:`, `refactor:`, `docs:`, `chore:`
|
|
180
|
+
- Run `uv run pytest tests/ -x --tb=short -m "not e2e"` before pushing
|
|
181
|
+
- See [CONTRIBUTING.md](CONTRIBUTING.md) for full development setup
|