Skip to content

Commit d2529b2

Browse files
AlexsJonesclaude
andcommitted
test: add Cypress tests for web-endpoint skill
Covers wizard creation with skill toggle, instance detail Web Endpoint tab, Gateway routes card, and API-level validation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0abc444 commit d2529b2

3 files changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Test: verify web-endpoint skill at the API level — instance spec contains
2+
// the skill after creation, and the skill can be toggled off via update.
3+
4+
const INSTANCE = `cy-webep-api-${Date.now()}`;
5+
6+
function authHeaders(): Record<string, string> {
7+
const token = Cypress.env("API_TOKEN");
8+
const h: Record<string, string> = { "Content-Type": "application/json" };
9+
if (token) h["Authorization"] = `Bearer ${token}`;
10+
return h;
11+
}
12+
13+
describe("Web-endpoint — API validation", () => {
14+
before(() => {
15+
cy.createLMStudioInstance(INSTANCE, { skills: ["web-endpoint"] });
16+
});
17+
18+
after(() => {
19+
cy.deleteInstance(INSTANCE);
20+
});
21+
22+
it("instance spec includes web-endpoint skill", () => {
23+
cy.request({
24+
url: `/api/v1/instances/${INSTANCE}?namespace=default`,
25+
headers: authHeaders(),
26+
}).then((resp) => {
27+
expect(resp.status).to.eq(200);
28+
const skills = resp.body.spec.skills as { skillPackRef: string }[];
29+
expect(skills).to.be.an("array");
30+
const hasWebEndpoint = skills.some(
31+
(s) => s.skillPackRef === "web-endpoint",
32+
);
33+
expect(hasWebEndpoint).to.be.true;
34+
});
35+
});
36+
37+
it("web-endpoint skill has default rate_limit_rpm", () => {
38+
cy.request({
39+
url: `/api/v1/instances/${INSTANCE}?namespace=default`,
40+
headers: authHeaders(),
41+
}).then((resp) => {
42+
const webSkill = (
43+
resp.body.spec.skills as { skillPackRef: string; params?: Record<string, string> }[]
44+
).find((s) => s.skillPackRef === "web-endpoint");
45+
// Default RPM is 60 (may be omitted or explicit).
46+
const rpm = webSkill?.params?.rate_limit_rpm;
47+
if (rpm) {
48+
expect(rpm).to.eq("60");
49+
}
50+
});
51+
});
52+
});
53+
54+
export {};
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Test: verify web-endpoint skill surfaces correctly on the instance detail
2+
// page (Web Endpoint tab) and on the Gateway routes card.
3+
4+
const INSTANCE = `cy-webep-detail-${Date.now()}`;
5+
6+
describe("Web-endpoint — detail page and gateway", () => {
7+
before(() => {
8+
cy.createLMStudioInstance(INSTANCE, { skills: ["web-endpoint"] });
9+
cy.wait(2000);
10+
});
11+
12+
after(() => {
13+
cy.deleteInstance(INSTANCE);
14+
});
15+
16+
it("shows the Web Endpoint tab with skill config", () => {
17+
cy.visit(`/instances/${INSTANCE}`);
18+
19+
// The Web Endpoint tab should exist.
20+
cy.contains("button", "Web Endpoint", { timeout: 20000 }).click();
21+
22+
// Tab content shows rate limit and hostname fields.
23+
cy.contains("Rate Limit").should("be.visible");
24+
cy.contains("60 req/min").should("be.visible");
25+
cy.contains("auto from gateway").should("be.visible");
26+
});
27+
28+
it("instance without web-endpoint shows disabled message", () => {
29+
// Create a bare instance (no skills).
30+
const BARE = `cy-bare-${Date.now()}`;
31+
cy.createLMStudioInstance(BARE);
32+
cy.wait(2000);
33+
34+
cy.visit(`/instances/${BARE}`);
35+
cy.contains("button", "Web Endpoint", { timeout: 20000 }).click();
36+
cy.contains("Web endpoint is not enabled").should("be.visible");
37+
38+
cy.deleteInstance(BARE);
39+
});
40+
41+
it("instance appears in the Gateway routes card", () => {
42+
cy.visit("/gateway");
43+
44+
// Routes card may be below the fold — scroll to it.
45+
cy.contains("Routes", { timeout: 20000 }).scrollIntoView().should("be.visible");
46+
cy.contains(INSTANCE, { timeout: 20000 }).scrollIntoView().should("be.visible");
47+
});
48+
});
49+
50+
export {};
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
// Test: create an instance via the wizard with the web-endpoint skill enabled.
2+
// Verifies the inline config (RPM, hostname) appears and persists to confirm step.
3+
4+
const INSTANCE = `cy-webep-wiz-${Date.now()}`;
5+
6+
describe("Create Instance — web-endpoint skill", () => {
7+
after(() => {
8+
cy.deleteInstance(INSTANCE);
9+
});
10+
11+
it("walks the wizard, enables web-endpoint, and creates the instance", () => {
12+
cy.visit("/instances");
13+
cy.contains("button", "Create Instance", { timeout: 20000 }).click();
14+
15+
// ── Step 1: Name ──────────────────────────────────────────
16+
cy.get("[role='dialog']")
17+
.find("input[placeholder='my-agent']")
18+
.clear()
19+
.type(INSTANCE);
20+
cy.wizardNext();
21+
22+
// ── Step 2: Provider — LM Studio ──────────────────────────
23+
cy.get("[role='dialog']")
24+
.find("button[role='combobox']")
25+
.click({ force: true });
26+
cy.get("[data-radix-popper-content-wrapper]")
27+
.contains("LM Studio")
28+
.click({ force: true });
29+
cy.wizardNext();
30+
31+
// ── Step 3: Auth ──────────────────────────────────────────
32+
cy.wizardNext();
33+
34+
// ── Step 4: Model ─────────────────────────────────────────
35+
cy.get("[role='dialog']")
36+
.find("input[placeholder='gpt-4o']")
37+
.clear()
38+
.type("qwen/qwen3.5-9b");
39+
cy.wizardNext();
40+
41+
// ── Step 5: Skills — toggle web-endpoint ──────────────────
42+
cy.get("[role='dialog']").contains("button", "web-endpoint").click();
43+
44+
// Inline config for web-endpoint should appear.
45+
cy.get("[role='dialog']").contains("Web Endpoint Config").should("exist");
46+
cy.get("[role='dialog']")
47+
.find("input[type='number']")
48+
.should("have.value", "60");
49+
cy.get("[role='dialog']")
50+
.find("input[placeholder='auto from gateway']")
51+
.should("exist");
52+
53+
// Set a custom RPM.
54+
cy.get("[role='dialog']")
55+
.find("input[type='number']")
56+
.focus()
57+
.type("{selectall}120");
58+
59+
cy.wizardNext();
60+
61+
// ── Step 6: Heartbeat ─────────────────────────────────────
62+
cy.get("[role='dialog']")
63+
.contains("button", "No heartbeat")
64+
.click({ force: true });
65+
cy.wizardNext();
66+
67+
// ── Step 7: Channels ──────────────────────────────────────
68+
cy.wizardNext();
69+
70+
// ── Step 8: Confirm ───────────────────────────────────────
71+
cy.get("[role='dialog']").contains(INSTANCE);
72+
cy.get("[role='dialog']").contains("lm-studio");
73+
cy.get("[role='dialog']").contains("Web Endpoint").scrollIntoView();
74+
cy.get("[role='dialog']").contains("120 rpm");
75+
76+
cy.get("[role='dialog']")
77+
.contains("button", "Create")
78+
.scrollIntoView()
79+
.click({ force: true });
80+
81+
// Dialog closes on success.
82+
cy.get("[role='dialog']").should("not.exist", { timeout: 20000 });
83+
84+
// Instance appears in the list.
85+
cy.contains(INSTANCE, { timeout: 20000 }).should("be.visible");
86+
});
87+
});
88+
89+
export {};

0 commit comments

Comments
 (0)