gitlab-mcp 1.1.0 → 1.2.1

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 (111) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +12 -1
  3. package/dist/config/dotenv.d.ts +2 -0
  4. package/dist/config/dotenv.js +40 -0
  5. package/dist/config/dotenv.js.map +1 -0
  6. package/dist/config/env.d.ts +55 -0
  7. package/dist/config/env.js +164 -0
  8. package/dist/config/env.js.map +1 -0
  9. package/dist/http-app.d.ts +45 -0
  10. package/dist/http-app.js +550 -0
  11. package/dist/http-app.js.map +1 -0
  12. package/dist/http.d.ts +2 -0
  13. package/dist/http.js +65 -0
  14. package/dist/http.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.js +65 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/lib/auth-context.d.ts +9 -0
  19. package/dist/lib/auth-context.js +9 -0
  20. package/dist/lib/auth-context.js.map +1 -0
  21. package/dist/lib/gitlab-client.d.ts +331 -0
  22. package/dist/lib/gitlab-client.js +1025 -0
  23. package/dist/lib/gitlab-client.js.map +1 -0
  24. package/dist/lib/logger.d.ts +2 -0
  25. package/dist/lib/logger.js +13 -0
  26. package/dist/lib/logger.js.map +1 -0
  27. package/dist/lib/network.d.ts +3 -0
  28. package/dist/lib/network.js +38 -0
  29. package/dist/lib/network.js.map +1 -0
  30. package/dist/lib/oauth.d.ts +29 -0
  31. package/dist/lib/oauth.js +220 -0
  32. package/dist/lib/oauth.js.map +1 -0
  33. package/dist/lib/output.d.ts +14 -0
  34. package/dist/lib/output.js +38 -0
  35. package/dist/lib/output.js.map +1 -0
  36. package/dist/lib/policy.d.ts +25 -0
  37. package/dist/lib/policy.js +48 -0
  38. package/dist/lib/policy.js.map +1 -0
  39. package/dist/lib/request-runtime.d.ts +26 -0
  40. package/dist/lib/request-runtime.js +323 -0
  41. package/dist/lib/request-runtime.js.map +1 -0
  42. package/dist/lib/sanitize.d.ts +1 -0
  43. package/dist/lib/sanitize.js +21 -0
  44. package/dist/lib/sanitize.js.map +1 -0
  45. package/dist/lib/session-capacity.d.ts +8 -0
  46. package/dist/lib/session-capacity.js +7 -0
  47. package/dist/lib/session-capacity.js.map +1 -0
  48. package/dist/server/build-server.d.ts +3 -0
  49. package/dist/server/build-server.js +13 -0
  50. package/dist/server/build-server.js.map +1 -0
  51. package/dist/tools/gitlab.d.ts +9 -0
  52. package/dist/tools/gitlab.js +2576 -0
  53. package/dist/tools/gitlab.js.map +1 -0
  54. package/dist/tools/health.d.ts +2 -0
  55. package/dist/tools/health.js +21 -0
  56. package/dist/tools/health.js.map +1 -0
  57. package/dist/tools/mr-code-context.d.ts +38 -0
  58. package/dist/tools/mr-code-context.js +330 -0
  59. package/dist/tools/mr-code-context.js.map +1 -0
  60. package/{src/types/context.ts → dist/types/context.d.ts} +5 -6
  61. package/dist/types/context.js +2 -0
  62. package/dist/types/context.js.map +1 -0
  63. package/docs/architecture.md +10 -10
  64. package/docs/configuration.md +12 -7
  65. package/docs/mcp-integration-testing-best-practices.md +981 -0
  66. package/package.json +13 -1
  67. package/.dockerignore +0 -7
  68. package/.editorconfig +0 -9
  69. package/.env.example +0 -75
  70. package/.github/workflows/nodejs.yml +0 -31
  71. package/.github/workflows/npm-publish.yml +0 -31
  72. package/.husky/pre-commit +0 -1
  73. package/.nvmrc +0 -1
  74. package/.prettierrc.json +0 -6
  75. package/Dockerfile +0 -20
  76. package/docker-compose.yml +0 -10
  77. package/eslint.config.js +0 -23
  78. package/scripts/get-oauth-token.example.sh +0 -15
  79. package/src/config/env.ts +0 -171
  80. package/src/http.ts +0 -620
  81. package/src/index.ts +0 -77
  82. package/src/lib/auth-context.ts +0 -19
  83. package/src/lib/gitlab-client.ts +0 -1810
  84. package/src/lib/logger.ts +0 -17
  85. package/src/lib/network.ts +0 -45
  86. package/src/lib/oauth.ts +0 -287
  87. package/src/lib/output.ts +0 -51
  88. package/src/lib/policy.ts +0 -78
  89. package/src/lib/request-runtime.ts +0 -376
  90. package/src/lib/sanitize.ts +0 -25
  91. package/src/lib/session-capacity.ts +0 -14
  92. package/src/server/build-server.ts +0 -17
  93. package/src/tools/gitlab.ts +0 -3135
  94. package/src/tools/health.ts +0 -27
  95. package/src/tools/mr-code-context.ts +0 -473
  96. package/tests/auth-context.test.ts +0 -102
  97. package/tests/gitlab-client.test.ts +0 -672
  98. package/tests/graphql-guard.test.ts +0 -121
  99. package/tests/integration/agent-loop.integration.test.ts +0 -558
  100. package/tests/integration/server.integration.test.ts +0 -543
  101. package/tests/mr-code-context.test.ts +0 -600
  102. package/tests/oauth.test.ts +0 -43
  103. package/tests/output.test.ts +0 -186
  104. package/tests/policy.test.ts +0 -324
  105. package/tests/request-runtime.test.ts +0 -252
  106. package/tests/sanitize.test.ts +0 -123
  107. package/tests/session-capacity.test.ts +0 -49
  108. package/tests/upload-reference.test.ts +0 -88
  109. package/tsconfig.build.json +0 -11
  110. package/tsconfig.json +0 -21
  111. package/vitest.config.ts +0 -12
@@ -1,186 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { OutputFormatter } from "../src/lib/output.js";
4
-
5
- describe("OutputFormatter", () => {
6
- describe("json mode", () => {
7
- it("formats objects with 2-space indentation", () => {
8
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
9
- const result = formatter.format({ hello: "world", count: 42 });
10
-
11
- expect(result.text).toBe(JSON.stringify({ hello: "world", count: 42 }, null, 2));
12
- expect(result.truncated).toBe(false);
13
- expect(result.bytes).toBeGreaterThan(0);
14
- });
15
-
16
- it("formats arrays", () => {
17
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
18
- const result = formatter.format([1, 2, 3]);
19
-
20
- expect(result.text).toBe(JSON.stringify([1, 2, 3], null, 2));
21
- expect(result.truncated).toBe(false);
22
- });
23
-
24
- it("formats null", () => {
25
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
26
- const result = formatter.format(null);
27
-
28
- expect(result.text).toBe("null");
29
- expect(result.truncated).toBe(false);
30
- });
31
-
32
- it("formats primitive strings", () => {
33
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
34
- const result = formatter.format("hello");
35
-
36
- expect(result.text).toBe('"hello"');
37
- expect(result.truncated).toBe(false);
38
- });
39
-
40
- it("formats numbers", () => {
41
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
42
- const result = formatter.format(42);
43
-
44
- expect(result.text).toBe("42");
45
- expect(result.truncated).toBe(false);
46
- });
47
-
48
- it("formats booleans", () => {
49
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
50
- const result = formatter.format(true);
51
-
52
- expect(result.text).toBe("true");
53
- });
54
- });
55
-
56
- describe("compact-json mode", () => {
57
- it("formats without whitespace", () => {
58
- const formatter = new OutputFormatter({ responseMode: "compact-json", maxBytes: 10_000 });
59
- const result = formatter.format({ hello: "world", nested: { a: 1 } });
60
-
61
- expect(result.text).toBe('{"hello":"world","nested":{"a":1}}');
62
- expect(result.truncated).toBe(false);
63
- });
64
-
65
- it("formats arrays compactly", () => {
66
- const formatter = new OutputFormatter({ responseMode: "compact-json", maxBytes: 10_000 });
67
- const result = formatter.format([1, 2, 3]);
68
-
69
- expect(result.text).toBe("[1,2,3]");
70
- });
71
- });
72
-
73
- describe("yaml mode", () => {
74
- it("formats objects as YAML", () => {
75
- const formatter = new OutputFormatter({ responseMode: "yaml", maxBytes: 10_000 });
76
- const result = formatter.format({ name: "test", value: 123 });
77
-
78
- expect(result.text).toContain("name: test");
79
- expect(result.text).toContain("value: 123");
80
- expect(result.truncated).toBe(false);
81
- });
82
-
83
- it("formats nested objects as YAML", () => {
84
- const formatter = new OutputFormatter({ responseMode: "yaml", maxBytes: 10_000 });
85
- const result = formatter.format({ outer: { inner: "value" } });
86
-
87
- expect(result.text).toContain("outer:");
88
- expect(result.text).toContain("inner: value");
89
- });
90
-
91
- it("formats arrays as YAML", () => {
92
- const formatter = new OutputFormatter({ responseMode: "yaml", maxBytes: 10_000 });
93
- const result = formatter.format({ items: ["a", "b", "c"] });
94
-
95
- expect(result.text).toContain("items:");
96
- expect(result.text).toContain("- a");
97
- expect(result.text).toContain("- b");
98
- expect(result.text).toContain("- c");
99
- });
100
- });
101
-
102
- describe("truncation", () => {
103
- it("truncates output exceeding maxBytes", () => {
104
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 20 });
105
- const result = formatter.format({ key: "this is a very long value that exceeds the limit" });
106
-
107
- expect(result.truncated).toBe(true);
108
- expect(result.text).toContain("[truncated");
109
- expect(result.bytes).toBeGreaterThan(20);
110
- });
111
-
112
- it("does not truncate when output fits within maxBytes", () => {
113
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 100_000 });
114
- const result = formatter.format({ small: "data" });
115
-
116
- expect(result.truncated).toBe(false);
117
- expect(result.text).not.toContain("[truncated");
118
- });
119
-
120
- it("reports correct original byte count even when truncated", () => {
121
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10 });
122
- const data = { message: "hello world this is a longer message" };
123
- const result = formatter.format(data);
124
-
125
- const expectedFull = JSON.stringify(data, null, 2);
126
- const expectedBytes = Buffer.byteLength(expectedFull, "utf8");
127
-
128
- expect(result.truncated).toBe(true);
129
- expect(result.bytes).toBe(expectedBytes);
130
- });
131
-
132
- it("handles exact boundary correctly", () => {
133
- const data = { a: 1 };
134
- const serialized = JSON.stringify(data, null, 2);
135
- const exactBytes = Buffer.byteLength(serialized, "utf8");
136
-
137
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: exactBytes });
138
- const result = formatter.format(data);
139
-
140
- expect(result.truncated).toBe(false);
141
- expect(result.bytes).toBe(exactBytes);
142
- });
143
-
144
- it("handles multi-byte characters in truncation", () => {
145
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 30 });
146
- const result = formatter.format({ emoji: "Hello 🌍🌍🌍🌍🌍" });
147
-
148
- expect(result.truncated).toBe(true);
149
- expect(result.text).toContain("[truncated");
150
- });
151
- });
152
-
153
- describe("edge cases", () => {
154
- it("handles empty object", () => {
155
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
156
- const result = formatter.format({});
157
-
158
- expect(result.text).toBe("{}");
159
- expect(result.truncated).toBe(false);
160
- });
161
-
162
- it("handles empty array", () => {
163
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
164
- const result = formatter.format([]);
165
-
166
- expect(result.text).toBe("[]");
167
- expect(result.truncated).toBe(false);
168
- });
169
-
170
- it("throws on undefined because JSON.stringify returns undefined", () => {
171
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
172
- // JSON.stringify(undefined) returns the JS value undefined, which is
173
- // not a valid argument for Buffer.byteLength.
174
- expect(() => formatter.format(undefined)).toThrow();
175
- });
176
-
177
- it("handles deeply nested objects", () => {
178
- const formatter = new OutputFormatter({ responseMode: "json", maxBytes: 10_000 });
179
- const deep = { a: { b: { c: { d: { e: "deep" } } } } };
180
- const result = formatter.format(deep);
181
-
182
- expect(result.text).toContain('"deep"');
183
- expect(result.truncated).toBe(false);
184
- });
185
- });
186
- });
@@ -1,324 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- import { ToolPolicyEngine, type ToolPolicyMeta } from "../src/lib/policy.js";
4
-
5
- const defaultFeatures = {
6
- wiki: true,
7
- milestone: true,
8
- pipeline: true,
9
- release: true
10
- };
11
-
12
- describe("ToolPolicyEngine", () => {
13
- describe("filterTools", () => {
14
- it("blocks mutating tools in readonly mode", () => {
15
- const engine = new ToolPolicyEngine({
16
- readOnlyMode: true,
17
- allowedTools: [],
18
- enabledFeatures: defaultFeatures
19
- });
20
-
21
- expect(
22
- engine.filterTools([
23
- { name: "readonly", mutating: false },
24
- { name: "mutate", mutating: true }
25
- ])
26
- ).toEqual([{ name: "readonly", mutating: false }]);
27
- });
28
-
29
- it("allows all tools when no restrictions are set", () => {
30
- const engine = new ToolPolicyEngine({
31
- readOnlyMode: false,
32
- allowedTools: [],
33
- enabledFeatures: defaultFeatures
34
- });
35
-
36
- const tools: ToolPolicyMeta[] = [
37
- { name: "tool_a", mutating: false },
38
- { name: "tool_b", mutating: true },
39
- { name: "tool_c", mutating: false }
40
- ];
41
-
42
- expect(engine.filterTools(tools)).toEqual(tools);
43
- });
44
-
45
- it("applies allowlist and deny regex", () => {
46
- const engine = new ToolPolicyEngine({
47
- readOnlyMode: false,
48
- allowedTools: ["gitlab_get_project", "gitlab_list_projects"],
49
- deniedToolsRegex: /^gitlab_list_/,
50
- enabledFeatures: defaultFeatures
51
- });
52
-
53
- expect(
54
- engine.filterTools([
55
- { name: "gitlab_get_project", mutating: false },
56
- { name: "gitlab_list_projects", mutating: false },
57
- { name: "gitlab_create_issue", mutating: true }
58
- ])
59
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
60
- });
61
-
62
- it("supports allowlist names without gitlab_ prefix", () => {
63
- const engine = new ToolPolicyEngine({
64
- readOnlyMode: false,
65
- allowedTools: ["get_project"],
66
- enabledFeatures: defaultFeatures
67
- });
68
-
69
- expect(
70
- engine.filterTools([
71
- { name: "gitlab_get_project", mutating: false },
72
- { name: "gitlab_list_projects", mutating: false }
73
- ])
74
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
75
- });
76
-
77
- it("supports allowlist names with gitlab_ prefix", () => {
78
- const engine = new ToolPolicyEngine({
79
- readOnlyMode: false,
80
- allowedTools: ["gitlab_get_project"],
81
- enabledFeatures: defaultFeatures
82
- });
83
-
84
- expect(
85
- engine.filterTools([
86
- { name: "gitlab_get_project", mutating: false },
87
- { name: "gitlab_list_projects", mutating: false }
88
- ])
89
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
90
- });
91
-
92
- it("respects feature flags", () => {
93
- const engine = new ToolPolicyEngine({
94
- readOnlyMode: false,
95
- allowedTools: [],
96
- enabledFeatures: {
97
- wiki: false,
98
- milestone: false,
99
- pipeline: true,
100
- release: true
101
- }
102
- });
103
-
104
- expect(
105
- engine.filterTools([
106
- { name: "wiki", mutating: false, requiresFeature: "wiki" },
107
- { name: "pipeline", mutating: false, requiresFeature: "pipeline" }
108
- ])
109
- ).toEqual([{ name: "pipeline", mutating: false, requiresFeature: "pipeline" }]);
110
- });
111
-
112
- it("allows tools without requiresFeature even when features are disabled", () => {
113
- const engine = new ToolPolicyEngine({
114
- readOnlyMode: false,
115
- allowedTools: [],
116
- enabledFeatures: {
117
- wiki: false,
118
- milestone: false,
119
- pipeline: false,
120
- release: false
121
- }
122
- });
123
-
124
- expect(
125
- engine.filterTools([
126
- { name: "generic_tool", mutating: false },
127
- { name: "wiki_tool", mutating: false, requiresFeature: "wiki" }
128
- ])
129
- ).toEqual([{ name: "generic_tool", mutating: false }]);
130
- });
131
-
132
- it("applies deniedToolsRegex without allowlist", () => {
133
- const engine = new ToolPolicyEngine({
134
- readOnlyMode: false,
135
- allowedTools: [],
136
- deniedToolsRegex: /^gitlab_delete_/,
137
- enabledFeatures: defaultFeatures
138
- });
139
-
140
- expect(
141
- engine.filterTools([
142
- { name: "gitlab_get_project", mutating: false },
143
- { name: "gitlab_delete_project", mutating: true },
144
- { name: "gitlab_delete_issue", mutating: true }
145
- ])
146
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
147
- });
148
-
149
- it("handles empty tools array", () => {
150
- const engine = new ToolPolicyEngine({
151
- readOnlyMode: false,
152
- allowedTools: [],
153
- enabledFeatures: defaultFeatures
154
- });
155
-
156
- expect(engine.filterTools([])).toEqual([]);
157
- });
158
-
159
- it("combines readOnly mode with feature flags", () => {
160
- const engine = new ToolPolicyEngine({
161
- readOnlyMode: true,
162
- allowedTools: [],
163
- enabledFeatures: {
164
- wiki: true,
165
- milestone: true,
166
- pipeline: true,
167
- release: true
168
- }
169
- });
170
-
171
- expect(
172
- engine.filterTools([
173
- { name: "read_wiki", mutating: false, requiresFeature: "wiki" },
174
- { name: "create_wiki", mutating: true, requiresFeature: "wiki" },
175
- { name: "get_project", mutating: false }
176
- ])
177
- ).toEqual([
178
- { name: "read_wiki", mutating: false, requiresFeature: "wiki" },
179
- { name: "get_project", mutating: false }
180
- ]);
181
- });
182
-
183
- it("handles allowlist with whitespace-padded names", () => {
184
- const engine = new ToolPolicyEngine({
185
- readOnlyMode: false,
186
- allowedTools: [" get_project "],
187
- enabledFeatures: defaultFeatures
188
- });
189
-
190
- expect(
191
- engine.filterTools([
192
- { name: "gitlab_get_project", mutating: false },
193
- { name: "gitlab_list_projects", mutating: false }
194
- ])
195
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
196
- });
197
-
198
- it("handles allowlist with empty strings", () => {
199
- const engine = new ToolPolicyEngine({
200
- readOnlyMode: false,
201
- allowedTools: ["", " ", "get_project"],
202
- enabledFeatures: defaultFeatures
203
- });
204
-
205
- expect(
206
- engine.filterTools([
207
- { name: "gitlab_get_project", mutating: false },
208
- { name: "gitlab_list_projects", mutating: false }
209
- ])
210
- ).toEqual([{ name: "gitlab_get_project", mutating: false }]);
211
- });
212
-
213
- it("respects release feature flag", () => {
214
- const engine = new ToolPolicyEngine({
215
- readOnlyMode: false,
216
- allowedTools: [],
217
- enabledFeatures: {
218
- wiki: true,
219
- milestone: true,
220
- pipeline: true,
221
- release: false
222
- }
223
- });
224
-
225
- expect(
226
- engine.filterTools([
227
- { name: "list_releases", mutating: false, requiresFeature: "release" },
228
- { name: "list_pipelines", mutating: false, requiresFeature: "pipeline" }
229
- ])
230
- ).toEqual([{ name: "list_pipelines", mutating: false, requiresFeature: "pipeline" }]);
231
- });
232
- });
233
-
234
- describe("assertCanExecute", () => {
235
- it("does not throw for enabled tools", () => {
236
- const engine = new ToolPolicyEngine({
237
- readOnlyMode: false,
238
- allowedTools: [],
239
- enabledFeatures: defaultFeatures
240
- });
241
-
242
- expect(() => {
243
- engine.assertCanExecute({ name: "any_tool", mutating: false });
244
- }).not.toThrow();
245
- });
246
-
247
- it("throws for disabled tools in readonly mode", () => {
248
- const engine = new ToolPolicyEngine({
249
- readOnlyMode: true,
250
- allowedTools: [],
251
- enabledFeatures: defaultFeatures
252
- });
253
-
254
- expect(() => {
255
- engine.assertCanExecute({ name: "create_issue", mutating: true });
256
- }).toThrow("disabled by policy");
257
- });
258
-
259
- it("throws for tools not in allowlist", () => {
260
- const engine = new ToolPolicyEngine({
261
- readOnlyMode: false,
262
- allowedTools: ["gitlab_get_project"],
263
- enabledFeatures: defaultFeatures
264
- });
265
-
266
- expect(() => {
267
- engine.assertCanExecute({ name: "gitlab_list_projects", mutating: false });
268
- }).toThrow("disabled by policy");
269
- });
270
-
271
- it("throws for tools matching denied regex", () => {
272
- const engine = new ToolPolicyEngine({
273
- readOnlyMode: false,
274
- allowedTools: [],
275
- deniedToolsRegex: /^gitlab_delete_/,
276
- enabledFeatures: defaultFeatures
277
- });
278
-
279
- expect(() => {
280
- engine.assertCanExecute({ name: "gitlab_delete_issue", mutating: true });
281
- }).toThrow("disabled by policy");
282
- });
283
-
284
- it("throws for tools requiring disabled features", () => {
285
- const engine = new ToolPolicyEngine({
286
- readOnlyMode: false,
287
- allowedTools: [],
288
- enabledFeatures: {
289
- wiki: false,
290
- milestone: true,
291
- pipeline: true,
292
- release: true
293
- }
294
- });
295
-
296
- expect(() => {
297
- engine.assertCanExecute({ name: "wiki_tool", mutating: false, requiresFeature: "wiki" });
298
- }).toThrow("disabled by policy");
299
- });
300
- });
301
-
302
- describe("isToolEnabled", () => {
303
- it("returns true for unrestricted tools", () => {
304
- const engine = new ToolPolicyEngine({
305
- readOnlyMode: false,
306
- allowedTools: [],
307
- enabledFeatures: defaultFeatures
308
- });
309
-
310
- expect(engine.isToolEnabled({ name: "any_tool", mutating: false })).toBe(true);
311
- });
312
-
313
- it("returns false for mutating tools in readonly mode", () => {
314
- const engine = new ToolPolicyEngine({
315
- readOnlyMode: true,
316
- allowedTools: [],
317
- enabledFeatures: defaultFeatures
318
- });
319
-
320
- expect(engine.isToolEnabled({ name: "create_something", mutating: true })).toBe(false);
321
- expect(engine.isToolEnabled({ name: "read_something", mutating: false })).toBe(true);
322
- });
323
- });
324
- });