diff --git a/.github/workflows/mobile.yml b/.github/workflows/mobile.yml index f6265737195..1ab747aa8ce 100644 --- a/.github/workflows/mobile.yml +++ b/.github/workflows/mobile.yml @@ -24,6 +24,7 @@ on: - 'src/main/runtime/orca-runtime.ts' - 'src/main/runtime/agent-session-*.ts' - 'src/main/native-chat/agent-session-wire/**' + - 'src/main/runtime/structured-attention-original-delivery.test-fixture.ts' # Why: this job holds the only checks that load the Fastfile, so edits to # it or to the release workflow it guards must re-run them. - '.github/workflows/mobile.yml' @@ -38,6 +39,16 @@ on: - 'config/scripts/pr-code-change-scope.mjs' - 'config/scripts/pr-package-test-paths.mjs' - 'config/scripts/pr-layout-oracle-route.mjs' + # Desktop suites do not feed mobile; keep fixtures and test/toolchain config inputs. + - '!src/shared/**/*.test.[jt]s' + - '!src/shared/**/*.test.[jt]sx' + - '!src/shared/**/*.test.[cm][jt]s' + - '!src/packages/process-host/**/*.test.[jt]s' + - '!src/packages/process-host/**/*.test.[jt]sx' + - '!src/packages/process-host/**/*.test.[cm][jt]s' + - '!src/main/**/*.test.[jt]s' + - '!src/main/**/*.test.[jt]sx' + - '!src/main/**/*.test.[cm][jt]s' # Why main too: two pull requests can each pass against their own base and disagree once both # land, and `verify` never runs on main. The same source paths as above, less the CI inputs only # `verify` reads. @@ -57,10 +68,21 @@ on: - 'src/main/runtime/orca-runtime.ts' - 'src/main/runtime/agent-session-*.ts' - 'src/main/native-chat/agent-session-wire/**' + - 'src/main/runtime/structured-attention-original-delivery.test-fixture.ts' - '.github/workflows/mobile.yml' - 'config/mobile-ci.vitest.config.mjs' - 'config/scripts/run-mobile-ci-tests.mjs' - 'config/.bun-version' + # Desktop suites do not feed mobile; keep fixtures and test/toolchain config inputs. + - '!src/shared/**/*.test.[jt]s' + - '!src/shared/**/*.test.[jt]sx' + - '!src/shared/**/*.test.[cm][jt]s' + - '!src/packages/process-host/**/*.test.[jt]s' + - '!src/packages/process-host/**/*.test.[jt]sx' + - '!src/packages/process-host/**/*.test.[cm][jt]s' + - '!src/main/**/*.test.[jt]s' + - '!src/main/**/*.test.[jt]sx' + - '!src/main/**/*.test.[cm][jt]s' concurrency: # Per commit on main, not per branch. GitHub cancels any PENDING run in a group when a new one diff --git a/config/scripts/mobile-test-change-scope.mjs b/config/scripts/mobile-test-change-scope.mjs index 3be5e7c5ec2..499a4ac88ad 100644 --- a/config/scripts/mobile-test-change-scope.mjs +++ b/config/scripts/mobile-test-change-scope.mjs @@ -15,6 +15,23 @@ const NON_TEST_FILES = new Set([ 'config/scripts/mobile-release-check-scope.test.mjs' ]) +const DESKTOP_SUITE_PREFIXES = [ + 'src/shared/', + 'src/packages/process-host/', + 'src/main/agent-launch/', + 'src/main/runtime/rpc/', + 'src/main/runtime/runtime-rpc/', + 'src/main/runtime/agent-session-', + 'src/main/native-chat/agent-session-wire/' +] + +function isDesktopSuite(file) { + return ( + DESKTOP_SUITE_PREFIXES.some((prefix) => file.startsWith(prefix)) && + /\.test\.(?:[cm]?[jt]s|[jt]sx)$/.test(file) + ) +} + export function shouldRunMobileTests(files) { return ( files.length === 0 || @@ -23,6 +40,7 @@ export function shouldRunMobileTests(files) { file.split('/').includes('..') || (!isDocsOnlyPath(file) && !NON_TEST_FILES.has(file) && + !isDesktopSuite(file) && !file.startsWith('mobile/docs/') && !file.startsWith('mobile/fastlane/')) ) diff --git a/config/scripts/mobile-test-change-scope.test.mjs b/config/scripts/mobile-test-change-scope.test.mjs index d5cdad6abbb..cd5d5393ee0 100644 --- a/config/scripts/mobile-test-change-scope.test.mjs +++ b/config/scripts/mobile-test-change-scope.test.mjs @@ -4,6 +4,15 @@ import { parse } from 'yaml' import { expect, it } from 'vitest' import { shouldRunMobileTests } from './mobile-test-change-scope.mjs' +function workflowIncludes(paths, files) { + return files.some((file) => + paths.reduce((included, pattern) => { + const excluded = pattern.startsWith('!') + return matchesGlob(file, excluded ? pattern.slice(1) : pattern) ? !excluded : included + }, false) + ) +} + it.each([ 'docs/ci.md', 'mobile/docs/release.md', @@ -54,16 +63,85 @@ it('runs Mobile Checks for shared modules, process-host tooling and the root loc 'src/shared/any/module.ts', 'src/packages/process-host/src/run-process.ts', 'src/packages/process-host/package.json', + 'src/main/runtime/structured-attention-original-delivery.test-fixture.ts', 'pnpm-lock.yaml' ]) { - expect(paths.some((pattern) => matchesGlob(file, pattern))).toBe(true) + expect(workflowIncludes(paths, [file])).toBe(true) } - expect( - paths.some((pattern) => matchesGlob('src/packages/unrelated/src/index.ts', pattern)) - ).toBe(false) + expect(workflowIncludes(paths, ['src/packages/unrelated/src/index.ts'])).toBe(false) } }) +it('honors ordered exclusions and later re-inclusion when matching workflow paths', () => { + const paths = ['src/**', '!src/**/*.test.ts', 'src/retained.test.ts'] + expect(workflowIncludes(paths, ['src/removed.test.ts'])).toBe(false) + expect(workflowIncludes(paths, ['src/retained.test.ts'])).toBe(true) +}) + +it.each([ + 'src/shared/model-id-label.test.ts', + 'src/shared/nested/view.test.tsx', + 'src/shared/nested/module.test.js', + 'src/shared/nested/view.test.jsx', + 'src/shared/nested/module.test.cjs', + 'src/shared/nested/module.test.mjs', + 'src/shared/nested/module.test.cts', + 'src/shared/nested/module.test.mts', + 'src/packages/process-host/src/run-process.test.ts', + 'src/main/agent-launch/dispatch.test.ts', + 'src/main/runtime/rpc/dispatcher.test.ts', + 'src/main/runtime/runtime-rpc/server.test.ts', + 'src/main/runtime/agent-session-store.test.ts', + 'src/main/native-chat/agent-session-wire/history.test.ts' +])('skips desktop suites on PRs and main, including removal: %s', (file) => { + const workflow = parse( + readFileSync(new URL('../../.github/workflows/mobile.yml', import.meta.url), 'utf8') + ) + expect(shouldRunMobileTests([file])).toBe(false) + expect(shouldRunMobileTests([file, 'mobile/docs/ci.md'])).toBe(false) + for (const paths of [workflow.on.pull_request.paths, workflow.on.push.paths]) { + expect(workflowIncludes(paths, [file])).toBe(false) + expect(workflowIncludes(paths, [file, 'src/shared/changed.ts'])).toBe(true) + expect(workflowIncludes(paths, [file, 'mobile/src/changed.test.ts'])).toBe(true) + // A rename is represented by the removed and added names, so moving a suite into source runs. + expect(workflowIncludes(paths, [file, file.replace('.test.', '.')])).toBe(true) + } + expect(shouldRunMobileTests([file, 'src/shared/changed.ts'])).toBe(true) + expect(shouldRunMobileTests([file, file.replace('.test.', '.')])).toBe(true) +}) + +it.each([ + 'mobile/src/changed.test.ts', + 'mobile/src/changed.test.tsx', + 'src/shared/project-section-parity.test-fixture.ts', + 'src/shared/fixtures/recording.json', + 'src/main/runtime/rpc/methods/agent-launch.test-fixture.ts', + 'src/main/runtime/agent-session-record-store-test-harness.ts', + 'src/packages/process-host/tsconfig.test.json', + 'src/packages/process-host/vitest.config.ts', + 'src/packages/process-host/package.json', + 'pnpm-lock.yaml', + 'mobile/pnpm-lock.yaml', + 'config/mobile-ci.vitest.config.mjs', + 'config/scripts/run-mobile-ci-tests.mjs', + 'config/.bun-version', + '.github/workflows/mobile.yml' +])('retains mobile suites, shared fixtures and toolchain inputs: %s', (file) => { + const workflow = parse( + readFileSync(new URL('../../.github/workflows/mobile.yml', import.meta.url), 'utf8') + ) + expect(shouldRunMobileTests([file])).toBe(true) + for (const paths of [workflow.on.pull_request.paths, workflow.on.push.paths]) { + expect(workflowIncludes(paths, [file])).toBe(true) + } +}) + +it('retains unknown suite paths and noncanonical traversal evidence', () => { + expect(shouldRunMobileTests(['unknown/module.test.ts'])).toBe(true) + expect(shouldRunMobileTests(['src/unknown/module.test.ts'])).toBe(true) + expect(shouldRunMobileTests(['src/shared/../main/module.test.ts'])).toBe(true) +}) + it('skips only tests, after successful detection, and retains all other mobile gates', () => { const workflow = parse( readFileSync(new URL('../../.github/workflows/mobile.yml', import.meta.url), 'utf8')