@enactprotocol/cli 1.2.13 → 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.
Files changed (73) hide show
  1. package/README.md +88 -0
  2. package/package.json +34 -38
  3. package/src/commands/auth/index.ts +940 -0
  4. package/src/commands/cache/index.ts +361 -0
  5. package/src/commands/config/README.md +239 -0
  6. package/src/commands/config/index.ts +164 -0
  7. package/src/commands/env/README.md +197 -0
  8. package/src/commands/env/index.ts +392 -0
  9. package/src/commands/exec/README.md +110 -0
  10. package/src/commands/exec/index.ts +195 -0
  11. package/src/commands/get/index.ts +198 -0
  12. package/src/commands/index.ts +30 -0
  13. package/src/commands/inspect/index.ts +264 -0
  14. package/src/commands/install/README.md +146 -0
  15. package/src/commands/install/index.ts +682 -0
  16. package/src/commands/list/README.md +115 -0
  17. package/src/commands/list/index.ts +138 -0
  18. package/src/commands/publish/index.ts +350 -0
  19. package/src/commands/report/index.ts +366 -0
  20. package/src/commands/run/README.md +124 -0
  21. package/src/commands/run/index.ts +686 -0
  22. package/src/commands/search/index.ts +368 -0
  23. package/src/commands/setup/index.ts +274 -0
  24. package/src/commands/sign/index.ts +652 -0
  25. package/src/commands/trust/README.md +214 -0
  26. package/src/commands/trust/index.ts +453 -0
  27. package/src/commands/unyank/index.ts +107 -0
  28. package/src/commands/yank/index.ts +143 -0
  29. package/src/index.ts +96 -0
  30. package/src/types.ts +81 -0
  31. package/src/utils/errors.ts +409 -0
  32. package/src/utils/exit-codes.ts +159 -0
  33. package/src/utils/ignore.ts +147 -0
  34. package/src/utils/index.ts +107 -0
  35. package/src/utils/output.ts +242 -0
  36. package/src/utils/spinner.ts +214 -0
  37. package/tests/commands/auth.test.ts +217 -0
  38. package/tests/commands/cache.test.ts +286 -0
  39. package/tests/commands/config.test.ts +277 -0
  40. package/tests/commands/env.test.ts +293 -0
  41. package/tests/commands/exec.test.ts +112 -0
  42. package/tests/commands/get.test.ts +179 -0
  43. package/tests/commands/inspect.test.ts +201 -0
  44. package/tests/commands/install-integration.test.ts +343 -0
  45. package/tests/commands/install.test.ts +288 -0
  46. package/tests/commands/list.test.ts +160 -0
  47. package/tests/commands/publish.test.ts +186 -0
  48. package/tests/commands/report.test.ts +194 -0
  49. package/tests/commands/run.test.ts +231 -0
  50. package/tests/commands/search.test.ts +131 -0
  51. package/tests/commands/sign.test.ts +164 -0
  52. package/tests/commands/trust.test.ts +236 -0
  53. package/tests/commands/unyank.test.ts +114 -0
  54. package/tests/commands/yank.test.ts +154 -0
  55. package/tests/e2e.test.ts +554 -0
  56. package/tests/fixtures/calculator/enact.yaml +34 -0
  57. package/tests/fixtures/echo-tool/enact.md +31 -0
  58. package/tests/fixtures/env-tool/enact.yaml +19 -0
  59. package/tests/fixtures/greeter/enact.yaml +18 -0
  60. package/tests/fixtures/invalid-tool/enact.yaml +4 -0
  61. package/tests/index.test.ts +8 -0
  62. package/tests/types.test.ts +84 -0
  63. package/tests/utils/errors.test.ts +303 -0
  64. package/tests/utils/exit-codes.test.ts +189 -0
  65. package/tests/utils/ignore.test.ts +461 -0
  66. package/tests/utils/output.test.ts +126 -0
  67. package/tsconfig.json +17 -0
  68. package/tsconfig.tsbuildinfo +1 -0
  69. package/dist/index.js +0 -231612
  70. package/dist/index.js.bak +0 -231611
  71. package/dist/web/static/app.js +0 -663
  72. package/dist/web/static/index.html +0 -117
  73. 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
@@ -0,0 +1,4 @@
1
+ enact: "2.0.0"
2
+ name: test/invalid-tool
3
+ version: "1.0.0"
4
+ description: An invalid tool manifest for testing validation
@@ -0,0 +1,8 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { version } from "../src/index";
3
+
4
+ describe("@enactprotocol/cli", () => {
5
+ test("exports version", () => {
6
+ expect(version).toBe("0.1.0");
7
+ });
8
+ });