fix: Project action terminal lifecycle (#3287)
* fix(terminal): make command sessions own action lifecycle * fix(ui): reconcile project action terminal state * feat(ui): show running project actions in terminal tabs * feat(ui): run project actions from linked worktrees * fix(ui): guard project action reconciliation * fix(ui): scope project action preview fallback * fix(ui): default project actions to worktrees * fix(ui): reveal project action terminals * fix(ui): retain terminal output after snapshot replay * fix(ui): restore running action terminals on revisit
This commit is contained in:
@@ -1,5 +1,8 @@
|
||||
import { afterEach, describe, expect, test } from 'bun:test';
|
||||
import { useTerminalStore } from './useTerminalStore';
|
||||
|
||||
import type { TerminalServerSession } from '@/lib/api/types';
|
||||
|
||||
import { directoryMayHaveActiveProjectAction, useTerminalStore } from './useTerminalStore';
|
||||
|
||||
const setup = () => {
|
||||
useTerminalStore.getState().clearAll();
|
||||
@@ -9,14 +12,35 @@ const setup = () => {
|
||||
|
||||
const buffer = (tabId: string) => useTerminalStore.getState().getBuffer('/repo', tabId);
|
||||
|
||||
const staleBuildSession: TerminalServerSession = {
|
||||
sessionId: 'srv-old',
|
||||
cwd: '/repo',
|
||||
status: 'running',
|
||||
createdAt: 1,
|
||||
mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-old' },
|
||||
};
|
||||
|
||||
const captureStartedActionMutationRevisions = (directory: string) => {
|
||||
return useTerminalStore.getState().captureStartedActionMutationRevisions(directory);
|
||||
};
|
||||
|
||||
const reconcileServerSessionsWithStartedRevisions = (
|
||||
directory: string,
|
||||
serverSessions: TerminalServerSession[],
|
||||
startedActionMutationRevisions: ReadonlyMap<string, number>,
|
||||
) => {
|
||||
useTerminalStore.getState().reconcileServerSessions(directory, serverSessions, { startedActionMutationRevisions });
|
||||
};
|
||||
|
||||
describe('terminal state reconciliation', () => {
|
||||
afterEach(() => useTerminalStore.getState().clearAll());
|
||||
|
||||
test('adopts unknown server sessions into the fresh placeholder tab', () => {
|
||||
test('reconciles unknown server sessions into the fresh placeholder tab', () => {
|
||||
setup();
|
||||
useTerminalStore.getState().adoptServerSessions('/repo', [
|
||||
{ sessionId: 'srv-1', status: 'running', createdAt: 100 },
|
||||
{ sessionId: 'srv-2', status: 'exited', createdAt: null },
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [
|
||||
{ sessionId: 'srv-1', cwd: '/repo', status: 'running', createdAt: 100 },
|
||||
{ sessionId: 'srv-2', cwd: '/repo', status: 'exited', createdAt: null },
|
||||
]);
|
||||
const state = useTerminalStore.getState().getDirectoryState('/repo')!;
|
||||
expect(state.tabs.map((tab) => tab.id)).toEqual(['srv-1', 'srv-2']);
|
||||
@@ -26,13 +50,13 @@ describe('terminal state reconciliation', () => {
|
||||
expect(state.activeTabId).toBe('srv-1');
|
||||
});
|
||||
|
||||
test('adoption is additive: existing tabs and referenced sessions survive', () => {
|
||||
test('reconciliation keeps existing interactive tabs and adopts unknown sessions', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().appendToBuffer('/repo', tabId, 'output', 1);
|
||||
useTerminalStore.getState().setTabSessionId('/repo', tabId, 'srv-live');
|
||||
useTerminalStore.getState().adoptServerSessions('/repo', [
|
||||
{ sessionId: 'srv-live', status: 'running', createdAt: 1 },
|
||||
{ sessionId: 'srv-orphan', status: 'running', createdAt: 2 },
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [
|
||||
{ sessionId: 'srv-live', cwd: '/repo', status: 'running', createdAt: 1 },
|
||||
{ sessionId: 'srv-orphan', cwd: '/repo', status: 'running', createdAt: 2 },
|
||||
]);
|
||||
const state = useTerminalStore.getState().getDirectoryState('/repo')!;
|
||||
expect(state.tabs).toHaveLength(2);
|
||||
@@ -41,18 +65,327 @@ describe('terminal state reconciliation', () => {
|
||||
expect(state.activeTabId).toBe(tabId);
|
||||
});
|
||||
|
||||
test('re-adopting the same sessions changes nothing', () => {
|
||||
test('re-reconciling the same sessions changes nothing', () => {
|
||||
setup();
|
||||
useTerminalStore.getState().adoptServerSessions('/repo', [
|
||||
{ sessionId: 'srv-1', status: 'running', createdAt: 100 },
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [
|
||||
{ sessionId: 'srv-1', cwd: '/repo', status: 'running', createdAt: 100 },
|
||||
]);
|
||||
const before = useTerminalStore.getState().sessions;
|
||||
useTerminalStore.getState().adoptServerSessions('/repo', [
|
||||
{ sessionId: 'srv-1', status: 'running', createdAt: 100 },
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [
|
||||
{ sessionId: 'srv-1', cwd: '/repo', status: 'running', createdAt: 100 },
|
||||
]);
|
||||
expect(useTerminalStore.getState().sessions).toBe(before);
|
||||
});
|
||||
|
||||
test('successful empty reconciliation with no local directory keeps the original sessions reference', () => {
|
||||
const before = useTerminalStore.getState().sessions;
|
||||
useTerminalStore.getState().reconcileServerSessions('/missing', []);
|
||||
expect(useTerminalStore.getState().sessions).toBe(before);
|
||||
});
|
||||
|
||||
test('hydrates persisted action tabs with live fields reset until server authority returns', () => {
|
||||
const mergePersistedState = useTerminalStore.persist.getOptions().merge;
|
||||
if (!mergePersistedState) {
|
||||
throw new Error('expected persisted merge helper');
|
||||
}
|
||||
const hydrated = mergePersistedState({
|
||||
sessions: [[
|
||||
'/repo',
|
||||
{
|
||||
activeTabId: 'tab-a',
|
||||
tabs: [{ id: 'tab-a', label: 'Build', iconKey: 'build', createdAt: 10, purpose: { type: 'project-action', actionId: 'build' } }],
|
||||
},
|
||||
]],
|
||||
nextTabId: 2,
|
||||
}, useTerminalStore.getState());
|
||||
const tab = hydrated.sessions.get('/repo')?.tabs[0];
|
||||
expect(tab?.purpose).toEqual({ type: 'project-action', actionId: 'build', executionId: null });
|
||||
expect(tab?.lifecycle).toBe('idle');
|
||||
expect(tab?.terminalSessionId).toBeNull();
|
||||
});
|
||||
|
||||
test('adopts an existing running project-action session by action id across clients', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().setTabPurpose('/repo', tabId, { type: 'project-action', actionId: 'build', executionId: null });
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [{
|
||||
sessionId: 'srv-shared',
|
||||
cwd: '/repo',
|
||||
status: 'running',
|
||||
createdAt: 1,
|
||||
mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-2' },
|
||||
}]);
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0]!;
|
||||
expect(tab.id).toBe(tabId);
|
||||
expect(tab.terminalSessionId).toBe('srv-shared');
|
||||
expect(tab.lifecycle).toBe('running');
|
||||
expect(tab.purpose).toEqual({ type: 'project-action', actionId: 'build', executionId: 'exec-2' });
|
||||
});
|
||||
|
||||
test('activates a rebound running project-action tab when the current active tab is idle and sessionless', () => {
|
||||
const actionTabId = setup();
|
||||
const shellTabId = useTerminalStore.getState().createTab('/repo');
|
||||
useTerminalStore.getState().setTabPurpose('/repo', actionTabId, { type: 'project-action', actionId: 'build', executionId: null });
|
||||
useTerminalStore.getState().setActiveTab('/repo', shellTabId);
|
||||
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [{
|
||||
sessionId: 'srv-build',
|
||||
cwd: '/repo',
|
||||
status: 'running',
|
||||
createdAt: 10,
|
||||
mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
||||
}]);
|
||||
|
||||
const state = useTerminalStore.getState().getDirectoryState('/repo')!;
|
||||
expect(state.activeTabId).toBe(actionTabId);
|
||||
});
|
||||
|
||||
test('does not activate a rebound running project-action tab when the current active tab is a live running terminal', () => {
|
||||
const actionTabId = setup();
|
||||
const shellTabId = useTerminalStore.getState().createTab('/repo');
|
||||
useTerminalStore.getState().setTabPurpose('/repo', actionTabId, { type: 'project-action', actionId: 'build', executionId: null });
|
||||
useTerminalStore.getState().setTabSessionId('/repo', shellTabId, 'srv-shell');
|
||||
useTerminalStore.getState().setActiveTab('/repo', shellTabId);
|
||||
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [{
|
||||
sessionId: 'srv-build',
|
||||
cwd: '/repo',
|
||||
status: 'running',
|
||||
createdAt: 10,
|
||||
mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
||||
}]);
|
||||
|
||||
const state = useTerminalStore.getState().getDirectoryState('/repo')!;
|
||||
expect(state.activeTabId).toBe(shellTabId);
|
||||
});
|
||||
|
||||
test('does not activate an exited adopted project-action tab', () => {
|
||||
const actionTabId = setup();
|
||||
const shellTabId = useTerminalStore.getState().createTab('/repo');
|
||||
useTerminalStore.getState().setTabPurpose('/repo', actionTabId, { type: 'project-action', actionId: 'build', executionId: null });
|
||||
useTerminalStore.getState().setActiveTab('/repo', shellTabId);
|
||||
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [{
|
||||
sessionId: 'srv-build',
|
||||
cwd: '/repo',
|
||||
status: 'exited',
|
||||
createdAt: 10,
|
||||
mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
||||
}]);
|
||||
|
||||
const state = useTerminalStore.getState().getDirectoryState('/repo')!;
|
||||
expect(state.activeTabId).toBe(shellTabId);
|
||||
});
|
||||
|
||||
test('activates the newest adopted running project action when multiple qualify in one reconciliation', () => {
|
||||
const firstActionTabId = setup();
|
||||
const secondActionTabId = useTerminalStore.getState().createTab('/repo');
|
||||
const shellTabId = useTerminalStore.getState().createTab('/repo');
|
||||
useTerminalStore.getState().setTabPurpose('/repo', firstActionTabId, { type: 'project-action', actionId: 'build', executionId: null });
|
||||
useTerminalStore.getState().setTabPurpose('/repo', secondActionTabId, { type: 'project-action', actionId: 'test', executionId: null });
|
||||
useTerminalStore.getState().setActiveTab('/repo', shellTabId);
|
||||
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [
|
||||
{
|
||||
sessionId: 'srv-build',
|
||||
cwd: '/repo',
|
||||
status: 'running',
|
||||
createdAt: 10,
|
||||
mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
||||
},
|
||||
{
|
||||
sessionId: 'srv-test',
|
||||
cwd: '/repo',
|
||||
status: 'running',
|
||||
createdAt: 20,
|
||||
mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'test', executionId: 'exec-2' },
|
||||
},
|
||||
]);
|
||||
|
||||
const state = useTerminalStore.getState().getDirectoryState('/repo')!;
|
||||
expect(state.activeTabId).toBe(secondActionTabId);
|
||||
});
|
||||
|
||||
// Activation transitions always rewrite the affected tab (session id or
|
||||
// lifecycle changes), so there is no reachable activation without a tab
|
||||
// update; this pins the in-place rebind case where only one tab mutates
|
||||
// and no tab is added, removed, or reordered.
|
||||
test('activates an in-place running rebind of an existing action tab without structural tab changes', () => {
|
||||
const actionTabId = setup();
|
||||
const shellTabId = useTerminalStore.getState().createTab('/repo');
|
||||
useTerminalStore.getState().setTabPurpose('/repo', actionTabId, { type: 'project-action', actionId: 'build', executionId: 'exec-1' });
|
||||
useTerminalStore.getState().setTabSessionId('/repo', actionTabId, 'srv-build', { expectedExecutionId: 'exec-1' });
|
||||
useTerminalStore.getState().setActiveTab('/repo', shellTabId);
|
||||
useTerminalStore.getState().setTabLifecycle('/repo', actionTabId, 'idle', { expectedExecutionId: 'exec-1' });
|
||||
|
||||
const beforeTabs = useTerminalStore.getState().getDirectoryState('/repo')!.tabs;
|
||||
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [{
|
||||
sessionId: 'srv-build',
|
||||
cwd: '/repo',
|
||||
status: 'running',
|
||||
createdAt: beforeTabs[0]!.createdAt,
|
||||
mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-1' },
|
||||
}]);
|
||||
|
||||
const state = useTerminalStore.getState().getDirectoryState('/repo')!;
|
||||
expect(state.tabs).not.toBe(beforeTabs);
|
||||
expect(state.activeTabId).toBe(actionTabId);
|
||||
});
|
||||
|
||||
test('gives unknown adopted action sessions a generic fallback label and icon', () => {
|
||||
setup();
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [{
|
||||
sessionId: 'srv-build',
|
||||
cwd: '/repo',
|
||||
status: 'running',
|
||||
createdAt: 1,
|
||||
mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'unknown-action', executionId: 'exec-1' },
|
||||
}]);
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0]!;
|
||||
expect(tab.label).toBe('unknown-action');
|
||||
expect(tab.iconKey).toBe('play');
|
||||
});
|
||||
|
||||
test('successful empty reconciliation exits known action sessions without touching unrelated directories', () => {
|
||||
const repoTab = setup();
|
||||
useTerminalStore.getState().setTabPurpose('/repo', repoTab, { type: 'project-action', actionId: 'build', executionId: 'exec-1' });
|
||||
useTerminalStore.getState().setTabSessionId('/repo', repoTab, 'srv-build');
|
||||
useTerminalStore.getState().ensureDirectory('/other');
|
||||
const otherBefore = useTerminalStore.getState().getDirectoryState('/other');
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', []);
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0]!;
|
||||
expect(tab.lifecycle).toBe('exited');
|
||||
expect(tab.purpose).toEqual({ type: 'project-action', actionId: 'build', executionId: null });
|
||||
expect(useTerminalStore.getState().getDirectoryState('/other')).toBe(otherBefore);
|
||||
});
|
||||
|
||||
test('normalizes executionId to null when adopting an exited project-action session', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().setTabPurpose('/repo', tabId, { type: 'project-action', actionId: 'build', executionId: null });
|
||||
|
||||
useTerminalStore.getState().reconcileServerSessions('/repo', [{
|
||||
sessionId: 'srv-build',
|
||||
cwd: '/repo',
|
||||
status: 'exited',
|
||||
createdAt: 10,
|
||||
mode: 'command',
|
||||
purpose: { type: 'project-action', actionId: 'build', executionId: 'exec-server' },
|
||||
}]);
|
||||
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0]!;
|
||||
expect(tab.purpose).toEqual({ type: 'project-action', actionId: 'build', executionId: null });
|
||||
});
|
||||
|
||||
test('an old empty snapshot preserves a newer starting action execution', () => {
|
||||
const tabId = setup();
|
||||
const startedActionMutationRevisions = captureStartedActionMutationRevisions('/repo');
|
||||
|
||||
const executionId = useTerminalStore.getState().allocateActionExecution('/repo', tabId, 'build');
|
||||
|
||||
expect(executionId).not.toBeNull();
|
||||
|
||||
reconcileServerSessionsWithStartedRevisions('/repo', [], startedActionMutationRevisions);
|
||||
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0]!;
|
||||
expect(tab.lifecycle).toBe('starting');
|
||||
expect(tab.purpose).toEqual({ type: 'project-action', actionId: 'build', executionId });
|
||||
expect(tab.terminalSessionId).toBeNull();
|
||||
});
|
||||
|
||||
test('an old empty snapshot still preserves the action after it becomes running before apply', () => {
|
||||
const tabId = setup();
|
||||
const startedActionMutationRevisions = captureStartedActionMutationRevisions('/repo');
|
||||
|
||||
const executionId = useTerminalStore.getState().allocateActionExecution('/repo', tabId, 'build');
|
||||
if (!executionId) {
|
||||
throw new Error('expected execution id');
|
||||
}
|
||||
useTerminalStore.getState().setTabSessionId('/repo', tabId, 'srv-build', { expectedExecutionId: executionId });
|
||||
|
||||
reconcileServerSessionsWithStartedRevisions('/repo', [], startedActionMutationRevisions);
|
||||
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0]!;
|
||||
expect(tab.lifecycle).toBe('running');
|
||||
expect(tab.purpose).toEqual({ type: 'project-action', actionId: 'build', executionId });
|
||||
expect(tab.terminalSessionId).toBe('srv-build');
|
||||
});
|
||||
|
||||
test('an old listed action session does not overwrite a newer execution after allocate', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().setTabPurpose('/repo', tabId, { type: 'project-action', actionId: 'build', executionId: 'exec-old' });
|
||||
useTerminalStore.getState().setTabSessionId('/repo', tabId, 'srv-old', { expectedExecutionId: 'exec-old' });
|
||||
|
||||
const startedActionMutationRevisions = captureStartedActionMutationRevisions('/repo');
|
||||
const executionId = useTerminalStore.getState().allocateActionExecution('/repo', tabId, 'build');
|
||||
|
||||
reconcileServerSessionsWithStartedRevisions('/repo', [staleBuildSession], startedActionMutationRevisions);
|
||||
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0]!;
|
||||
expect(tab.lifecycle).toBe('starting');
|
||||
expect(tab.purpose).toEqual({ type: 'project-action', actionId: 'build', executionId });
|
||||
expect(tab.terminalSessionId).toBe('srv-old');
|
||||
});
|
||||
|
||||
test('an old listed action session does not overwrite a newer running session after setTabSessionId', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().setTabPurpose('/repo', tabId, { type: 'project-action', actionId: 'build', executionId: 'exec-old' });
|
||||
useTerminalStore.getState().setTabSessionId('/repo', tabId, 'srv-old', { expectedExecutionId: 'exec-old' });
|
||||
|
||||
const startedActionMutationRevisions = captureStartedActionMutationRevisions('/repo');
|
||||
const executionId = useTerminalStore.getState().allocateActionExecution('/repo', tabId, 'build');
|
||||
if (!executionId) {
|
||||
throw new Error('expected execution id');
|
||||
}
|
||||
useTerminalStore.getState().setTabSessionId('/repo', tabId, 'srv-new', { expectedExecutionId: executionId });
|
||||
|
||||
reconcileServerSessionsWithStartedRevisions('/repo', [staleBuildSession], startedActionMutationRevisions);
|
||||
|
||||
const tabs = useTerminalStore.getState().getDirectoryState('/repo')!.tabs;
|
||||
expect(tabs).toHaveLength(1);
|
||||
const tab = tabs[0]!;
|
||||
expect(tab.lifecycle).toBe('running');
|
||||
expect(tab.purpose).toEqual({ type: 'project-action', actionId: 'build', executionId });
|
||||
expect(tab.terminalSessionId).toBe('srv-new');
|
||||
});
|
||||
|
||||
test('a fresh empty snapshot still clears an omitted action execution', () => {
|
||||
const tabId = setup();
|
||||
const executionId = useTerminalStore.getState().allocateActionExecution('/repo', tabId, 'build');
|
||||
if (!executionId) {
|
||||
throw new Error('expected execution id');
|
||||
}
|
||||
useTerminalStore.getState().setTabSessionId('/repo', tabId, 'srv-build', { expectedExecutionId: executionId });
|
||||
|
||||
const startedActionMutationRevisions = captureStartedActionMutationRevisions('/repo');
|
||||
reconcileServerSessionsWithStartedRevisions('/repo', [], startedActionMutationRevisions);
|
||||
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0]!;
|
||||
expect(tab.lifecycle).toBe('exited');
|
||||
expect(tab.purpose).toEqual({ type: 'project-action', actionId: 'build', executionId: null });
|
||||
expect(tab.terminalSessionId).toBe('srv-build');
|
||||
});
|
||||
|
||||
test('execution-guarded transitions ignore stale completions after a rerun', () => {
|
||||
const tabId = setup();
|
||||
const firstExecution = useTerminalStore.getState().allocateActionExecution('/repo', tabId, 'build')!;
|
||||
const secondExecution = useTerminalStore.getState().allocateActionExecution('/repo', tabId, 'build')!;
|
||||
expect(firstExecution).not.toBe(secondExecution);
|
||||
useTerminalStore.getState().setTabSessionId('/repo', tabId, 'srv-old', { expectedExecutionId: firstExecution });
|
||||
useTerminalStore.getState().setTabLifecycle('/repo', tabId, 'running', { expectedExecutionId: secondExecution });
|
||||
const tab = useTerminalStore.getState().getDirectoryState('/repo')!.tabs[0]!;
|
||||
expect(tab.terminalSessionId).toBeNull();
|
||||
expect(tab.lifecycle).toBe('running');
|
||||
expect(tab.purpose).toEqual({ type: 'project-action', actionId: 'build', executionId: secondExecution });
|
||||
});
|
||||
|
||||
test('applies snapshots atomically and deduplicates output by sequence', () => {
|
||||
const tabId = setup();
|
||||
useTerminalStore.getState().replaceBuffer('/repo', tabId, 'prompt', 4);
|
||||
@@ -199,3 +532,27 @@ describe('default terminal tab labels', () => {
|
||||
expect(labels()).toEqual(['build', 'Terminal']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('directoryMayHaveActiveProjectAction', () => {
|
||||
test('returns true for any non-exited project-action tab', () => {
|
||||
expect(directoryMayHaveActiveProjectAction({
|
||||
activeTabId: 'tab-1',
|
||||
tabs: [
|
||||
{
|
||||
id: 'tab-1',
|
||||
terminalSessionId: null,
|
||||
lifecycle: 'idle',
|
||||
purpose: { type: 'project-action', actionId: 'build', executionId: null },
|
||||
label: 'Build',
|
||||
iconKey: 'play',
|
||||
isConnecting: false,
|
||||
createdAt: 1,
|
||||
previewUrl: null,
|
||||
previewAutoOpened: false,
|
||||
previewUrlLocked: false,
|
||||
},
|
||||
],
|
||||
})).toBe(true);
|
||||
expect(directoryMayHaveActiveProjectAction(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user