@enactprotocol/shared 1.2.11 → 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 (134) hide show
  1. package/README.md +44 -0
  2. package/package.json +16 -58
  3. package/src/config.ts +476 -0
  4. package/src/constants.ts +36 -0
  5. package/src/execution/command.ts +314 -0
  6. package/src/execution/index.ts +73 -0
  7. package/src/execution/runtime.ts +308 -0
  8. package/src/execution/types.ts +379 -0
  9. package/src/execution/validation.ts +508 -0
  10. package/src/index.ts +237 -30
  11. package/src/manifest/index.ts +36 -0
  12. package/src/manifest/loader.ts +187 -0
  13. package/src/manifest/parser.ts +173 -0
  14. package/src/manifest/validator.ts +309 -0
  15. package/src/paths.ts +108 -0
  16. package/src/registry.ts +219 -0
  17. package/src/resolver.ts +345 -0
  18. package/src/types/index.ts +30 -0
  19. package/src/types/manifest.ts +255 -0
  20. package/src/types.ts +5 -188
  21. package/src/utils/fs.ts +281 -0
  22. package/src/utils/logger.ts +270 -59
  23. package/src/utils/version.ts +304 -36
  24. package/tests/config.test.ts +515 -0
  25. package/tests/execution/command.test.ts +317 -0
  26. package/tests/execution/validation.test.ts +384 -0
  27. package/tests/fixtures/invalid-tool.yaml +4 -0
  28. package/tests/fixtures/valid-tool.md +62 -0
  29. package/tests/fixtures/valid-tool.yaml +40 -0
  30. package/tests/index.test.ts +8 -0
  31. package/tests/manifest/loader.test.ts +291 -0
  32. package/tests/manifest/parser.test.ts +345 -0
  33. package/tests/manifest/validator.test.ts +394 -0
  34. package/tests/manifest-types.test.ts +358 -0
  35. package/tests/paths.test.ts +153 -0
  36. package/tests/registry.test.ts +231 -0
  37. package/tests/resolver.test.ts +272 -0
  38. package/tests/utils/fs.test.ts +388 -0
  39. package/tests/utils/logger.test.ts +480 -0
  40. package/tests/utils/version.test.ts +390 -0
  41. package/tsconfig.json +12 -0
  42. package/tsconfig.tsbuildinfo +1 -0
  43. package/dist/LocalToolResolver.d.ts +0 -84
  44. package/dist/LocalToolResolver.js +0 -353
  45. package/dist/api/enact-api.d.ts +0 -130
  46. package/dist/api/enact-api.js +0 -428
  47. package/dist/api/index.d.ts +0 -2
  48. package/dist/api/index.js +0 -2
  49. package/dist/api/types.d.ts +0 -103
  50. package/dist/api/types.js +0 -1
  51. package/dist/constants.d.ts +0 -7
  52. package/dist/constants.js +0 -10
  53. package/dist/core/DaggerExecutionProvider.d.ts +0 -169
  54. package/dist/core/DaggerExecutionProvider.js +0 -1029
  55. package/dist/core/DirectExecutionProvider.d.ts +0 -23
  56. package/dist/core/DirectExecutionProvider.js +0 -406
  57. package/dist/core/EnactCore.d.ts +0 -162
  58. package/dist/core/EnactCore.js +0 -597
  59. package/dist/core/NativeExecutionProvider.d.ts +0 -9
  60. package/dist/core/NativeExecutionProvider.js +0 -16
  61. package/dist/core/index.d.ts +0 -3
  62. package/dist/core/index.js +0 -3
  63. package/dist/exec/index.d.ts +0 -3
  64. package/dist/exec/index.js +0 -3
  65. package/dist/exec/logger.d.ts +0 -11
  66. package/dist/exec/logger.js +0 -57
  67. package/dist/exec/validate.d.ts +0 -5
  68. package/dist/exec/validate.js +0 -167
  69. package/dist/index.d.ts +0 -21
  70. package/dist/index.js +0 -25
  71. package/dist/lib/enact-direct.d.ts +0 -150
  72. package/dist/lib/enact-direct.js +0 -159
  73. package/dist/lib/index.d.ts +0 -1
  74. package/dist/lib/index.js +0 -1
  75. package/dist/security/index.d.ts +0 -3
  76. package/dist/security/index.js +0 -3
  77. package/dist/security/security.d.ts +0 -23
  78. package/dist/security/security.js +0 -137
  79. package/dist/security/sign.d.ts +0 -103
  80. package/dist/security/sign.js +0 -666
  81. package/dist/security/verification-enforcer.d.ts +0 -53
  82. package/dist/security/verification-enforcer.js +0 -204
  83. package/dist/services/McpCoreService.d.ts +0 -98
  84. package/dist/services/McpCoreService.js +0 -124
  85. package/dist/services/index.d.ts +0 -1
  86. package/dist/services/index.js +0 -1
  87. package/dist/types.d.ts +0 -132
  88. package/dist/types.js +0 -3
  89. package/dist/utils/config.d.ts +0 -111
  90. package/dist/utils/config.js +0 -342
  91. package/dist/utils/env-loader.d.ts +0 -54
  92. package/dist/utils/env-loader.js +0 -270
  93. package/dist/utils/help.d.ts +0 -36
  94. package/dist/utils/help.js +0 -248
  95. package/dist/utils/index.d.ts +0 -7
  96. package/dist/utils/index.js +0 -7
  97. package/dist/utils/logger.d.ts +0 -35
  98. package/dist/utils/logger.js +0 -75
  99. package/dist/utils/silent-monitor.d.ts +0 -67
  100. package/dist/utils/silent-monitor.js +0 -242
  101. package/dist/utils/timeout.d.ts +0 -5
  102. package/dist/utils/timeout.js +0 -23
  103. package/dist/utils/version.d.ts +0 -4
  104. package/dist/utils/version.js +0 -35
  105. package/dist/web/env-manager-server.d.ts +0 -29
  106. package/dist/web/env-manager-server.js +0 -367
  107. package/dist/web/index.d.ts +0 -1
  108. package/dist/web/index.js +0 -1
  109. package/src/LocalToolResolver.ts +0 -424
  110. package/src/api/enact-api.ts +0 -604
  111. package/src/api/index.ts +0 -2
  112. package/src/api/types.ts +0 -114
  113. package/src/core/DaggerExecutionProvider.ts +0 -1357
  114. package/src/core/DirectExecutionProvider.ts +0 -484
  115. package/src/core/EnactCore.ts +0 -847
  116. package/src/core/index.ts +0 -3
  117. package/src/exec/index.ts +0 -3
  118. package/src/exec/logger.ts +0 -63
  119. package/src/exec/validate.ts +0 -238
  120. package/src/lib/enact-direct.ts +0 -254
  121. package/src/lib/index.ts +0 -1
  122. package/src/services/McpCoreService.ts +0 -201
  123. package/src/services/index.ts +0 -1
  124. package/src/utils/config.ts +0 -438
  125. package/src/utils/env-loader.ts +0 -370
  126. package/src/utils/help.ts +0 -257
  127. package/src/utils/index.ts +0 -7
  128. package/src/utils/silent-monitor.ts +0 -328
  129. package/src/utils/timeout.ts +0 -26
  130. package/src/web/env-manager-server.ts +0 -465
  131. package/src/web/index.ts +0 -1
  132. package/src/web/static/app.js +0 -663
  133. package/src/web/static/index.html +0 -117
  134. package/src/web/static/style.css +0 -291
@@ -0,0 +1,515 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test } from "bun:test";
2
+ import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { homedir } from "node:os";
4
+ import { join } from "node:path";
5
+ import yaml from "js-yaml";
6
+ import {
7
+ DEFAULT_CONFIG,
8
+ type EnactConfig,
9
+ // Legacy aliases for backwards compatibility
10
+ addTrustedAuditor,
11
+ addTrustedIdentity,
12
+ configExists,
13
+ emailToProviderIdentity,
14
+ getConfigValue,
15
+ getMinimumAttestations,
16
+ getTrustPolicy,
17
+ getTrustedAuditors,
18
+ getTrustedIdentities,
19
+ isAuditorTrusted,
20
+ isIdentityTrusted,
21
+ loadConfig,
22
+ removeTrustedAuditor,
23
+ removeTrustedIdentity,
24
+ resetConfig,
25
+ saveConfig,
26
+ setConfigValue,
27
+ } from "../src/config";
28
+
29
+ // Use a test-specific home directory to avoid affecting real config
30
+ const TEST_HOME = join(import.meta.dir, "fixtures", "config-test-home");
31
+
32
+ describe("configuration manager", () => {
33
+ beforeAll(() => {
34
+ // Mock homedir to use test directory
35
+ // We need to mock the module-level function
36
+ // For this test, we'll manipulate the files directly and test the logic
37
+ });
38
+
39
+ beforeEach(() => {
40
+ // Clean up test directory before each test
41
+ if (existsSync(TEST_HOME)) {
42
+ rmSync(TEST_HOME, { recursive: true, force: true });
43
+ }
44
+ });
45
+
46
+ afterAll(() => {
47
+ // Clean up test directory
48
+ if (existsSync(TEST_HOME)) {
49
+ rmSync(TEST_HOME, { recursive: true, force: true });
50
+ }
51
+ });
52
+
53
+ describe("DEFAULT_CONFIG", () => {
54
+ test("has all required top-level keys", () => {
55
+ expect(DEFAULT_CONFIG.version).toBeDefined();
56
+ expect(DEFAULT_CONFIG.trust).toBeDefined();
57
+ expect(DEFAULT_CONFIG.cache).toBeDefined();
58
+ expect(DEFAULT_CONFIG.execution).toBeDefined();
59
+ expect(DEFAULT_CONFIG.registry).toBeDefined();
60
+ });
61
+
62
+ test("has sensible default values", () => {
63
+ expect(DEFAULT_CONFIG.trust?.policy).toBe("prompt");
64
+ expect(DEFAULT_CONFIG.trust?.minimum_attestations).toBe(1);
65
+ expect(DEFAULT_CONFIG.cache?.maxSizeMb).toBe(1024);
66
+ expect(DEFAULT_CONFIG.execution?.defaultTimeout).toBe("30s");
67
+ expect(DEFAULT_CONFIG.registry?.url).toBe("https://enact.tools");
68
+ });
69
+ });
70
+
71
+ describe("loadConfig", () => {
72
+ test("returns default config if file does not exist", () => {
73
+ // The actual homedir doesn't have config, or we test with mock
74
+ const config = loadConfig();
75
+ expect(config).toBeDefined();
76
+ expect(config.version).toBeDefined();
77
+ });
78
+
79
+ test("returns default config structure", () => {
80
+ const config = loadConfig();
81
+ expect(config.trust).toBeDefined();
82
+ expect(config.cache).toBeDefined();
83
+ expect(config.execution).toBeDefined();
84
+ expect(config.registry).toBeDefined();
85
+ });
86
+ });
87
+
88
+ describe("saveConfig", () => {
89
+ test("creates ~/.enact/ directory if needed", () => {
90
+ const config: EnactConfig = { ...DEFAULT_CONFIG };
91
+
92
+ // This will create in the real home directory
93
+ // For integration test purposes
94
+ saveConfig(config);
95
+
96
+ // Verify config was saved
97
+ expect(configExists()).toBe(true);
98
+ });
99
+
100
+ test("writes valid YAML", () => {
101
+ const config: EnactConfig = {
102
+ version: "1.0.0",
103
+ trust: {
104
+ auditors: ["github:alice"],
105
+ policy: "require_attestation",
106
+ },
107
+ cache: {
108
+ maxSizeMb: 2048,
109
+ },
110
+ };
111
+
112
+ saveConfig(config);
113
+
114
+ // Reload and verify
115
+ const loaded = loadConfig();
116
+ expect(loaded.trust?.auditors).toContain("github:alice");
117
+ expect(loaded.trust?.policy).toBe("require_attestation");
118
+ expect(loaded.cache?.maxSizeMb).toBe(2048);
119
+ });
120
+
121
+ test("preserves all config fields", () => {
122
+ const config: EnactConfig = {
123
+ version: "2.0.0",
124
+ trust: {
125
+ auditors: ["github:aud1", "google:aud2"],
126
+ policy: "require_attestation",
127
+ minimum_attestations: 2,
128
+ },
129
+ cache: {
130
+ maxSizeMb: 512,
131
+ ttlSeconds: 3600,
132
+ },
133
+ execution: {
134
+ defaultTimeout: "1m",
135
+ verbose: true,
136
+ },
137
+ registry: {
138
+ url: "https://custom.registry.com",
139
+ authTokenRef: "token-ref",
140
+ },
141
+ };
142
+
143
+ saveConfig(config);
144
+ const loaded = loadConfig();
145
+
146
+ expect(loaded.version).toBe("2.0.0");
147
+ expect(loaded.trust?.auditors).toEqual(["github:aud1", "google:aud2"]);
148
+ expect(loaded.trust?.policy).toBe("require_attestation");
149
+ expect(loaded.trust?.minimum_attestations).toBe(2);
150
+ expect(loaded.cache?.maxSizeMb).toBe(512);
151
+ expect(loaded.cache?.ttlSeconds).toBe(3600);
152
+ expect(loaded.execution?.defaultTimeout).toBe("1m");
153
+ expect(loaded.execution?.verbose).toBe(true);
154
+ expect(loaded.registry?.url).toBe("https://custom.registry.com");
155
+ expect(loaded.registry?.authTokenRef).toBe("token-ref");
156
+ });
157
+ });
158
+
159
+ describe("getConfigValue", () => {
160
+ test("returns default for missing keys", () => {
161
+ const value = getConfigValue("nonexistent.key", "default");
162
+ expect(value).toBe("default");
163
+ });
164
+
165
+ test("returns value for existing top-level key", () => {
166
+ saveConfig({ ...DEFAULT_CONFIG, version: "test-version" });
167
+ const value = getConfigValue("version", "fallback");
168
+ expect(value).toBe("test-version");
169
+ });
170
+
171
+ test("returns value for nested key", () => {
172
+ saveConfig({
173
+ ...DEFAULT_CONFIG,
174
+ trust: { policy: "require_attestation" },
175
+ });
176
+ const value = getConfigValue("trust.policy", "fallback");
177
+ expect(value).toBe("require_attestation");
178
+ });
179
+
180
+ test("returns value for deeply nested key", () => {
181
+ saveConfig({
182
+ ...DEFAULT_CONFIG,
183
+ cache: { maxSizeMb: 999 },
184
+ });
185
+ const value = getConfigValue("cache.maxSizeMb", 0);
186
+ expect(value).toBe(999);
187
+ });
188
+
189
+ test("returns default for partially missing path", () => {
190
+ saveConfig({ ...DEFAULT_CONFIG });
191
+ const value = getConfigValue("trust.nonexistent.deep", "default");
192
+ expect(value).toBe("default");
193
+ });
194
+ });
195
+
196
+ describe("setConfigValue", () => {
197
+ test("sets top-level value and persists", () => {
198
+ saveConfig({ ...DEFAULT_CONFIG });
199
+ setConfigValue("version", "new-version");
200
+
201
+ const loaded = loadConfig();
202
+ expect(loaded.version).toBe("new-version");
203
+ });
204
+
205
+ test("sets nested value and persists", () => {
206
+ saveConfig({ ...DEFAULT_CONFIG });
207
+ setConfigValue("trust.policy", "allow");
208
+
209
+ const loaded = loadConfig();
210
+ expect(loaded.trust?.policy).toBe("allow");
211
+ });
212
+
213
+ test("creates intermediate objects if needed", () => {
214
+ saveConfig({ ...DEFAULT_CONFIG });
215
+ setConfigValue("cache.newField", "newValue");
216
+
217
+ const value = getConfigValue("cache.newField", "fallback");
218
+ expect(value).toBe("newValue");
219
+ });
220
+
221
+ test("preserves other values when setting", () => {
222
+ saveConfig({
223
+ ...DEFAULT_CONFIG,
224
+ trust: { auditors: ["github:alice"], policy: "prompt" },
225
+ });
226
+
227
+ setConfigValue("trust.policy", "require_attestation");
228
+
229
+ const loaded = loadConfig();
230
+ expect(loaded.trust?.policy).toBe("require_attestation");
231
+ expect(loaded.trust?.auditors).toContain("github:alice");
232
+ });
233
+
234
+ test("throws error for empty key", () => {
235
+ expect(() => setConfigValue("", "value")).toThrow("Invalid configuration key");
236
+ });
237
+ });
238
+
239
+ describe("resetConfig", () => {
240
+ test("resets config to defaults", () => {
241
+ // First, set a custom config
242
+ saveConfig({
243
+ version: "custom",
244
+ trust: { policy: "allow" },
245
+ cache: { maxSizeMb: 1 },
246
+ });
247
+
248
+ // Reset
249
+ resetConfig();
250
+
251
+ // Verify defaults restored
252
+ const loaded = loadConfig();
253
+ expect(loaded.version).toBe(DEFAULT_CONFIG.version);
254
+ expect(loaded.trust?.policy).toBe(DEFAULT_CONFIG.trust?.policy);
255
+ expect(loaded.cache?.maxSizeMb).toBe(DEFAULT_CONFIG.cache?.maxSizeMb);
256
+ });
257
+ });
258
+
259
+ describe("configExists", () => {
260
+ test("returns true after saving config", () => {
261
+ saveConfig({ ...DEFAULT_CONFIG });
262
+ expect(configExists()).toBe(true);
263
+ });
264
+ });
265
+
266
+ describe("YAML format", () => {
267
+ test("config file is valid YAML", () => {
268
+ saveConfig({
269
+ ...DEFAULT_CONFIG,
270
+ trust: { auditors: ["github:test"] },
271
+ });
272
+
273
+ // Read raw file and parse with yaml
274
+ const configPath = join(homedir(), ".enact", "config.yaml");
275
+ const content = readFileSync(configPath, "utf-8");
276
+
277
+ // Should not throw
278
+ const parsed = yaml.load(content);
279
+ expect(parsed).toBeDefined();
280
+ expect(typeof parsed).toBe("object");
281
+ });
282
+
283
+ test("config file has readable formatting", () => {
284
+ saveConfig({
285
+ ...DEFAULT_CONFIG,
286
+ trust: {
287
+ auditors: ["github:alice", "github:bob"],
288
+ policy: "require_attestation",
289
+ },
290
+ });
291
+
292
+ const configPath = join(homedir(), ".enact", "config.yaml");
293
+ const content = readFileSync(configPath, "utf-8");
294
+
295
+ // Should have proper indentation (not be on one line)
296
+ expect(content).toContain("\n");
297
+ expect(content).toContain("trust:");
298
+ expect(content).toContain("auditors:");
299
+ });
300
+ });
301
+
302
+ describe("error handling", () => {
303
+ test("loadConfig handles malformed YAML gracefully", () => {
304
+ // Save valid config first
305
+ saveConfig({ ...DEFAULT_CONFIG });
306
+
307
+ // Corrupt the file
308
+ const configPath = join(homedir(), ".enact", "config.yaml");
309
+ writeFileSync(configPath, "invalid: yaml: [unclosed", "utf-8");
310
+
311
+ // Should return defaults, not throw
312
+ const config = loadConfig();
313
+ expect(config).toBeDefined();
314
+ expect(config.version).toBe(DEFAULT_CONFIG.version);
315
+ });
316
+
317
+ test("loadConfig handles empty file gracefully", () => {
318
+ saveConfig({ ...DEFAULT_CONFIG });
319
+
320
+ const configPath = join(homedir(), ".enact", "config.yaml");
321
+ writeFileSync(configPath, "", "utf-8");
322
+
323
+ const config = loadConfig();
324
+ expect(config).toBeDefined();
325
+ expect(config.version).toBe(DEFAULT_CONFIG.version);
326
+ });
327
+
328
+ test("loadConfig handles null content gracefully", () => {
329
+ saveConfig({ ...DEFAULT_CONFIG });
330
+
331
+ const configPath = join(homedir(), ".enact", "config.yaml");
332
+ writeFileSync(configPath, "null", "utf-8");
333
+
334
+ const config = loadConfig();
335
+ expect(config).toBeDefined();
336
+ });
337
+ });
338
+
339
+ describe("merge behavior", () => {
340
+ test("merges partial config with defaults", () => {
341
+ // Save config with only some fields
342
+ const configPath = join(homedir(), ".enact", "config.yaml");
343
+ mkdirSync(join(homedir(), ".enact"), { recursive: true });
344
+ writeFileSync(configPath, yaml.dump({ trust: { policy: "require_attestation" } }), "utf-8");
345
+
346
+ const loaded = loadConfig();
347
+
348
+ // Custom value should be preserved
349
+ expect(loaded.trust?.policy).toBe("require_attestation");
350
+
351
+ // Default values should be filled in
352
+ expect(loaded.cache?.maxSizeMb).toBe(DEFAULT_CONFIG.cache?.maxSizeMb);
353
+ expect(loaded.execution?.defaultTimeout).toBe(DEFAULT_CONFIG.execution?.defaultTimeout);
354
+ });
355
+ });
356
+
357
+ describe("Trust Management", () => {
358
+ describe("Unified Identity Model", () => {
359
+ test("getTrustedIdentities returns empty array by default", () => {
360
+ resetConfig();
361
+ const identities = getTrustedIdentities();
362
+ expect(identities).toEqual([]);
363
+ });
364
+
365
+ test("addTrustedIdentity adds identity and returns true", () => {
366
+ resetConfig();
367
+ const added = addTrustedIdentity("github:alice");
368
+ expect(added).toBe(true);
369
+
370
+ const identities = getTrustedIdentities();
371
+ expect(identities).toContain("github:alice");
372
+ });
373
+
374
+ test("addTrustedIdentity returns false for duplicate", () => {
375
+ resetConfig();
376
+ addTrustedIdentity("github:alice");
377
+ const added = addTrustedIdentity("github:alice");
378
+ expect(added).toBe(false);
379
+ });
380
+
381
+ test("removeTrustedIdentity removes identity and returns true", () => {
382
+ resetConfig();
383
+ addTrustedIdentity("github:alice");
384
+ const removed = removeTrustedIdentity("github:alice");
385
+ expect(removed).toBe(true);
386
+
387
+ const identities = getTrustedIdentities();
388
+ expect(identities).not.toContain("github:alice");
389
+ });
390
+
391
+ test("removeTrustedIdentity returns false for non-existent identity", () => {
392
+ resetConfig();
393
+ const removed = removeTrustedIdentity("github:nonexistent");
394
+ expect(removed).toBe(false);
395
+ });
396
+
397
+ test("isIdentityTrusted checks exact match", () => {
398
+ resetConfig();
399
+ addTrustedIdentity("github:alice");
400
+ expect(isIdentityTrusted("github:alice")).toBe(true);
401
+ expect(isIdentityTrusted("github:bob")).toBe(false);
402
+ });
403
+
404
+ test("isIdentityTrusted supports wildcard patterns", () => {
405
+ resetConfig();
406
+ addTrustedIdentity("github:my-org/*");
407
+ expect(isIdentityTrusted("github:my-org/alice")).toBe(true);
408
+ expect(isIdentityTrusted("github:my-org/bob")).toBe(true);
409
+ expect(isIdentityTrusted("github:other-org/alice")).toBe(false);
410
+ });
411
+
412
+ test("isIdentityTrusted supports email wildcards", () => {
413
+ resetConfig();
414
+ addTrustedIdentity("*@company.com");
415
+ expect(isIdentityTrusted("alice@company.com")).toBe(true);
416
+ expect(isIdentityTrusted("bob@company.com")).toBe(true);
417
+ expect(isIdentityTrusted("alice@other.com")).toBe(false);
418
+ });
419
+ });
420
+
421
+ describe("Legacy Aliases (Backwards Compatibility)", () => {
422
+ test("getTrustedAuditors works as alias", () => {
423
+ resetConfig();
424
+ const auditors = getTrustedAuditors();
425
+ expect(auditors).toEqual([]);
426
+ });
427
+
428
+ test("addTrustedAuditor works as alias", () => {
429
+ resetConfig();
430
+ const added = addTrustedAuditor("github:alice");
431
+ expect(added).toBe(true);
432
+
433
+ const auditors = getTrustedAuditors();
434
+ expect(auditors).toContain("github:alice");
435
+ });
436
+
437
+ test("removeTrustedAuditor works as alias", () => {
438
+ resetConfig();
439
+ addTrustedAuditor("github:alice");
440
+ const removed = removeTrustedAuditor("github:alice");
441
+ expect(removed).toBe(true);
442
+ });
443
+
444
+ test("isAuditorTrusted works as alias", () => {
445
+ resetConfig();
446
+ addTrustedAuditor("github:alice");
447
+ expect(isAuditorTrusted("github:alice")).toBe(true);
448
+ });
449
+ });
450
+
451
+ describe("Trust Policy", () => {
452
+ test("getTrustPolicy returns default policy", () => {
453
+ resetConfig();
454
+ const policy = getTrustPolicy();
455
+ expect(policy).toBe("prompt");
456
+ });
457
+
458
+ test("getTrustPolicy returns configured policy", () => {
459
+ saveConfig({
460
+ ...DEFAULT_CONFIG,
461
+ trust: { policy: "require_attestation" },
462
+ });
463
+ const policy = getTrustPolicy();
464
+ expect(policy).toBe("require_attestation");
465
+ });
466
+
467
+ test("getMinimumAttestations returns default", () => {
468
+ resetConfig();
469
+ const min = getMinimumAttestations();
470
+ expect(min).toBe(1);
471
+ });
472
+
473
+ test("getMinimumAttestations returns configured value", () => {
474
+ saveConfig({
475
+ ...DEFAULT_CONFIG,
476
+ trust: { minimum_attestations: 3 },
477
+ });
478
+ const min = getMinimumAttestations();
479
+ expect(min).toBe(3);
480
+ });
481
+ });
482
+
483
+ describe("emailToProviderIdentity", () => {
484
+ test("converts GitHub noreply email", () => {
485
+ const result = emailToProviderIdentity("alice@users.noreply.github.com");
486
+ expect(result).toBe("github:alice");
487
+ });
488
+
489
+ test("converts GitHub email", () => {
490
+ const result = emailToProviderIdentity("alice@github.com");
491
+ expect(result).toBe("github:alice");
492
+ });
493
+
494
+ test("converts Google email", () => {
495
+ const result = emailToProviderIdentity("alice@gmail.com");
496
+ expect(result).toBe("google:alice");
497
+ });
498
+
499
+ test("converts Microsoft email", () => {
500
+ const result = emailToProviderIdentity("alice@outlook.com");
501
+ expect(result).toBe("microsoft:alice");
502
+ });
503
+
504
+ test("converts GitHub workflow URL", () => {
505
+ const result = emailToProviderIdentity("https://github.com/my-org/my-workflow");
506
+ expect(result).toBe("github:my-org/my-workflow");
507
+ });
508
+
509
+ test("returns email as-is for unknown providers", () => {
510
+ const result = emailToProviderIdentity("alice@unknown.com");
511
+ expect(result).toBe("alice@unknown.com");
512
+ });
513
+ });
514
+ });
515
+ });