chore: initialize crabpot compat testbed
This commit is contained in:
commit
fbb0249980
24
.github/pull_request_template.md
vendored
Normal file
24
.github/pull_request_template.md
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
## Summary
|
||||
|
||||
- Problem:
|
||||
- What changed:
|
||||
- What this does not cover:
|
||||
|
||||
## Fixture impact
|
||||
|
||||
- [ ] Adds or updates fixture manifest entries
|
||||
- [ ] Updates submodule pins
|
||||
- [ ] Changes contract/smoke logic
|
||||
- [ ] Docs only
|
||||
|
||||
## Verification
|
||||
|
||||
- [ ] `npm test`
|
||||
- [ ] `node scripts/sync-fixtures.mjs --check`
|
||||
- [ ] `node scripts/run-contract-smoke.mjs`
|
||||
- [ ] strict fixture smoke, if submodules were materialized
|
||||
|
||||
## Notes
|
||||
|
||||
List any external services, secrets, or live checks intentionally skipped.
|
||||
|
||||
25
.github/workflows/check.yml
vendored
Normal file
25
.github/workflows/check.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
name: Check
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
manifest:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: recursive
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: openclaw/openclaw
|
||||
path: openclaw
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
- run: npm test
|
||||
- run: node scripts/sync-fixtures.mjs --check
|
||||
- run: node scripts/run-contract-smoke.mjs --strict --openclaw ./openclaw
|
||||
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
node_modules/
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
coverage/
|
||||
dist/
|
||||
tmp/
|
||||
|
||||
44
.gitmodules
vendored
Normal file
44
.gitmodules
vendored
Normal file
@ -0,0 +1,44 @@
|
||||
[submodule "plugins/agentchat"]
|
||||
path = plugins/agentchat
|
||||
url = https://github.com/agentchatme/agentchat.git
|
||||
shallow = true
|
||||
[submodule "plugins/wecom"]
|
||||
path = plugins/wecom
|
||||
url = https://github.com/sunnoy/openclaw-plugin-wecom.git
|
||||
shallow = true
|
||||
[submodule "plugins/a2a-gateway"]
|
||||
path = plugins/a2a-gateway
|
||||
url = https://github.com/win4r/openclaw-a2a-gateway.git
|
||||
shallow = true
|
||||
[submodule "plugins/hasdata"]
|
||||
path = plugins/hasdata
|
||||
url = https://github.com/HasData/hasdata-openclaw-plugin.git
|
||||
shallow = true
|
||||
[submodule "plugins/mcp-adapter"]
|
||||
path = plugins/mcp-adapter
|
||||
url = https://github.com/androidStern-personal/openclaw-mcp-adapter.git
|
||||
shallow = true
|
||||
[submodule "plugins/llm-trace-phoenix"]
|
||||
path = plugins/llm-trace-phoenix
|
||||
url = https://github.com/pingshian0131/openclaw-plugin-llm-trace-phoenix.git
|
||||
shallow = true
|
||||
[submodule "plugins/clawmetry"]
|
||||
path = plugins/clawmetry
|
||||
url = https://github.com/vivekchand/clawmetry.git
|
||||
shallow = true
|
||||
[submodule "plugins/codex-app-server"]
|
||||
path = plugins/codex-app-server
|
||||
url = https://github.com/pwrdrvr/openclaw-codex-app-server.git
|
||||
shallow = true
|
||||
[submodule "plugins/web-search-plus"]
|
||||
path = plugins/web-search-plus
|
||||
url = https://github.com/robbyczgw-cla/web-search-plus-plugin.git
|
||||
shallow = true
|
||||
[submodule "plugins/apify"]
|
||||
path = plugins/apify
|
||||
url = https://github.com/apify/apify-openclaw-plugin.git
|
||||
shallow = true
|
||||
[submodule "plugins/inworld-tts"]
|
||||
path = plugins/inworld-tts
|
||||
url = https://github.com/livingghost/openclaw-inworld-tts.git
|
||||
shallow = true
|
||||
12
AGENTS.md
Normal file
12
AGENTS.md
Normal file
@ -0,0 +1,12 @@
|
||||
# crabpot agent notes
|
||||
|
||||
- Keep this repo fixture-driven. Add plugins to `crabpot.config.json`; do not
|
||||
hardcode fixture lists in scripts.
|
||||
- External plugin code lives under `plugins/` as git submodules. Do not vendor or
|
||||
rewrite external plugin source here.
|
||||
- Default checks must stay cheap and credential-free. Live tests require explicit
|
||||
opt-in and secrets.
|
||||
- Prefer seam labels over product categories: `dynamic-tool`, `llm-observer`,
|
||||
`gateway-service`, `provider-capability`, and similar.
|
||||
- When adding a fixture, explain the unique seam it covers in `why`.
|
||||
|
||||
22
LICENSE
Normal file
22
LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 OpenClaw
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
58
README.md
Normal file
58
README.md
Normal file
@ -0,0 +1,58 @@
|
||||
# crabpot
|
||||
|
||||
Compatibility trap for OpenClaw plugin contracts.
|
||||
|
||||
`crabpot` keeps a curated set of real community plugins pinned under `plugins/`
|
||||
and runs seam-focused compatibility checks against OpenClaw plugin APIs. The goal
|
||||
is to catch contract drift before external plugin authors do.
|
||||
|
||||
## What this tests
|
||||
|
||||
- plugin manifests and install metadata
|
||||
- native tool registration and dynamic tool schemas
|
||||
- channel registration and message delivery seams
|
||||
- lifecycle hooks such as `gateway_start`, `gateway_stop`, and `before_install`
|
||||
- agent hooks such as `before_tool_call`, `before_prompt_build`, `llm_input`,
|
||||
`llm_output`, and `agent_end`
|
||||
- provider capability registration such as speech/TTS
|
||||
- plugin-owned services, routes, subprocesses, and async job patterns
|
||||
|
||||
## Layout
|
||||
|
||||
```text
|
||||
crabpot/
|
||||
crabpot.config.json fixture manifest and seam tags
|
||||
plugins/ external plugin repositories as git submodules
|
||||
scripts/ manifest and fixture helpers
|
||||
test/ repo-level checks
|
||||
docs/ operating notes and seam matrix
|
||||
```
|
||||
|
||||
## Quick start
|
||||
|
||||
```bash
|
||||
npm test
|
||||
node scripts/list-fixtures.mjs
|
||||
node scripts/sync-fixtures.mjs --check
|
||||
```
|
||||
|
||||
To materialize the fixture repos as submodules:
|
||||
|
||||
```bash
|
||||
node scripts/sync-fixtures.mjs --materialize
|
||||
git submodule update --init --recursive
|
||||
```
|
||||
|
||||
That command mutates `.gitmodules` and `plugins/*`. Commit those changes when
|
||||
you intentionally pin or update fixture revisions.
|
||||
|
||||
## Fixture policy
|
||||
|
||||
Fixtures should earn their spot by covering a distinct seam. Popularity is a
|
||||
useful signal, but a small plugin that exercises a rare hook is more valuable
|
||||
than the fourth web-search wrapper.
|
||||
|
||||
The first fixture set intentionally covers channels, dynamic tools, LLM
|
||||
observation, diagnostics, gateway-owned services, async jobs, provider
|
||||
capabilities, and security/policy hooks.
|
||||
|
||||
112
crabpot.config.json
Normal file
112
crabpot.config.json
Normal file
@ -0,0 +1,112 @@
|
||||
{
|
||||
"$schema": "./crabpot.schema.json",
|
||||
"version": 1,
|
||||
"submoduleRoot": "plugins",
|
||||
"openclaw": {
|
||||
"minimumNode": "22",
|
||||
"defaultCheckoutPath": "../openclaw"
|
||||
},
|
||||
"fixtures": [
|
||||
{
|
||||
"id": "agentchat",
|
||||
"name": "AgentChat OpenClaw Channel",
|
||||
"repo": "https://github.com/agentchatme/agentchat.git",
|
||||
"path": "plugins/agentchat",
|
||||
"subdir": "integrations/openclaw-channel",
|
||||
"priority": "high",
|
||||
"seams": ["channel", "prompt-mutation", "config-schema", "websocket", "backpressure"],
|
||||
"why": "Production-shaped channel plugin with WebSocket auth, reconnect, prompt hints, and typed runtime contracts."
|
||||
},
|
||||
{
|
||||
"id": "wecom",
|
||||
"name": "WeCom Enhanced Channel",
|
||||
"repo": "https://github.com/sunnoy/openclaw-plugin-wecom.git",
|
||||
"path": "plugins/wecom",
|
||||
"priority": "high",
|
||||
"seams": ["channel", "streaming", "dynamic-agent-routing", "message-policy", "media"],
|
||||
"why": "Large community channel fixture covering WS streaming, group/DM policy, command allowlists, media, and dynamic agents."
|
||||
},
|
||||
{
|
||||
"id": "a2a-gateway",
|
||||
"name": "A2A Gateway",
|
||||
"repo": "https://github.com/win4r/openclaw-a2a-gateway.git",
|
||||
"path": "plugins/a2a-gateway",
|
||||
"priority": "high",
|
||||
"seams": ["gateway-service", "http-routes", "agent-routing", "async-job", "auth"],
|
||||
"why": "Agent-to-agent server fixture with agent-card discovery, JSON-RPC/REST/gRPC transports, metrics, peer auth, and async task modes."
|
||||
},
|
||||
{
|
||||
"id": "hasdata",
|
||||
"name": "HasData",
|
||||
"repo": "https://github.com/HasData/hasdata-openclaw-plugin.git",
|
||||
"path": "plugins/hasdata",
|
||||
"priority": "high",
|
||||
"seams": ["tool", "tool-schema", "external-api", "cli", "config-schema"],
|
||||
"why": "Single native tool backed by a large OpenAPI-derived action catalog and strict per-action schemas."
|
||||
},
|
||||
{
|
||||
"id": "mcp-adapter",
|
||||
"name": "MCP Adapter",
|
||||
"repo": "https://github.com/androidStern-personal/openclaw-mcp-adapter.git",
|
||||
"path": "plugins/mcp-adapter",
|
||||
"priority": "high",
|
||||
"seams": ["dynamic-tool", "json-schema", "stdio", "http", "reconnect"],
|
||||
"why": "Dynamic tool discovery from MCP servers with stdio/http transports and schema passthrough."
|
||||
},
|
||||
{
|
||||
"id": "llm-trace-phoenix",
|
||||
"name": "Phoenix LLM Trace",
|
||||
"repo": "https://github.com/pingshian0131/openclaw-plugin-llm-trace-phoenix.git",
|
||||
"path": "plugins/llm-trace-phoenix",
|
||||
"priority": "high",
|
||||
"seams": ["llm-observer", "conversation-access", "telemetry", "external-api"],
|
||||
"why": "Small sharp fixture for llm_input/llm_output access and raw prompt/output privacy boundaries."
|
||||
},
|
||||
{
|
||||
"id": "clawmetry",
|
||||
"name": "ClawMetry",
|
||||
"repo": "https://github.com/vivekchand/clawmetry.git",
|
||||
"path": "plugins/clawmetry",
|
||||
"priority": "medium",
|
||||
"seams": ["diagnostics", "log-transport", "gateway-service", "sidecar", "telemetry"],
|
||||
"why": "Observability fixture for diagnostic events, log transport, telemetry buffering, and plugin-owned dashboard lifecycle."
|
||||
},
|
||||
{
|
||||
"id": "codex-app-server",
|
||||
"name": "Codex App Server Bridge",
|
||||
"repo": "https://github.com/pwrdrvr/openclaw-codex-app-server.git",
|
||||
"path": "plugins/codex-app-server",
|
||||
"priority": "medium",
|
||||
"seams": ["process-spawn", "unsafe-install", "channel-bridge", "interactive-ui", "sdk-compat"],
|
||||
"why": "Bridge fixture for process launch policy, install safety gates, and channel-specific SDK compatibility."
|
||||
},
|
||||
{
|
||||
"id": "web-search-plus",
|
||||
"name": "Web Search Plus",
|
||||
"repo": "https://github.com/robbyczgw-cla/web-search-plus-plugin.git",
|
||||
"path": "plugins/web-search-plus",
|
||||
"priority": "medium",
|
||||
"seams": ["tool", "provider-routing", "external-api", "env-auth", "extraction"],
|
||||
"why": "Multi-provider search/extract tool fixture with several auth/config permutations."
|
||||
},
|
||||
{
|
||||
"id": "apify",
|
||||
"name": "Apify",
|
||||
"repo": "https://github.com/apify/apify-openclaw-plugin.git",
|
||||
"path": "plugins/apify",
|
||||
"priority": "medium",
|
||||
"seams": ["tool", "async-job", "polling", "external-api", "catalog-discovery"],
|
||||
"why": "Async start/collect workflow fixture for remote jobs and huge third-party catalog discovery."
|
||||
},
|
||||
{
|
||||
"id": "inworld-tts",
|
||||
"name": "Inworld TTS",
|
||||
"repo": "https://github.com/livingghost/openclaw-inworld-tts.git",
|
||||
"path": "plugins/inworld-tts",
|
||||
"priority": "medium",
|
||||
"seams": ["provider-capability", "speech", "tts", "env-auth", "runtime-overrides"],
|
||||
"why": "Provider capability fixture for speech registration, config/env auth, and talk.speak request overrides."
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
44
crabpot.schema.json
Normal file
44
crabpot.schema.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"title": "Crabpot fixture manifest",
|
||||
"type": "object",
|
||||
"required": ["version", "submoduleRoot", "fixtures"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"version": { "type": "integer", "minimum": 1 },
|
||||
"submoduleRoot": { "type": "string", "pattern": "^[^/].*" },
|
||||
"openclaw": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"minimumNode": { "type": "string" },
|
||||
"defaultCheckoutPath": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"fixtures": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": {
|
||||
"type": "object",
|
||||
"required": ["id", "name", "repo", "path", "priority", "seams", "why"],
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"id": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
|
||||
"name": { "type": "string", "minLength": 1 },
|
||||
"repo": { "type": "string", "pattern": "^https://github.com/.+\\.git$" },
|
||||
"path": { "type": "string", "pattern": "^plugins/[a-z0-9][a-z0-9-]*$" },
|
||||
"subdir": { "type": "string", "minLength": 1 },
|
||||
"priority": { "enum": ["high", "medium", "low"] },
|
||||
"seams": {
|
||||
"type": "array",
|
||||
"minItems": 1,
|
||||
"items": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]*$" },
|
||||
"uniqueItems": true
|
||||
},
|
||||
"why": { "type": "string", "minLength": 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
31
docs/contract-matrix.md
Normal file
31
docs/contract-matrix.md
Normal file
@ -0,0 +1,31 @@
|
||||
# Contract Matrix
|
||||
|
||||
Crabpot should test contracts by seam, not by package popularity.
|
||||
|
||||
| Seam | Fixture examples | What should fail fast |
|
||||
| --- | --- | --- |
|
||||
| Channel runtime | `agentchat`, `wecom` | registration drift, message envelope drift, sender/thread metadata drift |
|
||||
| Native tools | `hasdata`, `web-search-plus`, `apify` | schema incompatibility, result-shape drift, auth/config drift |
|
||||
| Dynamic tools | `mcp-adapter` | JSON Schema passthrough, tool namespacing, reconnect behavior |
|
||||
| LLM observation | `llm-trace-phoenix` | conversation-access gating, prompt/output shape changes |
|
||||
| Diagnostics/logs | `clawmetry` | diagnostic event shape, log transport registration |
|
||||
| Gateway services | `a2a-gateway`, `clawmetry` | lifecycle start/stop, route registration, teardown |
|
||||
| Provider capability | `inworld-tts` | provider registration, runtime helper compatibility |
|
||||
| Unsafe/process policy | `codex-app-server` | install scan behavior, process-spawn declarations |
|
||||
| Async jobs | `a2a-gateway`, `apify` | start/poll/collect result contracts and timeout handling |
|
||||
| Prompt mutation | `agentchat` | prompt-injection opt-out and prompt builder contracts |
|
||||
|
||||
## Test levels
|
||||
|
||||
1. **Manifest smoke**: plugin metadata parses and declared OpenClaw compatibility
|
||||
can be read without executing plugin code.
|
||||
2. **SDK compile**: plugin TypeScript compiles against the target OpenClaw SDK.
|
||||
3. **Registration capture**: plugin `register(api)` can run against a captured
|
||||
API shim and records expected tools, hooks, providers, routes, and commands.
|
||||
4. **Contract smoke**: selected hooks/tools run against fixture events with
|
||||
synthetic inputs and validate stable return shapes.
|
||||
5. **Live smoke**: opt-in, credential-gated checks for real remote services.
|
||||
|
||||
Default CI should run levels 1-4. Live smoke belongs behind explicit labels,
|
||||
manual workflow dispatch, or secret availability.
|
||||
|
||||
39
docs/operations.md
Normal file
39
docs/operations.md
Normal file
@ -0,0 +1,39 @@
|
||||
# Operations
|
||||
|
||||
## Adding a fixture
|
||||
|
||||
1. Add an entry to `crabpot.config.json`.
|
||||
2. Run `npm test`.
|
||||
3. Run `node scripts/sync-fixtures.mjs --materialize`.
|
||||
4. Review `.gitmodules` and the pinned submodule commit.
|
||||
5. Commit the manifest and submodule pointer together.
|
||||
|
||||
## Updating fixtures
|
||||
|
||||
```bash
|
||||
git submodule update --remote --recursive plugins/<fixture-id>
|
||||
npm test
|
||||
```
|
||||
|
||||
Then inspect the diff. A fixture update is only useful if it either:
|
||||
|
||||
- tracks a plugin release we care about,
|
||||
- adds coverage for a new seam,
|
||||
- reproduces a compatibility break,
|
||||
- or proves a contract migration path still works.
|
||||
|
||||
## CI model
|
||||
|
||||
Use a cheap default workflow first:
|
||||
|
||||
- validate the manifest
|
||||
- ensure `.gitmodules` agrees with `crabpot.config.json`
|
||||
- initialize pinned submodules
|
||||
- run registration-capture smoke checks
|
||||
|
||||
Add heavier lanes later for SDK compilation against specific OpenClaw refs:
|
||||
|
||||
- `openclaw@main`
|
||||
- latest stable release tag
|
||||
- active beta branch or release candidate
|
||||
|
||||
17
package.json
Normal file
17
package.json
Normal file
@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "crabpot",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "Compatibility testbed for OpenClaw community plugins and plugin seams.",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"scripts": {
|
||||
"check": "npm test && node scripts/sync-fixtures.mjs --check && node scripts/run-contract-smoke.mjs",
|
||||
"fixtures:list": "node scripts/list-fixtures.mjs",
|
||||
"fixtures:sync": "node scripts/sync-fixtures.mjs --materialize",
|
||||
"smoke": "node scripts/run-contract-smoke.mjs",
|
||||
"test": "node --test test/*.test.mjs"
|
||||
}
|
||||
}
|
||||
1
plugins/a2a-gateway
Submodule
1
plugins/a2a-gateway
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit a335e59e926f7e1a8913e6cd7b1cbf2d44c33cb7
|
||||
1
plugins/agentchat
Submodule
1
plugins/agentchat
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit df6870bcf325ae34e2d41b3cb479b0c8e88aba83
|
||||
1
plugins/apify
Submodule
1
plugins/apify
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 41f49794d230f7ad092d1c699ee4d91fecf6ba91
|
||||
1
plugins/clawmetry
Submodule
1
plugins/clawmetry
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit b329bb3ed18b651d369bf35321ec58bd47dc33b4
|
||||
1
plugins/codex-app-server
Submodule
1
plugins/codex-app-server
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 4a87dce5d620a8fb30842bb1b726390fe442247e
|
||||
1
plugins/hasdata
Submodule
1
plugins/hasdata
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit bed32d8a0359392b7c4628f12b909b6e204c8426
|
||||
1
plugins/inworld-tts
Submodule
1
plugins/inworld-tts
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit d2abaeea330ebef7530f43f8b395671f6f404aea
|
||||
1
plugins/llm-trace-phoenix
Submodule
1
plugins/llm-trace-phoenix
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 05bc0f4ba67281c10fad7be356d32a54b00c59fd
|
||||
1
plugins/mcp-adapter
Submodule
1
plugins/mcp-adapter
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 5434ce21ac780a46a493c8125e52e80a03dd2640
|
||||
1
plugins/web-search-plus
Submodule
1
plugins/web-search-plus
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit be6580db68997a209e38cb43e978e9825f681d3e
|
||||
1
plugins/wecom
Submodule
1
plugins/wecom
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit b7849ac055c8fa699d01b48e83cf24028907307d
|
||||
14
scripts/list-fixtures.mjs
Normal file
14
scripts/list-fixtures.mjs
Normal file
@ -0,0 +1,14 @@
|
||||
#!/usr/bin/env node
|
||||
import { readManifest } from "./manifest-lib.mjs";
|
||||
|
||||
const manifest = await readManifest();
|
||||
|
||||
const rows = manifest.fixtures.map((fixture) => ({
|
||||
id: fixture.id,
|
||||
priority: fixture.priority,
|
||||
seams: fixture.seams.join(","),
|
||||
path: fixture.path,
|
||||
}));
|
||||
|
||||
console.table(rows);
|
||||
|
||||
63
scripts/manifest-lib.mjs
Normal file
63
scripts/manifest-lib.mjs
Normal file
@ -0,0 +1,63 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
export const repoRoot = path.resolve(import.meta.dirname, "..");
|
||||
export const manifestPath = path.join(repoRoot, "crabpot.config.json");
|
||||
|
||||
export async function readManifest() {
|
||||
const raw = await readFile(manifestPath, "utf8");
|
||||
const manifest = JSON.parse(raw);
|
||||
validateManifest(manifest);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
export function validateManifest(manifest) {
|
||||
const errors = [];
|
||||
|
||||
if (manifest.version !== 1) {
|
||||
errors.push("manifest.version must be 1");
|
||||
}
|
||||
|
||||
if (manifest.submoduleRoot !== "plugins") {
|
||||
errors.push('manifest.submoduleRoot must be "plugins"');
|
||||
}
|
||||
|
||||
if (!Array.isArray(manifest.fixtures) || manifest.fixtures.length === 0) {
|
||||
errors.push("manifest.fixtures must be a non-empty array");
|
||||
}
|
||||
|
||||
const ids = new Set();
|
||||
const paths = new Set();
|
||||
for (const fixture of manifest.fixtures ?? []) {
|
||||
if (!/^[a-z0-9][a-z0-9-]*$/.test(fixture.id ?? "")) {
|
||||
errors.push(`invalid fixture id: ${fixture.id}`);
|
||||
}
|
||||
if (ids.has(fixture.id)) {
|
||||
errors.push(`duplicate fixture id: ${fixture.id}`);
|
||||
}
|
||||
ids.add(fixture.id);
|
||||
|
||||
if (!fixture.path?.startsWith("plugins/")) {
|
||||
errors.push(`${fixture.id}: path must live under plugins/`);
|
||||
}
|
||||
if (paths.has(fixture.path)) {
|
||||
errors.push(`duplicate fixture path: ${fixture.path}`);
|
||||
}
|
||||
paths.add(fixture.path);
|
||||
|
||||
if (!fixture.repo?.startsWith("https://github.com/") || !fixture.repo.endsWith(".git")) {
|
||||
errors.push(`${fixture.id}: repo must be a GitHub HTTPS .git URL`);
|
||||
}
|
||||
if (!["high", "medium", "low"].includes(fixture.priority)) {
|
||||
errors.push(`${fixture.id}: priority must be high, medium, or low`);
|
||||
}
|
||||
if (!Array.isArray(fixture.seams) || fixture.seams.length === 0) {
|
||||
errors.push(`${fixture.id}: seams must be non-empty`);
|
||||
}
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
throw new Error(errors.join("\n"));
|
||||
}
|
||||
}
|
||||
|
||||
91
scripts/run-contract-smoke.mjs
Normal file
91
scripts/run-contract-smoke.mjs
Normal file
@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env node
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { readManifest, repoRoot } from "./manifest-lib.mjs";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
const strict = args.includes("--strict");
|
||||
const openclawArgIndex = args.indexOf("--openclaw");
|
||||
const openclawPath =
|
||||
openclawArgIndex === -1 ? "../openclaw" : args[openclawArgIndex + 1];
|
||||
|
||||
const manifest = await readManifest();
|
||||
const openclawRoot = path.resolve(repoRoot, openclawPath);
|
||||
|
||||
const openclawPackage = path.join(openclawRoot, "package.json");
|
||||
if (!existsSync(openclawPackage)) {
|
||||
throw new Error(`OpenClaw checkout not found at ${openclawRoot}`);
|
||||
}
|
||||
|
||||
const rows = [];
|
||||
const missing = [];
|
||||
|
||||
for (const fixture of manifest.fixtures) {
|
||||
const checkoutPath = path.join(repoRoot, fixture.path);
|
||||
const pluginRoot = fixture.subdir ? path.join(checkoutPath, fixture.subdir) : checkoutPath;
|
||||
|
||||
if (!existsSync(checkoutPath)) {
|
||||
rows.push(row(fixture, "missing", "not materialized"));
|
||||
missing.push(fixture.id);
|
||||
continue;
|
||||
}
|
||||
|
||||
const manifestFile = await firstExisting([
|
||||
path.join(pluginRoot, "openclaw.plugin.json"),
|
||||
path.join(checkoutPath, "openclaw.plugin.json"),
|
||||
]);
|
||||
const packageFile = await firstExisting([
|
||||
path.join(pluginRoot, "package.json"),
|
||||
path.join(checkoutPath, "package.json"),
|
||||
]);
|
||||
|
||||
if (!manifestFile && !packageFile) {
|
||||
rows.push(row(fixture, "incomplete", "no plugin manifest or package.json found"));
|
||||
continue;
|
||||
}
|
||||
|
||||
const pluginId = manifestFile
|
||||
? await readPluginId(manifestFile)
|
||||
: await readPackageName(packageFile);
|
||||
rows.push(row(fixture, "ok", pluginId));
|
||||
}
|
||||
|
||||
console.table(rows);
|
||||
|
||||
if (strict && missing.length > 0) {
|
||||
throw new Error(`missing fixture checkouts: ${missing.join(", ")}`);
|
||||
}
|
||||
|
||||
function row(fixture, status, detail) {
|
||||
return {
|
||||
id: fixture.id,
|
||||
priority: fixture.priority,
|
||||
status,
|
||||
detail,
|
||||
seams: fixture.seams.join(","),
|
||||
};
|
||||
}
|
||||
|
||||
async function firstExisting(paths) {
|
||||
return paths.find((candidate) => existsSync(candidate));
|
||||
}
|
||||
|
||||
async function readPluginId(filePath) {
|
||||
try {
|
||||
const json = JSON.parse(await readFile(filePath, "utf8"));
|
||||
return json.id ?? json.name ?? path.basename(path.dirname(filePath));
|
||||
} catch {
|
||||
return "invalid openclaw.plugin.json";
|
||||
}
|
||||
}
|
||||
|
||||
async function readPackageName(filePath) {
|
||||
try {
|
||||
const json = JSON.parse(await readFile(filePath, "utf8"));
|
||||
return json.name ?? path.basename(path.dirname(filePath));
|
||||
} catch {
|
||||
return "invalid package.json";
|
||||
}
|
||||
}
|
||||
|
||||
64
scripts/sync-fixtures.mjs
Normal file
64
scripts/sync-fixtures.mjs
Normal file
@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env node
|
||||
import { existsSync } from "node:fs";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { readManifest, repoRoot } from "./manifest-lib.mjs";
|
||||
|
||||
const args = new Set(process.argv.slice(2));
|
||||
const materialize = args.has("--materialize");
|
||||
const check = args.has("--check") || !materialize;
|
||||
|
||||
const manifest = await readManifest();
|
||||
|
||||
if (check) {
|
||||
await checkGitmodules(manifest);
|
||||
console.log(`crabpot: manifest ok (${manifest.fixtures.length} fixtures)`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
for (const fixture of manifest.fixtures) {
|
||||
const target = path.join(repoRoot, fixture.path);
|
||||
if (existsSync(target)) {
|
||||
run("git", ["submodule", "update", "--init", "--recursive", fixture.path]);
|
||||
continue;
|
||||
}
|
||||
|
||||
run("git", ["submodule", "add", "--depth", "1", fixture.repo, fixture.path]);
|
||||
}
|
||||
|
||||
console.log("crabpot: fixtures materialized. review .gitmodules and commit pinned revisions.");
|
||||
|
||||
async function checkGitmodules(manifest) {
|
||||
const gitmodulesPath = path.join(repoRoot, ".gitmodules");
|
||||
if (!existsSync(gitmodulesPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const gitmodules = await readFile(gitmodulesPath, "utf8");
|
||||
const missing = [];
|
||||
for (const fixture of manifest.fixtures) {
|
||||
if (!gitmodules.includes(`path = ${fixture.path}`)) {
|
||||
missing.push(fixture.path);
|
||||
}
|
||||
if (!gitmodules.includes(`url = ${fixture.repo}`)) {
|
||||
missing.push(fixture.repo);
|
||||
}
|
||||
}
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(`.gitmodules is missing manifest entries:\n${missing.join("\n")}`);
|
||||
}
|
||||
}
|
||||
|
||||
function run(command, args) {
|
||||
const result = spawnSync(command, args, {
|
||||
cwd: repoRoot,
|
||||
stdio: "inherit",
|
||||
env: process.env,
|
||||
});
|
||||
|
||||
if (result.status !== 0) {
|
||||
throw new Error(`${command} ${args.join(" ")} failed with ${result.status}`);
|
||||
}
|
||||
}
|
||||
35
test/manifest.test.mjs
Normal file
35
test/manifest.test.mjs
Normal file
@ -0,0 +1,35 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
import { readManifest } from "../scripts/manifest-lib.mjs";
|
||||
|
||||
test("fixture manifest is valid and seam-rich", async () => {
|
||||
const manifest = await readManifest();
|
||||
|
||||
assert.equal(manifest.submoduleRoot, "plugins");
|
||||
assert.ok(manifest.fixtures.length >= 10);
|
||||
|
||||
const seams = new Set(manifest.fixtures.flatMap((fixture) => fixture.seams));
|
||||
for (const seam of [
|
||||
"channel",
|
||||
"tool",
|
||||
"dynamic-tool",
|
||||
"llm-observer",
|
||||
"diagnostics",
|
||||
"gateway-service",
|
||||
"provider-capability",
|
||||
"async-job",
|
||||
"prompt-mutation",
|
||||
]) {
|
||||
assert.ok(seams.has(seam), `missing seam coverage: ${seam}`);
|
||||
}
|
||||
});
|
||||
|
||||
test("fixture paths are stable plugin submodule paths", async () => {
|
||||
const manifest = await readManifest();
|
||||
|
||||
for (const fixture of manifest.fixtures) {
|
||||
assert.match(fixture.path, /^plugins\/[a-z0-9][a-z0-9-]*$/);
|
||||
assert.ok(!fixture.path.includes(".."));
|
||||
}
|
||||
});
|
||||
|
||||
Loading…
Reference in New Issue
Block a user