mirror of
https://github.com/stablyai/orca.git
synced 2026-11-13 09:27:29 +00:00
test: remove repeated wiring and suspended orchestration suites (#41302)
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -4699,7 +4699,6 @@
|
||||
"src/renderer/src/components/github/code-host-rate-limit-stopped-wsl.test.tsx": 2457,
|
||||
"src/renderer/src/components/github/github-mention-option-filter.test.ts": 52,
|
||||
"src/renderer/src/components/github/github-pr-reviewer-candidate-filter.test.ts": 47,
|
||||
"src/renderer/src/components/github/github-work-item-issue-target.test.ts": 324,
|
||||
"src/renderer/src/components/github/github-work-item-mutation-target.test.ts": 348,
|
||||
"src/renderer/src/components/github/use-image-input.test.ts": 2619,
|
||||
"src/renderer/src/components/gitlab-item-dialog/use-gitlab-primary-actions.test.ts": 2141,
|
||||
|
||||
@@ -42,15 +42,6 @@ describe('DroidHookService', () => {
|
||||
removeTreeSync(hoisted.home)
|
||||
})
|
||||
|
||||
it('does not register PermissionRequest', () => {
|
||||
const status = new DroidHookService().install()
|
||||
|
||||
expect(status.state).toBe('installed')
|
||||
const hooks = readSettings().hooks
|
||||
expect(hooks?.PermissionRequest).toBeUndefined()
|
||||
expect(JSON.stringify(hooks?.Notification)).toContain('droid-hook')
|
||||
})
|
||||
|
||||
it('removes a managed PermissionRequest command and keeps a user command', () => {
|
||||
mkdirSync(dirname(settingsPath()), { recursive: true })
|
||||
writeFileSync(
|
||||
@@ -90,30 +81,4 @@ describe('DroidHookService', () => {
|
||||
}
|
||||
])
|
||||
})
|
||||
|
||||
it('drops a previously managed PermissionRequest hook but keeps the user hook on it', () => {
|
||||
const configPath = join(hoisted.home, '.factory', 'settings.json')
|
||||
const userDefinition = {
|
||||
matcher: 'Edit',
|
||||
hooks: [{ type: 'command', command: '/usr/local/bin/my-approval.sh' }]
|
||||
}
|
||||
const managedCommand = join(hoisted.home, '.orca', 'agent-hooks', 'droid-hook.sh')
|
||||
mkdirSync(dirname(configPath), { recursive: true })
|
||||
writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({
|
||||
hooks: {
|
||||
PermissionRequest: [
|
||||
{ matcher: '*', hooks: [{ type: 'command', command: managedCommand }] },
|
||||
userDefinition
|
||||
]
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
expect(new DroidHookService().install().state).toBe('installed')
|
||||
|
||||
const installed = readSettings()
|
||||
expect(installed.hooks?.PermissionRequest).toEqual([userDefinition])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { HEADLESS_RUNTIME_WINDOW_ID } from '../../../shared/runtime-session-contracts'
|
||||
import type { RuntimeMobileSessionTabsResult } from '../../../shared/runtime-session-contracts'
|
||||
import * as selection from './mobile-session-tab-selection'
|
||||
|
||||
vi.mock('../../../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
|
||||
@@ -19,43 +18,9 @@ const WORKTREE = `id:${TEST_WORKTREE_ID}`
|
||||
const CHAT = 'agent-session:s1'
|
||||
|
||||
/** The three saved tabs on a server with no window, whose spawns bind their pane as the host's do. */
|
||||
async function headlessRuntime(count = 3) {
|
||||
async function headlessRuntime() {
|
||||
const harness = threeTabSurfaceRuntime()
|
||||
const { runtime, bindPane } = harness
|
||||
if (count > 3) {
|
||||
const session = harness.store.getSession()
|
||||
const tabs = session.tabsByWorktree[TEST_WORKTREE_ID]!
|
||||
const extra = Array.from({ length: count - 3 }, (_, index) => ({
|
||||
...tabs[0]!,
|
||||
id: `00000000-0000-4000-8000-${index.toString().padStart(12, '0')}`,
|
||||
ptyId: null,
|
||||
sortOrder: index + 3,
|
||||
createdAt: index + 4
|
||||
}))
|
||||
harness.setSession({
|
||||
...session,
|
||||
tabsByWorktree: { ...session.tabsByWorktree, [TEST_WORKTREE_ID]: [...tabs, ...extra] },
|
||||
terminalLayoutsByTabId: {
|
||||
...session.terminalLayoutsByTabId,
|
||||
...Object.fromEntries(
|
||||
extra.map((tab, index) => {
|
||||
const leafId = `10000000-0000-4000-8000-${index.toString().padStart(12, '0')}`
|
||||
return [
|
||||
tab.id,
|
||||
{ root: { type: 'leaf', leafId }, activeLeafId: leafId, expandedLeafId: null }
|
||||
]
|
||||
})
|
||||
)
|
||||
},
|
||||
tabGroups: {
|
||||
...session.tabGroups,
|
||||
[TEST_WORKTREE_ID]: session.tabGroups![TEST_WORKTREE_ID]!.map((group) => ({
|
||||
...group,
|
||||
tabOrder: [...group.tabOrder, ...extra.map((tab) => tab.id)]
|
||||
}))
|
||||
}
|
||||
})
|
||||
}
|
||||
runtime.syncWindowGraph(HEADLESS_RUNTIME_WINDOW_ID, { tabs: [], leaves: [] })
|
||||
runtime.setPtyController({
|
||||
spawn: vi.fn(async (args: { worktreeId?: string; tabId?: string; leafId?: string }) => {
|
||||
@@ -83,60 +48,6 @@ const orderOf = (rows: readonly { type: string; id: string; parentTabId?: string
|
||||
]
|
||||
|
||||
describe("a phone's layout change on a windowless server (switch on)", () => {
|
||||
it('bounds explicit split selection traversal at N and 10N tabs', async () => {
|
||||
const measurements: number[] = []
|
||||
const select = selection.selectMobileSessionTab
|
||||
for (const count of [50, 500]) {
|
||||
const { runtime } = await headlessRuntime(count)
|
||||
let visits = 0
|
||||
function observed<T>(items: T[]): T[] {
|
||||
const rows = [...items]
|
||||
items.forEach((item, index) => {
|
||||
Object.defineProperty(rows, index, {
|
||||
get() {
|
||||
visits++
|
||||
return item
|
||||
}
|
||||
})
|
||||
})
|
||||
return rows
|
||||
}
|
||||
let selectionVisits = 0
|
||||
const selected = vi
|
||||
.spyOn(selection, 'selectMobileSessionTab')
|
||||
.mockImplementation((snapshot, active) => {
|
||||
const tabs = observed(snapshot.tabs)
|
||||
const tabGroups = snapshot.tabGroups?.map((group) => ({
|
||||
...group,
|
||||
tabOrder: observed(group.tabOrder)
|
||||
}))
|
||||
visits = 0
|
||||
const result = select({ ...snapshot, tabs, tabGroups }, active)
|
||||
selectionVisits += visits
|
||||
return result
|
||||
})
|
||||
await runtime.moveMobileSessionTab(WORKTREE, {
|
||||
kind: 'split',
|
||||
tabId: SECOND_TAB_ID,
|
||||
targetGroupId: SAVED_GROUP_ID,
|
||||
splitDirection: 'right'
|
||||
})
|
||||
expect(selected).toHaveBeenCalledTimes(1)
|
||||
// The original tab has two panes: resolve + flags + one row copy, plus one tab-order scan.
|
||||
expect(selectionVisits).toBeGreaterThan(0)
|
||||
expect(selectionVisits).toBeLessThanOrEqual(3 * (count + 1) + count)
|
||||
measurements.push(selectionVisits)
|
||||
const stored = runtime.mobileSessionTabsByWorktree.get(TEST_WORKTREE_ID)!
|
||||
expect(stored.tabs).toHaveLength(count + 1)
|
||||
expect(stored.activeTabId).toBe(`${SECOND_TAB_ID}::77777777-7777-4777-8777-777777777777`)
|
||||
expect(stored.tabGroups?.[1]?.tabOrder).toEqual([SECOND_TAB_ID])
|
||||
expect(stored.tabs.filter((row) => row.isActive).map((row) => row.id)).toEqual([
|
||||
`${SECOND_TAB_ID}::77777777-7777-4777-8777-777777777777`
|
||||
])
|
||||
selected.mockRestore()
|
||||
}
|
||||
expect(measurements[1]).toBeLessThanOrEqual(10 * measurements[0]!)
|
||||
})
|
||||
it("splits a tab into a group of its own in main's layout, and selects it there", async () => {
|
||||
const { runtime, store } = await headlessRuntime()
|
||||
const selected = `${SECOND_TAB_ID}::77777777-7777-4777-8777-777777777777`
|
||||
@@ -217,23 +128,6 @@ describe("a phone's layout change on a windowless server (switch on)", () => {
|
||||
)
|
||||
})
|
||||
|
||||
it("answers a split main refuses with main's refusal code", async () => {
|
||||
const { runtime } = await headlessRuntime()
|
||||
vi.spyOn(runtime, 'applyLayoutCommand').mockResolvedValue({ ok: false, code: 'invalid_params' })
|
||||
|
||||
await expect(
|
||||
runtime.moveMobileSessionTab(WORKTREE, {
|
||||
kind: 'split',
|
||||
tabId: SECOND_TAB_ID,
|
||||
targetGroupId: SAVED_GROUP_ID,
|
||||
splitDirection: 'right'
|
||||
})
|
||||
).rejects.toThrow('invalid_params')
|
||||
expect(tabStrip(await runtime.listMobileSessionTabs(WORKTREE))).toEqual([
|
||||
[SURFACE_TAB_ID, SECOND_TAB_ID, THIRD_TAB_ID]
|
||||
])
|
||||
})
|
||||
|
||||
it.each([
|
||||
{
|
||||
client: 'a phone with split-group placement',
|
||||
|
||||
@@ -76,27 +76,6 @@ describe('app icon selection', () => {
|
||||
expect(dockSetIconMock.mock.lastCall?.[0].isEmpty()).toBe(true)
|
||||
})
|
||||
|
||||
it('keeps an imported icon out of the app bundle and leaves the live Dock tile alone', async () => {
|
||||
const commands: string[][] = []
|
||||
dockSetIconMock.mockClear()
|
||||
persistMacDockIcon('custom', {
|
||||
appBundlePath: '/Applications/Orca.app',
|
||||
execFile: (file, args, _options, callback) => {
|
||||
commands.push([file, ...args])
|
||||
callback?.(null)
|
||||
},
|
||||
isDevApp: false,
|
||||
platform: 'darwin'
|
||||
})
|
||||
await waitForQueuedPersistence()
|
||||
|
||||
expect(commands.some((command) => command.join(' ').includes('setIcon:image'))).toBe(false)
|
||||
expect(commands.some((command) => command.join(' ').includes('setIcon:(missing value)'))).toBe(
|
||||
true
|
||||
)
|
||||
expect(dockSetIconMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('serializes rapid macOS dock icon persistence so the last icon request wins', async () => {
|
||||
const pendingCallbacks: (() => void)[] = []
|
||||
const execFile = vi.fn(
|
||||
|
||||
@@ -373,25 +373,6 @@ it('refuses normalized data-image HTML without dropping surrounding text', async
|
||||
expect(imageUploads.upload).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('leaves ordinary HTML paste on the editor parser path', async () => {
|
||||
const view = render(<GitHubMarkdownComposer value="" onChange={() => {}} placeholder="HTML" />)
|
||||
await waitFor(() => expect(view.container.querySelector('.tiptap')).not.toBeNull())
|
||||
const parse = vi.spyOn(DOMParser.prototype, 'parseFromString')
|
||||
try {
|
||||
fireEvent.paste(getComposerEditor(view.container), {
|
||||
clipboardData: {
|
||||
files: [],
|
||||
getData: (type: string) =>
|
||||
type === 'text/html' ? '<p>ordinary <strong>formatted</strong> text</p>' : ''
|
||||
}
|
||||
})
|
||||
expect(getComposerEditor(view.container).textContent).toBe('ordinary formatted text')
|
||||
expect(parse).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
parse.mockRestore()
|
||||
}
|
||||
})
|
||||
|
||||
it('shows a failed hosted image as its filename link without changing the uploaded Markdown', async () => {
|
||||
imageUploads.upload
|
||||
.mockReset()
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { runIssueUpdate, runWorkItemBodyUpdate } from './github-work-item-edit-mutations'
|
||||
import type { GitHubWorkItem } from '../../../../shared/github/work-item-types'
|
||||
import type { TaskSourceContext } from '../../../../shared/work-items/task-source-context'
|
||||
import { callRuntimeRpc } from '@/runtime/rpc/calls/runtime-rpc-call'
|
||||
|
||||
vi.mock('@/runtime/rpc/calls/runtime-rpc-call', () => ({
|
||||
callRuntimeRpc: vi.fn()
|
||||
}))
|
||||
vi.mock('@/store', () => ({ useAppStore: { getState: vi.fn(() => ({ repos: [] })) } }))
|
||||
vi.mock('@/components/github/github-work-item-comment-mutations', () => ({
|
||||
notifyWorkItemDetailsMutation: vi.fn()
|
||||
}))
|
||||
|
||||
const ORIGIN = { owner: 'fork-owner', repo: 'widgets', host: 'github.com' }
|
||||
const UPSTREAM = { owner: 'upstream-owner', repo: 'widgets', host: 'github.com' }
|
||||
const projectOrigin = {
|
||||
...UPSTREAM,
|
||||
number: 12,
|
||||
type: 'issue' as const,
|
||||
projectId: 'project-1',
|
||||
projectItemId: 'project-item-12',
|
||||
cacheKey: 'project-cache'
|
||||
}
|
||||
const localSource: TaskSourceContext = {
|
||||
kind: 'task-source',
|
||||
provider: 'github',
|
||||
projectId: 'project-1',
|
||||
hostId: 'local',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
const item: GitHubWorkItem = {
|
||||
id: 'issue:12',
|
||||
type: 'issue',
|
||||
number: 12,
|
||||
title: 'Origin issue',
|
||||
state: 'open',
|
||||
url: 'https://github.com/fork-owner/widgets/issues/12',
|
||||
labels: [],
|
||||
updatedAt: '',
|
||||
author: null,
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
|
||||
describe('issue edits retain the displayed repository', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(callRuntimeRpc).mockResolvedValue({ ok: true })
|
||||
vi.stubGlobal('window', {
|
||||
api: {
|
||||
gh: {
|
||||
updateIssue: vi.fn().mockResolvedValue({ ok: true }),
|
||||
updateIssueBySlug: vi.fn(),
|
||||
updatePullRequestBySlug: vi.fn()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe.each(['local', 'runtime:project-source'] as const)('Project source %s', (hostId) => {
|
||||
const sourceContext = { ...localSource, hostId }
|
||||
it('saves the Project row body through the original exact slug actor', async () => {
|
||||
const bodyImageUrls = {
|
||||
'https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488':
|
||||
'https://private-user-images.githubusercontent.com/1/image.png?jwt=TEST'
|
||||
}
|
||||
vi.mocked(window.api.gh.updateIssueBySlug).mockResolvedValue({ ok: true, bodyImageUrls })
|
||||
vi.mocked(callRuntimeRpc).mockResolvedValue({ ok: true, bodyImageUrls })
|
||||
const result = await runWorkItemBodyUpdate({
|
||||
item: { ...item, url: 'https://github.com/upstream-owner/widgets/issues/12' },
|
||||
repoPath: '/home/fixture/fork-widgets',
|
||||
sourceContext,
|
||||
projectOrigin,
|
||||
body: 'Project edit',
|
||||
parsedSlug: UPSTREAM
|
||||
})
|
||||
expect(result).toEqual({ bodyImageUrls })
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ state: 'closed' as const },
|
||||
{ addLabels: ['bug'] },
|
||||
{ addAssignees: ['fork-assignee'] }
|
||||
])('pins field edits to the opened issue repository: %j', async (updates) => {
|
||||
await runIssueUpdate({
|
||||
repoPath: 'C:\\workspace\\widgets',
|
||||
repoId: 'repo-1',
|
||||
projectOrigin: undefined,
|
||||
number: 12,
|
||||
issueRepo: ORIGIN,
|
||||
updates
|
||||
})
|
||||
|
||||
expect(window.api.gh.updateIssue).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
repoPath: 'C:\\workspace\\widgets',
|
||||
ownerRepo: ORIGIN,
|
||||
updates
|
||||
})
|
||||
)
|
||||
expect(window.api.gh.updateIssueBySlug).not.toHaveBeenCalled()
|
||||
})
|
||||
it.each([true, false])(
|
||||
'forwards local PR body PATCH maps without adding a reread (%s)',
|
||||
async (hasMap) => {
|
||||
const body =
|
||||
''
|
||||
vi.mocked(window.api.gh.updatePullRequestBySlug).mockResolvedValue(
|
||||
hasMap
|
||||
? {
|
||||
ok: true,
|
||||
bodyImageUrls: {
|
||||
'https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488':
|
||||
'https://private-user-images.githubusercontent.com/1/image.png?jwt=TEST'
|
||||
}
|
||||
}
|
||||
: { ok: true }
|
||||
)
|
||||
const result = await runWorkItemBodyUpdate({
|
||||
item: { ...item, type: 'pr' },
|
||||
repoPath: '/repo',
|
||||
sourceContext: localSource,
|
||||
projectOrigin: undefined,
|
||||
body,
|
||||
parsedSlug: ORIGIN
|
||||
})
|
||||
expect(result).toEqual({
|
||||
bodyImageUrls: hasMap
|
||||
? {
|
||||
'https://github.com/user-attachments/assets/ce11040a-fb66-4289-927f-547b16dfc488':
|
||||
'https://private-user-images.githubusercontent.com/1/image.png?jwt=TEST'
|
||||
}
|
||||
: undefined
|
||||
})
|
||||
expect(window.api.gh.updatePullRequestBySlug).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
updates: {
|
||||
body: ''
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
-37
@@ -166,41 +166,4 @@ describe('EphemeralVmsPane', () => {
|
||||
expect(container.textContent).toContain('Newest Recipe')
|
||||
expect(container.textContent).not.toContain('Stale Recipe')
|
||||
})
|
||||
|
||||
it('names the credential-like variables a recipe inherits, without values', async () => {
|
||||
const container = await renderPane()
|
||||
await vi.waitFor(() =>
|
||||
expect(container.querySelector('[data-testid="recipe-env-exposure"]')?.textContent).toContain(
|
||||
'ANTHROPIC_API_KEY, GITHUB_TOKEN'
|
||||
)
|
||||
)
|
||||
expect(container.textContent).toContain('Set passEnv on the recipe')
|
||||
})
|
||||
|
||||
it('lists only allowlisted credentials when the recipe sets passEnv', async () => {
|
||||
const listRecipeCatalog = window.api.ephemeralVm.listRecipeCatalog as ReturnType<typeof vi.fn>
|
||||
listRecipeCatalog.mockReset().mockResolvedValue([
|
||||
{
|
||||
repoId: 'repo-1',
|
||||
repoName: 'Repo',
|
||||
repoPath: '/repo',
|
||||
diagnostics: [],
|
||||
recipes: [
|
||||
{
|
||||
id: 'limited',
|
||||
name: 'Limited',
|
||||
create: 'create',
|
||||
passEnv: ['GITHUB_TOKEN']
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
const container = await renderPane()
|
||||
await vi.waitFor(() =>
|
||||
expect(container.querySelector('[data-testid="recipe-env-exposure"]')?.textContent).toContain(
|
||||
'from passEnv: GITHUB_TOKEN.'
|
||||
)
|
||||
)
|
||||
expect(container.textContent).not.toContain('ANTHROPIC_API_KEY')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { getStateMock, inspectRuntimeTerminalProcessMock, getStatusFeedMock } = vi.hoisted(() => ({
|
||||
getStateMock: vi.fn(),
|
||||
@@ -22,14 +22,6 @@ import { assessWindowCloseRunningWork } from './window-close-running-work'
|
||||
|
||||
const LOCAL_PTY = 'pty-local'
|
||||
const SSH_PTY = 'ssh:openclaw@@pty-7'
|
||||
const RUNTIME_PTY = 'remote:env-1@@handle-1'
|
||||
const OWNERLESS_RUNTIME_PTY = 'remote:handle-2'
|
||||
|
||||
const BUSY = {
|
||||
foregroundProcess: 'pnpm build',
|
||||
hasChildProcesses: true,
|
||||
foregroundProcessEvidence: {}
|
||||
}
|
||||
|
||||
const UNVERIFIABLE = {
|
||||
foregroundProcess: null,
|
||||
@@ -81,41 +73,7 @@ beforeEach(() => {
|
||||
getStatusFeedMock.mockReturnValue(feed(null, 'unverifiable'))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
describe('assessWindowCloseRunningWork', () => {
|
||||
it.each(['working', 'attention'])(
|
||||
'warns for a live %s chat without a PTY or ownership metadata',
|
||||
async (status) => {
|
||||
setState([])
|
||||
representChat()
|
||||
getStatusFeedMock.mockReturnValue(feed(status))
|
||||
|
||||
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
|
||||
kind: 'running'
|
||||
})
|
||||
expect(inspectRuntimeTerminalProcessMock).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it.each([
|
||||
['working', 'unverifiable', {}],
|
||||
['idle', 'live', { hostExecutionOwned: true, children: [{ kind: 'agent', state: 'working' }] }],
|
||||
['idle', 'live', { children: [{ kind: 'agent', state: 'unverifiable' }] }],
|
||||
[null, 'live', {}],
|
||||
[undefined, 'live', {}]
|
||||
])('does not warn for status %s observed as %s', async (status, observation, extra) => {
|
||||
setState([])
|
||||
representChat()
|
||||
getStatusFeedMock.mockReturnValue(feed(status, observation, extra))
|
||||
|
||||
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
|
||||
kind: 'none'
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps equal chat IDs separate across hosts and resolves legacy tabs through their workspace owner', async () => {
|
||||
setState([])
|
||||
const state = getStateMock()
|
||||
@@ -135,17 +93,6 @@ describe('assessWindowCloseRunningWork', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('does not use a local observation for an unserved SSH chat', async () => {
|
||||
setState([])
|
||||
representChat('ssh:openclaw')
|
||||
getStatusFeedMock.mockReturnValue(feed('working'))
|
||||
|
||||
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
|
||||
kind: 'none'
|
||||
})
|
||||
expect(getStatusFeedMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('uses current chat membership, owner and status after terminal probes settle', async () => {
|
||||
setState([LOCAL_PTY])
|
||||
representChat()
|
||||
@@ -183,72 +130,4 @@ describe('assessWindowCloseRunningWork', () => {
|
||||
})
|
||||
expect(getStatusFeedMock).not.toHaveBeenCalled()
|
||||
})
|
||||
// #29916: a paired runtime owns its terminals and keeps them after this client quits.
|
||||
it('does not warn on quit, or probe, for a live process on a paired runtime host', async () => {
|
||||
setState([RUNTIME_PTY, OWNERLESS_RUNTIME_PTY])
|
||||
answerWith({ [RUNTIME_PTY]: BUSY, [OWNERLESS_RUNTIME_PTY]: BUSY })
|
||||
|
||||
await expect(assessWindowCloseRunningWork({ isQuitting: true })).resolves.toEqual({
|
||||
kind: 'none'
|
||||
})
|
||||
expect(inspectRuntimeTerminalProcessMock).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('still warns when closing the window over a live process on a paired runtime host', async () => {
|
||||
setState([RUNTIME_PTY])
|
||||
answerWith({ [RUNTIME_PTY]: BUSY })
|
||||
|
||||
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
|
||||
kind: 'running'
|
||||
})
|
||||
})
|
||||
|
||||
// Unreachable remote work is unverifiable, never proof of exit; quitting silently could orphan it.
|
||||
it('warns rather than quitting silently when a remote host answers unverifiable', async () => {
|
||||
setState([SSH_PTY])
|
||||
answerWith({ [SSH_PTY]: UNVERIFIABLE })
|
||||
|
||||
await expect(assessWindowCloseRunningWork({ isQuitting: true })).resolves.toEqual({
|
||||
kind: 'unverifiable'
|
||||
})
|
||||
})
|
||||
|
||||
it('reports a live process even when a sibling remote pane is only unverifiable', async () => {
|
||||
setState([SSH_PTY, RUNTIME_PTY])
|
||||
answerWith({ [SSH_PTY]: UNVERIFIABLE, [RUNTIME_PTY]: BUSY })
|
||||
|
||||
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
|
||||
kind: 'running'
|
||||
})
|
||||
})
|
||||
|
||||
it('still warns about a live local process when closing the window', async () => {
|
||||
setState([LOCAL_PTY])
|
||||
answerWith({ [LOCAL_PTY]: BUSY })
|
||||
|
||||
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
|
||||
kind: 'running'
|
||||
})
|
||||
})
|
||||
|
||||
// Local probe failure has no lost-transport ambiguity and must not block closing.
|
||||
it('does not warn when only a local probe is unverifiable', async () => {
|
||||
setState([LOCAL_PTY])
|
||||
answerWith({ [LOCAL_PTY]: UNVERIFIABLE })
|
||||
|
||||
await expect(assessWindowCloseRunningWork({ isQuitting: false })).resolves.toEqual({
|
||||
kind: 'none'
|
||||
})
|
||||
})
|
||||
|
||||
// Quit consents to end this machine’s processes (#524); it does not consent to end remote work.
|
||||
it('leaves local-only quit unprompted, and never probes for it', async () => {
|
||||
setState([LOCAL_PTY])
|
||||
answerWith({ [LOCAL_PTY]: BUSY })
|
||||
|
||||
await expect(assessWindowCloseRunningWork({ isQuitting: true })).resolves.toEqual({
|
||||
kind: 'none'
|
||||
})
|
||||
expect(inspectRuntimeTerminalProcessMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,125 +0,0 @@
|
||||
import { expect, test, type Page } from '@playwright/test'
|
||||
|
||||
async function openFixture(page: Page, name: string) {
|
||||
await page.goto(`/fixtures.html?id=orchestration-crew/OrchestrationCrewPanel:${name}`)
|
||||
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
|
||||
const card = page.getByRole('complementary', { name: 'Orchestration' })
|
||||
await expect(card).toBeVisible()
|
||||
const rows = card.locator('.overflow-y-auto')
|
||||
const bounds = await card.boundingBox()
|
||||
if (!bounds) {
|
||||
throw new Error('Card has no bounds')
|
||||
}
|
||||
// Ten workers never fit a short pane, so the rows must scroll inside the card.
|
||||
expect(await rows.evaluate((element) => element.scrollHeight > element.clientHeight)).toBe(true)
|
||||
return bounds
|
||||
}
|
||||
|
||||
test('a saved orchestration opt-in keeps the native panel hidden and composer available', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(
|
||||
'/fixtures.html?id=orchestration-crew/OrchestrationCrewPanel:tenWorkersShortChatExpanded'
|
||||
)
|
||||
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
|
||||
await expect(page.getByRole('complementary', { name: 'Orchestration' })).toHaveCount(0)
|
||||
await expect(page.getByTestId('composer')).toBeVisible()
|
||||
})
|
||||
|
||||
test('in a short terminal the card keeps the bottom quarter clear and scrolls', async ({
|
||||
page
|
||||
}) => {
|
||||
const card = await openFixture(page, 'tenWorkersShortPaneExpanded')
|
||||
const pane = await page.locator('.pane').boundingBox()
|
||||
const limit = pane!.y + pane!.height * 0.75
|
||||
// It reaches the limit, since the rows are taller than the room, but never passes it.
|
||||
expect(card.y + card.height).toBeLessThanOrEqual(limit + 0.5)
|
||||
expect(card.y + card.height).toBeGreaterThan(limit - 2)
|
||||
})
|
||||
|
||||
test('in a terminal at its minimum split height the collapse button stays clickable', async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(
|
||||
'/fixtures.html?id=orchestration-crew/OrchestrationCrewPanel:twoWorkersMinimumPaneExpanded'
|
||||
)
|
||||
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
|
||||
const collapse = page.getByRole('button', { name: 'Collapse orchestration panel' })
|
||||
const button = await collapse.boundingBox()
|
||||
const pane = await page.locator('.pane').boundingBox()
|
||||
// Aim at the part of the button the pane doesn't clip.
|
||||
const x = button!.x + button!.width / 2
|
||||
const y = (button!.y + Math.min(button!.y + button!.height, pane!.y + pane!.height)) / 2
|
||||
expect(y).toBeLessThan(pane!.y + pane!.height)
|
||||
await page.mouse.click(x, y)
|
||||
await expect(page.getByRole('complementary', { name: 'Orchestration' })).toHaveCount(0)
|
||||
})
|
||||
|
||||
for (const [name, width] of [
|
||||
['twoWorkersNarrowPaneExpanded', 100],
|
||||
['twoWorkersMinimumWidthPaneExpanded', 50]
|
||||
] as const) {
|
||||
test(`in a ${width}px-wide pane the whole collapse button fits inside the card`, async ({
|
||||
page
|
||||
}) => {
|
||||
await page.goto(`/fixtures.html?id=orchestration-crew/OrchestrationCrewPanel:${name}`)
|
||||
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
|
||||
const card = await page.getByRole('complementary', { name: 'Orchestration' }).boundingBox()
|
||||
const collapse = page.getByRole('button', { name: 'Collapse orchestration panel' })
|
||||
const button = await collapse.boundingBox()
|
||||
expect(button!.x).toBeGreaterThanOrEqual(card!.x)
|
||||
expect(button!.x + button!.width).toBeLessThanOrEqual(card!.x + card!.width + 0.5)
|
||||
await collapse.click()
|
||||
await expect(page.getByRole('complementary', { name: 'Orchestration' })).toHaveCount(0)
|
||||
})
|
||||
}
|
||||
|
||||
for (const name of ['terminalQuestionDetails']) {
|
||||
for (const width of [280, 360]) {
|
||||
test(`${name} keeps long expanded details inside a ${width}px frame`, async ({ page }) => {
|
||||
await page.goto(`/fixtures.html?id=orchestration-crew/OrchestrationCrewPanel:${name}`)
|
||||
await expect(page.locator('html')).toHaveAttribute('data-fixture-state', 'ready')
|
||||
const frame = page.locator('#component-fixture-frame')
|
||||
await frame.evaluate((element, frameWidth) => {
|
||||
element.style.width = `${frameWidth}px`
|
||||
}, width)
|
||||
const card = page.getByRole('complementary', { name: 'Orchestration' })
|
||||
const details = card.locator('dl')
|
||||
await expect(details).toBeVisible()
|
||||
await expect(details).toContainText('dispatched')
|
||||
expect(await details.innerText()).toContain('unbroken'.repeat(128))
|
||||
const overflow = await details.evaluate((element) => {
|
||||
const card = element.closest('aside')!
|
||||
const bounds = card.getBoundingClientRect()
|
||||
const violations: string[] = []
|
||||
for (const value of element.querySelectorAll('dd')) {
|
||||
const rect = value.getBoundingClientRect()
|
||||
if (rect.left < bounds.left || rect.right > bounds.right + 1) {
|
||||
violations.push('detail value outside card')
|
||||
}
|
||||
if (value.scrollWidth > value.clientWidth + 1) {
|
||||
violations.push('detail value scrolls horizontally')
|
||||
}
|
||||
const walker = document.createTreeWalker(value, NodeFilter.SHOW_TEXT)
|
||||
while (walker.nextNode()) {
|
||||
const range = document.createRange()
|
||||
range.selectNodeContents(walker.currentNode)
|
||||
for (const line of range.getClientRects()) {
|
||||
if (line.left < bounds.left || line.right > bounds.right + 1) {
|
||||
violations.push('text outside card')
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return violations
|
||||
})
|
||||
await test.info().attach('expanded-details', {
|
||||
body: await frame.screenshot(),
|
||||
contentType: 'image/png'
|
||||
})
|
||||
expect(overflow).toEqual([])
|
||||
await card.getByRole('button', { name: /^Show .*task and latest message$/ }).click()
|
||||
await expect(details).toHaveCount(0)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user