@enactprotocol/cli 1.2.8 → 2.0.0
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 +88 -0
- package/package.json +34 -38
- package/src/commands/auth/index.ts +940 -0
- package/src/commands/cache/index.ts +361 -0
- package/src/commands/config/README.md +239 -0
- package/src/commands/config/index.ts +164 -0
- package/src/commands/env/README.md +197 -0
- package/src/commands/env/index.ts +392 -0
- package/src/commands/exec/README.md +110 -0
- package/src/commands/exec/index.ts +195 -0
- package/src/commands/get/index.ts +198 -0
- package/src/commands/index.ts +30 -0
- package/src/commands/inspect/index.ts +264 -0
- package/src/commands/install/README.md +146 -0
- package/src/commands/install/index.ts +682 -0
- package/src/commands/list/README.md +115 -0
- package/src/commands/list/index.ts +138 -0
- package/src/commands/publish/index.ts +350 -0
- package/src/commands/report/index.ts +366 -0
- package/src/commands/run/README.md +124 -0
- package/src/commands/run/index.ts +686 -0
- package/src/commands/search/index.ts +368 -0
- package/src/commands/setup/index.ts +274 -0
- package/src/commands/sign/index.ts +652 -0
- package/src/commands/trust/README.md +214 -0
- package/src/commands/trust/index.ts +453 -0
- package/src/commands/unyank/index.ts +107 -0
- package/src/commands/yank/index.ts +143 -0
- package/src/index.ts +96 -0
- package/src/types.ts +81 -0
- package/src/utils/errors.ts +409 -0
- package/src/utils/exit-codes.ts +159 -0
- package/src/utils/ignore.ts +147 -0
- package/src/utils/index.ts +107 -0
- package/src/utils/output.ts +242 -0
- package/src/utils/spinner.ts +214 -0
- package/tests/commands/auth.test.ts +217 -0
- package/tests/commands/cache.test.ts +286 -0
- package/tests/commands/config.test.ts +277 -0
- package/tests/commands/env.test.ts +293 -0
- package/tests/commands/exec.test.ts +112 -0
- package/tests/commands/get.test.ts +179 -0
- package/tests/commands/inspect.test.ts +201 -0
- package/tests/commands/install-integration.test.ts +343 -0
- package/tests/commands/install.test.ts +288 -0
- package/tests/commands/list.test.ts +160 -0
- package/tests/commands/publish.test.ts +186 -0
- package/tests/commands/report.test.ts +194 -0
- package/tests/commands/run.test.ts +231 -0
- package/tests/commands/search.test.ts +131 -0
- package/tests/commands/sign.test.ts +164 -0
- package/tests/commands/trust.test.ts +236 -0
- package/tests/commands/unyank.test.ts +114 -0
- package/tests/commands/yank.test.ts +154 -0
- package/tests/e2e.test.ts +554 -0
- package/tests/fixtures/calculator/enact.yaml +34 -0
- package/tests/fixtures/echo-tool/enact.md +31 -0
- package/tests/fixtures/env-tool/enact.yaml +19 -0
- package/tests/fixtures/greeter/enact.yaml +18 -0
- package/tests/fixtures/invalid-tool/enact.yaml +4 -0
- package/tests/index.test.ts +8 -0
- package/tests/types.test.ts +84 -0
- package/tests/utils/errors.test.ts +303 -0
- package/tests/utils/exit-codes.test.ts +189 -0
- package/tests/utils/ignore.test.ts +461 -0
- package/tests/utils/output.test.ts +126 -0
- package/tsconfig.json +17 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/dist/index.js +0 -231410
- package/dist/index.js.bak +0 -231409
- package/dist/web/static/app.js +0 -663
- package/dist/web/static/index.html +0 -117
- package/dist/web/static/style.css +0 -291
|
@@ -0,0 +1,554 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* End-to-End Integration Tests for Enact CLI
|
|
3
|
+
*
|
|
4
|
+
* These tests verify complete workflows using direct function imports.
|
|
5
|
+
* They test the underlying logic that the CLI commands use.
|
|
6
|
+
*
|
|
7
|
+
* Note: Tests that require actual container execution are marked with
|
|
8
|
+
* `test.skip` by default to allow running in CI without Docker.
|
|
9
|
+
* Set ENACT_E2E_DOCKER=true to run container tests.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
|
|
13
|
+
import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { dirname, join, resolve } from "node:path";
|
|
16
|
+
import {
|
|
17
|
+
type ToolManifest,
|
|
18
|
+
applyDefaults,
|
|
19
|
+
loadManifestFromDir,
|
|
20
|
+
prepareCommand,
|
|
21
|
+
toolNameToPath,
|
|
22
|
+
tryResolveTool,
|
|
23
|
+
validateInputs,
|
|
24
|
+
} from "@enactprotocol/shared";
|
|
25
|
+
|
|
26
|
+
// Test fixtures location
|
|
27
|
+
const FIXTURES_DIR = resolve(__dirname, "fixtures");
|
|
28
|
+
const GREETER_TOOL = join(FIXTURES_DIR, "greeter");
|
|
29
|
+
const ECHO_TOOL = join(FIXTURES_DIR, "echo-tool");
|
|
30
|
+
const CALCULATOR_TOOL = join(FIXTURES_DIR, "calculator");
|
|
31
|
+
const INVALID_TOOL = join(FIXTURES_DIR, "invalid-tool");
|
|
32
|
+
|
|
33
|
+
// Temporary directory for test operations
|
|
34
|
+
let tempDir: string;
|
|
35
|
+
|
|
36
|
+
// Check if Docker/container runtime is available
|
|
37
|
+
const hasDocker = await (async () => {
|
|
38
|
+
try {
|
|
39
|
+
const proc = Bun.spawn(["docker", "info"], { stdout: "pipe", stderr: "pipe" });
|
|
40
|
+
await proc.exited;
|
|
41
|
+
return proc.exitCode === 0;
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
|
|
47
|
+
// Whether to run container tests
|
|
48
|
+
const runContainerTests = process.env.ENACT_E2E_DOCKER === "true" && hasDocker;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Helper to install a tool to a directory (simulates install command)
|
|
52
|
+
*/
|
|
53
|
+
function installTool(
|
|
54
|
+
sourcePath: string,
|
|
55
|
+
destBase: string
|
|
56
|
+
): { manifest: ToolManifest; destPath: string } {
|
|
57
|
+
const loaded = loadManifestFromDir(sourcePath);
|
|
58
|
+
if (!loaded) {
|
|
59
|
+
throw new Error(`No valid manifest found in: ${sourcePath}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const manifest = loaded.manifest;
|
|
63
|
+
const toolPath = toolNameToPath(manifest.name);
|
|
64
|
+
const destPath = join(destBase, toolPath);
|
|
65
|
+
|
|
66
|
+
// Create destination and copy
|
|
67
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
68
|
+
cpSync(sourcePath, destPath, { recursive: true });
|
|
69
|
+
|
|
70
|
+
return { manifest, destPath };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
describe("E2E: Fixture Validation", () => {
|
|
74
|
+
test("greeter fixture has valid YAML manifest", () => {
|
|
75
|
+
const loaded = loadManifestFromDir(GREETER_TOOL);
|
|
76
|
+
expect(loaded).not.toBeNull();
|
|
77
|
+
expect(loaded?.manifest.name).toBe("test/greeter");
|
|
78
|
+
expect(loaded?.manifest.version).toBe("1.0.0");
|
|
79
|
+
expect(loaded?.manifest.from).toBe("alpine:latest");
|
|
80
|
+
expect(loaded?.manifest.command).toContain("echo");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("echo-tool fixture has valid Markdown manifest", () => {
|
|
84
|
+
const loaded = loadManifestFromDir(ECHO_TOOL);
|
|
85
|
+
expect(loaded).not.toBeNull();
|
|
86
|
+
expect(loaded?.manifest.name).toBe("test/echo-tool");
|
|
87
|
+
expect(loaded?.manifest.version).toBe("1.0.0");
|
|
88
|
+
expect(loaded?.format).toBe("md");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("calculator fixture has valid manifest with complex schema", () => {
|
|
92
|
+
const loaded = loadManifestFromDir(CALCULATOR_TOOL);
|
|
93
|
+
expect(loaded).not.toBeNull();
|
|
94
|
+
expect(loaded?.manifest.name).toBe("test/calculator");
|
|
95
|
+
expect(loaded?.manifest.from).toBe("python:3.12-alpine");
|
|
96
|
+
expect(loaded?.manifest.inputSchema?.properties?.operation).toBeDefined();
|
|
97
|
+
expect(loaded?.manifest.inputSchema?.required).toContain("operation");
|
|
98
|
+
expect(loaded?.manifest.inputSchema?.required).toContain("a");
|
|
99
|
+
expect(loaded?.manifest.inputSchema?.required).toContain("b");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("invalid-tool fixture is missing required fields", () => {
|
|
103
|
+
const loaded = loadManifestFromDir(INVALID_TOOL);
|
|
104
|
+
expect(loaded).not.toBeNull();
|
|
105
|
+
// It loads but doesn't have required fields
|
|
106
|
+
expect(loaded?.manifest.from).toBeUndefined();
|
|
107
|
+
expect(loaded?.manifest.command).toBeUndefined();
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe("E2E: Tool Installation Flow", () => {
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
tempDir = join(tmpdir(), `enact-e2e-install-${Date.now()}`);
|
|
114
|
+
mkdirSync(tempDir, { recursive: true });
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
afterEach(() => {
|
|
118
|
+
if (tempDir && existsSync(tempDir)) {
|
|
119
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("installs tool to project .enact/tools directory", () => {
|
|
124
|
+
const destBase = join(tempDir, ".enact", "tools");
|
|
125
|
+
const { manifest, destPath } = installTool(GREETER_TOOL, destBase);
|
|
126
|
+
|
|
127
|
+
expect(manifest.name).toBe("test/greeter");
|
|
128
|
+
expect(existsSync(destPath)).toBe(true);
|
|
129
|
+
expect(existsSync(join(destPath, "enact.yaml"))).toBe(true);
|
|
130
|
+
|
|
131
|
+
// Verify installed manifest can be loaded
|
|
132
|
+
const reloaded = loadManifestFromDir(destPath);
|
|
133
|
+
expect(reloaded).not.toBeNull();
|
|
134
|
+
expect(reloaded?.manifest.name).toBe("test/greeter");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("installs markdown tool correctly", () => {
|
|
138
|
+
const destBase = join(tempDir, ".enact", "tools");
|
|
139
|
+
const { manifest, destPath } = installTool(ECHO_TOOL, destBase);
|
|
140
|
+
|
|
141
|
+
expect(manifest.name).toBe("test/echo-tool");
|
|
142
|
+
expect(existsSync(join(destPath, "enact.md"))).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("installs multiple tools without conflict", () => {
|
|
146
|
+
const destBase = join(tempDir, ".enact", "tools");
|
|
147
|
+
|
|
148
|
+
const result1 = installTool(GREETER_TOOL, destBase);
|
|
149
|
+
const result2 = installTool(ECHO_TOOL, destBase);
|
|
150
|
+
const result3 = installTool(CALCULATOR_TOOL, destBase);
|
|
151
|
+
|
|
152
|
+
expect(existsSync(result1.destPath)).toBe(true);
|
|
153
|
+
expect(existsSync(result2.destPath)).toBe(true);
|
|
154
|
+
expect(existsSync(result3.destPath)).toBe(true);
|
|
155
|
+
|
|
156
|
+
// All should be in test/ namespace
|
|
157
|
+
expect(result1.destPath).toContain("test/greeter");
|
|
158
|
+
expect(result2.destPath).toContain("test/echo-tool");
|
|
159
|
+
expect(result3.destPath).toContain("test/calculator");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("overwrites existing tool on reinstall", () => {
|
|
163
|
+
const destBase = join(tempDir, ".enact", "tools");
|
|
164
|
+
|
|
165
|
+
// First install
|
|
166
|
+
const result1 = installTool(GREETER_TOOL, destBase);
|
|
167
|
+
|
|
168
|
+
// Add a marker file
|
|
169
|
+
const markerPath = join(result1.destPath, ".marker");
|
|
170
|
+
writeFileSync(markerPath, "original");
|
|
171
|
+
expect(existsSync(markerPath)).toBe(true);
|
|
172
|
+
|
|
173
|
+
// Reinstall - cpSync with recursive merges by default
|
|
174
|
+
// In real install command we'd rm first, but this tests the helper
|
|
175
|
+
installTool(GREETER_TOOL, destBase);
|
|
176
|
+
|
|
177
|
+
// Manifest should still be valid after reinstall
|
|
178
|
+
const reloaded = loadManifestFromDir(result1.destPath);
|
|
179
|
+
expect(reloaded).not.toBeNull();
|
|
180
|
+
expect(reloaded?.manifest.name).toBe("test/greeter");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("E2E: Tool Resolution Flow", () => {
|
|
185
|
+
let resolveTempDir: string;
|
|
186
|
+
|
|
187
|
+
beforeAll(() => {
|
|
188
|
+
resolveTempDir = join(tmpdir(), `enact-e2e-resolve-${Date.now()}`);
|
|
189
|
+
mkdirSync(resolveTempDir, { recursive: true });
|
|
190
|
+
|
|
191
|
+
// Install tools
|
|
192
|
+
const destBase = join(resolveTempDir, ".enact", "tools");
|
|
193
|
+
installTool(GREETER_TOOL, destBase);
|
|
194
|
+
installTool(ECHO_TOOL, destBase);
|
|
195
|
+
installTool(CALCULATOR_TOOL, destBase);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
afterAll(() => {
|
|
199
|
+
if (resolveTempDir && existsSync(resolveTempDir)) {
|
|
200
|
+
rmSync(resolveTempDir, { recursive: true, force: true });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("resolves installed tool by name", () => {
|
|
205
|
+
const resolution = tryResolveTool("test/greeter", { startDir: resolveTempDir });
|
|
206
|
+
expect(resolution).not.toBeNull();
|
|
207
|
+
expect(resolution?.manifest.name).toBe("test/greeter");
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("resolves tool by path", () => {
|
|
211
|
+
// Resolve from installed location
|
|
212
|
+
const toolPath = join(resolveTempDir, ".enact", "tools", "test", "greeter");
|
|
213
|
+
const resolution = tryResolveTool(toolPath);
|
|
214
|
+
expect(resolution).not.toBeNull();
|
|
215
|
+
expect(resolution?.manifest.name).toBe("test/greeter");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("returns null for non-existent tool", () => {
|
|
219
|
+
const resolution = tryResolveTool("non-existent/tool", { startDir: resolveTempDir });
|
|
220
|
+
expect(resolution).toBeNull();
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("resolves all installed tools", () => {
|
|
224
|
+
const tools = ["test/greeter", "test/echo-tool", "test/calculator"];
|
|
225
|
+
|
|
226
|
+
for (const toolName of tools) {
|
|
227
|
+
const resolution = tryResolveTool(toolName, { startDir: resolveTempDir });
|
|
228
|
+
expect(resolution).not.toBeNull();
|
|
229
|
+
expect(resolution?.manifest.name).toBe(toolName);
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("E2E: Input Validation Flow", () => {
|
|
235
|
+
let greeterManifest: ToolManifest;
|
|
236
|
+
let calculatorManifest: ToolManifest;
|
|
237
|
+
|
|
238
|
+
beforeAll(() => {
|
|
239
|
+
const greeterLoaded = loadManifestFromDir(GREETER_TOOL);
|
|
240
|
+
const calculatorLoaded = loadManifestFromDir(CALCULATOR_TOOL);
|
|
241
|
+
greeterManifest = greeterLoaded?.manifest;
|
|
242
|
+
calculatorManifest = calculatorLoaded?.manifest;
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("validates greeter with default input", () => {
|
|
246
|
+
// Apply defaults first, then validate
|
|
247
|
+
const withDefaults = applyDefaults({}, greeterManifest.inputSchema);
|
|
248
|
+
const result = validateInputs(withDefaults, greeterManifest.inputSchema);
|
|
249
|
+
expect(result.valid).toBe(true);
|
|
250
|
+
// Should have default applied
|
|
251
|
+
expect(withDefaults.name).toBe("World");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
test("validates greeter with custom input", () => {
|
|
255
|
+
const result = validateInputs({ name: "Alice" }, greeterManifest.inputSchema);
|
|
256
|
+
expect(result.valid).toBe(true);
|
|
257
|
+
expect(result.coercedValues?.name).toBe("Alice");
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
test("validates calculator with all required inputs", () => {
|
|
261
|
+
const result = validateInputs({ operation: "add", a: 5, b: 3 }, calculatorManifest.inputSchema);
|
|
262
|
+
expect(result.valid).toBe(true);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("fails validation when required input is missing", () => {
|
|
266
|
+
const result = validateInputs(
|
|
267
|
+
{ operation: "add", a: 5 }, // missing 'b'
|
|
268
|
+
calculatorManifest.inputSchema
|
|
269
|
+
);
|
|
270
|
+
expect(result.valid).toBe(false);
|
|
271
|
+
expect(result.errors.length).toBeGreaterThan(0);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("fails validation with invalid enum value", () => {
|
|
275
|
+
const result = validateInputs(
|
|
276
|
+
{ operation: "invalid", a: 5, b: 3 },
|
|
277
|
+
calculatorManifest.inputSchema
|
|
278
|
+
);
|
|
279
|
+
expect(result.valid).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe("E2E: Command Preparation Flow", () => {
|
|
284
|
+
test("prepares greeter command with input", () => {
|
|
285
|
+
const command = `echo '{"message": "Hello, \${name}!"}'`;
|
|
286
|
+
const prepared = prepareCommand(command, { name: "Alice" });
|
|
287
|
+
|
|
288
|
+
// prepareCommand returns string[] - join to check content
|
|
289
|
+
const preparedStr = prepared.join(" ");
|
|
290
|
+
expect(preparedStr).toContain("Alice");
|
|
291
|
+
expect(preparedStr).not.toContain("${name}");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("prepares calculator command with multiple inputs", () => {
|
|
295
|
+
const loaded = loadManifestFromDir(CALCULATOR_TOOL);
|
|
296
|
+
const command = loaded?.manifest.command!;
|
|
297
|
+
|
|
298
|
+
const prepared = prepareCommand(command, { operation: "add", a: 5, b: 3 });
|
|
299
|
+
const preparedStr = prepared.join(" ");
|
|
300
|
+
|
|
301
|
+
expect(preparedStr).toContain("add");
|
|
302
|
+
expect(preparedStr).toContain("5");
|
|
303
|
+
expect(preparedStr).toContain("3");
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test("escapes special characters in input", () => {
|
|
307
|
+
const command = `echo "\${text}"`;
|
|
308
|
+
const prepared = prepareCommand(command, { text: "hello; rm -rf /" });
|
|
309
|
+
const preparedStr = prepared.join(" ");
|
|
310
|
+
|
|
311
|
+
// Should contain the text (escaped appropriately)
|
|
312
|
+
expect(preparedStr).toContain("hello");
|
|
313
|
+
});
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
describe("E2E: Configuration Flow", () => {
|
|
317
|
+
beforeEach(() => {
|
|
318
|
+
tempDir = join(tmpdir(), `enact-e2e-config-${Date.now()}`);
|
|
319
|
+
mkdirSync(join(tempDir, ".enact"), { recursive: true });
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
afterEach(() => {
|
|
323
|
+
if (tempDir && existsSync(tempDir)) {
|
|
324
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("creates config file structure", () => {
|
|
329
|
+
const configPath = join(tempDir, ".enact", "config.json");
|
|
330
|
+
|
|
331
|
+
// Create a config file
|
|
332
|
+
const config = { test: { key: "value" } };
|
|
333
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
334
|
+
|
|
335
|
+
// Read it back
|
|
336
|
+
const loaded = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
337
|
+
expect(loaded.test.key).toBe("value");
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("config file supports nested values", () => {
|
|
341
|
+
const configPath = join(tempDir, ".enact", "config.json");
|
|
342
|
+
|
|
343
|
+
const config = {
|
|
344
|
+
registry: {
|
|
345
|
+
url: "https://registry.example.com",
|
|
346
|
+
timeout: 30000,
|
|
347
|
+
},
|
|
348
|
+
trust: {
|
|
349
|
+
policy: "strict",
|
|
350
|
+
requireAudit: true,
|
|
351
|
+
},
|
|
352
|
+
};
|
|
353
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2));
|
|
354
|
+
|
|
355
|
+
const loaded = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
356
|
+
expect(loaded.registry.url).toBe("https://registry.example.com");
|
|
357
|
+
expect(loaded.trust.policy).toBe("strict");
|
|
358
|
+
});
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
describe("E2E: Environment Variable Flow", () => {
|
|
362
|
+
beforeEach(() => {
|
|
363
|
+
tempDir = join(tmpdir(), `enact-e2e-env-${Date.now()}`);
|
|
364
|
+
mkdirSync(join(tempDir, ".enact"), { recursive: true });
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
afterEach(() => {
|
|
368
|
+
if (tempDir && existsSync(tempDir)) {
|
|
369
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
370
|
+
}
|
|
371
|
+
});
|
|
372
|
+
|
|
373
|
+
test("writes and reads .env file", () => {
|
|
374
|
+
const envPath = join(tempDir, ".enact", ".env");
|
|
375
|
+
|
|
376
|
+
// Write env vars
|
|
377
|
+
const envContent = "TEST_VAR=test-value\nANOTHER_VAR=another-value";
|
|
378
|
+
writeFileSync(envPath, envContent);
|
|
379
|
+
|
|
380
|
+
// Read back
|
|
381
|
+
const loaded = readFileSync(envPath, "utf-8");
|
|
382
|
+
expect(loaded).toContain("TEST_VAR=test-value");
|
|
383
|
+
expect(loaded).toContain("ANOTHER_VAR=another-value");
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
test("handles env vars with special characters", () => {
|
|
387
|
+
const envPath = join(tempDir, ".enact", ".env");
|
|
388
|
+
|
|
389
|
+
// Write env var with special chars (quoted)
|
|
390
|
+
const envContent = `SPECIAL_VAR="value with spaces and = sign"`;
|
|
391
|
+
writeFileSync(envPath, envContent);
|
|
392
|
+
|
|
393
|
+
const loaded = readFileSync(envPath, "utf-8");
|
|
394
|
+
expect(loaded).toContain("SPECIAL_VAR");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("supports comments in .env file", () => {
|
|
398
|
+
const envPath = join(tempDir, ".enact", ".env");
|
|
399
|
+
|
|
400
|
+
const envContent = "# This is a comment\nVAR1=value1\n# Another comment\nVAR2=value2";
|
|
401
|
+
writeFileSync(envPath, envContent);
|
|
402
|
+
|
|
403
|
+
const loaded = readFileSync(envPath, "utf-8");
|
|
404
|
+
expect(loaded).toContain("# This is a comment");
|
|
405
|
+
expect(loaded).toContain("VAR1=value1");
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
describe("E2E: Trust Policy Flow", () => {
|
|
410
|
+
beforeEach(() => {
|
|
411
|
+
tempDir = join(tmpdir(), `enact-e2e-trust-${Date.now()}`);
|
|
412
|
+
mkdirSync(join(tempDir, ".enact"), { recursive: true });
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
afterEach(() => {
|
|
416
|
+
if (tempDir && existsSync(tempDir)) {
|
|
417
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
test("creates trust policy file", () => {
|
|
422
|
+
const trustPath = join(tempDir, ".enact", "trust-policy.json");
|
|
423
|
+
|
|
424
|
+
const policy = {
|
|
425
|
+
name: "test-policy",
|
|
426
|
+
version: "1.0",
|
|
427
|
+
trustedPublishers: [{ identity: "alice@example.com", issuer: "https://accounts.google.com" }],
|
|
428
|
+
requireAttestation: true,
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
writeFileSync(trustPath, JSON.stringify(policy, null, 2));
|
|
432
|
+
|
|
433
|
+
const loaded = JSON.parse(readFileSync(trustPath, "utf-8"));
|
|
434
|
+
expect(loaded.trustedPublishers).toHaveLength(1);
|
|
435
|
+
expect(loaded.trustedPublishers[0].identity).toBe("alice@example.com");
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test("supports multiple trusted publishers", () => {
|
|
439
|
+
const trustPath = join(tempDir, ".enact", "trust-policy.json");
|
|
440
|
+
|
|
441
|
+
const policy = {
|
|
442
|
+
name: "multi-publisher-policy",
|
|
443
|
+
trustedPublishers: [
|
|
444
|
+
{ identity: "alice@example.com", issuer: "https://accounts.google.com" },
|
|
445
|
+
{
|
|
446
|
+
identity: "https://github.com/myorg/*",
|
|
447
|
+
issuer: "https://token.actions.githubusercontent.com",
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
trustedAuditors: [
|
|
451
|
+
{ identity: "security@auditfirm.com", issuer: "https://accounts.google.com" },
|
|
452
|
+
],
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
writeFileSync(trustPath, JSON.stringify(policy, null, 2));
|
|
456
|
+
|
|
457
|
+
const loaded = JSON.parse(readFileSync(trustPath, "utf-8"));
|
|
458
|
+
expect(loaded.trustedPublishers).toHaveLength(2);
|
|
459
|
+
expect(loaded.trustedAuditors).toHaveLength(1);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
describe("E2E: Full Workflow", () => {
|
|
464
|
+
beforeEach(() => {
|
|
465
|
+
tempDir = join(tmpdir(), `enact-e2e-workflow-${Date.now()}`);
|
|
466
|
+
mkdirSync(tempDir, { recursive: true });
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
afterEach(() => {
|
|
470
|
+
if (tempDir && existsSync(tempDir)) {
|
|
471
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test("complete install -> resolve -> validate -> prepare flow", () => {
|
|
476
|
+
// 1. Install tool
|
|
477
|
+
const destBase = join(tempDir, ".enact", "tools");
|
|
478
|
+
const { manifest } = installTool(GREETER_TOOL, destBase);
|
|
479
|
+
expect(manifest.name).toBe("test/greeter");
|
|
480
|
+
|
|
481
|
+
// 2. Resolve tool
|
|
482
|
+
const resolution = tryResolveTool("test/greeter", { startDir: tempDir });
|
|
483
|
+
expect(resolution).not.toBeNull();
|
|
484
|
+
|
|
485
|
+
// 3. Validate inputs
|
|
486
|
+
const validation = validateInputs({ name: "TestUser" }, manifest.inputSchema);
|
|
487
|
+
expect(validation.valid).toBe(true);
|
|
488
|
+
|
|
489
|
+
// 4. Prepare command
|
|
490
|
+
const prepared = prepareCommand(manifest.command!, validation.coercedValues!);
|
|
491
|
+
const preparedStr = prepared.join(" ");
|
|
492
|
+
expect(preparedStr).toContain("TestUser");
|
|
493
|
+
expect(preparedStr).toContain("Hello");
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
test("complete calculator workflow", () => {
|
|
497
|
+
// 1. Install tool
|
|
498
|
+
const destBase = join(tempDir, ".enact", "tools");
|
|
499
|
+
const { manifest } = installTool(CALCULATOR_TOOL, destBase);
|
|
500
|
+
|
|
501
|
+
// 2. Resolve tool
|
|
502
|
+
const resolution = tryResolveTool("test/calculator", { startDir: tempDir });
|
|
503
|
+
expect(resolution).not.toBeNull();
|
|
504
|
+
|
|
505
|
+
// 3. Validate multiple operations
|
|
506
|
+
const operations = [
|
|
507
|
+
{ operation: "add", a: 10, b: 5 },
|
|
508
|
+
{ operation: "subtract", a: 10, b: 5 },
|
|
509
|
+
{ operation: "multiply", a: 10, b: 5 },
|
|
510
|
+
{ operation: "divide", a: 10, b: 5 },
|
|
511
|
+
];
|
|
512
|
+
|
|
513
|
+
for (const inputs of operations) {
|
|
514
|
+
const validation = validateInputs(inputs, manifest.inputSchema);
|
|
515
|
+
expect(validation.valid).toBe(true);
|
|
516
|
+
|
|
517
|
+
const prepared = prepareCommand(manifest.command!, validation.coercedValues!);
|
|
518
|
+
const preparedStr = prepared.join(" ");
|
|
519
|
+
expect(preparedStr).toContain(inputs.operation);
|
|
520
|
+
expect(preparedStr).toContain(String(inputs.a));
|
|
521
|
+
expect(preparedStr).toContain(String(inputs.b));
|
|
522
|
+
}
|
|
523
|
+
});
|
|
524
|
+
|
|
525
|
+
test("handles markdown tool workflow", () => {
|
|
526
|
+
// Install markdown-based tool
|
|
527
|
+
const destBase = join(tempDir, ".enact", "tools");
|
|
528
|
+
const { manifest } = installTool(ECHO_TOOL, destBase);
|
|
529
|
+
expect(manifest.name).toBe("test/echo-tool");
|
|
530
|
+
|
|
531
|
+
// Resolve
|
|
532
|
+
const resolution = tryResolveTool("test/echo-tool", { startDir: tempDir });
|
|
533
|
+
expect(resolution).not.toBeNull();
|
|
534
|
+
|
|
535
|
+
// Validate
|
|
536
|
+
const validation = validateInputs({ text: "Hello from markdown tool!" }, manifest.inputSchema);
|
|
537
|
+
expect(validation.valid).toBe(true);
|
|
538
|
+
|
|
539
|
+
// Prepare
|
|
540
|
+
const prepared = prepareCommand(manifest.command!, validation.coercedValues!);
|
|
541
|
+
const preparedStr = prepared.join(" ");
|
|
542
|
+
expect(preparedStr).toContain("Hello from markdown tool!");
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
// Container execution tests - only run if Docker is available
|
|
547
|
+
describe.skipIf(!runContainerTests)("E2E: Container Execution", () => {
|
|
548
|
+
// These tests would actually run containers with Dagger
|
|
549
|
+
// They are skipped by default since they require Docker
|
|
550
|
+
|
|
551
|
+
test("placeholder for container tests", () => {
|
|
552
|
+
expect(runContainerTests).toBe(true);
|
|
553
|
+
});
|
|
554
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
enact: "2.0.0"
|
|
2
|
+
name: test/calculator
|
|
3
|
+
version: "2.0.0"
|
|
4
|
+
description: A calculator tool that performs basic math operations
|
|
5
|
+
from: python:3.12-alpine
|
|
6
|
+
command: "python3 -c \"import json; print(json.dumps({'result': eval('${a} + ${b}' if '${operation}'=='add' else '${a} - ${b}' if '${operation}'=='subtract' else '${a} * ${b}' if '${operation}'=='multiply' else '${a} / ${b}'), 'operation': '${operation}'}))\""
|
|
7
|
+
inputSchema:
|
|
8
|
+
type: object
|
|
9
|
+
properties:
|
|
10
|
+
operation:
|
|
11
|
+
type: string
|
|
12
|
+
enum:
|
|
13
|
+
- add
|
|
14
|
+
- subtract
|
|
15
|
+
- multiply
|
|
16
|
+
- divide
|
|
17
|
+
description: The math operation to perform
|
|
18
|
+
a:
|
|
19
|
+
type: number
|
|
20
|
+
description: First operand
|
|
21
|
+
b:
|
|
22
|
+
type: number
|
|
23
|
+
description: Second operand
|
|
24
|
+
required:
|
|
25
|
+
- operation
|
|
26
|
+
- a
|
|
27
|
+
- b
|
|
28
|
+
outputSchema:
|
|
29
|
+
type: object
|
|
30
|
+
properties:
|
|
31
|
+
result:
|
|
32
|
+
type: number
|
|
33
|
+
operation:
|
|
34
|
+
type: string
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
---
|
|
2
|
+
enact: "2.0.0"
|
|
3
|
+
name: test/echo-tool
|
|
4
|
+
version: "1.0.0"
|
|
5
|
+
description: A tool that echoes its input for testing
|
|
6
|
+
from: alpine:latest
|
|
7
|
+
command: echo '{"output":"${text}"}'
|
|
8
|
+
inputSchema:
|
|
9
|
+
type: object
|
|
10
|
+
properties:
|
|
11
|
+
text:
|
|
12
|
+
type: string
|
|
13
|
+
description: Text to echo
|
|
14
|
+
required:
|
|
15
|
+
- text
|
|
16
|
+
outputSchema:
|
|
17
|
+
type: object
|
|
18
|
+
properties:
|
|
19
|
+
output:
|
|
20
|
+
type: string
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
# Echo Tool
|
|
24
|
+
|
|
25
|
+
A simple tool that echoes back the input text. Used for testing.
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
enact run test/echo-tool --input text="Hello"
|
|
31
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
enact: "2.0.0"
|
|
2
|
+
name: test/env-tool
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
description: A tool that uses environment variables
|
|
5
|
+
from: alpine:latest
|
|
6
|
+
command: echo '{"status":"ok"}'
|
|
7
|
+
env:
|
|
8
|
+
TEST_VAR:
|
|
9
|
+
default: default_value
|
|
10
|
+
description: A test environment variable
|
|
11
|
+
SECRET_VAR:
|
|
12
|
+
secret: true
|
|
13
|
+
description: A secret environment variable
|
|
14
|
+
inputSchema:
|
|
15
|
+
type: object
|
|
16
|
+
properties:
|
|
17
|
+
showEnv:
|
|
18
|
+
type: boolean
|
|
19
|
+
default: false
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
enact: "2.0.0"
|
|
2
|
+
name: test/greeter
|
|
3
|
+
version: "1.0.0"
|
|
4
|
+
description: A simple greeting tool for testing
|
|
5
|
+
from: alpine:latest
|
|
6
|
+
command: echo '{"message":"Hello, ${name}!"}'
|
|
7
|
+
inputSchema:
|
|
8
|
+
type: object
|
|
9
|
+
properties:
|
|
10
|
+
name:
|
|
11
|
+
type: string
|
|
12
|
+
description: Name to greet
|
|
13
|
+
default: World
|
|
14
|
+
outputSchema:
|
|
15
|
+
type: object
|
|
16
|
+
properties:
|
|
17
|
+
message:
|
|
18
|
+
type: string
|