fix(codex): preview joined migration TOML repair

Closes #1635
This commit is contained in:
Tam Nhu Tran committed 2026-07-10 08:54:18 -04:00
1 parent da6c99e3df
commit 77e73dd091
4 files changed
+513 -27

No files matched your search

+193 -1
View File
@@ -40,7 +40,11 @@ vi.mock('@/components/shared/code-editor', () => ({
}));
vi.mock('@/components/compatible-cli/codex-control-center-tab', () => ({
CodexControlCenterTab: () => <div>Control Center</div>,
CodexControlCenterTab: ({ disabled }: { disabled?: boolean }) => (
<div data-testid="codex-control-center" data-disabled={String(Boolean(disabled))}>
Control Center
</div>
),
}));
vi.mock('@/components/compatible-cli/codex-docs-tab', () => ({
@@ -53,6 +57,15 @@ vi.mock('@/components/compatible-cli/codex-overview-tab', () => ({
import { CodexPage } from '@/pages/codex';
const joinedMigrationRawText = `[notice.model_migrations]
"gpt-5.3-codex" = "gpt-5.4"[agents.code_simplifier]
description = "Keep exact text"
`;
const repairedMigrationRawText = joinedMigrationRawText.replace(
'"gpt-5.4"[agents.code_simplifier]',
'"gpt-5.4"\n[agents.code_simplifier]'
);
const diagnostics = {
binary: {
installed: true,
@@ -228,5 +241,184 @@ describe('CodexPage', () => {
expect(screen.getByText(/Read-only: Refusing symlink file for safety\./)).toBeInTheDocument();
expect(screen.getByLabelText('codex raw editor')).toHaveAttribute('readonly');
expect(
screen.queryByRole('button', { name: 'Preview one-newline repair' })
).not.toBeInTheDocument();
});
it('previews the one-newline repair as an unsaved draft before explicitly saving it', async () => {
mocks.saveRawConfigAsync.mockResolvedValue({ success: true, mtime: 101 });
mocks.useCodex.mockReturnValue(
buildUseCodexResult({
rawConfig: {
path: '$CODEX_HOME/config.toml',
resolvedPath: '/tmp/.codex/config.toml',
exists: true,
mtime: 100,
rawText: joinedMigrationRawText,
config: null,
parseError: 'Invalid TOML',
readError: null,
},
})
);
render(<CodexPage />);
await userEvent.click(screen.getByRole('tab', { name: 'Control Center' }));
expect(screen.getByTestId('codex-control-center')).toHaveAttribute('data-disabled', 'true');
await userEvent.click(screen.getByRole('button', { name: 'Preview one-newline repair' }));
expect(screen.getByLabelText('codex raw editor')).toHaveValue(repairedMigrationRawText);
expect(screen.getByText('Unsaved')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Save' })).toBeEnabled();
expect(screen.getByTestId('codex-control-center')).toHaveAttribute('data-disabled', 'true');
expect(mocks.saveRawConfigAsync).not.toHaveBeenCalled();
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
await waitFor(() =>
expect(mocks.saveRawConfigAsync).toHaveBeenCalledWith({
rawText: repairedMigrationRawText,
expectedMtime: 100,
})
);
});
it('keeps the repair draft bound to its original mtime across a fetched snapshot update', async () => {
mocks.saveRawConfigAsync.mockResolvedValue({ success: true, mtime: 201 });
let rawConfig = {
path: '$CODEX_HOME/config.toml',
resolvedPath: '/tmp/.codex/config.toml',
exists: true,
mtime: 100,
rawText: joinedMigrationRawText,
config: null,
parseError: 'Invalid TOML',
readError: null,
};
mocks.useCodex.mockImplementation(() => buildUseCodexResult({ rawConfig }));
const view = render(<CodexPage />);
await userEvent.click(screen.getByRole('tab', { name: 'Control Center' }));
await userEvent.click(screen.getByRole('button', { name: 'Preview one-newline repair' }));
rawConfig = {
...rawConfig,
mtime: 200,
rawText: 'model = "gpt-5.4-mini"\n',
config: { model: 'gpt-5.4-mini' },
parseError: null,
};
view.rerender(<CodexPage />);
expect(screen.getByLabelText('codex raw editor')).toHaveValue(repairedMigrationRawText);
expect(screen.getByTestId('codex-control-center')).toHaveAttribute('data-disabled', 'true');
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
await waitFor(() =>
expect(mocks.saveRawConfigAsync).toHaveBeenCalledWith({
rawText: repairedMigrationRawText,
expectedMtime: 100,
})
);
});
it('keeps manual raw edits bound to the snapshot mtime captured on first edit', async () => {
mocks.saveRawConfigAsync.mockResolvedValue({ success: true, mtime: 201 });
let rawConfig = {
path: '$CODEX_HOME/config.toml',
resolvedPath: '/tmp/.codex/config.toml',
exists: true,
mtime: 100,
rawText: 'model = "gpt-5.4"\n',
config: { model: 'gpt-5.4' },
parseError: null,
readError: null,
};
mocks.useCodex.mockImplementation(() => buildUseCodexResult({ rawConfig }));
const view = render(<CodexPage />);
const editor = screen.getByLabelText('codex raw editor');
await userEvent.clear(editor);
await userEvent.type(editor, 'model = "gpt-5.4-mini"');
rawConfig = {
...rawConfig,
mtime: 200,
rawText: 'model = "gpt-5.4-high"\n',
config: { model: 'gpt-5.4-high' },
};
view.rerender(<CodexPage />);
expect(screen.getByLabelText('codex raw editor')).toHaveValue('model = "gpt-5.4-mini"');
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
await waitFor(() =>
expect(mocks.saveRawConfigAsync).toHaveBeenCalledWith({
rawText: 'model = "gpt-5.4-mini"',
expectedMtime: 100,
})
);
});
it('does not offer the targeted repair for generic invalid TOML', () => {
mocks.useCodex.mockReturnValue(
buildUseCodexResult({
rawConfig: {
path: '$CODEX_HOME/config.toml',
resolvedPath: '/tmp/.codex/config.toml',
exists: true,
mtime: 100,
rawText: 'model = "gpt-5.4"\n[features\n',
config: null,
parseError: 'Invalid TOML',
readError: null,
},
})
);
render(<CodexPage />);
expect(
screen.queryByRole('button', { name: 'Preview one-newline repair' })
).not.toBeInTheDocument();
});
it('re-enables structured controls only after the saved valid snapshot is loaded', async () => {
mocks.saveRawConfigAsync.mockResolvedValue({ success: true, mtime: 101 });
let rawConfig = {
path: '$CODEX_HOME/config.toml',
resolvedPath: '/tmp/.codex/config.toml',
exists: true,
mtime: 100,
rawText: joinedMigrationRawText,
config: null,
parseError: 'Invalid TOML',
readError: null,
};
mocks.useCodex.mockImplementation(() => buildUseCodexResult({ rawConfig }));
const view = render(<CodexPage />);
await userEvent.click(screen.getByRole('tab', { name: 'Control Center' }));
await userEvent.click(screen.getByRole('button', { name: 'Preview one-newline repair' }));
expect(screen.getByTestId('codex-control-center')).toHaveAttribute('data-disabled', 'true');
await userEvent.click(screen.getByRole('button', { name: 'Save' }));
await waitFor(() => expect(mocks.saveRawConfigAsync).toHaveBeenCalledTimes(1));
rawConfig = {
...rawConfig,
mtime: 101,
rawText: repairedMigrationRawText,
config: {
notice: { model_migrations: { 'gpt-5.3-codex': 'gpt-5.4' } },
agents: { code_simplifier: { description: 'Keep exact text' } },
},
parseError: null,
};
view.rerender(<CodexPage />);
expect(screen.getByTestId('codex-control-center')).toHaveAttribute('data-disabled', 'false');
});
});