diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml index 7991a48a4..ef0ca25d1 100644 --- a/.github/workflows/pr-ci.yml +++ b/.github/workflows/pr-ci.yml @@ -436,7 +436,27 @@ jobs: image: ghcr.io/tinyhumansai/openhuman_ci:rust-1.93.0 env: CARGO_INCREMENTAL: "0" + # The instrumented Tauri shell binary (whole tauri/zbus/CEF graph) is + # dominated by DWARF debug info, which exhausts the runner disk and + # SIGBUSes the linker (collect2: ld terminated with signal 7) during the + # final link. Drop debug info — llvm-cov reads coverage from the embedded + # __llvm_covmap/__llvm_covfun sections, NOT from DWARF, so coverage + # numbers are unaffected. Mirrors rust-core-coverage. + CARGO_PROFILE_DEV_DEBUG: "0" steps: + - name: Free disk space + run: | + # Coverage-instrumented builds produce huge binaries that exhaust the + # runner's ~14GB free disk during linking (SIGBUS). Inside a container, + # host paths are bind-mounted: + # /opt/hostedtoolcache → /__t + # /home/runner/work → /__w + # Deleting /__t frees ~8GB on the same partition the build uses. + # Mirrors rust-core-coverage. + rm -rf /__t/* || true + echo "Disk after cleanup:" + df -h /__w || true + - name: Checkout code uses: actions/checkout@v5 with: @@ -471,6 +491,11 @@ jobs: - name: Run cargo llvm-cov for Tauri shell run: bash scripts/ci-cancel-aware.sh cargo llvm-cov --manifest-path app/src-tauri/Cargo.toml --lcov --output-path lcov-tauri.info + env: + # Serialize codegen/link to cap peak memory + disk during the + # instrumented Tauri-shell link (SIGBUS otherwise). Mirrors + # rust-core-coverage. + CARGO_BUILD_JOBS: "1" - name: Save CEF binary distribution cache if: always() && steps.cef-cache.outputs.cache-hit != 'true' diff --git a/app/src/components/accounts/WebviewHost.tsx b/app/src/components/accounts/WebviewHost.tsx index 1cf8126bf..20e48f48b 100644 --- a/app/src/components/accounts/WebviewHost.tsx +++ b/app/src/components/accounts/WebviewHost.tsx @@ -226,7 +226,9 @@ const WebviewHost = ({ accountId, provider }: WebviewHostProps) => { /> - {isLoading ? `${t('accounts.webviewHost.loading')} ${providerName}...` : providerName} + {isLoading + ? t('accounts.webviewHost.loading').replace('{providerName}', providerName) + : providerName} {isLoading ? (
{ aria-live="polite" aria-label={t('accounts.webviewHost.loadTimeout')}>
-

{`${providerName} ${t('accounts.webviewHost.takingLonger')}`}

+

+ {t('accounts.webviewHost.takingLonger').replace('{providerName}', providerName)} +

{t('accounts.webviewHost.timeoutHint')}

diff --git a/app/src/components/accounts/__tests__/WebviewHost.test.tsx b/app/src/components/accounts/__tests__/WebviewHost.test.tsx index ba539890b..6ca0896dd 100644 --- a/app/src/components/accounts/__tests__/WebviewHost.test.tsx +++ b/app/src/components/accounts/__tests__/WebviewHost.test.tsx @@ -83,6 +83,25 @@ describe('WebviewHost — issue #1233 loading UX', () => { expect(screen.queryByTestId(`webview-loading-${ACCOUNT_ID}`)).not.toBeInTheDocument(); }); + // Issue #3759: the loading/timeout copy must SUBSTITUTE the provider name + // into the `{providerName}` slot, not concatenate it — otherwise the raw + // placeholder token leaks to screen ("Loading {providerName}... Slack..."). + it('substitutes the provider name into the loading copy (issue #3759)', () => { + seedAccount('loading'); + renderHost(); + const placeholder = screen.getByTestId(`webview-placeholder-${ACCOUNT_ID}`); + expect(placeholder).toHaveTextContent('Loading Slack...'); + expect(placeholder.textContent).not.toContain('{providerName}'); + }); + + it('substitutes the provider name into the timeout copy (issue #3759)', () => { + seedAccount('timeout'); + renderHost(); + const timeout = screen.getByTestId(`webview-timeout-${ACCOUNT_ID}`); + expect(timeout).toHaveTextContent('Slack is taking longer than expected.'); + expect(timeout.textContent).not.toContain('{providerName}'); + }); + it('renders the phase hint after 5s of loading and escalates after 10s', () => { seedAccount('loading'); renderHost(); diff --git a/app/src/components/composio/ComposioConnectModal.test.tsx b/app/src/components/composio/ComposioConnectModal.test.tsx index 8b14568e7..27b6eb80b 100644 --- a/app/src/components/composio/ComposioConnectModal.test.tsx +++ b/app/src/components/composio/ComposioConnectModal.test.tsx @@ -716,3 +716,30 @@ describe(' — WhatsApp WABA id parity (#2127)', () => { }); }); }); + +describe('ComposioConnectModal — connection-failed copy (issue #3759)', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('substitutes the status into the failure message when polling sees a FAILED connection', async () => { + // A PENDING connection resumes polling on mount; the first poll observes a + // FAILED connection and must SUBSTITUTE {status} into the error copy + // (issue #3759) — not concatenate/leak the literal placeholder. + vi.mocked(composioApi.listConnections).mockResolvedValue({ + connections: [{ id: 'ca_err', toolkit: 'gmail', status: 'FAILED' }], + }); + + render( + {}} + /> + ); + + expect(await screen.findByText('Connection failed (status: FAILED).')).toBeInTheDocument(); + // Regression guard: the raw placeholder must never reach the screen. + expect(screen.queryByText(/\{status\}/)).not.toBeInTheDocument(); + }); +}); diff --git a/app/src/components/composio/ComposioConnectModal.tsx b/app/src/components/composio/ComposioConnectModal.tsx index 10cc312ad..b11a47f72 100644 --- a/app/src/components/composio/ComposioConnectModal.tsx +++ b/app/src/components/composio/ComposioConnectModal.tsx @@ -298,7 +298,9 @@ export default function ComposioConnectModal({ if (state === 'error') { stopPolling(); setPhase('error'); - setError(`${t('composio.connect.connectionFailed')} (status: ${hit.status}).`); + setError( + t('composio.connect.connectionFailed').replace('{status}', String(hit.status)) + ); return; } if (state === 'expired') { @@ -539,7 +541,7 @@ export default function ComposioConnectModal({ } catch (err) { const msg = err instanceof Error ? err.message : String(err); setPhase('error'); - setError(`${t('composio.connect.disconnectFailed')}: ${msg}`); + setError(t('composio.connect.disconnectFailed').replace('{msg}', msg)); setClearMemoryOnDisconnect(false); } }, diff --git a/app/src/components/intelligence/IntelligenceTasksTab.tsx b/app/src/components/intelligence/IntelligenceTasksTab.tsx index 6d780d33b..a4d7fb466 100644 --- a/app/src/components/intelligence/IntelligenceTasksTab.tsx +++ b/app/src/components/intelligence/IntelligenceTasksTab.tsx @@ -527,7 +527,7 @@ export default function IntelligenceTasksTab() { const title = thread?.title && thread.title.trim().length > 0 ? thread.title - : `${t('intelligence.tasks.threadPrefix')} ${shortId(threadId)}`; + : t('intelligence.tasks.threadPrefix').replace('{thread}', shortId(threadId)); boardEntries.push({ threadId, title, board, live: Boolean(liveBoard) }); } diff --git a/app/src/lib/i18n/en.ts b/app/src/lib/i18n/en.ts index 21bd884dc..2199e7c21 100644 --- a/app/src/lib/i18n/en.ts +++ b/app/src/lib/i18n/en.ts @@ -3375,7 +3375,7 @@ const en: TranslationMap = { 'composio.connect.dynamicsOrgNameHint': 'For example, "myorg" for myorg.crm.dynamics.com. Enter the short org name only, not the full URL.', 'composio.connect.dynamicsOrgNameLabel': 'Dynamics 365 Organization Name', - 'composio.connect.connectionFailed': 'Connection failed (status: ${hit.status}).', + 'composio.connect.connectionFailed': 'Connection failed (status: {status}).', 'composio.connect.disconnectFailed': 'Disconnect failed: {msg}', 'composio.connect.disconnecting': 'Disconnecting…', 'composio.connect.idleDescription': 'Connect your', @@ -3594,7 +3594,7 @@ const en: TranslationMap = { 'intelligence.tasks.failedToLoad': 'Failed to load', 'intelligence.tasks.live': 'live', 'intelligence.tasks.loadingBoards': 'Loading task boards…', - 'intelligence.tasks.threadPrefix': 'Thread {id}', + 'intelligence.tasks.threadPrefix': 'Thread {thread}', 'intelligence.tasks.subtitle': 'Your tasks and agent task boards across the workspace.', 'intelligence.tasks.newTask': 'New task', 'intelligence.tasks.personalBoardTitle': 'Agent Tasks', diff --git a/app/src/lib/i18n/zh-CN.ts b/app/src/lib/i18n/zh-CN.ts index 4fee4a1aa..d2d0dca7b 100644 --- a/app/src/lib/i18n/zh-CN.ts +++ b/app/src/lib/i18n/zh-CN.ts @@ -2709,7 +2709,7 @@ const messages: TranslationMap = { 'composio.connect.dynamicsOrgNameHint': '例如,myorg.crm.dynamics.com 的组织名称为 "myorg"。仅输入简短的组织名称,而不是完整 URL。', 'composio.connect.dynamicsOrgNameLabel': 'Dynamics 365 组织名称', - 'composio.connect.connectionFailed': '连接失败。', + 'composio.connect.connectionFailed': '连接失败(状态:{status})。', 'composio.connect.disconnectFailed': '断开连接失败:{msg}', 'composio.connect.disconnecting': '断开中…', 'composio.connect.idleDescription': '连接你的', @@ -2899,7 +2899,7 @@ const messages: TranslationMap = { 'intelligence.tasks.failedToLoad': '加载失败', 'intelligence.tasks.live': '实时', 'intelligence.tasks.loadingBoards': '正在加载任务看板…', - 'intelligence.tasks.threadPrefix': '对话', + 'intelligence.tasks.threadPrefix': '对话 {thread}', 'intelligence.tasks.subtitle': '整个工作区中的你的任务和智能体任务看板。', 'intelligence.tasks.newTask': '新建任务', 'intelligence.tasks.personalBoardTitle': '智能体任务',