mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Speed up server CI tests (#2537)
* perf(ci): reduce server test latency Server test files use isolated resources, so they do not require suite-wide serialization. * fix(ci): scope server test parallelism to unit suite Keep real-provider and local-resource suites serialized because they may share user configuration and account limits. * fix(terminal): isolate zsh runtimes by process Prevent concurrent daemon and test processes from deleting or replacing shell integration files used by another process. * fix(ci): preserve required matrix check names GitHub evaluates job conditions before expanding a matrix. Expand active matrices first and gate their expensive steps so required check contexts are always reported. Allow superseded runs to cancel while retaining fail-open path detection.
This commit is contained in:
114
.github/workflows/ci.yml
vendored
114
.github/workflows/ci.yml
vendored
@@ -115,6 +115,9 @@ jobs:
|
|||||||
- 'packages/relay/**'
|
- 'packages/relay/**'
|
||||||
- 'packages/server/**'
|
- 'packages/server/**'
|
||||||
|
|
||||||
|
- name: Validate CI workflow
|
||||||
|
run: node --test scripts/ci-workflow.test.mjs
|
||||||
|
|
||||||
format:
|
format:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
@@ -138,7 +141,7 @@ jobs:
|
|||||||
lint:
|
lint:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: >-
|
if: >-
|
||||||
${{ always() &&
|
${{ !cancelled() &&
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
(github.event_name == 'workflow_dispatch' ||
|
||||||
needs.changes.result != 'success' ||
|
needs.changes.result != 'success' ||
|
||||||
needs.changes.outputs.quality != 'false') }}
|
needs.changes.outputs.quality != 'false') }}
|
||||||
@@ -168,7 +171,7 @@ jobs:
|
|||||||
typecheck:
|
typecheck:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: >-
|
if: >-
|
||||||
${{ always() &&
|
${{ !cancelled() &&
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
(github.event_name == 'workflow_dispatch' ||
|
||||||
needs.changes.result != 'success' ||
|
needs.changes.result != 'success' ||
|
||||||
needs.changes.outputs.quality != 'false') }}
|
needs.changes.outputs.quality != 'false') }}
|
||||||
@@ -199,11 +202,7 @@ jobs:
|
|||||||
|
|
||||||
server-tests:
|
server-tests:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: >-
|
if: ${{ !cancelled() }}
|
||||||
${{ always() &&
|
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.changes.result != 'success' ||
|
|
||||||
needs.changes.outputs.server != 'false') }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -212,28 +211,43 @@ jobs:
|
|||||||
name: server-tests (${{ matrix.os }})
|
name: server-tests (${{ matrix.os }})
|
||||||
env:
|
env:
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
|
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
|
||||||
|
RUN_TESTS: >-
|
||||||
|
${{ github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.changes.result != 'success' ||
|
||||||
|
needs.changes.outputs.server != 'false' }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Skip unaffected server tests
|
||||||
|
if: env.RUN_TESTS != 'true'
|
||||||
|
run: echo "No server changes detected."
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Fetch origin/main (worktree tests)
|
- name: Fetch origin/main (worktree tests)
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: git fetch --no-tags origin main:refs/remotes/origin/main
|
run: git fetch --no-tags origin main:refs/remotes/origin/main
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: node scripts/npm-retry.mjs ci
|
run: node scripts/npm-retry.mjs ci
|
||||||
- name: Install agent CLIs for provider tests
|
- name: Install agent CLIs for provider tests
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code opencode-ai
|
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code opencode-ai
|
||||||
|
|
||||||
- name: Build server dependencies
|
- name: Build server dependencies
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: npm run build:server-deps
|
run: npm run build:server-deps
|
||||||
|
|
||||||
- name: Run server tests
|
- name: Run server tests
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: npm run test --workspace=@getpaseo/server
|
run: npm run test --workspace=@getpaseo/server
|
||||||
env:
|
env:
|
||||||
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||||
@@ -242,11 +256,7 @@ jobs:
|
|||||||
|
|
||||||
desktop-tests:
|
desktop-tests:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: >-
|
if: ${{ !cancelled() }}
|
||||||
${{ always() &&
|
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.changes.result != 'success' ||
|
|
||||||
needs.changes.outputs.desktop != 'false') }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -255,39 +265,53 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
env:
|
||||||
|
RUN_TESTS: >-
|
||||||
|
${{ github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.changes.result != 'success' ||
|
||||||
|
needs.changes.outputs.desktop != 'false' }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Skip unaffected desktop tests
|
||||||
|
if: env.RUN_TESTS != 'true'
|
||||||
|
run: echo "No desktop changes detected."
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install dependencies with retry
|
- name: Install dependencies with retry
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: node scripts/npm-retry.mjs ci
|
run: node scripts/npm-retry.mjs ci
|
||||||
- name: Build server stack
|
- name: Build server stack
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: npm run build:server
|
run: npm run build:server
|
||||||
|
|
||||||
- name: Run desktop tests
|
- name: Run desktop tests
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: npm run test --workspace=@getpaseo/desktop
|
run: npm run test --workspace=@getpaseo/desktop
|
||||||
|
|
||||||
- name: Build app dependencies for desktop E2E
|
- name: Build app dependencies for desktop E2E
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: env.RUN_TESTS == 'true' && matrix.os == 'ubuntu-latest'
|
||||||
run: npm run build:app-deps
|
run: npm run build:app-deps
|
||||||
|
|
||||||
- name: Install virtual display
|
- name: Install virtual display
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: env.RUN_TESTS == 'true' && matrix.os == 'ubuntu-latest'
|
||||||
run: sudo apt-get update && sudo apt-get install -y xvfb xauth
|
run: sudo apt-get update && sudo apt-get install -y xvfb xauth
|
||||||
|
|
||||||
- name: Run real Electron browser tab bridge E2E
|
- name: Run real Electron browser tab bridge E2E
|
||||||
if: matrix.os == 'ubuntu-latest'
|
if: env.RUN_TESTS == 'true' && matrix.os == 'ubuntu-latest'
|
||||||
run: npm run test:e2e:browser-tab-bridge --workspace=@getpaseo/desktop
|
run: npm run test:e2e:browser-tab-bridge --workspace=@getpaseo/desktop
|
||||||
env:
|
env:
|
||||||
PASEO_TAB_BRIDGE_E2E_ARTIFACT_DIR: ${{ runner.temp }}/browser-tab-bridge-e2e
|
PASEO_TAB_BRIDGE_E2E_ARTIFACT_DIR: ${{ runner.temp }}/browser-tab-bridge-e2e
|
||||||
|
|
||||||
- name: Upload browser tab bridge diagnostics
|
- name: Upload browser tab bridge diagnostics
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: failure() && matrix.os == 'ubuntu-latest'
|
if: env.RUN_TESTS == 'true' && failure() && matrix.os == 'ubuntu-latest'
|
||||||
with:
|
with:
|
||||||
name: browser-tab-bridge-e2e
|
name: browser-tab-bridge-e2e
|
||||||
path: ${{ runner.temp }}/browser-tab-bridge-e2e
|
path: ${{ runner.temp }}/browser-tab-bridge-e2e
|
||||||
@@ -296,7 +320,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and smoke unpacked desktop app
|
- name: Build and smoke unpacked desktop app
|
||||||
if: >-
|
if: >-
|
||||||
matrix.os == 'ubuntu-latest' &&
|
env.RUN_TESTS == 'true' && matrix.os == 'ubuntu-latest' &&
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
(github.event_name == 'workflow_dispatch' ||
|
||||||
needs.changes.result != 'success' ||
|
needs.changes.result != 'success' ||
|
||||||
needs.changes.outputs.desktop_package != 'false')
|
needs.changes.outputs.desktop_package != 'false')
|
||||||
@@ -308,7 +332,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload packaged smoke diagnostics
|
- name: Upload packaged smoke diagnostics
|
||||||
if: >-
|
if: >-
|
||||||
failure() && matrix.os == 'ubuntu-latest' &&
|
env.RUN_TESTS == 'true' && failure() && matrix.os == 'ubuntu-latest' &&
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
(github.event_name == 'workflow_dispatch' ||
|
||||||
needs.changes.result != 'success' ||
|
needs.changes.result != 'success' ||
|
||||||
needs.changes.outputs.desktop_package != 'false')
|
needs.changes.outputs.desktop_package != 'false')
|
||||||
@@ -322,7 +346,7 @@ jobs:
|
|||||||
app-tests:
|
app-tests:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: >-
|
if: >-
|
||||||
${{ always() &&
|
${{ !cancelled() &&
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
(github.event_name == 'workflow_dispatch' ||
|
||||||
needs.changes.result != 'success' ||
|
needs.changes.result != 'success' ||
|
||||||
needs.changes.outputs.app != 'false') }}
|
needs.changes.outputs.app != 'false') }}
|
||||||
@@ -352,7 +376,7 @@ jobs:
|
|||||||
sdk-tests:
|
sdk-tests:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: >-
|
if: >-
|
||||||
${{ always() &&
|
${{ !cancelled() &&
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
(github.event_name == 'workflow_dispatch' ||
|
||||||
needs.changes.result != 'success' ||
|
needs.changes.result != 'success' ||
|
||||||
needs.changes.outputs.sdk != 'false') }}
|
needs.changes.outputs.sdk != 'false') }}
|
||||||
@@ -383,11 +407,7 @@ jobs:
|
|||||||
|
|
||||||
playwright:
|
playwright:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: >-
|
if: ${{ !cancelled() }}
|
||||||
${{ always() &&
|
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.changes.result != 'success' ||
|
|
||||||
needs.changes.outputs.playwright != 'false') }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -401,43 +421,57 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
|
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
|
||||||
|
RUN_TESTS: >-
|
||||||
|
${{ github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.changes.result != 'success' ||
|
||||||
|
needs.changes.outputs.playwright != 'false' }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Skip unaffected Playwright tests
|
||||||
|
if: env.RUN_TESTS != 'true'
|
||||||
|
run: echo "No Playwright changes detected."
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install dependencies with retry
|
- name: Install dependencies with retry
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: node scripts/npm-retry.mjs ci
|
run: node scripts/npm-retry.mjs ci
|
||||||
- name: Install Playwright browsers
|
- name: Install Playwright browsers
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
run: npx playwright install chromium
|
run: npx playwright install chromium
|
||||||
|
|
||||||
- name: Build app dependencies
|
- name: Build app dependencies
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: npm run build:app-deps
|
run: npm run build:app-deps
|
||||||
|
|
||||||
- name: Build server stack
|
- name: Build server stack
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: npm run build:server
|
run: npm run build:server
|
||||||
|
|
||||||
- name: Install agent CLIs for provider tests
|
- name: Install agent CLIs for provider tests
|
||||||
if: ${{ !matrix.desktop }}
|
if: env.RUN_TESTS == 'true' && !matrix.desktop
|
||||||
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code @openai/codex@0.105.0 opencode-ai
|
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code @openai/codex@0.105.0 opencode-ai
|
||||||
|
|
||||||
- name: Run Playwright E2E tests
|
- name: Run Playwright E2E tests
|
||||||
if: ${{ !matrix.desktop }}
|
if: env.RUN_TESTS == 'true' && !matrix.desktop
|
||||||
run: npm run test:e2e --workspace=@getpaseo/app -- --shard=${{ matrix.shard }}/4
|
run: npm run test:e2e --workspace=@getpaseo/app -- --shard=${{ matrix.shard }}/4
|
||||||
env:
|
env:
|
||||||
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
||||||
|
|
||||||
- name: Run desktop-overlay Playwright tests
|
- name: Run desktop-overlay Playwright tests
|
||||||
if: ${{ matrix.desktop }}
|
if: env.RUN_TESTS == 'true' && matrix.desktop
|
||||||
run: npm run test:e2e:desktop --workspace=@getpaseo/app
|
run: npm run test:e2e:desktop --workspace=@getpaseo/app
|
||||||
|
|
||||||
- name: Upload test artifacts
|
- name: Upload test artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
if: failure()
|
if: env.RUN_TESTS == 'true' && failure()
|
||||||
with:
|
with:
|
||||||
name: playwright-results-${{ matrix.shard }}
|
name: playwright-results-${{ matrix.shard }}
|
||||||
path: |
|
path: |
|
||||||
@@ -448,7 +482,7 @@ jobs:
|
|||||||
relay-tests:
|
relay-tests:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: >-
|
if: >-
|
||||||
${{ always() &&
|
${{ !cancelled() &&
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
(github.event_name == 'workflow_dispatch' ||
|
||||||
needs.changes.result != 'success' ||
|
needs.changes.result != 'success' ||
|
||||||
needs.changes.outputs.relay != 'false') }}
|
needs.changes.outputs.relay != 'false') }}
|
||||||
@@ -474,11 +508,7 @@ jobs:
|
|||||||
|
|
||||||
cli-tests:
|
cli-tests:
|
||||||
needs: changes
|
needs: changes
|
||||||
if: >-
|
if: ${{ !cancelled() }}
|
||||||
${{ always() &&
|
|
||||||
(github.event_name == 'workflow_dispatch' ||
|
|
||||||
needs.changes.result != 'success' ||
|
|
||||||
needs.changes.outputs.cli != 'false') }}
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
@@ -487,24 +517,38 @@ jobs:
|
|||||||
name: cli-tests (shard ${{ matrix.shard }}/3)
|
name: cli-tests (shard ${{ matrix.shard }}/3)
|
||||||
env:
|
env:
|
||||||
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
|
ELECTRON_SKIP_BINARY_DOWNLOAD: "1"
|
||||||
|
RUN_TESTS: >-
|
||||||
|
${{ github.event_name == 'workflow_dispatch' ||
|
||||||
|
needs.changes.result != 'success' ||
|
||||||
|
needs.changes.outputs.cli != 'false' }}
|
||||||
steps:
|
steps:
|
||||||
|
- name: Skip unaffected CLI tests
|
||||||
|
if: env.RUN_TESTS != 'true'
|
||||||
|
run: echo "No CLI changes detected."
|
||||||
|
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
|
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
with:
|
with:
|
||||||
node-version: "22"
|
node-version: "22"
|
||||||
cache: "npm"
|
cache: "npm"
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: node scripts/npm-retry.mjs ci
|
run: node scripts/npm-retry.mjs ci
|
||||||
|
|
||||||
- name: Build server stack
|
- name: Build server stack
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: npm run build:server
|
run: npm run build:server
|
||||||
|
|
||||||
- name: Install agent CLIs for provider tests
|
- name: Install agent CLIs for provider tests
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code @openai/codex@0.105.0 opencode-ai
|
run: node scripts/npm-retry.mjs install -g @anthropic-ai/claude-code @openai/codex@0.105.0 opencode-ai
|
||||||
|
|
||||||
- name: Run CLI tests
|
- name: Run CLI tests
|
||||||
|
if: env.RUN_TESTS == 'true'
|
||||||
run: npm run test --workspace=@getpaseo/cli
|
run: npm run test --workspace=@getpaseo/cli
|
||||||
env:
|
env:
|
||||||
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0"
|
PASEO_LOCAL_SPEECH_AUTO_DOWNLOAD: "0"
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"speech:download": "tsx scripts/download-speech-models.ts",
|
"speech:download": "tsx scripts/download-speech-models.ts",
|
||||||
"speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts",
|
"speech:transcribe:local": "tsx scripts/transcribe-local-wav.ts",
|
||||||
"test": "npm run test:unit && npm run test:integration",
|
"test": "npm run test:unit && npm run test:integration",
|
||||||
"test:unit": "vitest run --exclude \"**/*.e2e.test.ts\"",
|
"test:unit": "vitest run --fileParallelism --exclude \"**/*.e2e.test.ts\"",
|
||||||
"test:integration": "vitest run --maxWorkers=1 src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts",
|
"test:integration": "vitest run --maxWorkers=1 src/server/daemon-e2e/models.e2e.test.ts src/server/daemon-e2e/live-preferences.e2e.test.ts src/server/agent/model-catalog.e2e.test.ts",
|
||||||
"test:integration:all": "npm run test:e2e",
|
"test:integration:all": "npm run test:e2e",
|
||||||
"test:integration:real": "vitest run real.e2e.test.ts",
|
"test:integration:real": "vitest run real.e2e.test.ts",
|
||||||
|
|||||||
@@ -253,7 +253,7 @@ function lastNonEmptyLineIsPrompt(state: ReturnType<TerminalSession["getState"]>
|
|||||||
}
|
}
|
||||||
|
|
||||||
function removeZshShellIntegrationRuntimeDir(): void {
|
function removeZshShellIntegrationRuntimeDir(): void {
|
||||||
rmSync(join(tmpdir(), `${userInfo().username || "unknown"}-paseo-zsh`), {
|
rmSync(join(tmpdir(), `${userInfo().username || "unknown"}-paseo-zsh-${process.pid}`), {
|
||||||
recursive: true,
|
recursive: true,
|
||||||
force: true,
|
force: true,
|
||||||
});
|
});
|
||||||
@@ -272,7 +272,9 @@ describe.skipIf(isPlatform("win32"))("terminal POSIX-only", () => {
|
|||||||
expect(resolvedEnv.TERM).toBe("xterm-256color");
|
expect(resolvedEnv.TERM).toBe("xterm-256color");
|
||||||
expect(resolvedEnv.TERM_PROGRAM).toBe("kitty");
|
expect(resolvedEnv.TERM_PROGRAM).toBe("kitty");
|
||||||
expect(resolvedEnv.PASEO_ZSH_ZDOTDIR).toBe("/tmp/paseo-zdotdir");
|
expect(resolvedEnv.PASEO_ZSH_ZDOTDIR).toBe("/tmp/paseo-zdotdir");
|
||||||
expect(resolvedEnv.ZDOTDIR).not.toBe("/tmp/paseo-zdotdir");
|
expect(resolvedEnv.ZDOTDIR).toBe(
|
||||||
|
join(tmpdir(), `${userInfo().username || "unknown"}-paseo-zsh-${process.pid}`),
|
||||||
|
);
|
||||||
expect(existsSync(join(resolvedEnv.ZDOTDIR, ".zshenv"))).toBe(true);
|
expect(existsSync(join(resolvedEnv.ZDOTDIR, ".zshenv"))).toBe(true);
|
||||||
expect(existsSync(join(resolvedEnv.ZDOTDIR, "paseo-integration.zsh"))).toBe(true);
|
expect(existsSync(join(resolvedEnv.ZDOTDIR, "paseo-integration.zsh"))).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -384,7 +384,7 @@ function resolveZshShellIntegrationRuntimeDir(): string {
|
|||||||
} catch {
|
} catch {
|
||||||
// keep fallback
|
// keep fallback
|
||||||
}
|
}
|
||||||
return join(tmpdir(), `${username}-paseo-zsh`);
|
return join(tmpdir(), `${username}-paseo-zsh-${process.pid}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareZshShellIntegrationRuntimeDir(sourceDir = resolveZshShellIntegrationDir()): string {
|
function prepareZshShellIntegrationRuntimeDir(sourceDir = resolveZshShellIntegrationDir()): string {
|
||||||
|
|||||||
57
scripts/ci-workflow.test.mjs
Normal file
57
scripts/ci-workflow.test.mjs
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { readFileSync } from "node:fs";
|
||||||
|
import test from "node:test";
|
||||||
|
|
||||||
|
const workflowPath = new URL("../.github/workflows/ci.yml", import.meta.url);
|
||||||
|
|
||||||
|
function jobBlocks(source) {
|
||||||
|
const jobs = new Map();
|
||||||
|
let currentJob;
|
||||||
|
|
||||||
|
for (const line of source.split("\n")) {
|
||||||
|
const jobMatch = /^ ([a-z0-9-]+):\s*$/.exec(line);
|
||||||
|
if (jobMatch) {
|
||||||
|
currentJob = jobMatch[1];
|
||||||
|
jobs.set(currentJob, []);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentJob && (/^ \S/.test(line) || /^ \S/.test(line))) {
|
||||||
|
jobs.get(currentJob).push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return jobs;
|
||||||
|
}
|
||||||
|
|
||||||
|
test("matrix jobs expand before change gating", () => {
|
||||||
|
const workflow = readFileSync(workflowPath, "utf8");
|
||||||
|
const gatedMatrixJobs = [...jobBlocks(workflow)]
|
||||||
|
.filter(([, lines]) => {
|
||||||
|
const hasMatrix = lines.some((line) => line.startsWith(" matrix:"));
|
||||||
|
const unsafeJobCondition = lines.some(
|
||||||
|
(line) => line.startsWith(" if:") && line.trim() !== "if: ${{ !cancelled() }}",
|
||||||
|
);
|
||||||
|
return hasMatrix && unsafeJobCondition;
|
||||||
|
})
|
||||||
|
.map(([jobId]) => jobId);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
gatedMatrixJobs,
|
||||||
|
[],
|
||||||
|
"change-based job conditions skip a matrix before GitHub can emit its interpolated check names",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("change gating allows superseded workflow runs to cancel", () => {
|
||||||
|
const workflow = readFileSync(workflowPath, "utf8");
|
||||||
|
const cancellationBlockingJobs = [...jobBlocks(workflow)]
|
||||||
|
.filter(([, lines]) => lines.some((line) => line.trim().startsWith("${{ always()")))
|
||||||
|
.map(([jobId]) => jobId);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
cancellationBlockingJobs,
|
||||||
|
[],
|
||||||
|
"always() keeps jobs alive after concurrency cancellation; use !cancelled() for fail-open gating",
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user