mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
376 lines
15 KiB
JavaScript
376 lines
15 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)
|
|
const ORCHESTRATION_REFERENCES = [
|
|
'coordinator-loop.md',
|
|
'legacy-contract-migration.md',
|
|
'low-level-topology.md',
|
|
'messaging-and-gates.md',
|
|
'placement-and-remote.md',
|
|
'recovery-and-cleanup.md',
|
|
'worker-contract.md'
|
|
]
|
|
|
|
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 compact guides, version-matched reference packages, 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.aliases).toEqual(GUIDE_ALIASES[guide.name])
|
|
if (guide.name !== 'orchestration') {
|
|
expect(guide.fullMarkdown).toBe(source)
|
|
expect(guide.references).toEqual([])
|
|
continue
|
|
}
|
|
// Why: the per-reference selector serves these verbatim, so an entry that
|
|
// drifts from the file on disk ships a stale reference to every agent.
|
|
expect(guide.references.map((reference) => reference.name)).toEqual(
|
|
ORCHESTRATION_REFERENCES.map((reference) => reference.replace(/\.md$/u, ''))
|
|
)
|
|
for (const reference of guide.references) {
|
|
expect(reference.markdown).toBe(
|
|
normalizeMarkdown(
|
|
await readFile(
|
|
path.join(
|
|
projectDir,
|
|
'skill-guides',
|
|
'orchestration',
|
|
'references',
|
|
`${reference.name}.md`
|
|
),
|
|
'utf8'
|
|
)
|
|
)
|
|
)
|
|
}
|
|
expect(guide.fullMarkdown).not.toBe(guide.markdown)
|
|
expect(guide.fullMarkdown.length).toBeGreaterThan(guide.markdown.length)
|
|
expect(guide.fullMarkdown.startsWith(source.trimEnd())).toBe(true)
|
|
for (const reference of ORCHESTRATION_REFERENCES) {
|
|
const marker = `<!-- bundled-reference: references/${reference} -->`
|
|
expect(guide.fullMarkdown.split(marker)).toHaveLength(2)
|
|
expect(guide.fullMarkdown).toContain(
|
|
await readFile(
|
|
path.join(projectDir, 'skill-guides', 'orchestration', 'references', reference),
|
|
'utf8'
|
|
)
|
|
)
|
|
}
|
|
}
|
|
})
|
|
|
|
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'))
|
|
}
|
|
for (const reference of ORCHESTRATION_REFERENCES) {
|
|
const referencePath = path.join(
|
|
root,
|
|
'skill-guides',
|
|
'orchestration',
|
|
'references',
|
|
reference
|
|
)
|
|
const source = await readFile(referencePath, 'utf8')
|
|
await writeFile(referencePath, source.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')
|
|
})
|
|
|
|
it('rejects non-Markdown and empty bundled references', async () => {
|
|
const root = await createFixture()
|
|
const referenceRoot = path.join(root, 'skill-guides', 'orchestration', 'references')
|
|
|
|
await writeFile(path.join(referenceRoot, 'notes.txt'), 'not a reference\n')
|
|
await expect(buildArtifacts(root)).rejects.toThrow('Guide references must be Markdown files')
|
|
await rm(path.join(referenceRoot, 'notes.txt'))
|
|
await writeFile(path.join(referenceRoot, 'empty.md'), '\n')
|
|
await expect(buildArtifacts(root)).rejects.toThrow('Guide reference is empty')
|
|
})
|
|
})
|