Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,541 @@
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { test } from 'vitest'
|
||||
|
||||
import {
|
||||
DEFAULT_HEALTH_PROBE_TIMEOUT_MS,
|
||||
isAuthRejectionError,
|
||||
isGatedMissingHealthError,
|
||||
isMissingHealthEndpointError,
|
||||
isNousCloudAgentUrl,
|
||||
isReauthRequiredError,
|
||||
isServerSideHttpError,
|
||||
makeNousCloudBackendDownError,
|
||||
makeUnsignedOauthError,
|
||||
waitForHermesReady
|
||||
} from './backend-health'
|
||||
|
||||
const GATE_401 = '401: {"error":"unauthenticated","detail":"Unauthorized","reason":"no_cookie","login_url":"/login"}'
|
||||
|
||||
test('uses lightweight /api/health for current backends', async () => {
|
||||
const calls: string[][] = []
|
||||
|
||||
await waitForHermesReady('http://127.0.0.1:9000/', {
|
||||
token: 'secret-token',
|
||||
fetchPublicJson: async url => {
|
||||
calls.push(['public', url])
|
||||
|
||||
return { ok: true }
|
||||
},
|
||||
fetchJson: async url => {
|
||||
calls.push(['token', url])
|
||||
throw new Error('status should not be called')
|
||||
},
|
||||
sleep: async () => {},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
})
|
||||
|
||||
assert.deepEqual(calls, [['public', 'http://127.0.0.1:9000/api/health']])
|
||||
})
|
||||
|
||||
test('falls back to /api/status only for old backends without /api/health', async () => {
|
||||
const calls: string[][] = []
|
||||
|
||||
await waitForHermesReady('http://127.0.0.1:9000', {
|
||||
token: 'secret-token',
|
||||
fetchPublicJson: async url => {
|
||||
calls.push(['public', url])
|
||||
|
||||
throw new Error('404: {"detail":"Not Found"}')
|
||||
},
|
||||
fetchJson: async (url, token) => {
|
||||
calls.push(['token', url, token ?? ''])
|
||||
|
||||
return { version: 'old' }
|
||||
},
|
||||
sleep: async () => {},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
})
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
['public', 'http://127.0.0.1:9000/api/health'],
|
||||
['token', 'http://127.0.0.1:9000/api/status', 'secret-token']
|
||||
])
|
||||
})
|
||||
|
||||
test('does not fall back to heavyweight /api/status for transient health failures', async () => {
|
||||
const calls: string[][] = []
|
||||
let currentTime = 0
|
||||
|
||||
await assert.rejects(
|
||||
waitForHermesReady('http://127.0.0.1:9000', {
|
||||
fetchPublicJson: async url => {
|
||||
calls.push(['public', url])
|
||||
throw new Error('Timed out connecting to Hermes backend after 15000ms')
|
||||
},
|
||||
fetchJson: async url => {
|
||||
calls.push(['token', url])
|
||||
},
|
||||
sleep: async () => {},
|
||||
now: () => {
|
||||
currentTime += 20
|
||||
|
||||
return currentTime
|
||||
},
|
||||
timeoutMs: 50,
|
||||
pollMs: 1
|
||||
}),
|
||||
/Timed out connecting/
|
||||
)
|
||||
|
||||
assert.ok(calls.length > 0)
|
||||
assert.ok(calls.every(call => call[0] === 'public' && call[1].endsWith('/api/health')))
|
||||
})
|
||||
|
||||
test('probes health on a short timeout but leaves the legacy fallback its own', async () => {
|
||||
const timeouts: (number | undefined)[] = []
|
||||
|
||||
await waitForHermesReady('http://127.0.0.1:9000', {
|
||||
fetchPublicJson: async (_url, options) => {
|
||||
timeouts.push(options?.timeoutMs)
|
||||
|
||||
throw new Error('404: {"detail":"Not Found"}')
|
||||
},
|
||||
fetchJson: async (_url, _token, options) => {
|
||||
timeouts.push(options?.timeoutMs)
|
||||
|
||||
return { version: 'old' }
|
||||
},
|
||||
sleep: async () => {},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
})
|
||||
|
||||
assert.deepEqual(timeouts, [DEFAULT_HEALTH_PROBE_TIMEOUT_MS, undefined])
|
||||
})
|
||||
|
||||
test('aborts as superseded when the bootstrap signal fires', async () => {
|
||||
const controller = new AbortController()
|
||||
controller.abort()
|
||||
|
||||
await assert.rejects(
|
||||
waitForHermesReady('http://127.0.0.1:9000', {
|
||||
signal: controller.signal,
|
||||
fetchPublicJson: async () => {
|
||||
throw new Error('should not probe after abort')
|
||||
},
|
||||
fetchJson: async () => {
|
||||
throw new Error('should not probe after abort')
|
||||
},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
}),
|
||||
(error: any) => error.kind === 'superseded'
|
||||
)
|
||||
})
|
||||
|
||||
test('recognizes missing-route shapes only', () => {
|
||||
assert.equal(isMissingHealthEndpointError(new Error('404: {"detail":"Not Found"}')), true)
|
||||
assert.equal(
|
||||
isMissingHealthEndpointError(
|
||||
new Error('Expected JSON from /api/health but got HTML. The endpoint is likely missing on the Hermes backend.')
|
||||
),
|
||||
true
|
||||
)
|
||||
assert.equal(isMissingHealthEndpointError(new Error('Timed out connecting to Hermes backend after 15000ms')), false)
|
||||
assert.equal(isMissingHealthEndpointError(new Error('500: boom')), false)
|
||||
})
|
||||
|
||||
// --- Gated backends that predate /api/health (release 0.19.0 and earlier) ---
|
||||
//
|
||||
// The dashboard auth gate runs ahead of the SPA catch-all, so on a backend
|
||||
// without the route an ANONYMOUS probe is rejected as unauthenticated rather
|
||||
// than 404 — verified against a simulated 0.19.0 backend:
|
||||
// credential-free: /api/health -> 401 no_cookie, /api/status -> 200
|
||||
// credentialed: /api/health -> 404, /api/sessions -> 200
|
||||
|
||||
test('anonymous gate-shaped 401 falls back to /api/status (backend predates /api/health)', async () => {
|
||||
const calls: string[][] = []
|
||||
|
||||
await waitForHermesReady('http://192.168.1.132:9119', {
|
||||
token: null,
|
||||
fetchPublicJson: async url => {
|
||||
calls.push(['public', url])
|
||||
throw new Error(GATE_401)
|
||||
},
|
||||
fetchJson: async (url, token) => {
|
||||
calls.push(['token', url, token == null ? 'null' : token])
|
||||
|
||||
return { version: '0.19.0', auth_required: true }
|
||||
},
|
||||
sleep: async () => {},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
})
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
['public', 'http://192.168.1.132:9119/api/health'],
|
||||
['token', 'http://192.168.1.132:9119/api/status', 'null']
|
||||
])
|
||||
})
|
||||
|
||||
test('a credentialed 401 fails fast for reauth instead of reporting a dead session ready', async () => {
|
||||
// The regression a blanket 401->fallback introduces: /api/status is public,
|
||||
// so an expired session would answer 200 and boot would report "ready",
|
||||
// deferring the no_cookie to the first real API call.
|
||||
const calls: string[][] = []
|
||||
|
||||
await assert.rejects(
|
||||
waitForHermesReady('https://gateway.example', {
|
||||
token: 'session-token',
|
||||
fetchPublicJson: async () => {
|
||||
throw new Error('public probe must not be used when credentialed')
|
||||
},
|
||||
fetchJson: async url => {
|
||||
calls.push(['status', url])
|
||||
|
||||
return { version: '0.19.0' }
|
||||
},
|
||||
probeHealth: async url => {
|
||||
calls.push(['probe', url])
|
||||
throw new Error(GATE_401)
|
||||
},
|
||||
probeIsCredentialed: true,
|
||||
sleep: async () => {},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
}),
|
||||
(error: any) => {
|
||||
assert.equal(isReauthRequiredError(error), true)
|
||||
assert.equal(error.needsOauthLogin, true)
|
||||
assert.match(error.message, /remote gateway session has expired/i)
|
||||
|
||||
return true
|
||||
}
|
||||
)
|
||||
|
||||
// Fail fast: never reached the public /api/status leg.
|
||||
assert.deepEqual(calls, [['probe', 'https://gateway.example/api/health']])
|
||||
})
|
||||
|
||||
test('unsigned OAuth is a terminal reauth failure; needsOauthLogin alone is not', () => {
|
||||
// The unsigned-in throw must set isReauthRequired so startHermes latches.
|
||||
// needsOauthLogin alone (ticket 401/403) stays a Sign-in hint, not a latch —
|
||||
// a lapsed AT cookie can still rotate from a live RT on the next mint.
|
||||
const unsigned = makeUnsignedOauthError() as any
|
||||
|
||||
assert.equal(unsigned.needsOauthLogin, true)
|
||||
assert.equal(unsigned.isReauthRequired, true)
|
||||
assert.equal(isReauthRequiredError(unsigned), true)
|
||||
assert.match(unsigned.message, /not signed in/i)
|
||||
assert.equal(isReauthRequiredError({ needsOauthLogin: true }), false)
|
||||
assert.equal(isReauthRequiredError(new Error('Could not reach the remote Hermes gateway')), false)
|
||||
})
|
||||
|
||||
test('a credentialed 403 is also a terminal reauth failure', async () => {
|
||||
await assert.rejects(
|
||||
waitForHermesReady('https://gateway.example', {
|
||||
fetchPublicJson: async () => ({}),
|
||||
fetchJson: async () => ({}),
|
||||
probeHealth: async () => {
|
||||
throw new Error('403: {"detail":"Forbidden"}')
|
||||
},
|
||||
probeIsCredentialed: true,
|
||||
sleep: async () => {},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
}),
|
||||
(error: any) => isReauthRequiredError(error)
|
||||
)
|
||||
})
|
||||
|
||||
test('a credentialed probe still uses the 404 fallback for a genuinely missing route', async () => {
|
||||
// With credentials the gate lets the request through to the SPA catch-all,
|
||||
// so an old backend answers a real 404 — that must still fall back, not be
|
||||
// mistaken for a rejected session.
|
||||
const calls: string[][] = []
|
||||
|
||||
await waitForHermesReady('https://gateway.example', {
|
||||
token: 'session-token',
|
||||
fetchPublicJson: async () => {
|
||||
throw new Error('public probe must not be used when credentialed')
|
||||
},
|
||||
fetchJson: async url => {
|
||||
calls.push(['status', url])
|
||||
|
||||
return { version: '0.19.0' }
|
||||
},
|
||||
probeHealth: async url => {
|
||||
calls.push(['probe', url])
|
||||
throw new Error('404: {"detail":"Not Found"}')
|
||||
},
|
||||
probeIsCredentialed: true,
|
||||
sleep: async () => {},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
})
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
['probe', 'https://gateway.example/api/health'],
|
||||
['status', 'https://gateway.example/api/status']
|
||||
])
|
||||
})
|
||||
|
||||
test('a non-gate 401 keeps polling rather than skipping a misconfigured health route', async () => {
|
||||
const calls: string[][] = []
|
||||
let currentTime = 0
|
||||
|
||||
await assert.rejects(
|
||||
waitForHermesReady('http://127.0.0.1:9000', {
|
||||
fetchPublicJson: async url => {
|
||||
calls.push(['public', url])
|
||||
throw new Error('401: {"detail":"Unauthorized"}')
|
||||
},
|
||||
fetchJson: async url => {
|
||||
calls.push(['token', url])
|
||||
},
|
||||
sleep: async () => {},
|
||||
now: () => {
|
||||
currentTime += 20
|
||||
|
||||
return currentTime
|
||||
},
|
||||
timeoutMs: 50,
|
||||
pollMs: 1
|
||||
}),
|
||||
/401: \{"detail":"Unauthorized"\}/
|
||||
)
|
||||
|
||||
assert.ok(calls.length > 0)
|
||||
assert.ok(calls.every(call => call[0] === 'public' && call[1].endsWith('/api/health')))
|
||||
})
|
||||
|
||||
test('credentialed 5xx and 429 keep polling — only 401/403 are terminal', async () => {
|
||||
for (const transient of ['500: boom', '429: {"detail":"Too Many Requests"}']) {
|
||||
let attempts = 0
|
||||
let currentTime = 0
|
||||
|
||||
await assert.rejects(
|
||||
waitForHermesReady('https://gateway.example', {
|
||||
fetchPublicJson: async () => ({}),
|
||||
fetchJson: async () => ({}),
|
||||
probeHealth: async () => {
|
||||
attempts += 1
|
||||
throw new Error(transient)
|
||||
},
|
||||
probeIsCredentialed: true,
|
||||
sleep: async () => {},
|
||||
now: () => {
|
||||
currentTime += 20
|
||||
|
||||
return currentTime
|
||||
},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
}),
|
||||
(error: any) => isReauthRequiredError(error) === false
|
||||
)
|
||||
|
||||
assert.ok(attempts > 1, `${transient} should have retried, got ${attempts} attempt(s)`)
|
||||
}
|
||||
})
|
||||
|
||||
test('error-shape predicates', () => {
|
||||
assert.equal(isGatedMissingHealthError(new Error(GATE_401)), true)
|
||||
assert.equal(isGatedMissingHealthError(new Error('401: {"detail":"Unauthorized"}')), false)
|
||||
assert.equal(isGatedMissingHealthError(new Error('404: {"detail":"Not Found"}')), false)
|
||||
|
||||
assert.equal(isAuthRejectionError(new Error(GATE_401)), true)
|
||||
assert.equal(isAuthRejectionError(new Error('403: {"detail":"Forbidden"}')), true)
|
||||
assert.equal(isAuthRejectionError(new Error('404: {"detail":"Not Found"}')), false)
|
||||
assert.equal(isAuthRejectionError(new Error('429: slow down')), false)
|
||||
assert.equal(isAuthRejectionError(new Error('500: boom')), false)
|
||||
|
||||
// A gated 401 must NOT be conflated with a missing route by the 404 predicate.
|
||||
assert.equal(isMissingHealthEndpointError(new Error(GATE_401)), false)
|
||||
})
|
||||
|
||||
test('isServerSideHttpError detects 502/503/504', () => {
|
||||
// 503 — server-side fault
|
||||
const result503 = isServerSideHttpError(new Error('503: Service Unavailable'))
|
||||
assert.ok(result503, 'should detect 503')
|
||||
assert.equal(result503?.statusCode, 503)
|
||||
assert.equal(result503?.detail, '503: Service Unavailable')
|
||||
|
||||
// 502
|
||||
const result502 = isServerSideHttpError(new Error('502: Bad Gateway'))
|
||||
assert.ok(result502, 'should detect 502')
|
||||
assert.equal(result502?.statusCode, 502)
|
||||
|
||||
// 504
|
||||
const result504 = isServerSideHttpError(new Error('504: Gateway Timeout'))
|
||||
assert.ok(result504, 'should detect 504')
|
||||
assert.equal(result504?.statusCode, 504)
|
||||
|
||||
// 500 is NOT a server-side HTTP error per our definition (keeps polling)
|
||||
const result500 = isServerSideHttpError(new Error('500: Internal Server Error'))
|
||||
assert.equal(result500, null)
|
||||
|
||||
// 401/403/404/429 are not server-side faults
|
||||
assert.equal(isServerSideHttpError(new Error('401: Unauthorized')), null)
|
||||
assert.equal(isServerSideHttpError(new Error('403: Forbidden')), null)
|
||||
assert.equal(isServerSideHttpError(new Error('404: Not Found')), null)
|
||||
assert.equal(isServerSideHttpError(new Error('429: Too Many Requests')), null)
|
||||
|
||||
// Non-HTTP errors (timeouts, network failures) don't match the pattern
|
||||
assert.equal(isServerSideHttpError(new Error('connect ECONNREFUSED')), null)
|
||||
assert.equal(isServerSideHttpError(null), null)
|
||||
assert.equal(isServerSideHttpError('503: something'), null) // not an Error
|
||||
})
|
||||
|
||||
test('isNousCloudAgentUrl detects cloud agent hosts', () => {
|
||||
// Positive cases
|
||||
assert.equal(isNousCloudAgentUrl('https://ares-3009.agents.nousresearch.com'), true)
|
||||
assert.equal(isNousCloudAgentUrl('https://ares-3009.agents.nousresearch.com/api/health'), true)
|
||||
assert.equal(isNousCloudAgentUrl('http://test.agents.nousresearch.com'), true)
|
||||
|
||||
// Negative cases
|
||||
assert.equal(isNousCloudAgentUrl('http://127.0.0.1:9000'), false)
|
||||
assert.equal(isNousCloudAgentUrl('https://gateway.example.com'), false)
|
||||
assert.equal(isNousCloudAgentUrl('https://nousresearch.com'), false)
|
||||
assert.equal(isNousCloudAgentUrl('not-a-url'), false)
|
||||
})
|
||||
|
||||
test('waitForHermesReady surfaces actionable error for cloud agent 503', async () => {
|
||||
let attempts = 0
|
||||
const currentTime = { value: 0 }
|
||||
|
||||
try {
|
||||
await waitForHermesReady('https://ares-3009.agents.nousresearch.com', {
|
||||
fetchPublicJson: async () => {
|
||||
attempts++
|
||||
// Always return 503
|
||||
throw new Error('503: Service Unavailable')
|
||||
},
|
||||
fetchJson: async () => {
|
||||
throw new Error('503: Service Unavailable')
|
||||
},
|
||||
sleep: async () => {},
|
||||
// Advance the mock clock per poll — a frozen now() never crosses the
|
||||
// deadline and the readiness loop spins forever (hung the whole vitest
|
||||
// electron project for 20m in CI).
|
||||
now: () => {
|
||||
currentTime.value += 20
|
||||
|
||||
return currentTime.value
|
||||
},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
})
|
||||
assert.fail('should have thrown')
|
||||
} catch (error: any) {
|
||||
assert.ok(error.message.includes('Nous Cloud agent'), `unexpected message: ${error.message}`)
|
||||
assert.ok(error.message.includes('503'), `should mention status code: ${error.message}`)
|
||||
assert.ok(error.message.includes('portal.nousresearch.com'), `should mention portal: ${error.message}`)
|
||||
assert.ok(error.message.includes('discord.gg/NousResearch'), `should mention Discord: ${error.message}`)
|
||||
assert.equal(error.isCloudBackendDown, true)
|
||||
assert.equal(error.statusCode, 503)
|
||||
assert.ok(attempts > 1, 'should have retried before failing')
|
||||
}
|
||||
})
|
||||
|
||||
test('waitForHermesReady does not cloud-wrap non-cloud 503 errors', async () => {
|
||||
const currentTime = { value: 0 }
|
||||
|
||||
try {
|
||||
await waitForHermesReady('http://127.0.0.1:9000', {
|
||||
fetchPublicJson: async () => {
|
||||
throw new Error('503: Service Unavailable')
|
||||
},
|
||||
fetchJson: async () => {
|
||||
throw new Error('503: Service Unavailable')
|
||||
},
|
||||
sleep: async () => {},
|
||||
// Same advancing clock as above — frozen now() = infinite loop.
|
||||
now: () => {
|
||||
currentTime.value += 20
|
||||
|
||||
return currentTime.value
|
||||
},
|
||||
timeoutMs: 100,
|
||||
pollMs: 1
|
||||
})
|
||||
assert.fail('should have thrown')
|
||||
} catch (error: any) {
|
||||
// Non-cloud URLs get the generic message
|
||||
assert.ok(error.message.includes('did not become ready'), `unexpected message: ${error.message}`)
|
||||
assert.equal(error.isCloudBackendDown, undefined)
|
||||
}
|
||||
})
|
||||
|
||||
test('isServerSideHttpError detects structured statusCode even when the message is opaque', () => {
|
||||
const err = new Error('upstream unavailable') as any
|
||||
err.statusCode = 503
|
||||
const result = isServerSideHttpError(err)
|
||||
assert.ok(result)
|
||||
assert.equal(result?.statusCode, 503)
|
||||
assert.equal(result?.detail, 'upstream unavailable')
|
||||
|
||||
const err502 = new Error('bad gateway') as any
|
||||
err502.statusCode = 502
|
||||
assert.equal(isServerSideHttpError(err502)?.statusCode, 502)
|
||||
|
||||
const err504 = new Error('gateway timeout') as any
|
||||
err504.statusCode = 504
|
||||
assert.equal(isServerSideHttpError(err504)?.statusCode, 504)
|
||||
})
|
||||
|
||||
test('isServerSideHttpError rejects non-Error inputs even with a 503-shaped value', () => {
|
||||
// The structured path requires an actual Error (the fetch layer attaches
|
||||
// statusCode to an Error instance); a bare string/null/number must not be
|
||||
// misclassified by the legacy prefix fallback.
|
||||
assert.equal(isServerSideHttpError('503: something'), null)
|
||||
assert.equal(isServerSideHttpError({ statusCode: 503 }), null)
|
||||
assert.equal(isServerSideHttpError(null), null)
|
||||
assert.equal(isServerSideHttpError(503), null)
|
||||
})
|
||||
|
||||
test('isServerSideHttpError structured path excludes 500/401/403/404/429 even when statusCode is attached', () => {
|
||||
for (const code of [500, 401, 403, 404, 429]) {
|
||||
const err = new Error(`HTTP ${code}`) as any
|
||||
err.statusCode = code
|
||||
assert.equal(isServerSideHttpError(err), null, `should reject statusCode ${code}`)
|
||||
}
|
||||
})
|
||||
|
||||
test('makeNousCloudBackendDownError produces the Cloud shape and preserves cause', () => {
|
||||
const err = new Error('upstream unavailable') as any
|
||||
err.statusCode = 503
|
||||
const result = makeNousCloudBackendDownError('https://ares-3009.agents.nousresearch.com', err)
|
||||
assert.ok(result)
|
||||
assert.equal((result as any).isCloudBackendDown, true)
|
||||
assert.equal((result as any).statusCode, 503)
|
||||
assert.equal((result as any).cause, err)
|
||||
assert.ok(result?.message.includes('Nous Cloud agent ares-3009.agents.nousresearch.com is down'))
|
||||
})
|
||||
|
||||
test('makeNousCloudBackendDownError returns null for a Cloud 401 (routes to reauth)', () => {
|
||||
const err = new Error('Unauthorized') as any
|
||||
err.statusCode = 401
|
||||
assert.equal(makeNousCloudBackendDownError('https://ares-3009.agents.nousresearch.com', err), null)
|
||||
})
|
||||
|
||||
test('makeNousCloudBackendDownError returns null for a non-Cloud 503 (generic remote failure)', () => {
|
||||
const err = new Error('Service Unavailable') as any
|
||||
err.statusCode = 503
|
||||
assert.equal(makeNousCloudBackendDownError('https://gateway.example.com', err), null)
|
||||
assert.equal(makeNousCloudBackendDownError('http://127.0.0.1:9000', err), null)
|
||||
})
|
||||
|
||||
test('makeNousCloudBackendDownError preserves legacy string-prefix compatibility', () => {
|
||||
const result = makeNousCloudBackendDownError(
|
||||
'https://ares-3009.agents.nousresearch.com',
|
||||
new Error('503: Service Unavailable')
|
||||
)
|
||||
|
||||
assert.ok(result)
|
||||
assert.equal((result as any).isCloudBackendDown, true)
|
||||
assert.equal((result as any).statusCode, 503)
|
||||
})
|
||||
Reference in New Issue
Block a user