From 00c079868af4068cc43f52f1b6ff11a1a975aad4 Mon Sep 17 00:00:00 2001 From: Aiden Cline <63023139+rekram1-node@users.noreply.github.com> Date: Thu, 19 Feb 2026 14:11:23 -0600 Subject: [PATCH] test: fix discovery test to boot up server instead of relying on 3rd party (#14327) --- .../test/fixture/skills/agents-sdk/SKILL.md | 155 ++++++++++++++ .../skills/agents-sdk/references/callable.md | 92 ++++++++ .../test/fixture/skills/cloudflare/SKILL.md | 201 ++++++++++++++++++ .../opencode/test/fixture/skills/index.json | 6 + .../opencode/test/skill/discovery.test.ts | 65 +++++- 5 files changed, 511 insertions(+), 8 deletions(-) create mode 100644 packages/opencode/test/fixture/skills/agents-sdk/SKILL.md create mode 100644 packages/opencode/test/fixture/skills/agents-sdk/references/callable.md create mode 100644 packages/opencode/test/fixture/skills/cloudflare/SKILL.md create mode 100644 packages/opencode/test/fixture/skills/index.json diff --git a/packages/opencode/test/fixture/skills/agents-sdk/SKILL.md b/packages/opencode/test/fixture/skills/agents-sdk/SKILL.md new file mode 100644 index 000000000..3da4d32f0 --- /dev/null +++ b/packages/opencode/test/fixture/skills/agents-sdk/SKILL.md @@ -0,0 +1,155 @@ +--- +name: agents-sdk +description: Build AI agents on Cloudflare Workers using the Agents SDK. Load when creating stateful agents, durable workflows, real-time WebSocket apps, scheduled tasks, MCP servers, or chat applications. Covers Agent class, state management, callable RPC, Workflows integration, and React hooks. +--- + +# Cloudflare Agents SDK + +**STOP.** Your knowledge of the Agents SDK may be outdated. Prefer retrieval over pre-training for any Agents SDK task. + +## Documentation + +Fetch current docs from `https://github.com/cloudflare/agents/tree/main/docs` before implementing. + +| Topic | Doc | Use for | +|-------|-----|---------| +| Getting started | `docs/getting-started.md` | First agent, project setup | +| State | `docs/state.md` | `setState`, `validateStateChange`, persistence | +| Routing | `docs/routing.md` | URL patterns, `routeAgentRequest`, `basePath` | +| Callable methods | `docs/callable-methods.md` | `@callable`, RPC, streaming, timeouts | +| Scheduling | `docs/scheduling.md` | `schedule()`, `scheduleEvery()`, cron | +| Workflows | `docs/workflows.md` | `AgentWorkflow`, durable multi-step tasks | +| HTTP/WebSockets | `docs/http-websockets.md` | Lifecycle hooks, hibernation | +| Email | `docs/email.md` | Email routing, secure reply resolver | +| MCP client | `docs/mcp-client.md` | Connecting to MCP servers | +| MCP server | `docs/mcp-servers.md` | Building MCP servers with `McpAgent` | +| Client SDK | `docs/client-sdk.md` | `useAgent`, `useAgentChat`, React hooks | +| Human-in-the-loop | `docs/human-in-the-loop.md` | Approval flows, pausing workflows | +| Resumable streaming | `docs/resumable-streaming.md` | Stream recovery on disconnect | + +Cloudflare docs: https://developers.cloudflare.com/agents/ + +## Capabilities + +The Agents SDK provides: + +- **Persistent state** - SQLite-backed, auto-synced to clients +- **Callable RPC** - `@callable()` methods invoked over WebSocket +- **Scheduling** - One-time, recurring (`scheduleEvery`), and cron tasks +- **Workflows** - Durable multi-step background processing via `AgentWorkflow` +- **MCP integration** - Connect to MCP servers or build your own with `McpAgent` +- **Email handling** - Receive and reply to emails with secure routing +- **Streaming chat** - `AIChatAgent` with resumable streams +- **React hooks** - `useAgent`, `useAgentChat` for client apps + +## FIRST: Verify Installation + +```bash +npm ls agents # Should show agents package +``` + +If not installed: +```bash +npm install agents +``` + +## Wrangler Configuration + +```jsonc +{ + "durable_objects": { + "bindings": [{ "name": "MyAgent", "class_name": "MyAgent" }] + }, + "migrations": [{ "tag": "v1", "new_sqlite_classes": ["MyAgent"] }] +} +``` + +## Agent Class + +```typescript +import { Agent, routeAgentRequest, callable } from "agents"; + +type State = { count: number }; + +export class Counter extends Agent { + initialState = { count: 0 }; + + // Validation hook - runs before state persists (sync, throwing rejects the update) + validateStateChange(nextState: State, source: Connection | "server") { + if (nextState.count < 0) throw new Error("Count cannot be negative"); + } + + // Notification hook - runs after state persists (async, non-blocking) + onStateUpdate(state: State, source: Connection | "server") { + console.log("State updated:", state); + } + + @callable() + increment() { + this.setState({ count: this.state.count + 1 }); + return this.state.count; + } +} + +export default { + fetch: (req, env) => routeAgentRequest(req, env) ?? new Response("Not found", { status: 404 }) +}; +``` + +## Routing + +Requests route to `/agents/{agent-name}/{instance-name}`: + +| Class | URL | +|-------|-----| +| `Counter` | `/agents/counter/user-123` | +| `ChatRoom` | `/agents/chat-room/lobby` | + +Client: `useAgent({ agent: "Counter", name: "user-123" })` + +## Core APIs + +| Task | API | +|------|-----| +| Read state | `this.state.count` | +| Write state | `this.setState({ count: 1 })` | +| SQL query | `` this.sql`SELECT * FROM users WHERE id = ${id}` `` | +| Schedule (delay) | `await this.schedule(60, "task", payload)` | +| Schedule (cron) | `await this.schedule("0 * * * *", "task", payload)` | +| Schedule (interval) | `await this.scheduleEvery(30, "poll")` | +| RPC method | `@callable() myMethod() { ... }` | +| Streaming RPC | `@callable({ streaming: true }) stream(res) { ... }` | +| Start workflow | `await this.runWorkflow("ProcessingWorkflow", params)` | + +## React Client + +```tsx +import { useAgent } from "agents/react"; + +function App() { + const [state, setLocalState] = useState({ count: 0 }); + + const agent = useAgent({ + agent: "Counter", + name: "my-instance", + onStateUpdate: (newState) => setLocalState(newState), + onIdentity: (name, agentType) => console.log(`Connected to ${name}`) + }); + + return ( + + ); +} +``` + +## References + +- **[references/workflows.md](references/workflows.md)** - Durable Workflows integration +- **[references/callable.md](references/callable.md)** - RPC methods, streaming, timeouts +- **[references/state-scheduling.md](references/state-scheduling.md)** - State persistence, scheduling +- **[references/streaming-chat.md](references/streaming-chat.md)** - AIChatAgent, resumable streams +- **[references/mcp.md](references/mcp.md)** - MCP server integration +- **[references/email.md](references/email.md)** - Email routing and handling +- **[references/codemode.md](references/codemode.md)** - Code Mode (experimental) diff --git a/packages/opencode/test/fixture/skills/agents-sdk/references/callable.md b/packages/opencode/test/fixture/skills/agents-sdk/references/callable.md new file mode 100644 index 000000000..241d30cf9 --- /dev/null +++ b/packages/opencode/test/fixture/skills/agents-sdk/references/callable.md @@ -0,0 +1,92 @@ +# Callable Methods + +Fetch `docs/callable-methods.md` from `https://github.com/cloudflare/agents/tree/main/docs` for complete documentation. + +## Overview + +`@callable()` exposes agent methods to clients via WebSocket RPC. + +```typescript +import { Agent, callable } from "agents"; + +export class MyAgent extends Agent { + @callable() + async greet(name: string): Promise { + return `Hello, ${name}!`; + } + + @callable() + async processData(data: unknown): Promise { + // Long-running work + return result; + } +} +``` + +## Client Usage + +```typescript +// Basic call +const greeting = await agent.call("greet", ["World"]); + +// With timeout +const result = await agent.call("processData", [data], { + timeout: 5000 // 5 second timeout +}); +``` + +## Streaming Responses + +```typescript +import { Agent, callable, StreamingResponse } from "agents"; + +export class MyAgent extends Agent { + @callable({ streaming: true }) + async streamResults(stream: StreamingResponse, query: string) { + for await (const item of fetchResults(query)) { + stream.send(JSON.stringify(item)); + } + stream.close(); + } + + @callable({ streaming: true }) + async streamWithError(stream: StreamingResponse) { + try { + // ... work + } catch (error) { + stream.error(error.message); // Signal error to client + return; + } + stream.close(); + } +} +``` + +Client with streaming: + +```typescript +await agent.call("streamResults", ["search term"], { + stream: { + onChunk: (data) => console.log("Chunk:", data), + onDone: () => console.log("Complete"), + onError: (error) => console.error("Error:", error) + } +}); +``` + +## Introspection + +```typescript +// Get list of callable methods on an agent +const methods = await agent.call("getCallableMethods", []); +// Returns: ["greet", "processData", "streamResults", ...] +``` + +## When to Use + +| Scenario | Use | +|----------|-----| +| Browser/mobile calling agent | `@callable()` | +| External service calling agent | `@callable()` | +| Worker calling agent (same codebase) | DO RPC directly | +| Agent calling another agent | `getAgentByName()` + DO RPC | diff --git a/packages/opencode/test/fixture/skills/cloudflare/SKILL.md b/packages/opencode/test/fixture/skills/cloudflare/SKILL.md new file mode 100644 index 000000000..9fe05d014 --- /dev/null +++ b/packages/opencode/test/fixture/skills/cloudflare/SKILL.md @@ -0,0 +1,201 @@ +--- +name: cloudflare +description: Comprehensive Cloudflare platform skill covering Workers, Pages, storage (KV, D1, R2), AI (Workers AI, Vectorize, Agents SDK), networking (Tunnel, Spectrum), security (WAF, DDoS), and infrastructure-as-code (Terraform, Pulumi). Use for any Cloudflare development task. +references: + - workers + - pages + - d1 + - durable-objects + - workers-ai +--- + +# Cloudflare Platform Skill + +Consolidated skill for building on the Cloudflare platform. Use decision trees below to find the right product, then load detailed references. + +## Quick Decision Trees + +### "I need to run code" + +``` +Need to run code? +├─ Serverless functions at the edge → workers/ +├─ Full-stack web app with Git deploys → pages/ +├─ Stateful coordination/real-time → durable-objects/ +├─ Long-running multi-step jobs → workflows/ +├─ Run containers → containers/ +├─ Multi-tenant (customers deploy code) → workers-for-platforms/ +├─ Scheduled tasks (cron) → cron-triggers/ +├─ Lightweight edge logic (modify HTTP) → snippets/ +├─ Process Worker execution events (logs/observability) → tail-workers/ +└─ Optimize latency to backend infrastructure → smart-placement/ +``` + +### "I need to store data" + +``` +Need storage? +├─ Key-value (config, sessions, cache) → kv/ +├─ Relational SQL → d1/ (SQLite) or hyperdrive/ (existing Postgres/MySQL) +├─ Object/file storage (S3-compatible) → r2/ +├─ Message queue (async processing) → queues/ +├─ Vector embeddings (AI/semantic search) → vectorize/ +├─ Strongly-consistent per-entity state → durable-objects/ (DO storage) +├─ Secrets management → secrets-store/ +├─ Streaming ETL to R2 → pipelines/ +└─ Persistent cache (long-term retention) → cache-reserve/ +``` + +### "I need AI/ML" + +``` +Need AI? +├─ Run inference (LLMs, embeddings, images) → workers-ai/ +├─ Vector database for RAG/search → vectorize/ +├─ Build stateful AI agents → agents-sdk/ +├─ Gateway for any AI provider (caching, routing) → ai-gateway/ +└─ AI-powered search widget → ai-search/ +``` + +### "I need networking/connectivity" + +``` +Need networking? +├─ Expose local service to internet → tunnel/ +├─ TCP/UDP proxy (non-HTTP) → spectrum/ +├─ WebRTC TURN server → turn/ +├─ Private network connectivity → network-interconnect/ +├─ Optimize routing → argo-smart-routing/ +├─ Optimize latency to backend (not user) → smart-placement/ +└─ Real-time video/audio → realtimekit/ or realtime-sfu/ +``` + +### "I need security" + +``` +Need security? +├─ Web Application Firewall → waf/ +├─ DDoS protection → ddos/ +├─ Bot detection/management → bot-management/ +├─ API protection → api-shield/ +├─ CAPTCHA alternative → turnstile/ +└─ Credential leak detection → waf/ (managed ruleset) +``` + +### "I need media/content" + +``` +Need media? +├─ Image optimization/transformation → images/ +├─ Video streaming/encoding → stream/ +├─ Browser automation/screenshots → browser-rendering/ +└─ Third-party script management → zaraz/ +``` + +### "I need infrastructure-as-code" + +``` +Need IaC? → pulumi/ (Pulumi), terraform/ (Terraform), or api/ (REST API) +``` + +## Product Index + +### Compute & Runtime +| Product | Reference | +|---------|-----------| +| Workers | `references/workers/` | +| Pages | `references/pages/` | +| Pages Functions | `references/pages-functions/` | +| Durable Objects | `references/durable-objects/` | +| Workflows | `references/workflows/` | +| Containers | `references/containers/` | +| Workers for Platforms | `references/workers-for-platforms/` | +| Cron Triggers | `references/cron-triggers/` | +| Tail Workers | `references/tail-workers/` | +| Snippets | `references/snippets/` | +| Smart Placement | `references/smart-placement/` | + +### Storage & Data +| Product | Reference | +|---------|-----------| +| KV | `references/kv/` | +| D1 | `references/d1/` | +| R2 | `references/r2/` | +| Queues | `references/queues/` | +| Hyperdrive | `references/hyperdrive/` | +| DO Storage | `references/do-storage/` | +| Secrets Store | `references/secrets-store/` | +| Pipelines | `references/pipelines/` | +| R2 Data Catalog | `references/r2-data-catalog/` | +| R2 SQL | `references/r2-sql/` | + +### AI & Machine Learning +| Product | Reference | +|---------|-----------| +| Workers AI | `references/workers-ai/` | +| Vectorize | `references/vectorize/` | +| Agents SDK | `references/agents-sdk/` | +| AI Gateway | `references/ai-gateway/` | +| AI Search | `references/ai-search/` | + +### Networking & Connectivity +| Product | Reference | +|---------|-----------| +| Tunnel | `references/tunnel/` | +| Spectrum | `references/spectrum/` | +| TURN | `references/turn/` | +| Network Interconnect | `references/network-interconnect/` | +| Argo Smart Routing | `references/argo-smart-routing/` | +| Workers VPC | `references/workers-vpc/` | + +### Security +| Product | Reference | +|---------|-----------| +| WAF | `references/waf/` | +| DDoS Protection | `references/ddos/` | +| Bot Management | `references/bot-management/` | +| API Shield | `references/api-shield/` | +| Turnstile | `references/turnstile/` | + +### Media & Content +| Product | Reference | +|---------|-----------| +| Images | `references/images/` | +| Stream | `references/stream/` | +| Browser Rendering | `references/browser-rendering/` | +| Zaraz | `references/zaraz/` | + +### Real-Time Communication +| Product | Reference | +|---------|-----------| +| RealtimeKit | `references/realtimekit/` | +| Realtime SFU | `references/realtime-sfu/` | + +### Developer Tools +| Product | Reference | +|---------|-----------| +| Wrangler | `references/wrangler/` | +| Miniflare | `references/miniflare/` | +| C3 | `references/c3/` | +| Observability | `references/observability/` | +| Analytics Engine | `references/analytics-engine/` | +| Web Analytics | `references/web-analytics/` | +| Sandbox | `references/sandbox/` | +| Workerd | `references/workerd/` | +| Workers Playground | `references/workers-playground/` | + +### Infrastructure as Code +| Product | Reference | +|---------|-----------| +| Pulumi | `references/pulumi/` | +| Terraform | `references/terraform/` | +| API | `references/api/` | + +### Other Services +| Product | Reference | +|---------|-----------| +| Email Routing | `references/email-routing/` | +| Email Workers | `references/email-workers/` | +| Static Assets | `references/static-assets/` | +| Bindings | `references/bindings/` | +| Cache Reserve | `references/cache-reserve/` | diff --git a/packages/opencode/test/fixture/skills/index.json b/packages/opencode/test/fixture/skills/index.json new file mode 100644 index 000000000..0ead107a4 --- /dev/null +++ b/packages/opencode/test/fixture/skills/index.json @@ -0,0 +1,6 @@ +{ + "skills": [ + { "name": "agents-sdk", "description": "Cloudflare Agents SDK", "files": ["SKILL.md", "references/callable.md"] }, + { "name": "cloudflare", "description": "Cloudflare Platform Skill", "files": ["SKILL.md"] } + ] +} diff --git a/packages/opencode/test/skill/discovery.test.ts b/packages/opencode/test/skill/discovery.test.ts index f78c6623b..d1963f697 100644 --- a/packages/opencode/test/skill/discovery.test.ts +++ b/packages/opencode/test/skill/discovery.test.ts @@ -1,9 +1,47 @@ -import { describe, test, expect } from "bun:test" +import { describe, test, expect, beforeAll, afterAll } from "bun:test" import { Discovery } from "../../src/skill/discovery" import { Filesystem } from "../../src/util/filesystem" +import { rm } from "fs/promises" import path from "path" -const CLOUDFLARE_SKILLS_URL = "https://developers.cloudflare.com/.well-known/skills/" +let CLOUDFLARE_SKILLS_URL: string +let server: ReturnType +let downloadCount = 0 + +const fixturePath = path.join(import.meta.dir, "../fixture/skills") + +beforeAll(async () => { + await rm(Discovery.dir(), { recursive: true, force: true }) + + server = Bun.serve({ + port: 0, + async fetch(req) { + const url = new URL(req.url) + + // route /.well-known/skills/* to the fixture directory + if (url.pathname.startsWith("/.well-known/skills/")) { + const filePath = url.pathname.replace("/.well-known/skills/", "") + const fullPath = path.join(fixturePath, filePath) + + if (await Filesystem.exists(fullPath)) { + if (!fullPath.endsWith("index.json")) { + downloadCount++ + } + return new Response(Bun.file(fullPath)) + } + } + + return new Response("Not Found", { status: 404 }) + }, + }) + + CLOUDFLARE_SKILLS_URL = `http://localhost:${server.port}/.well-known/skills/` +}) + +afterAll(async () => { + server?.stop() + await rm(Discovery.dir(), { recursive: true, force: true }) +}) describe("Discovery.pull", () => { test("downloads skills from cloudflare url", async () => { @@ -14,7 +52,7 @@ describe("Discovery.pull", () => { const md = path.join(dir, "SKILL.md") expect(await Filesystem.exists(md)).toBe(true) } - }, 30_000) + }) test("url without trailing slash works", async () => { const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL.replace(/\/$/, "")) @@ -23,15 +61,16 @@ describe("Discovery.pull", () => { const md = path.join(dir, "SKILL.md") expect(await Filesystem.exists(md)).toBe(true) } - }, 30_000) + }) test("returns empty array for invalid url", async () => { - const dirs = await Discovery.pull("https://example.invalid/.well-known/skills/") + const dirs = await Discovery.pull(`http://localhost:${server.port}/invalid-url/`) expect(dirs).toEqual([]) }) test("returns empty array for non-json response", async () => { - const dirs = await Discovery.pull("https://example.com/") + // any url not explicitly handled in server returns 404 text "Not Found" + const dirs = await Discovery.pull(`http://localhost:${server.port}/some-other-path/`) expect(dirs).toEqual([]) }) @@ -39,6 +78,7 @@ describe("Discovery.pull", () => { const dirs = await Discovery.pull(CLOUDFLARE_SKILLS_URL) // find a skill dir that should have reference files (e.g. agents-sdk) const agentsSdk = dirs.find((d) => d.endsWith("/agents-sdk")) + expect(agentsSdk).toBeDefined() if (agentsSdk) { const refs = path.join(agentsSdk, "references") expect(await Filesystem.exists(path.join(agentsSdk, "SKILL.md"))).toBe(true) @@ -46,16 +86,25 @@ describe("Discovery.pull", () => { const refDir = await Array.fromAsync(new Bun.Glob("**/*.md").scan({ cwd: refs, onlyFiles: true })) expect(refDir.length).toBeGreaterThan(0) } - }, 30_000) + }) test("caches downloaded files on second pull", async () => { + // clear dir and downloadCount + await rm(Discovery.dir(), { recursive: true, force: true }) + downloadCount = 0 + // first pull to populate cache const first = await Discovery.pull(CLOUDFLARE_SKILLS_URL) expect(first.length).toBeGreaterThan(0) + const firstCount = downloadCount + expect(firstCount).toBeGreaterThan(0) // second pull should return same results from cache const second = await Discovery.pull(CLOUDFLARE_SKILLS_URL) expect(second.length).toBe(first.length) expect(second.sort()).toEqual(first.sort()) - }, 60_000) + + // second pull should NOT increment download count + expect(downloadCount).toBe(firstCount) + }) })