feat(ai): emit a stable code from assertExpectedIdentity (#13275) #3277
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Tests | |
| on: | |
| pull_request: | |
| branches: [dev] | |
| push: | |
| branches: [dev] | |
| concurrency: | |
| group: tests-${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| changes: | |
| name: Classify test scope | |
| runs-on: ubuntu-latest | |
| outputs: | |
| run_integration: ${{ steps.scope.outputs.run_integration }} | |
| run_unit: ${{ steps.scope.outputs.run_unit }} | |
| skip_reason: ${{ steps.scope.outputs.skip_reason }} | |
| steps: | |
| - name: Classify changed paths | |
| id: scope | |
| uses: actions/github-script@v9 | |
| with: | |
| script: | | |
| const eventName = context.eventName; | |
| async function getChangedFiles() { | |
| if (eventName === 'pull_request') { | |
| const files = await github.paginate(github.rest.pulls.listFiles, { | |
| owner : context.repo.owner, | |
| repo : context.repo.repo, | |
| pull_number: context.payload.pull_request.number, | |
| per_page : 100 | |
| }); | |
| return files.map(file => file.filename); | |
| } | |
| if (eventName === 'push') { | |
| const before = context.payload.before; | |
| const after = context.payload.after; | |
| if (!before || !after || /^0+$/.test(before)) { | |
| return null; | |
| } | |
| const comparison = await github.rest.repos.compareCommitsWithBasehead({ | |
| owner : context.repo.owner, | |
| repo : context.repo.repo, | |
| basehead: `${before}...${after}`, | |
| per_page: 100 | |
| }); | |
| return comparison.data.files?.map(file => file.filename) || null; | |
| } | |
| return null; | |
| } | |
| const files = await getChangedFiles(); | |
| if (!files || files.length === 0) { | |
| core.info('Changed files are unavailable; running full test suites.'); | |
| core.setOutput('run_integration', 'true'); | |
| core.setOutput('run_unit', 'true'); | |
| core.setOutput('skip_reason', 'changed files unavailable'); | |
| return; | |
| } | |
| const normalizedFiles = files.map(file => file.replace(/\\/g, '/')); | |
| // Keep explicit unit/integration statuses on docs-only PRs instead of | |
| // workflow-level paths-ignore. Missing checks can block protected branches. | |
| const isDocsOnlyPath = file => | |
| file.endsWith('.md') || | |
| file.startsWith('learn/') || | |
| file.startsWith('resources/content/'); | |
| // Unit has committed content guards, notably the release-note mutable | |
| // GitHub-link scan, discussion markdown fixture reads, and generated | |
| // portal index fixtures. | |
| const requiresUnitForContent = file => | |
| file.startsWith('resources/content/release-notes/') || | |
| file.startsWith('resources/content/discussions/') || | |
| file.startsWith('apps/portal/resources/data/'); | |
| // Whitelist: integration runs only on changes that can affect the integration | |
| // stack — engine (src/), Agent OS (ai/, incl. ai/deploy compose), the integration | |
| // specs/fixtures + shared test infra, dep manifests, or this workflow. Keep this | |
| // COMPLETE — a missed path silently skips integration (false-negative). | |
| const isIntegrationRelevantPath = file => | |
| file.startsWith('src/') || | |
| file.startsWith('ai/') || | |
| file.startsWith('test/playwright/integration/') || | |
| file.startsWith('test/playwright/util/') || | |
| file === 'test/playwright/fixtures.mjs' || | |
| file === 'test/playwright/setup.mjs' || | |
| file === 'test/playwright/playwright.config.integration.mjs' || | |
| file === 'package.json' || | |
| file === 'package-lock.json' || | |
| file === '.github/workflows/test.yml'; | |
| const runIntegration = normalizedFiles.some(isIntegrationRelevantPath); | |
| const runUnit = normalizedFiles.some(file => !isDocsOnlyPath(file) || requiresUnitForContent(file)); | |
| core.info(`Changed files: ${normalizedFiles.join(', ')}`); | |
| core.info(`run_integration=${runIntegration}`); | |
| core.info(`run_unit=${runUnit}`); | |
| core.setOutput('run_integration', String(runIntegration)); | |
| core.setOutput('run_unit', String(runUnit)); | |
| core.setOutput( | |
| 'skip_reason', | |
| 'change does not touch relevant paths for this suite (docs/content/config or unrelated test suite)' | |
| ); | |
| test: | |
| name: ${{ matrix.suite }} | |
| runs-on: ubuntu-latest | |
| needs: changes | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # Bucket A-F audit (#10903) closed via #10907/#10921/#10910/#10919/#10920. | |
| # Bucket G (#10924) substrate fixes landed via #10940 (TestLifecycleHelper | |
| # primitive + daemon-spec migration) + open trackers for residual flakes | |
| # (#10941/#10936/#10937/#10946 — all unit-skip-guarded via NEO_TEST_SKIP_CI). | |
| suite: [integration-unified, unit] | |
| steps: | |
| - name: Checkout repository | |
| if: ${{ (matrix.suite == 'integration-unified' && needs.changes.outputs.run_integration == 'true') || (matrix.suite == 'unit' && needs.changes.outputs.run_unit == 'true') }} | |
| uses: actions/checkout@v6 | |
| - name: Setup Node.js | |
| if: ${{ (matrix.suite == 'integration-unified' && needs.changes.outputs.run_integration == 'true') || (matrix.suite == 'unit' && needs.changes.outputs.run_unit == 'true') }} | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: '24' | |
| cache: 'npm' | |
| # Pre-create .neo-ai-data so the `prepare` lifecycle's downloadKnowledgeBase | |
| # short-circuits via its existing-dir guard. Integration tests use a tmpfs | |
| # Chroma per `ai/deploy/docker-compose.test.yml`; unit tests are mocked. | |
| # Neither suite needs the canonical KB artifact in CI. | |
| - name: Skip Knowledge Base download in prepare lifecycle | |
| if: ${{ (matrix.suite == 'integration-unified' && needs.changes.outputs.run_integration == 'true') || (matrix.suite == 'unit' && needs.changes.outputs.run_unit == 'true') }} | |
| run: mkdir -p .neo-ai-data | |
| - name: Install dependencies | |
| if: ${{ (matrix.suite == 'integration-unified' && needs.changes.outputs.run_integration == 'true') || (matrix.suite == 'unit' && needs.changes.outputs.run_unit == 'true') }} | |
| run: npm ci | |
| # Required for the unit-test runner per bootstrapWorktree.mjs precedent. | |
| # Harmless for the integration suite. | |
| - name: Bundle parse5 | |
| if: ${{ (matrix.suite == 'integration-unified' && needs.changes.outputs.run_integration == 'true') || (matrix.suite == 'unit' && needs.changes.outputs.run_unit == 'true') }} | |
| run: npm run bundle-parse5 | |
| - name: Skip ${{ matrix.suite }} tests | |
| if: ${{ (matrix.suite == 'integration-unified' && needs.changes.outputs.run_integration != 'true') || (matrix.suite == 'unit' && needs.changes.outputs.run_unit != 'true') }} | |
| run: | | |
| echo "${{ matrix.suite }} skipped: ${{ needs.changes.outputs.skip_reason }}" | |
| - name: Run ${{ matrix.suite }} tests | |
| if: ${{ (matrix.suite == 'integration-unified' && needs.changes.outputs.run_integration == 'true') || (matrix.suite == 'unit' && needs.changes.outputs.run_unit == 'true') }} | |
| run: npm run test-${{ matrix.suite }} | |
| env: | |
| NEO_INTEGRATION_STACK_TIMEOUT_MS: '240000' | |
| # Keep the unit residual skip guards active without gating integration specs. | |
| NEO_TEST_SKIP_CI: ${{ matrix.suite == 'unit' && 'true' || '' }} | |
| - name: Upload test artifacts on failure | |
| if: failure() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: test-results-${{ matrix.suite }}-${{ github.run_id }} | |
| path: test/playwright/test-results/ | |
| retention-days: 14 |