mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
test: remove 113 low-value tests (#34541)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -2921,7 +2921,6 @@
|
||||
"src/main/ipc/github-repo-routing-twin-host.test.ts": 91,
|
||||
"src/main/ipc/github-ssh-connection-routing.test.ts": 508,
|
||||
"src/main/ipc/github-star-telemetry.test.ts": 520,
|
||||
"src/main/ipc/github-work-item-args.test.ts": 33,
|
||||
"src/main/ipc/github-wsl-runtime-routing.test.ts": 726,
|
||||
"src/main/ipc/gitlab-repo-access.test.ts": 111,
|
||||
"src/main/ipc/gitlab.test.ts": 181,
|
||||
@@ -10601,7 +10600,6 @@
|
||||
"src/renderer/src/lib/ime-composition-keyboard-event.test.ts": 199,
|
||||
"src/renderer/src/lib/ime-overlay-escape.test.tsx": 615,
|
||||
"src/renderer/src/lib/ime-text-field.test.tsx": 463,
|
||||
"src/renderer/src/lib/ios-web-platform.test.ts": 48,
|
||||
"src/renderer/src/lib/ipc-error.test.ts": 69,
|
||||
"src/renderer/src/lib/jira-board-drag-payload.test.ts": 59,
|
||||
"src/renderer/src/lib/jira-issue-workspace-attachment.test.ts": 95,
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { agentBrowserSetMediaArguments } from './agent-browser-set-media-arguments'
|
||||
|
||||
describe('agentBrowserSetMediaArguments', () => {
|
||||
it.each([
|
||||
[undefined, 'reduce', ['set', 'media', 'reduced-motion']],
|
||||
[undefined, 'no-preference', ['set', 'media']],
|
||||
['dark', undefined, ['set', 'media', 'dark']],
|
||||
['light', 'reduce', ['set', 'media', 'light', 'reduced-motion']]
|
||||
])('maps color scheme %s and reduced motion %s', (colorScheme, reducedMotion, expected) => {
|
||||
expect(agentBrowserSetMediaArguments(colorScheme, reducedMotion)).toEqual(expected)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['sepia', undefined, 'Unsupported --color-scheme: sepia (expected dark or light)'],
|
||||
[
|
||||
undefined,
|
||||
'reduced-motion',
|
||||
'Unsupported --reduced-motion: reduced-motion (expected reduce or no-preference)'
|
||||
]
|
||||
])('rejects color scheme %s and reduced motion %s', (colorScheme, reducedMotion, message) => {
|
||||
expect(() => agentBrowserSetMediaArguments(colorScheme, reducedMotion)).toThrow(
|
||||
expect.objectContaining({ code: 'invalid_argument', message })
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,21 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { agentBrowserWaitArguments } from './agent-browser-wait-arguments'
|
||||
|
||||
describe('agentBrowserWaitArguments', () => {
|
||||
it.each([
|
||||
[
|
||||
{ fn: 'window.ready', timeout: 60_000 },
|
||||
['wait', '--fn', 'window.ready', '--timeout', '60000'],
|
||||
60_000,
|
||||
65_000
|
||||
],
|
||||
[{ selector: '#done' }, ['wait', '#done', '--timeout', '25000'], 25_000, 30_000],
|
||||
[{ timeout: 40_000 }, ['wait', '40000'], undefined, undefined]
|
||||
])('builds %j', (options, args, conditionTimeoutMs, processTimeoutMs) => {
|
||||
expect(agentBrowserWaitArguments(options)).toEqual({
|
||||
args,
|
||||
conditionTimeoutMs,
|
||||
processTimeoutMs
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -72,8 +72,4 @@ describe('a Claude informational frame', () => {
|
||||
|
||||
expect(items).toEqual([{ kind: 'status', tone: 'warning', text: content }])
|
||||
})
|
||||
|
||||
it('writes no row for a warning with nothing to say', async () => {
|
||||
expect(await itemsFor([informational('warning', ' ')])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { dispatchWorkItem } from './github-work-item-args'
|
||||
|
||||
describe('dispatchWorkItem', () => {
|
||||
const repo = { path: '/r', connectionId: null }
|
||||
|
||||
it('rejects non-integer numbers', () => {
|
||||
const fn = vi.fn()
|
||||
expect(dispatchWorkItem({ repoPath: '/r', number: 1.5 }, repo, fn)).toBeNull()
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects numbers < 1', () => {
|
||||
const fn = vi.fn()
|
||||
expect(dispatchWorkItem({ repoPath: '/r', number: 0 }, repo, fn)).toBeNull()
|
||||
expect(dispatchWorkItem({ repoPath: '/r', number: -5 }, repo, fn)).toBeNull()
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects non-number values coming across IPC', () => {
|
||||
const fn = vi.fn()
|
||||
// Model malformed renderer input despite the static type.
|
||||
const bogus = { repoPath: '/r', number: 'abc' as unknown as number }
|
||||
expect(dispatchWorkItem(bogus, repo, fn)).toBeNull()
|
||||
expect(fn).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('coerces unknown type values to undefined', async () => {
|
||||
const fn = vi.fn().mockResolvedValue(null)
|
||||
const bogus = {
|
||||
repoPath: '/r',
|
||||
number: 42,
|
||||
type: 'bogus' as unknown as 'issue' | 'pr'
|
||||
}
|
||||
await dispatchWorkItem(bogus, repo, fn)
|
||||
expect(fn).toHaveBeenCalledWith('/r', 42, undefined, null, undefined, undefined)
|
||||
})
|
||||
|
||||
it('passes valid issue type through', async () => {
|
||||
const fn = vi.fn().mockResolvedValue(null)
|
||||
await dispatchWorkItem({ repoPath: '/r', number: 42, type: 'issue' }, repo, fn)
|
||||
expect(fn).toHaveBeenCalledWith('/r', 42, 'issue', null, undefined, undefined)
|
||||
})
|
||||
|
||||
it('passes valid pr type through', async () => {
|
||||
const fn = vi.fn().mockResolvedValue(null)
|
||||
await dispatchWorkItem({ repoPath: '/r', number: 42, type: 'pr' }, repo, fn)
|
||||
expect(fn).toHaveBeenCalledWith('/r', 42, 'pr', null, undefined, undefined)
|
||||
})
|
||||
|
||||
it('passes SSH connection context through', async () => {
|
||||
const fn = vi.fn().mockResolvedValue(null)
|
||||
await dispatchWorkItem(
|
||||
{ repoPath: '/remote/repo', number: 42, type: 'issue' },
|
||||
{ path: '/remote/repo', connectionId: 'ssh-1' },
|
||||
fn
|
||||
)
|
||||
expect(fn).toHaveBeenCalledWith('/remote/repo', 42, 'issue', 'ssh-1', undefined, undefined)
|
||||
})
|
||||
|
||||
it('pins the repo issue source preference for open-by-number', async () => {
|
||||
const fn = vi.fn().mockResolvedValue(null)
|
||||
await dispatchWorkItem(
|
||||
{ repoPath: '/r', number: 42, type: 'pr' },
|
||||
{ path: '/r', connectionId: null, issueSourcePreference: 'origin' },
|
||||
fn,
|
||||
{ wslDistro: 'Ubuntu' }
|
||||
)
|
||||
expect(fn).toHaveBeenCalledWith('/r', 42, 'pr', null, { wslDistro: 'Ubuntu' }, 'origin')
|
||||
})
|
||||
|
||||
it('leaves upstream and auto preferences on the multi-candidate probe', async () => {
|
||||
const fn = vi.fn().mockResolvedValue(null)
|
||||
await dispatchWorkItem(
|
||||
{ repoPath: '/r', number: 7, type: 'pr' },
|
||||
{ path: '/r', connectionId: null, issueSourcePreference: 'upstream' },
|
||||
fn
|
||||
)
|
||||
await dispatchWorkItem(
|
||||
{ repoPath: '/r', number: 7, type: 'pr' },
|
||||
{ path: '/r', connectionId: null, issueSourcePreference: 'auto' },
|
||||
fn
|
||||
)
|
||||
expect(fn).toHaveBeenNthCalledWith(1, '/r', 7, 'pr', null, undefined, 'upstream')
|
||||
expect(fn).toHaveBeenNthCalledWith(2, '/r', 7, 'pr', null, undefined, 'auto')
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { formatMessageBanner, formatMessagePointer, formatMessagesForInjection } from './formatter'
|
||||
import { formatMessageBanner } from './formatter'
|
||||
import type { MessageRow } from './types'
|
||||
|
||||
function makeMessage(overrides: Partial<MessageRow> = {}): MessageRow {
|
||||
@@ -32,43 +32,6 @@ describe('formatMessageBanner', () => {
|
||||
expect(banner).not.toContain('[HIGH]')
|
||||
})
|
||||
|
||||
it('includes [HIGH] tag for high priority', () => {
|
||||
const banner = formatMessageBanner(makeMessage({ priority: 'high' }))
|
||||
expect(banner).toContain('[HIGH]')
|
||||
})
|
||||
|
||||
it('includes [URGENT] tag for urgent priority', () => {
|
||||
const banner = formatMessageBanner(makeMessage({ priority: 'urgent' }))
|
||||
expect(banner).toContain('[URGENT]')
|
||||
})
|
||||
|
||||
it('includes body after subject', () => {
|
||||
const banner = formatMessageBanner(makeMessage({ body: 'Some details here' }))
|
||||
const lines = banner.split('\n')
|
||||
const subjectIdx = lines.findIndex((l) => l.startsWith('Subject:'))
|
||||
const bodyIdx = lines.indexOf('Some details here')
|
||||
expect(subjectIdx).toBeGreaterThanOrEqual(0)
|
||||
expect(bodyIdx).toBeGreaterThan(subjectIdx)
|
||||
})
|
||||
|
||||
it('omits body line when body is empty', () => {
|
||||
const banner = formatMessageBanner(makeMessage({ body: '' }))
|
||||
const lines = banner.split('\n')
|
||||
const subjectIdx = lines.findIndex((l) => l.startsWith('Subject:'))
|
||||
expect(lines[subjectIdx + 1]).toMatch(/^\[Reply:/)
|
||||
})
|
||||
|
||||
it('includes payload when present', () => {
|
||||
const payload = '{"taskId":"task_1","exitCode":0}'
|
||||
const banner = formatMessageBanner(makeMessage({ payload }))
|
||||
expect(banner).toContain(`[Payload: ${payload}]`)
|
||||
})
|
||||
|
||||
it('omits payload line when payload is null', () => {
|
||||
const banner = formatMessageBanner(makeMessage({ payload: null }))
|
||||
expect(banner).not.toContain('[Payload:')
|
||||
})
|
||||
|
||||
it('includes reply hint with message ID', () => {
|
||||
const banner = formatMessageBanner(makeMessage({ id: 'msg_xyz789' }))
|
||||
expect(banner).toContain(
|
||||
@@ -143,46 +106,4 @@ describe('formatMessageBanner', () => {
|
||||
expect(banner).not.toContain('[Reply:')
|
||||
}
|
||||
})
|
||||
|
||||
it('ends with a separator line', () => {
|
||||
const banner = formatMessageBanner(makeMessage())
|
||||
const lines = banner.split('\n')
|
||||
expect(lines.at(-1)).toMatch(/^─+$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatMessagesForInjection', () => {
|
||||
it('returns empty string for empty array', () => {
|
||||
expect(formatMessagesForInjection([])).toBe('')
|
||||
})
|
||||
|
||||
it('wraps multiple banners with orchestration messages header', () => {
|
||||
const messages = [makeMessage({ id: 'msg_1' }), makeMessage({ id: 'msg_2' })]
|
||||
const result = formatMessagesForInjection(messages)
|
||||
expect(result).toContain('--- Orchestration Messages (2) ---')
|
||||
expect(result).toContain('msg_1')
|
||||
expect(result).toContain('msg_2')
|
||||
expect(result).toMatch(/\n---\n$/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatMessagePointer', () => {
|
||||
it('formats a singular pointer without message content', () => {
|
||||
expect(formatMessagePointer(1, 'run:run_1')).toBe(
|
||||
'\nYou have 1 orchestration message. Run `orca orchestration check --run run_1`.\n'
|
||||
)
|
||||
})
|
||||
|
||||
it('pluralizes a batched pointer', () => {
|
||||
expect(formatMessagePointer(3)).toContain('3 orchestration messages')
|
||||
})
|
||||
|
||||
it('uses the terminal-resolved CLI command', () => {
|
||||
expect(formatMessagePointer(1, 'run:run_wsl', 'orca-ide')).toContain(
|
||||
'`orca-ide orchestration check --run run_wsl`'
|
||||
)
|
||||
expect(formatMessagePointer(1, 'run:run_dev', 'orca-dev')).toContain(
|
||||
'`orca-dev orchestration check --run run_dev`'
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,30 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
assertSafeRemotePathSegment,
|
||||
getRemoteHostPlatform,
|
||||
joinRemotePath
|
||||
} from './ssh-remote-platform'
|
||||
|
||||
describe('joinRemotePath', () => {
|
||||
it('joins POSIX remote paths', () => {
|
||||
expect(joinRemotePath(getRemoteHostPlatform('linux-x64'), '/home/me', '.orca-remote')).toBe(
|
||||
'/home/me/.orca-remote'
|
||||
)
|
||||
})
|
||||
|
||||
it('normalizes and joins Windows remote paths with forward slashes for SFTP and Node', () => {
|
||||
expect(
|
||||
joinRemotePath(getRemoteHostPlatform('win32-x64'), 'C:\\Users\\me', '.orca-remote', 'relay')
|
||||
).toBe('C:/Users/me/.orca-remote/relay')
|
||||
})
|
||||
})
|
||||
import { assertSafeRemotePathSegment } from './ssh-remote-platform'
|
||||
|
||||
describe('assertSafeRemotePathSegment', () => {
|
||||
it('accepts ordinary names under both path flavors', () => {
|
||||
expect(() => assertSafeRemotePathSegment('report copy.txt', 'posix')).not.toThrow()
|
||||
expect(() => assertSafeRemotePathSegment('report copy.txt', 'windows')).not.toThrow()
|
||||
})
|
||||
|
||||
it.each(['.', '..', '../secret', 'child/name', 'nul\0byte'])(
|
||||
'rejects invalid segment %j under both path flavors',
|
||||
(segment) => {
|
||||
|
||||
@@ -78,21 +78,4 @@ describe('generateConversationName', () => {
|
||||
cancelLocalGeneration('conversation-name', '/repo')
|
||||
expect(await pending).toMatchObject({ success: false, canceled: true })
|
||||
})
|
||||
|
||||
it('returns a sanitized name from the local generation lane', async () => {
|
||||
const child = createMockDiscoveryChild()
|
||||
const spawnAgent = vi.fn(() => {
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: The test child implements every process member the generation lane reads.
|
||||
return child as unknown as SpawnedSourceControlAgentProcess
|
||||
})
|
||||
const pending = generateConversationName({
|
||||
context: { firstPrompt: 'Fix login' },
|
||||
params: { agentId: 'custom', model: '', customAgentCommand: 'agent' },
|
||||
target: { kind: 'local', cwd: '/repo' },
|
||||
spawnAgent
|
||||
})
|
||||
child.stdout.emit('data', Buffer.from('Title: "Fix login flow"\n'))
|
||||
child.emit('close', 0)
|
||||
expect(await pending).toEqual({ success: true, name: 'Fix login flow', agentLabel: 'agent' })
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { isIosWebPlatform } from './ios-web-platform'
|
||||
|
||||
const IPAD_DESKTOP_MODE_UA =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15'
|
||||
const IPAD_MOBILE_MODE_UA =
|
||||
'Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) CriOS/120.0.0.0 Mobile/15E148 Safari/604.1'
|
||||
const IPHONE_UA =
|
||||
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1'
|
||||
const IPOD_UA =
|
||||
'Mozilla/5.0 (iPod touch; CPU iPhone OS 14_8 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.1.2 Mobile/15E148 Safari/604.1'
|
||||
const MAC_DESKTOP_UA =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
||||
const MAC_ELECTRON_UA =
|
||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) orca/1.0.0 Chrome/126.0.0.0 Electron/31.0.0 Safari/537.36'
|
||||
const WINDOWS_UA =
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36'
|
||||
const ANDROID_UA =
|
||||
'Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Mobile Safari/537.36'
|
||||
|
||||
describe('isIosWebPlatform', () => {
|
||||
it('detects iPadOS desktop mode, which reports Macintosh and omits iPad', () => {
|
||||
expect(isIosWebPlatform(IPAD_DESKTOP_MODE_UA, 5)).toBe(true)
|
||||
})
|
||||
|
||||
it('detects iPad, iPhone and iPod mobile-mode user agents', () => {
|
||||
expect(isIosWebPlatform(IPAD_MOBILE_MODE_UA, 5)).toBe(true)
|
||||
expect(isIosWebPlatform(IPHONE_UA, 5)).toBe(true)
|
||||
expect(isIosWebPlatform(IPOD_UA, 5)).toBe(true)
|
||||
})
|
||||
|
||||
it('detects a mobile-mode iPad even when the touch count is unavailable', () => {
|
||||
expect(isIosWebPlatform(IPAD_MOBILE_MODE_UA, 0)).toBe(true)
|
||||
})
|
||||
|
||||
it('does not claim a Mac browser or the Electron app', () => {
|
||||
expect(isIosWebPlatform(MAC_DESKTOP_UA, 0)).toBe(false)
|
||||
expect(isIosWebPlatform(MAC_ELECTRON_UA, 0)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not claim a Mac whose touch peripheral reports a single point', () => {
|
||||
expect(isIosWebPlatform(MAC_DESKTOP_UA, 1)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not claim Windows, Linux or Android', () => {
|
||||
expect(isIosWebPlatform(WINDOWS_UA, 0)).toBe(false)
|
||||
expect(isIosWebPlatform(ANDROID_UA, 5)).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -36,39 +36,6 @@ describe('orderEmptyQueryWorktrees', () => {
|
||||
expect(result.switchableWorktreesForRows.map((w) => w.id)).toEqual(['ssh', 'local'])
|
||||
})
|
||||
|
||||
it('sorts visited worktrees by lastVisitedAt descending', () => {
|
||||
const a = wt({ id: 'a', displayName: 'a' })
|
||||
const b = wt({ id: 'b', displayName: 'b' })
|
||||
const result = orderEmptyQueryWorktrees({
|
||||
visibleWorktrees: [a, b],
|
||||
activeWorktreeId: null,
|
||||
lastVisitedAtByWorktreeId: { a: 100, b: 200 }
|
||||
})
|
||||
expect(result.switchableWorktreesForRows.map((w) => w.id)).toEqual(['b', 'a'])
|
||||
})
|
||||
|
||||
it('falls back to lastActivityAt for never-visited worktrees', () => {
|
||||
const a = wt({ id: 'a', displayName: 'a', lastActivityAt: 100 })
|
||||
const b = wt({ id: 'b', displayName: 'b', lastActivityAt: 200 })
|
||||
const result = orderEmptyQueryWorktrees({
|
||||
visibleWorktrees: [a, b],
|
||||
activeWorktreeId: null,
|
||||
lastVisitedAtByWorktreeId: {}
|
||||
})
|
||||
expect(result.switchableWorktreesForRows.map((w) => w.id)).toEqual(['b', 'a'])
|
||||
})
|
||||
|
||||
it('uses displayName as final tie-breaker', () => {
|
||||
const a = wt({ id: 'a', displayName: 'apple', lastActivityAt: 100 })
|
||||
const b = wt({ id: 'b', displayName: 'banana', lastActivityAt: 100 })
|
||||
const result = orderEmptyQueryWorktrees({
|
||||
visibleWorktrees: [b, a],
|
||||
activeWorktreeId: null,
|
||||
lastVisitedAtByWorktreeId: {}
|
||||
})
|
||||
expect(result.switchableWorktreesForRows.map((w) => w.id)).toEqual(['a', 'b'])
|
||||
})
|
||||
|
||||
it('does not crash the switcher when a visible worktree has no displayName (crash 99657ab1)', () => {
|
||||
// Persisted/discovered displayName can be undefined despite its type; raw localeCompare once crashed Cmd+J.
|
||||
const named = wt({ id: 'a', displayName: 'apple', lastActivityAt: 100 })
|
||||
@@ -86,18 +53,6 @@ describe('orderEmptyQueryWorktrees', () => {
|
||||
).not.toThrow()
|
||||
})
|
||||
|
||||
it('excludes current worktree from rows but includes it in state list', () => {
|
||||
const cur = wt({ id: 'cur', displayName: 'cur' })
|
||||
const other = wt({ id: 'other', displayName: 'other' })
|
||||
const result = orderEmptyQueryWorktrees({
|
||||
visibleWorktrees: [cur, other],
|
||||
activeWorktreeId: 'cur',
|
||||
lastVisitedAtByWorktreeId: {}
|
||||
})
|
||||
expect(result.switchableWorktreesForRows.map((w) => w.id)).toEqual(['other'])
|
||||
expect(result.visibleWorktreesForState.map((w) => w.id)).toEqual(['cur', 'other'])
|
||||
})
|
||||
|
||||
// Filter current workspace by host too or its same-ID SSH twin becomes unreachable.
|
||||
it('keeps the same-id worktree on the other host switchable', () => {
|
||||
const local = wt({ id: 'shared', displayName: 'local', hostId: 'local' })
|
||||
@@ -110,15 +65,4 @@ describe('orderEmptyQueryWorktrees', () => {
|
||||
})
|
||||
expect(result.switchableWorktreesForRows.map((w) => w.displayName)).toEqual(['ssh'])
|
||||
})
|
||||
|
||||
it('returns empty rows but non-empty state when only the current worktree is visible', () => {
|
||||
const cur = wt({ id: 'cur', displayName: 'cur' })
|
||||
const result = orderEmptyQueryWorktrees({
|
||||
visibleWorktrees: [cur],
|
||||
activeWorktreeId: 'cur',
|
||||
lastVisitedAtByWorktreeId: {}
|
||||
})
|
||||
expect(result.switchableWorktreesForRows).toEqual([])
|
||||
expect(result.visibleWorktreesForState).toHaveLength(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -57,35 +57,12 @@ function count(overrides: Partial<UnreadBadgeCountSources>): number {
|
||||
}
|
||||
|
||||
describe('getUnreadBadgeCount', () => {
|
||||
it('counts unread worktrees', () => {
|
||||
expect(
|
||||
count({
|
||||
worktreesByRepo: { repo1: [worktree('wt-1'), worktree('wt-2', { isUnread: false })] }
|
||||
})
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
it('skips archived worktrees, which the sidebar never shows', () => {
|
||||
expect(count({ worktreesByRepo: { repo1: [worktree('wt-1', { isArchived: true })] } })).toBe(0)
|
||||
})
|
||||
|
||||
it('preserves id-only deduplication across execution hosts', () => {
|
||||
const rows = [worktree('wt-1', { hostId: 'local' }), worktree('wt-1', { hostId: 'ssh:remote' })]
|
||||
expect(count({ worktreesByRepo: { repo1: rows } })).toBe(1)
|
||||
expect(count({ worktreesByRepo: { repo1: rows }, visibleHostIds: new Set(['local']) })).toBe(1)
|
||||
})
|
||||
|
||||
it('counts a row repeated across repo buckets once', () => {
|
||||
expect(
|
||||
count({
|
||||
worktreesByRepo: {
|
||||
'repo-a': [worktree('wt-1', { hostId: 'local' })],
|
||||
'repo-b': [worktree('wt-1', { hostId: 'local' })]
|
||||
}
|
||||
})
|
||||
).toBe(1)
|
||||
})
|
||||
|
||||
it('counts unread folder workspaces alongside worktrees', () => {
|
||||
expect(
|
||||
count({
|
||||
|
||||
@@ -29,15 +29,6 @@ function makeWorktree(overrides: Partial<Worktree> = {}): Worktree {
|
||||
}
|
||||
|
||||
describe('deriveAiVaultWorkspaceScopePaths', () => {
|
||||
it('returns the active workspace path', () => {
|
||||
const active = makeWorktree()
|
||||
expect(deriveAiVaultWorkspaceScopePaths(active, [active])).toEqual(['/repo/orca'])
|
||||
})
|
||||
|
||||
it('returns nothing without an active workspace', () => {
|
||||
expect(deriveAiVaultWorkspaceScopePaths(null, [makeWorktree()])).toEqual([])
|
||||
})
|
||||
|
||||
it('includes prior paths so renamed workspaces keep their transcripts', () => {
|
||||
const active = makeWorktree({
|
||||
id: 'repo-1::/repo/orca-renamed',
|
||||
@@ -51,25 +42,6 @@ describe('deriveAiVaultWorkspaceScopePaths', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('drops a prior path another live workspace now owns', () => {
|
||||
// Sessions use cwd identity; claiming an occupied prior path would expose another workspace's transcripts.
|
||||
const claimant = makeWorktree({ id: 'repo-1::/repo/orca', path: '/repo/orca' })
|
||||
const active = makeWorktree({
|
||||
id: 'repo-1::/repo/orca-renamed',
|
||||
path: '/repo/orca-renamed',
|
||||
priorWorktreeIds: ['repo-1::/repo/orca']
|
||||
})
|
||||
|
||||
expect(deriveAiVaultWorkspaceScopePaths(active, [claimant, active])).toEqual([
|
||||
'/repo/orca-renamed'
|
||||
])
|
||||
})
|
||||
|
||||
it('keeps a prior path the active workspace itself still owns', () => {
|
||||
const active = makeWorktree({ priorWorktreeIds: ['repo-1::/repo/orca'] })
|
||||
expect(deriveAiVaultWorkspaceScopePaths(active, [active])).toEqual(['/repo/orca'])
|
||||
})
|
||||
|
||||
it('drops a claimed prior path regardless of where the claimant sits in the list', () => {
|
||||
// Exclude the active workspace before indexing owners so duplicates cannot hide later claimants.
|
||||
const active = makeWorktree({
|
||||
@@ -93,27 +65,6 @@ describe('deriveAiVaultWorkspaceScopePaths', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('derives workspace scope paths at scale', () => {
|
||||
// The session-scope guard cannot catch quadratic deduplication confined to the workspace pass.
|
||||
const prefix = '/Users/dev/orca/workspaces/orca-monorepo/feature-'
|
||||
const worktrees = Array.from({ length: 1200 }, (_, i) =>
|
||||
makeWorktree({ id: `repo-1::${prefix}${i}`, path: `${prefix}${i}` })
|
||||
)
|
||||
const active = makeWorktree({
|
||||
id: `repo-1::${prefix}0`,
|
||||
path: `${prefix}0`,
|
||||
// Priors drive the claim check, which is the other per-call scan here.
|
||||
priorWorktreeIds: Array.from({ length: 25 }, (_, i) => `repo-1::${prefix}prior-${i}`)
|
||||
})
|
||||
|
||||
const startedAt = performance.now()
|
||||
const paths = deriveAiVaultWorkspaceScopePaths(active, worktrees)
|
||||
const elapsedMs = performance.now() - startedAt
|
||||
|
||||
expect(paths).toHaveLength(1 + 25)
|
||||
expect(elapsedMs).toBeLessThan(100)
|
||||
})
|
||||
|
||||
it('ignores prior ids belonging to another repo', () => {
|
||||
const active = makeWorktree({ priorWorktreeIds: ['repo-2::/repo/other'] })
|
||||
expect(deriveAiVaultWorkspaceScopePaths(active, [active])).toEqual(['/repo/orca'])
|
||||
@@ -161,20 +112,4 @@ describe('deriveAiVaultScopeSessionPaths', () => {
|
||||
|
||||
expect(deriveAiVaultScopeSessionPaths(active, [active, nfd])).toEqual([nfc])
|
||||
})
|
||||
|
||||
it('derives scope paths at scale without re-normalizing the accumulator', () => {
|
||||
// Match profile count/path lengths to catch quadratic normalization on workspace switches (~190ms before).
|
||||
const prefix = '/Users/dev/orca/workspaces/orca-monorepo/feature-'
|
||||
const worktrees = Array.from({ length: 1200 }, (_, i) =>
|
||||
makeWorktree({ id: `repo-1::${prefix}${i}`, path: `${prefix}${i}` })
|
||||
)
|
||||
|
||||
const startedAt = performance.now()
|
||||
const paths = deriveAiVaultScopeSessionPaths(worktrees[0], worktrees)
|
||||
const elapsedMs = performance.now() - startedAt
|
||||
|
||||
expect(paths).toHaveLength(worktrees.length)
|
||||
// Allow slow CI hosts while catching the previous ~190ms cost (now ~1ms).
|
||||
expect(elapsedMs).toBeLessThan(100)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
configureHardeningRetryBudget,
|
||||
hardeningRetryDelayMs,
|
||||
mayAttemptHardening,
|
||||
recordHardeningOutcome
|
||||
} from './secure-path-hardening-retry-budget'
|
||||
@@ -55,28 +54,6 @@ describe('secure path hardening retry budget', () => {
|
||||
return attemptedAt
|
||||
}
|
||||
|
||||
it('doubles the delay after each consecutive failure, up to the ceiling', () => {
|
||||
expect(hardeningRetryDelayMs(1)).toBe(1 * MINUTE)
|
||||
expect(hardeningRetryDelayMs(2)).toBe(2 * MINUTE)
|
||||
expect(hardeningRetryDelayMs(3)).toBe(4 * MINUTE)
|
||||
expect(hardeningRetryDelayMs(4)).toBe(8 * MINUTE)
|
||||
expect(hardeningRetryDelayMs(5)).toBe(16 * MINUTE)
|
||||
expect(hardeningRetryDelayMs(6)).toBe(30 * MINUTE)
|
||||
expect(hardeningRetryDelayMs(50)).toBe(30 * MINUTE)
|
||||
expect(hardeningRetryDelayMs(5000)).toBe(30 * MINUTE)
|
||||
})
|
||||
|
||||
it('allows the first attempt for a path it has never seen', () => {
|
||||
expect(mayAttemptHardening(PATH)).toBe(true)
|
||||
})
|
||||
|
||||
// The #4901 condition: the env store re-hardens on the read path about twice a second.
|
||||
it('collapses a read-path poll to a single attempt in the first minute', () => {
|
||||
const attemptedAt = pollUntil(55_000, 500)
|
||||
|
||||
expect(attemptedAt).toEqual([0])
|
||||
})
|
||||
|
||||
it('re-probes on the documented curve rather than on every read', () => {
|
||||
// Six hours of polling every 30s: 720 reads, and only the backoff decides how many run.
|
||||
const attemptedAt = pollUntil(6 * 60 * MINUTE, 30_000)
|
||||
@@ -88,14 +65,6 @@ describe('secure path hardening retry budget', () => {
|
||||
expect(trailingGaps.slice(0, -1)).toEqual([30, 30, 30])
|
||||
})
|
||||
|
||||
// The failure this replaced: three transient failures used to disable a path until restart.
|
||||
it('never abandons a path, however long it has been failing', () => {
|
||||
pollUntil(30 * 24 * 60 * MINUTE, 15 * MINUTE)
|
||||
|
||||
clock += 31 * MINUTE
|
||||
expect(mayAttemptHardening(PATH)).toBe(true)
|
||||
})
|
||||
|
||||
/** Backwards wall-clock steps previously held paths below retry delay until time caught up; monotonic elapsed time must keep probing. */
|
||||
it('re-probes after a backwards clock step rather than waiting for the wall clock', () => {
|
||||
pollUntil(6 * 60 * MINUTE, 30_000)
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
getStageAllPaths,
|
||||
getUnstageAllPaths,
|
||||
isStageableStatusEntry,
|
||||
isSubmoduleWorktreeOnlyChange,
|
||||
runDiscardAllForArea
|
||||
} from './source-control-discard-all-sequence'
|
||||
import type { GitStatusEntry } from './git-status-types'
|
||||
@@ -18,17 +17,6 @@ function entry(partial: Partial<GitStatusEntry> & { path: string }): GitStatusEn
|
||||
}
|
||||
|
||||
describe('getDiscardAllPaths', () => {
|
||||
it('returns only paths in the requested area', () => {
|
||||
const entries: GitStatusEntry[] = [
|
||||
entry({ path: 'a.ts', area: 'staged' }),
|
||||
entry({ path: 'b.ts', area: 'unstaged' }),
|
||||
entry({ path: 'c.ts', area: 'untracked', status: 'untracked' })
|
||||
]
|
||||
expect(getDiscardAllPaths(entries, 'staged')).toEqual(['a.ts'])
|
||||
expect(getDiscardAllPaths(entries, 'unstaged')).toEqual(['b.ts', 'c.ts'])
|
||||
expect(getDiscardAllPaths(entries, 'untracked')).toEqual(['c.ts'])
|
||||
})
|
||||
|
||||
it('skips entries with an unresolved conflict', () => {
|
||||
const entries: GitStatusEntry[] = [
|
||||
entry({ path: 'clean.ts', area: 'unstaged' }),
|
||||
@@ -56,24 +44,9 @@ describe('getDiscardAllPaths', () => {
|
||||
// Bulk discard must preserve local resolutions or users would have to resolve them again.
|
||||
expect(getDiscardAllPaths(entries, 'unstaged')).toEqual(['clean.ts'])
|
||||
})
|
||||
|
||||
it('returns an empty array when nothing matches', () => {
|
||||
expect(getDiscardAllPaths([], 'staged')).toEqual([])
|
||||
expect(getDiscardAllPaths([entry({ path: 'a.ts', area: 'staged' })], 'unstaged')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('getStageAllPaths', () => {
|
||||
it('returns only paths in the requested area', () => {
|
||||
const entries: GitStatusEntry[] = [
|
||||
entry({ path: 'a.ts', area: 'staged' }),
|
||||
entry({ path: 'b.ts', area: 'unstaged' }),
|
||||
entry({ path: 'c.ts', area: 'untracked', status: 'untracked' })
|
||||
]
|
||||
expect(getStageAllPaths(entries, 'unstaged')).toEqual(['b.ts'])
|
||||
expect(getStageAllPaths(entries, 'untracked')).toEqual(['c.ts'])
|
||||
})
|
||||
|
||||
it('skips entries with an unresolved conflict', () => {
|
||||
const entries: GitStatusEntry[] = [
|
||||
entry({ path: 'clean.ts', area: 'unstaged' }),
|
||||
@@ -117,36 +90,9 @@ describe('getStageAllPaths', () => {
|
||||
]
|
||||
expect(getStageAllPaths(entries, 'unstaged')).toEqual(['changed-gitlink'])
|
||||
})
|
||||
|
||||
it('returns an empty array when nothing matches', () => {
|
||||
expect(getStageAllPaths([], 'unstaged')).toEqual([])
|
||||
expect(getStageAllPaths([entry({ path: 'a.ts', area: 'staged' })], 'unstaged')).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('status entry stageability', () => {
|
||||
it('marks nested-only submodule changes as not stageable from the parent repo', () => {
|
||||
const nestedOnly = entry({
|
||||
path: 'nested-repo',
|
||||
area: 'unstaged',
|
||||
submodule: { commitChanged: false, trackedChanges: true, untrackedChanges: false }
|
||||
})
|
||||
|
||||
expect(isSubmoduleWorktreeOnlyChange(nestedOnly)).toBe(true)
|
||||
expect(isStageableStatusEntry(nestedOnly)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps changed submodule gitlinks stageable from the parent repo', () => {
|
||||
const changedGitlink = entry({
|
||||
path: 'nested-repo',
|
||||
area: 'unstaged',
|
||||
submodule: { commitChanged: true, trackedChanges: true, untrackedChanges: true }
|
||||
})
|
||||
|
||||
expect(isSubmoduleWorktreeOnlyChange(changedGitlink)).toBe(false)
|
||||
expect(isStageableStatusEntry(changedGitlink)).toBe(true)
|
||||
})
|
||||
|
||||
it('marks lazily-expanded submodule-internal entries as not stageable', () => {
|
||||
const innerEntry = entry({
|
||||
path: 'flutter_mine/lib/main.dart',
|
||||
@@ -161,15 +107,6 @@ describe('status entry stageability', () => {
|
||||
})
|
||||
|
||||
describe('getUnstageAllPaths', () => {
|
||||
it('returns only staged-area paths', () => {
|
||||
const entries: GitStatusEntry[] = [
|
||||
entry({ path: 'a.ts', area: 'staged' }),
|
||||
entry({ path: 'b.ts', area: 'unstaged' }),
|
||||
entry({ path: 'c.ts', area: 'untracked', status: 'untracked' })
|
||||
]
|
||||
expect(getUnstageAllPaths(entries)).toEqual(['a.ts'])
|
||||
})
|
||||
|
||||
it('includes staged conflict rows', () => {
|
||||
const entries: GitStatusEntry[] = [
|
||||
entry({ path: 'clean.ts', area: 'staged' }),
|
||||
@@ -189,11 +126,6 @@ describe('getUnstageAllPaths', () => {
|
||||
// Unstaging conflicts safely mirrors the per-row action; no conflict filtering is needed.
|
||||
expect(getUnstageAllPaths(entries)).toEqual(['clean.ts', 'conflict.ts', 'resolved.ts'])
|
||||
})
|
||||
|
||||
it('returns an empty array when nothing is staged', () => {
|
||||
expect(getUnstageAllPaths([])).toEqual([])
|
||||
expect(getUnstageAllPaths([entry({ path: 'a.ts', area: 'unstaged' })])).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('runDiscardAllForArea', () => {
|
||||
@@ -248,30 +180,6 @@ describe('runDiscardAllForArea', () => {
|
||||
}
|
||||
}
|
||||
|
||||
it('no-ops when the path list is empty', async () => {
|
||||
const ctx = makeDeps()
|
||||
const result = await runDiscardAllForArea('staged', [], ctx.deps)
|
||||
expect(result).toEqual({ discarded: [], failed: [], aborted: false })
|
||||
expect(ctx.bulkUnstage).not.toHaveBeenCalled()
|
||||
expect(ctx.discardOne).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('discards unstaged paths one-by-one without bulk-unstaging', async () => {
|
||||
const ctx = makeDeps()
|
||||
const result = await runDiscardAllForArea('unstaged', ['a.ts', 'b.ts'], ctx.deps)
|
||||
expect(result).toEqual({ discarded: ['a.ts', 'b.ts'], failed: [], aborted: false })
|
||||
expect(ctx.bulkUnstage).not.toHaveBeenCalled()
|
||||
expect(ctx.discardOneCalls).toEqual(['a.ts', 'b.ts'])
|
||||
})
|
||||
|
||||
it('discards untracked paths one-by-one without bulk-unstaging', async () => {
|
||||
const ctx = makeDeps()
|
||||
const result = await runDiscardAllForArea('untracked', ['new.ts'], ctx.deps)
|
||||
expect(result).toEqual({ discarded: ['new.ts'], failed: [], aborted: false })
|
||||
expect(ctx.bulkUnstage).not.toHaveBeenCalled()
|
||||
expect(ctx.discardOneCalls).toEqual(['new.ts'])
|
||||
})
|
||||
|
||||
it('bulk-unstages staged paths before the per-file discard loop', async () => {
|
||||
const ctx = makeDeps()
|
||||
const result = await runDiscardAllForArea('staged', ['a.ts', 'b.ts'], ctx.deps)
|
||||
@@ -284,14 +192,6 @@ describe('runDiscardAllForArea', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('uses bulk discard when the dependency is available', async () => {
|
||||
const ctx = makeDeps()
|
||||
const result = await runDiscardAllForArea('unstaged', ['a.ts', 'b.ts'], ctx.depsWithBulkDiscard)
|
||||
expect(result).toEqual({ discarded: ['a.ts', 'b.ts'], failed: [], aborted: false })
|
||||
expect(ctx.discardManyCalls).toEqual([['a.ts', 'b.ts']])
|
||||
expect(ctx.discardOne).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('bulk-unstages staged paths before bulk discard', async () => {
|
||||
const ctx = makeDeps()
|
||||
const result = await runDiscardAllForArea('staged', ['a.ts', 'b.ts'], ctx.depsWithBulkDiscard)
|
||||
@@ -339,10 +239,4 @@ describe('runDiscardAllForArea', () => {
|
||||
// Reserve aborted for failed bulk unstage so callers can distinguish no work from partial success.
|
||||
expect(ctx.errors).toEqual([error])
|
||||
})
|
||||
|
||||
it('does not invoke the error callback on a happy-path staged run', async () => {
|
||||
const ctx = makeDeps()
|
||||
await runDiscardAllForArea('staged', ['a.ts'], ctx.deps)
|
||||
expect(ctx.onError).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@ import { describe, expect, it } from 'vitest'
|
||||
import type { GitStatusEntry } from './git-status-types'
|
||||
import {
|
||||
buildSourceControlDisplaySections,
|
||||
getConflictReviewEntries,
|
||||
getSourceControlSectionViewAction,
|
||||
resolveSourceControlGroupOrder,
|
||||
splitPinnedSourceControlConflicts,
|
||||
@@ -26,35 +25,7 @@ function groups(partial: Partial<SourceControlEntryGroups>): SourceControlEntryG
|
||||
}
|
||||
}
|
||||
|
||||
describe('resolveSourceControlGroupOrder', () => {
|
||||
it('keeps Changes first by default', () => {
|
||||
expect(resolveSourceControlGroupOrder(undefined)).toEqual(['unstaged', 'staged'])
|
||||
})
|
||||
|
||||
it('supports staged-first and untracked-first presets', () => {
|
||||
expect(resolveSourceControlGroupOrder('staged-first')).toEqual(['staged', 'unstaged'])
|
||||
expect(resolveSourceControlGroupOrder('untracked-first')).toEqual(['unstaged', 'staged'])
|
||||
})
|
||||
})
|
||||
|
||||
describe('buildSourceControlDisplaySections', () => {
|
||||
it('uses the configured order for normal sections', () => {
|
||||
const sections = buildSourceControlDisplaySections(
|
||||
groups({
|
||||
staged: [entry({ area: 'staged', path: 'staged.ts' })],
|
||||
unstaged: [entry({ area: 'unstaged', path: 'changed.ts' })],
|
||||
untracked: [entry({ area: 'untracked', path: 'new.ts', status: 'untracked' })]
|
||||
}),
|
||||
resolveSourceControlGroupOrder('staged-first')
|
||||
)
|
||||
|
||||
expect(sections.map((section) => section.id)).toEqual(['staged', 'unstaged'])
|
||||
expect(sections[1]?.items.map(({ path, area }) => ({ path, area }))).toEqual([
|
||||
{ path: 'changed.ts', area: 'unstaged' },
|
||||
{ path: 'new.ts', area: 'untracked' }
|
||||
])
|
||||
})
|
||||
|
||||
it('combines working files in path order before staged changes by default', () => {
|
||||
const sections = buildSourceControlDisplaySections(
|
||||
groups({
|
||||
@@ -69,27 +40,6 @@ describe('buildSourceControlDisplaySections', () => {
|
||||
expect(sections[0]?.items.map((item) => item.path)).toEqual(['new.ts', 'working.ts'])
|
||||
})
|
||||
|
||||
it('keeps conflicts pinned before the configured normal order', () => {
|
||||
const sections = buildSourceControlDisplaySections(
|
||||
groups({
|
||||
staged: [entry({ area: 'staged', path: 'staged.ts' })],
|
||||
unstaged: [
|
||||
entry({
|
||||
area: 'unstaged',
|
||||
path: 'conflict.ts',
|
||||
conflictKind: 'both_modified',
|
||||
conflictStatus: 'unresolved'
|
||||
}),
|
||||
entry({ area: 'unstaged', path: 'changed.ts' })
|
||||
],
|
||||
untracked: [entry({ area: 'untracked', path: 'new.ts', status: 'untracked' })]
|
||||
}),
|
||||
resolveSourceControlGroupOrder('staged-first')
|
||||
)
|
||||
|
||||
expect(sections.map((section) => section.id)).toEqual(['conflicts', 'staged', 'unstaged'])
|
||||
})
|
||||
|
||||
it('pins conflict rows and removes them from the normal Changes section', () => {
|
||||
const unresolved = entry({
|
||||
area: 'unstaged',
|
||||
@@ -140,25 +90,6 @@ describe('buildSourceControlDisplaySections', () => {
|
||||
expect(sections[1]?.items).toEqual([staged])
|
||||
})
|
||||
|
||||
it('builds review entries only for unresolved conflicts', () => {
|
||||
expect(
|
||||
getConflictReviewEntries([
|
||||
entry({
|
||||
area: 'unstaged',
|
||||
path: 'conflict.ts',
|
||||
conflictKind: 'both_modified',
|
||||
conflictStatus: 'unresolved'
|
||||
}),
|
||||
entry({
|
||||
area: 'unstaged',
|
||||
path: 'resolved.ts',
|
||||
conflictKind: 'both_modified',
|
||||
conflictStatus: 'resolved_locally'
|
||||
})
|
||||
])
|
||||
).toEqual([{ path: 'conflict.ts', conflictKind: 'both_modified' }])
|
||||
})
|
||||
|
||||
it('routes the pinned Conflicts section to conflict review', () => {
|
||||
const sections = buildSourceControlDisplaySections(
|
||||
groups({
|
||||
@@ -186,26 +117,6 @@ describe('buildSourceControlDisplaySections', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('scopes normal combined-diff actions to the conflict-split section items', () => {
|
||||
const pinned = entry({
|
||||
area: 'unstaged',
|
||||
path: 'resolved.ts',
|
||||
conflictKind: 'both_modified',
|
||||
conflictStatus: 'resolved_locally'
|
||||
})
|
||||
const normal = entry({ area: 'unstaged', path: 'normal.ts' })
|
||||
const sections = buildSourceControlDisplaySections(
|
||||
groups({ unstaged: [pinned, normal] }),
|
||||
resolveSourceControlGroupOrder('changes-first')
|
||||
)
|
||||
|
||||
expect(getSourceControlSectionViewAction(sections[1]!)).toEqual({
|
||||
kind: 'combined-diff',
|
||||
area: 'unstaged',
|
||||
entries: [normal]
|
||||
})
|
||||
})
|
||||
|
||||
it('routes locally resolved-only conflict sections to combined diff', () => {
|
||||
const resolved = entry({
|
||||
area: 'unstaged',
|
||||
|
||||
@@ -2,80 +2,10 @@ import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
dispatchStructuredAgentSessionComposerCommand,
|
||||
isStructuredAgentSessionComposerCommand,
|
||||
structuredAgentSessionGoalObjective,
|
||||
structuredSessionSlashCommands,
|
||||
structuredSlashCommands
|
||||
} from './structured-agent-session-composer'
|
||||
|
||||
describe('structuredSlashCommands', () => {
|
||||
const hostController = {
|
||||
snapshot: [],
|
||||
invokeAction: async () => true,
|
||||
setOption: async () => true,
|
||||
conversationCommands: ['clear', 'compact'] as const,
|
||||
runConversationCommand: async () => ({ accepted: true, error: null })
|
||||
}
|
||||
const REFUSAL = /is not available in chat sessions/
|
||||
|
||||
// Menu and dispatcher must share policy or Codex-only tokens can reach Claude as literal prose instead of being handled.
|
||||
it.each(['codex', 'claude'] as const)(
|
||||
'offers %s only commands the host answers or the agent runs',
|
||||
async (agent) => {
|
||||
const offered = structuredSlashCommands(['clear', 'compact'], agent)
|
||||
expect(offered.length).toBeGreaterThan(0)
|
||||
for (const command of offered) {
|
||||
const outcome = await dispatchStructuredAgentSessionComposerCommand(`/${command.name}`, {
|
||||
...hostController,
|
||||
agent
|
||||
})
|
||||
expect(outcome.error ?? '').not.toMatch(REFUSAL)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// Codex reports no catalog; fallback rows make its working text-driven commands discoverable.
|
||||
it('offers Codex the /goal the model acts on, described from the catalog', () => {
|
||||
const offered = structuredSlashCommands(['clear', 'compact'], 'codex')
|
||||
expect(offered.map((command) => command.name)).toEqual([
|
||||
'model',
|
||||
'effort',
|
||||
'clear',
|
||||
'compact',
|
||||
'goal'
|
||||
])
|
||||
expect(offered.find((command) => command.name === 'goal')?.description).toBe(
|
||||
'Set or view the goal'
|
||||
)
|
||||
expect(isStructuredAgentSessionComposerCommand('/goal', 'codex')).toBe(false)
|
||||
})
|
||||
|
||||
it('adds nothing for an agent whose own harness expands its commands', () => {
|
||||
expect(structuredSlashCommands(['clear', 'compact'], 'claude').map((c) => c.name)).toEqual([
|
||||
'model',
|
||||
'effort',
|
||||
'clear',
|
||||
'compact'
|
||||
])
|
||||
})
|
||||
|
||||
it('offers only the commands a chat session can carry out', () => {
|
||||
expect(structuredSlashCommands().map((command) => command.name)).toEqual(['model', 'effort'])
|
||||
})
|
||||
it('adds only implemented host-supported conversation commands', () => {
|
||||
expect(structuredSlashCommands(['clear', 'compact']).map((command) => command.name)).toEqual([
|
||||
'model',
|
||||
'effort',
|
||||
'clear',
|
||||
'compact'
|
||||
])
|
||||
expect(structuredSlashCommands(['compact']).map((command) => command.name)).toEqual([
|
||||
'model',
|
||||
'effort',
|
||||
'compact'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('structuredSessionSlashCommands', () => {
|
||||
const names = (commands: readonly { name: string }[]) => commands.map(({ name }) => name)
|
||||
|
||||
@@ -111,22 +41,6 @@ describe('structuredSessionSlashCommands', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('isStructuredAgentSessionComposerCommand', () => {
|
||||
// Claim typed TUI-only commands even when hidden from the menu, so they do not reach the model as prose.
|
||||
it.each([
|
||||
['codex', 'vim'],
|
||||
['codex', 'clear'],
|
||||
['claude', 'compact'],
|
||||
['claude', 'clear']
|
||||
] as const)('claims the unoffered %s command /%s', (agent, name) => {
|
||||
expect(isStructuredAgentSessionComposerCommand(`/${name}`, agent)).toBe(true)
|
||||
})
|
||||
|
||||
it('leaves an unknown token to the chat path', () => {
|
||||
expect(isStructuredAgentSessionComposerCommand('/my-skill', 'claude')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dispatchStructuredAgentSessionComposerCommand', () => {
|
||||
const controller = {
|
||||
agent: 'codex' as const,
|
||||
@@ -134,14 +48,6 @@ describe('dispatchStructuredAgentSessionComposerCommand', () => {
|
||||
invokeAction: async () => true,
|
||||
setOption: async () => true
|
||||
}
|
||||
|
||||
it('names what does work when a TUI-only command is typed', async () => {
|
||||
const outcome = await dispatchStructuredAgentSessionComposerCommand('/vim', controller)
|
||||
expect(outcome.handled).toBe(true)
|
||||
expect(outcome.error).toBe(
|
||||
'/vim is not available in chat sessions. Use the slash menu to see available commands.'
|
||||
)
|
||||
})
|
||||
it.each(['claude', 'codex'] as const)(
|
||||
'handles %s conversation commands without message fallthrough',
|
||||
async (agent) => {
|
||||
@@ -196,21 +102,6 @@ describe('agent-implemented commands pass through to the agent', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it.each(['clear', 'compact', 'model', 'effort'] as const)(
|
||||
'still claims the host-owned /%s on Claude',
|
||||
async (name) => {
|
||||
expect(isStructuredAgentSessionComposerCommand(`/${name}`, 'claude')).toBe(true)
|
||||
expect(
|
||||
(
|
||||
await dispatchStructuredAgentSessionComposerCommand(`/${name}`, {
|
||||
...controller,
|
||||
agent: 'claude'
|
||||
})
|
||||
).handled
|
||||
).toBe(true)
|
||||
}
|
||||
)
|
||||
|
||||
// Codex app-server lacks a slash parser, but its model handles /goal through goal tools.
|
||||
it('passes /goal through on Codex, arguments and all', async () => {
|
||||
expect(isStructuredAgentSessionComposerCommand('/goal', 'codex')).toBe(false)
|
||||
@@ -244,14 +135,6 @@ describe('agent-implemented commands pass through to the agent', () => {
|
||||
).toEqual({ handled: true, accepted: false, error: null })
|
||||
})
|
||||
|
||||
it('reads the objective a goal-mode draft names, with or without a typed /goal', () => {
|
||||
expect(structuredAgentSessionGoalObjective(' Ship the parser ')).toBe('Ship the parser')
|
||||
expect(structuredAgentSessionGoalObjective('/goal Ship the parser ')).toBe('Ship the parser')
|
||||
expect(structuredAgentSessionGoalObjective('/GOAL')).toBe('')
|
||||
// Goal mode treats non-goal commands as objective text.
|
||||
expect(structuredAgentSessionGoalObjective('/model gpt-5')).toBe('/model gpt-5')
|
||||
})
|
||||
|
||||
it('asks for an objective when a goal-capable host gets a bare /goal', async () => {
|
||||
const setThreadGoalObjective = vi.fn(async () => true)
|
||||
expect(
|
||||
|
||||
@@ -26,10 +26,6 @@ describe('isStructuredAgentSessionThinking', () => {
|
||||
blocks: [{ type: 'text', text: 'Weighing two approaches' }]
|
||||
})
|
||||
|
||||
it('is true while reasoning is the newest thing the turn produced', () => {
|
||||
expect(isStructuredAgentSessionThinking({ items: [turnStart, reasoning(2)] })).toBe(true)
|
||||
})
|
||||
|
||||
it("reads a reasoning row's own state when its host keeps one", () => {
|
||||
const withState = (state: 'running' | 'completed'): AgentJournalRenderItem =>
|
||||
item('reasoning-state', 2, {
|
||||
@@ -64,21 +60,6 @@ describe('isStructuredAgentSessionThinking', () => {
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('is false when a turn produced no reasoning at all', () => {
|
||||
expect(isStructuredAgentSessionThinking({ items: [turnStart] })).toBe(false)
|
||||
expect(isStructuredAgentSessionThinking({ items: [] })).toBe(false)
|
||||
})
|
||||
|
||||
it('does not read an earlier turn as this one reasoning', () => {
|
||||
// A new turn with no content must not inherit the previous turn’s reasoning.
|
||||
const newTurn = item('turn-2-start', 2, {
|
||||
kind: 'status',
|
||||
text: 'Working',
|
||||
turnLifecycle: { turnId: 'turn-2', state: 'running' }
|
||||
})
|
||||
expect(isStructuredAgentSessionThinking({ items: [reasoning(1), newTurn] })).toBe(false)
|
||||
})
|
||||
|
||||
it('does not read a completed turn as reasoning during the next pending dispatch', () => {
|
||||
const completedTurn = item('turn-1', 1, {
|
||||
kind: 'turn',
|
||||
@@ -191,17 +172,6 @@ describe("the live-turn readers answer for the session's own agent", () => {
|
||||
).toBe(false)
|
||||
})
|
||||
|
||||
it('still reports the parent as thinking when the parent itself is reasoning', () => {
|
||||
const ownReasoning = item('own-reasoning', 3, {
|
||||
kind: 'message',
|
||||
role: 'reasoning',
|
||||
blocks: [{ type: 'text', text: 'Weighing two approaches' }]
|
||||
})
|
||||
expect(isStructuredAgentSessionThinking({ items: [turnStart, spawnCall, ownReasoning] })).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it("reports the parent's own running call while a subagent runs its own", () => {
|
||||
const childCall = child('child-grep', 3, {
|
||||
kind: 'tool-call',
|
||||
@@ -236,16 +206,4 @@ describe("the live-turn readers answer for the session's own agent", () => {
|
||||
'Task'
|
||||
)
|
||||
})
|
||||
|
||||
it("reads a row written before linkage existed as the parent's own", () => {
|
||||
const legacyChildCall = item('legacy-call', 3, {
|
||||
kind: 'tool-call',
|
||||
name: 'Grep',
|
||||
input: { pattern: 'x' },
|
||||
state: 'running'
|
||||
})
|
||||
expect(
|
||||
statusStructuredAgentSessionToolCall([turnStart, spawnCall, legacyChildCall])?.name
|
||||
).toBe('Grep')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,7 +7,6 @@ import {
|
||||
applyStructuredAgentSessionModelCatalog,
|
||||
applyStructuredAgentSessionOptions,
|
||||
createStructuredAgentSessionOptionState,
|
||||
settleStructuredAgentSessionBuiltinCatalog,
|
||||
structuredAgentSessionOptionSnapshot,
|
||||
canSetStructuredAgentSessionOption,
|
||||
commitStructuredAgentSessionOption
|
||||
@@ -36,29 +35,6 @@ const HOST_CATALOG: AgentSessionModelCatalogResult = {
|
||||
const LAUNCH = { newLaunch: true }
|
||||
|
||||
describe('structured option state from the host model catalog', () => {
|
||||
it('renders the quiet placeholder before the host answers, never a built-in label', () => {
|
||||
const state = createStructuredAgentSessionOptionState('codex', SEED)
|
||||
expect(structuredAgentSessionOptionSnapshot(state)).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'model',
|
||||
valueSource: 'unknown',
|
||||
settable: false,
|
||||
kind: { type: 'select', choices: [] }
|
||||
})
|
||||
])
|
||||
})
|
||||
|
||||
it('makes the built-in list pickable, naming nothing, once the host says it has none', () => {
|
||||
const state = settleStructuredAgentSessionBuiltinCatalog(
|
||||
createStructuredAgentSessionOptionState('codex', SEED)
|
||||
)
|
||||
expect(state.catalogSource).toBe('builtin')
|
||||
const model = structuredAgentSessionOptionSnapshot(state).find((d) => d.id === 'model')!
|
||||
expect(model.kind.type === 'select' && model.kind.choices.length).toBeGreaterThan(0)
|
||||
expect(model.settable).toBe(true)
|
||||
expect(model.kind.type === 'select' ? model.kind.currentValue : null).toBeUndefined()
|
||||
})
|
||||
|
||||
it('upgrades the seed with host models as a provisional, uncommitted default', () => {
|
||||
const state = applyStructuredAgentSessionModelCatalog(
|
||||
createStructuredAgentSessionOptionState('codex', SEED),
|
||||
@@ -154,58 +130,6 @@ describe('structured option state from the host model catalog', () => {
|
||||
).toBe(hosted)
|
||||
})
|
||||
|
||||
it('refreshes inventory after a live answer while retaining the chat’s own selection', () => {
|
||||
const live = applyStructuredAgentSessionOptions(
|
||||
createStructuredAgentSessionOptionState('codex', SEED),
|
||||
SEED,
|
||||
{
|
||||
models: [{ id: 'gpt-live', label: 'GPT Live', isDefault: true, efforts: [] }],
|
||||
current: { model: 'gpt-live', confirmed: ['model'] }
|
||||
}
|
||||
)
|
||||
expect(live.catalogSource).toBe('live')
|
||||
const refreshed = applyStructuredAgentSessionModelCatalog(live, SEED, HOST_CATALOG, {
|
||||
...LAUNCH,
|
||||
coherentCatalog: true
|
||||
})
|
||||
for (const agent of ['grok', 'pi']) {
|
||||
const sourced = { ...live, record: { ...live.record, agent } }
|
||||
expect(applyStructuredAgentSessionModelCatalog(sourced, SEED, HOST_CATALOG, LAUNCH)).toBe(
|
||||
sourced
|
||||
)
|
||||
}
|
||||
expect(refreshed.catalog?.models.map((row) => row.id)).toEqual(['gpt-hosted'])
|
||||
expect(refreshed.record).toBe(live.record)
|
||||
const snapshot = structuredAgentSessionOptionSnapshot(live)
|
||||
const model = snapshot.find((descriptor) => descriptor.id === 'model')!
|
||||
expect(model.kind.type === 'select' ? model.kind.currentValue : null).toBe('gpt-live')
|
||||
})
|
||||
|
||||
it('shows Pi live choices immediately and keeps them after a late, narrower host list', () => {
|
||||
const seed = structuredAgentSessionSeedCatalog('pi')!
|
||||
const models = [
|
||||
{ id: 'A', label: 'A', isDefault: true, efforts: [] },
|
||||
{ id: 'B', label: 'B', isDefault: false, efforts: [] }
|
||||
]
|
||||
const launch = { ...LAUNCH, coherentCatalog: true }
|
||||
const live = applyStructuredAgentSessionOptions(
|
||||
createStructuredAgentSessionOptionState('pi', seed),
|
||||
seed,
|
||||
{ models, current: { model: 'A', defaulted: [] } },
|
||||
launch
|
||||
)
|
||||
const choices = (state: typeof live) => structuredAgentSessionOptionSnapshot(state)[0]?.kind
|
||||
expect(choices(live)).toMatchObject({ choices: [{ value: 'A' }, { value: 'B' }] })
|
||||
const settled = applyStructuredAgentSessionModelCatalog(
|
||||
live,
|
||||
seed,
|
||||
{ origin: 'probe', models: [models[0]!], fetchedAt: 1 },
|
||||
launch
|
||||
)
|
||||
expect(choices(settled)).toEqual(choices(live))
|
||||
expect(canSetStructuredAgentSessionOption(settled, 'model', 'B')).toBe(true)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['codex', false],
|
||||
['pi', true]
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { AgentSessionQueuedMessage } from './agent-session-wire'
|
||||
import {
|
||||
newestSteerableQueuedMessageCard,
|
||||
projectQueuedMessageCards,
|
||||
queuedMessageCardSteers,
|
||||
queuedMessagesQueuePause
|
||||
} from './structured-agent-session-queued-cards'
|
||||
|
||||
@@ -51,16 +50,6 @@ function handOff(
|
||||
const IDLE = {}
|
||||
|
||||
describe('queued message cards', () => {
|
||||
it('orders by host position whatever order the list arrives in', () => {
|
||||
const cards = projectQueuedMessageCards([draft('b', 2), draft('a', 1), draft('c', 3)], [], IDLE)
|
||||
expect(cards.map((card) => card.messageId)).toEqual(['a', 'b', 'c'])
|
||||
expect(cards[0]?.text).toBe('text of a')
|
||||
})
|
||||
|
||||
it('holds: a waiting card waits its turn', () => {
|
||||
expect(projectQueuedMessageCards([draft('a', 1)], [], IDLE)[0]?.hold).toBe('turn')
|
||||
})
|
||||
|
||||
it('a returned card carries its stored reason and blocks the label of drafts behind it', () => {
|
||||
const cards = projectQueuedMessageCards(
|
||||
[
|
||||
@@ -83,15 +72,6 @@ describe('queued message cards', () => {
|
||||
expect(cards[1]?.hold).toBe('behind-returned')
|
||||
})
|
||||
|
||||
it('a paused draft says so, carrying the host marker for the caption to localize', () => {
|
||||
const cards = projectQueuedMessageCards(
|
||||
[draft('a', 1, { paused: true, pausedReason: 'send_failed' })],
|
||||
[],
|
||||
IDLE
|
||||
)
|
||||
expect(cards[0]).toMatchObject({ hold: 'paused', pausedReason: 'send_failed' })
|
||||
})
|
||||
|
||||
it('shows a draft a Stop put back: its rejected hand-off is what sent it back', () => {
|
||||
// The Stop pause belongs to the queue, so the draft itself carries no hold.
|
||||
const requeued = draft('requeued', 1)
|
||||
@@ -147,15 +127,6 @@ describe('queued message cards', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('a paused queue holds every waiting card, in order', () => {
|
||||
const cards = projectQueuedMessageCards([draft('held', 1), draft('typed-after', 2)], [], {
|
||||
queuePaused: true
|
||||
})
|
||||
expect(cards.map((card) => card.hold)).toEqual(['queue-paused', 'queue-paused'])
|
||||
// Still Steer: the header row, not the card, says it waits.
|
||||
expect(cards.map((card) => queuedMessageCardSteers(card))).toEqual([true, true])
|
||||
})
|
||||
|
||||
it("the header names the queue's pause while it holds a card, and none over cards Resume would not send", () => {
|
||||
const stopped = { reason: 'stopped' } as const
|
||||
const project = (messages: AgentSessionQueuedMessage[], queuePaused = true) =>
|
||||
@@ -187,12 +158,6 @@ describe('queued message cards', () => {
|
||||
expect(cards.map((card) => card.hold)).toEqual(['paused', 'turn', 'paused', 'turn'])
|
||||
})
|
||||
|
||||
it('steers the newest card', () => {
|
||||
const cards = projectQueuedMessageCards([draft('a', 1), draft('b', 2)], [], IDLE)
|
||||
expect(newestSteerableQueuedMessageCard(cards)?.messageId).toBe('b')
|
||||
expect(newestSteerableQueuedMessageCard([])).toBeNull()
|
||||
})
|
||||
|
||||
it('marks a command card, which the chord never steers', () => {
|
||||
const compact: AgentSessionQueuedMessage = {
|
||||
...draft('c', 2),
|
||||
@@ -218,25 +183,3 @@ describe('queued message cards', () => {
|
||||
expect(cards[1]).not.toHaveProperty('waitsForAgent')
|
||||
})
|
||||
})
|
||||
|
||||
describe("another agent's card", () => {
|
||||
it('carries who it is from, read through the shared reader', () => {
|
||||
const from = {
|
||||
kind: 'agent' as const,
|
||||
senders: [
|
||||
{
|
||||
party: { address: 'term_a', terminalHandle: 'term_a', orcaSessionId: null },
|
||||
name: 'Coder'
|
||||
}
|
||||
],
|
||||
orchestration: null
|
||||
}
|
||||
const agentDraft = draft('a', 1)
|
||||
const cards = projectQueuedMessageCards(
|
||||
[{ ...agentDraft, body: { ...agentDraft.body, from } }, draft('b', 2)],
|
||||
[],
|
||||
IDLE
|
||||
)
|
||||
expect(cards.map((card) => card.from)).toEqual([from, undefined])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,47 +5,6 @@ import {
|
||||
} from './terminal-composer-draft'
|
||||
|
||||
describe('detectTerminalComposerDraft', () => {
|
||||
it('separates a cursor-right suggestion from the composer line', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
rows: ['────────', '❯ proceed with the release'],
|
||||
typedRows: ['────────', '❯'],
|
||||
promptGlyphBoldRows: [false, false],
|
||||
rowsBelow: [],
|
||||
typedRowsBelow: [],
|
||||
beforeCursor: '❯ ',
|
||||
afterCursor: '',
|
||||
rawAfterCursor: 'proceed with the release',
|
||||
cursorHidden: false,
|
||||
cursorViewportRow: 8
|
||||
})
|
||||
).toEqual({
|
||||
text: 'proceed with the release',
|
||||
promptRow: 8,
|
||||
cursorRow: 8,
|
||||
endRow: 8,
|
||||
promptGlyph: '❯'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps stock dim placeholders out of draft metadata', () => {
|
||||
const context = {
|
||||
rows: ['› Ask Codex to do anything'],
|
||||
typedRows: ['›'],
|
||||
promptGlyphBoldRows: [true],
|
||||
rowsBelow: ['', 'gpt-5.6 · ~/repo'],
|
||||
typedRowsBelow: ['', 'gpt-5.6 · ~/repo'],
|
||||
beforeCursor: '› ',
|
||||
afterCursor: '',
|
||||
rawAfterCursor: 'Ask Codex to do anything',
|
||||
cursorHidden: false,
|
||||
cursorViewportRow: 4
|
||||
}
|
||||
|
||||
expect(detectTerminalComposerDraft(context)).toBeNull()
|
||||
expect(hasTerminalComposerPlaceholder(context)).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps a typed draft whose stock placeholder is still rendered to its right', () => {
|
||||
// Repaints can leave the placeholder beside typed text; do not mask that user draft.
|
||||
const context = {
|
||||
@@ -71,23 +30,6 @@ describe('detectTerminalComposerDraft', () => {
|
||||
expect(hasTerminalComposerPlaceholder(context)).toBe(false)
|
||||
})
|
||||
|
||||
it('does not duplicate real typed text left of the cursor', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
rows: ['────────', '❯ review the change'],
|
||||
typedRows: ['────────', '❯ review the change'],
|
||||
promptGlyphBoldRows: [false, false],
|
||||
rowsBelow: [],
|
||||
typedRowsBelow: [],
|
||||
beforeCursor: '❯ review the change',
|
||||
afterCursor: '',
|
||||
rawAfterCursor: '',
|
||||
cursorHidden: false,
|
||||
cursorViewportRow: 2
|
||||
})?.text
|
||||
).toBe('review the change')
|
||||
})
|
||||
|
||||
it('preserves a shell prompt that happens to use the Claude glyph', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
@@ -145,58 +87,6 @@ describe('detectTerminalComposerDraft', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('recognizes the bold Codex ultra composer glyph', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
rows: ['» review the change'],
|
||||
typedRows: ['» review the change'],
|
||||
promptGlyphBoldRows: [true],
|
||||
rowsBelow: ['gpt-5.6 · ~/repo'],
|
||||
typedRowsBelow: ['gpt-5.6 · ~/repo'],
|
||||
beforeCursor: '» review the change',
|
||||
afterCursor: '',
|
||||
rawAfterCursor: '',
|
||||
cursorHidden: false,
|
||||
cursorViewportRow: 4
|
||||
})
|
||||
).toMatchObject({ text: 'review the change', promptGlyph: '»' })
|
||||
})
|
||||
|
||||
it('recognizes the bold Codex composer when its footer is visible', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
rows: ['› review the change'],
|
||||
typedRows: ['› review the change'],
|
||||
promptGlyphBoldRows: [true],
|
||||
rowsBelow: ['gpt-5.6 · ~/repo'],
|
||||
typedRowsBelow: ['gpt-5.6 · ~/repo'],
|
||||
beforeCursor: '› review the change',
|
||||
afterCursor: '',
|
||||
rawAfterCursor: '',
|
||||
cursorHidden: false,
|
||||
cursorViewportRow: 4
|
||||
})
|
||||
).toMatchObject({ text: 'review the change', promptGlyph: '›' })
|
||||
})
|
||||
|
||||
it('recognizes a Codex composer with a context-only status footer', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
rows: ['› review the change'],
|
||||
typedRows: ['› review the change'],
|
||||
promptGlyphBoldRows: [true],
|
||||
rowsBelow: ['', ' Context 12% used'],
|
||||
typedRowsBelow: ['', ''],
|
||||
rowsBelowWrapped: [false, false],
|
||||
beforeCursor: '› review the change',
|
||||
afterCursor: '',
|
||||
rawAfterCursor: '',
|
||||
cursorHidden: false,
|
||||
cursorViewportRow: 4
|
||||
})
|
||||
).toMatchObject({ text: 'review the change', promptGlyph: '›' })
|
||||
})
|
||||
|
||||
it('recognizes a Codex composer with an arbitrary dimmed thread-title footer', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
@@ -233,23 +123,6 @@ describe('detectTerminalComposerDraft', () => {
|
||||
expect(hasTerminalComposerPlaceholder(context)).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps the side-thread placeholder out of draft metadata', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
rows: ['› Ask a follow-up question'],
|
||||
typedRows: ['›'],
|
||||
promptGlyphBoldRows: [true],
|
||||
rowsBelow: [],
|
||||
typedRowsBelow: [],
|
||||
beforeCursor: '› ',
|
||||
afterCursor: '',
|
||||
rawAfterCursor: 'Ask a follow-up question',
|
||||
cursorHidden: false,
|
||||
cursorViewportRow: 4
|
||||
})
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('keeps a wrapped stock placeholder out of draft metadata', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
@@ -267,25 +140,6 @@ describe('detectTerminalComposerDraft', () => {
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('joins soft-wrapped continuation rows without inserting a newline', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
rows: ['────────', '❯ proceed with the'],
|
||||
typedRows: ['────────', '❯'],
|
||||
promptGlyphBoldRows: [false, false],
|
||||
rowsWrapped: [false, false],
|
||||
rowsBelow: ['release'],
|
||||
typedRowsBelow: [''],
|
||||
rowsBelowWrapped: [true],
|
||||
beforeCursor: '❯ ',
|
||||
afterCursor: '',
|
||||
rawAfterCursor: 'proceed with the ',
|
||||
cursorHidden: false,
|
||||
cursorViewportRow: 8
|
||||
})?.text
|
||||
).toBe('proceed with the release')
|
||||
})
|
||||
|
||||
it('keeps a draft continuation containing a middle dot', () => {
|
||||
expect(
|
||||
detectTerminalComposerDraft({
|
||||
|
||||
@@ -4,13 +4,6 @@ import { TerminalKittyKeyboardModeTracker } from './terminal-kitty-keyboard-mode
|
||||
import { CONFIRMED_SHELL_MODE_RESET } from './terminal-mode-reset-profiles'
|
||||
|
||||
describe('TerminalKittyKeyboardModeTracker', () => {
|
||||
it('starts inactive and ignores non-kitty sequences', () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
expect(tracker.flags).toBe(0)
|
||||
tracker.scan('plain output \x1b[?2004h\x1b[38;5;10mcolored\x1b[0m')
|
||||
expect(tracker.flags).toBe(0)
|
||||
})
|
||||
|
||||
// Missing snapshot data must not reset already observed mode flags.
|
||||
it('treats a nullish chunk as a no-op', () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
@@ -23,12 +16,6 @@ describe('TerminalKittyKeyboardModeTracker', () => {
|
||||
expect(tracker.snapshotFlags).toBe(5)
|
||||
})
|
||||
|
||||
it('does not treat CSI u (restore cursor) or the CSI ? u query as kitty state', () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
tracker.scan('\x1b[u\x1b[?u')
|
||||
expect(tracker.flags).toBe(0)
|
||||
})
|
||||
|
||||
it('tracks push and pop like xterm, including the pop-to-empty zeroing', () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
tracker.scan('\x1b[>1u')
|
||||
@@ -57,17 +44,6 @@ describe('TerminalKittyKeyboardModeTracker', () => {
|
||||
expect(tracker.flags).toBe(4)
|
||||
})
|
||||
|
||||
it("clears state for Orca's defensive reset sequence and RIS", () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
tracker.scan('\x1b[>1u')
|
||||
tracker.scan('\x1b[<99u\x1b[=0u')
|
||||
expect(tracker.flags).toBe(0)
|
||||
|
||||
tracker.scan('\x1b[>1u')
|
||||
tracker.scan('\x1bc')
|
||||
expect(tracker.flags).toBe(0)
|
||||
})
|
||||
|
||||
// Why: a confirmed-shell reset lands between live chunks in both records, so they must agree.
|
||||
it('agrees with xterm when the confirmed-shell reset interrupts a split live sequence', async () => {
|
||||
const term = new Terminal({ allowProposedApi: true, vtExtensions: { kittyKeyboard: true } })
|
||||
@@ -95,21 +71,6 @@ describe('TerminalKittyKeyboardModeTracker', () => {
|
||||
term.dispose()
|
||||
})
|
||||
|
||||
it('keeps per-screen flags across alternate-screen switches', () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
tracker.scan('\x1b[>1u')
|
||||
expect(tracker.flags).toBe(1)
|
||||
tracker.scan('\x1b[?1049h')
|
||||
expect(tracker.hasObservedAlternateScreenSwitch).toBe(true)
|
||||
expect(tracker.isAlternateScreen).toBe(true)
|
||||
expect(tracker.flags).toBe(0)
|
||||
tracker.scan('\x1b[>2u')
|
||||
expect(tracker.flags).toBe(2)
|
||||
tracker.scan('\x1b[?1049l')
|
||||
expect(tracker.isAlternateScreen).toBe(false)
|
||||
expect(tracker.flags).toBe(1)
|
||||
})
|
||||
|
||||
it('handles sequences split across chunks and C1 CSI', () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
tracker.scan('\x1b[>')
|
||||
@@ -122,22 +83,6 @@ describe('TerminalKittyKeyboardModeTracker', () => {
|
||||
expect(tracker.flags).toBe(7)
|
||||
})
|
||||
|
||||
it('leaves negotiated state unchanged for plain output chunks', () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
tracker.scan('\x1b[>1u')
|
||||
tracker.scan('shell output '.repeat(100))
|
||||
expect(tracker.flags).toBe(1)
|
||||
expect(tracker.snapshotFlags).toBe(1)
|
||||
})
|
||||
|
||||
it('caps the mirrored stack without losing the current flags', () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
for (let i = 0; i < 40; i++) {
|
||||
tracker.scan(`\x1b[>${(i % 3) + 1}u`)
|
||||
}
|
||||
expect(tracker.flags).toBe((39 % 3) + 1)
|
||||
})
|
||||
|
||||
it('reset() returns to the inactive state', () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
tracker.scan('\x1b[>1u\x1b[?1049h\x1b[>2u')
|
||||
@@ -206,19 +151,6 @@ describe('TerminalKittyKeyboardModeTracker', () => {
|
||||
expect(fresh.snapshotFlags).toBeUndefined()
|
||||
})
|
||||
|
||||
it('restores known 0 and known 8 onto the active screen', () => {
|
||||
const inactive = new TerminalKittyKeyboardModeTracker()
|
||||
inactive.resetForSnapshot()
|
||||
inactive.restoreSnapshotFlags(0)
|
||||
expect(inactive.snapshotFlags).toBe(0)
|
||||
|
||||
const negotiated = new TerminalKittyKeyboardModeTracker()
|
||||
negotiated.resetForSnapshot()
|
||||
negotiated.restoreSnapshotFlags(8)
|
||||
expect(negotiated.flags).toBe(8)
|
||||
expect(negotiated.snapshotFlags).toBe(8)
|
||||
})
|
||||
|
||||
it('ignores values that are not non-negative safe integers', () => {
|
||||
const tracker = new TerminalKittyKeyboardModeTracker()
|
||||
tracker.resetForSnapshot()
|
||||
@@ -322,16 +254,6 @@ describe('TerminalKittyKeyboardModeTracker', () => {
|
||||
tracker.restoreSnapshotFlags(8)
|
||||
expect(tracker.hasProvenBaseline).toBe(true)
|
||||
})
|
||||
|
||||
it('grounds on RIS and DECSTR arriving in the stream', () => {
|
||||
const ris = new TerminalKittyKeyboardModeTracker()
|
||||
ris.scan('\x1bc')
|
||||
expect(ris.hasProvenBaseline).toBe(true)
|
||||
|
||||
const softReset = new TerminalKittyKeyboardModeTracker()
|
||||
softReset.scan('\x1b[!p')
|
||||
expect(softReset.hasProvenBaseline).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// Why: xterm ignores CSI u while the protocol is withheld, so the mirror must stay at 0 too.
|
||||
|
||||
Reference in New Issue
Block a user