From ec20cffb35d8b08be84d35aa73be63814e20bd15 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:04:47 -0800 Subject: [PATCH 1/4] feat(connectors): one named Disconnect per hosted connection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A hosted entry with two or more connections offered no Disconnect, since nothing told the connections apart. The catalogue now gives each hosted row its connectedAt and, for an operator key, its agent, so each gets its own Disconnect, labelled "Signed in · Oct 30" or "Key · Scout · Oct 31". A connection the server does not describe still offers none. Dropping one of several connections no longer marks the entry's grants revoked in place: a grant does not name its connection, so the re-read decides. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- frontend/src/i18n/locales/en.json | 6 +- frontend/src/i18n/locales/zh-CN.json | 6 +- .../__tests__/ConnectorsPage.tools.test.tsx | 52 ++++++++++++++- frontend/src/v2/connectors/ToolPanels.tsx | 63 ++++++++++++++++--- frontend/src/v2/connectors/toolModel.ts | 13 +++- .../src/v2/connectors/useToolConnectors.ts | 7 ++- 6 files changed, 132 insertions(+), 15 deletions(-) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index f6617a62a..5db86b548 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -2728,7 +2728,11 @@ "trailError": "Could not read the trail.", "viewGrant": "View {{tool}} in {{pod}}", "listSeparator": ", ", - "sentenceSeparator": " " + "sentenceSeparator": " ", + "connectionSeparator": " · ", + "connectionSignedIn": "Signed in", + "connectionKey": "Key", + "disconnectConnectionLabel": "Disconnect {{label}}: {{connection}}" }, "agentManage": { "runtime": { diff --git a/frontend/src/i18n/locales/zh-CN.json b/frontend/src/i18n/locales/zh-CN.json index da78fef33..c238dfeda 100644 --- a/frontend/src/i18n/locales/zh-CN.json +++ b/frontend/src/i18n/locales/zh-CN.json @@ -2731,7 +2731,11 @@ "trailError": "无法读取调用记录。", "viewGrant": "在 {{pod}} 中查看 {{tool}}", "listSeparator": "、", - "sentenceSeparator": "" + "sentenceSeparator": "", + "connectionSeparator": " · ", + "connectionSignedIn": "已登录", + "connectionKey": "密钥", + "disconnectConnectionLabel": "断开 {{label}}:{{connection}}" }, "agentManage": { "runtime": { diff --git a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx index d5fb519d7..490d28560 100644 --- a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx +++ b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx @@ -679,7 +679,7 @@ describe('Connectors page: tools', () => { expect(within(aside).queryByText(/Revoked by/)).not.toBeInTheDocument(); }); - it('two connections of one entry offer no Disconnect, since a grant does not name its connection', async () => { + it('two connections the server does not describe offer no Disconnect, since neither can be named', async () => { const two = { ...atlassian, connections: [...atlassian.connections, { connectionId: 'conn-atl-2', owner: 'me', repo: 'atlassian' }] }; mockTools({ entries: [two], grants: [] }); renderPage(); @@ -689,6 +689,56 @@ describe('Connectors page: tools', () => { expect(screen.queryByRole('button', { name: /Disconnect/ })).not.toBeInTheDocument(); }); + // TASK-330: one sign-in row plus operator-key rows bound per agent. The + // server describes each row, so each gets its own named Disconnect. + const twoNamed = { + ...atlassian, + connections: [ + { connectionId: 'conn-atl', owner: '', repo: '', connectedAt: '2026-10-30T12:00:00Z' }, + { connectionId: 'conn-atl-key', owner: '', repo: '', agentId: scout.userId, connectedAt: '2026-10-31T12:00:00Z' }, + ], + }; + + it('two described connections each offer a named Disconnect that drops only that connection', async () => { + mockTools({ entries: [twoNamed], grants: [] }); + axios.delete.mockResolvedValue({ data: { message: 'ok' } }); + renderPage(); + + const popover = await openToolPopover('Atlassian'); + expect(within(popover).getByText('Signed in · Oct 30')).toBeInTheDocument(); + expect(within(popover).getByText('Key · Scout · Oct 31')).toBeInTheDocument(); + fireEvent.click(within(popover).getByRole('button', { name: 'Disconnect Atlassian: Key · Scout · Oct 31' })); + expect(within(popover).getAllByRole('button', { name: 'Yes, disconnect' })).toHaveLength(1); + expect(within(popover).getByRole('button', { name: 'Disconnect Atlassian: Signed in · Oct 30' })).toBeInTheDocument(); + fireEvent.click(within(popover).getByRole('button', { name: 'Yes, disconnect' })); + await waitFor(() => expect(axios.delete).toHaveBeenCalledWith('/api/integrations/conn-atl-key', expect.any(Object))); + expect(axios.delete).toHaveBeenCalledTimes(1); + }); + + it('dropping one of two connections leaves the grant to the re-read, not marked revoked in place', async () => { + mockTools({ entries: [twoNamed], grants: [grantAtl] }); + axios.delete.mockResolvedValue({ data: { message: 'ok' } }); + renderPage(); + + const { aside } = await openRow('Atlassian', 'View Atlassian in Launch pod'); + fireEvent.click(within(aside).getByRole('button', { name: 'Disconnect Atlassian: Signed in · Oct 30' })); + fireEvent.click(within(aside).getByRole('button', { name: 'Yes, disconnect' })); + await waitFor(() => expect(axios.delete).toHaveBeenCalledWith('/api/integrations/conn-atl', expect.any(Object))); + const card = await screen.findByRole('complementary', { name: 'Grant details' }); + // A grant does not name its connection, so only the server can say it ended. + expect(within(card).queryByText(/Revoked by/)).not.toBeInTheDocument(); + expect(within(card).getByRole('button', { name: 'Revoke' })).toBeInTheDocument(); + }); + + it('a key connection whose agent is not on any roster is named by key and date', async () => { + const unknownAgent = { ...twoNamed, connections: [twoNamed.connections[0], { ...twoNamed.connections[1], agentId: 'someone-else' }] }; + mockTools({ entries: [unknownAgent], grants: [] }); + renderPage(); + + const popover = await openToolPopover('Atlassian'); + expect(within(popover).getByText('Key · Oct 31')).toBeInTheDocument(); + }); + it('GitHub (the instance App connection) never offers Disconnect', async () => { renderPage(); await openTool('GitHub'); diff --git a/frontend/src/v2/connectors/ToolPanels.tsx b/frontend/src/v2/connectors/ToolPanels.tsx index b3bbb6aec..0dfb37681 100644 --- a/frontend/src/v2/connectors/ToolPanels.tsx +++ b/frontend/src/v2/connectors/ToolPanels.tsx @@ -19,6 +19,7 @@ import type { ToolOutcome, ToolCatalogEntry, HiddenToolConnection, + ToolConnection, } from './toolModel'; import type { ToolConnectorsModel } from './useToolConnectors'; @@ -128,12 +129,53 @@ export const useToolPanels = (m: ToolConnectorsModel) => {

); }; + const agentNameById = (agentId?: string): string => { + if (!agentId) return ''; + const seat = Object.values(seats).flat().find((candidate) => candidate.userId === agentId); + return seat?.displayName || seat?.name || ''; + }; + // Names one hosted connection: a key row by its agent, the sign-in as such, + // and both by when. Null when the server did not describe the row (no + // connectedAt), so it cannot be told apart. + const connectionLabel = (connection: ToolConnection): string | null => { + if (!connection.connectedAt || Number.isNaN(Date.parse(connection.connectedAt))) return null; + const date = new Date(connection.connectedAt).toLocaleDateString(i18n.language, { month: 'short', day: 'numeric' }); + const separator = t('tools.connectionSeparator', { defaultValue: ' · ' }); + if (connection.agentId) { + return [t('tools.connectionKey', { defaultValue: 'Key' }), agentNameById(connection.agentId), date].filter(Boolean).join(separator); + } + return [t('tools.connectionSignedIn', { defaultValue: 'Signed in' }), date].join(separator); + }; // Hosted connections only: a GitHub App connection is the instance's, not the viewer's to drop. const renderDisconnect = (entry: ToolCatalogEntry | null, variant: 'row' | 'aside') => { - // One connection only: a grant does not say which connection it was minted on, - // so with several the button could drop the wrong one. - const connection = entry && entry.connectionType === 'hosted-mcp' && entry.connections.length === 1 ? entry.connections[0] : null; - if (!entry || !connection) return null; + const hosted = entry && entry.connectionType === 'hosted-mcp' ? entry.connections : []; + if (!entry || hosted.length === 0) return null; + if (hosted.length === 1) { + const button = renderDisconnectOne(entry, hosted[0], variant, null); + return variant === 'aside' ?
{button}
: button; + } + // Several connections: each is named, so each gets its own Disconnect. A + // connection the server did not describe cannot be told apart, so none is + // offered rather than one that might drop the wrong connection. + const labelled = hosted.map((connection) => ({ connection, label: connectionLabel(connection) })); + if (labelled.some((row) => !row.label)) return null; + return ( + <> + {labelled.map(({ connection, label }) => (variant === 'aside' ? ( +
+ {label} + {renderDisconnectOne(entry, connection, variant, label)} +
+ ) : ( + + {label} + {renderDisconnectOne(entry, connection, variant, label)} + + )))} + + ); + }; + const renderDisconnectOne = (entry: ToolCatalogEntry, connection: ToolConnection, variant: 'row' | 'aside', label: string | null) => { const cls = variant === 'row' ? 'v2-connector-row__action v2-connector-row__action--secondary' : 'v2-connector-aside__secondary'; @@ -150,7 +192,14 @@ export const useToolPanels = (m: ToolConnectorsModel) => { ); } return ( - ); @@ -583,9 +632,7 @@ export const useToolPanels = (m: ToolConnectorsModel) => { ))} - {isGranter(grant) && ( -
{renderDisconnect(entry, 'aside')}
- )} + {isGranter(grant) && renderDisconnect(entry, 'aside')} {error &&

{error}

}
diff --git a/frontend/src/v2/connectors/toolModel.ts b/frontend/src/v2/connectors/toolModel.ts index 65c4969ca..b7968c79f 100644 --- a/frontend/src/v2/connectors/toolModel.ts +++ b/frontend/src/v2/connectors/toolModel.ts @@ -76,7 +76,18 @@ export interface ToolCatalogEntry { unavailableReason?: string; broker?: { id: string }; tools: Array<{ name: string; description?: string; requiredWriteMode: GrantWriteMode; irreversible: boolean }>; - connections: Array<{ connectionId: string; owner: string; repo: string }>; + connections: ToolConnection[]; +} + +/** A hosted row also says when it was made and, for an operator key, its agent, + * so two of them can be told apart. */ +export interface ToolConnection { + connectionId: string; + owner: string; + repo: string; + connectedAt?: string; + /** The bot User a key row is bound to; absent on the OAuth sign-in. */ + agentId?: string; } export interface HiddenToolConnection { diff --git a/frontend/src/v2/connectors/useToolConnectors.ts b/frontend/src/v2/connectors/useToolConnectors.ts index 55d7cfa1b..e51164820 100644 --- a/frontend/src/v2/connectors/useToolConnectors.ts +++ b/frontend/src/v2/connectors/useToolConnectors.ts @@ -505,8 +505,9 @@ export const useToolConnectors = (pods: V2Pod[], manualRevokeNotice: HostedMcpMa setConfirmDisconnect(null); setDisconnectedHere((current) => [...current, connectionId]); // The removal revokes every grant on the connection, so the page says so - // now rather than waiting on a re-read that may fail. - const ended = (grants || []).filter((grant) => !isDead(grant) && entryCovers(entry, grant)); + // now rather than waiting on a re-read that may fail. A grant does not say + // which connection it was minted on, so with several only the re-read can. + const ended = entry.connections.length > 1 ? [] : (grants || []).filter((grant) => !isDead(grant) && entryCovers(entry, grant)); setRevokedHere((current) => ({ ...current, ...Object.fromEntries(ended.map((grant) => [grant.grantId, new Date().toISOString()])) })); setRemovedHiddenNotice({ label: entry.label, @@ -523,7 +524,7 @@ export const useToolConnectors = (pods: V2Pod[], manualRevokeNotice: HostedMcpMa const partial = response?.status === 502 && (response.data?.code === 'provider_revoke_failed' || response.data?.code === 'provider_revoked_removal_incomplete'); if (partial) { - const ended = (grants || []).filter((grant) => !isDead(grant) && entryCovers(entry, grant)); + const ended = entry.connections.length > 1 ? [] : (grants || []).filter((grant) => !isDead(grant) && entryCovers(entry, grant)); setRevokedHere((current) => ({ ...current, ...Object.fromEntries(ended.map((grant) => [grant.grantId, new Date().toISOString()])) })); setConfirmDisconnect(null); setDisconnectedHere((current) => [...current, connectionId]); From 666da9f960d30ef3052df20c2d3da39d2d567011 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:11:27 -0800 Subject: [PATCH 2/4] fix(connectors): two connections that read the same get no Disconnect MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two keys bound the same day for agents not on a loaded roster both read "Key · Oct 31", so their buttons could not be told apart. Treat a repeated label like a missing one and offer none. Also pin the partial-502 branch with two connections: the grant stays live until the re-read. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- .../__tests__/ConnectorsPage.tools.test.tsx | 30 +++++++++++++++++++ frontend/src/v2/connectors/ToolPanels.tsx | 8 +++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx index 490d28560..27507383d 100644 --- a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx +++ b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx @@ -739,6 +739,36 @@ describe('Connectors page: tools', () => { expect(within(popover).getByText('Key · Oct 31')).toBeInTheDocument(); }); + it('two connections that read the same offer no Disconnect, since neither can be told apart', async () => { + const lookAlike = { + ...twoNamed, + connections: [ + { connectionId: 'conn-atl-k1', owner: '', repo: '', agentId: 'off-roster-1', connectedAt: '2026-10-31T09:00:00Z' }, + { connectionId: 'conn-atl-k2', owner: '', repo: '', agentId: 'off-roster-2', connectedAt: '2026-10-31T15:00:00Z' }, + ], + }; + mockTools({ entries: [lookAlike], grants: [] }); + renderPage(); + + const popover = await openToolPopover('Atlassian'); + expect(within(popover).getByRole('button', { name: 'Add' })).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: /Disconnect/ })).not.toBeInTheDocument(); + }); + + it('a 502 partial removal of one of two connections leaves the grant to the re-read', async () => { + mockTools({ entries: [twoNamed], grants: [grantAtl] }); + axios.delete.mockRejectedValue({ response: { status: 502, data: { code: 'provider_revoke_failed', revokeAt: 'https://id.atlassian.com/manage-profile/apps' } } }); + renderPage(); + + const { aside } = await openRow('Atlassian', 'View Atlassian in Launch pod'); + fireEvent.click(within(aside).getByRole('button', { name: 'Disconnect Atlassian: Signed in · Oct 30' })); + fireEvent.click(within(aside).getByRole('button', { name: 'Yes, disconnect' })); + expect(await screen.findByRole('link', { name: 'Open provider settings' })).toBeInTheDocument(); + const card = await screen.findByRole('complementary', { name: 'Grant details' }); + expect(within(card).queryByText(/Revoked by/)).not.toBeInTheDocument(); + expect(within(card).getByRole('button', { name: 'Revoke' })).toBeInTheDocument(); + }); + it('GitHub (the instance App connection) never offers Disconnect', async () => { renderPage(); await openTool('GitHub'); diff --git a/frontend/src/v2/connectors/ToolPanels.tsx b/frontend/src/v2/connectors/ToolPanels.tsx index 0dfb37681..f4d937c36 100644 --- a/frontend/src/v2/connectors/ToolPanels.tsx +++ b/frontend/src/v2/connectors/ToolPanels.tsx @@ -155,10 +155,12 @@ export const useToolPanels = (m: ToolConnectorsModel) => { return variant === 'aside' ?
{button}
: button; } // Several connections: each is named, so each gets its own Disconnect. A - // connection the server did not describe cannot be told apart, so none is - // offered rather than one that might drop the wrong connection. + // connection that cannot be told apart (the server did not describe it, or + // it reads the same as another) gets none rather than one that might drop + // the wrong connection. const labelled = hosted.map((connection) => ({ connection, label: connectionLabel(connection) })); - if (labelled.some((row) => !row.label)) return null; + const labels = labelled.map((row) => row.label); + if (labels.some((label) => !label) || new Set(labels).size < labels.length) return null; return ( <> {labelled.map(({ connection, label }) => (variant === 'aside' ? ( From 72ce77c01bb3c9b61872804be47d4cf52959ed6f Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:15:38 -0800 Subject: [PATCH 3/4] fix(connectors): centre a hosted connection's name on its Disconnect In the grant aside the label reused the note class, whose margin-top: 12px !important dropped it about 6px below the button's centre in the flex row. Give it its own class with the note's type and no margin; guarded by a layout invariant. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- frontend/src/v2/__tests__/v2-layout-invariants.test.ts | 5 +++++ frontend/src/v2/connectors/ToolPanels.tsx | 2 +- frontend/src/v2/v2.css | 2 ++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts index 084c4edce..8b8e4180e 100644 --- a/frontend/src/v2/__tests__/v2-layout-invariants.test.ts +++ b/frontend/src/v2/__tests__/v2-layout-invariants.test.ts @@ -2565,6 +2565,11 @@ describe('v2 layout invariants (CSS rule presence)', () => { expect(asideIcon).toContain('height: 32px'); expect(asideIcon).toContain('min-height: 32px'); expect(v2).toContain('.v2-root button.v2-connector-aside__secondary.v2-connector-aside__icon { width: 44px; height: 44px; min-height: 44px; }'); + // A hosted connection's name sits in the aside's flex row beside its own + // Disconnect. The note class's `margin-top: 12px !important` dropped it + // about 6px below the button's centre, so this label carries no margin. + expect(ruleBody(v2, '.v2-connector-aside__conn')).toContain('font-size: 12px'); + expect(ruleBody(v2, '.v2-connector-aside__conn')).not.toContain('margin'); expect(ruleBody(v2, '.v2-connector-row__details')).toContain('padding-right: 8px'); expect(ruleBody(v2, '.v2-page-head__meta')).toContain('var(--v2-font-mono)'); expect(ruleBody(v2, '.v2-connector-row__glyph')).toContain('width: 20px'); diff --git a/frontend/src/v2/connectors/ToolPanels.tsx b/frontend/src/v2/connectors/ToolPanels.tsx index f4d937c36..457cbbb74 100644 --- a/frontend/src/v2/connectors/ToolPanels.tsx +++ b/frontend/src/v2/connectors/ToolPanels.tsx @@ -165,7 +165,7 @@ export const useToolPanels = (m: ToolConnectorsModel) => { <> {labelled.map(({ connection, label }) => (variant === 'aside' ? (
- {label} + {label} {renderDisconnectOne(entry, connection, variant, label)}
) : ( diff --git a/frontend/src/v2/v2.css b/frontend/src/v2/v2.css index 19058ff56..c47ebe973 100644 --- a/frontend/src/v2/v2.css +++ b/frontend/src/v2/v2.css @@ -4238,6 +4238,8 @@ body.modern-ui.v2-canvas { .v2-connector-aside__refusal a { color: var(--v2-accent-text); text-decoration: underline; } .v2-connector-aside__note { margin-top: 12px !important; color: var(--v2-text-tertiary) !important; font-size: 12px !important; } +/* A hosted connection's name beside its own Disconnect: the note's type, centred on the button. */ +.v2-connector-aside__conn { color: var(--v2-text-tertiary); font-size: 12px; } /* Row C: the refusal the row's own button earned, beside that button in the aside (and on the row itself) — same bordered grammar as the page slot. */ .v2-connector-aside__refusal { margin-top: 10px !important; padding: 8px 10px; border: 1px solid var(--v2-border); border-radius: var(--v2-radius-sm); background: var(--v2-surface-tint); color: var(--v2-ink); font-size: 13px; line-height: 18px; overflow-wrap: anywhere; } From db9151004f2e5b702a01b180e6033a4701a457e6 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 1 Nov 2026 07:26:22 -0800 Subject: [PATCH 4/4] test(connectors): connection labels hold in any time zone The labels use the viewer's local date, and the fixtures asserted fixed dates: in Auckland five tests failed, and in Honolulu the two look-alike keys landed on different days. Compute the expected day the way the page does, and give the look-alike keys one instant. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015AKgpMD6Qh13f7fj6mbGT3 --- .../__tests__/ConnectorsPage.tools.test.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx index 27507383d..f92f3a487 100644 --- a/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx +++ b/frontend/src/v2/__tests__/ConnectorsPage.tools.test.tsx @@ -691,6 +691,11 @@ describe('Connectors page: tools', () => { // TASK-330: one sign-in row plus operator-key rows bound per agent. The // server describes each row, so each gets its own named Disconnect. + // Labels carry the viewer's local date, so expected text is computed the + // same way and holds in any time zone the suite runs in. + const day = (at) => new Date(at).toLocaleDateString('en', { month: 'short', day: 'numeric' }); + const SIGNED_IN = `Signed in · ${day('2026-10-30T12:00:00Z')}`; + const KEY_SCOUT = `Key · Scout · ${day('2026-10-31T12:00:00Z')}`; const twoNamed = { ...atlassian, connections: [ @@ -705,11 +710,11 @@ describe('Connectors page: tools', () => { renderPage(); const popover = await openToolPopover('Atlassian'); - expect(within(popover).getByText('Signed in · Oct 30')).toBeInTheDocument(); - expect(within(popover).getByText('Key · Scout · Oct 31')).toBeInTheDocument(); - fireEvent.click(within(popover).getByRole('button', { name: 'Disconnect Atlassian: Key · Scout · Oct 31' })); + expect(within(popover).getByText(SIGNED_IN)).toBeInTheDocument(); + expect(within(popover).getByText(KEY_SCOUT)).toBeInTheDocument(); + fireEvent.click(within(popover).getByRole('button', { name: `Disconnect Atlassian: ${KEY_SCOUT}` })); expect(within(popover).getAllByRole('button', { name: 'Yes, disconnect' })).toHaveLength(1); - expect(within(popover).getByRole('button', { name: 'Disconnect Atlassian: Signed in · Oct 30' })).toBeInTheDocument(); + expect(within(popover).getByRole('button', { name: `Disconnect Atlassian: ${SIGNED_IN}` })).toBeInTheDocument(); fireEvent.click(within(popover).getByRole('button', { name: 'Yes, disconnect' })); await waitFor(() => expect(axios.delete).toHaveBeenCalledWith('/api/integrations/conn-atl-key', expect.any(Object))); expect(axios.delete).toHaveBeenCalledTimes(1); @@ -721,7 +726,7 @@ describe('Connectors page: tools', () => { renderPage(); const { aside } = await openRow('Atlassian', 'View Atlassian in Launch pod'); - fireEvent.click(within(aside).getByRole('button', { name: 'Disconnect Atlassian: Signed in · Oct 30' })); + fireEvent.click(within(aside).getByRole('button', { name: `Disconnect Atlassian: ${SIGNED_IN}` })); fireEvent.click(within(aside).getByRole('button', { name: 'Yes, disconnect' })); await waitFor(() => expect(axios.delete).toHaveBeenCalledWith('/api/integrations/conn-atl', expect.any(Object))); const card = await screen.findByRole('complementary', { name: 'Grant details' }); @@ -736,15 +741,15 @@ describe('Connectors page: tools', () => { renderPage(); const popover = await openToolPopover('Atlassian'); - expect(within(popover).getByText('Key · Oct 31')).toBeInTheDocument(); + expect(within(popover).getByText(`Key · ${day('2026-10-31T12:00:00Z')}`)).toBeInTheDocument(); }); it('two connections that read the same offer no Disconnect, since neither can be told apart', async () => { const lookAlike = { ...twoNamed, connections: [ - { connectionId: 'conn-atl-k1', owner: '', repo: '', agentId: 'off-roster-1', connectedAt: '2026-10-31T09:00:00Z' }, - { connectionId: 'conn-atl-k2', owner: '', repo: '', agentId: 'off-roster-2', connectedAt: '2026-10-31T15:00:00Z' }, + { connectionId: 'conn-atl-k1', owner: '', repo: '', agentId: 'off-roster-1', connectedAt: '2026-10-31T12:00:00Z' }, + { connectionId: 'conn-atl-k2', owner: '', repo: '', agentId: 'off-roster-2', connectedAt: '2026-10-31T12:00:00Z' }, ], }; mockTools({ entries: [lookAlike], grants: [] }); @@ -761,7 +766,7 @@ describe('Connectors page: tools', () => { renderPage(); const { aside } = await openRow('Atlassian', 'View Atlassian in Launch pod'); - fireEvent.click(within(aside).getByRole('button', { name: 'Disconnect Atlassian: Signed in · Oct 30' })); + fireEvent.click(within(aside).getByRole('button', { name: `Disconnect Atlassian: ${SIGNED_IN}` })); fireEvent.click(within(aside).getByRole('button', { name: 'Yes, disconnect' })); expect(await screen.findByRole('link', { name: 'Open provider settings' })).toBeInTheDocument(); const card = await screen.findByRole('complementary', { name: 'Grant details' });