Skip to content

Commit 66927a5

Browse files
committed
Add CI/publish workflows and petstore integration test
- CI workflow: lint, test, build on push/PR to main (uses mise-action) - Publish workflow: npm trusted publishing via OIDC on GitHub release - Integration test: request limiting against live Petstore API
1 parent bbc7b1e commit 66927a5

3 files changed

Lines changed: 208 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
ci:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: jdx/mise-action@v2
16+
17+
- run: task install
18+
19+
- run: task ci

.github/workflows/publish.yml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
name: Publish
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
publish:
9+
runs-on: ubuntu-latest
10+
permissions:
11+
contents: read
12+
id-token: write
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- uses: jdx/mise-action@v2
17+
18+
- run: task install
19+
20+
- run: task ci
21+
22+
- run: pnpm publish --access public --no-git-checks

test/petstore-limit.test.ts

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import { describe, it, expect, afterEach } from "vitest";
2+
import { CodeMode } from "../src/codemode.js";
3+
import type { Executor, ExecuteResult } from "../src/types.js";
4+
5+
const PETSTORE_BASE = "https://petstore3.swagger.io";
6+
7+
const petstoreSpec = {
8+
openapi: "3.0.0",
9+
info: { title: "Swagger Petstore", version: "1.0.27" },
10+
paths: {
11+
"/api/v3/pet/findByStatus": {
12+
get: { summary: "Finds Pets by status", operationId: "findPetsByStatus" },
13+
},
14+
"/api/v3/store/inventory": {
15+
get: { summary: "Returns pet inventories by status", operationId: "getInventory" },
16+
},
17+
"/api/v3/user/login": {
18+
get: { summary: "Logs user into the system", operationId: "loginUser" },
19+
},
20+
},
21+
};
22+
23+
class TestExecutor implements Executor {
24+
async execute(
25+
code: string,
26+
globals: Record<string, unknown>,
27+
): Promise<ExecuteResult> {
28+
const globalNames = Object.keys(globals);
29+
const globalValues = Object.values(globals);
30+
const noopConsole = { log: () => {}, warn: () => {}, error: () => {} };
31+
32+
try {
33+
const fn = new Function("console", ...globalNames, `return (${code})();`);
34+
const result = await fn(noopConsole, ...globalValues);
35+
return { result };
36+
} catch (err) {
37+
return {
38+
result: undefined,
39+
error: err instanceof Error ? err.message : String(err),
40+
};
41+
}
42+
}
43+
}
44+
45+
describe("request limiting against live Petstore API", () => {
46+
let cm: CodeMode;
47+
48+
afterEach(() => {
49+
cm?.dispose();
50+
});
51+
52+
it("allows requests within the limit", async () => {
53+
cm = new CodeMode({
54+
spec: petstoreSpec,
55+
request: fetch,
56+
baseUrl: PETSTORE_BASE,
57+
namespace: "petstore",
58+
maxRequests: 3,
59+
executor: new TestExecutor(),
60+
});
61+
62+
const result = await cm.execute(`
63+
async () => {
64+
const r1 = await petstore.request({ method: "GET", path: "/api/v3/pet/findByStatus", query: { status: "available" } });
65+
const r2 = await petstore.request({ method: "GET", path: "/api/v3/pet/findByStatus", query: { status: "sold" } });
66+
return { count: 2, r1Status: r1.status, r2Status: r2.status };
67+
}
68+
`);
69+
70+
// We're testing that the request bridge didn't block these — not the petstore response codes
71+
expect(result.isError).toBeUndefined();
72+
const data = JSON.parse(result.content[0]!.text);
73+
expect(data.count).toBe(2);
74+
expect(typeof data.r1Status).toBe("number");
75+
expect(typeof data.r2Status).toBe("number");
76+
});
77+
78+
it("returns error (not crash) when limit exceeded", async () => {
79+
cm = new CodeMode({
80+
spec: petstoreSpec,
81+
request: fetch,
82+
baseUrl: PETSTORE_BASE,
83+
namespace: "petstore",
84+
maxRequests: 1,
85+
executor: new TestExecutor(),
86+
});
87+
88+
const result = await cm.execute(`
89+
async () => {
90+
await petstore.request({ method: "GET", path: "/api/v3/pet/findByStatus", query: { status: "available" } });
91+
await petstore.request({ method: "GET", path: "/api/v3/user/login", query: { username: "test", password: "test" } });
92+
return "should not reach here";
93+
}
94+
`);
95+
96+
expect(result.isError).toBe(true);
97+
expect(result.content[0]!.text).toContain("Request limit exceeded");
98+
});
99+
100+
it("resets counter between execute() calls", async () => {
101+
cm = new CodeMode({
102+
spec: petstoreSpec,
103+
request: fetch,
104+
baseUrl: PETSTORE_BASE,
105+
namespace: "petstore",
106+
maxRequests: 2,
107+
executor: new TestExecutor(),
108+
});
109+
110+
// First execution: 2 requests (at limit)
111+
const r1 = await cm.execute(`
112+
async () => {
113+
await petstore.request({ method: "GET", path: "/api/v3/pet/findByStatus", query: { status: "available" } });
114+
await petstore.request({ method: "GET", path: "/api/v3/user/login", query: { username: "test", password: "test" } });
115+
return "first-ok";
116+
}
117+
`);
118+
expect(r1.isError).toBeUndefined();
119+
expect(r1.content[0]!.text).toBe("first-ok");
120+
121+
// Second execution: gets a fresh counter, should also succeed
122+
const r2 = await cm.execute(`
123+
async () => {
124+
await petstore.request({ method: "GET", path: "/api/v3/pet/findByStatus", query: { status: "sold" } });
125+
await petstore.request({ method: "GET", path: "/api/v3/user/login", query: { username: "a", password: "b" } });
126+
return "second-ok";
127+
}
128+
`);
129+
expect(r2.isError).toBeUndefined();
130+
expect(r2.content[0]!.text).toBe("second-ok");
131+
});
132+
133+
it("blocks exactly at the boundary", async () => {
134+
cm = new CodeMode({
135+
spec: petstoreSpec,
136+
request: fetch,
137+
baseUrl: PETSTORE_BASE,
138+
namespace: "petstore",
139+
maxRequests: 2,
140+
executor: new TestExecutor(),
141+
});
142+
143+
// 2 requests: OK (exactly at limit)
144+
const ok = await cm.execute(`
145+
async () => {
146+
await petstore.request({ method: "GET", path: "/api/v3/pet/findByStatus", query: { status: "sold" } });
147+
await petstore.request({ method: "GET", path: "/api/v3/user/login", query: { username: "test", password: "test" } });
148+
return "within-limit";
149+
}
150+
`);
151+
expect(ok.isError).toBeUndefined();
152+
expect(ok.content[0]!.text).toBe("within-limit");
153+
154+
// 3 requests in a new execute(): should fail on the 3rd
155+
const fail = await cm.execute(`
156+
async () => {
157+
await petstore.request({ method: "GET", path: "/api/v3/pet/findByStatus", query: { status: "available" } });
158+
await petstore.request({ method: "GET", path: "/api/v3/pet/findByStatus", query: { status: "sold" } });
159+
await petstore.request({ method: "GET", path: "/api/v3/user/login", query: { username: "a", password: "b" } });
160+
return "should not reach here";
161+
}
162+
`);
163+
expect(fail.isError).toBe(true);
164+
expect(fail.content[0]!.text).toContain("Request limit exceeded");
165+
expect(fail.content[0]!.text).toContain("max 2");
166+
});
167+
});

0 commit comments

Comments
 (0)