@indigoai-us/hq-cli 5.75.0 → 5.76.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 (39) hide show
  1. package/dist/commands/mcp-registration.d.ts +4 -5
  2. package/dist/commands/mcp-registration.js +5 -4
  3. package/dist/commands/outposts.d.ts +20 -4
  4. package/dist/commands/outposts.js +77 -8
  5. package/dist/commands/pack-install.d.ts +14 -17
  6. package/dist/commands/pack-install.js +53 -29
  7. package/dist/commands/pkg-install.js +3 -1
  8. package/dist/commands/run.d.ts +2 -0
  9. package/dist/commands/run.js +9 -3
  10. package/dist/commands/secrets.js +189 -87
  11. package/dist/run/hq-plugin.js +94 -31
  12. package/dist/utils/sandbox-runner-client.d.ts +1 -0
  13. package/dist/utils/sandbox-runner-client.js +1 -0
  14. package/dist/utils/secrets-cache.d.ts +4 -5
  15. package/dist/utils/secrets-cache.js +5 -8
  16. package/package.json +3 -2
  17. package/pnpm-workspace.yaml +2 -0
  18. package/src/commands/mcp-registration.ts +9 -9
  19. package/src/commands/outposts.test.ts +118 -24
  20. package/src/commands/outposts.ts +197 -43
  21. package/src/commands/pack-install-secret-authorization.test.ts +115 -0
  22. package/src/commands/pack-install.test.ts +5 -1
  23. package/src/commands/pack-install.ts +67 -29
  24. package/src/commands/pkg-install.ts +3 -1
  25. package/src/commands/run.test.ts +45 -0
  26. package/src/commands/run.ts +20 -4
  27. package/src/commands/secrets.test.ts +366 -25
  28. package/src/commands/secrets.ts +222 -96
  29. package/src/run/hq-plugin.test.ts +186 -10
  30. package/src/run/hq-plugin.ts +102 -32
  31. package/src/utils/__fixtures__/scan-packages.generated-block.sh +23 -0
  32. package/src/utils/pack-contributions.test.ts +90 -31
  33. package/src/utils/sandbox-runner-client.test.ts +28 -0
  34. package/src/utils/sandbox-runner-client.ts +2 -0
  35. package/src/utils/secrets-cache.ts +5 -8
  36. package/test/commands/signals.test.ts +2 -2
  37. package/test/commands/sources.test.ts +2 -2
  38. package/test/helpers/vault-service-mock.ts +76 -17
  39. package/test/sources-signals/smoke.test.ts +2 -2
@@ -42,10 +42,9 @@ vi.mock("../utils/secrets-cache.js", async (importOriginal) => {
42
42
  const original = (await importOriginal()) as Record<string, unknown>;
43
43
  return {
44
44
  ...original,
45
- // Default: cold cache (every key is a miss → forces a batch-load request).
46
- // Tests that exercise the warm path override readCache per-case.
47
45
  readCache: vi.fn(() => null),
48
46
  writeCache: vi.fn(() => undefined),
47
+ removeCacheEntry: vi.fn(() => undefined),
49
48
  };
50
49
  });
51
50
 
@@ -64,7 +63,11 @@ import {
64
63
  import { spawn } from "node:child_process";
65
64
  import { ensureCognitoToken } from "../utils/cognito-session.js";
66
65
  import { getEntityUid, vaultApiFetch } from "../utils/vault-api.js";
67
- import { readCache, writeCache } from "../utils/secrets-cache.js";
66
+ import {
67
+ readCache,
68
+ removeCacheEntry,
69
+ writeCache,
70
+ } from "../utils/secrets-cache.js";
68
71
  import { SandboxRunnerClient } from "../utils/sandbox-runner-client.js";
69
72
 
70
73
  let logSpy: MockInstance<typeof console.log>;
@@ -377,12 +380,69 @@ describe("secrets sandbox", () => {
377
380
  expect(stdoutSpy).not.toHaveBeenCalled();
378
381
  });
379
382
 
380
- it("exits non-zero when the sandbox command fails", async () => {
383
+ it("renders the server error without inventing an exit code when sandbox execution fails", async () => {
384
+ pollJobSpy.mockResolvedValueOnce({
385
+ jobId: "job_123",
386
+ status: "failed",
387
+ error: "Cloudflare worker timed out with API_KEY=secret-value",
388
+ success: false,
389
+ });
390
+
391
+ const program = buildProgram();
392
+ await expect(
393
+ program.parseAsync([
394
+ "node",
395
+ "hq",
396
+ "secrets",
397
+ "sandbox",
398
+ "--only",
399
+ "API_KEY",
400
+ "--",
401
+ "my-skill",
402
+ ]),
403
+ ).rejects.toThrow("__exit__");
404
+
405
+ const renderedError = errSpy.mock.calls.flat().join(" ");
406
+ expect(renderedError).toContain("Cloudflare worker timed out with API_KEY=[REDACTED]");
407
+ // The redacted form appearing is not enough — the raw value must be gone.
408
+ expect(renderedError).not.toContain("secret-value");
409
+ expect(renderedError).not.toMatch(/exit code 1/i);
410
+ expect(exitSpy).toHaveBeenCalledWith(1);
411
+ });
412
+
413
+ it("still explains a failure that carries neither an exit code nor a reason", async () => {
414
+ pollJobSpy.mockResolvedValueOnce({
415
+ jobId: "job_123",
416
+ status: "failed",
417
+ success: false,
418
+ });
419
+
420
+ const program = buildProgram();
421
+ await expect(
422
+ program.parseAsync([
423
+ "node",
424
+ "hq",
425
+ "secrets",
426
+ "sandbox",
427
+ "--only",
428
+ "API_KEY",
429
+ "--",
430
+ "my-skill",
431
+ ]),
432
+ ).rejects.toThrow("__exit__");
433
+
434
+ const renderedError = errSpy.mock.calls.flat().join(" ");
435
+ expect(renderedError).toMatch(/sandbox execution failed/i);
436
+ expect(renderedError).not.toMatch(/exit code 1/i);
437
+ expect(exitSpy).toHaveBeenCalledWith(1);
438
+ });
439
+
440
+ it("preserves the real exit code when the sandbox command exits non-zero", async () => {
381
441
  pollJobSpy.mockResolvedValueOnce({
382
442
  jobId: "job_123",
383
443
  status: "failed",
384
444
  output: "boom\n",
385
- exitCode: 7,
445
+ exitCode: 2,
386
446
  success: false,
387
447
  });
388
448
 
@@ -401,8 +461,8 @@ describe("secrets sandbox", () => {
401
461
  ).rejects.toThrow("__exit__");
402
462
 
403
463
  expect(stdoutSpy).toHaveBeenCalledWith("boom\n");
404
- expect(errSpy.mock.calls.flat().join(" ")).toMatch(/exit code 7/i);
405
- expect(exitSpy).toHaveBeenCalledWith(7);
464
+ expect(errSpy.mock.calls.flat().join(" ")).toMatch(/exit code 2/i);
465
+ expect(exitSpy).toHaveBeenCalledWith(2);
406
466
  expect(getJobSpy).not.toHaveBeenCalled();
407
467
  });
408
468
  });
@@ -1038,6 +1098,7 @@ describe("secrets exec/env batch-load (HQ-4H)", () => {
1038
1098
  path: "/secrets/prs_alice/load",
1039
1099
  method: "POST",
1040
1100
  body: { names: ["MY_KEY", "OTHER"] },
1101
+ signal: expect.any(AbortSignal),
1041
1102
  });
1042
1103
  // The crux: the capturing GET route is never touched.
1043
1104
  expect(vaultApiFetch).not.toHaveBeenCalledWith(CAPTURING_GET);
@@ -1045,11 +1106,17 @@ describe("secrets exec/env batch-load (HQ-4H)", () => {
1045
1106
  expect(writeCache).toHaveBeenCalledWith("prs_alice", "OTHER", "v2", 300000);
1046
1107
  });
1047
1108
 
1048
- it("controlled secrets with cacheTtlMs=0 skip the disk cache", async () => {
1109
+ it("controlled policy metadata overrides a malformed positive cache TTL", async () => {
1049
1110
  vi.mocked(vaultApiFetch).mockResolvedValueOnce(
1050
1111
  jsonRes({
1051
1112
  secrets: [
1052
- { name: "LOCKED", value: "v1", cacheTtlMs: 0 },
1113
+ {
1114
+ name: "LOCKED",
1115
+ value: "v1",
1116
+ tier: "sensitive",
1117
+ scriptLock: { mode: "enforced" },
1118
+ cacheTtlMs: 300000,
1119
+ },
1053
1120
  ],
1054
1121
  errors: [],
1055
1122
  }),
@@ -1061,6 +1128,62 @@ describe("secrets exec/env batch-load (HQ-4H)", () => {
1061
1128
 
1062
1129
  expect(out.get("LOCKED")).toBe("v1");
1063
1130
  expect(writeCache).not.toHaveBeenCalled();
1131
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "LOCKED");
1132
+ });
1133
+
1134
+ it("treats a malformed cache TTL as zero instead of defaulting to a warm cache", async () => {
1135
+ vi.mocked(vaultApiFetch).mockResolvedValueOnce(
1136
+ jsonRes({
1137
+ secrets: [{ name: "LOCKED", value: "v1", cacheTtlMs: "300000" }],
1138
+ errors: [],
1139
+ }),
1140
+ );
1141
+
1142
+ const out = await loadRevealedSecrets("test-token", "prs_alice", [
1143
+ "LOCKED",
1144
+ ]);
1145
+
1146
+ expect(out.get("LOCKED")).toBe("v1");
1147
+ expect(writeCache).not.toHaveBeenCalled();
1148
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "LOCKED");
1149
+ });
1150
+
1151
+ it("evicts a stale cache entry when the server returns metadata without plaintext", async () => {
1152
+ vi.mocked(vaultApiFetch).mockResolvedValueOnce(
1153
+ jsonRes({
1154
+ secrets: [{ name: "LOCKED", cacheTtlMs: 0 }],
1155
+ errors: [],
1156
+ }),
1157
+ );
1158
+
1159
+ await expect(
1160
+ loadRevealedSecrets("test-token", "prs_alice", ["LOCKED"]),
1161
+ ).rejects.toThrow("Secret 'LOCKED' has no value");
1162
+
1163
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "LOCKED");
1164
+ });
1165
+
1166
+ it("evicts every unresolved cache entry before reporting the first batch failure", async () => {
1167
+ vi.mocked(vaultApiFetch).mockResolvedValueOnce(
1168
+ jsonRes({
1169
+ secrets: [{ name: "VALUELESS", cacheTtlMs: 0 }],
1170
+ errors: [
1171
+ { name: "DENIED", code: "forbidden", message: "No read permission" },
1172
+ ],
1173
+ }),
1174
+ );
1175
+
1176
+ await expect(
1177
+ loadRevealedSecrets("test-token", "prs_alice", [
1178
+ "VALUELESS",
1179
+ "DENIED",
1180
+ "OMITTED",
1181
+ ]),
1182
+ ).rejects.toThrow("Secret 'VALUELESS' has no value");
1183
+
1184
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "VALUELESS");
1185
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "DENIED");
1186
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "OMITTED");
1064
1187
  });
1065
1188
 
1066
1189
  it("a missing key fails cleanly WITHOUT hitting the GET-404 capture path", async () => {
@@ -1100,15 +1223,155 @@ describe("secrets exec/env batch-load (HQ-4H)", () => {
1100
1223
  expect(vaultApiFetch).not.toHaveBeenCalledWith(CAPTURING_GET);
1101
1224
  });
1102
1225
 
1103
- it("warm-cache keys cost no request at all", async () => {
1104
- vi.mocked(readCache).mockReturnValue("cached-value");
1226
+ it("revalidates a previously cached secret and evicts it after remote ACL revocation", async () => {
1227
+ vi.mocked(vaultApiFetch)
1228
+ .mockResolvedValueOnce(
1229
+ jsonRes({
1230
+ secrets: [
1231
+ { name: "CACHED", value: "cached-value", cacheTtlMs: 300000 },
1232
+ ],
1233
+ errors: [],
1234
+ }),
1235
+ )
1236
+ .mockResolvedValueOnce(
1237
+ jsonRes({
1238
+ secrets: [],
1239
+ errors: [
1240
+ {
1241
+ name: "CACHED",
1242
+ code: "forbidden",
1243
+ message: "No read permission",
1244
+ },
1245
+ ],
1246
+ }),
1247
+ );
1105
1248
 
1106
- const out = await loadRevealedSecrets("test-token", "prs_alice", [
1249
+ const first = await loadRevealedSecrets("test-token", "prs_alice", [
1107
1250
  "CACHED",
1108
1251
  ]);
1252
+ expect(first.get("CACHED")).toBe("cached-value");
1253
+ expect(writeCache).toHaveBeenCalledWith(
1254
+ "prs_alice",
1255
+ "CACHED",
1256
+ "cached-value",
1257
+ 300000,
1258
+ );
1109
1259
 
1110
- expect(out.get("CACHED")).toBe("cached-value");
1111
- expect(vaultApiFetch).not.toHaveBeenCalled();
1260
+ // Model the encrypted disk entry left by the first call. A pre-fix warm
1261
+ // lookup would return this value without contacting the server.
1262
+ vi.mocked(readCache).mockReturnValue("cached-value");
1263
+
1264
+ await expect(
1265
+ loadRevealedSecrets("test-token", "prs_alice", ["CACHED"]),
1266
+ ).rejects.toThrow("Failed to fetch secret 'CACHED': No read permission");
1267
+
1268
+ expect(vaultApiFetch).toHaveBeenCalledTimes(2);
1269
+ expect(readCache).not.toHaveBeenCalled();
1270
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "CACHED");
1271
+ });
1272
+
1273
+ it("evicts every requested entry when a stale session is rejected", async () => {
1274
+ vi.mocked(vaultApiFetch).mockResolvedValueOnce(
1275
+ jsonRes({ error: "Unauthorized", code: "session_expired" }, 401),
1276
+ );
1277
+
1278
+ await expect(
1279
+ loadRevealedSecrets("stale-token", "prs_alice", ["FIRST", "SECOND"]),
1280
+ ).rejects.toThrow("Unauthorized");
1281
+
1282
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "FIRST");
1283
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "SECOND");
1284
+ });
1285
+
1286
+ it("fails closed and evicts every entry on an offline network failure", async () => {
1287
+ vi.mocked(vaultApiFetch).mockRejectedValueOnce(
1288
+ new TypeError("fetch failed: ENETUNREACH"),
1289
+ );
1290
+
1291
+ await expect(
1292
+ loadRevealedSecrets("test-token", "prs_alice", ["FIRST", "SECOND"]),
1293
+ ).rejects.toThrow("ENETUNREACH");
1294
+
1295
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "FIRST");
1296
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "SECOND");
1297
+ });
1298
+
1299
+ it("fails closed and evicts every entry when authorization times out", async () => {
1300
+ vi.mocked(vaultApiFetch).mockRejectedValueOnce(
1301
+ new DOMException("The operation was aborted due to timeout", "TimeoutError"),
1302
+ );
1303
+
1304
+ await expect(
1305
+ loadRevealedSecrets("test-token", "prs_alice", ["FIRST", "SECOND"]),
1306
+ ).rejects.toThrow("timeout");
1307
+
1308
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "FIRST");
1309
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "SECOND");
1310
+ });
1311
+
1312
+ it("evicts earlier successful chunks when a later authorization chunk fails", async () => {
1313
+ const keys = Array.from({ length: 101 }, (_, i) => `KEY_${i}`);
1314
+ vi.mocked(vaultApiFetch)
1315
+ .mockResolvedValueOnce(
1316
+ jsonRes({
1317
+ secrets: keys.slice(0, 100).map((name) => ({
1318
+ name,
1319
+ value: `value-for-${name}`,
1320
+ cacheTtlMs: 300000,
1321
+ })),
1322
+ errors: [],
1323
+ }),
1324
+ )
1325
+ .mockResolvedValueOnce(
1326
+ jsonRes({ error: "No read permission", code: "forbidden" }, 403),
1327
+ );
1328
+
1329
+ await expect(
1330
+ loadRevealedSecrets("test-token", "prs_alice", keys),
1331
+ ).rejects.toThrow("No read permission");
1332
+
1333
+ expect(writeCache).toHaveBeenCalledWith(
1334
+ "prs_alice",
1335
+ "KEY_0",
1336
+ "value-for-KEY_0",
1337
+ 300000,
1338
+ );
1339
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "KEY_0");
1340
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "KEY_100");
1341
+ });
1342
+
1343
+ it("rejects mixed success/error rows and evicts without exposing the value", async () => {
1344
+ const secretValue = "sentinel-secret-value";
1345
+ vi.mocked(vaultApiFetch).mockResolvedValueOnce(
1346
+ jsonRes({
1347
+ secrets: [{ name: "MIXED", value: secretValue, cacheTtlMs: 300000 }],
1348
+ errors: [{ name: "MIXED", code: "forbidden", message: "Denied" }],
1349
+ }),
1350
+ );
1351
+
1352
+ let failure: Error | undefined;
1353
+ try {
1354
+ await loadRevealedSecrets("test-token", "prs_alice", ["MIXED"]);
1355
+ } catch (err) {
1356
+ failure = err as Error;
1357
+ }
1358
+
1359
+ expect(failure?.message).toBe("Invalid secret load response from vault");
1360
+ expect(failure?.message).not.toContain(secretValue);
1361
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "MIXED");
1362
+ });
1363
+
1364
+ it("rejects malformed response envelopes and evicts every requested entry", async () => {
1365
+ vi.mocked(vaultApiFetch).mockResolvedValueOnce(
1366
+ jsonRes({ secrets: { name: "FIRST", value: "hidden" }, errors: [] }),
1367
+ );
1368
+
1369
+ await expect(
1370
+ loadRevealedSecrets("test-token", "prs_alice", ["FIRST", "SECOND"]),
1371
+ ).rejects.toThrow("Invalid secret load response from vault");
1372
+
1373
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "FIRST");
1374
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "SECOND");
1112
1375
  });
1113
1376
 
1114
1377
  it("a non-2xx batch response throws the batch-load error", async () => {
@@ -1145,6 +1408,8 @@ describe("secrets script usage", () => {
1145
1408
  "MY_KEY",
1146
1409
  "--script",
1147
1410
  scriptPath,
1411
+ "--script-id",
1412
+ "scripts/exec-script.sh",
1148
1413
  "--",
1149
1414
  "env",
1150
1415
  ]);
@@ -1158,13 +1423,14 @@ describe("secrets script usage", () => {
1158
1423
  usage: {
1159
1424
  channel: "exec",
1160
1425
  script: {
1161
- scriptId: scriptPath,
1426
+ scriptId: "scripts/exec-script.sh",
1162
1427
  path: scriptPath,
1163
1428
  sha256: expectedSha,
1164
1429
  attestationLevel: "self-asserted-hash",
1165
1430
  },
1166
1431
  },
1167
1432
  },
1433
+ signal: expect.any(AbortSignal),
1168
1434
  });
1169
1435
  expect(spawn).toHaveBeenCalledWith(
1170
1436
  "env",
@@ -1220,6 +1486,7 @@ describe("secrets script usage", () => {
1220
1486
  },
1221
1487
  },
1222
1488
  },
1489
+ signal: expect.any(AbortSignal),
1223
1490
  });
1224
1491
  expect(stdoutWriteSpy).toHaveBeenCalledWith("export MY_KEY='v1'\n");
1225
1492
  });
@@ -1368,6 +1635,7 @@ describe("secrets reveal and policy controls", () => {
1368
1635
  });
1369
1636
  expect(logSpy).toHaveBeenCalledWith(" Tier: nuclear");
1370
1637
  expect(logSpy).toHaveBeenCalledWith(" Script Lock: enforced");
1638
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "LOCKED");
1371
1639
  });
1372
1640
 
1373
1641
  it("script approve hashes the local file and hits the approval endpoint", async () => {
@@ -1403,6 +1671,7 @@ describe("secrets reveal and policy controls", () => {
1403
1671
  attestationLevel: "self-asserted-hash",
1404
1672
  },
1405
1673
  });
1674
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "LOCKED");
1406
1675
  });
1407
1676
 
1408
1677
  it("script revoke hits the revoke endpoint", async () => {
@@ -1429,23 +1698,38 @@ describe("secrets reveal and policy controls", () => {
1429
1698
  scriptId: "deploy-script",
1430
1699
  },
1431
1700
  });
1701
+ expect(removeCacheEntry).toHaveBeenCalledWith("prs_alice", "LOCKED");
1432
1702
  });
1433
1703
 
1434
- it("script list renders the current policy and approved scripts", async () => {
1704
+ it("script list maps the server's scriptLock.approvedScripts response shape", async () => {
1435
1705
  vi.mocked(vaultApiFetch).mockResolvedValueOnce(
1436
1706
  jsonRes({
1437
1707
  policy: {
1438
1708
  path: "LOCKED",
1439
1709
  tier: "nuclear",
1440
- scriptLock: { mode: "enforced" },
1441
- scripts: [
1442
- {
1443
- scriptId: "deploy-script",
1444
- scriptPath: "/tmp/deploy.sh",
1445
- sha256: "abc123",
1446
- attestationLevel: "self-asserted-hash",
1447
- },
1448
- ],
1710
+ scriptLock: {
1711
+ mode: "enforced",
1712
+ requiredAttestation: "self-asserted-hash",
1713
+ approvedScripts: [
1714
+ {
1715
+ scriptId: "deploy-script",
1716
+ path: "/tmp/deploy.sh",
1717
+ sha256: "abc123",
1718
+ attestationLevel: "self-asserted-hash",
1719
+ approvedAt: "2026-01-01T00:00:00.000Z",
1720
+ approvedBy: "prs_owner",
1721
+ },
1722
+ {
1723
+ scriptId: "revoked-script",
1724
+ path: "/tmp/old.sh",
1725
+ sha256: "def456",
1726
+ attestationLevel: "self-asserted-hash",
1727
+ approvedAt: "2025-12-01T00:00:00.000Z",
1728
+ approvedBy: "prs_owner",
1729
+ revokedAt: "2026-01-02T00:00:00.000Z",
1730
+ },
1731
+ ],
1732
+ },
1449
1733
  },
1450
1734
  }),
1451
1735
  );
@@ -1470,5 +1754,62 @@ describe("secrets reveal and policy controls", () => {
1470
1754
  expect(logSpy).toHaveBeenCalledWith(
1471
1755
  expect.stringContaining("deploy-script"),
1472
1756
  );
1757
+ expect(logSpy).toHaveBeenCalledWith(
1758
+ expect.stringContaining("/tmp/deploy.sh"),
1759
+ );
1760
+ expect(logSpy).not.toHaveBeenCalledWith(
1761
+ expect.stringContaining("revoked-script"),
1762
+ );
1763
+ });
1764
+
1765
+ it("script list ignores malformed modern rows without falling back to stale legacy rows", async () => {
1766
+ vi.mocked(vaultApiFetch).mockResolvedValueOnce(
1767
+ jsonRes({
1768
+ policy: {
1769
+ path: "LOCKED",
1770
+ tier: "sensitive",
1771
+ scriptLock: {
1772
+ mode: "enforced",
1773
+ approvedScripts: [
1774
+ null,
1775
+ { scriptId: "missing-path", attestationLevel: "self-asserted-hash" },
1776
+ {
1777
+ scriptId: "active-modern",
1778
+ path: "/tmp/active.sh",
1779
+ attestationLevel: "self-asserted-hash",
1780
+ },
1781
+ ],
1782
+ },
1783
+ scripts: [
1784
+ {
1785
+ scriptId: "stale-legacy",
1786
+ scriptPath: "/tmp/stale.sh",
1787
+ sha256: "stale",
1788
+ attestationLevel: "self-asserted-hash",
1789
+ },
1790
+ ],
1791
+ },
1792
+ }),
1793
+ );
1794
+
1795
+ const program = buildProgram();
1796
+ await program.parseAsync([
1797
+ "node",
1798
+ "hq",
1799
+ "secrets",
1800
+ "script",
1801
+ "list",
1802
+ "LOCKED",
1803
+ ]);
1804
+
1805
+ expect(logSpy).toHaveBeenCalledWith(
1806
+ expect.stringContaining("active-modern"),
1807
+ );
1808
+ expect(logSpy).not.toHaveBeenCalledWith(
1809
+ expect.stringContaining("missing-path"),
1810
+ );
1811
+ expect(logSpy).not.toHaveBeenCalledWith(
1812
+ expect.stringContaining("stale-legacy"),
1813
+ );
1473
1814
  });
1474
1815
  });