mirror of
https://github.com/stablyai/orca.git
synced 2026-11-14 09:57:48 +00:00
test: remove 104 low-value test declarations (#37290)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseHostPluginManifest, parsePluginManifest } from '../manifest/plugin-manifest'
|
||||
import { parsePluginManifest } from '../manifest/plugin-manifest'
|
||||
import { pluginSettingValueIssue } from './plugin-settings-contribution'
|
||||
import { pluginSettingDefaults } from './plugin-settings-values'
|
||||
|
||||
function manifest(settings: unknown[], capabilities = [{ kind: 'settings:own' }]) {
|
||||
return {
|
||||
@@ -29,36 +28,6 @@ function issues(raw: unknown): string {
|
||||
}
|
||||
|
||||
describe('contributes.settings', () => {
|
||||
it('accepts a typed setting list and reads defaults from it', () => {
|
||||
const parsed = parseHostPluginManifest(
|
||||
manifest([
|
||||
apiUrl,
|
||||
{
|
||||
key: 'pageSize',
|
||||
title: 'Page size',
|
||||
type: 'integer',
|
||||
default: 20,
|
||||
minimum: 1,
|
||||
maximum: 100
|
||||
},
|
||||
{
|
||||
key: 'mode',
|
||||
title: 'Mode',
|
||||
type: 'string',
|
||||
enum: ['fast', 'safe'],
|
||||
enumLabels: ['Fast', 'Safe']
|
||||
}
|
||||
])
|
||||
)
|
||||
expect(parsed.ok).toBe(true)
|
||||
if (parsed.ok) {
|
||||
expect(pluginSettingDefaults(parsed.manifest.contributes.settings)).toEqual({
|
||||
apiUrl: 'https://example.com',
|
||||
pageSize: 20
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('needs settings:own, unique keys, and defaults that fit the setting', () => {
|
||||
expect(issues(manifest([apiUrl], []))).toContain('settings:own')
|
||||
expect(issues(manifest([apiUrl, apiUrl]))).toContain('duplicate setting key')
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isPluginDeclarativeComponentName,
|
||||
PLUGIN_DECLARATIVE_CATALOG
|
||||
} from './plugin-declarative-catalog'
|
||||
import { isPluginDeclarativeImagePath } from './plugin-declarative-content-props'
|
||||
import {
|
||||
PLUGIN_DECLARATIVE_EDIT_PROP,
|
||||
PLUGIN_DECLARATIVE_VALUE_CONTROLS
|
||||
} from './plugin-declarative-message-names'
|
||||
import { PLUGIN_DECLARATIVE_LIMITS as LIMITS } from './plugin-declarative-limits'
|
||||
import { parsePluginDeclarativeTree } from './plugin-declarative-tree'
|
||||
|
||||
@@ -20,51 +12,6 @@ const parse = (raw: unknown) => parsePluginDeclarativeTree(raw)
|
||||
const columns = [{ key: 'name', label: 'Name', sortable: true }]
|
||||
|
||||
describe('catalog 2 components', () => {
|
||||
it('gives every value control its value prop, change callback and edit tag', () => {
|
||||
for (const [type, { value, change }] of Object.entries(PLUGIN_DECLARATIVE_VALUE_CONTROLS)) {
|
||||
expect(isPluginDeclarativeComponentName(type)).toBe(true)
|
||||
const shape: Record<string, unknown> = isPluginDeclarativeComponentName(type)
|
||||
? PLUGIN_DECLARATIVE_CATALOG[type].props.shape
|
||||
: {}
|
||||
expect({
|
||||
type,
|
||||
props: [value, change, PLUGIN_DECLARATIVE_EDIT_PROP].filter((p) => p in shape)
|
||||
}).toEqual({ type, props: [value, change, PLUGIN_DECLARATIVE_EDIT_PROP] })
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts every new component in its place', () => {
|
||||
const result = parse(
|
||||
node('Stack', {}, [
|
||||
node('Field', { label: 'Notes', error: 'Required' }, [
|
||||
node('Textarea', { label: 'Notes', rows: 3 })
|
||||
]),
|
||||
node('Checkbox', { label: 'Agree', defaultChecked: true }),
|
||||
node('RadioGroup', { label: 'Size', options: [{ value: 's', label: 'Small' }] }),
|
||||
node('Slider', { label: 'Volume', min: 0, max: 10, step: 0.5 }),
|
||||
node('Tabs', { label: 'Views', defaultValue: 'a' }, [
|
||||
node('Tab', { value: 'a', label: 'A' }, [node('Text', {}, ['first'])]),
|
||||
node('Tab', { value: 'b', label: 'B' })
|
||||
]),
|
||||
node('Collapsible', { title: 'More' }, [node('Text', {}, ['hidden'])]),
|
||||
node('Toolbar', { label: 'Actions' }, [node('Button', { tooltip: 'Run it' }, ['Run'])]),
|
||||
node('Grid', { columns: 3 }, [node('Card', { title: 'One' })]),
|
||||
node('Table', { label: 'Files', columns, rows: [{ key: 'r1', cells: { name: 'a.ts' } }] }),
|
||||
node('Tree', {
|
||||
label: 'Folders',
|
||||
items: [{ key: 'src', label: 'src', children: [{ key: 'src/a', label: 'a' }] }]
|
||||
}),
|
||||
node('Markdown', { content: '# Hi\n\n[docs](https://example.com)' }),
|
||||
node('CodeView', { code: 'let a = 1', language: 'typescript', lineNumbers: true }),
|
||||
node('Image', { src: 'assets/logo.png', alt: 'Logo' }),
|
||||
node('KeyValue', { items: [{ label: 'Branch', value: 'main', code: true }] }),
|
||||
node('Callout', { tone: 'warning', title: 'Heads up' }, ['Careful']),
|
||||
node('Skeleton', { lines: 2 })
|
||||
])
|
||||
)
|
||||
expect(result).toMatchObject({ ok: true, warnings: [] })
|
||||
})
|
||||
|
||||
it('keeps Tab inside Tabs and Tabs holding only Tab', () => {
|
||||
expect(parse(node('Stack', {}, [node('Tab', { value: 'a', label: 'A' })]))).toMatchObject({
|
||||
ok: false,
|
||||
@@ -80,15 +27,6 @@ describe('catalog 2 components', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps new controls out of a pressable ListItem', () => {
|
||||
const result = parse(
|
||||
node('List', {}, [
|
||||
node('ListItem', { onPress: { $callback: 'c1' } }, [node('Checkbox', { label: 'x' })])
|
||||
])
|
||||
)
|
||||
expect(result).toMatchObject({ ok: false, error: expect.stringContaining('Checkbox') })
|
||||
})
|
||||
|
||||
it('draws a placeholder for a Table or Tree with unusable data, and keeps the rest', () => {
|
||||
const duplicateRows = node('Table', {
|
||||
label: 'T',
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parsePluginDeclarativeTree } from './plugin-declarative-tree'
|
||||
import {
|
||||
formatPluginRelativeTime,
|
||||
pluginRelativeTimeParts,
|
||||
pluginRelativeTimeTickMs
|
||||
} from './plugin-declarative-relative-time'
|
||||
|
||||
const NOW = Date.UTC(2026, 10, 1, 12)
|
||||
|
||||
describe('relative time', () => {
|
||||
it('picks the largest whole unit and words it in each format', () => {
|
||||
expect(pluginRelativeTimeParts(-45_000)).toEqual({ value: -45, unit: 'second' })
|
||||
expect(pluginRelativeTimeParts(-5 * 60_000 - 10)).toEqual({ value: -5, unit: 'minute' })
|
||||
expect(pluginRelativeTimeParts(3 * 86_400_000)).toEqual({ value: 3, unit: 'day' })
|
||||
const at = NOW - 5 * 60_000
|
||||
expect(formatPluginRelativeTime(at, NOW, 'short', 'en')).toBe('5m ago')
|
||||
expect(formatPluginRelativeTime(at, NOW, 'long', 'en')).toBe('5 minutes ago')
|
||||
expect(formatPluginRelativeTime(at, NOW, 'duration', 'en')).toBe('5m')
|
||||
expect(formatPluginRelativeTime(NOW + 2 * 3_600_000, NOW, 'long', 'en')).toBe('in 2 hours')
|
||||
})
|
||||
|
||||
it('ticks often only while the words change often', () => {
|
||||
expect(pluginRelativeTimeTickMs(-10_000)).toBe(1000)
|
||||
expect(pluginRelativeTimeTickMs(-10 * 60_000)).toBe(15_000)
|
||||
expect(pluginRelativeTimeTickMs(-5 * 3_600_000)).toBe(300_000)
|
||||
})
|
||||
|
||||
it('accepts RelativeTime inside Text and relative Table cells', () => {
|
||||
const parsed = parsePluginDeclarativeTree({
|
||||
type: 'Stack',
|
||||
props: {},
|
||||
children: [
|
||||
{
|
||||
type: 'Text',
|
||||
props: {},
|
||||
children: ['Started ', { type: 'RelativeTime', props: { at: NOW, format: 'long' } }]
|
||||
},
|
||||
{
|
||||
type: 'Table',
|
||||
props: {
|
||||
label: 'Agents',
|
||||
columns: [{ key: 'since', label: 'Since' }],
|
||||
rows: [
|
||||
{ key: 'a', cells: { since: { type: 'relative', at: NOW, format: 'duration' } } }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
expect(parsed).toMatchObject({ ok: true, warnings: [] })
|
||||
const bad = parsePluginDeclarativeTree({
|
||||
type: 'RelativeTime',
|
||||
props: { at: Number.POSITIVE_INFINITY }
|
||||
})
|
||||
expect(bad.ok && bad.tree?.type).toBe('Unsupported')
|
||||
})
|
||||
})
|
||||
@@ -1,13 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
PLUGIN_DECLARATIVE_CATALOG,
|
||||
PLUGIN_DECLARATIVE_COMPONENT_NAMES
|
||||
} from './plugin-declarative-catalog'
|
||||
import { PLUGIN_DECLARATIVE_LIMITS } from './plugin-declarative-limits'
|
||||
import {
|
||||
PLUGIN_DECLARATIVE_COMPONENT_PROPS,
|
||||
PLUGIN_DECLARATIVE_EDIT_PROP
|
||||
} from './plugin-declarative-message-names'
|
||||
import {
|
||||
parsePluginDeclarativeRender,
|
||||
pluginDeclarativeEventPayloadSchema
|
||||
@@ -26,47 +18,6 @@ function errorOf(tree: unknown): string {
|
||||
}
|
||||
|
||||
describe('parsePluginDeclarativeTree', () => {
|
||||
it('keeps the zod catalog and the runtime prop table in step', () => {
|
||||
expect([...PLUGIN_DECLARATIVE_COMPONENT_NAMES].sort()).toEqual(
|
||||
Object.keys(PLUGIN_DECLARATIVE_COMPONENT_PROPS).sort()
|
||||
)
|
||||
for (const name of PLUGIN_DECLARATIVE_COMPONENT_NAMES) {
|
||||
const zodProps = Object.keys(PLUGIN_DECLARATIVE_CATALOG[name].props.shape).filter(
|
||||
(prop) => prop !== PLUGIN_DECLARATIVE_EDIT_PROP
|
||||
)
|
||||
const runtimeProps: readonly string[] = PLUGIN_DECLARATIVE_COMPONENT_PROPS[name]
|
||||
expect({ name, props: [...runtimeProps].sort() }).toEqual({ name, props: zodProps.sort() })
|
||||
}
|
||||
})
|
||||
|
||||
it('accepts a valid tree, normalizes numbers and drops nothing it knows', () => {
|
||||
const code = node('Code', {}, ['x'])
|
||||
const text = node('Text', { tone: 'accent', muted: true }, ['Count ', 4, code])
|
||||
const rest = [
|
||||
node('Button', { onPress: { $callback: 'c1' }, icon: 'add', tone: 'danger' }, ['Add']),
|
||||
node('List', { label: 'Items' }, [{ ...node('ListItem', { title: 'A' }), key: 'a' }]),
|
||||
node('Select', {
|
||||
label: 'Pick',
|
||||
options: [{ value: 'a', label: 'A' }],
|
||||
onChange: { $callback: 'c2' }
|
||||
}),
|
||||
node('Link', { href: 'https://example.com' }, ['docs'])
|
||||
]
|
||||
const tree = node('Stack', { gap: 'sm' }, [text, ...rest])
|
||||
const result = parsePluginDeclarativeTree(tree)
|
||||
expect(result).toMatchObject({ ok: true, nodes: 8, warnings: [] })
|
||||
expect(result.ok && result.tree).toEqual({
|
||||
...tree,
|
||||
children: [{ ...text, children: ['Count ', '4', code] }, ...rest]
|
||||
})
|
||||
expect(parsePluginDeclarativeTree(null)).toEqual({
|
||||
ok: true,
|
||||
tree: null,
|
||||
nodes: 0,
|
||||
warnings: []
|
||||
})
|
||||
})
|
||||
|
||||
it('drops unknown props and enum values with a warning instead of refusing', () => {
|
||||
const result = parsePluginDeclarativeTree(
|
||||
node('Stack', { className: 'p-4', gap: 'huge' }, [
|
||||
@@ -134,11 +85,6 @@ describe('parsePluginDeclarativeTree', () => {
|
||||
expect(errorOf(tree)).toMatch(message)
|
||||
})
|
||||
|
||||
it('allows controls inside a ListItem that has no onPress', () => {
|
||||
const tree = node('List', {}, [node('ListItem', {}, [node('Button', {}, ['x'])])])
|
||||
expect(parsePluginDeclarativeTree(tree).ok).toBe(true)
|
||||
})
|
||||
|
||||
it('caps depth without recursing', () => {
|
||||
let tree = node('Text')
|
||||
for (let depth = 0; depth < 10_000; depth++) {
|
||||
|
||||
@@ -1,175 +0,0 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import ts from 'typescript-api'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
PLUGIN_DECLARATIVE_CATALOG,
|
||||
PLUGIN_DECLARATIVE_COMPONENT_NAMES,
|
||||
PLUGIN_DECLARATIVE_ICONS,
|
||||
PLUGIN_DECLARATIVE_TONES
|
||||
} from './plugin-declarative-catalog'
|
||||
import { spliceOrcaHostTypings } from './plugin-declarative-host-typings'
|
||||
import * as orcaModule from './runtime/orca-module'
|
||||
|
||||
const DECLARATIVE = path.resolve(import.meta.dirname, '../../../../resources/plugins/declarative')
|
||||
const TYPINGS = path.join(DECLARATIVE, 'orca.d.ts')
|
||||
// The `orca` module spans these files; `orca plugin types` writes them as one.
|
||||
const FILES = [
|
||||
'orca.d.ts',
|
||||
'orca-components.d.ts',
|
||||
'orca-components-layout.d.ts',
|
||||
'orca-components-data.d.ts'
|
||||
]
|
||||
|
||||
function orcaModules(): ts.ModuleBlock[] {
|
||||
return FILES.flatMap((file) => {
|
||||
const full = path.join(DECLARATIVE, file)
|
||||
const source = ts.createSourceFile(
|
||||
full,
|
||||
readFileSync(full, 'utf8'),
|
||||
ts.ScriptTarget.Latest,
|
||||
true
|
||||
)
|
||||
return source.statements.flatMap((statement) =>
|
||||
ts.isModuleDeclaration(statement) &&
|
||||
ts.isStringLiteral(statement.name) &&
|
||||
statement.name.text === 'orca' &&
|
||||
statement.body &&
|
||||
ts.isModuleBlock(statement.body)
|
||||
? [statement.body]
|
||||
: []
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
const aliases = new Map<string, ts.TypeNode>()
|
||||
const components = new Map<string, string>()
|
||||
for (const block of orcaModules()) {
|
||||
for (const statement of block.statements) {
|
||||
if (ts.isTypeAliasDeclaration(statement)) {
|
||||
aliases.set(statement.name.text, statement.type)
|
||||
}
|
||||
if (ts.isVariableStatement(statement)) {
|
||||
for (const declaration of statement.declarationList.declarations) {
|
||||
const type = declaration.type
|
||||
if (
|
||||
ts.isIdentifier(declaration.name) &&
|
||||
type &&
|
||||
ts.isTypeReferenceNode(type) &&
|
||||
type.typeName.getText() === 'OrcaComponent' &&
|
||||
type.typeArguments?.[0]
|
||||
) {
|
||||
components.set(declaration.name.text, type.typeArguments[0].getText())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** String literals of a union, following local aliases; null when it is not a literal union. */
|
||||
function literals(node: ts.TypeNode): string[] | null {
|
||||
if (ts.isTypeReferenceNode(node)) {
|
||||
const target = aliases.get(node.typeName.getText())
|
||||
return target ? literals(target) : null
|
||||
}
|
||||
if (ts.isUnionTypeNode(node)) {
|
||||
const parts = node.types.map(literals)
|
||||
return parts.every((part) => part !== null) ? parts.flat() : null
|
||||
}
|
||||
if (ts.isLiteralTypeNode(node) && ts.isStringLiteral(node.literal)) {
|
||||
return [node.literal.text]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
type Member = { optional: boolean; type: ts.TypeNode | undefined }
|
||||
|
||||
/** Members of a props alias such as `WithChildren & { ... }`, plus whether it is a leaf. */
|
||||
function propsOf(alias: string): { members: Map<string, Member>; leaf: boolean } {
|
||||
const node = aliases.get(alias)
|
||||
if (!node) {
|
||||
throw new Error(`orca.d.ts has no type ${alias}`)
|
||||
}
|
||||
const parts = ts.isIntersectionTypeNode(node) ? node.types : [node]
|
||||
const members = new Map<string, Member>()
|
||||
let leaf = false
|
||||
for (const part of parts) {
|
||||
if (ts.isTypeReferenceNode(part)) {
|
||||
leaf ||= part.typeName.getText() === 'Leaf'
|
||||
} else if (ts.isTypeLiteralNode(part)) {
|
||||
for (const member of part.members) {
|
||||
if (ts.isPropertySignature(member) && member.name) {
|
||||
members.set(member.name.getText(), {
|
||||
optional: member.questionToken !== undefined,
|
||||
type: member.type
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { members, leaf }
|
||||
}
|
||||
|
||||
type ShapeEntry = {
|
||||
safeParse(value: unknown): { success: boolean }
|
||||
unwrap?: () => unknown
|
||||
}
|
||||
|
||||
function enumOptions(schema: ShapeEntry): string[] | null {
|
||||
const inner: unknown = schema.unwrap ? schema.unwrap() : schema
|
||||
const options: unknown =
|
||||
typeof inner === 'object' && inner !== null && 'options' in inner ? inner.options : undefined
|
||||
return Array.isArray(options) && options.every((option) => typeof option === 'string')
|
||||
? options
|
||||
: null
|
||||
}
|
||||
|
||||
describe('orca.d.ts matches the catalog', () => {
|
||||
it('declares exactly the catalog components', () => {
|
||||
expect([...components.keys()].sort()).toEqual([...PLUGIN_DECLARATIVE_COMPONENT_NAMES].sort())
|
||||
})
|
||||
|
||||
it.each(PLUGIN_DECLARATIVE_COMPONENT_NAMES)('%s props, requiredness and enums', (name) => {
|
||||
const entry = PLUGIN_DECLARATIVE_CATALOG[name]
|
||||
const shape: Record<string, ShapeEntry> = entry.props.shape
|
||||
const { members, leaf } = propsOf(components.get(name) ?? '')
|
||||
expect(leaf).toBe(entry.children === 'none')
|
||||
// `$`-props are wire-only (the runtime adds them); authors never write them.
|
||||
const authored = Object.entries(shape).filter(([prop]) => !prop.startsWith('$'))
|
||||
expect([...members.keys()].sort()).toEqual(authored.map(([prop]) => prop).sort())
|
||||
for (const [prop, schema] of authored) {
|
||||
const member = members.get(prop)
|
||||
expect({ prop, optional: member?.optional }).toEqual({
|
||||
prop,
|
||||
optional: schema.safeParse(undefined).success
|
||||
})
|
||||
const options = enumOptions(schema)
|
||||
if (options && member?.type) {
|
||||
expect({ prop, values: literals(member.type)?.sort() }).toEqual({
|
||||
prop,
|
||||
values: [...options].sort()
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('lists the same component names, and generates host methods from the host API table', () => {
|
||||
expect(literals(aliases.get('ComponentName')!)?.sort()).toEqual(
|
||||
[...PLUGIN_DECLARATIVE_COMPONENT_NAMES].sort()
|
||||
)
|
||||
const file = readFileSync(TYPINGS, 'utf8')
|
||||
expect(spliceOrcaHostTypings(file)).toBe(file)
|
||||
})
|
||||
|
||||
it('exports every catalog component from the guest `orca` module, as its own tag', () => {
|
||||
const exported: Record<string, unknown> = { ...orcaModule }
|
||||
for (const name of PLUGIN_DECLARATIVE_COMPONENT_NAMES) {
|
||||
expect({ name, tag: exported[name] }).toEqual({ name, tag: name })
|
||||
}
|
||||
})
|
||||
|
||||
it('names the same tones and icons', () => {
|
||||
expect(literals(aliases.get('Tone')!)?.sort()).toEqual([...PLUGIN_DECLARATIVE_TONES].sort())
|
||||
expect(literals(aliases.get('IconName')!)?.sort()).toEqual([...PLUGIN_DECLARATIVE_ICONS].sort())
|
||||
})
|
||||
})
|
||||
@@ -69,10 +69,4 @@ describe('plugin errors as logged', () => {
|
||||
})
|
||||
expect(describePluginError(refused, base)).toBe('src/lib/a.ts:2:19: cannot import "react"')
|
||||
})
|
||||
|
||||
it('logs the plain stack outside a guest', () => {
|
||||
const error = Object.assign(new Error('boom'), { stack: 'Error: boom\n at x (/a.js:1:1)' })
|
||||
expect(describePluginErrorForLog(error, null)).toBe('Error: boom\n at x (/a.js:1:1)')
|
||||
expect(describePluginErrorForLog('thrown', base)).toBe('thrown')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PLUGIN_FRAME_RESERVED_SEGMENT } from '../../frames/plugin-frame-guest'
|
||||
import { atom, read, set } from './declarative-atoms'
|
||||
import { h } from './declarative-elements'
|
||||
import {
|
||||
describePluginError,
|
||||
ORCA_RESERVED_PATH_PREFIX,
|
||||
pluginErrorLocation
|
||||
} from './declarative-error-location'
|
||||
import { createOrcaDeclarativeRuntime, type OrcaBridge } from './declarative-renderer'
|
||||
|
||||
function booted() {
|
||||
@@ -120,32 +114,3 @@ describe('dev hot reload of keyed memory atoms', () => {
|
||||
runtime.dispose()
|
||||
})
|
||||
})
|
||||
|
||||
describe('plugin error locations', () => {
|
||||
const base = 'orca-plugin-frame://0123456789abcdef/'
|
||||
|
||||
it('names the first plugin stack frame, skipping Orca runtime frames', () => {
|
||||
const stack = [
|
||||
'TypeError: x is undefined',
|
||||
` at evaluate (${base}__orca/orca-runtime.mjs:120:7)`,
|
||||
` at Panel (${base}src/my%20panel.tsx:12:5)`,
|
||||
` at ${base}src/other.ts:1:1`
|
||||
].join('\n')
|
||||
expect(pluginErrorLocation(stack, base)).toBe('src/my panel.tsx:12:5')
|
||||
const error = Object.assign(new TypeError('x is undefined'), { stack })
|
||||
expect(describePluginError(error, base)).toBe('src/my panel.tsx:12:5: x is undefined')
|
||||
})
|
||||
|
||||
it("keeps a message that already names its file (Orca's transpile errors)", () => {
|
||||
const error = Object.assign(new Error('src/panel.tsx:3:5: Unexpected token'), {
|
||||
stack: `Error: src/panel.tsx:3:5: Unexpected token\n at ${base}src/panel.tsx:1:7`
|
||||
})
|
||||
expect(describePluginError(error, base)).toBe('src/panel.tsx:3:5: Unexpected token')
|
||||
})
|
||||
|
||||
it('falls back to the bare message outside a guest or without a plugin frame', () => {
|
||||
expect(describePluginError(new Error('boom'), null)).toBe('boom')
|
||||
expect(describePluginError('thrown string', base)).toBe('thrown string')
|
||||
expect(ORCA_RESERVED_PATH_PREFIX).toBe(`/${PLUGIN_FRAME_RESERVED_SEGMENT}/`)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -135,14 +135,6 @@ describe('components panel lifecycle', () => {
|
||||
expect(seen.signal?.aborted).toBe(true)
|
||||
})
|
||||
|
||||
it('says clearly when the module has no component', async () => {
|
||||
const { runtime, host } = booted()
|
||||
host.send('ui.hydrate', {})
|
||||
await expect(runtime.boot({ default: 'nope' })).rejects.toThrow(
|
||||
/must export a component function as its default export \(got string\)/
|
||||
)
|
||||
})
|
||||
|
||||
it('refuses duplicate atom keys', () => {
|
||||
atom('lifecycle.once', 1)
|
||||
expect(() => atom('lifecycle.once', 2)).toThrow(/already used/)
|
||||
|
||||
@@ -89,9 +89,4 @@ describe('orca/testing and the plugin bridge', () => {
|
||||
await expect(settings.get('theme')).resolves.toBe('dark')
|
||||
expect(host.data().storage).toEqual({ filter: 'open' })
|
||||
})
|
||||
|
||||
it('refuses storage without the capability, as Orca would', async () => {
|
||||
panel = await mount(() => h('Text', null, 'x'), { host: mockHost({ capabilities: [] }) })
|
||||
await expect(storage.get('k')).rejects.toMatchObject({ code: 'capability_denied' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -158,41 +158,3 @@ describe('orca/testing with catalog 2', () => {
|
||||
expect(panel.errors).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('summary and responsive columns', () => {
|
||||
it('summarizes what a person notices first, and which columns fit a width', async () => {
|
||||
panel = await mount(() =>
|
||||
h(
|
||||
'Stack',
|
||||
null,
|
||||
h('Heading', null, 'Review queue'),
|
||||
h('Callout', { tone: 'danger', title: 'Could not load the queue' }, 'offline'),
|
||||
h('Table', {
|
||||
label: 'PRs',
|
||||
columns: [
|
||||
{ key: 'title', label: 'Title', minWidth: 'sm' },
|
||||
{ key: 'author', label: 'Author', hideBelow: 'sm' },
|
||||
{ key: 'checks', label: 'Checks', hideBelow: 'md' }
|
||||
],
|
||||
rows: [
|
||||
{ key: 'a', cells: { title: 'One' } },
|
||||
{ key: 'b', cells: { title: 'Two' } }
|
||||
]
|
||||
}),
|
||||
h('List', { label: 'Later' }, h('ListItem', { title: 'x' })),
|
||||
h('EmptyState', { title: 'All caught up' })
|
||||
)
|
||||
)
|
||||
expect(panel.summary()).toEqual([
|
||||
'Heading "Review queue"',
|
||||
'Callout danger "Could not load the queue"',
|
||||
'Table "PRs": 2 row(s)',
|
||||
'List "Later": 1 item(s)',
|
||||
'EmptyState "All caught up"'
|
||||
])
|
||||
const table = { type: 'Table' }
|
||||
expect(panel.columnsOf(table)).toEqual(['title', 'author', 'checks'])
|
||||
expect(panel.columnsOf(table, { width: 400 })).toEqual(['title', 'author'])
|
||||
expect(panel.columnsOf(table, { width: 300 })).toEqual(['title'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { exportedTypeShape, runtimeShape } from '../../authoring/plugin-author-typings.test-fixture'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { agentStatusChangedPayloadSchema } from '../../events/plugin-events'
|
||||
import { atom, read, set } from '../runtime/declarative-atoms'
|
||||
import { h } from '../runtime/declarative-elements'
|
||||
@@ -128,15 +127,6 @@ describe('mountWorker', () => {
|
||||
)
|
||||
).rejects.toThrow(/registered undeclared command other/)
|
||||
})
|
||||
|
||||
it('hands activate the same shape of orca object as Orca does', async () => {
|
||||
let captured: unknown
|
||||
await mountWorker(
|
||||
{ default: (orca: unknown) => (captured = orca) },
|
||||
{ events: [], commands: [] }
|
||||
)
|
||||
expect(runtimeShape(captured)).toEqual(exportedTypeShape('orca/worker', 'OrcaWorkerApi').shape)
|
||||
})
|
||||
})
|
||||
|
||||
describe('panel refusals', () => {
|
||||
@@ -197,30 +187,3 @@ describe('a panel and its worker on one host', () => {
|
||||
await worker.deactivate()
|
||||
})
|
||||
})
|
||||
|
||||
describe('RelativeTime in the test kit', () => {
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('reads as Orca draws it now, in a node and in a Table cell', async () => {
|
||||
// Only Date is faked: the kit settles on real macrotasks.
|
||||
vi.useFakeTimers({ now: Date.UTC(2026, 10, 1, 12), toFake: ['Date'] })
|
||||
const at = Date.UTC(2026, 10, 1, 12) - 5 * 60_000
|
||||
panel = await mount(() =>
|
||||
h(
|
||||
'Stack',
|
||||
null,
|
||||
h('Text', null, 'Started ', h('RelativeTime', { at })),
|
||||
h('Table', {
|
||||
label: 'Agents',
|
||||
columns: [{ key: 'since', label: 'Since' }],
|
||||
rows: [{ key: 'a', cells: { since: { type: 'relative', at, format: 'duration' } } }]
|
||||
})
|
||||
)
|
||||
)
|
||||
expect(panel.find({ role: 'time' })?.text).toBe('5m ago')
|
||||
expect(panel.find({ role: 'grid' })?.text).toBe('5m')
|
||||
expect(panel.warnings).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { atom, read, set, update } from '../runtime/declarative-atoms'
|
||||
import { h } from '../runtime/declarative-elements'
|
||||
import { host, on, shell } from '../runtime/orca-module'
|
||||
import { host, shell } from '../runtime/orca-module'
|
||||
import { hostError, mockHost, mount, stored, type TestPanel } from './orca-testing'
|
||||
|
||||
let panel: TestPanel | null = null
|
||||
@@ -156,28 +156,6 @@ describe('orca/testing', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('delivers events and visibility, and runs setup cleanup on unmount', async () => {
|
||||
const seen: string[] = []
|
||||
let cleaned = false
|
||||
panel = await mount({
|
||||
setup: (ctx: {
|
||||
subscriptions: { dispose(): void }[]
|
||||
visible: Parameters<typeof read<boolean>>[0]
|
||||
}) => {
|
||||
ctx.subscriptions.push(on('theme', (payload) => seen.push(JSON.stringify(payload))))
|
||||
return () => {
|
||||
cleaned = true
|
||||
}
|
||||
},
|
||||
default: () => h('Text', null, 'x')
|
||||
})
|
||||
await panel.emit('theme', { colorScheme: 'light' })
|
||||
expect(seen).toEqual(['{"colorScheme":"light"}'])
|
||||
await panel.unmount()
|
||||
panel = null
|
||||
expect(cleaned).toBe(true)
|
||||
})
|
||||
|
||||
it('fails a test that never settles instead of hanging', async () => {
|
||||
const ticks = atom(0)
|
||||
const loop = () => {
|
||||
|
||||
@@ -56,17 +56,6 @@ describe('window.orca.plugin', () => {
|
||||
answer(sent.id, { output: [{ id: '1' }] })
|
||||
await expect(todos).resolves.toEqual([{ id: '1' }])
|
||||
})
|
||||
|
||||
it('hands worker posts to plugin.on by event name', () => {
|
||||
const { orca, push } = mountShell()
|
||||
const changed = vi.fn()
|
||||
const other = vi.fn()
|
||||
orca.plugin.on('todos.changed', changed)
|
||||
orca.plugin.on('other', other)
|
||||
push({ kind: 'message', event: 'todos.changed', payload: { count: 3 } })
|
||||
expect(changed).toHaveBeenCalledWith({ count: 3 })
|
||||
expect(other).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('window.orca events and data', () => {
|
||||
@@ -85,21 +74,4 @@ describe('window.orca events and data', () => {
|
||||
push({ kind: 'event', event: 'agent.status.changed', payload: { state: 'done' } })
|
||||
expect(first).toHaveBeenCalledWith({ state: 'done' })
|
||||
})
|
||||
|
||||
it('reads storage and settings, and hears which key changed', async () => {
|
||||
const { orca, requests, answer, push } = mountShell()
|
||||
const value = orca.storage.get('todos')
|
||||
const setting = orca.settings.get('apiUrl')
|
||||
const [get, settings] = requests()
|
||||
answer(get.id, { value: null })
|
||||
answer(settings.id, { settings: { apiUrl: 'https://x' } })
|
||||
await expect(value).resolves.toBeNull()
|
||||
await expect(setting).resolves.toBe('https://x')
|
||||
const changed = vi.fn()
|
||||
orca.storage.onDidChange(changed)
|
||||
push({ kind: 'changed', area: 'settings', key: 'apiUrl' })
|
||||
push({ kind: 'changed', area: 'storage', key: 'todos' })
|
||||
expect(changed).toHaveBeenCalledOnce()
|
||||
expect(changed).toHaveBeenCalledWith({ key: 'todos' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createHash } from 'node:crypto'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
isPluginFrameOriginUrl,
|
||||
@@ -12,11 +11,6 @@ import {
|
||||
const KEY = pluginFrameKey('acme.charts')
|
||||
|
||||
describe('plugin frame guest identity', () => {
|
||||
it('derives the key as the first 16 hex chars of sha256(publisher.id)', () => {
|
||||
expect(KEY).toBe(createHash('sha256').update('acme.charts').digest('hex').slice(0, 16))
|
||||
expect(pluginFrameKey('acme.other')).not.toBe(KEY)
|
||||
})
|
||||
|
||||
it('round-trips the partition and refuses look-alikes', () => {
|
||||
expect(pluginFramePartition(KEY)).toBe(`persist:orca-plugin-${KEY}`)
|
||||
expect(readPluginFramePartitionKey(pluginFramePartition(KEY))).toBe(KEY)
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
exportedTypeShape,
|
||||
runtimeShape,
|
||||
type TypingsShape
|
||||
} from '../authoring/plugin-author-typings.test-fixture'
|
||||
import { buildPluginGuestShellHtml, buildPluginPanelShellHtml } from './plugin-panel-shell'
|
||||
|
||||
/** The `window.orca` a frame shell defines, read without leaving it on the test window. */
|
||||
function frameOrca(html: string): unknown {
|
||||
const script = html.match(/<script>\n([\s\S]*?)<\/script>/)?.[1] ?? ''
|
||||
const transport = { send: () => {}, subscribe: () => {} }
|
||||
Object.defineProperty(window, '__orcaPluginFrameTransport', {
|
||||
value: transport,
|
||||
configurable: true
|
||||
})
|
||||
window.eval(script.replaceAll('configurable: false', 'configurable: true'))
|
||||
const orca: unknown = Object.getOwnPropertyDescriptor(window, 'orca')?.value
|
||||
Reflect.deleteProperty(window, 'orca')
|
||||
return orca
|
||||
}
|
||||
|
||||
function without(shape: TypingsShape, keys: string[]): TypingsShape {
|
||||
return typeof shape === 'string'
|
||||
? shape
|
||||
: Object.fromEntries(Object.entries(shape).filter(([key]) => !keys.includes(key)))
|
||||
}
|
||||
|
||||
const typings = exportedTypeShape('orca/frame', 'OrcaFrameApi')
|
||||
|
||||
describe('orca-frame.d.ts matches window.orca', () => {
|
||||
it('declares every member an isolated editor frame gets', () => {
|
||||
expect(runtimeShape(frameOrca(buildPluginGuestShellHtml('', 'editor')))).toEqual(typings.shape)
|
||||
})
|
||||
|
||||
it('marks optional exactly what an iframe panel lacks', () => {
|
||||
const panel = runtimeShape(frameOrca(buildPluginPanelShellHtml('', 'panel')))
|
||||
const absent = ['shell', 'files', 'document']
|
||||
expect(typings.optional.filter((name) => !name.includes('.')).sort()).toEqual(absent.sort())
|
||||
expect(panel).toEqual(without(typings.shape, absent))
|
||||
})
|
||||
})
|
||||
@@ -17,11 +17,4 @@ describe('files.saveAs blocked extensions', () => {
|
||||
])('refuses %s', (name, extension) => {
|
||||
expect(blockedPluginSaveAsExtension(name)).toBe(extension)
|
||||
})
|
||||
|
||||
it.each(['scene.excalidraw', 'chart.png', 'data.json', 'notes', '.bashrc', 'archive.tar.gz'])(
|
||||
'allows %s',
|
||||
(name) => {
|
||||
expect(blockedPluginSaveAsExtension(name)).toBeNull()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
@@ -2,13 +2,11 @@ import { describe, expect, it } from 'vitest'
|
||||
import { z } from 'zod'
|
||||
import { pluginEngineFloor } from './plugin-engine-floor'
|
||||
import { PLUGIN_ENUM_SINCE_KEY } from './plugin-engine-floor-keys'
|
||||
import { PLUGIN_SCHEMA_NEWEST_SINCE, pluginHostVersion } from '../installation/plugin-host-version'
|
||||
import { parseHostPluginManifest, parsePluginManifest } from './plugin-manifest'
|
||||
import {
|
||||
PLUGIN_DERIVED_PUBLISHER_SINCE,
|
||||
PLUGIN_MANIFEST_V2_SINCE
|
||||
} from './plugin-schema-field-docs'
|
||||
import { PLUGIN_RESERVED_CONTRIBUTION_POINTS } from '../contributions/plugin-contribution-points'
|
||||
|
||||
function v2(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
||||
return {
|
||||
@@ -69,49 +67,6 @@ describe('manifest format 2 rules', () => {
|
||||
).toMatchObject({ ok: true })
|
||||
})
|
||||
|
||||
it('refuses host API majors this Orca does not implement', () => {
|
||||
const worker = { execution: 'trusted-worker', main: 'main.mjs' }
|
||||
expect(parsePluginManifest(v2({ ...worker, pluginApi: 2 }))).toEqual({
|
||||
ok: false,
|
||||
error: 'pluginApi: targets host API 2, but this Orca supports only 1; update Orca'
|
||||
})
|
||||
expect(parsePluginManifest(v2({ ...worker, pluginApi: 0 }))).toEqual({
|
||||
ok: false,
|
||||
error: 'pluginApi: must be 1'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps contributes.agentHarnesses reserved and leaves agents unclaimed', () => {
|
||||
expect(
|
||||
parsePluginManifest(
|
||||
v2({ execution: 'declarative', contributes: { agentHarnesses: [{ path: 'a.json' }] } })
|
||||
)
|
||||
).toEqual({
|
||||
ok: false,
|
||||
error:
|
||||
'contributes.agentHarnesses: is reserved for coding-agent harness definitions and must be empty for now'
|
||||
})
|
||||
expect(parsePluginManifest(v2({ contributes: { agents: [] } }))).toMatchObject({ ok: false })
|
||||
})
|
||||
|
||||
it.each(Object.entries(PLUGIN_RESERVED_CONTRIBUTION_POINTS))(
|
||||
'reserves contributes.%s: empty is accepted, any item is refused',
|
||||
(key, purpose) => {
|
||||
expect(parsePluginManifest(v2({ contributes: { [key]: [] } }))).toMatchObject({ ok: true })
|
||||
expect(parsePluginManifest(v2({ contributes: { [key]: [{}] } }))).toEqual({
|
||||
ok: false,
|
||||
error: `contributes.${key}: is reserved for ${purpose} and must be empty for now`
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('requires engines.orca to cover every field the manifest uses', () => {
|
||||
expect(parsePluginManifest(v2({ engines: { orca: '>=1.4.160' } }))).toEqual({
|
||||
ok: false,
|
||||
error: `engines.orca: must be at least >=${PLUGIN_MANIFEST_V2_SINCE}, because manifestVersion needs Orca ${PLUGIN_MANIFEST_V2_SINCE}`
|
||||
})
|
||||
})
|
||||
|
||||
it('skips only the engines floor when reading a manifest at runtime', () => {
|
||||
const underDeclared = v2({ engines: { orca: '>=1.4.160' } })
|
||||
expect(parseHostPluginManifest(underDeclared)).toMatchObject({ ok: true })
|
||||
@@ -143,10 +98,4 @@ describe('manifest format 2 rules', () => {
|
||||
})
|
||||
expect(pluginEngineFloor(schema, { kind: 'old', list: [], extra: 'y' })?.version).toBe('5.0.0')
|
||||
})
|
||||
|
||||
it('gates development builds as the newest release their schemas describe', () => {
|
||||
expect(pluginHostVersion('1.4.214')).toBe(PLUGIN_SCHEMA_NEWEST_SINCE)
|
||||
expect(pluginHostVersion('1.4.214-local.1.abc')).toBe(PLUGIN_SCHEMA_NEWEST_SINCE)
|
||||
expect(pluginHostVersion('99.0.0')).toBe('99.0.0')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -72,21 +72,6 @@ describe('pluginManifestSchema boundaries', () => {
|
||||
expect(result.ok).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts documented dotted command namespaces with camel-case actions', () => {
|
||||
const result = parsePluginManifest(
|
||||
manifest({
|
||||
main: 'main.mjs',
|
||||
contributes: {
|
||||
panels: [],
|
||||
commands: [{ id: 'jupyter.restartKernel', title: 'Restart kernel' }],
|
||||
events: []
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expect(result).toMatchObject({ ok: true })
|
||||
})
|
||||
|
||||
it('rejects oversized identities and invalid semantic versions', () => {
|
||||
expect(parsePluginManifest(manifest({ id: 'a'.repeat(PLUGIN_ID_MAX_LENGTH + 1) })).ok).toBe(
|
||||
false
|
||||
@@ -97,33 +82,6 @@ describe('pluginManifestSchema boundaries', () => {
|
||||
expect(parsePluginManifest(manifest({ version: '1.0.0-alpha.1+build.5' })).ok).toBe(true)
|
||||
})
|
||||
|
||||
it('reports one specific issue per bad id or path', () => {
|
||||
const reserved = pluginManifestSchema.safeParse(manifest({ id: 'constructor' }))
|
||||
expect(reserved.error?.issues.map((issue) => issue.message)).toEqual([
|
||||
'must not be a reserved name'
|
||||
])
|
||||
const badPath = pluginManifestSchema.safeParse(manifest({ main: '../escape.js' }))
|
||||
expect(badPath.error?.issues).toHaveLength(1)
|
||||
const device = pluginManifestSchema.safeParse(manifest({ main: 'dist/CON.js' }))
|
||||
expect(device.error?.issues).toHaveLength(1)
|
||||
})
|
||||
|
||||
it('accepts a $schema URL in v2 manifests only as an ignored hint', () => {
|
||||
const $schema = 'https://example.com/orca-plugin.schema.json'
|
||||
const v2 = parsePluginManifest(
|
||||
manifest({
|
||||
manifestVersion: 2,
|
||||
execution: 'data',
|
||||
contributes: {},
|
||||
pluginApi: undefined,
|
||||
engines: { orca: `>=${PLUGIN_MANIFEST_V2_SINCE}` },
|
||||
$schema
|
||||
})
|
||||
)
|
||||
expect(v2).toMatchObject({ ok: true, manifest: { $schema } })
|
||||
expect(parsePluginManifest(manifest({ $schema }))).toMatchObject({ ok: true })
|
||||
})
|
||||
|
||||
it('rejects duplicate contribution ids', () => {
|
||||
const parsed = pluginManifestSchema.safeParse(
|
||||
manifest({
|
||||
|
||||
@@ -2,45 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import { parsePluginShotActs, PLUGIN_SHOT_MAX_ACTS } from './plugin-shot-acts'
|
||||
|
||||
describe('parsePluginShotActs', () => {
|
||||
it('takes the four verbs, and one act without a list', () => {
|
||||
const json = JSON.stringify([
|
||||
{ press: { text: 'Add' } },
|
||||
{ type: { label: 'New task' }, text: 'Ship', submit: true },
|
||||
{ select: { label: 'View' }, value: 'done' },
|
||||
{ toggle: { testId: 'hide-done' } }
|
||||
])
|
||||
expect(parsePluginShotActs(json)).toMatchObject({ ok: true, acts: { length: 4 } })
|
||||
expect(parsePluginShotActs('{"press":{"role":"button"}}')).toEqual({
|
||||
ok: true,
|
||||
acts: [{ press: { role: 'button' } }]
|
||||
})
|
||||
})
|
||||
|
||||
it("takes the kit's Slider, Table and Tree verbs with their arguments", () => {
|
||||
const table = { type: 'Table' }
|
||||
const tree = { label: 'Folders' }
|
||||
const json = JSON.stringify([
|
||||
{ slide: { label: 'Volume' }, value: 7 },
|
||||
{ sort: table, column: 'name' },
|
||||
{ selectRows: table, keys: ['a', 'b'] },
|
||||
{ activateRow: table, key: 'a' },
|
||||
{ loadMore: table },
|
||||
{ expand: tree, key: 'src' },
|
||||
{ collapse: tree, key: 'src' },
|
||||
{ selectItem: tree, key: 'src/main.ts' },
|
||||
{ activateItem: tree, key: 'src/main.ts' }
|
||||
])
|
||||
expect(parsePluginShotActs(json)).toMatchObject({ ok: true, acts: { length: 9 } })
|
||||
expect(parsePluginShotActs('{"slide":{"label":"Volume"},"value":"7"}')).toMatchObject({
|
||||
ok: false,
|
||||
error: expect.stringMatching(/^action 1 \(slide\) \(value\): /)
|
||||
})
|
||||
expect(parsePluginShotActs('{"expand":{"label":"Folders"}}')).toMatchObject({
|
||||
ok: false,
|
||||
error: expect.stringMatching(/^action 1 \(expand\) \(key\): /)
|
||||
})
|
||||
})
|
||||
|
||||
it('names the act and field that is wrong', () => {
|
||||
expect(parsePluginShotActs('[{"press":{"text":"A"}},{"click":{}}]')).toEqual({
|
||||
ok: false,
|
||||
|
||||
@@ -165,19 +165,6 @@ describe('Windows secure files through the native ACL addon', () => {
|
||||
})
|
||||
|
||||
describe('directory relock', () => {
|
||||
it('relocks the parent once, on the thread pool, across many writes', async () => {
|
||||
const dir = tempDir()
|
||||
for (let i = 0; i < 5; i++) {
|
||||
writeSecureFile(join(dir, `secret-${i}.json`), `contents-${i}`)
|
||||
}
|
||||
await settle()
|
||||
|
||||
expect(fake.callsFor('setAsync', dir)).toEqual([
|
||||
expect.objectContaining({ op: 'setAsync', path: dir, sddl: DIRECTORY_SDDL })
|
||||
])
|
||||
expect(fake.callsFor('set', dir)).toEqual([])
|
||||
})
|
||||
|
||||
it('never rewrites a directory the icacls-era hardening already locked', async () => {
|
||||
const dir = tempDir()
|
||||
fake.dacls.set(dir, ICACLS_ERA_DIRECTORY_SDDL)
|
||||
|
||||
@@ -104,27 +104,4 @@ describe('resolveSourceControlNextStep', () => {
|
||||
expect(step({ review: { kind: 'none' } }).kind).toBe('none')
|
||||
expect(step({ review: { kind: 'loading' }, reviewCreation: 'create' }).kind).toBe('none')
|
||||
})
|
||||
|
||||
it('turns on auto-merge while GitHub waits on a requirement, and says why when nothing can', () => {
|
||||
expect(
|
||||
step({
|
||||
review: {
|
||||
kind: 'github',
|
||||
pr: { ...OPEN_PR, mergeStateStatus: 'BLOCKED' },
|
||||
failedChecks: 0,
|
||||
pendingChecks: 2
|
||||
}
|
||||
})
|
||||
).toEqual({ kind: 'merge_when_ready', mergeState: 'blocked', pendingChecks: 2 })
|
||||
expect(step({}, { reviewDecision: 'REVIEW_REQUIRED', mergeStateStatus: 'BLOCKED' })).toEqual({
|
||||
kind: 'merge_when_ready',
|
||||
mergeState: 'review_required',
|
||||
pendingChecks: 0
|
||||
})
|
||||
expect(step({}, { mergeStateStatus: 'BLOCKED', autoMergeAllowed: false })).toEqual({
|
||||
kind: 'merge_blocked',
|
||||
mergeState: 'blocked'
|
||||
})
|
||||
expect(step({}, { state: 'draft' })).toEqual({ kind: 'merge_blocked', mergeState: 'draft' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -78,34 +78,6 @@ describe('SSH retained payload admission', () => {
|
||||
expect(getUtf8ByteLength(detail ?? '')).toBe(SSH_CONNECTION_ERROR_MAX_UTF8_BYTES)
|
||||
})
|
||||
|
||||
it('keeps ordinary connection state while stripping unknown payload fields', () => {
|
||||
const admitted = admitSshConnectionState(
|
||||
{
|
||||
targetId: 'ssh-a',
|
||||
status: 'connected',
|
||||
error: null,
|
||||
reconnectAttempt: 2,
|
||||
providerEpoch: 'provider-a',
|
||||
connectionGeneration: 3,
|
||||
supportsFolderDownload: true,
|
||||
remotePlatform: 'linux',
|
||||
unexpected: 'x'.repeat(1024)
|
||||
},
|
||||
'ssh-a'
|
||||
)
|
||||
|
||||
expect(admitted).toEqual({
|
||||
targetId: 'ssh-a',
|
||||
status: 'connected',
|
||||
error: null,
|
||||
reconnectAttempt: 2,
|
||||
providerEpoch: 'provider-a',
|
||||
connectionGeneration: 3,
|
||||
supportsFolderDownload: true,
|
||||
remotePlatform: 'linux'
|
||||
})
|
||||
})
|
||||
|
||||
it('admits the plain SSH mode and drops a malformed one without dropping the state', () => {
|
||||
const state = { targetId: 'ssh-a', status: 'connected', error: null, reconnectAttempt: 0 }
|
||||
const plainSsh = { reason: 'home_noexec', message: 'Home is noexec.' }
|
||||
@@ -116,15 +88,6 @@ describe('SSH retained payload admission', () => {
|
||||
expect(malformed).not.toHaveProperty('plainSsh')
|
||||
})
|
||||
|
||||
it('admits only a literal Host Node runtime flag', () => {
|
||||
const state = { targetId: 'ssh-a', status: 'connected', error: null, reconnectAttempt: 0 }
|
||||
|
||||
const flagged = admitSshConnectionState({ ...state, hostNodeRuntime: true }, 'ssh-a')
|
||||
expect(flagged?.hostNodeRuntime).toBe(true)
|
||||
const malformed = admitSshConnectionState({ ...state, hostNodeRuntime: 'yes' }, 'ssh-a')
|
||||
expect(malformed).not.toHaveProperty('hostNodeRuntime')
|
||||
})
|
||||
|
||||
it("drops an older host's move offer and admits only a literal elsewhere flag", () => {
|
||||
const state = { targetId: 'ssh-a', status: 'connected', error: null, reconnectAttempt: 0 }
|
||||
const relay = { kind: 'relay', reason: 'relay_terminals_live', terminals: 2 }
|
||||
@@ -147,23 +110,6 @@ describe('SSH retained payload admission', () => {
|
||||
).toEqual(relay)
|
||||
})
|
||||
|
||||
it('admits only a literal "Move anyway" offer', () => {
|
||||
const state = { targetId: 'ssh-a', status: 'connected', error: null, reconnectAttempt: 0 }
|
||||
const refused = { kind: 'relay', reason: 'refused', detail: 'saved tabs and panes' }
|
||||
expect(
|
||||
admitSshConnectionState(
|
||||
{ ...state, managedServer: { ...refused, canLeaveBehind: true } },
|
||||
'ssh-a'
|
||||
)?.managedServer
|
||||
).toEqual({ ...refused, canLeaveBehind: true })
|
||||
expect(
|
||||
admitSshConnectionState(
|
||||
{ ...state, managedServer: { ...refused, canLeaveBehind: 1 } },
|
||||
'ssh-a'
|
||||
)?.managedServer
|
||||
).toEqual(refused)
|
||||
})
|
||||
|
||||
it('rejects partial and malformed provider authority', () => {
|
||||
const state = {
|
||||
targetId: 'ssh-a',
|
||||
@@ -238,26 +184,6 @@ describe('SSH retained payload admission', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('normalizes legacy authority to unknown', () => {
|
||||
expect(
|
||||
admitSshConnectionState(
|
||||
{
|
||||
targetId: 'ssh-a',
|
||||
status: 'disconnected',
|
||||
error: null,
|
||||
reconnectAttempt: 0
|
||||
},
|
||||
'ssh-a'
|
||||
)
|
||||
).toEqual({
|
||||
targetId: 'ssh-a',
|
||||
status: 'disconnected',
|
||||
error: null,
|
||||
reconnectAttempt: 0,
|
||||
providerEpoch: null
|
||||
})
|
||||
})
|
||||
|
||||
it('caps connection errors without splitting a UTF-8 code point', () => {
|
||||
const admitted = admitSshConnectionState(
|
||||
{
|
||||
|
||||
@@ -105,10 +105,6 @@ describe.skipIf(process.platform === 'win32')(
|
||||
expect(fishRequirementViolation(FISH)).toBeNull()
|
||||
})
|
||||
|
||||
it('found shells to test against', () => {
|
||||
expect(SHELLS.map((shell) => shell.name)).toContain('sh')
|
||||
})
|
||||
|
||||
describe.each(SHELLS)('$name', (shell) => {
|
||||
it.each(QUOTING_CASES)('quotes %j so the shell yields it back verbatim', (value) => {
|
||||
expect(runInShell(shell, `printf '%s' ${quoteStartupArg(value, 'posix')}`)).toBe(value)
|
||||
|
||||
@@ -103,27 +103,6 @@ describe('resolveTabAgentFromSignals — Pi/OMP identity', () => {
|
||||
).toBe('omp')
|
||||
})
|
||||
|
||||
it('does not flap between OMP and Pi as a launchAgent-less pane cycles hooks', () => {
|
||||
const withLiveHook = resolveTabAgentFromSignals({
|
||||
hasObservedAgentSignal: true,
|
||||
isRemote: true,
|
||||
title: '⠋ Pi',
|
||||
hookAgent: 'omp',
|
||||
focusedCompletedHookAgent: 'omp',
|
||||
launchAgent: undefined
|
||||
})
|
||||
const afterHookCleared = resolveTabAgentFromSignals({
|
||||
hasObservedAgentSignal: true,
|
||||
isRemote: true,
|
||||
title: '⠋ Pi',
|
||||
hookAgent: null,
|
||||
focusedCompletedHookAgent: 'omp',
|
||||
launchAgent: undefined
|
||||
})
|
||||
expect(withLiveHook).toBe('omp')
|
||||
expect(afterHookCleared).toBe('omp')
|
||||
})
|
||||
|
||||
it('does not flap between OMP and Pi as the foreground process oscillates', () => {
|
||||
// OMP’s nested pi alternates foreground reads at command boundaries; normalize both to the known OMP owner instead of flickering icons.
|
||||
const readsPi = resolveTabAgentFromSignals({
|
||||
@@ -204,19 +183,6 @@ describe('resolveTabAgentFromSignals — Pi/OMP identity', () => {
|
||||
|
||||
// Hook records identify panes across working/done turns; title activity must never replace durable identity.
|
||||
describe('resolveTabAgentFromSignals — identity vs liveness', () => {
|
||||
it('surfaces the focused idle identity from the record, not the title', () => {
|
||||
expect(
|
||||
resolveTabAgentFromSignals({
|
||||
hasObservedAgentSignal: true,
|
||||
isRemote: true,
|
||||
title: 'Terminal',
|
||||
hookAgent: null,
|
||||
focusedCompletedHookAgent: 'omp',
|
||||
launchAgent: undefined
|
||||
})
|
||||
).toBe('omp')
|
||||
})
|
||||
|
||||
it('ranks the focused idle identity above a hibernated session and launch bootstrap', () => {
|
||||
// Completed local hook identity beats sleeping records and stale launch intent.
|
||||
expect(
|
||||
@@ -257,28 +223,6 @@ describe('resolveTabAgentFromSignals — identity vs liveness', () => {
|
||||
).toBe('claude')
|
||||
})
|
||||
|
||||
it('keeps a launchAgent-less pane with a live Pi hook stable on Pi', () => {
|
||||
expect(
|
||||
resolveTabAgentFromSignals({
|
||||
hasObservedAgentSignal: true,
|
||||
isRemote: true,
|
||||
title: '⠋ Pi',
|
||||
hookAgent: 'pi',
|
||||
launchAgent: undefined
|
||||
})
|
||||
).toBe('pi')
|
||||
expect(
|
||||
resolveTabAgentFromSignals({
|
||||
hasObservedAgentSignal: true,
|
||||
isRemote: true,
|
||||
title: '⠋ Pi',
|
||||
hookAgent: null,
|
||||
focusedCompletedHookAgent: 'pi',
|
||||
launchAgent: undefined
|
||||
})
|
||||
).toBe('pi')
|
||||
})
|
||||
|
||||
it('keeps a sibling idle identity when the focused pane returns to its shell', () => {
|
||||
// Focused-pane exit evidence must not clear still-live sibling identity.
|
||||
expect(
|
||||
|
||||
@@ -14,11 +14,6 @@ describe('detectTerminalFileUriLinks', () => {
|
||||
expect(link.displayText).toBe('file:///Users/dev/orca/report.html')
|
||||
})
|
||||
|
||||
it('percent-decodes spaces in the path', () => {
|
||||
const [link] = detectTerminalFileUriLinks('open file:///Users/dev/My%20Reports/out.html now')
|
||||
expect(link.pathText).toBe('/Users/dev/My Reports/out.html')
|
||||
})
|
||||
|
||||
it('keeps standard unescaped parentheses and apostrophes inside the path', () => {
|
||||
const uri = "file:///tmp/Brennan's%20Report%20(final)"
|
||||
const [link] = detectTerminalFileUriLinks(`open (${uri})`)
|
||||
@@ -56,11 +51,6 @@ describe('detectTerminalFileUriLinks', () => {
|
||||
expect(detectTerminalFileUriLinks('file:///tmp/%E0%A4%A.txt')).toEqual([])
|
||||
})
|
||||
|
||||
it('finds multiple file URIs on one line', () => {
|
||||
const links = detectTerminalFileUriLinks('a file:///tmp/a.txt b file:///tmp/b.txt')
|
||||
expect(links.map((link) => link.pathText)).toEqual(['/tmp/a.txt', '/tmp/b.txt'])
|
||||
})
|
||||
|
||||
it('does not expose oversized terminal tokens to filesystem probing', () => {
|
||||
expect(detectTerminalFileUriLinks(`file:///tmp/${'a'.repeat(10_000)}`)).toEqual([])
|
||||
})
|
||||
|
||||
@@ -15,41 +15,8 @@ import {
|
||||
SEARCH_JSON_STRUCTURE_LIMITS
|
||||
} from './text-search'
|
||||
import { splitSearchGlobPatterns, toGitGlobPathspecs } from './text-search-glob-patterns'
|
||||
import { normalizeRelativePath } from './text-search-paths'
|
||||
|
||||
describe('normalizeRelativePath', () => {
|
||||
it('collapses mixed separators and strips leading slashes', () => {
|
||||
expect(normalizeRelativePath('a\\b\\c')).toBe('a/b/c')
|
||||
expect(normalizeRelativePath('/a/b')).toBe('a/b')
|
||||
expect(normalizeRelativePath('///a//b')).toBe('a/b')
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildRgArgs', () => {
|
||||
it('defaults to case-insensitive, fixed-strings, hidden, exclude .git', () => {
|
||||
const args = buildRgArgs('needle', '/root', {})
|
||||
expect(args).toContain('--json')
|
||||
expect(args).toContain('--hidden')
|
||||
expect(args).toContain('--ignore-case')
|
||||
expect(args).toContain('--fixed-strings')
|
||||
expect(args.indexOf('!.git')).toBeGreaterThan(args.indexOf('--glob'))
|
||||
expect(args.slice(-3)).toEqual(['--', 'needle', '/root'])
|
||||
})
|
||||
|
||||
it('honors caseSensitive, wholeWord, useRegex', () => {
|
||||
const args = buildRgArgs('q', '/r', { caseSensitive: true, wholeWord: true, useRegex: true })
|
||||
expect(args).not.toContain('--ignore-case')
|
||||
expect(args).toContain('--word-regexp')
|
||||
expect(args).not.toContain('--fixed-strings')
|
||||
})
|
||||
|
||||
it('splits comma-separated include/exclude patterns', () => {
|
||||
const args = buildRgArgs('q', '/r', { includePattern: '*.ts, *.tsx', excludePattern: '*.md' })
|
||||
expect(args).toContain('*.ts')
|
||||
expect(args).toContain('*.tsx')
|
||||
expect(args).toContain('!*.md')
|
||||
})
|
||||
|
||||
it('keeps escaped commas inside a single generated folder glob', () => {
|
||||
const args = buildRgArgs('q', '/r', { includePattern: 'foo\\,bar/**, *.ts' })
|
||||
expect(args).toContain('foo\\,bar/**')
|
||||
@@ -58,14 +25,6 @@ describe('buildRgArgs', () => {
|
||||
})
|
||||
|
||||
describe('splitSearchGlobPatterns', () => {
|
||||
it('splits comma-separated patterns while preserving escaped commas', () => {
|
||||
expect(splitSearchGlobPatterns('foo\\,bar/**, *.ts, dist/**')).toEqual([
|
||||
'foo\\,bar/**',
|
||||
'*.ts',
|
||||
'dist/**'
|
||||
])
|
||||
})
|
||||
|
||||
it('preserves trailing escapes as literal glob input', () => {
|
||||
expect(splitSearchGlobPatterns('src\\')).toEqual(['src\\'])
|
||||
})
|
||||
@@ -104,19 +63,6 @@ describe('ingestRgJsonLine', () => {
|
||||
expect(files[0].matches[0]).toEqual({ line: 2, column: 1, matchLength: 3, lineContent: 'abc' })
|
||||
})
|
||||
|
||||
it('ignores non-match messages', () => {
|
||||
const acc = createAccumulator()
|
||||
ingestRgJsonLine(JSON.stringify({ type: 'begin', data: {} }), '/root', acc, 100)
|
||||
expect(acc.totalMatches).toBe(0)
|
||||
})
|
||||
|
||||
it('skips malformed JSON', () => {
|
||||
const acc = createAccumulator()
|
||||
const verdict = ingestRgJsonLine('not json', '/root', acc, 100)
|
||||
expect(verdict).toBe('continue')
|
||||
expect(acc.totalMatches).toBe(0)
|
||||
})
|
||||
|
||||
it('rejects excessive nesting before JSON.parse', () => {
|
||||
const parseSpy = vi.spyOn(JSON, 'parse')
|
||||
const acc = createAccumulator()
|
||||
@@ -218,26 +164,6 @@ describe('ingestRgJsonLine', () => {
|
||||
})
|
||||
|
||||
describe('buildGitGrepArgs', () => {
|
||||
it('adds -i, --fixed-strings and `.` default pathspec', () => {
|
||||
const args = buildGitGrepArgs('q', {})
|
||||
expect(args).toContain('-i')
|
||||
expect(args).toContain('--fixed-strings')
|
||||
expect(args).toContain('--no-recurse-submodules')
|
||||
expect(args.at(-1)).toBe('.')
|
||||
})
|
||||
|
||||
it('uses --extended-regexp when useRegex is true', () => {
|
||||
const args = buildGitGrepArgs('q', { useRegex: true })
|
||||
expect(args).toContain('--extended-regexp')
|
||||
expect(args).not.toContain('--fixed-strings')
|
||||
})
|
||||
|
||||
it('includes/excludes use :(glob) pathspecs', () => {
|
||||
const args = buildGitGrepArgs('q', { includePattern: '*.ts', excludePattern: 'dist/**' })
|
||||
expect(args).toContain(':(glob)**/*.ts')
|
||||
expect(args).toContain(':(exclude,glob)dist/**')
|
||||
})
|
||||
|
||||
it('keeps escaped commas inside one generated folder pathspec', () => {
|
||||
const args = buildGitGrepArgs('q', { includePattern: 'foo\\,bar/**, *.ts' })
|
||||
expect(args).toContain(':(glob)foo\\,bar/**')
|
||||
@@ -246,12 +172,6 @@ describe('buildGitGrepArgs', () => {
|
||||
})
|
||||
|
||||
describe('toGitGlobPathspecs', () => {
|
||||
it('wraps bare globs with **/ to match recursively', () => {
|
||||
expect(toGitGlobPathspecs('*.ts')).toContain(':(glob)**/*.ts')
|
||||
expect(toGitGlobPathspecs('src/*.ts')).toContain(':(glob)src/*.ts')
|
||||
expect(toGitGlobPathspecs('*.ts', true)).toContain(':(exclude,glob)**/*.ts')
|
||||
})
|
||||
|
||||
it('also covers the subtree so a directory name behaves like rg --glob', () => {
|
||||
expect(toGitGlobPathspecs('node_modules', true)).toEqual([
|
||||
':(exclude,glob)**/node_modules',
|
||||
@@ -265,30 +185,9 @@ describe('toGitGlobPathspecs', () => {
|
||||
expect(toGitGlobPathspecs('node_modules/', true)).toEqual([':(exclude,glob)**/node_modules/**'])
|
||||
expect(toGitGlobPathspecs('src/')).toEqual([':(glob)**/src/**'])
|
||||
})
|
||||
|
||||
it('drops a pattern that is only separators', () => {
|
||||
expect(toGitGlobPathspecs('/')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSubmatchRegex', () => {
|
||||
it('escapes literal query by default, gi flags', () => {
|
||||
const re = buildSubmatchRegex('a.b', {})
|
||||
expect(re?.flags).toBe('gi')
|
||||
expect(re?.source).toBe('a\\.b')
|
||||
})
|
||||
|
||||
it('wholeWord wraps in \\b', () => {
|
||||
const re = buildSubmatchRegex('foo', { wholeWord: true })
|
||||
expect(re?.source).toBe('\\bfoo\\b')
|
||||
})
|
||||
|
||||
it('useRegex passes through', () => {
|
||||
const re = buildSubmatchRegex('a|b', { useRegex: true, caseSensitive: true })
|
||||
expect(re?.source).toBe('a|b')
|
||||
expect(re?.flags).toBe('g')
|
||||
})
|
||||
|
||||
it('returns null for queries git grep accepts but JS RegExp rejects', () => {
|
||||
// Unbalanced group — git grep ERE accepts it as literal, JS RegExp throws.
|
||||
expect(buildSubmatchRegex('(foo', { useRegex: true })).toBeNull()
|
||||
@@ -460,42 +359,6 @@ describe('ingestGitGrepLine', () => {
|
||||
})
|
||||
|
||||
describe('finalize', () => {
|
||||
it('returns the expected SearchResult shape', () => {
|
||||
const acc = createAccumulator()
|
||||
acc.fileMap.set('/r/a.ts', {
|
||||
filePath: '/r/a.ts',
|
||||
relativePath: 'a.ts',
|
||||
matchCount: 1,
|
||||
matches: [{ line: 1, column: 1, matchLength: 3, lineContent: 'foo' }]
|
||||
})
|
||||
acc.totalMatches = 1
|
||||
acc.truncated = true
|
||||
expect(finalize(acc)).toEqual({
|
||||
files: [
|
||||
{
|
||||
filePath: '/r/a.ts',
|
||||
relativePath: 'a.ts',
|
||||
matchCount: 1,
|
||||
matches: [{ line: 1, column: 1, matchLength: 3, lineContent: 'foo' }]
|
||||
}
|
||||
],
|
||||
totalMatches: 1,
|
||||
truncated: true
|
||||
})
|
||||
})
|
||||
|
||||
it('filters impossible empty file rows before returning results', () => {
|
||||
const acc = createAccumulator()
|
||||
acc.fileMap.set('/r/a.ts', { filePath: '/r/a.ts', relativePath: 'a.ts', matches: [] })
|
||||
acc.fileMap.set('/r/b.ts', {
|
||||
filePath: '/r/b.ts',
|
||||
relativePath: 'b.ts',
|
||||
matches: [{ line: 1, column: 1, matchLength: 3, lineContent: 'foo' }]
|
||||
})
|
||||
acc.totalMatches = 1
|
||||
expect(finalize(acc).files.map((file) => file.relativePath)).toEqual(['b.ts'])
|
||||
})
|
||||
|
||||
it('normalizes missing and too-low per-file match counts', () => {
|
||||
const acc = createAccumulator()
|
||||
acc.fileMap.set('/r/a.ts', {
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
buildShellCommandFromArgv,
|
||||
clearEnvCommand,
|
||||
commandSeparator,
|
||||
isPosixStartupShell,
|
||||
quoteStartupArg,
|
||||
tokenizeStartupCommand
|
||||
} from './tui-agent-startup-shell'
|
||||
import { clearEnvCommand, quoteStartupArg, tokenizeStartupCommand } from './tui-agent-startup-shell'
|
||||
import { buildAgentDraftLaunchPlan } from './tui-agent-startup'
|
||||
|
||||
function expectSpansCoverTokens(source: string, shell: 'powershell' | 'cmd'): string[] {
|
||||
@@ -104,42 +97,6 @@ describe('one Unix startup dialect', () => {
|
||||
expect(quoteStartupArg('', 'posix')).toBe(`''`)
|
||||
})
|
||||
|
||||
it('keeps the sh-family grammar claims that do hold for fish', () => {
|
||||
expect(commandSeparator('posix')).toBe('; ')
|
||||
expect(isPosixStartupShell('posix')).toBe(true)
|
||||
expect(isPosixStartupShell('powershell')).toBe(false)
|
||||
expect(buildShellCommandFromArgv(['codex', 'resume', 'a b'], 'posix')).toBe(
|
||||
`'codex' 'resume' 'a b'`
|
||||
)
|
||||
})
|
||||
|
||||
it('round-trips an adversarial argument through quote then tokenize', () => {
|
||||
for (const value of [
|
||||
String.raw`use \d+ and \\server\share`,
|
||||
"it's mine",
|
||||
'ends\\',
|
||||
'a "b" c',
|
||||
'$PATH *.ts'
|
||||
]) {
|
||||
const tokenized = tokenizeStartupCommand(quoteStartupArg(value, 'posix'), 'posix')
|
||||
expect(tokenized.ok && tokenized.tokens).toEqual([value])
|
||||
}
|
||||
})
|
||||
|
||||
it('clears an agent draft prefill variable with the portable teardown', () => {
|
||||
const plan = buildAgentDraftLaunchPlan({
|
||||
agent: 'pi',
|
||||
draft: 'hello',
|
||||
cmdOverrides: {},
|
||||
platform: 'darwin'
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe(
|
||||
`pi; command test -n "$fish_pid" && set --erase -g ORCA_PI_PREFILL; command test -z "$fish_pid" && unset ORCA_PI_PREFILL; true`
|
||||
)
|
||||
expect(plan?.env?.ORCA_PI_PREFILL).toBe('hello')
|
||||
})
|
||||
|
||||
it('leaves a multi-line argv draft to the paste fallback in cmd.exe', () => {
|
||||
const draft = 'Linked Linear issue: ENG-1\nhttps://linear.app/acme/issue/ENG-1'
|
||||
const args = { agent: 'claude' as const, draft, cmdOverrides: {}, platform: 'win32' as const }
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
buildShellCommandFromArgv,
|
||||
planAgentCliArgsSuffix
|
||||
} from './tui-agent-startup'
|
||||
import { TUI_AGENT_CONFIG } from './tui-agent-config'
|
||||
import { normalizeTuiAgentArgsRecord, resolveTuiAgentLaunchArgs } from './tui-agent-launch-defaults'
|
||||
import { tokenizeStartupCommand } from './tui-agent-startup-shell'
|
||||
import {
|
||||
@@ -41,21 +40,6 @@ describe('tui agent startup plans', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it('passes the whole configured argument string to a terminal launch', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'claude',
|
||||
prompt: '',
|
||||
agentArgs: resolveTuiAgentLaunchArgs('claude', {
|
||||
agentDefaultArgs: { claude: '--dangerously-skip-permissions --model Opus' }
|
||||
}),
|
||||
cmdOverrides: {},
|
||||
platform: 'linux',
|
||||
allowEmptyPromptLaunch: true
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe("claude '--dangerously-skip-permissions' '--model' 'Opus'")
|
||||
})
|
||||
|
||||
it('uses POSIX quoting when the target shell is Linux', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'claude',
|
||||
@@ -109,28 +93,6 @@ describe('tui agent startup plans', () => {
|
||||
expect(plan?.launchCommand).toBe('claude "fix ^"quoted^" ^& ^%PATH^%"')
|
||||
})
|
||||
|
||||
it('terminates Grok options before a flag-shaped POSIX prompt', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'grok',
|
||||
prompt: '--version',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe("grok -- '--version'")
|
||||
})
|
||||
|
||||
it('terminates Grok options before a flag-shaped PowerShell prompt', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'grok',
|
||||
prompt: '-h',
|
||||
cmdOverrides: {},
|
||||
platform: 'win32'
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe("grok -- '-h'")
|
||||
})
|
||||
|
||||
it('terminates Grok options before a subcommand-shaped cmd prompt', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'grok',
|
||||
@@ -155,17 +117,6 @@ describe('tui agent startup plans', () => {
|
||||
expect(plan?.launchCommand).toBe("grok '--always-approve' -- '--version'")
|
||||
})
|
||||
|
||||
it('does not add the Grok prompt separator to other argv agents', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'codex',
|
||||
prompt: '--version',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe("codex '-c' 'disable_paste_burst=true' '--version'")
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ platform: 'linux' as const, shell: 'posix' as const },
|
||||
{ platform: 'win32' as const, shell: 'powershell' as const },
|
||||
@@ -230,18 +181,6 @@ describe('tui agent startup plans', () => {
|
||||
expect(tokens.ok && tokens.tokens.at(-1)).toMatch(/\\0[0-7]{3}/)
|
||||
})
|
||||
|
||||
it('does not launch Codex with the Orca profile when agent status hooks are enabled', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'codex',
|
||||
prompt: 'fix it',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe("codex '-c' 'disable_paste_burst=true' 'fix it'")
|
||||
expect(plan?.startupCommandDelivery).toBe('shell-ready')
|
||||
})
|
||||
|
||||
it('keeps plain empty Codex startup on the fast delivery path', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'codex',
|
||||
@@ -260,18 +199,6 @@ describe('tui agent startup plans', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('launches Claude without Orca settings injection', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'claude',
|
||||
prompt: 'fix it',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe("claude 'fix it'")
|
||||
expect(plan?.launchCommand).not.toContain('--settings')
|
||||
})
|
||||
|
||||
it('uses the Linux Orca CLI command for Claude Agent Teams launches', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'claude-agent-teams',
|
||||
@@ -312,101 +239,6 @@ describe('tui agent startup plans', () => {
|
||||
expect(plan?.launchCommand).toBe('orca.cmd claude-teams')
|
||||
})
|
||||
|
||||
it('keeps the Linux orca-ide wrapper for local (non-remote) Claude Agent Teams', () => {
|
||||
// Local Linux keeps orca-ide to avoid the GNOME screen-reader command clash.
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'claude-agent-teams',
|
||||
prompt: '',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux',
|
||||
isRemote: false,
|
||||
allowEmptyPromptLaunch: true
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe('orca-ide claude-teams')
|
||||
})
|
||||
|
||||
it('launches OpenClaude as a distinct argv agent', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'openclaude',
|
||||
prompt: 'fix it',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan).toEqual({
|
||||
agent: 'openclaude',
|
||||
launchCommand: "openclaude 'fix it'",
|
||||
expectedProcess: 'openclaude',
|
||||
followupPrompt: null,
|
||||
launchConfig: { agentCommand: 'openclaude', agentArgs: '', agentEnv: {} }
|
||||
})
|
||||
})
|
||||
|
||||
it('launches Mistral Vibe through the installed vibe executable', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'mistral-vibe',
|
||||
prompt: 'fix it',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan).toEqual({
|
||||
agent: 'mistral-vibe',
|
||||
launchCommand: 'vibe',
|
||||
expectedProcess: 'vibe',
|
||||
followupPrompt: 'fix it',
|
||||
launchConfig: { agentCommand: 'vibe', agentArgs: '', agentEnv: {} }
|
||||
})
|
||||
})
|
||||
|
||||
it('launches Qwen Code through the installed qwen executable', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'qwen-code',
|
||||
prompt: 'fix it',
|
||||
cmdOverrides: {},
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan).toEqual({
|
||||
agent: 'qwen-code',
|
||||
launchCommand: 'qwen',
|
||||
expectedProcess: 'qwen',
|
||||
followupPrompt: 'fix it',
|
||||
launchConfig: { agentCommand: 'qwen', agentArgs: '', agentEnv: {} }
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['yolo', 'linux', 'posix', undefined, "muse --trust-workspace '--yolo'"],
|
||||
['manual', 'linux', 'posix', { muse: '' }, 'muse --trust-workspace'],
|
||||
['yolo', 'darwin', 'posix', undefined, "muse --trust-workspace '--yolo'"],
|
||||
['manual', 'darwin', 'posix', { muse: '' }, 'muse --trust-workspace'],
|
||||
['yolo', 'win32', 'powershell', undefined, "muse --trust-workspace '--yolo'"],
|
||||
['manual', 'win32', 'powershell', { muse: '' }, 'muse --trust-workspace'],
|
||||
['yolo', 'win32', 'cmd', undefined, 'muse --trust-workspace "--yolo"'],
|
||||
['manual', 'win32', 'cmd', { muse: '' }, 'muse --trust-workspace']
|
||||
] as const)(
|
||||
'launches Muse in %s mode on %s/%s before delivering its prompt',
|
||||
(_, platform, shell, defaults, command) => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'muse',
|
||||
prompt: 'fix it',
|
||||
cmdOverrides: {},
|
||||
platform,
|
||||
shell,
|
||||
agentArgs: resolveTuiAgentLaunchArgs('muse', { agentDefaultArgs: defaults })
|
||||
})
|
||||
|
||||
expect(plan).toMatchObject({
|
||||
agent: 'muse',
|
||||
launchCommand: command,
|
||||
expectedProcess: 'muse',
|
||||
followupPrompt: 'fix it'
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['exec', 'resume', '--help'])(
|
||||
'delivers the reserved Muse prompt %s as text',
|
||||
(prompt) => {
|
||||
@@ -421,17 +253,6 @@ describe('tui agent startup plans', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it('leaves Claude command overrides untouched', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'claude',
|
||||
prompt: 'fix it',
|
||||
cmdOverrides: { claude: 'claude --dangerously-skip-permissions' },
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe("claude --dangerously-skip-permissions 'fix it'")
|
||||
})
|
||||
|
||||
it('leaves Codex command overrides untouched', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'codex',
|
||||
@@ -507,19 +328,6 @@ describe('tui agent startup plans', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('honors command overrides when building POSIX resume plans', () => {
|
||||
const plan = buildAgentResumeStartupPlan({
|
||||
agent: 'codex',
|
||||
providerSession: { key: 'session_id', id: 's1' },
|
||||
cmdOverrides: { codex: 'codex --profile work' },
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe(
|
||||
"codex --profile work '-c' 'disable_paste_burst=true' 'resume' 's1'"
|
||||
)
|
||||
})
|
||||
|
||||
it('uses a captured launch command when building resume plans after overrides change', () => {
|
||||
const plan = buildAgentResumeStartupPlan({
|
||||
agent: 'codex',
|
||||
@@ -583,25 +391,6 @@ describe('tui agent startup plans', () => {
|
||||
expect(plan?.launchCommand).toBe("claude '--model' 'sonnet' '--name' 'Bob''s' 'fix it'")
|
||||
})
|
||||
|
||||
it('carries agent launch environment defaults into startup plans', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'goose',
|
||||
prompt: '',
|
||||
cmdOverrides: {},
|
||||
agentEnv: { GOOSE_MODE: 'auto' },
|
||||
platform: 'linux',
|
||||
allowEmptyPromptLaunch: true
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe('goose')
|
||||
expect(plan?.env).toEqual({ GOOSE_MODE: 'auto' })
|
||||
expect(plan?.launchConfig).toEqual({
|
||||
agentCommand: 'goose',
|
||||
agentArgs: '',
|
||||
agentEnv: { GOOSE_MODE: 'auto' }
|
||||
})
|
||||
})
|
||||
|
||||
it('captures empty args and env as explicit launch config values', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'claude',
|
||||
@@ -631,75 +420,6 @@ describe('tui agent startup plans', () => {
|
||||
expect(plan?.launchCommand).toBe("opencode --prompt 'fix it'")
|
||||
})
|
||||
|
||||
it('keeps opencode and mimo-code on the cursor-gated paste draft route', () => {
|
||||
expect(TUI_AGENT_CONFIG.opencode.draftPasteReadySignal).toBe(
|
||||
'render-cursor-after-bracketed-paste'
|
||||
)
|
||||
expect(TUI_AGENT_CONFIG.opencode.draftPromptFlag).toBeUndefined()
|
||||
expect(TUI_AGENT_CONFIG.opencode.draftPromptEnvVar).toBeUndefined()
|
||||
expect(TUI_AGENT_CONFIG['mimo-code'].draftPasteReadySignal).toBe(
|
||||
'render-cursor-after-bracketed-paste'
|
||||
)
|
||||
expect(TUI_AGENT_CONFIG['mimo-code'].draftPromptFlag).toBeUndefined()
|
||||
expect(TUI_AGENT_CONFIG['mimo-code'].draftPromptEnvVar).toBeUndefined()
|
||||
// Without native draft plans, these agents must use cursor-gated paste readiness.
|
||||
expect(
|
||||
buildAgentDraftLaunchPlan({
|
||||
agent: 'opencode',
|
||||
draft: 'x',
|
||||
cmdOverrides: {},
|
||||
platform: 'darwin'
|
||||
})
|
||||
).toBeNull()
|
||||
expect(
|
||||
buildAgentDraftLaunchPlan({
|
||||
agent: 'mimo-code',
|
||||
draft: 'x',
|
||||
cmdOverrides: {},
|
||||
platform: 'darwin'
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps grok on the composer-glyph paste draft route', () => {
|
||||
// Grok has no native prefill and its animated logo never settles; drafts must wait for its composer glyph.
|
||||
expect(TUI_AGENT_CONFIG.grok.draftPasteReadySignal).toBe('grok-composer-prompt')
|
||||
expect(TUI_AGENT_CONFIG.grok.draftPromptFlag).toBeUndefined()
|
||||
expect(TUI_AGENT_CONFIG.grok.draftPromptEnvVar).toBeUndefined()
|
||||
expect(
|
||||
buildAgentDraftLaunchPlan({
|
||||
agent: 'grok',
|
||||
draft: 'x',
|
||||
cmdOverrides: {},
|
||||
platform: 'darwin'
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('appends Kiro trust defaults to the chat subcommand that accepts them', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'kiro',
|
||||
prompt: 'fix it',
|
||||
cmdOverrides: {},
|
||||
agentArgs: '--trust-all-tools',
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe("kiro-cli chat --tui '--trust-all-tools'")
|
||||
})
|
||||
|
||||
it('launches Continue through the documented cn binary', () => {
|
||||
const plan = buildAgentStartupPlan({
|
||||
agent: 'continue',
|
||||
prompt: 'fix it',
|
||||
cmdOverrides: {},
|
||||
agentArgs: '--allow "*"',
|
||||
platform: 'linux'
|
||||
})
|
||||
|
||||
expect(plan?.launchCommand).toBe("cn '--allow' '*'")
|
||||
})
|
||||
|
||||
it('clears draft environment variables with the target shell syntax', () => {
|
||||
expect(
|
||||
buildAgentDraftLaunchPlan({
|
||||
@@ -812,10 +532,4 @@ describe('tui agent startup plans', () => {
|
||||
agentEnv: { ORCA_AGENT_MODE: 'managed' }
|
||||
})
|
||||
})
|
||||
|
||||
it('appends Devin default permission-mode bypass before stdin prompt delivery', () => {
|
||||
expect(resolveTuiAgentLaunchArgs('devin', null)).toBe(
|
||||
'--permission-mode bypass --respect-workspace-trust false'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
formatWindowMaterialArgument,
|
||||
parseWindowMaterialArgument,
|
||||
resolveWindowMaterial
|
||||
} from './window-material'
|
||||
|
||||
const mac = { platform: 'darwin', osRelease: '25.0.0' } as const
|
||||
const windows11 = { platform: 'win32', osRelease: '10.0.22631' } as const
|
||||
const linux = { platform: 'linux', osRelease: '6.8.0' } as const
|
||||
|
||||
describe('resolveWindowMaterial', () => {
|
||||
it('migrates the legacy blur switch when windowMaterial is unset, except on Linux', () => {
|
||||
const legacy = { windowBackgroundBlur: true, terminalBackgroundOpacity: 0.8 }
|
||||
expect(resolveWindowMaterial(legacy, mac, false)).toBe('blur')
|
||||
expect(resolveWindowMaterial(legacy, linux, false)).toBe('off')
|
||||
expect(resolveWindowMaterial({ ...legacy, windowMaterial: 'off' }, mac, false)).toBe('off')
|
||||
})
|
||||
|
||||
it('stays off while nothing translucent can reveal the material (#8482)', () => {
|
||||
for (const terminalBackgroundOpacity of [undefined, 1]) {
|
||||
expect(
|
||||
resolveWindowMaterial({ windowMaterial: 'blur', terminalBackgroundOpacity }, mac, false)
|
||||
).toBe('off')
|
||||
}
|
||||
})
|
||||
|
||||
it('turns off for Reduce Visual Effects and the OS reduced-transparency preference', () => {
|
||||
const settings = { windowMaterial: 'blur' as const, terminalBackgroundOpacity: 0.8 }
|
||||
expect(
|
||||
resolveWindowMaterial({ ...settings, reduceVisualEffects: true }, windows11, false)
|
||||
).toBe('off')
|
||||
expect(resolveWindowMaterial(settings, windows11, true)).toBe('off')
|
||||
})
|
||||
|
||||
it('maps a choice onto what the platform can render', () => {
|
||||
const translucent = { windowMaterial: 'translucent' as const, terminalBackgroundOpacity: 0.5 }
|
||||
const blur = { windowMaterial: 'blur' as const, terminalBackgroundOpacity: 0.5 }
|
||||
expect(resolveWindowMaterial(translucent, mac, false)).toBe('translucent')
|
||||
expect(resolveWindowMaterial(translucent, windows11, false)).toBe('off')
|
||||
expect(resolveWindowMaterial(blur, linux, false)).toBe('translucent')
|
||||
// Acrylic needs Windows 11 22H2; older builds would paint the clear window black.
|
||||
expect(resolveWindowMaterial(blur, { platform: 'win32', osRelease: '10.0.19045' }, false)).toBe(
|
||||
'off'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('window material argument', () => {
|
||||
it('round-trips and rejects unknown values', () => {
|
||||
expect(parseWindowMaterialArgument(['x', formatWindowMaterialArgument('blur')])).toBe('blur')
|
||||
expect(parseWindowMaterialArgument(['--orca-window-material=glass'])).toBe('off')
|
||||
expect(parseWindowMaterialArgument([])).toBe('off')
|
||||
})
|
||||
})
|
||||
@@ -68,10 +68,6 @@ function requestFrom(args: readonly string[]) {
|
||||
}
|
||||
|
||||
describe('relay Windows breakaway launcher', () => {
|
||||
it('is not requested by an ordinary relay launch', () => {
|
||||
expect(parseRelayWindowsBreakawayLaunch(['node', 'relay.js', '--detached'])).toBeNull()
|
||||
})
|
||||
|
||||
it('takes its own flags before -- and hands the rest to the relay', () => {
|
||||
expect(parseRelayWindowsBreakawayLaunch(argv)).toEqual({
|
||||
stdoutPath: 'C:/Users/me user/relay.log',
|
||||
@@ -138,13 +134,6 @@ describe('relay Windows breakaway launcher', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('reports a missing launcher as unavailable', () => {
|
||||
expect(launchRelayOutsideJob(requestFrom(argv), 'addon-missing', runtime)).toEqual({
|
||||
report: { method: 'unavailable', reason: 'addon-missing' },
|
||||
exitCode: RELAY_WINDOWS_BREAKAWAY_EXIT_CODES.unavailable
|
||||
})
|
||||
})
|
||||
|
||||
it('names an absent addon and one that predates the launcher', () => {
|
||||
expect(
|
||||
loadWindowsBreakawayLauncher(() => {
|
||||
|
||||
@@ -42,19 +42,6 @@ describe('resolveEmptyPageOutcome', () => {
|
||||
).toEqual({ reason: 'load-failed', clampTotalPagesTo: null })
|
||||
})
|
||||
|
||||
it('reports a failure without clamping for transient or unclassified errors', () => {
|
||||
expect(resolveEmptyPageOutcome({ ...base, target: 5, failedCount: 2 })).toEqual({
|
||||
reason: 'load-failed',
|
||||
clampTotalPagesTo: null
|
||||
})
|
||||
for (const type of ['permission_denied', 'not_found', 'rate_limited', 'unknown'] as const) {
|
||||
expect(resolveEmptyPageOutcome({ ...base, target: 5, errorTypes: [type] })).toEqual({
|
||||
reason: 'load-failed',
|
||||
clampTotalPagesTo: null
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('withdraws the speculative page on clean empty only while the count is unknown', () => {
|
||||
expect(resolveEmptyPageOutcome({ ...base, target: 2 })).toEqual({
|
||||
reason: 'end-of-data',
|
||||
@@ -84,11 +71,6 @@ describe('applyEmptyPageClamp', () => {
|
||||
expect(applyEmptyPageClamp(33, { ...clean, target: 2 })).toBe(33)
|
||||
})
|
||||
|
||||
it('withdraws the speculative page while the count is unknown or failed', () => {
|
||||
expect(applyEmptyPageClamp(null, { ...clean, target: 2 })).toBe(2)
|
||||
expect(applyEmptyPageClamp(0, { ...clean, target: 2 })).toBe(2)
|
||||
})
|
||||
|
||||
it('never touches the count for window or failure outcomes', () => {
|
||||
expect(
|
||||
applyEmptyPageClamp(39, { target: 33, failedCount: 0, errorTypes: ['validation_error'] })
|
||||
|
||||
@@ -2,15 +2,12 @@ import { describe, expect, it } from 'vitest'
|
||||
import { MoveTab } from '../../rpc-contract/session-tabs-schemas-params'
|
||||
import { TerminalCreateParams } from '../../rpc-contract/terminal-unary-params'
|
||||
import {
|
||||
translateCreateTerminalTab,
|
||||
translateSessionTabClose,
|
||||
translateSessionTabMove,
|
||||
translateSetTabProps,
|
||||
translateTerminalClose,
|
||||
translateTerminalCloseTab,
|
||||
translateTerminalCreate,
|
||||
translateTerminalRename,
|
||||
translateTerminalSplit,
|
||||
type MethodTranslation
|
||||
} from './old-client-method-translation'
|
||||
import { translateUpdatePaneLayout } from './old-client-pane-layout-translation'
|
||||
@@ -132,28 +129,6 @@ describe('translateTerminalCreate', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('creates a tab with unused ids', () => {
|
||||
const { model } = setup()
|
||||
expect(
|
||||
translateTerminalCreate(
|
||||
model,
|
||||
WS,
|
||||
createParams({ tabId: 'tab-new', leafId: NEW_LEAF, title: 'api' })
|
||||
)
|
||||
).toEqual({
|
||||
ok: true,
|
||||
commands: [
|
||||
{
|
||||
type: 'createTerminalTab',
|
||||
workspace: WS,
|
||||
tabId: 'tab-new',
|
||||
leafId: NEW_LEAF,
|
||||
title: 'api'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['no ids (CLI, orchestration)', {}],
|
||||
['a tab id alone', { tabId: 'tab-new' }],
|
||||
@@ -258,28 +233,6 @@ describe('session.tabs translations', () => {
|
||||
translateSetTabProps(WS, model.workspaces[WS]!, { tabId: 'gone', isPinned: true })
|
||||
).toEqual({ ok: true, commands: [] })
|
||||
})
|
||||
|
||||
it('creates a terminal tab after the tab a row names', () => {
|
||||
const { model, tabA, leafA } = setup()
|
||||
expect(
|
||||
translateCreateTerminalTab(WS, model.workspaces[WS], {
|
||||
afterTabId: terminalRowId(tabA, leafA),
|
||||
targetGroupId: 'group-1',
|
||||
viewMode: 'terminal'
|
||||
})
|
||||
).toEqual({
|
||||
ok: true,
|
||||
commands: [
|
||||
{
|
||||
type: 'createTerminalTab',
|
||||
workspace: WS,
|
||||
afterTabId: tabA,
|
||||
groupId: 'group-1',
|
||||
viewMode: 'terminal'
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('session.tabs.move translation', () => {
|
||||
@@ -438,19 +391,3 @@ describe('terminal.close and terminal.closeTab keep the pinned refusal', () => {
|
||||
).toEqual([{ type: 'closePane', workspace: WS, tabId: tabA, leafId: leafA }])
|
||||
})
|
||||
})
|
||||
|
||||
describe('terminal.* translations on a resolved pane', () => {
|
||||
it('splits below by default and resets an empty title', () => {
|
||||
const { model, tabA, leafA } = setup()
|
||||
const pane = { tab: terminalTab(model, tabA), leafId: leafA }
|
||||
expect(commandsOf(translateTerminalSplit(WS, pane, undefined))).toEqual([
|
||||
{ type: 'splitPane', workspace: WS, tabId: tabA, leafId: leafA, direction: 'horizontal' }
|
||||
])
|
||||
expect(commandsOf(translateTerminalRename(WS, pane, ''))).toEqual([
|
||||
{ type: 'renameTab', workspace: WS, tabId: tabA, kind: 'custom', title: null }
|
||||
])
|
||||
expect(commandsOf(translateTerminalRename(WS, pane, 'api'))).toEqual([
|
||||
{ type: 'renameTab', workspace: WS, tabId: tabA, kind: 'custom', title: 'api' }
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user