Merge pull request #963 from open-gsd/ci/audit-pipeline-fixes
ci(#962): pipeline audit fixes — scoped matrix, merged coverage gate, suite seeding, bug-* ratchet
This commit is contained in:
103
.github/workflows/test.yml
vendored
103
.github/workflows/test.yml
vendored
@@ -103,18 +103,13 @@ jobs:
|
||||
# lint scripts) require() the built modules — so build them explicitly.
|
||||
- name: Build runtime lib (required by lint scripts)
|
||||
run: npm run build:lib
|
||||
- name: Lint — ESLint (source-grep + timing + no-only-tests + quality)
|
||||
run: npx eslint . --cache --cache-location node_modules/.cache/eslint/
|
||||
- name: Lint — skill dependency graph
|
||||
run: npm run lint:skill-deps
|
||||
- name: Lint — test file count per module
|
||||
run: node scripts/lint-test-file-count.cjs
|
||||
- name: Lint — command contract (ADR-0002)
|
||||
run: node scripts/lint-command-contract.cjs
|
||||
- name: Lint — PR checks use projectDir
|
||||
run: node scripts/lint-pr-check-project-dir.cjs
|
||||
- name: Lint — legacy directory name guard (#604)
|
||||
run: npm run lint:legacy-name
|
||||
# Single orchestrated lint entry point (npm run lint:ci) so local and CI
|
||||
# always run the identical set. It composes `npm run lint`, keeping one
|
||||
# eslint invocation home; the --cache flag inside it is a no-op in CI
|
||||
# (node_modules/.cache is never restored) but still speeds local runs.
|
||||
# Each sub-lint prints its own banner, so a failure identifies itself.
|
||||
- name: Lint — all (ESLint, skill deps, test-file count, command contract, PR checks, legacy name, regression-test names)
|
||||
run: npm run lint:ci
|
||||
|
||||
test:
|
||||
name: test (${{ matrix.os }}, ${{ matrix.node-version }})
|
||||
@@ -196,9 +191,37 @@ jobs:
|
||||
if: matrix.scope != 'full'
|
||||
run: node scripts/run-tests.cjs --files-from .ci-selected-tests.txt
|
||||
|
||||
- name: Run unit tests
|
||||
# The unit suite runs ONCE here, under c8 with the coverage gate — the
|
||||
# former standalone `coverage` job duplicated this lane's entire unit
|
||||
# run (~4 min of runner time per PR) just to collect the same numbers.
|
||||
- name: Run unit tests (coverage gate ≥70% on gsd-core/bin/lib)
|
||||
if: matrix.scope == 'full'
|
||||
run: npm run test:unit
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
run: npm run test:coverage:unit
|
||||
|
||||
# Second-tier floor over the CI/release/lint tooling itself. Re-slices
|
||||
# the SAME V8 coverage data left in coverage/tmp by the run above — no
|
||||
# extra suite execution. Audit 2026-06: scripts/ measured 65.95%; the
|
||||
# 55% floor prevents a collapse to zero-coverage tooling while leaving
|
||||
# headroom for variance. The threshold lives in package.json next to
|
||||
# the 70% lib gate — raise deliberately, never lower.
|
||||
- name: Coverage floor — scripts/ tooling (≥55%)
|
||||
if: matrix.scope == 'full'
|
||||
run: npm run test:coverage:scripts-floor
|
||||
|
||||
- name: Upload coverage artifact
|
||||
if: always() && matrix.scope == 'full'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: coverage-unit
|
||||
# coverage/tmp holds raw per-process V8 dumps (>1 GB for the full
|
||||
# unit suite) — exclude it; the rendered reports are the artifact.
|
||||
path: |
|
||||
coverage/
|
||||
!coverage/tmp
|
||||
.nyc_output/
|
||||
if-no-files-found: ignore
|
||||
|
||||
- name: Run integration tests
|
||||
if: matrix.scope == 'full'
|
||||
@@ -333,49 +356,6 @@ jobs:
|
||||
- name: Run security tests
|
||||
run: npm run test:security
|
||||
|
||||
coverage:
|
||||
needs: changes
|
||||
if: needs.changes.outputs.product_changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
env:
|
||||
GSD_PLUGIN_ROOT: .ci-gsd-plugin-root-disabled
|
||||
steps:
|
||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: true
|
||||
token: ${{ github.token }}
|
||||
- name: Guard — require GitHub-hosted runner
|
||||
run: node scripts/ci-guard-runner.cjs
|
||||
- name: Rebase check — merge PR base branch into PR head
|
||||
if: github.event_name == 'pull_request'
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
run: node scripts/ci-rebase-check.cjs
|
||||
- name: Set up Node.js 24
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: 24
|
||||
cache: 'npm'
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Dependency integrity gate
|
||||
run: node scripts/check-npm-integrity.cjs
|
||||
- name: Unit coverage
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=6144
|
||||
run: npm run test:coverage:unit
|
||||
- name: Upload coverage artifact
|
||||
if: always()
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: coverage-unit
|
||||
path: |
|
||||
coverage/
|
||||
.nyc_output/
|
||||
if-no-files-found: ignore
|
||||
|
||||
required-tests:
|
||||
name: Required tests
|
||||
needs:
|
||||
@@ -384,7 +364,6 @@ jobs:
|
||||
- test
|
||||
- test-inert
|
||||
- test-full
|
||||
- coverage
|
||||
if: always()
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 1
|
||||
@@ -398,7 +377,6 @@ jobs:
|
||||
TEST_RESULT: ${{ needs.test.result }}
|
||||
INERT_RESULT: ${{ needs.test-inert.result }}
|
||||
FULL_TEST_RESULT: ${{ needs.test-full.result }}
|
||||
COVERAGE_RESULT: ${{ needs.coverage.result }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "code_changed=$CODE_CHANGED"
|
||||
@@ -408,7 +386,6 @@ jobs:
|
||||
echo "test=$TEST_RESULT"
|
||||
echo "test-inert=$INERT_RESULT"
|
||||
echo "test-full=$FULL_TEST_RESULT"
|
||||
echo "coverage=$COVERAGE_RESULT"
|
||||
|
||||
if [ "$CHANGES_RESULT" != "success" ]; then
|
||||
echo "::error::test scope detection did not pass"
|
||||
@@ -430,14 +407,12 @@ jobs:
|
||||
echo "::error::test matrix did not pass"
|
||||
exit 1
|
||||
fi
|
||||
# The coverage gates (lib 70% + scripts/ 55% floor) run inside the
|
||||
# ubuntu/24 full lane of the `test` matrix, so TEST_RESULT covers them.
|
||||
if [ "$FULL_TEST_RESULT" != "success" ] && [ "$FULL_TEST_RESULT" != "skipped" ]; then
|
||||
echo "::error::full parity matrix did not pass"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$COVERAGE_RESULT" != "success" ]; then
|
||||
echo "::error::coverage did not pass"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if [ "$INERT_RESULT" != "success" ]; then
|
||||
echo "::error::inert CI lane did not pass"
|
||||
|
||||
@@ -521,7 +521,7 @@ The canonical lint infrastructure adopted in ADR 452 (`docs/adr/452-eslint-lint-
|
||||
`DEFECT.SUPERSEDED-CONCURRENT-PRS.fix-forward=close superseded PRs via gh api PATCH state=closed; do not comment on self-authored PRs (k101); the link to the merged PR makes supersession discoverable in PR history`
|
||||
|
||||
`DEFECT.PROMPT-INJECTION-SCAN-COLLISION.symptom=custom XML element name in agent .md file matches scripts/scan-prompt-injection regex; legitimate agent vocabulary trips the security gate`
|
||||
`DEFECT.PROMPT-INJECTION-SCAN-COLLISION.examples=#3309 added a bare 'human' element (angle-bracket-wrapped) for verify-block harvesting; tests/prompt-injection-scan.test.cjs flags angle-bracket-wrapped names matching system|assistant|human (open or close form)`
|
||||
`DEFECT.PROMPT-INJECTION-SCAN-COLLISION.examples=#3309 added a bare 'human' element (angle-bracket-wrapped) for verify-block harvesting; tests/prompt-injection-scan.security.test.cjs flags angle-bracket-wrapped names matching system|assistant|human (open or close form)`
|
||||
`DEFECT.PROMPT-INJECTION-SCAN-COLLISION.detect=any new bare <system|assistant|human|user> tag in agents/*.md`
|
||||
`DEFECT.PROMPT-INJECTION-SCAN-COLLISION.fix-forward=hyphenate the tag (<human-check>, <assistant-prompt>) — scanner regex matches bare names only`
|
||||
|
||||
|
||||
@@ -1293,7 +1293,7 @@ PreToolUse hook that scans Write/Edit calls targeting `.planning/` for injection
|
||||
**3. Workflow Guard Hook** (`gsd-workflow-guard.js`)
|
||||
PreToolUse hook that detects when Claude attempts file edits outside a GSD workflow context. Advises using `/gsd-quick` or `/gsd-fast` instead of direct edits. Configurable via `hooks.workflow_guard` (default: false).
|
||||
|
||||
**4. CI-Ready Injection Scanner** (`prompt-injection-scan.test.cjs`)
|
||||
**4. CI-Ready Injection Scanner** (`prompt-injection-scan.security.test.cjs`)
|
||||
Test suite that scans all agent, workflow, and command files for embedded injection vectors.
|
||||
|
||||
**Requirements:**
|
||||
|
||||
@@ -29,6 +29,29 @@ Examples:
|
||||
|
||||
The suite-suffix convention was chosen over a directory layout (`tests/security/`) so the 545+ existing test files don't need to move. Existing files all classify as `unit` until someone explicitly retags them.
|
||||
|
||||
## Regression tests
|
||||
|
||||
**Do not create new top-level `tests/bug-NNNN-*.test.cjs` files.** Add the
|
||||
regression case to the owning module's main test file instead (e.g. a
|
||||
`describe('regressions')` block in `tests/<module>.test.cjs`).
|
||||
|
||||
`node --test` spawns one child process per FILE, so file count — not test
|
||||
count — is the unit of CI overhead, and it is worst on Windows lanes where
|
||||
every spawn is Defender-scanned. The 2026-06 CI audit found 244 one-off
|
||||
`bug-*` files (~38% of the suite). That population is grandfathered in
|
||||
`scripts/lint-regression-test-names.allowlist.json` and enforced by an
|
||||
identity ratchet (`npm run lint:regression-names`, part of `npm run lint:ci`):
|
||||
|
||||
- A **new** `bug-*` file fails CI — fold it into the owning module's file.
|
||||
- **Deleting/consolidating** a grandfathered file requires pruning its
|
||||
allowlist entry, so the baseline only ever shrinks.
|
||||
|
||||
The ratchet deliberately covers only `bug-*`. Files named `feat-NNNN-*` /
|
||||
`enh-NNNN-*` are *feature* test files — one (or one per suite) per feature is
|
||||
the sanctioned layout (see the #443 strategy below), not a one-off regression
|
||||
pattern. If `issue-*`/`perf-*` one-offs start accumulating the same way
|
||||
`bug-*` did, extend the ratchet's regex and regenerate the allowlist.
|
||||
|
||||
## Running suites locally
|
||||
|
||||
```bash
|
||||
@@ -53,6 +76,12 @@ node scripts/run-tests.cjs --files "tests/command-contract.test.cjs tests/core.t
|
||||
node scripts/run-tests.cjs --files-from .ci-selected-tests.txt
|
||||
```
|
||||
|
||||
`npm run test:affected` (scripts/run-affected-tests.cjs) is a **local-only**
|
||||
convenience that selects tests via the `require()` dependency graph of your
|
||||
working-tree diff. CI does not use it — CI selection is the rule table in
|
||||
`scripts/ci-test-scope.cjs`, which is the authoritative mapping. If the two
|
||||
disagree, trust (and fix) the rule table.
|
||||
|
||||
Unknown suites exit non-zero with the list of valid suites. Empty suites (e.g. `--suite security` before any security-tagged file exists) exit `0` with a `no tests in suite "..."` notice on stderr so CI lanes don't go red while a suite is being populated.
|
||||
|
||||
## CI matrix
|
||||
@@ -72,14 +101,30 @@ The `Tests` workflow runs every PR through a scoped gate generated by
|
||||
smoke set. They are for confidence on the affected surface, not for counting
|
||||
tests.
|
||||
|
||||
The default PR gate runs the broad `unit`, `integration`, and `security` suites
|
||||
once on Ubuntu / Node 24, scoped smoke on Ubuntu / Node 22, scoped
|
||||
Windows/path/shell tests on Windows / Node 24, and unit coverage once on Ubuntu /
|
||||
Node 24. PRs touching workflow, package, test-runner, install, release, or
|
||||
The default PR gate runs the broad `unit` (under the c8 coverage gate),
|
||||
`integration`, and `security` suites once on Ubuntu / Node 24, scoped tests on
|
||||
Ubuntu / Node 22, and scoped tests on Windows / Node 24. "Scoped" means the
|
||||
diff-selected list from the rule table — not the full suite and not a fixed
|
||||
smoke set (the fixed smoke list is only the empty-selection fallback). The
|
||||
Windows lane's list is the Windows-sensitive subset of the selection, plus
|
||||
**every changed test file, unconditionally** (the #494 invariant, narrowed): a
|
||||
modified test is exercised on the divergent OS before merge at per-file cost,
|
||||
without paying for the three full parity lanes.
|
||||
|
||||
PRs touching workflow, package, test-runner, install, release, or
|
||||
Windows-sensitive surfaces also run the full parity matrix on macOS and the
|
||||
older Windows runtime, plus `install` and `slow` on the primary Ubuntu lane.
|
||||
Coverage stays single-lane because multiplying coverage across OS/runtime lanes
|
||||
adds cost without improving the threshold signal.
|
||||
Everything (including the full parity matrix) runs on every push to `next`,
|
||||
which covers the residual macOS / Windows-Node-22 cross-product for scoped PRs.
|
||||
|
||||
Coverage runs inside the Ubuntu / Node 24 full lane (not a separate job — that
|
||||
duplicated the entire unit run) and stays single-lane because multiplying
|
||||
coverage across OS/runtime lanes adds cost without improving the threshold
|
||||
signal. Note the gate's deliberate blind spot: it measures
|
||||
`gsd-core/bin/lib/*.cjs` only — `scripts/`, `hooks/`, and `bin/` are
|
||||
unenforced, and `stryker.config.mjs` additionally excludes ~48% of lib lines
|
||||
from mutation testing (see the UNMUTATED list there). Widening either gate is
|
||||
tracked work, not an accident to "fix" silently by raising thresholds.
|
||||
|
||||
To inspect the scope locally:
|
||||
|
||||
|
||||
@@ -386,7 +386,7 @@ GSD generates markdown files that become LLM system prompts. This means any user
|
||||
- `gsd-prompt-guard.js` — Scans Write/Edit calls to `.planning/` for injection patterns (always active, advisory-only)
|
||||
- `gsd-workflow-guard.js` — Warns on file edits outside GSD workflow context (opt-in via `hooks.workflow_guard`)
|
||||
|
||||
**CI Scanner:** `prompt-injection-scan.test.cjs` scans all agent, workflow, and command files for embedded injection vectors.
|
||||
**CI Scanner:** `prompt-injection-scan.security.test.cjs` scans all agent, workflow, and command files for embedded injection vectors.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ injected instructions in untrusted content — catching cases where an attacker
|
||||
has embedded instructions in a file that GSD is about to incorporate into an
|
||||
agent's context.
|
||||
|
||||
**CI scanner.** `prompt-injection-scan.test.cjs` scans all agent, workflow,
|
||||
**CI scanner.** `prompt-injection-scan.security.test.cjs` scans all agent, workflow,
|
||||
and command files for embedded injection vectors as part of the test suite.
|
||||
This catches injection attempts in the GSD source itself — for example, a
|
||||
supply-chain attack that modified a workflow file to add a role-override
|
||||
|
||||
@@ -1227,7 +1227,7 @@ fix(03-01): correct auth token expiry
|
||||
**3. ワークフローガードフック**(`gsd-workflow-guard.js`)
|
||||
Claude が GSD ワークフローコンテキスト外でファイル編集を試行した際に検出する PreToolUse フック。直接編集の代わりに `/gsd-quick` や `/gsd-fast` の使用をアドバイスします。`hooks.workflow_guard`(デフォルト: false)で設定可能です。
|
||||
|
||||
**4. CI 対応インジェクションスキャナー**(`prompt-injection-scan.test.cjs`)
|
||||
**4. CI 対応インジェクションスキャナー**(`prompt-injection-scan.security.test.cjs`)
|
||||
すべてのエージェント、ワークフロー、コマンドファイルに埋め込まれたインジェクションベクターをスキャンするテストスイート。
|
||||
|
||||
**要件:**
|
||||
|
||||
@@ -369,7 +369,7 @@ GSD は LLM のシステムプロンプトになるマークダウンファイ
|
||||
- `gsd-prompt-guard.js` — `.planning/` への Write/Edit 呼び出しでインジェクションパターンをスキャンする(常時有効、アドバイザリーのみ)
|
||||
- `gsd-workflow-guard.js` — GSD ワークフローコンテキスト外でのファイル編集を警告する(`hooks.workflow_guard` 経由でオプトイン)
|
||||
|
||||
**CI スキャナー:** `prompt-injection-scan.test.cjs` はすべてのエージェント、ワークフロー、コマンドファイルに埋め込まれたインジェクションベクターをスキャンします。
|
||||
**CI スキャナー:** `prompt-injection-scan.security.test.cjs` はすべてのエージェント、ワークフロー、コマンドファイルに埋め込まれたインジェクションベクターをスキャンします。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ GSD Core はプロンプトインジェクションを 3 つのレベルで対
|
||||
|
||||
**ランタイムフック:`gsd-read-injection-scanner.js`。** このフックはすべての Read ツール呼び出しの出力で発火します。GSD がエージェントのコンテキストに組み込もうとしているファイルの *読み取ったばかりのコンテンツ* をスキャンし、攻撃者が命令を埋め込んでいるケースをキャッチします。
|
||||
|
||||
**CI スキャナー。** `prompt-injection-scan.test.cjs` はテストスイートの一部として、すべてのエージェント、ワークフロー、コマンドファイルに埋め込まれたインジェクションベクターをスキャンします。これは GSD ソース自体でのインジェクション試みをキャッチします——たとえば、ワークフローファイルにロールオーバーライド命令を追加するよう変更したサプライチェーン攻撃。
|
||||
**CI スキャナー。** `prompt-injection-scan.security.test.cjs` はテストスイートの一部として、すべてのエージェント、ワークフロー、コマンドファイルに埋め込まれたインジェクションベクターをスキャンします。これは GSD ソース自体でのインジェクション試みをキャッチします——たとえば、ワークフローファイルにロールオーバーライド命令を追加するよう変更したサプライチェーン攻撃。
|
||||
|
||||
### Read Injection Scanner vs Prompt Guard
|
||||
|
||||
|
||||
@@ -1131,7 +1131,7 @@ fix(03-01): correct auth token expiry
|
||||
**3. 워크플로우 가드 훅** (`gsd-workflow-guard.js`)
|
||||
Claude가 GSD 워크플로우 컨텍스트 밖에서 파일 편집을 시도하는 것을 감지하는 PreToolUse 훅입니다. 직접 편집 대신 `/gsd-quick` 또는 `/gsd-fast` 사용을 권고합니다. `hooks.workflow_guard`로 구성 가능합니다(기본값: false).
|
||||
|
||||
**4. CI 준비 주입 스캐너** (`prompt-injection-scan.test.cjs`)
|
||||
**4. CI 준비 주입 스캐너** (`prompt-injection-scan.security.test.cjs`)
|
||||
모든 에이전트, 워크플로우, 명령어 파일에서 포함된 주입 벡터를 스캔하는 테스트 스위트입니다.
|
||||
|
||||
**요구사항.**
|
||||
|
||||
@@ -369,7 +369,7 @@ GSD는 LLM 시스템 프롬프트가 되는 마크다운 파일을 생성합니
|
||||
- `gsd-prompt-guard.js` — `.planning/`에 대한 Write/Edit 호출에서 인젝션 패턴 스캔 (항상 활성, 자문 전용)
|
||||
- `gsd-workflow-guard.js` — GSD 워크플로우 컨텍스트 외부에서 파일 편집 시 경고 (`hooks.workflow_guard`를 통한 옵트인)
|
||||
|
||||
**CI 스캐너:** `prompt-injection-scan.test.cjs`는 모든 에이전트, 워크플로우, 명령어 파일에서 삽입된 인젝션 벡터를 스캔합니다.
|
||||
**CI 스캐너:** `prompt-injection-scan.security.test.cjs`는 모든 에이전트, 워크플로우, 명령어 파일에서 삽입된 인젝션 벡터를 스캔합니다.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -79,7 +79,7 @@ GSD Core는 세 가지 수준에서 프롬프트 인젝션을 다룬다.
|
||||
|
||||
**런타임 훅: `gsd-read-injection-scanner.js`.** 이 훅은 모든 Read 도구 호출의 출력에서 실행된다. 방금 읽은 *콘텐츠*를 신뢰할 수 없는 콘텐츠의 주입된 지시 사항으로 스캔한다 — 공격자가 GSD가 에이전트 컨텍스트에 통합하려는 파일에 지시 사항을 내장한 경우를 잡아낸다.
|
||||
|
||||
**CI 스캐너.** `prompt-injection-scan.test.cjs`는 테스트 스위트의 일부로 내장된 인젝션 벡터가 있는지 모든 에이전트, 워크플로우, 명령 파일을 스캔한다. 이는 GSD 소스 자체의 인젝션 시도를 잡아낸다 — 예를 들어 워크플로우 파일을 수정하여 역할 재정의 지시 사항을 추가하는 공급망 공격.
|
||||
**CI 스캐너.** `prompt-injection-scan.security.test.cjs`는 테스트 스위트의 일부로 내장된 인젝션 벡터가 있는지 모든 에이전트, 워크플로우, 명령 파일을 스캔한다. 이는 GSD 소스 자체의 인젝션 시도를 잡아낸다 — 예를 들어 워크플로우 파일을 수정하여 역할 재정의 지시 사항을 추가하는 공급망 공격.
|
||||
|
||||
### 읽기 인젝션 스캐너 vs 프롬프트 가드
|
||||
|
||||
|
||||
@@ -369,7 +369,7 @@ O GSD gera arquivos markdown que se tornam prompts de sistema de LLM. Isso signi
|
||||
- `gsd-prompt-guard.js` — Verifica chamadas Write/Edit para `.planning/` em busca de padrões de injeção (sempre ativo, somente consultivo)
|
||||
- `gsd-workflow-guard.js` — Avisa sobre edições de arquivos fora do contexto do workflow GSD (opt-in via `hooks.workflow_guard`)
|
||||
|
||||
**Scanner de CI:** `prompt-injection-scan.test.cjs` verifica todos os arquivos de agentes, workflows e comandos em busca de vetores de injeção incorporados.
|
||||
**Scanner de CI:** `prompt-injection-scan.security.test.cjs` verifica todos os arquivos de agentes, workflows e comandos em busca de vetores de injeção incorporados.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -168,7 +168,7 @@ de ser lido* em busca de instruções injetadas em conteúdo não confiável —
|
||||
capturando casos em que um atacante incorporou instruções em um arquivo que o
|
||||
GSD está prestes a incorporar ao contexto de um agente.
|
||||
|
||||
**Scanner de CI.** `prompt-injection-scan.test.cjs` escaneia todos os arquivos
|
||||
**Scanner de CI.** `prompt-injection-scan.security.test.cjs` escaneia todos os arquivos
|
||||
de agente, workflow e comando em busca de vetores de injeção embutidos como
|
||||
parte do conjunto de testes. Isso detecta tentativas de injeção no próprio
|
||||
código-fonte do GSD — por exemplo, um ataque de cadeia de suprimentos que
|
||||
|
||||
@@ -1244,7 +1244,7 @@ PreToolUse 钩子,扫描针对 `.planning/` 的 Write/Edit 调用中的注入
|
||||
**3. 工作流守护钩子**(`gsd-workflow-guard.js`)
|
||||
PreToolUse 钩子,检测 Claude 在 GSD 工作流上下文之外尝试文件编辑的情况。建议使用 `/gsd-quick` 或 `/gsd-fast` 替代直接编辑。可通过 `hooks.workflow_guard` 配置(默认:false)。
|
||||
|
||||
**4. CI 就绪注入扫描器**(`prompt-injection-scan.test.cjs`)
|
||||
**4. CI 就绪注入扫描器**(`prompt-injection-scan.security.test.cjs`)
|
||||
扫描所有智能体、工作流和命令文件中嵌入注入向量的测试套件。
|
||||
|
||||
**需求:**
|
||||
|
||||
@@ -368,7 +368,7 @@ GSD 生成的 Markdown 文件会成为 LLM 系统提示。这意味着流入规
|
||||
- `gsd-prompt-guard.js` — 扫描写入 `.planning/` 的 Write/Edit 调用中的注入模式(始终活跃,仅建议)
|
||||
- `gsd-workflow-guard.js` — 对 GSD 工作流上下文之外的文件编辑发出警告(通过 `hooks.workflow_guard` 选择性启用)
|
||||
|
||||
**CI 扫描器:** `prompt-injection-scan.test.cjs` 扫描所有 agent、工作流和命令文件中的嵌入式注入向量。
|
||||
**CI 扫描器:** `prompt-injection-scan.security.test.cjs` 扫描所有 agent、工作流和命令文件中的嵌入式注入向量。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -73,7 +73,7 @@ GSD Core 在三个层面应对提示注入。
|
||||
|
||||
**运行时钩子:`gsd-read-injection-scanner.js`。** 该钩子在每次 Read 工具调用的输出时触发。它扫描*刚刚读取的内容*中在不可信内容中注入的指令——捕获攻击者在 GSD 即将纳入代理上下文的文件中嵌入指令的情况。
|
||||
|
||||
**CI 扫描器。** `prompt-injection-scan.test.cjs` 作为测试套件的一部分,扫描所有代理、工作流和命令文件中嵌入的注入向量。这能捕获 GSD 源代码本身的注入尝试——例如,修改工作流文件以添加角色覆盖指令的供应链攻击。
|
||||
**CI 扫描器。** `prompt-injection-scan.security.test.cjs` 作为测试套件的一部分,扫描所有代理、工作流和命令文件中嵌入的注入向量。这能捕获 GSD 源代码本身的注入尝试——例如,修改工作流文件以添加角色覆盖指令的供应链攻击。
|
||||
|
||||
### 读取注入扫描器与提示守卫的对比
|
||||
|
||||
|
||||
@@ -91,6 +91,8 @@
|
||||
"pretest:coverage": "npm run build:lib && npm run lint:skill-deps",
|
||||
"lint": "eslint . --cache --cache-location node_modules/.cache/eslint/",
|
||||
"lint:fix": "eslint . --fix",
|
||||
"lint:ci": "npm run lint && npm run lint:skill-deps && node scripts/lint-test-file-count.cjs && node scripts/lint-command-contract.cjs && node scripts/lint-pr-check-project-dir.cjs && npm run lint:legacy-name && node scripts/lint-regression-test-names.cjs",
|
||||
"lint:regression-names": "node scripts/lint-regression-test-names.cjs",
|
||||
"lint:descriptions": "node scripts/lint-descriptions.cjs",
|
||||
"lint:skill-deps": "node scripts/lint-skill-deps.cjs",
|
||||
"lint:test-file-count": "node scripts/lint-test-file-count.cjs",
|
||||
@@ -109,6 +111,7 @@
|
||||
"test:slow": "node scripts/run-tests.cjs --suite slow",
|
||||
"test:affected": "node scripts/run-affected-tests.cjs",
|
||||
"test:coverage": "c8 --check-coverage --lines 70 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs",
|
||||
"test:coverage:scripts-floor": "c8 check-coverage --lines 55 --include 'scripts/**/*.cjs' --exclude 'tests/**' --all",
|
||||
"test:coverage:unit": "c8 --check-coverage --lines 70 --reporter text --include 'gsd-core/bin/lib/*.cjs' --exclude 'tests/**' --all node scripts/run-tests.cjs --suite unit",
|
||||
"test:coverage:all": "npm run test:coverage",
|
||||
"test:mutation": "stryker run",
|
||||
|
||||
@@ -156,7 +156,7 @@ should_skip_file() {
|
||||
# Skip the scan scripts themselves and test files
|
||||
case "$file" in
|
||||
*/base64-scan.sh) return 0 ;;
|
||||
*/security-scan.test.cjs) return 0 ;;
|
||||
*/security-scan.security.test.cjs) return 0 ;;
|
||||
esac
|
||||
# Skip scanner fixture directories — they contain deliberate injection samples
|
||||
case "$file" in
|
||||
|
||||
@@ -169,11 +169,11 @@ const RULES = [
|
||||
path.includes('prompt-injection-scan') ||
|
||||
path.startsWith('tests/fixtures/adversarial/security/'),
|
||||
tests: [
|
||||
'tests/secret-scan-lint.test.cjs',
|
||||
'tests/prompt-injection-scan.test.cjs',
|
||||
'tests/security-prompt-injection.test.cjs',
|
||||
'tests/read-injection-scanner.test.cjs',
|
||||
'tests/security-scan.test.cjs',
|
||||
'tests/secret-scan-lint.security.test.cjs',
|
||||
'tests/prompt-injection-scan.security.test.cjs',
|
||||
'tests/security-prompt-injection.security.test.cjs',
|
||||
'tests/read-injection-scanner.security.test.cjs',
|
||||
'tests/security-scan.security.test.cjs',
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -308,7 +308,13 @@ function addAll(set, values) {
|
||||
for (const value of values) set.add(value);
|
||||
}
|
||||
|
||||
const WINDOWS_HINTS = ['windows', 'path', 'shell', 'workflow', 'install', 'hook'];
|
||||
// Windows-sensitive filename hints — deliberately narrow. 'workflow',
|
||||
// 'install', and 'hook' were dropped from this list: workflow-lint tests are
|
||||
// platform-independent YAML/policy checks, and the installer/hooks RULES set
|
||||
// fullMatrix=true, so the full Windows lane already runs when those paths
|
||||
// change. The old six-hint list pulled 102 of ~633 test files into the scoped
|
||||
// windows lane, turning it into a ~10-minute job on every PR.
|
||||
const WINDOWS_HINTS = ['windows', 'win32', 'shell', 'path'];
|
||||
const isWindowsHint = s => WINDOWS_HINTS.some(k => s.toLowerCase().includes(k));
|
||||
|
||||
function classify(files) {
|
||||
@@ -343,10 +349,15 @@ function classify(files) {
|
||||
|
||||
if (file.startsWith('tests/') && file.endsWith('.test.cjs')) {
|
||||
targeted.add(file);
|
||||
fullMatrix = true;
|
||||
if (isWindowsHint(file)) {
|
||||
windows.add(file);
|
||||
}
|
||||
// #494 invariant, narrowed: a changed test must still be exercised on
|
||||
// the divergent OS before merge, but at per-file cost — it ALWAYS joins
|
||||
// the scoped windows lane instead of triggering the three full parity
|
||||
// lanes. (full_matrix fired on 15/15 sampled PRs because test-driven
|
||||
// PRs always touch tests/, costing ~25 runner-minutes each.) Changed
|
||||
// tests already run on ubuntu-22 and ubuntu-24 via targeted_tests; the
|
||||
// residual macOS / windows-node-22 cross-product is covered by the full
|
||||
// matrix on every push to next.
|
||||
windows.add(file);
|
||||
}
|
||||
|
||||
for (const rule of RULES) {
|
||||
|
||||
259
scripts/lint-regression-test-names.allowlist.json
Normal file
259
scripts/lint-regression-test-names.allowlist.json
Normal file
@@ -0,0 +1,259 @@
|
||||
[
|
||||
"bug-10-semver-policy-consolidation.test.cjs",
|
||||
"bug-130-finishinstall-opencode-testmode.test.cjs",
|
||||
"bug-131-release-tarball-smoke-explicit-home.test.cjs",
|
||||
"bug-14-progress-auto-flag-dropped.test.cjs",
|
||||
"bug-167-query-meta-command.test.cjs",
|
||||
"bug-17-askuserquestion-option-cap.test.cjs",
|
||||
"bug-170-workflow-fallback-install-hint.test.cjs",
|
||||
"bug-1736-local-install-commands.test.cjs",
|
||||
"bug-1754-js-hook-guard.test.cjs",
|
||||
"bug-1817-sh-hook-guard.test.cjs",
|
||||
"bug-1818-unknown-flags.test.cjs",
|
||||
"bug-1826-phases-clear-confirm.test.cjs",
|
||||
"bug-1829-inherit-model-profile.test.cjs",
|
||||
"bug-1834-sh-hooks-installed.test.cjs",
|
||||
"bug-1891-file-resolution.test.cjs",
|
||||
"bug-190-bridge-collapse.test.cjs",
|
||||
"bug-1906-hook-relative-paths.test.cjs",
|
||||
"bug-1908-uninstall-manifest.test.cjs",
|
||||
"bug-1924-preserve-user-artifacts.test.cjs",
|
||||
"bug-1967-cache-invalidation.test.cjs",
|
||||
"bug-1974-context-exhaustion-record.test.cjs",
|
||||
"bug-2002-offer-next-context.test.cjs",
|
||||
"bug-2004-pr-branch-milestone.test.cjs",
|
||||
"bug-21-state-md-template-frontmatter.test.cjs",
|
||||
"bug-211-launcher-home-fallback.test.cjs",
|
||||
"bug-2136-sh-hook-version.test.cjs",
|
||||
"bug-214-phase-researcher-write-truncation-contract.test.cjs",
|
||||
"bug-214-writer-agents-write-truncation-contract.test.cjs",
|
||||
"bug-222-research-synthesizer-write-contract.test.cjs",
|
||||
"bug-224-pick-stdout-capture.test.cjs",
|
||||
"bug-2248-local-install-statusline.test.cjs",
|
||||
"bug-2256-model-overrides-transport.test.cjs",
|
||||
"bug-2268-parallel-discuss.test.cjs",
|
||||
"bug-2344-read-guard-claudecode-env.test.cjs",
|
||||
"bug-2346-agent-read-loop-guards.test.cjs",
|
||||
"bug-2351-intel-kilo-layout.test.cjs",
|
||||
"bug-2376-opencode-windows-home-path.test.cjs",
|
||||
"bug-2384-post-merge-deletion-audit.test.cjs",
|
||||
"bug-2388-plan-phase-no-branch-rename.test.cjs",
|
||||
"bug-2396-makefile-test-priority.test.cjs",
|
||||
"bug-2399-commit-docs-plan-phase.test.cjs",
|
||||
"bug-2410-stream-checkpoint-heartbeats.test.cjs",
|
||||
"bug-2418-antigravity-bare-path.test.cjs",
|
||||
"bug-2419-project-researcher-agent.test.cjs",
|
||||
"bug-2421-planner-grep-gate-hygiene.test.cjs",
|
||||
"bug-2424-reapply-patches-baseline-detection.test.cjs",
|
||||
"bug-2432-quick-plan-predispatch-commit.test.cjs",
|
||||
"bug-2451-context-monitor-over-report.test.cjs",
|
||||
"bug-2470-update-md-claude-path.test.cjs",
|
||||
"bug-2492-context-coverage-gate.test.cjs",
|
||||
"bug-2501-resurrection-detection.test.cjs",
|
||||
"bug-2502-insert-phase-state-update.test.cjs",
|
||||
"bug-2504-uat-foundation-phases.test.cjs",
|
||||
"bug-2506-settings-profile-nonclaude-warning.test.cjs",
|
||||
"bug-2516-inherit-model-execute-phase.test.cjs",
|
||||
"bug-2520-read-guard-hook-subprocess-env.test.cjs",
|
||||
"bug-2523-quick-deferred-items.test.cjs",
|
||||
"bug-2530-valid-config-keys.test.cjs",
|
||||
"bug-2543-gsd-slash-namespace.test.cjs",
|
||||
"bug-2545-copilot-unreplaced-paths.test.cjs",
|
||||
"bug-2549-2550-2552-discuss-phase-context.test.cjs",
|
||||
"bug-2554-decimal-phase-filter.test.cjs",
|
||||
"bug-2557-gemini-local-hook-paths.test.cjs",
|
||||
"bug-2559-stale-search-year.test.cjs",
|
||||
"bug-260-worktree-path-guard.test.cjs",
|
||||
"bug-2601-inherit-model-profile.test.cjs",
|
||||
"bug-261-worktree-force-add-guard.test.cjs",
|
||||
"bug-2630-state-frontmatter-milestone-switch.test.cjs",
|
||||
"bug-2638-sub-repos-canonical-location.test.cjs",
|
||||
"bug-2643-skill-frontmatter-name.test.cjs",
|
||||
"bug-2659-audit-open-crash.test.cjs",
|
||||
"bug-2660-one-liner-extraction.test.cjs",
|
||||
"bug-2661-roadmap-sync-parallel.test.cjs",
|
||||
"bug-2686-review-fix-worktree.test.cjs",
|
||||
"bug-2698-crlf-install.test.cjs",
|
||||
"bug-2760-codex-install-defensive.test.cjs",
|
||||
"bug-2769-requirements-header-variants.test.cjs",
|
||||
"bug-2770-annotate-deps-int-coerce.test.cjs",
|
||||
"bug-2771-user-profile-manifest.test.cjs",
|
||||
"bug-2772-gitmodules-path-intersection.test.cjs",
|
||||
"bug-2784-update-cache-clear-path.test.cjs",
|
||||
"bug-279-codex-agent-mapping.test.cjs",
|
||||
"bug-2794-opencode-model-profile-overrides.test.cjs",
|
||||
"bug-2798-context-window-config-key.test.cjs",
|
||||
"bug-2801-ingest-docs-handler.test.cjs",
|
||||
"bug-2808-skill-hyphen-name.test.cjs",
|
||||
"bug-2831-opencode-home-path-prefix.test.cjs",
|
||||
"bug-2836-audit-open-summary-uat-drift.test.cjs",
|
||||
"bug-2838-summary-rescue-gitignored-planning.test.cjs",
|
||||
"bug-2839-review-fix-transactional-cleanup.test.cjs",
|
||||
"bug-2851-workflow-bare-gsd-tools.test.cjs",
|
||||
"bug-2866-codex-strip-no-trailing-newline.test.cjs",
|
||||
"bug-2876-skill-frontmatter-quote.test.cjs",
|
||||
"bug-2911-audit-open-output-shape.test.cjs",
|
||||
"bug-2912-progress-context-authority.test.cjs",
|
||||
"bug-2916-handle-branching-default-base.test.cjs",
|
||||
"bug-2942-detect-custom-skills.test.cjs",
|
||||
"bug-2943-config-get-context-window-default.test.cjs",
|
||||
"bug-2948-spike-wrap-up-dispatch.test.cjs",
|
||||
"bug-2949-sketch-wrap-up-dispatch.test.cjs",
|
||||
"bug-2950-stale-command-refs.test.cjs",
|
||||
"bug-2954-help-md-slash-command-stubs.test.cjs",
|
||||
"bug-2957-claude-global-postinstall-message.test.cjs",
|
||||
"bug-2969-verify-reapply-patches.test.cjs",
|
||||
"bug-2973-profile-user-skills-path.test.cjs",
|
||||
"bug-2979-hook-absolute-node.test.cjs",
|
||||
"bug-2986-config-schema-mutation-killers.test.cjs",
|
||||
"bug-2990-code-fixer-worktree-branch.test.cjs",
|
||||
"bug-2992-check-latest-version.test.cjs",
|
||||
"bug-2994-verify-reapply-patches-installed-path.test.cjs",
|
||||
"bug-2995-post-install-script-paths.test.cjs",
|
||||
"bug-2998-pristine-dir-populated.test.cjs",
|
||||
"bug-3017-codex-hook-absolute-node.test.cjs",
|
||||
"bug-3018-codex-discuss-fallback.test.cjs",
|
||||
"bug-3019-help-passthrough.test.cjs",
|
||||
"bug-3037-gemini-duplicate-commands.test.cjs",
|
||||
"bug-3050-update-backup-eacces-nonfatal.test.cjs",
|
||||
"bug-3054-stale-gsd-next-references.test.cjs",
|
||||
"bug-3072-optional-sketch-findings-guard.test.cjs",
|
||||
"bug-3083-resume-route-clear.test.cjs",
|
||||
"bug-3086-git-create-tag-config-gate.test.cjs",
|
||||
"bug-3087-planner-directive-language.test.cjs",
|
||||
"bug-3096-ai-integration-phase-parallel-race.test.cjs",
|
||||
"bug-3097-3099-executor-worktree-path-safety.test.cjs",
|
||||
"bug-3120-secure-phase-empty-register.test.cjs",
|
||||
"bug-3126-global-skills-base-runtime-path.test.cjs",
|
||||
"bug-3127-state-begin-phase-idempotent.test.cjs",
|
||||
"bug-3128-roadmap-plan-count-slug-layout.test.cjs",
|
||||
"bug-3129-validate-commit-git-bypass.test.cjs",
|
||||
"bug-3130-update-npx-robust-invocation.test.cjs",
|
||||
"bug-3135-capture-backlog-workflow.test.cjs",
|
||||
"bug-3150-stats-json-decimal-phase-gaps.test.cjs",
|
||||
"bug-3156-plan-phase-opencode-dispatch.test.cjs",
|
||||
"bug-3163-codex-agents-md.test.cjs",
|
||||
"bug-3168-task-to-agent-rename.test.cjs",
|
||||
"bug-3181-node-cellar-path.test.cjs",
|
||||
"bug-3195-quick-resurrection-guard.test.cjs",
|
||||
"bug-3197-gsd-tools-config-whitelist.test.cjs",
|
||||
"bug-321-config-defaults-clone-strategy.test.cjs",
|
||||
"bug-3212-execute-phase-stall-safe-resume.test.cjs",
|
||||
"bug-3227-config-set-model-overrides.test.cjs",
|
||||
"bug-3236-capture-seed-one-shot.test.cjs",
|
||||
"bug-3242-state-update-progress-trample.test.cjs",
|
||||
"bug-3243-dotted-command-form.test.cjs",
|
||||
"bug-3245-codex-toml-floats.test.cjs",
|
||||
"bug-3257-nested-plans-undercount.test.cjs",
|
||||
"bug-3258-no-stale-gsd-intel-references.test.cjs",
|
||||
"bug-3275-fmstr-non-string-scalars.test.cjs",
|
||||
"bug-3285-codex-hooks-state-allowed.test.cjs",
|
||||
"bug-3286-state-write-routing.test.cjs",
|
||||
"bug-3288-model-catalog-install-path.test.cjs",
|
||||
"bug-3290-intel-updater-layout-block.test.cjs",
|
||||
"bug-33-settings-model-profile-adaptive.test.cjs",
|
||||
"bug-3320-planner-deep-work-rules.test.cjs",
|
||||
"bug-3321-verifier-runs-probes.test.cjs",
|
||||
"bug-3346-codex-aot-toml-key.test.cjs",
|
||||
"bug-3357-codex-legacy-hooks-json-migration.test.cjs",
|
||||
"bug-3360-codex-execute-phase-worktrees.test.cjs",
|
||||
"bug-338-local-install-settings-local-json.test.cjs",
|
||||
"bug-3381-verify-work-workstream.test.cjs",
|
||||
"bug-3384-secondary-defects.test.cjs",
|
||||
"bug-3407-pristine-stale-content.test.cjs",
|
||||
"bug-3413-shell-command-projection.test.cjs",
|
||||
"bug-3418-progress-flag-routing.test.cjs",
|
||||
"bug-3426-codex-windows-hooks.test.cjs",
|
||||
"bug-3427-3433-codex-install-shape.test.cjs",
|
||||
"bug-3430-planner-phase-contract.test.cjs",
|
||||
"bug-3431-debug-command-yaml.test.cjs",
|
||||
"bug-3441-path-action-projection.test.cjs",
|
||||
"bug-3442-codex-legacy-hooks-json-migration.test.cjs",
|
||||
"bug-3442-shim-projection-drift-guard.test.cjs",
|
||||
"bug-3446-resume-continue-here-discovery.test.cjs",
|
||||
"bug-3454-state-dollar-backreference-growth.test.cjs",
|
||||
"bug-3489-complete-phase-idempotent.test.cjs",
|
||||
"bug-3491-nested-git-worktree.test.cjs",
|
||||
"bug-3509-path-spaces.test.cjs",
|
||||
"bug-3516-reapply-patches-gsd-update-filter.test.cjs",
|
||||
"bug-3521-quick-cleanup-cwd-pin.test.cjs",
|
||||
"bug-3523-cjs-loadconfig-branching-strategy-warning.test.cjs",
|
||||
"bug-3537-padded-id-against-unpadded-roadmap.test.cjs",
|
||||
"bug-3541-installer-migration-prompt-user-resolution.test.cjs",
|
||||
"bug-3542-executor-git-stash-prohibition.test.cjs",
|
||||
"bug-3562-codex-install-skill-surface.test.cjs",
|
||||
"bug-3566-codex-hooks-feature-canonical-key.test.cjs",
|
||||
"bug-3571-configuration-manifest-install-path.test.cjs",
|
||||
"bug-3582-codex-skills-materialized.test.cjs",
|
||||
"bug-3584-runtime-slash-emitters.test.cjs",
|
||||
"bug-3584-runtime-slash-formatter.test.cjs",
|
||||
"bug-3588-npm-audit-clean.test.cjs",
|
||||
"bug-3599-roadmap-get-phase-project-code-prefix.test.cjs",
|
||||
"bug-3605-stale-research-insert-phase-agent-refs.test.cjs",
|
||||
"bug-3608-antigravity-update-runtime-classification.test.cjs",
|
||||
"bug-3610-installer-migration-bundled-hooks-classification.test.cjs",
|
||||
"bug-3628-bundled-hook-classifier-whitelist.test.cjs",
|
||||
"bug-3631-router-raw-flag.test.cjs",
|
||||
"bug-3657-verify-reapply-patches-pristine-drift.test.cjs",
|
||||
"bug-3659-applysurface-prune-skill-dirs.test.cjs",
|
||||
"bug-3668-workflow-runtime-resolution.test.cjs",
|
||||
"bug-3670-cursor-local-install-migration-lock.test.cjs",
|
||||
"bug-3677-agent-colon-namespace-leak.test.cjs",
|
||||
"bug-3678-executor-commit-docs-respect.test.cjs",
|
||||
"bug-3683-command-colon-namespace-leak.test.cjs",
|
||||
"bug-3683-command-cross-reference-invariant.test.cjs",
|
||||
"bug-3683-workflow-colon-namespace-leak.test.cjs",
|
||||
"bug-3689-resume-glob-nomatch.test.cjs",
|
||||
"bug-3691-annotate-deps-plans-block-variants.test.cjs",
|
||||
"bug-3706-ui-safety-gate-false-positives.test.cjs",
|
||||
"bug-3707-locked-worktree-cleanup.test.cjs",
|
||||
"bug-3727-code-review-fix-flag-dispatch.test.cjs",
|
||||
"bug-3735-profiles-core-includes-surface.test.cjs",
|
||||
"bug-3739-gap-checker-padded-prefix-context.test.cjs",
|
||||
"bug-376-claude-js-hook-gsd-rewriter.test.cjs",
|
||||
"bug-378-update-check-scoped-name.test.cjs",
|
||||
"bug-3784-gsd-settings-model-profile-ui-omits-adaptive.test.cjs",
|
||||
"bug-3805-fast-md-log-to-state-schema.test.cjs",
|
||||
"bug-3808-codex-adapter-text-mode-fallback.test.cjs",
|
||||
"bug-3810-no-gsd-sdk-runtime-refs.test.cjs",
|
||||
"bug-384-agents-runtime-aware.test.cjs",
|
||||
"bug-397-state-preserve-executor-authored.test.cjs",
|
||||
"bug-410-install-defaults-test-mode-guard.test.cjs",
|
||||
"bug-416-archive-dir-null.test.cjs",
|
||||
"bug-442-config-dir-equals-in-path.test.cjs",
|
||||
"bug-444-resolver-local-claude-install.test.cjs",
|
||||
"bug-447-gap-analysis-phase-req-ids.test.cjs",
|
||||
"bug-474-clock-seam-date-determinism.test.cjs",
|
||||
"bug-492-effort-manifest-fallback.test.cjs",
|
||||
"bug-500-planned-phase-progress-corruption.test.cjs",
|
||||
"bug-501-flat-phase-details-milestone-leak.test.cjs",
|
||||
"bug-503-update-agent-antigravity-detection.test.cjs",
|
||||
"bug-505-remove-dead-sdk-verification.test.cjs",
|
||||
"bug-549-total-phases-overcounts-with-phase-section-heading.test.cjs",
|
||||
"bug-557-details-summary-milestone-strip.test.cjs",
|
||||
"bug-570-codex-leak-scanner.test.cjs",
|
||||
"bug-571-doc-writer-fix-mode-edit-only.test.cjs",
|
||||
"bug-580-local-sh-hook-bash-wrapper.test.cjs",
|
||||
"bug-619-codebase-drift-gate-shim.test.cjs",
|
||||
"bug-621-plan-phase-gap-analysis-gsd-run.test.cjs",
|
||||
"bug-622-graphify-optional-graph-html.test.cjs",
|
||||
"bug-630-wave-cleanup-orchestrator-root.test.cjs",
|
||||
"bug-637-workflow-no-hardcoded-home-tool.test.cjs",
|
||||
"bug-641-files-from-suite-token.test.cjs",
|
||||
"bug-663-redos-roadmap-phase-parsing.test.cjs",
|
||||
"bug-685-windowshide-spawn.test.cjs",
|
||||
"bug-687-agy-timeout.test.cjs",
|
||||
"bug-704-codex-launcher-path-corruption.test.cjs",
|
||||
"bug-730-milestone-phase-details-scope.test.cjs",
|
||||
"bug-782-cline-skills-emission.test.cjs",
|
||||
"bug-783-kilo-global-skills-base.test.cjs",
|
||||
"bug-853-bg-dispatch-runtime-gating.test.cjs",
|
||||
"bug-866-profile-pipeline-temp-root.test.cjs",
|
||||
"bug-891-non-claude-runtime-home-fallback.test.cjs",
|
||||
"bug-892-validate-checklist-roadmap-phases.test.cjs",
|
||||
"bug-905-state-syncstatefrontmatter-preserve-scalars.test.cjs",
|
||||
"bug-924-claude-flat-skill-layout.test.cjs",
|
||||
"bug-925-context-monitor-hook-event-name.test.cjs",
|
||||
"bug-936-no-nested-spawner-wrap.test.cjs",
|
||||
"bug-941-managed-hooks-registry-manifest.test.cjs"
|
||||
]
|
||||
78
scripts/lint-regression-test-names.cjs
Normal file
78
scripts/lint-regression-test-names.cjs
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env node
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
* lint-regression-test-names.cjs — ban NEW top-level bug-NNNN test files.
|
||||
*
|
||||
* ## Why
|
||||
*
|
||||
* The 2026-06 CI audit found 244 one-off `tests/bug-NNNN-*.test.cjs` files —
|
||||
* ~38% of the suite. `node --test` spawns one child process per FILE, so file
|
||||
* count (not test count) is the unit of CI overhead, and it is worst on the
|
||||
* Windows lanes where every spawn is Defender-scanned. Each regression test
|
||||
* belongs in the owning module's main test file as a regression case (e.g. a
|
||||
* `describe('regressions')` block in `tests/<module>.test.cjs`), where it
|
||||
* costs zero additional processes.
|
||||
*
|
||||
* ## What this enforces
|
||||
*
|
||||
* Identity ratchet (scripts/lib/allowlist-ratchet.cjs) over basenames matching
|
||||
* /^bug-\d+.*\.test\.cjs$/ in tests/:
|
||||
* - A NEW bug-* file (not in the allowlist) fails: fold the regression into
|
||||
* the owning module's test file instead.
|
||||
* - A REMOVED bug-* file with a stale allowlist entry also fails: prune the
|
||||
* entry from scripts/lint-regression-test-names.allowlist.json so the
|
||||
* baseline only ever shrinks.
|
||||
*
|
||||
* See docs/TESTING-SUITES.md ("Regression tests") for the placement policy.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { assertWithinAllowlist } = require('./lib/allowlist-ratchet.cjs');
|
||||
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
// Env overrides exist for the lint's own tests only (sandbox fixture dirs).
|
||||
const TESTS_DIR = process.env.GSD_LINT_REGRESSION_TESTS_DIR || path.join(ROOT, 'tests');
|
||||
const ALLOWLIST_PATH =
|
||||
process.env.GSD_LINT_REGRESSION_ALLOWLIST ||
|
||||
path.join(__dirname, 'lint-regression-test-names.allowlist.json');
|
||||
|
||||
const BUG_FILE_RE = /^bug-\d+.*\.test\.cjs$/;
|
||||
|
||||
function main() {
|
||||
const current = fs
|
||||
.readdirSync(TESTS_DIR)
|
||||
.filter(f => BUG_FILE_RE.test(f))
|
||||
.sort();
|
||||
const known = JSON.parse(fs.readFileSync(ALLOWLIST_PATH, 'utf8'));
|
||||
|
||||
const failures = [];
|
||||
const { novel } = assertWithinAllowlist({
|
||||
label: 'regression-test-names',
|
||||
current,
|
||||
known,
|
||||
fail: msg => failures.push(msg),
|
||||
pruneHint: 'edit scripts/lint-regression-test-names.allowlist.json',
|
||||
});
|
||||
|
||||
if (failures.length > 0) {
|
||||
for (const msg of failures) console.error(msg);
|
||||
if (novel.length > 0) {
|
||||
console.error(
|
||||
'\nNew bug-NNNN test files are no longer accepted. Add the regression ' +
|
||||
"case to the owning module's test file (e.g. a describe('regressions') " +
|
||||
'block in tests/<module>.test.cjs) instead of creating a new file. ' +
|
||||
'See docs/TESTING-SUITES.md.'
|
||||
);
|
||||
}
|
||||
throw new ExitError(1);
|
||||
}
|
||||
|
||||
console.log(
|
||||
`ok lint-regression-test-names: ${current.length} grandfathered bug-* file(s), no novel offenders`
|
||||
);
|
||||
}
|
||||
|
||||
runMain(main);
|
||||
@@ -26,7 +26,7 @@
|
||||
"graphify": {
|
||||
"files": [
|
||||
"bug-622-graphify-optional-graph-html.test.cjs",
|
||||
"graphify-auto-update.test.cjs",
|
||||
"graphify-auto-update.slow.test.cjs",
|
||||
"graphify-query.test.cjs",
|
||||
"graphify-visualization.test.cjs",
|
||||
"graphify.test.cjs"
|
||||
@@ -82,8 +82,8 @@
|
||||
},
|
||||
"security": {
|
||||
"files": [
|
||||
"security-prompt-injection.test.cjs",
|
||||
"security-scan.test.cjs",
|
||||
"security-prompt-injection.security.test.cjs",
|
||||
"security-scan.security.test.cjs",
|
||||
"security.test.cjs"
|
||||
],
|
||||
"issue": "TBD"
|
||||
|
||||
@@ -69,15 +69,15 @@ ALLOWLIST=(
|
||||
'scripts/prompt-injection-scan.sh'
|
||||
'scripts/base64-scan.sh'
|
||||
'scripts/secret-scan.sh'
|
||||
'tests/security-scan.test.cjs'
|
||||
'tests/security-scan.security.test.cjs'
|
||||
'tests/security.test.cjs'
|
||||
'tests/prompt-injection-scan.test.cjs'
|
||||
'tests/prompt-injection-scan.security.test.cjs'
|
||||
'tests/verify.test.cjs'
|
||||
'gsd-core/bin/lib/security.cjs'
|
||||
'hooks/gsd-prompt-guard.js'
|
||||
'hooks/gsd-read-injection-scanner.js'
|
||||
'tests/read-injection-scanner.test.cjs'
|
||||
'tests/security-prompt-injection.test.cjs'
|
||||
'tests/read-injection-scanner.security.test.cjs'
|
||||
'tests/security-prompt-injection.security.test.cjs'
|
||||
'tests/fixtures/adversarial/security/'
|
||||
'SECURITY.md'
|
||||
# These files contain intentional injection examples / security-model prose
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
'use strict';
|
||||
|
||||
const { spawnSync } = require('child_process');
|
||||
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
||||
|
||||
const CROSS_PLATFORM_TEST_REASON = Object.freeze({
|
||||
PASS: 'pass',
|
||||
TEST_FAILURE: 'test_failure',
|
||||
INFRA_FAILURE: 'infra_failure',
|
||||
UNKNOWN_FAILURE: 'unknown_failure',
|
||||
});
|
||||
|
||||
function classify(exitCode, output) {
|
||||
if (exitCode === 0) return CROSS_PLATFORM_TEST_REASON.PASS;
|
||||
if (exitCode === 2 || /infrastructure failure|worktree\.Construct/i.test(output)) {
|
||||
return CROSS_PLATFORM_TEST_REASON.INFRA_FAILURE;
|
||||
}
|
||||
if (/\bFAIL\b|\d+\s+failures?\)/i.test(output)) {
|
||||
return CROSS_PLATFORM_TEST_REASON.TEST_FAILURE;
|
||||
}
|
||||
return CROSS_PLATFORM_TEST_REASON.UNKNOWN_FAILURE;
|
||||
}
|
||||
|
||||
function runCrossPlatformTests(options = {}, deps = {}) {
|
||||
const {
|
||||
base = 'next',
|
||||
head = 'HEAD',
|
||||
source = '.',
|
||||
targets = 'linux,macos',
|
||||
cwd = process.cwd(),
|
||||
} = options;
|
||||
const runner = deps.spawnSync || spawnSync;
|
||||
|
||||
const args = ['--targets', targets, '--base', base, '--head', head, '--source', source];
|
||||
const result = runner('gsd-test', args, { cwd, encoding: 'utf8' });
|
||||
|
||||
const stdout = result.stdout || '';
|
||||
const stderr = result.stderr || '';
|
||||
const output = `${stdout}\n${stderr}`;
|
||||
const exitCode = Number(result.status ?? 1);
|
||||
const reason = classify(exitCode, output);
|
||||
|
||||
return {
|
||||
ok: exitCode === 0,
|
||||
reason,
|
||||
exitCode,
|
||||
command: ['gsd-test', ...args].join(' '),
|
||||
stdout,
|
||||
stderr,
|
||||
};
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
function main() {
|
||||
const result = runCrossPlatformTests();
|
||||
const line = `[cross-platform-tests] reason=${result.reason} exit=${result.exitCode}`;
|
||||
if (result.ok) {
|
||||
process.stdout.write(`${line}\n`);
|
||||
return 0;
|
||||
}
|
||||
process.stderr.write(`${line}\n`);
|
||||
throw new ExitError(result.exitCode);
|
||||
}
|
||||
runMain(main);
|
||||
}
|
||||
|
||||
module.exports = { CROSS_PLATFORM_TEST_REASON, runCrossPlatformTests };
|
||||
@@ -218,9 +218,9 @@ should_skip_file() {
|
||||
# Skip the scan scripts themselves and test files
|
||||
case "$file" in
|
||||
*/secret-scan.sh) return 0 ;;
|
||||
*/secret-scan-lint.test.cjs) return 0 ;;
|
||||
*/security-scan.test.cjs) return 0 ;;
|
||||
*/security-prompt-injection.test.cjs) return 0 ;;
|
||||
*/secret-scan-lint.security.test.cjs) return 0 ;;
|
||||
*/security-scan.security.test.cjs) return 0 ;;
|
||||
*/security-prompt-injection.security.test.cjs) return 0 ;;
|
||||
tests/fixtures/adversarial/security/*|*/tests/fixtures/adversarial/security/*) return 0 ;;
|
||||
esac
|
||||
return 1
|
||||
|
||||
@@ -29,6 +29,15 @@
|
||||
// force a full tsc rebuild per mutant — far too slow for the 30-min CI budget.)
|
||||
// Large/low-coverage modules are excluded (the command's test set does not
|
||||
// exercise them, so they would only ever produce survived mutants).
|
||||
//
|
||||
// KNOWN BLIND SPOT (2026-06 CI audit): this list excludes ~14.2k of ~29.8k
|
||||
// lib lines (~48%), including the most central modules (state, core,
|
||||
// commands, phase, verify). Mutation results therefore speak only for the
|
||||
// well-tested half of the lib. Shrinking the list is deliberate tracked work:
|
||||
// bring one module into scope per release by first giving it per-module
|
||||
// *.unit.test.cjs / *.property.test.cjs coverage, then deleting its entry —
|
||||
// never delete an entry without that coverage (it will only produce survived
|
||||
// mutants and trip the break threshold).
|
||||
const UNMUTATED = [
|
||||
'!gsd-core/bin/lib/command-aliases.cjs',
|
||||
'!gsd-core/bin/lib/commands.cjs',
|
||||
|
||||
@@ -279,18 +279,36 @@ describe('ci-test-scope.cjs', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('ci-test-scope superset invariant (#494)', () => {
|
||||
// Facet A: any tests/** change → full_matrix === true
|
||||
test('A1: a specific changed test file forces full_matrix', () => {
|
||||
describe('ci-test-scope superset invariant (#494, narrowed)', () => {
|
||||
// Facet A (narrowed): a changed test file no longer triggers the full
|
||||
// parity matrix — instead it must ALWAYS run on the scoped windows lane,
|
||||
// so OS-specific breakage in the changed test (the #482 class) is still
|
||||
// exercised pre-merge. Ubuntu 22/24 coverage comes via targeted_tests.
|
||||
test('A1: a changed test file joins the windows scoped lane without full_matrix', () => {
|
||||
const result = scopeFor(['tests/bug-1974-context-exhaustion-record.test.cjs']);
|
||||
assert.strictEqual(result.full_matrix, true,
|
||||
`expected full_matrix=true for tests/** change, got: ${JSON.stringify(result)}`);
|
||||
assert.strictEqual(result.full_matrix, false,
|
||||
`expected full_matrix=false for a tests/**-only change, got: ${JSON.stringify(result)}`);
|
||||
assert.ok(result.targeted_tests.includes('tests/bug-1974-context-exhaustion-record.test.cjs'),
|
||||
`expected the changed test in targeted_tests, got: ${JSON.stringify(result.targeted_tests)}`);
|
||||
assert.ok(result.windows_tests.includes('tests/bug-1974-context-exhaustion-record.test.cjs'),
|
||||
`expected the changed test in windows_tests, got: ${JSON.stringify(result.windows_tests)}`);
|
||||
});
|
||||
|
||||
test('A2: any tests/** path forces full_matrix', () => {
|
||||
test('A2: a changed test file with no windows hint still joins the windows lane', () => {
|
||||
// commands.test.cjs matches none of the WINDOWS_HINTS substrings — the
|
||||
// unconditional changed-test → windows lane rule must include it anyway.
|
||||
const result = scopeFor(['tests/commands.test.cjs']);
|
||||
assert.strictEqual(result.full_matrix, false);
|
||||
assert.ok(result.windows_tests.includes('tests/commands.test.cjs'),
|
||||
`expected hint-less changed test in windows_tests, got: ${JSON.stringify(result.windows_tests)}`);
|
||||
});
|
||||
|
||||
test('A3: a deleted/nonexistent test path falls back to the unit token, no full_matrix', () => {
|
||||
const result = scopeFor(['tests/some-new.test.cjs']);
|
||||
assert.strictEqual(result.full_matrix, true,
|
||||
`expected full_matrix=true for tests/** change, got: ${JSON.stringify(result)}`);
|
||||
assert.strictEqual(result.full_matrix, false);
|
||||
// The nonexistent file is filtered by existingTests(); with nothing left,
|
||||
// the #408 fallback applies so the targeted lane still runs something.
|
||||
assert.deepStrictEqual(result.targeted_tests, ['unit']);
|
||||
});
|
||||
|
||||
// Facet B: commands/**, agents/** → code_changed AND docs-parity selected
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Adversarial security fixtures (#3596)
|
||||
|
||||
Reusable hostile payloads consumed by
|
||||
`tests/security-prompt-injection.test.cjs`.
|
||||
`tests/security-prompt-injection.security.test.cjs`.
|
||||
|
||||
The fixtures here are pure data — they are loaded by the test as input
|
||||
to the production code under test (hooks, validators, sanitizers, CLI).
|
||||
|
||||
96
tests/lint-regression-test-names.test.cjs
Normal file
96
tests/lint-regression-test-names.test.cjs
Normal file
@@ -0,0 +1,96 @@
|
||||
'use strict';
|
||||
|
||||
// Tests for scripts/lint-regression-test-names.cjs — the identity ratchet
|
||||
// that bans NEW top-level bug-NNNN test files (2026-06 CI audit). Uses the
|
||||
// script's env overrides to point at sandbox fixture dirs; never touches the
|
||||
// real tests/ directory or allowlist.
|
||||
|
||||
const { describe, test, before, after } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const { spawnSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
const SCRIPT = path.join(ROOT, 'scripts', 'lint-regression-test-names.cjs');
|
||||
|
||||
let sandbox;
|
||||
|
||||
let fixtureCount = 0;
|
||||
|
||||
function runLint({ files, allowlist }) {
|
||||
const testsDir = path.join(sandbox, `tests-${fixtureCount++}`);
|
||||
fs.mkdirSync(testsDir, { recursive: true });
|
||||
for (const f of files) fs.writeFileSync(path.join(testsDir, f), '');
|
||||
const allowlistPath = path.join(testsDir, 'allowlist.json');
|
||||
fs.writeFileSync(allowlistPath, JSON.stringify(allowlist));
|
||||
return spawnSync(process.execPath, [SCRIPT], {
|
||||
cwd: ROOT,
|
||||
encoding: 'utf8',
|
||||
env: {
|
||||
...process.env,
|
||||
GSD_LINT_REGRESSION_TESTS_DIR: testsDir,
|
||||
GSD_LINT_REGRESSION_ALLOWLIST: allowlistPath,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('lint-regression-test-names', () => {
|
||||
before(() => {
|
||||
sandbox = createTempDir('gsd-lint-regression-');
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cleanup(sandbox);
|
||||
});
|
||||
|
||||
test('passes when every bug-* file is grandfathered', () => {
|
||||
const r = runLint({
|
||||
files: ['bug-100-old.test.cjs', 'module.test.cjs'],
|
||||
allowlist: ['bug-100-old.test.cjs'],
|
||||
});
|
||||
assert.strictEqual(r.status, 0, `stderr: ${r.stderr}`);
|
||||
});
|
||||
|
||||
test('fails on a novel bug-* file with fold-into-module guidance', () => {
|
||||
const r = runLint({
|
||||
files: ['bug-100-old.test.cjs', 'bug-200-new.test.cjs'],
|
||||
allowlist: ['bug-100-old.test.cjs'],
|
||||
});
|
||||
assert.notStrictEqual(r.status, 0);
|
||||
assert.match(r.stderr, /bug-200-new\.test\.cjs/);
|
||||
assert.match(r.stderr, /owning module/);
|
||||
});
|
||||
|
||||
test('fails on a stale allowlist entry (ratchet-down enforcement)', () => {
|
||||
const r = runLint({
|
||||
files: ['bug-100-old.test.cjs'],
|
||||
allowlist: ['bug-100-old.test.cjs', 'bug-300-gone.test.cjs'],
|
||||
});
|
||||
assert.notStrictEqual(r.status, 0);
|
||||
assert.match(r.stderr, /bug-300-gone\.test\.cjs/);
|
||||
});
|
||||
|
||||
test('ignores non-bug test files and suite-marked non-bug names', () => {
|
||||
const r = runLint({
|
||||
files: ['module.test.cjs', 'feature.integration.test.cjs', 'debug-1-not-a-bug.test.cjs'],
|
||||
allowlist: [],
|
||||
});
|
||||
assert.strictEqual(r.status, 0, `stderr: ${r.stderr}`);
|
||||
});
|
||||
|
||||
test('catches a suite-marked bug-* file too (no marker escape hatch)', () => {
|
||||
const r = runLint({
|
||||
files: ['bug-400-sneaky.security.test.cjs'],
|
||||
allowlist: [],
|
||||
});
|
||||
assert.notStrictEqual(r.status, 0);
|
||||
assert.match(r.stderr, /bug-400-sneaky\.security\.test\.cjs/);
|
||||
});
|
||||
|
||||
test('repo baseline passes (real tests/ dir against real allowlist)', () => {
|
||||
const r = spawnSync(process.execPath, [SCRIPT], { cwd: ROOT, encoding: 'utf8' });
|
||||
assert.strictEqual(r.status, 0, `stderr: ${r.stderr}\nstdout: ${r.stdout}`);
|
||||
});
|
||||
});
|
||||
@@ -12,7 +12,7 @@
|
||||
* deeper than 50, which is intentional.
|
||||
*
|
||||
* Note: security-scan.yml legitimately uses fetch-depth: 0 and is NOT covered
|
||||
* by this test (see tests/security-scan.test.cjs).
|
||||
* by this test (see tests/security-scan.security.test.cjs).
|
||||
*/
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
|
||||
@@ -58,7 +58,7 @@ const ALLOWLIST = new Set([
|
||||
'hooks/gsd-prompt-guard.js', // The prompt guard hook
|
||||
'hooks/gsd-read-injection-scanner.js', // The read injection scanner (contains patterns)
|
||||
'tests/security.test.cjs', // Security tests
|
||||
'tests/prompt-injection-scan.test.cjs', // This file
|
||||
'tests/prompt-injection-scan.security.test.cjs', // This file
|
||||
]);
|
||||
|
||||
// Workflows that exceed the 50K strict-mode size threshold due to legitimate
|
||||
@@ -1,46 +0,0 @@
|
||||
const { test, describe } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
|
||||
const { CROSS_PLATFORM_TEST_REASON, runCrossPlatformTests } = require('../scripts/run-cross-platform-tests.cjs');
|
||||
|
||||
describe('run cross-platform tests module', () => {
|
||||
test('returns pass reason on zero exit', () => {
|
||||
const out = runCrossPlatformTests({}, {
|
||||
spawnSync: () => ({ status: 0, stdout: 'ok', stderr: '' }),
|
||||
});
|
||||
assert.strictEqual(out.ok, true);
|
||||
assert.strictEqual(out.reason, CROSS_PLATFORM_TEST_REASON.PASS);
|
||||
assert.ok(out.command.includes('--base next'));
|
||||
});
|
||||
|
||||
test('classifies infrastructure failure', () => {
|
||||
const out = runCrossPlatformTests({}, {
|
||||
spawnSync: () => ({ status: 2, stdout: '', stderr: 'infrastructure failure' }),
|
||||
});
|
||||
assert.strictEqual(out.ok, false);
|
||||
assert.strictEqual(out.reason, CROSS_PLATFORM_TEST_REASON.INFRA_FAILURE);
|
||||
});
|
||||
|
||||
test('classifies test failure from FAIL marker', () => {
|
||||
const out = runCrossPlatformTests({}, {
|
||||
spawnSync: () => ({ status: 1, stdout: 'linux FAIL 1/2 tests (1 failures)', stderr: '' }),
|
||||
});
|
||||
assert.strictEqual(out.ok, false);
|
||||
assert.strictEqual(out.reason, CROSS_PLATFORM_TEST_REASON.TEST_FAILURE);
|
||||
});
|
||||
|
||||
test('passes options through to command args', () => {
|
||||
let cmd = null;
|
||||
let args = null;
|
||||
runCrossPlatformTests({ base: 'main', head: 'abc123', source: '/tmp/x', targets: 'linux' }, {
|
||||
spawnSync: (c, a) => {
|
||||
cmd = c;
|
||||
args = a;
|
||||
return { status: 0, stdout: '', stderr: '' };
|
||||
},
|
||||
});
|
||||
|
||||
assert.strictEqual(cmd, 'gsd-test');
|
||||
assert.deepStrictEqual(args, ['--targets', 'linux', '--base', 'main', '--head', 'abc123', '--source', '/tmp/x']);
|
||||
});
|
||||
});
|
||||
@@ -49,12 +49,12 @@ const SELF = path.basename(__filename);
|
||||
const KNOWN_OFFENDERS = Object.freeze({
|
||||
splitNewlineOnFileContent: new Set([
|
||||
'release-coverage-scope.test.cjs',
|
||||
'secret-scan-lint.test.cjs',
|
||||
'security-scan.test.cjs',
|
||||
'secret-scan-lint.security.test.cjs',
|
||||
'security-scan.security.test.cjs',
|
||||
]),
|
||||
fenceRegexLiteralNewline: new Set([
|
||||
'bug-2995-post-install-script-paths.test.cjs',
|
||||
'security-scan.test.cjs',
|
||||
'security-scan.security.test.cjs',
|
||||
]),
|
||||
frontmatterAnchorLiteralNewline: new Set([
|
||||
'bug-1967-cache-invalidation.test.cjs',
|
||||
|
||||
Reference in New Issue
Block a user