@enactprotocol/trust 2.1.15 → 2.1.17

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.
@@ -0,0 +1,371 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import {
5
+ type ChecksumManifest,
6
+ MANIFEST_VERSION,
7
+ computeManifestHash,
8
+ createChecksumManifest,
9
+ parseChecksumManifest,
10
+ serializeChecksumManifest,
11
+ verifyChecksumManifest,
12
+ } from "../src/manifest";
13
+
14
+ const TEST_DIR = join(import.meta.dir, "fixtures", "manifest-test");
15
+ const TOOL_DIR = join(TEST_DIR, "sample-tool");
16
+
17
+ describe("checksum manifest", () => {
18
+ beforeAll(() => {
19
+ // Create test directory structure
20
+ mkdirSync(TOOL_DIR, { recursive: true });
21
+ mkdirSync(join(TOOL_DIR, "src"), { recursive: true });
22
+
23
+ // Create sample tool files
24
+ writeFileSync(
25
+ join(TOOL_DIR, "SKILL.md"),
26
+ `---
27
+ name: test/sample-tool
28
+ version: 1.0.0
29
+ description: A sample tool for testing
30
+ ---
31
+
32
+ # Sample Tool
33
+
34
+ This is a test tool.
35
+ `
36
+ );
37
+
38
+ writeFileSync(
39
+ join(TOOL_DIR, "src", "main.py"),
40
+ `#!/usr/bin/env python3
41
+ def main():
42
+ print("Hello, World!")
43
+
44
+ if __name__ == "__main__":
45
+ main()
46
+ `
47
+ );
48
+
49
+ writeFileSync(join(TOOL_DIR, "requirements.txt"), "requests>=2.28.0\n");
50
+
51
+ // Create files that should be ignored
52
+ writeFileSync(join(TOOL_DIR, ".gitignore"), "*.pyc\n__pycache__/\n");
53
+ writeFileSync(join(TOOL_DIR, ".DS_Store"), "");
54
+ });
55
+
56
+ afterAll(() => {
57
+ // Clean up test files
58
+ if (existsSync(TEST_DIR)) {
59
+ rmSync(TEST_DIR, { recursive: true, force: true });
60
+ }
61
+ });
62
+
63
+ describe("createChecksumManifest", () => {
64
+ test("creates manifest with correct structure", async () => {
65
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
66
+
67
+ expect(manifest.version).toBe(MANIFEST_VERSION);
68
+ expect(manifest.tool.name).toBe("test/sample-tool");
69
+ expect(manifest.tool.version).toBe("1.0.0");
70
+ expect(manifest.files).toBeArray();
71
+ expect(manifest.manifestHash).toBeDefined();
72
+ expect(manifest.manifestHash.algorithm).toBe("sha256");
73
+ expect(manifest.manifestHash.digest).toHaveLength(64); // SHA-256 hex
74
+ });
75
+
76
+ test("includes expected files", async () => {
77
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
78
+
79
+ const filePaths = manifest.files.map((f) => f.path);
80
+
81
+ expect(filePaths).toContain("SKILL.md");
82
+ expect(filePaths).toContain("src/main.py");
83
+ expect(filePaths).toContain("requirements.txt");
84
+ });
85
+
86
+ test("excludes ignored files", async () => {
87
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
88
+
89
+ const filePaths = manifest.files.map((f) => f.path);
90
+
91
+ // Should not include .gitignore or .DS_Store
92
+ expect(filePaths).not.toContain(".gitignore");
93
+ expect(filePaths).not.toContain(".DS_Store");
94
+ });
95
+
96
+ test("files are sorted by path", async () => {
97
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
98
+
99
+ const paths = manifest.files.map((f) => f.path);
100
+ const sortedPaths = [...paths].sort();
101
+
102
+ expect(paths).toEqual(sortedPaths);
103
+ });
104
+
105
+ test("each file has correct hash format", async () => {
106
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
107
+
108
+ for (const file of manifest.files) {
109
+ expect(file.path).toBeString();
110
+ expect(file.sha256).toHaveLength(64); // SHA-256 hex
111
+ expect(file.sha256).toMatch(/^[a-f0-9]{64}$/);
112
+ expect(file.size).toBeNumber();
113
+ expect(file.size).toBeGreaterThan(0);
114
+ }
115
+ });
116
+
117
+ test("calls progress callback for each file", async () => {
118
+ const processedFiles: string[] = [];
119
+
120
+ await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0", {
121
+ onProgress: (file) => processedFiles.push(file),
122
+ });
123
+
124
+ expect(processedFiles.length).toBeGreaterThan(0);
125
+ expect(processedFiles).toContain("SKILL.md");
126
+ });
127
+
128
+ test("respects custom ignore patterns", async () => {
129
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0", {
130
+ ignorePatterns: ["requirements.txt"],
131
+ });
132
+
133
+ const filePaths = manifest.files.map((f) => f.path);
134
+
135
+ expect(filePaths).not.toContain("requirements.txt");
136
+ expect(filePaths).toContain("SKILL.md"); // Should still include other files
137
+ });
138
+
139
+ test("produces deterministic output", async () => {
140
+ const manifest1 = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
141
+
142
+ const manifest2 = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
143
+
144
+ expect(manifest1.manifestHash.digest).toBe(manifest2.manifestHash.digest);
145
+ expect(manifest1.files).toEqual(manifest2.files);
146
+ });
147
+ });
148
+
149
+ describe("computeManifestHash", () => {
150
+ test("computes hash excluding manifestHash field", async () => {
151
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
152
+
153
+ const hash = computeManifestHash(manifest);
154
+
155
+ expect(hash.algorithm).toBe("sha256");
156
+ expect(hash.digest).toHaveLength(64);
157
+ expect(hash.digest).toBe(manifest.manifestHash.digest);
158
+ });
159
+
160
+ test("produces different hashes for different content", async () => {
161
+ const manifest1 = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
162
+
163
+ const manifest2 = await createChecksumManifest(
164
+ TOOL_DIR,
165
+ "test/sample-tool",
166
+ "2.0.0" // Different version
167
+ );
168
+
169
+ expect(manifest1.manifestHash.digest).not.toBe(manifest2.manifestHash.digest);
170
+ });
171
+
172
+ test("is deterministic", async () => {
173
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
174
+
175
+ const hash1 = computeManifestHash(manifest);
176
+ const hash2 = computeManifestHash(manifest);
177
+
178
+ expect(hash1.digest).toBe(hash2.digest);
179
+ });
180
+ });
181
+
182
+ describe("verifyChecksumManifest", () => {
183
+ test("returns valid for matching directory", async () => {
184
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
185
+
186
+ const result = await verifyChecksumManifest(TOOL_DIR, manifest);
187
+
188
+ expect(result.valid).toBe(true);
189
+ expect(result.errors).toBeUndefined();
190
+ expect(result.missingFiles).toBeUndefined();
191
+ expect(result.modifiedFiles).toBeUndefined();
192
+ expect(result.extraFiles).toBeUndefined();
193
+ });
194
+
195
+ test("detects modified files", async () => {
196
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
197
+
198
+ // Modify a file
199
+ const originalContent = "requests>=2.28.0\n";
200
+ writeFileSync(join(TOOL_DIR, "requirements.txt"), "modified content\n");
201
+
202
+ try {
203
+ const result = await verifyChecksumManifest(TOOL_DIR, manifest);
204
+
205
+ expect(result.valid).toBe(false);
206
+ expect(result.modifiedFiles).toContain("requirements.txt");
207
+ expect(result.errors).toBeDefined();
208
+ expect(result.errors?.some((e) => e.includes("Modified file"))).toBe(true);
209
+ } finally {
210
+ // Restore original content
211
+ writeFileSync(join(TOOL_DIR, "requirements.txt"), originalContent);
212
+ }
213
+ });
214
+
215
+ test("detects missing files", async () => {
216
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
217
+
218
+ // Remove a file
219
+ const filePath = join(TOOL_DIR, "requirements.txt");
220
+ const originalContent = "requests>=2.28.0\n";
221
+ rmSync(filePath);
222
+
223
+ try {
224
+ const result = await verifyChecksumManifest(TOOL_DIR, manifest);
225
+
226
+ expect(result.valid).toBe(false);
227
+ expect(result.missingFiles).toContain("requirements.txt");
228
+ expect(result.errors).toBeDefined();
229
+ } finally {
230
+ // Restore file
231
+ writeFileSync(filePath, originalContent);
232
+ }
233
+ });
234
+
235
+ test("detects extra files", async () => {
236
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
237
+
238
+ // Add a new file
239
+ const newFilePath = join(TOOL_DIR, "extra-file.txt");
240
+ writeFileSync(newFilePath, "extra content");
241
+
242
+ try {
243
+ const result = await verifyChecksumManifest(TOOL_DIR, manifest);
244
+
245
+ expect(result.valid).toBe(false);
246
+ expect(result.extraFiles).toContain("extra-file.txt");
247
+ expect(result.errors).toBeDefined();
248
+ } finally {
249
+ // Remove extra file
250
+ rmSync(newFilePath);
251
+ }
252
+ });
253
+
254
+ test("detects corrupted manifest hash", async () => {
255
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
256
+
257
+ // Corrupt the manifest hash
258
+ const corruptedManifest: ChecksumManifest = {
259
+ ...manifest,
260
+ manifestHash: {
261
+ algorithm: "sha256",
262
+ digest: "0".repeat(64), // Invalid hash
263
+ },
264
+ };
265
+
266
+ const result = await verifyChecksumManifest(TOOL_DIR, corruptedManifest);
267
+
268
+ expect(result.valid).toBe(false);
269
+ expect(result.errors?.some((e) => e.includes("Manifest hash mismatch"))).toBe(true);
270
+ });
271
+ });
272
+
273
+ describe("parseChecksumManifest", () => {
274
+ test("parses valid manifest JSON", async () => {
275
+ const original = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
276
+
277
+ const json = serializeChecksumManifest(original);
278
+ const parsed = parseChecksumManifest(json);
279
+
280
+ expect(parsed.version).toBe(original.version);
281
+ expect(parsed.tool).toEqual(original.tool);
282
+ expect(parsed.files).toEqual(original.files);
283
+ expect(parsed.manifestHash).toEqual(original.manifestHash);
284
+ });
285
+
286
+ test("throws on invalid version", () => {
287
+ const invalidJson = JSON.stringify({
288
+ version: "99.0",
289
+ tool: { name: "test", version: "1.0.0" },
290
+ files: [],
291
+ manifestHash: { algorithm: "sha256", digest: "abc" },
292
+ });
293
+
294
+ expect(() => parseChecksumManifest(invalidJson)).toThrow("Invalid manifest version");
295
+ });
296
+
297
+ test("throws on missing tool info", () => {
298
+ const invalidJson = JSON.stringify({
299
+ version: "1.0",
300
+ files: [],
301
+ manifestHash: { algorithm: "sha256", digest: "abc" },
302
+ });
303
+
304
+ expect(() => parseChecksumManifest(invalidJson)).toThrow("missing tool name");
305
+ });
306
+
307
+ test("throws on invalid files array", () => {
308
+ const invalidJson = JSON.stringify({
309
+ version: "1.0",
310
+ tool: { name: "test", version: "1.0.0" },
311
+ files: "not an array",
312
+ manifestHash: { algorithm: "sha256", digest: "abc" },
313
+ });
314
+
315
+ expect(() => parseChecksumManifest(invalidJson)).toThrow("files must be an array");
316
+ });
317
+
318
+ test("throws on missing manifestHash", () => {
319
+ const invalidJson = JSON.stringify({
320
+ version: "1.0",
321
+ tool: { name: "test", version: "1.0.0" },
322
+ files: [],
323
+ });
324
+
325
+ expect(() => parseChecksumManifest(invalidJson)).toThrow("missing manifestHash");
326
+ });
327
+ });
328
+
329
+ describe("serializeChecksumManifest", () => {
330
+ test("produces valid JSON", async () => {
331
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
332
+
333
+ const json = serializeChecksumManifest(manifest);
334
+
335
+ expect(() => JSON.parse(json)).not.toThrow();
336
+ });
337
+
338
+ test("produces pretty-printed output", async () => {
339
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
340
+
341
+ const json = serializeChecksumManifest(manifest);
342
+
343
+ // Should contain newlines (pretty-printed)
344
+ expect(json).toContain("\n");
345
+ expect(json).toContain(" "); // 2-space indent
346
+ });
347
+
348
+ test("round-trips correctly", async () => {
349
+ const original = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
350
+
351
+ const json = serializeChecksumManifest(original);
352
+ const parsed = parseChecksumManifest(json);
353
+
354
+ expect(parsed).toEqual(original);
355
+ });
356
+ });
357
+
358
+ describe("cross-platform path handling", () => {
359
+ test("uses forward slashes in file paths", async () => {
360
+ const manifest = await createChecksumManifest(TOOL_DIR, "test/sample-tool", "1.0.0");
361
+
362
+ for (const file of manifest.files) {
363
+ expect(file.path).not.toContain("\\");
364
+ // Should use forward slashes even on Windows
365
+ if (file.path.includes("/")) {
366
+ expect(file.path).toMatch(/^[^\\]+$/);
367
+ }
368
+ }
369
+ });
370
+ });
371
+ });