Four bugs fixed:
1. Banner delay (up to 5 s): DraftConfigContext now exposes isDirty as
reactive useState so App.jsx re-renders immediately when any section
marks itself dirty, instead of waiting for the next checkPending() poll.
2. Banner re-triggers after Apply (race): For non-'*' container restarts
(e.g., cell_name → DNS restart) the background thread took ~300 ms to
clear _pending_restart. A concurrent checkPending() poll could see
needs_restart=True and overwrite the frontend's optimistic clear.
Fix: set needs_restart=False and applying=True synchronously before
spawning the thread.
3. Apply showed banner during applyPending() when hasDirty()==false:
setApplyStatus('saving') was skipped for the auto-save-then-apply
path, leaving applyStatus=null while applyPending() ran and the
banner stayed visible. Always set 'saving' before applyPending().
4. Cert status always 'unknown' in pic_ngo mode: _check_cert_via_ssl
connected to cell-caddy:443 but sent SNI='cell-caddy'. Caddy finds no
matching cert and returns nothing. Fix: pass the effective public
domain (e.g. pic1.pic.ngo) as SNI so Caddy returns the right cert.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -51,7 +51,9 @@ vi.mock('../contexts/DraftConfigContext', () => ({
|
||||
registerFlusher: vi.fn(() => vi.fn()),
|
||||
setDirty: (...a) => mockSetDirty(...a),
|
||||
hasDirty: vi.fn(() => false),
|
||||
isDirty: false,
|
||||
flushAll: vi.fn(),
|
||||
clearAllDirty: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -230,3 +232,53 @@ describe('Cell Identity — Accept path (saveIdentity called by flusher)', () =>
|
||||
expect(mockUpdateConfig.mock.calls[0][0]).toMatchObject({ cell_name: 'pic2' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('Cell Identity — DraftConfig dirty state is set synchronously on change', () => {
|
||||
/**
|
||||
* Verifies that draftConfig.setDirty is called in the same synchronous
|
||||
* event handler as the identity state change, not in a deferred effect.
|
||||
* This is what allows the banner to appear immediately (the isDirty reactive
|
||||
* state in DraftConfigContext re-renders App.jsx without waiting for a poll).
|
||||
*/
|
||||
beforeEach(() => { vi.useFakeTimers({ shouldAdvanceTime: true }); vi.clearAllMocks(); defaultMocks(); });
|
||||
afterEach(async () => { await act(async () => { vi.runAllTimers(); }); vi.useRealTimers(); vi.resetModules(); });
|
||||
|
||||
it('setDirty("identity", true) is called before any timer fires after cell_name change', async () => {
|
||||
await renderSettings();
|
||||
mockSetDirty.mockClear();
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue('pic1'), { target: { value: 'pic2' } });
|
||||
|
||||
// setDirty must be called synchronously within the event, not in a timer
|
||||
expect(mockSetDirty).toHaveBeenCalledWith('identity', true);
|
||||
expect(mockSetDirty).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('setDirty("identity", true) is called before any timer fires after ip_range change', async () => {
|
||||
await renderSettings();
|
||||
mockSetDirty.mockClear();
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue('172.20.0.0/16'), { target: { value: '10.0.0.0/8' } });
|
||||
|
||||
expect(mockSetDirty).toHaveBeenCalledWith('identity', true);
|
||||
expect(mockSetDirty).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('setDirty("identity", false) is called after saveIdentity completes', async () => {
|
||||
mockGetConfig
|
||||
.mockResolvedValueOnce({ data: makeCfg() })
|
||||
.mockResolvedValue({ data: makeCfg({ ip_range: '10.0.0.0/8' }) });
|
||||
await renderSettings();
|
||||
mockSetDirty.mockClear();
|
||||
|
||||
fireEvent.change(screen.getByDisplayValue('172.20.0.0/16'), { target: { value: '10.0.0.0/8' } });
|
||||
|
||||
// auto-save fires after 800 ms
|
||||
await act(async () => { vi.advanceTimersByTime(900); });
|
||||
await act(async () => { await Promise.resolve(); });
|
||||
|
||||
const calls = mockSetDirty.mock.calls;
|
||||
expect(calls.some(([k, v]) => k === 'identity' && v === true)).toBe(true);
|
||||
expect(calls.some(([k, v]) => k === 'identity' && v === false)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user