mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 00:27:39 +00:00
Adds the request/refusal shapes every `agentSession.*` method speaks, the four-field idempotency envelope and its lease admission, the item-key inverse an upsert needs, and the `agent-session.structured.v1` capability. The protocol version stays at 3: the surface is additive, and bumping it would strand every paired device on the current release rather than degrade one feature.
171 lines
5.2 KiB
TypeScript
171 lines
5.2 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import type { AgentSessionOperationDecision } from './agent-session-operation-ledger'
|
|
import type { AgentSessionLease } from './agent-session-record'
|
|
import {
|
|
admitAgentSessionMutation,
|
|
agentSessionFingerprintConflict,
|
|
computeAgentSessionPayloadFingerprint
|
|
} from './agent-session-mutation-envelope'
|
|
import type { AgentSessionMutationEnvelope } from './agent-session-wire'
|
|
|
|
const LEASE: AgentSessionLease = {
|
|
sessionId: 'session-1',
|
|
runtimeKind: 'native',
|
|
provenHandleLinkId: 'link-1',
|
|
deathEvidence: null,
|
|
runtimeFence: 4,
|
|
ownerProcess: { hostId: 'host-1', pid: 42, processStartTimeMs: null, spawnToken: 'token-1' },
|
|
reservedSpawnToken: null,
|
|
leaseDeadlineAt: 10_000,
|
|
lastRenewedAt: 9_000,
|
|
handoffOperationId: null,
|
|
journalCheckpoint: null,
|
|
claimKeyId: 'key-1',
|
|
claimStatus: 'live',
|
|
unreconciled: false,
|
|
handoffStage: null
|
|
}
|
|
|
|
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 first-time operation under a live lease at the expected fence', () => {
|
|
expect(admitAgentSessionMutation({ ...base, ledger: ADMIT('f'.repeat(64)) }).decision).toBe(
|
|
'admit'
|
|
)
|
|
})
|
|
|
|
it('replays a recorded operation without re-checking the fence', () => {
|
|
const admission = admitAgentSessionMutation({
|
|
...base,
|
|
envelope: envelope({ expectedRuntimeFence: 1 }),
|
|
ledger: { decision: 'replay', row: row('f'.repeat(64)) }
|
|
})
|
|
expect(admission.decision).toBe('replay')
|
|
})
|
|
|
|
it('refuses a stale fence and hands back the current one', () => {
|
|
const admission = admitAgentSessionMutation({
|
|
...base,
|
|
envelope: envelope({ expectedRuntimeFence: 3 }),
|
|
ledger: ADMIT('f'.repeat(64))
|
|
})
|
|
expect(admission).toMatchObject({
|
|
decision: 'refused',
|
|
refusal: { code: 'agent_session_checkpoint_stale', currentFence: 4 }
|
|
})
|
|
})
|
|
|
|
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 mid-handoff', () => {
|
|
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' }
|
|
})
|
|
})
|
|
|
|
it('surfaces a ledger refusal verbatim', () => {
|
|
const admission = admitAgentSessionMutation({
|
|
...base,
|
|
ledger: { decision: 'refused', code: 'agent_session_operation_expired' }
|
|
})
|
|
expect(admission).toMatchObject({
|
|
decision: 'refused',
|
|
refusal: { code: 'agent_session_operation_expired' }
|
|
})
|
|
})
|
|
})
|