toolfactory 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +166 -0
- package/dist/bindings/index.d.ts +23 -0
- package/dist/bindings/index.js +29 -0
- package/dist/bindings/python.d.ts +33 -0
- package/dist/bindings/python.js +805 -0
- package/dist/bindings/python.test.d.ts +1 -0
- package/dist/bindings/python.test.js +316 -0
- package/dist/bindings/typescript.d.ts +39 -0
- package/dist/bindings/typescript.js +758 -0
- package/dist/bindings/typescript.test.d.ts +1 -0
- package/dist/bindings/typescript.test.js +264 -0
- package/dist/commands.d.ts +188 -0
- package/dist/commands.js +662 -0
- package/dist/commands.test.d.ts +1 -0
- package/dist/commands.test.js +215 -0
- package/dist/e2e.test.d.ts +1 -0
- package/dist/e2e.test.js +124 -0
- package/dist/hosts/browser.d.ts +10 -0
- package/dist/hosts/browser.js +94 -0
- package/dist/hosts/github.d.ts +66 -0
- package/dist/hosts/github.js +247 -0
- package/dist/hosts/openclaw.d.ts +4 -0
- package/dist/hosts/openclaw.js +94 -0
- package/dist/hosts/template.d.ts +15 -0
- package/dist/hosts/template.js +142 -0
- package/dist/hosts/web.d.ts +4 -0
- package/dist/hosts/web.js +89 -0
- package/dist/identity/name.d.ts +20 -0
- package/dist/identity/name.js +33 -0
- package/dist/identity/name.test.d.ts +1 -0
- package/dist/identity/name.test.js +37 -0
- package/dist/identity/read.d.ts +9 -0
- package/dist/identity/read.js +106 -0
- package/dist/identity/read.test.d.ts +1 -0
- package/dist/identity/read.test.js +76 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/introspect/index.d.ts +15 -0
- package/dist/introspect/index.js +63 -0
- package/dist/model.d.ts +236 -0
- package/dist/model.js +214 -0
- package/dist/ops.d.ts +255 -0
- package/dist/ops.js +297 -0
- package/dist/project/apply.d.ts +35 -0
- package/dist/project/apply.js +360 -0
- package/dist/project/apply.test.d.ts +1 -0
- package/dist/project/apply.test.js +285 -0
- package/dist/project/gate.d.ts +129 -0
- package/dist/project/gate.js +597 -0
- package/dist/project/history.d.ts +62 -0
- package/dist/project/history.js +41 -0
- package/dist/project/load.d.ts +21 -0
- package/dist/project/load.js +82 -0
- package/dist/project/lock.d.ts +28 -0
- package/dist/project/lock.js +42 -0
- package/dist/project/plan.d.ts +5 -0
- package/dist/project/plan.js +96 -0
- package/dist/report/coverage.d.ts +22 -0
- package/dist/report/coverage.js +72 -0
- package/dist/report/coverage.test.d.ts +1 -0
- package/dist/report/coverage.test.js +93 -0
- package/dist/surfaces/agent-plugins.d.ts +10 -0
- package/dist/surfaces/agent-plugins.js +36 -0
- package/dist/surfaces/agents.d.ts +35 -0
- package/dist/surfaces/agents.js +609 -0
- package/dist/surfaces/agents.template.d.ts +8 -0
- package/dist/surfaces/agents.template.js +21 -0
- package/dist/surfaces/agents.test.d.ts +1 -0
- package/dist/surfaces/agents.test.js +181 -0
- package/dist/surfaces/browser-extension.d.ts +101 -0
- package/dist/surfaces/browser-extension.js +1034 -0
- package/dist/surfaces/browser-extension.test.d.ts +1 -0
- package/dist/surfaces/browser-extension.test.js +145 -0
- package/dist/surfaces/claude.d.ts +2 -0
- package/dist/surfaces/claude.js +81 -0
- package/dist/surfaces/codex.d.ts +13 -0
- package/dist/surfaces/codex.js +101 -0
- package/dist/surfaces/codex.test.d.ts +1 -0
- package/dist/surfaces/codex.test.js +67 -0
- package/dist/surfaces/cursor.d.ts +2 -0
- package/dist/surfaces/cursor.js +50 -0
- package/dist/surfaces/distribution.test.d.ts +1 -0
- package/dist/surfaces/distribution.test.js +146 -0
- package/dist/surfaces/dsh.d.ts +25 -0
- package/dist/surfaces/dsh.js +147 -0
- package/dist/surfaces/dsh.test.d.ts +1 -0
- package/dist/surfaces/dsh.test.js +100 -0
- package/dist/surfaces/external.d.ts +9 -0
- package/dist/surfaces/external.js +4 -0
- package/dist/surfaces/gemini.d.ts +29 -0
- package/dist/surfaces/gemini.js +68 -0
- package/dist/surfaces/gemini.test.d.ts +1 -0
- package/dist/surfaces/gemini.test.js +74 -0
- package/dist/surfaces/hermes-native.d.ts +53 -0
- package/dist/surfaces/hermes-native.js +507 -0
- package/dist/surfaces/hermes-native.test.d.ts +1 -0
- package/dist/surfaces/hermes-native.test.js +156 -0
- package/dist/surfaces/kernel.d.ts +5 -0
- package/dist/surfaces/kernel.js +63 -0
- package/dist/surfaces/mcp-registry.d.ts +19 -0
- package/dist/surfaces/mcp-registry.js +160 -0
- package/dist/surfaces/mcp-registry.test.d.ts +1 -0
- package/dist/surfaces/mcp-registry.test.js +79 -0
- package/dist/surfaces/mcpb.d.ts +11 -0
- package/dist/surfaces/mcpb.js +118 -0
- package/dist/surfaces/mcpb.test.d.ts +1 -0
- package/dist/surfaces/mcpb.test.js +104 -0
- package/dist/surfaces/npm.d.ts +6 -0
- package/dist/surfaces/npm.js +72 -0
- package/dist/surfaces/openclaw-native.d.ts +93 -0
- package/dist/surfaces/openclaw-native.js +956 -0
- package/dist/surfaces/openclaw-native.test.d.ts +1 -0
- package/dist/surfaces/openclaw-native.test.js +246 -0
- package/dist/surfaces/pypi.d.ts +5 -0
- package/dist/surfaces/pypi.js +82 -0
- package/dist/surfaces/readme.d.ts +7 -0
- package/dist/surfaces/readme.js +181 -0
- package/dist/surfaces/registry.d.ts +6 -0
- package/dist/surfaces/registry.js +73 -0
- package/dist/surfaces/shared.d.ts +49 -0
- package/dist/surfaces/shared.js +112 -0
- package/dist/surfaces/skill.d.ts +7 -0
- package/dist/surfaces/skill.js +118 -0
- package/dist/surfaces/skill.test.d.ts +1 -0
- package/dist/surfaces/skill.test.js +87 -0
- package/dist/surfaces/web.d.ts +97 -0
- package/dist/surfaces/web.js +1285 -0
- package/dist/surfaces/web.test.d.ts +1 -0
- package/dist/surfaces/web.test.js +255 -0
- package/dist/surfaces/workflows.d.ts +2 -0
- package/dist/surfaces/workflows.js +809 -0
- package/dist/surfaces/workflows.test.d.ts +1 -0
- package/dist/surfaces/workflows.test.js +563 -0
- package/dist/toolfactory/cli.d.ts +2 -0
- package/dist/toolfactory/cli.js +65 -0
- package/dist/toolfactory/config.d.ts +3 -0
- package/dist/toolfactory/config.js +27 -0
- package/dist/toolfactory/mcp.d.ts +37 -0
- package/dist/toolfactory/mcp.js +269 -0
- package/dist/toolfactory/types.d.ts +31 -0
- package/dist/toolfactory/types.js +10 -0
- package/dist/toolfactory/web.d.ts +4 -0
- package/dist/toolfactory/web.js +40 -0
- package/dist/validate/agent-plugins.d.ts +5 -0
- package/dist/validate/agent-plugins.js +46 -0
- package/package.json +68 -0
- package/schemas/agent-plugins/mcp.schema.json +120 -0
- package/schemas/agent-plugins/plugin.schema.json +65 -0
- package/src/bindings/index.ts +47 -0
- package/src/bindings/python.test.ts +374 -0
- package/src/bindings/python.ts +862 -0
- package/src/bindings/typescript.test.ts +329 -0
- package/src/bindings/typescript.ts +818 -0
- package/src/commands.test.ts +253 -0
- package/src/commands.ts +886 -0
- package/src/e2e.test.ts +169 -0
- package/src/hosts/browser.ts +106 -0
- package/src/hosts/github.ts +321 -0
- package/src/hosts/openclaw.ts +110 -0
- package/src/hosts/template.ts +149 -0
- package/src/hosts/web.ts +111 -0
- package/src/identity/name.test.ts +43 -0
- package/src/identity/name.ts +39 -0
- package/src/identity/read.test.ts +92 -0
- package/src/identity/read.ts +119 -0
- package/src/index.ts +11 -0
- package/src/introspect/index.ts +72 -0
- package/src/model.ts +386 -0
- package/src/ops.ts +333 -0
- package/src/project/apply.test.ts +348 -0
- package/src/project/apply.ts +417 -0
- package/src/project/gate.ts +717 -0
- package/src/project/history.ts +46 -0
- package/src/project/load.ts +104 -0
- package/src/project/lock.ts +54 -0
- package/src/project/plan.ts +101 -0
- package/src/report/coverage.test.ts +108 -0
- package/src/report/coverage.ts +113 -0
- package/src/surfaces/agent-plugins.ts +47 -0
- package/src/surfaces/agents.template.ts +32 -0
- package/src/surfaces/agents.test.ts +206 -0
- package/src/surfaces/agents.ts +729 -0
- package/src/surfaces/browser-extension.test.ts +175 -0
- package/src/surfaces/browser-extension.ts +1095 -0
- package/src/surfaces/claude.ts +99 -0
- package/src/surfaces/codex.test.ts +71 -0
- package/src/surfaces/codex.ts +105 -0
- package/src/surfaces/cursor.ts +62 -0
- package/src/surfaces/distribution.test.ts +166 -0
- package/src/surfaces/dsh.test.ts +110 -0
- package/src/surfaces/dsh.ts +159 -0
- package/src/surfaces/external.ts +13 -0
- package/src/surfaces/gemini.test.ts +80 -0
- package/src/surfaces/gemini.ts +84 -0
- package/src/surfaces/hermes-native.test.ts +179 -0
- package/src/surfaces/hermes-native.ts +584 -0
- package/src/surfaces/kernel.ts +68 -0
- package/src/surfaces/mcp-registry.test.ts +99 -0
- package/src/surfaces/mcp-registry.ts +190 -0
- package/src/surfaces/mcpb.test.ts +112 -0
- package/src/surfaces/mcpb.ts +130 -0
- package/src/surfaces/npm.ts +78 -0
- package/src/surfaces/openclaw-native.test.ts +283 -0
- package/src/surfaces/openclaw-native.ts +1070 -0
- package/src/surfaces/pypi.ts +84 -0
- package/src/surfaces/readme.ts +218 -0
- package/src/surfaces/registry.ts +79 -0
- package/src/surfaces/shared.ts +140 -0
- package/src/surfaces/skill.test.ts +103 -0
- package/src/surfaces/skill.ts +136 -0
- package/src/surfaces/web.test.ts +303 -0
- package/src/surfaces/web.ts +1310 -0
- package/src/surfaces/workflows.test.ts +677 -0
- package/src/surfaces/workflows.ts +936 -0
- package/src/toolfactory/cli.ts +74 -0
- package/src/toolfactory/config.ts +33 -0
- package/src/toolfactory/mcp.ts +311 -0
- package/src/toolfactory/types.ts +52 -0
- package/src/toolfactory/web.ts +45 -0
- package/src/validate/agent-plugins.ts +53 -0
- package/web/dist/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/web/dist/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/web/dist/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/web/dist/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/web/dist/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/web/dist/assets/index-CCXhQKK3.css +2 -0
- package/web/dist/assets/index-D1PwZ3qS.js +10 -0
- package/web/dist/index.html +14 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { scaffoldDrift } from "../hosts/openclaw.js";
|
|
4
|
+
import type { Operation, Project } from "../model.js";
|
|
5
|
+
import { computeCoverage, renderCoverageMarkdown } from "../report/coverage.js";
|
|
6
|
+
import { HOST_DIR, OPENCLAW_ADDITIONS, OPENCLAW_SCAFFOLD, surface } from "./openclaw-native.js";
|
|
7
|
+
|
|
8
|
+
const echo: Operation = {
|
|
9
|
+
name: "echo",
|
|
10
|
+
description: "Return the text you pass in.",
|
|
11
|
+
inputSchema: {
|
|
12
|
+
$schema: "https://json-schema.org/draft/2020-12/schema",
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: { text: { type: "string" } },
|
|
15
|
+
required: ["text"],
|
|
16
|
+
},
|
|
17
|
+
outputSchema: {
|
|
18
|
+
type: "object",
|
|
19
|
+
properties: { echoed: { type: "string" } },
|
|
20
|
+
required: ["echoed"],
|
|
21
|
+
},
|
|
22
|
+
requires: [],
|
|
23
|
+
};
|
|
24
|
+
const shoot: Operation = { name: "shoot", inputSchema: { type: "object" }, requires: ["browser"] };
|
|
25
|
+
|
|
26
|
+
function project(overrides: Partial<Project> = {}): Project {
|
|
27
|
+
return {
|
|
28
|
+
root: "/repo",
|
|
29
|
+
tool: {
|
|
30
|
+
schemaVersion: 1,
|
|
31
|
+
identity: "package.json",
|
|
32
|
+
binding: "typescript",
|
|
33
|
+
surfaces: ["openclaw-native", "mcp"],
|
|
34
|
+
bundle: { runtime: "package" },
|
|
35
|
+
tests: { examples: { echo: { text: "hello" } } },
|
|
36
|
+
},
|
|
37
|
+
identity: { name: "hello", version: "0.1.0", description: "Say hello" },
|
|
38
|
+
identityExtra: {},
|
|
39
|
+
operations: [echo, shoot],
|
|
40
|
+
toolfactoryVersion: "0.1.0",
|
|
41
|
+
...overrides,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** A zero-operation plugin whose whole contribution is one declared host registration. */
|
|
46
|
+
function voice(): Project {
|
|
47
|
+
const base = project();
|
|
48
|
+
return {
|
|
49
|
+
...base,
|
|
50
|
+
tool: {
|
|
51
|
+
...base.tool,
|
|
52
|
+
openclaw: {
|
|
53
|
+
registers: [
|
|
54
|
+
{
|
|
55
|
+
api: "registerRealtimeVoiceProvider",
|
|
56
|
+
contract: "realtimeVoiceProviders",
|
|
57
|
+
ids: ["codex"],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
pluginApi: ">=2026.8.2",
|
|
61
|
+
dependencies: { ws: "^8.18.0" },
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
operations: [],
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Every planned file as text: full files verbatim, region files as the region toolfactory owns. */
|
|
69
|
+
function emitted(target: Project): Record<string, string> {
|
|
70
|
+
return Object.fromEntries(
|
|
71
|
+
surface
|
|
72
|
+
.plan(target)
|
|
73
|
+
.map((file) => [
|
|
74
|
+
file.path,
|
|
75
|
+
file.kind === "file"
|
|
76
|
+
? file.content
|
|
77
|
+
: file.kind === "region"
|
|
78
|
+
? file.regions.map((region) => region.content).join("")
|
|
79
|
+
: JSON.stringify(file.patch),
|
|
80
|
+
]),
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe("openclaw-native", () => {
|
|
85
|
+
it("projects only the operations OpenClaw can run and pins the scaffold in one place", () => {
|
|
86
|
+
const files = emitted(project());
|
|
87
|
+
const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
|
|
88
|
+
expect(manifest.contracts).toEqual({ tools: ["echo"] });
|
|
89
|
+
expect(manifest.activation).toEqual({ onStartup: true });
|
|
90
|
+
expect(surface.verdict?.(shoot, project())).toEqual({
|
|
91
|
+
kind: "excluded",
|
|
92
|
+
reason: "excluded:implement-in-hosts",
|
|
93
|
+
});
|
|
94
|
+
// The escape hatch is where the author writes it; the README has to say so.
|
|
95
|
+
expect(files[`${HOST_DIR}/README.md`]).toContain("excluded:implement-in-hosts");
|
|
96
|
+
expect(files[`${HOST_DIR}/README.md`]).toContain("`shoot` — requires browser");
|
|
97
|
+
|
|
98
|
+
const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
|
|
99
|
+
expect(pkg.openclaw).toEqual({
|
|
100
|
+
extensions: ["./dist/index.js"],
|
|
101
|
+
compat: { pluginApi: OPENCLAW_SCAFFOLD.pluginApi },
|
|
102
|
+
build: { openclawVersion: OPENCLAW_SCAFFOLD.openclawVersion },
|
|
103
|
+
});
|
|
104
|
+
// Nothing to expect of the inspector until the plugin declares a registration.
|
|
105
|
+
expect(pkg.pluginInspector).toBeUndefined();
|
|
106
|
+
// TypeScript core: in-process import of the author's module, no subprocess.
|
|
107
|
+
expect(pkg.dependencies.hello).toBe("file:../..");
|
|
108
|
+
expect(files[`${HOST_DIR}/src/index.ts`]).toContain('from "hello/dist/ops.js"');
|
|
109
|
+
// JSON Schema goes through untouched via Type.Unsafe; $schema is what OpenClaw strips.
|
|
110
|
+
expect(files[`${HOST_DIR}/src/index.ts`]).toContain('"text"');
|
|
111
|
+
expect(files[`${HOST_DIR}/src/index.ts`]).not.toContain("$schema");
|
|
112
|
+
// Module load must be side-effect-free: every host loader, and `plugin-inspector check
|
|
113
|
+
// --runtime`, import the entry long before a tool runs.
|
|
114
|
+
expect(files[`${HOST_DIR}/src/index.ts`]).toContain("const dataDir = () => join(");
|
|
115
|
+
expect(files[`${HOST_DIR}/src/index.ts`]).toContain("dataDir: dataDir(),");
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("serves the web app as a gateway route and a Control UI tab, in process, only in TypeScript", () => {
|
|
119
|
+
const withWeb = project({
|
|
120
|
+
tool: { ...project().tool, surfaces: ["openclaw-native", "mcp", "web", "npm"] },
|
|
121
|
+
});
|
|
122
|
+
const files = emitted(withWeb);
|
|
123
|
+
const index = files[`${HOST_DIR}/src/index.ts`] ?? "";
|
|
124
|
+
// The route hands requests to the core's own router, so `<prefix>/mcp` needs no second
|
|
125
|
+
// listener and `/env` stays off — the gateway's working directory is not the author's.
|
|
126
|
+
expect(index).toContain('import { handler } from "hello/dist/toolfactory/mcp.js";');
|
|
127
|
+
expect(index).toContain('const WEB_PATH = "/plugins/hello/web";');
|
|
128
|
+
expect(index).toContain('auth: "gateway"');
|
|
129
|
+
expect(index).toContain("handler({ path: `${WEB_PATH}/mcp`, prefix: WEB_PATH, env: false })");
|
|
130
|
+
expect(index).toContain('surface: "tab" as const');
|
|
131
|
+
expect(index).toContain("api.registerControlUiDescriptor(descriptor)");
|
|
132
|
+
// Neither registrar is a manifest contract key; both are what the inspector expects to see.
|
|
133
|
+
expect(JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}").contracts).toEqual({
|
|
134
|
+
tools: ["echo"],
|
|
135
|
+
});
|
|
136
|
+
expect(
|
|
137
|
+
JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}").pluginInspector.plugin.expect
|
|
138
|
+
.registrations,
|
|
139
|
+
).toEqual(["registerHttpRoute", "registerControlUiDescriptor"]);
|
|
140
|
+
expect(files[`${HOST_DIR}/src/index.test.ts`]).toContain(
|
|
141
|
+
"serves the web app as a Control UI tab",
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// A Python core's plugin is an out-of-process shim: there is no handler to mount.
|
|
145
|
+
const python = emitted(
|
|
146
|
+
project({
|
|
147
|
+
tool: {
|
|
148
|
+
...withWeb.tool,
|
|
149
|
+
binding: "python",
|
|
150
|
+
surfaces: ["openclaw-native", "mcp", "web", "pypi"],
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
);
|
|
154
|
+
expect(python[`${HOST_DIR}/src/index.ts`]).not.toContain("registerHttpRoute");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("degrades a non-TypeScript core to an out-of-process shim", () => {
|
|
158
|
+
const python = project({
|
|
159
|
+
tool: { ...project().tool, binding: "python" },
|
|
160
|
+
});
|
|
161
|
+
expect(surface.verdict?.(echo, python)).toEqual({
|
|
162
|
+
kind: "degraded",
|
|
163
|
+
reason: "degraded:out-of-process",
|
|
164
|
+
});
|
|
165
|
+
const index = emitted(python)[`${HOST_DIR}/src/index.ts`] ?? "";
|
|
166
|
+
expect(index).toContain('"run", "python", "-m", "hello.toolfactory.cli"');
|
|
167
|
+
// The host's state directory reaches an out-of-process kernel as the environment variable
|
|
168
|
+
// every surface reads it from.
|
|
169
|
+
expect(index).toContain('env["HELLO_DATA_DIR"] = dataDir();');
|
|
170
|
+
expect(index).not.toContain("ops.js");
|
|
171
|
+
expect(JSON.parse(emitted(python)[`${HOST_DIR}/package.json`] ?? "{}").dependencies).toEqual(
|
|
172
|
+
OPENCLAW_SCAFFOLD.dependencies,
|
|
173
|
+
);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("drives manifest, inspector and test from one `registers` declaration", () => {
|
|
177
|
+
const target = voice();
|
|
178
|
+
const files = emitted(target);
|
|
179
|
+
const manifest = JSON.parse(files[`${HOST_DIR}/openclaw.plugin.json`] ?? "{}");
|
|
180
|
+
expect(manifest.contracts).toEqual({ tools: [], realtimeVoiceProviders: ["codex"] });
|
|
181
|
+
|
|
182
|
+
const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
|
|
183
|
+
expect(pkg.pluginInspector.plugin.expect).toEqual({
|
|
184
|
+
registrations: ["registerRealtimeVoiceProvider"],
|
|
185
|
+
manifestContracts: ["realtimeVoiceProviders"],
|
|
186
|
+
});
|
|
187
|
+
// The author's extra dependency merges after the scaffold's; the core is not imported at all.
|
|
188
|
+
expect(pkg.dependencies).toEqual({ ...OPENCLAW_SCAFFOLD.dependencies, ws: "^8.18.0" });
|
|
189
|
+
expect(pkg.peerDependencies.openclaw).toBe(">=2026.8.2");
|
|
190
|
+
expect(pkg.openclaw.compat.pluginApi).toBe(">=2026.8.2");
|
|
191
|
+
|
|
192
|
+
// The generated region stops at the entry: the registration itself is the author's tail.
|
|
193
|
+
const region = files[`${HOST_DIR}/src/index.ts`] ?? "";
|
|
194
|
+
expect(region).toContain("const entry = defineToolPlugin({");
|
|
195
|
+
expect(region).not.toContain("registerRealtimeVoiceProvider(");
|
|
196
|
+
expect(region).toContain('Parameters<OpenClawPluginApi["registerRealtimeVoiceProvider"]>[0]');
|
|
197
|
+
expect(region).not.toContain("typebox");
|
|
198
|
+
const template = surface.plan(target).find((file) => file.path.endsWith("src/index.ts"));
|
|
199
|
+
expect(template?.kind === "region" && template.template).toContain("export default entry;");
|
|
200
|
+
|
|
201
|
+
// The generated test records the registration the declaration promised.
|
|
202
|
+
const test = files[`${HOST_DIR}/src/index.test.ts`] ?? "";
|
|
203
|
+
expect(test).toContain("entry.register(api as OpenClawPluginApi)");
|
|
204
|
+
expect(test).toContain('registered["registerRealtimeVoiceProvider"]');
|
|
205
|
+
expect(files[`${HOST_DIR}/README.md`]).toContain("codex-app-server-extensions.ts");
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("projects a discriminating scripted-model e2e lane, and none without a tool to call", () => {
|
|
209
|
+
const files = emitted(project());
|
|
210
|
+
const fixtures = JSON.parse(files[`${HOST_DIR}/e2e/fixtures.json`] ?? "{}");
|
|
211
|
+
// Leg 1 asks for the operation with the authored example arguments; leg 2 answers OK only
|
|
212
|
+
// when the tool's own result carries a name its output schema promises; leg 3 catches the rest.
|
|
213
|
+
expect(fixtures.fixtures).toEqual([
|
|
214
|
+
{
|
|
215
|
+
match: { toolName: "echo", hasToolResult: false },
|
|
216
|
+
response: { toolCalls: [{ name: "echo", arguments: { text: "hello" } }] },
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
match: { hasToolResult: true, toolResultContains: "echoed" },
|
|
220
|
+
response: { content: "HELLO_OK" },
|
|
221
|
+
},
|
|
222
|
+
{ match: { hasToolResult: true }, response: { content: "HELLO_FAIL" } },
|
|
223
|
+
]);
|
|
224
|
+
const e2e = files[`${HOST_DIR}/e2e/openclaw.e2e.test.ts`] ?? "";
|
|
225
|
+
expect(e2e).toContain('expect(turn.payloads[0].text).toBe("HELLO_OK")');
|
|
226
|
+
expect(e2e).toContain('"plugins", "install", "--link", pluginDir');
|
|
227
|
+
// The three constraints that are not discoverable anywhere else.
|
|
228
|
+
expect(e2e).toContain('key !== "VITEST" && !key.startsWith("VITEST_")');
|
|
229
|
+
expect(files[`${HOST_DIR}/vitest.e2e.config.ts`]).toContain(
|
|
230
|
+
'include: ["e2e/**/*.e2e.test.ts"]',
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const pkg = JSON.parse(files[`${HOST_DIR}/package.json`] ?? "{}");
|
|
234
|
+
expect(pkg.scripts["test:e2e"]).toBe(OPENCLAW_ADDITIONS.e2eScript);
|
|
235
|
+
expect(pkg.devDependencies["@copilotkit/aimock"]).toBe(OPENCLAW_ADDITIONS.aimock);
|
|
236
|
+
// `file:../..` becomes a node_modules symlink out of the install root, which OpenClaw's
|
|
237
|
+
// install-time safety scan refuses; install-links materialises it instead.
|
|
238
|
+
expect(files[`${HOST_DIR}/.npmrc`]).toContain("install-links=true");
|
|
239
|
+
// Without it `tsc` emits a dist/ from source that failed type-checking.
|
|
240
|
+
expect(
|
|
241
|
+
JSON.parse(files[`${HOST_DIR}/tsconfig.json`] ?? "{}").compilerOptions.noEmitOnError,
|
|
242
|
+
).toBe(true);
|
|
243
|
+
|
|
244
|
+
// The lane is opt-in: a real turn really runs the operation, so no `tests.examples` entry
|
|
245
|
+
// (and no operation at all) means no lane.
|
|
246
|
+
const base = project();
|
|
247
|
+
const unexampled = emitted({ ...base, tool: { ...base.tool, tests: { examples: {} } } });
|
|
248
|
+
expect(unexampled[`${HOST_DIR}/e2e/fixtures.json`]).toBeUndefined();
|
|
249
|
+
const none = emitted(voice());
|
|
250
|
+
expect(none[`${HOST_DIR}/e2e/fixtures.json`]).toBeUndefined();
|
|
251
|
+
expect(
|
|
252
|
+
JSON.parse(none[`${HOST_DIR}/package.json`] ?? "{}").scripts["test:e2e"],
|
|
253
|
+
).toBeUndefined();
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("reports what a zero-operation plugin contributes instead of an empty table", () => {
|
|
257
|
+
const coverage = computeCoverage(voice(), [surface]);
|
|
258
|
+
expect(coverage.rows).toEqual([]);
|
|
259
|
+
const markdown = renderCoverageMarkdown(coverage, [], 7);
|
|
260
|
+
expect(markdown).not.toContain("| operation |");
|
|
261
|
+
expect(markdown).toContain(
|
|
262
|
+
"`registerRealtimeVoiceProvider` → `realtimeVoiceProviders`: `codex`",
|
|
263
|
+
);
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
/** C2: the scaffold constants are only trustworthy while the real generator still writes them. */
|
|
268
|
+
function hasOpenclaw(): boolean {
|
|
269
|
+
try {
|
|
270
|
+
execFileSync("openclaw", ["--version"], { stdio: "ignore" });
|
|
271
|
+
return true;
|
|
272
|
+
} catch {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
describe.skipIf(!hasOpenclaw())("openclaw scaffold", () => {
|
|
278
|
+
it("still matches `openclaw plugins init --type tool`", () => {
|
|
279
|
+
expect(scaffoldDrift(project())).toEqual([]);
|
|
280
|
+
// A declared `pluginApi` is an intentional override, not drift.
|
|
281
|
+
expect(scaffoldDrift(voice())).toEqual([]);
|
|
282
|
+
});
|
|
283
|
+
});
|