import { describe, expect, it, mock } from "bun:test"; import { indexedDB } from "fake-indexeddb"; import { NoyaManager } from "../NoyaManager"; import type { GitInterface } from "../sync/gitTypes"; import { localStorageSync } from "../sync/localStorageSync"; // Polyfill indexedDB for tests globalThis.indexedDB = indexedDB; function createNoyaManager() { return new NoyaManager(null); } /** * Create a mock GitInterface for testing */ function createMockGitInterface(): GitInterface { const repos = new Map< string, { files: Map; branches: Array<{ name: string; oid: string }>; currentOid: string; } >(); let repoCounter = 0; let oidCounter = 0; const generateOid = () => `oid-${oidCounter++}`; return { async createRepo() { const id = `repo-${repoCounter++}`; repos.set(id, { files: new Map(), branches: [{ name: "main", oid: generateOid() }], currentOid: generateOid(), }); return { id }; }, async listFiles(repoId, _ref, includeContent) { const repo = repos.get(repoId); if (!repo) { throw new Error(`Repository ${repoId} not found`); } const result: { files: string[]; ref: string; oid: string; fileContents?: Array<{ path: string; content: string }>; } = { files: Array.from(repo.files.keys()), ref: "main", oid: repo.currentOid, }; // Include base64-encoded file contents when requested if (includeContent) { result.fileContents = Array.from(repo.files.entries()).map( ([path, content]) => ({ path, content: btoa(content), }) ); } return result; }, async listBranches(repoId) { const repo = repos.get(repoId); if (!repo) { throw new Error(`Repository ${repoId} not found`); } return { branches: repo.branches }; }, async readFile(repoId, filePath, _ref) { const repo = repos.get(repoId); if (!repo) { throw new Error(`Repository ${repoId} not found`); } const content = repo.files.get(filePath); if (content === undefined) { throw new Error(`File ${filePath} not found`); } return { content, oid: repo.currentOid }; }, async patchFiles(repoId, patch, _ref, includeContent) { const repo = repos.get(repoId); if (!repo) { throw new Error(`Repository ${repoId} not found`); } // Apply creates for (const op of patch.create ?? []) { repo.files.set(op.path, op.content); } // Apply deletes for (const op of patch.delete ?? []) { repo.files.delete(op.path); } // Apply renames for (const op of patch.rename ?? []) { const content = repo.files.get(op.oldPath); if (content !== undefined) { repo.files.delete(op.oldPath); repo.files.set(op.newPath, content); } } const newOid = generateOid(); repo.currentOid = newOid; repo.branches[0].oid = newOid; const result: { oid: string; ref: string; files: string[]; fileContents?: Array<{ path: string; content: string }>; } = { oid: newOid, ref: "main", files: Array.from(repo.files.keys()), }; // Include base64-encoded file contents when requested if (includeContent) { result.fileContents = Array.from(repo.files.entries()).map( ([path, content]) => ({ path, content: btoa(content), }) ); } return result; }, }; } describe("localStorageSync with git interface", () => { it("should create a git repository", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-create", git: async (_storageAdapter) => mockGit, })({ noyaManager }); const result = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const body = JSON.parse(result.body); expect(body).toEqual({ id: "repo-0" }); cleanup(); }); it("should list files in a repository", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-list-files", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo first const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); // Add some files via patch await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [ { path: "README.md", content: "# Hello" }, { path: "src/index.ts", content: "export {}" }, ], }), }, }); // List files const listResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "GET" }, }); const listBody = JSON.parse(listResult.body); expect(listBody.files).toContain("README.md"); expect(listBody.files).toContain("src/index.ts"); expect(listBody.ref).toBe("main"); cleanup(); }); it("should list branches in a repository", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-list-branches", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); // List branches const branchResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/branches`, options: { method: "GET" }, }); const branchBody = JSON.parse(branchResult.body); expect(branchBody.branches).toHaveLength(1); expect(branchBody.branches[0].name).toBe("main"); cleanup(); }); it("should patch files (create, delete, rename)", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-patch", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); // Create files const createPatchResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [ { path: "file1.txt", content: "content1" }, { path: "file2.txt", content: "content2" }, { path: "old-name.txt", content: "to be renamed" }, ], }), }, }); const createPatchBody = JSON.parse(createPatchResult.body); expect(createPatchBody.files).toContain("file1.txt"); expect(createPatchBody.files).toContain("file2.txt"); expect(createPatchBody.files).toContain("old-name.txt"); expect(createPatchBody.oid).toBeDefined(); // Delete and rename const modifyPatchResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: [{ path: "file2.txt" }], rename: [{ oldPath: "old-name.txt", newPath: "new-name.txt" }], }), }, }); const modifyPatchBody = JSON.parse(modifyPatchResult.body); expect(modifyPatchBody.files).toContain("file1.txt"); expect(modifyPatchBody.files).not.toContain("file2.txt"); expect(modifyPatchBody.files).not.toContain("old-name.txt"); expect(modifyPatchBody.files).toContain("new-name.txt"); cleanup(); }); it("should support ref query parameter for listing files", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-ref-param", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo and add files const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [{ path: "test.txt", content: "test" }], }), }, }); // List files with ref parameter const listResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files?ref=main` as `/api/git/repos/${string}/files`, options: { method: "GET" }, }); const listBody = JSON.parse(listResult.body); expect(listBody.files).toContain("test.txt"); cleanup(); }); it("should not include file contents by default", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-no-content-default", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo and add files const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [{ path: "test.txt", content: "hello world" }], }), }, }); // List files without includeContent parameter const listResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "GET" }, }); const listBody = JSON.parse(listResult.body); expect(listBody.files).toContain("test.txt"); expect(listBody.fileContents).toBeUndefined(); cleanup(); }); it("should include base64-encoded file contents when includeContent=true", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-include-content", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo and add files const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); const fileContent = "hello world"; await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [{ path: "test.txt", content: fileContent }], }), }, }); // List files with includeContent=true const listResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files?includeContent=true` as `/api/git/repos/${string}/files`, options: { method: "GET" }, }); const listBody = JSON.parse(listResult.body); expect(listBody.files).toContain("test.txt"); expect(listBody.fileContents).toBeDefined(); expect(listBody.fileContents).toHaveLength(1); expect(listBody.fileContents[0].path).toBe("test.txt"); // Decode and verify the base64 content expect(atob(listBody.fileContents[0].content)).toBe(fileContent); cleanup(); }); it("should include contents for multiple files", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-include-content-multi", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo and add multiple files const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [ { path: "file1.txt", content: "content one" }, { path: "src/file2.ts", content: "export const x = 1;" }, ], }), }, }); // List files with includeContent=true const listResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files?includeContent=true` as `/api/git/repos/${string}/files`, options: { method: "GET" }, }); const listBody = JSON.parse(listResult.body); expect(listBody.fileContents).toHaveLength(2); const file1 = listBody.fileContents.find( (f: { path: string }) => f.path === "file1.txt" ); const file2 = listBody.fileContents.find( (f: { path: string }) => f.path === "src/file2.ts" ); expect(file1).toBeDefined(); expect(file2).toBeDefined(); expect(atob(file1.content)).toBe("content one"); expect(atob(file2.content)).toBe("export const x = 1;"); cleanup(); }); it("should support both ref and includeContent query parameters", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-ref-and-content", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo and add files const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [{ path: "test.txt", content: "test content" }], }), }, }); // List files with both ref and includeContent parameters const listResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files?ref=main&includeContent=true` as `/api/git/repos/${string}/files`, options: { method: "GET" }, }); const listBody = JSON.parse(listResult.body); expect(listBody.files).toContain("test.txt"); expect(listBody.ref).toBe("main"); expect(listBody.fileContents).toBeDefined(); expect(atob(listBody.fileContents[0].content)).toBe("test content"); cleanup(); }); it("should return file contents from patchFiles when includeContent=true", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-patch-include-content", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); // Create files with includeContent=true const patchResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files?includeContent=true` as `/api/git/repos/${string}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [ { path: "file1.txt", content: "hello world" }, { path: "file2.txt", content: "goodbye world" }, ], }), }, }); const patchBody = JSON.parse(patchResult.body); expect(patchBody.files).toContain("file1.txt"); expect(patchBody.files).toContain("file2.txt"); expect(patchBody.fileContents).toBeDefined(); expect(patchBody.fileContents).toHaveLength(2); const file1 = patchBody.fileContents.find( (f: { path: string }) => f.path === "file1.txt" ); const file2 = patchBody.fileContents.find( (f: { path: string }) => f.path === "file2.txt" ); expect(atob(file1.content)).toBe("hello world"); expect(atob(file2.content)).toBe("goodbye world"); cleanup(); }); it("should not return file contents from patchFiles by default", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-patch-no-content", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); // Create files without includeContent const patchResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [{ path: "file1.txt", content: "hello world" }], }), }, }); const patchBody = JSON.parse(patchResult.body); expect(patchBody.files).toContain("file1.txt"); expect(patchBody.fileContents).toBeUndefined(); cleanup(); }); it("should return updated file contents after delete with includeContent=true", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-patch-delete-content", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo and add files const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); // Create two files await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [ { path: "keep.txt", content: "keep me" }, { path: "delete.txt", content: "delete me" }, ], }), }, }); // Delete one file with includeContent=true const deleteResult = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files?includeContent=true` as `/api/git/repos/${string}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ delete: [{ path: "delete.txt" }], }), }, }); const deleteBody = JSON.parse(deleteResult.body); expect(deleteBody.files).toContain("keep.txt"); expect(deleteBody.files).not.toContain("delete.txt"); expect(deleteBody.fileContents).toBeDefined(); expect(deleteBody.fileContents).toHaveLength(1); expect(deleteBody.fileContents[0].path).toBe("keep.txt"); expect(atob(deleteBody.fileContents[0].content)).toBe("keep me"); cleanup(); }); it("should lazily initialize git interface on first use", async () => { const mockGit = createMockGitInterface(); const gitFactory = mock(async (_storageAdapter: any) => mockGit); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-lazy", git: gitFactory, })({ noyaManager }); // Factory should not be called yet expect(gitFactory).not.toHaveBeenCalled(); // Make a git request await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); // Now factory should be called expect(gitFactory).toHaveBeenCalledTimes(1); // Make another request await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); // Factory should still only be called once (cached) expect(gitFactory).toHaveBeenCalledTimes(1); cleanup(); }); it("should return error for non-existent repository", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-not-found", git: async (_storageAdapter) => mockGit, })({ noyaManager }); try { await noyaManager.rpcManager.request({ url: "/api/git/repos/non-existent/files", options: { method: "GET" }, }); expect(false).toBe(true); // Should not reach here } catch (e: any) { // Error could be a string or an Error object const errorMessage = typeof e === "string" ? e : e?.message ?? String(e); expect(errorMessage).toInclude("not found"); } cleanup(); }); it("should work with multiple repositories", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-multiple-repos", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create two repos const result1 = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const repo1 = JSON.parse(result1.body); const result2 = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const repo2 = JSON.parse(result2.body); expect(repo1.id).not.toBe(repo2.id); // Add different files to each await noyaManager.rpcManager.request({ url: `/api/git/repos/${repo1.id}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [{ path: "repo1-file.txt", content: "repo1" }], }), }, }); await noyaManager.rpcManager.request({ url: `/api/git/repos/${repo2.id}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [{ path: "repo2-file.txt", content: "repo2" }], }), }, }); // Verify each repo has its own files const files1 = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repo1.id}/files`, options: { method: "GET" }, }); const files1Body = JSON.parse(files1.body); expect(files1Body.files).toContain("repo1-file.txt"); expect(files1Body.files).not.toContain("repo2-file.txt"); const files2 = await noyaManager.rpcManager.request({ url: `/api/git/repos/${repo2.id}/files`, options: { method: "GET" }, }); const files2Body = JSON.parse(files2.body); expect(files2Body.files).toContain("repo2-file.txt"); expect(files2Body.files).not.toContain("repo1-file.txt"); cleanup(); }); describe("git file editing", () => { it("should handle gitFileEdit.open message and respond with object", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-file-edit-open", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo and add a file const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); // Add a file to the repo await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [{ path: "test.txt", content: "Hello World" }], }), }, }); // Track received messages const receivedMessages: any[] = []; const unsubscribe = noyaManager.connectionEventManager.events$.subscribe( (events) => { const last = events.at(-1); if (last?.type === "receive") { receivedMessages.push(last.message); } } ); // Send gitFileEdit.open message noyaManager.multiplayerStateManager.sendMessage({ type: "gitFileEdit.open", repoId, filePath: "test.txt", ref: "main", } as any); // Wait for async processing await new Promise((resolve) => setTimeout(resolve, 50)); // Check that we received a gitFileEdit.object message const objectMsg = receivedMessages.find( (m) => m.type === "gitFileEdit.object" ); expect(objectMsg).toBeDefined(); expect(objectMsg.repoId).toBe(repoId); expect(objectMsg.filePath).toBe("test.txt"); expect(objectMsg.content).toBe("Hello World"); unsubscribe(); cleanup(); }); it("should handle gitFileEdit.patch message and respond with acceptPatch", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-file-edit-patch", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo and add a file const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [{ path: "test.txt", content: "Hello" }], }), }, }); // Track received messages const receivedMessages: any[] = []; noyaManager.connectionEventManager.events$.subscribe((events) => { const last = events.at(-1); if (last?.type === "receive") { receivedMessages.push(last.message); } }); // Open the file first noyaManager.multiplayerStateManager.sendMessage({ type: "gitFileEdit.open", repoId, filePath: "test.txt", ref: "main", } as any); await new Promise((resolve) => setTimeout(resolve, 50)); // Apply a patch noyaManager.multiplayerStateManager.sendMessage({ type: "gitFileEdit.patch", repoId, filePath: "test.txt", ref: "main", id: "patch-123", patches: [{ op: "add", path: [5], value: " World" }], baseHash: "abc", } as any); await new Promise((resolve) => setTimeout(resolve, 50)); // Check that we received acceptPatch const acceptMsg = receivedMessages.find( (m) => m.type === "gitFileEdit.acceptPatch" ); expect(acceptMsg).toBeDefined(); expect(acceptMsg.id).toBe("patch-123"); expect(acceptMsg.repoId).toBe(repoId); cleanup(); }); it("should persist changes and send committed message after debounce", async () => { const mockGit = createMockGitInterface(); const noyaManager = createNoyaManager(); const cleanup = localStorageSync({ key: "test-git-file-edit-persist", git: async (_storageAdapter) => mockGit, })({ noyaManager }); // Create a repo and add a file const createResult = await noyaManager.rpcManager.request({ url: "/api/git/repos", options: { method: "POST", headers: { "Content-Type": "application/json" }, }, }); const { id: repoId } = JSON.parse(createResult.body); await noyaManager.rpcManager.request({ url: `/api/git/repos/${repoId}/files`, options: { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ create: [{ path: "test.txt", content: "Hello" }], }), }, }); // Track received messages const receivedMessages: any[] = []; noyaManager.connectionEventManager.events$.subscribe((events) => { const last = events.at(-1); if (last?.type === "receive") { receivedMessages.push(last.message); } }); // Open the file noyaManager.multiplayerStateManager.sendMessage({ type: "gitFileEdit.open", repoId, filePath: "test.txt", ref: "main", } as any); await new Promise((resolve) => setTimeout(resolve, 50)); // Apply a patch noyaManager.multiplayerStateManager.sendMessage({ type: "gitFileEdit.patch", repoId, filePath: "test.txt", ref: "main", id: "patch-123", patches: [{ op: "add", path: [5], value: " World" }], baseHash: "abc", } as any); await new Promise((resolve) => setTimeout(resolve, 50)); // Verify the patch was accepted expect( receivedMessages.some((m) => m.type === "gitFileEdit.acceptPatch") ).toBe(true); // Wait for the debounce period (5 seconds + buffer) // Note: In real tests we'd use fake timers, but for now we skip the wait // The debounce is 5000ms which is too long for a unit test cleanup(); }); }); });