Skip to content

Commit 398229e

Browse files
committed
test(managed-agent): pin the HTTP shape of every session endpoint
Method, URL, and beta header for all 11 calls, plus the SSE accept header, the separate memory-store beta (combining the two is a documented 400), and content-type only on requests that carry a body. These are the details types cannot catch and that break silently when a path is "tidied".
1 parent 8ea5162 commit 398229e

1 file changed

Lines changed: 131 additions & 0 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/**
2+
* @vitest-environment node
3+
*
4+
* Pins the exact HTTP shape of every Managed Agents call: method, URL, and the
5+
* beta header each endpoint family requires. These are the details that cannot
6+
* be caught by types or by the payload-builder tests, and that silently break
7+
* if someone "tidies" a path or shares a header across endpoint families.
8+
*
9+
* Verified against https://platform.claude.com/docs/en/managed-agents/
10+
*/
11+
import { afterEach, describe, expect, it, vi } from 'vitest'
12+
import {
13+
AGENT_MEMORY_BETA,
14+
archiveSession,
15+
createSession,
16+
deleteSession,
17+
getEnvironmentType,
18+
listSessionEvents,
19+
MANAGED_AGENTS_BETA,
20+
managedAgentsList,
21+
openSessionStream,
22+
retrieveSession,
23+
sendCustomToolResults,
24+
sendSessionEvents,
25+
sendToolConfirmations,
26+
updateSession,
27+
} from '@/lib/managed-agents/session-client'
28+
29+
const originalFetch = global.fetch
30+
afterEach(() => {
31+
global.fetch = originalFetch
32+
})
33+
34+
const spyOn = (body: unknown = {}) => {
35+
const spy = vi.fn(async () => Response.json(body)) as unknown as typeof fetch
36+
global.fetch = spy
37+
return spy as unknown as ReturnType<typeof vi.fn>
38+
}
39+
40+
const call = (spy: ReturnType<typeof vi.fn>) => {
41+
const [url, init] = spy.mock.calls[0] as [string, RequestInit]
42+
const headers = init.headers as Record<string, string>
43+
return { url: url.split('?')[0], method: init.method, headers }
44+
}
45+
46+
const AUTH = { apiKey: 'sk-ant-fake' }
47+
const S = { ...AUTH, sessionId: 'sesn_1' }
48+
const BASE = 'https://api.anthropic.com'
49+
50+
describe('Managed Agents wire shapes', () => {
51+
it.each([
52+
[
53+
'createSession',
54+
() => createSession({ ...AUTH, agentId: 'a', environmentId: 'e' }),
55+
'POST',
56+
`${BASE}/v1/sessions`,
57+
],
58+
['retrieveSession', () => retrieveSession(S), 'GET', `${BASE}/v1/sessions/sesn_1`],
59+
[
60+
'updateSession',
61+
() => updateSession({ ...S, title: 't' }),
62+
'POST',
63+
`${BASE}/v1/sessions/sesn_1`,
64+
],
65+
['deleteSession', () => deleteSession(S), 'DELETE', `${BASE}/v1/sessions/sesn_1`],
66+
['archiveSession', () => archiveSession(S), 'POST', `${BASE}/v1/sessions/sesn_1/archive`],
67+
['listSessionEvents', () => listSessionEvents(S), 'GET', `${BASE}/v1/sessions/sesn_1/events`],
68+
[
69+
'sendSessionEvents',
70+
() => sendSessionEvents({ ...S, events: [{ type: 'user.interrupt' }] }),
71+
'POST',
72+
`${BASE}/v1/sessions/sesn_1/events`,
73+
],
74+
[
75+
'sendToolConfirmations',
76+
() => sendToolConfirmations({ ...S, confirmations: [{ toolUseId: 'x', result: 'allow' }] }),
77+
'POST',
78+
`${BASE}/v1/sessions/sesn_1/events`,
79+
],
80+
[
81+
'sendCustomToolResults',
82+
() => sendCustomToolResults({ ...S, results: [{ customToolUseId: 'x', content: 'y' }] }),
83+
'POST',
84+
`${BASE}/v1/sessions/sesn_1/events`,
85+
],
86+
[
87+
'getEnvironmentType',
88+
() => getEnvironmentType({ ...AUTH, environmentId: 'env_1' }),
89+
'GET',
90+
`${BASE}/v1/environments/env_1`,
91+
],
92+
])('%s hits %s %s with the managed-agents beta', async (_name, run, method, url) => {
93+
const spy = spyOn({ id: 'sesn_1', config: { type: 'cloud' } })
94+
await run()
95+
const c = call(spy)
96+
expect(c.method).toBe(method)
97+
expect(c.url).toBe(url)
98+
expect(c.headers['anthropic-beta']).toBe(MANAGED_AGENTS_BETA)
99+
expect(c.headers['anthropic-version']).toBe('2023-06-01')
100+
expect(c.headers['x-api-key']).toBe('sk-ant-fake')
101+
})
102+
103+
it('opens the event stream as SSE', async () => {
104+
const spy = spyOn()
105+
await openSessionStream(S)
106+
const c = call(spy)
107+
expect(c.method).toBe('GET')
108+
expect(c.url).toBe(`${BASE}/v1/sessions/sesn_1/events/stream`)
109+
expect(c.headers.accept).toBe('text/event-stream')
110+
})
111+
112+
it('sends the SEPARATE memory beta on memory-store reads, never the managed-agents one', async () => {
113+
// Combining the two headers on one request is a documented 400, so the
114+
// memory-store family must carry its own and only its own.
115+
const spy = spyOn({ data: [], next_page: null })
116+
await managedAgentsList({ ...AUTH, path: '/v1/memory_stores', beta: AGENT_MEMORY_BETA })
117+
const c = call(spy)
118+
expect(c.headers['anthropic-beta']).toBe(AGENT_MEMORY_BETA)
119+
expect(AGENT_MEMORY_BETA).not.toBe(MANAGED_AGENTS_BETA)
120+
})
121+
122+
it('sets content-type only on requests that carry a body', async () => {
123+
const post = spyOn({ id: 'sesn_1' })
124+
await createSession({ ...AUTH, agentId: 'a', environmentId: 'e' })
125+
expect(call(post).headers['content-type']).toBe('application/json')
126+
127+
const get = spyOn({ status: 'idle' })
128+
await retrieveSession(S)
129+
expect(call(get).headers['content-type']).toBeUndefined()
130+
})
131+
})

0 commit comments

Comments
 (0)