mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
* fix(mobile): break the transport and terminal-document import cycles and gate them after mobile's install client-context re-exported the host-client hooks, which import client-context back: the re-export barrel is gone and importers (and their test mocks) name host-client-hooks directly. In the terminal document, the cell-metric getters move to a leaf cell-metrics module and select-mode entry/exit to selection-mode, so fit, transform, overlay and range no longer import each other. pr.yml lints imports before mobile's install, when mobile's tsconfig can't load, so Mobile verify now runs the same import rules over mobile after its install with zero warnings allowed. * fix(mobile): the browser-pane render harness imports useHostClient from host-client-hooks Its route source is a template string, so no typecheck saw the removed client-context re-export; the mobile web bundle check did. * fix(ci): the e2e typecheck ratchet counts the same with or without mobile's install Specs reach mobile/src/transport, whose expo-crypto and async-storage imports resolve only after mobile's install and then load React Native's globals (numeric timers, a required NODE_ENV), adding errors in unrelated specs. An empty stub pins both, so the static job reads 232 either way.
250 lines
9.1 KiB
JavaScript
250 lines
9.1 KiB
JavaScript
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { dirname, join, matchesGlob, resolve } from 'node:path'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { parse } from 'yaml'
|
|
import { runProcessSync } from '@orca/process-host'
|
|
import { shouldRunMobileReleaseChecks } from './mobile-release-check-scope.mjs'
|
|
|
|
const root = resolve(import.meta.dirname, '../..')
|
|
const workflow = parse(readFileSync(join(root, '.github/workflows/mobile.yml'), 'utf8'))
|
|
const steps = workflow.jobs.verify.steps
|
|
const detector = steps.find((step) => step.id === 'ruby-scope')
|
|
const directories = []
|
|
|
|
afterEach(() => {
|
|
for (const directory of directories.splice(0)) {
|
|
rmSync(directory, { recursive: true, force: true })
|
|
}
|
|
})
|
|
|
|
it.each([
|
|
'src/main/runtime/rpc/dispatcher.ts',
|
|
'src/shared/rpc-contract/params.ts',
|
|
'mobile/src/session/session.test.ts',
|
|
'mobile/app/index.tsx',
|
|
'mobile/src/theme.css',
|
|
'mobile/src/session/README.md',
|
|
'mobile/docs/release.md',
|
|
'mobile/README.md',
|
|
'docs/reference/mobile.md'
|
|
])('skips Ruby checks for application/documentation-only changes: %s', (file) => {
|
|
expect(shouldRunMobileReleaseChecks([file])).toBe(false)
|
|
})
|
|
|
|
it.each([
|
|
'mobile/fastlane/Fastfile',
|
|
'mobile/fastlane/Appfile',
|
|
'mobile/fastlane/ios_release_version.rb',
|
|
'mobile/fastlane/ios_release_version_test.rb',
|
|
'mobile/Gemfile',
|
|
'mobile/Gemfile.lock',
|
|
'mobile/.ruby-version',
|
|
'mobile/.bundle/config',
|
|
'mobile/app.json',
|
|
'mobile/app.config.ts',
|
|
'mobile/ios/Podfile',
|
|
'mobile/package.json',
|
|
'mobile/pnpm-lock.yaml',
|
|
'mobile/scripts/release.mts',
|
|
'mobile/src/release.rb',
|
|
'mobile/src/release.json',
|
|
'mobile/new-toolchain/input',
|
|
'package.json',
|
|
'.github/workflows/mobile.yml',
|
|
'.github/workflows/mobile-ios-release.yml',
|
|
'.github/actions/install-node-dependencies/action.yml',
|
|
'config/scripts/mobile-release-check-scope.mjs',
|
|
'config/scripts/mobile-release-check-scope.test.mjs'
|
|
])('retains Ruby release coverage for changed or unknown inputs: %s', (file) => {
|
|
expect(shouldRunMobileReleaseChecks([file])).toBe(true)
|
|
expect(shouldRunMobileReleaseChecks(['mobile/src/view.tsx', file])).toBe(true)
|
|
})
|
|
|
|
it('skips Ruby checks for a root lockfile change, which fastlane never reads', () => {
|
|
expect(shouldRunMobileReleaseChecks(['pnpm-lock.yaml'])).toBe(false)
|
|
expect(shouldRunMobileReleaseChecks(['pnpm-lock.yaml', 'mobile/fastlane/Fastfile'])).toBe(true)
|
|
})
|
|
|
|
it('runs Ruby checks when the changed-file evidence is empty', () => {
|
|
expect(shouldRunMobileReleaseChecks([])).toBe(true)
|
|
})
|
|
|
|
it('gates Ruby independently and retains mobile static validation', () => {
|
|
expect(steps[0].with['fetch-depth']).toBe(2)
|
|
expect(detector['working-directory']).toBe('.')
|
|
expect(steps.indexOf(detector)).toBeGreaterThan(
|
|
steps.findIndex((step) => step.uses === './.github/actions/install-node-dependencies')
|
|
)
|
|
expect(steps.find((step) => step.name === 'Setup Bun for tests').if).toBe(
|
|
steps.find((step) => step.name === 'Test').if
|
|
)
|
|
const gated = steps.filter(
|
|
(step) =>
|
|
step.if !== undefined &&
|
|
step.name !== 'Test' &&
|
|
step.name !== 'Setup Bun for tests' &&
|
|
step.name !== 'Summarize RPC recording changes'
|
|
)
|
|
expect(gated.map((step) => step.name)).toEqual([
|
|
'Setup Ruby and fastlane',
|
|
'Test iOS release version resolution',
|
|
'Test TestFlight lane arguments',
|
|
'Smoke-check the Fastfile',
|
|
'Smoke-check CocoaPods'
|
|
])
|
|
for (const step of gated) {
|
|
expect(step.if).toBe("steps.ruby-scope.outputs.should_run != 'false'")
|
|
}
|
|
for (const name of [
|
|
'Lint mobile imports with mobile dependencies installed',
|
|
'Typecheck',
|
|
'Typecheck tests (ratchet)',
|
|
'Lint',
|
|
'Check formatting'
|
|
]) {
|
|
expect(steps.find((step) => step.name === name)?.if).toBeUndefined()
|
|
expect(steps.some((step) => step.name === name)).toBe(true)
|
|
}
|
|
for (const file of [
|
|
'config/scripts/mobile-release-check-scope.mjs',
|
|
'config/scripts/mobile-release-check-scope.test.mjs',
|
|
'config/scripts/pr-code-change-scope.mjs',
|
|
'config/scripts/pr-package-test-paths.mjs'
|
|
]) {
|
|
expect(workflow.on.pull_request.paths.some((pattern) => matchesGlob(file, pattern))).toBe(true)
|
|
}
|
|
expect(workflow.jobs.verify.env.BUNDLE_FROZEN).toBe('true')
|
|
expect(steps.find((step) => step.name === 'Setup Ruby and fastlane').with['bundler-cache']).toBe(
|
|
true
|
|
)
|
|
expect(steps.find((step) => step.name === 'Smoke-check the Fastfile').run).toBe(
|
|
'bundle exec fastlane lanes'
|
|
)
|
|
})
|
|
|
|
function fixture(detectorStep = detector) {
|
|
const directory = mkdtempSync(join(tmpdir(), 'mobile-release-scope-'))
|
|
directories.push(directory)
|
|
const git = (...args) => {
|
|
const result = runProcessSync({ program: 'git', args, cwd: directory })
|
|
expect(result.code, result.stderr).toBe(0)
|
|
return result.stdout.trim()
|
|
}
|
|
git('init', '--quiet')
|
|
git('symbolic-ref', 'HEAD', 'refs/heads/main')
|
|
git('config', 'user.name', 'Workflow fixture')
|
|
git('config', 'user.email', 'workflow@example.invalid')
|
|
const write = (file, content) => {
|
|
mkdirSync(dirname(join(directory, file)), { recursive: true })
|
|
writeFileSync(join(directory, file), content)
|
|
}
|
|
write('mobile/src/view.tsx', 'export const view = 1\n')
|
|
write('mobile/fastlane/Fastfile', 'default_platform(:ios)\n')
|
|
mkdirSync(join(directory, 'config/scripts'), { recursive: true })
|
|
for (const file of [
|
|
'package.json',
|
|
'config/scripts/pr-code-change-scope.mjs',
|
|
'config/scripts/pr-package-test-paths.mjs',
|
|
'config/scripts/pr-layout-oracle-route.mjs',
|
|
'config/scripts/mobile-release-check-scope.mjs',
|
|
'config/scripts/mobile-test-change-scope.mjs'
|
|
]) {
|
|
copyFileSync(join(root, file), join(directory, file))
|
|
}
|
|
const commit = () => {
|
|
git('add', '-A')
|
|
git(
|
|
'-c',
|
|
'commit.gpgsign=false',
|
|
'-c',
|
|
'core.hooksPath=/dev/null',
|
|
'commit',
|
|
'--quiet',
|
|
'-m',
|
|
'fixture'
|
|
)
|
|
}
|
|
commit()
|
|
const detect = () => {
|
|
const output = join(directory, 'github-output')
|
|
const result = runProcessSync({
|
|
program: 'bash',
|
|
args: ['-e', '-c', detectorStep.run],
|
|
cwd: directory,
|
|
env: { ...process.env, GITHUB_OUTPUT: output, RUNNER_TEMP: directory }
|
|
})
|
|
expect(result.code, result.stderr).toBe(0)
|
|
return readFileSync(output, 'utf8')
|
|
}
|
|
return { directory, git, write, commit, detect }
|
|
}
|
|
|
|
describe.skipIf(process.platform === 'win32')('the Linux workflow detector command', () => {
|
|
it('skips Ruby after a source-only commit', () => {
|
|
const repo = fixture()
|
|
repo.write('mobile/src/view.tsx', 'export const view = 2\n')
|
|
repo.commit()
|
|
expect(repo.detect()).toBe('should_run=false\n')
|
|
})
|
|
|
|
it('skips reliability metadata alone and retains mixed Fastfile changes', () => {
|
|
const repo = fixture()
|
|
repo.write('config/reliability-gates.jsonc', '{"gates": []}\n')
|
|
repo.commit()
|
|
expect(repo.detect()).toBe('should_run=false\n')
|
|
repo.write('config/reliability-gates.jsonc', '{"gates": [1]}\n')
|
|
repo.write('mobile/fastlane/Fastfile', 'default_platform(:android)\n')
|
|
repo.commit()
|
|
expect(repo.detect()).toBe('should_run=false\nshould_run=true\n')
|
|
})
|
|
|
|
it('keeps a deleted release input when a rename moves it into an excluded directory', () => {
|
|
const repo = fixture()
|
|
repo.git('mv', 'mobile/fastlane/Fastfile', 'mobile/src/old-release.md')
|
|
repo.commit()
|
|
expect(repo.detect()).toBe('should_run=true\n')
|
|
})
|
|
|
|
it('runs Ruby when the merge parent is unavailable', () => {
|
|
expect(fixture().detect()).toBe('should_run=true\n')
|
|
})
|
|
|
|
it('runs Ruby when the classifier cannot execute', () => {
|
|
const repo = fixture()
|
|
repo.write('mobile/src/view.tsx', 'export const view = 2\n')
|
|
repo.commit()
|
|
rmSync(join(repo.directory, 'config/scripts/mobile-release-check-scope.mjs'))
|
|
expect(repo.detect()).toBe('should_run=true\n')
|
|
})
|
|
})
|
|
|
|
describe.skipIf(process.platform === 'win32')('the mobile test detector command', () => {
|
|
const testDetector = steps.find((step) => step.id === 'test-scope')
|
|
it('skips release-only changes while retaining source changes', () => {
|
|
const repo = fixture(testDetector)
|
|
repo.write('mobile/fastlane/Fastfile', 'default_platform(:android)\n')
|
|
repo.commit()
|
|
expect(repo.detect()).toBe('should_run=false\n')
|
|
repo.write('mobile/src/view.tsx', 'export const view = 2\n')
|
|
repo.commit()
|
|
expect(repo.detect()).toBe('should_run=false\nshould_run=true\n')
|
|
})
|
|
it('retains tests after source moves into a documentation directory', () => {
|
|
const repo = fixture(testDetector)
|
|
repo.git('mv', 'mobile/src/view.tsx', 'mobile/README.md')
|
|
repo.commit()
|
|
expect(repo.detect()).toBe('should_run=true\n')
|
|
})
|
|
it('retains tests when the diff is unavailable', () => {
|
|
expect(fixture(testDetector).detect()).toBe('should_run=true\n')
|
|
})
|
|
it('retains tests when the classifier cannot execute', () => {
|
|
const repo = fixture(testDetector)
|
|
repo.write('mobile/fastlane/Fastfile', 'default_platform(:android)\n')
|
|
repo.commit()
|
|
rmSync(join(repo.directory, 'config/scripts/mobile-test-change-scope.mjs'))
|
|
expect(repo.detect()).toBe('should_run=true\n')
|
|
})
|
|
})
|