From b4e184aab383495bdd9785d9327c862f84c865f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Tue, 14 Apr 2026 09:55:38 +0900 Subject: [PATCH 1/8] fix(ci): prepare macOS resources before optional signing --- .github/workflows/build-desktop-tauri.yml | 11 ++++++-- .../ci/build-desktop-tauri-workflow.test.mjs | 28 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 scripts/ci/build-desktop-tauri-workflow.test.mjs diff --git a/.github/workflows/build-desktop-tauri.yml b/.github/workflows/build-desktop-tauri.yml index f19e3a3..64b46ab 100644 --- a/.github/workflows/build-desktop-tauri.yml +++ b/.github/workflows/build-desktop-tauri.yml @@ -315,8 +315,7 @@ jobs: printf 'signing_identity=%s\n' "$CERT_ID" >> "$GITHUB_OUTPUT" echo "Imported signing identity: $CERT_ID" - - name: Pre-sign backend resources (macOS) - if: ${{ steps.import_apple_certificate.outputs.signing_identity != '' }} + - name: Prepare desktop resources (macOS) env: ASTRBOT_SOURCE_GIT_URL: ${{ needs.resolve_build_context.outputs.source_git_url }} ASTRBOT_SOURCE_GIT_REF: ${{ needs.resolve_build_context.outputs.source_git_ref }} @@ -332,6 +331,12 @@ jobs: echo "::error::resources/backend not found after prepare:resources." >&2 exit 1 fi + + - name: Pre-sign backend resources (macOS) + if: ${{ steps.import_apple_certificate.outputs.signing_identity != '' }} + shell: bash + run: | + set -euo pipefail echo "Pre-signing Mach-O binaries in resources/backend..." bash scripts/ci/codesign-macos-nested.sh "resources/backend" "src-tauri/entitlements.plist" @@ -380,7 +385,7 @@ jobs: max_attempts="${ASTRBOT_MACOS_BUILD_MAX_ATTEMPTS}" retry_sleep_seconds="${ASTRBOT_MACOS_BUILD_RETRY_SLEEP_SECONDS}" - # Resources are already prepared and pre-signed in the previous step. + # Resources are already prepared and, when available, pre-signed in earlier steps. tauri_config_override='{"build":{"beforeBuildCommand":""}}' # Retry only for known transient cargo/crates network failures. # Spurious retry hints emitted by cargo. diff --git a/scripts/ci/build-desktop-tauri-workflow.test.mjs b/scripts/ci/build-desktop-tauri-workflow.test.mjs new file mode 100644 index 0000000..35725be --- /dev/null +++ b/scripts/ci/build-desktop-tauri-workflow.test.mjs @@ -0,0 +1,28 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const projectRoot = path.resolve(scriptDir, '..', '..'); +const workflowPath = path.join(projectRoot, '.github', 'workflows', 'build-desktop-tauri.yml'); + +const readWorkflow = async () => readFile(workflowPath, 'utf8'); + +test('macOS workflow prepares resources before any conditional pre-signing', async () => { + const workflow = await readWorkflow(); + + assert.match( + workflow, + /- name: Prepare desktop resources \(macOS\)[\s\S]*?run: \|[\s\S]*?pnpm run prepare:resources/, + ); + assert.doesNotMatch( + workflow, + /- name: Pre-sign backend resources \(macOS\)[\s\S]*?run: \|[\s\S]*?pnpm run prepare:resources/, + ); + assert.match( + workflow, + /Build desktop app bundle \(macOS\)[\s\S]*?# Resources are already prepared and, when available, pre-signed in earlier steps\./, + ); +}); From c990ac9ce687d9a7ea4d177028d4b7659177d437 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Tue, 14 Apr 2026 10:03:44 +0900 Subject: [PATCH 2/8] test(ci): parse macOS workflow steps structurally --- .../ci/build-desktop-tauri-workflow.test.mjs | 146 ++++++++++++++++-- 1 file changed, 135 insertions(+), 11 deletions(-) diff --git a/scripts/ci/build-desktop-tauri-workflow.test.mjs b/scripts/ci/build-desktop-tauri-workflow.test.mjs index 35725be..0f88ec6 100644 --- a/scripts/ci/build-desktop-tauri-workflow.test.mjs +++ b/scripts/ci/build-desktop-tauri-workflow.test.mjs @@ -10,19 +10,143 @@ const workflowPath = path.join(projectRoot, '.github', 'workflows', 'build-deskt const readWorkflow = async () => readFile(workflowPath, 'utf8'); -test('macOS workflow prepares resources before any conditional pre-signing', async () => { +const TOP_LEVEL_JOB_PATTERN = /^ [A-Za-z0-9_-]+:\s*$/; +const MACOS_STEP_PREFIX = ' - '; +const STEP_FIELD_PREFIX = ' '; +const RUN_BLOCK_PREFIX = ' '; + +const extractWorkflowJobSteps = (workflow, jobName) => { + const lines = workflow.split(/\r?\n/); + const jobHeader = ` ${jobName}:`; + const jobStart = lines.findIndex((line) => line === jobHeader); + assert.notEqual(jobStart, -1, `Expected workflow job ${jobName} to exist.`); + + let stepsStart = -1; + let jobEnd = lines.length; + for (let index = jobStart + 1; index < lines.length; index += 1) { + const line = lines[index]; + + if (stepsStart === -1 && line === ' steps:') { + stepsStart = index; + continue; + } + + if (TOP_LEVEL_JOB_PATTERN.test(line)) { + jobEnd = index; + break; + } + } + + assert.notEqual(stepsStart, -1, `Expected workflow job ${jobName} to define steps.`); + + const steps = []; + let currentStep = null; + let collectingRunBlock = false; + + const finalizeCurrentStep = () => { + if (!currentStep) { + return; + } + + currentStep.run = currentStep.runLines.join('\n').trimEnd(); + delete currentStep.runLines; + steps.push(currentStep); + currentStep = null; + collectingRunBlock = false; + }; + + for (let index = stepsStart + 1; index < jobEnd; index += 1) { + const line = lines[index]; + + if (line.startsWith(MACOS_STEP_PREFIX)) { + finalizeCurrentStep(); + currentStep = { + name: null, + if: null, + run: '', + runLines: [], + }; + + const inlineName = line.trim().match(/^- name:\s*(.+)$/); + if (inlineName) { + currentStep.name = inlineName[1]; + } + continue; + } + + if (!currentStep) { + continue; + } + + if (collectingRunBlock) { + if (line.startsWith(RUN_BLOCK_PREFIX)) { + currentStep.runLines.push(line.slice(RUN_BLOCK_PREFIX.length)); + continue; + } + if (line.trim() === '') { + currentStep.runLines.push(''); + continue; + } + collectingRunBlock = false; + } + + if (!line.startsWith(STEP_FIELD_PREFIX)) { + continue; + } + + const trimmed = line.trim(); + if (trimmed.startsWith('name: ')) { + currentStep.name = trimmed.slice('name: '.length); + continue; + } + if (trimmed.startsWith('if: ')) { + currentStep.if = trimmed.slice('if: '.length); + continue; + } + if (trimmed === 'run: |') { + collectingRunBlock = true; + continue; + } + if (trimmed.startsWith('run: ')) { + currentStep.run = trimmed.slice('run: '.length); + } + } + + finalizeCurrentStep(); + return steps; +}; + +const findStepByName = (steps, stepName) => { + const step = steps.find((candidate) => candidate.name === stepName); + assert.ok(step, `Expected workflow step ${stepName} to exist.`); + return step; +}; + +test('macOS workflow exposes structured build-macos steps', async () => { const workflow = await readWorkflow(); + const steps = extractWorkflowJobSteps(workflow, 'build-macos'); + + assert.ok(findStepByName(steps, 'Prepare desktop resources (macOS)')); + assert.ok(findStepByName(steps, 'Pre-sign backend resources (macOS)')); + assert.ok(findStepByName(steps, 'Build desktop app bundle (macOS)')); +}); + +test('macOS workflow prepares resources before optional pre-signing', async () => { + const workflow = await readWorkflow(); + const steps = extractWorkflowJobSteps(workflow, 'build-macos'); + const prepareStep = findStepByName(steps, 'Prepare desktop resources (macOS)'); + const preSignStep = findStepByName(steps, 'Pre-sign backend resources (macOS)'); + const buildStep = findStepByName(steps, 'Build desktop app bundle (macOS)'); + + assert.equal(prepareStep.if, null); + assert.match(prepareStep.run, /pnpm run prepare:resources/); + assert.match(prepareStep.run, /resources\/backend not found after prepare:resources/); + + assert.equal(preSignStep.if, "${{ steps.import_apple_certificate.outputs.signing_identity != '' }}"); + assert.doesNotMatch(preSignStep.run, /pnpm run prepare:resources/); assert.match( - workflow, - /- name: Prepare desktop resources \(macOS\)[\s\S]*?run: \|[\s\S]*?pnpm run prepare:resources/, - ); - assert.doesNotMatch( - workflow, - /- name: Pre-sign backend resources \(macOS\)[\s\S]*?run: \|[\s\S]*?pnpm run prepare:resources/, - ); - assert.match( - workflow, - /Build desktop app bundle \(macOS\)[\s\S]*?# Resources are already prepared and, when available, pre-signed in earlier steps\./, + buildStep.run, + /# Resources are already prepared and, when available, pre-signed in earlier steps\./, ); }); From 17e9d2410c58729542e16cfb69924635c4d6b973 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Tue, 14 Apr 2026 10:10:11 +0900 Subject: [PATCH 3/8] test(ci): parse workflow YAML structurally --- package.json | 3 + pnpm-lock.yaml | 17 ++- .../ci/build-desktop-tauri-workflow.test.mjs | 128 +++--------------- 3 files changed, 37 insertions(+), 111 deletions(-) diff --git a/package.json b/package.json index 7ebd168..5d97fdc 100644 --- a/package.json +++ b/package.json @@ -12,5 +12,8 @@ "prepare:resources": "pnpm run prepare:webui && pnpm run prepare:backend", "dev": "cargo tauri dev", "build": "cargo tauri build" + }, + "devDependencies": { + "yaml": "^2.8.1" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b60ae1..6c18f64 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,4 +6,19 @@ settings: importers: - .: {} + .: + devDependencies: + yaml: + specifier: ^2.8.1 + version: 2.8.3 + +packages: + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + +snapshots: + + yaml@2.8.3: {} diff --git a/scripts/ci/build-desktop-tauri-workflow.test.mjs b/scripts/ci/build-desktop-tauri-workflow.test.mjs index 0f88ec6..c188caa 100644 --- a/scripts/ci/build-desktop-tauri-workflow.test.mjs +++ b/scripts/ci/build-desktop-tauri-workflow.test.mjs @@ -3,117 +3,23 @@ import path from 'node:path'; import { readFile } from 'node:fs/promises'; import { test } from 'node:test'; import { fileURLToPath } from 'node:url'; +import { parse } from 'yaml'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(scriptDir, '..', '..'); const workflowPath = path.join(projectRoot, '.github', 'workflows', 'build-desktop-tauri.yml'); -const readWorkflow = async () => readFile(workflowPath, 'utf8'); - -const TOP_LEVEL_JOB_PATTERN = /^ [A-Za-z0-9_-]+:\s*$/; -const MACOS_STEP_PREFIX = ' - '; -const STEP_FIELD_PREFIX = ' '; -const RUN_BLOCK_PREFIX = ' '; - -const extractWorkflowJobSteps = (workflow, jobName) => { - const lines = workflow.split(/\r?\n/); - const jobHeader = ` ${jobName}:`; - const jobStart = lines.findIndex((line) => line === jobHeader); - assert.notEqual(jobStart, -1, `Expected workflow job ${jobName} to exist.`); - - let stepsStart = -1; - let jobEnd = lines.length; - for (let index = jobStart + 1; index < lines.length; index += 1) { - const line = lines[index]; - - if (stepsStart === -1 && line === ' steps:') { - stepsStart = index; - continue; - } - - if (TOP_LEVEL_JOB_PATTERN.test(line)) { - jobEnd = index; - break; - } - } - - assert.notEqual(stepsStart, -1, `Expected workflow job ${jobName} to define steps.`); - - const steps = []; - let currentStep = null; - let collectingRunBlock = false; - - const finalizeCurrentStep = () => { - if (!currentStep) { - return; - } - - currentStep.run = currentStep.runLines.join('\n').trimEnd(); - delete currentStep.runLines; - steps.push(currentStep); - currentStep = null; - collectingRunBlock = false; - }; - - for (let index = stepsStart + 1; index < jobEnd; index += 1) { - const line = lines[index]; - - if (line.startsWith(MACOS_STEP_PREFIX)) { - finalizeCurrentStep(); - currentStep = { - name: null, - if: null, - run: '', - runLines: [], - }; - - const inlineName = line.trim().match(/^- name:\s*(.+)$/); - if (inlineName) { - currentStep.name = inlineName[1]; - } - continue; - } - - if (!currentStep) { - continue; - } - - if (collectingRunBlock) { - if (line.startsWith(RUN_BLOCK_PREFIX)) { - currentStep.runLines.push(line.slice(RUN_BLOCK_PREFIX.length)); - continue; - } - if (line.trim() === '') { - currentStep.runLines.push(''); - continue; - } - collectingRunBlock = false; - } - - if (!line.startsWith(STEP_FIELD_PREFIX)) { - continue; - } - - const trimmed = line.trim(); - if (trimmed.startsWith('name: ')) { - currentStep.name = trimmed.slice('name: '.length); - continue; - } - if (trimmed.startsWith('if: ')) { - currentStep.if = trimmed.slice('if: '.length); - continue; - } - if (trimmed === 'run: |') { - collectingRunBlock = true; - continue; - } - if (trimmed.startsWith('run: ')) { - currentStep.run = trimmed.slice('run: '.length); - } - } +const readWorkflowObject = async () => { + const content = await readFile(workflowPath, 'utf8'); + return parse(content); +}; - finalizeCurrentStep(); - return steps; +const extractWorkflowJobSteps = (workflowObject, jobName) => { + assert.ok(workflowObject.jobs, 'Expected workflow to define jobs.'); + const job = workflowObject.jobs[jobName]; + assert.ok(job, `Expected workflow job ${jobName} to exist.`); + assert.ok(Array.isArray(job.steps), `Expected workflow job ${jobName} to define steps.`); + return job.steps; }; const findStepByName = (steps, stepName) => { @@ -123,8 +29,8 @@ const findStepByName = (steps, stepName) => { }; test('macOS workflow exposes structured build-macos steps', async () => { - const workflow = await readWorkflow(); - const steps = extractWorkflowJobSteps(workflow, 'build-macos'); + const workflowObject = await readWorkflowObject(); + const steps = extractWorkflowJobSteps(workflowObject, 'build-macos'); assert.ok(findStepByName(steps, 'Prepare desktop resources (macOS)')); assert.ok(findStepByName(steps, 'Pre-sign backend resources (macOS)')); @@ -132,19 +38,21 @@ test('macOS workflow exposes structured build-macos steps', async () => { }); test('macOS workflow prepares resources before optional pre-signing', async () => { - const workflow = await readWorkflow(); - const steps = extractWorkflowJobSteps(workflow, 'build-macos'); + const workflowObject = await readWorkflowObject(); + const steps = extractWorkflowJobSteps(workflowObject, 'build-macos'); const prepareStep = findStepByName(steps, 'Prepare desktop resources (macOS)'); const preSignStep = findStepByName(steps, 'Pre-sign backend resources (macOS)'); const buildStep = findStepByName(steps, 'Build desktop app bundle (macOS)'); - assert.equal(prepareStep.if, null); + assert.equal(prepareStep.if, undefined); assert.match(prepareStep.run, /pnpm run prepare:resources/); assert.match(prepareStep.run, /resources\/backend not found after prepare:resources/); assert.equal(preSignStep.if, "${{ steps.import_apple_certificate.outputs.signing_identity != '' }}"); assert.doesNotMatch(preSignStep.run, /pnpm run prepare:resources/); + assert.ok(steps.indexOf(prepareStep) < steps.indexOf(preSignStep)); + assert.ok(steps.indexOf(preSignStep) < steps.indexOf(buildStep)); assert.match( buildStep.run, /# Resources are already prepared and, when available, pre-signed in earlier steps\./, From fb4bab4056d4a5cb2067fab7b489ad9e76bd885c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Tue, 14 Apr 2026 10:18:30 +0900 Subject: [PATCH 4/8] test(ci): relax workflow assertions and install test deps --- .github/workflows/check-scripts.yml | 14 ++++- .../ci/build-desktop-tauri-workflow.test.mjs | 36 +++++++++---- scripts/ci/check-scripts-workflow.test.mjs | 53 +++++++++++++++++++ 3 files changed, 90 insertions(+), 13 deletions(-) create mode 100644 scripts/ci/check-scripts-workflow.test.mjs diff --git a/.github/workflows/check-scripts.yml b/.github/workflows/check-scripts.yml index 9ed7c27..50cd391 100644 --- a/.github/workflows/check-scripts.yml +++ b/.github/workflows/check-scripts.yml @@ -24,6 +24,11 @@ jobs: node-version: '20' python-version: '3.12' + - name: Setup pnpm + uses: pnpm/action-setup@v4.4.0 + with: + version: 10.28.2 + - name: Check Node scripts syntax run: | set -euo pipefail @@ -32,10 +37,15 @@ jobs: exit 0 fi while IFS= read -r -d '' file; do - echo "Checking ${file}" - node --check "${file}" + echo "Checking ${file}" + node --check "${file}" done < <(find scripts -type f \( -name '*.mjs' -o -name '*.cjs' -o -name '*.js' \) -print0 | sort -z) + - name: Install Node test dependencies + run: | + set -euo pipefail + pnpm install --frozen-lockfile + - name: Run Node script behavior tests run: | set -euo pipefail diff --git a/scripts/ci/build-desktop-tauri-workflow.test.mjs b/scripts/ci/build-desktop-tauri-workflow.test.mjs index c188caa..f9a3035 100644 --- a/scripts/ci/build-desktop-tauri-workflow.test.mjs +++ b/scripts/ci/build-desktop-tauri-workflow.test.mjs @@ -22,39 +22,53 @@ const extractWorkflowJobSteps = (workflowObject, jobName) => { return job.steps; }; -const findStepByName = (steps, stepName) => { - const step = steps.find((candidate) => candidate.name === stepName); - assert.ok(step, `Expected workflow step ${stepName} to exist.`); +const findStepByName = (steps, stepNameOrPattern) => { + const matcher = + stepNameOrPattern instanceof RegExp + ? (candidateName) => stepNameOrPattern.test(candidateName ?? '') + : (candidateName) => (candidateName ?? '').includes(stepNameOrPattern); + const step = steps.find((candidate) => matcher(candidate.name)); + assert.ok(step, `Expected workflow step ${String(stepNameOrPattern)} to exist.`); return step; }; +test('findStepByName supports substring and regex matching', () => { + const steps = [ + { name: 'Prepare desktop resources (macOS) [unsigned-compatible]' }, + { name: 'Build desktop app bundle (macOS) release artifacts' }, + ]; + + assert.equal(findStepByName(steps, 'Prepare desktop resources (macOS)'), steps[0]); + assert.equal(findStepByName(steps, /Build desktop app bundle \(macOS\)/), steps[1]); +}); + test('macOS workflow exposes structured build-macos steps', async () => { const workflowObject = await readWorkflowObject(); const steps = extractWorkflowJobSteps(workflowObject, 'build-macos'); - assert.ok(findStepByName(steps, 'Prepare desktop resources (macOS)')); - assert.ok(findStepByName(steps, 'Pre-sign backend resources (macOS)')); - assert.ok(findStepByName(steps, 'Build desktop app bundle (macOS)')); + assert.ok(findStepByName(steps, 'Prepare desktop resources')); + assert.ok(findStepByName(steps, 'Pre-sign backend resources')); + assert.ok(findStepByName(steps, 'Build desktop app bundle')); }); test('macOS workflow prepares resources before optional pre-signing', async () => { const workflowObject = await readWorkflowObject(); const steps = extractWorkflowJobSteps(workflowObject, 'build-macos'); - const prepareStep = findStepByName(steps, 'Prepare desktop resources (macOS)'); - const preSignStep = findStepByName(steps, 'Pre-sign backend resources (macOS)'); - const buildStep = findStepByName(steps, 'Build desktop app bundle (macOS)'); + const prepareStep = findStepByName(steps, 'Prepare desktop resources'); + const preSignStep = findStepByName(steps, 'Pre-sign backend resources'); + const buildStep = findStepByName(steps, 'Build desktop app bundle'); assert.equal(prepareStep.if, undefined); assert.match(prepareStep.run, /pnpm run prepare:resources/); assert.match(prepareStep.run, /resources\/backend not found after prepare:resources/); - assert.equal(preSignStep.if, "${{ steps.import_apple_certificate.outputs.signing_identity != '' }}"); + assert.match(preSignStep.if ?? '', /import_apple_certificate\.outputs\.signing_identity/); assert.doesNotMatch(preSignStep.run, /pnpm run prepare:resources/); assert.ok(steps.indexOf(prepareStep) < steps.indexOf(preSignStep)); assert.ok(steps.indexOf(preSignStep) < steps.indexOf(buildStep)); assert.match( buildStep.run, - /# Resources are already prepared and, when available, pre-signed in earlier steps\./, + /Resources are already prepared/, ); }); diff --git a/scripts/ci/check-scripts-workflow.test.mjs b/scripts/ci/check-scripts-workflow.test.mjs new file mode 100644 index 0000000..82d7559 --- /dev/null +++ b/scripts/ci/check-scripts-workflow.test.mjs @@ -0,0 +1,53 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { test } from 'node:test'; +import { fileURLToPath } from 'node:url'; +import { parse } from 'yaml'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const projectRoot = path.resolve(scriptDir, '..', '..'); +const workflowPath = path.join(projectRoot, '.github', 'workflows', 'check-scripts.yml'); + +const readWorkflowObject = async () => { + const content = await readFile(workflowPath, 'utf8'); + return parse(content); +}; + +const extractWorkflowJobSteps = (workflowObject, jobName) => { + assert.ok(workflowObject.jobs, 'Expected workflow to define jobs.'); + const job = workflowObject.jobs[jobName]; + assert.ok(job, `Expected workflow job ${jobName} to exist.`); + assert.ok(Array.isArray(job.steps), `Expected workflow job ${jobName} to define steps.`); + return job.steps; +}; + +const findStepIndex = (steps, predicate, label) => { + const index = steps.findIndex(predicate); + assert.notEqual(index, -1, `Expected workflow step ${label} to exist.`); + return index; +}; + +test('check-scripts workflow installs node dependencies before running node tests', async () => { + const workflowObject = await readWorkflowObject(); + const steps = extractWorkflowJobSteps(workflowObject, 'scripts'); + + const pnpmSetupIndex = findStepIndex( + steps, + (step) => (step.uses ?? '').includes('pnpm/action-setup'), + 'using pnpm/action-setup', + ); + const installIndex = findStepIndex( + steps, + (step) => /pnpm install/.test(step.run ?? ''), + 'installing node dependencies', + ); + const nodeTestIndex = findStepIndex( + steps, + (step) => /node --test/.test(step.run ?? ''), + 'running Node script behavior tests', + ); + + assert.ok(pnpmSetupIndex < installIndex); + assert.ok(installIndex < nodeTestIndex); +}); From 3d3fbd33bc0170905a1243670ccd24a3dc10c6a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Tue, 14 Apr 2026 10:32:04 +0900 Subject: [PATCH 5/8] test(ci): share workflow test helpers --- .github/workflows/check-scripts.yml | 4 +- .../ci/build-desktop-tauri-workflow.test.mjs | 74 +++++++------------ scripts/ci/check-scripts-workflow.test.mjs | 37 +++------- scripts/ci/workflow-test-utils.mjs | 35 +++++++++ 4 files changed, 73 insertions(+), 77 deletions(-) create mode 100644 scripts/ci/workflow-test-utils.mjs diff --git a/.github/workflows/check-scripts.yml b/.github/workflows/check-scripts.yml index 50cd391..c8e3b4e 100644 --- a/.github/workflows/check-scripts.yml +++ b/.github/workflows/check-scripts.yml @@ -37,8 +37,8 @@ jobs: exit 0 fi while IFS= read -r -d '' file; do - echo "Checking ${file}" - node --check "${file}" + echo "Checking ${file}" + node --check "${file}" done < <(find scripts -type f \( -name '*.mjs' -o -name '*.cjs' -o -name '*.js' \) -print0 | sort -z) - name: Install Node test dependencies diff --git a/scripts/ci/build-desktop-tauri-workflow.test.mjs b/scripts/ci/build-desktop-tauri-workflow.test.mjs index f9a3035..20e094a 100644 --- a/scripts/ci/build-desktop-tauri-workflow.test.mjs +++ b/scripts/ci/build-desktop-tauri-workflow.test.mjs @@ -1,62 +1,42 @@ import assert from 'node:assert/strict'; -import path from 'node:path'; -import { readFile } from 'node:fs/promises'; import { test } from 'node:test'; -import { fileURLToPath } from 'node:url'; -import { parse } from 'yaml'; - -const scriptDir = path.dirname(fileURLToPath(import.meta.url)); -const projectRoot = path.resolve(scriptDir, '..', '..'); -const workflowPath = path.join(projectRoot, '.github', 'workflows', 'build-desktop-tauri.yml'); - -const readWorkflowObject = async () => { - const content = await readFile(workflowPath, 'utf8'); - return parse(content); -}; - -const extractWorkflowJobSteps = (workflowObject, jobName) => { - assert.ok(workflowObject.jobs, 'Expected workflow to define jobs.'); - const job = workflowObject.jobs[jobName]; - assert.ok(job, `Expected workflow job ${jobName} to exist.`); - assert.ok(Array.isArray(job.steps), `Expected workflow job ${jobName} to define steps.`); - return job.steps; -}; - -const findStepByName = (steps, stepNameOrPattern) => { - const matcher = - stepNameOrPattern instanceof RegExp - ? (candidateName) => stepNameOrPattern.test(candidateName ?? '') - : (candidateName) => (candidateName ?? '').includes(stepNameOrPattern); - const step = steps.find((candidate) => matcher(candidate.name)); - assert.ok(step, `Expected workflow step ${String(stepNameOrPattern)} to exist.`); - return step; -}; - -test('findStepByName supports substring and regex matching', () => { +import { + extractWorkflowJobSteps, + findStep, + readWorkflowObject, +} from './workflow-test-utils.mjs'; + +const WORKFLOW_FILE = 'build-desktop-tauri.yml'; +const BUILD_MACOS_JOB = 'build-macos'; +const PREPARE_RESOURCES_RUN = /pnpm run prepare:resources/; +const PRESIGN_BACKEND_RUN = /codesign-macos-nested\.sh\s+"resources\/backend"/; +const BUILD_APP_BUNDLE_RUN = /cargo tauri build --verbose --target/; + +test('findStep supports predicate and regex matching', () => { const steps = [ - { name: 'Prepare desktop resources (macOS) [unsigned-compatible]' }, - { name: 'Build desktop app bundle (macOS) release artifacts' }, + { name: 'Prepare desktop resources (macOS) [unsigned-compatible]', run: 'pnpm run prepare:resources' }, + { name: 'Build desktop app bundle (macOS) release artifacts', run: 'cargo tauri build --verbose --target x86_64-apple-darwin' }, ]; - assert.equal(findStepByName(steps, 'Prepare desktop resources (macOS)'), steps[0]); - assert.equal(findStepByName(steps, /Build desktop app bundle \(macOS\)/), steps[1]); + assert.equal(findStep(steps, 'prepare resources run', (step) => PREPARE_RESOURCES_RUN.test(step.run ?? '')), steps[0]); + assert.equal(findStep(steps, /Build desktop app bundle/, (step) => BUILD_APP_BUNDLE_RUN.test(step.run ?? '')), steps[1]); }); test('macOS workflow exposes structured build-macos steps', async () => { - const workflowObject = await readWorkflowObject(); - const steps = extractWorkflowJobSteps(workflowObject, 'build-macos'); + const workflowObject = await readWorkflowObject(WORKFLOW_FILE); + const steps = extractWorkflowJobSteps(workflowObject, BUILD_MACOS_JOB); - assert.ok(findStepByName(steps, 'Prepare desktop resources')); - assert.ok(findStepByName(steps, 'Pre-sign backend resources')); - assert.ok(findStepByName(steps, 'Build desktop app bundle')); + assert.ok(findStep(steps, 'prepare resources step', (step) => PREPARE_RESOURCES_RUN.test(step.run ?? ''))); + assert.ok(findStep(steps, 'pre-sign resources step', (step) => PRESIGN_BACKEND_RUN.test(step.run ?? ''))); + assert.ok(findStep(steps, 'build app bundle step', (step) => BUILD_APP_BUNDLE_RUN.test(step.run ?? ''))); }); test('macOS workflow prepares resources before optional pre-signing', async () => { - const workflowObject = await readWorkflowObject(); - const steps = extractWorkflowJobSteps(workflowObject, 'build-macos'); - const prepareStep = findStepByName(steps, 'Prepare desktop resources'); - const preSignStep = findStepByName(steps, 'Pre-sign backend resources'); - const buildStep = findStepByName(steps, 'Build desktop app bundle'); + const workflowObject = await readWorkflowObject(WORKFLOW_FILE); + const steps = extractWorkflowJobSteps(workflowObject, BUILD_MACOS_JOB); + const prepareStep = findStep(steps, 'prepare resources step', (step) => PREPARE_RESOURCES_RUN.test(step.run ?? '')); + const preSignStep = findStep(steps, 'pre-sign resources step', (step) => PRESIGN_BACKEND_RUN.test(step.run ?? '')); + const buildStep = findStep(steps, 'build app bundle step', (step) => BUILD_APP_BUNDLE_RUN.test(step.run ?? '')); assert.equal(prepareStep.if, undefined); assert.match(prepareStep.run, /pnpm run prepare:resources/); diff --git a/scripts/ci/check-scripts-workflow.test.mjs b/scripts/ci/check-scripts-workflow.test.mjs index 82d7559..2518a75 100644 --- a/scripts/ci/check-scripts-workflow.test.mjs +++ b/scripts/ci/check-scripts-workflow.test.mjs @@ -1,36 +1,17 @@ import assert from 'node:assert/strict'; -import path from 'node:path'; -import { readFile } from 'node:fs/promises'; import { test } from 'node:test'; -import { fileURLToPath } from 'node:url'; -import { parse } from 'yaml'; +import { + extractWorkflowJobSteps, + findStepIndex, + readWorkflowObject, +} from './workflow-test-utils.mjs'; -const scriptDir = path.dirname(fileURLToPath(import.meta.url)); -const projectRoot = path.resolve(scriptDir, '..', '..'); -const workflowPath = path.join(projectRoot, '.github', 'workflows', 'check-scripts.yml'); - -const readWorkflowObject = async () => { - const content = await readFile(workflowPath, 'utf8'); - return parse(content); -}; - -const extractWorkflowJobSteps = (workflowObject, jobName) => { - assert.ok(workflowObject.jobs, 'Expected workflow to define jobs.'); - const job = workflowObject.jobs[jobName]; - assert.ok(job, `Expected workflow job ${jobName} to exist.`); - assert.ok(Array.isArray(job.steps), `Expected workflow job ${jobName} to define steps.`); - return job.steps; -}; - -const findStepIndex = (steps, predicate, label) => { - const index = steps.findIndex(predicate); - assert.notEqual(index, -1, `Expected workflow step ${label} to exist.`); - return index; -}; +const WORKFLOW_FILE = 'check-scripts.yml'; +const SCRIPTS_JOB = 'scripts'; test('check-scripts workflow installs node dependencies before running node tests', async () => { - const workflowObject = await readWorkflowObject(); - const steps = extractWorkflowJobSteps(workflowObject, 'scripts'); + const workflowObject = await readWorkflowObject(WORKFLOW_FILE); + const steps = extractWorkflowJobSteps(workflowObject, SCRIPTS_JOB); const pnpmSetupIndex = findStepIndex( steps, diff --git a/scripts/ci/workflow-test-utils.mjs b/scripts/ci/workflow-test-utils.mjs new file mode 100644 index 0000000..71778da --- /dev/null +++ b/scripts/ci/workflow-test-utils.mjs @@ -0,0 +1,35 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { readFile } from 'node:fs/promises'; +import { fileURLToPath } from 'node:url'; +import { parse } from 'yaml'; + +const scriptDir = path.dirname(fileURLToPath(import.meta.url)); +const projectRoot = path.resolve(scriptDir, '..', '..'); +const workflowsDir = path.join(projectRoot, '.github', 'workflows'); + +export const readWorkflowObject = async (workflowFileName) => { + const workflowPath = path.join(workflowsDir, workflowFileName); + const content = await readFile(workflowPath, 'utf8'); + return parse(content); +}; + +export const extractWorkflowJobSteps = (workflowObject, jobName) => { + assert.ok(workflowObject.jobs, 'Expected workflow to define jobs.'); + const job = workflowObject.jobs[jobName]; + assert.ok(job, `Expected workflow job ${jobName} to exist.`); + assert.ok(Array.isArray(job.steps), `Expected workflow job ${jobName} to define steps.`); + return job.steps; +}; + +export const findStep = (steps, label, predicate) => { + const step = steps.find(predicate); + assert.ok(step, `Expected workflow step ${String(label)} to exist.`); + return step; +}; + +export const findStepIndex = (steps, predicate, label) => { + const index = steps.findIndex(predicate); + assert.notEqual(index, -1, `Expected workflow step ${String(label)} to exist.`); + return index; +}; From 5468c0604f8ccf649a9d0a736351430b626fb764 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Tue, 14 Apr 2026 10:42:46 +0900 Subject: [PATCH 6/8] test(ci): harden script workflow dependency setup --- .github/actions/setup-toolchains/action.yml | 10 ++++++++ .github/workflows/check-scripts.yml | 4 ++- scripts/ci/check-scripts-workflow.test.mjs | 17 ++++++++++++- scripts/ci/setup-toolchains-action.test.mjs | 27 +++++++++++++++++++++ scripts/ci/workflow-test-utils.mjs | 27 +++++++++++++++++++-- 5 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 scripts/ci/setup-toolchains-action.test.mjs diff --git a/.github/actions/setup-toolchains/action.yml b/.github/actions/setup-toolchains/action.yml index 8944eb0..4f9b4ab 100644 --- a/.github/actions/setup-toolchains/action.yml +++ b/.github/actions/setup-toolchains/action.yml @@ -10,6 +10,14 @@ inputs: description: Node.js version. required: false default: '20' + node-cache: + description: Optional actions/setup-node cache backend. + required: false + default: '' + node-cache-dependency-path: + description: Optional dependency file path used by actions/setup-node cache. + required: false + default: '' setup-python: description: Whether to setup Python. required: false @@ -27,6 +35,8 @@ runs: uses: actions/setup-node@v6.2.0 with: node-version: ${{ inputs.node-version }} + cache: ${{ inputs.node-cache }} + cache-dependency-path: ${{ inputs.node-cache-dependency-path }} - name: Setup Python if: ${{ inputs.setup-python == 'true' }} diff --git a/.github/workflows/check-scripts.yml b/.github/workflows/check-scripts.yml index c8e3b4e..14ca1e1 100644 --- a/.github/workflows/check-scripts.yml +++ b/.github/workflows/check-scripts.yml @@ -22,6 +22,8 @@ jobs: uses: ./.github/actions/setup-toolchains with: node-version: '20' + node-cache: 'pnpm' + node-cache-dependency-path: pnpm-lock.yaml python-version: '3.12' - name: Setup pnpm @@ -44,7 +46,7 @@ jobs: - name: Install Node test dependencies run: | set -euo pipefail - pnpm install --frozen-lockfile + pnpm install --frozen-lockfile --ignore-scripts - name: Run Node script behavior tests run: | diff --git a/scripts/ci/check-scripts-workflow.test.mjs b/scripts/ci/check-scripts-workflow.test.mjs index 2518a75..30c4405 100644 --- a/scripts/ci/check-scripts-workflow.test.mjs +++ b/scripts/ci/check-scripts-workflow.test.mjs @@ -2,6 +2,7 @@ import assert from 'node:assert/strict'; import { test } from 'node:test'; import { extractWorkflowJobSteps, + findStep, findStepIndex, readWorkflowObject, } from './workflow-test-utils.mjs'; @@ -12,6 +13,16 @@ const SCRIPTS_JOB = 'scripts'; test('check-scripts workflow installs node dependencies before running node tests', async () => { const workflowObject = await readWorkflowObject(WORKFLOW_FILE); const steps = extractWorkflowJobSteps(workflowObject, SCRIPTS_JOB); + const setupToolchainsStep = findStep( + steps, + 'setup toolchains step', + (step) => (step.uses ?? '').includes('./.github/actions/setup-toolchains'), + ); + const installStep = findStep( + steps, + 'installing node dependencies', + (step) => /pnpm install/.test(step.run ?? ''), + ); const pnpmSetupIndex = findStepIndex( steps, @@ -20,7 +31,7 @@ test('check-scripts workflow installs node dependencies before running node test ); const installIndex = findStepIndex( steps, - (step) => /pnpm install/.test(step.run ?? ''), + (step) => step === installStep, 'installing node dependencies', ); const nodeTestIndex = findStepIndex( @@ -29,6 +40,10 @@ test('check-scripts workflow installs node dependencies before running node test 'running Node script behavior tests', ); + assert.equal(setupToolchainsStep.with['node-cache'], 'pnpm'); + assert.equal(setupToolchainsStep.with['node-cache-dependency-path'], 'pnpm-lock.yaml'); + assert.match(installStep.run, /pnpm install --frozen-lockfile --ignore-scripts/); + assert.ok(pnpmSetupIndex < installIndex); assert.ok(installIndex < nodeTestIndex); }); diff --git a/scripts/ci/setup-toolchains-action.test.mjs b/scripts/ci/setup-toolchains-action.test.mjs new file mode 100644 index 0000000..29b6635 --- /dev/null +++ b/scripts/ci/setup-toolchains-action.test.mjs @@ -0,0 +1,27 @@ +import assert from 'node:assert/strict'; +import { test } from 'node:test'; +import { + extractCompositeActionSteps, + findStep, + readActionObject, +} from './workflow-test-utils.mjs'; + +const ACTION_DIR = 'setup-toolchains'; + +test('setup-toolchains action forwards optional node cache settings to setup-node', async () => { + const actionObject = await readActionObject(ACTION_DIR); + const steps = extractCompositeActionSteps(actionObject, ACTION_DIR); + const setupNodeStep = findStep( + steps, + 'setup-node step', + (step) => (step.uses ?? '').includes('actions/setup-node'), + ); + + assert.equal(actionObject.inputs['node-cache'].default, ''); + assert.equal(actionObject.inputs['node-cache-dependency-path'].default, ''); + assert.equal(setupNodeStep.with.cache, '${{ inputs.node-cache }}'); + assert.equal( + setupNodeStep.with['cache-dependency-path'], + '${{ inputs.node-cache-dependency-path }}', + ); +}); diff --git a/scripts/ci/workflow-test-utils.mjs b/scripts/ci/workflow-test-utils.mjs index 71778da..03b31cb 100644 --- a/scripts/ci/workflow-test-utils.mjs +++ b/scripts/ci/workflow-test-utils.mjs @@ -7,11 +7,21 @@ import { parse } from 'yaml'; const scriptDir = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(scriptDir, '..', '..'); const workflowsDir = path.join(projectRoot, '.github', 'workflows'); +const actionsDir = path.join(projectRoot, '.github', 'actions'); + +const readYamlObject = async (targetPath) => { + const content = await readFile(targetPath, 'utf8'); + return parse(content); +}; export const readWorkflowObject = async (workflowFileName) => { const workflowPath = path.join(workflowsDir, workflowFileName); - const content = await readFile(workflowPath, 'utf8'); - return parse(content); + return readYamlObject(workflowPath); +}; + +export const readActionObject = async (actionDirName) => { + const actionPath = path.join(actionsDir, actionDirName, 'action.yml'); + return readYamlObject(actionPath); }; export const extractWorkflowJobSteps = (workflowObject, jobName) => { @@ -33,3 +43,16 @@ export const findStepIndex = (steps, predicate, label) => { assert.notEqual(index, -1, `Expected workflow step ${String(label)} to exist.`); return index; }; + +export const extractCompositeActionSteps = (actionObject, actionLabel) => { + assert.equal( + actionObject.runs?.using, + 'composite', + `Expected action ${actionLabel} to be a composite action.`, + ); + assert.ok( + Array.isArray(actionObject.runs?.steps), + `Expected action ${actionLabel} to define composite steps.`, + ); + return actionObject.runs.steps; +}; From 943ef99fa296fdbcedfff99db7da7ccfa05c0d98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Tue, 14 Apr 2026 10:47:02 +0900 Subject: [PATCH 7/8] fix(ci): setup pnpm before enabling pnpm cache --- .github/workflows/check-scripts.yml | 10 +++++----- scripts/ci/check-scripts-workflow.test.mjs | 6 ++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/check-scripts.yml b/.github/workflows/check-scripts.yml index 14ca1e1..a6f8822 100644 --- a/.github/workflows/check-scripts.yml +++ b/.github/workflows/check-scripts.yml @@ -18,6 +18,11 @@ jobs: - name: Checkout uses: actions/checkout@v6.0.2 + - name: Setup pnpm + uses: pnpm/action-setup@v4.4.0 + with: + version: 10.28.2 + - name: Setup Toolchains uses: ./.github/actions/setup-toolchains with: @@ -26,11 +31,6 @@ jobs: node-cache-dependency-path: pnpm-lock.yaml python-version: '3.12' - - name: Setup pnpm - uses: pnpm/action-setup@v4.4.0 - with: - version: 10.28.2 - - name: Check Node scripts syntax run: | set -euo pipefail diff --git a/scripts/ci/check-scripts-workflow.test.mjs b/scripts/ci/check-scripts-workflow.test.mjs index 30c4405..acfde1c 100644 --- a/scripts/ci/check-scripts-workflow.test.mjs +++ b/scripts/ci/check-scripts-workflow.test.mjs @@ -29,6 +29,11 @@ test('check-scripts workflow installs node dependencies before running node test (step) => (step.uses ?? '').includes('pnpm/action-setup'), 'using pnpm/action-setup', ); + const setupToolchainsIndex = findStepIndex( + steps, + (step) => step === setupToolchainsStep, + 'using setup toolchains', + ); const installIndex = findStepIndex( steps, (step) => step === installStep, @@ -44,6 +49,7 @@ test('check-scripts workflow installs node dependencies before running node test assert.equal(setupToolchainsStep.with['node-cache-dependency-path'], 'pnpm-lock.yaml'); assert.match(installStep.run, /pnpm install --frozen-lockfile --ignore-scripts/); + assert.ok(pnpmSetupIndex < setupToolchainsIndex); assert.ok(pnpmSetupIndex < installIndex); assert.ok(installIndex < nodeTestIndex); }); From b664faa41ab1dc859bd300c2724428d255be017b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Tue, 14 Apr 2026 10:51:16 +0900 Subject: [PATCH 8/8] test(ci): relax workflow step assertions --- .../ci/build-desktop-tauri-workflow.test.mjs | 26 +++++++++++++++---- scripts/ci/check-scripts-workflow.test.mjs | 4 ++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/scripts/ci/build-desktop-tauri-workflow.test.mjs b/scripts/ci/build-desktop-tauri-workflow.test.mjs index 20e094a..8e73c13 100644 --- a/scripts/ci/build-desktop-tauri-workflow.test.mjs +++ b/scripts/ci/build-desktop-tauri-workflow.test.mjs @@ -3,6 +3,7 @@ import { test } from 'node:test'; import { extractWorkflowJobSteps, findStep, + findStepIndex, readWorkflowObject, } from './workflow-test-utils.mjs'; @@ -34,9 +35,24 @@ test('macOS workflow exposes structured build-macos steps', async () => { test('macOS workflow prepares resources before optional pre-signing', async () => { const workflowObject = await readWorkflowObject(WORKFLOW_FILE); const steps = extractWorkflowJobSteps(workflowObject, BUILD_MACOS_JOB); - const prepareStep = findStep(steps, 'prepare resources step', (step) => PREPARE_RESOURCES_RUN.test(step.run ?? '')); - const preSignStep = findStep(steps, 'pre-sign resources step', (step) => PRESIGN_BACKEND_RUN.test(step.run ?? '')); - const buildStep = findStep(steps, 'build app bundle step', (step) => BUILD_APP_BUNDLE_RUN.test(step.run ?? '')); + const prepareStepIndex = findStepIndex( + steps, + (step) => PREPARE_RESOURCES_RUN.test(step.run ?? ''), + 'prepare resources step', + ); + const preSignStepIndex = findStepIndex( + steps, + (step) => PRESIGN_BACKEND_RUN.test(step.run ?? ''), + 'pre-sign resources step', + ); + const buildStepIndex = findStepIndex( + steps, + (step) => BUILD_APP_BUNDLE_RUN.test(step.run ?? ''), + 'build app bundle step', + ); + const prepareStep = steps[prepareStepIndex]; + const preSignStep = steps[preSignStepIndex]; + const buildStep = steps[buildStepIndex]; assert.equal(prepareStep.if, undefined); assert.match(prepareStep.run, /pnpm run prepare:resources/); @@ -45,8 +61,8 @@ test('macOS workflow prepares resources before optional pre-signing', async () = assert.match(preSignStep.if ?? '', /import_apple_certificate\.outputs\.signing_identity/); assert.doesNotMatch(preSignStep.run, /pnpm run prepare:resources/); - assert.ok(steps.indexOf(prepareStep) < steps.indexOf(preSignStep)); - assert.ok(steps.indexOf(preSignStep) < steps.indexOf(buildStep)); + assert.ok(prepareStepIndex < preSignStepIndex); + assert.ok(preSignStepIndex < buildStepIndex); assert.match( buildStep.run, /Resources are already prepared/, diff --git a/scripts/ci/check-scripts-workflow.test.mjs b/scripts/ci/check-scripts-workflow.test.mjs index acfde1c..e0aaea8 100644 --- a/scripts/ci/check-scripts-workflow.test.mjs +++ b/scripts/ci/check-scripts-workflow.test.mjs @@ -47,7 +47,9 @@ test('check-scripts workflow installs node dependencies before running node test assert.equal(setupToolchainsStep.with['node-cache'], 'pnpm'); assert.equal(setupToolchainsStep.with['node-cache-dependency-path'], 'pnpm-lock.yaml'); - assert.match(installStep.run, /pnpm install --frozen-lockfile --ignore-scripts/); + assert.match(installStep.run, /pnpm install/); + assert.match(installStep.run, /--frozen-lockfile/); + assert.match(installStep.run, /--ignore-scripts/); assert.ok(pnpmSetupIndex < setupToolchainsIndex); assert.ok(pnpmSetupIndex < installIndex);