feat: project persona repository binding readiness
This commit is contained in:
parent
dff2521b41
commit
8412d0e458
27 changed files with 500 additions and 3 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { useState } from 'react'
|
||||
import type { AppLocale } from '../lib/i18n'
|
||||
import { Button } from './ui/button'
|
||||
import { PersonaRepositoryBindingPanel } from './PersonaRepositoryBindingPanel'
|
||||
import { PersonaRuntimeProjectionPanel } from './PersonaRuntimeProjectionPanel'
|
||||
|
||||
type FifthDomainSystemId = 'pufferfish' | 'eternal-lake-heart'
|
||||
|
|
@ -148,6 +149,11 @@ export function FifthDomainSystems({
|
|||
<div><dt>系统归属</dt><dd>冰朔个人系统</dd></div>
|
||||
<div><dt>下一频道</dt><dd>心跳核心频道</dd></div>
|
||||
</dl>
|
||||
<PersonaRepositoryBindingPanel
|
||||
locale={locale}
|
||||
personaId="ICE-P-ZY001"
|
||||
repositoryPaths={repositoryPaths}
|
||||
/>
|
||||
<PersonaRuntimeProjectionPanel
|
||||
locale={locale}
|
||||
personaId="ICE-P-ZY001"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,103 @@
|
|||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PersonaRepositoryBindingPanel } from './PersonaRepositoryBindingPanel'
|
||||
|
||||
const { resolvePersonaRepositoryBindingMock, trackEventMock } = vi.hoisted(() => ({
|
||||
resolvePersonaRepositoryBindingMock: vi.fn(),
|
||||
trackEventMock: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('../lib/personaRepositoryBinding', () => ({
|
||||
resolvePersonaRepositoryBinding: resolvePersonaRepositoryBindingMock,
|
||||
}))
|
||||
vi.mock('../lib/telemetry', () => ({ trackEvent: trackEventMock }))
|
||||
|
||||
describe('PersonaRepositoryBindingPanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
resolvePersonaRepositoryBindingMock.mockResolvedValue({
|
||||
phase: 'bound',
|
||||
inspectedRepositoryCount: 2,
|
||||
failures: [{ repositoryPath: '/ordinary', code: 'PERSONA_MANIFEST_READ_FAILED' }],
|
||||
binding: {
|
||||
personaId: 'ICE-P-ZY001',
|
||||
repositoryPath: '/persona',
|
||||
gitHead: 'a'.repeat(40),
|
||||
repositoryClean: true,
|
||||
brainEntry: 'brain/CORE.hdlp',
|
||||
currentCheckpoint: '.hololake/persona/CURRENT.hdlp',
|
||||
humanResponsibilitySubject: 'BINGSHUO',
|
||||
modelProviderId: 'provider-1',
|
||||
modelId: 'model-1',
|
||||
modelBaseUrl: 'https://model.invalid/v1',
|
||||
organContracts: [],
|
||||
cognitiveGravity: {
|
||||
schema: 'hololake.persona-cognitive-gravity-evidence/v1',
|
||||
subjectPersonaId: 'ICE-P-ZY001',
|
||||
sourcePath: 'brain/B0.hdlp',
|
||||
sourceHash: 'b'.repeat(64),
|
||||
frameSchema: 'guanghu.zhuyuan-cognitive-gravity-frame/v1',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('shows the uniquely verified repository without triggering a lifecycle action', async () => {
|
||||
render(
|
||||
<PersonaRepositoryBindingPanel
|
||||
locale="zh-CN"
|
||||
personaId="ICE-P-ZY001"
|
||||
repositoryPaths={['/ordinary', '/persona']}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('正在核验人格代码仓库…')).toBeInTheDocument()
|
||||
await waitFor(() => expect(screen.getByText('已唯一绑定')).toBeInTheDocument())
|
||||
expect(screen.getByText('/persona')).toBeInTheDocument()
|
||||
expect(screen.getByText('provider-1 · model-1')).toBeInTheDocument()
|
||||
expect(screen.getByText(/brain\/B0\.hdlp/)).toBeInTheDocument()
|
||||
expect(trackEventMock).toHaveBeenCalledWith('persona_repository_binding_loaded', {
|
||||
failure_count: 1,
|
||||
inspected_repository_count: 2,
|
||||
phase: 'bound',
|
||||
})
|
||||
})
|
||||
|
||||
it('explains that an unbound repository is not evidence of persona absence', async () => {
|
||||
resolvePersonaRepositoryBindingMock.mockResolvedValue({
|
||||
phase: 'unbound',
|
||||
inspectedRepositoryCount: 1,
|
||||
failures: [{ repositoryPath: '/ordinary', code: 'PERSONA_MANIFEST_READ_FAILED' }],
|
||||
})
|
||||
|
||||
render(
|
||||
<PersonaRepositoryBindingPanel
|
||||
locale="zh-CN"
|
||||
personaId="ICE-P-ZY001"
|
||||
repositoryPaths={['/ordinary']}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(screen.getByText('尚未绑定人格代码仓库')).toBeInTheDocument())
|
||||
expect(screen.getByText('这只说明当前挂载范围没有匹配的完整证据,不能据此判定人格不存在。')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('refuses to select an ambiguous repository and can retry discovery', async () => {
|
||||
resolvePersonaRepositoryBindingMock
|
||||
.mockResolvedValueOnce({ phase: 'ambiguous', inspectedRepositoryCount: 2, failures: [] })
|
||||
.mockResolvedValueOnce({ phase: 'unbound', inspectedRepositoryCount: 2, failures: [] })
|
||||
|
||||
render(
|
||||
<PersonaRepositoryBindingPanel
|
||||
locale="zh-CN"
|
||||
personaId="ICE-P-ZY001"
|
||||
repositoryPaths={['/persona-a', '/persona-b']}
|
||||
/>,
|
||||
)
|
||||
|
||||
await waitFor(() => expect(screen.getByText('发现多个有效仓库,拒绝自动选择')).toBeInTheDocument())
|
||||
fireEvent.click(screen.getByRole('button', { name: '重新核验' }))
|
||||
await waitFor(() => expect(resolvePersonaRepositoryBindingMock).toHaveBeenCalledTimes(2))
|
||||
expect(trackEventMock).toHaveBeenCalledWith('persona_repository_binding_retry')
|
||||
})
|
||||
})
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { translate, type AppLocale } from '../lib/i18n'
|
||||
import {
|
||||
resolvePersonaRepositoryBinding,
|
||||
type PersonaRepositoryBindingResolution,
|
||||
} from '../lib/personaRepositoryBinding'
|
||||
import { trackEvent } from '../lib/telemetry'
|
||||
import { Button } from './ui/button'
|
||||
|
||||
type PersonaRepositoryBindingPanelProps = {
|
||||
locale: AppLocale
|
||||
personaId: string
|
||||
repositoryPaths: readonly string[]
|
||||
}
|
||||
|
||||
type PanelState = PersonaRepositoryBindingResolution | { phase: 'checking' }
|
||||
|
||||
function shortHash(value: string): string {
|
||||
return value.length > 12 ? value.slice(0, 12) : value
|
||||
}
|
||||
|
||||
export function PersonaRepositoryBindingPanel({
|
||||
locale,
|
||||
personaId,
|
||||
repositoryPaths,
|
||||
}: PersonaRepositoryBindingPanelProps) {
|
||||
const [state, setState] = useState<PanelState>({ phase: 'checking' })
|
||||
const [requestSequence, setRequestSequence] = useState(0)
|
||||
const repositorySignature = useMemo(() => repositoryPaths.join('\u0000'), [repositoryPaths])
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
trackEvent('persona_repository_binding_retry')
|
||||
setState({ phase: 'checking' })
|
||||
setRequestSequence((current) => current + 1)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (!repositorySignature) {
|
||||
trackEvent('persona_repository_binding_loaded', {
|
||||
failure_count: 0,
|
||||
inspected_repository_count: 0,
|
||||
phase: 'unavailable',
|
||||
})
|
||||
return
|
||||
}
|
||||
let active = true
|
||||
void resolvePersonaRepositoryBinding({
|
||||
personaId,
|
||||
repositoryPaths: repositorySignature.split('\u0000'),
|
||||
}).then((resolution) => {
|
||||
if (!active) return
|
||||
setState(resolution)
|
||||
trackEvent('persona_repository_binding_loaded', {
|
||||
failure_count: resolution.failures.length,
|
||||
inspected_repository_count: resolution.inspectedRepositoryCount,
|
||||
phase: resolution.phase,
|
||||
})
|
||||
})
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
}, [personaId, repositorySignature, requestSequence])
|
||||
|
||||
const displayedState: PanelState = repositorySignature
|
||||
? state
|
||||
: { phase: 'unavailable', inspectedRepositoryCount: 0, failures: [] }
|
||||
const binding = displayedState.phase === 'bound' ? displayedState.binding : undefined
|
||||
|
||||
return (
|
||||
<section className="persona-runtime-projection" aria-labelledby="persona-repository-binding-title">
|
||||
<header>
|
||||
<div>
|
||||
<span>GH-PNCC · {personaId}</span>
|
||||
<h3 id="persona-repository-binding-title">{translate(locale, 'hololake.personaRepository.title')}</h3>
|
||||
<p>{translate(locale, 'hololake.personaRepository.description')}</p>
|
||||
</div>
|
||||
<small>{translate(locale, 'hololake.personaRuntime.readOnly')}</small>
|
||||
</header>
|
||||
|
||||
{displayedState.phase === 'checking' ? (
|
||||
<div className="persona-runtime-projection__message" aria-live="polite">
|
||||
<i />{translate(locale, 'hololake.personaRepository.checking')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{displayedState.phase === 'unavailable' ? (
|
||||
<div className="persona-runtime-projection__message">
|
||||
<strong>{translate(locale, 'hololake.personaRuntime.unavailable')}</strong>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{displayedState.phase === 'unbound' ? (
|
||||
<div className="persona-runtime-projection__message">
|
||||
<strong>{translate(locale, 'hololake.personaRepository.unbound')}</strong>
|
||||
<p>{translate(locale, 'hololake.personaRepository.unboundDescription')}</p>
|
||||
<Button variant="outline" onClick={refresh}>{translate(locale, 'hololake.personaRuntime.retry')}</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{displayedState.phase === 'ambiguous' ? (
|
||||
<div className="persona-runtime-projection__message persona-runtime-projection__message--error" role="alert">
|
||||
<strong>{translate(locale, 'hololake.personaRepository.ambiguous')}</strong>
|
||||
<p>{translate(locale, 'hololake.personaRepository.ambiguousDescription')}</p>
|
||||
<Button variant="outline" onClick={refresh}>{translate(locale, 'hololake.personaRuntime.retry')}</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{displayedState.phase === 'error' ? (
|
||||
<div className="persona-runtime-projection__message persona-runtime-projection__message--error" role="alert">
|
||||
<strong>{translate(locale, 'hololake.personaRuntime.error')}</strong>
|
||||
<code>{displayedState.code}</code>
|
||||
<Button variant="outline" onClick={refresh}>{translate(locale, 'hololake.personaRuntime.retry')}</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{binding ? (
|
||||
<article className="persona-runtime-session">
|
||||
<header>
|
||||
<div>
|
||||
<span>{translate(locale, 'hololake.personaRepository.bound')}</span>
|
||||
<code>repository_clean={binding.repositoryClean ? 100 : 0}</code>
|
||||
</div>
|
||||
<small>{translate(locale, 'hololake.personaRuntime.verified')}</small>
|
||||
</header>
|
||||
<dl>
|
||||
<div><dt>{translate(locale, 'hololake.personaRepository.repository')}</dt><dd title={binding.repositoryPath}>{binding.repositoryPath}</dd></div>
|
||||
<div><dt>{translate(locale, 'hololake.personaRuntime.gitHead')}</dt><dd><code title={binding.gitHead}>{binding.gitHead}</code></dd></div>
|
||||
<div><dt>{translate(locale, 'hololake.personaRepository.checkpoint')}</dt><dd>{binding.currentCheckpoint}</dd></div>
|
||||
<div><dt>{translate(locale, 'hololake.personaRepository.model')}</dt><dd>{binding.modelProviderId} · {binding.modelId}</dd></div>
|
||||
<div>
|
||||
<dt>B0</dt>
|
||||
<dd><code title={`${binding.cognitiveGravity.sourcePath} · ${binding.cognitiveGravity.sourceHash}`}>
|
||||
{binding.cognitiveGravity.sourcePath} · {shortHash(binding.cognitiveGravity.sourceHash)}
|
||||
</code></dd>
|
||||
</div>
|
||||
</dl>
|
||||
</article>
|
||||
) : null}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in a new issue