mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
fix(i18n): substitute {providerName} & sibling placeholder tokens instead of concatenating (#3759) (#3792)
Co-authored-by: Steven Enamakel <enamakel@tinyhumans.ai>
This commit is contained in:
co-authored by
Steven Enamakel
parent
c8d71b7325
commit
7c54fd7c7d
@@ -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'
|
||||
|
||||
@@ -226,7 +226,9 @@ const WebviewHost = ({ accountId, provider }: WebviewHostProps) => {
|
||||
/>
|
||||
<span
|
||||
className={`text-xs font-medium tracking-wide ${isLoading ? '' : 'text-stone-500 dark:text-neutral-400'}`}>
|
||||
{isLoading ? `${t('accounts.webviewHost.loading')} ${providerName}...` : providerName}
|
||||
{isLoading
|
||||
? t('accounts.webviewHost.loading').replace('{providerName}', providerName)
|
||||
: providerName}
|
||||
</span>
|
||||
{isLoading ? (
|
||||
<div
|
||||
@@ -251,7 +253,9 @@ const WebviewHost = ({ accountId, provider }: WebviewHostProps) => {
|
||||
aria-live="polite"
|
||||
aria-label={t('accounts.webviewHost.loadTimeout')}>
|
||||
<div className="max-w-sm space-y-1">
|
||||
<p className="text-sm font-semibold text-stone-800 dark:text-neutral-100">{`${providerName} ${t('accounts.webviewHost.takingLonger')}`}</p>
|
||||
<p className="text-sm font-semibold text-stone-800 dark:text-neutral-100">
|
||||
{t('accounts.webviewHost.takingLonger').replace('{providerName}', providerName)}
|
||||
</p>
|
||||
<p className="text-xs text-stone-600 dark:text-neutral-300">
|
||||
{t('accounts.webviewHost.timeoutHint')}
|
||||
</p>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -716,3 +716,30 @@ describe('<ComposioConnectModal> — 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(
|
||||
<ComposioConnectModal
|
||||
toolkit={mockToolkit}
|
||||
connections={[{ id: 'ca_pending', toolkit: 'gmail', status: 'PENDING' }]}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
);
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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) });
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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': '智能体任务',
|
||||
|
||||
Reference in New Issue
Block a user