mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
* [comment audit] Align legacy failure-read counts with cached display * [comment audit] Condense shared provider lifecycle and browser recovery comments [skip ci] * [comment audit] Condense shared generation, path and preview comments [skip ci] * [comment audit] Condense host, file and Git contract rationale [skip ci] * [comment audit] Tighten Git lifecycle and provider contract comments [skip ci] * [comment audit] Clarify shared host authority and launch isolation [skip ci] * [comment audit] simplify local access mobile and native chat contracts [skip ci] * [comment audit] simplify native chat presentation and state contracts [skip ci] * [comment audit] simplify native runtime and provider contracts [skip ci] * [comment audit] simplify migration and orchestration contracts [skip ci] * [comment audit] simplify plugin trust and process capture rationale [skip ci] * [comment audit] simplify terminal delivery and remote compatibility rationale [skip ci] * [comment audit] clarify remote recovery and launch wire rationale [skip ci] * [comment audit] shorten shared RPC compatibility and runtime evidence comments [skip ci] * [comment audit] simplify secure storage shell startup and sidebar rationale [skip ci] * [comment audit] clarify sidebar, skill and SSH invariants [skip ci] * [comment audit] condense structured chat delivery and timing rationale [skip ci] * [comment audit] condense terminal replay and telemetry invariants [skip ci] * [comment audit] clarify launch permission and terminal delivery rationale [skip ci] * [comment audit] clarify workspace ownership and layout contracts [skip ci] * [comment audit] wait for fitted and painted SSH terminal links * [comment audit] clarify layout persistence and session ownership [skip ci] * [comment audit] clarify host routing and workspace lifecycle contracts [skip ci] * [comment audit] clarify startup persistence and app lifetime comments [skip ci] * [comment audit] clarify recovery and activity boundaries [skip ci] * [comment audit] condense activity and artifact invariants [skip ci] * [comment audit] condense automation authority contracts [skip ci] * [comment audit] simplify automation scheduling and history contracts [skip ci] * [comment audit] clarify browser chrome and annotation lifecycles [skip ci] * [comment audit] clarify browser routing and retained guest ownership [skip ci] * [comment audit] clarify browser recovery and document ownership comments [skip ci] * [comment audit] restore hosted runners required by native IME fixtures * [comment audit] shorten preview, palette and dashboard invariants [skip ci] * [comment audit] clarify terminal preview, dashboard and retention comments [skip ci] * [comment audit] clarify diff-note recovery and editor lifecycle comments [skip ci] * [comment audit] shorten rich-text and combined-diff ownership comments [skip ci] * [comment audit] shorten autosave, draft recovery and model ownership comments [skip ci] * [comment audit] shorten save-queue, notebook and Markdown constraints [skip ci] * [comment audit] shorten Monaco, PDF and rich Markdown safeguards [skip ci] * [comment audit] clarify rich Markdown source and selection safeguards [skip ci] * [comment audit] clarify editor loading and emulator stream lifetimes [skip ci] * [comment audit] trim emulator and feature tour narration [skip ci] * [comment audit] clarify floating workspace and GitHub cache ownership [skip ci] * [comment audit] trim provider controls and Linear filter explanations [skip ci] * [comment audit] clarify pairing and chat resume lifetimes [skip ci] * [comment audit] clarify chat delivery and transcript lifetimes [skip ci] * [comment audit] clarify session options and host-owned status [skip ci] * [comment audit] clarify draft durability and live-status precedence [skip ci] * [comment audit] Trim chat send and model ownership explanations [skip ci] * [comment audit] Shorten transcript ownership and send recovery comments [skip ci] * [comment audit] Simplify native chat composer and transcript hook comments [skip ci] * [comment audit] Shorten native chat scroll and structured session explanations [skip ci] * [comment audit] Trim workspace picker and onboarding commentary [skip ci] * [comment audit] Simplify pet rendering, review cache and quick open comments [skip ci] * [comment audit] Trim sidebar and session history explanations [skip ci] * [comment audit] Condense session resume and review checks comments [skip ci] * [comment audit] Shorten checks state and file explorer refresh comments [skip ci] * [comment audit] Trim plugin bridge and source-control action rationale [skip ci] * [comment audit] Trim source-control listing and review rationale [skip ci] * [comment audit] Shorten sidebar lifecycle and explorer safeguards [skip ci] * [comment audit] Condense Settings account and appearance rationale [skip ci] * [comment audit] Trim pairing and repository Settings explanations [skip ci] * [comment audit] Condense Settings search SSH and terminal safeguards [skip ci] * [comment audit] Shorten Settings voice and provider ownership comments [skip ci] * [comment audit] Condense Settings draft persistence and host-state rationale [skip ci] * [comment audit] Trim Settings hooks and sidebar setup rationale [skip ci] * [comment audit] Condense sidebar deletion and board-filter safeguards [skip ci] * [comment audit] Condense worktree cards, host ownership and Markdown contracts [skip ci] * [comment audit] Condense sidebar drag, folder and filtering contracts [skip ci] * [comment audit] Condense sidebar parity, sleep and host ownership contracts [skip ci] * [comment audit] Prune focus-read baseline after comment cleanup * [comment audit] Clarify sidebar visibility, board drops and agent evidence [skip ci] * [comment audit] Tighten sidebar listing, drag and reveal contracts [skip ci] * [comment audit] Clarify viewport geometry and skill update safeguards [skip ci] * [comment audit] Tighten skill inventory, usage and host status comments [skip ci] * [comment audit] Clarify usage ownership and resource kill safeguards [skip ci] * [comment audit] Tighten tab selection, rename and search contracts [skip ci] * [comment audit] Tighten tab-strip geometry and drag ownership comments [skip ci] * [comment audit] Tighten Tasks recovery, pagination and mutation comments [skip ci] * [comment audit] Tighten provider loading and terminal recovery comments [skip ci] * [comment audit] Tighten completion, cadence and PTY ownership comments [skip ci] * [comment audit] Clarify terminal restore and parked-pane boundaries [skip ci] * [comment audit] Tighten PTY startup and replay explanations [skip ci] * [comment audit] Clarify PTY identity and delivery ownership [skip ci] * [comment audit] Shorten remote reconnect and input replay rationale [skip ci] * [comment audit] Condense terminal parking clipboard and file link comments [skip ci] * [comment audit] Clarify IME ownership and trim trace history [skip ci] * [comment audit] Shorten pane recovery and parked watcher explanations [skip ci] * [comment audit] Condense snapshot replay and visibility recovery rationale [skip ci] * [comment audit] Shorten terminal parking and keyboard ownership rationale [skip ci] * [comment audit] Trim renderer lifecycle and menu rationale [skip ci] * [comment audit] Condense workspace cleanup ownership and settlement notes [skip ci] * [comment audit] Clarify renderer hook evidence and source ownership [skip ci] * [comment audit] Tighten IPC ownership and reconnect explanations [skip ci] * [comment audit] Trim renderer focus, paste and attention comments [skip ci] * [comment audit] Shorten scroll, locale and agent lifecycle comments [skip ci] * [comment audit] Trim agent launch, resume and ownership commentary [skip ci] * [comment audit] Shorten account, browser and editor comments [skip ci] * [comment audit] Clarify host-mirror waits, previews and input ownership [skip ci] * [comment audit] Condense launch recovery and editor tokenization comments [skip ci] * [comment audit] Condense workspace selection and terminal pane rationale [skip ci] * [comment audit] Condense terminal fitting and rendering recovery comments [skip ci] * [comment audit] Condense terminal input and host routing explanations [skip ci] * [comment audit] Condense ownership and sleeping-session recovery rationale [skip ci] * [comment audit] Condense structured launch and terminal link explanations [skip ci] * [comment audit] Condense terminal ownership and workspace file routing rationale [skip ci] * [comment audit] Condense session persistence and workspace activation rationale [skip ci] * [comment audit] Condense workspace routing and browser ownership rationale [skip ci] * [comment audit] Condense mirror recovery and layout authority rationale [skip ci] * [comment audit] Condense runtime transport and file safety rationale [skip ci] * [comment audit] Condense runtime graph and structured session rationale [skip ci] * [comment audit] Condense browser creation and remote tab recovery rationale [skip ci] * [comment audit] Condense snapshot authority and publisher recovery rationale [skip ci] * [comment audit] Condense frame, startup and provider cache rationale [skip ci] * [comment audit] Condense host twin and agent lifecycle rationale [skip ci] * [comment audit] Condense browser authority and editor cleanup rationale [skip ci] * [comment audit] Condense editor buffer identity and review cache rationale [skip ci] * [comment audit] Condense provider lifetime and repository ownership rationale [skip ci] * [comment audit] Condense reconnect, hydration and terminal retirement rationale [skip ci] * [comment audit] Condense recovery, reconciliation and persistence rationale [skip ci] * [comment audit] Condense cleanup ownership and listing admission rationale [skip ci] * [comment audit] Condense teardown ordering and terminal ownership rationale [skip ci] * [comment audit] Condense recovery ledger and web host-boundary rationale [skip ci] * [comment audit] Condense web reconnect and credential lifecycle rationale [skip ci] * [comment audit] Condense cloud rollout and assignment safety rationale [skip ci] * [comment audit] Condense relay session and database recovery rationale [skip ci] * [comment audit] Condense relay placement and protocol compatibility rationale [skip ci] * [comment audit] Condense build and packaging constraints [skip ci] * [comment audit] Condense CI gates and validation isolation rationale [skip ci] * [comment audit] Condense catalog identity and browser validation rationale [skip ci] * [comment audit] Condense mobile browser and frame-budget rationale [skip ci] * [comment audit] Shorten mobile route and browser test explanations [skip ci] * [comment audit] Condense editor and session seam test rationale [skip ci] * [comment audit] Trim session and terminal closure history [skip ci] * [comment audit] Shorten terminal browser and census explanations [skip ci] * [comment audit] Preserve root-only E2E diagnostic baseline * [comment audit] Condense mobile seam and HTML preview test rationale [skip ci] * [comment audit] Condense preview instrumentation and render harness rationale [skip ci] * [comment audit] Condense bundle contracts and native-build constraints [skip ci] * [comment audit] Shorten native packaging and CI routing rationale [skip ci] * [comment audit] Condense release tooling and benchmark constraints [skip ci] * [comment audit] Condense packaging and platform gate rationale [skip ci] * [comment audit] Shorten mobile route and history rationale [skip ci] * [comment audit] Condense mobile browser and component rationale [skip ci] * [comment audit] Shorten drawer and workspace-form rationale [skip ci] * [comment audit] Trim mobile review and editor-document explanations [skip ci] * [comment audit] Condense editor lifecycle and Markdown explanations [skip ci] * [comment audit] Trim mobile file, diagnostics and dictation commentary [skip ci] * [comment audit] Simplify mobile host-list and workspace-view explanations [skip ci] * [comment audit] Clarify mobile shell lifetime and bridge delivery constraints [skip ci] * [comment audit] Condense bridge stream, native request and audio rationale [skip ci] * [comment audit] Reduce bridge schema and page-client commentary [skip ci] * [comment audit] Tighten mobile page bridge contracts [skip ci] * [comment audit] Clarify mobile cache and session constraints [skip ci] * [comment audit] Condense mobile route and lifecycle rationale [skip ci] * [comment audit] Tighten mobile shell hook timing rationale [skip ci] * [comment audit] Clarify route handoff and notification compatibility [skip ci] * [comment audit] Clarify push registration and dictation cleanup constraints [skip ci] * [comment audit] Condense keyboard geometry and native media ownership rationale [skip ci] * [comment audit] Tighten mobile chat scroll and composer concurrency rationale [skip ci] * [comment audit] Condense mobile reply compatibility and upload ownership rationale [skip ci] * [comment audit] Condense optimistic echo and scroll ownership rationale [skip ci] * [comment audit] Tighten stream replay and queued session ownership rationale [skip ci] * [comment audit] Condense mobile composer and reply-schema constraints [skip ci] * [comment audit] Clarify mobile transcript recovery and session ownership [skip ci] * [comment audit] Clarify mobile queue and source-control compatibility [skip ci] * [comment audit] Condense storage and task launch constraints [skip ci] * [comment audit] Clarify task schemas and terminal document ownership [skip ci] * [comment audit] Tighten terminal input and viewport rationale [skip ci] * [comment audit] Shorten terminal and RPC recorder contracts [skip ci] * [comment audit] Condense recorder engine and connection contracts [skip ci] * [comment audit] Shorten host credential and relay recovery comments [skip ci] * [comment audit] Tighten pairing recovery and bundle transport rationale [skip ci] * [comment audit] Clarify RPC delivery and workspace catalog boundaries [skip ci] * [comment audit] Condense catalog and relay entry contracts [skip ci] * [comment audit] Shorten shutdown and SSH relay delivery contracts [skip ci] * [comment audit] Condense WSL relay and E2E regression rationale [skip ci] * [comment audit] Shorten cross-version harness and renderer fixture explanations [skip ci] * [comment audit] Condense paired-host and E2E lifecycle rationale [skip ci] * [comment audit] Shorten layout oracle and regression explanations [skip ci] * [comment audit] Condense paired recovery and ownership regression contracts [skip ci] * [comment audit] Shorten restart, sidebar and SSH lifecycle contracts [skip ci] * [comment audit] Condense terminal rendering and IME regression rationale [skip ci] * [comment audit] Shorten terminal recovery and benchmark contracts [skip ci] * [comment audit] Condense tooling and Windows lifecycle proof rationale [skip ci] * [comment audit] Condense workflow routing and release rationale [skip ci] * [comment audit] Condense mobile security and installer contracts [skip ci] * [comment audit] Shorten native rationale and stylesheet workarounds [skip ci] * [comment audit] Shorten Windows visibility probe rationale [skip ci] * [comment audit] Condense embedded release and signing rationale [skip ci] * [comment audit] Retain current main test behavior in final reconciliation [skip ci] * [comment audit] Complete final comment review across current source inventory * [comment audit] Handle consolidated audit reports before final CI * [comment audit] Preserve credential storage census detail [skip ci] * [comment audit] Scale broad changed E2E selections with verified timing shards * [comment audit] Align E2E assertions with browser close and settled session metadata * [comment audit] Match existing E2Es to current resource and windowing contracts * [comment audit] Observe the shared server outage notice in reconnect coverage * [comment audit] Make existing renderer probes measure settled tab and PTY work * [comment audit] Align palette E2E selector with task search * [comment audit] Refresh remaining palette search selectors * [comment audit] Keep selector correction within selected audit specs * [comment audit] Await emoji fixture settings inside renderer evaluation * [comment audit] Cancel fixture fit retries between terminal tests --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
341 lines
14 KiB
JavaScript
341 lines
14 KiB
JavaScript
import { execFileSync } from 'node:child_process'
|
||
import {
|
||
chmod,
|
||
copyFile,
|
||
mkdir,
|
||
mkdtemp,
|
||
readFile,
|
||
realpath,
|
||
rm,
|
||
symlink,
|
||
writeFile
|
||
} from 'node:fs/promises'
|
||
import { tmpdir } from 'node:os'
|
||
import path from 'node:path'
|
||
import { afterEach, describe, expect, it } from 'vitest'
|
||
import { parse } from 'yaml'
|
||
import { observeSkillPackage } from '../../src/main/skills/skill-package-identity'
|
||
import {
|
||
classifyFile,
|
||
collectPackageFiles,
|
||
describeFile,
|
||
findDroppedReleasedDigests,
|
||
normalizeText,
|
||
packageDigest,
|
||
sortManifestFiles,
|
||
withDigests
|
||
} from './generate-skill-bundle-manifest.mjs'
|
||
|
||
const temporaryDirectories = []
|
||
const REPO_ROOT = path.resolve(import.meta.dirname, '..', '..')
|
||
|
||
async function createPackage() {
|
||
const directory = await mkdtemp(path.join(tmpdir(), 'orca-skill-manifest-'))
|
||
temporaryDirectories.push(directory)
|
||
return directory
|
||
}
|
||
|
||
// Why strip GITHUB_*: on a CI pull_request the add-only check looks for a base the sandbox lacks.
|
||
function localEnv() {
|
||
return Object.fromEntries(
|
||
Object.entries(process.env).filter(([name]) => !name.startsWith('GITHUB_'))
|
||
)
|
||
}
|
||
|
||
// Copy generator into throwaway tree so its location-based root resolves there, protecting resources/skills.
|
||
async function createGeneratorSandbox() {
|
||
// Resolve sandbox path: Node resolves script symlinks before its root check.
|
||
const root = await realpath(await createPackage())
|
||
const skillRoot = path.join(root, 'skills', 'demo')
|
||
const script = path.join(root, 'config', 'scripts', 'generate-skill-bundle-manifest.mjs')
|
||
await mkdir(path.dirname(script), { recursive: true })
|
||
await mkdir(skillRoot, { recursive: true })
|
||
for (const name of ['generate-skill-bundle-manifest.mjs', 'git-pull-request-diff-base.mjs']) {
|
||
await copyFile(path.join(import.meta.dirname, name), path.join(path.dirname(script), name))
|
||
}
|
||
await writeFile(path.join(skillRoot, 'SKILL.md'), 'demo skill\n')
|
||
const run = (env, args) =>
|
||
execFileSync(process.execPath, [script, ...args], { stdio: 'pipe', env })
|
||
return {
|
||
root,
|
||
generate: (...args) => run(localEnv(), args),
|
||
generateOnPullRequest: (...args) =>
|
||
run({ ...localEnv(), GITHUB_EVENT_NAME: 'pull_request' }, args),
|
||
git: (...args) =>
|
||
execFileSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', ...args], {
|
||
cwd: root,
|
||
stdio: 'pipe',
|
||
env: localEnv()
|
||
}),
|
||
read: (name) => readFile(path.join(root, 'resources', 'skills', name), 'utf8'),
|
||
readSources: () =>
|
||
readFile(
|
||
path.join(
|
||
root,
|
||
'src',
|
||
'main',
|
||
'skills',
|
||
'bundled-skill-install',
|
||
'bundled-skill-sources.ts'
|
||
),
|
||
'utf8'
|
||
),
|
||
editSkill: (body) => writeFile(path.join(skillRoot, 'SKILL.md'), body)
|
||
}
|
||
}
|
||
|
||
afterEach(async () => {
|
||
await Promise.all(
|
||
temporaryDirectories.splice(0).map((directory) => rm(directory, { recursive: true }))
|
||
)
|
||
})
|
||
|
||
describe('skill bundle manifest generator', () => {
|
||
it('folds platform line endings for text identity', () => {
|
||
const lf = Buffer.from('first\nsecond\n')
|
||
const crlf = Buffer.from('first\r\nsecond\r\n')
|
||
|
||
expect(classifyFile(lf)).toBe('text')
|
||
expect(normalizeText(crlf)).toEqual(lf)
|
||
})
|
||
|
||
it('classifies null-containing and invalid UTF-8 content as binary', () => {
|
||
expect(classifyFile(Buffer.from([0, 1, 2]))).toBe('binary')
|
||
expect(classifyFile(Buffer.from([0xc3, 0x28]))).toBe('binary')
|
||
})
|
||
|
||
it('uses normalized text identity but exact executable identity', () => {
|
||
const skillFile = describeFile('SKILL.md', Buffer.from('line one\r\nline two\r\n'), false)
|
||
const executable = describeFile('run.sh', Buffer.from('#!/bin/sh\r\necho ok\r\n'), true)
|
||
|
||
expect(skillFile.identitySha256).toBe(skillFile.textNormalizedSha256)
|
||
expect(skillFile.identitySha256).not.toBe(skillFile.exactSha256)
|
||
expect(executable.exactSha256).not.toBe(executable.textNormalizedSha256)
|
||
expect(executable.identitySha256).toBe(executable.exactSha256)
|
||
expect(packageDigest([skillFile, executable])).toMatch(/^[a-f0-9]{64}$/)
|
||
})
|
||
|
||
it('orders files identically however they were listed', async () => {
|
||
const packageRoot = await createPackage()
|
||
await mkdir(path.join(packageRoot, 'sub'))
|
||
for (const name of ['apple.md', 'sub.md', 'Zebra.md', path.join('sub', 'inner.txt')]) {
|
||
await writeFile(path.join(packageRoot, name), `${name}\n`)
|
||
}
|
||
const walked = await collectPackageFiles(packageRoot)
|
||
|
||
// Git tree ordering differs from depth-first filesystem ordering; hash producers must agree.
|
||
const gitOrdered = ['Zebra.md', 'apple.md', 'sub.md', 'sub/inner.txt'].map((manifestPath) =>
|
||
walked.find((file) => file.path === manifestPath)
|
||
)
|
||
|
||
expect(sortManifestFiles(gitOrdered)).toEqual(walked)
|
||
expect(packageDigest(sortManifestFiles(gitOrdered))).toBe(packageDigest(walked))
|
||
expect(walked.map((file) => file.path)).toEqual([
|
||
'Zebra.md',
|
||
'apple.md',
|
||
'sub/inner.txt',
|
||
'sub.md'
|
||
])
|
||
})
|
||
|
||
it('writes a schema-3 manifest and fails verification once a skill changes', async () => {
|
||
const sandbox = await createGeneratorSandbox()
|
||
sandbox.generate('--write')
|
||
const manifest = JSON.parse(await sandbox.read('current-manifest.json'))
|
||
expect(manifest.schemaVersion).toBe(3)
|
||
expect(manifest.skills.map((skill) => Object.keys(skill))).toEqual([
|
||
['name', 'sourcePath', 'packageDigest', 'files']
|
||
])
|
||
expect(() => sandbox.generate()).not.toThrow()
|
||
|
||
await sandbox.editSkill('changed\n')
|
||
expect(() => sandbox.generate()).toThrow()
|
||
})
|
||
|
||
it('keeps every shipped SKILL.md digest and embeds the current LF bytes', async () => {
|
||
const sandbox = await createGeneratorSandbox()
|
||
sandbox.generate('--write')
|
||
const [first] = JSON.parse(await sandbox.read('released-digests.json')).skills.demo
|
||
|
||
await sandbox.editSkill('changed\r\n')
|
||
sandbox.generate('--write')
|
||
const ledger = JSON.parse(await sandbox.read('released-digests.json'))
|
||
expect(ledger.skills.demo).toHaveLength(2)
|
||
expect(ledger.skills.demo).toContain(first)
|
||
expect(await sandbox.readSources()).toContain(
|
||
JSON.stringify({ name: 'demo', text: 'changed\n', releasedDigests: ledger.skills.demo })
|
||
)
|
||
expect(() => sandbox.generate()).not.toThrow()
|
||
})
|
||
|
||
it('collects every SKILL.md version in git history, even ones never generated', async () => {
|
||
const sandbox = await createGeneratorSandbox()
|
||
sandbox.git('init', '-q')
|
||
sandbox.git('add', 'skills')
|
||
sandbox.git('commit', '-qm', 'one')
|
||
await sandbox.editSkill('second version\n')
|
||
sandbox.git('commit', '-qam', 'two')
|
||
await sandbox.editSkill('third version\n')
|
||
sandbox.generate('--write')
|
||
expect(JSON.parse(await sandbox.read('released-digests.json')).skills.demo).toHaveLength(3)
|
||
expect(() => sandbox.generate()).not.toThrow()
|
||
})
|
||
|
||
it('fails when a digest is missing or the pull request base cannot be read', async () => {
|
||
const sandbox = await createGeneratorSandbox()
|
||
sandbox.generate('--write')
|
||
expect(() => sandbox.generateOnPullRequest()).toThrow(/Cannot read the pull request base/)
|
||
await writeFile(
|
||
path.join(sandbox.root, 'resources', 'skills', 'released-digests.json'),
|
||
`${JSON.stringify({ schemaVersion: 1, skills: { demo: [] } }, null, 2)}\n`
|
||
)
|
||
expect(() => sandbox.generate()).toThrow(/stale/)
|
||
})
|
||
|
||
it('merges digests without duplicates and reports any the base had but HEAD lost', () => {
|
||
const merged = withDigests({ skills: { demo: ['b'] } }, [['demo', ['a', 'b', 'a']]])
|
||
expect(merged.skills.demo).toEqual(['a', 'b'])
|
||
expect(findDroppedReleasedDigests({ skills: { demo: ['a', 'b'] } }, merged)).toEqual([])
|
||
expect(
|
||
findDroppedReleasedDigests({ skills: { demo: ['a', 'c'], gone: ['d'] } }, merged)
|
||
).toEqual(['demo:c', 'gone:d'])
|
||
})
|
||
|
||
it.runIf(process.platform !== 'win32')(
|
||
'rejects executable files in shipped skill packages',
|
||
async () => {
|
||
const packageRoot = await createPackage()
|
||
await writeFile(path.join(packageRoot, 'SKILL.md'), 'skill\n')
|
||
await writeFile(path.join(packageRoot, 'run.sh'), '#!/bin/sh\necho ok\n')
|
||
await chmod(path.join(packageRoot, 'run.sh'), 0o755)
|
||
|
||
await expect(collectPackageFiles(packageRoot)).rejects.toThrow(
|
||
'Executable file is not allowed in a shipped skill: run.sh'
|
||
)
|
||
}
|
||
)
|
||
|
||
it.runIf(process.platform === 'linux')('rejects case-colliding paths', async () => {
|
||
const packageRoot = await createPackage()
|
||
await writeFile(path.join(packageRoot, 'SKILL.md'), 'skill')
|
||
await writeFile(path.join(packageRoot, 'Readme.md'), 'one')
|
||
await writeFile(path.join(packageRoot, 'README.md'), 'two')
|
||
|
||
await expect(collectPackageFiles(packageRoot)).rejects.toThrow('Case-colliding skill paths')
|
||
})
|
||
|
||
it.runIf(process.platform !== 'win32')('rejects symlinks inside shipped packages', async () => {
|
||
const packageRoot = await createPackage()
|
||
await writeFile(path.join(packageRoot, 'SKILL.md'), 'skill')
|
||
await symlink('SKILL.md', path.join(packageRoot, 'linked.md'))
|
||
|
||
await expect(collectPackageFiles(packageRoot)).rejects.toThrow(
|
||
'Symlink is not allowed in a shipped skill'
|
||
)
|
||
})
|
||
|
||
it('ignores OS-authored sidecars a working tree may carry', async () => {
|
||
const packageRoot = await createPackage()
|
||
await writeFile(path.join(packageRoot, 'SKILL.md'), 'demo skill\n')
|
||
await mkdir(path.join(packageRoot, 'references'))
|
||
await writeFile(path.join(packageRoot, 'references', 'guide.md'), 'nested\n')
|
||
const pristine = await collectPackageFiles(packageRoot)
|
||
expect(pristine.map((file) => file.path)).toEqual(['SKILL.md', 'references/guide.md'])
|
||
// Ignored Finder sidecars must not stale committed artifacts or create manifests no runtime observation matches.
|
||
await writeFile(path.join(packageRoot, '.DS_Store'), Buffer.from([0, 1, 2, 3]))
|
||
await writeFile(path.join(packageRoot, '._SKILL.md'), Buffer.from([0, 5]))
|
||
await writeFile(path.join(packageRoot, 'Thumbs.db'), Buffer.from([9]))
|
||
// Nested folders get browsed too, and a sidecar there shifts the same index-aligned list.
|
||
await writeFile(path.join(packageRoot, 'references', '.DS_Store'), Buffer.from([7]))
|
||
|
||
expect(await collectPackageFiles(packageRoot)).toEqual(pristine)
|
||
})
|
||
|
||
it('still records an unexpected file that is not OS metadata', async () => {
|
||
const packageRoot = await createPackage()
|
||
await writeFile(path.join(packageRoot, 'SKILL.md'), 'demo skill\n')
|
||
await writeFile(path.join(packageRoot, 'payload.sh'), 'echo hi\n')
|
||
|
||
expect((await collectPackageFiles(packageRoot)).map((file) => file.path)).toEqual([
|
||
'SKILL.md',
|
||
'payload.sh'
|
||
])
|
||
})
|
||
|
||
it('keeps guarding a directory or link that only wears an OS metadata name', async () => {
|
||
const packageRoot = await createPackage()
|
||
await writeFile(path.join(packageRoot, 'SKILL.md'), 'demo skill\n')
|
||
// Sidecar-named directories are real content; dropping them ships unrecorded subtrees.
|
||
await mkdir(path.join(packageRoot, '.DS_Store'))
|
||
await writeFile(path.join(packageRoot, '.DS_Store', 'payload.sh'), 'echo hi\n')
|
||
|
||
expect((await collectPackageFiles(packageRoot)).map((file) => file.path)).toEqual([
|
||
'.DS_Store/payload.sh',
|
||
'SKILL.md'
|
||
])
|
||
|
||
if (process.platform !== 'win32') {
|
||
await rm(path.join(packageRoot, '.DS_Store'), { recursive: true })
|
||
await symlink('SKILL.md', path.join(packageRoot, '._SKILL.md'))
|
||
await expect(collectPackageFiles(packageRoot)).rejects.toThrow(
|
||
'Symlink is not allowed in a shipped skill'
|
||
)
|
||
}
|
||
})
|
||
|
||
// Compare both walkers: asymmetric sidecar rules strand installs as unrecognized; include order and case-fold behavior too.
|
||
it('skips exactly the names the scanner skips', async () => {
|
||
const packageRoot = await createPackage()
|
||
for (const name of [
|
||
'SKILL.md',
|
||
'.DS_Store',
|
||
'.ds_store',
|
||
'.DS_STORE',
|
||
'Thumbs.db',
|
||
'THUMBS.DB',
|
||
'ehthumbs.db',
|
||
'desktop.ini',
|
||
'Desktop.INI',
|
||
'._SKILL.md',
|
||
'._',
|
||
// Near misses that both sides must keep.
|
||
'.dsstore',
|
||
'ds_store.md',
|
||
'_SKILL.md',
|
||
'.DS_Store.md'
|
||
]) {
|
||
await writeFile(path.join(packageRoot, name), `${name}\n`)
|
||
}
|
||
|
||
const generated = (await collectPackageFiles(packageRoot)).map((file) => file.path)
|
||
expect(generated).toEqual((await observeSkillPackage(packageRoot)).files.map((f) => f.path))
|
||
expect(generated).toEqual(['.DS_Store.md', '.dsstore', 'SKILL.md', '_SKILL.md', 'ds_store.md'])
|
||
})
|
||
|
||
// Release steps share index; a prior step can stage regenerated manifest into bump commit. Pin cut’s committed-artifact guard and spellings.
|
||
it('keeps the whole release-cut job off skill regeneration', async () => {
|
||
const workflow = parse(
|
||
await readFile(path.join(REPO_ROOT, '.github/workflows/release-cut.yml'), 'utf8')
|
||
)
|
||
const runSteps = workflow.jobs.cut.steps
|
||
.filter((step) => typeof step.run === 'string')
|
||
.map((step) => ({ name: step.name ?? '(unnamed)', run: step.run.replace(/^\s*#.*$/gm, '') }))
|
||
const bumpStep = runSteps.find((step) => step.name === 'Bump package.json and tag')
|
||
|
||
// Check commit, not index (commit flags can bypass it); -F keeps dot literal, -m exposes merge changes.
|
||
expect(bumpStep.run).toMatch(
|
||
/git diff-tree --no-commit-id --name-only -r -m --first-parent HEAD\s*\|\s*grep -vxF -e 'package\.json'/
|
||
)
|
||
expect(bumpStep.run.indexOf('grep -vxF')).toBeLessThan(bumpStep.run.indexOf('git tag'))
|
||
// ...and that it aborts. A guard degraded to a warning still reads as covered.
|
||
expect(bumpStep.run).toMatch(
|
||
/if \[\[ -n "\$committed" \]\]; then(?:(?!\bfi\b)[\s\S])*exit 1[\s\S]*?fi/
|
||
)
|
||
for (const step of runSteps) {
|
||
expect(step.run, step.name).not.toMatch(
|
||
/resources[/\\]skills|--write|generate:skill-bundle-manifest/
|
||
)
|
||
}
|
||
})
|
||
})
|