125 lines
5.6 KiB
JavaScript
125 lines
5.6 KiB
JavaScript
"use strict"
|
|
|
|
const test = require("node:test")
|
|
const assert = require("node:assert/strict")
|
|
const { execSync } = require("child_process")
|
|
const path = require("path")
|
|
|
|
const root = path.join(__dirname, "..")
|
|
|
|
function getCompose() {
|
|
try {
|
|
const output = execSync(
|
|
"docker compose --env-file .env.example --profile pre-gateway --profile post-gateway config --format json",
|
|
{ cwd: root, encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
)
|
|
return JSON.parse(output)
|
|
} catch (err) {
|
|
return null
|
|
}
|
|
}
|
|
|
|
test("compose config is valid and has correct service structure", (t) => {
|
|
const config = getCompose()
|
|
if (!config) {
|
|
t.skip("docker compose not available or config invalid")
|
|
return
|
|
}
|
|
|
|
const services = config.services || {}
|
|
const serviceNames = Object.keys(services)
|
|
|
|
// Must have all public services plus one shared native Hermes gateway upstream.
|
|
assert(serviceNames.includes("hermes-postgres"), "missing hermes-postgres")
|
|
assert(serviceNames.includes("hermes-control-plane"), "missing hermes-control-plane")
|
|
assert(serviceNames.includes("hermes-ai-upstream"), "missing hermes-ai-upstream")
|
|
assert(serviceNames.includes("hermes-pre-api"), "missing hermes-pre-api")
|
|
assert(serviceNames.includes("hermes-post-api"), "missing hermes-post-api")
|
|
|
|
// Upstream services must NOT have host ports
|
|
const aiUpstream = services["hermes-ai-upstream"]
|
|
const preApi = services["hermes-pre-api"]
|
|
const postApi = services["hermes-post-api"]
|
|
|
|
assert.deepEqual(aiUpstream.profiles, ["pre-gateway", "post-gateway"], "hermes-ai-upstream should start with either API profile")
|
|
assert.deepEqual(preApi.profiles, ["pre-gateway"], "hermes-pre-api should be opt-in")
|
|
assert.deepEqual(postApi.profiles, ["post-gateway"], "hermes-post-api should be opt-in")
|
|
|
|
assert(!aiUpstream.ports || aiUpstream.ports.length === 0, "hermes-ai-upstream should not publish host ports")
|
|
|
|
// Public gateway services must publish correct ports
|
|
assert(preApi.ports && preApi.ports.length > 0, "hermes-pre-api should publish ports")
|
|
assert(postApi.ports && postApi.ports.length > 0, "hermes-post-api should publish ports")
|
|
|
|
const controlPlane = services["hermes-control-plane"]
|
|
assert.equal(
|
|
controlPlane.image,
|
|
"10.0.3.6:4000/zachariahsharma/hermes-control-plane:latest",
|
|
"Hermes services should default to the internal HTTP registry image"
|
|
)
|
|
const controlPlanePort = controlPlane.ports?.find((port) => Number(port.target) === 7843)
|
|
const preApiPort = preApi.ports?.find((port) => Number(port.target) === 8645)
|
|
const postApiPort = postApi.ports?.find((port) => Number(port.target) === 8646)
|
|
|
|
assert.equal(controlPlanePort?.host_ip, "127.0.0.1", "control plane should bind to loopback by default")
|
|
assert.equal(preApiPort?.host_ip, "127.0.0.1", "pre API should bind to loopback by default")
|
|
assert.equal(postApiPort?.host_ip, "127.0.0.1", "post API should bind to loopback by default")
|
|
|
|
// Shared native Hermes upstream must have API_SERVER_ENABLED=true.
|
|
const aiUpstreamEnv = aiUpstream.environment || {}
|
|
assert.equal(String(aiUpstreamEnv.API_SERVER_ENABLED), "true", "hermes-ai-upstream API_SERVER_ENABLED must be true")
|
|
assert(aiUpstreamEnv.API_SERVER_KEY, "hermes-ai-upstream must have an internal API server key")
|
|
|
|
// Control plane and gateway services must receive DATABASE_URL
|
|
const controlPlaneEnv = services["hermes-control-plane"].environment || {}
|
|
assert("DATABASE_URL" in controlPlaneEnv, "hermes-control-plane must have DATABASE_URL")
|
|
assert.equal(
|
|
controlPlaneEnv.HERMES_EXE,
|
|
"/opt/hermes-agent/venv/bin/hermes",
|
|
"Hermes executable must be baked outside the mutable Hermes state mount"
|
|
)
|
|
|
|
const expectedStateTargets = [
|
|
"/home/hermes/.hermes",
|
|
"/home/hermes/.codex",
|
|
"/home/hermes/.claude",
|
|
"/home/hermes/.gemini",
|
|
]
|
|
for (const serviceName of ["hermes-control-plane", "hermes-ai-upstream"]) {
|
|
const volumes = services[serviceName].volumes || []
|
|
for (const target of expectedStateTargets) {
|
|
const mount = volumes.find((volume) => volume.target === target)
|
|
assert.equal(mount?.type, "bind", `${serviceName} should keep ${target} as a host bind mount`)
|
|
}
|
|
}
|
|
|
|
const preApiEnv = preApi.environment || {}
|
|
assert("DATABASE_URL" in preApiEnv, "hermes-pre-api must have DATABASE_URL")
|
|
assert.equal(
|
|
preApiEnv.HERMES_UPSTREAM_API_KEY,
|
|
aiUpstreamEnv.API_SERVER_KEY,
|
|
"pre API must authenticate to the native Hermes API with the same internal key"
|
|
)
|
|
|
|
const postApiEnv = postApi.environment || {}
|
|
assert("DATABASE_URL" in postApiEnv, "hermes-post-api must have DATABASE_URL")
|
|
assert.equal(
|
|
postApiEnv.HERMES_UPSTREAM_API_KEY,
|
|
aiUpstreamEnv.API_SERVER_KEY,
|
|
"post API must authenticate to the native Hermes API with the same internal key"
|
|
)
|
|
|
|
// Admin variables only go to control plane
|
|
assert("HERMES_ADMIN_USERNAME" in controlPlaneEnv, "hermes-control-plane must have HERMES_ADMIN_USERNAME")
|
|
assert(!("HERMES_ADMIN_USERNAME" in preApiEnv), "hermes-pre-api must NOT have HERMES_ADMIN_USERNAME")
|
|
assert(!("HERMES_ADMIN_USERNAME" in postApiEnv), "hermes-post-api must NOT have HERMES_ADMIN_USERNAME")
|
|
|
|
// Gateway services run api-gateway.cjs
|
|
const preApiCommand = preApi.command
|
|
const postApiCommand = postApi.command
|
|
const preApiCmd = Array.isArray(preApiCommand) ? preApiCommand.join(" ") : String(preApiCommand || "")
|
|
const postApiCmd = Array.isArray(postApiCommand) ? postApiCommand.join(" ") : String(postApiCommand || "")
|
|
assert.match(preApiCmd, /api-gateway\.cjs/, "hermes-pre-api must run api-gateway.cjs")
|
|
assert.match(postApiCmd, /api-gateway\.cjs/, "hermes-post-api must run api-gateway.cjs")
|
|
})
|