From cb99df4a2fc45bc983e0a64f862f79fab0ce899a Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 25 Oct 2026 05:39:53 -0700 Subject: [PATCH] fix(diff-scope): count Python test_*.py and nested test dirs as tests (#3335) (#3382) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 10 ++++++++++ VERSION | 2 +- agents-digest/gstack-AGENTS.md | 2 +- bin/gstack-diff-scope | 8 ++++++++ package.json | 2 +- test/diff-scope.test.ts | 28 +++++++++++++++++++++++++++- 6 files changed, 48 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10a868365..52714c8ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## [1.91.211.0] - 2026-10-25 + +**`gstack-diff-scope` sees Python tests and nested test directories (#3335).** A branch that changed `components/orch/services/shared/tests/test_native_store_custody.py` reported `SCOPE_TESTS=false`, because the Tests rule only knew `*.test.*`-style suffixes and `test/`, `tests/`, `spec/` and `__tests__/` at the repository root. /ship and /review use that flag to route the testing specialist, so a backend change whose tests follow pytest's `test_*.py` convention skipped it. The test-bootstrap resolver already counted these files, so the two detectors disagreed. + +### Fixed +- **`bin/gstack-diff-scope`**: a changed path is a test when it matches `TEST_PATH_RE`, the same regex the test-bootstrap resolver uses for its TESTFILES count (`scripts/resolvers/testing.ts`): test dirs at any depth, `test_*.py`, `tests.py`, `_test.`, `.test.`/`.spec.` JS and TS, `_spec.rb` and `*Test.java`/`.kt`. `conftest.py` counts too. The existing globs stay. + +### Tests +- `test/diff-scope.test.ts` adds the issue's three paths, `pkg/test_store.py`, Django's `tests.py`, `conftest.py`, and nested `__tests__/` and `spec/` dirs; all eight fail on main. Controls that stay `SCOPE_TESTS=false`: `latest.py`, `contest_rules.py`, `attest_signer.py` and a `testing/` dir. A new test checks that `TEST_PATH_RE` is byte-identical to the resolver's regex, so the two can't drift again. + ## [1.91.210.0] - 2026-10-25 **/autoplan works when Claude Code's project directory is a subfolder of the repository (#3296).** When the git root sits above the project (for example the root is `$HOME` and the session starts in `$HOME/projects/app`), a worktree session's journal lives in `.claude/worktrees/` under that subfolder. The publication guard only looked for `/.git/worktrees`, found nothing, and denied every phase Read with `foreign_cwd`, so the reporter had to restart the session from the repository root. diff --git a/VERSION b/VERSION index e41ac1723..6811b3c0a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.91.210.0 +1.91.211.0 diff --git a/agents-digest/gstack-AGENTS.md b/agents-digest/gstack-AGENTS.md index 6035c125d..ad700de53 100644 --- a/agents-digest/gstack-AGENTS.md +++ b/agents-digest/gstack-AGENTS.md @@ -1,4 +1,4 @@ -# gstack digest v1.91.210.0 — regenerate/re-copy after upgrading gstack +# gstack digest v1.91.211.0 — regenerate/re-copy after upgrading gstack Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed for agent hosts without a full skill install. The full skills add workflows, diff --git a/bin/gstack-diff-scope b/bin/gstack-diff-scope index b923f737a..2fa10b41d 100755 --- a/bin/gstack-diff-scope +++ b/bin/gstack-diff-scope @@ -42,6 +42,12 @@ MIGRATIONS=false API=false AUTH=false +# Test-file paths, the same rule the test-bootstrap resolver counts as +# TESTFILES (scripts/resolvers/testing.ts; test/diff-scope.test.ts keeps the two +# identical). Nested test dirs and Python's test_*.py / tests.py were missed by +# the root-anchored globs alone (#3335). +TEST_PATH_RE='(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs|dart)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' + _print_flags() { echo "SCOPE_FRONTEND=$FRONTEND" echo "SCOPE_BACKEND=$BACKEND" @@ -126,7 +132,9 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do case "$f" in *.test.*|*.spec.*|*_test.*|*_spec.*) m_tests=true ;; test/*|tests/*|spec/*|__tests__/*|cypress/*|e2e/*) m_tests=true ;; + conftest.py|*/conftest.py) m_tests=true ;; esac + if [[ "$f" =~ $TEST_PATH_RE ]]; then m_tests=true; fi # Docs case "$f" in diff --git a/package.json b/package.json index 77850610a..f806401c5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "gstack", - "version": "1.91.210", + "version": "1.91.211", "description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.", "license": "MIT", "type": "module", diff --git a/test/diff-scope.test.ts b/test/diff-scope.test.ts index b5bc4fd10..9597362a9 100644 --- a/test/diff-scope.test.ts +++ b/test/diff-scope.test.ts @@ -5,7 +5,7 @@ * the correct SCOPE_* variables are output. */ import { describe, test, expect, afterAll } from 'bun:test'; -import { mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'fs'; +import { mkdtempSync, writeFileSync, mkdirSync, rmSync, readFileSync } from 'fs'; import { join } from 'path'; import { tmpdir } from 'os'; import { spawnSync } from 'child_process'; @@ -245,6 +245,20 @@ describe('glob classes (table-driven, #2526 + #2455)', () => { { name: 'plain backend', file: 'server.go', expects: ['SCOPE_BACKEND'], alsoFalse: ['SCOPE_FRONTEND'] }, { name: 'docs', file: 'docs/guide.md', expects: ['SCOPE_DOCS'] }, { name: 'prompts', file: 'app/services/prompt_builder.rb', expects: ['SCOPE_PROMPTS', 'SCOPE_BACKEND'] }, + // Python tests and nested test dirs (#3335): the issue's three paths. + { name: 'nested tests/test_*.py (#3335)', file: 'components/orch/services/shared/tests/test_native_store_custody.py', expects: ['SCOPE_TESTS', 'SCOPE_BACKEND'] }, + { name: 'second nested tests/test_*.py (#3335)', file: 'components/orch/services/shared/tests/test_infra_promoted_first_apply.py', expects: ['SCOPE_TESTS', 'SCOPE_BACKEND'] }, + { name: 'hyphenated service tests/test_*.py (#3335)', file: 'components/orch/services/api-gateway/tests/test_native_chat_checkpoint_store.py', expects: ['SCOPE_TESTS', 'SCOPE_BACKEND'] }, + { name: 'test_*.py outside a tests dir', file: 'pkg/test_store.py', expects: ['SCOPE_TESTS', 'SCOPE_BACKEND'] }, + { name: 'Django tests.py', file: 'shop/tests.py', expects: ['SCOPE_TESTS', 'SCOPE_BACKEND'] }, + { name: 'pytest conftest.py', file: 'services/api/conftest.py', expects: ['SCOPE_TESTS', 'SCOPE_BACKEND'] }, + { name: 'nested __tests__ dir', file: 'packages/ui/__tests__/render.ts', expects: ['SCOPE_TESTS', 'SCOPE_BACKEND'] }, + { name: 'nested spec dir', file: 'engines/billing/spec/invoice_checker.rb', expects: ['SCOPE_TESTS', 'SCOPE_BACKEND'] }, + // Controls: a name that only contains "test" is not a test file. + { name: 'latest.py is not a test', file: 'pkg/latest.py', expects: ['SCOPE_BACKEND'], alsoFalse: ['SCOPE_TESTS'] }, + { name: 'contest_rules.py is not a test', file: 'pkg/contest_rules.py', expects: ['SCOPE_BACKEND'], alsoFalse: ['SCOPE_TESTS'] }, + { name: 'testing/ dir is not a test dir', file: 'pkg/testing/fixtures_loader.py', expects: ['SCOPE_BACKEND'], alsoFalse: ['SCOPE_TESTS'] }, + { name: 'attest_*.py is not a test', file: 'pkg/attest_signer.py', expects: ['SCOPE_BACKEND'], alsoFalse: ['SCOPE_TESTS'] }, ]; for (const row of TABLE) { @@ -262,6 +276,18 @@ describe('glob classes (table-driven, #2526 + #2455)', () => { } }); +describe('one test-path rule (#3335)', () => { + test('TEST_PATH_RE is byte-identical to the test-bootstrap TESTFILES regex', () => { + const script = readFileSync(SCRIPT, 'utf8'); + const scriptRe = script.match(/^TEST_PATH_RE='([^']+)'$/m)?.[1]; + const resolver = readFileSync(join(import.meta.dir, '..', 'scripts', 'resolvers', 'testing.ts'), 'utf8'); + const resolverRe = resolver.match(/git ls-files \| grep -cE '([^']+)'/)?.[1]?.replace(/\\\\/g, '\\'); + expect(scriptRe).toBeDefined(); + expect(resolverRe).toBeDefined(); + expect(scriptRe).toBe(resolverRe); + }); +}); + describe('exit-code contract (#2526)', () => { test('clean tree, no changes → all false, exit 0, no SCOPE_ERROR', () => { const dir = createRepo([]);