test(web): cover user-only skill invocation end to end

The policy scenario now expects the user-only quadrant in the menu with
its marker (riding the description — the hint field is claim-state ghost
text, which the menu never renders), and a new skill-user-invoke scenario
drives /name args through the composer against the real host: the claim
lands skill.invoke, the transcript shows the dedicated card with the
collapsed <skill_content> body, and a paced replay answers the injected
turn deterministically.
This commit is contained in:
Yichen Jiang
2026-08-08 01:27:25 +08:00
parent 011e3e4e63
commit 0fb474f672
6 changed files with 189 additions and 8 deletions

View File

@@ -157,8 +157,9 @@ export function apply(ctx: ClientContext): void {
.filter(skill => skill.name.startsWith(query))
.map(skill => ({
name: skill.name,
description: skill.description,
...skill.modelInvocable ? {} : { hint: userOnlyHint() },
// The user-only marker rides the description (the menu's only
// secondary text); `hint` is the claim-state ghost text, not a badge.
description: skill.modelInvocable ? skill.description : `${userOnlyHint()} · ${skill.description}`,
}))
},
warm(session) {

View File

@@ -388,7 +388,7 @@ describe('adjudication', () => {
})
describe('user-only marking', () => {
it('carries the user-only hint on candidates the model cannot invoke', async () => {
it('prefixes the description of candidates the model cannot invoke', async () => {
const rows: SkillRow[] = [
{ name: 'shared-skill', description: 'both surfaces', modelInvocable: true },
{ name: 'user-only-skill', description: 'user surface only', modelInvocable: false },
@@ -397,7 +397,7 @@ describe('user-only marking', () => {
const candidates = await source.candidates(proj('s1'), req(''))
expect(candidates).toEqual([
{ name: 'shared-skill', description: 'both surfaces' },
{ name: 'user-only-skill', description: 'user surface only', hint: '仅用户' },
{ name: 'user-only-skill', description: '仅用户 · user surface only' },
])
})
})