Files
orca/src/shared/agent-session-mutation-envelope.test.ts
T

199 lines
6.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { AgentSessionOperationDecision } from './agent-session-operation-ledger'
import { agentSessionLeaseFixture } from './agent-session-record.test-fixture'
import {
admitAgentSessionMutation,
agentSessionFingerprintConflict,
computeAgentSessionPayloadFingerprint
} from './agent-session-mutation-envelope'
import type { AgentSessionMutationEnvelope } from './agent-session-wire'
const LEASE = agentSessionLeaseFixture({
sessionId: 'session-1',
runtimeKind: 'native',
runtimeFence: 4
})
function envelope(overrides: Partial<AgentSessionMutationEnvelope> = {}) {
return {
sessionId: 'session-1',
clientOperationId: 'op-1',
expectedRuntimeFence: 4,
payloadFingerprint: 'f'.repeat(64),
...overrides
}
}
function row(fingerprint: string) {
return {
callerKey: 'caller-1',
operationId: 'op-1',
fingerprint,
operationTimestamp: 1_000,
recordedAt: 1_000,
expiresAt: 100_000,
outcome: { status: 'pending' as const }
}
}
const ADMIT = (fingerprint: string): AgentSessionOperationDecision => ({
decision: 'admit',
row: row(fingerprint)
})
describe('computeAgentSessionPayloadFingerprint', () => {
it('is stable across key order at every depth', () => {
const a = computeAgentSessionPayloadFingerprint({
method: 'agentSession.send',
sessionId: 'session-1',
fields: { body: { kind: 'message', blocks: [{ type: 'text', text: 'hi' }] }, extra: 1 }
})
const b = computeAgentSessionPayloadFingerprint({
method: 'agentSession.send',
sessionId: 'session-1',
fields: { extra: 1, body: { blocks: [{ text: 'hi', type: 'text' }], kind: 'message' } }
})
expect(a).toBe(b)
expect(a).toMatch(/^[0-9a-f]{64}$/)
})
it('separates one payload from another and one method from another', () => {
const send = computeAgentSessionPayloadFingerprint({
method: 'agentSession.send',
sessionId: 'session-1',
fields: { text: 'hi' }
})
expect(
computeAgentSessionPayloadFingerprint({
method: 'agentSession.send',
sessionId: 'session-1',
fields: { text: 'hi there' }
})
).not.toBe(send)
expect(
computeAgentSessionPayloadFingerprint({
method: 'agentSession.cancel',
sessionId: 'session-1',
fields: { text: 'hi' }
})
).not.toBe(send)
})
})
describe('agentSessionFingerprintConflict', () => {
it('refuses a payload that does not match what the client declared', () => {
const conflict = agentSessionFingerprintConflict(envelope(), 'a'.repeat(64))
expect(conflict?.code).toBe('agent_session_operation_conflict')
})
it('passes a matching declaration', () => {
expect(agentSessionFingerprintConflict(envelope(), 'f'.repeat(64))).toBeNull()
})
})
describe('admitAgentSessionMutation', () => {
const base = { envelope: envelope(), hostFingerprint: 'f'.repeat(64), lease: LEASE }
it('admits a matching first-time operation and refuses a mismatched payload', () => {
expect(admitAgentSessionMutation({ ...base, ledger: ADMIT('f'.repeat(64)) }).decision).toBe(
'admit'
)
expect(
admitAgentSessionMutation({
...base,
hostFingerprint: 'a'.repeat(64),
ledger: ADMIT('a'.repeat(64))
})
).toMatchObject({
decision: 'refused',
refusal: {
code: 'agent_session_operation_conflict',
details: { reason: 'fingerprintMismatch' }
}
})
})
it('replays a recorded outcome even while the lease is unreconciled', () => {
const recorded = {
...row('f'.repeat(64)),
outcome: { status: 'succeeded' as const, sessionId: 'created-session' }
}
const admission = admitAgentSessionMutation({
...base,
envelope: envelope({ expectedRuntimeFence: 1 }),
lease: { ...LEASE, unreconciled: true },
ledger: { decision: 'replay', row: recorded }
})
expect(admission).toEqual({ decision: 'replay', row: recorded })
})
it('admits a writer whatever fence the client last saw', () => {
for (const expectedRuntimeFence of [3, 5, null]) {
const admission = admitAgentSessionMutation({
...base,
envelope: envelope({ expectedRuntimeFence }),
ledger: ADMIT('f'.repeat(64))
})
expect(admission, String(expectedRuntimeFence)).toMatchObject({ decision: 'admit' })
}
})
it('refuses a writer while the lease is unreconciled', () => {
const admission = admitAgentSessionMutation({
...base,
lease: { ...LEASE, unreconciled: true },
ledger: ADMIT('f'.repeat(64))
})
expect(admission).toMatchObject({
decision: 'refused',
refusal: { code: 'execution_owner_reconciling' }
})
})
it('refuses a writer while the chat is still starting', () => {
const admission = admitAgentSessionMutation({
...base,
lease: { ...LEASE, handoffStage: 'new-owner-proving' },
ledger: ADMIT('f'.repeat(64))
})
expect(admission).toMatchObject({
decision: 'refused',
refusal: { code: 'agent_session_conflict', message: 'The chat is still starting.' }
})
})
it('names recovery rather than a handoff when a restart left the owner unproven', () => {
const admission = admitAgentSessionMutation({
...base,
lease: { ...LEASE, handoffStage: 'recovering' },
ledger: ADMIT('f'.repeat(64))
})
expect(admission).toMatchObject({
decision: 'refused',
refusal: {
code: 'agent_session_conflict',
message:
"Orca has not yet confirmed that this chat's previous agent process stopped. Reopen the chat to check again."
}
})
})
it('surfaces a ledger refusal verbatim', () => {
const admission = admitAgentSessionMutation({
...base,
ledger: {
decision: 'refused',
code: 'agent_session_operation_expired',
details: { reason: 'operationExpired' }
}
})
expect(admission).toMatchObject({
decision: 'refused',
refusal: {
code: 'agent_session_operation_expired',
details: { reason: 'operationExpired' }
}
})
})
})