Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 2 additions & 9 deletions config/scripts/ci-unit-files.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import { globSync } from 'node:fs'
import { defaultExclude } from 'vitest/config'
import { UNIT_INCLUDE } from './unit-test-file-patterns.mjs'
export { UNIT_INCLUDE } from './unit-test-file-patterns.mjs'

// Why one constant: the cross-version-wire job runs this directory, so a new file there runs with no list to update.
export const CROSS_VERSION_WIRE_DIR = 'tests/e2e/cross-version-wire/'

export const UNIT_INCLUDE = [
'src/**/*.test.ts',
'src/**/*.test.tsx',
'config/scripts/**/*.test.ts',
'config/scripts/**/*.test.mjs',
'tests/tools/**/*.test.mjs',
'tests/e2e/**/*.unit.test.ts'
]

export const UNIT_EXCLUDE = [
...defaultExclude,
'src/main/daemon/repro-13767-shell-ready-marker-lost-to-exec.test.ts',
Expand Down
13 changes: 3 additions & 10 deletions config/scripts/node-server-change-scope.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { appendFileSync, globSync, readFileSync } from 'node:fs'
import { appendFileSync, readFileSync } from 'node:fs'
import { resolve } from 'node:path'
import { pathToFileURL } from 'node:url'
import { nodeServerTestPaths } from './node-server-test-paths.mjs'
import { nodeServerTestFiles, nodeServerTestPaths } from './node-server-test-paths.mjs'
import { nodeServerQualification } from './node-server-qualification.mjs'

const ROOT = resolve(import.meta.dirname, '../..')
Expand Down Expand Up @@ -54,14 +54,7 @@ const ALWAYS_PREFIXES = [
]

export function discoverNodeServerTests(root = ROOT) {
const selectors = nodeServerTestPaths({ artifact: true, crossRuntime: true })
return globSync(
['src/**/*.test.{ts,tsx}', 'config/scripts/**/*.test.{ts,mjs}', 'tests/e2e/**/*.unit.test.ts'],
{ cwd: root }
)
.map((file) => file.replaceAll('\\', '/'))
.filter((file) => selectors.some((selector) => file.includes(selector)))
.sort()
return nodeServerTestFiles({ root, artifact: true, crossRuntime: true })
}

export async function collectNodeServerInputs({ root = ROOT, entryPoints } = {}) {
Expand Down
23 changes: 17 additions & 6 deletions config/scripts/node-server-change-scope.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,15 @@ it.each([
])('fails closed or requests dependencies in an uninstalled checkout: %j', (scenario) => {
const root = moduleTree(
Object.fromEntries(
['node-server-change-scope', 'node-server-test-paths', 'node-server-qualification'].map(
(name) => [
`config/scripts/${name}.mjs`,
readFileSync(new URL(`./${name}.mjs`, import.meta.url), 'utf8')
]
)
[
'node-server-change-scope',
'node-server-test-paths',
'node-server-qualification',
'unit-test-file-patterns'
].map((name) => [
`config/scripts/${name}.mjs`,
readFileSync(new URL(`./${name}.mjs`, import.meta.url), 'utf8')
])
)
)
const changes = join(root, 'changes')
Expand Down Expand Up @@ -215,6 +218,14 @@ describe('the actual Bun build and profile-test dependency graph', () => {
expect((await classifyNodeServerChanges([file], async () => inputs)).shouldRun).toBe(true)
})

it('keeps desktop Electron probes outside the headless server selection', () => {
const tests = discoverNodeServerTests()
expect(tests).toContain('src/main/persistence/profile-state/profile-state-access.test.ts')
expect(tests).not.toContain(
'src/main/persistence/profile-state/profile-state-writer-stall.electron.test.ts'
)
})

it('retains all selected tests and the selectors the Bun runner uses', () => {
const tests = discoverNodeServerTests()
expect(tests.length).toBeGreaterThan(80)
Expand Down
21 changes: 21 additions & 0 deletions config/scripts/node-server-test-paths.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import { globSync } from 'node:fs'
import { resolve } from 'node:path'
import { UNIT_INCLUDE } from './unit-test-file-patterns.mjs'

/** Mirrors ORCA_REQUIRED_TEST_INPUTS_ENV in src/main/orcad/orcad-node-slot-fixture.ts. */
export const REQUIRED_TEST_INPUTS_ENV = 'ORCA_REQUIRED_TEST_INPUTS'

Expand Down Expand Up @@ -40,3 +44,20 @@ export function nodeServerTestPaths({ artifact = false, crossRuntime = false } =
...(crossRuntime ? CROSS_RUNTIME_TEST_PATHS : [])
]
}

export function nodeServerTestFiles({
root = resolve(import.meta.dirname, '../..'),
artifact = false,
crossRuntime = false
} = {}) {
const selectors = nodeServerTestPaths({ artifact, crossRuntime })
return globSync(UNIT_INCLUDE, { cwd: root })
.map((file) => file.replaceAll('\\', '/'))
.filter(
(file) =>
!file.endsWith('.electron.test.ts') &&
(crossRuntime || !CROSS_RUNTIME_TEST_PATHS.includes(file)) &&
selectors.some((selector) => file.includes(selector))
)
.sort()
}
20 changes: 3 additions & 17 deletions config/scripts/run-node-server-tests.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Runs the headless-server suites under the pinned Node (design D4/D4a), not the host's.
import { join, resolve } from 'node:path'
import { randomUUID } from 'node:crypto'
import { globSync, readFileSync } from 'node:fs'
import { readFileSync } from 'node:fs'
import { ORCAD_VERSION_FILENAME } from '../../src/shared/orcad-artifacts.ts'
import { NODE_RUNTIME_PIN } from '../../src/shared/node-runtime-pin.ts'
import {
Expand All @@ -11,13 +11,8 @@ import {
import { packagedNodeRuntimePath } from './build-orcad-node.mjs'
import { ensurePinnedNodeExecutable } from './pinned-node-downloads.mjs'
import { currentTarget } from './server-build-target.mjs'
import { UNIT_INCLUDE } from './ci-unit-files.mjs'
import { describeProcessFailure, runProcessSync } from './script-child-process.mjs'
import {
CROSS_RUNTIME_TEST_PATHS,
nodeServerTestPaths,
REQUIRED_TEST_INPUTS_ENV
} from './node-server-test-paths.mjs'
import { nodeServerTestFiles, REQUIRED_TEST_INPUTS_ENV } from './node-server-test-paths.mjs'

const root = resolve(import.meta.dirname, '../..')
const target = currentTarget()
Expand Down Expand Up @@ -85,14 +80,5 @@ run(runtimePath, [
])

function defaultTestArgs() {
// Why: vitest 5 drops CLI --exclude for inline projects, so the selectors' substring matches
// are resolved to files here and filtered before vitest sees them.
// Electron probes run in desktop jobs; headless compatibility containers have no display.
const selectors = nodeServerTestPaths({ artifact, crossRuntime })
const skipped = new Set(crossRuntime ? [] : CROSS_RUNTIME_TEST_PATHS)
return globSync(UNIT_INCLUDE, { cwd: root })
.map((path) => path.replaceAll('\\', '/'))
.filter((path) => selectors.some((selector) => path.includes(selector)))
.filter((path) => !path.endsWith('.electron.test.ts') && !skipped.has(path))
.sort()
return nodeServerTestFiles({ root, artifact, crossRuntime })
}
8 changes: 8 additions & 0 deletions config/scripts/unit-test-file-patterns.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export const UNIT_INCLUDE = [
'src/**/*.test.ts',
'src/**/*.test.tsx',
'config/scripts/**/*.test.ts',
'config/scripts/**/*.test.mjs',
'tests/tools/**/*.test.mjs',
'tests/e2e/**/*.unit.test.ts'
]
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export const WORKSPACE_SESSION_FIELD_DISPOSITION = {
onTransfer: 'copiedByBespokeRule'
},
openFilesByWorktree: { onRepoRemoval: 'prunedByOwnerKey', onTransfer: 'copiedByBespokeRule' },
// Recovery copies stay in the source profile even if its workspace is removed or transferred.
recoveredEditorDraftsByWorktree: { onRepoRemoval: 'notRepoScoped', onTransfer: 'notTransferred' },
activeFileIdByWorktree: { onRepoRemoval: 'prunedByOwnerKey', onTransfer: 'copiedByOwnerKey' },
markdownFrontmatterVisible: {
onRepoRemoval: 'prunedByBespokeRule',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const WORKSPACE_SESSION_WORKTREE_REFERENCE_KIND = {
localOnlyScrollbackByTabId: 'none',
activeWorktreeIdsOnShutdown: 'worktree-id-array',
openFilesByWorktree: 'owner-keyed-row-arrays',
recoveredEditorDraftsByWorktree: 'none',
activeFileIdByWorktree: 'owner-keyed',
markdownFrontmatterVisible: 'none',
browserTabsByWorktree: 'owner-keyed-browser-row-arrays',
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,10 @@
"f2e00db373": "File not found: {{value0}}",
"checkRunDetailsUnavailable": "No details are available for this check.",
"checkRunDetailsLoadFailed": "Failed to load check details.",
"checkRunDetailsRepoUnavailable": "Repository details are unavailable for this check."
"checkRunDetailsRepoUnavailable": "Repository details are unavailable for this check.",
"parkedDraftBlockedByUnsavedChanges": "{{path}} is open with unsaved changes. Save or close it, then reopen to recover the parked draft.",
"parkedDraftBlockedByOtherOwner": "{{path}} is open on another host. Close that tab, then reopen to recover the parked draft.",
"parkedDraftBlockedByReadOnlyTab": "{{path}} is open read-only. Close it, then reopen to recover the parked draft."
},
"github": {
"f129c42773": "GitHub did not return the new comment.",
Expand Down
7 changes: 7 additions & 0 deletions src/renderer/src/lib/workspace-session-patch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { buildPersistedUnifiedTabSessionData } from './workspace-session-unified
import { buildLastVisitedAtByWorktreeId } from './workspace-session-focus-recency'
import { buildSleepingAgentSessionData } from './workspace-session-sleeping-agents'

import { buildPersistedRecoveredEditorDrafts } from './workspace-session-recovered-editor-drafts'

type SessionRelevantField = keyof WorkspaceSessionSnapshot

function hasAnyChangedField(
Expand Down Expand Up @@ -151,6 +153,11 @@ export function buildWorkspaceSessionPatch(
)
)
}
if (changed.has('recentlyClosedEditorTabsByWorktree')) {
patch.recoveredEditorDraftsByWorktree = buildPersistedRecoveredEditorDrafts(
snapshot.recentlyClosedEditorTabsByWorktree
)
}
// Why: withoutStagedBrowserTabs hides rows based on the handle map, so clearing a staged flag
// changes which browser and tab rows persist even when the rows themselves are untouched.
const stagedVisibilityChanged = changed.has('remoteBrowserPageHandlesByPageId')
Expand Down
28 changes: 28 additions & 0 deletions src/renderer/src/lib/workspace-session-recovered-editor-drafts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { getPersistedEditorOwnerFields } from './editor-file-operation-owner'
import type { PersistedOpenFile } from '../../../shared/workspace-session-state-types'
import type { ClosedEditorTabSnapshot } from '../store/slices/editor/types/open-file'

export function buildPersistedRecoveredEditorDrafts(
snapshotsByWorktree: Record<string, ClosedEditorTabSnapshot[]> | undefined
): Record<string, PersistedOpenFile[]> {
const archive: Record<string, PersistedOpenFile[]> = {}
for (const [worktreeId, snapshots] of Object.entries(snapshotsByWorktree ?? {})) {
const drafts = snapshots.filter(
(snapshot) => snapshot.dirtyDraftContent !== undefined && snapshot.readOnly !== true
)
if (drafts.length === 0) {
continue
}
archive[worktreeId] = drafts.map((snapshot) => ({
filePath: snapshot.filePath,
relativePath: snapshot.relativePath,
worktreeId,
language: snapshot.language,
isPreview: false,
...getPersistedEditorOwnerFields(snapshot),
dirtyDraftContent: snapshot.dirtyDraftContent,
lastKnownDiskSignature: snapshot.lastKnownDiskSignature
}))
}
return archive
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ describe('SESSION_RELEVANT_FIELDS', () => {
localOnlyScrollbackByTabId: true,
activeTabIdByWorktree: true,
openFiles: true,
recentlyClosedEditorTabsByWorktree: true,
editorDrafts: true,
markdownFrontmatterVisible: true,
activeFileIdByWorktree: true,
Expand Down
6 changes: 6 additions & 0 deletions src/renderer/src/lib/workspace-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { buildSleepingAgentSessionData } from './workspace-session-sleeping-agen
import { buildActiveConnectionIdsAtShutdown } from './workspace-session-reconnect-targets'
import { withoutStagedBrowserTabs } from './workspace-session-staged-browser-tabs'
import { buildBrowserSessionData } from './workspace-session-browser-tabs'
import { buildPersistedRecoveredEditorDrafts } from './workspace-session-recovered-editor-drafts'

export { buildActiveConnectionIdsAtShutdown }

Expand Down Expand Up @@ -55,6 +56,7 @@ export type WorkspaceSessionSnapshot = Pick<
| 'lastVisitedAtByWorktreeId'
| 'defaultTerminalTabsAppliedByWorktreeId'
> & {
recentlyClosedEditorTabsByWorktree?: AppState['recentlyClosedEditorTabsByWorktree']
activeWorkspaceExecutionHostId?: AppState['activeWorkspaceExecutionHostId']
sleepingAgentSessionsByPaneKey?: AppState['sleepingAgentSessionsByPaneKey']
clientHostedBrowserCloseIntentsByEnvironment?: AppState['clientHostedBrowserCloseIntentsByEnvironment']
Expand All @@ -76,6 +78,7 @@ export const SESSION_RELEVANT_FIELDS = [
'localOnlyScrollbackByTabId',
'activeTabIdByWorktree',
'openFiles',
'recentlyClosedEditorTabsByWorktree',
'editorDrafts',
'markdownFrontmatterVisible',
'activeFileIdByWorktree',
Expand Down Expand Up @@ -284,6 +287,9 @@ export function buildWorkspaceSessionPayload(
// Why: session:set fully replaces the persisted object, so dropping this silently disables eager terminal reconnect on restart.
activeWorktreeIdsOnShutdown: terminalSessionData.activeWorktreeIdsOnShutdown,
activeTabIdByWorktree: snapshot.activeTabIdByWorktree,
recoveredEditorDraftsByWorktree: buildPersistedRecoveredEditorDrafts(
snapshot.recentlyClosedEditorTabsByWorktree
),
...buildEditorSessionData(
snapshot.openFiles,
snapshot.editorDrafts,
Expand Down
Loading
Loading