Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/ai-workspace-cli-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: AI Workspace CLI E2E (daily)

# Daily end-to-end check that the `ap` CLI's AI Workspace behaviour persists
# against a real platform-api backend. It builds the platform-api image and the
# CLI from source, boots platform-api (SQLite) as the AI Workspace backend, then
# drives the CLI: register the workspace, `ap project init` the three artifact
# kinds (LLM provider, LLM proxy, MCP proxy), and `ap ai-workspace build`/`apply`
# each — asserting create, read-back, and update all persist.
#
# This is intentionally NOT on the per-PR critical path (it builds the
# platform-api image and CLI). It runs once a day and on demand.
on:
schedule:
- cron: '0 3 * * *' # 03:00 UTC daily
workflow_dispatch:

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true

jobs:
ai-workspace-cli-e2e:
runs-on: ubuntu-24.04
steps:
- name: Checkout code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
persist-credentials: false

- name: Set up Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0
with:
go-version: '1.26.5'
cache-dependency-path: '**/go.sum'

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0

- name: Build platform-api image
run: make -C platform-api build IMAGE_NAME=platform-api VERSION=it-e2e

- name: Build ap CLI binary
run: make -C cli/src build-skip-tests

- name: Run AI Workspace CLI e2e
# PLATFORM_API_IMAGE pins the compose to the image built above; the suite
# boots it, logs in, and drives the CLI built at cli/src/build/ap.
env:
PLATFORM_API_IMAGE: platform-api:it-e2e
run: go test -run TestFeatures -count=1 -v -timeout 20m ./...
working-directory: tests/ai-workspace-cli-e2e
1 change: 1 addition & 0 deletions go.work
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use (
./samples/sample-service
./sdk/ai
./sdk/core
./tests/ai-workspace-cli-e2e
./tests/integration-e2e
./tests/mock-servers/mock-platform-api
)
83 changes: 83 additions & 0 deletions tests/ai-workspace-cli-e2e/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# AI Workspace CLI end-to-end tests

A godog (BDD) suite that verifies the `ap` CLI's **AI Workspace** behaviour
persists against a real `platform-api` backend. It exercises the documented
end-to-end flow from [`docs/cli/end-to-end-workflow.md`](../../docs/cli/end-to-end-workflow.md)
and [`docs/cli/ai-workspace/README.md`](../../docs/cli/ai-workspace/README.md):

1. Boot `platform-api` (SQLite, standalone) as the AI Workspace backend.
2. Log in as `admin`/`admin` at `/api/portal/v0.9/auth/login` for a bearer token.
3. Create a server-side project (`POST /api/v0.9/projects`) — its handle is the
`--project-id` for the project-scoped kinds — and register the gateways the
artifacts associate with (`POST /api/v0.9/gateways`, handles `prod-eu-01` /
`prod-eu-02`), since `associatedGateways` handles are validated server-side.
4. Register `platform-api` as an AI Workspace target in an isolated CLI config
(`ap ai-workspace add --auth oauth`); the token is supplied per call via
`WSO2AP_AIWORKSPACE_TOKEN`.
5. For each of the three artifact kinds — **LLM provider**, **LLM proxy**,
**MCP proxy** — `ap project init`, apply the `create` content, read it back
with both `ap ai-workspace <group> get --id` and `ap ai-workspace <group>
list`, then apply the `edit` content (which adds `spec.associatedGateways`) to
confirm the update path persists — verified by reading the associated gateway
back from the server.

The artifact content comes from a real demo set (`create`/`edit` variants per
kind). The `edit` metadata adds `spec.associatedGateways`; the LLM proxy
references the LLM provider, so scenarios run provider → proxy → MCP.

It runs **once a day** in CI (see
[`.github/workflows/ai-workspace-cli-e2e.yml`](../../.github/workflows/ai-workspace-cli-e2e.yml)),
not on every PR, because it builds the `platform-api` image and the CLI from
source.

## Layout

```
tests/ai-workspace-cli-e2e/
├── docker-compose.yaml # platform-api only (SQLite, standalone)
├── platform-api-config.toml # file-based auth (admin/admin)
├── suite_test.go # boot/login/create-project/register-workspace/teardown + CLI runner
├── steps_test.go # per-kind init/build/apply/get step definitions
├── features/
│ └── ai-workspace-cli.feature
└── resources/ # demo artifacts copied over the scaffold
├── llm-provider/
│ ├── definition.yaml # shared OpenAPI spec (create + edit)
│ ├── create/{metadata,runtime}.yaml
│ └── edit/{metadata,runtime}.yaml # edit metadata adds spec.associatedGateways
├── llm-proxy/ (same shape; runtime references the claude-provider)
└── mcp/ (same shape; definition.yaml is capabilities, not OpenAPI)
```

The `llm-proxy` artifact references the `claude-provider` created by the
`llm-provider` scenario, so scenarios run provider → proxy → MCP in order and
share the suite-scoped backend, token, project, and registered gateways.

## Running locally

Prerequisites: Docker running, Go 1.26.5.

```shell
# 1. Build the platform-api image (tag must match PLATFORM_API_IMAGE below).
make -C platform-api build IMAGE_NAME=platform-api VERSION=it-e2e

# 2. Build the ap CLI binary (lands at cli/src/build/ap).
make -C cli/src build-skip-tests

# 3. Run the suite.
cd tests/ai-workspace-cli-e2e
PLATFORM_API_IMAGE=platform-api:it-e2e go test -run TestFeatures -count=1 -v -timeout 20m ./...
```

### Knobs

| Variable | Default | Purpose |
| -------------------- | ----------------------- | --------------------------------------------------- |
| `PLATFORM_API_IMAGE` | `platform-api:it-e2e` | platform-api image the compose stack runs. |
| `AP_CLI_BINARY` | `../../cli/src/build/ap`| Path to the `ap` binary under test. |
| `PA_HOST_PORT` | `9243` | Host port platform-api is published on. |
| `AW_TAGS` | (all) | godog tag filter, e.g. `@mcp-proxy`. |
| `AW_KEEP` | (unset) | If set, the compose stack is left running for debug.|

The CLI runs with an isolated `HOME`, so it never touches your real
`~/.wso2ap/config.yaml`.
38 changes: 38 additions & 0 deletions tests/ai-workspace-cli-e2e/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Standalone platform-api stack for the AI Workspace CLI integration test.
#
# platform-api is the AI Workspace backend the `ap ai-workspace` commands target.
# The gateway data plane is NOT needed here: the test only exercises the CLI ->
# platform-api control-plane path (login, projects, llm-providers, llm-proxies,
# mcp-proxies), so a single platform-api process on SQLite is enough.
#
# The image tag is overridable with PLATFORM_API_IMAGE; the CI workflow builds
# platform-api:it-e2e from source and passes it through (same pattern the
# platform-api-devportal-e2e / platform-api-gateway-e2e suites use).
services:
platform-api:
image: ${PLATFORM_API_IMAGE:-platform-api:it-e2e}
command: ["./platform-api", "-config", "/etc/platform-api/config.toml"]
environment:
- DATABASE_DRIVER=sqlite3
- DATABASE_PATH=/app/data/platform.db
- DATABASE_EXECUTE_SCHEMA_DDL=true
# Single encryption key — must be 32 bytes (64 hex chars). Used for all
# at-rest encryption (secrets, subscription tokens, HMAC).
- ENCRYPTION_KEY=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
- AUTH_FILE_BASED_ENABLED=true
- AUTH_FILE_BASED_ORGANIZATION_ID=default
- AUTH_FILE_BASED_ORGANIZATION_UUID=99089a17-72e0-4dd8-a2f4-c8dfbb085295
- AUTH_FILE_BASED_ORGANIZATION_DISPLAY_NAME=Default
- AUTH_FILE_BASED_ORGANIZATION_REGION=us
# Signs login JWTs; must be 32 bytes (64 hex chars). Overrides the
# placeholder in platform-api-config.toml (env wins in koanf).
- AUTH_JWT_SECRET_KEY=fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210
volumes:
- ./platform-api-config.toml:/etc/platform-api/config.toml:ro
ports:
- "${PA_HOST_PORT:-9243}:9243"
networks: [aiwscli]

networks:
aiwscli:
driver: bridge
57 changes: 57 additions & 0 deletions tests/ai-workspace-cli-e2e/features/ai-workspace-cli.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
Feature: AI Workspace CLI publish and persistence
As a platform user driving the `ap` CLI
I want to publish AI Workspace artifacts to platform-api
So that I can confirm the CLI create/update/read behaviour persists on the backend

The suite boots a real platform-api (the AI Workspace backend), logs in as
admin/admin for a bearer token, creates a server-side project, and registers the
gateways the artifacts associate with. Each scenario scaffolds a project with
`ap project init`, applies the "create" demo content, reads it back with both
`get` and `list`, then applies the "edit" demo content (which adds
spec.associatedGateways) to confirm the update path persists too.

Background:
Given the platform-api AI Workspace backend is running
And I am authenticated to the AI Workspace

@llm-provider
Scenario: Publish and update an LLM provider through the CLI
When the "llm-provider" project artifact is initialized
And I build the "llm-provider" artifact
And I apply the "llm-provider" artifact
Then the CLI reports the "llm-provider" artifact was created
And the "llm-provider" artifact is retrievable from the AI Workspace
And the "llm-provider" artifact is listed in the AI Workspace
When I edit the "llm-provider" artifact
And I build the "llm-provider" artifact
And I re-apply the "llm-provider" artifact
Then the CLI reports the "llm-provider" artifact was updated
And the "llm-provider" artifact is associated with gateway "prod-eu-01"

@llm-proxy
Scenario: Publish and update an LLM proxy through the CLI
When the "llm-proxy" project artifact is initialized
And I build the "llm-proxy" artifact
And I apply the "llm-proxy" artifact
Then the CLI reports the "llm-proxy" artifact was created
And the "llm-proxy" artifact is retrievable from the AI Workspace
And the "llm-proxy" artifact is listed in the AI Workspace
When I edit the "llm-proxy" artifact
And I build the "llm-proxy" artifact
And I re-apply the "llm-proxy" artifact
Then the CLI reports the "llm-proxy" artifact was updated
And the "llm-proxy" artifact is associated with gateway "prod-eu-01"

@mcp-proxy
Scenario: Publish and update an MCP proxy through the CLI
When the "mcp-proxy" project artifact is initialized
And I build the "mcp-proxy" artifact
And I apply the "mcp-proxy" artifact
Then the CLI reports the "mcp-proxy" artifact was created
And the "mcp-proxy" artifact is retrievable from the AI Workspace
And the "mcp-proxy" artifact is listed in the AI Workspace
When I edit the "mcp-proxy" artifact
And I build the "mcp-proxy" artifact
And I re-apply the "mcp-proxy" artifact
Then the CLI reports the "mcp-proxy" artifact was updated
And the "mcp-proxy" artifact is associated with gateway "prod-eu-01"
15 changes: 15 additions & 0 deletions tests/ai-workspace-cli-e2e/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
module github.com/wso2/api-platform/tests/ai-workspace-cli-e2e

go 1.26.5

require github.com/cucumber/godog v0.15.0

require (
github.com/cucumber/gherkin/go/v26 v26.2.0 // indirect
github.com/cucumber/messages/go/v21 v21.0.1 // indirect
github.com/gofrs/uuid v4.3.1+incompatible // indirect
github.com/hashicorp/go-immutable-radix v1.3.1 // indirect
github.com/hashicorp/go-memdb v1.3.4 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/spf13/pflag v1.0.5 // indirect
)
48 changes: 48 additions & 0 deletions tests/ai-workspace-cli-e2e/go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/cucumber/gherkin/go/v26 v26.2.0 h1:EgIjePLWiPeslwIWmNQ3XHcypPsWAHoMCz/YEBKP4GI=
github.com/cucumber/gherkin/go/v26 v26.2.0/go.mod h1:t2GAPnB8maCT4lkHL99BDCVNzCh1d7dBhCLt150Nr/0=
github.com/cucumber/godog v0.15.0 h1:51AL8lBXF3f0cyA5CV4TnJFCTHpgiy+1x1Hb3TtZUmo=
github.com/cucumber/godog v0.15.0/go.mod h1:FX3rzIDybWABU4kuIXLZ/qtqEe1Ac5RdXmqvACJOces=
github.com/cucumber/messages/go/v21 v21.0.1 h1:wzA0LxwjlWQYZd32VTlAVDTkW6inOFmSM+RuOwHZiMI=
github.com/cucumber/messages/go/v21 v21.0.1/go.mod h1:zheH/2HS9JLVFukdrsPWoPdmUtmYQAQPLk7w5vWsk5s=
github.com/cucumber/messages/go/v22 v22.0.0/go.mod h1:aZipXTKc0JnjCsXrJnuZpWhtay93k7Rn3Dee7iyPJjs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/gofrs/uuid v4.3.1+incompatible h1:0/KbAdpx3UXAx1kEOWHJeOkpbgRFGHVgv+CFIY7dBJI=
github.com/gofrs/uuid v4.3.1+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/hashicorp/go-immutable-radix v1.3.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-immutable-radix v1.3.1 h1:DKHmCUm2hRBK510BaiZlwvpD40f8bJFeZnpfm2KLowc=
github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-memdb v1.3.4 h1:XSL3NR682X/cVk2IeV0d70N4DZ9ljI885xAEU8IoK3c=
github.com/hashicorp/go-memdb v1.3.4/go.mod h1:uBTr1oQbtuMgd1SSGoR8YV27eT3sBHbYiNm53bMpgSg=
github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.4 h1:YDjusn29QI/Das2iO9M0BHnIbxPeyuCHsjMW+lJfyTc=
github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
26 changes: 26 additions & 0 deletions tests/ai-workspace-cli-e2e/platform-api-config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# platform-api configuration for the combined e2e stack.
# Database settings come from environment variables (see docker-compose.yaml);
# this file supplies file-based auth so the scenario can log in (admin/admin).

[auth.jwt]
enabled = true
issuer = "platform-api"
secret_key = "e2e-integration-secret-key-0123456789"

[auth.file_based]
enabled = true

[auth.file_based.organization]
id = "default" # organization handle (URL-safe slug)
display_name = "Default"
region = "us"

# Default login: admin / admin (bcrypt hash of "admin", reused from the shipped sample config).
[[auth.file_based.users]]
username = "admin"
password_hash = "$2y$10$U2yKMwGamGwDoMu0hRPT7u8nCuP8z/qxHFOKV6dhIxkJN9NJ0eVQ."
# NOTE: platform-api ignores file-based users from a mounted config (defaultConfig's
# admin wins in koanf's slice merge), so scopes here are not authoritative. The
# @devportal stack instead injects the admin (with dp:* scopes) via the
# AUTH_FILE_BASED_USERS env var, which does override the default. See suite_test.go.
scopes = "ap:organization:manage ap:gateway:manage ap:gateway_custom_policy:manage ap:rest_api:manage ap:llm_provider:manage ap:llm_proxy:manage ap:mcp_proxy:manage ap:webbroker_api:manage ap:websub_api:manage ap:application:manage ap:subscription:manage ap:subscription_plan:manage ap:project:manage ap:llm_template:manage ap:devportal:manage ap:api_key:read ap:secret:manage"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: ai-workspace.api-platform.wso2.com/v1alpha
kind: LlmProvider
metadata:
name: claude-provider
spec:
displayName: wso2 claude provider
version: v1.0
Loading
Loading