mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-04 15:57:33 +01:00
fix(scope,review,ship): recognise Dart and Flutter in diff scope and test detection (#3284)
Port of arlophoenix's #3284 onto current main, squashed: - gstack-diff-scope: .dart is frontend and backend; pubspec.lock is config. - lib/frontend-scope.ts mirrors the frontend half; gstack-design-detect never sends .dart to the impeccable engine. - Test bootstrap, coverage audits and review-army detect Dart/Flutter (RUNTIME:dart, FRAMEWORK:flutter, dart_test.yaml, *_test.dart). - Adversarial DIFF_SIZE sums git diff --numstat. On top of the contributor's diff: shorten the bootstrap's ecosystem-marker comment so /design-review stays at its SKILL.md size pin, lower the 31 pins that shrank, and add the CHANGELOG entry. Co-authored-by: arlophoenix <arlo.phoenix@gmail.com> Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
9cf105ccff
commit
f4fb0ebabe
22 files changed
+254
-94
No files matched your search
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
**Dart and Flutter projects get the right reviewers and the right test command.** A Dart change matched no diff-scope category except its test file, so a Flutter widget PR ran only the testing pass and skipped design, performance and security. Contributed by @arlophoenix (#3284).
|
||||
|
||||
### Fixed
|
||||
- **`gstack-diff-scope` counts `.dart` as both frontend and backend.** The extension cannot tell a Flutter widget from a repository class or a pure-Dart server, and a skipped reviewer is invisible, so both specialists run. `pubspec.lock` (at the root or in a workspace package) is config. `lib/frontend-scope.ts` mirrors the frontend half, and `gstack-design-detect` still never hands a `.dart` file to the impeccable engine, which reads web sources only.
|
||||
- **Test detection knows Dart.** `/ship`'s bootstrap and coverage audit, `/design-review` and `/plan-eng-review` print `RUNTIME:dart` for a `pubspec.yaml` and `FRAMEWORK:flutter` for an indented `sdk: flutter` dependency (a commented-out one or an `environment.sdk` constraint does not count). They treat `dart_test.yaml` and `*_test.dart` files anywhere as existing tests and offer `flutter test` or `dart test`. Review-army reports `STACK: dart` and `TEST_FW: dart-test` or `flutter-test`.
|
||||
- **Adversarial `DIFF_SIZE` sums `git diff --numstat`,** the same count review-army's `DIFF_LINES` uses, instead of parsing the `--stat` summary line. Binary files count as zero.
|
||||
|
||||
### Changed
|
||||
- The test-bootstrap prose is shorter so the generated skills stay inside the parity size cap and `/design-review` stays at its SKILL.md size pin. The rules it states are unchanged. `/design-review`, `/plan-eng-review` and `/review` shrink, so their pins in `test/fixtures/skill-md-size-allowlist.json` go down (31 entries).
|
||||
- `test/frontend-scope.test.ts`'s bash parity test gets a 30 s budget. It spawns git six times and the scope script once per sample, and it already ran past Bun's 5 s default on a 4-core machine before this change.
|
||||
|
||||
## [1.91.149.0] - 2026-10-19
|
||||
|
||||
**Paid eval cases can now run on the Codex host, and the Monday census runs two arms: Claude Opus 5.5 and Codex gpt-6.1-sol.** `EVALS_HOST=codex` runs the 52 adapter-only gate cases through a full Codex runtime per case. On its first pilot gpt-6.1-sol passed 48 of 52 with no retries, at $4.63 for the 52 cases, against $28.93 for the same cases on Fable (0.16×). Claude-only cases are skipped on Codex with a recorded reason and never count as failed. GSTA-49 TM2.
|
||||
|
||||
@@ -829,7 +829,8 @@ function resolveTargets(args: ScanArgs, p: Probe): { targets: string[]; refusedB
|
||||
runZ(['diff', '-z', '--name-only', '--diff-filter=ACMR', 'HEAD']);
|
||||
runZ(['ls-files', '-z', '--others', '--exclude-standard']);
|
||||
for (const rel of [...files].sort()) {
|
||||
if (!isFrontendPath(rel)) continue;
|
||||
// impeccable reads web sources; a Dart file is frontend scope but not a scan target.
|
||||
if (!isFrontendPath(rel) || rel.endsWith('.dart')) continue;
|
||||
const abs = path.join(top, rel);
|
||||
try { if (fs.lstatSync(abs).isSymbolicLink()) { refuse(rel, 'symlink named by git is never scanned'); continue; } } catch { continue; }
|
||||
const real = realpathOrNull(abs);
|
||||
|
||||
@@ -104,6 +104,11 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
|
||||
*.tsx|*.jsx|*.vue|*.svelte|*.astro) m_frontend=true ;;
|
||||
*.erb|*.haml|*.slim|*.hbs|*.ejs) m_frontend=true ;;
|
||||
*.html) m_frontend=true ;;
|
||||
# Dart: the extension cannot tell a Flutter widget from a repository or a
|
||||
# pure-Dart server, so it is BOTH frontend and backend (BACKEND set below).
|
||||
# A skipped reviewer is invisible; an extra one (design-lite included) is a
|
||||
# visible pass that costs a review.
|
||||
*.dart) m_frontend=true ;;
|
||||
tailwind.config.*|postcss.config.*) m_frontend=true ;;
|
||||
app/views/*|*/components/*|styles/*|css/*|app/assets/stylesheets/*) m_frontend=true ;;
|
||||
esac
|
||||
@@ -135,6 +140,7 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
|
||||
*.yml|*.yaml) m_config=true ;;
|
||||
.github/*) m_config=true ;;
|
||||
requirements.txt|pyproject.toml|go.mod|Cargo.toml|composer.json) m_config=true ;;
|
||||
pubspec.lock|*/pubspec.lock) m_config=true ;;
|
||||
esac
|
||||
|
||||
# Migrations: database migration files. Bare migrations/* covers a
|
||||
@@ -169,6 +175,9 @@ for f in ${FILES_LIST[@]+"${FILES_LIST[@]}"}; do
|
||||
*.ts|*.js|*.mjs|*.cjs|*.mts|*.cts) m_backend=true ;;
|
||||
esac
|
||||
fi
|
||||
case "$f" in
|
||||
*.dart) m_backend=true ;;
|
||||
esac
|
||||
|
||||
[ "$m_frontend" = true ] && FRONTEND=true
|
||||
[ "$m_prompts" = true ] && PROMPTS=true
|
||||
|
||||
+12
-9
@@ -535,17 +535,18 @@ Label `$B` output with the same evidence lines (`URL=`, `CONSOLE_ERRORS=`, `DIFF
|
||||
|
||||
**Read the project's CLAUDE.md (and TESTING.md if present) FIRST.** If it documents a test command, the project already told you: no detection, no bootstrap. Skip the rest of bootstrap and use that command in Step 5.
|
||||
|
||||
**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker tells you which ecosystem you're in and which command to OFFER. It does not tell you the command works. Do not execute a candidate test command to "check" it: a probe on a project that never had that runner fails loudly and teaches you nothing, and installing a second framework over a working one is worse.
|
||||
**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker names the ecosystem and the command to OFFER; it does not prove the command works. Never execute a candidate to "check" it: probing a runner the project never had fails loudly and teaches nothing.
|
||||
|
||||
```bash
|
||||
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||
# Definitive ecosystem markers (presence = ecosystem, NOT a command to run)
|
||||
# Ecosystem markers
|
||||
[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django MARKER:manage.py"
|
||||
{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python"
|
||||
{ [ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ]; } && echo "RUNTIME:ruby"
|
||||
[ -f package.json ] && echo "RUNTIME:node"
|
||||
[ -f go.mod ] && echo "RUNTIME:go"
|
||||
[ -f Cargo.toml ] && echo "RUNTIME:rust"
|
||||
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
|
||||
[ -f composer.json ] && echo "RUNTIME:php"
|
||||
[ -f mix.exs ] && echo "RUNTIME:elixir"
|
||||
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
|
||||
@@ -553,14 +554,14 @@ setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||
# Detect sub-frameworks
|
||||
[ -f Gemfile ] && grep -q "rails" Gemfile 2>/dev/null && echo "FRAMEWORK:rails"
|
||||
[ -f package.json ] && grep -q '"next"' package.json 2>/dev/null && echo "FRAMEWORK:nextjs"
|
||||
# Existing test path — config files, declared scripts, AND test FILES.
|
||||
# A project with real tests and no config file is the common miss.
|
||||
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* 2>/dev/null
|
||||
grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && echo "FRAMEWORK:flutter"
|
||||
# Test evidence: config files, declared scripts, AND test FILES (tested projects often lack a config)
|
||||
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* dart_test.yaml 2>/dev/null
|
||||
[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test"
|
||||
[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test"
|
||||
[ -f pyproject.toml ] && grep -q "pytest" pyproject.toml && echo "CONFIG:pyproject pytest"
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
# Rust keeps unit tests inside src/, so file names alone miss them
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs|dart)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
# Rust unit tests live in src/; file names alone miss them
|
||||
[ -f Cargo.toml ] && git grep -lF '#[test]' -- 'src' >/dev/null 2>&1 && echo "TESTS:rust in-source"
|
||||
# Check opt-out marker
|
||||
[ -f .gstack/no-test-bootstrap ] && echo "BOOTSTRAP_DECLINED"
|
||||
@@ -574,6 +575,7 @@ Map the markers to the command you will OFFER — never to one you run on a gues
|
||||
| `pytest.ini` / `tox.ini` / pytest in `pyproject.toml` / `test_*.py` | Python | `pytest` |
|
||||
| `go.mod` (+ any `*_test.go`) | Go | `go test ./...` |
|
||||
| `Cargo.toml` | Rust | `cargo test` |
|
||||
| `pubspec.yaml` | Dart / Flutter | `flutter test` (FRAMEWORK:flutter) or `dart test`; `fvm` prefix if `.fvmrc` |
|
||||
| `pom.xml` | JVM (Maven) | `mvn test` |
|
||||
| `build.gradle` / `build.gradle.kts` | JVM (Gradle) | `./gradlew test` |
|
||||
| `Gemfile` / `Rakefile` / `.rspec` | Ruby | `bundle exec rspec`, `bin/rails test`, or `rake test` |
|
||||
@@ -582,11 +584,11 @@ Map the markers to the command you will OFFER — never to one you run on a gues
|
||||
| `package.json` with a `test` script | Node | that script, run with the package manager the lockfile names |
|
||||
| `Makefile` with a `test:` target | any | `make test` |
|
||||
|
||||
**If ANY existing-test evidence appears** (a config file, a declared test script or make target, a nonzero `TESTFILES:` count, or `TESTS:rust in-source`): the project has tests. **Do NOT bootstrap.** Print "Existing tests detected: {the evidence}." Then get the command the same way Step 5 does — CLAUDE.md/TESTING.md if documented, otherwise AskUserQuestion offering the candidates from the table above plus "Other", and persist the answer to CLAUDE.md's `## Testing` section so it is never asked again. When the ecosystem ships a runner (Django, Go, Rust, Elixir, Maven/Gradle), that runner is the candidate — never install a second framework beside a working one.
|
||||
**If ANY existing-test evidence appears** (a config file, a declared test script or make target, a nonzero `TESTFILES:` count, or `TESTS:rust in-source`): the project has tests. **Do NOT bootstrap.** Print "Existing tests detected: {the evidence}." Then get the command the same way Step 5 does — CLAUDE.md/TESTING.md if documented, otherwise AskUserQuestion offering the candidates from the table above plus "Other", and persist the answer to CLAUDE.md's `## Testing` section so it is never asked again. When the ecosystem ships a runner (Django, Go, Rust, Dart, Elixir, Maven/Gradle), that runner is the candidate — never install a second framework beside a working one.
|
||||
Read 2-3 existing test files to learn conventions (naming, imports, assertion style, setup patterns).
|
||||
Store conventions as prose context for use in Phase 8e.5 or Step 7. **Skip the rest of bootstrap.**
|
||||
|
||||
Absent config files and absent `tests/` directories are NOT evidence of "no tests": Django keeps tests in `<app>/tests.py`, Go in `*_test.go` beside the source, Rust in `#[test]` blocks inside `src/`. A green `python manage.py test` with no `pytest.ini` is a tested project, not a bootstrap candidate.
|
||||
Absent config files and absent `tests/` directories are NOT evidence of "no tests": Django keeps tests in `<app>/tests.py`, Go in `*_test.go` beside the source, Rust in `#[test]` blocks inside `src/`.
|
||||
|
||||
**If BOOTSTRAP_DECLINED** appears: Print "Test bootstrap previously declined — skipping." **Skip the rest of bootstrap.**
|
||||
|
||||
@@ -630,6 +632,7 @@ If Aside is not installed or not running (`command -v aside` prints nothing, or
|
||||
| Go | stdlib testing + testify | stdlib only |
|
||||
| JVM (Maven/Gradle) | JUnit 5 + AssertJ | JUnit 5 only |
|
||||
| Rust | cargo test (built-in) + mockall | — |
|
||||
| Dart | test / flutter_test + mocktail | mockito |
|
||||
| PHP | phpunit + mockery | pest |
|
||||
| Elixir | ExUnit (built-in) + ex_machina | — |
|
||||
|
||||
|
||||
+2
-2
@@ -413,7 +413,7 @@ It runs an 80-item visual audit on your live site — in your Aside browser, so
|
||||
|
||||
The self-regulation heuristic is tuned for design work — CSS-only changes get a free pass (they are inherently safe and reversible), but changes to component JSX/TSX files count against the risk budget. Hard cap at 30 fixes. If the risk score exceeds 20%, it stops and asks.
|
||||
|
||||
**Design detector.** When you have [impeccable](https://impeccable.style) installed, its engine runs first: on a URL the page's rendered DOM is dumped (linked styles inlined) and scanned; on a feature branch with no URL the changed frontend files are scanned. Every mechanical finding arrives as a `FINDING-NNN` tagged with its rule id (`[nested-cards]`, `[low-contrast]`), and the report closes with `Detector: N → M`. gstack never runs impeccable's installer; when no engine is present it offers, once, to download the engine binary (checksum-pinned, logged in the egress ledger) and remembers the answer; without it the audit is unchanged. `gstack-config set design_detector off` disables the pre-pass.
|
||||
**Design detector.** When you have [impeccable](https://impeccable.style) installed, its engine runs first: on a URL the page's rendered DOM is dumped (linked styles inlined) and scanned; on a feature branch with no URL the changed frontend files are scanned (`.dart` files excepted: they count as frontend scope, but the engine reads web sources). Every mechanical finding arrives as a `FINDING-NNN` tagged with its rule id (`[nested-cards]`, `[low-contrast]`), and the report closes with `Detector: N → M`. gstack never runs impeccable's installer; when no engine is present it offers, once, to download the engine binary (checksum-pinned, logged in the egress ledger) and remembers the answer; without it the audit is unchanged. `gstack-config set design_detector off` disables the pre-pass.
|
||||
|
||||
### Example
|
||||
|
||||
@@ -586,7 +586,7 @@ Findings get action, not just listed. Obvious mechanical fixes (dead code, stale
|
||||
|
||||
One exception: a shortcut you took deliberately and logged. A `gstack-shortcut(dec-<id>)` marker whose decision id resolves in the decision ledger downgrades the finding to acknowledged debt. An orphan marker — one with no ledger entry behind it — doesn't suppress anything; the gap is reported normally and the marker itself gets flagged.
|
||||
|
||||
**Design pass.** When the diff touches frontend files, the Design specialist reads `review/design-checklist.md`, which is generated from `lib/design-catalog.ts`, so `/review`, `/ship`, and `/design-review` flag the same patterns under the same rule ids. If you have [impeccable](https://impeccable.style) installed, its engine scans the changed frontend files first: its rows bucket by tier (auto-fix, ask, possible), a detector hit and a checklist hit at the same file:line collapse into one row, and your repo's `.impeccable/config*.json` ignores are read as settled decisions. Without it, the checklist pass runs alone.
|
||||
**Design pass.** When the diff touches frontend files, the Design specialist reads `review/design-checklist.md`, which is generated from `lib/design-catalog.ts`, so `/review`, `/ship`, and `/design-review` flag the same patterns under the same rule ids. If you have [impeccable](https://impeccable.style) installed, its engine scans the changed frontend files first (`.dart` files get the checklist pass only): its rows bucket by tier (auto-fix, ask, possible), a detector hit and a checklist hit at the same file:line collapse into one row, and your repo's `.impeccable/config*.json` ignores are read as settled decisions. Without it, the checklist pass runs alone.
|
||||
|
||||
### Example
|
||||
|
||||
|
||||
@@ -11,6 +11,9 @@ const EXTENSIONS = new Set([
|
||||
'.tsx', '.jsx', '.vue', '.svelte', '.astro',
|
||||
'.erb', '.haml', '.slim', '.hbs', '.ejs',
|
||||
'.html',
|
||||
// Dart: a Flutter widget and a repository share the extension, so the bash
|
||||
// script counts .dart as frontend AND backend; only the frontend half lives here.
|
||||
'.dart',
|
||||
]);
|
||||
|
||||
// Root-level only: the bash arm's glob (`tailwind.config.*`) is matched against the
|
||||
|
||||
@@ -585,20 +585,21 @@ Before analyzing coverage, detect the project's test framework:
|
||||
|
||||
```bash
|
||||
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||
# Detect project runtime (markers are evidence, not commands to run blind)
|
||||
# Runtime markers: evidence, not commands to run
|
||||
[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django"
|
||||
{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python"
|
||||
{ [ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ]; } && echo "RUNTIME:ruby"
|
||||
[ -f package.json ] && echo "RUNTIME:node"
|
||||
[ -f go.mod ] && echo "RUNTIME:go"
|
||||
[ -f Cargo.toml ] && echo "RUNTIME:rust"
|
||||
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
|
||||
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
|
||||
{ [ -f build.gradle ] || [ -f build.gradle.kts ]; } && echo "RUNTIME:jvm BUILD:gradle"
|
||||
# Check for existing test infrastructure — config files, scripts, AND test files
|
||||
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml 2>/dev/null
|
||||
# Test infra: configs, scripts, test files
|
||||
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml dart_test.yaml 2>/dev/null
|
||||
[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test"
|
||||
[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test"
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs|dart)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
```
|
||||
|
||||
3. **If no framework detected:** State that the framework is unknown; continue the diagram and planned assertions. If proposing a new framework, settle that choice through Decision procedure in Test step 5. Reuse an exact prior approval; with no selection proposed, ask no framework question. Do not install a framework or write the proposed tests during this review.
|
||||
|
||||
@@ -8,9 +8,7 @@ Every diff gets the Claude adversarial pass. Add Codex when its preflight is rea
|
||||
|
||||
```bash
|
||||
DIFF_BASE=$(git merge-base origin/<base> HEAD)
|
||||
DIFF_INS=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0")
|
||||
DIFF_DEL=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0")
|
||||
DIFF_TOTAL=$((DIFF_INS + DIFF_DEL))
|
||||
DIFF_TOTAL=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
|
||||
echo "DIFF_SIZE: $DIFF_TOTAL"
|
||||
```
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ STACK=""
|
||||
[ -f requirements.txt ] || [ -f pyproject.toml ] && STACK="${STACK}python "
|
||||
[ -f go.mod ] && STACK="${STACK}go "
|
||||
[ -f Cargo.toml ] && STACK="${STACK}rust "
|
||||
[ -f pubspec.yaml ] && STACK="${STACK}dart "
|
||||
echo "STACK: ${STACK:-unknown}"
|
||||
DIFF_BASE=$(git merge-base origin/<base> HEAD)
|
||||
DIFF_LINES=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
|
||||
@@ -25,6 +26,7 @@ TEST_FW=""
|
||||
{ [ -f spec/spec_helper.rb ] || [ -f .rspec ]; } && TEST_FW="rspec"
|
||||
{ [ -f pytest.ini ] || [ -f conftest.py ]; } && TEST_FW="pytest"
|
||||
[ -f go.mod ] && TEST_FW="go-test"
|
||||
[ -f pubspec.yaml ] && TEST_FW="dart-test" && grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && TEST_FW="flutter-test"
|
||||
echo "TEST_FW: ${TEST_FW:-unknown}"
|
||||
```
|
||||
|
||||
|
||||
@@ -243,9 +243,7 @@ Every diff gets the ${outsideVoiceFor(ctx).nativeLabel} adversarial pass. Add ${
|
||||
|
||||
\`\`\`bash
|
||||
DIFF_BASE=$(git merge-base origin/<base> HEAD)
|
||||
DIFF_INS=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0")
|
||||
DIFF_DEL=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0")
|
||||
DIFF_TOTAL=$((DIFF_INS + DIFF_DEL))
|
||||
DIFF_TOTAL=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
|
||||
echo "DIFF_SIZE: $DIFF_TOTAL"
|
||||
\`\`\`
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ STACK=""
|
||||
[ -f requirements.txt ] || [ -f pyproject.toml ] && STACK="\${STACK}python "
|
||||
[ -f go.mod ] && STACK="\${STACK}go "
|
||||
[ -f Cargo.toml ] && STACK="\${STACK}rust "
|
||||
[ -f pubspec.yaml ] && STACK="\${STACK}dart "
|
||||
echo "STACK: \${STACK:-unknown}"
|
||||
DIFF_BASE=$(git merge-base origin/<base> HEAD)
|
||||
DIFF_LINES=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
|
||||
@@ -44,6 +45,7 @@ TEST_FW=""
|
||||
{ [ -f spec/spec_helper.rb ] || [ -f .rspec ]; } && TEST_FW="rspec"
|
||||
{ [ -f pytest.ini ] || [ -f conftest.py ]; } && TEST_FW="pytest"
|
||||
[ -f go.mod ] && TEST_FW="go-test"
|
||||
[ -f pubspec.yaml ] && TEST_FW="dart-test" && grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && TEST_FW="flutter-test"
|
||||
echo "TEST_FW: \${TEST_FW:-unknown}"
|
||||
\`\`\`
|
||||
|
||||
|
||||
@@ -8,17 +8,18 @@ export function generateTestBootstrap(ctx: TemplateContext): string {
|
||||
|
||||
**Read the project's CLAUDE.md (and TESTING.md if present) FIRST.** If it documents a test command, the project already told you: no detection, no bootstrap. Skip the rest of bootstrap and use that command in Step 5.
|
||||
|
||||
**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker tells you which ecosystem you're in and which command to OFFER. It does not tell you the command works. Do not execute a candidate test command to "check" it: a probe on a project that never had that runner fails loudly and teaches you nothing, and installing a second framework over a working one is worse.
|
||||
**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker names the ecosystem and the command to OFFER; it does not prove the command works. Never execute a candidate to "check" it: probing a runner the project never had fails loudly and teaches nothing.
|
||||
|
||||
\`\`\`bash
|
||||
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||
# Definitive ecosystem markers (presence = ecosystem, NOT a command to run)
|
||||
# Ecosystem markers
|
||||
[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django MARKER:manage.py"
|
||||
{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python"
|
||||
{ [ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ]; } && echo "RUNTIME:ruby"
|
||||
[ -f package.json ] && echo "RUNTIME:node"
|
||||
[ -f go.mod ] && echo "RUNTIME:go"
|
||||
[ -f Cargo.toml ] && echo "RUNTIME:rust"
|
||||
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
|
||||
[ -f composer.json ] && echo "RUNTIME:php"
|
||||
[ -f mix.exs ] && echo "RUNTIME:elixir"
|
||||
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
|
||||
@@ -26,14 +27,14 @@ setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||
# Detect sub-frameworks
|
||||
[ -f Gemfile ] && grep -q "rails" Gemfile 2>/dev/null && echo "FRAMEWORK:rails"
|
||||
[ -f package.json ] && grep -q '"next"' package.json 2>/dev/null && echo "FRAMEWORK:nextjs"
|
||||
# Existing test path — config files, declared scripts, AND test FILES.
|
||||
# A project with real tests and no config file is the common miss.
|
||||
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* 2>/dev/null
|
||||
grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && echo "FRAMEWORK:flutter"
|
||||
# Test evidence: config files, declared scripts, AND test FILES (tested projects often lack a config)
|
||||
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* dart_test.yaml 2>/dev/null
|
||||
[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test"
|
||||
[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test"
|
||||
[ -f pyproject.toml ] && grep -q "pytest" pyproject.toml && echo "CONFIG:pyproject pytest"
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\\.py$|(^|/)test_[^/]+\\.py$|_test\\.(go|py|rb|ts|js|exs)$|\\.(test|spec)\\.[jt]sx?$|_spec\\.rb$|Test\\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
# Rust keeps unit tests inside src/, so file names alone miss them
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\\.py$|(^|/)test_[^/]+\\.py$|_test\\.(go|py|rb|ts|js|exs|dart)$|\\.(test|spec)\\.[jt]sx?$|_spec\\.rb$|Test\\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
# Rust unit tests live in src/; file names alone miss them
|
||||
[ -f Cargo.toml ] && git grep -lF '#[test]' -- 'src' >/dev/null 2>&1 && echo "TESTS:rust in-source"
|
||||
# Check opt-out marker
|
||||
[ -f .gstack/no-test-bootstrap ] && echo "BOOTSTRAP_DECLINED"
|
||||
@@ -47,6 +48,7 @@ Map the markers to the command you will OFFER — never to one you run on a gues
|
||||
| \`pytest.ini\` / \`tox.ini\` / pytest in \`pyproject.toml\` / \`test_*.py\` | Python | \`pytest\` |
|
||||
| \`go.mod\` (+ any \`*_test.go\`) | Go | \`go test ./...\` |
|
||||
| \`Cargo.toml\` | Rust | \`cargo test\` |
|
||||
| \`pubspec.yaml\` | Dart / Flutter | \`flutter test\` (FRAMEWORK:flutter) or \`dart test\`; \`fvm\` prefix if \`.fvmrc\` |
|
||||
| \`pom.xml\` | JVM (Maven) | \`mvn test\` |
|
||||
| \`build.gradle\` / \`build.gradle.kts\` | JVM (Gradle) | \`./gradlew test\` |
|
||||
| \`Gemfile\` / \`Rakefile\` / \`.rspec\` | Ruby | \`bundle exec rspec\`, \`bin/rails test\`, or \`rake test\` |
|
||||
@@ -55,11 +57,11 @@ Map the markers to the command you will OFFER — never to one you run on a gues
|
||||
| \`package.json\` with a \`test\` script | Node | that script, run with the package manager the lockfile names |
|
||||
| \`Makefile\` with a \`test:\` target | any | \`make test\` |
|
||||
|
||||
**If ANY existing-test evidence appears** (a config file, a declared test script or make target, a nonzero \`TESTFILES:\` count, or \`TESTS:rust in-source\`): the project has tests. **Do NOT bootstrap.** Print "Existing tests detected: {the evidence}." Then get the command the same way Step 5 does — CLAUDE.md/TESTING.md if documented, otherwise AskUserQuestion offering the candidates from the table above plus "Other", and persist the answer to CLAUDE.md's \`## Testing\` section so it is never asked again. When the ecosystem ships a runner (Django, Go, Rust, Elixir, Maven/Gradle), that runner is the candidate — never install a second framework beside a working one.
|
||||
**If ANY existing-test evidence appears** (a config file, a declared test script or make target, a nonzero \`TESTFILES:\` count, or \`TESTS:rust in-source\`): the project has tests. **Do NOT bootstrap.** Print "Existing tests detected: {the evidence}." Then get the command the same way Step 5 does — CLAUDE.md/TESTING.md if documented, otherwise AskUserQuestion offering the candidates from the table above plus "Other", and persist the answer to CLAUDE.md's \`## Testing\` section so it is never asked again. When the ecosystem ships a runner (Django, Go, Rust, Dart, Elixir, Maven/Gradle), that runner is the candidate — never install a second framework beside a working one.
|
||||
Read 2-3 existing test files to learn conventions (naming, imports, assertion style, setup patterns).
|
||||
Store conventions as prose context for use in ${ctx.skillName === 'ship' ? 'Step 7' : 'Phase 8e.5 or Step 7'}. **Skip the rest of bootstrap.**
|
||||
|
||||
Absent config files and absent \`tests/\` directories are NOT evidence of "no tests": Django keeps tests in \`<app>/tests.py\`, Go in \`*_test.go\` beside the source, Rust in \`#[test]\` blocks inside \`src/\`. A green \`python manage.py test\` with no \`pytest.ini\` is a tested project, not a bootstrap candidate.
|
||||
Absent config files and absent \`tests/\` directories are NOT evidence of "no tests": Django keeps tests in \`<app>/tests.py\`, Go in \`*_test.go\` beside the source, Rust in \`#[test]\` blocks inside \`src/\`.
|
||||
|
||||
${ctx.skillName === 'ship'
|
||||
? '**If BOOTSTRAP_DECLINED** appears:\n- Step 5\'s explicit Add tests choice overrides that marker for this invocation only: continue to runtime detection and B2–B3, including framework approval.\n- Otherwise print "Test bootstrap previously declined — skipping" and **skip the rest of bootstrap**.'
|
||||
@@ -99,6 +101,7 @@ If Aside is not installed or not running (\`command -v aside\` prints nothing, o
|
||||
| Go | stdlib testing + testify | stdlib only |
|
||||
| JVM (Maven/Gradle) | JUnit 5 + AssertJ | JUnit 5 only |
|
||||
| Rust | cargo test (built-in) + mockall | — |
|
||||
| Dart | test / flutter_test + mocktail | mockito |
|
||||
| PHP | phpunit + mockery | pest |
|
||||
| Elixir | ExUnit (built-in) + ex_machina | — |
|
||||
|
||||
@@ -269,20 +272,21 @@ Before analyzing coverage, detect the project's test framework:
|
||||
|
||||
\`\`\`bash
|
||||
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||
# Detect project runtime (markers are evidence, not commands to run blind)
|
||||
# Runtime markers: evidence, not commands to run
|
||||
[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django"
|
||||
{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python"
|
||||
{ [ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ]; } && echo "RUNTIME:ruby"
|
||||
[ -f package.json ] && echo "RUNTIME:node"
|
||||
[ -f go.mod ] && echo "RUNTIME:go"
|
||||
[ -f Cargo.toml ] && echo "RUNTIME:rust"
|
||||
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
|
||||
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
|
||||
{ [ -f build.gradle ] || [ -f build.gradle.kts ]; } && echo "RUNTIME:jvm BUILD:gradle"
|
||||
# Check for existing test infrastructure — config files, scripts, AND test files
|
||||
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml 2>/dev/null
|
||||
# Test infra: configs, scripts, test files
|
||||
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml dart_test.yaml 2>/dev/null
|
||||
[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test"
|
||||
[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test"
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\\.py$|(^|/)test_[^/]+\\.py$|_test\\.(go|py|rb|ts|js|exs)$|\\.(test|spec)\\.[jt]sx?$|_spec\\.rb$|Test\\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\\.py$|(^|/)test_[^/]+\\.py$|_test\\.(go|py|rb|ts|js|exs|dart)$|\\.(test|spec)\\.[jt]sx?$|_spec\\.rb$|Test\\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
\`\`\`
|
||||
|
||||
3. **If no framework detected:**${mode === 'ship' ? ' use the bootstrap decision already made in Step 4; report diagram-only coverage if setup was declined. Do not restart bootstrap from this audit.' : ' State that the framework is unknown; continue the diagram and planned assertions. If proposing a new framework, settle that choice through Decision procedure in Test step 5. Reuse an exact prior approval; with no selection proposed, ask no framework question. Do not install a framework or write the proposed tests during this review.'}`);
|
||||
|
||||
@@ -8,9 +8,7 @@ Every diff gets the Claude adversarial pass. Add Codex when its preflight is rea
|
||||
|
||||
```bash
|
||||
DIFF_BASE=$(git merge-base origin/<base> HEAD)
|
||||
DIFF_INS=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ insertion' | grep -oE '[0-9]+' || echo "0")
|
||||
DIFF_DEL=$(git diff "$DIFF_BASE" --stat | tail -1 | grep -oE '[0-9]+ deletion' | grep -oE '[0-9]+' || echo "0")
|
||||
DIFF_TOTAL=$((DIFF_INS + DIFF_DEL))
|
||||
DIFF_TOTAL=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
|
||||
echo "DIFF_SIZE: $DIFF_TOTAL"
|
||||
```
|
||||
|
||||
|
||||
@@ -276,6 +276,7 @@ STACK=""
|
||||
[ -f requirements.txt ] || [ -f pyproject.toml ] && STACK="${STACK}python "
|
||||
[ -f go.mod ] && STACK="${STACK}go "
|
||||
[ -f Cargo.toml ] && STACK="${STACK}rust "
|
||||
[ -f pubspec.yaml ] && STACK="${STACK}dart "
|
||||
echo "STACK: ${STACK:-unknown}"
|
||||
DIFF_BASE=$(git merge-base origin/<base> HEAD)
|
||||
DIFF_LINES=$(git diff "$DIFF_BASE" --numstat | awk '{n+=$1+$2} END {print n+0}')
|
||||
@@ -287,6 +288,7 @@ TEST_FW=""
|
||||
{ [ -f spec/spec_helper.rb ] || [ -f .rspec ]; } && TEST_FW="rspec"
|
||||
{ [ -f pytest.ini ] || [ -f conftest.py ]; } && TEST_FW="pytest"
|
||||
[ -f go.mod ] && TEST_FW="go-test"
|
||||
[ -f pubspec.yaml ] && TEST_FW="dart-test" && grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && TEST_FW="flutter-test"
|
||||
echo "TEST_FW: ${TEST_FW:-unknown}"
|
||||
```
|
||||
|
||||
|
||||
@@ -48,20 +48,21 @@ Before analyzing coverage, detect the project's test framework:
|
||||
|
||||
```bash
|
||||
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||
# Detect project runtime (markers are evidence, not commands to run blind)
|
||||
# Runtime markers: evidence, not commands to run
|
||||
[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django"
|
||||
{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python"
|
||||
{ [ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ]; } && echo "RUNTIME:ruby"
|
||||
[ -f package.json ] && echo "RUNTIME:node"
|
||||
[ -f go.mod ] && echo "RUNTIME:go"
|
||||
[ -f Cargo.toml ] && echo "RUNTIME:rust"
|
||||
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
|
||||
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
|
||||
{ [ -f build.gradle ] || [ -f build.gradle.kts ]; } && echo "RUNTIME:jvm BUILD:gradle"
|
||||
# Check for existing test infrastructure — config files, scripts, AND test files
|
||||
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml 2>/dev/null
|
||||
# Test infra: configs, scripts, test files
|
||||
ls jest.config.* vitest.config.* playwright.config.* cypress.config.* .rspec pytest.ini tox.ini phpunit.xml dart_test.yaml 2>/dev/null
|
||||
[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test"
|
||||
[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test"
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs|dart)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
```
|
||||
|
||||
3. **If no framework detected:** use the bootstrap decision already made in Step 4; report diagram-only coverage if setup was declined. Do not restart bootstrap from this audit.
|
||||
|
||||
+12
-9
@@ -6,17 +6,18 @@
|
||||
|
||||
**Read the project's CLAUDE.md (and TESTING.md if present) FIRST.** If it documents a test command, the project already told you: no detection, no bootstrap. Skip the rest of bootstrap and use that command in Step 5.
|
||||
|
||||
**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker tells you which ecosystem you're in and which command to OFFER. It does not tell you the command works. Do not execute a candidate test command to "check" it: a probe on a project that never had that runner fails loudly and teaches you nothing, and installing a second framework over a working one is worse.
|
||||
**Otherwise gather markers. Every marker below is EVIDENCE for the question you ask — never a command to run blind.** A marker names the ecosystem and the command to OFFER; it does not prove the command works. Never execute a candidate to "check" it: probing a runner the project never had fails loudly and teaches nothing.
|
||||
|
||||
```bash
|
||||
setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||
# Definitive ecosystem markers (presence = ecosystem, NOT a command to run)
|
||||
# Ecosystem markers
|
||||
[ -f manage.py ] && echo "RUNTIME:python FRAMEWORK:django MARKER:manage.py"
|
||||
{ [ -f pyproject.toml ] || [ -f pytest.ini ] || [ -f tox.ini ] || [ -f setup.cfg ] || [ -f requirements.txt ]; } && echo "RUNTIME:python"
|
||||
{ [ -f Gemfile ] || [ -f Rakefile ] || [ -f .rspec ]; } && echo "RUNTIME:ruby"
|
||||
[ -f package.json ] && echo "RUNTIME:node"
|
||||
[ -f go.mod ] && echo "RUNTIME:go"
|
||||
[ -f Cargo.toml ] && echo "RUNTIME:rust"
|
||||
[ -f pubspec.yaml ] && echo "RUNTIME:dart"
|
||||
[ -f composer.json ] && echo "RUNTIME:php"
|
||||
[ -f mix.exs ] && echo "RUNTIME:elixir"
|
||||
[ -f pom.xml ] && echo "RUNTIME:jvm BUILD:maven"
|
||||
@@ -24,14 +25,14 @@ setopt +o nomatch 2>/dev/null || true # zsh compat
|
||||
# Detect sub-frameworks
|
||||
[ -f Gemfile ] && grep -q "rails" Gemfile 2>/dev/null && echo "FRAMEWORK:rails"
|
||||
[ -f package.json ] && grep -q '"next"' package.json 2>/dev/null && echo "FRAMEWORK:nextjs"
|
||||
# Existing test path — config files, declared scripts, AND test FILES.
|
||||
# A project with real tests and no config file is the common miss.
|
||||
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* 2>/dev/null
|
||||
grep -qsE '^ +sdk: *.?flutter' pubspec.yaml && echo "FRAMEWORK:flutter"
|
||||
# Test evidence: config files, declared scripts, AND test FILES (tested projects often lack a config)
|
||||
ls jest.config.* vitest.config.* playwright.config.* .rspec pytest.ini tox.ini phpunit.xml* dart_test.yaml 2>/dev/null
|
||||
[ -f package.json ] && grep -q '"test"[[:space:]]*:' package.json && echo "SCRIPT:package.json test"
|
||||
[ -f Makefile ] && grep -qE '^(test|check):' Makefile && echo "TARGET:make test"
|
||||
[ -f pyproject.toml ] && grep -q "pytest" pyproject.toml && echo "CONFIG:pyproject pytest"
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
# Rust keeps unit tests inside src/, so file names alone miss them
|
||||
git ls-files | grep -cE '(^|/)(tests?|spec|__tests__)/|(^|/)tests?\.py$|(^|/)test_[^/]+\.py$|_test\.(go|py|rb|ts|js|exs|dart)$|\.(test|spec)\.[jt]sx?$|_spec\.rb$|Test\.(java|kt)$' | sed 's/^/TESTFILES:/'
|
||||
# Rust unit tests live in src/; file names alone miss them
|
||||
[ -f Cargo.toml ] && git grep -lF '#[test]' -- 'src' >/dev/null 2>&1 && echo "TESTS:rust in-source"
|
||||
# Check opt-out marker
|
||||
[ -f .gstack/no-test-bootstrap ] && echo "BOOTSTRAP_DECLINED"
|
||||
@@ -45,6 +46,7 @@ Map the markers to the command you will OFFER — never to one you run on a gues
|
||||
| `pytest.ini` / `tox.ini` / pytest in `pyproject.toml` / `test_*.py` | Python | `pytest` |
|
||||
| `go.mod` (+ any `*_test.go`) | Go | `go test ./...` |
|
||||
| `Cargo.toml` | Rust | `cargo test` |
|
||||
| `pubspec.yaml` | Dart / Flutter | `flutter test` (FRAMEWORK:flutter) or `dart test`; `fvm` prefix if `.fvmrc` |
|
||||
| `pom.xml` | JVM (Maven) | `mvn test` |
|
||||
| `build.gradle` / `build.gradle.kts` | JVM (Gradle) | `./gradlew test` |
|
||||
| `Gemfile` / `Rakefile` / `.rspec` | Ruby | `bundle exec rspec`, `bin/rails test`, or `rake test` |
|
||||
@@ -53,11 +55,11 @@ Map the markers to the command you will OFFER — never to one you run on a gues
|
||||
| `package.json` with a `test` script | Node | that script, run with the package manager the lockfile names |
|
||||
| `Makefile` with a `test:` target | any | `make test` |
|
||||
|
||||
**If ANY existing-test evidence appears** (a config file, a declared test script or make target, a nonzero `TESTFILES:` count, or `TESTS:rust in-source`): the project has tests. **Do NOT bootstrap.** Print "Existing tests detected: {the evidence}." Then get the command the same way Step 5 does — CLAUDE.md/TESTING.md if documented, otherwise AskUserQuestion offering the candidates from the table above plus "Other", and persist the answer to CLAUDE.md's `## Testing` section so it is never asked again. When the ecosystem ships a runner (Django, Go, Rust, Elixir, Maven/Gradle), that runner is the candidate — never install a second framework beside a working one.
|
||||
**If ANY existing-test evidence appears** (a config file, a declared test script or make target, a nonzero `TESTFILES:` count, or `TESTS:rust in-source`): the project has tests. **Do NOT bootstrap.** Print "Existing tests detected: {the evidence}." Then get the command the same way Step 5 does — CLAUDE.md/TESTING.md if documented, otherwise AskUserQuestion offering the candidates from the table above plus "Other", and persist the answer to CLAUDE.md's `## Testing` section so it is never asked again. When the ecosystem ships a runner (Django, Go, Rust, Dart, Elixir, Maven/Gradle), that runner is the candidate — never install a second framework beside a working one.
|
||||
Read 2-3 existing test files to learn conventions (naming, imports, assertion style, setup patterns).
|
||||
Store conventions as prose context for use in Step 7. **Skip the rest of bootstrap.**
|
||||
|
||||
Absent config files and absent `tests/` directories are NOT evidence of "no tests": Django keeps tests in `<app>/tests.py`, Go in `*_test.go` beside the source, Rust in `#[test]` blocks inside `src/`. A green `python manage.py test` with no `pytest.ini` is a tested project, not a bootstrap candidate.
|
||||
Absent config files and absent `tests/` directories are NOT evidence of "no tests": Django keeps tests in `<app>/tests.py`, Go in `*_test.go` beside the source, Rust in `#[test]` blocks inside `src/`.
|
||||
|
||||
**If BOOTSTRAP_DECLINED** appears:
|
||||
- Step 5's explicit Add tests choice overrides that marker for this invocation only: continue to runtime detection and B2–B3, including framework approval.
|
||||
@@ -103,6 +105,7 @@ If Aside is not installed or not running (`command -v aside` prints nothing, or
|
||||
| Go | stdlib testing + testify | stdlib only |
|
||||
| JVM (Maven/Gradle) | JUnit 5 + AssertJ | JUnit 5 only |
|
||||
| Rust | cargo test (built-in) + mockall | — |
|
||||
| Dart | test / flutter_test + mocktail | mockito |
|
||||
| PHP | phpunit + mockery | pest |
|
||||
| Elixir | ExUnit (built-in) + ex_machina | — |
|
||||
|
||||
|
||||
@@ -87,6 +87,19 @@ describe('gstack-diff-scope', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// A Dart change matched no category but its test file, so a Flutter widget PR
|
||||
// set SCOPE_TESTS alone and skipped the design, performance and security passes.
|
||||
test('detects Dart as both frontend and backend; pubspec.lock is config', () => {
|
||||
const scope = runScope(createRepo(['lib/theme.dart', 'lib/widgets/drawer.dart', 'test/drawer_test.dart']));
|
||||
expect(scope.SCOPE_FRONTEND).toBe('true');
|
||||
expect(scope.SCOPE_BACKEND).toBe('true');
|
||||
expect(scope.SCOPE_TESTS).toBe('true');
|
||||
expect(runScope(createRepo(['pubspec.lock'])).SCOPE_CONFIG).toBe('true');
|
||||
// Value: protects=a workspace package's pubspec.lock still reads as config; fails_when=the */pubspec.lock arm is dropped;
|
||||
// why_new=only the root lock was checked; seam=none; no_claim=none beyond protects
|
||||
expect(runScope(createRepo(['packages/cache/pubspec.lock'])).SCOPE_CONFIG).toBe('true');
|
||||
});
|
||||
|
||||
test('detects test files', () => {
|
||||
const dir = createRepo(['test/app.test.ts']);
|
||||
const scope = runScope(dir);
|
||||
|
||||
+31
-31
@@ -4,92 +4,92 @@
|
||||
"pins": {
|
||||
"agy/gstack-autoplan": 130339,
|
||||
"agy/gstack-design-consultation": 108037,
|
||||
"agy/gstack-design-review": 143386,
|
||||
"agy/gstack-design-review": 143381,
|
||||
"agy/gstack-land-and-deploy": 116578,
|
||||
"agy/gstack-plan-design-review": 139980,
|
||||
"agy/gstack-plan-devex-review": 135727,
|
||||
"agy/gstack-plan-eng-review": 152669,
|
||||
"agy/gstack-review": 124266,
|
||||
"claude/design-review": 136826,
|
||||
"agy/gstack-plan-eng-review": 152668,
|
||||
"agy/gstack-review": 124086,
|
||||
"claude/design-review": 136821,
|
||||
"codex/gstack-autoplan": 130298,
|
||||
"codex/gstack-design-consultation": 109239,
|
||||
"codex/gstack-design-review": 144820,
|
||||
"codex/gstack-design-review": 144815,
|
||||
"codex/gstack-land-and-deploy": 121155,
|
||||
"codex/gstack-office-hours": 102108,
|
||||
"codex/gstack-plan-design-review": 142367,
|
||||
"codex/gstack-plan-devex-review": 138550,
|
||||
"codex/gstack-plan-eng-review": 155149,
|
||||
"codex/gstack-review": 125071,
|
||||
"codex/gstack-plan-eng-review": 155148,
|
||||
"codex/gstack-review": 124891,
|
||||
"copilot/gstack-autoplan": 131472,
|
||||
"copilot/gstack-codex": 100103,
|
||||
"copilot/gstack-design-consultation": 109389,
|
||||
"copilot/gstack-design-review": 144691,
|
||||
"copilot/gstack-design-review": 144686,
|
||||
"copilot/gstack-land-and-deploy": 117848,
|
||||
"copilot/gstack-office-hours": 100396,
|
||||
"copilot/gstack-plan-design-review": 140851,
|
||||
"copilot/gstack-plan-devex-review": 136879,
|
||||
"copilot/gstack-plan-eng-review": 153394,
|
||||
"copilot/gstack-review": 125537,
|
||||
"copilot/gstack-plan-eng-review": 153393,
|
||||
"copilot/gstack-review": 125357,
|
||||
"cursor/gstack-autoplan": 129113,
|
||||
"cursor/gstack-design-consultation": 107075,
|
||||
"cursor/gstack-design-review": 142365,
|
||||
"cursor/gstack-design-review": 142360,
|
||||
"cursor/gstack-land-and-deploy": 115517,
|
||||
"cursor/gstack-plan-design-review": 138935,
|
||||
"cursor/gstack-plan-devex-review": 134558,
|
||||
"cursor/gstack-plan-eng-review": 151523,
|
||||
"cursor/gstack-review": 140637,
|
||||
"cursor/gstack-plan-eng-review": 151522,
|
||||
"cursor/gstack-review": 140618,
|
||||
"factory/gstack-autoplan": 131397,
|
||||
"factory/gstack-codex": 100523,
|
||||
"factory/gstack-design-consultation": 108523,
|
||||
"factory/gstack-design-review": 144155,
|
||||
"factory/gstack-design-review": 144150,
|
||||
"factory/gstack-land-and-deploy": 119700,
|
||||
"factory/gstack-office-hours": 101275,
|
||||
"factory/gstack-plan-design-review": 141716,
|
||||
"factory/gstack-plan-devex-review": 137927,
|
||||
"factory/gstack-plan-eng-review": 154637,
|
||||
"factory/gstack-review": 144416,
|
||||
"factory/gstack-plan-eng-review": 154636,
|
||||
"factory/gstack-review": 144397,
|
||||
"gbrain/gstack-autoplan": 129650,
|
||||
"gbrain/gstack-design-review": 132733,
|
||||
"gbrain/gstack-design-review": 132728,
|
||||
"gbrain/gstack-land-and-deploy": 116039,
|
||||
"gbrain/gstack-plan-design-review": 127855,
|
||||
"gbrain/gstack-plan-devex-review": 115129,
|
||||
"gbrain/gstack-plan-eng-review": 134288,
|
||||
"gbrain/gstack-plan-eng-review": 134287,
|
||||
"hermes/gstack-autoplan": 129740,
|
||||
"hermes/gstack-design-review": 132840,
|
||||
"hermes/gstack-design-review": 132835,
|
||||
"hermes/gstack-land-and-deploy": 116141,
|
||||
"hermes/gstack-plan-design-review": 127964,
|
||||
"hermes/gstack-plan-devex-review": 115215,
|
||||
"hermes/gstack-plan-eng-review": 134373,
|
||||
"hermes/gstack-plan-eng-review": 134372,
|
||||
"kiro/gstack-autoplan": 128875,
|
||||
"kiro/gstack-design-consultation": 106917,
|
||||
"kiro/gstack-design-review": 142179,
|
||||
"kiro/gstack-design-review": 142174,
|
||||
"kiro/gstack-land-and-deploy": 115407,
|
||||
"kiro/gstack-plan-design-review": 138775,
|
||||
"kiro/gstack-plan-devex-review": 134398,
|
||||
"kiro/gstack-plan-eng-review": 151373,
|
||||
"kiro/gstack-review": 140449,
|
||||
"kiro/gstack-plan-eng-review": 151372,
|
||||
"kiro/gstack-review": 140430,
|
||||
"openclaw/gstack-autoplan": 129355,
|
||||
"openclaw/gstack-design-review": 131538,
|
||||
"openclaw/gstack-design-review": 131533,
|
||||
"openclaw/gstack-land-and-deploy": 115627,
|
||||
"openclaw/gstack-plan-design-review": 126706,
|
||||
"openclaw/gstack-plan-devex-review": 113968,
|
||||
"openclaw/gstack-plan-eng-review": 132685,
|
||||
"openclaw/gstack-plan-eng-review": 132684,
|
||||
"opencode/gstack-autoplan": 131475,
|
||||
"opencode/gstack-design-consultation": 109210,
|
||||
"opencode/gstack-design-review": 144563,
|
||||
"opencode/gstack-design-review": 144558,
|
||||
"opencode/gstack-land-and-deploy": 117625,
|
||||
"opencode/gstack-office-hours": 100230,
|
||||
"opencode/gstack-plan-design-review": 140705,
|
||||
"opencode/gstack-plan-devex-review": 136779,
|
||||
"opencode/gstack-plan-eng-review": 153275,
|
||||
"opencode/gstack-review": 142872,
|
||||
"opencode/gstack-plan-eng-review": 153274,
|
||||
"opencode/gstack-review": 142853,
|
||||
"slate/gstack-autoplan": 128994,
|
||||
"slate/gstack-design-consultation": 106996,
|
||||
"slate/gstack-design-review": 142272,
|
||||
"slate/gstack-design-review": 142267,
|
||||
"slate/gstack-land-and-deploy": 115462,
|
||||
"slate/gstack-plan-design-review": 138855,
|
||||
"slate/gstack-plan-devex-review": 134478,
|
||||
"slate/gstack-plan-eng-review": 151448,
|
||||
"slate/gstack-review": 140543
|
||||
"slate/gstack-plan-eng-review": 151447,
|
||||
"slate/gstack-review": 140524
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,9 @@ const SAMPLES: Array<[string, boolean]> = [
|
||||
['components.md', false],
|
||||
['public/Index.HTML', false], // bash globs are case-sensitive; the mirror must agree
|
||||
['src/App.TSX', false],
|
||||
['lib/widgets/bbtm_drawer/view/bbtm_drawer.dart', true],
|
||||
['packages/cache/lib/cache.dart', true],
|
||||
['test/ui/drawer_widget_test.dart', true],
|
||||
];
|
||||
|
||||
describe('isFrontendPath', () => {
|
||||
@@ -86,5 +89,5 @@ describe.skipIf(!POSIX)('parity with bin/gstack-diff-scope', () => {
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
}, 30_000); // one git commit + one bash run per sample: past bun's 5 s default
|
||||
});
|
||||
@@ -29,7 +29,7 @@ let IMPECCABLE_HOME: string;
|
||||
|
||||
function git(cwd: string, ...args: string[]) {
|
||||
const r = spawnSync('git', args, { cwd, encoding: 'utf-8', timeout: 30_000 });
|
||||
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: ${r.stderr}`);
|
||||
if (r.status !== 0) throw new Error(`git ${args.join(' ')} failed: status=${r.status} signal=${r.signal} error=${r.error?.message ?? ''} stderr=${r.stderr ?? ''}`);
|
||||
return r.stdout.trim();
|
||||
}
|
||||
|
||||
@@ -907,6 +907,10 @@ describe('scan', () => {
|
||||
fs.mkdirSync(path.join(REPO, 'styles'), { recursive: true });
|
||||
fs.writeFileSync(path.join(REPO, 'styles', 'new.css'), 'body { color: red }\n'); // untracked frontend
|
||||
fs.writeFileSync(path.join(REPO, 'notes.md'), 'x\n'); // untracked non-frontend
|
||||
// Value: protects=a .dart file (frontend scope since lib/frontend-scope.ts counts it) never reaches the web-source engine;
|
||||
// fails_when=the .dart exclusion in resolveTargets is dropped; why_new=no --changed case had a Dart file; seam=none;
|
||||
// no_claim=the real impeccable engine (fake engine logs argv)
|
||||
fs.writeFileSync(path.join(REPO, 'drawer.dart'), 'class Drawer {}\n'); // untracked Dart: frontend scope, not a scan target
|
||||
const log = path.join(SANDBOX, 'argv4.log');
|
||||
fs.rmSync(log, { force: true });
|
||||
try {
|
||||
@@ -918,6 +922,7 @@ describe('scan', () => {
|
||||
} finally {
|
||||
fs.rmSync(path.join(REPO, 'styles'), { recursive: true, force: true });
|
||||
fs.rmSync(path.join(REPO, 'notes.md'), { force: true });
|
||||
fs.rmSync(path.join(REPO, 'drawer.dart'), { force: true });
|
||||
git(REPO, 'checkout', '-q', 'main');
|
||||
git(REPO, 'branch', '-q', '-D', 'feature');
|
||||
}
|
||||
|
||||
@@ -240,3 +240,31 @@ describe('outside reviewer runtime discovery in fresh shells', () => {
|
||||
expect(fs.existsSync(path.join(f.repo, 'NEVER'))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// Value: protects=the adversarial DIFF_SIZE sums insertions+deletions across files and ignores binary rows;
|
||||
// fails_when=the numstat/awk sum reads the wrong field or a binary "-" row breaks it; why_new=no test ran the
|
||||
// size block; seam=none; no_claim=the size thresholds that consume DIFF_SIZE
|
||||
test('adversarial DIFF_SIZE counts changed lines from numstat', () => {
|
||||
const ctx: TemplateContext = { host: 'claude', skillName: 'ship', tmplPath: 'ship/SKILL.md.tmpl', paths: HOST_PATHS.claude };
|
||||
const fence = generateAdversarialStep(ctx).match(/```bash\n(DIFF_BASE=[\s\S]*?)\n```/);
|
||||
expect(fence).not.toBeNull();
|
||||
const repo = fs.mkdtempSync(path.join(TEMP, 'diff-size-'));
|
||||
const git = (...args: string[]) => {
|
||||
const r = spawnSync('git', ['-c', 'user.email=t@t', '-c', 'user.name=t', ...args], { cwd: repo, encoding: 'utf8', timeout: 30_000 });
|
||||
expect(r.status).toBe(0);
|
||||
};
|
||||
git('init', '-q', '-b', 'main');
|
||||
fs.writeFileSync(path.join(repo, 'a.txt'), 'one\ntwo\n');
|
||||
git('add', '-A');
|
||||
git('commit', '-qm', 'base');
|
||||
git('update-ref', 'refs/remotes/origin/main', 'HEAD');
|
||||
git('checkout', '-q', '-b', 'feature');
|
||||
fs.writeFileSync(path.join(repo, 'a.txt'), 'one\nTWO\n'); // 1 insertion + 1 deletion
|
||||
fs.writeFileSync(path.join(repo, 'b.txt'), 'x\ny\nz\n'); // 3 insertions
|
||||
fs.writeFileSync(path.join(repo, 'c.bin'), Buffer.from([0, 1, 2, 0])); // binary: "-\t-" in numstat
|
||||
git('add', '-A');
|
||||
git('commit', '-qm', 'change');
|
||||
const r = spawnSync('bash', ['-c', fence![1].replaceAll('<base>', 'main')], { cwd: repo, encoding: 'utf8', timeout: 30_000 });
|
||||
expect(r.stderr).toBe('');
|
||||
expect(r.stdout.trim()).toBe('DIFF_SIZE: 5');
|
||||
});
|
||||
@@ -20,21 +20,23 @@ import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
import { generateTestBootstrap } from '../scripts/resolvers/testing';
|
||||
import { generateTestBootstrap, generateTestCoverageAuditShip } from '../scripts/resolvers/testing';
|
||||
import { generateReviewArmy } from '../scripts/resolvers/review-army';
|
||||
import { HOST_PATHS } from '../scripts/resolvers/types';
|
||||
|
||||
const section = generateTestBootstrap({} as never);
|
||||
|
||||
/** The detection block the skill tells the agent to run (first bash fence). */
|
||||
function detectionScript(): string {
|
||||
const open = section.indexOf('```bash\n');
|
||||
function detectionScript(from: string = section): string {
|
||||
const open = from.indexOf('```bash\n');
|
||||
expect(open).toBeGreaterThan(-1);
|
||||
const start = open + '```bash\n'.length;
|
||||
const end = section.indexOf('```', start);
|
||||
return section.slice(start, end);
|
||||
const end = from.indexOf('```', start);
|
||||
return from.slice(start, end);
|
||||
}
|
||||
|
||||
/** Run the detection block in a throwaway git repo laid out by `files`. */
|
||||
function detect(files: Record<string, string>): string {
|
||||
function detect(files: Record<string, string>, script: string = detectionScript()): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ship-detect-'));
|
||||
try {
|
||||
for (const [rel, body] of Object.entries(files)) {
|
||||
@@ -42,15 +44,15 @@ function detect(files: Record<string, string>): string {
|
||||
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
||||
fs.writeFileSync(abs, body);
|
||||
}
|
||||
const script = path.join(dir, '.detect.sh');
|
||||
fs.writeFileSync(script, detectionScript());
|
||||
const scriptPath = path.join(dir, '.detect.sh');
|
||||
fs.writeFileSync(scriptPath, script);
|
||||
const git = (...args: string[]) => execFileSync('git', args, { timeout: 30_000, cwd: dir });
|
||||
git('init', '-q', '.');
|
||||
git('add', '-A');
|
||||
git('-c', 'user.email=t@t', '-c', 'user.name=t', 'commit', '-qm', 'fixture');
|
||||
// The block's last line is a `[ -f marker ] && echo`, so a clean project
|
||||
// exits 1 by design — read stdout, don't trust the status.
|
||||
return execFileSync('bash', [script], { timeout: 30_000, cwd: dir, encoding: 'utf-8' });
|
||||
return execFileSync('bash', [scriptPath], { timeout: 30_000, cwd: dir, encoding: 'utf-8' });
|
||||
} catch (err: unknown) {
|
||||
const e = err as { stdout?: string };
|
||||
if (typeof e.stdout === 'string') return e.stdout;
|
||||
@@ -93,6 +95,37 @@ describe('/ship Step 4 detection is multi-ecosystem', () => {
|
||||
expect(rust).toContain('TESTS:rust in-source');
|
||||
});
|
||||
|
||||
test('Dart and Flutter report their runtime, the Flutter SDK, and *_test.dart outside test/', () => {
|
||||
const flutter = detect({
|
||||
'pubspec.yaml': 'name: app\ndependencies:\n flutter:\n sdk: flutter\n',
|
||||
'lib/src/theme_test.dart': 'void main() {}\n',
|
||||
});
|
||||
expect(flutter).toContain('RUNTIME:dart');
|
||||
expect(flutter).toContain('FRAMEWORK:flutter');
|
||||
expect(testFileCount(flutter)).toBe(1);
|
||||
|
||||
// A commented-out Flutter dependency is not Flutter.
|
||||
const dart = detect({ 'pubspec.yaml': 'name: x\n# flutter:\n# sdk: flutter\n', 'lib/x.dart': 'int x = 1;\n' });
|
||||
expect(dart).toContain('RUNTIME:dart');
|
||||
expect(dart).not.toContain('FRAMEWORK:flutter');
|
||||
expect(testFileCount(dart)).toBe(0);
|
||||
|
||||
// dart_test.yaml can move discovery off *_test.dart (paths/filename), so it is test evidence on its own.
|
||||
const configured = detect({ 'pubspec.yaml': 'name: x\n', 'dart_test.yaml': 'paths: [checks]\n', 'checks/test_math.dart': 'void main() {}\n' });
|
||||
expect(configured).toContain('dart_test.yaml');
|
||||
});
|
||||
|
||||
// Value: protects=the Step 7 coverage audit sees a Dart project and its *_test.dart files, like bootstrap does;
|
||||
// fails_when=the coverage-audit block drops RUNTIME:dart or `dart` from its _test regex; why_new=only the
|
||||
// bootstrap block was run; seam=none; no_claim=none beyond protects
|
||||
test('the Step 7 coverage-audit detection block agrees with bootstrap on Dart', () => {
|
||||
const audit = generateTestCoverageAuditShip({ host: 'claude', skillName: 'ship', tmplPath: 'ship/SKILL.md.tmpl', paths: HOST_PATHS.claude } as never);
|
||||
const block = audit.slice(audit.indexOf('Test Framework Detection'));
|
||||
const out = detect({ 'pubspec.yaml': 'name: x\n', 'lib/src/theme_test.dart': 'void main() {}\n' }, detectionScript(block));
|
||||
expect(out).toContain('RUNTIME:dart');
|
||||
expect(testFileCount(out)).toBe(1);
|
||||
});
|
||||
|
||||
test('a genuinely untested project reports no test evidence', () => {
|
||||
const out = detect({ 'go.mod': 'module x\n', 'x.go': 'package x\n' });
|
||||
expect(out).toContain('RUNTIME:go');
|
||||
@@ -103,7 +136,7 @@ describe('/ship Step 4 detection is multi-ecosystem', () => {
|
||||
test('every ecosystem marker maps to a command to OFFER, not to run blind', () => {
|
||||
expect(section).toContain('never a command to run blind');
|
||||
for (const marker of ['manage.py', 'go.mod', 'Cargo.toml', 'pom.xml',
|
||||
'build.gradle', 'mix.exs', 'composer.json', 'Gemfile', 'pytest.ini']) {
|
||||
'build.gradle', 'mix.exs', 'composer.json', 'Gemfile', 'pytest.ini', 'pubspec.yaml']) {
|
||||
expect(section).toContain(marker);
|
||||
}
|
||||
// The ask-and-persist contract, not a hardcoded project command.
|
||||
@@ -111,3 +144,43 @@ describe('/ship Step 4 detection is multi-ecosystem', () => {
|
||||
expect(section).toContain('persist the answer to CLAUDE.md');
|
||||
});
|
||||
});
|
||||
|
||||
// Value: protects=review-army STACK/TEST_FW name Dart and tell flutter_test from dart test; fails_when=the pubspec.yaml lines
|
||||
// are dropped or the Flutter grep matches any `sdk:` (environment.sdk), a comment, or misses a quoted value; why_new=no free test runs the review-army
|
||||
// detection lines; seam=none; no_claim=specialist selection from these values (only the emitted lines run)
|
||||
describe('review-army stack detection names Dart and Flutter', () => {
|
||||
const block = generateReviewArmy({ host: 'claude', skillName: 'ship', tmplPath: 'ship/SKILL.md.tmpl', paths: HOST_PATHS.claude });
|
||||
const lines = (from: string, to: string): string => {
|
||||
const start = block.indexOf(from);
|
||||
const end = block.indexOf(to, start);
|
||||
expect(start).toBeGreaterThan(-1);
|
||||
expect(end).toBeGreaterThan(start);
|
||||
return block.slice(start, block.indexOf('\n', end));
|
||||
};
|
||||
const script = `${lines('STACK=""', 'echo "STACK:')}\n${lines('TEST_FW=""', 'echo "TEST_FW:')}\n`;
|
||||
|
||||
function stack(files: Record<string, string>): string {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'review-army-stack-'));
|
||||
try {
|
||||
for (const [rel, body] of Object.entries(files)) fs.writeFileSync(path.join(dir, rel), body);
|
||||
return execFileSync('bash', ['-c', script], { timeout: 30_000, cwd: dir, encoding: 'utf-8' });
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
test('Flutter app, pure Dart package, and a Dart SDK constraint that is not Flutter', () => {
|
||||
// Quoted `sdk: "flutter"` is the same YAML value.
|
||||
const flutter = stack({ 'pubspec.yaml': 'name: app\ndependencies:\n flutter:\n sdk: "flutter"\n' });
|
||||
expect(flutter).toContain('STACK: dart');
|
||||
expect(flutter).toContain('TEST_FW: flutter-test');
|
||||
|
||||
const dart = stack({ 'pubspec.yaml': 'name: x\nenvironment:\n sdk: ">=3.0.0 <4.0.0"\n# sdk: flutter\n' });
|
||||
expect(dart).toContain('STACK: dart');
|
||||
expect(dart).toContain('TEST_FW: dart-test');
|
||||
|
||||
const none = stack({ 'README.md': 'x\n' });
|
||||
expect(none).toContain('STACK: unknown');
|
||||
expect(none).toContain('TEST_FW: unknown');
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user