mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Wait for retained editor ownership before listing Markdown documents (#26590)
This commit is contained in:
@@ -354,7 +354,8 @@ jobs:
|
||||
. != "tests/e2e/ssh-orcad-browser-capabilities.spec.ts" and
|
||||
. != "tests/e2e/ssh-orcad-browser-service-status.spec.ts" and
|
||||
. != "tests/e2e/ssh-orcad-browser-routing.spec.ts" and
|
||||
. != "tests/e2e/ssh-orcad-editor-ownership.spec.ts"
|
||||
. != "tests/e2e/ssh-orcad-editor-ownership.spec.ts" and
|
||||
. != "tests/e2e/ssh-orcad-markdown-conversion.spec.ts"
|
||||
)' <<<"$TEST_FILES_JSON" > "$RUNNER_TEMP/general-e2e-specs"
|
||||
fi
|
||||
mapfile -t TEST_FILES < "$RUNNER_TEMP/general-e2e-specs"
|
||||
@@ -588,7 +589,7 @@ jobs:
|
||||
orcad-auto-convert-docker:
|
||||
name: ssh host auto-converts to managed orcad (Docker)
|
||||
needs: [build, prepare-native-cache]
|
||||
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-auto-convert.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-idle-exit.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-capabilities.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-service-status.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-routing.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-editor-ownership.spec.ts')
|
||||
if: inputs.test_files == '' || contains(inputs.test_files, 'tests/e2e/ssh-orcad-auto-convert.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-idle-exit.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-capabilities.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-service-status.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-browser-routing.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-editor-ownership.spec.ts') || contains(inputs.test_files, 'tests/e2e/ssh-orcad-markdown-conversion.spec.ts')
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 45
|
||||
env:
|
||||
@@ -624,7 +625,7 @@ jobs:
|
||||
ORCA_RELAY_PATH: ${{ github.workspace }}/out/relay
|
||||
run: |
|
||||
export ORCA_E2E_ORCAD_CONVERT_TEMPLATE="$RUNNER_TEMP/orcad-convert-template"
|
||||
xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/ssh-orcad-auto-convert.spec.ts tests/e2e/ssh-orcad-idle-exit.spec.ts tests/e2e/ssh-orcad-browser-capabilities.spec.ts tests/e2e/ssh-orcad-browser-service-status.spec.ts tests/e2e/ssh-orcad-browser-routing.spec.ts tests/e2e/ssh-orcad-editor-ownership.spec.ts --project=electron-headless --workers=1
|
||||
xvfb-run --auto-servernum bash .github/scripts/e2e-with-window-manager.sh pnpm exec playwright test --config tests/playwright.config.ts tests/e2e/ssh-orcad-auto-convert.spec.ts tests/e2e/ssh-orcad-idle-exit.spec.ts tests/e2e/ssh-orcad-browser-capabilities.spec.ts tests/e2e/ssh-orcad-browser-service-status.spec.ts tests/e2e/ssh-orcad-browser-routing.spec.ts tests/e2e/ssh-orcad-editor-ownership.spec.ts tests/e2e/ssh-orcad-markdown-conversion.spec.ts --project=electron-headless --workers=1
|
||||
- uses: actions/upload-artifact@v7
|
||||
if: failure()
|
||||
with:
|
||||
|
||||
@@ -57,6 +57,7 @@ export const ORCAD_BROWSER_SERVICE_STATUS_E2E_SPEC =
|
||||
'tests/e2e/ssh-orcad-browser-service-status.spec.ts'
|
||||
export const ORCAD_BROWSER_ROUTING_E2E_SPEC = 'tests/e2e/ssh-orcad-browser-routing.spec.ts'
|
||||
export const ORCAD_EDITOR_OWNERSHIP_E2E_SPEC = 'tests/e2e/ssh-orcad-editor-ownership.spec.ts'
|
||||
export const ORCAD_MARKDOWN_CONVERSION_E2E_SPEC = 'tests/e2e/ssh-orcad-markdown-conversion.spec.ts'
|
||||
export const DEDICATED_E2E_SPECS = [
|
||||
...DOCKER_SSH_E2E_SPECS,
|
||||
NODE_NETWORK_E2E_SPEC,
|
||||
@@ -70,7 +71,8 @@ export const DEDICATED_E2E_SPECS = [
|
||||
ORCAD_BROWSER_CAPABILITIES_E2E_SPEC,
|
||||
ORCAD_BROWSER_SERVICE_STATUS_E2E_SPEC,
|
||||
ORCAD_BROWSER_ROUTING_E2E_SPEC,
|
||||
ORCAD_EDITOR_OWNERSHIP_E2E_SPEC
|
||||
ORCAD_EDITOR_OWNERSHIP_E2E_SPEC,
|
||||
ORCAD_MARKDOWN_CONVERSION_E2E_SPEC
|
||||
]
|
||||
const dedicatedSpecs = new Set(DEDICATED_E2E_SPECS)
|
||||
const dockerSpecs = new Set(DOCKER_SSH_E2E_SPECS)
|
||||
@@ -116,7 +118,8 @@ export function classifyE2eJobs(input, sshSourceChanged = 'false') {
|
||||
spec === ORCAD_BROWSER_CAPABILITIES_E2E_SPEC ||
|
||||
spec === ORCAD_BROWSER_SERVICE_STATUS_E2E_SPEC ||
|
||||
spec === ORCAD_BROWSER_ROUTING_E2E_SPEC ||
|
||||
spec === ORCAD_EDITOR_OWNERSHIP_E2E_SPEC
|
||||
spec === ORCAD_EDITOR_OWNERSHIP_E2E_SPEC ||
|
||||
spec === ORCAD_MARKDOWN_CONVERSION_E2E_SPEC
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,10 @@ it('skips the general consumer only when every requested spec has a dedicated ow
|
||||
}
|
||||
expect(classify(DEDICATED_E2E_SPECS).e2e_run_changed).toBe(false)
|
||||
const future = 'tests/e2e/future-unclassified.spec.ts'
|
||||
expect(classify([future])).toEqual({ e2e_run_changed: true, e2e_needs_build: true })
|
||||
expect(classify([future])).toEqual({
|
||||
e2e_run_changed: true,
|
||||
e2e_needs_build: true
|
||||
})
|
||||
expect(selectGeneralE2eSpecs([...DEDICATED_E2E_SPECS, future])).toEqual([future])
|
||||
expect(classify([...DEDICATED_E2E_SPECS, future]).e2e_run_changed).toBe(true)
|
||||
})
|
||||
@@ -181,7 +184,28 @@ it.each([
|
||||
])('routes %s to the template-building editor ownership lane', (file) => {
|
||||
const spec = 'tests/e2e/ssh-orcad-editor-ownership.spec.ts'
|
||||
expect(selectPrE2eSpecs([file])).toContain(spec)
|
||||
expect(classify([spec])).toEqual({ e2e_run_changed: false, e2e_needs_build: true })
|
||||
expect(classify([spec])).toEqual({
|
||||
e2e_run_changed: false,
|
||||
e2e_needs_build: true
|
||||
})
|
||||
const job = workflow.jobs['orcad-auto-convert-docker']
|
||||
expect(job.if).toContain(spec)
|
||||
expect(job.steps.find((step) => step.name === 'Convert a relay-era Docker host').run).toContain(
|
||||
spec
|
||||
)
|
||||
})
|
||||
|
||||
it.each([
|
||||
'src/renderer/src/components/editor/useMarkdownDocuments.ts',
|
||||
'src/renderer/src/components/editor/restored-editor-workspace-runtime-owner.ts',
|
||||
'src/renderer/src/components/editor/migrate-restored-editor-file-owner.ts'
|
||||
])('routes %s to the template-building Markdown conversion lane', (file) => {
|
||||
const spec = 'tests/e2e/ssh-orcad-markdown-conversion.spec.ts'
|
||||
expect(selectPrE2eSpecs([file])).toContain(spec)
|
||||
expect(classify([spec])).toEqual({
|
||||
e2e_run_changed: false,
|
||||
e2e_needs_build: true
|
||||
})
|
||||
const job = workflow.jobs['orcad-auto-convert-docker']
|
||||
expect(job.if).toContain(spec)
|
||||
expect(job.steps.find((step) => step.name === 'Convert a relay-era Docker host').run).toContain(
|
||||
|
||||
@@ -23,6 +23,15 @@ export const PR_E2E_SOURCE_ROUTES = [
|
||||
file
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'ssh.orcad-markdown-conversion',
|
||||
specs: ['tests/e2e/ssh-orcad-markdown-conversion.spec.ts'],
|
||||
matches: (file) =>
|
||||
isProductSource(file) &&
|
||||
/^src\/renderer\/src\/components\/editor\/(?:useMarkdownDocuments|restored-editor-workspace-runtime-owner|migrate-restored-editor-file-owner)\.ts$/.test(
|
||||
file
|
||||
)
|
||||
},
|
||||
{
|
||||
id: 'serve.orcad-mode-switch',
|
||||
specs: ['tests/e2e/orcad-serve-mode-switch.spec.ts'],
|
||||
|
||||
@@ -19,7 +19,14 @@ export type RestoredEditorWorkspaceRuntimeOwner = {
|
||||
*/
|
||||
export function findRestoredEditorWorkspaceRuntimeOwner(
|
||||
state: AppState,
|
||||
file: OpenFile,
|
||||
file: Pick<
|
||||
OpenFile,
|
||||
| 'worktreeId'
|
||||
| 'filePath'
|
||||
| 'externalSshTargetId'
|
||||
| 'operationProvenance'
|
||||
| 'runtimeEnvironmentId'
|
||||
>,
|
||||
worktreeId: string | undefined
|
||||
): RestoredEditorWorkspaceRuntimeOwner | null {
|
||||
if (!worktreeId || file.worktreeId !== worktreeId || hasOpenFileExecutionHostEvidence(file)) {
|
||||
|
||||
@@ -20,6 +20,7 @@ const target = {
|
||||
}
|
||||
const state = {
|
||||
settings: {},
|
||||
repos: [],
|
||||
worktreesByRepo: { repo: [{ id: 'wt', path: '/repo' }] },
|
||||
openFile: vi.fn(),
|
||||
openMarkdownPreview: vi.fn()
|
||||
@@ -30,7 +31,7 @@ vi.mock('@/store', () => ({
|
||||
getState: () => state
|
||||
})
|
||||
}))
|
||||
vi.mock('@/lib/connection-context', () => ({ getConnectionId: () => runtimeConnectionId }))
|
||||
vi.mock('@/lib/connection-context', () => ({ getConnectionIdFromState: () => runtimeConnectionId }))
|
||||
vi.mock('@/runtime/runtime-file-client', () => ({ statRuntimePath: runtime.stat }))
|
||||
vi.mock('@/runtime/runtime-rpc-client', () => ({
|
||||
settingsForRuntimeOwner: (_settings: unknown, owner: string | null | undefined) => ({ owner })
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { act, cleanup, renderHook, waitFor } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, expect, it, vi } from 'vitest'
|
||||
import { useAppStore } from '@/store'
|
||||
import type { OpenFile } from '@/store/slices/editor'
|
||||
import { makeWorktree } from '../../store/slices/worktrees-slice-test-fixtures'
|
||||
import { useMarkdownDocuments } from './useMarkdownDocuments'
|
||||
|
||||
const mocks = vi.hoisted(() => ({ list: vi.fn(), toastError: vi.fn() }))
|
||||
vi.mock('./markdown-document-list-request', () => ({
|
||||
requestSharedMarkdownDocumentList: mocks.list
|
||||
}))
|
||||
vi.mock('sonner', () => ({ toast: { error: mocks.toastError } }))
|
||||
|
||||
const initialState = useAppStore.getInitialState()
|
||||
const worktreeId = 'repo-1::/remote/repo'
|
||||
const file: OpenFile = {
|
||||
id: 'restored',
|
||||
filePath: '/remote/repo/README.md',
|
||||
relativePath: 'README.md',
|
||||
worktreeId,
|
||||
language: 'markdown',
|
||||
isDirty: false,
|
||||
runtimeEnvironmentId: null,
|
||||
mode: 'edit'
|
||||
}
|
||||
const save = vi.fn(async () => true)
|
||||
const document = {
|
||||
filePath: file.filePath,
|
||||
relativePath: 'README.md',
|
||||
basename: 'README.md',
|
||||
name: 'README'
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
useAppStore.setState(initialState, true)
|
||||
mocks.list.mockReset().mockResolvedValue([document])
|
||||
mocks.toastError.mockClear()
|
||||
useAppStore.setState({
|
||||
worktreesByRepo: {
|
||||
'repo-1': [makeWorktree({ id: worktreeId, repoId: 'repo-1', path: '/remote/repo' })]
|
||||
}
|
||||
})
|
||||
})
|
||||
afterEach(() => {
|
||||
cleanup()
|
||||
useAppStore.setState(initialState, true)
|
||||
})
|
||||
|
||||
function render(file: OpenFile) {
|
||||
return renderHook(({ file }) => useMarkdownDocuments(file, true, 'rich', save), {
|
||||
initialProps: { file }
|
||||
})
|
||||
}
|
||||
|
||||
it('waits for the retained editor to take its managed workspace owner before listing', async () => {
|
||||
useAppStore.setState({
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: '/remote/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: '#737373',
|
||||
addedAt: 1,
|
||||
executionHostId: 'runtime:env-1'
|
||||
}
|
||||
],
|
||||
worktreesByRepo: {
|
||||
'repo-1': [
|
||||
makeWorktree({
|
||||
id: worktreeId,
|
||||
repoId: 'repo-1',
|
||||
path: '/remote/repo',
|
||||
hostId: 'runtime:env-1',
|
||||
runtimeOwnerEnvironmentId: 'env-1'
|
||||
})
|
||||
]
|
||||
}
|
||||
})
|
||||
const { result, rerender } = render(file)
|
||||
expect(mocks.list).not.toHaveBeenCalled()
|
||||
expect(mocks.toastError).not.toHaveBeenCalled()
|
||||
rerender({ file: { ...file, id: 'owned', runtimeEnvironmentId: 'env-1' } })
|
||||
await waitFor(() => expect(result.current.markdownDocuments).toEqual([document]))
|
||||
expect(mocks.list).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.list).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ settings: { activeRuntimeEnvironmentId: 'env-1' }, worktreeId }),
|
||||
'/remote/repo',
|
||||
{ requireFresh: false }
|
||||
)
|
||||
})
|
||||
|
||||
it('waits for missing ownership and resumes when the SSH catalog lands without a path change', async () => {
|
||||
const { result } = render(file)
|
||||
expect(mocks.list).not.toHaveBeenCalled()
|
||||
act(() =>
|
||||
useAppStore.setState({
|
||||
repos: [
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: '/remote/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: '#737373',
|
||||
addedAt: 1,
|
||||
connectionId: 'ssh-1'
|
||||
}
|
||||
]
|
||||
})
|
||||
)
|
||||
await waitFor(() => expect(result.current.markdownDocuments).toEqual([document]))
|
||||
expect(mocks.list).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.list).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ connectionId: 'ssh-1' }),
|
||||
'/remote/repo',
|
||||
{ requireFresh: false }
|
||||
)
|
||||
})
|
||||
|
||||
it('uses an explicit server owner even before its desktop repository catalog is present', async () => {
|
||||
const { result } = render({ ...file, runtimeEnvironmentId: 'env-1' })
|
||||
await waitFor(() => expect(result.current.markdownDocuments).toEqual([document]))
|
||||
expect(mocks.list).toHaveBeenCalledTimes(1)
|
||||
expect(mocks.list).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ settings: { activeRuntimeEnvironmentId: 'env-1' } }),
|
||||
'/remote/repo',
|
||||
{ requireFresh: false }
|
||||
)
|
||||
})
|
||||
|
||||
it('waits for ownership migration in a non-git folder workspace too', async () => {
|
||||
useAppStore.setState({
|
||||
worktreesByRepo: {},
|
||||
projectGroups: [
|
||||
{
|
||||
id: 'group-1',
|
||||
name: 'Notes',
|
||||
parentPath: '/notes',
|
||||
connectionId: null,
|
||||
executionHostId: 'runtime:env-1',
|
||||
parentGroupId: null,
|
||||
createdFrom: 'manual',
|
||||
tabOrder: 0,
|
||||
isCollapsed: false,
|
||||
color: null,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
],
|
||||
folderWorkspaces: [
|
||||
{
|
||||
id: 'notes',
|
||||
projectGroupId: 'group-1',
|
||||
name: 'Notes',
|
||||
folderPath: '/notes',
|
||||
connectionId: null,
|
||||
linkedTask: null,
|
||||
comment: '',
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 1,
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
]
|
||||
})
|
||||
const folderFile = { ...file, worktreeId: 'folder:notes', filePath: '/notes/README.md' }
|
||||
const { result, rerender } = render(folderFile)
|
||||
expect(mocks.list).not.toHaveBeenCalled()
|
||||
rerender({ file: { ...folderFile, id: 'folder-owned', runtimeEnvironmentId: 'env-1' } })
|
||||
await waitFor(() => expect(result.current.markdownDocuments).toEqual([document]))
|
||||
expect(mocks.list).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
settings: { activeRuntimeEnvironmentId: 'env-1' },
|
||||
worktreeId: 'folder:notes'
|
||||
}),
|
||||
'/notes',
|
||||
{ requireFresh: false }
|
||||
)
|
||||
})
|
||||
@@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { MarkdownDocument } from '../../../../shared/filesystem-entry-types'
|
||||
import { useAppStore } from '@/store'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
import { getConnectionId } from '@/lib/connection-context'
|
||||
import { getConnectionIdFromState } from '@/lib/connection-context'
|
||||
import { statRuntimePath } from '@/runtime/runtime-file-client'
|
||||
import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client'
|
||||
import type { MarkdownViewMode, OpenFile } from '@/store/slices/editor'
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from './markdown-doc-links'
|
||||
import { selectMarkdownDocumentWorktreePath } from './markdown-document-worktree-path-selector'
|
||||
import { requestSharedMarkdownDocumentList } from './markdown-document-list-request'
|
||||
import { findRestoredEditorWorkspaceRuntimeOwner } from './restored-editor-workspace-runtime-owner'
|
||||
|
||||
type OpenMarkdownDocumentOptions = {
|
||||
anchor?: string | null
|
||||
@@ -61,7 +62,7 @@ export function useMarkdownDocuments(
|
||||
const worktreePath = useAppStore((s) => selectMarkdownDocumentWorktreePath(s, worktreeId))
|
||||
const openFile = useAppStore((s) => s.openFile)
|
||||
const openMarkdownPreview = useAppStore((s) => s.openMarkdownPreview)
|
||||
const connectionId = getConnectionId(worktreeId)
|
||||
const connectionId = useAppStore((state) => getConnectionIdFromState(state, worktreeId))
|
||||
const scopeKey = JSON.stringify([
|
||||
activeFile.runtimeEnvironmentId,
|
||||
connectionId,
|
||||
@@ -78,16 +79,32 @@ export function useMarkdownDocuments(
|
||||
if (!worktreeId || !worktreePath) {
|
||||
return
|
||||
}
|
||||
const state = useAppStore.getState()
|
||||
const settings = settingsForRuntimeOwner(state.settings, activeFile.runtimeEnvironmentId)
|
||||
// The content loader reowns retained tabs before metadata may use their new host.
|
||||
if (
|
||||
findRestoredEditorWorkspaceRuntimeOwner(
|
||||
state,
|
||||
{
|
||||
worktreeId,
|
||||
filePath: activeFile.filePath,
|
||||
externalSshTargetId: activeFile.externalSshTargetId,
|
||||
operationProvenance: activeFile.operationProvenance,
|
||||
runtimeEnvironmentId: activeFile.runtimeEnvironmentId
|
||||
},
|
||||
worktreeId
|
||||
) ||
|
||||
(connectionId === undefined && !settings?.activeRuntimeEnvironmentId?.trim())
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const requestId = requestRef.current + 1
|
||||
requestRef.current = requestId
|
||||
try {
|
||||
const documents = await requestSharedMarkdownDocumentList(
|
||||
{
|
||||
settings: settingsForRuntimeOwner(
|
||||
useAppStore.getState().settings,
|
||||
activeFile.runtimeEnvironmentId
|
||||
),
|
||||
settings,
|
||||
worktreeId,
|
||||
worktreePath,
|
||||
connectionId: connectionId ?? undefined
|
||||
@@ -116,7 +133,16 @@ export function useMarkdownDocuments(
|
||||
}
|
||||
}
|
||||
},
|
||||
[activeFile.runtimeEnvironmentId, connectionId, worktreeId, worktreePath, scopeKey]
|
||||
[
|
||||
activeFile.filePath,
|
||||
activeFile.externalSshTargetId,
|
||||
activeFile.operationProvenance,
|
||||
activeFile.runtimeEnvironmentId,
|
||||
connectionId,
|
||||
worktreeId,
|
||||
worktreePath,
|
||||
scopeKey
|
||||
]
|
||||
)
|
||||
|
||||
const openMarkdownDocument = useCallback(
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import type { ElectronApplication } from '@stablyai/playwright-test'
|
||||
import { expect, test } from './helpers/orca-app'
|
||||
import { createRestartSession } from './helpers/orca-restart'
|
||||
import { startOrcadConvertHost } from './helpers/orcad-convert-host'
|
||||
import { seedRelayEraProfile } from './helpers/orcad-upgrade-profile'
|
||||
import { convertAndRetain, serverCall } from './helpers/orcad-convert-flow'
|
||||
import { waitForSessionReady } from './helpers/store'
|
||||
import { dismissTransientAnnouncement } from './helpers/ssh-config-host-picker'
|
||||
import { toRuntimeExecutionHostId } from '../../src/shared/execution-host'
|
||||
|
||||
const TEMPLATE = process.env.ORCA_E2E_ORCAD_CONVERT_TEMPLATE
|
||||
test.skip(!TEMPLATE || process.env.ORCA_E2E_SSH_DOCKER !== '1', 'Needs Docker and server template')
|
||||
|
||||
test('retained Markdown lists documents on its managed owner after conversion', async (// oxlint-disable-next-line no-empty-pattern -- This exercise owns its app launch.
|
||||
{}, testInfo) => {
|
||||
test.setTimeout(4 * 60_000)
|
||||
const host = startOrcadConvertHost('docker', testInfo)
|
||||
const session = createRestartSession(testInfo, { ORCA_ORCAD_TEMPLATE_PATH: TEMPLATE! })
|
||||
let app: ElectronApplication | null = null
|
||||
try {
|
||||
const first = await session.launch()
|
||||
app = first.app
|
||||
await waitForSessionReady(first.page)
|
||||
await session.close(app)
|
||||
app = null
|
||||
const marker = `REMOTE_MARKDOWN_${Date.now()}`
|
||||
host.exec!(
|
||||
`printf '# ${marker}\n\nHost-owned Markdown.\n\n[[NOTES]]\n' > '${host.remoteRepoPath}/README.md'`
|
||||
)
|
||||
host.exec!(`printf '# SIBLING_${marker}\n' > '${host.remoteRepoPath}/NOTES.md'`)
|
||||
const seeded = seedRelayEraProfile(session.userDataDir, host.input, {
|
||||
repoPath: host.remoteRepoPath,
|
||||
folderPath: host.remoteFolderPath
|
||||
})
|
||||
const launched = await session.launch()
|
||||
app = launched.app
|
||||
const page = launched.page
|
||||
const errors: string[] = []
|
||||
let failureScreenshot: Promise<void> | undefined
|
||||
page.on('console', (message) => {
|
||||
if (message.type() === 'error') {
|
||||
errors.push(message.text())
|
||||
console.log('[renderer-error]', message.text())
|
||||
if (message.text().includes('Failed to list markdown documents') && !failureScreenshot) {
|
||||
failureScreenshot = (async () => {
|
||||
await page.screenshot({ path: testInfo.outputPath('markdown-list-failure.png') })
|
||||
})()
|
||||
}
|
||||
}
|
||||
})
|
||||
await waitForSessionReady(page)
|
||||
await convertAndRetain(page, session.userDataDir, seeded)
|
||||
const environment = (await page.evaluate(() => window.api.runtimeEnvironments.list())).find(
|
||||
(e) => e.orcadDeployment?.sshTargetId === seeded.targetId
|
||||
)
|
||||
if (!environment) {
|
||||
throw new Error('Missing managed environment')
|
||||
}
|
||||
await page.evaluate(
|
||||
({ id, hostId }) => window.__store?.getState().setActiveWorktree(id, hostId),
|
||||
{
|
||||
id: seeded.worktreeId,
|
||||
hostId: toRuntimeExecutionHostId(environment.id)
|
||||
}
|
||||
)
|
||||
await dismissTransientAnnouncement(page)
|
||||
await expect(page.locator('.rich-markdown-editor')).toContainText(marker, { timeout: 30_000 })
|
||||
console.log(
|
||||
'[markdown-conversion]',
|
||||
JSON.stringify({
|
||||
errors,
|
||||
files: await page.evaluate(() => window.__store?.getState().openFiles)
|
||||
})
|
||||
)
|
||||
await page.screenshot({ path: testInfo.outputPath('markdown-converted.png') })
|
||||
await failureScreenshot
|
||||
expect(errors.filter((e) => e.includes('Failed to list markdown documents'))).toEqual([])
|
||||
expect(
|
||||
await serverCall(page, environment.id, 'files.listMarkdownDocuments', {
|
||||
worktree: `id:${seeded.worktreeId}`
|
||||
})
|
||||
).toContain('NOTES.md')
|
||||
const link = page.locator('.rich-markdown-editor [data-doc-link-target="NOTES"]')
|
||||
await expect(link).toBeVisible()
|
||||
console.log('[rich-link-state]', await link.getAttribute('class'))
|
||||
await page.evaluate(
|
||||
({ filePath, worktreeId, environmentId }) => {
|
||||
window.__store?.getState().openMarkdownPreview({
|
||||
filePath,
|
||||
relativePath: 'README.md',
|
||||
worktreeId,
|
||||
language: 'markdown',
|
||||
runtimeEnvironmentId: environmentId
|
||||
})
|
||||
},
|
||||
{
|
||||
filePath: seeded.sessionFilePath,
|
||||
worktreeId: seeded.worktreeId,
|
||||
environmentId: environment.id
|
||||
}
|
||||
)
|
||||
const previewLink = page
|
||||
.locator('.markdown-preview')
|
||||
.getByRole('link', { name: 'NOTES', exact: true })
|
||||
await expect(previewLink).toBeVisible()
|
||||
await expect(previewLink).not.toHaveClass(/markdown-doc-link-broken/)
|
||||
await previewLink.click()
|
||||
await expect(
|
||||
page.getByRole('heading', { name: `SIBLING_${marker}`, exact: true })
|
||||
).toBeVisible()
|
||||
expect(errors.filter((e) => e.includes('Failed to list markdown documents'))).toEqual([])
|
||||
await page.screenshot({ path: testInfo.outputPath('markdown-converted-ready.png') })
|
||||
} finally {
|
||||
if (app) {
|
||||
await session.close(app)
|
||||
}
|
||||
await session.dispose()
|
||||
host.cleanup()
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user