test: fix discovery test to boot up server instead of relying on 3rd party (#14327)
This commit is contained in:
155
packages/opencode/test/fixture/skills/agents-sdk/SKILL.md
Normal file
155
packages/opencode/test/fixture/skills/agents-sdk/SKILL.md
Normal file
@@ -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<Env, State> {
|
||||
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 (
|
||||
<button onClick={() => agent.setState({ count: state.count + 1 })}>
|
||||
Count: {state.count}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 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)
|
||||
@@ -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<Env, State> {
|
||||
@callable()
|
||||
async greet(name: string): Promise<string> {
|
||||
return `Hello, ${name}!`;
|
||||
}
|
||||
|
||||
@callable()
|
||||
async processData(data: unknown): Promise<Result> {
|
||||
// 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<Env, State> {
|
||||
@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 |
|
||||
201
packages/opencode/test/fixture/skills/cloudflare/SKILL.md
Normal file
201
packages/opencode/test/fixture/skills/cloudflare/SKILL.md
Normal file
@@ -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/` |
|
||||
6
packages/opencode/test/fixture/skills/index.json
Normal file
6
packages/opencode/test/fixture/skills/index.json
Normal file
@@ -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"] }
|
||||
]
|
||||
}
|
||||
@@ -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<typeof Bun.serve>
|
||||
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)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user