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>
This commit is contained in:
Garry Tanandcapy-ai[bot] authored and GitHub committed 2026-10-25 12:39:53 +00:00
1 parent c279ce045d
commit cb99df4a2f
6 files changed
+48 -4

No files matched your search

+10
View File
@@ -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.<ext>`, `.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/<name>` under that subfolder. The publication guard only looked for `<project dir>/.git/worktrees`, found nothing, and denied every phase Read with `foreign_cwd`, so the reporter had to restart the session from the repository root.
+1 -1
View File
@@ -1 +1 @@
1.91.210.0
1.91.211.0
+1 -1
View File
@@ -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,
+8
View File
@@ -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
+1 -1
View File
@@ -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",
+27 -1
View File
@@ -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([]);