mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 00:04:01 +00:00
Merge origin/main into the SSH host-key + reconnect branch
One conflict, and it was structural rather than textual: main split ssh-connection.test.ts from 2904 lines into seventeen files with a shared harness and fixtures (#14728), taking it off the max-lines suppression list, while this branch had added host key verification tests to the old monolith. Resolved by adopting main's split rather than reinstating the big file, since restoring it would put back the suppression the refactor just removed. The seven host key tests move to ssh-connection-host-key-verification.test.ts unchanged, including their comments — they are the reasoning, not decoration. The load-bearing piece was the ssh2 mock, which now lives in the shared harness and is therefore used by all seventeen files. It has to keep the callback form: ssh2 calls hostVerifier(key, verify) and only accepts synchronously when the return is not undefined, so the old single-argument mock would pass against a verifier that never decides — exactly the regression this work exists to prevent. It also has to abort on denial, or a refused key looks like a successful connect. Defaulting the presented key to a valid ed25519 blob keeps the other sixteen files unaffected, which the full run confirms: 1549 passed, 13 skipped, zero failures, typecheck clean. The one remaining caller of the old signature — the obsolete-generation fingerprint test — moved to real key blobs, because the verifier now identifies a key before recording a fingerprint and a placeholder string would be refused before reaching the generation check that test covers.
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
import { appendFileSync, readFileSync } from 'node:fs'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { mergeLocBlock, renderLocBlock, sumChangedFiles } from './pr-test-loc-table.mjs'
|
||||
|
||||
export const PR_FILES_PAGE_LIMIT = 3000
|
||||
|
||||
export function nextLink(linkHeader) {
|
||||
if (linkHeader == null || linkHeader.length === 0) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
for (const part of linkHeader.split(',')) {
|
||||
const match = part.match(/<([^>]+)>\s*;\s*rel="next"/)
|
||||
if (match != null) {
|
||||
return match[1]
|
||||
}
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
function githubHeaders(token) {
|
||||
return {
|
||||
Accept: 'application/vnd.github+json',
|
||||
Authorization: `Bearer ${token}`,
|
||||
'User-Agent': 'orca-pr-test-loc',
|
||||
'X-GitHub-Api-Version': '2022-11-28'
|
||||
}
|
||||
}
|
||||
|
||||
export async function listPullFiles({ owner, repo, pullNumber, token, fetchImpl = fetch }) {
|
||||
const files = []
|
||||
let url = `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}/files?per_page=100`
|
||||
|
||||
while (url != null) {
|
||||
const response = await fetchImpl(url, { headers: githubHeaders(token) })
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to list PR #${pullNumber} files: ${response.status} ${response.statusText}`
|
||||
)
|
||||
}
|
||||
const page = await response.json()
|
||||
if (!Array.isArray(page)) {
|
||||
throw new Error(`Unexpected PR files payload for #${pullNumber}`)
|
||||
}
|
||||
files.push(...page)
|
||||
if (files.length >= PR_FILES_PAGE_LIMIT) {
|
||||
console.log(
|
||||
`PR #${pullNumber} file list hit GitHub's ${PR_FILES_PAGE_LIMIT}-file cap; totals may be short.`
|
||||
)
|
||||
return files.slice(0, PR_FILES_PAGE_LIMIT)
|
||||
}
|
||||
url = nextLink(response.headers.get('link'))
|
||||
}
|
||||
|
||||
return files
|
||||
}
|
||||
|
||||
function writeGithubOutput(totals) {
|
||||
const block = `${renderLocBlock(totals)}\n`
|
||||
const outputPath = process.env.GITHUB_OUTPUT
|
||||
if (outputPath != null) {
|
||||
appendFileSync(outputPath, `summary<<ORCA_PR_LOC_EOF\n${block}ORCA_PR_LOC_EOF\n`)
|
||||
}
|
||||
const summaryPath = process.env.GITHUB_STEP_SUMMARY
|
||||
if (summaryPath != null) {
|
||||
appendFileSync(summaryPath, `${block}\n`)
|
||||
}
|
||||
}
|
||||
|
||||
async function updatePullRequest({ owner, repo, pullNumber, token, totals, fetchImpl = fetch }) {
|
||||
const headers = githubHeaders(token)
|
||||
const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${pullNumber}`
|
||||
const response = await fetchImpl(url, { headers })
|
||||
if (response.status === 403) {
|
||||
console.log('Skipping PR body update: token cannot write (likely a fork PR).')
|
||||
return 0
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to read PR #${pullNumber}: ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const pull = await response.json()
|
||||
const nextBody = mergeLocBlock(pull.body, totals)
|
||||
if (nextBody === (pull.body ?? '')) {
|
||||
console.log(`PR #${pullNumber} LoC header already current.`)
|
||||
return 0
|
||||
}
|
||||
|
||||
const update = await fetchImpl(url, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
...headers,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ body: nextBody })
|
||||
})
|
||||
if (update.status === 403) {
|
||||
console.log('Skipping PR body update: token cannot write (likely a fork PR).')
|
||||
return 0
|
||||
}
|
||||
if (!update.ok) {
|
||||
throw new Error(`Failed to update PR #${pullNumber}: ${update.status} ${update.statusText}`)
|
||||
}
|
||||
|
||||
console.log(`Updated LoC header on PR #${pullNumber}.`)
|
||||
return 0
|
||||
}
|
||||
|
||||
function resolveRepository() {
|
||||
const repository = process.env.GITHUB_REPOSITORY
|
||||
if (repository == null || !repository.includes('/')) {
|
||||
return undefined
|
||||
}
|
||||
const slash = repository.indexOf('/')
|
||||
return { owner: repository.slice(0, slash), repo: repository.slice(slash + 1) }
|
||||
}
|
||||
|
||||
function resolveToken() {
|
||||
return process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN
|
||||
}
|
||||
|
||||
function printUsage() {
|
||||
console.error(
|
||||
`Usage: ${process.argv[1]} --from-pr <number> | --update-pr <number> [--files-json <file>] [--merge-body <file>]`
|
||||
)
|
||||
}
|
||||
|
||||
async function main(argv) {
|
||||
let filesJsonPath
|
||||
let mergeBodyPath
|
||||
let fromPrNumber
|
||||
let updatePrNumber
|
||||
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const arg = argv[i]
|
||||
if (arg === '--files-json') {
|
||||
filesJsonPath = argv[i + 1]
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
if (arg === '--merge-body') {
|
||||
mergeBodyPath = argv[i + 1]
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
if (arg === '--from-pr') {
|
||||
fromPrNumber = argv[i + 1]
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
if (arg === '--update-pr') {
|
||||
updatePrNumber = argv[i + 1]
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
printUsage()
|
||||
return 2
|
||||
}
|
||||
|
||||
const pullNumber = updatePrNumber ?? fromPrNumber
|
||||
if (
|
||||
(argv.includes('--files-json') && filesJsonPath == null) ||
|
||||
(argv.includes('--merge-body') && mergeBodyPath == null) ||
|
||||
(argv.includes('--from-pr') && fromPrNumber == null) ||
|
||||
(argv.includes('--update-pr') && updatePrNumber == null) ||
|
||||
(filesJsonPath == null && pullNumber == null)
|
||||
) {
|
||||
printUsage()
|
||||
return 2
|
||||
}
|
||||
|
||||
let files
|
||||
if (filesJsonPath != null) {
|
||||
files = JSON.parse(readFileSync(filesJsonPath, 'utf8'))
|
||||
} else {
|
||||
const repository = resolveRepository()
|
||||
const token = resolveToken()
|
||||
if (repository == null || token == null) {
|
||||
console.error('GITHUB_REPOSITORY and GITHUB_TOKEN are required to read a pull request.')
|
||||
return 2
|
||||
}
|
||||
files = await listPullFiles({
|
||||
...repository,
|
||||
pullNumber: Number(pullNumber),
|
||||
token
|
||||
})
|
||||
}
|
||||
|
||||
const totals = sumChangedFiles(files)
|
||||
writeGithubOutput(totals)
|
||||
|
||||
if (mergeBodyPath != null) {
|
||||
process.stdout.write(mergeLocBlock(readFileSync(mergeBodyPath, 'utf8'), totals))
|
||||
return 0
|
||||
}
|
||||
|
||||
console.log(renderLocBlock(totals))
|
||||
|
||||
if (updatePrNumber == null) {
|
||||
return 0
|
||||
}
|
||||
|
||||
const repository = resolveRepository()
|
||||
const token = resolveToken()
|
||||
if (repository == null || token == null) {
|
||||
console.error('GITHUB_REPOSITORY and GITHUB_TOKEN are required with --update-pr.')
|
||||
return 2
|
||||
}
|
||||
|
||||
return updatePullRequest({
|
||||
...repository,
|
||||
pullNumber: Number(updatePrNumber),
|
||||
token,
|
||||
totals
|
||||
})
|
||||
}
|
||||
|
||||
if (process.argv[1] != null && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
main(process.argv.slice(2))
|
||||
.then((code) => {
|
||||
process.exitCode = code
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error instanceof Error ? error.message : error)
|
||||
process.exitCode = 1
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
export const LOC_BLOCK_START = '<!-- orca-pr-loc -->'
|
||||
export const LOC_BLOCK_END = '<!-- /orca-pr-loc -->'
|
||||
export const LOC_HANDS_OFF_COMMENT =
|
||||
'<!-- Programmatic LoC summary. Do not edit by hand; rewritten on every commit. -->'
|
||||
|
||||
const TEST_DIR_SEGMENT = /(?:^|\/)(?:__tests__|e2e|tests)(?:\/|$)/i
|
||||
const TEST_FILENAME = /\.(?:test|spec|e2e)\.[^/]+$/i
|
||||
|
||||
export function isTestPath(path) {
|
||||
const normalized = path.replaceAll('\\', '/')
|
||||
return TEST_DIR_SEGMENT.test(normalized) || TEST_FILENAME.test(normalized)
|
||||
}
|
||||
|
||||
export function emptyLocTotals() {
|
||||
return {
|
||||
test: { files: 0, added: 0, deleted: 0 },
|
||||
nonTest: { files: 0, added: 0, deleted: 0 }
|
||||
}
|
||||
}
|
||||
|
||||
export function sumChangedFiles(files) {
|
||||
const totals = emptyLocTotals()
|
||||
for (const file of files) {
|
||||
const path = file.filename
|
||||
if (path == null) {
|
||||
continue
|
||||
}
|
||||
const bucket = isTestPath(path) ? totals.test : totals.nonTest
|
||||
bucket.files += 1
|
||||
bucket.added += Number(file.additions ?? 0)
|
||||
bucket.deleted += Number(file.deletions ?? 0)
|
||||
}
|
||||
return totals
|
||||
}
|
||||
|
||||
function signed(count) {
|
||||
if (count === 0) {
|
||||
return '0'
|
||||
}
|
||||
return count > 0 ? `+${count}` : `−${Math.abs(count)}`
|
||||
}
|
||||
|
||||
function locTableRow(label, bucket) {
|
||||
return `| ${label} | ${bucket.files ?? 0} | ${signed(bucket.added)} | ${signed(-(bucket.deleted ?? 0))} | ${signed((bucket.added ?? 0) - (bucket.deleted ?? 0))} |`
|
||||
}
|
||||
|
||||
export function formatLocTable({ test, nonTest }) {
|
||||
return [
|
||||
'| | Files | Added | Deleted | Net |',
|
||||
'| :--- | ---: | ---: | ---: | ---: |',
|
||||
locTableRow('Test', test),
|
||||
locTableRow('Prod', nonTest)
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function renderLocBlock(totals) {
|
||||
return [
|
||||
LOC_BLOCK_START,
|
||||
LOC_HANDS_OFF_COMMENT,
|
||||
'',
|
||||
formatLocTable(totals),
|
||||
'',
|
||||
LOC_BLOCK_END
|
||||
].join('\n')
|
||||
}
|
||||
|
||||
export function mergeLocBlock(body, totals) {
|
||||
const block = renderLocBlock(totals)
|
||||
const current = body ?? ''
|
||||
const start = current.indexOf(LOC_BLOCK_START)
|
||||
const end = current.indexOf(LOC_BLOCK_END)
|
||||
|
||||
if (start !== -1 && end !== -1 && end > start) {
|
||||
const rest = current.slice(end + LOC_BLOCK_END.length).replace(/^\r?\n/, '')
|
||||
if (rest.trim().length === 0) {
|
||||
return `${current.slice(0, start)}${block}\n`
|
||||
}
|
||||
return `${current.slice(0, start)}${block}\n\n${rest.replace(/^\r?\n+/, '')}`
|
||||
}
|
||||
|
||||
if (current.trim().length === 0) {
|
||||
return `${block}\n`
|
||||
}
|
||||
|
||||
return `${block}\n\n${current.replace(/^\r?\n+/, '')}`
|
||||
}
|
||||
@@ -29,6 +29,9 @@ on:
|
||||
- 'electron.vite.config.ts'
|
||||
- 'config/build-plugins/**'
|
||||
- 'src/main/daemon/**'
|
||||
- 'src/main/providers/local-pty-provider.ts'
|
||||
- 'src/main/providers/windows-shell-args.ts'
|
||||
- 'src/main/providers/windows-shell-preflight-runtime.windows.test.ts'
|
||||
- 'native/computer-use-macos/**'
|
||||
- 'native/computer-use-linux/**'
|
||||
- 'native/computer-use-windows/**'
|
||||
@@ -106,6 +109,7 @@ jobs:
|
||||
src/main/computer/macos-native-provider-socket.test.ts
|
||||
src/main/computer/macos-computer-use-permissions.test.ts
|
||||
src/main/computer/macos-computer-use-permission-status.test.ts
|
||||
src/main/providers/windows-shell-preflight-runtime.windows.test.ts
|
||||
src/main/computer/desktop-script-provider-client.test.ts
|
||||
src/main/computer/desktop-script-provider-cache.test.ts
|
||||
src/main/computer/desktop-script-provider-actions.test.ts
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
name: PR test LoC
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
- reopened
|
||||
- ready_for_review
|
||||
|
||||
concurrency:
|
||||
group: pr-test-loc-${{ github.event.pull_request.number }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
loc:
|
||||
name: test vs non-test LoC
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 2
|
||||
steps:
|
||||
# Why no checkout: the Files API already has per-file additions/deletions.
|
||||
- name: Count test vs non-test LoC
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_REPOSITORY: ${{ github.repository }}
|
||||
run: |
|
||||
for script in pr-test-loc-table.mjs pr-test-loc-summary.mjs; do
|
||||
gh api "repos/${GITHUB_REPOSITORY}/contents/.github/scripts/${script}?ref=pull/${{ github.event.pull_request.number }}/head" \
|
||||
--jq .content | base64 --decode > "$RUNNER_TEMP/${script}"
|
||||
done
|
||||
node "$RUNNER_TEMP/pr-test-loc-summary.mjs" --update-pr "${{ github.event.pull_request.number }}"
|
||||
@@ -50,6 +50,27 @@ jobs:
|
||||
- name: Check reliability gate manifest
|
||||
run: pnpm run check:reliability-gates
|
||||
|
||||
- name: Check VM runtime rollback compatibility
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
||||
run: |
|
||||
if git diff --quiet --merge-base "$BASE_SHA" "$HEAD_SHA" -- \
|
||||
src/shared/ephemeral-vm-runtime-store.ts \
|
||||
src/shared/ephemeral-vm-runtime-feature-store.ts \
|
||||
src/shared/ephemeral-vm-runtime-rollback-projection.ts \
|
||||
src/shared/ephemeral-vm-runtimes.ts \
|
||||
src/shared/ephemeral-vm-recipes.ts \
|
||||
src/shared/orca-yaml-hook-types.ts \
|
||||
src/main/ephemeral-vm-runtime-service.ts \
|
||||
src/main/ephemeral-vm-runtime-provisioning-persistence.ts \
|
||||
src/main/ephemeral-vm-failed-start-cleanup.ts; then
|
||||
echo "VM runtime persistence is unchanged."
|
||||
exit 0
|
||||
fi
|
||||
node config/scripts/run-ephemeral-vm-runtime-store-rollback-repro.mjs \
|
||||
config/scripts/ephemeral-vm-runtime-store-cross-version.test.ts
|
||||
|
||||
- name: Enforce max-lines ratchet
|
||||
run: pnpm run check:max-lines-ratchet
|
||||
|
||||
@@ -243,7 +264,10 @@ jobs:
|
||||
src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts \
|
||||
src/main/daemon/shell-ready.test.ts \
|
||||
src/main/daemon/node-pty-fd-leak.test.ts \
|
||||
src/main/providers/local-pty-shell-ready.test.ts \
|
||||
src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \
|
||||
src/main/providers/local-pty-shell-ready-zsh-startup-file-behavior.test.ts \
|
||||
src/main/providers/local-pty-shell-ready-zsh-zdotdir-discovery.test.ts \
|
||||
src/main/providers/local-pty-shell-ready-zsh-zdotdir-normalization.test.ts \
|
||||
src/main/providers/__tests__/shell-ready-framework-example.test.ts \
|
||||
src/main/pty/omp-shell-wrapper.node-pty.test.ts \
|
||||
src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \
|
||||
@@ -279,7 +303,10 @@ jobs:
|
||||
--exclude=src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts \
|
||||
--exclude=src/main/daemon/shell-ready.test.ts \
|
||||
--exclude=src/main/daemon/node-pty-fd-leak.test.ts \
|
||||
--exclude=src/main/providers/local-pty-shell-ready.test.ts \
|
||||
--exclude=src/main/providers/local-pty-shell-ready-zsh-launch-environment.test.ts \
|
||||
--exclude=src/main/providers/local-pty-shell-ready-zsh-startup-file-behavior.test.ts \
|
||||
--exclude=src/main/providers/local-pty-shell-ready-zsh-zdotdir-discovery.test.ts \
|
||||
--exclude=src/main/providers/local-pty-shell-ready-zsh-zdotdir-normalization.test.ts \
|
||||
--exclude=src/main/providers/__tests__/shell-ready-framework-example.test.ts \
|
||||
--exclude=src/main/pty/omp-shell-wrapper.node-pty.test.ts \
|
||||
--exclude=src/renderer/src/components/terminal-pane/fish-color-scheme-child-stdin.node-pty.test.ts \
|
||||
@@ -479,6 +506,9 @@ jobs:
|
||||
HEAD="${{ github.event.pull_request.head.sha }}"
|
||||
CHANGED="$(git diff --name-only --diff-filter=AMCR --merge-base "$BASE" "$HEAD")"
|
||||
TEST_FILES="$(printf '%s\n' "$CHANGED" | grep -E '^tests/e2e/.*\.spec\.ts$' || true)"
|
||||
if printf '%s\n' "$CHANGED" | grep -Eq '^src/(main/ephemeral-vm-(runtime-(service|provisioning-persistence)|failed-start-cleanup)|shared/(ephemeral-vm-runtime-(store|feature-store|rollback-projection|runtimes)|ephemeral-vm-recipes|orca-yaml-hook-types))\.ts$'; then
|
||||
TEST_FILES="$(printf '%s\n%s\n' "$TEST_FILES" 'tests/e2e/ephemeral-vm-provisioned-root.spec.ts' | sort -u)"
|
||||
fi
|
||||
TEST_FILES_JSON="$(printf '%s\n' "$TEST_FILES" | jq --raw-input --slurp --compact-output 'split("\n") | map(select(length > 0))')"
|
||||
echo "test_files=$TEST_FILES_JSON" >> "$GITHUB_OUTPUT"
|
||||
if [ "$TEST_FILES_JSON" != '[]' ]; then
|
||||
|
||||
Reference in New Issue
Block a user