mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: remove low-value inventories, passthroughs and redundant cases (#33796)
* test: remove low-value inventories, passthroughs and redundant cases * style: format test suites after pruning redundant cases * test: remove obsolete workflow and flaky-test references * test: prune deleted builder test from Windows shim allowlist --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -95,7 +95,6 @@ jobs:
|
||||
config/scripts/macos-computer-helper-owner-loss-processes.test.mjs
|
||||
config/scripts/computer-use-smoke.test.mjs
|
||||
src/main/computer/computer-provider-lifecycle.test.ts
|
||||
src/main/computer/computer-provider-unavailable-message.test.ts
|
||||
src/main/computer/sidecar-client.test.ts
|
||||
src/main/computer/macos-native-provider-client.test.ts
|
||||
src/main/computer/macos-native-provider-socket.test.ts
|
||||
|
||||
@@ -1333,7 +1333,6 @@
|
||||
6 test-timeout-needs-reason:src/main/windows/windows-pty-job.win32.test.ts
|
||||
8 test-timeout-needs-reason:src/main/worker-thread-child-process/worker-thread-child-process.test.ts
|
||||
6 test-timeout-needs-reason:src/main/wsl/wsl-runner.wsl.test.ts
|
||||
3 test-timeout-needs-reason:src/main/zcode/interactive-capability.live.test.ts
|
||||
4 test-timeout-needs-reason:src/packages/process-host/src/run-process.test.ts
|
||||
5 test-timeout-needs-reason:src/renderer/src/components/sidebar/WorktreeCard.compact-hover.test.tsx
|
||||
2 test-timeout-needs-reason:src/renderer/src/components/sidebar/WorktreeCard.host-badge-toggle.test.tsx
|
||||
|
||||
@@ -1,92 +0,0 @@
|
||||
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { parse } from 'yaml'
|
||||
import { expect, it } from 'vitest'
|
||||
|
||||
// Runner truncates/rewrites GITHUB_EVENT_PATH at each step start; background JS payload imports/composite inner steps race foreground starts and parse empty JSON.
|
||||
const workflowFiles = readdirSync('.github/workflows')
|
||||
.filter((name) => /\.ya?ml$/.test(name))
|
||||
.map((name) => join('.github/workflows', name))
|
||||
const actionFiles = readdirSync('.github/actions')
|
||||
.map((name) => join('.github/actions', name, 'action.yml'))
|
||||
.filter((file) => existsSync(file))
|
||||
|
||||
// Single-step JavaScript actions whose bundle never reads the payload (checked by searching dist/index.js).
|
||||
const payloadFreeActions = ['ruby/setup-ruby@v1']
|
||||
// The runner runs 10 at once; a queued step starts (and rewrites the payload) mid-foreground-step.
|
||||
const backgroundSlots = 10
|
||||
|
||||
function stepLists(file) {
|
||||
const document = parse(readFileSync(file, 'utf8'))
|
||||
return document.jobs
|
||||
? Object.entries(document.jobs).map(([job, value]) => [job, value.steps ?? []])
|
||||
: [['composite', document.runs?.steps ?? []]]
|
||||
}
|
||||
|
||||
function backgroundSteps() {
|
||||
const found = []
|
||||
for (const file of [...workflowFiles, ...actionFiles]) {
|
||||
for (const [job, steps] of stepLists(file)) {
|
||||
for (const step of steps) {
|
||||
if (step.background) {
|
||||
found.push({ where: `${file} ${job} ${step.id ?? step.name}`, step })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
function readsEventPayload(run) {
|
||||
const scripts = [...run.matchAll(/(?:config\/scripts|\.github\/actions)\/[\w./-]+/g)].map(
|
||||
(match) => match[0]
|
||||
)
|
||||
return (
|
||||
run.includes('GITHUB_EVENT_PATH') ||
|
||||
scripts.some(
|
||||
(file) =>
|
||||
existsSync(file) &&
|
||||
statSync(file).isFile() &&
|
||||
readFileSync(file, 'utf8').includes('GITHUB_EVENT_PATH')
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
it('finds the background steps it guards', () => {
|
||||
expect(backgroundSteps().length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('backgrounds only single run steps, never actions that start later steps or parse the payload', () => {
|
||||
const actions = backgroundSteps()
|
||||
.filter(({ step }) => typeof step.run !== 'string' && !payloadFreeActions.includes(step.uses))
|
||||
.map(({ where }) => where)
|
||||
expect(actions).toEqual([])
|
||||
})
|
||||
|
||||
it('keeps the event payload out of background run steps', () => {
|
||||
const readers = backgroundSteps()
|
||||
.filter(({ step }) => typeof step.run === 'string' && readsEventPayload(step.run))
|
||||
.map(({ where }) => where)
|
||||
expect(readers).toEqual([])
|
||||
})
|
||||
|
||||
it('never queues a background step behind a full slot pool', () => {
|
||||
const crowded = []
|
||||
for (const file of workflowFiles) {
|
||||
for (const [job, steps] of stepLists(file)) {
|
||||
const open = new Set()
|
||||
for (const [index, step] of steps.entries()) {
|
||||
if (step.background) {
|
||||
open.add(step.id ?? index)
|
||||
}
|
||||
for (const id of [step.wait ?? []].flat()) {
|
||||
open.delete(id)
|
||||
}
|
||||
if (open.size > backgroundSlots) {
|
||||
crowded.push(`${file} ${job}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
expect(crowded).toEqual([])
|
||||
})
|
||||
@@ -6,64 +6,13 @@ import {
|
||||
classifyE2eJobs,
|
||||
DEDICATED_E2E_SPECS,
|
||||
generalE2eShards,
|
||||
MANAGED_SSH_E2E_SPEC_PREFIX,
|
||||
NATIVE_IME_E2E_SPEC,
|
||||
NODE_NETWORK_E2E_SPEC,
|
||||
selectGeneralE2eSpecs
|
||||
} from './ci-e2e-job-selection.mjs'
|
||||
import { selectPrE2eSpecs } from './pr-e2e-source-routing.mjs'
|
||||
|
||||
it.each([
|
||||
'src/main/runtime/runtime-browser-commands-factory.ts',
|
||||
'src/main/runtime/orca-runtime-get-status.ts',
|
||||
'src/main/orcad/orcad-browser-startup.ts',
|
||||
'src/main/runtime/browser/browser-client-page-opening.ts'
|
||||
])('routes the managed browser capability oracle from %s', (path) => {
|
||||
expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-browser-capabilities.spec.ts')
|
||||
})
|
||||
|
||||
it.each(['errors', 'status', 'lifecycle', 'restart-attempt'])(
|
||||
'routes the real browser service status check from %s changes',
|
||||
(name) => {
|
||||
expect(
|
||||
selectPrE2eSpecs([
|
||||
`src/renderer/src/components/browser-pane/stream-remote/remote-browser-stream-${name}.ts`
|
||||
])
|
||||
).toContain('tests/e2e/ssh-orcad-browser-service-status.spec.ts')
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
'src/renderer/src/lib/ssh-workspace-browser-route-eligibility.ts',
|
||||
'src/renderer/src/components/browser-pane/use-ssh-workspace-browser-route.ts',
|
||||
'src/main/browser/local-ssh-browser-route.ts',
|
||||
'src/renderer/src/store/repos/converted-ssh-browser-pages.ts',
|
||||
'src/renderer/src/store/slices/browser/browser-tab-actions.ts',
|
||||
'src/renderer/src/hooks/ipc-events/ssh-managed-server-state-effects.ts'
|
||||
])('routes the managed browser routing oracle from %s', (path) => {
|
||||
expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-browser-routing.spec.ts')
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src/renderer/src/lib/workspace-browser-tab-open.ts',
|
||||
'src/renderer/src/lib/http-link-destinations.ts',
|
||||
'src/renderer/src/components/terminal-pane/terminal-http-link-source-owner.ts'
|
||||
])('routes the managed terminal link oracle from %s', (path) => {
|
||||
expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-terminal-browser-link.spec.ts')
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src/shared/desktop-browser-bridge-protocol.ts',
|
||||
'src/main/runtime/desktop-browser-bridge/desktop-browser-bridge-route.ts',
|
||||
'src/main/browser/desktop-browser-bridge/desktop-browser-bridge-executor.ts',
|
||||
'src/main/ipc/desktop-browser-bridge-wiring.ts',
|
||||
'src/main/runtime/rpc/methods/desktop-browser-bridge.ts'
|
||||
])('routes the managed desktop browser bridge oracle from %s', (path) => {
|
||||
expect(selectPrE2eSpecs([path])).toContain('tests/e2e/ssh-orcad-desktop-browser-bridge.spec.ts')
|
||||
})
|
||||
|
||||
const workflow = parse(readFileSync('.github/workflows/e2e.yml', 'utf8'))
|
||||
const prWorkflow = parse(readFileSync('.github/workflows/pr.yml', 'utf8'))
|
||||
const classify = (specs) => classifyE2eJobs(JSON.stringify(specs))
|
||||
|
||||
it('scales broad general selections without charging extra VMs for duplicate or dedicated specs', () => {
|
||||
@@ -85,34 +34,6 @@ it('scales broad general selections without charging extra VMs for duplicate or
|
||||
}
|
||||
})
|
||||
|
||||
function expectManagedSshLane(spec) {
|
||||
expect(classify([spec])).toEqual({ e2e_run_changed: false, e2e_needs_build: true })
|
||||
expect(spec.startsWith(MANAGED_SSH_E2E_SPEC_PREFIX)).toBe(true)
|
||||
const lane = workflow.jobs['orcad-ssh-docker']
|
||||
expect(lane.if).toContain(`contains(inputs.test_files, '${MANAGED_SSH_E2E_SPEC_PREFIX}')`)
|
||||
expect(
|
||||
lane.steps.some(
|
||||
(step) =>
|
||||
step.run?.includes('selection.selectManagedSshE2eSpecs') &&
|
||||
step.env?.ORCA_E2E_SSH_DOCKER === '1'
|
||||
)
|
||||
).toBe(true)
|
||||
}
|
||||
|
||||
it('runs real browser file-drop ownership in the template-building Docker job', () => {
|
||||
const spec = 'tests/e2e/ssh-orcad-browser-drop-owner.spec.ts'
|
||||
expectManagedSshLane(spec)
|
||||
for (const source of [
|
||||
'src/renderer/src/lib/file-preview.ts',
|
||||
'src/renderer/src/lib/workspace-file-drag.ts',
|
||||
'src/renderer/src/components/browser-pane/navigate/use-browser-page-navigation-downloads.ts',
|
||||
'src/renderer/src/components/right-sidebar/FileExplorerRow.tsx',
|
||||
'tests/e2e/helpers/browser-split-guest-probes.ts'
|
||||
]) {
|
||||
expect(selectPrE2eSpecs([source]), source).toContain(spec)
|
||||
}
|
||||
})
|
||||
|
||||
it('skips the general consumer only when every requested spec has a dedicated owner', () => {
|
||||
for (const spec of DEDICATED_E2E_SPECS) {
|
||||
expect(classify([spec]).e2e_run_changed, spec).toBe(false)
|
||||
@@ -164,28 +85,6 @@ it('preserves the requested specs across source-routed and mixed selections', ()
|
||||
}
|
||||
})
|
||||
|
||||
it('applies allocation hints only to PRs and retains other callers and full references', () => {
|
||||
for (const name of ['run_changed_e2e', 'needs_build']) {
|
||||
expect(workflow.on.workflow_call.inputs[name]).toMatchObject({
|
||||
type: 'boolean',
|
||||
required: false,
|
||||
default: true
|
||||
})
|
||||
}
|
||||
expect(workflow.jobs.build.if).toBe(
|
||||
"inputs.test_files == '' || github.event_name != 'pull_request' || inputs.needs_build"
|
||||
)
|
||||
expect(workflow.jobs.e2e.if).toBe("inputs.test_files == ''")
|
||||
const changed = workflow.jobs['changed-e2e']
|
||||
expect(changed.if).toContain("github.event_name != 'pull_request' || inputs.run_changed_e2e")
|
||||
const command = changed.steps.find((step) => step.name === 'Run changed E2E specs').run
|
||||
expect(command).toContain('node config/scripts/ci-e2e-job-selection.mjs >')
|
||||
expect(command).not.toContain('mapfile -t TEST_FILES < <(')
|
||||
const fallback = [...command.matchAll(/\. != "([^"]+)"/g)].map((match) => match[1])
|
||||
expect(fallback).toEqual(DEDICATED_E2E_SPECS)
|
||||
expect(command).toContain(`(startswith("${MANAGED_SSH_E2E_SPEC_PREFIX}") | not)`)
|
||||
})
|
||||
|
||||
it('publishes conservative hints for malformed evidence and refuses a malformed consumer list', async () => {
|
||||
const program = 'config/scripts/ci-e2e-job-selection.mjs'
|
||||
const consumer = await runProcess({
|
||||
@@ -206,112 +105,3 @@ it('publishes conservative hints for malformed evidence and refuses a malformed
|
||||
'e2e_run_changed=true\ne2e_needs_build=true\ne2e_ssh_shards=["1/2","2/2"]\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('classifies PR consumers in the existing detector and passes conservative allocation hints', () => {
|
||||
const detector = prWorkflow.jobs.code_paths
|
||||
expect(detector.steps[0].with['sparse-checkout']).toContain(
|
||||
'/config/scripts/ci-e2e-job-selection.mjs'
|
||||
)
|
||||
for (const name of ['e2e_run_changed', 'e2e_needs_build']) {
|
||||
expect(detector.outputs[name]).toBe(`\${{ steps.e2e_filter.outputs.${name} }}`)
|
||||
}
|
||||
const command = detector.steps.find((step) => step.id === 'e2e_filter').run
|
||||
expect(command).toContain('ci-e2e-job-selection.mjs --job-outputs >> "$GITHUB_OUTPUT"')
|
||||
expect(prWorkflow.jobs.e2e.with.run_changed_e2e).toBe(
|
||||
"${{ needs.code_paths.outputs.e2e_run_changed != 'false' }}"
|
||||
)
|
||||
expect(prWorkflow.jobs.e2e.with.needs_build).toBe(
|
||||
"${{ needs.code_paths.outputs.e2e_needs_build != 'false' }}"
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src/renderer/src/runtime/web-session-tabs-sync/mirrored-editor-file-identity.ts',
|
||||
'src/renderer/src/runtime/web-session-tabs-sync/tab-builders.ts',
|
||||
'src/renderer/src/runtime/web-session-tabs-sync/apply-preparation-browser.ts',
|
||||
'src/renderer/src/runtime/web-session-existing-tab-index.ts'
|
||||
])('routes %s to the template-building editor ownership lane', (file) => {
|
||||
const spec = 'tests/e2e/ssh-orcad-editor-ownership.spec.ts'
|
||||
expect(selectPrE2eSpecs([file])).toContain(spec)
|
||||
expectManagedSshLane(spec)
|
||||
})
|
||||
|
||||
const MARKDOWN_CONVERSION_SPEC = 'tests/e2e/ssh-orcad-markdown-conversion.spec.ts'
|
||||
const MARKDOWN_LINK_REFRESH_SPEC = 'tests/e2e/ssh-orcad-markdown-link-refresh.spec.ts'
|
||||
const MARKDOWN_LIVE_DOCUMENTS_SPEC = 'tests/e2e/ssh-orcad-markdown-live-documents.spec.ts'
|
||||
|
||||
it.each([
|
||||
['src/renderer/src/components/editor/useMarkdownDocuments.ts', MARKDOWN_CONVERSION_SPEC],
|
||||
[
|
||||
'src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.ts',
|
||||
MARKDOWN_CONVERSION_SPEC
|
||||
],
|
||||
[
|
||||
'src/renderer/src/components/editor/migrate-restored-editor-file-owner.ts',
|
||||
MARKDOWN_CONVERSION_SPEC
|
||||
],
|
||||
['src/renderer/src/components/editor/rich-markdown-doc-link.ts', MARKDOWN_LINK_REFRESH_SPEC],
|
||||
[
|
||||
'src/renderer/src/components/editor/useRichMarkdownProgrammaticSync.ts',
|
||||
MARKDOWN_LINK_REFRESH_SPEC
|
||||
],
|
||||
['src/renderer/src/components/editor/useMarkdownDocuments.ts', MARKDOWN_LIVE_DOCUMENTS_SPEC],
|
||||
[
|
||||
'src/renderer/src/components/editor/use-markdown-document-watch-refresh.ts',
|
||||
MARKDOWN_LIVE_DOCUMENTS_SPEC
|
||||
],
|
||||
[
|
||||
'src/renderer/src/components/editor/markdown-document-list-request.ts',
|
||||
MARKDOWN_LIVE_DOCUMENTS_SPEC
|
||||
]
|
||||
])('routes %s to the template-building Markdown lane %s', (file, spec) => {
|
||||
expect(selectPrE2eSpecs([file])).toContain(spec)
|
||||
expectManagedSshLane(spec)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src/renderer/src/hooks/useEditorExternalWatch.ts',
|
||||
'src/renderer/src/hooks/editor-runtime-file-watch.ts'
|
||||
])('routes %s to the template-building editor watch recovery lane', (file) => {
|
||||
const spec = 'tests/e2e/ssh-orcad-editor-watch-recovery.spec.ts'
|
||||
expect(selectPrE2eSpecs([file])).toContain(spec)
|
||||
expectManagedSshLane(spec)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts',
|
||||
'src/renderer/src/hooks/worktree-file-change-event.ts'
|
||||
])('routes %s to the template-building explorer watch recovery lane', (file) => {
|
||||
const spec = 'tests/e2e/ssh-orcad-explorer-watch-recovery.spec.ts'
|
||||
expect(selectPrE2eSpecs([file])).toContain(spec)
|
||||
expectManagedSshLane(spec)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts',
|
||||
'src/renderer/src/components/right-sidebar/file-explorer-operation-owner.ts',
|
||||
'src/renderer/src/components/right-sidebar/FileExplorer.tsx',
|
||||
'src/renderer/src/hooks/editor-external-watch-targets.ts',
|
||||
'src/renderer/src/components/right-sidebar/useFileExplorerTree.ts',
|
||||
'src/renderer/src/components/right-sidebar/use-file-explorer-tree-load-effects.ts'
|
||||
])('routes %s to the template-building selected-host explorer lane', (file) => {
|
||||
const spec = 'tests/e2e/ssh-orcad-explorer-selected-host.spec.ts'
|
||||
expect(selectPrE2eSpecs([file])).toContain(spec)
|
||||
expectManagedSshLane(spec)
|
||||
})
|
||||
|
||||
it('runs managed terminal root ownership in the Docker conversion lane', () => {
|
||||
const spec = 'tests/e2e/ssh-orcad-terminal-root-owner.spec.ts'
|
||||
expectManagedSshLane(spec)
|
||||
for (const source of [
|
||||
'src/renderer/src/components/terminal-pane/terminal-worktree-path-link.ts',
|
||||
'src/renderer/src/components/terminal-pane/terminal-file-open-routing.ts',
|
||||
'src/renderer/src/components/terminal-pane/terminal-file-link-actions.ts',
|
||||
'src/renderer/src/components/terminal-pane/terminal-file-link-hit-testing.ts',
|
||||
'src/renderer/src/components/terminal-pane/terminal-link-handlers.ts',
|
||||
'src/renderer/src/lib/worktree-owner-route.ts',
|
||||
'tests/e2e/helpers/terminal-workspace-root-link.ts'
|
||||
]) {
|
||||
expect(selectPrE2eSpecs([source]), source).toContain(spec)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -11,16 +11,8 @@ const REPO_ROOT = join(import.meta.dirname, '..', '..')
|
||||
const require = createRequire(import.meta.url)
|
||||
const electronBuilderConfig = require('../electron-builder.config.cjs')
|
||||
const { copyFiles, FileMatcher } = require('app-builder-lib/out/fileMatcher')
|
||||
const FpmTarget = require('app-builder-lib/out/targets/FpmTarget').default
|
||||
const electronBuilderNativeRebuild = require('./electron-builder-native-rebuild.cjs')
|
||||
|
||||
describe('electron-builder config', () => {
|
||||
it('keeps the packaged app identity aligned with local-build validation', () => {
|
||||
expect(electronBuilderConfig.appId).toBe(
|
||||
require('../../src/shared/local-build-compatibility-contract.json').appId
|
||||
)
|
||||
})
|
||||
|
||||
it('excludes repo-only source trees from app.asar', () => {
|
||||
const matcher = new FileMatcher('/app', '/dest', (value) => value, electronBuilderConfig.files)
|
||||
matcher.prependPattern('**/*')
|
||||
@@ -215,64 +207,6 @@ describe('electron-builder config', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps runtime resources available through extraResources', () => {
|
||||
const bundledPluginResources = expect.objectContaining({
|
||||
from: 'resources/plugins/launch',
|
||||
to: 'plugins/launch'
|
||||
})
|
||||
for (const platform of ['mac', 'linux', 'win']) {
|
||||
expect(electronBuilderConfig[platform].extraResources).toContainEqual({
|
||||
from: 'resources/skills',
|
||||
to: 'skills'
|
||||
})
|
||||
expect(electronBuilderConfig[platform].extraResources).toEqual(
|
||||
expect.arrayContaining([bundledPluginResources])
|
||||
)
|
||||
}
|
||||
expect(electronBuilderConfig.mac.extraResources).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
from: 'native/computer-use-macos/.build/release/Orca Computer Use.app',
|
||||
to: 'Orca Computer Use.app'
|
||||
})
|
||||
])
|
||||
)
|
||||
expect(electronBuilderConfig.linux.extraResources).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
from: 'native/computer-use-linux/runtime.py',
|
||||
to: 'computer-use-linux/runtime.py'
|
||||
})
|
||||
])
|
||||
)
|
||||
expect(electronBuilderConfig.win.extraResources).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
from: 'native/computer-use-windows/runtime.ps1',
|
||||
to: 'computer-use-windows/runtime.ps1'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
from: 'native/windows-cli-launcher/.build/orca.exe',
|
||||
to: 'bin/orca.exe'
|
||||
}),
|
||||
expect.objectContaining({
|
||||
from: 'native/windows-cli-launcher/.build/orca-administrator-restart.exe',
|
||||
to: 'bin/orca-administrator-restart.exe'
|
||||
})
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it('ships one macOS serve-sim package through the runtime closure', () => {
|
||||
const serveSimResources = electronBuilderConfig.mac.extraResources.filter((resource) =>
|
||||
[join('node_modules', 'serve-sim'), 'serve-sim'].includes(resource.to)
|
||||
)
|
||||
|
||||
expect(serveSimResources).toEqual([
|
||||
expect.objectContaining({ to: join('node_modules', 'serve-sim') })
|
||||
])
|
||||
})
|
||||
|
||||
it.each(['mac', 'linux'])(
|
||||
'loads Codex TOML parsing after %s runtime pruning',
|
||||
async (platform) => {
|
||||
@@ -305,84 +239,12 @@ describe('electron-builder config', () => {
|
||||
)
|
||||
|
||||
// Why: serve-sim's addon is a Mach-O, and Windows signing rejects every *.node that is not PE.
|
||||
it('keeps serve-sim out of the Windows and Linux runtime closures', () => {
|
||||
const {
|
||||
PACKAGED_RUNTIME_PACKAGE_ROOTS,
|
||||
createPackagedRuntimeNodeModuleResources
|
||||
} = require('../packaged-runtime-node-modules.cjs')
|
||||
expect(PACKAGED_RUNTIME_PACKAGE_ROOTS).not.toContain('serve-sim')
|
||||
const serveSimTarget = join('node_modules', 'serve-sim')
|
||||
expect(createPackagedRuntimeNodeModuleResources('linux').map((r) => r.to)).not.toContain(
|
||||
serveSimTarget
|
||||
)
|
||||
expect(electronBuilderConfig.linux.extraResources.map((r) => r.to)).not.toContain(
|
||||
serveSimTarget
|
||||
)
|
||||
expect(electronBuilderConfig.win.extraResources.map((r) => r.to)).not.toContain(serveSimTarget)
|
||||
})
|
||||
|
||||
// Source shim unpacked under resources/win32/bin lacks adjacent orca.exe (#7351); only extraResources/bin copy works.
|
||||
it('keeps the Windows CLI shim source tree out of app.asar', () => {
|
||||
expect(electronBuilderConfig.files).toEqual(
|
||||
expect.arrayContaining(['!resources/win32{,/**/*}'])
|
||||
)
|
||||
// Regression guard: the working shim must still ship via extraResources.
|
||||
expect(electronBuilderConfig.win.extraResources).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
from: 'resources/win32/bin/orca.cmd',
|
||||
to: 'bin/orca.cmd'
|
||||
})
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
// macOS 26 UNUserNotificationCenter aborts helpers launched from Resources; ship under MacOS (#7929).
|
||||
it('ships the mac notification-status helper in Contents/MacOS, not Resources', () => {
|
||||
expect(electronBuilderConfig.mac.extraFiles).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
from: 'native/notification-status-macos/.build/release/orca-notification-status',
|
||||
to: 'MacOS/orca-notification-status'
|
||||
})
|
||||
])
|
||||
)
|
||||
expect(electronBuilderConfig.mac.extraResources).not.toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ to: 'orca-notification-status' })])
|
||||
)
|
||||
})
|
||||
|
||||
it('ships the mac keyboard-layout helper in Contents/MacOS, not Resources', () => {
|
||||
expect(electronBuilderConfig.mac.extraFiles).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
from: 'native/keyboard-layout-macos/.build/release/orca-keyboard-layout',
|
||||
to: 'MacOS/orca-keyboard-layout'
|
||||
})
|
||||
])
|
||||
)
|
||||
expect(electronBuilderConfig.mac.extraResources).not.toEqual(
|
||||
expect.arrayContaining([expect.objectContaining({ to: 'orca-keyboard-layout' })])
|
||||
)
|
||||
})
|
||||
|
||||
it('unpacks the compiled CommonJS boundary with CLI runtime files', () => {
|
||||
expect(electronBuilderConfig.asarUnpack).toEqual(
|
||||
expect.arrayContaining(['out/package.json', 'out/cli/**', 'out/shared/**'])
|
||||
)
|
||||
})
|
||||
|
||||
// Missing unpacked entry silently restores in-process watcher and #7547 main crashes.
|
||||
it('unpacks the forked parcel-watcher process entry', () => {
|
||||
expect(electronBuilderConfig.asarUnpack).toEqual(
|
||||
expect.arrayContaining(['out/main/parcel-watcher-process-entry.js'])
|
||||
)
|
||||
})
|
||||
|
||||
it('unpacks the replaceable WSL transcript filesystem process entry', async () => {
|
||||
const entryFilename = 'wsl-transcript-fs-process-entry.js'
|
||||
expect(electronBuilderConfig.asarUnpack).toContain(`out/main/${entryFilename}`)
|
||||
})
|
||||
|
||||
// CLI Node mode cannot read asar; packed entry sends orca serve to Electron (#27307).
|
||||
it('unpacks the orcad serve-selection entry the CLI runs', async () => {
|
||||
@@ -425,41 +287,6 @@ describe('electron-builder config', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the worker-thread hang watchdog inside app.asar', () => {
|
||||
expect(electronBuilderConfig.asarUnpack).not.toContain(
|
||||
'out/main/main-thread-hang-watchdog-entry.js'
|
||||
)
|
||||
})
|
||||
|
||||
it('uses the multi-size icon source for Linux packages', () => {
|
||||
expect(electronBuilderConfig.linux.icon).toBe('resources/build/icon.icns')
|
||||
})
|
||||
|
||||
it('matches the Linux desktop entry to Electron window class', () => {
|
||||
expect(electronBuilderConfig.linux.desktop.entry.StartupWMClass).toBe('orca')
|
||||
})
|
||||
|
||||
it('uses the release artifact set as local Linux targets without changing existing names', () => {
|
||||
expect(electronBuilderConfig.linux.target).toEqual(['AppImage', 'deb', 'rpm'])
|
||||
expect(electronBuilderConfig.toolsets).toEqual({ appimage: '1.0.3' })
|
||||
expect(electronBuilderConfig.appImage.artifactName).toBe('orca-linux.${ext}')
|
||||
expect(electronBuilderConfig.deb.artifactName).toBe('orca-ide_${version}_${arch}.${ext}')
|
||||
expect(electronBuilderConfig.rpm).toMatchObject({
|
||||
packageName: 'orca-ide',
|
||||
artifactName: 'orca-ide-${version}.${arch}.${ext}'
|
||||
})
|
||||
})
|
||||
|
||||
it('retains electron-builder runtime dependencies in deb and rpm packages', () => {
|
||||
for (const target of ['deb', 'rpm']) {
|
||||
const dependencies = electronBuilderConfig[target].depends
|
||||
expect(dependencies).toEqual(
|
||||
expect.arrayContaining(FpmTarget.prototype.getDefaultDepends(target))
|
||||
)
|
||||
expect(new Set(dependencies).size).toBe(dependencies.length)
|
||||
}
|
||||
})
|
||||
|
||||
it('validates each AppImage before electron-builder publishes it', async () => {
|
||||
const root = await mkdtemp(join(tmpdir(), 'orca-electron-builder-appimage-'))
|
||||
try {
|
||||
@@ -548,11 +375,6 @@ describe('electron-builder config', () => {
|
||||
require('../electron-builder.config.cjs')
|
||||
}
|
||||
})
|
||||
|
||||
it('uses Orca native rebuild hook instead of electron-builder default rebuild', () => {
|
||||
expect(electronBuilderConfig.beforeBuild).toBe(electronBuilderNativeRebuild)
|
||||
expect(electronBuilderConfig.npmRebuild).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('arch-aware packaging guard', () => {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { existsSync, readFileSync, readdirSync } from 'node:fs'
|
||||
import { cp, mkdir, mkdtemp, readFile, readdir, stat, writeFile } from 'node:fs/promises'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { delimiter, dirname, join, relative, resolve } from 'node:path'
|
||||
import { delimiter, dirname, join, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { removeTree } from '../../src/shared/windows-transient-lock-removal.ts'
|
||||
|
||||
@@ -10,10 +9,6 @@ const require = createRequire(import.meta.url)
|
||||
const projectRoot = resolve(import.meta.dirname, '..', '..')
|
||||
const electronBuilderConfig = require('../electron-builder.config.cjs')
|
||||
const {
|
||||
createPackagedRuntimeNodeModuleResources,
|
||||
findAsarEntry,
|
||||
isPackagedExternalSpecifier,
|
||||
packageNameFromSpecifier,
|
||||
prunePackagedNodePty,
|
||||
prunePackagedParcelWatcher,
|
||||
prunePackagedSherpaOnnx,
|
||||
@@ -22,11 +17,6 @@ const {
|
||||
verifyPackagedMainRuntimeDeps
|
||||
} = require('../packaged-runtime-node-modules.cjs')
|
||||
|
||||
// Only os:win32 process-tree presence selects Windows plan; universal registry workspace link proves no binary exists.
|
||||
const windowsAddonsInstalled = existsSync(
|
||||
join(projectRoot, 'node_modules', '@vscode', 'windows-process-tree', 'package.json')
|
||||
)
|
||||
|
||||
describe('packaged runtime resources', () => {
|
||||
it('verifies packaged main runtime deps from Windows-style asar entries', async () => {
|
||||
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-runtime-deps-'))
|
||||
@@ -176,13 +166,6 @@ describe('packaged runtime resources', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('normalizes host-specific asar entry separators', () => {
|
||||
expect(findAsarEntry(['\\out\\main\\index.js'], 'out/main/index.js')).toBe(
|
||||
'\\out\\main\\index.js'
|
||||
)
|
||||
expect(findAsarEntry(['/out/main/index.js'], 'out/main/index.js')).toBe('/out/main/index.js')
|
||||
})
|
||||
|
||||
it('prunes non-target node-pty architecture outputs from packaged runtime resources', async () => {
|
||||
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-node-pty-prune-'))
|
||||
try {
|
||||
@@ -251,30 +234,6 @@ describe('packaged runtime resources', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('includes external main dependencies in the packaged runtime closure', () => {
|
||||
// Without packaged @parcel/watcher, serve silently stops propagating file changes (#4851).
|
||||
const packaged = createPackagedRuntimeNodeModuleResources()
|
||||
const packagedTargets = packaged.map((resource) => resource.to)
|
||||
expect(packagedTargets).toContain(join('node_modules', '@parcel', 'watcher'))
|
||||
expect(
|
||||
packagedTargets.some((target) =>
|
||||
target.startsWith(join('node_modules', '@parcel', 'watcher-'))
|
||||
)
|
||||
).toBe(true)
|
||||
expect(packagedTargets).toContain(join('node_modules', 'proper-lockfile'))
|
||||
})
|
||||
|
||||
it('includes the Claude agent SDK in every desktop package plan', () => {
|
||||
for (const platform of windowsAddonsInstalled
|
||||
? ['darwin', 'linux', 'win32']
|
||||
: ['darwin', 'linux']) {
|
||||
const packagedTargets = createPackagedRuntimeNodeModuleResources(platform).map(
|
||||
(resource) => resource.to
|
||||
)
|
||||
expect(packagedTargets).toContain(join('node_modules', '@anthropic-ai', 'claude-agent-sdk'))
|
||||
}
|
||||
})
|
||||
|
||||
it('prunes non-target @parcel/watcher architecture subpackages', async () => {
|
||||
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-parcel-watcher-prune-'))
|
||||
try {
|
||||
@@ -573,85 +532,7 @@ async function seedBundledRipgrep(resourcesDir) {
|
||||
}
|
||||
}
|
||||
|
||||
// Bundler renames createRequire result, hiding these from bare-require scans; source-anchor or packaging passes before runtime MODULE_NOT_FOUND.
|
||||
function collectLazyRequireSpecifiers(directory, found = new Map()) {
|
||||
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
||||
const entryPath = join(directory, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
collectLazyRequireSpecifiers(entryPath, found)
|
||||
continue
|
||||
}
|
||||
if (!entry.isFile() || !entry.name.endsWith('.ts') || entry.name.includes('.test.')) {
|
||||
continue
|
||||
}
|
||||
const source = readFileSync(entryPath, 'utf8')
|
||||
if (!source.includes('createRequire(')) {
|
||||
continue
|
||||
}
|
||||
for (const match of source.matchAll(/\brequire[A-Za-z0-9_]*\(\s*'([^']+)'\s*\)/g)) {
|
||||
if (isPackagedExternalSpecifier(match[1])) {
|
||||
found.set(match[1], relative(projectRoot, entryPath).replaceAll('\\', '/'))
|
||||
}
|
||||
}
|
||||
}
|
||||
return found
|
||||
}
|
||||
|
||||
function packagedResourceDestinations(platform) {
|
||||
return new Set(
|
||||
(electronBuilderConfig[platform].extraResources ?? []).map((resource) =>
|
||||
String(resource.to).replaceAll('\\', '/')
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
describe('lazily required packages reach Resources/node_modules', () => {
|
||||
it('excludes runtime builtins while retaining ordinary lazy dependencies', async () => {
|
||||
const sourceDir = await mkdtemp(join(tmpdir(), 'orca-lazy-builtins-'))
|
||||
try {
|
||||
await writeFile(
|
||||
join(sourceDir, 'runtime.ts'),
|
||||
[
|
||||
'const requireFromMain = createRequire(import.meta.url)',
|
||||
"requireFromMain('node:fs')",
|
||||
"requireFromMain('zod')"
|
||||
].join('\n')
|
||||
)
|
||||
expect([...collectLazyRequireSpecifiers(sourceDir).keys()]).toEqual(['zod'])
|
||||
} finally {
|
||||
await removeTree(sourceDir)
|
||||
}
|
||||
})
|
||||
|
||||
it('copies every createRequire specifier main uses into the packaged resource plan', () => {
|
||||
const specifiers = collectLazyRequireSpecifiers(join(projectRoot, 'src', 'main'))
|
||||
expect(specifiers.size).toBeGreaterThan(0)
|
||||
|
||||
const destinations = {
|
||||
win: packagedResourceDestinations('win'),
|
||||
mac: packagedResourceDestinations('mac'),
|
||||
linux: packagedResourceDestinations('linux')
|
||||
}
|
||||
for (const [specifier, source] of specifiers) {
|
||||
const packageName = packageNameFromSpecifier(specifier)
|
||||
const covered = (platform) =>
|
||||
destinations[platform].has(`node_modules/${packageName}`) ||
|
||||
destinations[platform].has(`node_modules/${specifier}`)
|
||||
// The Windows CI lane checks the full closure with its native addons installed.
|
||||
if (windowsAddonsInstalled) {
|
||||
expect(
|
||||
covered('win'),
|
||||
`${source} lazily requires '${specifier}', but nothing copies it to Resources/node_modules`
|
||||
).toBe(true)
|
||||
}
|
||||
if (covered('mac') && covered('linux')) {
|
||||
continue
|
||||
}
|
||||
// Only the Windows-native loaders may be absent from the mac/linux plans.
|
||||
expect(source, `'${specifier}' is packaged for Windows only`).toContain('windows')
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves the copied emoji dataset the way the packaged main bundle does', async () => {
|
||||
const resourcesDir = await mkdtemp(join(tmpdir(), 'orca-lazy-require-'))
|
||||
try {
|
||||
|
||||
@@ -2,15 +2,11 @@
|
||||
// Web seam removes native audio/keep-awake: their web stubs deny microphone and do nothing, rather than capturing through shell.
|
||||
// Resolve package names before absence checks to catch typos; assert eight excluded vendored modules, not drifting total closure size.
|
||||
import { createRequire } from 'node:module'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { mobileWebAppRouteClosure } from '../build-mobile-web-app-bundle.mjs'
|
||||
import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs'
|
||||
import { MOBILE_WEB_PAGE_ROUTES } from '../mobile-web-page-routes.mjs'
|
||||
import {
|
||||
PAGE_ROUTE_MODULES,
|
||||
pageRouteModulesCoverTheManifest
|
||||
} from './mobile-web-app-page-route-modules.mjs'
|
||||
import { PAGE_ROUTE_MODULES } from './mobile-web-app-page-route-modules.mjs'
|
||||
import { MobileWebBundleRouteSchema } from '../../../src/shared/mobile-web-bundle/manifest-contract.ts'
|
||||
|
||||
// Gate mobile transforms too: nearest tsconfig extends Expo package absent from root-only shards.
|
||||
@@ -21,7 +17,6 @@ const SEAM = 'src/platform/dictation-capture.web.ts'
|
||||
|
||||
const NATIVE_SEAM = 'src/platform/dictation-capture.ts'
|
||||
|
||||
// Use census’s verb set; bridge-audio-verbs.test.ts checks it against shell table.
|
||||
const DICTATION_GRANTS = ['native.audio.start', 'native.audio.read', 'native.audio.stop']
|
||||
|
||||
// Native modules reach JSI; browser substitutes denied microphone and no-op wake lock.
|
||||
@@ -74,24 +69,6 @@ describeClosure(
|
||||
expect(await grantsMissingForRoutes(MOBILE_WEB_PAGE_ROUTES)).toEqual([])
|
||||
})
|
||||
|
||||
it('finds the seam in exactly one closure, which is the session route', async () => {
|
||||
const reaching = []
|
||||
for (const route of MOBILE_WEB_PAGE_ROUTES) {
|
||||
const module = PAGE_ROUTE_MODULES.get(route.pathname)
|
||||
if (module === undefined) {
|
||||
throw new Error(`No route module for ${route.pathname}`)
|
||||
}
|
||||
const closure = await closureOf(module)
|
||||
if (closure.local.includes(SEAM)) {
|
||||
reaching.push(route.pathname)
|
||||
}
|
||||
}
|
||||
// Require session route reaches seam; absence checks must not pass because dictation left page.
|
||||
expect(reaching).toEqual([SESSION_PATHNAME])
|
||||
const session = await closureOf(SESSION)
|
||||
expect(session.local).toContain(SEAM)
|
||||
})
|
||||
|
||||
it('reds the same rule when the session route is registered without them', async () => {
|
||||
// Drive same rule over under-granted entry to prove missing grants are detected.
|
||||
expect(
|
||||
@@ -133,30 +110,3 @@ describeClosure(
|
||||
},
|
||||
240_000
|
||||
)
|
||||
|
||||
describe('the census rule itself', () => {
|
||||
it('names a route module for every registered pathname', () => {
|
||||
const { mapped, declared } = pageRouteModulesCoverTheManifest(MOBILE_WEB_PAGE_ROUTES)
|
||||
expect(mapped).toEqual(declared)
|
||||
})
|
||||
|
||||
// Dynamic import delays load but still transforms mobile tsconfig extending absent Expo; dependency gate must enclose import on root-only shards.
|
||||
it.skipIf(!bundles)(
|
||||
'names only verbs the shell actually serves, read from its own table',
|
||||
async () => {
|
||||
// Require shell verb-table entries; route census agreeing with its own copied names proves nothing.
|
||||
const { BRIDGE_NATIVE_VERB_NAMES } =
|
||||
await import('../../../mobile/src/mobile-web-shell/bridge/bridge-native-verbs.ts')
|
||||
expect(new Set(DICTATION_GRANTS).size).toBe(3)
|
||||
for (const grant of DICTATION_GRANTS) {
|
||||
expect(BRIDGE_NATIVE_VERB_NAMES, grant).toContain(grant)
|
||||
expect(
|
||||
MobileWebBundleRouteSchema.safeParse({ pathname: '/h', grants: [grant] }).success,
|
||||
grant
|
||||
).toBe(true)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
export const MOBILE_DIR = fileURLToPath(new URL('../../../mobile/', import.meta.url))
|
||||
|
||||
@@ -1,27 +1,13 @@
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { beforeAll, describe, expect, it } from 'vitest'
|
||||
import { mobileWebAppRouteClosure } from '../build-mobile-web-app-bundle.mjs'
|
||||
import { mobileWebAppDependenciesPresent } from './mobile-web-app-bundle-dependencies.mjs'
|
||||
import { mobileWebAppRouteChunkClosure } from './mobile-web-app-route-chunk-closure.mjs'
|
||||
import {
|
||||
editableHostFontSizeOffenders,
|
||||
editableHostsIn,
|
||||
unresolvedEditableHostStyles
|
||||
} from './platform-seams/mobile-web-app-editable-host-font-size.mjs'
|
||||
import {
|
||||
textInputFontSizeOffenders,
|
||||
unresolvedTextInputStyles
|
||||
} from './platform-seams/mobile-web-app-text-input-font-size-seam.mjs'
|
||||
|
||||
// Require terminal source modules/xterm, excluding WebView engine strings and generated document/factory to avoid shipping document twice.
|
||||
// Remeasure closure after generators and upstream merges; shared inputs make summed historical module deltas unreliable.
|
||||
// Input metafile includes dynamic imports: only emitted static import-statement edges prove Mermaid is deferred.
|
||||
// Prebundle Mermaid as one artifact; package splitting once emitted 103 scripts against shell’s 256-asset limit. Deferral saves fetch/evaluation, not OTA download.
|
||||
|
||||
const projectDir = fileURLToPath(new URL('../../..', import.meta.url))
|
||||
const mobileDir = join(projectDir, 'mobile')
|
||||
|
||||
const SESSION_ROUTE = 'app/h/[hostId]/session/[worktreeId].tsx'
|
||||
|
||||
// WebView-only generated programs cannot execute in page.
|
||||
@@ -49,9 +35,6 @@ const GAINED_OUTSIDE_THE_DOCUMENT = [
|
||||
|
||||
const XTERM_PACKAGES = ['@xterm/xterm', '@xterm/addon-unicode11', '@xterm/addon-webgl']
|
||||
|
||||
// Keep all route text inputs above focus-zoom floor, including newly imported terminal inputs.
|
||||
const EXPECTED_OFFENDERS = 0
|
||||
|
||||
const MERMAID_PAGE_ENGINE = 'src/components/pr-sidebar/mermaid-page-engine.generated.ts'
|
||||
const MERMAID_PACKAGE = 'node_modules/mermaid/'
|
||||
|
||||
@@ -123,25 +106,6 @@ describeClosure(
|
||||
// Require route chunk in walk so empty static download cannot pass.
|
||||
expect(download.staticChunks).toContain(download.routeChunk)
|
||||
}, 600_000)
|
||||
|
||||
it('leaves the 16px seam census exactly where C7.2 left it', async () => {
|
||||
// Require seam reachable and every style resolved before interpreting zero offenders.
|
||||
expect(closure.local).toContain('src/platform/text-input-font-size.web.ts')
|
||||
expect(unresolvedTextInputStyles(mobileDir, closure)).toEqual([])
|
||||
expect(textInputFontSizeOffenders(mobileDir, closure)).toHaveLength(EXPECTED_OFFENDERS)
|
||||
expect(closure.local).toContain('src/session/mobile-native-chat-input-styles.web.ts')
|
||||
expect(closure.local).not.toContain('src/session/mobile-native-chat-input-styles.ts')
|
||||
}, 300_000)
|
||||
|
||||
it('holds the editables the TextInput census cannot see to the same floor', async () => {
|
||||
// Rich editor contenteditable lives in markup/CSS, invisible to JSX text-input census; check same font floor through markup.
|
||||
expect(editableHostsIn(mobileDir, closure)).toContainEqual({
|
||||
file: 'src/components/rich-markdown/document-markup.ts',
|
||||
id: 'editor'
|
||||
})
|
||||
expect(unresolvedEditableHostStyles(mobileDir, closure)).toEqual([])
|
||||
expect(editableHostFontSizeOffenders(mobileDir, closure)).toEqual([])
|
||||
}, 300_000)
|
||||
},
|
||||
900_000
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
buildOrcadEntry,
|
||||
escapeLegalCommentTail,
|
||||
findNonAsciiSource,
|
||||
moduleAtOffset,
|
||||
ORCAD_COMMONJS_MODULE_OPTIONS
|
||||
} from './orcad-entry-build.mjs'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
@@ -111,11 +110,6 @@ it('keeps the server bundle ASCII, so V8 stores its source one byte per characte
|
||||
expect(findNonAsciiSource(readFileSync(bundle, 'utf8'))).toBeNull()
|
||||
})
|
||||
|
||||
it('names the line of a non-ASCII character a regex literal or tagged template left', () => {
|
||||
expect(findNonAsciiSource('a=1\nb=/[\\u2726]/')).toBeNull()
|
||||
expect(findNonAsciiSource('a=1\nb=/[\u2726]/')).toMatchObject({ offset: 8, line: 2 })
|
||||
})
|
||||
|
||||
it('escapes only the trailing license comment, never code', () => {
|
||||
const code = 'a=/[\u2726]/;'
|
||||
const escaped = escapeLegalCommentTail(
|
||||
@@ -127,12 +121,6 @@ it('escapes only the trailing license comment, never code', () => {
|
||||
expect(escapeLegalCommentTail(code)).toBe(code)
|
||||
})
|
||||
|
||||
it('names the module an unminified build placed an offset in', () => {
|
||||
const source = 'x\n// src/a.ts\nvar a = 1;\n// node_modules/b/index.js\nvar b = /\u2726/;\n'
|
||||
expect(moduleAtOffset(source, source.indexOf('\u2726'))).toBe('node_modules/b/index.js')
|
||||
expect(moduleAtOffset(source, source.indexOf('var a'))).toBe('src/a.ts')
|
||||
})
|
||||
|
||||
it('keeps a build with every dark switch turned on ASCII', () => {
|
||||
const darkBundle = join(directory, 'orcad-server-dark.js')
|
||||
const built = runProcessSync({
|
||||
|
||||
@@ -1,155 +1,13 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { runInNewContext } from 'node:vm'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { compileAggregateExpression } from './pr-aggregate-gate-context.mjs'
|
||||
|
||||
const projectDir = resolve(import.meta.dirname, '../..')
|
||||
const require = createRequire(import.meta.url)
|
||||
const { createPackagedRuntimeNodeModuleResources } = require('../packaged-runtime-node-modules.cjs')
|
||||
const readProject = (file) => readFileSync(join(projectDir, file), 'utf8')
|
||||
const packageJson = JSON.parse(readProject('package.json'))
|
||||
const pnpmWorkspace = parse(readProject('pnpm-workspace.yaml'))
|
||||
// Resolve the win32 plan wherever its OS-gated addon is installed; windows-registry is a workspace link on every platform.
|
||||
const windowsAddonsInstalled = existsSync(
|
||||
join(projectDir, 'node_modules', '@vscode', 'windows-process-tree', 'package.json')
|
||||
)
|
||||
|
||||
describe('Electron runtime package contract', () => {
|
||||
const packageTargets = {
|
||||
win32: windowsAddonsInstalled ? createPackagedRuntimeNodeModuleResources('win32') : [],
|
||||
darwin: createPackagedRuntimeNodeModuleResources('darwin'),
|
||||
linux: createPackagedRuntimeNodeModuleResources('linux')
|
||||
}
|
||||
|
||||
it('keeps the native Windows registry addon optional and platform-gated', () => {
|
||||
expect(packageJson.optionalDependencies['@orca/windows-registry']).toBe('workspace:*')
|
||||
// allowBuilds suppresses install-time node-gyp; the Windows rebuild owns the addon’s runtime ABI.
|
||||
expect(pnpmWorkspace.allowBuilds['@orca/windows-registry']).toBe(false)
|
||||
if (windowsAddonsInstalled) {
|
||||
expect(packageTargets.win32).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ to: join('node_modules', '@orca', 'windows-registry') }),
|
||||
expect.objectContaining({ to: join('node_modules', 'node-addon-api') })
|
||||
])
|
||||
)
|
||||
}
|
||||
for (const platform of ['darwin', 'linux']) {
|
||||
expect(packageTargets[platform]).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ to: join('node_modules', '@orca', 'windows-registry') })
|
||||
])
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// Without it the secure-file writes report unrestricted and orcad's privacy check fails closed.
|
||||
it('ships the native Windows ACL addon on Windows only, built by the Windows rebuild', () => {
|
||||
expect(packageJson.optionalDependencies['@orca/windows-security']).toBe('workspace:*')
|
||||
expect(pnpmWorkspace.allowBuilds['@orca/windows-security']).toBe(false)
|
||||
if (windowsAddonsInstalled) {
|
||||
expect(packageTargets.win32).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ to: join('node_modules', '@orca', 'windows-security') })
|
||||
])
|
||||
)
|
||||
}
|
||||
for (const platform of ['darwin', 'linux']) {
|
||||
expect(packageTargets[platform]).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ to: join('node_modules', '@orca', 'windows-security') })
|
||||
])
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the native Windows process-table addon optional and platform-gated', () => {
|
||||
expect(packageJson.optionalDependencies['@vscode/windows-process-tree']).toBe('0.8.0')
|
||||
// As with registry, suppress install-time node-gyp so the Windows rebuild selects the runtime ABI.
|
||||
expect(pnpmWorkspace.allowBuilds['@vscode/windows-process-tree']).toBe(false)
|
||||
// Patch removes unavailable Spectre-library requirement and 1024-process enumeration cap that hides descendants on busy hosts.
|
||||
expect(pnpmWorkspace.patchedDependencies['@vscode/windows-process-tree@0.8.0']).toBe(
|
||||
'config/patches/@vscode__windows-process-tree@0.8.0.patch'
|
||||
)
|
||||
if (windowsAddonsInstalled) {
|
||||
expect(packageTargets.win32).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ to: join('node_modules', '@vscode', 'windows-process-tree') })
|
||||
])
|
||||
)
|
||||
}
|
||||
for (const platform of ['darwin', 'linux']) {
|
||||
expect(packageTargets[platform]).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ to: join('node_modules', '@vscode', 'windows-process-tree') })
|
||||
])
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps release-cut signing provenance on GitHub-hosted runners', () => {
|
||||
const releaseWorkflow = parse(
|
||||
readFileSync(join(projectDir, '.github/workflows/release-cut.yml'), 'utf8')
|
||||
)
|
||||
const buildMatrixRunners = releaseWorkflow.jobs.build.strategy.matrix.include.map(
|
||||
({ os }) => os
|
||||
)
|
||||
const releaseWorkflowText = readFileSync(
|
||||
join(projectDir, '.github/workflows/release-cut.yml'),
|
||||
'utf8'
|
||||
)
|
||||
const macDispatchStep = releaseWorkflow.jobs['build-mac'].steps.find(
|
||||
(step) => step.name === 'Run isolated macOS release build'
|
||||
)
|
||||
|
||||
expect(releaseWorkflowText).not.toContain('blacksmith-')
|
||||
for (const caller of Object.values(releaseWorkflow.jobs)) {
|
||||
if (!caller.uses?.startsWith('./.github/workflows/')) {
|
||||
continue
|
||||
}
|
||||
const child = parse(readProject(caller.uses))
|
||||
const inputs = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(child.on.workflow_call.inputs).map(([name, input]) => [
|
||||
name,
|
||||
input.default ?? ''
|
||||
])
|
||||
),
|
||||
...caller.with
|
||||
}
|
||||
for (const job of Object.values(child.jobs)) {
|
||||
if (!job['runs-on']) {
|
||||
continue
|
||||
}
|
||||
const runner = job['runs-on'].startsWith('${{')
|
||||
? compileAggregateExpression(job['runs-on'], ['inputs'])({ inputs }).coerceString()
|
||||
: job['runs-on']
|
||||
expect(runner).not.toMatch(/^(blacksmith-|orca-)/)
|
||||
}
|
||||
}
|
||||
expect(releaseWorkflow.jobs['build-mac']['runs-on']).toBe('ubuntu-latest')
|
||||
expect(releaseWorkflow.jobs['build-mac'].permissions.actions).toBe('write')
|
||||
expect(macDispatchStep.run).toBe(
|
||||
'node config/scripts/run-release-mac-build-workflow.mjs dispatch'
|
||||
)
|
||||
expect(macDispatchStep.env.RELEASE_MAC_BUILD_WORKFLOW).toBe('release-mac-build.yml')
|
||||
expect(macDispatchStep.env.RELEASE_MAC_BUILD_TAG).toBe('${{ needs.cut.outputs.tag }}')
|
||||
expect(buildMatrixRunners).not.toContain('blacksmith-6vcpu-macos-15')
|
||||
expect(releaseWorkflow.jobs['publish-release'].needs).toContain('build')
|
||||
expect(releaseWorkflow.jobs['publish-release'].needs).toContain('build-mac')
|
||||
expect(releaseWorkflow.jobs['publish-release'].needs).toContain('verify-mac-build')
|
||||
const macVerification = releaseWorkflow.jobs['verify-mac-build']
|
||||
expect(macVerification.needs).toEqual(['cut', 'build-mac', 'build'])
|
||||
expect(macVerification.if).toContain('!cancelled()')
|
||||
expect(macVerification.if).not.toContain('needs.build.result')
|
||||
expect(macVerification.permissions).toEqual({ actions: 'read', contents: 'read' })
|
||||
expect(releaseWorkflow.jobs['publish-release'].if).toContain(
|
||||
"needs.verify-mac-build.result == 'success'"
|
||||
)
|
||||
})
|
||||
|
||||
it('qualifies macOS after failed sibling builds and publishes only complete successful builds', () => {
|
||||
const { jobs } = parse(readProject('.github/workflows/release-cut.yml'))
|
||||
const evaluate = (source, needs, cancelled) =>
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
@@ -15,7 +7,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
findCachedElevatePaths,
|
||||
replaceCachedElevateHelpers,
|
||||
resolveElectronBuilderCacheDir,
|
||||
summarizeSwap
|
||||
} from './replace-cached-nsis-elevate.mjs'
|
||||
|
||||
@@ -116,44 +107,7 @@ describe('cached elevate.exe swap covers the real electron-builder layouts', ()
|
||||
).toEqual([join(overrideDir, 'elevate.exe')])
|
||||
})
|
||||
|
||||
it('resolves the cache root the same way app-builder-lib does', () => {
|
||||
expect(
|
||||
resolveElectronBuilderCacheDir({
|
||||
env: { LOCALAPPDATA: 'C:\\Users\\runneradmin\\AppData\\Local' },
|
||||
platform: 'win32'
|
||||
})
|
||||
).toBe(join('C:\\Users\\runneradmin\\AppData\\Local', 'electron-builder', 'Cache'))
|
||||
expect(resolveElectronBuilderCacheDir({ env: {}, platform: 'darwin', home: '/Users/a' })).toBe(
|
||||
join('/Users/a', 'Library', 'Caches', 'electron-builder')
|
||||
)
|
||||
expect(resolveElectronBuilderCacheDir({ env: { ELECTRON_BUILDER_CACHE: '/mnt/cache' } })).toBe(
|
||||
'/mnt/cache'
|
||||
)
|
||||
})
|
||||
|
||||
// Compare real cache scan against independent unbounded walk to catch vacuous empty matches; skip only if no bundle is cached.
|
||||
it('finds every elevate.exe the real electron-builder cache holds', (ctx) => {
|
||||
const cacheDir = resolveElectronBuilderCacheDir()
|
||||
if (!existsSync(cacheDir)) {
|
||||
// Report missing real cache as skipped, never passed; fixture-only coverage cannot validate unknown layouts.
|
||||
ctx.skip()
|
||||
return
|
||||
}
|
||||
const walk = (dir) =>
|
||||
readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
return walk(path)
|
||||
}
|
||||
return entry.name.toLowerCase() === 'elevate.exe' ? [path] : []
|
||||
})
|
||||
const onDisk = walk(cacheDir)
|
||||
if (onDisk.length === 0) {
|
||||
ctx.skip()
|
||||
return
|
||||
}
|
||||
expect(findCachedElevatePaths(cacheDir, { env: {} }).sort()).toEqual(onDisk.sort())
|
||||
})
|
||||
})
|
||||
|
||||
describe('the probe, not the scan, decides whether the swap worked', () => {
|
||||
|
||||
@@ -10,19 +10,6 @@ const projectRoot = resolve(import.meta.dirname, '../..')
|
||||
|
||||
describe('7za path resolution for the Windows signing gates (#6487)', () => {
|
||||
// Match upstream mac/win/linux architecture dirs; darwin/ or collapsed-arch guesses silently fail to resolve.
|
||||
it.each([
|
||||
['darwin', 'arm64', 'mac/arm64/7za'],
|
||||
['darwin', 'x64', 'mac/x64/7za'],
|
||||
['win32', 'x64', 'win/x64/7za.exe'],
|
||||
['win32', 'ia32', 'win/ia32/7za.exe'],
|
||||
['win32', 'arm64', 'win/arm64/7za.exe'],
|
||||
['linux', 'x64', 'linux/x64/7za'],
|
||||
['linux', 'arm64', 'linux/arm64/7za']
|
||||
])('maps the %s/%s legacy layout to %s', (platform, arch, expected) => {
|
||||
expect(legacy7zaRelativePath(platform, arch).join('/')).toBe(
|
||||
`node_modules/7zip-bin/${expected}`
|
||||
)
|
||||
})
|
||||
|
||||
it('prefers an existing legacy file over the downloaded toolset', async () => {
|
||||
const scratch = mkdtempSync(join(tmpdir(), 'orca 7za legacy '))
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { runProcessSync } from '@orca/process-host'
|
||||
import { DEFAULT_MAX_OUTPUT_BYTES } from '@orca/process-host/process-spec'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { parseAllDocuments } from 'yaml'
|
||||
|
||||
import { auditToolEntry, runAuditTool } from './run-audit-tool.mjs'
|
||||
import { classifyPrJobs } from './pr-code-change-scope.mjs'
|
||||
|
||||
const root = resolve(import.meta.dirname, '../..')
|
||||
const success = { code: 0, signal: null, timedOut: false }
|
||||
@@ -97,34 +95,4 @@ describe('locked audit tool execution', () => {
|
||||
expect(() => runAuditTool('../arbitrary-package', [], { run })).toThrow('Unknown audit tool')
|
||||
expect(run).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['package.json', 'pnpm-lock.yaml', 'pnpm-workspace.yaml'])(
|
||||
'selects the real-tool CI smoke for an isolated %s update',
|
||||
(file) => {
|
||||
expect(classifyPrJobs([`config/audit-tools/${file}`]).static_analysis).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps the tool workspace isolated, exact and integrity-locked', () => {
|
||||
const manifest = JSON.parse(readFileSync(join(root, 'config/audit-tools/package.json'), 'utf8'))
|
||||
const rootManifest = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'))
|
||||
expect(manifest.packageManager).toBe(rootManifest.packageManager)
|
||||
const [policy] = parseAllDocuments(
|
||||
readFileSync(join(root, 'config/audit-tools/pnpm-workspace.yaml'), 'utf8')
|
||||
).map((document) => document.toJS())
|
||||
expect(policy).toMatchObject({ minimumReleaseAge: 4320, ignoreScripts: true, allowBuilds: {} })
|
||||
const documents = parseAllDocuments(
|
||||
readFileSync(join(root, 'config/audit-tools/pnpm-lock.yaml'), 'utf8')
|
||||
).map((document) => document.toJS())
|
||||
const lock = documents.find((document) => document.importers?.['.']?.dependencies)
|
||||
expect(Object.keys(lock.importers)).toEqual(['.'])
|
||||
for (const [name, version] of Object.entries(manifest.dependencies)) {
|
||||
expect(version).toMatch(/^\d+\.\d+\.\d+$/)
|
||||
expect(lock.importers['.'].dependencies[name].specifier).toBe(version)
|
||||
expect(rootManifest.dependencies[name] ?? rootManifest.devDependencies[name]).toBeUndefined()
|
||||
}
|
||||
for (const entry of Object.values(lock.packages)) {
|
||||
expect(entry.resolution.integrity).toMatch(/^sha512-/)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { expect, it } from 'vitest'
|
||||
import { importsSqliteRuntime, SQLITE_MODULES } from './vitest-sqlite-runtime-scan.mjs'
|
||||
|
||||
// A moved module would silently send its importers back to Bun.
|
||||
it.each(SQLITE_MODULES)('scans for a SQLite module that still exists: %s', (module) => {
|
||||
expect(existsSync(`${module}.ts`)).toBe(true)
|
||||
})
|
||||
import { importsSqliteRuntime } from './vitest-sqlite-runtime-scan.mjs'
|
||||
|
||||
it.each([
|
||||
['import { createRestTestRig } from "./structured-agent-session-rest-test-rig"', true],
|
||||
|
||||
@@ -29,7 +29,6 @@ const WINDOWS_SHIM_SPAWN_ALLOWLIST = [
|
||||
// Write or assert on shim files rather than spawning one.
|
||||
'config/scripts/dev-cli-terminal-wrapper.mjs',
|
||||
'config/scripts/dev-cli-terminal-wrapper.test.mjs',
|
||||
'config/scripts/electron-builder-config.test.mjs',
|
||||
'config/scripts/live-remote-freeze-rpc.mjs',
|
||||
'config/scripts/pty-transcript-secret-scan.test.mjs',
|
||||
'config/scripts/remote-agent-session-authority-repro.mjs',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
// Exercise table-listed audio verbs through the engine seam; denied permission, full rings and post-stop reads are device-independent conditions expensive to force in simulators.
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { MobileWebBundleRouteSchema } from '../../../../src/shared/mobile-web-bundle/manifest-contract'
|
||||
|
||||
import { BridgeNativeVerbRefusedError } from '../bridge-host-errors'
|
||||
import { createNativeAudioCapture, type NativeAudioEngine } from '../../platform/native-audio'
|
||||
import {
|
||||
@@ -36,8 +36,6 @@ function gate() {
|
||||
return { promise, release: () => release() }
|
||||
}
|
||||
|
||||
const AUDIO_VERBS = ['native.audio.start', 'native.audio.read', 'native.audio.stop'] as const
|
||||
|
||||
function createTestEngine(
|
||||
overrides: Partial<{
|
||||
permission: NativeAudioEngine['requestPermission']
|
||||
@@ -121,24 +119,6 @@ function decode(base64: string): Uint8Array {
|
||||
return bytes
|
||||
}
|
||||
|
||||
describe('the audio verbs in the table', () => {
|
||||
it('accepts the audio verbs as route grants', () => {
|
||||
for (const verb of AUDIO_VERBS) {
|
||||
expect(
|
||||
MobileWebBundleRouteSchema.safeParse({ pathname: '/h', grants: [verb] }).success,
|
||||
verb
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('refuses the camel-cased spelling of the read, which is what makes the name load-bearing', () => {
|
||||
expect(
|
||||
MobileWebBundleRouteSchema.safeParse({ pathname: '/h', grants: ['native.audio.readChunk'] })
|
||||
.success
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('what the audio schemas refuse', () => {
|
||||
it('refuses a start with no rate, a rate off the grid, and an unknown param', () => {
|
||||
expect(audioStartParamsSchema.safeParse({}).success).toBe(false)
|
||||
|
||||
@@ -178,13 +178,6 @@ describe('MobileNativeChatSessionOptionPickers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('lets the pill shrink so a long label never pushes the composer buttons off the row', () => {
|
||||
mount([MODEL_DESCRIPTOR, EFFORT_DESCRIPTOR])
|
||||
expect(findAllByHostType(renderer!.root, 'View')[0]!.props.style).toMatchObject({
|
||||
flexShrink: 1
|
||||
})
|
||||
})
|
||||
|
||||
it('opens the model sheet and applies a picked model', async () => {
|
||||
mount([MODEL_DESCRIPTOR, EFFORT_DESCRIPTOR])
|
||||
await act(async () => pill('Model').props.onPress())
|
||||
|
||||
@@ -480,12 +480,6 @@ describe('ToolRun', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('lets a tap inside the run land while the keyboard is up, as the transcript list does', () => {
|
||||
const tree = render([{ type: 'tool-call', name: 'Bash', input: { command: 'ls' } }])
|
||||
const boxes = tree.root.findAll((node) => String(node.type) === 'ScrollView')
|
||||
expect(boxes.map((box) => box.props.keyboardShouldPersistTaps)).toEqual(['handled'])
|
||||
})
|
||||
|
||||
it('follows new work at the end of a live run, but holds still when the reader opens a row', () => {
|
||||
const live = (blocks: NativeChatBlock[]) =>
|
||||
createElement(ToolRun, {
|
||||
@@ -530,23 +524,6 @@ describe('ToolRun', () => {
|
||||
expect(scrolls.toEnd).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it("keeps a row's output out of inline selection where the platform forbids it", () => {
|
||||
const tree = render([
|
||||
{ type: 'tool-call', name: 'Bash', input: { command: 'ls' } },
|
||||
{ type: 'tool-result', output: 'a.ts' }
|
||||
])
|
||||
act(() => {
|
||||
tree.root
|
||||
.findAll((node) => String(node.type) === 'Pressable')
|
||||
.findLast((node) => textIn(node).includes('ls'))!
|
||||
.props.onPress()
|
||||
})
|
||||
const output = tree.root.findAll(
|
||||
(node) => String(node.type) === 'Text' && textIn(node)[0] === 'a.ts'
|
||||
)[0]!
|
||||
expect(output.props.selectable).toBe(false)
|
||||
})
|
||||
|
||||
it('does not save a follow for a later row opening when new work left the height unchanged', () => {
|
||||
const live = (blocks: NativeChatBlock[]) =>
|
||||
createElement(ToolRun, {
|
||||
|
||||
@@ -28,19 +28,6 @@ function unresolved(
|
||||
describe('rebaseMobileNativeChatPendingBaselines', () => {
|
||||
const history = [userTurn('m1', 'run the tests', 1000), assistantTurn('m2', 'passed', 1100)]
|
||||
|
||||
it('returns the same array when every baseline is already resolved', () => {
|
||||
const pending: MobileNativeChatPendingMessage[] = [
|
||||
{
|
||||
id: 'p1',
|
||||
text: 'hi',
|
||||
expectedOccurrence: 1,
|
||||
baselineTailMessageId: 'm2',
|
||||
baselineResolved: true
|
||||
}
|
||||
]
|
||||
expect(rebaseMobileNativeChatPendingBaselines(history, pending)).toBe(pending)
|
||||
})
|
||||
|
||||
it('gives an unresolved send the loaded tail as its glue boundary', () => {
|
||||
expect(
|
||||
rebaseMobileNativeChatPendingBaselines(history, [unresolved('p1', 'run the tests')])
|
||||
|
||||
@@ -1,34 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
summarizeToolInput,
|
||||
toolFilePath
|
||||
} from '../../../src/shared/native-chat/tools/native-chat-tool-summary'
|
||||
import { summarizeToolInput } from '../../../src/shared/native-chat/tools/native-chat-tool-summary'
|
||||
|
||||
describe('summarizeToolInput', () => {
|
||||
it('passes short strings through, collapsing whitespace', () => {
|
||||
expect(summarizeToolInput('ls -la\n/tmp')).toBe('ls -la /tmp')
|
||||
})
|
||||
|
||||
it('serializes objects to compact JSON', () => {
|
||||
expect(summarizeToolInput({ path: 'a.ts', limit: 5 })).toBe('{"path":"a.ts","limit":5}')
|
||||
})
|
||||
|
||||
it('truncates long previews with an ellipsis', () => {
|
||||
const long = `prefix ${'x'.repeat(200)}`
|
||||
const out = summarizeToolInput(long)
|
||||
expect(out).toBe(`prefix ${'x'.repeat(72)}…`)
|
||||
})
|
||||
|
||||
it('returns empty string for null/undefined', () => {
|
||||
expect(summarizeToolInput(null)).toBe('')
|
||||
expect(summarizeToolInput(undefined)).toBe('')
|
||||
})
|
||||
|
||||
it('handles numbers and booleans', () => {
|
||||
expect(summarizeToolInput(42)).toBe('42')
|
||||
expect(summarizeToolInput(true)).toBe('true')
|
||||
})
|
||||
|
||||
it('does not traverse an unbounded tool-input object for a short preview', () => {
|
||||
const input: Record<string, unknown> = Object.fromEntries(
|
||||
Array.from({ length: 8 }, (_unused, index) => [`key${index}`, index])
|
||||
@@ -40,17 +13,3 @@ describe('summarizeToolInput', () => {
|
||||
expect(lateGetter).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('toolFilePath', () => {
|
||||
it('extracts file_path / filePath / path / notebook_path', () => {
|
||||
expect(toolFilePath({ file_path: 'src/a.ts' })).toBe('src/a.ts')
|
||||
expect(toolFilePath({ filePath: 'src/b.ts' })).toBe('src/b.ts')
|
||||
expect(toolFilePath({ path: 'src/c.ts' })).toBe('src/c.ts')
|
||||
expect(toolFilePath({ notebook_path: 'n.ipynb' })).toBe('n.ipynb')
|
||||
})
|
||||
it('returns null when there is no file target', () => {
|
||||
expect(toolFilePath({ command: 'ls' })).toBeNull()
|
||||
expect(toolFilePath('x')).toBeNull()
|
||||
expect(toolFilePath(null)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -48,22 +48,6 @@ const startRow: AgentJournalRenderItem = {
|
||||
}
|
||||
|
||||
describe("the phone's line under a not-sent message", () => {
|
||||
it.each([
|
||||
['claude', 'Claude'],
|
||||
['codex', 'Codex']
|
||||
])('names %s when its terminal holds the conversation', (agent, name) => {
|
||||
const notices = mobileNativeChatUnsentNotices(
|
||||
{
|
||||
items: [],
|
||||
submissions: [submission('held', { rejection: { kind: 'conversationOpenInTerminal' } })]
|
||||
},
|
||||
agent
|
||||
)
|
||||
expect(notices.get(agentJournalSubmissionKey('held'))).toBe(
|
||||
`${name} is already running this conversation in another terminal. Quit ${name} there or close that terminal to continue here.`
|
||||
)
|
||||
})
|
||||
|
||||
it("words each rejected message as the desktop does, naming the chat's agent", () => {
|
||||
const notices = mobileNativeChatUnsentNotices(
|
||||
{
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { AGENT_SESSION_COHERENT_MODEL_CATALOG_CAPABILITY } from '../../../src/shared/structured-agent-session-surface-capabilities'
|
||||
import {
|
||||
AGENT_SESSION_ATTACHMENTS_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_ITEM_DETAIL_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CONVERSATION_STOP_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_PHONE_ATTACHMENTS_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QUESTION_ANSWERS_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QUEUED_COMMANDS_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QUEUED_MESSAGE_EDIT_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QUEUED_MESSAGE_IMAGE_EDIT_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QUEUED_MESSAGES_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_STATUS_FEED_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_REPEATED_STOP_RUNTIME_CAPABILITY
|
||||
} from '../../../src/shared/protocol-version'
|
||||
import { AGENT_SESSION_SEND_OPTIONS_RUNTIME_CAPABILITY } from '../../../src/shared/agent-session-create-capabilities'
|
||||
import { AGENT_SESSION_PERMISSION_MODES_RUNTIME_CAPABILITY } from '../../../src/shared/agent-session-permission-modes-capability'
|
||||
import { structuredAgentSessionHostSupport } from './mobile-structured-agent-session-host-support'
|
||||
|
||||
describe('structuredAgentSessionHostSupport', () => {
|
||||
it('reads each structured-session feature from the host capability list', () => {
|
||||
const none = {
|
||||
coherentModelCatalog: false,
|
||||
promptCancel: false,
|
||||
questionAnswers: false,
|
||||
queuedMessages: false,
|
||||
queuedCommands: false,
|
||||
queuedMessageEdit: false,
|
||||
queuedMessageImageEdit: false,
|
||||
quietRepeatedStop: false,
|
||||
statusFeed: false,
|
||||
sendOptions: false,
|
||||
conversationStop: false,
|
||||
permissionModes: false,
|
||||
permissionOwn: false,
|
||||
itemDetail: false,
|
||||
phoneAttachments: false
|
||||
}
|
||||
expect(structuredAgentSessionHostSupport([])).toEqual(none)
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_COHERENT_MODEL_CATALOG_CAPABILITY])
|
||||
).toEqual({ ...none, coherentModelCatalog: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_QUESTION_ANSWERS_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, questionAnswers: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_PROMPT_CANCEL_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, promptCancel: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_QUEUED_MESSAGES_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, queuedMessages: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_QUEUED_COMMANDS_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, queuedCommands: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_QUEUED_MESSAGE_EDIT_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, queuedMessageEdit: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([
|
||||
AGENT_SESSION_QUEUED_MESSAGE_IMAGE_EDIT_RUNTIME_CAPABILITY
|
||||
])
|
||||
).toEqual({ ...none, queuedMessageImageEdit: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_REPEATED_STOP_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, quietRepeatedStop: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_STATUS_FEED_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, statusFeed: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_SEND_OPTIONS_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, sendOptions: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_CONVERSATION_STOP_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, conversationStop: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_PERMISSION_MODES_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, permissionModes: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_PHONE_ATTACHMENTS_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, phoneAttachments: true })
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_ITEM_DETAIL_RUNTIME_CAPABILITY])
|
||||
).toEqual({ ...none, itemDetail: true })
|
||||
// A host with the store but from before phones could upload into it offers them nothing.
|
||||
expect(
|
||||
structuredAgentSessionHostSupport([AGENT_SESSION_ATTACHMENTS_RUNTIME_CAPABILITY])
|
||||
).toEqual(none)
|
||||
})
|
||||
})
|
||||
-12
@@ -294,18 +294,6 @@ describe('useMobileNativeChatImageAttachments', () => {
|
||||
expect(hook!.attachments).toEqual([])
|
||||
})
|
||||
|
||||
it('delegates straight to baseSend when there are no attachments', async () => {
|
||||
const client = makeClient([])
|
||||
const baseSend = vi.fn().mockResolvedValue('accepted')
|
||||
mount(baseArgs({ client: client as unknown as RpcClient, baseSend }))
|
||||
|
||||
await act(async () => {
|
||||
await hook!.sendNativeChat('just text')
|
||||
})
|
||||
expect(baseSend).toHaveBeenCalledWith(...textSend('just text'))
|
||||
expect(client.calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('keeps the chips and does not submit when the image paste is rejected', async () => {
|
||||
pick.mockResolvedValue([{ base64: 'AAAA', uri: 'file:///a.jpg' }])
|
||||
const client = makeClient([
|
||||
|
||||
@@ -11,7 +11,6 @@ const clearDraftForSend = vi.fn()
|
||||
const restoreRejectedDraft = vi.fn()
|
||||
const holdUnconfirmedSend = vi.fn()
|
||||
|
||||
// Mutable resolution/transcript doubles let launch-draft cases reuse the send-seam harness.
|
||||
const viewMode = { isTabChatView: (_tabId: string) => true }
|
||||
const sessionState = { messages: [] as unknown[], status: 'ready', transcriptLoading: false }
|
||||
const structuredSendWithOutcome = vi.fn()
|
||||
@@ -67,7 +66,6 @@ const structuredSessionState = {
|
||||
loadingEarlier: false,
|
||||
loadEarlier: vi.fn()
|
||||
}
|
||||
const draftsArgs: Record<string, unknown>[] = []
|
||||
const promptsState = {
|
||||
permission: null as unknown,
|
||||
question: null as unknown,
|
||||
@@ -107,8 +105,7 @@ vi.mock('./use-mobile-structured-agent-session', () => ({
|
||||
})
|
||||
}))
|
||||
vi.mock('./use-mobile-native-chat-drafts', () => ({
|
||||
useMobileNativeChatDrafts: (args: Record<string, unknown>) => {
|
||||
draftsArgs.push(args)
|
||||
useMobileNativeChatDrafts: () => {
|
||||
return {
|
||||
composerText: '',
|
||||
setComposerText: vi.fn(),
|
||||
@@ -245,10 +242,6 @@ describe('useMobileNativeChatController handleNativeChatSend', () => {
|
||||
controller = null
|
||||
})
|
||||
|
||||
it('leaves structured prompt cancellation unavailable on the legacy bridge lane', () => {
|
||||
expect(controller?.handleNativeChatCancelPrompt).toBeUndefined()
|
||||
})
|
||||
|
||||
it('clears an orphaned image paste before a question-card answer (#10228)', async () => {
|
||||
markMobileNativeChatInputStale('term-1')
|
||||
clientStub.sendRequest.mockResolvedValue({
|
||||
@@ -317,19 +310,6 @@ describe('useMobileNativeChatController handleNativeChatSend', () => {
|
||||
expect(onSendResolved).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('threads the optimistic-echo image URIs into acceptSend on an accepted send', async () => {
|
||||
sendWithOutcome.mockResolvedValue('accepted')
|
||||
let accepted = false
|
||||
await act(async () => {
|
||||
accepted = await controller!.handleNativeChatSend('look', ['file:///a.jpg'])
|
||||
})
|
||||
expect(accepted).toBe(true)
|
||||
expect(acceptSend).toHaveBeenCalledWith(ORIGIN, 'look', ['file:///a.jpg'])
|
||||
expect(controller!.nativeChatAcceptsImages).toBe(true)
|
||||
expect(clearDraftForSend).toHaveBeenCalledWith(ORIGIN, 'look')
|
||||
expect(restoreRejectedDraft).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('routes structured agent-session sends away from terminal/nativeChat transports', async () => {
|
||||
await act(async () => {
|
||||
renderer?.update(createElement(Harness, structuredTab('codex')))
|
||||
@@ -511,97 +491,6 @@ describe('useMobileNativeChatController handleNativeChatSend', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('useMobileNativeChatController launch-draft wiring', () => {
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
const clientStub = { sendRequest: vi.fn() }
|
||||
|
||||
const chatTab = {
|
||||
type: 'terminal',
|
||||
id: 'tab-1',
|
||||
title: 'Claude',
|
||||
terminal: 'term-1',
|
||||
launchAgent: 'claude',
|
||||
launchDraft: 'https://github.com/o/r/issues/12',
|
||||
launchDraftCreatedAt: 7,
|
||||
isActive: true
|
||||
}
|
||||
|
||||
function Harness({ tab }: { tab: unknown }): null {
|
||||
useMobileNativeChatController({
|
||||
client: clientStub as unknown as RpcClient,
|
||||
connState: 'connected',
|
||||
hostId: 'h',
|
||||
worktreeId: 'w',
|
||||
activeSessionTab: tab as never,
|
||||
activeSessionTabId: 'tab-1',
|
||||
activeHandleRef: { current: 'term-1' },
|
||||
deviceTokenRef: { current: null },
|
||||
nativeChatTranscriptIsLocalReadable: true,
|
||||
nativeChatInputLeaseReady: true,
|
||||
onSendError: vi.fn(),
|
||||
onSendResolved: vi.fn()
|
||||
})
|
||||
return null
|
||||
}
|
||||
|
||||
function render(tab: unknown): void {
|
||||
act(() => {
|
||||
renderer = create(createElement(Harness, { tab }))
|
||||
})
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
draftsArgs.length = 0
|
||||
viewMode.isTabChatView = () => true
|
||||
sessionState.messages = []
|
||||
sessionState.status = 'ready'
|
||||
sessionState.transcriptLoading = false
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
act(() => renderer?.unmount())
|
||||
renderer = null
|
||||
})
|
||||
|
||||
it('forwards the tab launch draft and chat-active flag for a chat-resolved tab', () => {
|
||||
render(chatTab)
|
||||
|
||||
expect(draftsArgs.at(-1)).toMatchObject({
|
||||
tabId: 'tab-1',
|
||||
launchDraft: 'https://github.com/o/r/issues/12',
|
||||
launchDraftCreatedAt: 7,
|
||||
chatActive: true,
|
||||
transcriptLoading: false
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards the raw draft with chatActive false when the tab shows the terminal', () => {
|
||||
// Keep off-chat distinct from host retraction so toggling cannot permanently decline launch prefill.
|
||||
viewMode.isTabChatView = () => false
|
||||
render(chatTab)
|
||||
|
||||
expect(draftsArgs.at(-1)).toMatchObject({
|
||||
launchDraft: 'https://github.com/o/r/issues/12',
|
||||
chatActive: false
|
||||
})
|
||||
})
|
||||
|
||||
it('forwards the session hook’s transcriptLoading, not its status', () => {
|
||||
// Working status masks loading; only read-phase state establishes transcript trust.
|
||||
sessionState.status = 'working'
|
||||
sessionState.transcriptLoading = true
|
||||
render(chatTab)
|
||||
|
||||
expect(draftsArgs.at(-1)).toMatchObject({ transcriptLoading: true })
|
||||
})
|
||||
|
||||
it('forwards a null draft for a tab that publishes none', () => {
|
||||
render({ ...chatTab, launchDraft: undefined })
|
||||
|
||||
expect(draftsArgs.at(-1)).toMatchObject({ launchDraft: null, chatActive: true })
|
||||
})
|
||||
})
|
||||
|
||||
describe('useMobileNativeChatController ask dismissal across a transcript reload', () => {
|
||||
let renderer: ReactTestRenderer | null = null
|
||||
let controller: MobileNativeChatController | null = null
|
||||
|
||||
@@ -107,14 +107,6 @@ describe('useMobileNativeChatMessageSend', () => {
|
||||
api = null
|
||||
})
|
||||
|
||||
it('sizes the pre-clear to every line of a parked launch draft', async () => {
|
||||
mount(() => ({ text: DRAFT, createdAt: 1 }))
|
||||
await act(async () => {
|
||||
await api!.send('hello')
|
||||
})
|
||||
expect(clearArgs().clearInput).toBe(buildAgentTuiClearInputForText(DRAFT))
|
||||
})
|
||||
|
||||
it('issues the burst as its OWN write, before the body', async () => {
|
||||
mount(() => ({ text: DRAFT, createdAt: 1 }))
|
||||
await act(async () => {
|
||||
@@ -204,15 +196,6 @@ describe('useMobileNativeChatMessageSend', () => {
|
||||
expect(sentArgs().resolvedLaunchDraft).toBeUndefined()
|
||||
})
|
||||
|
||||
it('creates an optimistic echo for an ordinary chat send', async () => {
|
||||
mount(() => null)
|
||||
await act(async () => {
|
||||
await api!.send('hello')
|
||||
})
|
||||
expect(acceptSend).toHaveBeenCalledTimes(1)
|
||||
expect(onCommandSend).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
// TUI commands never echo as user turns; optimistic bubbles would remain queued forever.
|
||||
it('never creates an optimistic echo for a catalog command send', async () => {
|
||||
mount(() => null)
|
||||
|
||||
@@ -57,22 +57,6 @@ describe('useMobileNativeChatSessionOptions', () => {
|
||||
api = null
|
||||
})
|
||||
|
||||
it('serves the shared catalog snapshot for the agent', () => {
|
||||
mount()
|
||||
expect(api!.snapshot[0]).toMatchObject({ id: 'model', category: 'model' })
|
||||
expect(api!.snapshot[0]!.kind).toMatchObject({ type: 'select' })
|
||||
})
|
||||
|
||||
it('returns an empty snapshot for agents without a catalog', () => {
|
||||
mount({ agent: 'amp' })
|
||||
expect(api!.snapshot).toEqual([])
|
||||
})
|
||||
|
||||
it('does not expose catalog-backed agents outside the Claude and Codex scope', () => {
|
||||
mount({ agent: 'gemini' })
|
||||
expect(api!.snapshot).toEqual([])
|
||||
})
|
||||
|
||||
it('shows an OMP session under the model its hook reported', () => {
|
||||
mount({ agent: 'omp' })
|
||||
expect(api!.snapshot).toEqual([])
|
||||
|
||||
@@ -3,7 +3,7 @@ import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import { markRpcDeliveryUnknown } from '../transport/rpc-delivery-ambiguity'
|
||||
import { MOBILE_NATIVE_CHAT_SEND_TIMEOUT_MS } from './native-chat/mobile-native-chat-send'
|
||||
|
||||
import { useMobileNativeChatStop } from './use-mobile-native-chat-stop'
|
||||
|
||||
const reportWorkerTerminalUserInput = vi.fn()
|
||||
@@ -157,21 +157,6 @@ describe('useMobileNativeChatStop', () => {
|
||||
expect(onSendError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('bounds the Escape on a reconnect wait instead of parking forever', async () => {
|
||||
await render(true, 'stream-1')
|
||||
|
||||
act(() => stop?.())
|
||||
|
||||
expect(sendRequest).toHaveBeenCalledWith(
|
||||
'terminal.send',
|
||||
expect.anything(),
|
||||
expect.objectContaining({
|
||||
timeoutMs: MOBILE_NATIVE_CHAT_SEND_TIMEOUT_MS,
|
||||
budgetSpansConnect: true
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('suppresses an older Stop verdict after a newer Stop succeeds', async () => {
|
||||
let rejectFirst!: (error: Error) => void
|
||||
const first = new Promise((_, reject) => {
|
||||
|
||||
@@ -534,21 +534,6 @@ describe('mobile structured send actions', () => {
|
||||
expect(new Set(sentIds()).size).toBe(2)
|
||||
})
|
||||
|
||||
it('never reads an old phone journal, even when storage is full or unreadable', async () => {
|
||||
asyncStorage.getItem.mockRejectedValue(new Error('unreadable old journal'))
|
||||
asyncStorage.setItem.mockRejectedValue(new Error('disk full'))
|
||||
sendRequest.mockImplementation(async (method) =>
|
||||
method === 'agentSession.send' ? sendResult('accepted') : ok({ models: [], current: {} })
|
||||
)
|
||||
await mountSession()
|
||||
await act(async () => {
|
||||
expect(await hook!.sendWithOutcome('new action')).toBe('accepted')
|
||||
})
|
||||
expect(asyncStorage.getItem).not.toHaveBeenCalled()
|
||||
expect(asyncStorage.setItem).not.toHaveBeenCalled()
|
||||
expect(onSendError).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('sends nothing once the action budget expires', async () => {
|
||||
await mountSession()
|
||||
await act(async () => {
|
||||
|
||||
@@ -1,18 +1,6 @@
|
||||
import { readdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { C5_PAGE_CLOSURE } from './c5-page-closure'
|
||||
import {
|
||||
pageClosureDrift,
|
||||
pageClosureExclusions,
|
||||
pageClosureTotals,
|
||||
type PageClosureObservation
|
||||
} from './page-closure'
|
||||
import { C1_PAGE_CLOSURE } from './c1-page-closure'
|
||||
import { BRIDGED_PARITY_EXCLUSIONS } from './divergence-classes'
|
||||
import { readGolden } from '../rpc-recording/golden-recording'
|
||||
|
||||
const GOLDENS = resolve(import.meta.dirname, '../../../rpc-foundation/goldens')
|
||||
import { pageClosureDrift, type PageClosureObservation } from './page-closure'
|
||||
|
||||
function asPinned(): Map<string, PageClosureObservation> {
|
||||
const run = new Map<string, PageClosureObservation>()
|
||||
@@ -24,89 +12,7 @@ function asPinned(): Map<string, PageClosureObservation> {
|
||||
return run
|
||||
}
|
||||
|
||||
describe('the C5 page closure', () => {
|
||||
it('assigns each pinned golden to one closure family', () => {
|
||||
const goldens = Object.values(C5_PAGE_CLOSURE).flatMap((family) => Object.keys(family))
|
||||
expect(new Set(goldens).size).toBe(goldens.length)
|
||||
})
|
||||
|
||||
// Assert class totals beside per-id pins; a self-consistent misderived table can satisfy each pin while losing wrapped entries.
|
||||
it('pins how many goldens land in each class, which a per-id walk cannot see move', () => {
|
||||
expect(pageClosureTotals(C5_PAGE_CLOSURE)).toEqual({
|
||||
identical: 61,
|
||||
'result-absent-settlement': 49,
|
||||
'params-undefined': 7,
|
||||
'result-absent-stream-release': 3,
|
||||
'write-ordinal': 7
|
||||
})
|
||||
})
|
||||
|
||||
it("inherits C1's families whole, with the verdicts C1 committed", () => {
|
||||
// C5 inlines its pins, so compare imported C1 directly; composed spread tables need source parsing to detect overwritten inherited entries.
|
||||
for (const [family, pinned] of Object.entries(C1_PAGE_CLOSURE)) {
|
||||
expect(C5_PAGE_CLOSURE[family], family).toEqual(pinned)
|
||||
}
|
||||
expect(Object.keys(C1_PAGE_CLOSURE).length).toBe(20)
|
||||
})
|
||||
|
||||
it('adds five families and nothing else, all of them AI Vault', () => {
|
||||
const added = Object.keys(C5_PAGE_CLOSURE).filter((family) => !(family in C1_PAGE_CLOSURE))
|
||||
expect(added.sort()).toEqual([
|
||||
'aiVault.history',
|
||||
'aiVault.history-screen',
|
||||
'aiVault.resume-launch',
|
||||
'aiVault.resume-preparation',
|
||||
'settings.resume-metadata'
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps byte-identical controls outside History and strict ordinal controls within it', () => {
|
||||
const added = Object.entries(C5_PAGE_CLOSURE).filter(([family]) => !(family in C1_PAGE_CLOSURE))
|
||||
const wholly = added
|
||||
.filter(([, pins]) => !Object.values(pins).includes('identical'))
|
||||
.map(([family]) => family)
|
||||
expect(wholly.sort()).toEqual(['aiVault.history', 'aiVault.history-screen'])
|
||||
for (const family of wholly) {
|
||||
expect(Object.values(C5_PAGE_CLOSURE[family] ?? {})).toContain('write-ordinal')
|
||||
}
|
||||
})
|
||||
|
||||
it('pins goldens that exist, in the family the corpus records them under', () => {
|
||||
for (const [family, goldens] of Object.entries(C5_PAGE_CLOSURE)) {
|
||||
for (const id of Object.keys(goldens)) {
|
||||
// Use the corpus decoder instead of inventing another golden-shape check.
|
||||
const recorded = readGolden(GOLDENS, id)
|
||||
expect({ id, family: recorded.family }).toEqual({ id, family })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('claims no golden the corpus does not have', () => {
|
||||
const corpus = new Set(
|
||||
readdirSync(GOLDENS)
|
||||
.filter((name) => name.endsWith('.json'))
|
||||
.map((name) => name.replace(/\.json$/, ''))
|
||||
)
|
||||
const missing = Object.values(C5_PAGE_CLOSURE)
|
||||
.flatMap((family) => Object.keys(family))
|
||||
.filter((id) => !corpus.has(id))
|
||||
expect(missing).toEqual([])
|
||||
})
|
||||
|
||||
it('excludes a closure golden only into a class that has a reason', () => {
|
||||
const exclusions = pageClosureExclusions(C5_PAGE_CLOSURE)
|
||||
expect(exclusions.length).toBe(66)
|
||||
expect(exclusions.filter(([, name]) => BRIDGED_PARITY_EXCLUSIONS[name] === undefined)).toEqual(
|
||||
[]
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('reading a run against the C5 pin', () => {
|
||||
it('says nothing when the run is the pin', () => {
|
||||
expect(pageClosureDrift(C5_PAGE_CLOSURE, asPinned())).toEqual([])
|
||||
})
|
||||
|
||||
it('names a closure golden that changed verdict', () => {
|
||||
const run = asPinned()
|
||||
const found = [...run].find(([, seen]) => seen.verdict !== 'params-undefined')
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { readdirSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
bridgedParityMembershipDrift,
|
||||
bridgedParityTallyDrift,
|
||||
classifyBridgedParity,
|
||||
BRIDGED_PARITY_BASELINE,
|
||||
BRIDGED_PARITY_EXCLUSIONS,
|
||||
BRIDGED_PARITY_MEMBERS,
|
||||
BRIDGED_PARITY_NAMEABLE,
|
||||
type BridgedParityClass,
|
||||
type BridgedParityEvidence,
|
||||
type BridgedParityTally
|
||||
@@ -142,35 +138,6 @@ describe('what the pin still admits', () => {
|
||||
(name): name is BridgedParityClass => name !== 'identical'
|
||||
)
|
||||
|
||||
it('gives every class it still counts a reason, and every closed one none', () => {
|
||||
const reasoned = classes.filter((name) => BRIDGED_PARITY_EXCLUSIONS[name] !== undefined)
|
||||
const counted = classes.filter((name) => BRIDGED_PARITY_BASELINE[name] > 0)
|
||||
expect([...reasoned].sort()).toEqual([...counted].sort())
|
||||
})
|
||||
|
||||
it('accounts for every golden in the corpus, once each', () => {
|
||||
// Pin the class sum to corpus size so a partial run cannot satisfy per-class expectations.
|
||||
const goldens = readdirSync(resolve(import.meta.dirname, '../../../rpc-foundation/goldens'))
|
||||
const counted = Object.values(BRIDGED_PARITY_BASELINE).reduce((sum, count) => sum + count, 0)
|
||||
expect({ counted }).toEqual({
|
||||
counted: goldens.filter((name) => name.endsWith('.json')).length
|
||||
})
|
||||
})
|
||||
|
||||
it('names the goldens in a class small enough to name, and as many as it counts', () => {
|
||||
const nameable = classes.filter(
|
||||
(name) =>
|
||||
BRIDGED_PARITY_BASELINE[name] > 0 &&
|
||||
BRIDGED_PARITY_BASELINE[name] <= BRIDGED_PARITY_NAMEABLE
|
||||
)
|
||||
expect(nameable.length).toBeGreaterThan(0)
|
||||
for (const name of nameable) {
|
||||
const pinned = BRIDGED_PARITY_MEMBERS[name] ?? []
|
||||
expect({ [name]: pinned.length }).toEqual({ [name]: BRIDGED_PARITY_BASELINE[name] })
|
||||
expect({ [name]: new Set(pinned).size }).toEqual({ [name]: pinned.length })
|
||||
}
|
||||
})
|
||||
|
||||
function asPinned(): Map<string, readonly string[]> {
|
||||
const run = new Map<string, readonly string[]>()
|
||||
for (const name of classes) {
|
||||
@@ -216,10 +183,6 @@ describe('what the pin still admits', () => {
|
||||
return { identical: BRIDGED_PARITY_BASELINE.identical, counts }
|
||||
}
|
||||
|
||||
it('says nothing about the run the numbers were taken from', () => {
|
||||
expect(bridgedParityTallyDrift(asCounted())).toEqual([])
|
||||
})
|
||||
|
||||
it('goes red on a golden that stopped diverging, which every other check lets through', () => {
|
||||
// Excluded-to-identical moves leave broad parity gates satisfied; exact tallies must detect that direction too.
|
||||
const tally = asCounted()
|
||||
@@ -250,9 +213,4 @@ describe('what the pin still admits', () => {
|
||||
`identical: pinned ${tally.identical}, ran ${tally.identical - 1}`
|
||||
])
|
||||
})
|
||||
|
||||
it('leaves nothing for the reader to close: the `_meta` class is zero', () => {
|
||||
expect(BRIDGED_PARITY_BASELINE['reply-meta-required']).toBe(0)
|
||||
expect(BRIDGED_PARITY_EXCLUSIONS['reply-meta-required']).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -366,10 +366,6 @@ describe('the fields that diverged', () => {
|
||||
})
|
||||
).toEqual(['checkpoints'])
|
||||
})
|
||||
|
||||
it('is empty when the two agree', () => {
|
||||
expect(divergingFields(recording({}), recording({}))).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('reading the scenario', () => {
|
||||
|
||||
@@ -1,32 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseClaudeUsageRecord } from './transcript-record-parser'
|
||||
|
||||
function assistantLine(overrides: Record<string, unknown> = {}): string {
|
||||
return JSON.stringify({
|
||||
type: 'assistant',
|
||||
sessionId: 'session-1',
|
||||
timestamp: '2026-01-01T00:00:00.000Z',
|
||||
message: { usage: { input_tokens: 3, output_tokens: 5 } },
|
||||
...overrides
|
||||
})
|
||||
}
|
||||
|
||||
describe('assistant-record prefilter', () => {
|
||||
it('still parses an ordinary assistant record', () => {
|
||||
expect(parseClaudeUsageRecord(assistantLine())?.inputTokens).toBe(3)
|
||||
})
|
||||
|
||||
it('rejects a user record that never mentions assistant', () => {
|
||||
const userLine = JSON.stringify({
|
||||
type: 'user',
|
||||
sessionId: 'session-1',
|
||||
timestamp: '2026-01-01T00:00:00.000Z',
|
||||
message: { content: 'x'.repeat(200) }
|
||||
})
|
||||
|
||||
expect(parseClaudeUsageRecord(userLine)).toBeNull()
|
||||
})
|
||||
|
||||
it('rejects a non-assistant record that happens to contain the word assistant', () => {
|
||||
const userLine = JSON.stringify({
|
||||
type: 'user',
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { computerProviderUnavailableMessage } from './computer-provider-unavailable-message'
|
||||
|
||||
describe('computerProviderUnavailableMessage', () => {
|
||||
it('gives macOS developers the helper build and restart step', () => {
|
||||
expect(computerProviderUnavailableMessage('darwin')).toContain(
|
||||
'run pnpm build:computer-macos and restart Orca from this worktree'
|
||||
)
|
||||
expect(computerProviderUnavailableMessage('darwin')).toContain(
|
||||
'Orca Computer Use.app was not found or this macOS version is unsupported'
|
||||
)
|
||||
})
|
||||
|
||||
it('names the runtime file a Linux or Windows install is missing', () => {
|
||||
expect(computerProviderUnavailableMessage('linux')).toContain('computer-use-linux/runtime.py')
|
||||
expect(computerProviderUnavailableMessage('win32')).toContain(
|
||||
'computer-use-windows/runtime.ps1'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps unsupported platforms explicit', () => {
|
||||
expect(computerProviderUnavailableMessage('freebsd')).toBe(
|
||||
'computer-use has no native provider for freebsd'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -2,10 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import { runCoalescedDaemonRestart, settledDaemonRestart } from './daemon-restart-state'
|
||||
|
||||
describe('settledDaemonRestart', () => {
|
||||
it('is undefined with no restart running', () => {
|
||||
expect(settledDaemonRestart()).toBeUndefined()
|
||||
})
|
||||
|
||||
it('holds until the running restart settles, whether it succeeds or fails', async () => {
|
||||
for (const outcome of ['resolve', 'reject'] as const) {
|
||||
let finish!: () => void
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { setBadgeMock } = vi.hoisted(() => ({
|
||||
setBadgeMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
app: {
|
||||
dock: {
|
||||
setBadge: setBadgeMock
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
describe('unread Dock badge', () => {
|
||||
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
|
||||
|
||||
afterEach(() => {
|
||||
if (originalPlatform) {
|
||||
Object.defineProperty(process, 'platform', originalPlatform)
|
||||
}
|
||||
setBadgeMock.mockReset()
|
||||
vi.resetModules()
|
||||
})
|
||||
|
||||
it('clears the native badge when unread count is zero', async () => {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' })
|
||||
const { setUnreadDockBadgeCount } = await import('./unread-badge')
|
||||
|
||||
setUnreadDockBadgeCount(5)
|
||||
expect(setBadgeMock).toHaveBeenLastCalledWith('5')
|
||||
|
||||
setUnreadDockBadgeCount(0)
|
||||
expect(setBadgeMock).toHaveBeenLastCalledWith('')
|
||||
})
|
||||
|
||||
it('caps unread counts', async () => {
|
||||
Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' })
|
||||
const { setUnreadDockBadgeCount } = await import('./unread-badge')
|
||||
|
||||
setUnreadDockBadgeCount(104)
|
||||
expect(setBadgeMock).toHaveBeenLastCalledWith('99+')
|
||||
})
|
||||
})
|
||||
@@ -17,10 +17,6 @@ describe('gitCommandTimeoutMs', () => {
|
||||
expect(gitCommandTimeoutMs(['status'], 7)).toBe(7)
|
||||
expect(gitCommandTimeoutMs(['fetch'], 7)).toBe(7)
|
||||
})
|
||||
|
||||
it('provides a deterministic read-timeout seam', () => {
|
||||
expect(gitCommandTimeoutMs(['status'], undefined, 25)).toBe(25)
|
||||
})
|
||||
})
|
||||
|
||||
describe('GitCommandTimeoutError', () => {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { resolveHangWatchdogWorkerPath } from './hang-watchdog-worker-path'
|
||||
|
||||
describe('resolveHangWatchdogWorkerPath', () => {
|
||||
it('resolves packaged workers inside app.asar', () => {
|
||||
const appPath = join('/apps', 'orca', 'app.asar')
|
||||
expect(resolveHangWatchdogWorkerPath(appPath, true)).toBe(
|
||||
join(appPath, 'out', 'main', 'main-thread-hang-watchdog-entry.js')
|
||||
)
|
||||
})
|
||||
|
||||
it('uses an adjacent entry when the dev app path is out/main', () => {
|
||||
const appPath = join('/repo', 'out', 'main')
|
||||
const adjacent = join(appPath, 'main-thread-hang-watchdog-entry.js')
|
||||
const pathExists = vi.fn((candidate: string) => candidate === adjacent)
|
||||
expect(resolveHangWatchdogWorkerPath(appPath, false, pathExists)).toBe(adjacent)
|
||||
})
|
||||
|
||||
it('resolves through out/main from a dev project root', () => {
|
||||
const appPath = join('/repo', 'orca')
|
||||
expect(resolveHangWatchdogWorkerPath(appPath, false, () => false)).toBe(
|
||||
join(appPath, 'out', 'main', 'main-thread-hang-watchdog-entry.js')
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -2,24 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import { eventSchemas } from '../../shared/telemetry-events'
|
||||
|
||||
describe('main_thread_hang_detected telemetry schema', () => {
|
||||
it('accepts an unresolved hang', () => {
|
||||
expect(
|
||||
eventSchemas.main_thread_hang_detected.safeParse({
|
||||
unresponsive_ms: 50_000,
|
||||
self_recovered: false
|
||||
}).success
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
it('accepts a self-recovered stall', () => {
|
||||
expect(
|
||||
eventSchemas.main_thread_hang_detected.safeParse({
|
||||
unresponsive_ms: 61_000,
|
||||
self_recovered: true
|
||||
}).success
|
||||
).toBe(true)
|
||||
})
|
||||
|
||||
// Strict schema excludes paths, PIDs and window titles from telemetry.
|
||||
it('rejects unknown fields and non-integer durations', () => {
|
||||
expect(
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { readdirSync, readFileSync } from 'node:fs'
|
||||
import { join, relative } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const MAIN_ROOT = join(__dirname, '..')
|
||||
// Route keychain access through the run-wide guard so withheld keychains cannot block the main thread.
|
||||
const ALLOWED = new Set(['host/safe-storage-availability.ts', 'host/electron-secret-store.ts'])
|
||||
|
||||
function listSources(dir: string): string[] {
|
||||
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
return listSources(path)
|
||||
}
|
||||
return entry.name.endsWith('.ts') && !entry.name.endsWith('.test.ts') ? [path] : []
|
||||
})
|
||||
}
|
||||
|
||||
describe('electron.safeStorage importers', () => {
|
||||
it('stay behind the keychain withhold', () => {
|
||||
const importers = listSources(MAIN_ROOT)
|
||||
.filter((path) =>
|
||||
/import\s*\{[^}]*\bsafeStorage\b[^}]*\}\s*from\s*'electron'/.test(
|
||||
readFileSync(path, 'utf8')
|
||||
)
|
||||
)
|
||||
.map((path) => relative(MAIN_ROOT, path).split('\\').join('/'))
|
||||
expect(importers.filter((path) => !ALLOWED.has(path))).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import emojiShortcodes from 'emojibase-data/en/shortcodes/emojibase.json'
|
||||
import { requireEmojiShortcodeDataset } from './deferred-emoji-shortcode-dataset'
|
||||
|
||||
// Keep main imports outside shared tsconfig projects; the lazy catalog test enforces that boundary.
|
||||
describe('deferred emoji shortcode dataset', () => {
|
||||
it('loads the main-side dataset synchronously into an identical catalog', async () => {
|
||||
vi.resetModules()
|
||||
const eager = await import('../../shared/emoji-shortcode-catalog.js')
|
||||
eager.setEmojiShortcodeDatasetLoader(() => emojiShortcodes)
|
||||
const eagerEntries = eager.getStandardEmojiShortcodeEntries()
|
||||
const eagerTransform = eager.replaceKnownEmojiWithShortcodes('ship \u{1F389} \u{1F44D}')
|
||||
|
||||
vi.resetModules()
|
||||
const deferred = await import('../../shared/emoji-shortcode-catalog.js')
|
||||
deferred.setEmojiShortcodeDatasetLoader(requireEmojiShortcodeDataset)
|
||||
|
||||
// Immediate use must work without awaiting an async import.
|
||||
expect(deferred.getStandardEmojiShortcodeEntries()).toEqual(eagerEntries)
|
||||
expect(deferred.replaceKnownEmojiWithShortcodes('ship \u{1F389} \u{1F44D}')).toBe(
|
||||
eagerTransform
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getTuiAgentDetectionProbeCommands,
|
||||
KNOWN_TUI_AGENT_DETECTION_COMMANDS,
|
||||
resolveDetectedTuiAgentIds
|
||||
} from '../../shared/tui-agent-detection-commands'
|
||||
|
||||
describe('Freebuff detection', () => {
|
||||
it.each(['darwin', 'linux', 'win32', 'wsl'] as const)(
|
||||
'detects Freebuff independently of Codebuff on %s',
|
||||
(runtime) => {
|
||||
expect(
|
||||
getTuiAgentDetectionProbeCommands(KNOWN_TUI_AGENT_DETECTION_COMMANDS, runtime)
|
||||
).toContain('freebuff')
|
||||
expect(
|
||||
resolveDetectedTuiAgentIds(
|
||||
KNOWN_TUI_AGENT_DETECTION_COMMANDS,
|
||||
new Set(['freebuff']),
|
||||
runtime
|
||||
)
|
||||
).toEqual(['freebuff'])
|
||||
expect(
|
||||
resolveDetectedTuiAgentIds(
|
||||
KNOWN_TUI_AGENT_DETECTION_COMMANDS,
|
||||
new Set(['codebuff']),
|
||||
runtime
|
||||
)
|
||||
).toEqual(['codebuff'])
|
||||
expect(
|
||||
resolveDetectedTuiAgentIds(
|
||||
KNOWN_TUI_AGENT_DETECTION_COMMANDS,
|
||||
new Set(['freebuff', 'codebuff']),
|
||||
runtime
|
||||
)
|
||||
).toEqual(expect.arrayContaining(['freebuff', 'codebuff']))
|
||||
}
|
||||
)
|
||||
})
|
||||
-36
@@ -1,36 +0,0 @@
|
||||
import { afterEach, expect, it } from 'vitest'
|
||||
import { setActiveSink } from '../../observability/tracer'
|
||||
import { sharedControlEnvironmentPolicy } from './runtime-environment-shared-control-policy'
|
||||
|
||||
afterEach(() => setActiveSink(null))
|
||||
|
||||
it('traces each control socket close for the support bundle', () => {
|
||||
const records: unknown[] = []
|
||||
setActiveSink({ push: (record) => records.push(record), flush() {}, close() {} })
|
||||
|
||||
sharedControlEnvironmentPolicy('env-1').onSocketClosed?.({
|
||||
cause: 'keepalive_timeout',
|
||||
errorCode: 'remote_runtime_unavailable',
|
||||
closeCode: null,
|
||||
state: 'ready',
|
||||
lastRttMs: 320,
|
||||
socketUptimeMs: 60_000,
|
||||
readyUptimeMs: 59_000,
|
||||
reconnectAttempt: 0,
|
||||
pendingRequestCount: 1,
|
||||
subscriptionCount: 4
|
||||
})
|
||||
|
||||
expect(records).toEqual([
|
||||
expect.objectContaining({
|
||||
type: 'effect-span',
|
||||
name: 'remote-runtime.control-socket-close',
|
||||
attributes: expect.objectContaining({
|
||||
environmentId: 'env-1',
|
||||
cause: 'keepalive_timeout',
|
||||
lastRttMs: 320,
|
||||
socketUptimeMs: 60_000
|
||||
})
|
||||
})
|
||||
])
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const handlers = vi.hoisted(() => new Map<string, () => unknown>())
|
||||
const retrySharedControl = vi.hoisted(() => vi.fn())
|
||||
const retrySsh = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
removeHandler: (channel: string) => handlers.delete(channel),
|
||||
handle: (channel: string, handler: () => unknown) => handlers.set(channel, handler)
|
||||
}
|
||||
}))
|
||||
vi.mock('./runtime-environment-request-connections', () => ({
|
||||
retryRemoteRuntimeSharedControlConnectionsNow: retrySharedControl
|
||||
}))
|
||||
vi.mock('../ssh/ssh-host-sleep-reconnect', () => ({
|
||||
retrySshConnectionsAfterNetworkReturn: retrySsh
|
||||
}))
|
||||
|
||||
import { registerRuntimeEnvironmentRecoveryHandler } from './runtime-environment-recovery-handler'
|
||||
|
||||
describe('runtimeEnvironments:retryConnectionsNow', () => {
|
||||
it('retries SSH transports as well as shared control when the window reports the network back', () => {
|
||||
registerRuntimeEnvironmentRecoveryHandler()
|
||||
handlers.get('runtimeEnvironments:retryConnectionsNow')?.()
|
||||
expect(retrySharedControl).toHaveBeenCalledTimes(1)
|
||||
expect(retrySsh).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import type { IpcMainInvokeEvent } from 'electron'
|
||||
import { sendBundleInstallProgress, sendSkillInstallProgress } from './skill-install-progress-ipc'
|
||||
import { sendSkillInstallProgress } from './skill-install-progress-ipc'
|
||||
|
||||
function event(destroyed = false): IpcMainInvokeEvent {
|
||||
return {
|
||||
@@ -9,23 +9,6 @@ function event(destroyed = false): IpcMainInvokeEvent {
|
||||
}
|
||||
|
||||
describe('skill install progress IPC', () => {
|
||||
it('projects destination-owned bundle progress without paths or grants', () => {
|
||||
const target = event()
|
||||
sendBundleInstallProgress(target, {
|
||||
operationId: 'operation_1',
|
||||
skillId: 'alpha',
|
||||
skillName: 'alpha',
|
||||
skillIndex: 2,
|
||||
skillCount: 30
|
||||
})
|
||||
|
||||
expect(target.sender.send).toHaveBeenCalledWith('skills:installProgress', {
|
||||
operationId: 'operation_1',
|
||||
phase: 'installing',
|
||||
currentSkill: { id: 'alpha', name: 'alpha', index: 2, total: 30 }
|
||||
})
|
||||
})
|
||||
|
||||
it('does not publish after the invoking renderer is destroyed', () => {
|
||||
const target = event(true)
|
||||
sendSkillInstallProgress(target, { operationId: 'operation_1', phase: 'authorizing' })
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getWorktreeWatcherRemoval, setWorktreeWatcherRemoval } from './worktree-watcher-removal'
|
||||
|
||||
/** The renderer-less default stays inert; installed ports route method calls. */
|
||||
describe('WorktreeWatcherRemoval port', () => {
|
||||
const METHODS = ['closeLocal', 'restoreLocal', 'forgetLocal'] as const
|
||||
|
||||
it('defaults to inert so a renderer-less host is honest, not broken', async () => {
|
||||
setWorktreeWatcherRemoval(null)
|
||||
const inert = getWorktreeWatcherRemoval()
|
||||
for (const method of METHODS) {
|
||||
await expect(
|
||||
Promise.resolve(inert[method]('repo::/tmp/w', '/tmp/w' as never))
|
||||
).resolves.not.toThrow()
|
||||
}
|
||||
})
|
||||
|
||||
it('routes every method to the installed binding', async () => {
|
||||
const calls: string[] = []
|
||||
setWorktreeWatcherRemoval({
|
||||
closeLocal: async () => void calls.push('closeLocal'),
|
||||
restoreLocal: async () => void calls.push('restoreLocal'),
|
||||
forgetLocal: () => void calls.push('forgetLocal')
|
||||
})
|
||||
for (const method of METHODS) {
|
||||
await getWorktreeWatcherRemoval()[method]('conn', '/tmp/w' as never)
|
||||
}
|
||||
expect(calls).toEqual([...METHODS])
|
||||
setWorktreeWatcherRemoval(null)
|
||||
})
|
||||
})
|
||||
@@ -1,11 +1,6 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { jsonBytes, shortenJsonStringsToFit } from './json-string-shortening'
|
||||
|
||||
it('returns the value itself when it already fits', () => {
|
||||
const value = { text: 'short' }
|
||||
expect(shortenJsonStringsToFit(value, 1_000)).toBe(value)
|
||||
})
|
||||
|
||||
it('cuts long strings to fit and marks them, never splitting a surrogate pair', () => {
|
||||
const value = { text: '😀'.repeat(2_000), other: 'x'.repeat(4_000) }
|
||||
const fitted = shortenJsonStringsToFit(value, 2_000)
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { refreshV1, refreshV2 } = vi.hoisted(() => ({
|
||||
refreshV1: vi.fn<() => void>(),
|
||||
refreshV2: vi.fn<() => void>()
|
||||
}))
|
||||
vi.mock('./hook-service', () => ({
|
||||
openCodeHookService: { refreshInstalledPlugins: refreshV1 },
|
||||
openCode2HookService: { refreshInstalledPlugins: refreshV2 }
|
||||
}))
|
||||
|
||||
import { refreshInstalledOpenCodeStatusPlugins } from './opencode-status-plugin-startup-refresh'
|
||||
|
||||
describe('refreshInstalledOpenCodeStatusPlugins', () => {
|
||||
beforeEach(() => {
|
||||
refreshV1.mockClear()
|
||||
refreshV2.mockClear()
|
||||
})
|
||||
|
||||
it('refreshes both variants by default', () => {
|
||||
refreshInstalledOpenCodeStatusPlugins(null)
|
||||
expect(refreshV1).toHaveBeenCalledTimes(1)
|
||||
expect(refreshV2).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('touches nothing while agent status hooks are off', () => {
|
||||
refreshInstalledOpenCodeStatusPlugins({ agentStatusHooksEnabled: false })
|
||||
expect(refreshV1).not.toHaveBeenCalled()
|
||||
expect(refreshV2).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('skips a disabled variant only', () => {
|
||||
refreshInstalledOpenCodeStatusPlugins({ disabledTuiAgents: ['opencode2'] })
|
||||
expect(refreshV1).toHaveBeenCalledTimes(1)
|
||||
expect(refreshV2).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -15,13 +15,4 @@ describe('resolveProviderChildEnv', () => {
|
||||
expect(baseEnv).toEqual({ PATH: '/bin', AGENT_HOME: '/inherited', PARENT_AGENT: 'parent' })
|
||||
expect(overlay.PARENT_AGENT).toBe('overlay')
|
||||
})
|
||||
|
||||
it('copies the inherited env when the launch carries no env', () => {
|
||||
const baseEnv = { PATH: '/bin' }
|
||||
|
||||
const childEnv = resolveProviderChildEnv({}, baseEnv)
|
||||
|
||||
expect(childEnv).toEqual(baseEnv)
|
||||
expect(childEnv).not.toBe(baseEnv)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,27 +1,7 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
installWindowsPathRegistryChangeListener,
|
||||
WINDOWS_SETTING_CHANGE_MESSAGE
|
||||
} from './windows-path-registry-change'
|
||||
import { installWindowsPathRegistryChangeListener } from './windows-path-registry-change'
|
||||
|
||||
describe('installWindowsPathRegistryChangeListener', () => {
|
||||
it('invalidates the persisted PATH cache on a Windows settings change', () => {
|
||||
const hooks = new Map<number, (wParam: Buffer, lParam: Buffer) => void>()
|
||||
const invalidate = vi.fn()
|
||||
installWindowsPathRegistryChangeListener(
|
||||
{
|
||||
hookWindowMessage: (message, callback) => {
|
||||
hooks.set(message, callback)
|
||||
}
|
||||
},
|
||||
{ invalidate, platform: 'win32' }
|
||||
)
|
||||
|
||||
hooks.get(WINDOWS_SETTING_CHANGE_MESSAGE)?.(Buffer.alloc(0), Buffer.alloc(0))
|
||||
|
||||
expect(invalidate).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('does not hook native messages outside Windows', () => {
|
||||
const hookWindowMessage = vi.fn()
|
||||
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { spawnProcess } from '@orca/process-host'
|
||||
import { stopBundledRipgrep } from './bundled-ripgrep-stop'
|
||||
|
||||
const { signalTree, killChild } = vi.hoisted(() => ({ signalTree: vi.fn(), killChild: vi.fn() }))
|
||||
vi.mock('@orca/process-host/process-tree-termination', () => ({
|
||||
signalProcessTree: signalTree
|
||||
}))
|
||||
vi.mock('../../shared/ripgrep-process-availability', () => ({
|
||||
killSpawnedRipgrepProcess: killChild
|
||||
}))
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('bundled search process termination', () => {
|
||||
it('uses the existing bounded tree terminator for Windows WSL and coalesces duplicate stops', async () => {
|
||||
const child = spawnProcess({
|
||||
program: process.execPath,
|
||||
args: ['-e', 'setInterval(() => {}, 1000)']
|
||||
})
|
||||
const exited = new Promise((resolve) => child.once('close', resolve))
|
||||
try {
|
||||
vi.spyOn(process, 'platform', 'get').mockReturnValue('win32')
|
||||
signalTree.mockResolvedValue(true)
|
||||
stopBundledRipgrep(child, true)
|
||||
stopBundledRipgrep(child, true)
|
||||
expect(signalTree).toHaveBeenCalledExactlyOnceWith(child)
|
||||
expect(killChild).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
child.kill()
|
||||
await exited
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,24 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ALL_RPC_METHODS } from '../rpc/methods'
|
||||
import { MOBILE_RPC_METHOD_ALLOWLIST } from '../runtime-rpc/runtime-rpc-mobile-method-allowlist'
|
||||
import { MOBILE_RPC_METHOD_ROUTES } from '../runtime-rpc/runtime-rpc-mobile-method-routing'
|
||||
import { RELAYED_STREAM_CARRIERS } from './relayed-stream-carriers'
|
||||
|
||||
describe('relayed stream carrier census', () => {
|
||||
const relayableStreams: string[] = ALL_RPC_METHODS.filter(
|
||||
(method) =>
|
||||
'stream' in method &&
|
||||
MOBILE_RPC_METHOD_ALLOWLIST.has(method.name) &&
|
||||
MOBILE_RPC_METHOD_ROUTES.get(method.name) === 'execution-host'
|
||||
).map((method) => method.name)
|
||||
|
||||
it('classifies how every relayable stream names itself', () => {
|
||||
expect(relayableStreams.filter((name) => !RELAYED_STREAM_CARRIERS.has(name))).toEqual([])
|
||||
})
|
||||
|
||||
it('classifies nothing that is not a relayable stream', () => {
|
||||
expect(
|
||||
[...RELAYED_STREAM_CARRIERS.keys()].filter((name) => !relayableStreams.includes(name))
|
||||
).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
/** terminal-stream-id renumbers result/header ids; none carries no id (one phone screencast at a time).
|
||||
* Refuse phone-binary-frames because the relay cannot forward their input; census tests cover new streams. */
|
||||
* Refuse phone-binary-frames because the relay cannot forward their input. */
|
||||
export type RelayedStreamCarrier = 'terminal-stream-id' | 'none' | 'phone-binary-frames'
|
||||
|
||||
export const RELAYED_STREAM_CARRIERS: ReadonlyMap<string, RelayedStreamCarrier> = new Map([
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { computeTerminalTailWaitState } from './terminal-wait-tail-state'
|
||||
|
||||
describe('terminal tail wait state', () => {
|
||||
@@ -19,19 +19,4 @@ describe('terminal tail wait state', () => {
|
||||
expect(blocked.fromTail).toBe(true)
|
||||
expect(blocked.signal?.reason).toBe('agent-update-prompt')
|
||||
})
|
||||
|
||||
it('does not rebuild or repeatedly scan an ordinary saturated tail', () => {
|
||||
const lines = Array.from({ length: 2000 }, () => 'x'.repeat(126))
|
||||
const lastIndexOf = vi.spyOn(String.prototype, 'lastIndexOf')
|
||||
|
||||
try {
|
||||
const state = computeTerminalTailWaitState(lines, '', '')
|
||||
|
||||
expect(state.signal).toBeNull()
|
||||
expect(state.waitText).toBe('')
|
||||
expect(lastIndexOf).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
lastIndexOf.mockRestore()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import '../unused-default-rpc-methods.test-fixture'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { RpcDispatcher } from '../dispatcher'
|
||||
import type { RpcRequest } from '../core'
|
||||
import type { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import { REPO_METHODS } from './repo'
|
||||
|
||||
function makeRequest(method: string, params?: unknown): RpcRequest {
|
||||
return { id: 'req-1', authToken: 'tok', method, params }
|
||||
}
|
||||
|
||||
describe('repo badge color RPC updates', () => {
|
||||
it('normalizes repo badge colors before runtime updates', async () => {
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'test-runtime',
|
||||
updateRepo: vi.fn().mockResolvedValue({
|
||||
id: 'repo-1',
|
||||
path: '/srv/repo',
|
||||
badgeColor: '#abcdef'
|
||||
})
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: REPO_METHODS })
|
||||
|
||||
const response = await dispatcher.dispatch(
|
||||
makeRequest('repo.update', {
|
||||
repo: 'repo-1',
|
||||
updates: { badgeColor: ' ABCDEF ' }
|
||||
})
|
||||
)
|
||||
|
||||
expect(runtime.updateRepo).toHaveBeenCalledWith('repo-1', {
|
||||
badgeColor: '#abcdef'
|
||||
})
|
||||
expect(response).toMatchObject({
|
||||
ok: true,
|
||||
result: { repo: { id: 'repo-1', badgeColor: '#abcdef' } }
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,17 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
EXECUTION_HOST_METHODS,
|
||||
MOBILE_RPC_METHOD_ROUTES
|
||||
} from './runtime-rpc-mobile-method-routing'
|
||||
import { PAIRED_DESKTOP_METHODS } from './runtime-rpc-mobile-paired-desktop-methods'
|
||||
import { MOBILE_RPC_METHOD_ROUTES } from './runtime-rpc-mobile-method-routing'
|
||||
|
||||
describe('mobile RPC method routing census', () => {
|
||||
it('tags every routed method exactly once', () => {
|
||||
expect(PAIRED_DESKTOP_METHODS.length + EXECUTION_HOST_METHODS.length).toBe(
|
||||
MOBILE_RPC_METHOD_ROUTES.size
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'jira.status',
|
||||
'jira.listIssues',
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { expect, it } from 'vitest'
|
||||
import { PI_RPC_RUNTIME_REGISTRATION } from '../pi/rpc-runtime-registration'
|
||||
import {
|
||||
STRUCTURED_AGENT_RUNTIME_METADATA,
|
||||
structuredAgentRuntimeMetadata
|
||||
} from './structured-agent-runtime-metadata'
|
||||
import {
|
||||
STRUCTURED_AGENT_RUNTIME_REGISTRATIONS,
|
||||
structuredAgentRuntimeRegistration
|
||||
} from './structured-agent-runtime-registrations'
|
||||
|
||||
it.each(
|
||||
STRUCTURED_AGENT_RUNTIME_METADATA.map(
|
||||
(metadata, index) => [metadata.definition.agent, metadata, index] as const
|
||||
)
|
||||
)(
|
||||
'keeps the metadata and synchronous factory on the same %s registration',
|
||||
(agent, metadata, index) => {
|
||||
const registration = STRUCTURED_AGENT_RUNTIME_REGISTRATIONS[index]
|
||||
expect(registration).toBe(metadata)
|
||||
expect(structuredAgentRuntimeRegistration(agent)).toBe(registration)
|
||||
expect(structuredAgentRuntimeMetadata(agent)).toBe(registration)
|
||||
expect(typeof registration.createAdapter).toBe('function')
|
||||
expect(structuredAgentRuntimeRegistration(agent)?.createAdapter).toBe(
|
||||
registration.createAdapter
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps the standalone Pi registration in the shared roster', () => {
|
||||
expect(structuredAgentRuntimeRegistration('pi')).toBe(PI_RPC_RUNTIME_REGISTRATION)
|
||||
})
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseContentRange, parseRetryAfterMs } from './speech-model-download-response'
|
||||
import { isRetryableDownloadError } from '../network/transient-download-error'
|
||||
|
||||
describe('speech model download response contracts', () => {
|
||||
it('accepts only internally consistent byte ranges', () => {
|
||||
@@ -15,12 +14,4 @@ describe('speech model download response contracts', () => {
|
||||
expect(parseRetryAfterMs(String(Number.MAX_SAFE_INTEGER))).toBeUndefined()
|
||||
expect(parseRetryAfterMs('invalid')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('retries only allowlisted HTTP and network failures', () => {
|
||||
expect(
|
||||
isRetryableDownloadError(Object.assign(new Error('HTTP 503'), { httpStatusCode: 503 }))
|
||||
).toBe(true)
|
||||
expect(isRetryableDownloadError(new Error('net::ERR_CONNECTION_RESET'))).toBe(true)
|
||||
expect(isRetryableDownloadError(new Error('certificate rejected'))).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -21,10 +21,4 @@ describe('node:sqlite reader admission', () => {
|
||||
expect(hasNodeSqliteReaderApi(sqlite)).toBe(admitted)
|
||||
expect(probe(sqlite)).toBe(admitted)
|
||||
})
|
||||
|
||||
it('admits the actual runtime module', () => {
|
||||
const sqlite: unknown = process.getBuiltinModule('node:sqlite')
|
||||
expect(hasNodeSqliteReaderApi(sqlite)).toBe(true)
|
||||
expect(probe(sqlite)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { ORCAD_WINDOWS_PROCESS_TREE_FILENAME } from '../../shared/orcad-artifacts'
|
||||
import { relayArtifactFilenames } from './relay-install-artifacts'
|
||||
|
||||
describe('relay install artifacts', () => {
|
||||
it('keeps the optional process-table addon out of the required set', () => {
|
||||
// A complete install never had to contain it: only a Windows build machine could emit it.
|
||||
expect(relayArtifactFilenames(true)).not.toContain(ORCAD_WINDOWS_PROCESS_TREE_FILENAME)
|
||||
expect(relayArtifactFilenames(false)).not.toContain(ORCAD_WINDOWS_PROCESS_TREE_FILENAME)
|
||||
})
|
||||
|
||||
it('requires the Windows-only patches only on Windows', () => {
|
||||
expect(relayArtifactFilenames(true)).toContain('relay.js')
|
||||
expect(relayArtifactFilenames(true)).toContain('node-pty-1.1.0-console-list-agent-patch.cjs')
|
||||
expect(relayArtifactFilenames(false)).not.toContain(
|
||||
'node-pty-1.1.0-console-list-agent-patch.cjs'
|
||||
)
|
||||
})
|
||||
|
||||
it('lists the pty-master cloexec patch on every platform', () => {
|
||||
for (const isWindows of [true, false]) {
|
||||
expect(relayArtifactFilenames(isWindows)).toContain('node-pty-1.1.0-master-cloexec-patch.cjs')
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { currentWorkerEntryLayout, resolveWorkerThreadEntryPath } from './worker-thread-entry-path'
|
||||
|
||||
describe('worker-thread entry path', () => {
|
||||
const outMain = join('/repo', 'out', 'main')
|
||||
|
||||
it('resolves an unpackaged caller bundled into a shared chunk beside the main entry', () => {
|
||||
const layout = currentWorkerEntryLayout(join(outMain, 'chunks'))
|
||||
expect(resolveWorkerThreadEntryPath({ ...layout, isPackaged: false }, 'entry.js')).toBe(
|
||||
join(outMain, 'entry.js')
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps an unpackaged caller already in the main entry folder', () => {
|
||||
const layout = currentWorkerEntryLayout(outMain)
|
||||
expect(resolveWorkerThreadEntryPath({ ...layout, isPackaged: false }, 'entry.js')).toBe(
|
||||
join(outMain, 'entry.js')
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves packaged builds from resources, whatever the caller chunk', () => {
|
||||
const layout = { isPackaged: true, resourcesPath: '/res', moduleDir: join(outMain, 'chunks') }
|
||||
expect(resolveWorkerThreadEntryPath(layout, 'entry.js')).toBe(
|
||||
join('/res', 'app.asar', 'out', 'main', 'entry.js')
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { WorkspaceSpaceWorktree } from '../shared/workspace-space-types'
|
||||
import { summarizeWorkspaceSpaceRows } from './workspace-space-repo-scan'
|
||||
|
||||
describe('summarizeWorkspaceSpaceRows', () => {
|
||||
it('reads each summary field once per row', () => {
|
||||
const reads = { status: 0, sizeBytes: 0, reclaimableBytes: 0 }
|
||||
const makeRow = (status: WorkspaceSpaceWorktree['status']) =>
|
||||
new Proxy({ status, sizeBytes: 10, reclaimableBytes: 4 } as WorkspaceSpaceWorktree, {
|
||||
get(target, property, receiver) {
|
||||
if (
|
||||
property === 'status' ||
|
||||
property === 'sizeBytes' ||
|
||||
property === 'reclaimableBytes'
|
||||
) {
|
||||
reads[property] += 1
|
||||
}
|
||||
// oxlint-disable-next-line anti-slop/no-reflect-get -- Proxy get trap default forward.
|
||||
return Reflect.get(target, property, receiver)
|
||||
}
|
||||
})
|
||||
|
||||
expect(summarizeWorkspaceSpaceRows([makeRow('ok'), makeRow('unavailable')])).toEqual({
|
||||
scannedWorktreeCount: 1,
|
||||
unavailableWorktreeCount: 1,
|
||||
totalSizeBytes: 20,
|
||||
reclaimableBytes: 8
|
||||
})
|
||||
expect(reads).toEqual({ status: 2, sizeBytes: 2, reclaimableBytes: 2 })
|
||||
})
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
import { existsSync } from 'node:fs'
|
||||
import { describe, expect, it, beforeEach } from 'vitest'
|
||||
import {
|
||||
readZCodeInteractiveCapability,
|
||||
_resetZCodeInteractiveCapabilityForTests
|
||||
} from './interactive-capability'
|
||||
|
||||
// Compare real bundled/no-TUI and CLI/TUI builds only where those local fixtures exist.
|
||||
const WITHOUT_TUI = '/tmp/zcode-bin-notui/zcode'
|
||||
const WITH_TUI = '/tmp/zcode-bin-tui/zcode'
|
||||
const bothPresent = existsSync(WITHOUT_TUI) && existsSync(WITH_TUI)
|
||||
const describeLive = bothPresent ? describe : describe.skip
|
||||
|
||||
beforeEach(() => _resetZCodeInteractiveCapabilityForTests())
|
||||
|
||||
describeLive('readZCodeInteractiveCapability against real ZCode builds', () => {
|
||||
it("reports missing-tui for the desktop app's bundled runtime", async () => {
|
||||
await expect(readZCodeInteractiveCapability(WITHOUT_TUI)).resolves.toBe('missing-tui')
|
||||
}, 30_000)
|
||||
|
||||
it('reports interactive for a CLI built from source', async () => {
|
||||
await expect(readZCodeInteractiveCapability(WITH_TUI)).resolves.toBe('interactive')
|
||||
}, 30_000)
|
||||
|
||||
it('fails open when the command does not exist', async () => {
|
||||
await expect(readZCodeInteractiveCapability('/tmp/definitely-not-zcode')).resolves.toBe(
|
||||
'unknown'
|
||||
)
|
||||
}, 30_000)
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
formatBrowserDownloadProgress,
|
||||
type BrowserDownloadState
|
||||
} from './browser-download-progress'
|
||||
|
||||
function download(overrides: Partial<BrowserDownloadState> = {}): BrowserDownloadState {
|
||||
return {
|
||||
downloadId: 'd1',
|
||||
browserPageId: 'p1',
|
||||
filename: 'file.bin',
|
||||
mimeType: 'application/octet-stream',
|
||||
origin: 'https://example.com',
|
||||
totalBytes: 2048,
|
||||
receivedBytes: 1024,
|
||||
status: 'downloading',
|
||||
savePath: null,
|
||||
error: null,
|
||||
progressState: 'progressing',
|
||||
completedAt: null,
|
||||
url: 'https://example.com/file.bin',
|
||||
...overrides
|
||||
} as BrowserDownloadState
|
||||
}
|
||||
|
||||
describe('formatBrowserDownloadProgress', () => {
|
||||
it('joins received and total when both are known', () => {
|
||||
expect(formatBrowserDownloadProgress(download())).toBe('1.0 KB / 2.0 KB')
|
||||
})
|
||||
|
||||
it('falls back to the single known side', () => {
|
||||
expect(formatBrowserDownloadProgress(download({ totalBytes: null }))).toBe('1.0 KB')
|
||||
expect(formatBrowserDownloadProgress(download({ receivedBytes: -1, totalBytes: 2048 }))).toBe(
|
||||
'2.0 KB'
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -19,10 +19,6 @@ function keyboardEvent(
|
||||
}
|
||||
|
||||
describe('remote browser keyboard serialization', () => {
|
||||
it('serializes modified letter shortcuts', () => {
|
||||
expect(getRemoteBrowserKeyboardShortcut(keyboardEvent({ ctrlKey: true }))).toBe('Control+r')
|
||||
})
|
||||
|
||||
it('preserves Shift on modified letter shortcuts', () => {
|
||||
expect(
|
||||
getRemoteBrowserKeyboardShortcut(keyboardEvent({ key: 'R', ctrlKey: true, shiftKey: true }))
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
getCommentReplyTargetCandidates,
|
||||
resolveCommentReplyTarget
|
||||
} from './comment-reply-target-state'
|
||||
|
||||
describe('comment reply target state', () => {
|
||||
it('preserves a visible reply target', () => {
|
||||
expect(resolveCommentReplyTarget(2, [{ id: 1 }, { id: 2 }])).toBe(2)
|
||||
})
|
||||
|
||||
it('clears a hidden reply target', () => {
|
||||
expect(resolveCommentReplyTarget(3, [{ id: 1 }, { id: 2 }])).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps an empty reply target empty', () => {
|
||||
expect(resolveCommentReplyTarget(null, [{ id: 1 }])).toBeNull()
|
||||
})
|
||||
|
||||
it('uses filtered comments as reply targets for PRs', () => {
|
||||
expect(getCommentReplyTargetCandidates('pr', [{ id: 1 }, { id: 2 }], [{ id: 2 }])).toEqual([
|
||||
{ id: 2 }
|
||||
])
|
||||
})
|
||||
|
||||
it('uses all comments as reply targets for issues', () => {
|
||||
expect(getCommentReplyTargetCandidates('issue', [{ id: 1 }, { id: 2 }], [{ id: 2 }])).toEqual([
|
||||
{ id: 1 },
|
||||
{ id: 2 }
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,40 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { toast } from 'sonner'
|
||||
import { submitDiffSectionComment } from './diff-section-comment-submit'
|
||||
import type { DiffSection } from './diff-section-types'
|
||||
|
||||
vi.mock('sonner', () => ({ toast: { error: vi.fn() } }))
|
||||
|
||||
const section: DiffSection = {
|
||||
key: 'section-1',
|
||||
path: 'src/example.ts',
|
||||
status: 'modified',
|
||||
originalContent: 'before',
|
||||
modifiedContent: 'after',
|
||||
collapsed: false,
|
||||
loading: false,
|
||||
dirty: false,
|
||||
diffResult: null,
|
||||
largeDiffRenderLimit: null
|
||||
}
|
||||
|
||||
describe('submitDiffSectionComment', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(toast.error).mockClear()
|
||||
})
|
||||
|
||||
it('surfaces persistence failures while keeping the draft open', async () => {
|
||||
const addDiffComment = vi.fn().mockResolvedValue(null)
|
||||
|
||||
const result = await submitDiffSectionComment({
|
||||
addDiffComment,
|
||||
body: 'Needs revision',
|
||||
target: { lineNumber: 4 },
|
||||
section,
|
||||
worktreeId: 'worktree-1'
|
||||
})
|
||||
|
||||
expect(result).toBe(false)
|
||||
expect(toast.error).toHaveBeenCalledWith('Failed to save comment')
|
||||
})
|
||||
})
|
||||
@@ -1,41 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getEditorDisplayLabel } from './editor-labels'
|
||||
import type { OpenFile } from '@/store/slices/editor'
|
||||
|
||||
function makeOpenFile(overrides: Partial<OpenFile> = {}): OpenFile {
|
||||
return {
|
||||
id: '/repo/docs/README.md',
|
||||
filePath: '/repo/docs/README.md',
|
||||
relativePath: 'docs/README.md',
|
||||
worktreeId: 'wt-1',
|
||||
language: 'markdown',
|
||||
isDirty: false,
|
||||
mode: 'edit',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('getEditorDisplayLabel', () => {
|
||||
it('adds a preview suffix for markdown preview tabs', () => {
|
||||
expect(
|
||||
getEditorDisplayLabel(
|
||||
makeOpenFile({
|
||||
id: 'markdown-preview::/repo/docs/README.md',
|
||||
mode: 'markdown-preview'
|
||||
})
|
||||
)
|
||||
).toBe('README.md (preview)')
|
||||
})
|
||||
|
||||
it('uses the requested label variant for markdown preview tabs', () => {
|
||||
expect(
|
||||
getEditorDisplayLabel(
|
||||
makeOpenFile({
|
||||
id: 'markdown-preview::/repo/docs/README.md',
|
||||
mode: 'markdown-preview'
|
||||
}),
|
||||
'relativePath'
|
||||
)
|
||||
).toBe('docs/README.md (preview)')
|
||||
})
|
||||
})
|
||||
@@ -1,19 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
clearGitHubLinkCopied,
|
||||
createGitHubLinkCopyState,
|
||||
markGitHubLinkCopied,
|
||||
resolveGitHubLinkCopyState,
|
||||
type GitHubLinkCopyState
|
||||
resolveGitHubLinkCopyState
|
||||
} from './github-link-copy-state'
|
||||
|
||||
describe('GitHub link copy state', () => {
|
||||
it('keeps copied state for the same work item', () => {
|
||||
const state = markGitHubLinkCopied('item-1')
|
||||
|
||||
expect(resolveGitHubLinkCopyState(state, 'item-1')).toBe(state)
|
||||
})
|
||||
|
||||
it('resets copied state when the rendered work item changes', () => {
|
||||
const state = markGitHubLinkCopied('item-1')
|
||||
|
||||
@@ -28,10 +20,4 @@ describe('GitHub link copy state', () => {
|
||||
|
||||
expect(clearGitHubLinkCopied(state, 'item-1')).toBe(state)
|
||||
})
|
||||
|
||||
it('preserves identity when clearing an already-clear state', () => {
|
||||
const state: GitHubLinkCopyState = createGitHubLinkCopyState('item-1')
|
||||
|
||||
expect(clearGitHubLinkCopied(state, 'item-1')).toBe(state)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { cleanup, render, screen } from '@testing-library/react'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { ProjectItemsEmptyState } from './ProjectViewStates'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
const FILTERED_COPY = "No items match this view's filter."
|
||||
const UNFILTERED_COPY = 'This view has no items yet.'
|
||||
const TRANSIENCE_HINT = 'Recently added items can take a while to appear.'
|
||||
|
||||
describe('ProjectItemsEmptyState', () => {
|
||||
it('blames the filter only when the view actually has one', () => {
|
||||
render(<ProjectItemsEmptyState filter="status:Todo" />)
|
||||
expect(screen.getByText(FILTERED_COPY)).toBeTruthy()
|
||||
expect(screen.queryByText(UNFILTERED_COPY)).toBeNull()
|
||||
})
|
||||
|
||||
it.each(['', ' ', '\n\t'])('reports an unfiltered view as empty for filter %j', (filter) => {
|
||||
render(<ProjectItemsEmptyState filter={filter} />)
|
||||
expect(screen.getByText(UNFILTERED_COPY)).toBeTruthy()
|
||||
expect(screen.getByText(TRANSIENCE_HINT)).toBeTruthy()
|
||||
expect(screen.queryByText(FILTERED_COPY)).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -1,34 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
CLOSED_LINEAR_API_KEY_DIALOG_STATE,
|
||||
resolveLinearApiKeyDialogState,
|
||||
type LinearApiKeyDialogState
|
||||
} from './linear-api-key-dialog-state'
|
||||
|
||||
describe('resolveLinearApiKeyDialogState', () => {
|
||||
it('preserves draft and error state while the dialog is open', () => {
|
||||
const state: LinearApiKeyDialogState = {
|
||||
apiKeyDraft: 'lin_api_key',
|
||||
connectState: 'error',
|
||||
connectError: 'Nope'
|
||||
}
|
||||
|
||||
expect(resolveLinearApiKeyDialogState(state, true)).toBe(state)
|
||||
})
|
||||
|
||||
it('preserves identity for an already-reset closed dialog', () => {
|
||||
expect(resolveLinearApiKeyDialogState(CLOSED_LINEAR_API_KEY_DIALOG_STATE, false)).toBe(
|
||||
CLOSED_LINEAR_API_KEY_DIALOG_STATE
|
||||
)
|
||||
})
|
||||
|
||||
it('resets draft and connection state when the dialog is closed', () => {
|
||||
const state: LinearApiKeyDialogState = {
|
||||
apiKeyDraft: 'lin_api_key',
|
||||
connectState: 'connecting',
|
||||
connectError: 'Previous error'
|
||||
}
|
||||
|
||||
expect(resolveLinearApiKeyDialogState(state, false)).toEqual(CLOSED_LINEAR_API_KEY_DIALOG_STATE)
|
||||
})
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { matchNativeChatSplitShortcut } from './native-chat-split-shortcut'
|
||||
|
||||
describe('matchNativeChatSplitShortcut', () => {
|
||||
it('uses the existing platform split bindings', () => {
|
||||
expect(
|
||||
matchNativeChatSplitShortcut(
|
||||
{ key: 'd', metaKey: true, ctrlKey: false, altKey: false, shiftKey: false },
|
||||
'darwin',
|
||||
{}
|
||||
)
|
||||
).toBe('right')
|
||||
expect(
|
||||
matchNativeChatSplitShortcut(
|
||||
{ key: 'd', metaKey: false, ctrlKey: true, altKey: false, shiftKey: true },
|
||||
'win32',
|
||||
{}
|
||||
)
|
||||
).toBe('right')
|
||||
expect(
|
||||
matchNativeChatSplitShortcut(
|
||||
{ key: 'd', metaKey: false, ctrlKey: false, altKey: true, shiftKey: true },
|
||||
'linux',
|
||||
{}
|
||||
)
|
||||
).toBe('down')
|
||||
})
|
||||
|
||||
it('respects customized bindings', () => {
|
||||
expect(
|
||||
matchNativeChatSplitShortcut(
|
||||
{ key: 'ArrowRight', metaKey: false, ctrlKey: true, altKey: true, shiftKey: false },
|
||||
'linux',
|
||||
{ 'terminal.splitRight': ['Ctrl+Alt+Right'] }
|
||||
)
|
||||
).toBe('right')
|
||||
})
|
||||
})
|
||||
@@ -1,27 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
appendPushFailureCustomInstruction,
|
||||
buildFixPushFailurePrompt
|
||||
} from '../../../../shared/source-control-push-failure'
|
||||
|
||||
describe('SourceControl push failure recovery prompt', () => {
|
||||
it('adds one-time custom instructions before the response contract', () => {
|
||||
const prompt = buildFixPushFailurePrompt({
|
||||
summary: 'Pre-push hook failed.',
|
||||
error: 'lint failed',
|
||||
branchName: 'main',
|
||||
worktreePath: null,
|
||||
entries: [],
|
||||
customInstruction: 'Only change TypeScript files.'
|
||||
})
|
||||
|
||||
expect(prompt).toContain('Additional user instruction for this fix:')
|
||||
expect(prompt).toContain('Only change TypeScript files.')
|
||||
expect(prompt.trim().endsWith('anything left for the user.')).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves the base prompt unchanged for empty custom instructions', () => {
|
||||
const prompt = 'Fix the failed push.'
|
||||
expect(appendPushFailureCustomInstruction(prompt, ' ')).toBe(prompt)
|
||||
})
|
||||
})
|
||||
@@ -1,33 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { aiVaultSearchHitHostLabels } from './ai-vault-search-hit-hosts'
|
||||
|
||||
describe('aiVaultSearchHitHostLabels', () => {
|
||||
it('returns null while every hit came from one host', () => {
|
||||
expect(
|
||||
aiVaultSearchHitHostLabels(
|
||||
[{ executionHostId: 'runtime:devbox' }, { executionHostId: 'runtime:devbox' }],
|
||||
[{ id: 'runtime:devbox', label: 'devbox' }]
|
||||
)
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('names each host the way the host menu does, with the id label as fallback', () => {
|
||||
const labels = aiVaultSearchHitHostLabels(
|
||||
[
|
||||
{ executionHostId: 'local' },
|
||||
{ executionHostId: 'runtime:env-1' },
|
||||
{ executionHostId: 'runtime:env-gone' }
|
||||
],
|
||||
[
|
||||
{ id: 'local', label: 'Local Mac' },
|
||||
{ id: 'runtime:env-1', label: 'devbox' }
|
||||
]
|
||||
)
|
||||
|
||||
expect(labels && Object.fromEntries(labels)).toEqual({
|
||||
local: 'Local Mac',
|
||||
'runtime:env-1': 'devbox',
|
||||
'runtime:env-gone': 'env-gone'
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,36 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { fileExplorerRefreshConcurrency } from './file-explorer-refresh-concurrency'
|
||||
|
||||
describe('fileExplorerRefreshConcurrency', () => {
|
||||
it('allows the widest fan-out for local disk reads', () => {
|
||||
expect(fileExplorerRefreshConcurrency({ kind: 'local' })).toBe(16)
|
||||
})
|
||||
|
||||
it('caps SSH-owned worktrees at the remote tier', () => {
|
||||
expect(fileExplorerRefreshConcurrency({ kind: 'ssh', connectionId: 'target-1' })).toBe(4)
|
||||
})
|
||||
|
||||
it('treats an SSH-backed runtime as remote', () => {
|
||||
expect(
|
||||
fileExplorerRefreshConcurrency({
|
||||
kind: 'runtime',
|
||||
environmentId: 'env-1',
|
||||
executionHostId: 'ssh:target-1'
|
||||
})
|
||||
).toBe(4)
|
||||
})
|
||||
|
||||
it('uses the runtime tier for a non-SSH runtime host', () => {
|
||||
expect(
|
||||
fileExplorerRefreshConcurrency({
|
||||
kind: 'runtime',
|
||||
environmentId: 'env-1',
|
||||
executionHostId: 'runtime:env-1'
|
||||
})
|
||||
).toBe(8)
|
||||
})
|
||||
|
||||
it('falls back to the conservative remote tier when ownership is unresolved', () => {
|
||||
expect(fileExplorerRefreshConcurrency({ kind: 'unresolved' })).toBe(4)
|
||||
})
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
RIGHT_SIDEBAR_ABSOLUTE_FALLBACK_MAX_WIDTH,
|
||||
RIGHT_SIDEBAR_MIN_WIDTH,
|
||||
clampRightSidebarPanelWidth,
|
||||
computeMaxRightSidebarPanelWidth
|
||||
} from './right-sidebar-width'
|
||||
|
||||
describe('right sidebar rendered width', () => {
|
||||
it('leaves the reserved non-sidebar area when persisted width is too large', () => {
|
||||
expect(clampRightSidebarPanelWidth(900, 900, 0)).toBe(580)
|
||||
})
|
||||
|
||||
it('includes rendered extra width in the reservation math', () => {
|
||||
expect(computeMaxRightSidebarPanelWidth(900, 40)).toBe(540)
|
||||
expect(clampRightSidebarPanelWidth(900, 900, 40)).toBe(540)
|
||||
})
|
||||
|
||||
it('preserves the minimum sidebar width on narrow windows', () => {
|
||||
expect(clampRightSidebarPanelWidth(900, 400, 40)).toBe(RIGHT_SIDEBAR_MIN_WIDTH)
|
||||
})
|
||||
|
||||
it('uses the fallback max outside DOM environments', () => {
|
||||
expect(computeMaxRightSidebarPanelWidth(null, 40)).toBe(
|
||||
RIGHT_SIDEBAR_ABSOLUTE_FALLBACK_MAX_WIDTH
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,25 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { HostedReviewProvider } from '../../../../shared/hosted-review'
|
||||
import { resolveCreatedHostedReviewLink } from './source-control-created-review-link'
|
||||
|
||||
describe('resolveCreatedHostedReviewLink', () => {
|
||||
it.each([
|
||||
['github', { linkedPR: 42 }, { linkedGitHubPR: 42 }],
|
||||
['gitlab', { linkedGitLabMR: 42 }, { linkedGitLabMR: 42 }],
|
||||
['bitbucket', { linkedBitbucketPR: 42 }, { linkedBitbucketPR: 42 }],
|
||||
['azure-devops', { linkedAzureDevOpsPR: 42 }, { linkedAzureDevOpsPR: 42 }],
|
||||
['gitea', { linkedGiteaPR: 42 }, { linkedGiteaPR: 42 }]
|
||||
] as const)('links a created %s review by number', (provider, worktree, lookup) => {
|
||||
expect(resolveCreatedHostedReviewLink(provider as HostedReviewProvider, 42)).toEqual({
|
||||
worktree,
|
||||
lookup
|
||||
})
|
||||
})
|
||||
|
||||
it('does not link an unsupported review provider', () => {
|
||||
expect(resolveCreatedHostedReviewLink('unsupported', 42)).toEqual({
|
||||
worktree: {},
|
||||
lookup: {}
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,31 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getFloatingWorkspaceDirectoryInputValue } from './FloatingWorkspacePane'
|
||||
|
||||
describe('getFloatingWorkspaceDirectoryInputValue', () => {
|
||||
it('shows home shorthand for the default terminal directory', () => {
|
||||
expect(
|
||||
getFloatingWorkspaceDirectoryInputValue({
|
||||
configuredFloatingWorkspacePath: '~',
|
||||
resolvedFloatingWorkspacePath: '/Users/example'
|
||||
})
|
||||
).toBe('~')
|
||||
})
|
||||
|
||||
it('shows home shorthand for legacy blank terminal directory settings', () => {
|
||||
expect(
|
||||
getFloatingWorkspaceDirectoryInputValue({
|
||||
configuredFloatingWorkspacePath: '',
|
||||
resolvedFloatingWorkspacePath: '/Users/example'
|
||||
})
|
||||
).toBe('~')
|
||||
})
|
||||
|
||||
it('shows the main-resolved trusted custom directory', () => {
|
||||
expect(
|
||||
getFloatingWorkspaceDirectoryInputValue({
|
||||
configuredFloatingWorkspacePath: '/Users/example/notes',
|
||||
resolvedFloatingWorkspacePath: '/Users/example/notes'
|
||||
})
|
||||
).toBe('/Users/example/notes')
|
||||
})
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
import { describe, expect, it, afterEach, vi } from 'vitest'
|
||||
import { shadowedKeybindingTitles } from './PluginKeybindingConsentPreview'
|
||||
|
||||
describe('plugin keybinding consent preview', () => {
|
||||
it('names built-in shortcuts shadowed by plugin chords and user overrides', () => {
|
||||
expect(shadowedKeybindingTitles('Mod+P', 'darwin')).toContain('Go to File')
|
||||
expect(
|
||||
shadowedKeybindingTitles('Mod+Alt+T', 'linux', { 'view.tasks': ['Mod+Alt+T'] })
|
||||
).toContain('Open Tasks')
|
||||
})
|
||||
})
|
||||
|
||||
const { rejectAppStoreAccess } = vi.hoisted(() => ({
|
||||
rejectAppStoreAccess: vi.fn(() => {
|
||||
throw new Error('Pure helper tests must not access the app store')
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign(rejectAppStoreAccess, {
|
||||
getState: rejectAppStoreAccess,
|
||||
getInitialState: rejectAppStoreAccess,
|
||||
setState: rejectAppStoreAccess,
|
||||
subscribe: rejectAppStoreAccess
|
||||
})
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
expect(rejectAppStoreAccess).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -1,32 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { revealRepoInProjectFilter } from './project-filter-reveal'
|
||||
|
||||
function makeState(filterRepoIds: readonly string[]) {
|
||||
return { filterRepoIds, setFilterRepoIds: vi.fn() }
|
||||
}
|
||||
|
||||
describe('revealRepoInProjectFilter', () => {
|
||||
it('keeps the existing selection and adds the revealed project', () => {
|
||||
const state = makeState(['repo-a', 'repo-b'])
|
||||
|
||||
revealRepoInProjectFilter(state, 'repo-c')
|
||||
|
||||
expect(state.setFilterRepoIds).toHaveBeenCalledWith(['repo-a', 'repo-b', 'repo-c'])
|
||||
})
|
||||
|
||||
it('does nothing when no project filter is active', () => {
|
||||
const state = makeState([])
|
||||
|
||||
revealRepoInProjectFilter(state, 'repo-c')
|
||||
|
||||
expect(state.setFilterRepoIds).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('does nothing when the project is already selected', () => {
|
||||
const state = makeState(['repo-a', 'repo-c'])
|
||||
|
||||
revealRepoInProjectFilter(state, 'repo-c')
|
||||
|
||||
expect(state.setFilterRepoIds).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isEventTargetInsideCurrentTarget } from './worktree-card-dom-events'
|
||||
|
||||
describe('worktree card DOM events', () => {
|
||||
it('recognizes DOM events that originate inside the current target', () => {
|
||||
const currentTarget = document.createElement('div')
|
||||
const child = document.createElement('button')
|
||||
currentTarget.appendChild(child)
|
||||
|
||||
expect(isEventTargetInsideCurrentTarget(currentTarget, child)).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects portaled DOM events that bubble through the React tree', () => {
|
||||
const currentTarget = document.createElement('div')
|
||||
const portaledTarget = document.createElement('button')
|
||||
|
||||
expect(isEventTargetInsideCurrentTarget(currentTarget, portaledTarget)).toBe(false)
|
||||
})
|
||||
|
||||
it('supports text-node event targets inside the current target', () => {
|
||||
const currentTarget = document.createElement('div')
|
||||
const textTarget = document.createTextNode('Rename')
|
||||
currentTarget.appendChild(textTarget)
|
||||
|
||||
expect(isEventTargetInsideCurrentTarget(currentTarget, textTarget)).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,30 +0,0 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { isTerminalHttpLinkActivation } from './terminal-http-link-activation'
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
describe('isTerminalHttpLinkActivation', () => {
|
||||
it('leaves Alt+Cmd gestures to the child TUI on macOS', () => {
|
||||
vi.stubGlobal('navigator', { userAgent: 'Macintosh' })
|
||||
|
||||
expect(
|
||||
isTerminalHttpLinkActivation({ metaKey: true, ctrlKey: false, altKey: false } as MouseEvent)
|
||||
).toBe(true)
|
||||
expect(
|
||||
isTerminalHttpLinkActivation({ metaKey: true, ctrlKey: false, altKey: true } as MouseEvent)
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('leaves Alt+Ctrl gestures to the child TUI on other platforms', () => {
|
||||
vi.stubGlobal('navigator', { userAgent: 'Windows' })
|
||||
|
||||
expect(
|
||||
isTerminalHttpLinkActivation({ metaKey: false, ctrlKey: true, altKey: false } as MouseEvent)
|
||||
).toBe(true)
|
||||
expect(
|
||||
isTerminalHttpLinkActivation({ metaKey: false, ctrlKey: true, altKey: true } as MouseEvent)
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -15,17 +15,6 @@ describe('terminal process exit restart', () => {
|
||||
).toBe(startup)
|
||||
})
|
||||
|
||||
it('restarts a shell when a capacity failure had no startup request', () => {
|
||||
expect(
|
||||
resolveTerminalProcessExitRestartStartup({
|
||||
paneId: 1,
|
||||
exitCode: 1,
|
||||
reason: 'git-bash-console-capacity',
|
||||
startup: null
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('restarts other process failures as a shell', () => {
|
||||
expect(
|
||||
resolveTerminalProcessExitRestartStartup({
|
||||
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
import { describe, expect, it, afterEach, vi } from 'vitest'
|
||||
import { combineTerminalWorktreeParkIds } from './use-manual-terminal-worktree-parking'
|
||||
|
||||
describe('combineTerminalWorktreeParkIds', () => {
|
||||
it('adds manually parked worktrees without dropping automatic parks', () => {
|
||||
expect(combineTerminalWorktreeParkIds(new Set(['automatic']), new Set(['manual']))).toEqual(
|
||||
new Set(['automatic', 'manual'])
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the automatic set reference when there are no manual parks', () => {
|
||||
const automatic = new Set(['automatic'])
|
||||
|
||||
expect(combineTerminalWorktreeParkIds(automatic, new Set())).toBe(automatic)
|
||||
})
|
||||
})
|
||||
|
||||
const { rejectAppStoreAccess } = vi.hoisted(() => ({
|
||||
rejectAppStoreAccess: vi.fn(() => {
|
||||
throw new Error('Pure helper tests must not access the app store')
|
||||
})
|
||||
}))
|
||||
|
||||
vi.mock('@/store', () => ({
|
||||
useAppStore: Object.assign(rejectAppStoreAccess, {
|
||||
getState: rejectAppStoreAccess,
|
||||
getInitialState: rejectAppStoreAccess,
|
||||
setState: rejectAppStoreAccess,
|
||||
subscribe: rejectAppStoreAccess
|
||||
})
|
||||
}))
|
||||
|
||||
afterEach(() => {
|
||||
expect(rejectAppStoreAccess).not.toHaveBeenCalled()
|
||||
})
|
||||
@@ -1,28 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import { getRepoMultiComboboxDetail } from './repo-multi-combobox'
|
||||
|
||||
function repo(overrides: Partial<Repo> = {}): Repo {
|
||||
return {
|
||||
id: 'repo-1',
|
||||
path: '/Users/jinwoo/orca',
|
||||
displayName: 'orca',
|
||||
badgeColor: '#999999',
|
||||
addedAt: 1,
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('getRepoMultiComboboxDetail', () => {
|
||||
it('shows host context before the path when available', () => {
|
||||
expect(getRepoMultiComboboxDetail(repo(), 'Local Mac')).toBe('Local Mac · /Users/jinwoo/orca')
|
||||
expect(getRepoMultiComboboxDetail(repo({ path: '/home/orca/orca' }), 'openclaw 2')).toBe(
|
||||
'openclaw 2 · /home/orca/orca'
|
||||
)
|
||||
})
|
||||
|
||||
it('keeps the existing path-only detail when no host label is provided', () => {
|
||||
expect(getRepoMultiComboboxDetail(repo(), null)).toBe('/Users/jinwoo/orca')
|
||||
expect(getRepoMultiComboboxDetail(repo(), ' ')).toBe('/Users/jinwoo/orca')
|
||||
})
|
||||
})
|
||||
@@ -1,39 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act } from 'react'
|
||||
import { createRoot, type Root } from 'react-dom/client'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { ScrollArea } from './scroll-area'
|
||||
|
||||
globalThis.IS_REACT_ACT_ENVIRONMENT = true
|
||||
|
||||
const roots: Root[] = []
|
||||
|
||||
afterEach(() => {
|
||||
roots.splice(0).forEach((root) => {
|
||||
act(() => root.unmount())
|
||||
})
|
||||
document.body.replaceChildren()
|
||||
})
|
||||
|
||||
describe('ScrollArea sizing footgun', () => {
|
||||
// Radix Root’s inline position beats absolute classes; size ScrollArea with height instead.
|
||||
it('keeps Radix inline position:relative even when given an absolute className', async () => {
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
const root = createRoot(container)
|
||||
roots.push(root)
|
||||
|
||||
await act(async () => {
|
||||
root.render(
|
||||
<ScrollArea className="absolute inset-0">
|
||||
<div />
|
||||
</ScrollArea>
|
||||
)
|
||||
})
|
||||
|
||||
const el = container.querySelector<HTMLElement>('[data-slot="scroll-area"]')
|
||||
expect(el).not.toBeNull()
|
||||
expect(el!.style.position).toBe('relative')
|
||||
})
|
||||
})
|
||||
@@ -1,17 +1,8 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { Switch, SwitchIndicator } from './switch'
|
||||
import { SwitchIndicator } from './switch'
|
||||
|
||||
describe('Switch', () => {
|
||||
it('renders the checked state with symmetric track geometry', () => {
|
||||
const html = renderToStaticMarkup(<Switch checked aria-label="Example setting" />)
|
||||
|
||||
expect(html).toContain('role="switch"')
|
||||
expect(html).toContain('aria-checked="true"')
|
||||
expect(html).toContain('p-0.5')
|
||||
expect(html).toContain('data-[state=checked]:translate-x-4')
|
||||
})
|
||||
|
||||
it('renders a visual-only indicator without switch semantics', () => {
|
||||
const html = renderToStaticMarkup(<SwitchIndicator checked={false} />)
|
||||
|
||||
@@ -19,12 +10,4 @@ describe('Switch', () => {
|
||||
expect(html).toContain('data-state="unchecked"')
|
||||
expect(html).not.toContain('role="switch"')
|
||||
})
|
||||
|
||||
it('renders compact indicator geometry', () => {
|
||||
const html = renderToStaticMarkup(<SwitchIndicator checked size="compact" />)
|
||||
|
||||
expect(html).toContain('h-3.5 w-6')
|
||||
expect(html).toContain('size-2.5')
|
||||
expect(html).toContain('data-[state=checked]:translate-x-2.5')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,14 +26,4 @@ describe('useResetCountdownClock', () => {
|
||||
})
|
||||
expect(result.current).toBe(START + 30_000 + 1)
|
||||
})
|
||||
|
||||
it('does not schedule a tick when there is no future reset', () => {
|
||||
const { result } = renderHook(() => useResetCountdownClock([START - 1000]))
|
||||
expect(result.current).toBe(START)
|
||||
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(10 * MIN)
|
||||
})
|
||||
expect(result.current).toBe(START)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shouldCancelVirtualizedScrollOffsetRestore } from './virtualizedScrollOffsetRestore'
|
||||
|
||||
describe('shouldCancelVirtualizedScrollOffsetRestore', () => {
|
||||
it('keeps a pending restore when direct user scroll input is not tracked', () => {
|
||||
expect(
|
||||
shouldCancelVirtualizedScrollOffsetRestore({
|
||||
restoring: true
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps a pending restore during programmatic scroll movement', () => {
|
||||
expect(
|
||||
shouldCancelVirtualizedScrollOffsetRestore({
|
||||
hasDirectScrollInput: () => false,
|
||||
restoring: true
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('does not cancel when there is no pending restore', () => {
|
||||
expect(
|
||||
shouldCancelVirtualizedScrollOffsetRestore({
|
||||
hasDirectScrollInput: () => true,
|
||||
restoring: false
|
||||
})
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('cancels a pending restore while direct user scroll input is active', () => {
|
||||
expect(
|
||||
shouldCancelVirtualizedScrollOffsetRestore({
|
||||
hasDirectScrollInput: () => true,
|
||||
restoring: true
|
||||
})
|
||||
).toBe(true)
|
||||
})
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
import { renderToStaticMarkup } from 'react-dom/server'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { AgentTypeIcon } from './agent-type-icon'
|
||||
|
||||
function render(agentType: string | null | undefined): string {
|
||||
return renderToStaticMarkup(<AgentTypeIcon agentType={agentType} />)
|
||||
}
|
||||
|
||||
describe('AgentTypeIcon', () => {
|
||||
it("renders a program's first letter", () => {
|
||||
expect(render('brew')).toContain('>B</text>')
|
||||
expect(render('terraform')).toContain('>T</text>')
|
||||
})
|
||||
|
||||
it('skips leading punctuation the app id grammar allows', () => {
|
||||
expect(render('_9tool')).toContain('>9</text>')
|
||||
expect(render('.+-')).toContain('>?</text>')
|
||||
})
|
||||
|
||||
it('keeps "?" for unidentified agents', () => {
|
||||
expect(render('unknown')).toContain('>?</text>')
|
||||
expect(render(null)).toContain('>?</text>')
|
||||
expect(render(undefined)).toContain('>?</text>')
|
||||
})
|
||||
|
||||
it('still renders the Claude icon for known agents', () => {
|
||||
expect(render('claude')).toContain('#D97757')
|
||||
})
|
||||
})
|
||||
@@ -24,10 +24,4 @@ describe('createOptionKeyLocationTracker', () => {
|
||||
tracker.keyUp({ key: 'Alt', location: 0 })
|
||||
expect(tracker.get()).toBe(0)
|
||||
})
|
||||
|
||||
it('ignores non-Option keys', () => {
|
||||
const tracker = createOptionKeyLocationTracker()
|
||||
tracker.keyDown({ key: 'Shift', location: 1 })
|
||||
expect(tracker.get()).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getLinkedWorkItemProvider } from './linked-work-item-provider'
|
||||
|
||||
describe('getLinkedWorkItemProvider', () => {
|
||||
it.each([
|
||||
[
|
||||
'explicit provider metadata',
|
||||
{
|
||||
type: 'issue',
|
||||
provider: 'jira',
|
||||
number: 0,
|
||||
title: 'ORCA-123 Fix Jira',
|
||||
url: 'https://example.atlassian.net/browse/ORCA-123',
|
||||
jiraIdentifier: 'ORCA-123'
|
||||
},
|
||||
'jira'
|
||||
],
|
||||
[
|
||||
'Jira issue URL with no numeric issue id',
|
||||
{
|
||||
type: 'issue',
|
||||
number: 0,
|
||||
title: 'ORCA-123 Fix Jira',
|
||||
url: 'https://example.atlassian.net/browse/ORCA-123'
|
||||
},
|
||||
'jira'
|
||||
],
|
||||
[
|
||||
'legacy Linear linked issue',
|
||||
{
|
||||
type: 'issue',
|
||||
number: 0,
|
||||
title: 'Fix Linear',
|
||||
url: 'https://linear.app/team/issue/ENG-123/fix-linear',
|
||||
linearIdentifier: 'ENG-123'
|
||||
},
|
||||
'linear'
|
||||
]
|
||||
] as const)('detects %s', (_label, item, provider) => {
|
||||
expect(getLinkedWorkItemProvider(item)).toBe(provider)
|
||||
})
|
||||
})
|
||||
@@ -1,29 +0,0 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { hashOrcaHookScript } from './orca-hook-trust'
|
||||
|
||||
const realCrypto = globalThis.crypto
|
||||
|
||||
afterEach(() => {
|
||||
Object.defineProperty(globalThis, 'crypto', { value: realCrypto, configurable: true })
|
||||
})
|
||||
|
||||
function stubCrypto(value: unknown): void {
|
||||
Object.defineProperty(globalThis, 'crypto', { value, configurable: true })
|
||||
}
|
||||
|
||||
describe('hashOrcaHookScript', () => {
|
||||
it('produces a stable hex digest via crypto.subtle', async () => {
|
||||
const hash = await hashOrcaHookScript('echo hi')
|
||||
expect(hash).toMatch(/^[0-9a-f]+$/)
|
||||
expect(await hashOrcaHookScript(' echo hi ')).toBe(hash)
|
||||
})
|
||||
|
||||
// Plain-HTTP LAN clients lack crypto.subtle; fallback must still produce deterministic matching trust hashes.
|
||||
it('falls back to a deterministic hash when crypto.subtle is unavailable', async () => {
|
||||
stubCrypto(undefined)
|
||||
const hash = await hashOrcaHookScript('echo hi')
|
||||
expect(hash).toMatch(/^[0-9a-f]+$/)
|
||||
expect(await hashOrcaHookScript('echo hi')).toBe(hash)
|
||||
expect(await hashOrcaHookScript('echo bye')).not.toBe(hash)
|
||||
})
|
||||
})
|
||||
@@ -1,37 +0,0 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { shouldFocusTerminalFromPanePointerDown } from './pane-pointer-focus'
|
||||
|
||||
class FakeElement {
|
||||
constructor(private readonly closestResult: FakeElement | null = null) {}
|
||||
|
||||
closest(): FakeElement | null {
|
||||
return this.closestResult
|
||||
}
|
||||
}
|
||||
|
||||
describe('shouldFocusTerminalFromPanePointerDown', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubGlobal('Element', FakeElement)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('focuses the terminal for non-element targets', () => {
|
||||
expect(shouldFocusTerminalFromPanePointerDown({} as EventTarget)).toBe(true)
|
||||
})
|
||||
|
||||
it('focuses the terminal for ordinary pane surface clicks', () => {
|
||||
const target = new FakeElement()
|
||||
|
||||
expect(shouldFocusTerminalFromPanePointerDown(target as unknown as Element)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not steal focus from pane-local controls', () => {
|
||||
const control = new FakeElement()
|
||||
const target = new FakeElement(control)
|
||||
|
||||
expect(shouldFocusTerminalFromPanePointerDown(target as unknown as Element)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -4,12 +4,6 @@ import { placeCreatedSessionTab } from './session-tab-placement'
|
||||
const created = { id: 'new' }
|
||||
|
||||
describe('placeCreatedSessionTab', () => {
|
||||
it('inserts directly after the anchor', () => {
|
||||
expect(
|
||||
placeCreatedSessionTab([{ id: 'a' }, { id: 'b' }, { id: 'c' }], created, 'a').map((t) => t.id)
|
||||
).toEqual(['a', 'new', 'b', 'c'])
|
||||
})
|
||||
|
||||
it('inserts after all leaves of a split parent', () => {
|
||||
expect(
|
||||
placeCreatedSessionTab(
|
||||
@@ -54,27 +48,12 @@ describe('placeCreatedSessionTab', () => {
|
||||
).toEqual(['split::left', 'trailing', 'split::right', 'new'])
|
||||
})
|
||||
|
||||
it('appends when the anchor is the last tab', () => {
|
||||
expect(
|
||||
placeCreatedSessionTab([{ id: 'a' }, { id: 'b' }], created, 'b').map((t) => t.id)
|
||||
).toEqual(['a', 'b', 'new'])
|
||||
})
|
||||
|
||||
it('appends when the anchor is absent', () => {
|
||||
expect(
|
||||
placeCreatedSessionTab([{ id: 'a' }, { id: 'b' }], created, 'missing').map((t) => t.id)
|
||||
).toEqual(['a', 'b', 'new'])
|
||||
})
|
||||
|
||||
it('appends when no anchor is given', () => {
|
||||
for (const anchor of [undefined, null, '']) {
|
||||
expect(placeCreatedSessionTab([{ id: 'a' }], created, anchor).map((t) => t.id)).toEqual([
|
||||
'a',
|
||||
'new'
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
it('re-places a tab that is already in the list instead of duplicating it', () => {
|
||||
expect(
|
||||
placeCreatedSessionTab([{ id: 'a' }, { id: 'new' }, { id: 'b' }], created, 'b').map(
|
||||
@@ -82,10 +61,4 @@ describe('placeCreatedSessionTab', () => {
|
||||
)
|
||||
).toEqual(['a', 'b', 'new'])
|
||||
})
|
||||
|
||||
it('does not mutate the input list', () => {
|
||||
const tabs = [{ id: 'a' }, { id: 'b' }]
|
||||
placeCreatedSessionTab(tabs, created, 'a')
|
||||
expect(tabs.map((t) => t.id)).toEqual(['a', 'b'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildWorkspaceReviewFacts,
|
||||
prGroupOfReviewFact,
|
||||
readWorkspaceReviewFact
|
||||
} from './workspace-review-facts'
|
||||
import { buildWorkspaceReviewFacts, readWorkspaceReviewFact } from './workspace-review-facts'
|
||||
|
||||
const repo = { id: 'repo-1', path: '/r', connectionId: null, executionHostId: null }
|
||||
const row = { id: 'repo-1::/r/a', repoId: 'repo-1', branch: 'refs/heads/a', linkedPR: null }
|
||||
@@ -43,15 +39,6 @@ describe('readWorkspaceReviewFact', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('prGroupOfReviewFact', () => {
|
||||
it('keeps rows without a PR in progress and lanes the rest by state', () => {
|
||||
expect(prGroupOfReviewFact(null)).toBe('in-progress')
|
||||
expect(prGroupOfReviewFact({ provider: 'github', reviewState: 'draft' })).toBe('in-progress')
|
||||
expect(prGroupOfReviewFact({ provider: 'github', reviewState: '' })).toBe('in-review')
|
||||
expect(prGroupOfReviewFact({ provider: 'github', reviewState: 'merged' })).toBe('done')
|
||||
})
|
||||
})
|
||||
|
||||
it('lists only rows with a PR, under the key the workspaces domain lists them at', () => {
|
||||
const other = { ...row, id: 'repo-1::/r/b', branch: 'b' }
|
||||
expect(buildWorkspaceReviewFacts([row, other], [repo], { 'repo-1::a': pr('open') })).toEqual({
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
MAX_SKILL_DELETE_BATCH,
|
||||
SkillDeletePlanSchema,
|
||||
SkillDeleteRequestSchema,
|
||||
SkillDeleteResultSchema
|
||||
} from './skill-delete-contract'
|
||||
import { MAX_SKILL_DELETE_BATCH, SkillDeleteRequestSchema } from './skill-delete-contract'
|
||||
|
||||
const SKILL = {
|
||||
id: 'skill-id',
|
||||
@@ -15,13 +10,6 @@ const SKILL = {
|
||||
}
|
||||
|
||||
describe('SkillDeleteRequestSchema', () => {
|
||||
it('accepts a request with no target, which is what the page sends', () => {
|
||||
expect(SkillDeleteRequestSchema.parse({ operationId: 'op', skills: [SKILL] })).toEqual({
|
||||
operationId: 'op',
|
||||
skills: [SKILL]
|
||||
})
|
||||
})
|
||||
|
||||
it('rejects an unknown top-level field rather than ignoring it', () => {
|
||||
// Old hosts must reject delete-affecting unknown fields instead of silently dropping them.
|
||||
expect(() =>
|
||||
@@ -56,55 +44,3 @@ describe('SkillDeleteRequestSchema', () => {
|
||||
).toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('plan and result schemas', () => {
|
||||
it('carries a typed block reason so the band can group without string matching', () => {
|
||||
const plan = SkillDeletePlanSchema.parse({
|
||||
operationId: 'op',
|
||||
skills: [
|
||||
{
|
||||
id: 'a',
|
||||
name: 'demo',
|
||||
canonicalPath: '/root/demo/SKILL.md',
|
||||
placements: [],
|
||||
blocked: 'bundled'
|
||||
}
|
||||
]
|
||||
})
|
||||
expect(plan.skills[0].blocked).toBe('bundled')
|
||||
})
|
||||
|
||||
it('rejects a block reason outside the shared vocabulary', () => {
|
||||
expect(() =>
|
||||
SkillDeletePlanSchema.parse({
|
||||
operationId: 'op',
|
||||
skills: [{ id: 'a', name: 'demo', canonicalPath: '/p', placements: [], blocked: 'because' }]
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('has no `unsupported` status, because such a delete is never issued', () => {
|
||||
expect(() =>
|
||||
SkillDeleteResultSchema.parse({
|
||||
operationId: 'op',
|
||||
skills: [{ id: 'a', name: 'demo', status: 'unsupported', removedPaths: [] }]
|
||||
})
|
||||
).toThrow()
|
||||
})
|
||||
|
||||
it('accepts a partial result carrying the staged paths', () => {
|
||||
const result = SkillDeleteResultSchema.parse({
|
||||
operationId: 'op',
|
||||
skills: [
|
||||
{
|
||||
id: 'a',
|
||||
name: 'demo',
|
||||
status: 'partial',
|
||||
removedPaths: [],
|
||||
stagedPaths: ['/root/.demo.orca-skill-delete-1']
|
||||
}
|
||||
]
|
||||
})
|
||||
expect(result.skills[0].stagedPaths).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
nativeSkillPathSemantics,
|
||||
normalizedSkillPath,
|
||||
POSIX_SKILL_PATH_SEMANTICS,
|
||||
skillPathDepthBelow,
|
||||
skillPathInside,
|
||||
@@ -74,17 +72,3 @@ describe('skillPathDepthBelow', () => {
|
||||
expect(skillPathDepthBelow('/r', '/other/SKILL.md', POSIX_SKILL_PATH_SEMANTICS)).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
describe('nativeSkillPathSemantics', () => {
|
||||
it('reads the platform it is given rather than the process it runs in', () => {
|
||||
expect(nativeSkillPathSemantics('win32')).toEqual(WINDOWS_SKILL_PATH_SEMANTICS)
|
||||
expect(nativeSkillPathSemantics('linux')).toEqual(POSIX_SKILL_PATH_SEMANTICS)
|
||||
expect(nativeSkillPathSemantics('darwin')).toEqual(POSIX_SKILL_PATH_SEMANTICS)
|
||||
})
|
||||
})
|
||||
|
||||
describe('normalizedSkillPath', () => {
|
||||
it('resolves before comparing', () => {
|
||||
expect(normalizedSkillPath('/a/b/../c', POSIX_SKILL_PATH_SEMANTICS)).toBe('/a/c')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,49 +2,11 @@ import { describe, expect, it } from 'vitest'
|
||||
import type { CommitMessageDraftContext } from './commit-message-generation'
|
||||
import {
|
||||
formatLinkedIssueTemplateValue,
|
||||
SOURCE_CONTROL_ACTION_VARIABLE_INFO,
|
||||
SOURCE_CONTROL_ACTION_VARIABLES,
|
||||
withLinkedIssueDraftContext
|
||||
} from './source-control-ai-action-variables'
|
||||
import {
|
||||
renderSourceControlActionCommandTemplate,
|
||||
SOURCE_CONTROL_LAUNCH_ACTION_IDS
|
||||
} from './source-control-ai-actions'
|
||||
|
||||
describe('source-control AI variable registry', () => {
|
||||
it('documents every registered variable so chip hover cards cannot crash', () => {
|
||||
// Keep runtime coverage if the registry’s compile-time constraint ever loosens.
|
||||
const documented = new Set<string>(Object.keys(SOURCE_CONTROL_ACTION_VARIABLE_INFO))
|
||||
const undocumented = [...new Set(Object.values(SOURCE_CONTROL_ACTION_VARIABLES).flat())].filter(
|
||||
(variable) => !documented.has(variable)
|
||||
)
|
||||
|
||||
expect(undocumented).toEqual([])
|
||||
})
|
||||
|
||||
it('offers linkedIssue on commit message and pull request only', () => {
|
||||
expect(SOURCE_CONTROL_ACTION_VARIABLES.commitMessage).toContain('linkedIssue')
|
||||
expect(SOURCE_CONTROL_ACTION_VARIABLES.pullRequest).toContain('linkedIssue')
|
||||
expect(SOURCE_CONTROL_ACTION_VARIABLES.branchName).not.toContain('linkedIssue')
|
||||
for (const actionId of SOURCE_CONTROL_LAUNCH_ACTION_IDS) {
|
||||
expect(SOURCE_CONTROL_ACTION_VARIABLES[actionId]).not.toContain('linkedIssue')
|
||||
}
|
||||
})
|
||||
|
||||
it('names GitHub and the empty case in the linkedIssue description', () => {
|
||||
const info = SOURCE_CONTROL_ACTION_VARIABLE_INFO.linkedIssue
|
||||
expect(info.description).toContain('GitHub')
|
||||
expect(info.description).toContain('Empty')
|
||||
expect(info.example).toBe('123')
|
||||
})
|
||||
})
|
||||
import { renderSourceControlActionCommandTemplate } from './source-control-ai-actions'
|
||||
|
||||
describe('formatLinkedIssueTemplateValue', () => {
|
||||
it('renders positive integers as decimal strings', () => {
|
||||
expect(formatLinkedIssueTemplateValue(123)).toBe('123')
|
||||
expect(formatLinkedIssueTemplateValue(1)).toBe('1')
|
||||
})
|
||||
|
||||
it('renders anything that is not a positive integer as an empty string', () => {
|
||||
// Corrupt issue numbers must not produce references like Fixes #-7 or Fixes #1e+21.
|
||||
for (const value of [
|
||||
|
||||
@@ -23,11 +23,6 @@ describe('synthetic agent titles', () => {
|
||||
expect(isSyntheticAgentPermissionTitle(title)).toBe(false)
|
||||
})
|
||||
|
||||
it('provides terminal-state titles for Codex hook completion', () => {
|
||||
expect(getSyntheticAgentTerminalTitle('codex', 'done')).toBe('Codex ready')
|
||||
expect(getSyntheticAgentTerminalTitle('codex', 'waiting')).toBe('Codex - action required')
|
||||
})
|
||||
|
||||
it('does not synthesize Codex working titles over Codex native spinner titles', () => {
|
||||
expect(shouldDriveSyntheticAgentTitleFromHook('codex', 'working')).toBe(false)
|
||||
expect(shouldDriveSyntheticAgentTitleFromHook('codex', 'done')).toBe(true)
|
||||
@@ -40,23 +35,4 @@ describe('synthetic agent titles', () => {
|
||||
expect(shouldDriveSyntheticAgentTitleFromHook('opencode', 'done')).toBe(false)
|
||||
expect(shouldDriveSyntheticAgentTitleFromHook('opencode', 'waiting')).toBe(false)
|
||||
})
|
||||
|
||||
it('provides Devin titles for hook-driven status updates', () => {
|
||||
expect(getSyntheticAgentTerminalTitle('devin', 'done')).toBe('Devin ready')
|
||||
expect(getSyntheticAgentTerminalTitle('devin', 'waiting')).toBe('Devin - action required')
|
||||
expect(shouldDriveSyntheticAgentTitleFromHook('devin', 'working')).toBe(true)
|
||||
})
|
||||
|
||||
it('provides Pi-compatible OMP titles for hook-driven status updates', () => {
|
||||
expect(getSyntheticAgentTerminalTitle('omp', 'done')).toBe('OMP ready')
|
||||
expect(getSyntheticAgentTerminalTitle('omp', 'waiting')).toBe('OMP - action required')
|
||||
// Why: the native π working title carries session name/cwd; synthesizing would clobber it.
|
||||
expect(shouldDriveSyntheticAgentTitleFromHook('omp', 'working')).toBe(false)
|
||||
})
|
||||
|
||||
it('provides Pi titles for hook-driven status updates', () => {
|
||||
expect(getSyntheticAgentTerminalTitle('pi', 'done')).toBe('Pi ready')
|
||||
expect(getSyntheticAgentTerminalTitle('pi', 'waiting')).toBe('Pi - action required')
|
||||
expect(shouldDriveSyntheticAgentTitleFromHook('pi', 'working')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,23 +2,10 @@ import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
filterAvailableTaskProviders,
|
||||
normalizeTaskProviderSettings,
|
||||
normalizeVisibleTaskProviders,
|
||||
restoreAvailableDefaultTaskProvider,
|
||||
resolveVisibleTaskProvider
|
||||
restoreAvailableDefaultTaskProvider
|
||||
} from './task-providers'
|
||||
|
||||
describe('task providers', () => {
|
||||
it('normalizes provider lists while preserving supported order', () => {
|
||||
expect(normalizeVisibleTaskProviders(['gitlab', 'unknown', 'gitlab', 'linear'])).toEqual([
|
||||
'gitlab',
|
||||
'linear'
|
||||
])
|
||||
})
|
||||
|
||||
it('falls back to all providers when none are visible', () => {
|
||||
expect(normalizeVisibleTaskProviders([])).toEqual(['github', 'gitlab', 'linear', 'jira'])
|
||||
})
|
||||
|
||||
it('restores a valid saved default when provider settings drifted', () => {
|
||||
expect(
|
||||
normalizeTaskProviderSettings({
|
||||
@@ -43,10 +30,6 @@ describe('task providers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('resolves hidden preferred providers to the first visible provider', () => {
|
||||
expect(resolveVisibleTaskProvider('github', ['linear'])).toBe('linear')
|
||||
})
|
||||
|
||||
it('filters runtime-unavailable providers without changing preference normalization', () => {
|
||||
expect(
|
||||
filterAvailableTaskProviders(['github', 'gitlab', 'linear'], {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user