File size: 9,147 Bytes
08c07af
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
import { mkdtempSync, readFileSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { spawnSync } from 'node:child_process'
import { runInNewContext } from 'node:vm'
import * as yaml from 'js-yaml'
import { describe, expect, it } from 'vitest'

interface Step {
  name?: string
  uses?: string
  if?: string
  run?: string
  env?: Record<string, string>
  with?: Record<string, unknown>
}

interface CompatibilityJob {
  'runs-on': string
  if: string
  env: Record<string, string>
  strategy: { 'fail-fast': boolean; matrix: { include: Array<{ node: string | number; name: string; runner: string; gate_concurrency: string }> } }
  steps: Step[]
}

const workflow = yaml.load(readFileSync(resolve(import.meta.dirname, '../.github/workflows/ci.yml'), 'utf8')) as {
  jobs: { 'node-compat': CompatibilityJob; 'python-sdk': { 'runs-on': string } }
}
const job = workflow.jobs['node-compat']
const labels = ['self-hosted', 'linux', 'x64', 'vm-backup']

// This wiring check uses equal-typed, canonical-case fixtures. Actions compares
// strings case-insensitively; JavaScript does not. This is not an Actions evaluator.
function evaluate(expression: string, context: Record<string, unknown>): unknown {
  const body = expression.trim().slice(3, -2)
  return runInNewContext(body, {
    ...context, fromJSON: JSON.parse,
  }, { timeout: 1000 }) as unknown
}

function route(options: { mode?: string; author?: string; repository?: string; fork?: boolean; actor?: string } = {}): unknown {
  return evaluate(job['runs-on'], {
    vars: { DSH_CI_FAILOVER_LINUX: options.mode ?? 'selfhosted' },
    github: {
      repository: 'deepseek-harness/deepseek-harness',
      actor: options.actor ?? 'maintainer',
      event: { pull_request: {
        user: { login: options.author ?? 'maintainer' },
        head: { repo: { full_name: options.repository ?? 'deepseek-harness/deepseek-harness', fork: options.fork ?? false } },
      } },
    },
    matrix: { runner: 'ubuntu-latest' },
  })
}

describe('Node compatibility self-hosted routing', () => {
  it('uses the Linux pool only for opted-in repository-owned PRs', () => {
    expect(route()).toEqual(labels)
    for (const mode of ['', 'hosted', 'unexpected']) expect(route({ mode })).toBe('ubuntu-latest')
    // The blacksmith value routes the compatibility legs onto Blacksmith's
    // standard Linux runner regardless of PR ownership (ephemeral runners).
    expect(route({ mode: 'blacksmith' })).toBe('blacksmith-4vcpu-ubuntu-2404')
    expect(route({ author: 'dependabot[bot]', actor: 'maintainer' })).toBe('ubuntu-latest')
    expect(route({ repository: 'outsider/fork', fork: true })).toBe('ubuntu-latest')
    expect(route({ repository: 'outsider/fork', fork: false })).toBe('ubuntu-latest')
    expect(route({ fork: true })).toBe('ubuntu-latest')
    expect(route({ repository: '' })).toBe('ubuntu-latest')
  })

  it('preserves all three required version jobs and their concurrency', () => {
    expect(job.if).toBe("github.event_name == 'pull_request'")
    expect(job.strategy['fail-fast']).toBe(false)
    expect(job.strategy.matrix.include).toEqual([
      { node: '22.19', name: 'node 22.19', runner: 'ubuntu-latest', gate_concurrency: '1' },
      { node: '24.9', name: 'node 24.9', runner: 'ubuntu-latest', gate_concurrency: '1' },
      { node: 26, name: 'node 26', runner: 'ubuntu-latest', gate_concurrency: '1' },
    ])
    expect(job.env.DSH_GATE_CONCURRENCY).toBe('${{ matrix.gate_concurrency }}')
    expect(job.steps.map(step => step.run)).toContain('pnpm run check:node-compat')
    expect(job.steps.map(step => step.run)).toContain('pnpm exec vitest run packages/boot/app-boot/tests/loader-shape.compat.spec.ts')
    expect(workflow.jobs['python-sdk']['runs-on']).toBe('ubuntu-latest')
  })

  it('isolates version installs and enables hosted package caching only on hosted runners', () => {
    const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')!
    expect(setup.env).toEqual({
      NODE_OPTIONS: "${{ runner.environment == 'self-hosted' && '--import=./scripts/ci-compatible-toolcache.mjs' || '' }}",
    })
    expect(setup.with?.['node-version']).toBe('${{ matrix.node }}')
    expect(setup.with?.['package-manager-cache']).toBe(false)
    for (const [environment, cache] of [['github-hosted', 'pnpm'], ['self-hosted', '']]) {
      const context = { runner: { environment, temp: '/runner/temp', tool_cache: '/runner/toolcache' } }
      expect(evaluate(setup.with?.cache as string, context)).toBe(cache)
      expect(evaluate(setup.env!.NODE_OPTIONS!, context)).toBe(
        environment === 'self-hosted' ? '--import=./scripts/ci-compatible-toolcache.mjs' : '',
      )
    }
    expect(job.steps[0]?.with).toEqual({ 'persist-credentials': false })
    expect(job.steps.some(step => step.uses?.startsWith('actions/cache/'))).toBe(false)
  })

  it('overrides runner exports inside setup-node without affecting later Node processes', () => {
    const setup = job.steps.find(step => step.uses === 'actions/setup-node@v6')!
    const nodeOptions = evaluate(setup.env!.NODE_OPTIONS!, { runner: { environment: 'self-hosted' } }) as string
    const root = mkdtempSync(join(tmpdir(), 'ci-compatible preload-'))
    try {
      const env = { PATH: process.env.PATH, RUNNER_TEMP: root, RUNNER_TOOL_CACHE: join(root, 'persistent') }
      const probe = (options: Record<string, string | undefined>) => {
        const child = spawnSync(process.execPath, ['-p', 'process.env.RUNNER_TOOL_CACHE'], {
          cwd: resolve(import.meta.dirname, '..'), env: options, encoding: 'utf8', timeout: 10_000,
        })
        expect(child.error).toBeUndefined()
        expect(child.signal).toBeNull()
        return child
      }
      const setupChild = probe({ ...env, NODE_OPTIONS: nodeOptions })
      expect(setupChild.status, setupChild.stderr).toBe(0)
      expect(setupChild.stdout.trim()).toBe(join(root, 'node-compat-toolcache'))
      const normalChild = probe(env)
      expect(normalChild.status, normalChild.stderr).toBe(0)
      expect(normalChild.stdout.trim()).toBe(env.RUNNER_TOOL_CACHE)
      const missingTemp = probe({ ...env, RUNNER_TEMP: undefined, NODE_OPTIONS: nodeOptions })
      expect(missingTemp.status).not.toBe(0)
      expect(missingTemp.stderr).toContain('requires an absolute RUNNER_TEMP')
      expect(job.env).not.toHaveProperty('NODE_OPTIONS')
      expect(job.steps.filter(step => step.env?.NODE_OPTIONS)).toEqual([setup])
    } finally {
      rmSync(root, { recursive: true, force: true })
    }
  })

  it.skipIf(process.platform === 'win32')('rejects a Node executable outside its runner temporary installation', () => {
    const step = job.steps.find(candidate => candidate.name === 'Verify isolated Node installation')!
    expect(step.if).toBe("runner.environment == 'self-hosted'")
    for (const [executable, status] of [
      ['/runner temp/node-compat-toolcache/node/24.9.0/x64/bin/node', 0],
      ['/shared/toolcache/node/24.9.0/x64/bin/node', 1],
      ['/runner temp/node-compat-toolcache-other/node', 1],
    ] as const) {
      const child = spawnSync('bash', ['-e', '-u', '-o', 'pipefail', '-c', 'node() { printf "%s" "$TEST_EXECUTABLE"; }; ' + step.run!], {
        env: { PATH: process.env.PATH, RUNNER_TEMP: '/runner temp', TEST_EXECUTABLE: executable }, encoding: 'utf8', timeout: 10_000,
      })
      expect(child.error).toBeUndefined()
      expect(child.signal).toBeNull()
      expect(child.status, child.stderr).toBe(status)
    }
  })

  it.skipIf(process.platform === 'win32')('configures generated caches before pnpm without changing HOME or global links', () => {
    const index = job.steps.findIndex(step => step.name === 'Isolate compatibility caches')
    const step = job.steps[index]!
    expect(index).toBeGreaterThan(0)
    expect(index).toBeLessThan(job.steps.findIndex(candidate => candidate.uses === 'pnpm/action-setup@v4'))
    expect(step.if).toBe("runner.environment == 'self-hosted'")
    const root = mkdtempSync(join(tmpdir(), 'ci-compatible-selfhosted-'))
    try {
      const outputs = ['runner-a', 'runner-b'].map((runner) => {
        const envFile = join(root, runner + '.env')
        const temp = join(root, runner)
        const child = spawnSync('bash', ['-e', '-u', '-o', 'pipefail', '-c', step.run!], {
          env: { PATH: process.env.PATH, HOME: join(root, 'shared home'), RUNNER_TEMP: temp, GITHUB_ENV: envFile },
          encoding: 'utf8', timeout: 10_000,
        })
        expect(child.error).toBeUndefined()
        expect(child.signal).toBeNull()
        expect(child.status, child.stderr).toBe(0)
        const output = readFileSync(envFile, 'utf8')
        expect(output).toBe([
          'NODE_COMPILE_CACHE=' + temp + '/node-compile-cache',
          'npm_config_devdir=' + temp + '/node-gyp',
          'PNPM_CONFIG_STORE_DIR=' + join(root, 'shared home') + '/.local/share/pnpm/store',
          '',
        ].join('\n'))
        return output
      })
      expect(outputs[0]).not.toBe(outputs[1])
    } finally {
      rmSync(root, { recursive: true, force: true })
    }
  })
})