From 7de30dd8d846b60eae7b956b197db4fb3b9b5c7f Mon Sep 17 00:00:00 2001 From: CelestialCreator Date: Fri, 27 Mar 2026 18:15:22 +0530 Subject: [PATCH 1/3] feat(infra): add Helm chart for production K8s deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a standalone Helm chart with gateway and web deployments, HPA for both, and three ingress options: Traefik IngressRoute, Emissary v2 Mapping (with optional gRPC), and standard K8s Ingress. External-only infra deps (PostgreSQL, Valkey, RabbitMQ) — no Bitnami subcharts. --- charts/openzosma/.helmignore | 9 ++ charts/openzosma/Chart.yaml | 18 +++ charts/openzosma/templates/NOTES.txt | 48 ++++++ charts/openzosma/templates/_helpers.tpl | 97 +++++++++++ charts/openzosma/templates/configmap.yaml | 57 +++++++ .../templates/gateway-deployment.yaml | 77 +++++++++ charts/openzosma/templates/gateway-hpa.yaml | 23 +++ .../openzosma/templates/gateway-service.yaml | 16 ++ .../openzosma/templates/ingress-emissary.yaml | 106 ++++++++++++ .../openzosma/templates/ingress-traefik.yaml | 77 +++++++++ charts/openzosma/templates/ingress.yaml | 60 +++++++ charts/openzosma/templates/secret.yaml | 56 +++++++ .../openzosma/templates/serviceaccount.yaml | 12 ++ .../openzosma/templates/web-deployment.yaml | 69 ++++++++ charts/openzosma/templates/web-hpa.yaml | 23 +++ charts/openzosma/templates/web-service.yaml | 18 +++ charts/openzosma/values.yaml | 153 ++++++++++++++++++ 17 files changed, 919 insertions(+) create mode 100644 charts/openzosma/.helmignore create mode 100644 charts/openzosma/Chart.yaml create mode 100644 charts/openzosma/templates/NOTES.txt create mode 100644 charts/openzosma/templates/_helpers.tpl create mode 100644 charts/openzosma/templates/configmap.yaml create mode 100644 charts/openzosma/templates/gateway-deployment.yaml create mode 100644 charts/openzosma/templates/gateway-hpa.yaml create mode 100644 charts/openzosma/templates/gateway-service.yaml create mode 100644 charts/openzosma/templates/ingress-emissary.yaml create mode 100644 charts/openzosma/templates/ingress-traefik.yaml create mode 100644 charts/openzosma/templates/ingress.yaml create mode 100644 charts/openzosma/templates/secret.yaml create mode 100644 charts/openzosma/templates/serviceaccount.yaml create mode 100644 charts/openzosma/templates/web-deployment.yaml create mode 100644 charts/openzosma/templates/web-hpa.yaml create mode 100644 charts/openzosma/templates/web-service.yaml create mode 100644 charts/openzosma/values.yaml diff --git a/charts/openzosma/.helmignore b/charts/openzosma/.helmignore new file mode 100644 index 0000000..f87ba5b --- /dev/null +++ b/charts/openzosma/.helmignore @@ -0,0 +1,9 @@ +.DS_Store +.git +.gitignore +.vscode +*.swp +*.bak +*.tmp +*.md +CLAUDE.md diff --git a/charts/openzosma/Chart.yaml b/charts/openzosma/Chart.yaml new file mode 100644 index 0000000..a27a33f --- /dev/null +++ b/charts/openzosma/Chart.yaml @@ -0,0 +1,18 @@ +apiVersion: v2 +name: openzosma +description: Self-hosted AI agent platform with sandboxed execution +type: application +version: 0.1.0 +appVersion: "0.0.4" +home: https://github.com/zosmaai/openzosma +sources: + - https://github.com/zosmaai/openzosma +keywords: + - ai + - agents + - sandbox + - llm + - self-hosted +maintainers: + - name: zosmaai + url: https://zosma.ai diff --git a/charts/openzosma/templates/NOTES.txt b/charts/openzosma/templates/NOTES.txt new file mode 100644 index 0000000..ee5ada7 --- /dev/null +++ b/charts/openzosma/templates/NOTES.txt @@ -0,0 +1,48 @@ + +OpenZosma has been deployed! + +{{- if .Values.ingress.enabled }} + +Access the application at: +{{- if .Values.ingress.tls.enabled }} + https://{{ .Values.ingress.host }} +{{- else }} + http://{{ .Values.ingress.host }} +{{- end }} + +Ingress type: {{ .Values.ingress.type }} +{{- end }} + +Components: + - Gateway: {{ include "openzosma.fullname" . }}-gateway (port {{ .Values.gateway.port }}) +{{- if .Values.web.enabled }} + - Web: {{ include "openzosma.fullname" . }}-web (port {{ .Values.web.port }}) +{{- end }} + +Sandbox mode: {{ .Values.sandbox.mode }} + +{{- if not .Values.postgresql.host }} + +WARNING: postgresql.host is not set. The gateway will fail to start without a database connection. +Set it with: --set postgresql.host=your-db-host +{{- end }} + +{{- if not .Values.auth.secret }} + +WARNING: auth.secret is not set. Set a secure random string for session signing: + --set auth.secret=$(openssl rand -hex 32) +{{- end }} + +{{- if and (not .Values.llm.anthropicApiKey) (not .Values.llm.openaiApiKey) (not .Values.llm.localModelUrl) }} + +WARNING: No LLM API key configured. Set at least one: + --set llm.anthropicApiKey=sk-ant-... + --set llm.openaiApiKey=sk-... + --set llm.localModelUrl=http://your-model:8080/v1 +{{- end }} + +Run database migrations: + kubectl exec -it deploy/{{ include "openzosma.fullname" . }}-gateway -- npx db-migrate up + kubectl exec -it deploy/{{ include "openzosma.fullname" . }}-gateway -- npx db-migrate up --config config/auth-database.json + +Documentation: https://github.com/zosmaai/openzosma diff --git a/charts/openzosma/templates/_helpers.tpl b/charts/openzosma/templates/_helpers.tpl new file mode 100644 index 0000000..906125c --- /dev/null +++ b/charts/openzosma/templates/_helpers.tpl @@ -0,0 +1,97 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "openzosma.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +*/}} +{{- define "openzosma.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "openzosma.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels. +*/}} +{{- define "openzosma.labels" -}} +helm.sh/chart: {{ include "openzosma.chart" . }} +{{ include "openzosma.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels. +*/}} +{{- define "openzosma.selectorLabels" -}} +app.kubernetes.io/name: {{ include "openzosma.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Gateway selector labels. +*/}} +{{- define "openzosma.gateway.selectorLabels" -}} +{{ include "openzosma.selectorLabels" . }} +app.kubernetes.io/component: gateway +{{- end }} + +{{/* +Web selector labels. +*/}} +{{- define "openzosma.web.selectorLabels" -}} +{{ include "openzosma.selectorLabels" . }} +app.kubernetes.io/component: web +{{- end }} + +{{/* +Service account name. +*/}} +{{- define "openzosma.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "openzosma.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{/* +Construct the DATABASE_URL from postgresql values. +*/}} +{{- define "openzosma.databaseUrl" -}} +postgresql://{{ .Values.postgresql.username }}:$(DB_PASS)@{{ .Values.postgresql.host }}:{{ .Values.postgresql.port }}/{{ .Values.postgresql.database }} +{{- end }} + +{{/* +Name of the secret containing sensitive values. +*/}} +{{- define "openzosma.secretName" -}} +{{ include "openzosma.fullname" . }} +{{- end }} + +{{/* +Name of the configmap. +*/}} +{{- define "openzosma.configmapName" -}} +{{ include "openzosma.fullname" . }} +{{- end }} diff --git a/charts/openzosma/templates/configmap.yaml b/charts/openzosma/templates/configmap.yaml new file mode 100644 index 0000000..ef5f98b --- /dev/null +++ b/charts/openzosma/templates/configmap.yaml @@ -0,0 +1,57 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "openzosma.configmapName" . }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} +data: + # Database + DB_HOST: {{ .Values.postgresql.host | quote }} + DB_PORT: {{ .Values.postgresql.port | quote }} + DB_NAME: {{ .Values.postgresql.database | quote }} + DB_USER: {{ .Values.postgresql.username | quote }} + DB_POOL_SIZE: {{ .Values.postgresql.poolSize | quote }} + + # Valkey + {{- if .Values.valkey.url }} + VALKEY_URL: {{ .Values.valkey.url | quote }} + {{- end }} + + # RabbitMQ + {{- if .Values.rabbitmq.url }} + RABBITMQ_URL: {{ .Values.rabbitmq.url | quote }} + {{- end }} + + # Gateway + GATEWAY_PORT: {{ .Values.gateway.port | quote }} + GATEWAY_HOST: "0.0.0.0" + {{- if .Values.auth.url }} + PUBLIC_URL: {{ .Values.auth.url | quote }} + {{- else }} + PUBLIC_URL: {{ printf "http://%s" .Values.ingress.host | quote }} + {{- end }} + + # Agent + OPENZOSMA_MODEL_PROVIDER: {{ .Values.agent.provider | quote }} + OPENZOSMA_MODEL_ID: {{ .Values.agent.modelId | quote }} + OPENZOSMA_WORKSPACE: {{ .Values.agent.workspace | quote }} + + # Sandbox / Orchestrator + OPENZOSMA_SANDBOX_MODE: {{ .Values.sandbox.mode | quote }} + SANDBOX_IMAGE: {{ .Values.sandbox.image | quote }} + SANDBOX_AGENT_PORT: {{ .Values.sandbox.agentPort | quote }} + MAX_SANDBOXES: {{ .Values.sandbox.maxSandboxes | quote }} + SANDBOX_IDLE_SUSPEND_MS: {{ .Values.sandbox.idleSuspendMs | int | quote }} + SANDBOX_HEALTH_CHECK_INTERVAL_MS: {{ .Values.sandbox.healthCheckIntervalMs | quote }} + SANDBOX_READY_TIMEOUT_MS: {{ .Values.sandbox.readyTimeoutMs | quote }} + SANDBOX_POLICY_PATH: {{ .Values.sandbox.policyPath | quote }} + + # Auth + {{- if .Values.auth.url }} + AUTH_URL: {{ .Values.auth.url | quote }} + {{- end }} + + # Web + {{- if .Values.web.enabled }} + NEXT_PUBLIC_GATEWAY_URL: {{ printf "http://%s-gateway:%d" (include "openzosma.fullname" .) (.Values.gateway.port | int) | quote }} + {{- end }} diff --git a/charts/openzosma/templates/gateway-deployment.yaml b/charts/openzosma/templates/gateway-deployment.yaml new file mode 100644 index 0000000..63692b8 --- /dev/null +++ b/charts/openzosma/templates/gateway-deployment.yaml @@ -0,0 +1,77 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "openzosma.fullname" . }}-gateway + labels: + {{- include "openzosma.labels" . | nindent 4 }} + app.kubernetes.io/component: gateway +spec: + {{- if not .Values.gateway.autoscaling.enabled }} + replicas: {{ .Values.gateway.replicas }} + {{- end }} + selector: + matchLabels: + {{- include "openzosma.gateway.selectorLabels" . | nindent 6 }} + template: + metadata: + annotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha256sum }} + checksum/secret: {{ include (print $.Template.BasePath "/secret.yaml") . | sha256sum }} + labels: + {{- include "openzosma.gateway.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "openzosma.serviceAccountName" . }} + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + fsGroup: 1001 + containers: + - name: gateway + image: "{{ .Values.gateway.image.repository }}:{{ .Values.gateway.image.tag }}" + imagePullPolicy: {{ .Values.gateway.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.gateway.port }} + protocol: TCP + envFrom: + - configMapRef: + name: {{ include "openzosma.configmapName" . }} + - secretRef: + name: {{ include "openzosma.secretName" . }} + {{- if .Values.postgresql.existingSecret }} + env: + - name: DB_PASS + valueFrom: + secretKeyRef: + name: {{ .Values.postgresql.existingSecret }} + key: password + {{- end }} + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + {{- toYaml .Values.gateway.resources | nindent 12 }} + {{- with .Values.gateway.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.gateway.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.gateway.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/charts/openzosma/templates/gateway-hpa.yaml b/charts/openzosma/templates/gateway-hpa.yaml new file mode 100644 index 0000000..8bad0e0 --- /dev/null +++ b/charts/openzosma/templates/gateway-hpa.yaml @@ -0,0 +1,23 @@ +{{- if .Values.gateway.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "openzosma.fullname" . }}-gateway + labels: + {{- include "openzosma.labels" . | nindent 4 }} + app.kubernetes.io/component: gateway +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "openzosma.fullname" . }}-gateway + minReplicas: {{ .Values.gateway.autoscaling.minReplicas }} + maxReplicas: {{ .Values.gateway.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.gateway.autoscaling.targetCPU }} +{{- end }} diff --git a/charts/openzosma/templates/gateway-service.yaml b/charts/openzosma/templates/gateway-service.yaml new file mode 100644 index 0000000..6ddcd2e --- /dev/null +++ b/charts/openzosma/templates/gateway-service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "openzosma.fullname" . }}-gateway + labels: + {{- include "openzosma.labels" . | nindent 4 }} + app.kubernetes.io/component: gateway +spec: + type: ClusterIP + ports: + - port: {{ .Values.gateway.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "openzosma.gateway.selectorLabels" . | nindent 4 }} diff --git a/charts/openzosma/templates/ingress-emissary.yaml b/charts/openzosma/templates/ingress-emissary.yaml new file mode 100644 index 0000000..75395f6 --- /dev/null +++ b/charts/openzosma/templates/ingress-emissary.yaml @@ -0,0 +1,106 @@ +{{- if and .Values.ingress.enabled (eq .Values.ingress.type "emissary") }} +{{- $fullName := include "openzosma.fullname" . -}} +{{- $gatewayPort := .Values.gateway.port -}} +{{- $webPort := .Values.web.port -}} +{{- $ns := .Release.Namespace -}} +{{- $host := .Values.ingress.host -}} +{{- $timeout := .Values.ingress.emissary.timeoutMs -}} +{{- $idleTimeout := .Values.ingress.emissary.idleTimeoutMs -}} +# Gateway REST API +apiVersion: getambassador.io/v2 +kind: Mapping +metadata: + name: {{ $fullName }}-rest + namespace: {{ $ns }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + host: {{ $host }} + prefix: /api/ + service: {{ $fullName }}-gateway.{{ $ns }}:{{ $gatewayPort }} + timeout_ms: {{ $timeout }} +--- +# A2A protocol +apiVersion: getambassador.io/v2 +kind: Mapping +metadata: + name: {{ $fullName }}-a2a + namespace: {{ $ns }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} +spec: + host: {{ $host }} + prefix: /a2a + service: {{ $fullName }}-gateway.{{ $ns }}:{{ $gatewayPort }} + timeout_ms: {{ $timeout }} +--- +# Agent card +apiVersion: getambassador.io/v2 +kind: Mapping +metadata: + name: {{ $fullName }}-agent-card + namespace: {{ $ns }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} +spec: + host: {{ $host }} + prefix: /.well-known/agent.json + service: {{ $fullName }}-gateway.{{ $ns }}:{{ $gatewayPort }} + timeout_ms: {{ $timeout }} +--- +# WebSocket +apiVersion: getambassador.io/v2 +kind: Mapping +metadata: + name: {{ $fullName }}-ws + namespace: {{ $ns }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} +spec: + host: {{ $host }} + prefix: /ws + service: {{ $fullName }}-gateway.{{ $ns }}:{{ $gatewayPort }} + timeout_ms: {{ $idleTimeout }} + {{- if .Values.ingress.emissary.allowUpgrade }} + allow_upgrade: + - websocket + {{- end }} +{{- if .Values.ingress.emissary.grpc.enabled }} +--- +# gRPC (orchestrator) +apiVersion: getambassador.io/v2 +kind: Mapping +metadata: + name: {{ $fullName }}-grpc + namespace: {{ $ns }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} +spec: + host: {{ $host }} + prefix: {{ .Values.ingress.emissary.grpc.prefix | default "/openzosma.orchestrator/" }} + service: {{ $fullName }}-gateway.{{ $ns }}:{{ .Values.ingress.emissary.grpc.port | default 50051 }} + grpc: true + rewrite: "" + timeout_ms: {{ $timeout }} +{{- end }} +{{- if .Values.web.enabled }} +--- +# Web dashboard (catch-all, lowest precedence) +apiVersion: getambassador.io/v2 +kind: Mapping +metadata: + name: {{ $fullName }}-web + namespace: {{ $ns }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} +spec: + host: {{ $host }} + prefix: / + service: {{ $fullName }}-web.{{ $ns }}:{{ $webPort }} + timeout_ms: {{ $timeout }} +{{- end }} +{{- end }} diff --git a/charts/openzosma/templates/ingress-traefik.yaml b/charts/openzosma/templates/ingress-traefik.yaml new file mode 100644 index 0000000..a91077b --- /dev/null +++ b/charts/openzosma/templates/ingress-traefik.yaml @@ -0,0 +1,77 @@ +{{- if and .Values.ingress.enabled (eq .Values.ingress.type "traefik") }} +{{- $fullName := include "openzosma.fullname" . -}} +{{- $gatewayPort := .Values.gateway.port -}} +{{- $webPort := .Values.web.port -}} +{{- $host := .Values.ingress.host -}} +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: {{ $fullName }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + entryPoints: + {{- toYaml .Values.ingress.traefik.entryPoints | nindent 4 }} + routes: + # Gateway REST API + - match: Host(`{{ $host }}`) && PathPrefix(`/api`) + kind: Rule + services: + - name: {{ $fullName }}-gateway + port: {{ $gatewayPort }} + {{- if .Values.ingress.traefik.middlewares }} + middlewares: + {{- range .Values.ingress.traefik.middlewares }} + - name: {{ . }} + {{- end }} + {{- end }} + # A2A protocol + - match: Host(`{{ $host }}`) && PathPrefix(`/a2a`) + kind: Rule + services: + - name: {{ $fullName }}-gateway + port: {{ $gatewayPort }} + # Agent card + - match: Host(`{{ $host }}`) && Path(`/.well-known/agent.json`) + kind: Rule + services: + - name: {{ $fullName }}-gateway + port: {{ $gatewayPort }} + # WebSocket + - match: Host(`{{ $host }}`) && PathPrefix(`/ws`) + kind: Rule + services: + - name: {{ $fullName }}-gateway + port: {{ $gatewayPort }} + {{- if .Values.ingress.traefik.grpc.enabled }} + # gRPC + - match: Host(`{{ $host }}`) && PathPrefix(`{{ .Values.ingress.traefik.grpc.prefix | default "/openzosma.orchestrator/" }}`) + kind: Rule + services: + - name: {{ $fullName }}-gateway + port: {{ .Values.ingress.traefik.grpc.port | default 50051 }} + scheme: h2c + {{- end }} + {{- if .Values.web.enabled }} + # Web dashboard (catch-all) + - match: Host(`{{ $host }}`) + kind: Rule + services: + - name: {{ $fullName }}-web + port: {{ $webPort }} + {{- if .Values.ingress.traefik.middlewares }} + middlewares: + {{- range .Values.ingress.traefik.middlewares }} + - name: {{ . }} + {{- end }} + {{- end }} + {{- end }} + {{- if .Values.ingress.tls.enabled }} + tls: + secretName: {{ .Values.ingress.tls.secretName }} + {{- end }} +{{- end }} diff --git a/charts/openzosma/templates/ingress.yaml b/charts/openzosma/templates/ingress.yaml new file mode 100644 index 0000000..6aae314 --- /dev/null +++ b/charts/openzosma/templates/ingress.yaml @@ -0,0 +1,60 @@ +{{- if and .Values.ingress.enabled (eq .Values.ingress.type "ingress") }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "openzosma.fullname" . }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.tls.enabled }} + tls: + - hosts: + - {{ .Values.ingress.host }} + secretName: {{ .Values.ingress.tls.secretName }} + {{- end }} + rules: + - host: {{ .Values.ingress.host }} + http: + paths: + - path: /api + pathType: Prefix + backend: + service: + name: {{ include "openzosma.fullname" . }}-gateway + port: + number: {{ .Values.gateway.port }} + - path: /ws + pathType: Prefix + backend: + service: + name: {{ include "openzosma.fullname" . }}-gateway + port: + number: {{ .Values.gateway.port }} + - path: /a2a + pathType: Prefix + backend: + service: + name: {{ include "openzosma.fullname" . }}-gateway + port: + number: {{ .Values.gateway.port }} + - path: /.well-known/agent.json + pathType: Exact + backend: + service: + name: {{ include "openzosma.fullname" . }}-gateway + port: + number: {{ .Values.gateway.port }} + {{- if .Values.web.enabled }} + - path: / + pathType: Prefix + backend: + service: + name: {{ include "openzosma.fullname" . }}-web + port: + number: {{ .Values.web.port }} + {{- end }} +{{- end }} diff --git a/charts/openzosma/templates/secret.yaml b/charts/openzosma/templates/secret.yaml new file mode 100644 index 0000000..07e01e9 --- /dev/null +++ b/charts/openzosma/templates/secret.yaml @@ -0,0 +1,56 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "openzosma.secretName" . }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} +type: Opaque +stringData: + {{- if and .Values.postgresql.password (not .Values.postgresql.existingSecret) }} + DB_PASS: {{ .Values.postgresql.password | quote }} + DATABASE_URL: {{ printf "postgresql://%s:%s@%s:%d/%s" .Values.postgresql.username .Values.postgresql.password .Values.postgresql.host (.Values.postgresql.port | int) .Values.postgresql.database | quote }} + {{- end }} + + {{- if .Values.auth.secret }} + AUTH_SECRET: {{ .Values.auth.secret | quote }} + {{- end }} + + {{- if .Values.auth.encryptionKey }} + ENCRYPTION_KEY: {{ .Values.auth.encryptionKey | quote }} + {{- end }} + + {{- if .Values.llm.anthropicApiKey }} + ANTHROPIC_API_KEY: {{ .Values.llm.anthropicApiKey | quote }} + {{- end }} + + {{- if .Values.llm.openaiApiKey }} + OPENAI_API_KEY: {{ .Values.llm.openaiApiKey | quote }} + {{- end }} + + {{- if .Values.llm.localModelUrl }} + OPENZOSMA_LOCAL_MODEL_URL: {{ .Values.llm.localModelUrl | quote }} + {{- end }} + + {{- if .Values.llm.localModelId }} + OPENZOSMA_LOCAL_MODEL_ID: {{ .Values.llm.localModelId | quote }} + {{- end }} + + {{- if .Values.llm.localModelApiKey }} + OPENZOSMA_LOCAL_MODEL_API_KEY: {{ .Values.llm.localModelApiKey | quote }} + {{- end }} + + {{- if .Values.auth.google.clientId }} + GOOGLE_CLIENT_ID: {{ .Values.auth.google.clientId | quote }} + {{- end }} + + {{- if .Values.auth.google.clientSecret }} + GOOGLE_CLIENT_SECRET: {{ .Values.auth.google.clientSecret | quote }} + {{- end }} + + {{- if .Values.auth.github.clientId }} + GITHUB_CLIENT_ID: {{ .Values.auth.github.clientId | quote }} + {{- end }} + + {{- if .Values.auth.github.clientSecret }} + GITHUB_CLIENT_SECRET: {{ .Values.auth.github.clientSecret | quote }} + {{- end }} diff --git a/charts/openzosma/templates/serviceaccount.yaml b/charts/openzosma/templates/serviceaccount.yaml new file mode 100644 index 0000000..55c7728 --- /dev/null +++ b/charts/openzosma/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "openzosma.serviceAccountName" . }} + labels: + {{- include "openzosma.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/charts/openzosma/templates/web-deployment.yaml b/charts/openzosma/templates/web-deployment.yaml new file mode 100644 index 0000000..cda8aed --- /dev/null +++ b/charts/openzosma/templates/web-deployment.yaml @@ -0,0 +1,69 @@ +{{- if .Values.web.enabled }} +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "openzosma.fullname" . }}-web + labels: + {{- include "openzosma.labels" . | nindent 4 }} + app.kubernetes.io/component: web +spec: + {{- if not .Values.web.autoscaling.enabled }} + replicas: {{ .Values.web.replicas }} + {{- end }} + selector: + matchLabels: + {{- include "openzosma.web.selectorLabels" . | nindent 6 }} + template: + metadata: + labels: + {{- include "openzosma.web.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "openzosma.serviceAccountName" . }} + securityContext: + runAsNonRoot: true + runAsUser: 1001 + runAsGroup: 1001 + containers: + - name: web + image: "{{ .Values.web.image.repository }}:{{ .Values.web.image.tag }}" + imagePullPolicy: {{ .Values.web.image.pullPolicy }} + ports: + - name: http + containerPort: {{ .Values.web.port }} + protocol: TCP + env: + - name: NEXT_PUBLIC_GATEWAY_URL + value: {{ printf "http://%s-gateway:%d" (include "openzosma.fullname" .) (.Values.gateway.port | int) | quote }} + {{- if .Values.auth.url }} + - name: NEXT_PUBLIC_BASE_URL + value: {{ .Values.auth.url | quote }} + {{- end }} + readinessProbe: + tcpSocket: + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + livenessProbe: + tcpSocket: + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + resources: + {{- toYaml .Values.web.resources | nindent 12 }} + {{- with .Values.web.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.web.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.web.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} +{{- end }} diff --git a/charts/openzosma/templates/web-hpa.yaml b/charts/openzosma/templates/web-hpa.yaml new file mode 100644 index 0000000..96b6c80 --- /dev/null +++ b/charts/openzosma/templates/web-hpa.yaml @@ -0,0 +1,23 @@ +{{- if and .Values.web.enabled .Values.web.autoscaling.enabled }} +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include "openzosma.fullname" . }}-web + labels: + {{- include "openzosma.labels" . | nindent 4 }} + app.kubernetes.io/component: web +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include "openzosma.fullname" . }}-web + minReplicas: {{ .Values.web.autoscaling.minReplicas }} + maxReplicas: {{ .Values.web.autoscaling.maxReplicas }} + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: {{ .Values.web.autoscaling.targetCPU }} +{{- end }} diff --git a/charts/openzosma/templates/web-service.yaml b/charts/openzosma/templates/web-service.yaml new file mode 100644 index 0000000..bb0cb3e --- /dev/null +++ b/charts/openzosma/templates/web-service.yaml @@ -0,0 +1,18 @@ +{{- if .Values.web.enabled }} +apiVersion: v1 +kind: Service +metadata: + name: {{ include "openzosma.fullname" . }}-web + labels: + {{- include "openzosma.labels" . | nindent 4 }} + app.kubernetes.io/component: web +spec: + type: ClusterIP + ports: + - port: {{ .Values.web.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "openzosma.web.selectorLabels" . | nindent 4 }} +{{- end }} diff --git a/charts/openzosma/values.yaml b/charts/openzosma/values.yaml new file mode 100644 index 0000000..ba3a41e --- /dev/null +++ b/charts/openzosma/values.yaml @@ -0,0 +1,153 @@ +# ============================================================================= +# OpenZosma Helm Chart - values.yaml +# ============================================================================= + +# -- Gateway (Hono HTTP server) +gateway: + image: + repository: openzosma/gateway + tag: latest + pullPolicy: IfNotPresent + replicas: 1 + port: 4000 + resources: + requests: + cpu: 250m + memory: 256Mi + limits: + cpu: 1000m + memory: 512Mi + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 5 + targetCPU: 80 + nodeSelector: {} + tolerations: [] + affinity: {} + +# -- Web Dashboard (Next.js) +web: + enabled: true + image: + repository: openzosma/web + tag: latest + pullPolicy: IfNotPresent + replicas: 1 + port: 3000 + resources: + requests: + cpu: 100m + memory: 128Mi + limits: + cpu: 500m + memory: 256Mi + autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 3 + targetCPU: 80 + nodeSelector: {} + tolerations: [] + affinity: {} + +# -- Sandbox / Orchestrator +sandbox: + # "local" = pi-agent runs in-process inside gateway (dev) + # "orchestrator" = per-user OpenShell sandboxes (production) + mode: local + image: openzosma/sandbox-server:latest + agentPort: 8080 + maxSandboxes: 0 # 0 = unlimited + idleSuspendMs: 1800000 # 30 min + healthCheckIntervalMs: 60000 + readyTimeoutMs: 300000 + policyPath: /app/policies/default.yaml + +# -- Agent / LLM +agent: + provider: anthropic # anthropic | openai | google | groq | xai | mistral | local + modelId: claude-sonnet-4-20250514 + workspace: /workspace + +# -- LLM API Keys (stored in Secret) +llm: + anthropicApiKey: "" + openaiApiKey: "" + # Local/self-hosted model (Ollama, vLLM, llama.cpp, etc.) + localModelUrl: "" + localModelId: "" + localModelApiKey: "" + +# -- Auth (Better Auth) +auth: + secret: "" # REQUIRED - session signing key + encryptionKey: "" # AES-256 key for stored credentials + url: "" # Public URL, e.g. https://openzosma.example.com + google: + clientId: "" + clientSecret: "" + github: + clientId: "" + clientSecret: "" + +# -- PostgreSQL (external, user-managed) +postgresql: + host: "" # REQUIRED + port: 5432 + database: openzosma + username: openzosma + password: "" # or use existingSecret + existingSecret: "" # K8s secret name, must have key "password" + poolSize: 20 + +# -- Valkey / Redis (external, user-managed) +valkey: + url: "" # redis://host:6379 + +# -- RabbitMQ (external, user-managed) +rabbitmq: + url: "" # amqp://user:pass@host:5672 + +# -- Ingress +ingress: + enabled: false # set true to create ingress resources + # "traefik" = Traefik IngressRoute CRD + # "emissary" = Emissary Mapping CRD (v2) + # "ingress" = Standard K8s Ingress resource + type: traefik + host: openzosma.example.com + tls: + enabled: false + secretName: "" + annotations: {} + # Traefik-specific + traefik: + entryPoints: + - websecure + middlewares: [] + grpc: + enabled: false # enable when gRPC orchestrator is wired + prefix: /openzosma.orchestrator/ + port: 50051 + # Emissary-specific (v2 API) + emissary: + prefix: / + timeoutMs: 15000 + idleTimeoutMs: 300000 # long timeout for WebSocket + allowUpgrade: true # WebSocket upgrade support + grpc: + enabled: false # enable when gRPC orchestrator is wired + prefix: /openzosma.orchestrator/ + port: 50051 + +# -- Service Account +serviceAccount: + create: true + name: "" + annotations: {} + +# -- Common +nameOverride: "" +fullnameOverride: "" +imagePullSecrets: [] From 61caa619dd616a9c4bb37bbb615ea3fbbf6ecc9c Mon Sep 17 00:00:00 2001 From: CelestialCreator Date: Wed, 1 Apr 2026 13:08:10 +0530 Subject: [PATCH 2/3] test(gateway): add integration tests for HTTP routes, auth middleware, and SSE MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 36 integration tests covering all gateway HTTP endpoints using Hono's app.request() — no server required, fast and CI-friendly. Test suites: - health: smoke test - sessions: CRUD + message send/receive (10 tests) - auth-middleware: API key, session cookie, RBAC enforcement (8 tests) - agents: agent config list/get/404 (3 tests) - api-keys: create/list/delete + validation (4 tests) - kb-sync: write/delete/pull + input validation (7 tests) - sse: stream content-type, event delivery, 404 (3 tests) Includes shared test helpers (StubSessionManager, mock factories) and adds "test" script to gateway package.json. Part of: #53 --- packages/gateway/package.json | 3 +- packages/gateway/src/__tests__/agents.test.ts | 89 +++++++++++ .../gateway/src/__tests__/api-keys.test.ts | 96 ++++++++++++ .../src/__tests__/auth-middleware.test.ts | 148 ++++++++++++++++++ packages/gateway/src/__tests__/health.test.ts | 11 ++ packages/gateway/src/__tests__/helpers.ts | 144 +++++++++++++++++ .../gateway/src/__tests__/kb-sync.test.ts | 119 ++++++++++++++ .../gateway/src/__tests__/sessions.test.ts | 129 +++++++++++++++ packages/gateway/src/__tests__/sse.test.ts | 57 +++++++ 9 files changed, 795 insertions(+), 1 deletion(-) create mode 100644 packages/gateway/src/__tests__/agents.test.ts create mode 100644 packages/gateway/src/__tests__/api-keys.test.ts create mode 100644 packages/gateway/src/__tests__/auth-middleware.test.ts create mode 100644 packages/gateway/src/__tests__/health.test.ts create mode 100644 packages/gateway/src/__tests__/helpers.ts create mode 100644 packages/gateway/src/__tests__/kb-sync.test.ts create mode 100644 packages/gateway/src/__tests__/sessions.test.ts create mode 100644 packages/gateway/src/__tests__/sse.test.ts diff --git a/packages/gateway/package.json b/packages/gateway/package.json index bef2832..b28c362 100644 --- a/packages/gateway/package.json +++ b/packages/gateway/package.json @@ -26,7 +26,8 @@ "scripts": { "build": "tsc", "check": "tsc --noEmit", - "dev": "tsx watch --env-file=../../.env.local src/index.ts" + "dev": "tsx watch --env-file=../../.env.local src/index.ts", + "test": "vitest --run" }, "dependencies": { "@hono/node-server": "^1.14.1", diff --git a/packages/gateway/src/__tests__/agents.test.ts b/packages/gateway/src/__tests__/agents.test.ts new file mode 100644 index 0000000..43bd4c4 --- /dev/null +++ b/packages/gateway/src/__tests__/agents.test.ts @@ -0,0 +1,89 @@ +import { describe, expect, it, vi, beforeEach } from "vitest" +import { createTestApp } from "./helpers.js" +import type { Hono } from "hono" +import { agentConfigQueries } from "@openzosma/db" + +vi.mock("@openzosma/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + validateApiKey: vi.fn().mockResolvedValue({ valid: false }), + } +}) + +vi.mock("@openzosma/a2a", () => ({ + buildDefaultAgentCard: vi.fn().mockResolvedValue(null), +})) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Any = any + +describe("Agent config routes", () => { + let app: Hono + + beforeEach(async () => { + const ctx = await createTestApp() + app = ctx.app + }) + + describe("GET /api/v1/agents", () => { + it("lists agent configs", async () => { + vi.spyOn(agentConfigQueries, "listAgentConfigs").mockResolvedValue([ + { + id: "agent-1", + name: "Test Agent", + description: "A test agent", + model: "claude-sonnet-4-20250514", + provider: "anthropic", + skills: ["code"], + createdAt: new Date("2026-01-01"), + systemPrompt: null, + systemPromptPrefix: null, + toolsEnabled: null, + maxTokens: null, + temperature: null, + updatedAt: new Date("2026-01-01"), + } as unknown as Awaited>[number], + ]) + + const res = await app.request("/api/v1/agents") + expect(res.status).toBe(200) + const body = (await res.json()) as Any + expect(body.agents).toHaveLength(1) + expect(body.agents[0].name).toBe("Test Agent") + }) + }) + + describe("GET /api/v1/agents/:id", () => { + it("returns a single agent config", async () => { + vi.spyOn(agentConfigQueries, "getAgentConfig").mockResolvedValue({ + id: "agent-1", + name: "Test Agent", + description: "A test agent", + model: "claude-sonnet-4-20250514", + provider: "anthropic", + skills: ["code"], + systemPrompt: "You are a test agent", + systemPromptPrefix: null, + toolsEnabled: null, + maxTokens: null, + temperature: null, + createdAt: new Date("2026-01-01"), + updatedAt: new Date("2026-01-01"), + } as unknown as Awaited>) + + const res = await app.request("/api/v1/agents/agent-1") + expect(res.status).toBe(200) + const body = (await res.json()) as Any + expect(body.id).toBe("agent-1") + expect(body.name).toBe("Test Agent") + }) + + it("returns 404 for unknown agent config", async () => { + vi.spyOn(agentConfigQueries, "getAgentConfig").mockResolvedValue(null) + + const res = await app.request("/api/v1/agents/nonexistent") + expect(res.status).toBe(404) + }) + }) +}) diff --git a/packages/gateway/src/__tests__/api-keys.test.ts b/packages/gateway/src/__tests__/api-keys.test.ts new file mode 100644 index 0000000..7483084 --- /dev/null +++ b/packages/gateway/src/__tests__/api-keys.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it, vi, beforeEach } from "vitest" +import { createTestApp } from "./helpers.js" +import type { Hono } from "hono" +import { apiKeyQueries } from "@openzosma/db" + +vi.mock("@openzosma/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + validateApiKey: vi.fn().mockResolvedValue({ valid: false }), + } +}) + +vi.mock("@openzosma/a2a", () => ({ + buildDefaultAgentCard: vi.fn().mockResolvedValue(null), +})) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Any = any + +describe("API key routes", () => { + let app: Hono + + beforeEach(async () => { + const ctx = await createTestApp() + app = ctx.app + }) + + describe("POST /api/v1/api-keys", () => { + it("creates an API key", async () => { + vi.spyOn(apiKeyQueries, "createApiKey").mockResolvedValue({ + id: "key-1", + name: "test-key", + keyHash: "hash", + keyPrefix: "ozk_abc", + scopes: [], + createdAt: new Date(), + lastUsedAt: null, + expiresAt: null, + } as Awaited>) + + const res = await app.request("/api/v1/api-keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "test-key" }), + }) + expect(res.status).toBe(201) + const body = (await res.json()) as Any + expect(body).toHaveProperty("id") + expect(body).toHaveProperty("key") + expect(body.key).toMatch(/^ozk_/) + }) + + it("returns 400 when name is missing", async () => { + const res = await app.request("/api/v1/api-keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }) + expect(res.status).toBe(400) + }) + }) + + describe("GET /api/v1/api-keys", () => { + it("lists API keys", async () => { + vi.spyOn(apiKeyQueries, "listApiKeys").mockResolvedValue([ + { + id: "key-1", + name: "test-key", + keyHash: "hash", + keyPrefix: "ozk_abc", + scopes: ["sessions:read"], + createdAt: new Date(), + lastUsedAt: null, + expiresAt: null, + } as Awaited>[number], + ]) + + const res = await app.request("/api/v1/api-keys") + expect(res.status).toBe(200) + const body = (await res.json()) as Any + expect(body.keys).toHaveLength(1) + expect(body.keys[0].keyPrefix).toBe("ozk_abc") + }) + }) + + describe("DELETE /api/v1/api-keys/:id", () => { + it("deletes an API key", async () => { + vi.spyOn(apiKeyQueries, "deleteApiKey").mockResolvedValue(undefined as never) + + const res = await app.request("/api/v1/api-keys/key-1", { method: "DELETE" }) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ ok: true }) + }) + }) +}) diff --git a/packages/gateway/src/__tests__/auth-middleware.test.ts b/packages/gateway/src/__tests__/auth-middleware.test.ts new file mode 100644 index 0000000..7f64963 --- /dev/null +++ b/packages/gateway/src/__tests__/auth-middleware.test.ts @@ -0,0 +1,148 @@ +import { describe, expect, it, vi, beforeEach } from "vitest" +import { createTestApp, createMockAuth, createMockPool, StubSessionManager } from "./helpers.js" +import type { SessionManager } from "../session-manager.js" + +const mockValidateApiKey = vi.fn() + +vi.mock("@openzosma/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + validateApiKey: (...args: unknown[]) => mockValidateApiKey(...args), + } +}) + +vi.mock("@openzosma/a2a", () => ({ + buildDefaultAgentCard: vi.fn().mockResolvedValue(null), +})) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Any = any + +describe("Auth middleware", () => { + beforeEach(() => { + mockValidateApiKey.mockReset() + mockValidateApiKey.mockResolvedValue({ valid: false }) + }) + + it("rejects unauthenticated requests with 401", async () => { + const { app } = await createTestApp({ authenticated: false }) + + const res = await app.request("/api/v1/sessions/test-id") + expect(res.status).toBe(401) + const body = (await res.json()) as Any + expect(body.error.code).toBe("AUTH_REQUIRED") + }) + + it("accepts valid API key", async () => { + mockValidateApiKey.mockResolvedValue({ + valid: true, + keyId: "key-1", + scopes: ["sessions:read", "sessions:write"], + }) + + const { app, sessionManager } = await createTestApp({ authenticated: false }) + await sessionManager.createSession("s1") + + const res = await app.request("/api/v1/sessions/s1", { + headers: { Authorization: "Bearer ozk_test123" }, + }) + expect(res.status).toBe(200) + }) + + it("rejects invalid API key with 401", async () => { + mockValidateApiKey.mockResolvedValue({ valid: false }) + + const { app } = await createTestApp({ authenticated: false }) + + const res = await app.request("/api/v1/sessions/test-id", { + headers: { Authorization: "Bearer ozk_invalid" }, + }) + expect(res.status).toBe(401) + const body = (await res.json()) as Any + expect(body.error.code).toBe("INVALID_API_KEY") + }) + + it("accepts valid session cookie (Better Auth)", async () => { + const { app, sessionManager } = await createTestApp({ authenticated: true }) + await sessionManager.createSession("s1") + + const res = await app.request("/api/v1/sessions/s1") + expect(res.status).toBe(200) + }) + + it("rejects API key missing required scope with 403", async () => { + mockValidateApiKey.mockResolvedValue({ + valid: true, + keyId: "key-2", + scopes: ["sessions:read"], // missing sessions:write + }) + + const { app } = await createTestApp({ authenticated: false }) + + const res = await app.request("/api/v1/sessions", { + method: "POST", + headers: { Authorization: "Bearer ozk_limited" }, + }) + expect(res.status).toBe(403) + const body = (await res.json()) as Any + expect(body.error.code).toBe("FORBIDDEN") + }) + + it("allows API key with correct scope", async () => { + mockValidateApiKey.mockResolvedValue({ + valid: true, + keyId: "key-3", + scopes: ["sessions:write"], + }) + + const { app } = await createTestApp({ authenticated: false }) + + const res = await app.request("/api/v1/sessions", { + method: "POST", + headers: { Authorization: "Bearer ozk_full" }, + }) + expect(res.status).toBe(201) + }) + + it("rejects member role for api_keys:write with 403", async () => { + const { app } = await createTestApp({ role: "member" }) + + const res = await app.request("/api/v1/api-keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "test-key" }), + }) + expect(res.status).toBe(403) + const body = (await res.json()) as Any + expect(body.error.code).toBe("FORBIDDEN") + }) + + it("allows admin role for api_keys:write", async () => { + const { createApp } = await import("../app.js") + const sm = new StubSessionManager() + const pool = createMockPool() + const auth = createMockAuth({ role: "admin" }) + + const { apiKeyQueries } = await import("@openzosma/db") + vi.spyOn(apiKeyQueries, "createApiKey").mockResolvedValue({ + id: "key-new", + name: "test", + keyHash: "hash", + keyPrefix: "ozk_test12345", + scopes: [], + createdAt: new Date(), + lastUsedAt: null, + expiresAt: null, + } as Awaited>) + + const app = createApp(sm as unknown as SessionManager, pool, auth) + + const res = await app.request("/api/v1/api-keys", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name: "test-key" }), + }) + expect(res.status).toBe(201) + }) +}) diff --git a/packages/gateway/src/__tests__/health.test.ts b/packages/gateway/src/__tests__/health.test.ts new file mode 100644 index 0000000..8d51476 --- /dev/null +++ b/packages/gateway/src/__tests__/health.test.ts @@ -0,0 +1,11 @@ +import { describe, expect, it } from "vitest" +import { createTestApp } from "./helpers.js" + +describe("GET /health", () => { + it("returns 200 with status ok", async () => { + const { app } = await createTestApp() + const res = await app.request("/health") + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ status: "ok" }) + }) +}) diff --git a/packages/gateway/src/__tests__/helpers.ts b/packages/gateway/src/__tests__/helpers.ts new file mode 100644 index 0000000..cc72897 --- /dev/null +++ b/packages/gateway/src/__tests__/helpers.ts @@ -0,0 +1,144 @@ +import type { Hono } from "hono" +import { vi } from "vitest" +import type { Session, GatewayEvent } from "../types.js" +import type { SessionManager } from "../session-manager.js" +import type { Auth } from "@openzosma/auth" +import type { Pool } from "@openzosma/db" + +/** + * Minimal stub that satisfies the SessionManager interface used by createApp(). + * Operates entirely in-memory — no filesystem, no agent provider, no DB. + */ +export class StubSessionManager { + sessions = new Map() + kbFiles: Array<{ path: string; content: string; sizeBytes: number; modifiedAt: string }> = [] + /** Events yielded by sendMessage(). Override per test. */ + messageEvents: GatewayEvent[] = [{ type: "message_update", text: "Hello from stub" }] + /** Events yielded by subscribe(). Override per test. */ + subscribeEvents: GatewayEvent[] = [] + + async createSession( + id?: string, + _agentConfigId?: string, + _resolvedConfig?: unknown, + _userId?: string, + ): Promise { + const sessionId = id ?? `stub-${Date.now()}` + const existing = this.sessions.get(sessionId) + if (existing) return existing + + const session: Session = { + id: sessionId, + createdAt: new Date().toISOString(), + messages: [], + } + this.sessions.set(sessionId, session) + return session + } + + getSession(id: string): Session | undefined { + return this.sessions.get(id) + } + + deleteSession(id: string): boolean { + return this.sessions.delete(id) + } + + async *sendMessage( + sessionId: string, + _content: string, + _signal?: AbortSignal, + _userId?: string, + ): AsyncGenerator { + const session = this.sessions.get(sessionId) + if (!session) { + yield { type: "error", error: `Session ${sessionId} not found` } + return + } + for (const event of this.messageEvents) { + yield event + } + } + + async *subscribe(_sessionId: string, signal?: AbortSignal): AsyncGenerator { + for (const event of this.subscribeEvents) { + if (signal?.aborted) return + yield event + } + } + + async getSandboxInfo(_userId: string) { + return null + } + + async destroySandbox(_userId: string) { + return false + } + + async pushKBFile(_userId: string, _path: string, _content: string) {} + async deleteKBFile(_userId: string, _path: string) {} + + async pullKB(_userId: string) { + return this.kbFiles + } + + async resolveUserByEmail(_email: string) { + return null + } +} + +/** Create a mock Auth object that authenticates as the given user by default. */ +export function createMockAuth(opts?: { + userId?: string + role?: string + authenticated?: boolean +}): Auth { + const userId = opts?.userId ?? "test-user-id" + const role = opts?.role ?? "admin" + const authenticated = opts?.authenticated ?? true + + return { + api: { + getSession: vi.fn().mockResolvedValue( + authenticated + ? { user: { id: userId, role } } + : null, + ), + }, + handler: vi.fn().mockImplementation(() => new Response("ok")), + } as unknown as Auth +} + +/** Create a minimal mock Pool satisfying the type. */ +export function createMockPool(): Pool { + return { query: vi.fn() } as unknown as Pool +} + +/** + * Create a test Hono app with all dependencies stubbed. + * Returns the app + all mocks for test manipulation. + */ +export async function createTestApp(opts?: { + authenticated?: boolean + userId?: string + role?: string + withPool?: boolean +}): Promise<{ app: Hono; sessionManager: StubSessionManager; pool: Pool | undefined; auth: Auth }> { + const { createApp } = await import("../app.js") + + const sessionManager = new StubSessionManager() + const pool = (opts?.withPool ?? true) ? createMockPool() : undefined + const auth = createMockAuth({ + userId: opts?.userId ?? "test-user-id", + role: opts?.role ?? "admin", + authenticated: opts?.authenticated ?? true, + }) + + const app = createApp( + sessionManager as unknown as SessionManager, + pool, + auth, + ) + + return { app: app as unknown as Hono, sessionManager, pool, auth } +} diff --git a/packages/gateway/src/__tests__/kb-sync.test.ts b/packages/gateway/src/__tests__/kb-sync.test.ts new file mode 100644 index 0000000..cbf99b7 --- /dev/null +++ b/packages/gateway/src/__tests__/kb-sync.test.ts @@ -0,0 +1,119 @@ +import { describe, expect, it, vi, beforeEach } from "vitest" +import { createTestApp, type StubSessionManager } from "./helpers.js" +import type { Hono } from "hono" + +vi.mock("@openzosma/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + validateApiKey: vi.fn().mockResolvedValue({ valid: false }), + } +}) + +vi.mock("@openzosma/a2a", () => ({ + buildDefaultAgentCard: vi.fn().mockResolvedValue(null), +})) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Any = any + +describe("KB sync routes", () => { + let app: Hono + let sm: StubSessionManager + + beforeEach(async () => { + const ctx = await createTestApp() + app = ctx.app + sm = ctx.sessionManager + }) + + describe("POST /api/v1/kb/sync", () => { + it("pushes a KB file (write action)", async () => { + const pushSpy = vi.spyOn(sm, "pushKBFile") + + const res = await app.request("/api/v1/kb/sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "write", path: "notes.md", content: "# Notes" }), + }) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ ok: true }) + expect(pushSpy).toHaveBeenCalledWith("test-user-id", "notes.md", "# Notes") + }) + + it("deletes a KB file (delete action)", async () => { + const deleteSpy = vi.spyOn(sm, "deleteKBFile") + + const res = await app.request("/api/v1/kb/sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "delete", path: "notes.md" }), + }) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ ok: true }) + expect(deleteSpy).toHaveBeenCalledWith("test-user-id", "notes.md") + }) + + it("rejects missing action or path", async () => { + const res = await app.request("/api/v1/kb/sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "write" }), + }) + expect(res.status).toBe(400) + }) + + it("rejects write without content", async () => { + const res = await app.request("/api/v1/kb/sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "write", path: "notes.md" }), + }) + expect(res.status).toBe(400) + }) + + it("rejects unknown action", async () => { + const res = await app.request("/api/v1/kb/sync", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ action: "move", path: "notes.md" }), + }) + expect(res.status).toBe(400) + }) + + it("returns 400 when no userId (API key auth without userId)", async () => { + const mockValidateApiKey = (await import("@openzosma/auth")).validateApiKey as ReturnType + mockValidateApiKey.mockResolvedValue({ + valid: true, + keyId: "key-1", + scopes: ["sessions:write"], + }) + + const { app: apiKeyApp } = await createTestApp({ authenticated: false }) + + const res = await apiKeyApp.request("/api/v1/kb/sync", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer ozk_testkey", + }, + body: JSON.stringify({ action: "write", path: "notes.md", content: "# Test" }), + }) + expect(res.status).toBe(400) + }) + }) + + describe("GET /api/v1/kb/pull", () => { + it("pulls KB files", async () => { + sm.kbFiles = [ + { path: "notes.md", content: "# Notes", sizeBytes: 7, modifiedAt: "2026-01-01T00:00:00Z" }, + ] + + const res = await app.request("/api/v1/kb/pull") + expect(res.status).toBe(200) + const body = (await res.json()) as Any + expect(body.files).toHaveLength(1) + expect(body.files[0].path).toBe("notes.md") + }) + }) +}) diff --git a/packages/gateway/src/__tests__/sessions.test.ts b/packages/gateway/src/__tests__/sessions.test.ts new file mode 100644 index 0000000..5f3218c --- /dev/null +++ b/packages/gateway/src/__tests__/sessions.test.ts @@ -0,0 +1,129 @@ +import { describe, expect, it, vi, beforeEach } from "vitest" +import { createTestApp, type StubSessionManager } from "./helpers.js" +import type { Hono } from "hono" + +vi.mock("@openzosma/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + validateApiKey: vi.fn().mockResolvedValue({ valid: false }), + } +}) + +vi.mock("@openzosma/a2a", () => ({ + buildDefaultAgentCard: vi.fn().mockResolvedValue(null), +})) + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +type Any = any + +describe("Session routes", () => { + let app: Hono + let sm: StubSessionManager + + beforeEach(async () => { + const ctx = await createTestApp() + app = ctx.app + sm = ctx.sessionManager + }) + + describe("POST /api/v1/sessions", () => { + it("creates a session", async () => { + const res = await app.request("/api/v1/sessions", { method: "POST" }) + expect(res.status).toBe(201) + const body = (await res.json()) as Any + expect(body).toHaveProperty("id") + expect(body).toHaveProperty("createdAt") + }) + }) + + describe("GET /api/v1/sessions/:id", () => { + it("returns session details", async () => { + await sm.createSession("test-s1") + const res = await app.request("/api/v1/sessions/test-s1") + expect(res.status).toBe(200) + const body = (await res.json()) as Any + expect(body.id).toBe("test-s1") + expect(body).toHaveProperty("messageCount") + }) + + it("returns 404 for unknown session", async () => { + const res = await app.request("/api/v1/sessions/nonexistent") + expect(res.status).toBe(404) + }) + }) + + describe("DELETE /api/v1/sessions/:id", () => { + it("deletes an existing session", async () => { + await sm.createSession("del-1") + const res = await app.request("/api/v1/sessions/del-1", { method: "DELETE" }) + expect(res.status).toBe(200) + expect(await res.json()).toEqual({ ok: true }) + expect(sm.getSession("del-1")).toBeUndefined() + }) + + it("returns 404 for unknown session", async () => { + const res = await app.request("/api/v1/sessions/nonexistent", { method: "DELETE" }) + expect(res.status).toBe(404) + }) + }) + + describe("POST /api/v1/sessions/:id/messages", () => { + it("sends a message and gets text response", async () => { + await sm.createSession("msg-1") + sm.messageEvents = [{ type: "message_update", text: "Hi there!" }] + + const res = await app.request("/api/v1/sessions/msg-1/messages", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: "Hello" }), + }) + expect(res.status).toBe(200) + const body = (await res.json()) as Any + expect(body.role).toBe("assistant") + expect(body.content).toBe("Hi there!") + }) + + it("returns 404 for unknown session", async () => { + const res = await app.request("/api/v1/sessions/nope/messages", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ content: "Hello" }), + }) + expect(res.status).toBe(404) + }) + + it("returns 400 when content is missing", async () => { + await sm.createSession("msg-2") + const res = await app.request("/api/v1/sessions/msg-2/messages", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }) + expect(res.status).toBe(400) + }) + }) + + describe("GET /api/v1/sessions/:id/messages", () => { + it("lists messages for a session", async () => { + const session = await sm.createSession("list-1") + session.messages.push({ + id: "m1", + role: "user", + content: "Hello", + createdAt: new Date().toISOString(), + }) + + const res = await app.request("/api/v1/sessions/list-1/messages") + expect(res.status).toBe(200) + const body = (await res.json()) as Any[] + expect(body).toHaveLength(1) + expect(body[0].content).toBe("Hello") + }) + + it("returns 404 for unknown session", async () => { + const res = await app.request("/api/v1/sessions/nope/messages") + expect(res.status).toBe(404) + }) + }) +}) diff --git a/packages/gateway/src/__tests__/sse.test.ts b/packages/gateway/src/__tests__/sse.test.ts new file mode 100644 index 0000000..d83ff90 --- /dev/null +++ b/packages/gateway/src/__tests__/sse.test.ts @@ -0,0 +1,57 @@ +import { describe, expect, it, vi, beforeEach } from "vitest" +import { createTestApp, type StubSessionManager } from "./helpers.js" +import type { Hono } from "hono" + +vi.mock("@openzosma/auth", async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + validateApiKey: vi.fn().mockResolvedValue({ valid: false }), + } +}) + +vi.mock("@openzosma/a2a", () => ({ + buildDefaultAgentCard: vi.fn().mockResolvedValue(null), +})) + +describe("SSE stream routes", () => { + let app: Hono + let sm: StubSessionManager + + beforeEach(async () => { + const ctx = await createTestApp() + app = ctx.app + sm = ctx.sessionManager + }) + + describe("GET /api/v1/sessions/:id/stream", () => { + it("returns 404 for unknown session", async () => { + const res = await app.request("/api/v1/sessions/nope/stream") + expect(res.status).toBe(404) + }) + + it("returns SSE content-type for valid session", async () => { + await sm.createSession("sse-1") + sm.subscribeEvents = [{ type: "message_update", text: "streaming" }] + + const res = await app.request("/api/v1/sessions/sse-1/stream") + expect(res.status).toBe(200) + expect(res.headers.get("content-type")).toContain("text/event-stream") + }) + + it("streams events as SSE data lines", async () => { + await sm.createSession("sse-2") + sm.subscribeEvents = [ + { type: "message_start", id: "m1" }, + { type: "message_update", text: "hello" }, + ] + + const res = await app.request("/api/v1/sessions/sse-2/stream") + const text = await res.text() + + expect(text).toContain("data:") + expect(text).toContain("message_start") + expect(text).toContain("message_update") + }) + }) +}) From 7ec915f4c8716c22e9fd58cc266989fd79e26552 Mon Sep 17 00:00:00 2001 From: CelestialCreator Date: Wed, 1 Apr 2026 14:05:55 +0530 Subject: [PATCH 3/3] fix(test): resolve biome lint and formatting issues in gateway tests - Sort imports alphabetically (biome organizeImports) - Replace `type Any = any` with `Json` type from helpers (biome noExplicitAny) - Fix formatting: inline short expressions, remove extra blank lines --- packages/gateway/src/__tests__/agents.test.ts | 13 +++++------ .../gateway/src/__tests__/api-keys.test.ts | 13 +++++------ .../src/__tests__/auth-middleware.test.ts | 15 +++++-------- packages/gateway/src/__tests__/helpers.ts | 22 ++++++++----------- .../gateway/src/__tests__/kb-sync.test.ts | 13 ++++------- .../gateway/src/__tests__/sessions.test.ts | 15 +++++-------- packages/gateway/src/__tests__/sse.test.ts | 4 ++-- 7 files changed, 37 insertions(+), 58 deletions(-) diff --git a/packages/gateway/src/__tests__/agents.test.ts b/packages/gateway/src/__tests__/agents.test.ts index 43bd4c4..1eb89fb 100644 --- a/packages/gateway/src/__tests__/agents.test.ts +++ b/packages/gateway/src/__tests__/agents.test.ts @@ -1,7 +1,7 @@ -import { describe, expect, it, vi, beforeEach } from "vitest" -import { createTestApp } from "./helpers.js" -import type { Hono } from "hono" import { agentConfigQueries } from "@openzosma/db" +import type { Hono } from "hono" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { type Json, createTestApp } from "./helpers.js" vi.mock("@openzosma/auth", async (importOriginal) => { const actual = await importOriginal() @@ -15,9 +15,6 @@ vi.mock("@openzosma/a2a", () => ({ buildDefaultAgentCard: vi.fn().mockResolvedValue(null), })) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Any = any - describe("Agent config routes", () => { let app: Hono @@ -48,7 +45,7 @@ describe("Agent config routes", () => { const res = await app.request("/api/v1/agents") expect(res.status).toBe(200) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body.agents).toHaveLength(1) expect(body.agents[0].name).toBe("Test Agent") }) @@ -74,7 +71,7 @@ describe("Agent config routes", () => { const res = await app.request("/api/v1/agents/agent-1") expect(res.status).toBe(200) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body.id).toBe("agent-1") expect(body.name).toBe("Test Agent") }) diff --git a/packages/gateway/src/__tests__/api-keys.test.ts b/packages/gateway/src/__tests__/api-keys.test.ts index 7483084..571ed58 100644 --- a/packages/gateway/src/__tests__/api-keys.test.ts +++ b/packages/gateway/src/__tests__/api-keys.test.ts @@ -1,7 +1,7 @@ -import { describe, expect, it, vi, beforeEach } from "vitest" -import { createTestApp } from "./helpers.js" -import type { Hono } from "hono" import { apiKeyQueries } from "@openzosma/db" +import type { Hono } from "hono" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { type Json, createTestApp } from "./helpers.js" vi.mock("@openzosma/auth", async (importOriginal) => { const actual = await importOriginal() @@ -15,9 +15,6 @@ vi.mock("@openzosma/a2a", () => ({ buildDefaultAgentCard: vi.fn().mockResolvedValue(null), })) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Any = any - describe("API key routes", () => { let app: Hono @@ -45,7 +42,7 @@ describe("API key routes", () => { body: JSON.stringify({ name: "test-key" }), }) expect(res.status).toBe(201) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body).toHaveProperty("id") expect(body).toHaveProperty("key") expect(body.key).toMatch(/^ozk_/) @@ -78,7 +75,7 @@ describe("API key routes", () => { const res = await app.request("/api/v1/api-keys") expect(res.status).toBe(200) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body.keys).toHaveLength(1) expect(body.keys[0].keyPrefix).toBe("ozk_abc") }) diff --git a/packages/gateway/src/__tests__/auth-middleware.test.ts b/packages/gateway/src/__tests__/auth-middleware.test.ts index 7f64963..cba821b 100644 --- a/packages/gateway/src/__tests__/auth-middleware.test.ts +++ b/packages/gateway/src/__tests__/auth-middleware.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it, vi, beforeEach } from "vitest" -import { createTestApp, createMockAuth, createMockPool, StubSessionManager } from "./helpers.js" +import { beforeEach, describe, expect, it, vi } from "vitest" import type { SessionManager } from "../session-manager.js" +import { type Json, StubSessionManager, createMockAuth, createMockPool, createTestApp } from "./helpers.js" const mockValidateApiKey = vi.fn() @@ -16,9 +16,6 @@ vi.mock("@openzosma/a2a", () => ({ buildDefaultAgentCard: vi.fn().mockResolvedValue(null), })) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Any = any - describe("Auth middleware", () => { beforeEach(() => { mockValidateApiKey.mockReset() @@ -30,7 +27,7 @@ describe("Auth middleware", () => { const res = await app.request("/api/v1/sessions/test-id") expect(res.status).toBe(401) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body.error.code).toBe("AUTH_REQUIRED") }) @@ -59,7 +56,7 @@ describe("Auth middleware", () => { headers: { Authorization: "Bearer ozk_invalid" }, }) expect(res.status).toBe(401) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body.error.code).toBe("INVALID_API_KEY") }) @@ -85,7 +82,7 @@ describe("Auth middleware", () => { headers: { Authorization: "Bearer ozk_limited" }, }) expect(res.status).toBe(403) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body.error.code).toBe("FORBIDDEN") }) @@ -114,7 +111,7 @@ describe("Auth middleware", () => { body: JSON.stringify({ name: "test-key" }), }) expect(res.status).toBe(403) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body.error.code).toBe("FORBIDDEN") }) diff --git a/packages/gateway/src/__tests__/helpers.ts b/packages/gateway/src/__tests__/helpers.ts index cc72897..b282e77 100644 --- a/packages/gateway/src/__tests__/helpers.ts +++ b/packages/gateway/src/__tests__/helpers.ts @@ -1,9 +1,13 @@ +import type { Auth } from "@openzosma/auth" +import type { Pool } from "@openzosma/db" import type { Hono } from "hono" import { vi } from "vitest" -import type { Session, GatewayEvent } from "../types.js" import type { SessionManager } from "../session-manager.js" -import type { Auth } from "@openzosma/auth" -import type { Pool } from "@openzosma/db" +import type { GatewayEvent, Session } from "../types.js" + +/** Recursive JSON type for test assertion casts. Avoids `any` while allowing nested access. */ +// biome-ignore lint/suspicious/noExplicitAny: needed for flexible test JSON assertions +export type Json = Record /** * Minimal stub that satisfies the SessionManager interface used by createApp(). @@ -99,11 +103,7 @@ export function createMockAuth(opts?: { return { api: { - getSession: vi.fn().mockResolvedValue( - authenticated - ? { user: { id: userId, role } } - : null, - ), + getSession: vi.fn().mockResolvedValue(authenticated ? { user: { id: userId, role } } : null), }, handler: vi.fn().mockImplementation(() => new Response("ok")), } as unknown as Auth @@ -134,11 +134,7 @@ export async function createTestApp(opts?: { authenticated: opts?.authenticated ?? true, }) - const app = createApp( - sessionManager as unknown as SessionManager, - pool, - auth, - ) + const app = createApp(sessionManager as unknown as SessionManager, pool, auth) return { app: app as unknown as Hono, sessionManager, pool, auth } } diff --git a/packages/gateway/src/__tests__/kb-sync.test.ts b/packages/gateway/src/__tests__/kb-sync.test.ts index cbf99b7..9b5db57 100644 --- a/packages/gateway/src/__tests__/kb-sync.test.ts +++ b/packages/gateway/src/__tests__/kb-sync.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it, vi, beforeEach } from "vitest" -import { createTestApp, type StubSessionManager } from "./helpers.js" import type { Hono } from "hono" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { type Json, type StubSessionManager, createTestApp } from "./helpers.js" vi.mock("@openzosma/auth", async (importOriginal) => { const actual = await importOriginal() @@ -14,9 +14,6 @@ vi.mock("@openzosma/a2a", () => ({ buildDefaultAgentCard: vi.fn().mockResolvedValue(null), })) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Any = any - describe("KB sync routes", () => { let app: Hono let sm: StubSessionManager @@ -105,13 +102,11 @@ describe("KB sync routes", () => { describe("GET /api/v1/kb/pull", () => { it("pulls KB files", async () => { - sm.kbFiles = [ - { path: "notes.md", content: "# Notes", sizeBytes: 7, modifiedAt: "2026-01-01T00:00:00Z" }, - ] + sm.kbFiles = [{ path: "notes.md", content: "# Notes", sizeBytes: 7, modifiedAt: "2026-01-01T00:00:00Z" }] const res = await app.request("/api/v1/kb/pull") expect(res.status).toBe(200) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body.files).toHaveLength(1) expect(body.files[0].path).toBe("notes.md") }) diff --git a/packages/gateway/src/__tests__/sessions.test.ts b/packages/gateway/src/__tests__/sessions.test.ts index 5f3218c..910835b 100644 --- a/packages/gateway/src/__tests__/sessions.test.ts +++ b/packages/gateway/src/__tests__/sessions.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it, vi, beforeEach } from "vitest" -import { createTestApp, type StubSessionManager } from "./helpers.js" import type { Hono } from "hono" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { type Json, type StubSessionManager, createTestApp } from "./helpers.js" vi.mock("@openzosma/auth", async (importOriginal) => { const actual = await importOriginal() @@ -14,9 +14,6 @@ vi.mock("@openzosma/a2a", () => ({ buildDefaultAgentCard: vi.fn().mockResolvedValue(null), })) -// eslint-disable-next-line @typescript-eslint/no-explicit-any -type Any = any - describe("Session routes", () => { let app: Hono let sm: StubSessionManager @@ -31,7 +28,7 @@ describe("Session routes", () => { it("creates a session", async () => { const res = await app.request("/api/v1/sessions", { method: "POST" }) expect(res.status).toBe(201) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body).toHaveProperty("id") expect(body).toHaveProperty("createdAt") }) @@ -42,7 +39,7 @@ describe("Session routes", () => { await sm.createSession("test-s1") const res = await app.request("/api/v1/sessions/test-s1") expect(res.status).toBe(200) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body.id).toBe("test-s1") expect(body).toHaveProperty("messageCount") }) @@ -79,7 +76,7 @@ describe("Session routes", () => { body: JSON.stringify({ content: "Hello" }), }) expect(res.status).toBe(200) - const body = (await res.json()) as Any + const body = (await res.json()) as Json expect(body.role).toBe("assistant") expect(body.content).toBe("Hi there!") }) @@ -116,7 +113,7 @@ describe("Session routes", () => { const res = await app.request("/api/v1/sessions/list-1/messages") expect(res.status).toBe(200) - const body = (await res.json()) as Any[] + const body = (await res.json()) as Json[] expect(body).toHaveLength(1) expect(body[0].content).toBe("Hello") }) diff --git a/packages/gateway/src/__tests__/sse.test.ts b/packages/gateway/src/__tests__/sse.test.ts index d83ff90..29280a5 100644 --- a/packages/gateway/src/__tests__/sse.test.ts +++ b/packages/gateway/src/__tests__/sse.test.ts @@ -1,6 +1,6 @@ -import { describe, expect, it, vi, beforeEach } from "vitest" -import { createTestApp, type StubSessionManager } from "./helpers.js" import type { Hono } from "hono" +import { beforeEach, describe, expect, it, vi } from "vitest" +import { type StubSessionManager, createTestApp } from "./helpers.js" vi.mock("@openzosma/auth", async (importOriginal) => { const actual = await importOriginal()