@stigmer/server 3.27.1 → 3.28.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 (245) hide show
  1. package/README.md +2 -0
  2. package/dist/authorization/authorizer.d.ts +7 -5
  3. package/dist/authorization/authorizer.d.ts.map +1 -1
  4. package/dist/authorization/authorizer.js +7 -5
  5. package/dist/authorization/authorizer.js.map +1 -1
  6. package/dist/authorization/derived-tuples.d.ts +3 -1
  7. package/dist/authorization/derived-tuples.d.ts.map +1 -1
  8. package/dist/authorization/derived-tuples.js +52 -2
  9. package/dist/authorization/derived-tuples.js.map +1 -1
  10. package/dist/authorization/evaluator.d.ts +9 -10
  11. package/dist/authorization/evaluator.d.ts.map +1 -1
  12. package/dist/authorization/evaluator.js.map +1 -1
  13. package/dist/authorization/list-read-scope.d.ts.map +1 -1
  14. package/dist/authorization/list-read-scope.js +29 -18
  15. package/dist/authorization/list-read-scope.js.map +1 -1
  16. package/dist/authorization/model/bindings.d.ts +41 -0
  17. package/dist/authorization/model/bindings.d.ts.map +1 -0
  18. package/dist/authorization/model/bindings.js +89 -0
  19. package/dist/authorization/model/bindings.js.map +1 -0
  20. package/dist/authorization/model/data/authorization-model.json +3114 -0
  21. package/dist/authorization/model/default-of.d.ts +2 -2
  22. package/dist/authorization/model/execution-viewer.d.ts +3 -0
  23. package/dist/authorization/model/execution-viewer.d.ts.map +1 -0
  24. package/dist/authorization/model/execution-viewer.js +50 -0
  25. package/dist/authorization/model/execution-viewer.js.map +1 -0
  26. package/dist/authorization/model/index.d.ts +33 -21
  27. package/dist/authorization/model/index.d.ts.map +1 -1
  28. package/dist/authorization/model/index.js +74 -52
  29. package/dist/authorization/model/index.js.map +1 -1
  30. package/dist/authorization/model/openfga-json.d.ts +18 -0
  31. package/dist/authorization/model/openfga-json.d.ts.map +1 -0
  32. package/dist/authorization/model/openfga-json.js +266 -0
  33. package/dist/authorization/model/openfga-json.js.map +1 -0
  34. package/dist/authorization/model/rewrite.d.ts +39 -67
  35. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  36. package/dist/authorization/model/rewrite.js +1 -117
  37. package/dist/authorization/model/rewrite.js.map +1 -1
  38. package/dist/authorization/strict-json.d.ts +20 -0
  39. package/dist/authorization/strict-json.d.ts.map +1 -0
  40. package/dist/authorization/strict-json.js +45 -0
  41. package/dist/authorization/strict-json.js.map +1 -0
  42. package/dist/boot/compose.d.ts.map +1 -1
  43. package/dist/boot/compose.js +9 -7
  44. package/dist/boot/compose.js.map +1 -1
  45. package/dist/boot/list-indexes.d.ts.map +1 -1
  46. package/dist/boot/list-indexes.js +6 -0
  47. package/dist/boot/list-indexes.js.map +1 -1
  48. package/dist/domain/iampolicy/list-index.d.ts +2 -0
  49. package/dist/domain/iampolicy/list-index.d.ts.map +1 -0
  50. package/dist/domain/iampolicy/list-index.js +32 -0
  51. package/dist/domain/iampolicy/list-index.js.map +1 -0
  52. package/dist/domain/iampolicy/resource-store.d.ts.map +1 -1
  53. package/dist/domain/iampolicy/resource-store.js +24 -11
  54. package/dist/domain/iampolicy/resource-store.js.map +1 -1
  55. package/dist/domain/workflowexecution/list-index.d.ts +1 -1
  56. package/dist/extensions/list-read-scope.d.ts +20 -3
  57. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  58. package/dist/extensions/list-read-scope.js +22 -2
  59. package/dist/extensions/list-read-scope.js.map +1 -1
  60. package/dist/index.d.ts +8 -8
  61. package/dist/index.d.ts.map +1 -1
  62. package/dist/index.js +20 -5
  63. package/dist/index.js.map +1 -1
  64. package/package.json +7 -6
  65. package/src/authorization/README.md +48 -39
  66. package/src/authorization/__tests__/authorizer.test.ts +5 -14
  67. package/src/authorization/__tests__/derived-tuples.test.ts +192 -17
  68. package/src/authorization/__tests__/evaluator.test.ts +53 -160
  69. package/src/authorization/__tests__/facts.test.ts +5 -11
  70. package/src/authorization/__tests__/list-read-scope.measure.test.ts +424 -62
  71. package/src/authorization/__tests__/list-read-scope.test.ts +15 -14
  72. package/src/authorization/__tests__/organization-directory.test.ts +2 -2
  73. package/src/authorization/__tests__/store-test-kit.ts +246 -0
  74. package/src/authorization/__tests__/store-tests.test.ts +91 -223
  75. package/src/authorization/__tests__/support.ts +30 -2
  76. package/src/authorization/__tests__/throwaway-model.ts +75 -0
  77. package/src/authorization/__tests__/wire-permissions.test.ts +141 -107
  78. package/src/authorization/authorizer.ts +7 -5
  79. package/src/authorization/derived-tuples.ts +105 -16
  80. package/src/authorization/evaluator.ts +8 -10
  81. package/src/authorization/list-read-scope.ts +37 -22
  82. package/src/authorization/model/__tests__/openfga-json.test.ts +353 -0
  83. package/src/authorization/model/__tests__/registry.test.ts +102 -276
  84. package/src/authorization/model/bindings.ts +131 -0
  85. package/src/authorization/model/data/authorization-model.json +3114 -0
  86. package/src/authorization/model/default-of.ts +2 -2
  87. package/src/authorization/model/execution-viewer.ts +56 -0
  88. package/src/authorization/model/index.ts +81 -72
  89. package/src/authorization/model/openfga-json.ts +310 -0
  90. package/src/authorization/model/rewrite.ts +39 -188
  91. package/src/authorization/strict-json.ts +52 -0
  92. package/src/boot/__tests__/list-indexes.test.ts +4 -0
  93. package/src/boot/compose.ts +9 -7
  94. package/src/boot/list-indexes.ts +6 -0
  95. package/src/domain/iampolicy/__tests__/resource-store.test.ts +65 -5
  96. package/src/domain/iampolicy/list-index.ts +33 -0
  97. package/src/domain/iampolicy/resource-store.ts +24 -11
  98. package/src/extensions/__tests__/built-in-authorization-composed.test.ts +3 -6
  99. package/src/extensions/list-read-scope.ts +32 -4
  100. package/src/index.ts +20 -44
  101. package/dist/authorization/model/agent.d.ts +0 -2
  102. package/dist/authorization/model/agent.d.ts.map +0 -1
  103. package/dist/authorization/model/agent.js +0 -43
  104. package/dist/authorization/model/agent.js.map +0 -1
  105. package/dist/authorization/model/agent_channel.d.ts +0 -2
  106. package/dist/authorization/model/agent_channel.d.ts.map +0 -1
  107. package/dist/authorization/model/agent_channel.js +0 -39
  108. package/dist/authorization/model/agent_channel.js.map +0 -1
  109. package/dist/authorization/model/agent_execution.d.ts +0 -2
  110. package/dist/authorization/model/agent_execution.d.ts.map +0 -1
  111. package/dist/authorization/model/agent_execution.js +0 -25
  112. package/dist/authorization/model/agent_execution.js.map +0 -1
  113. package/dist/authorization/model/agent_instance.d.ts +0 -2
  114. package/dist/authorization/model/agent_instance.d.ts.map +0 -1
  115. package/dist/authorization/model/agent_instance.js +0 -48
  116. package/dist/authorization/model/agent_instance.js.map +0 -1
  117. package/dist/authorization/model/agent_share.d.ts +0 -2
  118. package/dist/authorization/model/agent_share.d.ts.map +0 -1
  119. package/dist/authorization/model/agent_share.js +0 -29
  120. package/dist/authorization/model/agent_share.js.map +0 -1
  121. package/dist/authorization/model/api_key.d.ts +0 -2
  122. package/dist/authorization/model/api_key.d.ts.map +0 -1
  123. package/dist/authorization/model/api_key.js +0 -27
  124. package/dist/authorization/model/api_key.js.map +0 -1
  125. package/dist/authorization/model/artifact.d.ts +0 -2
  126. package/dist/authorization/model/artifact.d.ts.map +0 -1
  127. package/dist/authorization/model/artifact.js +0 -27
  128. package/dist/authorization/model/artifact.js.map +0 -1
  129. package/dist/authorization/model/channel_app.d.ts +0 -2
  130. package/dist/authorization/model/channel_app.d.ts.map +0 -1
  131. package/dist/authorization/model/channel_app.js +0 -28
  132. package/dist/authorization/model/channel_app.js.map +0 -1
  133. package/dist/authorization/model/environment.d.ts +0 -2
  134. package/dist/authorization/model/environment.d.ts.map +0 -1
  135. package/dist/authorization/model/environment.js +0 -33
  136. package/dist/authorization/model/environment.js.map +0 -1
  137. package/dist/authorization/model/execution_context.d.ts +0 -2
  138. package/dist/authorization/model/execution_context.d.ts.map +0 -1
  139. package/dist/authorization/model/execution_context.js +0 -19
  140. package/dist/authorization/model/execution_context.js.map +0 -1
  141. package/dist/authorization/model/iam_policy.d.ts +0 -2
  142. package/dist/authorization/model/iam_policy.d.ts.map +0 -1
  143. package/dist/authorization/model/iam_policy.js +0 -31
  144. package/dist/authorization/model/iam_policy.js.map +0 -1
  145. package/dist/authorization/model/identity_account.d.ts +0 -2
  146. package/dist/authorization/model/identity_account.d.ts.map +0 -1
  147. package/dist/authorization/model/identity_account.js +0 -25
  148. package/dist/authorization/model/identity_account.js.map +0 -1
  149. package/dist/authorization/model/mcp_server.d.ts +0 -2
  150. package/dist/authorization/model/mcp_server.d.ts.map +0 -1
  151. package/dist/authorization/model/mcp_server.js +0 -38
  152. package/dist/authorization/model/mcp_server.js.map +0 -1
  153. package/dist/authorization/model/memory.d.ts +0 -2
  154. package/dist/authorization/model/memory.d.ts.map +0 -1
  155. package/dist/authorization/model/memory.js +0 -26
  156. package/dist/authorization/model/memory.js.map +0 -1
  157. package/dist/authorization/model/oauth_app.d.ts +0 -2
  158. package/dist/authorization/model/oauth_app.d.ts.map +0 -1
  159. package/dist/authorization/model/oauth_app.js +0 -31
  160. package/dist/authorization/model/oauth_app.js.map +0 -1
  161. package/dist/authorization/model/organization.d.ts +0 -2
  162. package/dist/authorization/model/organization.d.ts.map +0 -1
  163. package/dist/authorization/model/organization.js +0 -58
  164. package/dist/authorization/model/organization.js.map +0 -1
  165. package/dist/authorization/model/platform_client.d.ts +0 -2
  166. package/dist/authorization/model/platform_client.d.ts.map +0 -1
  167. package/dist/authorization/model/platform_client.js +0 -31
  168. package/dist/authorization/model/platform_client.js.map +0 -1
  169. package/dist/authorization/model/plugin.d.ts +0 -2
  170. package/dist/authorization/model/plugin.d.ts.map +0 -1
  171. package/dist/authorization/model/plugin.js +0 -45
  172. package/dist/authorization/model/plugin.js.map +0 -1
  173. package/dist/authorization/model/schedule.d.ts +0 -2
  174. package/dist/authorization/model/schedule.d.ts.map +0 -1
  175. package/dist/authorization/model/schedule.js +0 -34
  176. package/dist/authorization/model/schedule.js.map +0 -1
  177. package/dist/authorization/model/session.d.ts +0 -2
  178. package/dist/authorization/model/session.d.ts.map +0 -1
  179. package/dist/authorization/model/session.js +0 -38
  180. package/dist/authorization/model/session.js.map +0 -1
  181. package/dist/authorization/model/skill.d.ts +0 -2
  182. package/dist/authorization/model/skill.d.ts.map +0 -1
  183. package/dist/authorization/model/skill.js +0 -39
  184. package/dist/authorization/model/skill.js.map +0 -1
  185. package/dist/authorization/model/workflow.d.ts +0 -2
  186. package/dist/authorization/model/workflow.d.ts.map +0 -1
  187. package/dist/authorization/model/workflow.js +0 -39
  188. package/dist/authorization/model/workflow.js.map +0 -1
  189. package/dist/authorization/model/workflow_execution.d.ts +0 -2
  190. package/dist/authorization/model/workflow_execution.d.ts.map +0 -1
  191. package/dist/authorization/model/workflow_execution.js +0 -33
  192. package/dist/authorization/model/workflow_execution.js.map +0 -1
  193. package/dist/authorization/model/workflow_instance.d.ts +0 -2
  194. package/dist/authorization/model/workflow_instance.d.ts.map +0 -1
  195. package/dist/authorization/model/workflow_instance.js +0 -96
  196. package/dist/authorization/model/workflow_instance.js.map +0 -1
  197. package/dist/authorization/store-test-kit.d.ts +0 -62
  198. package/dist/authorization/store-test-kit.d.ts.map +0 -1
  199. package/dist/authorization/store-test-kit.js +0 -265
  200. package/dist/authorization/store-test-kit.js.map +0 -1
  201. package/src/authorization/__tests__/fixtures/fga/agent-channel-owner.fga.yaml +0 -112
  202. package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +0 -177
  203. package/src/authorization/__tests__/fixtures/fga/agent-share-owner.fga.yaml +0 -109
  204. package/src/authorization/__tests__/fixtures/fga/artifact-org-and-owner.fga.yaml +0 -128
  205. package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +0 -318
  206. package/src/authorization/__tests__/fixtures/fga/channel-app-owner.fga.yaml +0 -125
  207. package/src/authorization/__tests__/fixtures/fga/channel-participation.fga.yaml +0 -201
  208. package/src/authorization/__tests__/fixtures/fga/channel-session-visibility.fga.yaml +0 -187
  209. package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +0 -214
  210. package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +0 -129
  211. package/src/authorization/__tests__/fixtures/fga/memory-subject-only.fga.yaml +0 -72
  212. package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +0 -308
  213. package/src/authorization/__tests__/fixtures/fga/org-shared-environment.fga.yaml +0 -144
  214. package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +0 -199
  215. package/src/authorization/__tests__/fixtures/fga/provider-standing-viewer.fga.yaml +0 -40
  216. package/src/authorization/__tests__/fixtures/fga/reserved-label-writer.fga.yaml +0 -41
  217. package/src/authorization/__tests__/fixtures/fga/schedule-owner.fga.yaml +0 -113
  218. package/src/authorization/__tests__/fixtures/fga/schedule-session-visibility.fga.yaml +0 -214
  219. package/src/authorization/__tests__/fixtures/fga/session-personal-resource.fga.yaml +0 -117
  220. package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +0 -190
  221. package/src/authorization/model/agent.ts +0 -64
  222. package/src/authorization/model/agent_channel.ts +0 -57
  223. package/src/authorization/model/agent_execution.ts +0 -33
  224. package/src/authorization/model/agent_instance.ts +0 -66
  225. package/src/authorization/model/agent_share.ts +0 -40
  226. package/src/authorization/model/api_key.ts +0 -28
  227. package/src/authorization/model/artifact.ts +0 -38
  228. package/src/authorization/model/channel_app.ts +0 -39
  229. package/src/authorization/model/environment.ts +0 -48
  230. package/src/authorization/model/execution_context.ts +0 -20
  231. package/src/authorization/model/iam_policy.ts +0 -42
  232. package/src/authorization/model/identity_account.ts +0 -26
  233. package/src/authorization/model/mcp_server.ts +0 -59
  234. package/src/authorization/model/memory.ts +0 -27
  235. package/src/authorization/model/oauth_app.ts +0 -43
  236. package/src/authorization/model/organization.ts +0 -59
  237. package/src/authorization/model/platform_client.ts +0 -43
  238. package/src/authorization/model/plugin.ts +0 -66
  239. package/src/authorization/model/schedule.ts +0 -49
  240. package/src/authorization/model/session.ts +0 -50
  241. package/src/authorization/model/skill.ts +0 -60
  242. package/src/authorization/model/workflow.ts +0 -60
  243. package/src/authorization/model/workflow_execution.ts +0 -46
  244. package/src/authorization/model/workflow_instance.ts +0 -125
  245. package/src/authorization/store-test-kit.ts +0 -395
@@ -0,0 +1,246 @@
1
+ /**
2
+ * The OpenFGA store-test kit: runs a `.fga.yaml` document (the format
3
+ * `fga model test` reads: fixture tuples, then named tests with `check`
4
+ * and `list_objects` assertions) against the built-in evaluator. The
5
+ * documents are the model's own suites (fga/tests/), which the real
6
+ * engine runs too (`make test-authorization-model`), so every assertion
7
+ * answered here is answered by OpenFGA over the same compiled model: the
8
+ * proof that the evaluator and the engine mean one thing.
9
+ *
10
+ * One case per named test. A case asserts every `check`, relation by
11
+ * relation, and every `list_objects`, and fails on the first disagreement
12
+ * with a message in the document's own words. Nothing is skipped: the
13
+ * built-in model declares every type a document can name.
14
+ *
15
+ * `list_objects` is evaluated exactly. An object holds a relation only
16
+ * through a tuple on that object: a direct tuple, a tupleset link, or a
17
+ * computed relation that bottoms out in one of those, which are the only
18
+ * forms the model uses (the reader refuses wildcards, the one form that
19
+ * would grant without a tuple on the object). So the objects of the asked
20
+ * type that the document's tuples name are the complete candidate set;
21
+ * each is checked, and the kept ones are compared with the expectation as
22
+ * a set, the way OpenFGA answers it (in no order). This is also how the
23
+ * server's list scope works: rows first, then checks.
24
+ *
25
+ * The parser is strict on purpose (strict-json.ts): an unknown key
26
+ * anywhere is a thrown fault, so a change to the format upstream is loud
27
+ * here rather than half-run. The model declares no condition, so a
28
+ * tuple's `condition` and a check's `context` are among the keys refused.
29
+ */
30
+ import assert from "node:assert/strict";
31
+
32
+ import { checkRelation } from "../evaluator.js";
33
+ import type { Model } from "../model/index.js";
34
+ import { list, optionalList, record, text } from "../strict-json.js";
35
+ import type { Person, Tuple } from "../tuples.js";
36
+ import {
37
+ ACCOUNT_TYPE,
38
+ formatObjectRef,
39
+ newInMemoryTupleSource,
40
+ parseObjectRef,
41
+ parseSubject,
42
+ } from "../tuples.js";
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // The document, typed exactly as the suites use the format.
46
+ // ---------------------------------------------------------------------------
47
+
48
+ export interface StoreTestTuple {
49
+ readonly user: string;
50
+ readonly relation: string;
51
+ readonly object: string;
52
+ }
53
+
54
+ export interface StoreTestCheck {
55
+ readonly user: string;
56
+ readonly object: string;
57
+ readonly assertions: Readonly<Record<string, boolean>>;
58
+ }
59
+
60
+ export interface StoreTestListObjects {
61
+ readonly user: string;
62
+ readonly type: string;
63
+ readonly assertions: Readonly<Record<string, ReadonlyArray<string>>>;
64
+ }
65
+
66
+ export interface StoreTest {
67
+ readonly name: string;
68
+ readonly check: ReadonlyArray<StoreTestCheck>;
69
+ readonly list_objects: ReadonlyArray<StoreTestListObjects>;
70
+ }
71
+
72
+ export interface StoreTestDocument {
73
+ readonly name: string;
74
+ readonly tuples: ReadonlyArray<StoreTestTuple>;
75
+ readonly tests: ReadonlyArray<StoreTest>;
76
+ }
77
+
78
+ /**
79
+ * Parses a loaded YAML value into a document, refusing anything the
80
+ * format the suites use does not carry (`sourceName` names the file in
81
+ * every message).
82
+ */
83
+ export function parseStoreTestDocument(
84
+ value: unknown,
85
+ sourceName: string,
86
+ ): StoreTestDocument {
87
+ const root = record(value, sourceName, [
88
+ "name",
89
+ "model_file",
90
+ "tuples",
91
+ "tests",
92
+ ]);
93
+ return {
94
+ name: text(root["name"], `${sourceName}: name`),
95
+ tuples: list(root["tuples"], `${sourceName}: tuples`).map((entry, index) =>
96
+ parseTuple(entry, `${sourceName}: tuples[${index}]`),
97
+ ),
98
+ tests: list(root["tests"], `${sourceName}: tests`).map((entry, index) =>
99
+ parseTest(entry, `${sourceName}: tests[${index}]`),
100
+ ),
101
+ };
102
+ }
103
+
104
+ function parseTuple(value: unknown, where: string): StoreTestTuple {
105
+ const entry = record(value, where, ["user", "relation", "object"]);
106
+ return {
107
+ user: text(entry["user"], `${where}.user`),
108
+ relation: text(entry["relation"], `${where}.relation`),
109
+ object: text(entry["object"], `${where}.object`),
110
+ };
111
+ }
112
+
113
+ function parseTest(value: unknown, where: string): StoreTest {
114
+ const entry = record(value, where, ["name", "check", "list_objects"]);
115
+ return {
116
+ name: text(entry["name"], `${where}.name`),
117
+ check: optionalList(entry["check"], `${where}.check`).map((item, index) =>
118
+ parseCheck(item, `${where}.check[${index}]`),
119
+ ),
120
+ list_objects: optionalList(
121
+ entry["list_objects"],
122
+ `${where}.list_objects`,
123
+ ).map((item, index) =>
124
+ parseListObjects(item, `${where}.list_objects[${index}]`),
125
+ ),
126
+ };
127
+ }
128
+
129
+ function parseCheck(value: unknown, where: string): StoreTestCheck {
130
+ const entry = record(value, where, ["user", "object", "assertions"]);
131
+ const assertions = record(entry["assertions"], `${where}.assertions`);
132
+ const parsed: Record<string, boolean> = {};
133
+ for (const [relation, expected] of Object.entries(assertions)) {
134
+ if (typeof expected !== "boolean") {
135
+ throw new Error(`${where}.assertions.${relation}: expected a boolean`);
136
+ }
137
+ parsed[relation] = expected;
138
+ }
139
+ return {
140
+ user: text(entry["user"], `${where}.user`),
141
+ object: text(entry["object"], `${where}.object`),
142
+ assertions: parsed,
143
+ };
144
+ }
145
+
146
+ function parseListObjects(value: unknown, where: string): StoreTestListObjects {
147
+ const entry = record(value, where, ["user", "type", "assertions"]);
148
+ const assertions = record(entry["assertions"], `${where}.assertions`);
149
+ const parsed: Record<string, ReadonlyArray<string>> = {};
150
+ for (const [relation, expected] of Object.entries(assertions)) {
151
+ parsed[relation] = list(expected, `${where}.assertions.${relation}`).map(
152
+ (item, index) => text(item, `${where}.assertions.${relation}[${index}]`),
153
+ );
154
+ }
155
+ return {
156
+ user: text(entry["user"], `${where}.user`),
157
+ type: text(entry["type"], `${where}.type`),
158
+ assertions: parsed,
159
+ };
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // The cases.
164
+ // ---------------------------------------------------------------------------
165
+
166
+ export interface StoreTestCase {
167
+ /** `<document> :: <test name>`: the framework prints it as the test name. */
168
+ readonly name: string;
169
+ run(): Promise<void>;
170
+ }
171
+
172
+ /** The document's tests as runnable cases over `model`. */
173
+ export function storeTestCases(
174
+ document: StoreTestDocument,
175
+ model: Model,
176
+ ): ReadonlyArray<StoreTestCase> {
177
+ const tuples = document.tuples.map(tupleOf);
178
+ const source = newInMemoryTupleSource(tuples);
179
+ const deps = { model, source };
180
+
181
+ return document.tests.map((test) => ({
182
+ name: `${document.name} :: ${test.name}`,
183
+ async run() {
184
+ for (const item of test.check) {
185
+ const object = parseObjectRef(item.object);
186
+ const person = personOf(item.user);
187
+ for (const [relation, expected] of Object.entries(item.assertions)) {
188
+ const actual = await checkRelation(deps, object, relation, person);
189
+ assert.equal(
190
+ actual,
191
+ expected,
192
+ `${document.name} :: ${test.name}: ${item.user} ${relation} ${item.object} expected ${String(expected)}, got ${String(actual)}`,
193
+ );
194
+ }
195
+ }
196
+ for (const item of test.list_objects) {
197
+ const person = personOf(item.user);
198
+ const candidates = objectsOfType(tuples, item.type);
199
+ for (const [relation, expected] of Object.entries(item.assertions)) {
200
+ const kept: string[] = [];
201
+ for (const candidate of candidates) {
202
+ if (await checkRelation(deps, parseObjectRef(candidate), relation, person)) {
203
+ kept.push(candidate);
204
+ }
205
+ }
206
+ assert.deepEqual(
207
+ kept.sort(),
208
+ [...expected].sort(),
209
+ `${document.name} :: ${test.name}: ${item.user} ${relation} objects of type ${item.type}`,
210
+ );
211
+ }
212
+ }
213
+ },
214
+ }));
215
+ }
216
+
217
+ /** Every object of `type` a tuple is written on, each once, in first-seen order: the complete candidate set (the module header). */
218
+ export function objectsOfType(
219
+ tuples: ReadonlyArray<Tuple>,
220
+ type: string,
221
+ ): ReadonlyArray<string> {
222
+ const seen = new Set<string>();
223
+ for (const tuple of tuples) {
224
+ if (tuple.object.type === type) {
225
+ seen.add(formatObjectRef(tuple.object));
226
+ }
227
+ }
228
+ return [...seen];
229
+ }
230
+
231
+ function tupleOf(entry: StoreTestTuple): Tuple {
232
+ return {
233
+ object: parseObjectRef(entry.object),
234
+ relation: entry.relation,
235
+ subject: parseSubject(entry.user),
236
+ };
237
+ }
238
+
239
+ /** The document's users are plain accounts; a user of another type is a format the kit does not run. */
240
+ function personOf(user: string): Person {
241
+ const object = parseObjectRef(user);
242
+ if (object.type !== ACCOUNT_TYPE || object.id === "*") {
243
+ throw new Error(`store test users are accounts; got '${user}'`);
244
+ }
245
+ return { accountId: object.id, aliases: new Set([object.id]) };
246
+ }
@@ -1,24 +1,20 @@
1
1
  /**
2
- * Runs the cloud's own OpenFGA store tests against the built-in
3
- * evaluator: the documents under fixtures/fga/ are pinned copies of
4
- * stigmer-cloud's `fga/tests/*.fga.yaml` (each header names its source
5
- * and commit), written by the model's authors and run against real
6
- * OpenFGA in the cloud's CI. Every `check` assertion on a kind this
7
- * edition declares must answer the same here — the proof that "the model
8
- * evaluated over derived tuples" means the SAME model.
2
+ * Runs the model's own suites (fga/tests/*.fga.yaml) through the built-in
3
+ * evaluator. The same documents run on the real OpenFGA engine against the
4
+ * same compiled model (`make test-authorization-model`, and CI's
5
+ * authorization-model lane), so every `check` and every `list_objects`
6
+ * assertion answered here is answered by OpenFGA too: the proof that the
7
+ * evaluator and the engine mean one thing by the model.
9
8
  *
10
- * What is not run is reported, never dropped: a check that targets a kind
11
- * this edition does not serve, every `list_objects` assertion (the list
12
- * scope's proof), and an assertion whose walk read a grant through a
13
- * type this edition does not declare come back from the kit as a skipped
14
- * set this file pins entry by entry, so a fixture that gains a scenario
15
- * is a visible diff here and not a silently smaller proof. The second
16
- * describe below pins the walk-dependent reason's own behaviour over a
17
- * throwaway model, so the kit is proven and not only used.
9
+ * Nothing is skipped. `DOCUMENTS` is the folder's listing, so a suite
10
+ * added to the folder without a line here fails, and a suite is never
11
+ * silently out of the proof.
18
12
  *
19
- * The cloud re-runs the LIVE documents through the same kit at the
20
- * re-pin (the drift test's second table), which is what makes the pinned
21
- * copies safe to hold.
13
+ * The second describe proves the kit's `list_objects` rule over a
14
+ * throwaway model: candidates are the objects the document's tuples are
15
+ * written on, checked one by one, so an object reached through a tupleset
16
+ * link is listed, one no relation reaches is not, and a wrong expectation
17
+ * fails.
22
18
  */
23
19
  import { readdirSync, readFileSync } from "node:fs";
24
20
  import { fileURLToPath } from "node:url";
@@ -28,26 +24,28 @@ import { describe, expect, it } from "vitest";
28
24
 
29
25
  import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
30
26
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
27
+ import { OrganizationSchema } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/api_pb";
31
28
 
32
29
  import { builtInModel, newModel } from "../model/index.js";
30
+ import { parseObjectRef } from "../tuples.js";
31
+ import type { StoreTestDocument } from "./store-test-kit.js";
32
+ import {
33
+ objectsOfType,
34
+ parseStoreTestDocument,
35
+ storeTestCases,
36
+ } from "./store-test-kit.js";
33
37
  import {
34
38
  computed,
35
- declareKind,
36
39
  direct,
40
+ from,
37
41
  objectOf,
42
+ throwawayDeclaration,
38
43
  union,
39
- usersetOf,
40
- } from "../model/rewrite.js";
41
- import type { StoreTestDocument, StoreTestSkip } from "../store-test-kit.js";
42
- import { parseStoreTestDocument, storeTestCases } from "../store-test-kit.js";
44
+ } from "./throwaway-model.js";
43
45
 
44
- const FIXTURES = new URL("./fixtures/fga/", import.meta.url);
46
+ const SUITES = new URL("../../../fga/tests/", import.meta.url);
45
47
 
46
- /**
47
- * Every document the cloud's `fga/tests/` folder holds, by file name — the
48
- * fixtures folder is a byte-exact mirror of it; a file added to either
49
- * without a line here fails.
50
- */
48
+ /** Every suite the model's folder holds, by file name. */
51
49
  const DOCUMENTS = [
52
50
  "agent-channel-owner.fga.yaml",
53
51
  "agent-instance-creation.fga.yaml",
@@ -58,247 +56,117 @@ const DOCUMENTS = [
58
56
  "channel-participation.fga.yaml",
59
57
  "channel-session-visibility.fga.yaml",
60
58
  "default-instance-inheritance.fga.yaml",
59
+ "identity-provider-administration.fga.yaml",
60
+ "invitation-administration.fga.yaml",
61
+ "license-issuer.fga.yaml",
61
62
  "mcp-server-authoring.fga.yaml",
62
63
  "memory-subject-only.fga.yaml",
63
64
  "org-admin-owner-inheritance.fga.yaml",
64
65
  "org-shared-environment.fga.yaml",
65
66
  "platform-visibility.fga.yaml",
67
+ "plugin-owner.fga.yaml",
66
68
  "provider-standing-viewer.fga.yaml",
67
69
  "reserved-label-writer.fga.yaml",
68
70
  "schedule-owner.fga.yaml",
69
71
  "schedule-session-visibility.fga.yaml",
70
72
  "session-personal-resource.fga.yaml",
73
+ "team-membership.fga.yaml",
71
74
  "workflow-execution-sharing.fga.yaml",
72
75
  ] as const;
73
76
 
74
- function skipLine(skip: StoreTestSkip): string {
75
- const relation = skip.relation === undefined ? "" : ` ${skip.relation}`;
76
- const types = skip.types === undefined ? "" : ` (${skip.types.join(", ")})`;
77
- return `${skip.document} :: ${skip.test} :: ${skip.subject}${relation} ${skip.object} :: ${skip.reason}${types}`;
78
- }
79
-
80
- /**
81
- * Every assertion the kit did not run, pinned, in the kit's three
82
- * reasons. `list-objects` lines are the list scope's proof and run when
83
- * it lands. `undeclared-kind` lines target `platform`, a kind this
84
- * edition never serves (the driver refuses it before any evaluation).
85
- * `undeclared-subject-type` lines are the checks whose walk read the
86
- * cloud's platform-tenancy grant, `identity_provider:<idp>#platform_user`
87
- * — a userset on a kind this edition does not declare, so the answer here
88
- * (nobody) is not the oracle's and is not asserted; the cloud runs the
89
- * same lines over its full model at the re-pin.
90
- */
91
- const SKIPPED_PINNED: ReadonlyArray<string> = [
92
- // list-objects
93
- "agent-channel-owner :: listing channels reflects ownership and grants, not membership :: identity_account:alice <agent_channel> :: list-objects",
94
- "agent-channel-owner :: listing channels reflects ownership and grants, not membership :: identity_account:carol <agent_channel> :: list-objects",
95
- "agent-channel-owner :: listing channels reflects ownership and grants, not membership :: identity_account:mallory <agent_channel> :: list-objects",
96
- "agent-share-owner :: listing shares reflects ownership and grants, not membership :: identity_account:alice <agent_share> :: list-objects",
97
- "agent-share-owner :: listing shares reflects ownership and grants, not membership :: identity_account:carol <agent_share> :: list-objects",
98
- "agent-share-owner :: listing shares reflects ownership and grants, not membership :: identity_account:mallory <agent_share> :: list-objects",
99
- "artifact-org-and-owner :: listing reflects org inheritance plus creator ownership :: identity_account:acme-guest <artifact> :: list-objects",
100
- "artifact-org-and-owner :: listing reflects org inheritance plus creator ownership :: identity_account:eve <artifact> :: list-objects",
101
- "artifact-org-and-owner :: listing reflects org inheritance plus creator ownership :: identity_account:victor <artifact> :: list-objects",
102
- "blueprint-private-visibility :: org admin's listable agents include private ones (T08) :: identity_account:oscar <agent> :: list-objects",
103
- "blueprint-private-visibility :: org member's listable agents exclude private ones :: identity_account:dave <agent> :: list-objects",
104
- "blueprint-private-visibility :: org viewer's listable agents are exactly the viewer-shaped org-visible ones :: identity_account:vera <agent> :: list-objects",
105
- "blueprint-private-visibility :: owner's listable agents include everything they authored :: identity_account:carol <agent> :: list-objects",
106
- "channel-app-owner :: listing channel apps reflects ownership and grants, not membership :: identity_account:alice <channel_app> :: list-objects",
107
- "channel-app-owner :: listing channel apps reflects ownership and grants, not membership :: identity_account:carol <channel_app> :: list-objects",
108
- "channel-app-owner :: listing channel apps reflects ownership and grants, not membership :: identity_account:mallory <channel_app> :: list-objects",
109
- "channel-participation :: list viewable and participable channels resolve per grant (drives listConversations) :: identity_account:carol <agent_channel> :: list-objects",
110
- "channel-participation :: list viewable and participable channels resolve per grant (drives listConversations) :: identity_account:dave <agent_channel> :: list-objects",
111
- "channel-participation :: list viewable and participable channels resolve per grant (drives listConversations) :: identity_account:trina <agent_channel> :: list-objects",
112
- "channel-session-visibility :: list viewable sessions resolves the channel chain (drives listByChannel) :: identity_account:alice <session> :: list-objects",
113
- "channel-session-visibility :: list viewable sessions resolves the channel chain (drives listByChannel) :: identity_account:bob <session> :: list-objects",
114
- "channel-session-visibility :: list viewable sessions resolves the channel chain (drives listByChannel) :: identity_account:carol <session> :: list-objects",
115
- "default-instance-inheritance :: list managed-org member's viewable agent instances resolves the default_of chain :: identity_account:alice <agent_instance> :: list-objects",
116
- "memory-subject-only :: list-objects returns only the subject's own records :: identity_account:alice <memory> :: list-objects",
117
- "memory-subject-only :: list-objects returns only the subject's own records :: identity_account:mallory <memory> :: list-objects",
118
- "org-admin-owner-inheritance :: admin's listable resources include private blueprints, never personal kinds :: identity_account:ana <agent> :: list-objects",
119
- "org-admin-owner-inheritance :: admin's listable resources include private blueprints, never personal kinds :: identity_account:ana <agent_instance> :: list-objects",
120
- "org-admin-owner-inheritance :: admin's listable resources include private blueprints, never personal kinds :: identity_account:ana <session> :: list-objects",
121
- "org-shared-environment :: org-members-can-list-shared-environments-only :: identity_account:dave <environment> :: list-objects",
122
- "platform-visibility :: list managed-org member's viewable agents includes only the shared one :: identity_account:alice <agent> :: list-objects",
123
- "schedule-owner :: listing schedules reflects ownership and grants, not membership :: identity_account:alice <schedule> :: list-objects",
124
- "schedule-owner :: listing schedules reflects ownership and grants, not membership :: identity_account:carol <schedule> :: list-objects",
125
- "schedule-owner :: listing schedules reflects ownership and grants, not membership :: identity_account:mallory <schedule> :: list-objects",
126
- "schedule-session-visibility :: list viewable sessions resolves the schedule chain :: identity_account:alice <session> :: list-objects",
127
- "schedule-session-visibility :: list viewable sessions resolves the schedule chain :: identity_account:bob <session> :: list-objects",
128
- "schedule-session-visibility :: list viewable sessions resolves the schedule chain :: identity_account:carol <session> :: list-objects",
129
- "workflow-execution-sharing :: list a teammate's viewable runs reflects both sharing axes :: identity_account:bob <workflow_execution> :: list-objects",
130
- // undeclared-kind: platform
131
- "provider-standing-viewer :: operator may view provider standing, tenants may not :: identity_account:mallory platform:stigmer :: undeclared-kind",
132
- "provider-standing-viewer :: operator may view provider standing, tenants may not :: identity_account:opal platform:stigmer :: undeclared-kind",
133
- "reserved-label-writer :: operator may write reserved labels, tenants may not :: identity_account:machine platform:stigmer :: undeclared-kind",
134
- "reserved-label-writer :: operator may write reserved labels, tenants may not :: identity_account:mallory platform:stigmer :: undeclared-kind",
135
- // undeclared-subject-type: identity_provider#platform_user
136
- "agent-instance-creation :: the tenant-family case, the agent half from platform_viewer and the organization half at home :: identity_account:mia can_create_instance agent:open-bot :: undeclared-subject-type (identity_provider)",
137
- "default-instance-inheritance :: managed-org member reaches the default instance of a platform-shared agent :: identity_account:alice can_execute agent_instance:sara-default :: undeclared-subject-type (identity_provider)",
138
- "default-instance-inheritance :: managed-org member reaches the default instance of a platform-shared agent :: identity_account:alice can_view agent_instance:sara-default :: undeclared-subject-type (identity_provider)",
139
- "default-instance-inheritance :: managed-org member reaches the default instance of a platform-shared workflow :: identity_account:alice can_execute workflow_instance:onboarding-default :: undeclared-subject-type (identity_provider)",
140
- "default-instance-inheritance :: managed-org member reaches the default instance of a platform-shared workflow :: identity_account:alice can_view workflow_instance:onboarding-default :: undeclared-subject-type (identity_provider)",
141
- "default-instance-inheritance :: outsiders get nothing from the default_of link :: identity_account:mallory can_execute agent_instance:sara-default :: undeclared-subject-type (identity_provider)",
142
- "default-instance-inheritance :: outsiders get nothing from the default_of link :: identity_account:mallory can_view agent_instance:sara-default :: undeclared-subject-type (identity_provider)",
143
- "platform-visibility :: managed-org members can view and execute a platform-shared agent :: identity_account:alice can_create_instance agent:sara :: undeclared-subject-type (identity_provider)",
144
- "platform-visibility :: managed-org members can view and execute a platform-shared agent :: identity_account:alice can_execute agent:sara :: undeclared-subject-type (identity_provider)",
145
- "platform-visibility :: managed-org members can view and execute a platform-shared agent :: identity_account:alice can_view agent:sara :: undeclared-subject-type (identity_provider)",
146
- "platform-visibility :: managed-org members can view and execute a platform-shared agent :: identity_account:bob can_execute agent:sara :: undeclared-subject-type (identity_provider)",
147
- "platform-visibility :: managed-org members can view and execute a platform-shared agent :: identity_account:bob can_view agent:sara :: undeclared-subject-type (identity_provider)",
148
- "platform-visibility :: outsiders and unrelated orgs get nothing from platform visibility :: identity_account:mallory can_create_instance agent:sara :: undeclared-subject-type (identity_provider)",
149
- "platform-visibility :: outsiders and unrelated orgs get nothing from platform visibility :: identity_account:mallory can_execute agent:sara :: undeclared-subject-type (identity_provider)",
150
- "platform-visibility :: outsiders and unrelated orgs get nothing from platform visibility :: identity_account:mallory can_view agent:sara :: undeclared-subject-type (identity_provider)",
151
- "platform-visibility :: platform-shared mcp_server is usable by managed-org members :: identity_account:alice can_connect mcp_server:github :: undeclared-subject-type (identity_provider)",
152
- "platform-visibility :: platform-shared mcp_server is usable by managed-org members :: identity_account:alice can_use mcp_server:github :: undeclared-subject-type (identity_provider)",
153
- "platform-visibility :: platform-shared mcp_server is usable by managed-org members :: identity_account:alice can_view mcp_server:github :: undeclared-subject-type (identity_provider)",
154
- "platform-visibility :: platform-shared skill is usable by managed-org members :: identity_account:alice can_use skill:research :: undeclared-subject-type (identity_provider)",
155
- "platform-visibility :: platform-shared skill is usable by managed-org members :: identity_account:alice can_view skill:research :: undeclared-subject-type (identity_provider)",
156
- "platform-visibility :: platform-shared workflow is viewable and executable by managed-org members :: identity_account:alice can_execute workflow:onboarding :: undeclared-subject-type (identity_provider)",
157
- "platform-visibility :: platform-shared workflow is viewable and executable by managed-org members :: identity_account:alice can_view workflow:onboarding :: undeclared-subject-type (identity_provider)",
158
- ];
159
-
160
- describe("the cloud's OpenFGA store tests over the built-in evaluator", () => {
161
- it("the fixtures folder holds exactly the documents this file pins", () => {
77
+ describe("the model's suites over the built-in evaluator", () => {
78
+ it("the suites folder holds exactly the documents this file pins", () => {
162
79
  expect(
163
- readdirSync(FIXTURES)
80
+ readdirSync(SUITES)
164
81
  .filter((name) => name.endsWith(".fga.yaml"))
165
82
  .sort(),
166
83
  ).toEqual([...DOCUMENTS]);
167
84
  });
168
85
 
169
- const kits = DOCUMENTS.map((name) => {
86
+ for (const name of DOCUMENTS) {
170
87
  const document = parseStoreTestDocument(
171
- yaml.load(readFileSync(fileURLToPath(new URL(name, FIXTURES)), "utf8")),
88
+ yaml.load(readFileSync(fileURLToPath(new URL(name, SUITES)), "utf8")),
172
89
  name,
173
90
  );
174
- return { document, kit: storeTestCases(document, builtInModel) };
175
- });
176
-
177
- for (const { document, kit } of kits) {
178
- if (kit.cases.length === 0) {
179
- // Every check targets a kind this edition does not declare (the
180
- // platform documents); reported as skipped, pinned below.
181
- it.skip(`${document.name}: no check targets a declared kind`, () => {});
182
- continue;
183
- }
184
91
  describe(document.name, () => {
185
- for (const storeTestCase of kit.cases) {
92
+ for (const storeTestCase of storeTestCases(document, builtInModel)) {
186
93
  it(storeTestCase.name, storeTestCase.run);
187
94
  }
188
95
  });
189
96
  }
190
-
191
- // Declared after every case: a kit's `skipped` is complete once its
192
- // cases have run (the walk-dependent reason is decided per assertion).
193
- it("reports every assertion it did not run, and the list is the pinned one", () => {
194
- const skipped = kits.flatMap(({ kit }) => kit.skipped.map(skipLine));
195
- expect(skipped.sort()).toEqual([...SKIPPED_PINNED].sort());
196
- });
197
97
  });
198
98
 
199
99
  /**
200
- * The kit's walk-dependent skip, proven over a throwaway model shaped
201
- * like a blueprint whose `viewer` line reads, in order, a direct person,
202
- * `owner`, then a userset on a type the model does not declare — the
203
- * platform-tenancy shape — so each arm below settles at a different point
204
- * of the union.
100
+ * The kit's `list_objects` rule over a throwaway model: an agent whose
101
+ * `owner` is a direct person or an admin of its organization, reached
102
+ * through the `organization` tupleset link.
205
103
  */
206
- describe("the kit skips an assertion whose walk read a type the model does not declare", () => {
104
+ describe("the kit lists exactly the objects a relation reaches", () => {
207
105
  const model = newModel([
208
- declareKind({
106
+ throwawayDeclaration({
107
+ kind: ApiResourceKind.organization,
108
+ schema: OrganizationSchema,
109
+ relations: [["admin", direct(objectOf("identity_account"))]],
110
+ }),
111
+ throwawayDeclaration({
209
112
  kind: ApiResourceKind.agent,
210
113
  schema: AgentSchema,
211
- source: "fga/model/agentic/agent.fga",
212
114
  relations: [
213
- ["owner", direct(objectOf("identity_account"))],
214
- [
215
- "platform_viewer",
216
- direct(usersetOf("identity_provider", "platform_user")),
217
- ],
218
- [
219
- "viewer",
220
- union(
221
- direct(objectOf("identity_account")),
222
- computed("owner"),
223
- computed("platform_viewer"),
224
- ),
225
- ],
226
- ["can_view", computed("viewer")],
115
+ ["organization", direct(objectOf("organization"))],
116
+ ["owner", union(direct(objectOf("identity_account")), from("admin", "organization"))],
227
117
  ["can_edit", computed("owner")],
228
118
  ],
229
119
  }),
230
120
  ]);
231
121
 
232
- const document: StoreTestDocument = {
233
- name: "throwaway",
234
- tuples: [
235
- { user: "identity_account:carol", relation: "owner", object: "agent:a" },
236
- {
237
- user: "identity_provider:idp#platform_user",
238
- relation: "platform_viewer",
239
- object: "agent:a",
240
- },
241
- { user: "identity_account:carol", relation: "owner", object: "agent:b" },
242
- ],
243
- tests: [
244
- {
245
- name: "arms",
246
- check: [
247
- // The owner: `viewer` settles on the direct line; `can_edit` never reads platform_viewer.
248
- {
249
- user: "identity_account:carol",
250
- object: "agent:a",
251
- assertions: { can_view: true, can_edit: true },
252
- },
253
- // A stranger: the union walks to platform_viewer and reads the undeclared userset.
254
- {
255
- user: "identity_account:mallory",
256
- object: "agent:a",
257
- assertions: { can_view: false, can_edit: false },
258
- },
259
- // No such tuple on this object: the same stranger's walk reads nothing undeclared.
260
- {
261
- user: "identity_account:mallory",
262
- object: "agent:b",
263
- assertions: { can_view: false },
264
- },
265
- ],
266
- list_objects: [],
267
- },
268
- ],
269
- };
270
-
271
- it("runs the arms the walk settles before the undeclared type, skips exactly the one that read it — whatever it would have answered", async () => {
272
- const kit = storeTestCases(document, model);
273
- expect(kit.skipped).toEqual([]);
274
- for (const storeTestCase of kit.cases) {
275
- await storeTestCase.run();
276
- }
277
- expect(kit.skipped.map(skipLine)).toEqual([
278
- "throwaway :: arms :: identity_account:mallory can_view agent:a :: undeclared-subject-type (identity_provider)",
279
- ]);
280
- });
122
+ const tuples = [
123
+ { user: "identity_account:ada", relation: "owner", object: "agent:mine" },
124
+ { user: "organization:acme", relation: "organization", object: "agent:org" },
125
+ { user: "identity_account:ada", relation: "admin", object: "organization:acme" },
126
+ { user: "organization:other", relation: "organization", object: "agent:elsewhere" },
127
+ ];
281
128
 
282
- it("asserts the arms it runs: a wrong expectation on a settled arm still fails", async () => {
283
- const wrong: StoreTestDocument = {
284
- ...document,
129
+ function documentExpecting(expected: ReadonlyArray<string>): StoreTestDocument {
130
+ return {
131
+ name: "throwaway",
132
+ tuples,
285
133
  tests: [
286
134
  {
287
- name: "wrong",
288
- check: [
289
- {
290
- user: "identity_account:carol",
291
- object: "agent:a",
292
- assertions: { can_edit: false },
293
- },
135
+ name: "listing",
136
+ check: [],
137
+ list_objects: [
138
+ { user: "identity_account:ada", type: "agent", assertions: { can_edit: expected } },
294
139
  ],
295
- list_objects: [],
296
140
  },
297
141
  ],
298
142
  };
299
- const kit = storeTestCases(wrong, model);
300
- await expect(kit.cases[0]?.run()).rejects.toThrow(
301
- /expected false, got true/,
143
+ }
144
+
145
+ it("takes as candidates every object of the type a tuple is written on", () => {
146
+ expect(
147
+ objectsOfType(
148
+ tuples.map((tuple) => ({
149
+ object: parseObjectRef(tuple.object),
150
+ relation: tuple.relation,
151
+ subject: { form: "object" as const, object: parseObjectRef(tuple.user) },
152
+ })),
153
+ "agent",
154
+ ),
155
+ ).toEqual(["agent:mine", "agent:org", "agent:elsewhere"]);
156
+ });
157
+
158
+ it("lists the direct object and the one reached through the tupleset, in any order", async () => {
159
+ const [listing] = storeTestCases(documentExpecting(["agent:org", "agent:mine"]), model);
160
+ await expect(listing?.run()).resolves.toBeUndefined();
161
+ });
162
+
163
+ it("fails an expectation that lists an object no relation reaches, or misses one", async () => {
164
+ const [extra] = storeTestCases(
165
+ documentExpecting(["agent:mine", "agent:org", "agent:elsewhere"]),
166
+ model,
302
167
  );
168
+ await expect(extra?.run()).rejects.toThrow("identity_account:ada can_edit objects of type agent");
169
+ const [missing] = storeTestCases(documentExpecting(["agent:mine"]), model);
170
+ await expect(missing?.run()).rejects.toThrow("identity_account:ada can_edit objects of type agent");
303
171
  });
304
172
  });