next.toConfig.test.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. import { test, expect } from "bun:test"
  2. import { Permission } from "../../src/permission"
  3. // toConfig tests (inverse of fromConfig)
  4. test("toConfig - single wildcard rule uses object format", () => {
  5. const result = Permission.toConfig([{ permission: "read", pattern: "*", action: "allow" }])
  6. expect(result).toEqual({ read: { "*": "allow" } })
  7. })
  8. test("toConfig - single non-wildcard rule uses object format", () => {
  9. const result = Permission.toConfig([{ permission: "bash", pattern: "npm *", action: "allow" }])
  10. expect(result).toEqual({ bash: { "npm *": "allow" } })
  11. })
  12. test("toConfig - multiple rules for same permission use object format", () => {
  13. const result = Permission.toConfig([
  14. { permission: "bash", pattern: "*", action: "ask" },
  15. { permission: "bash", pattern: "npm *", action: "allow" },
  16. ])
  17. expect(result).toEqual({ bash: { "*": "ask", "npm *": "allow" } })
  18. })
  19. test("toConfig - mixed permissions", () => {
  20. const result = Permission.toConfig([
  21. { permission: "read", pattern: "*", action: "allow" },
  22. { permission: "bash", pattern: "npm *", action: "allow" },
  23. { permission: "bash", pattern: "git *", action: "allow" },
  24. ])
  25. expect(result).toEqual({
  26. read: { "*": "allow" },
  27. bash: { "npm *": "allow", "git *": "allow" },
  28. })
  29. })
  30. test("toConfig - empty rules returns empty object", () => {
  31. const result = Permission.toConfig([])
  32. expect(result).toEqual({})
  33. })
  34. test("toConfig - wildcard then specific promotes to object", () => {
  35. const result = Permission.toConfig([
  36. { permission: "bash", pattern: "*", action: "ask" },
  37. { permission: "bash", pattern: "rm *", action: "deny" },
  38. ])
  39. expect(result).toEqual({ bash: { "*": "ask", "rm *": "deny" } })
  40. })
  41. test("toConfig - roundtrip with fromConfig (simple) always uses object format", () => {
  42. const config = { read: "allow" as const, bash: "ask" as const }
  43. const rules = Permission.fromConfig(config)
  44. const result = Permission.toConfig(rules)
  45. // toConfig always uses object format to avoid erasing existing granular rules on merge
  46. expect(result).toEqual({ read: { "*": "allow" }, bash: { "*": "ask" } })
  47. })
  48. test("toConfig - roundtrip with fromConfig (object)", () => {
  49. const config = { bash: { "*": "ask" as const, "npm *": "allow" as const, "git *": "allow" as const } }
  50. const rules = Permission.fromConfig(config)
  51. const result = Permission.toConfig(rules)
  52. expect(result).toEqual(config)
  53. })
  54. test("toConfig - scalar-only permission uses scalar format", () => {
  55. const result = Permission.toConfig([{ permission: "websearch", pattern: "*", action: "allow" }])
  56. expect(result).toEqual({ websearch: "allow" })
  57. })
  58. test("toConfig - scalar-only permission with non-wildcard pattern is skipped", () => {
  59. // doom_loop uses always: [toolName], so pattern can be "bash" etc.
  60. // Non-wildcard patterns for scalar-only permissions can't be represented
  61. // in the config schema — they only work in-memory (known limitation).
  62. const result = Permission.toConfig([{ permission: "doom_loop", pattern: "bash", action: "allow" }])
  63. expect(result).toEqual({})
  64. })
  65. test("toConfig - mixed scalar-only and rule-capable permissions", () => {
  66. const result = Permission.toConfig([
  67. { permission: "websearch", pattern: "*", action: "allow" },
  68. { permission: "todowrite", pattern: "*", action: "allow" },
  69. { permission: "bash", pattern: "npm *", action: "allow" },
  70. ])
  71. expect(result).toEqual({
  72. websearch: "allow",
  73. todowrite: "allow",
  74. bash: { "npm *": "allow" },
  75. })
  76. })
  77. // Tests for null delete sentinel handling (null = "remove this key from config")
  78. test("fromConfig - null entries in PermissionObject are skipped", () => {
  79. const config = { bash: { "*": "ask" as const, "npm *": null } }
  80. const rules = Permission.fromConfig(config)
  81. // null is a delete sentinel — only the non-null entry should produce a rule
  82. expect(rules).toEqual([{ permission: "bash", pattern: "*", action: "ask" }])
  83. })
  84. test("fromConfig - null top-level PermissionRule is skipped", () => {
  85. const config = { bash: null }
  86. const rules = Permission.fromConfig(config)
  87. expect(rules).toEqual([])
  88. })
  89. test("toConfig - null existing entry is treated as absent (new rule wins)", () => {
  90. // If result[permission] is null (shouldn't happen in practice but defensive),
  91. // the new rule should be written as a fresh object entry.
  92. const result = Permission.toConfig([{ permission: "bash", pattern: "npm *", action: "allow" }])
  93. expect(result).toEqual({ bash: { "npm *": "allow" } })
  94. })