Skip to content
Merged
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
78 changes: 78 additions & 0 deletions frontend/src/v2/__tests__/V2AgentSettingsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -579,3 +579,81 @@ test('runner facts refresh without resetting unsaved controls, and a failed read
expect(input).toHaveValue('unsaved-draft');
} finally { view.unmount(); jest.useRealTimers(); }
});

test('the key list re-reads while visible, so an outside call updates Last used', async () => {
jest.useFakeTimers();
let used = false;
const defaultGet = axios.get.getMockImplementation();
axios.get.mockImplementation((url, options) => (
url === '/api/credentials'
? Promise.resolve({ data: [{ _id: 'tok-1', label: 'Laptop', createdAt: '2026-10-01T00:00:00Z',
lastUsedAt: used ? '2026-10-31T09:15:00Z' : null, revokedAt: null, status: 'active' }] })
: defaultGet(url, options)
));
const view = renderPage();
try {
const before = await screen.findByText(/last used/i);
const beforeText = before.textContent;
used = true;
await act(async () => { jest.advanceTimersByTime(15_000); });
expect(screen.getByText(/last used/i).textContent).not.toEqual(beforeText);
} finally { view.unmount(); jest.useRealTimers(); }
});

test('key polling stops on unmount', async () => {
jest.useFakeTimers();
const view = renderPage();
try {
await screen.findByText(/last used/i);
view.unmount();
const calls = axios.get.mock.calls.filter(([url]) => url === '/api/credentials').length;
await act(async () => { jest.advanceTimersByTime(60_000); });
expect(axios.get.mock.calls.filter(([url]) => url === '/api/credentials').length).toBe(calls);
} finally { jest.useRealTimers(); }
});

test('a revoke from outside moves the header off Connected without a remount', async () => {
jest.useFakeTimers();
let state = 'connected';
axios.get.mockImplementation((url) => {
if (url.startsWith('/api/agent-memory/')) return Promise.resolve({ data: {
displayName: 'Maya', pods: [{ id: 'pod-1', name: 'Workspace' }],
} });
if (url.endsWith('/manage-settings')) return Promise.resolve({ data: {
writablePodId: 'pod-1', installations: [{ podId: 'pod-1', podName: 'Workspace', status: 'active' }],
} });
if (url === '/api/registry/pods/pod-1/agents') return Promise.resolve({ data: { agents: [{
name: 'maya-claude', instanceId: 'default', runtime: { runtimeType: 'webhook' },
connection: { state, lastSeenAt: null },
}] } });
if (url.startsWith('/api/registry/pods/')) return Promise.resolve({ data: { agent: {
name: 'maya-claude', instanceId: 'default', displayName: 'Maya', runtime: { runtimeType: 'webhook' },
} } });
return Promise.resolve({ data: [] });
});
const view = renderPage();
try {
await act(async () => { await Promise.resolve(); });
expect(screen.queryByRole('link', { name: 'Reconnect' })).toBeNull();
state = 'revoked';
await act(async () => { jest.advanceTimersByTime(30_000); });
expect(await screen.findByRole('link', { name: 'Reconnect' })).toBeInTheDocument();
} finally { view.unmount(); jest.useRealTimers(); }
});

test('key polling pauses while hidden and re-reads on focus', async () => {
jest.useFakeTimers();
const view = renderPage();
const keyReads = () => axios.get.mock.calls.filter(([url]) => url === '/api/credentials').length;
try {
await screen.findByText(/last used/i);
const hidden = jest.spyOn(document, 'hidden', 'get').mockReturnValue(true);
const before = keyReads();
await act(async () => { jest.advanceTimersByTime(45_000); });
expect(keyReads()).toBe(before);
hidden.mockReturnValue(false);
await act(async () => { window.dispatchEvent(new Event('focus')); });
expect(keyReads()).toBe(before + 1);
hidden.mockRestore();
} finally { view.unmount(); jest.useRealTimers(); }
});
29 changes: 29 additions & 0 deletions frontend/src/v2/agents/V2AgentSettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,35 @@ const V2AgentSettingsPage: React.FC = () => {
return () => { active = false; };
}, [access, podId, fetchKeys, identityKey, t]);

// A key used or revoked from outside this page only shows up when the list
// is re-read. Poll while the tab is visible, and re-read on focus.
useEffect(() => {
if (!access || !podId) return undefined;
let active = true;
let pending = false;
const refreshKeys = async () => {
if (pending || document.hidden) return;
pending = true;
try {
const rows = await fetchKeys();
if (active) setKeys(rows);
} catch {
// Keep the last list; the next tick or focus retries.
} finally {
pending = false;
}
};
const timer = window.setInterval(refreshKeys, 15_000);
window.addEventListener('focus', refreshKeys);
document.addEventListener('visibilitychange', refreshKeys);
return () => {
active = false;
window.clearInterval(timer);
window.removeEventListener('focus', refreshKeys);
document.removeEventListener('visibilitychange', refreshKeys);
};
}, [access, podId, fetchKeys]);

const replaceKey = async () => {
if (!access || !podId || pendingAction !== 'replace' || busy || installation?.runtime?.runtimeType === 'wrapper') return;
const requestIdentity = identityKey;
Expand Down
Loading