@checkstack/gitops-backend 0.1.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.
@@ -0,0 +1,296 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { gitlabScraper } from "./gitlab-scraper";
3
+ import type { ScraperOptions, FetchFn } from "./types";
4
+ import type { Logger } from "@checkstack/backend-api";
5
+
6
+ const mockLogger: Logger = {
7
+ info: () => {},
8
+ error: () => {},
9
+ warn: () => {},
10
+ debug: () => {},
11
+ };
12
+
13
+ const BASE_OPTIONS: Omit<ScraperOptions, "fetch"> = {
14
+ target: "my-group",
15
+ pathPattern: ".checkstack/**/*.yaml",
16
+ authToken: "glpat-test_token",
17
+ logger: mockLogger,
18
+ };
19
+
20
+ /**
21
+ * Creates a mock fetch for GitLab API requests.
22
+ * Uses if/else URL matching to handle overlapping patterns correctly.
23
+ */
24
+ function createGitLabMockFetch(config: {
25
+ projects: Array<{
26
+ id: number;
27
+ path_with_namespace: string;
28
+ default_branch: string;
29
+ tree: Array<{ id: string; name: string; type: "blob" | "tree"; path: string }>;
30
+ files: Record<string, string>;
31
+ }>;
32
+ /** If true, the target is treated as a group with multiple projects. */
33
+ groupMode?: boolean;
34
+ }): FetchFn {
35
+ return async (input: RequestInfo | URL) => {
36
+ const url = typeof input === "string" ? input : input.toString();
37
+
38
+ // Group projects listing
39
+ if (url.includes("/groups/") && url.includes("/projects")) {
40
+ return new Response(JSON.stringify(config.projects), {
41
+ headers: { "Content-Type": "application/json" },
42
+ });
43
+ }
44
+
45
+ // Check each project
46
+ for (const project of config.projects) {
47
+ // File content (raw)
48
+ const rawFilePattern = `/projects/${project.id}/repository/files/`;
49
+ if (url.includes(rawFilePattern) && url.includes("/raw")) {
50
+ // Extract file path from URL
51
+ const pathMatch = url.match(
52
+ /\/files\/([^/]+)\/raw/,
53
+ );
54
+ if (pathMatch) {
55
+ const decodedPath = decodeURIComponent(pathMatch[1]);
56
+ const content = project.files[decodedPath];
57
+ if (content) {
58
+ return new Response(content, {
59
+ headers: { "Content-Type": "text/plain" },
60
+ });
61
+ }
62
+ }
63
+ return new Response("Not Found", { status: 404 });
64
+ }
65
+
66
+ // Repository tree
67
+ const treePattern = `/projects/${project.id}/repository/tree`;
68
+ if (url.includes(treePattern)) {
69
+ return new Response(JSON.stringify(project.tree), {
70
+ headers: { "Content-Type": "application/json" },
71
+ });
72
+ }
73
+
74
+ // Project metadata (single project mode)
75
+ const encodedPath = encodeURIComponent(project.path_with_namespace);
76
+ if (url.includes(`/projects/${encodedPath}`)) {
77
+ return new Response(JSON.stringify(project), {
78
+ headers: { "Content-Type": "application/json" },
79
+ });
80
+ }
81
+ }
82
+
83
+ return new Response("Not Found", { status: 404 });
84
+ };
85
+ }
86
+
87
+ describe("gitlabScraper", () => {
88
+ const sampleProject = {
89
+ id: 42,
90
+ path_with_namespace: "my-group/my-project",
91
+ default_branch: "main",
92
+ tree: [
93
+ { id: "a1", name: "systems.yaml", type: "blob" as const, path: ".checkstack/systems.yaml" },
94
+ { id: "a2", name: "README.md", type: "blob" as const, path: "README.md" },
95
+ { id: "a3", name: "src", type: "tree" as const, path: "src" },
96
+ ],
97
+ files: {
98
+ ".checkstack/systems.yaml": "apiVersion: checkstack.io/v1alpha1\nkind: System",
99
+ },
100
+ };
101
+
102
+ it("discovers files from a single project target", async () => {
103
+ const mockFetch = createGitLabMockFetch({
104
+ projects: [sampleProject],
105
+ });
106
+
107
+ const files = await gitlabScraper.discoverFiles({
108
+ ...BASE_OPTIONS,
109
+ target: "my-group/my-project",
110
+ fetch: mockFetch,
111
+ });
112
+
113
+ expect(files).toHaveLength(1);
114
+ expect(files[0].repository).toBe("my-group/my-project");
115
+ expect(files[0].filePath).toBe(".checkstack/systems.yaml");
116
+ expect(files[0].content).toContain("checkstack.io/v1alpha1");
117
+ expect(files[0].branch).toBe("main");
118
+ });
119
+
120
+ it("enumerates projects from a group target", async () => {
121
+ const mockFetch = createGitLabMockFetch({
122
+ projects: [
123
+ sampleProject,
124
+ {
125
+ id: 43,
126
+ path_with_namespace: "my-group/empty-project",
127
+ default_branch: "develop",
128
+ tree: [],
129
+ files: {},
130
+ },
131
+ ],
132
+ groupMode: true,
133
+ });
134
+
135
+ const files = await gitlabScraper.discoverFiles({
136
+ ...BASE_OPTIONS,
137
+ fetch: mockFetch,
138
+ });
139
+
140
+ expect(files).toHaveLength(1);
141
+ expect(files[0].repository).toBe("my-group/my-project");
142
+ });
143
+
144
+ it("filters files using minimatch pattern", async () => {
145
+ const project = {
146
+ ...sampleProject,
147
+ tree: [
148
+ { id: "a1", name: "systems.yaml", type: "blob" as const, path: ".checkstack/systems.yaml" },
149
+ { id: "a2", name: "nested.yaml", type: "blob" as const, path: ".checkstack/deep/nested.yaml" },
150
+ { id: "a3", name: "file.yaml", type: "blob" as const, path: "other/file.yaml" },
151
+ { id: "a4", name: "readme.md", type: "blob" as const, path: ".checkstack/readme.md" },
152
+ ],
153
+ files: {
154
+ ".checkstack/systems.yaml": "content1",
155
+ ".checkstack/deep/nested.yaml": "content2",
156
+ },
157
+ };
158
+
159
+ const mockFetch = createGitLabMockFetch({ projects: [project] });
160
+
161
+ const files = await gitlabScraper.discoverFiles({
162
+ ...BASE_OPTIONS,
163
+ target: "my-group/my-project",
164
+ fetch: mockFetch,
165
+ });
166
+
167
+ expect(files).toHaveLength(2);
168
+ expect(files[0].filePath).toBe(".checkstack/systems.yaml");
169
+ expect(files[1].filePath).toBe(".checkstack/deep/nested.yaml");
170
+ });
171
+
172
+ it("handles pagination via x-next-page header", async () => {
173
+ let requestCount = 0;
174
+
175
+ const mockFetch: FetchFn = async (input) => {
176
+ const url = typeof input === "string" ? input : input.toString();
177
+
178
+ if (url.includes("/groups/") && url.includes("/projects")) {
179
+ requestCount++;
180
+ if (!url.includes("page=2")) {
181
+ return new Response(
182
+ JSON.stringify([
183
+ { id: 1, path_with_namespace: "g/p1", default_branch: "main" },
184
+ ]),
185
+ {
186
+ headers: {
187
+ "Content-Type": "application/json",
188
+ "x-next-page": "2",
189
+ },
190
+ },
191
+ );
192
+ }
193
+ return new Response(
194
+ JSON.stringify([
195
+ { id: 2, path_with_namespace: "g/p2", default_branch: "main" },
196
+ ]),
197
+ { headers: { "Content-Type": "application/json" } },
198
+ );
199
+ }
200
+
201
+ if (url.includes("/repository/tree")) {
202
+ return new Response(JSON.stringify([]), {
203
+ headers: { "Content-Type": "application/json" },
204
+ });
205
+ }
206
+
207
+ return new Response("Not Found", { status: 404 });
208
+ };
209
+
210
+ await gitlabScraper.discoverFiles({
211
+ ...BASE_OPTIONS,
212
+ fetch: mockFetch,
213
+ });
214
+
215
+ // Should have made 2 requests for projects (page 1 + page 2)
216
+ expect(requestCount).toBe(2);
217
+ });
218
+
219
+ it("continues on individual file fetch errors", async () => {
220
+ const project = {
221
+ id: 42,
222
+ path_with_namespace: "my-group/my-project",
223
+ default_branch: "main",
224
+ tree: [
225
+ { id: "a1", name: "good.yaml", type: "blob" as const, path: ".checkstack/good.yaml" },
226
+ { id: "a2", name: "bad.yaml", type: "blob" as const, path: ".checkstack/bad.yaml" },
227
+ ],
228
+ files: {
229
+ ".checkstack/good.yaml": "good-content",
230
+ // bad.yaml deliberately missing from files to simulate error
231
+ },
232
+ };
233
+
234
+ const mockFetch = createGitLabMockFetch({ projects: [project] });
235
+
236
+ const files = await gitlabScraper.discoverFiles({
237
+ ...BASE_OPTIONS,
238
+ target: "my-group/my-project",
239
+ fetch: mockFetch,
240
+ });
241
+
242
+ expect(files).toHaveLength(1);
243
+ expect(files[0].filePath).toBe(".checkstack/good.yaml");
244
+ });
245
+
246
+ it("uses custom baseUrl for self-managed instances", async () => {
247
+ const selfHostedUrl = "https://gitlab.acme.corp/api/v4";
248
+ const requestedUrls: string[] = [];
249
+
250
+ const mockFetch: FetchFn = async (input) => {
251
+ const url = typeof input === "string" ? input : input.toString();
252
+ requestedUrls.push(url);
253
+
254
+ if (url.includes("/repository/tree")) {
255
+ return new Response(
256
+ JSON.stringify([
257
+ { id: "a1", name: "sys.yaml", type: "blob", path: ".checkstack/sys.yaml" },
258
+ ]),
259
+ { headers: { "Content-Type": "application/json" } },
260
+ );
261
+ }
262
+
263
+ if (url.includes("/repository/files/") && url.includes("/raw")) {
264
+ return new Response("yaml-content", {
265
+ headers: { "Content-Type": "text/plain" },
266
+ });
267
+ }
268
+
269
+ if (url.includes("/projects/")) {
270
+ return new Response(
271
+ JSON.stringify({
272
+ id: 99,
273
+ path_with_namespace: "acme/infra",
274
+ default_branch: "main",
275
+ }),
276
+ { headers: { "Content-Type": "application/json" } },
277
+ );
278
+ }
279
+
280
+ return new Response("Not Found", { status: 404 });
281
+ };
282
+
283
+ const files = await gitlabScraper.discoverFiles({
284
+ ...BASE_OPTIONS,
285
+ target: "acme/infra",
286
+ baseUrl: selfHostedUrl,
287
+ fetch: mockFetch,
288
+ });
289
+
290
+ expect(files).toHaveLength(1);
291
+ // All requests should use the self-hosted URL, not gitlab.com
292
+ for (const url of requestedUrls) {
293
+ expect(url).toStartWith(selfHostedUrl);
294
+ }
295
+ });
296
+ });
@@ -0,0 +1,242 @@
1
+ import { minimatch } from "minimatch";
2
+ import type { DiscoveredFile, ScraperOptions, Scraper, FetchFn } from "./types";
3
+
4
+ const DEFAULT_GITLAB_API_URL = "https://gitlab.com/api/v4";
5
+
6
+ // ─── GitLab API Types ──────────────────────────────────────────────────────
7
+
8
+ interface GitLabProject {
9
+ id: number;
10
+ path_with_namespace: string;
11
+ default_branch: string;
12
+ }
13
+
14
+ interface GitLabTreeItem {
15
+ id: string;
16
+ name: string;
17
+ type: "blob" | "tree";
18
+ path: string;
19
+ }
20
+
21
+ // ─── Helpers ───────────────────────────────────────────────────────────────
22
+
23
+ /**
24
+ * Makes an authenticated request to the GitLab API.
25
+ */
26
+ async function gitlabFetch(params: {
27
+ url: string;
28
+ authToken: string;
29
+ fetchFn: FetchFn;
30
+ }): Promise<Response> {
31
+ const { url, authToken, fetchFn } = params;
32
+ return fetchFn(url, {
33
+ headers: {
34
+ "PRIVATE-TOKEN": authToken,
35
+ },
36
+ });
37
+ }
38
+
39
+ /**
40
+ * Paginates through a GitLab API endpoint that uses `x-next-page` header.
41
+ */
42
+ async function paginateGitLab<T>(params: {
43
+ initialUrl: string;
44
+ authToken: string;
45
+ fetchFn: FetchFn;
46
+ }): Promise<T[]> {
47
+ const { initialUrl, authToken, fetchFn } = params;
48
+ const results: T[] = [];
49
+ let url: string | undefined = initialUrl;
50
+
51
+ while (url) {
52
+ const response = await gitlabFetch({ url, authToken, fetchFn });
53
+ if (!response.ok) {
54
+ throw new Error(
55
+ `GitLab API error: ${response.status} ${response.statusText} (${url})`,
56
+ );
57
+ }
58
+
59
+ const data = (await response.json()) as T[];
60
+ results.push(...data);
61
+
62
+ const nextPage = response.headers.get("x-next-page");
63
+ if (nextPage && nextPage.trim() !== "") {
64
+ // Build next page URL
65
+ const urlObj: URL = new URL(url);
66
+ urlObj.searchParams.set("page", nextPage);
67
+ url = urlObj.toString();
68
+ } else {
69
+ url = undefined;
70
+ }
71
+ }
72
+
73
+ return results;
74
+ }
75
+
76
+ // ─── Core Logic ────────────────────────────────────────────────────────────
77
+
78
+ /**
79
+ * Enumerates projects for a group target (including subgroups).
80
+ */
81
+ async function enumerateProjects(params: {
82
+ target: string;
83
+ authToken: string;
84
+ fetchFn: FetchFn;
85
+ apiUrl: string;
86
+ }): Promise<GitLabProject[]> {
87
+ const { target, authToken, fetchFn, apiUrl } = params;
88
+ const encodedTarget = encodeURIComponent(target);
89
+ const url = `${apiUrl}/groups/${encodedTarget}/projects?include_subgroups=true&per_page=100`;
90
+ return paginateGitLab<GitLabProject>({ initialUrl: url, authToken, fetchFn });
91
+ }
92
+
93
+ /**
94
+ * Gets the file tree for a project and filters paths using minimatch.
95
+ */
96
+ async function getMatchingFiles(params: {
97
+ project: GitLabProject;
98
+ pathPattern: string;
99
+ authToken: string;
100
+ fetchFn: FetchFn;
101
+ apiUrl: string;
102
+ }): Promise<string[]> {
103
+ const { project, pathPattern, authToken, fetchFn, apiUrl } = params;
104
+
105
+ const url = `${apiUrl}/projects/${project.id}/repository/tree?recursive=true&ref=${encodeURIComponent(project.default_branch)}&per_page=100`;
106
+ const items = await paginateGitLab<GitLabTreeItem>({
107
+ initialUrl: url,
108
+ authToken,
109
+ fetchFn,
110
+ });
111
+
112
+ return items
113
+ .filter((item) => item.type === "blob")
114
+ .map((item) => item.path)
115
+ .filter((path) => minimatch(path, pathPattern));
116
+ }
117
+
118
+ /**
119
+ * Fetches the raw content of a file from a GitLab project.
120
+ */
121
+ async function fetchFileContent(params: {
122
+ projectId: number;
123
+ filePath: string;
124
+ branch: string;
125
+ authToken: string;
126
+ fetchFn: FetchFn;
127
+ apiUrl: string;
128
+ }): Promise<string> {
129
+ const { projectId, filePath, branch, authToken, fetchFn, apiUrl } = params;
130
+ const encodedPath = encodeURIComponent(filePath);
131
+ const url = `${apiUrl}/projects/${projectId}/repository/files/${encodedPath}/raw?ref=${encodeURIComponent(branch)}`;
132
+ const response = await gitlabFetch({ url, authToken, fetchFn });
133
+
134
+ if (!response.ok) {
135
+ throw new Error(
136
+ `GitLab API error fetching ${filePath} from project ${projectId}: ${response.status} ${response.statusText}`,
137
+ );
138
+ }
139
+
140
+ return response.text();
141
+ }
142
+
143
+ // ─── Scraper ───────────────────────────────────────────────────────────────
144
+
145
+ /**
146
+ * GitLab scraper implementation.
147
+ *
148
+ * Supports:
149
+ * - Group target: enumerates all projects including subgroups with pagination
150
+ * - Single project target: `group/project` format
151
+ * - Default branch resolution per-project
152
+ * - Recursive tree walking with minimatch filtering
153
+ * - Custom base URL for self-managed GitLab instances
154
+ */
155
+ export const gitlabScraper: Scraper = {
156
+ async discoverFiles(options: ScraperOptions): Promise<DiscoveredFile[]> {
157
+ const {
158
+ target,
159
+ pathPattern,
160
+ authToken,
161
+ baseUrl,
162
+ logger,
163
+ fetch: fetchFn = globalThis.fetch,
164
+ } = options;
165
+
166
+ const apiUrl = baseUrl ?? DEFAULT_GITLAB_API_URL;
167
+ const isSingleProject = target.includes("/");
168
+ const files: DiscoveredFile[] = [];
169
+
170
+ let projects: GitLabProject[];
171
+
172
+ if (isSingleProject) {
173
+ // Single project mode: fetch project metadata directly
174
+ const encodedTarget = encodeURIComponent(target);
175
+ const url = `${apiUrl}/projects/${encodedTarget}`;
176
+ const response = await gitlabFetch({ url, authToken, fetchFn });
177
+ if (!response.ok) {
178
+ throw new Error(
179
+ `GitLab API error fetching project ${target}: ${response.status} ${response.statusText}`,
180
+ );
181
+ }
182
+ projects = [(await response.json()) as GitLabProject];
183
+ } else {
184
+ projects = await enumerateProjects({
185
+ target,
186
+ authToken,
187
+ fetchFn,
188
+ apiUrl,
189
+ });
190
+ }
191
+
192
+ logger.debug(
193
+ `GitLab scraper: found ${projects.length} project(s) for target "${target}"`,
194
+ );
195
+
196
+ for (const project of projects) {
197
+ try {
198
+ const matchingPaths = await getMatchingFiles({
199
+ project,
200
+ pathPattern,
201
+ authToken,
202
+ fetchFn,
203
+ apiUrl,
204
+ });
205
+
206
+ logger.debug(
207
+ `GitLab scraper: ${matchingPaths.length} matching file(s) in ${project.path_with_namespace}`,
208
+ );
209
+
210
+ for (const filePath of matchingPaths) {
211
+ try {
212
+ const content = await fetchFileContent({
213
+ projectId: project.id,
214
+ filePath,
215
+ branch: project.default_branch,
216
+ authToken,
217
+ fetchFn,
218
+ apiUrl,
219
+ });
220
+
221
+ files.push({
222
+ repository: project.path_with_namespace,
223
+ filePath,
224
+ content,
225
+ branch: project.default_branch,
226
+ });
227
+ } catch (error) {
228
+ logger.error(
229
+ `GitLab scraper: failed to fetch ${filePath} from ${project.path_with_namespace}: ${error}`,
230
+ );
231
+ }
232
+ }
233
+ } catch (error) {
234
+ logger.error(
235
+ `GitLab scraper: failed to process project ${project.path_with_namespace}: ${error}`,
236
+ );
237
+ }
238
+ }
239
+
240
+ return files;
241
+ },
242
+ };
@@ -0,0 +1,52 @@
1
+ import type { Logger } from "@checkstack/backend-api";
2
+
3
+ /**
4
+ * A fetch function compatible with the standard Fetch API.
5
+ * We use a custom type alias instead of `typeof globalThis.fetch` because Bun's
6
+ * native fetch includes a non-standard `preconnect` method that breaks mock implementations.
7
+ */
8
+ export type FetchFn = (
9
+ input: RequestInfo | URL,
10
+ init?: RequestInit,
11
+ ) => Promise<Response>;
12
+
13
+ /**
14
+ * A file discovered by a scraper from a Git provider.
15
+ */
16
+ export interface DiscoveredFile {
17
+ /** Full repository identifier (e.g., "my-org/my-repo"). */
18
+ repository: string;
19
+ /** Path to the file within the repository. */
20
+ filePath: string;
21
+ /** Raw file content (decoded from base64 if needed). */
22
+ content: string;
23
+ /** The branch the file was read from. */
24
+ branch: string;
25
+ }
26
+
27
+ /**
28
+ * Options passed to a scraper's discoverFiles method.
29
+ */
30
+ export interface ScraperOptions {
31
+ /** Target: org/user name or "owner/repo" for single-repo mode. */
32
+ target: string;
33
+ /** Glob pattern for matching file paths (e.g., ".checkstack/**\/*.yaml"). */
34
+ pathPattern: string;
35
+ /** Decrypted auth token for the Git provider API. */
36
+ authToken: string;
37
+ /** Custom API base URL for enterprise/on-prem installations. */
38
+ baseUrl?: string;
39
+ /** Logger for diagnostic output. */
40
+ logger: Logger;
41
+ /** Optional fetch override (defaults to global fetch). Used for testing. */
42
+ fetch?: FetchFn;
43
+ }
44
+
45
+ /**
46
+ * Interface for Git provider scrapers.
47
+ * Each provider type (GitHub, GitLab) implements this interface.
48
+ */
49
+ export interface Scraper {
50
+ /** Discover all YAML descriptor files matching the configured path pattern. */
51
+ discoverFiles(options: ScraperOptions): Promise<DiscoveredFile[]>;
52
+ }
@@ -0,0 +1,86 @@
1
+ import { describe, it, expect } from "bun:test";
2
+ import { resolveSecrets } from "./secret-resolver";
3
+
4
+ const mockSecretStore = {
5
+ resolve: async (name: string): Promise<string> => {
6
+ const secrets: Record<string, string> = {
7
+ "prod-db-password": "s3cret!",
8
+ "api-key": "key-12345",
9
+ };
10
+ const value = secrets[name];
11
+ if (!value) throw new Error(`Secret not found: ${name}`);
12
+ return value;
13
+ },
14
+ };
15
+
16
+ describe("resolveSecrets", () => {
17
+ it("resolves a top-level secretRef", async () => {
18
+ const spec = { password: { secretRef: "prod-db-password" } };
19
+ const resolved = await resolveSecrets({
20
+ spec,
21
+ secretStore: mockSecretStore,
22
+ });
23
+ expect(resolved).toEqual({ password: "s3cret!" });
24
+ });
25
+
26
+ it("leaves plain strings untouched", async () => {
27
+ const spec = { host: "localhost", port: 5432 };
28
+ const resolved = await resolveSecrets({
29
+ spec,
30
+ secretStore: mockSecretStore,
31
+ });
32
+ expect(resolved).toEqual({ host: "localhost", port: 5432 });
33
+ });
34
+
35
+ it("resolves nested secretRefs", async () => {
36
+ const spec = {
37
+ connection: {
38
+ host: "db.internal",
39
+ password: { secretRef: "prod-db-password" },
40
+ },
41
+ };
42
+ const resolved = await resolveSecrets({
43
+ spec,
44
+ secretStore: mockSecretStore,
45
+ });
46
+ expect(resolved).toEqual({
47
+ connection: { host: "db.internal", password: "s3cret!" },
48
+ });
49
+ });
50
+
51
+ it("resolves secretRefs in arrays", async () => {
52
+ const spec = {
53
+ credentials: [
54
+ { name: "db", secret: { secretRef: "prod-db-password" } },
55
+ { name: "api", secret: { secretRef: "api-key" } },
56
+ ],
57
+ };
58
+ const resolved = await resolveSecrets({
59
+ spec,
60
+ secretStore: mockSecretStore,
61
+ });
62
+ expect(resolved).toEqual({
63
+ credentials: [
64
+ { name: "db", secret: "s3cret!" },
65
+ { name: "api", secret: "key-12345" },
66
+ ],
67
+ });
68
+ });
69
+
70
+ it("throws when a referenced secret is not found", async () => {
71
+ const spec = { password: { secretRef: "nonexistent" } };
72
+ await expect(
73
+ resolveSecrets({ spec, secretStore: mockSecretStore }),
74
+ ).rejects.toThrow("Secret not found: nonexistent");
75
+ });
76
+
77
+ it("handles null and undefined values gracefully", async () => {
78
+ const spec = { a: null, b: undefined, c: "value" };
79
+ const resolved = await resolveSecrets({
80
+ spec,
81
+ secretStore: mockSecretStore,
82
+ });
83
+ expect(resolved.a).toBeNull();
84
+ expect(resolved.c).toBe("value");
85
+ });
86
+ });