mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
* fix(ssh): fence stale kills and retired pane replay * fix(ssh): support cancellable interactive authentication * fix(ssh): await remote catalog before snapshot adoption * fix(pty): contain Windows ConPTY input failures * fix(power): avoid redundant macOS display blocking * perf(editor): narrow markdown override subscriptions * fix(quick-open): close directory handles after reads * refactor(linux): remove unused proc socket scanner * fix(usage): apply flat Sonnet 4.6 pricing * ci: prime Node next native test cache * docs(skills): resolve snapshot cleanup data path * fix(ssh): recover install locks after host reboot * test(ssh): recognize boot-aware install locks * test(ssh): prove previous-boot lock recovery live * test(wire): pin pre-metadata release coverage * fix(terminal): preserve remote tab ownership through recovery races * test(runtime): fence replaced terminal handles in agent guard * fix(ssh): preserve remote snapshot authority across polls * fix(pty): contain late ConPTY output EPIPE * test(pty): register Windows exit watcher before kill * fix: close SSH and tab readiness race gaps * fix(tabs): retain headless order and placeholder titles * fix(build): avoid parallel electron-vite config race * test(windows): avoid MSYS temp path rewriting * test(windows): avoid killing exited PTY * fix(pty): avoid late ConPTY input teardown race * fix(terminal): sync reconnect error ownership after commit * fix(runtime): use canonical worktree identity comparison * test(ssh): assert complete cold-hydration baseline * test(windows): invoke quoted retention fixture via PowerShell * test(windows): read ConPTY grid through mode con * fix(terminal): publish PTY replacements atomically * fix(terminal): infer stale identity on reattach * fix(terminal): fence stale pane PTY callbacks * fix(terminal): fence stale pane binds after rebind * fix(terminal): reject stale pane transport callbacks * fix(terminal): fence mirrored reattach spawn callbacks * fix(terminal): replace stale pane PTYs on remount * fix(ci): size the Windows launcher-compile test budget from measurement `native-smoke (windows-latest)` fails ~4.5% of runs on `preserves a multiline argument through the compiled remote launcher` with "Test timed out in 15000ms" — on unrelated PRs, for reasons that have nothing to do with them. Across 176 sampled attempts it is the only red that job produced, and it hit seven different PRs in two days: #16900, #16904, #16915, #16955 (twice), #16979, #17014, #17085. The test is six process creations: powershell.exe forks csc.exe, then the freshly compiled orca.exe forks node.exe, twice. Hosted Windows runners periodically slow process creation down, and this test amplifies that far harder than anything else in the job. Comparing the 80 attempts where it ran under 3s against the 12 where it ran over 12s, its own median goes 2198ms -> 15917ms (7.2x) while the same file's powershell-only test moves 556 -> 686ms (1.2x), the cmd.exe and Git Bash process tests in the neighbouring file move 1.4x, and the other 35 files put together move 1.5x. Measured across those 176 attempts: 1881ms to 35438ms, p50 4264ms, correlation +0.881 with the job's total Vitest duration. 8 of 176 (4.5%) exceeded the 15s cap; 2 of 176 (1.1%) also exceeded the shared 30s testTimeout, so deleting the override and inheriting the config is not enough on its own. 60s clears all 176 with 1.7x headroom on the worst. This is slow, not hung. Every body here is synchronous spawnSync, so Vitest cannot interrupt one — the timer fires only after the body returns and the reported duration is real elapsed time. That is why a failure reads `× ... 22464ms` under `Test timed out in 15000ms`. The work finished; the stopwatch was short. Seven reruns at one identical head measured 2053 / 4680 / 5551 / 8732 / 13506 / 14868 / 21937ms — the last of those would have been red on code that had not changed. The 15s came from #8897, which raised this test off Vitest's built-in 5s default because the job then ran bare `pnpm vitest run`. #8909 landed 3h27m later and pointed the job at config/vitest.config.ts, which is the real fix for that. The constant stayed behind and has been the binding budget ever since. * fix(terminal): fence stale remount reattach ownership * fix(terminal): reconcile mounted pane identity after replacement * fix(terminal): fence stale reattach fallback ownership * fix(terminal): fence deferred SSH reattach ownership * fix(terminal): fence stale split pane ownership callbacks * fix(terminal): keep stale spawns from consuming startup --------- Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
307 lines
12 KiB
JavaScript
307 lines
12 KiB
JavaScript
import { execFile } from 'node:child_process'
|
|
import { cp, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import path from 'node:path'
|
|
import { promisify } from 'node:util'
|
|
import { afterEach, describe, expect, it } from 'vitest'
|
|
import { BUNDLED_SKILL_GUIDES } from '../../src/cli/bundled-skill-guides'
|
|
import {
|
|
CANONICAL_GUIDE_NAMES,
|
|
GUIDE_ALIASES,
|
|
STUB_TOPICS,
|
|
assertAliasContract,
|
|
buildArtifacts,
|
|
frontmatterBlock,
|
|
normalizeMarkdown,
|
|
parseFrontmatter,
|
|
toPosixRelativePath,
|
|
verifyArtifacts,
|
|
writeArtifacts
|
|
} from './generate-bundled-skill-guides.mjs'
|
|
|
|
const projectDir = path.resolve(import.meta.dirname, '..', '..')
|
|
const temporaryDirectories = []
|
|
const execFileAsync = promisify(execFile)
|
|
|
|
async function createFixture() {
|
|
const root = await mkdtemp(path.join(tmpdir(), 'orca-bundled-skill-guides-'))
|
|
temporaryDirectories.push(root)
|
|
await Promise.all([
|
|
cp(path.join(projectDir, 'skill-guides'), path.join(root, 'skill-guides'), {
|
|
recursive: true
|
|
}),
|
|
cp(path.join(projectDir, 'skill-stubs'), path.join(root, 'skill-stubs'), {
|
|
recursive: true
|
|
}),
|
|
cp(path.join(projectDir, 'skills'), path.join(root, 'skills'), { recursive: true }),
|
|
mkdir(path.join(root, 'src', 'cli'), { recursive: true })
|
|
])
|
|
return root
|
|
}
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(
|
|
temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true }))
|
|
)
|
|
})
|
|
|
|
describe('bundled skill guide generator', () => {
|
|
it('keeps every fat (non-stub) projection byte-identical to its authoritative source', async () => {
|
|
for (const name of CANONICAL_GUIDE_NAMES) {
|
|
if (STUB_TOPICS.includes(name)) {
|
|
continue
|
|
}
|
|
const source = await readFile(path.join(projectDir, 'skill-guides', `${name}.md`))
|
|
const projection = await readFile(path.join(projectDir, 'skills', name, 'SKILL.md'))
|
|
expect(projection, name).toEqual(source)
|
|
}
|
|
})
|
|
|
|
it('projects stub topics as hybrid discovery stubs that reuse the guide frontmatter', async () => {
|
|
expect(STUB_TOPICS.length).toBeGreaterThan(0)
|
|
for (const name of STUB_TOPICS) {
|
|
const source = await readFile(path.join(projectDir, 'skill-guides', `${name}.md`), 'utf8')
|
|
const projection = await readFile(path.join(projectDir, 'skills', name, 'SKILL.md'), 'utf8')
|
|
|
|
// The routing frontmatter is the unchanged discovery surface.
|
|
expect(projection.startsWith(frontmatterBlock(source, `${name}.md`))).toBe(true)
|
|
// The stub is a thin hybrid pointer, not the full guide.
|
|
expect(projection).not.toEqual(source)
|
|
expect(projection.length).toBeLessThan(source.length)
|
|
expect(projection).toContain('discovery stub')
|
|
expect(projection).toContain(`skills get ${name}`)
|
|
}
|
|
})
|
|
|
|
it('keeps pre-guide fallback useful and read-only for every converted domain', async () => {
|
|
const expectedFallbackCommands = {
|
|
'computer-use': ['ORCA computer capabilities --json', 'ORCA computer list-apps --json'],
|
|
'linear-tickets': ['ORCA linear --help', 'ORCA linear issue --current --full --json'],
|
|
'orca-emulator': ['ORCA emulator list --json'],
|
|
'orca-emulator-android': ['ORCA emulator devices --json'],
|
|
'orca-linear': ['ORCA linear --help', 'ORCA linear issue --current --full --json'],
|
|
'orca-per-workspace-env': ['ORCA vm recipe doctor <recipe-id> --repo-path <repo> --json'],
|
|
orchestration: ['ORCA orchestration task-list --json', 'ORCA terminal list --json']
|
|
}
|
|
|
|
for (const [name, commands] of Object.entries(expectedFallbackCommands)) {
|
|
const stub = await readFile(path.join(projectDir, 'skill-stubs', `${name}.md`), 'utf8')
|
|
const fallback = stub.split('## If an older Orca does not recognize `skills get`')[1]
|
|
|
|
expect(fallback, name).toBeDefined()
|
|
for (const command of commands) {
|
|
expect(fallback, name).toContain(command)
|
|
}
|
|
expect(fallback, name).not.toContain('ORCA worktree ps --json')
|
|
}
|
|
})
|
|
|
|
it('uses the exported recipe id variable in per-workspace environment examples', async () => {
|
|
const source = await readFile(
|
|
path.join(projectDir, 'skill-guides', 'orca-per-workspace-env.md'),
|
|
'utf8'
|
|
)
|
|
|
|
expect(source).toContain('ORCA_RECIPE_ID')
|
|
expect(source).not.toContain('ORCA_VM_RECIPE_ID')
|
|
expect(source).toContain('recipe_id="${recipe_id//./-}"')
|
|
expect(source).toContain('max_recipe_id_length=$((128 - ${#instance_id} - 6))')
|
|
expect(source).toContain('name="orca-${recipe_id:0:max_recipe_id_length}-${instance_id}"')
|
|
})
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'resolves snapshot cleanup through Orca user-data precedence',
|
|
async () => {
|
|
const source = await readFile(
|
|
path.join(projectDir, 'skill-guides', 'orca-per-workspace-env.md'),
|
|
'utf8'
|
|
)
|
|
const assignment =
|
|
'orca_user_data_path="${ORCA_USER_DATA_PATH:-${XDG_CONFIG_HOME:-$HOME/.config}/orca}"'
|
|
expect(source).toContain(assignment)
|
|
const renderPath = async (env) =>
|
|
(
|
|
await execFileAsync(
|
|
'bash',
|
|
['-u', '-c', `${assignment}; printf '%s' "$orca_user_data_path"`],
|
|
{
|
|
env
|
|
}
|
|
)
|
|
).stdout
|
|
|
|
await expect(renderPath({ HOME: '/home/orca' })).resolves.toBe('/home/orca/.config/orca')
|
|
await expect(
|
|
renderPath({ HOME: '/home/orca', XDG_CONFIG_HOME: '/srv/config' })
|
|
).resolves.toBe('/srv/config/orca')
|
|
await expect(
|
|
renderPath({
|
|
HOME: '/home/orca',
|
|
XDG_CONFIG_HOME: '/srv/config',
|
|
ORCA_USER_DATA_PATH: '/var/lib/orca-custom'
|
|
})
|
|
).resolves.toBe('/var/lib/orca-custom')
|
|
}
|
|
)
|
|
|
|
it.skipIf(process.platform === 'win32')(
|
|
'keeps Vercel sandbox names valid while preserving the instance suffix',
|
|
async () => {
|
|
const source = await readFile(
|
|
path.join(projectDir, 'skill-guides', 'orca-per-workspace-env.md'),
|
|
'utf8'
|
|
)
|
|
const startMarker = 'recipe_id="${ORCA_RECIPE_ID:-vercel-sandbox}"'
|
|
const endMarker = 'name="orca-${recipe_id:0:max_recipe_id_length}-${instance_id}"'
|
|
const start = source.indexOf(startMarker)
|
|
const endStart = source.indexOf(endMarker, start)
|
|
expect(start).toBeGreaterThanOrEqual(0)
|
|
expect(endStart).toBeGreaterThan(start)
|
|
const script = `${source.slice(start, endStart + endMarker.length)}\nprintf '%s' "$name"`
|
|
const renderName = async (recipeId, instanceId) =>
|
|
(
|
|
await execFileAsync('bash', ['-u', '-c', script], {
|
|
env: { ...process.env, ORCA_RECIPE_ID: recipeId, ORCA_VM_INSTANCE_ID: instanceId }
|
|
})
|
|
).stdout
|
|
|
|
const instanceId = 'orca-123e4567-e89b-12d3-a456-426614174000'
|
|
const dotted = await renderName('provider.cloud_sandbox', instanceId)
|
|
const maximum = await renderName(`a${'.'.repeat(63)}`, instanceId)
|
|
const longInstanceId = 'i'.repeat(100)
|
|
const capped = await renderName(
|
|
'provider.cloud_sandbox.with.a.long.recipe.identifier',
|
|
longInstanceId
|
|
)
|
|
|
|
expect(dotted).toBe(`orca-provider-cloud_sandbox-${instanceId}`)
|
|
expect(maximum).toMatch(/^[a-zA-Z0-9_-]{1,128}$/u)
|
|
expect(capped).toHaveLength(128)
|
|
expect(capped.endsWith(`-${longInstanceId}`)).toBe(true)
|
|
}
|
|
)
|
|
|
|
it('embeds canonical names, discovery descriptions, Markdown, and append-only aliases', async () => {
|
|
expect(BUNDLED_SKILL_GUIDES.map((guide) => guide.name)).toEqual(
|
|
[...CANONICAL_GUIDE_NAMES].sort((left, right) => left.localeCompare(right, 'en'))
|
|
)
|
|
|
|
for (const guide of BUNDLED_SKILL_GUIDES) {
|
|
const source = await readFile(
|
|
path.join(projectDir, 'skill-guides', `${guide.name}.md`),
|
|
'utf8'
|
|
)
|
|
const frontmatter = parseFrontmatter(source, `${guide.name}.md`)
|
|
expect(guide.description).toBe(frontmatter.description)
|
|
expect(guide.markdown).toBe(source)
|
|
expect(guide.fullMarkdown).toBe(source)
|
|
expect(guide.aliases).toEqual(GUIDE_ALIASES[guide.name])
|
|
}
|
|
})
|
|
|
|
it('keeps CLI guide examples safe across shells and Linux command names', async () => {
|
|
for (const name of ['orca-cli', 'computer-use', 'orca-emulator', 'orca-emulator-android']) {
|
|
const source = await readFile(path.join(projectDir, 'skill-guides', `${name}.md`), 'utf8')
|
|
|
|
expect(source).toContain('ORCA_CLI_COMMAND')
|
|
expect(source).toContain('orca-dev')
|
|
expect(source).toContain('orca-ide')
|
|
expect(source).toContain('PowerShell')
|
|
expect(source).toContain('cmd.exe')
|
|
expect(source).toMatch(/^ORCA .+--json$/mu)
|
|
// Why: bare command lines can launch GNOME Orca, while shell variables make
|
|
// the same guide unusable from PowerShell and cmd.exe.
|
|
expect(source).not.toMatch(/^orca /mu)
|
|
expect(source).not.toMatch(/\$ORCA(?:_|\b)/u)
|
|
}
|
|
})
|
|
|
|
it('builds deterministic artifacts and verifies the checked-in outputs', async () => {
|
|
const first = await buildArtifacts(projectDir)
|
|
const second = await buildArtifacts(projectDir)
|
|
|
|
expect(second).toEqual(first)
|
|
await expect(verifyArtifacts(first, projectDir)).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('generates platform-identical output from CRLF guide sources', async () => {
|
|
const expected = await buildArtifacts(projectDir)
|
|
const root = await createFixture()
|
|
for (const name of CANONICAL_GUIDE_NAMES) {
|
|
const sourcePath = path.join(root, 'skill-guides', `${name}.md`)
|
|
const source = await readFile(sourcePath, 'utf8')
|
|
await writeFile(sourcePath, source.replaceAll('\n', '\r\n'))
|
|
}
|
|
for (const name of STUB_TOPICS) {
|
|
const stubPath = path.join(root, 'skill-stubs', `${name}.md`)
|
|
const stubSource = await readFile(stubPath, 'utf8')
|
|
await writeFile(stubPath, stubSource.replaceAll('\n', '\r\n'))
|
|
}
|
|
|
|
const actual = await buildArtifacts(root)
|
|
expect(actual.map((artifact) => artifact.content)).toEqual(
|
|
expected.map((artifact) => artifact.content)
|
|
)
|
|
})
|
|
|
|
it('pins guide sources, projections, and embedded output to LF in Git', async () => {
|
|
const attributes = await readFile(path.join(projectDir, '.gitattributes'), 'utf8')
|
|
expect(normalizeMarkdown(attributes)).toContain('/skill-guides/*.md text eol=lf\n')
|
|
expect(normalizeMarkdown(attributes)).toContain('/skill-stubs/*.md text eol=lf\n')
|
|
expect(normalizeMarkdown(attributes)).toContain('/skills/*/SKILL.md text eol=lf\n')
|
|
expect(normalizeMarkdown(attributes)).toContain(
|
|
'/src/cli/bundled-skill-guides.ts text eol=lf\n'
|
|
)
|
|
})
|
|
|
|
it('reports stale outputs and write mode repairs all projections', async () => {
|
|
const root = await createFixture()
|
|
const artifacts = await buildArtifacts(root)
|
|
|
|
await expect(verifyArtifacts(artifacts, root)).rejects.toThrow(
|
|
'src/cli/bundled-skill-guides.ts'
|
|
)
|
|
await writeArtifacts(artifacts)
|
|
await expect(verifyArtifacts(artifacts, root)).resolves.toBeUndefined()
|
|
|
|
await writeFile(path.join(root, 'skills', 'computer-use', 'SKILL.md'), 'stale\n')
|
|
await expect(verifyArtifacts(artifacts, root)).rejects.toThrow('skills/computer-use/SKILL.md')
|
|
})
|
|
|
|
// Why: the stale-artifact assertions above only hit the Windows separator when the host is
|
|
// Windows; injecting path.win32 makes the Linux/macOS shards catch the regression too.
|
|
it('formats contributor-facing paths with forward slashes on every platform', () => {
|
|
expect(
|
|
toPosixRelativePath('C:\\repo', 'C:\\repo\\src\\cli\\bundled-skill-guides.ts', path.win32)
|
|
).toBe('src/cli/bundled-skill-guides.ts')
|
|
expect(
|
|
toPosixRelativePath('C:\\repo', 'C:\\repo\\skills\\computer-use\\SKILL.md', path.win32)
|
|
).toBe('skills/computer-use/SKILL.md')
|
|
expect(toPosixRelativePath('/repo', '/repo/skills/computer-use/SKILL.md', path.posix)).toBe(
|
|
'skills/computer-use/SKILL.md'
|
|
)
|
|
})
|
|
|
|
it('rejects mismatched source names and ambiguous aliases', async () => {
|
|
const root = await createFixture()
|
|
await writeFile(
|
|
path.join(root, 'skill-guides', 'computer-use.md'),
|
|
'---\nname: wrong\ndescription: present\n---\n'
|
|
)
|
|
await expect(buildArtifacts(root)).rejects.toThrow('declares mismatched name wrong')
|
|
|
|
expect(() =>
|
|
assertAliasContract([
|
|
{ name: 'first', aliases: ['legacy'] },
|
|
{ name: 'second', aliases: ['legacy'] }
|
|
])
|
|
).toThrow('assigned more than once')
|
|
expect(() =>
|
|
assertAliasContract([
|
|
{ name: 'first', aliases: ['second'] },
|
|
{ name: 'second', aliases: [] }
|
|
])
|
|
).toThrow('collides with canonical name')
|
|
})
|
|
})
|