mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-11-07 06:19:43 -10:00
fix(cli): preserve OpenTUI extension update state (#15716)
* fix(cli): align OpenTUI cancellation and retry with Ink * fix(cli): discard abandoned OpenTUI tool attempts * feat(cli): preserve OpenTUI session replay and rewind * fix(cli): preserve checkpoint restore ownership and cold continuation * test(cli): cover committed conversation rewind across renderers * feat(cli): capture and restore OpenTUI checkpoints Candidate checkpoint before parent test integration; full normal and combined product validation are still pending. * test(cli): remove duplicate checkpoint regression declarations * fix(cli): preserve Ink compact tool summaries after restore * fix(cli): preserve OpenTUI clipboard source through draft recovery * test(cli): cover OpenTUI raw recall and framed Vim submissions * fix(ci): register clipboard regression in TUI parity gate * feat(cli): complete OpenTUI extension discovery and source management * fix(cli): consolidate catalog interactions and focused key subscriptions * fix(cli): release catalog keyboard subscriptions during consent * fix(cli): preserve complete OpenTUI source information * test(cli): wait for restored assistant before recovery assertions * fix(test): preserve runner Git in installed CLI isolation * fix(cli): preserve OpenTUI extension update state
This commit is contained in:
1 parent
36ab56dd36
commit
341d6626a4
4 files changed
+103
-13
No files matched your search
@@ -421,6 +421,29 @@ describe('OpenTuiExtensionsDialog management keys (#44)', () => {
|
||||
expect(screen.getByText('Update Now')).toBeTruthy();
|
||||
});
|
||||
|
||||
it.each(['up-to-date', undefined] as const)(
|
||||
'adopts a settled update state: %s',
|
||||
async (state) => {
|
||||
const onDetailAction = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce('update-available')
|
||||
.mockResolvedValueOnce(state);
|
||||
renderDialog({ onDetailAction });
|
||||
await press('return');
|
||||
await press('down');
|
||||
await press('down');
|
||||
await press('down');
|
||||
await press('return');
|
||||
expect(screen.getByText('Update Now')).toBeTruthy();
|
||||
await press('down');
|
||||
await press('return');
|
||||
expect(onDetailAction).toHaveBeenLastCalledWith(ROWS[0], 'update');
|
||||
expect(screen.queryByText('Update Now') !== null).toBe(
|
||||
state === undefined,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it('falls back to the list when the open row disappears after a reload', async () => {
|
||||
const { rerender } = render(<div />);
|
||||
const onClose = vi.fn();
|
||||
|
||||
@@ -383,8 +383,8 @@ export function OpenTuiExtensionsDialog(props: OpenTuiExtensionsDialogProps) {
|
||||
setView('uninstall-confirm');
|
||||
return;
|
||||
}
|
||||
if (action === 'mark-update') {
|
||||
void Promise.resolve(onDetailAction?.(currentRow, 'mark-update'))
|
||||
if (action === 'mark-update' || action === 'update') {
|
||||
void Promise.resolve(onDetailAction?.(currentRow, action))
|
||||
.then((state) => {
|
||||
if (state) setCheckedUpdateState(state);
|
||||
})
|
||||
|
||||
@@ -33,6 +33,8 @@ import { ExtensionRefreshState } from '../../config/extension-refresh-state.js';
|
||||
import { AppEvent } from '../../utils/events.js';
|
||||
import {
|
||||
addWorkspaceDirectory,
|
||||
applyExtensionUpdate,
|
||||
applyExtensionUpdateCheck,
|
||||
applyModelSelection,
|
||||
applyMcpServerAction,
|
||||
applyThemeSelection,
|
||||
@@ -152,8 +154,8 @@ vi.mock('./dialog-data.js', () => ({
|
||||
applyExtensionFavorite: () => {},
|
||||
applyExtensionUninstall: async () => {},
|
||||
applyExtensionScopeChange: async () => {},
|
||||
applyExtensionUpdate: async () => {},
|
||||
applyExtensionUpdateCheck: async () => null,
|
||||
applyExtensionUpdate: vi.fn(async () => {}),
|
||||
applyExtensionUpdateCheck: vi.fn(async () => null),
|
||||
buildModelEntries: () => [],
|
||||
computeModelDialogInitialKey: () => undefined,
|
||||
applyModelSelection: vi.fn(async () => ({ ok: true as const })),
|
||||
@@ -321,6 +323,66 @@ describe('OpenTuiDialogMount routing', () => {
|
||||
mockGetHookSystem.mockReset();
|
||||
});
|
||||
|
||||
it.each([
|
||||
'update-available',
|
||||
'up-to-date',
|
||||
'not-updatable',
|
||||
'error',
|
||||
] as const)('returns the extension update check state: %s', async (state) => {
|
||||
const message = `Update check result: ${state}`;
|
||||
vi.mocked(applyExtensionUpdateCheck).mockResolvedValueOnce({
|
||||
message,
|
||||
state,
|
||||
});
|
||||
mount({ dialog: 'extensions_manage' });
|
||||
let result: unknown;
|
||||
await act(async () => {
|
||||
result = await dialogProp('extensions_manage', 'onDetailAction')(
|
||||
{ key: 'owned-extension' },
|
||||
'mark-update',
|
||||
);
|
||||
});
|
||||
expect(applyExtensionUpdateCheck).toHaveBeenCalledWith(
|
||||
CONFIG,
|
||||
'owned-extension',
|
||||
);
|
||||
expect(result).toBe(state);
|
||||
expect(mocks.state.dialogProps['extensions_manage']['status']).toEqual({
|
||||
type: state === 'error' ? 'error' : 'info',
|
||||
text: message,
|
||||
});
|
||||
expect(mocks.state.dialogProps['extensions_manage']['busy']).toBe(false);
|
||||
});
|
||||
|
||||
it.each([true, false])(
|
||||
'settles an extension update with changed=%s',
|
||||
async (changed) => {
|
||||
vi.mocked(applyExtensionUpdate).mockResolvedValueOnce({
|
||||
message: 'Update result',
|
||||
changed,
|
||||
level: changed ? 'success' : 'error',
|
||||
});
|
||||
mount({ dialog: 'extensions_manage' });
|
||||
let state: unknown;
|
||||
await act(async () => {
|
||||
state = await dialogProp('extensions_manage', 'onDetailAction')(
|
||||
{ key: 'owned-extension' },
|
||||
'update',
|
||||
);
|
||||
});
|
||||
expect(applyExtensionUpdate).toHaveBeenCalledWith(
|
||||
CONFIG,
|
||||
'owned-extension',
|
||||
);
|
||||
expect(state).toBe(changed ? 'up-to-date' : undefined);
|
||||
expect(mocks.state.dialogProps['extensions_manage']['busy']).toBe(false);
|
||||
expect(mocks.state.dialogProps['extensions_manage']['status']).toEqual({
|
||||
type: changed ? 'success' : 'error',
|
||||
text: 'Update result',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('routes every dialog request to its own component', () => {
|
||||
expect(REQUESTS).toHaveLength(25);
|
||||
for (const [expected, request] of REQUESTS) {
|
||||
|
||||
@@ -283,19 +283,25 @@ export function OpenTuiDialogMount(props: OpenTuiDialogMountProps) {
|
||||
).then(settle);
|
||||
break;
|
||||
case 'update':
|
||||
void applyExtensionUpdate(config, name).then(settle);
|
||||
break;
|
||||
return applyExtensionUpdate(config, name).then((result) => {
|
||||
settle(result);
|
||||
return result.changed ? ('up-to-date' as const) : undefined;
|
||||
});
|
||||
case 'mark-update':
|
||||
void applyExtensionUpdateCheck(config, name).then((state) => {
|
||||
return applyExtensionUpdateCheck(config, name).then((result) => {
|
||||
setExtBusy(false);
|
||||
if (state) setExtStatus({ type: 'info', text: 'Update available.' });
|
||||
setExtStatus({
|
||||
type: result.state === 'error' ? 'error' : 'info',
|
||||
text: result.message,
|
||||
});
|
||||
return result.state;
|
||||
});
|
||||
break;
|
||||
default: {
|
||||
const exhaustive: never = action;
|
||||
void exhaustive;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// --- model dialog error (kept open on failed selection) -----------------
|
||||
@@ -600,10 +606,9 @@ export function OpenTuiDialogMount(props: OpenTuiDialogMountProps) {
|
||||
onRowAction={(row, action) =>
|
||||
runExtensionAction(row.key, action as ExtensionDetailAction)
|
||||
}
|
||||
onDetailAction={(row, action, arg) => {
|
||||
runExtensionAction(row.key, action, arg);
|
||||
return undefined;
|
||||
}}
|
||||
onDetailAction={(row, action, arg) =>
|
||||
runExtensionAction(row.key, action, arg)
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user