fmt(js): npm run fix on merge (#107545)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
96fbc47f14
commit
6f8b8e77dd
@@ -7,7 +7,9 @@ import { stubResizeObserver } from '@/test/jsdom'
|
||||
|
||||
// Every vault RPC is routed to the OWNER profile's socket; the mock records which profile each
|
||||
// call targeted so the tests can prove a draft never crosses owners.
|
||||
const { calls } = vi.hoisted(() => ({ calls: [] as { method: string; params: Record<string, unknown>; profile: string }[] }))
|
||||
const { calls } = vi.hoisted(() => ({
|
||||
calls: [] as { method: string; params: Record<string, unknown>; profile: string }[]
|
||||
}))
|
||||
let respond: (profile: string, method: string) => Promise<unknown> = async () => ({})
|
||||
|
||||
vi.mock('@/store/gateway', async importActual => ({
|
||||
@@ -59,7 +61,8 @@ beforeEach(() => {
|
||||
queryClient.clear()
|
||||
$activeGatewayProfile.set('default')
|
||||
$gatewayState.set('open')
|
||||
respond = async (_profile, method) => (method === 'vault.sources' ? { sources } : method === 'vault.list' ? { items: [] } : { ok: true })
|
||||
respond = async (_profile, method) =>
|
||||
method === 'vault.sources' ? { sources } : method === 'vault.list' ? { items: [] } : { ok: true }
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -80,7 +83,7 @@ it('a master-password draft is wiped on a profile switch and never submitted to
|
||||
await waitFor(() => expect(calls.some(c => c.profile === 'other-profile' && c.method === 'vault.list')).toBe(true))
|
||||
})
|
||||
|
||||
it("a late list response from profile A never paints under profile B", async () => {
|
||||
it('a late list response from profile A never paints under profile B', async () => {
|
||||
let resolveA!: (value: unknown) => void
|
||||
const held = new Promise(r => (resolveA = r))
|
||||
|
||||
@@ -103,25 +106,51 @@ it("a late list response from profile A never paints under profile B", async ()
|
||||
await waitFor(() => expect(calls.some(c => c.profile === 'other-profile' && c.method === 'vault.list')).toBe(true))
|
||||
|
||||
await act(async () => {
|
||||
resolveA({ items: [{ id: 'a', kind: 'login', label: 'A-only private account', origin: 'https://a.example', identifier: 'a@example.com', created_at: '' }] })
|
||||
resolveA({
|
||||
items: [
|
||||
{
|
||||
id: 'a',
|
||||
kind: 'login',
|
||||
label: 'A-only private account',
|
||||
origin: 'https://a.example',
|
||||
identifier: 'a@example.com',
|
||||
created_at: ''
|
||||
}
|
||||
]
|
||||
})
|
||||
await held
|
||||
})
|
||||
expect(screen.queryByText('A-only private account')).toBeNull()
|
||||
})
|
||||
|
||||
it('vault.add secrets never enter the mutation cache', async () => {
|
||||
respond = async (_profile, method) => (method === 'vault.sources' ? { sources } : method === 'vault.list' ? { items: [] } : { id: 'created' })
|
||||
respond = async (_profile, method) =>
|
||||
method === 'vault.sources' ? { sources } : method === 'vault.list' ? { items: [] } : { id: 'created' }
|
||||
const view = mount()
|
||||
fireEvent.click(await screen.findByRole('button', { name: 'Add' }))
|
||||
|
||||
for (const [label, value] of [['Label', 'fixture'], ['Site origin', 'https://example.com'], ['Identifier', 'fixture@example.com'], ['Password', 'fixture-retained-password']] as const) {
|
||||
for (const [label, value] of [
|
||||
['Label', 'fixture'],
|
||||
['Site origin', 'https://example.com'],
|
||||
['Identifier', 'fixture@example.com'],
|
||||
['Password', 'fixture-retained-password']
|
||||
] as const) {
|
||||
fireEvent.change(screen.getByLabelText(label), { target: { value } })
|
||||
}
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Save' }))
|
||||
await waitFor(() => expect(calls.some(c => c.method === 'vault.add')).toBe(true))
|
||||
expect((calls.find(c => c.method === 'vault.add')!.params.secret as Record<string, string>).password).toBe('fixture-retained-password')
|
||||
expect((calls.find(c => c.method === 'vault.add')!.params.secret as Record<string, string>).password).toBe(
|
||||
'fixture-retained-password'
|
||||
)
|
||||
await waitFor(() => expect(screen.queryByLabelText('Password')).toBeNull())
|
||||
view.unmount()
|
||||
expect(JSON.stringify(queryClient.getMutationCache().getAll().map(m => m.state.variables))).not.toContain('fixture-retained-password')
|
||||
expect(
|
||||
JSON.stringify(
|
||||
queryClient
|
||||
.getMutationCache()
|
||||
.getAll()
|
||||
.map(m => m.state.variables)
|
||||
)
|
||||
).not.toContain('fixture-retained-password')
|
||||
})
|
||||
|
||||
@@ -143,8 +143,22 @@ describe('VaultSettings', () => {
|
||||
|
||||
it('unlocks a password manager from Settings; the master password leaves only via vault.unlock', async () => {
|
||||
const sources = [
|
||||
{ name: 'onepassword', display_name: '1Password', enabled: true, needs_unlock: true, unlocked: false, installed: true },
|
||||
{ name: 'bitwarden', display_name: 'Bitwarden', enabled: false, needs_unlock: true, unlocked: false, installed: false }
|
||||
{
|
||||
name: 'onepassword',
|
||||
display_name: '1Password',
|
||||
enabled: true,
|
||||
needs_unlock: true,
|
||||
unlocked: false,
|
||||
installed: true
|
||||
},
|
||||
{
|
||||
name: 'bitwarden',
|
||||
display_name: 'Bitwarden',
|
||||
enabled: false,
|
||||
needs_unlock: true,
|
||||
unlocked: false,
|
||||
installed: false
|
||||
}
|
||||
]
|
||||
|
||||
requestGateway.mockImplementation(async (method: string) => {
|
||||
@@ -176,7 +190,9 @@ describe('VaultSettings', () => {
|
||||
await waitFor(() => expect(screen.getByText('Unlock 1Password')).toBeTruthy())
|
||||
|
||||
fireEvent.change(screen.getByPlaceholderText('Master password'), { target: { value: 'correct horse' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Unlock' }).closest('form')!.querySelector('button[type=submit]')!)
|
||||
fireEvent.click(
|
||||
screen.getByRole('button', { name: 'Unlock' }).closest('form')!.querySelector('button[type=submit]')!
|
||||
)
|
||||
|
||||
await waitFor(() =>
|
||||
expect(requestGateway).toHaveBeenCalledWith('vault.unlock', { name: 'onepassword', password: 'correct horse' })
|
||||
|
||||
@@ -182,7 +182,6 @@ export function VaultSettings() {
|
||||
const pendingMasterPassword = useRef('')
|
||||
const pendingSecret = useRef<null | Record<string, string>>(null)
|
||||
|
||||
|
||||
const { data: sourcesData } = useQuery({
|
||||
enabled: gatewayState === 'open',
|
||||
queryKey: VAULT_SOURCES_QUERY_KEY,
|
||||
@@ -298,10 +297,7 @@ export function VaultSettings() {
|
||||
setSearchParams(next, { replace: true })
|
||||
}, [openAdd, searchParams, setSearchParams])
|
||||
|
||||
const invalidate = useCallback(
|
||||
() => queryClient.invalidateQueries({ queryKey: VAULT_QUERY_KEY }),
|
||||
[queryClient]
|
||||
)
|
||||
const invalidate = useCallback(() => queryClient.invalidateQueries({ queryKey: VAULT_QUERY_KEY }), [queryClient])
|
||||
|
||||
const addMutation = useMutation({
|
||||
mutationFn: async (payload: { kind: VaultKind; label: string; origin?: string }) => {
|
||||
@@ -419,11 +415,17 @@ export function VaultSettings() {
|
||||
{item.identifier && <span className="truncate">{v.identifierShown(item.identifier)}</span>}
|
||||
{item.origin && item.origin.replace(/^https?:\/\//, '') !== item.label && (
|
||||
<>
|
||||
{item.identifier && <span aria-hidden className="text-(--ui-text-tertiary)">·</span>}
|
||||
{item.identifier && (
|
||||
<span aria-hidden className="text-(--ui-text-tertiary)">
|
||||
·
|
||||
</span>
|
||||
)}
|
||||
<span className="truncate">{item.origin}</span>
|
||||
</>
|
||||
)}
|
||||
<span aria-hidden className="text-(--ui-text-tertiary)">·</span>
|
||||
<span aria-hidden className="text-(--ui-text-tertiary)">
|
||||
·
|
||||
</span>
|
||||
<span>{v.createdOn(formatCreated(item.created_at))}</span>
|
||||
</span>
|
||||
}
|
||||
@@ -462,7 +464,13 @@ export function VaultSettings() {
|
||||
{v.sources.lock}
|
||||
</Button>
|
||||
) : (
|
||||
<Button className="gap-1.5" onClick={() => setUnlockTarget(source)} size="sm" type="button" variant="outline">
|
||||
<Button
|
||||
className="gap-1.5"
|
||||
onClick={() => setUnlockTarget(source)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<KeyRound className="size-3.5" />
|
||||
{v.sources.unlock}
|
||||
</Button>
|
||||
@@ -565,10 +573,7 @@ export function VaultSettings() {
|
||||
>
|
||||
<div className="grid items-start gap-4 sm:grid-cols-2">
|
||||
<Field htmlFor="vault-kind" label={v.kindField}>
|
||||
<Select
|
||||
onValueChange={value => setForm(f => ({ ...f, kind: value as VaultKind }))}
|
||||
value={form.kind}
|
||||
>
|
||||
<Select onValueChange={value => setForm(f => ({ ...f, kind: value as VaultKind }))} value={form.kind}>
|
||||
<SelectTrigger className={CONTROL_TEXT} id="vault-kind">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -38,7 +38,12 @@ it('sends identifier + password as one vault.save_login.respond to the owning pr
|
||||
const ambient = vi.fn().mockResolvedValue({ status: 'ok' })
|
||||
$activeSessionId.set('session-b')
|
||||
$gateway.set({ request: ambient } as never)
|
||||
setVaultSaveLoginRequest({ origin: 'https://github.com', requestId: 'req-s', sessionId: 'session-a', site: 'github.com' })
|
||||
setVaultSaveLoginRequest({
|
||||
origin: 'https://github.com',
|
||||
requestId: 'req-s',
|
||||
sessionId: 'session-a',
|
||||
site: 'github.com'
|
||||
})
|
||||
|
||||
render(<PromptOverlays sessionId="session-a" />)
|
||||
expect(document.body.textContent).toContain('Save your github.com login?')
|
||||
@@ -55,7 +60,10 @@ it('sends identifier + password as one vault.save_login.respond to the owning pr
|
||||
await waitFor(() => expect(gatewayMocks.requestGatewayForAgent).toHaveBeenCalledTimes(1))
|
||||
const [conn, profile, method, params] = gatewayMocks.requestGatewayForAgent.mock.calls[0] as unknown[]
|
||||
expect([conn, profile, method]).toEqual(['conn-1', 'owner', 'vault.save_login.respond'])
|
||||
expect(JSON.parse((params as { login: string }).login)).toEqual({ identifier: 'tek@acme.test', password: 'fixture-pw' })
|
||||
expect(JSON.parse((params as { login: string }).login)).toEqual({
|
||||
identifier: 'tek@acme.test',
|
||||
password: 'fixture-pw'
|
||||
})
|
||||
expect(ambient).not.toHaveBeenCalled()
|
||||
await waitFor(() => expect(sessionVaultSaveLoginRequest('session-a').get()).toBeNull())
|
||||
})
|
||||
@@ -64,13 +72,21 @@ it("Don't save answers an empty login and clears the card", async () => {
|
||||
$profiles.set([{ name: 'owner' }] as never)
|
||||
setSessionOwnerHint('session-a', { connectionId: 'conn-1', profile: 'owner' })
|
||||
$gateway.set({ request: vi.fn() } as never)
|
||||
setVaultSaveLoginRequest({ origin: 'https://github.com', requestId: 'req-d', sessionId: 'session-a', site: 'github.com' })
|
||||
setVaultSaveLoginRequest({
|
||||
origin: 'https://github.com',
|
||||
requestId: 'req-d',
|
||||
sessionId: 'session-a',
|
||||
site: 'github.com'
|
||||
})
|
||||
|
||||
render(<PromptOverlays sessionId="session-a" />)
|
||||
const decline = Array.from(document.querySelectorAll('button')).find(b => b.textContent === "Don't save")!
|
||||
fireEvent.click(decline)
|
||||
|
||||
await waitFor(() => expect(gatewayMocks.requestGatewayForAgent).toHaveBeenCalledTimes(1))
|
||||
expect((gatewayMocks.requestGatewayForAgent.mock.calls[0] as unknown[])[3]).toEqual({ login: '', request_id: 'req-d' })
|
||||
expect((gatewayMocks.requestGatewayForAgent.mock.calls[0] as unknown[])[3]).toEqual({
|
||||
login: '',
|
||||
request_id: 'req-d'
|
||||
})
|
||||
await waitFor(() => expect(sessionVaultSaveLoginRequest('session-a').get()).toBeNull())
|
||||
})
|
||||
|
||||
@@ -47,7 +47,11 @@ it('routes the master password to the owning profile socket, never the ambient g
|
||||
fireEvent.submit(input.closest('form')!)
|
||||
|
||||
await waitFor(() => expect(gatewayMocks.requestGatewayForAgent).toHaveBeenCalledTimes(1))
|
||||
expect(gatewayMocks.requestGatewayForAgent.mock.calls[0].slice(0, 3)).toEqual(['conn-1', 'owner', 'vault.unlock.respond'])
|
||||
expect(gatewayMocks.requestGatewayForAgent.mock.calls[0].slice(0, 3)).toEqual([
|
||||
'conn-1',
|
||||
'owner',
|
||||
'vault.unlock.respond'
|
||||
])
|
||||
expect(ambient).not.toHaveBeenCalled()
|
||||
await waitFor(() => expect(sessionVaultUnlockRequest('session-a').get()).toBeNull())
|
||||
})
|
||||
|
||||
@@ -518,7 +518,7 @@ export const en: Translations = {
|
||||
loadFailed: 'Could not load vault items',
|
||||
empty: 'Nothing saved yet',
|
||||
emptyDesc:
|
||||
'You don\'t have to add anything here. Ask the agent to sign into a site and it will ask you for the login once, on the spot. Use Add if you prefer to enter one ahead of time.',
|
||||
"You don't have to add anything here. Ask the agent to sign into a site and it will ask you for the login once, on the spot. Use Add if you prefer to enter one ahead of time.",
|
||||
add: 'Add',
|
||||
addTitle: 'Add a login, card or address',
|
||||
addDescription: 'Stored encrypted on this machine. The agent never sees the password.',
|
||||
@@ -562,7 +562,8 @@ export const en: Translations = {
|
||||
blurb:
|
||||
'Installed password managers are picked up automatically. The agent asks you to unlock one the first time it needs a login from it (once per session); only a session token stays in memory, and the agent never sees your master password or any login.',
|
||||
toggleFailed: 'Could not update password manager',
|
||||
notInstalled: name => `Not detected. Install the ${name} command-line tool and sign in to it; Hermes picks it up automatically.`,
|
||||
notInstalled: name =>
|
||||
`Not detected. Install the ${name} command-line tool and sign in to it; Hermes picks it up automatically.`,
|
||||
disabledDesc: 'Detected but turned off for Hermes.',
|
||||
lockedDesc: 'Detected. The agent will ask you to unlock it when it needs a login, or unlock now.',
|
||||
unlockedDesc: 'Unlocked for this session. Locks automatically after 30 minutes idle or when Hermes closes.',
|
||||
|
||||
@@ -383,9 +383,11 @@ export const ja = defineLocale({
|
||||
blurb:
|
||||
'インストール済みのパスワードマネージャーは自動的に検出されます。エージェントがそこからログイン情報を初めて必要とするときにロック解除を求めます(セッションごとに一度)。メモリに残るのはセッショントークンのみで、エージェントはマスターパスワードやログイン情報を一切見ません。',
|
||||
toggleFailed: 'パスワードマネージャーの設定を更新できませんでした',
|
||||
notInstalled: name => `未検出です。${name} のコマンドラインツールをインストールしてサインインすると、Hermes が自動的に検出します。`,
|
||||
notInstalled: name =>
|
||||
`未検出です。${name} のコマンドラインツールをインストールしてサインインすると、Hermes が自動的に検出します。`,
|
||||
disabledDesc: '検出済みですが、Hermes では無効になっています。',
|
||||
lockedDesc: '検出済み。エージェントがログイン情報を必要とするときにロック解除を求めます。今すぐ解除することもできます。',
|
||||
lockedDesc:
|
||||
'検出済み。エージェントがログイン情報を必要とするときにロック解除を求めます。今すぐ解除することもできます。',
|
||||
unlockedDesc: 'このセッションでロック解除済み。30分間操作がないか Hermes を閉じると自動的にロックされます。',
|
||||
statusLocked: 'ロック中',
|
||||
statusNotDetected: '未検出',
|
||||
|
||||
@@ -324,7 +324,8 @@ export const zhHant = defineLocale({
|
||||
},
|
||||
vault: {
|
||||
title: '密碼與登入',
|
||||
blurb: '說一句「登入 GitHub」,代理就會代你登入。第一次遇到登入頁時它會當場向你索取登入資訊,之後就自動完成。密碼在本機加密儲存並直接填入頁面——模型永遠看不到。',
|
||||
blurb:
|
||||
'說一句「登入 GitHub」,代理就會代你登入。第一次遇到登入頁時它會當場向你索取登入資訊,之後就自動完成。密碼在本機加密儲存並直接填入頁面——模型永遠看不到。',
|
||||
count: n => `已儲存 ${n} 項`,
|
||||
loadFailed: '無法載入保險庫項目',
|
||||
empty: '尚未儲存任何內容',
|
||||
|
||||
@@ -430,7 +430,8 @@ export const zh: Translations = {
|
||||
},
|
||||
vault: {
|
||||
title: '密码与登录',
|
||||
blurb: '说一句“登录 GitHub”,智能体就会代你登录。第一次遇到登录页时它会当场向你要登录信息,之后就自动完成。密码在本机加密保存并直接填入页面——模型永远看不到。',
|
||||
blurb:
|
||||
'说一句“登录 GitHub”,智能体就会代你登录。第一次遇到登录页时它会当场向你要登录信息,之后就自动完成。密码在本机加密保存并直接填入页面——模型永远看不到。',
|
||||
count: n => `已保存 ${n} 项`,
|
||||
loadFailed: '无法加载保险库条目',
|
||||
empty: '尚未保存任何内容',
|
||||
|
||||
@@ -270,10 +270,10 @@ export const hasBlockingPromptRequest = (sessionId: string | null | undefined):
|
||||
|
||||
return Boolean(
|
||||
approval.$all.get()[key] ||
|
||||
sudo.$all.get()[key] ||
|
||||
secret.$all.get()[key] ||
|
||||
vaultUnlock.$all.get()[key] ||
|
||||
vaultSave.$all.get()[key]
|
||||
sudo.$all.get()[key] ||
|
||||
secret.$all.get()[key] ||
|
||||
vaultUnlock.$all.get()[key] ||
|
||||
vaultSave.$all.get()[key]
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -646,7 +646,12 @@ export function useMainApp(gw: GatewayClient) {
|
||||
// Format: `<marker> <session name> · <model> · <cwd>` — name/cwd omitted when absent.
|
||||
const model = ui.info?.model?.replace(/^.*\//, '') ?? ''
|
||||
|
||||
const marker = overlay.approval || overlay.sudo || overlay.secret || overlay.vaultUnlock || overlay.clarify ? '⚠' : ui.busy ? '⏳' : '✓'
|
||||
const marker =
|
||||
overlay.approval || overlay.sudo || overlay.secret || overlay.vaultUnlock || overlay.clarify
|
||||
? '⚠'
|
||||
: ui.busy
|
||||
? '⏳'
|
||||
: '✓'
|
||||
|
||||
const tabCwd = ui.info?.cwd
|
||||
|
||||
|
||||
@@ -767,8 +767,16 @@ export type GatewayEvent =
|
||||
}
|
||||
| { payload: { request_id: string }; session_id?: string; type: 'sudo.request' }
|
||||
| { payload: { env_var: string; prompt: string; request_id: string }; session_id?: string; type: 'secret.request' }
|
||||
| { payload: { request_id: string }; session_id?: string; type: 'secret.expire' | 'sudo.expire' | 'vault.unlock.expire' }
|
||||
| { payload: { backend: string; display_name: string; request_id: string }; session_id?: string; type: 'vault.unlock.request' }
|
||||
| {
|
||||
payload: { request_id: string }
|
||||
session_id?: string
|
||||
type: 'secret.expire' | 'sudo.expire' | 'vault.unlock.expire'
|
||||
}
|
||||
| {
|
||||
payload: { backend: string; display_name: string; request_id: string }
|
||||
session_id?: string
|
||||
type: 'vault.unlock.request'
|
||||
}
|
||||
| { payload: { task_id: string; text: string }; session_id?: string; type: 'background.complete' }
|
||||
| { payload: { question?: string; task_id: string; text: string }; session_id?: string; type: 'btw.complete' }
|
||||
| { payload?: { text?: string }; session_id?: string; type: 'review.summary' }
|
||||
|
||||
Reference in New Issue
Block a user