mirror of
https://github.com/stablyai/orca.git
synced 2026-11-05 19:47:27 +00:00
test: remove duplicate coverage and strengthen boundary assertions (#33047)
* test: strengthen launch, subject identity, and release witnesses * test: strengthen approval paths and prompt answer boundaries * test: strengthen conversation names and mobile log persistence * refactor: group background task stop contract with stop operations * test: strengthen question, title, and release validation * test: distinguish row text and generated dispatch titles * test: verify mobile revival events and retry exclusivity * test: reject incomplete child row output assertions * test: verify copied modules and option selection contracts * test: verify mobile host state and status retry boundaries * test: observe connector refusals and real diagnostic delivery * test: consolidate platform memory coverage and verify bounded work * test: allow cached legacy failure reads within work bounds * test: verify packaging boundaries with independent fixtures --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
@@ -149,7 +149,8 @@ describe('buildAgentChildRowModels: the display vocabulary', () => {
|
||||
{ displayState: 'unverifiable', detail: { kind: 'no-update' } }
|
||||
]
|
||||
])('%s', (_name, views, expected) => {
|
||||
expect(only(views)).toMatchObject(expected)
|
||||
const row = only(views)
|
||||
expect({ displayState: row.displayState, detail: row.detail }).toEqual(expected)
|
||||
})
|
||||
|
||||
it('suppresses the tool line while monitoring, even when a stale operation arrives', () => {
|
||||
@@ -356,7 +357,12 @@ describe('agentChildRowContextForParent: the reader clock', () => {
|
||||
|
||||
it("measures a mirrored parent on this machine's receipt clock, never the host's", () => {
|
||||
const context = agentChildRowContextForParent(MIRRORED_PARENT, false)
|
||||
expect(context).toMatchObject({ parentObservedAt: 1_190_000, hostClockOffsetMs: -20 * 60_000 })
|
||||
expect(context).toEqual({
|
||||
parentEvidenceFresh: false,
|
||||
transportObservation: 'live',
|
||||
parentObservedAt: 1_190_000,
|
||||
hostClockOffsetMs: -20 * 60_000
|
||||
})
|
||||
const [legacy] = buildLegacyAgentChildRowModels(
|
||||
[{ id: 'a', state: 'working', startedAt: 10 }],
|
||||
context
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('fields the launch fingerprint covers', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('separates two launches that reserved different panes', () => {
|
||||
it('separates two launches that reserved different panes or sessions', () => {
|
||||
// The key is baked into the pane's env; replaying another pane's key strands the new reservation.
|
||||
expect(computeAgentLaunchFingerprint({ ...BASE, paneKey: PANE_KEY })).not.toBe(
|
||||
computeAgentLaunchFingerprint({ ...BASE, paneKey: OTHER_PANE_KEY })
|
||||
@@ -63,6 +63,13 @@ describe('fields the launch fingerprint covers', () => {
|
||||
expect(computeAgentLaunchFingerprint({ ...BASE, paneKey: PANE_KEY })).not.toBe(
|
||||
computeAgentLaunchFingerprint(BASE)
|
||||
)
|
||||
const sessionId = '11111111-1111-4111-8111-111111111111'
|
||||
expect(computeAgentLaunchFingerprint({ ...BASE, sessionId })).not.toBe(
|
||||
computeAgentLaunchFingerprint({ ...BASE, sessionId: '22222222-2222-4222-8222-222222222222' })
|
||||
)
|
||||
expect(computeAgentLaunchFingerprint({ ...BASE, sessionId })).not.toBe(
|
||||
computeAgentLaunchFingerprint(BASE)
|
||||
)
|
||||
})
|
||||
|
||||
it('separates a retry that picked another surface, or none, from the first launch', () => {
|
||||
@@ -122,13 +129,4 @@ describe('compatibility with rows written before these fields existed', () => {
|
||||
computeAgentLaunchFingerprint({ ...BASE, agentArgs: '--model opus', cwd: '/repo/packages/a' })
|
||||
).toBe(beforePaneKey)
|
||||
})
|
||||
|
||||
it('still matches when the caller sends only telemetry the digest excludes', () => {
|
||||
const previousBuild = canonicalAgentSessionDigest({
|
||||
method: 'agent.launch',
|
||||
agent: BASE.agent,
|
||||
target: BASE.target
|
||||
})
|
||||
expect(fingerprintOfWirePayload({ ...BASE, launchSource: 'quick_command' })).toBe(previousBuild)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('repoIsRemote', () => {
|
||||
expect(repoIsRemote({ connectionId: null, executionHostId: 'runtime:vm-1' })).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps plain local and WSL rows local', () => {
|
||||
it('keeps plain local rows local', () => {
|
||||
expect(repoIsRemote({ connectionId: null, executionHostId: null })).toBe(false)
|
||||
expect(repoIsRemote({ connectionId: null, executionHostId: 'local' })).toBe(false)
|
||||
})
|
||||
|
||||
@@ -20,24 +20,23 @@ function waiting(interactivePrompt: string, toolName?: string) {
|
||||
|
||||
describe('readAgentPromptAnswers', () => {
|
||||
it('reads an approval with the cards’ own allow and deny keys', () => {
|
||||
const read = readAgentPromptAnswers(waiting(approval), 'claude', 'p1', true)
|
||||
expect(read?.prompt).toEqual({
|
||||
kind: 'approval',
|
||||
promptId: 'p1',
|
||||
tool: 'Bash',
|
||||
summary: 'rm -rf build',
|
||||
options: [
|
||||
{ id: 'allow_once', label: 'Allow once' },
|
||||
{ id: 'reject_once', label: 'Reject' }
|
||||
]
|
||||
})
|
||||
expect(Object.fromEntries(read!.keysByOption)).toEqual({
|
||||
allow_once: '1',
|
||||
reject_once: '\x1b'
|
||||
})
|
||||
expect(
|
||||
readAgentPromptAnswers(waiting(approval), 'codex', 'p1', true)?.keysByOption.get('allow_once')
|
||||
).toBe('1')
|
||||
for (const agent of ['claude', 'codex']) {
|
||||
const read = readAgentPromptAnswers(waiting(approval), agent, 'p1', true)
|
||||
expect(read?.prompt).toEqual({
|
||||
kind: 'approval',
|
||||
promptId: 'p1',
|
||||
tool: 'Bash',
|
||||
summary: 'rm -rf build',
|
||||
options: [
|
||||
{ id: 'allow_once', label: 'Allow once' },
|
||||
{ id: 'reject_once', label: 'Reject' }
|
||||
]
|
||||
})
|
||||
expect(Object.fromEntries(read!.keysByOption)).toEqual({
|
||||
allow_once: '1',
|
||||
reject_once: '\x1b'
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('reads a one-question single choice with each option’s selector key', () => {
|
||||
@@ -58,7 +57,10 @@ describe('readAgentPromptAnswers', () => {
|
||||
})
|
||||
|
||||
it('leaves to Orca what one keystroke cannot answer', () => {
|
||||
const unsupported = { kind: 'unsupported', promptId: 'p' }
|
||||
const unsupported = {
|
||||
prompt: { kind: 'unsupported', promptId: 'p' },
|
||||
keysByOption: new Map()
|
||||
}
|
||||
const cases = [
|
||||
waiting(question({}, 2)),
|
||||
waiting(question({ multiSelect: true })),
|
||||
@@ -66,18 +68,29 @@ describe('readAgentPromptAnswers', () => {
|
||||
waiting('not json'),
|
||||
{ state: 'blocked' as const }
|
||||
]
|
||||
for (const row of cases) {
|
||||
expect(readAgentPromptAnswers(row, 'claude', 'p', true)?.prompt).toEqual(unsupported)
|
||||
for (const agent of ['claude', 'codex']) {
|
||||
for (const row of cases) {
|
||||
expect(readAgentPromptAnswers(row, agent, 'p', true)).toEqual(unsupported)
|
||||
}
|
||||
}
|
||||
const asyncQuestion = JSON.stringify({
|
||||
questions: [{ title: 'Which color?', options: ['Red', 'Blue'] }]
|
||||
})
|
||||
expect(
|
||||
readAgentPromptAnswers(waiting(asyncQuestion, 'request_user_input_async'), 'codex', 'p', true)
|
||||
).toEqual(unsupported)
|
||||
})
|
||||
|
||||
it('leaves every prompt to Orca for an agent without hooks or known keys', () => {
|
||||
expect(readAgentPromptAnswers(waiting(approval), 'claude', 'p', false)?.prompt.kind).toBe(
|
||||
'unsupported'
|
||||
)
|
||||
expect(readAgentPromptAnswers(waiting(approval), 'aider', 'p', true)?.prompt.kind).toBe(
|
||||
'unsupported'
|
||||
)
|
||||
const unsupported = {
|
||||
prompt: { kind: 'unsupported', promptId: 'p' },
|
||||
keysByOption: new Map()
|
||||
}
|
||||
expect(readAgentPromptAnswers(waiting(approval), 'claude', 'p', false)).toEqual(unsupported)
|
||||
expect(readAgentPromptAnswers(waiting(approval), 'aider', 'p', true)).toEqual(unsupported)
|
||||
expect(
|
||||
readAgentPromptAnswers(waiting(question(), 'AskUserQuestion'), 'grok', 'p', true)
|
||||
).toEqual(unsupported)
|
||||
})
|
||||
|
||||
it('reads nothing while the agent is not waiting on the user', () => {
|
||||
|
||||
@@ -25,10 +25,13 @@ describe('getAgentRowConversationName', () => {
|
||||
})
|
||||
|
||||
it('keeps OpenCode semantic session titles whole', () => {
|
||||
const tab = makeTab({ title: 'OC | build the release pipeline' })
|
||||
const tab = makeTab({ title: 'OC | build the release pipeline', generatedTitle: 'Older title' })
|
||||
expect(getAgentRowConversationName(tab, 'opencode', false)).toBe(
|
||||
'OC | build the release pipeline'
|
||||
)
|
||||
expect(getAgentRowConversationName(tab, 'opencode', true)).toBe(
|
||||
'OC | build the release pipeline'
|
||||
)
|
||||
})
|
||||
|
||||
it("shows the title owned by the row's provider session", () => {
|
||||
@@ -40,6 +43,11 @@ describe('getAgentRowConversationName', () => {
|
||||
expect(getAgentRowConversationName(tab, 'claude', true, undefined, 's1')).toBe(
|
||||
'Fix the lease probe'
|
||||
)
|
||||
expect(getAgentRowConversationName(tab, 'claude', true, undefined, 's2')).toBe(
|
||||
'Fix intake flow'
|
||||
)
|
||||
expect(getAgentRowConversationName(tab, 'codex', true, undefined, 's1')).toBe('Fix intake flow')
|
||||
expect(getAgentRowConversationName(tab, 'claude', true)).toBe('Fix intake flow')
|
||||
})
|
||||
|
||||
it('lets a manual rename and a quick command still outrank the provider title', () => {
|
||||
@@ -208,6 +216,13 @@ describe('getAgentRowConversationName', () => {
|
||||
})
|
||||
|
||||
it('rejects empty, glyph-only, and default terminal titles', () => {
|
||||
expect(
|
||||
getAgentRowConversationName(
|
||||
makeTab({ title: 'Workbench', defaultTitle: 'Workbench' }),
|
||||
'claude',
|
||||
false
|
||||
)
|
||||
).toBeNull()
|
||||
expect(getAgentRowConversationName(makeTab(), 'claude', false)).toBeNull()
|
||||
expect(getAgentRowConversationName(makeTab({ title: '✳' }), 'claude', false)).toBeNull()
|
||||
expect(
|
||||
|
||||
@@ -72,7 +72,7 @@ Keep the raw preamble out of the sidebar.`
|
||||
})
|
||||
|
||||
it('falls back to the raw prompt outside orchestration workers', () => {
|
||||
expect(getAgentRowPrimaryText({ prompt: 'Fix checkout race' })).toBe('Fix checkout race')
|
||||
expect(getAgentRowPrimaryText({ prompt: ' Fix checkout race\n ' })).toBe('Fix checkout race')
|
||||
})
|
||||
|
||||
// Why: full preambles bury === TASK === after multi-KB CLI instructions. The
|
||||
@@ -226,12 +226,10 @@ Checkout race body`,
|
||||
}
|
||||
})
|
||||
).toBe('Fix checkout race')
|
||||
})
|
||||
|
||||
it('ignores sticky orchestration for non-dispatch generated titles', () => {
|
||||
expect(
|
||||
getAgentRowGeneratedTitleText({
|
||||
prompt: 'Refactor the auth middleware',
|
||||
prompt:
|
||||
'You are working inside Orca, a multi-agent IDE. Your task ID is: task-2 === TASK === New worker body',
|
||||
orchestration: {
|
||||
taskId: 'task-1',
|
||||
dispatchId: 'ctx-1',
|
||||
@@ -239,6 +237,20 @@ Checkout race body`,
|
||||
displayName: 'Stale worker label'
|
||||
}
|
||||
})
|
||||
).toBe('Refactor the auth middleware')
|
||||
).toBe('New worker body')
|
||||
})
|
||||
|
||||
it('ignores sticky orchestration for non-dispatch generated titles', () => {
|
||||
expect(
|
||||
getAgentRowGeneratedTitleText({
|
||||
prompt: ' Refactor the auth middleware\n ',
|
||||
orchestration: {
|
||||
taskId: 'task-1',
|
||||
dispatchId: 'ctx-1',
|
||||
taskTitle: 'Stale task',
|
||||
displayName: 'Stale worker label'
|
||||
}
|
||||
})
|
||||
).toBe(' Refactor the auth middleware\n ')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -40,6 +40,13 @@ describe('approvalBlockedPathToShow', () => {
|
||||
})
|
||||
|
||||
it('checks only the text the card draws: a plan replaces the detail', () => {
|
||||
expect(
|
||||
approvalBlockedPathToShow({
|
||||
blockedPath: '/repo/notes.md',
|
||||
subject: { kind: 'plan', text: 'Read /repo/notes.md' },
|
||||
detail: 'unused'
|
||||
})
|
||||
).toBeNull()
|
||||
expect(
|
||||
approvalBlockedPathToShow({
|
||||
blockedPath: '/repo/plan.md',
|
||||
@@ -72,7 +79,10 @@ describe('approvalBlockedPathToShow', () => {
|
||||
).toBe('/home/me/secret.txt')
|
||||
})
|
||||
|
||||
it('shows a path the text names only as the start of a longer name', () => {
|
||||
it('distinguishes whole paths from longer names across all occurrences', () => {
|
||||
expect(
|
||||
approvalBlockedPathToShow({ blockedPath: '/srv/data', detail: 'ls /srv/data-old /srv/data' })
|
||||
).toBeNull()
|
||||
expect(
|
||||
approvalBlockedPathToShow({ blockedPath: '/srv/data', detail: 'ls /srv/data-old/x' })
|
||||
).toBe('/srv/data')
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
pendingPromptsAllUnanswerableHere
|
||||
} from './agent-session-approval-subject'
|
||||
import type { AgentJournalItemBody } from './agent-session-journal-types'
|
||||
import { AGENT_JOURNAL_APPROVAL_SUBJECT_KINDS } from './agent-session-journal-schemas'
|
||||
import {
|
||||
AGENT_JOURNAL_APPROVAL_SUBJECT_KINDS,
|
||||
isAdmissibleAgentJournalItemBody
|
||||
} from './agent-session-journal-schemas'
|
||||
|
||||
it('draws exactly the subject kinds the journal schema knows', () => {
|
||||
expect([...DRAWN_APPROVAL_SUBJECT_KINDS].sort()).toEqual(
|
||||
@@ -20,14 +23,14 @@ it("reads only a subject of a kind it does not draw as a newer build's", () => {
|
||||
})
|
||||
|
||||
function pending(body: Record<string, unknown>): { body: AgentJournalItemBody } {
|
||||
return JSON.parse(
|
||||
JSON.stringify({
|
||||
body: {
|
||||
...body,
|
||||
resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null }
|
||||
}
|
||||
})
|
||||
)
|
||||
const promptBody = {
|
||||
...body,
|
||||
resolution: { state: 'pending', selectedOptionId: null, resolvedBy: null, resolvedAt: null }
|
||||
}
|
||||
if (!isAdmissibleAgentJournalItemBody(promptBody)) {
|
||||
throw new Error('Invalid pending prompt fixture')
|
||||
}
|
||||
return { body: promptBody }
|
||||
}
|
||||
const NEWER = pending({
|
||||
kind: 'approval',
|
||||
@@ -43,7 +46,7 @@ const PLAN = pending({
|
||||
subject: { kind: 'plan', text: 'do it' },
|
||||
options: [{ id: 'allow', label: 'Approve' }]
|
||||
})
|
||||
const QUESTION = pending({ kind: 'question', title: 'Which?', options: [] })
|
||||
const QUESTION = pending({ kind: 'question', question: 'Which?', options: [] })
|
||||
|
||||
it('holds only while every pending prompt is an approval this build cannot answer', () => {
|
||||
expect(pendingPromptsAllUnanswerableHere([])).toBe(false)
|
||||
@@ -54,6 +57,8 @@ it('holds only while every pending prompt is an approval this build cannot answe
|
||||
const cancelled = JSON.parse(
|
||||
JSON.stringify(NEWER).replace('"state":"pending"', '"state":"cancelled"')
|
||||
)
|
||||
expect(pendingPromptsAllUnanswerableHere([cancelled, NEWER])).toBe(true)
|
||||
expect(pendingPromptsAllUnanswerableHere([NEWER, cancelled])).toBe(true)
|
||||
expect(pendingPromptsAllUnanswerableHere([cancelled, PLAN])).toBe(false)
|
||||
expect(pendingPromptsAllUnanswerableHere([cancelled])).toBe(false)
|
||||
})
|
||||
|
||||
@@ -31,7 +31,8 @@ describe('normalizeAgentSessionConversationName', () => {
|
||||
})
|
||||
|
||||
it('bounds a pasted essay to the stored maximum', () => {
|
||||
const normalized = normalizeAgentSessionConversationName('a'.repeat(1000))
|
||||
const normalized = normalizeAgentSessionConversationName(`First ${'x'.repeat(994)}`)
|
||||
expect(normalized).toBe(`First ${'x'.repeat(194)}`)
|
||||
expect(normalized).toHaveLength(AGENT_SESSION_CONVERSATION_NAME_MAX_LENGTH)
|
||||
expect(isAgentSessionConversationName(normalized)).toBe(true)
|
||||
})
|
||||
@@ -50,14 +51,9 @@ describe('isAgentSessionConversationName', () => {
|
||||
})
|
||||
|
||||
describe('normalizeAgentSessionConversationName hostile text', () => {
|
||||
it('strips control characters and bidi overrides', () => {
|
||||
// U+202E renders what follows right-to-left, so a tab could show a label
|
||||
// that reads as text the name does not contain.
|
||||
expect(normalizeAgentSessionConversationName('Fix\u202Egnp.exe probe')).toBe(
|
||||
'Fix gnp.exe probe'
|
||||
)
|
||||
it('strips controls and lone surrogates', () => {
|
||||
expect(normalizeAgentSessionConversationName('Fix\u0007the probe')).toBe('Fix the probe')
|
||||
expect(normalizeAgentSessionConversationName('Fix\u200Bthe probe')).toBe('Fix the probe')
|
||||
expect(normalizeAgentSessionConversationName('Fix\uD800 the\uDC00 probe')).toBe('Fix the probe')
|
||||
expect(normalizeAgentSessionConversationName('\u202E\u200B ')).toBeNull()
|
||||
})
|
||||
|
||||
|
||||
@@ -18,10 +18,21 @@ describe('agent-session grouped question answers', () => {
|
||||
{ questionId: 'q2', optionIds: [], other: 'SSH host' }
|
||||
]
|
||||
|
||||
it('round-trips grouped multi-select and free-text answers', () => {
|
||||
it('round-trips grouped answers and rejects malformed wire payloads', () => {
|
||||
expect(decodeAgentSessionQuestionAnswers(encodeAgentSessionQuestionAnswers(answers))).toEqual(
|
||||
answers
|
||||
)
|
||||
const malformed = [
|
||||
'not-grouped',
|
||||
'question-group:%',
|
||||
'question-group:not-json',
|
||||
'question-group:{}',
|
||||
`${GROUP_ANSWER_PREFIX}${JSON.stringify([answers[0], { questionId: 'q2', optionIds: [1] }])}`,
|
||||
`${GROUP_ANSWER_PREFIX}${JSON.stringify([{ questionId: 'q1', optionIds: [], other: 1 }])}`
|
||||
]
|
||||
for (const value of malformed) {
|
||||
expect(decodeAgentSessionQuestionAnswers(value)).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps compact answers readable by the original percent-decoding contract', () => {
|
||||
@@ -48,9 +59,12 @@ describe('agent-session grouped question answers', () => {
|
||||
expect(
|
||||
decodeAgentSessionQuestionAnswers(encodeAgentSessionQuestionAnswers(unicodeAnswers))
|
||||
).toEqual(unicodeAnswers)
|
||||
expect(
|
||||
decodeWithOriginalPercentDecoder(encodeAgentSessionQuestionAnswers(unicodeAnswers))
|
||||
).toEqual(unicodeAnswers)
|
||||
})
|
||||
|
||||
it("fits Claude's maximum choice group within a 512-character host response bound", () => {
|
||||
it("keeps Claude's maximum choice group compact without losing selections", () => {
|
||||
const maximumSelections = Array.from({ length: 4 }, (_, questionIndex) => ({
|
||||
questionId: `q${questionIndex + 1}`,
|
||||
optionIds: Array.from(
|
||||
@@ -59,7 +73,10 @@ describe('agent-session grouped question answers', () => {
|
||||
)
|
||||
}))
|
||||
|
||||
expect(encodeAgentSessionQuestionAnswers(maximumSelections).length).toBeLessThanOrEqual(512)
|
||||
const encoded = encodeAgentSessionQuestionAnswers(maximumSelections)
|
||||
expect(encoded.length).toBeLessThanOrEqual(512)
|
||||
expect(decodeAgentSessionQuestionAnswers(encoded)).toEqual(maximumSelections)
|
||||
expect(decodeWithOriginalPercentDecoder(encoded)).toEqual(maximumSelections)
|
||||
})
|
||||
|
||||
it('validates each grouped answer against its question shape', () => {
|
||||
@@ -83,6 +100,24 @@ describe('agent-session grouped question answers', () => {
|
||||
]
|
||||
|
||||
expect(isValidAgentSessionQuestionAnswers(questions, answers)).toBe(true)
|
||||
expect(isValidAgentSessionQuestionAnswers(questions, answers.toReversed())).toBe(true)
|
||||
const invalid = [
|
||||
[],
|
||||
[...answers, { questionId: 'q3', optionIds: [] }],
|
||||
[answers[0]!, answers[0]!],
|
||||
[{ ...answers[0]!, questionId: 'unknown' }, answers[1]!],
|
||||
[{ ...answers[0]!, optionIds: [] }, answers[1]!],
|
||||
[{ ...answers[0]!, other: 'not offered' }, answers[1]!]
|
||||
]
|
||||
for (const candidate of invalid) {
|
||||
expect(isValidAgentSessionQuestionAnswers(questions, candidate)).toBe(false)
|
||||
}
|
||||
expect(
|
||||
isValidAgentSessionQuestionAnswers(
|
||||
[{ ...questions[0]!, multiSelect: false }, questions[1]!],
|
||||
answers
|
||||
)
|
||||
).toBe(false)
|
||||
expect(
|
||||
isValidAgentSessionQuestionAnswers(questions, [
|
||||
{ questionId: 'q1', optionIds: ['unknown'] },
|
||||
|
||||
@@ -14,18 +14,22 @@ describe('provider free-text question metadata', () => {
|
||||
freeTextInput: { allowEmpty: true, multiline: true, initialValue: 'first\nsecond' }
|
||||
}
|
||||
|
||||
it.each(['', ' ', ' first\nsecond\u2028third\u2029fourth '])(
|
||||
'preserves the exact answer %j across the older wire form',
|
||||
(other) => {
|
||||
const answers = [{ questionId: 'editor:1', optionIds: [], other }]
|
||||
const encoded = legacyAgentSessionSelectedOptionId(body, answers)
|
||||
expect(encoded).not.toBeNull()
|
||||
expect(legacyAgentSessionQuestionAnswers(body, encoded!)).toEqual(answers)
|
||||
expect(isValidAgentSessionQuestionAnswers(agentSessionPromptQuestions(body), answers)).toBe(
|
||||
true
|
||||
)
|
||||
}
|
||||
)
|
||||
it.each([
|
||||
['', 'editor%3A1:'],
|
||||
[' ', 'editor%3A1:%20%20'],
|
||||
[
|
||||
' first\nsecond\u2028third\u2029fourth ',
|
||||
'editor%3A1:%20first%0Asecond%E2%80%A8third%E2%80%A9fourth%20'
|
||||
]
|
||||
])('preserves the exact answer %j across the older wire form', (other, expectedEncoded) => {
|
||||
const answers = [{ questionId: 'editor:1', optionIds: [], other }]
|
||||
const encoded = legacyAgentSessionSelectedOptionId(body, answers)
|
||||
expect(encoded).toBe(expectedEncoded)
|
||||
expect(legacyAgentSessionQuestionAnswers(body, encoded!)).toEqual(answers)
|
||||
expect(isValidAgentSessionQuestionAnswers(agentSessionPromptQuestions(body), answers)).toBe(
|
||||
true
|
||||
)
|
||||
})
|
||||
|
||||
it('requires an explicitly supplied answer even when empty is allowed', () => {
|
||||
expect(
|
||||
|
||||
@@ -46,15 +46,18 @@ describe('agent status subjects', () => {
|
||||
makePtyRunAgentStatusSubject(scope({ wslDistro: 'Ubuntu' }), identity),
|
||||
makePtyRunAgentStatusSubject(scope({ executionHostId: 'ssh:target-a' }), identity),
|
||||
makePtyRunAgentStatusSubject(scope({ executionHostId: 'runtime:peer-a' }), identity),
|
||||
makePtyRunAgentStatusSubject(
|
||||
scope({ workspaceId: 'folder-1', workspaceKind: 'folder' }),
|
||||
identity
|
||||
),
|
||||
makePtyRunAgentStatusSubject(scope({ workspaceId: 'workspace-2' }), identity),
|
||||
makePtyRunAgentStatusSubject(scope({ workspaceKind: 'folder' }), identity),
|
||||
makePtyAgentStatusSubject(scope(), identity),
|
||||
makeStructuredAgentStatusSubject(scope(), identity)
|
||||
]
|
||||
|
||||
expect(new Set(subjects.map(serializeAgentStatusSubject))).toHaveLength(subjects.length)
|
||||
for (const [index, subject] of subjects.entries()) {
|
||||
for (const other of subjects.slice(index + 1)) {
|
||||
expect(agentStatusSubjectsEqual(subject, other)).toBe(false)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps the existing structured-session encoding byte-for-byte stable', () => {
|
||||
@@ -69,7 +72,7 @@ describe('agent status subjects', () => {
|
||||
|
||||
it.each([
|
||||
null,
|
||||
{ kind: 'future' },
|
||||
{ kind: 'future', ...scope(), paneKey: PANE_KEY },
|
||||
{
|
||||
kind: 'pty-run',
|
||||
...scope(),
|
||||
@@ -125,7 +128,8 @@ describe('agent status subjects', () => {
|
||||
'',
|
||||
'agent-status-subject-v1:not-json',
|
||||
'agent-status-subject-v1:["pty-run","local",null,"workspace-1","git-worktree"]',
|
||||
'agent-status-subject-v1:["unknown","local",null,"workspace-1","git-worktree","id"]'
|
||||
'agent-status-subject-v1:["unknown","local",null,"workspace-1","git-worktree","id"]',
|
||||
'agent-status-subject-v1:["pty-run","local",null,"workspace-1","git-worktree","run_11111111-1111-4111-8111-111111111111","extra"]'
|
||||
])('rejects malformed serialized subject %#', (value) => {
|
||||
expect(deserializeAgentStatusSubject(value)).toBeNull()
|
||||
})
|
||||
|
||||
@@ -13,13 +13,24 @@ describe('structured agent session option codec', () => {
|
||||
})
|
||||
|
||||
it('rejects string booleans on the UI side and non-canonical strings on the wire side', () => {
|
||||
expect(encodeStructuredAgentSessionOptionValue('fastMode', 'true')).toBeNull()
|
||||
expect(decodeStructuredAgentSessionOptionValue('fastMode', 'TRUE')).toBeNull()
|
||||
expect(decodeStructuredAgentSessionOptionValue('fastMode', '1')).toBeNull()
|
||||
for (const value of ['true', 'false']) {
|
||||
expect(encodeStructuredAgentSessionOptionValue('fastMode', value)).toBeNull()
|
||||
}
|
||||
for (const value of ['TRUE', '1', 'False', ' true', 'false ', '', '0']) {
|
||||
expect(decodeStructuredAgentSessionOptionValue('fastMode', value)).toBeNull()
|
||||
}
|
||||
})
|
||||
|
||||
it('passes existing string options through unchanged', () => {
|
||||
expect(encodeStructuredAgentSessionOptionValue('model', 'gpt-example')).toBe('gpt-example')
|
||||
expect(decodeStructuredAgentSessionOptionValue('effort', 'high')).toBe('high')
|
||||
it('preserves string options in both directions and rejects boolean values', () => {
|
||||
for (const [id, value] of [
|
||||
['model', 'gpt-example'],
|
||||
['effort', 'high'],
|
||||
['serviceTier', 'flex']
|
||||
] as const) {
|
||||
expect(encodeStructuredAgentSessionOptionValue(id, value)).toBe(value)
|
||||
expect(decodeStructuredAgentSessionOptionValue(id, value)).toBe(value)
|
||||
expect(encodeStructuredAgentSessionOptionValue(id, true)).toBeNull()
|
||||
expect(encodeStructuredAgentSessionOptionValue(id, false)).toBeNull()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -82,7 +82,7 @@ describe('a Claude model its CLI no longer lists', () => {
|
||||
}
|
||||
})
|
||||
|
||||
it('keeps a remembered model and effort, including under a model with no listed effort', () => {
|
||||
it('waits for an unlisted seed and carries a current effort without an offered menu', () => {
|
||||
// The placeholder stands until the host answers with the chat's own model; create seeds it.
|
||||
expect(carried(withHostList(), remembered)).toBeUndefined()
|
||||
const haiku = { id: 'haiku', label: 'Haiku', isDefault: false, efforts: [] }
|
||||
@@ -92,6 +92,13 @@ describe('a Claude model its CLI no longer lists', () => {
|
||||
current: { model: 'haiku', effort: 'high' }
|
||||
})
|
||||
expect(shown(listed, undefined, {})).toEqual({ model: 'haiku', effort: 'high' })
|
||||
expect(carried(listed)).toEqual({ model: 'haiku', effort: 'high' })
|
||||
expect(
|
||||
structuredAgentSessionShownSnapshot(listed).find((entry) => entry.id === 'effort')
|
||||
).toMatchObject({
|
||||
settable: false,
|
||||
kind: { currentValue: 'high', choices: [{ value: 'high', label: 'high' }] }
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -102,10 +109,22 @@ describe('what a send carries', () => {
|
||||
expect(carried(withHostList(fresh(), listing))).toEqual({ model: 'sonnet' })
|
||||
})
|
||||
|
||||
it('carries a pick on its way as given', () => {
|
||||
const listed = withHostList(fresh(), { ...LISTED, models: [OPUS, SONNET] })
|
||||
expect(carried(listed, remembered, { model: 'sonnet' })).toMatchObject({ model: 'sonnet' })
|
||||
expect(carried(withHostList(), undefined, { model: 'claude-retired' })).toMatchObject({
|
||||
it('carries held picks without committing them to the chat', () => {
|
||||
const listed = {
|
||||
...withHostList(fresh(), { ...LISTED, models: [OPUS, SONNET] }),
|
||||
pendingId: 'model'
|
||||
}
|
||||
const originalRecord = structuredClone(listed.record)
|
||||
const view = structuredAgentSessionOptionView(listed, remembered, {
|
||||
model: 'sonnet',
|
||||
effort: 'high'
|
||||
})
|
||||
expect(structuredAgentSessionSendSelection(view)).toEqual({ model: 'sonnet', effort: 'high' })
|
||||
expect(view.pendingId).toBe('model')
|
||||
expect(view.record).not.toBe(listed.record)
|
||||
expect(listed.record).toEqual(originalRecord)
|
||||
expect(carried(listed, remembered, { model: 'sonnet' })).toEqual({ model: 'sonnet' })
|
||||
expect(carried(withHostList(), undefined, { model: 'claude-retired' })).toEqual({
|
||||
model: 'claude-retired'
|
||||
})
|
||||
})
|
||||
@@ -152,6 +171,12 @@ describe("a chat's model and effort, apart from the defaults filling them in", (
|
||||
const state = resumed()
|
||||
expect(shown(state, undefined, {})).toEqual({ model: 'opus', effort: 'low' })
|
||||
expect(carried(state)).toEqual({ model: 'opus', effort: 'low' })
|
||||
expect(
|
||||
structuredAgentSessionShownSnapshot(state).map(({ id, valueSource }) => ({ id, valueSource }))
|
||||
).toEqual([
|
||||
{ id: 'model', valueSource: 'dispatched' },
|
||||
{ id: 'effort', valueSource: 'dispatched' }
|
||||
])
|
||||
})
|
||||
|
||||
it('lets a new chat pick its effort before its first message, under the default it shows', () => {
|
||||
|
||||
Reference in New Issue
Block a user