Repository: vailabel/vailabel-studio
Pull request #97: feat(core) : add test to core
Author: login: vicheanath
Created: 2025-05-19T01:43:37Z | Merged: 2025-05-19T02:24:33Z



Diff:
diff --git a/.github/workflows/test-and-sonarcloud.yml b/.github/workflows/test-and-sonarcloud.yml
new file mode 100644
index 00000000..e872fd95
--- /dev/null
+++ b/.github/workflows/test-and-sonarcloud.yml
@@ -0,0 +1,30 @@
+name: Test and SonarCloud
+permissions:
+  contents: read
+  security-events: write
+on:
+ push:
+   branches:
+     - main
+ pull_request:
+   types: [opened, synchronize, reopened]
+jobs:
+  sonarqube:
+    name: SonarQube
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: 0 
+      - name: Set up Node.js
+        uses: actions/setup-node@v3
+        with:
+          node-version: 22
+      - name: Install dependencies
+        run: yarn
+      - name: Test and coverage
+        run: yarn jest --coverage
+      - name: SonarQube Scan
+        uses: SonarSource/sonarqube-scan-action@v4
+        env:
+          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
new file mode 100644
index 00000000..6d026502
--- /dev/null
+++ b/.vscode/settings.json
@@ -0,0 +1,3 @@
+{
+  "liveServer.settings.port": 5001
+}
diff --git a/apps/core/index.test.ts b/apps/core/index.test.ts
new file mode 100644
index 00000000..3d8a944c
--- /dev/null
+++ b/apps/core/index.test.ts
@@ -0,0 +1,8 @@
+import { describe, it, expect } from "@jest/globals"
+import * as coreIndex from "./src/index"
+
+describe("core index exports", () => {
+  it("should export something", () => {
+    expect(coreIndex).toBeDefined()
+  })
+})
diff --git a/apps/core/jest.config.ts b/apps/core/jest.config.ts
new file mode 100644
index 00000000..ff609cc0
--- /dev/null
+++ b/apps/core/jest.config.ts
@@ -0,0 +1,14 @@
+import type { Config } from "jest"
+
+const config: Config = {
+  preset: "ts-jest",
+  testEnvironment: "jsdom",
+  moduleNameMapper: {
+    "^@/(.*)$": "<rootDir>/src/$1",
+  },
+  transform: {
+    "^.+\\.(ts|tsx)$": "ts-jest",
+  },
+}
+
+export default config
diff --git a/apps/core/package.json b/apps/core/package.json
index ef423cad..d3863891 100644
--- a/apps/core/package.json
+++ b/apps/core/package.json
@@ -27,5 +27,10 @@
     "better-sqlite3": "^11.10.0",
     "dexie": "^4.0.11",
     "sqlite3": "^5.1.7"
+  },
+  "devDependencies": {
+    "fake-indexeddb": "^6.0.1",
+    "jest": "^29.7.0",
+    "jest-environment-jsdom": "^29.7.0"
   }
 }
diff --git a/apps/core/src/core.test.ts b/apps/core/src/core.test.ts
new file mode 100644
index 00000000..81ca09f7
--- /dev/null
+++ b/apps/core/src/core.test.ts
@@ -0,0 +1,40 @@
+import { describe, it, expect } from "@jest/globals"
+import { DexieDataAccess } from "./data/sources/dexie/DexieDataAccess"
+import { ApiClient } from "./data/sources/api/ApiClient"
+import { ApiDataAccess } from "./data/sources/api/ApiDataAccess"
+import { SQLiteDataAccess } from "./data/sources/sqlite/SQLiteDataAccess"
+import { FileSystemStorageAdapter } from "./storage/adapters/filesystem/FileSystemStorageAdapter"
+import { Base64StorageAdapter } from "./storage/adapters/base64/Base64StorageAdapter"
+import { S3StorageAdapter } from "./storage/adapters/s3/S3StorageAdapter"
+import { AzureBlobStorageAdapter } from "./storage/adapters/azure/AzureBlobStorageAdapter"
+import { HybridAdapter } from "./storage/adapters/hybrid/HybridAdapter"
+
+describe("Core adapters and data access", () => {
+  it("DexieDataAccess should be defined", () => {
+    expect(DexieDataAccess).toBeDefined()
+  })
+  it("ApiClient should be defined", () => {
+    expect(ApiClient).toBeDefined()
+  })
+  it("ApiDataAccess should be defined", () => {
+    expect(ApiDataAccess).toBeDefined()
+  })
+  it("SQLiteDataAccess should be defined", () => {
+    expect(SQLiteDataAccess).toBeDefined()
+  })
+  it("FileSystemStorageAdapter should be defined", () => {
+    expect(FileSystemStorageAdapter).toBeDefined()
+  })
+  it("Base64StorageAdapter should be defined", () => {
+    expect(Base64StorageAdapter).toBeDefined()
+  })
+  it("S3StorageAdapter should be defined", () => {
+    expect(S3StorageAdapter).toBeDefined()
+  })
+  it("AzureBlobStorageAdapter should be defined", () => {
+    expect(AzureBlobStorageAdapter).toBeDefined()
+  })
+  it("HybridAdapter should be defined", () => {
+    expect(HybridAdapter).toBeDefined()
+  })
+})
diff --git a/apps/core/src/data/sources/api/ApiClient.test.ts b/apps/core/src/data/sources/api/ApiClient.test.ts
new file mode 100644
index 00000000..94336e9b
--- /dev/null
+++ b/apps/core/src/data/sources/api/ApiClient.test.ts
@@ -0,0 +1,191 @@
+import { describe, it, expect, beforeEach, jest } from "@jest/globals"
+import { ApiClient } from "./ApiClient"
+import { ApiDataAccess } from "./ApiDataAccess"
+
+global.fetch = jest.fn() as any
+
+describe("ApiClient", () => {
+  let client: ApiClient
+  beforeEach(() => {
+    client = new ApiClient({ baseUrl: "https://test.local" })
+    ;(global.fetch as any).mockClear()
+  })
+
+  it("should be defined", () => {
+    expect(ApiClient).toBeDefined()
+  })
+
+  it("should call fetch with correct URL and method for GET", async () => {
+    ;(global.fetch as any).mockResolvedValue({
+      json: () => Promise.resolve({ ok: true }),
+      status: 200,
+      ok: true,
+    })
+    await client.get("/foo")
+    expect(global.fetch).toHaveBeenCalledWith(
+      "https://test.local/foo",
+      expect.objectContaining({ method: "GET" })
+    )
+  })
+
+  it("should call fetch with correct URL, method, and body for POST", async () => {
+    ;(global.fetch as any).mockResolvedValue({
+      json: () => Promise.resolve({ ok: true }),
+      status: 200,
+      ok: true,
+    })
+    await client.post("/bar", { a: 1 })
+    expect(global.fetch).toHaveBeenCalledWith(
+      "https://test.local/bar",
+      expect.objectContaining({
+        method: "POST",
+        body: JSON.stringify({ a: 1 }),
+      })
+    )
+  })
+
+  it("should call fetch with correct URL, method, and body for PUT", async () => {
+    ;(global.fetch as any).mockResolvedValue({
+      json: () => Promise.resolve({ ok: true }),
+      status: 200,
+      ok: true,
+    })
+    await client.put("/baz", { b: 2 })
+    expect(global.fetch).toHaveBeenCalledWith(
+      "https://test.local/baz",
+      expect.objectContaining({ method: "PUT", body: JSON.stringify({ b: 2 }) })
+    )
+  })
+
+  it("should call fetch with correct URL and method for DELETE", async () => {
+    ;(global.fetch as any).mockResolvedValue({
+      json: () => Promise.resolve({ ok: true }),
+      status: 200,
+      ok: true,
+    })
+    await client.delete("/qux")
+    expect(global.fetch).toHaveBeenCalledWith(
+      "https://test.local/qux",
+      expect.objectContaining({ method: "DELETE" })
+    )
+    // Optionally check that body is not set for DELETE
+    const fetchCallArgs = (global.fetch as any).mock.calls[0][1]
+    expect(fetchCallArgs.body).toBeUndefined()
+  })
+
+  it("should cache GET requests if enabled", async () => {
+    client = new ApiClient({
+      baseUrl: "https://test.local",
+      cache: true,
+      cacheDuration: 1000,
+    })
+    ;(global.fetch as any).mockResolvedValue({
+      json: () => Promise.resolve({ foo: 123 }),
+      status: 200,
+      ok: true,
+    })
+    const result1 = await client.get("/cache")
+    const result2 = await client.get("/cache")
+    expect(result1).toEqual({ foo: 123 })
+    expect(result2).toEqual({ foo: 123 })
+    expect((global.fetch as any).mock.calls.length).toBe(1)
+  })
+
+  it("should use default options in constructor", () => {
+    const defaultClient = new ApiClient()
+    expect(defaultClient).toBeInstanceOf(ApiClient)
+  })
+
+  it("should add and use request interceptors", async () => {
+    const interceptor: import("./ApiClient").RequestInterceptor = async (
+      input,
+      init
+    ) => [
+      input + "?intercepted",
+      { ...init, headers: { ...(init.headers || {}), foo: "bar" } },
+    ]
+    client.addRequestInterceptor(interceptor)
+    ;(global.fetch as any).mockResolvedValue({
+      json: () => Promise.resolve({ ok: true }),
+      status: 200,
+      ok: true,
+    })
+    await client.get("/foo")
+    expect(global.fetch).toHaveBeenCalledWith(
+      expect.stringContaining("intercepted"),
+      expect.objectContaining({
+        headers: expect.objectContaining({ foo: "bar" }),
+      })
+    )
+  })
+
+  it("should add and use response interceptors", async () => {
+    const responseInterceptor: import("./ApiClient").ResponseInterceptor =
+      async (res) => {
+        Object.defineProperty(res, "intercepted", {
+          value: true,
+          configurable: true,
+        })
+        return res
+      }
+    client.addResponseInterceptor(responseInterceptor)
+    ;(global.fetch as any).mockResolvedValue({
+      json: () => Promise.resolve({ ok: true }),
+      status: 200,
+      ok: true,
+    })
+    await client.get("/foo")
+    // No error means the interceptor was called
+  })
+
+  it("should use getAuthToken if provided", async () => {
+    const getAuthToken = () => Promise.resolve("token123")
+    client = new ApiClient({ baseUrl: "https://test.local", getAuthToken })
+    ;(global.fetch as any).mockResolvedValue({
+      json: () => Promise.resolve({ ok: true }),
+      status: 200,
+      ok: true,
+    })
+    await client.get("/foo")
+    const fetchCallArgs = (global.fetch as any).mock.calls[0][1]
+    expect(fetchCallArgs.headers.Authorization).toBe("Bearer token123")
+  })
+
+  it("should not set Authorization if getAuthToken returns null", async () => {
+    const getAuthToken = () => Promise.resolve(null)
+    client = new ApiClient({ baseUrl: "https://test.local", getAuthToken })
+    ;(global.fetch as any).mockResolvedValue({
+      json: () => Promise.resolve({ ok: true }),
+      status: 200,
+      ok: true,
+    })
+    await client.get("/foo")
+    const fetchCallArgs = (global.fetch as any).mock.calls[0][1]
+    expect(fetchCallArgs.headers.Authorization).toBeUndefined()
+  })
+
+  it("should clear cache with clearCache", () => {
+    client = new ApiClient({ baseUrl: "https://test.local", cache: true })
+    // @ts-ignore
+    client.cacheStore.set("foo", { value: 1, expires: Date.now() + 1000 })
+    client.clearCache()
+    // @ts-ignore
+    expect(client.cacheStore.size).toBe(0)
+  })
+
+  it("should handle 204 No Content for DELETE", async () => {
+    ;(global.fetch as any).mockResolvedValue({
+      status: 204,
+      ok: true,
+      json: () => Promise.resolve(undefined),
+    })
+    const result = await client.delete("/no-content")
+    expect(result).toBeUndefined()
+  })
+})
+
+describe("ApiDataAccess", () => {
+  it("should be defined", () => {
+    expect(ApiDataAccess).toBeDefined()
+  })
+})
diff --git a/apps/core/src/data/sources/api/ApiDataAccess.test.ts b/apps/core/src/data/sources/api/ApiDataAccess.test.ts
new file mode 100644
index 00000000..6415593a
--- /dev/null
+++ b/apps/core/src/data/sources/api/ApiDataAccess.test.ts
@@ -0,0 +1,207 @@
+import { ApiDataAccess } from "./ApiDataAccess"
+import { ApiClient } from "@vailabel/core/src/data/sources/api/ApiClient"
+import type {
+  Project,
+  ImageData,
+  Annotation,
+  Label,
+  History as ModelHistory,
+} from "../../../models/types"
+
+jest.mock("@vailabel/core/src/data/sources/api/ApiClient")
+
+const mockGet = jest.fn()
+const mockPost = jest.fn()
+const mockPut = jest.fn()
+const mockDelete = jest.fn()
+
+;(ApiClient as jest.Mock).mockImplementation(() => ({
+  get: mockGet,
+  post: mockPost,
+  put: mockPut,
+  delete: mockDelete,
+}))
+
+describe("ApiDataAccess", () => {
+  let dataAccess: ApiDataAccess
+  let apiClientInstance: any
+
+  beforeEach(() => {
+    jest.clearAllMocks()
+    apiClientInstance = {
+      get: mockGet,
+      post: mockPost,
+      put: mockPut,
+      delete: mockDelete,
+    }
+    dataAccess = new ApiDataAccess(apiClientInstance)
+    mockGet.mockResolvedValue(undefined)
+    mockPost.mockResolvedValue(undefined)
+    mockPut.mockResolvedValue(undefined)
+    mockDelete.mockResolvedValue(undefined)
+  })
+
+  it("getProjectWithImages calls correct endpoint", async () => {
+    await dataAccess.getProjectWithImages("1")
+    expect(mockGet).toHaveBeenCalledWith("/projects/1")
+  })
+
+  it("getNextImageId calls correct endpoint", async () => {
+    await dataAccess.getNextImageId("img1")
+    expect(mockGet).toHaveBeenCalledWith("/images/img1/next")
+  })
+
+  it("deleteLabel calls correct endpoint", async () => {
+    await dataAccess.deleteLabel("label1")
+    expect(mockDelete).toHaveBeenCalledWith("/labels/label1")
+  })
+
+  it("createProject posts to correct endpoint", async () => {
+    const project = { id: "2", name: "New Project" } as Project
+    mockPost.mockResolvedValueOnce(undefined)
+    await dataAccess.createProject(project)
+    expect(mockPost).toHaveBeenCalledWith("/projects", project)
+  })
+
+  it("getAnnotationsWithFilter calls correct endpoint with query", async () => {
+    const filter = { type: "box" } as Partial<Annotation>
+    mockGet.mockResolvedValueOnce(undefined)
+    await dataAccess.getAnnotationsWithFilter("img1", filter)
+    expect(mockGet).toHaveBeenCalledWith("/images/img1/annotations?type=box")
+  })
+
+  it("createLabel posts label and updates annotations", async () => {
+    const label = { id: "l1", name: "Label1" } as Label
+    const annotationIds = ["a1", "a2"]
+    mockPost.mockResolvedValueOnce(undefined)
+    mockPut.mockResolvedValue(undefined)
+    await dataAccess.createLabel(label, annotationIds)
+    expect(mockPost).toHaveBeenCalledWith("/labels", label)
+    expect(mockPut).toHaveBeenNthCalledWith(1, "/annotations/a1", {
+      labelId: "l1",
+    })
+    expect(mockPut).toHaveBeenNthCalledWith(2, "/annotations/a2", {
+      labelId: "l1",
+    })
+  })
+
+  it("updateProject calls correct endpoint", async () => {
+    const updates = { name: "Updated Project" }
+    await dataAccess.updateProject("1", updates)
+    expect(mockPut).toHaveBeenCalledWith("/projects/1", updates)
+  })
+
+  it("deleteProject calls correct endpoint", async () => {
+    await dataAccess.deleteProject("1")
+    expect(mockDelete).toHaveBeenCalledWith("/projects/1")
+  })
+
+  it("createImage posts to correct endpoint", async () => {
+    const image = { id: "img1", url: "url" } as ImageData
+    await dataAccess.createImage(image)
+    expect(mockPost).toHaveBeenCalledWith("/images", image)
+  })
+
+  it("updateImage calls correct endpoint", async () => {
+    const updates = { url: "new-url" }
+    await dataAccess.updateImage("img1", updates)
+    expect(mockPut).toHaveBeenCalledWith("/images/img1", updates)
+  })
+
+  it("deleteImage calls correct endpoint", async () => {
+    await dataAccess.deleteImage("img1")
+    expect(mockDelete).toHaveBeenCalledWith("/images/img1")
+  })
+
+  it("createAnnotation posts to correct endpoint", async () => {
+    const annotation = { id: "a1", imageId: "img1" } as Annotation
+    await dataAccess.createAnnotation(annotation)
+    expect(mockPost).toHaveBeenCalledWith("/annotations", annotation)
+  })
+
+  it("updateAnnotation calls correct endpoint", async () => {
+    const updates = { labelId: "l1" }
+    await dataAccess.updateAnnotation("a1", updates)
+    expect(mockPut).toHaveBeenCalledWith("/annotations/a1", updates)
+  })
+
+  it("deleteAnnotation calls correct endpoint", async () => {
+    await dataAccess.deleteAnnotation("a1")
+    expect(mockDelete).toHaveBeenCalledWith("/annotations/a1")
+  })
+
+  it("getSettings calls correct endpoint", async () => {
+    await dataAccess.getSettings()
+    expect(mockGet).toHaveBeenCalledWith("/settings")
+  })
+
+  it("updateSetting calls correct endpoint", async () => {
+    await dataAccess.updateSetting("theme", "dark")
+    expect(mockPut).toHaveBeenCalledWith("/settings/theme", { value: "dark" })
+  })
+
+  it("getHistory calls correct endpoint", async () => {
+    await dataAccess.getHistory()
+    expect(mockGet).toHaveBeenCalledWith("/history")
+  })
+
+  it("updateHistory calls correct endpoint", async () => {
+    const history: ModelHistory = {
+      id: "h1",
+      labels: [],
+      historyIndex: 0,
+      canUndo: false,
+      canRedo: false,
+    }
+    await dataAccess.updateHistory(history)
+    expect(mockPut).toHaveBeenCalledWith("/history", history)
+  })
+
+  it("getImagesWithPagination calls correct endpoint", async () => {
+    await dataAccess.getImagesWithPagination("p1", 10, 5)
+    expect(mockGet).toHaveBeenCalledWith(
+      "/projects/p1/images?offset=10&limit=5"
+    )
+  })
+
+  it("getLabels calls correct endpoint", async () => {
+    await dataAccess.getLabels()
+    expect(mockGet).toHaveBeenCalledWith("/labels")
+  })
+
+  it("getLabelById calls correct endpoint", async () => {
+    await dataAccess.getLabelById("l1")
+    expect(mockGet).toHaveBeenCalledWith("/labels/l1")
+  })
+
+  it("updateLabel calls correct endpoint", async () => {
+    const updates = { name: "Updated Label" }
+    await dataAccess.updateLabel("l1", updates)
+    expect(mockPut).toHaveBeenCalledWith("/labels/l1", updates)
+  })
+
+  it("getPreviousImageId calls correct endpoint", async () => {
+    await dataAccess.getPreviousImageId("img1")
+    expect(mockGet).toHaveBeenCalledWith("/images/img1/previous")
+  })
+
+  it("getProjects calls correct endpoint", async () => {
+    await dataAccess.getProjects()
+    expect(mockGet).toHaveBeenCalledWith("/projects")
+  })
+
+  it("getProjectById calls correct endpoint", async () => {
+    await dataAccess.getProjectById("1")
+    expect(mockGet).toHaveBeenCalledWith("/projects/1")
+  })
+
+  it("getImages calls correct endpoint", async () => {
+    await dataAccess.getImages("p1")
+    expect(mockGet).toHaveBeenCalledWith("/projects/p1/images")
+  })
+
+  it("getAnnotations calls correct endpoint", async () => {
+    await dataAccess.getAnnotations("img1")
+    expect(mockGet).toHaveBeenCalledWith("/images/img1/annotations")
+  })
+})
diff --git a/apps/core/src/data/sources/api/ApiDataAccess.ts b/apps/core/src/data/sources/api/ApiDataAccess.ts
index edbd96c7..48c3081a 100644
--- a/apps/core/src/data/sources/api/ApiDataAccess.ts
+++ b/apps/core/src/data/sources/api/ApiDataAccess.ts
@@ -8,81 +8,85 @@ import type {
 } from "../../../models/types"
 import { ApiClient } from "@vailabel/core/src/data/sources/api/ApiClient"
 
-const api = new ApiClient()
-
 export class ApiDataAccess implements IDataAccess {
+  private api: ApiClient
+
+  constructor(apiClient?: ApiClient) {
+    this.api = apiClient || new ApiClient()
+  }
+
   getProjectWithImages(id: string): Promise<Project | undefined> {
-    return api.get<Project>(`/projects/${id}`)
+    return this.api.get<Project>(`/projects/${id}`)
   }
   async getNextImageId(currentImageId: string): Promise<string | null> {
-    return api.get<string | null>(`/images/${currentImageId}/next`)
+    return this.api.get<string | null>(`/images/${currentImageId}/next`)
   }
   async getPreviousImageId(currentImageId: string): Promise<string | null> {
-    return api.get<string | null>(`/images/${currentImageId}/previous`)
+    return this.api.get<string | null>(`/images/${currentImageId}/previous`)
   }
   async deleteLabel(id: string): Promise<void> {
-    await api.delete(`/labels/${id}`)
+    await this.api.delete(`/labels/${id}`)
   }
   async getProjects(): Promise<Project[]> {
-    return api.get<Project[]>("/projects")
+    return this.api.get<Project[]>("/projects")
   }
   async getProjectById(id: string): Promise<Project | undefined> {
-    return api.get<Project>(`/projects/${id}`)
+    return this.api.get<Project>(`/projects/${id}`)
   }
   async getImages(projectId: string): Promise<ImageData[]> {
-    return api.get<ImageData[]>(`/projects/${projectId}/images`)
+    return this.api.get<ImageData[]>(`/projects/${projectId}/images`)
   }
   async getAnnotations(imageId: string): Promise<Annotation[]> {
-    return api.get<Annotation[]>(`/images/${imageId}/annotations`)
+    return this.api.get<Annotation[]>(`/images/${imageId}/annotations`)
   }
   async createProject(project: Project): Promise<void> {
-    await api.post("/projects", project)
+    await this.api.post("/projects", project)
   }
   async updateProject(id: string, updates: Partial<Project>): Promise<void> {
-    await api.put(`/projects/${id}`, updates)
+    await this.api.put(`/projects/${id}`, updates)
   }
   async deleteProject(id: string): Promise<void> {
-    await api.delete(`/projects/${id}`)
+    await this.api.delete(`/projects/${id}`)
   }
   async createImage(image: ImageData): Promise<void> {
-    await api.post("/images", image)
+    await this.api.post("/images", image)
   }
   async updateImage(id: string, updates: Partial<ImageData>): Promise<void> {
-    await api.put(`/images/${id}`, updates)
+    await this.api.put(`/images/${id}`, updates)
   }
   async deleteImage(id: string): Promise<void> {
-    await api.delete(`/images/${id}`)
+    await this.api.delete(`/images/${id}`)
   }
   async createAnnotation(annotation: Annotation): Promise<void> {
-    await api.post("/annotations", annotation)
+    await this.api.post("/annotations", annotation)
   }
   async updateAnnotation(
     id: string,
     updates: Partial<Annotation>
   ): Promise<void> {
-    await api.put(`/annotations/${id}`, updates)
+    await this.api.put(`/annotations/${id}`, updates)
   }
   async deleteAnnotation(id: string): Promise<void> {
-    await api.delete(`/annotations/${id}`)
+    await this.api.delete(`/annotations/${id}`)
   }
   async getSettings(): Promise<{ key: string; value: string }[]> {
-    return api.get<{ key: string; value: string }[]>("/settings")
+    return this.api.get<{ key: string; value: string }[]>("/settings")
   }
   async updateSetting(key: string, value: string): Promise<void> {
-    await api.put(`/settings/${key}`, { value })
+    await this.api.put(`/settings/${key}`, { value })
   }
   async getHistory(): Promise<History[]> {
-    return api.get<History[]>("/history")
+    return this.api.get<History[]>("/history")
   }
   async updateHistory(history: History): Promise<void> {
-    await api.put("/history", history)
+    await this.api.put("/history", history)
   }
   async getImagesWithPagination(
     projectId: string,
     offset: number,
     limit: number
   ): Promise<ImageData[]> {
-    return api.get<ImageData[]>(
+    return this.api.get<ImageData[]>(
       `/projects/${projectId}/images?offset=${offset}&limit=${limit}`
     )
   }
@@ -93,21 +97,21 @@ export class ApiDataAccess implements IDataAccess {
     const query = new URLSearchParams(
       filter as Record<string, string>
     ).toString()
-    return api.get<Annotation[]>(`/images/${imageId}/annotations?${query}`)
+    return this.api.get<Annotation[]>(`/images/${imageId}/annotations?${query}`)
   }
   async createLabel(label: Label, annotationIds: string[]): Promise<void> {
-    await api.post("/labels", label)
+    await this.api.post("/labels", label)
     for (const annotationId of annotationIds) {
-      await api.put(`/annotations/${annotationId}`, { labelId: label.id })
+      await this.api.put(`/annotations/${annotationId}`, { labelId: label.id })
     }
   }
   async getLabels(): Promise<Label[]> {
-    return api.get<Label[]>("/labels")
+    return this.api.get<Label[]>("/labels")
   }
   async getLabelById(id: string): Promise<Label | undefined> {
-    return api.get<Label>(`/labels/${id}`)
+    return this.api.get<Label>(`/labels/${id}`)
   }
   async updateLabel(id: string, updates: Partial<Label>): Promise<void> {
-    await api.put(`/labels/${id}`, updates)
+    await this.api.put(`/labels/${id}`, updates)
   }
 }
diff --git a/apps/core/src/data/sources/dexie/DexieDataAccess.test.ts b/apps/core/src/data/sources/dexie/DexieDataAccess.test.ts
new file mode 100644
index 00000000..6bf77b4d
--- /dev/null
+++ b/apps/core/src/data/sources/dexie/DexieDataAccess.test.ts
@@ -0,0 +1,291 @@
+import "fake-indexeddb/auto"
+// Polyfill structuredClone for Node.js/Jest if not present
+if (typeof global.structuredClone !== "function") {
+  global.structuredClone = (obj) => JSON.parse(JSON.stringify(obj))
+}
+
+import { describe, it, expect, beforeEach } from "@jest/globals"
+import { DexieDataAccess } from "./DexieDataAccess"
+import { db } from "@vailabel/core/src/data/db/dexieDb"
+
+describe("DexieDataAccess", () => {
+  let dataAccess: DexieDataAccess
+  const createProject = (id: string, name?: string) => ({
+    id,
+    name: name || id,
+    createdAt: new Date(),
+    lastModified: new Date(),
+    images: [],
+  })
+  const createImage = (id: string, projectId: string, name?: string) => ({
+    id,
+    name: name || id,
+    data: "",
+    width: 1,
+    height: 1,
+    projectId,
+    createdAt: new Date(),
+  })
+
+  beforeEach(async () => {
+    dataAccess = new DexieDataAccess()
+    // Clear all tables before each test to avoid cross-test pollution
+    await db.projects.clear()
+    await db.images.clear()
+    await db.annotations.clear()
+    await db.labels.clear()
+    await db.settings.clear()
+    await db.history.clear()
+  })
+
+  it("should be defined", () => {
+    expect(DexieDataAccess).toBeDefined()
+  })
+
+  it("should add and retrieve a project", async () => {
+    // Arrange
+    const project = createProject("test-id", "Test Project")
+    // Act
+    await dataAccess.createProject(project)
+    const projects = await dataAccess.getProjects()
+    // Assert
+    expect(projects).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ id: "test-id", name: "Test Project" }),
+      ])
+    )
+  })
+
+  it("should update a project", async () => {
+    const project = createProject("p1", "P1")
+    await dataAccess.createProject(project)
+    await dataAccess.updateProject("p1", { name: "P1-updated" })
+    const updated = await dataAccess.getProjectById("p1")
+    expect(updated?.name).toBe("P1-updated")
+  })
+
+  it("should delete a project", async () => {
+    const project = createProject("p2", "P2")
+    await dataAccess.createProject(project)
+    await dataAccess.deleteProject("p2")
+    const deleted = await dataAccess.getProjectById("p2")
+    expect(deleted).toBeUndefined()
+  })
+
+  it("should add and retrieve an image", async () => {
+    const project = createProject("p3", "P3")
+    await dataAccess.createProject(project)
+    const image = createImage("img1", "p3", "img")
+    await dataAccess.createImage(image)
+    const images = await dataAccess.getImages("p3")
+    expect(images).toEqual(
+      expect.arrayContaining([expect.objectContaining({ id: "img1" })])
+    )
+  })
+
+  it("should update and delete an image", async () => {
+    const project = createProject("p4", "P4")
+    await dataAccess.createProject(project)
+    const image = createImage("img2", "p4", "img2")
+    await dataAccess.createImage(image)
+    await dataAccess.updateImage("img2", { name: "img2-upd" })
+    let updated = await dataAccess.getImages("p4")
+    expect(updated[0].name).toBe("img2-upd")
+    await dataAccess.deleteImage("img2")
+    const afterDelete = await dataAccess.getImages("p4")
+    expect(afterDelete.length).toBe(0)
+  })
+
+  it("should add, update, and delete an annotation", async () => {
+    const dataAccess = new DexieDataAccess()
+    const project = {
+      id: "p5",
+      name: "P5",
+      createdAt: new Date(),
+      lastModified: new Date(),
+      images: [],
+    }
+    await dataAccess.createProject(project)
+    const image = {
+      id: "img3",
+      name: "img3",
+      data: "",
+      width: 1,
+      height: 1,
+      projectId: "p5",
+      createdAt: new Date(),
+    }
+    await dataAccess.createImage(image)
+    const annotation = {
+      id: "a1",
+      imageId: "img3",
+      labelId: "",
+      name: "ann",
+      type: "box" as "box",
+      coordinates: [],
+      color: "red",
+      isAIGenerated: false,
+      createdAt: new Date(),
+      updatedAt: new Date(),
+    }
+    await dataAccess.createAnnotation(annotation)
+    await dataAccess.updateAnnotation("a1", { name: "ann-upd" })
+    let anns = await dataAccess.getAnnotations("img3")
+    expect(anns[0].name).toBe("ann-upd")
+    await dataAccess.deleteAnnotation("a1")
+    anns = await dataAccess.getAnnotations("img3")
+    expect(anns.length).toBe(0)
+  })
+
+  it("should add, update, and delete a label", async () => {
+    const dataAccess = new DexieDataAccess()
+    const label = {
+      id: "l1",
+      name: "Label1",
+      color: "blue",
+      category: "cat",
+      isAIGenerated: false,
+      projectId: "p6",
+      createdAt: new Date(),
+      updatedAt: new Date(),
+    }
+    await dataAccess.createLabel(label, [])
+    let labels = await dataAccess.getLabels()
+    expect(labels[0].name).toBe("Label1")
+    await dataAccess.updateLabel("l1", { name: "Label1-upd" })
+    const updated = await dataAccess.getLabelById("l1")
+    expect(updated?.name).toBe("Label1-upd")
+    await dataAccess.deleteLabel("l1")
+    labels = await dataAccess.getLabels()
+    expect(labels.length).toBe(0)
+  })
+
+  it("should store and retrieve settings", async () => {
+    const dataAccess = new DexieDataAccess()
+    await dataAccess.updateSetting("theme", "dark")
+    const settings = await dataAccess.getSettings()
+    expect(settings).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ key: "theme", value: "dark" }),
+      ])
+    )
+  })
+
+  it("should store and retrieve history", async () => {
+    const dataAccess = new DexieDataAccess()
+    const history = {
+      id: "h1",
+      action: "edit",
+      timestamp: new Date(),
+      userId: "u1",
+      labels: [],
+      historyIndex: 0,
+      canUndo: true,
+      canRedo: false,
+    }
+    await dataAccess.updateHistory(history)
+    const histories = await dataAccess.getHistory()
+    expect(histories).toEqual(
+      expect.arrayContaining([
+        expect.objectContaining({ id: "h1", action: "edit" }),
+      ])
+    )
+  })
+
+  it("should paginate images", async () => {
+    const dataAccess = new DexieDataAccess()
+    const project = {
+      id: "p7",
+      name: "P7",
+      createdAt: new Date(),
+      lastModified: new Date(),
+      images: [],
+    }
+    await dataAccess.createProject(project)
+    for (let i = 0; i < 5; i++) {
+      await dataAccess.createImage({
+        id: `img${i}`,
+        name: `img${i}`,
+        data: "",
+        width: 1,
+        height: 1,
+        projectId: "p7",
+        createdAt: new Date(),
+      })
+    }
+    const paged = await dataAccess.getImagesWithPagination("p7", 1, 2)
+    expect(paged.length).toBe(2)
+  })
+
+  it("should filter annotations", async () => {
+    const dataAccess = new DexieDataAccess()
+    const project = {
+      id: "p8",
+      name: "P8",
+      createdAt: new Date(),
+      lastModified: new Date(),
+      images: [],
+    }
+    await dataAccess.createProject(project)
+    const image = {
+      id: "img8",
+      name: "img8",
+      data: "",
+      width: 1,
+      height: 1,
+      projectId: "p8",
+      createdAt: new Date(),
+    }
+    await dataAccess.createImage(image)
+    await dataAccess.createAnnotation({
+      id: "a8",
+      imageId: "img8",
+      labelId: "l8",
+      name: "ann8",
+      type: "box",
+      coordinates: [],
+      color: "red",
+      isAIGenerated: false,
+      createdAt: new Date(),
+      updatedAt: new Date(),
+    })
+    const filtered = await dataAccess.getAnnotationsWithFilter("img8", {
+      labelId: "l8",
+    })
+    expect(filtered.length).toBe(1)
+  })
+
+  it("should get next and previous image ids", async () => {
+    const dataAccess = new DexieDataAccess()
+    const project = {
+      id: "p9",
+      name: "P9",
+      createdAt: new Date(),
+      lastModified: new Date(),
+      images: [],
+    }
+    await dataAccess.createProject(project)
+    await dataAccess.createImage({
+      id: "imgA",
+      name: "A",
+      data: "",
+      width: 1,
+      height: 1,
+      projectId: "p9",
+      createdAt: new Date(),
+    })
+    await dataAccess.createImage({
+      id: "imgB",
+      name: "B",
+      data: "",
+      width: 1,
+      height: 1,
+      projectId: "p9",
+      createdAt: new Date(),
+    })
+    const next = await dataAccess.getNextImageId("imgA")
+    const prev = await dataAccess.getPreviousImageId("imgB")
+    expect(next).toBe("imgB")
+    expect(prev).toBe("imgA")
+  })
+})
diff --git a/apps/core/src/data/sources/sqlite/SQLiteDataAccess.test.ts b/apps/core/src/data/sources/sqlite/SQLiteDataAccess.test.ts
new file mode 100644
index 00000000..4b83d3d7
--- /dev/null
+++ b/apps/core/src/data/sources/sqlite/SQLiteDataAccess.test.ts
@@ -0,0 +1,384 @@
+import { describe, it, expect, beforeEach, jest } from "@jest/globals"
+import { SQLiteDataAccess } from "./SQLiteDataAccess"
+
+global.window = Object.create(window)
+window.ipc = {
+  invoke: jest.fn() as unknown as jest.Mock,
+} as any
+
+describe("SQLiteDataAccess", () => {
+  let adapter: SQLiteDataAccess
+  let mockInvoke: jest.Mock
+
+  beforeEach(() => {
+    mockInvoke = window.ipc.invoke as jest.Mock
+    mockInvoke.mockReset()
+    adapter = new SQLiteDataAccess()
+  })
+
+  it("should be defined", () => {
+    expect(SQLiteDataAccess).toBeDefined()
+  })
+
+  it("getProjectWithImages returns project with images", async () => {
+    // Arrange
+    ;(mockInvoke as any)
+      .mockResolvedValueOnce({ id: "p1", name: "proj" }) // for project
+      .mockResolvedValueOnce([{ id: "img1" }, { id: "img2" }]) // for images
+    // Act
+    const result = await adapter.getProjectWithImages("p1")
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:get", [
+      "SELECT * FROM projects WHERE id = ?",
+      ["p1"],
+    ])
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:all", [
+      "SELECT * FROM images WHERE projectId = ?",
+      ["p1"],
+    ])
+    expect(result).toEqual({
+      id: "p1",
+      name: "proj",
+      images: [{ id: "img1" }, { id: "img2" }],
+    })
+  })
+
+  it("getProjectWithImages returns undefined if no project", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce(undefined)
+    // Act
+    const result = await adapter.getProjectWithImages("p1")
+    // Assert
+    expect(result).toBeUndefined()
+  })
+
+  it("getProjectById returns project", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce({ id: "p1" })
+    // Act
+    const result = await adapter.getProjectById("p1")
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:get", [
+      "SELECT * FROM projects WHERE id = ?",
+      ["p1"],
+    ])
+    expect(result).toEqual({ id: "p1" })
+  })
+
+  it("createProject calls sqlite:run", async () => {
+    // Arrange
+    const project = { id: "p1", name: "n", createdAt: 1, lastModified: 2 }
+    // Act
+    await adapter.createProject(project as any)
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      "INSERT INTO projects (id, name, createdAt, lastModified) VALUES (?, ?, ?, ?)",
+      [project.id, project.name, project.createdAt, project.lastModified],
+    ])
+  })
+
+  it("updateProject calls sqlite:run with correct SQL", async () => {
+    // Arrange
+    // Act
+    await adapter.updateProject("p1", { name: "new" })
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      "UPDATE projects SET name = ? WHERE id = ?",
+      ["new", "p1"],
+    ])
+  })
+
+  it("deleteProject deletes annotations, images, labels, and project", async () => {
+    // Arrange
+    ;(mockInvoke as any)
+      .mockResolvedValueOnce([{ id: "img1" }, { id: "img2" }]) // images
+      .mockResolvedValue(undefined)
+    // Act
+    await adapter.deleteProject("p1")
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:all", [
+      "SELECT id FROM images WHERE projectId = ?",
+      ["p1"],
+    ])
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      "DELETE FROM annotations WHERE imageId IN (?,?)",
+      ["img1", "img2"],
+    ])
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      "DELETE FROM images WHERE projectId = ?",
+      ["p1"],
+    ])
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      "DELETE FROM labels WHERE projectId = ?",
+      ["p1"],
+    ])
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      "DELETE FROM projects WHERE id = ?",
+      ["p1"],
+    ])
+  })
+
+  it("getImages returns images", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce([{ id: "img1" }])
+    // Act
+    const result = await adapter.getImages("p1")
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:all", [
+      "SELECT * FROM images WHERE projectId = ?",
+      ["p1"],
+    ])
+    expect(result).toEqual([{ id: "img1" }])
+  })
+
+  it("getImagesWithPagination returns images", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce([{ id: "img1" }])
+    // Act
+    const result = await adapter.getImagesWithPagination("p1", 10, 5)
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:all", [
+      "SELECT * FROM images WHERE projectId = ? LIMIT ? OFFSET ?",
+      ["p1", 5, 10],
+    ])
+    expect(result).toEqual([{ id: "img1" }])
+  })
+
+  it("getNextImageId returns id or null", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce({ id: "img2" })
+    // Act
+    const result = await adapter.getNextImageId("img1")
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:get", [
+      "SELECT id FROM images WHERE id > ? ORDER BY id ASC LIMIT 1",
+      ["img1"],
+    ])
+    expect(result).toBe("img2")
+  })
+
+  it("getNextImageId returns null if not found", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce(undefined)
+    // Act
+    const result = await adapter.getNextImageId("img1")
+    // Assert
+    expect(result).toBeNull()
+  })
+
+  it("getAnnotations parses coordinates if string", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce([
+      { id: "a", coordinates: JSON.stringify([1, 2]) },
+      { id: "b", coordinates: [3, 4] },
+    ])
+    // Act
+    const result = await adapter.getAnnotations("img1")
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:all", [
+      "SELECT * FROM annotations WHERE imageId = ?",
+      ["img1"],
+    ])
+    expect(result[0].coordinates).toEqual([1, 2])
+    expect(result[1].coordinates).toEqual([3, 4])
+  })
+
+  it("createAnnotation stringifies coordinates", async () => {
+    // Arrange
+    const annotation = {
+      id: "a",
+      imageId: "img1",
+      labelId: "l1",
+      name: "n",
+      type: "rect",
+      coordinates: [1, 2],
+      color: "red",
+      isAIGenerated: false,
+      createdAt: 1,
+      updatedAt: 2,
+    }
+    // Act
+    await adapter.createAnnotation(annotation as any)
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      expect.stringContaining("INSERT INTO annotations"),
+      expect.arrayContaining([
+        annotation.id,
+        annotation.imageId,
+        annotation.labelId,
+        annotation.name,
+        annotation.type,
+        JSON.stringify(annotation.coordinates),
+        annotation.color,
+        annotation.isAIGenerated,
+        annotation.createdAt,
+        annotation.updatedAt,
+      ]),
+    ])
+  })
+
+  it("updateAnnotation stringifies coordinates if present", async () => {
+    // Arrange
+    // Act
+    await adapter.updateAnnotation("a", {
+      coordinates: [1, 2] as any,
+      name: "n",
+    })
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      expect.stringContaining("UPDATE annotations SET"),
+      expect.arrayContaining([JSON.stringify([1, 2]), "n", "a"]),
+    ])
+  })
+
+  it("deleteAnnotation calls sqlite:run", async () => {
+    // Arrange
+    // Act
+    await adapter.deleteAnnotation("a")
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      "DELETE FROM annotations WHERE id = ?",
+      ["a"],
+    ])
+  })
+
+  it("createLabel calls sqlite:run", async () => {
+    // Arrange
+    const label = {
+      id: "l1",
+      name: "label",
+      category: "cat",
+      isAIGenerated: false,
+      projectId: "p1",
+      color: "red",
+      createdAt: 1,
+      updatedAt: 2,
+    }
+    // Act
+    await adapter.createLabel(label as any)
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      expect.stringContaining("INSERT INTO labels"),
+      expect.arrayContaining([
+        label.id,
+        label.name,
+        label.category,
+        label.isAIGenerated,
+        label.projectId,
+        label.color,
+        label.createdAt,
+        label.updatedAt,
+      ]),
+    ])
+  })
+
+  it("getLabels returns labels", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce([{ id: "l1" }])
+    // Act
+    const result = await adapter.getLabels()
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:all", [
+      "SELECT * FROM labels",
+      [],
+    ])
+    expect(result).toEqual([{ id: "l1" }])
+  })
+
+  it("getLabelById returns label", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce({ id: "l1" })
+    // Act
+    const result = await adapter.getLabelById("l1")
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:get", [
+      "SELECT * FROM labels WHERE id = ?",
+      ["l1"],
+    ])
+    expect(result).toEqual({ id: "l1" })
+  })
+
+  it("updateLabel calls sqlite:run", async () => {
+    // Arrange
+    // Act
+    await adapter.updateLabel("l1", { name: "new" })
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      "UPDATE labels SET name = ? WHERE id = ?",
+      ["new", "l1"],
+    ])
+  })
+
+  it("deleteLabel calls sqlite:run", async () => {
+    // Arrange
+    // Act
+    await adapter.deleteLabel("l1")
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      "DELETE FROM labels WHERE id = ?",
+      ["l1"],
+    ])
+  })
+
+  it("getSettings returns settings", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce([{ key: "a", value: "b" }])
+    // Act
+    const result = await adapter.getSettings()
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:all", [
+      "SELECT * FROM settings",
+      [],
+    ])
+    expect(result).toEqual([{ key: "a", value: "b" }])
+  })
+
+  it("updateSetting calls sqlite:run", async () => {
+    // Arrange
+    // Act
+    await adapter.updateSetting("k", "v")
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      "UPDATE settings SET value = ? WHERE key = ?",
+      ["v", "k"],
+    ])
+  })
+
+  it("getHistory returns history", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce([{ id: "h1" }])
+    // Act
+    const result = await adapter.getHistory()
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:all", [
+      "SELECT * FROM history",
+      [],
+    ])
+    expect(result).toEqual([{ id: "h1" }])
+  })
+
+  it("updateHistory calls sqlite:run", async () => {
+    // Arrange
+    const history = { id: "h1", foo: "bar" }
+    // Act
+    await adapter.updateHistory(history as any)
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:run", [
+      expect.stringContaining("UPDATE history SET"),
+      expect.arrayContaining(["bar", "h1"]),
+    ])
+  })
+
+  it("getProjects returns projects", async () => {
+    // Arrange
+    ;(mockInvoke as any).mockResolvedValueOnce([{ id: "p1" }])
+    // Act
+    const result = await adapter.getProjects()
+    // Assert
+    expect(mockInvoke).toHaveBeenCalledWith("sqlite:all", [
+      "SELECT * FROM projects",
+      [],
+    ])
+    expect(result).toEqual([{ id: "p1" }])
+  })
+})
diff --git a/apps/core/src/index.test.ts b/apps/core/src/index.test.ts
new file mode 100644
index 00000000..b2b954f7
--- /dev/null
+++ b/apps/core/src/index.test.ts
@@ -0,0 +1,8 @@
+import { describe, it, expect } from "@jest/globals"
+import * as coreIndex from "./index"
+
+describe("core index", () => {
+  it("should export something", () => {
+    expect(coreIndex).toBeDefined()
+  })
+})
diff --git a/apps/core/src/models/types.test.ts b/apps/core/src/models/types.test.ts
new file mode 100644
index 00000000..1840ecbd
--- /dev/null
+++ b/apps/core/src/models/types.test.ts
@@ -0,0 +1,8 @@
+import { describe, it, expect } from "@jest/globals"
+import * as types from "./types"
+
+describe("types", () => {
+  it("should export something", () => {
+    expect(types).toBeDefined()
+  })
+})
diff --git a/apps/core/src/storage/adapters/azure/AzureBlobStorageAdapter.test.ts b/apps/core/src/storage/adapters/azure/AzureBlobStorageAdapter.test.ts
new file mode 100644
index 00000000..cf9f399d
--- /dev/null
+++ b/apps/core/src/storage/adapters/azure/AzureBlobStorageAdapter.test.ts
@@ -0,0 +1,85 @@
+import { describe, it, expect, beforeEach, jest } from "@jest/globals"
+import { AzureBlobStorageAdapter } from "./AzureBlobStorageAdapter"
+
+const createMockBlob = () => ({
+  uploadData: jest.fn(),
+  download: jest.fn(),
+})
+
+describe("AzureBlobStorageAdapter", () => {
+  let containerClient: any
+  let adapter: AzureBlobStorageAdapter
+
+  beforeEach(() => {
+    containerClient = {
+      getBlockBlobClient: jest.fn(),
+      deleteBlob: jest.fn(),
+      listBlobsFlat: jest.fn(),
+    }
+    adapter = new AzureBlobStorageAdapter(containerClient)
+  })
+
+  it("should be defined", () => {
+    expect(AzureBlobStorageAdapter).toBeDefined()
+  })
+
+  it("should save image using uploadData", async () => {
+    // Arrange
+    const id = "img1"
+    const data = Buffer.from("data")
+    const mockBlob = createMockBlob()
+    containerClient.getBlockBlobClient.mockReturnValue(mockBlob)
+    // Act
+    await adapter.saveImage(id, data)
+    // Assert
+    expect(containerClient.getBlockBlobClient).toHaveBeenCalledWith(id)
+    expect(mockBlob.uploadData).toHaveBeenCalledWith(data)
+  })
+
+  it("should load image and return buffer", async () => {
+    // Arrange
+    const id = "img2"
+    const chunk1 = Buffer.from([1, 2])
+    const chunk2 = Buffer.from([3, 4])
+    const mockBlob = createMockBlob()
+    // Use 'as any' to avoid type error for mock resolved value
+    ;(mockBlob as any).download.mockResolvedValue({
+      readableStreamBody: {
+        async *[Symbol.asyncIterator]() {
+          yield chunk1
+          yield chunk2
+        },
+      },
+    })
+    containerClient.getBlockBlobClient.mockReturnValue(mockBlob)
+    // Act
+    const result = await adapter.loadImage(id)
+    // Assert
+    expect(containerClient.getBlockBlobClient).toHaveBeenCalledWith(id)
+    expect(mockBlob.download).toHaveBeenCalled()
+    expect(result).toEqual(Buffer.concat([chunk1, chunk2]))
+  })
+
+  it("should delete image using deleteBlob", async () => {
+    // Arrange
+    const id = "img3"
+    // Act
+    await adapter.deleteImage(id)
+    // Assert
+    expect(containerClient.deleteBlob).toHaveBeenCalledWith(id)
+  })
+
+  it("should list all blob names", async () => {
+    // Arrange
+    const blobs = [{ name: "a.png" }, { name: "b.png" }]
+    containerClient.listBlobsFlat.mockReturnValue({
+      async *[Symbol.asyncIterator]() {
+        for (const blob of blobs) yield blob
+      },
+    })
+    // Act
+    const result = await adapter.listImages()
+    // Assert
+    expect(result).toEqual(["a.png", "b.png"])
+  })
+})
diff --git a/apps/core/src/storage/adapters/base64/Base64StorageAdapter.test.ts b/apps/core/src/storage/adapters/base64/Base64StorageAdapter.test.ts
new file mode 100644
index 00000000..158d70af
--- /dev/null
+++ b/apps/core/src/storage/adapters/base64/Base64StorageAdapter.test.ts
@@ -0,0 +1,108 @@
+import { describe, it, expect, beforeEach, jest } from "@jest/globals"
+import { Base64StorageAdapter } from "./Base64StorageAdapter"
+
+describe("Base64StorageAdapter", () => {
+  let adapter: Base64StorageAdapter
+  let store: Record<string, string>
+  let localStorageMock: any
+
+  beforeEach(() => {
+    store = {}
+    localStorageMock = {
+      getItem: jest.fn((key: string) => store[key] || null),
+      setItem: jest.fn((key: string, value: string) => {
+        store[key] = value
+      }),
+      removeItem: jest.fn((key: string) => {
+        delete store[key]
+      }),
+      key: jest.fn((i: number) => Object.keys(store)[i] || null),
+      get length() {
+        return Object.keys(store).length
+      },
+      clear: jest.fn(() => {
+        for (const k in store) delete store[k]
+      }),
+    }
+    Object.defineProperty(global, "localStorage", {
+      value: localStorageMock,
+      configurable: true,
+    })
+    adapter = new Base64StorageAdapter()
+  })
+
+  it("should be defined", () => {
+    expect(Base64StorageAdapter).toBeDefined()
+  })
+
+  it("should save image to localStorage", async () => {
+    // Arrange
+    const id = "img1"
+    const data = "base64data"
+    // Act
+    await adapter.saveImage(id, data)
+    // Assert
+    expect(localStorage.setItem).toHaveBeenCalledWith("img_" + id, data)
+    expect(store["img_" + id]).toBe(data)
+  })
+
+  it("should throw error if localStorage.setItem fails", async () => {
+    // Arrange
+    ;(localStorage.setItem as jest.Mock).mockImplementation(() => {
+      throw new Error("fail")
+    })
+    // Act & Assert
+    await expect(adapter.saveImage("id", "data")).rejects.toThrow(
+      "Failed to save image: fail"
+    )
+  })
+
+  it("should load image from localStorage", async () => {
+    // Arrange
+    const id = "img2"
+    const data = "base64img"
+    store["img_" + id] = data
+    // Act
+    const result = await adapter.loadImage(id)
+    // Assert
+    expect(localStorage.getItem).toHaveBeenCalledWith("img_" + id)
+    expect(result).toBe(data)
+  })
+
+  it("should throw error if image not found in loadImage", async () => {
+    // Arrange
+    const id = "notfound"
+    // Act & Assert
+    await expect(adapter.loadImage(id)).rejects.toThrow("Image not found")
+  })
+
+  it("should delete image from localStorage", async () => {
+    // Arrange
+    const id = "img3"
+    store["img_" + id] = "data"
+    // Act
+    await adapter.deleteImage(id)
+    // Assert
+    expect(localStorage.removeItem).toHaveBeenCalledWith("img_" + id)
+    expect(store["img_" + id]).toBeUndefined()
+  })
+
+  it("should list all image ids from localStorage", async () => {
+    // Arrange
+    // Use deterministic IDs for compatibility and test stability
+    const idA = "idA"
+    const idB = "idB"
+    // Directly set the store to simulate localStorage content
+    store["img_" + idA] = "data"
+    store["img_" + idB] = "data"
+    // Act
+    const result = await adapter.listImages()
+    // Assert
+    const expected = [idA, idB]
+    expect(localStorage.key).toHaveBeenCalledTimes(2)
+    expect(result).toEqual(expected)
+    expect(result.length).toBe(2)
+    expect(result).toContain(idA)
+    expect(result).toContain(idB)
+  })
+})
diff --git a/apps/core/src/storage/adapters/base64/Base64StorageAdapter.ts b/apps/core/src/storage/adapters/base64/Base64StorageAdapter.ts
index a701f3f2..cb327059 100644
--- a/apps/core/src/storage/adapters/base64/Base64StorageAdapter.ts
+++ b/apps/core/src/storage/adapters/base64/Base64StorageAdapter.ts
@@ -22,8 +22,13 @@ export class Base64StorageAdapter implements IStorageAdapter {
   }
 
   async listImages(): Promise<string[]> {
-    return Object.keys(localStorage)
-      .filter((key) => key.startsWith(Base64StorageAdapter.prefix))
-      .map((key) => key.replace(Base64StorageAdapter.prefix, ""))
+    const keys: string[] = []
+    for (let i = 0; i < localStorage.length; i++) {
+      const key = localStorage.key(i)
+      if (key && key.startsWith(Base64StorageAdapter.prefix)) {
+        keys.push(key.replace(Base64StorageAdapter.prefix, ""))
+      }
+    }
+    return keys
   }
 }
diff --git a/apps/core/src/storage/adapters/filesystem/FileSystemStorageAdapter.test.ts b/apps/core/src/storage/adapters/filesystem/FileSystemStorageAdapter.test.ts
new file mode 100644
index 00000000..ba534ec9
--- /dev/null
+++ b/apps/core/src/storage/adapters/filesystem/FileSystemStorageAdapter.test.ts
@@ -0,0 +1,98 @@
+import { describe, it, expect, beforeEach, jest } from "@jest/globals"
+import { FileSystemStorageAdapter } from "./FileSystemStorageAdapter"
+
+// Add a global type declaration for window.ipc
+export {} // Ensure this file is a module
+
+declare global {
+  interface Window {
+    ipc: {
+      invoke: (channel: string, ...args: any[]) => Promise<any>
+    }
+  }
+}
+
+// Arrange: mock window.ipc.invoke
+globalThis.window = Object.create(window)
+;(window as any).ipc = {
+  invoke: jest.fn(),
+} as Window["ipc"]
+
+describe("FileSystemStorageAdapter", () => {
+  const directory = "/mock/dir"
+  let adapter: FileSystemStorageAdapter
+
+  beforeEach(() => {
+    ;(window.ipc.invoke as jest.Mock).mockClear()
+    // Default mock implementation for all calls
+    ;(window.ipc.invoke as jest.Mock).mockImplementation((...args: any[]) => {
+      const channel = args[0]
+      if (channel === "fs-load-image")
+        return Promise.resolve(Buffer.from("mock"))
+      if (channel === "fs-list-images")
+        return Promise.resolve(["a.png", "b.png"])
+      if (channel === "fs-get-base-name") return Promise.resolve("a")
+      return Promise.resolve()
+    })
+    adapter = new FileSystemStorageAdapter(directory)
+  })
+
+  it("should be defined", () => {
+    expect(FileSystemStorageAdapter).toBeDefined()
+  })
+
+  it("should throw if directory is not provided", () => {
+    expect(() => new FileSystemStorageAdapter("")).toThrow(
+      "Directory is required"
+    )
+  })
+
+  it("should ensure directory exists on construction", () => {
+    expect(window.ipc.invoke).toHaveBeenCalledWith("fs-ensure-directory", {
+      path: directory,
+    })
+  })
+
+  it("should save image via IPC", async () => {
+    await adapter.saveImage("img1", Buffer.from("data"))
+    expect(window.ipc.invoke).toHaveBeenCalledWith("fs-save-image", {
+      path: `${directory}/img1`,
+      data: Buffer.from("data"),
+    })
+  })
+
+  it("should load image via IPC", async () => {
+    const mockBuffer = Buffer.from("imgdata")
+    ;(window.ipc.invoke as any).mockResolvedValueOnce(mockBuffer)
+    const result = await adapter.loadImage("img2")
+    expect(window.ipc.invoke).toHaveBeenCalledWith("fs-load-image", {
+      path: `${directory}/img2`,
+    })
+    expect(result).toBe(mockBuffer)
+  })
+
+  it("should delete image via IPC", async () => {
+    await adapter.deleteImage("img3")
+    expect(window.ipc.invoke).toHaveBeenCalledWith("fs-delete-image", {
+      path: `${directory}/img3`,
+    })
+  })
+
+  it("should list images and return base names", async () => {
+    ;(window.ipc.invoke as any)
+      .mockResolvedValueOnce(["a.png", "b.txt", "c.png"]) // fs-list-images
+      .mockResolvedValueOnce("a") // fs-get-base-name for a.png
+      .mockResolvedValueOnce("c") // fs-get-base-name for c.png
+    const result = await adapter.listImages()
+    expect(window.ipc.invoke).toHaveBeenCalledWith("fs-list-images", {
+      directory,
+    })
+    expect(window.ipc.invoke).toHaveBeenCalledWith("fs-get-base-name", {
+      file: "a.png",
+    })
+    expect(window.ipc.invoke).toHaveBeenCalledWith("fs-get-base-name", {
+      file: "c.png",
+    })
+    expect(result).toEqual(["a", "c"])
+  })
+})
diff --git a/apps/core/src/storage/adapters/filesystem/FileSystemStorageAdapter.ts b/apps/core/src/storage/adapters/filesystem/FileSystemStorageAdapter.ts
index e1594d8a..10a5bed0 100644
--- a/apps/core/src/storage/adapters/filesystem/FileSystemStorageAdapter.ts
+++ b/apps/core/src/storage/adapters/filesystem/FileSystemStorageAdapter.ts
@@ -19,7 +19,9 @@ export class FileSystemStorageAdapter implements IStorageAdapter {
   }
 
   loadImage = async (id: string): Promise<Buffer> => {
-    return window.ipc.invoke("fs-load-image", { path: this.getPath(id) })
+    return (await window.ipc.invoke("fs-load-image", {
+      path: this.getPath(id),
+    })) as Buffer
   }
 
   deleteImage = async (id: string): Promise<void> => {
@@ -27,13 +29,18 @@ export class FileSystemStorageAdapter implements IStorageAdapter {
   }
 
   listImages = async (): Promise<string[]> => {
-    const files = await window.ipc.invoke("fs-list-images", {
+    const files = (await window.ipc.invoke("fs-list-images", {
       directory: this.directory,
-    })
+    })) as string[]
     const baseNames = await Promise.all(
       files
         .filter((f: string) => f.endsWith(".png"))
-        .map((f: string) => window.ipc.invoke("fs-get-base-name", { file: f }))
+        .map(
+          (f: string) =>
+            window.ipc.invoke("fs-get-base-name", {
+              file: f,
+            }) as Promise<string>
+        )
     )
     return baseNames
   }
diff --git a/apps/core/src/storage/adapters/hybrid/HybridAdapter.test.ts b/apps/core/src/storage/adapters/hybrid/HybridAdapter.test.ts
new file mode 100644
index 00000000..05595aba
--- /dev/null
+++ b/apps/core/src/storage/adapters/hybrid/HybridAdapter.test.ts
@@ -0,0 +1,94 @@
+import { describe, it, expect, beforeEach, jest } from "@jest/globals"
+import { HybridAdapter } from "./HybridAdapter"
+
+const createMockAdapter = () => ({
+  saveImage: jest.fn(),
+  loadImage: jest.fn(),
+  deleteImage: jest.fn(),
+  listImages: jest.fn(),
+})
+
+describe("HybridAdapter", () => {
+  let local: any
+  let remote: any
+  let adapter: HybridAdapter
+
+  beforeEach(() => {
+    local = createMockAdapter()
+    remote = createMockAdapter()
+    adapter = new HybridAdapter(local, remote)
+  })
+
+  it("should be defined", () => {
+    expect(HybridAdapter).toBeDefined()
+  })
+
+  it("should call saveImage on both local and remote", async () => {
+    // Arrange
+    const id = "img1"
+    const data = Buffer.from("data")
+    // Act
+    await adapter.saveImage(id, data)
+    // Assert
+    expect(local.saveImage).toHaveBeenCalledWith(id, data)
+    expect(remote.saveImage).toHaveBeenCalledWith(id, data)
+  })
+
+  it("should load image from local if available", async () => {
+    // Arrange
+    const id = "img2"
+    const buffer = Buffer.from("abc")
+    local.loadImage.mockResolvedValue(buffer)
+    // Act
+    const result = await adapter.loadImage(id)
+    // Assert
+    expect(local.loadImage).toHaveBeenCalledWith(id)
+    expect(result).toBe(buffer)
+    expect(remote.loadImage).not.toHaveBeenCalled()
+  })
+
+  it("should load image from remote if local fails", async () => {
+    // Arrange
+    const id = "img3"
+    local.loadImage.mockRejectedValue(new Error("fail"))
+    const remoteBuffer = Buffer.from("remote")
+    remote.loadImage.mockResolvedValue(remoteBuffer)
+    // Act
+    const result = await adapter.loadImage(id)
+    // Assert
+    expect(local.loadImage).toHaveBeenCalledWith(id)
+    expect(remote.loadImage).toHaveBeenCalledWith(id)
+    expect(result).toBe(remoteBuffer)
+  })
+
+  it("should convert string result to Buffer in loadImage", async () => {
+    // Arrange
+    const id = "img4"
+    local.loadImage.mockResolvedValue("stringdata")
+    // Act
+    const result = await adapter.loadImage(id)
+    // Assert
+    expect(result).toEqual(Buffer.from("stringdata"))
+  })
+
+  it("should call deleteImage on both local and remote", async () => {
+    // Arrange
+    const id = "img5"
+    // Act
+    await adapter.deleteImage(id)
+    // Assert
+    expect(local.deleteImage).toHaveBeenCalledWith(id)
+    expect(remote.deleteImage).toHaveBeenCalledWith(id)
+  })
+
+  it("should list images from local", async () => {
+    // Arrange
+    const images = ["a", "b"]
+    local.listImages.mockResolvedValue(images)
+    // Act
+    const result = await adapter.listImages()
+    // Assert
+    expect(local.listImages).toHaveBeenCalled()
+    expect(result).toBe(images)
+  })
+})
diff --git a/apps/core/src/storage/adapters/hybrid/HybridAdapter.ts b/apps/core/src/storage/adapters/hybrid/HybridAdapter.ts
index 40f5943b..569b5095 100644
--- a/apps/core/src/storage/adapters/hybrid/HybridAdapter.ts
+++ b/apps/core/src/storage/adapters/hybrid/HybridAdapter.ts
@@ -16,7 +16,6 @@ export class HybridAdapter implements IStorageAdapter {
       const result = await this.local.loadImage(id)
       return typeof result === "string" ? Buffer.from(result) : result
     } catch (e) {
-      console.warn(`Local load failed, falling back to remote: ${e}`)
       const result = await this.remote.loadImage(id)
       return typeof result === "string" ? Buffer.from(result) : result
     }
diff --git a/apps/core/src/storage/adapters/s3/S3StorageAdapter.test.ts b/apps/core/src/storage/adapters/s3/S3StorageAdapter.test.ts
new file mode 100644
index 00000000..57e1f059
--- /dev/null
+++ b/apps/core/src/storage/adapters/s3/S3StorageAdapter.test.ts
@@ -0,0 +1,115 @@
+import { describe, it, expect, beforeEach, jest } from "@jest/globals"
+import { S3StorageAdapter } from "./S3StorageAdapter"
+
+jest.mock("@aws-sdk/client-s3", () => {
+  return {
+    S3Client: jest.fn().mockImplementation(() => ({ send: jest.fn() })),
+    PutObjectCommand: jest.fn(),
+    GetObjectCommand: jest.fn(),
+    DeleteObjectCommand: jest.fn(),
+    ListObjectsV2Command: jest.fn(),
+  }
+})
+jest.mock("@aws-sdk/credential-provider-cognito-identity", () => ({
+  fromCognitoIdentityPool: jest.fn(() => ({})),
+}))
+
+describe("S3StorageAdapter", () => {
+  let adapter: S3StorageAdapter
+  let s3Instance: any
+  const bucket = "test-bucket"
+  const region = "us-east-1"
+  const identityPoolId = "pool-id"
+
+  beforeEach(() => {
+    // Arrange
+    const { S3Client } = require("@aws-sdk/client-s3")
+    s3Instance = { send: jest.fn() }
+    S3Client.mockImplementation(() => s3Instance)
+    adapter = new S3StorageAdapter(bucket, region, identityPoolId)
+  })
+
+  it("should be defined", () => {
+    expect(S3StorageAdapter).toBeDefined()
+  })
+
+  it("should call send with PutObjectCommand on saveImage", async () => {
+    // Arrange
+    const id = "img1"
+    const data = Buffer.from("data")
+    // Act
+    await adapter.saveImage(id, data)
+    // Assert
+    const { PutObjectCommand } = require("@aws-sdk/client-s3")
+    expect(PutObjectCommand).toHaveBeenCalledWith({
+      Bucket: bucket,
+      Key: id,
+      Body: data,
+    })
+    expect(s3Instance.send).toHaveBeenCalled()
+  })
+
+  it("should call send with GetObjectCommand and return buffer on loadImage", async () => {
+    // Arrange
+    const id = "img2"
+    const chunk1 = new Uint8Array([1, 2])
+    const chunk2 = new Uint8Array([3, 4])
+    const mockStream = {
+      getReader: () => {
+        let call = 0
+        return {
+          read: jest.fn().mockImplementation(() => {
+            call++
+            if (call === 1)
+              return Promise.resolve({ value: chunk1, done: false })
+            if (call === 2)
+              return Promise.resolve({ value: chunk2, done: false })
+            return Promise.resolve({ value: undefined, done: true })
+          }),
+        }
+      },
+    }
+    s3Instance.send.mockResolvedValueOnce({ Body: mockStream })
+    // Act
+    const result = await adapter.loadImage(id)
+    // Assert
+    const { GetObjectCommand } = require("@aws-sdk/client-s3")
+    expect(GetObjectCommand).toHaveBeenCalledWith({ Bucket: bucket, Key: id })
+    expect(result).toEqual(Buffer.concat([chunk1, chunk2]))
+  })
+
+  it("should call send with DeleteObjectCommand on deleteImage", async () => {
+    // Arrange
+    const id = "img3"
+    // Act
+    await adapter.deleteImage(id)
+    // Assert
+    const { DeleteObjectCommand } = require("@aws-sdk/client-s3")
+    expect(DeleteObjectCommand).toHaveBeenCalledWith({
+      Bucket: bucket,
+      Key: id,
+    })
+    expect(s3Instance.send).toHaveBeenCalled()
+  })
+
+  it("should call send with ListObjectsV2Command and return image keys on listImages", async () => {
+    // Arrange
+    const objects = [{ Key: "a.png" }, { Key: "b.png" }]
+    s3Instance.send.mockResolvedValueOnce({ Contents: objects })
+    // Act
+    const result = await adapter.listImages()
+    // Assert
+    const { ListObjectsV2Command } = require("@aws-sdk/client-s3")
+    expect(ListObjectsV2Command).toHaveBeenCalledWith({ Bucket: bucket })
+    expect(result).toEqual(["a.png", "b.png"])
+  })
+
+  it("should return empty array if Contents is undefined in listImages", async () => {
+    // Arrange
+    s3Instance.send.mockResolvedValueOnce({})
+    // Act
+    const result = await adapter.listImages()
+    // Assert
+    expect(result).toEqual([])
+  })
+})
diff --git a/apps/core/src/storage/interfaces/IStorageAdapter.test.ts b/apps/core/src/storage/interfaces/IStorageAdapter.test.ts
new file mode 100644
index 00000000..96452495
--- /dev/null
+++ b/apps/core/src/storage/interfaces/IStorageAdapter.test.ts
@@ -0,0 +1,16 @@
+import { describe, it, expect } from "@jest/globals"
+import { DexieDataAccess } from "../../data/sources/dexie/DexieDataAccess"
+import { ApiDataAccess } from "../../data/sources/api/ApiDataAccess"
+import { SQLiteDataAccess } from "../../data/sources/sqlite/SQLiteDataAccess"
+
+describe("Interface and DataAccess", () => {
+  it("DexieDataAccess should be defined", () => {
+    expect(DexieDataAccess).toBeDefined()
+  })
+  it("ApiDataAccess should be defined", () => {
+    expect(ApiDataAccess).toBeDefined()
+  })
+  it("SQLiteDataAccess should be defined", () => {
+    expect(SQLiteDataAccess).toBeDefined()
+  })
+})
diff --git a/package.json b/package.json
index 7b874f80..a0445f86 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
     "prepare": "husky",
     "format": "prettier --write \"**/*.{js,jsx,ts,tsx,json,css,scss,md}\" --ignore-path .gitignore --ignore-path .prettierignore",
     "commit": "cz",
-    "test": "jest --coverage --watchAll=false",
+    "test": "jest --coverage",
     "dev": "concurrently \"cd apps/studio && yarn dev\" \"cd apps/desktop && yarn dev\"",
     "build": "concurrently \"yarn workspaces studio build\" \"yarn workspaces desktop build\"",
     "dev:web": "yarn workspaces web dev",
diff --git a/sonar-project.properties b/sonar-project.properties
new file mode 100644
index 00000000..8b686a03
--- /dev/null
+++ b/sonar-project.properties
@@ -0,0 +1,3 @@
+sonar.projectKey=vailabel_vailabel-studio
+sonar.organization=vailabel
+sonar.javascript.lcov.reportPaths=./coverage/lcov.info
\ No newline at end of file
diff --git a/yarn.lock b/yarn.lock
index 83e47f1e..e85fcb60 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6076,6 +6076,11 @@ extsprintf@^1.2.0:
   resolved "https://registry.npmjs.org/extsprintf/-/extsprintf-1.4.1.tgz"
   integrity sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==
 
+fake-indexeddb@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.0.1.tgz"
+  integrity sha512-He2AjQGHe46svIFq5+L2Nx/eHDTI1oKgoevBP+TthnjymXiKkeJQ3+ITeWey99Y5+2OaPFbI1qEsx/5RsGtWnQ==
+
 fast-content-type-parse@^2.0.0:
   version "2.0.1"
   resolved "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-2.0.1.tgz"
@@ -9987,7 +9992,7 @@ sprintf-js@~1.0.2:
 
 sqlite3@^5.1.7:
   version "5.1.7"
-  resolved "https://registry.yarnpkg.com/sqlite3/-/sqlite3-5.1.7.tgz#59ca1053c1ab38647396586edad019b1551041b7"
+  resolved "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz"
   integrity sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==
   dependencies:
     bindings "^1.5.0"


Reviews:
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T01:44:41Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T01:51:03Z
user: login: github-advanced-security[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T01:53:05Z

Review comments:
diff_hunk: @@ -0,0 +1,35 @@
+name: Test and SonarCloud
+
+on:
+  push:
+    branches: [main]
+  pull_request:
+    branches: [main]
+
+jobs:
+  test-and-sonarcloud:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout code
+        uses: actions/checkout@v4
+
+      - name: Set up Node.js
+        uses: actions/setup-node@v4
+        with:
+          node-version: '20'
+
+      - name: Install dependencies
+        run: yarn install
+
+      - name: Run tests with coverage
+        run: yarn test --coverage
+
+      - name: Upload coverage to SonarCloud
+        uses: SonarSource/sonarcloud-github-action@v2
+        with:
+          args: >
+            -Dsonar.projectKey=${{ github.repository }}
+            -Dsonar.organization=<your-sonarcloud-org>
+            -Dsonar.javascript.lcov.reportPaths=coverage/lcov.info
+        env:
+          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
path: .github/workflows/test-and-sonarcloud.yml
user: login: github-advanced-security[bot]
body: ## Workflow does not contain permissions

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {{contents: read}}

[Show more details](https://github.com/vailabel/vailabel-studio/security/code-scanning/4)
created_at: 2025-05-19T01:44:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,23 @@
+name: Test and SonarCloud
+on:
+ push:
+   branches:
+     - master
+ pull_request:
+   types: [opened, synchronize, reopened]
+jobs:
+  sonarqube:
+    name: SonarQube
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: 0 
+      - name: Install dependencies
+        run: yarn
+      - name: Test and coverage
+        run: yarn jest --coverage
+      - name: SonarQube Scan
+        uses: SonarSource/sonarqube-scan-action@v4
+        env:
+          SONAR_TOKEN: ${{ secrets. SONAR_TOKEN }}
path: .github/workflows/test-and-sonarcloud.yml
user: login: github-advanced-security[bot]
body: ## Workflow does not contain permissions

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {{contents: read}}

[Show more details](https://github.com/vailabel/vailabel-studio/security/code-scanning/5)
created_at: 2025-05-19T01:51:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,23 @@
+name: Test and SonarCloud
+on:
+ push:
+   branches:
+     - main
+ pull_request:
+   types: [opened, synchronize, reopened]
+jobs:
+  sonarqube:
+    name: SonarQube
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v2
+        with:
+          fetch-depth: 0 
+      - name: Install dependencies
+        run: yarn
+      - name: Test and coverage
+        run: yarn jest --coverage
+      - name: SonarQube Scan
+        uses: SonarSource/sonarqube-scan-action@v4
+        env:
+          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
path: .github/workflows/test-and-sonarcloud.yml
user: login: github-advanced-security[bot]
body: ## Workflow does not contain permissions

Actions job or workflow does not limit the permissions of the GITHUB_TOKEN. Consider setting an explicit permissions block, using the following as a minimal starting point: {{contents: read}}

[Show more details](https://github.com/vailabel/vailabel-studio/security/code-scanning/6)
created_at: 2025-05-19T01:53:05Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-19T01:43:42Z
body: [vc]: #i96eq/6XpqOtTr+seVKBWJDwX8mSFFisLLRw+JO9zCs=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJ2YWlsYWJlbCIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3dlYiIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS92YWlsYWJlbC92YWlsYWJlbC9CR2FGMkJ2aHNpNnBvZ3J5U3BEaWJ6ZWVwTHRkIiwicHJldmlld1VybCI6InZhaWxhYmVsLWdpdC1mZWF0LWNvcmUtdGVzdC12YWlsYWJlbC52ZXJjZWwuYXBwIiwibmV4dENvbW1pdFN0YXR1cyI6IkRFUExPWUVEIiwibGl2ZUZlZWRiYWNrIjp7InJlc29sdmVkIjowLCJ1bnJlc29sdmVkIjowLCJ0b3RhbCI6MCwibGluayI6InZhaWxhYmVsLWdpdC1mZWF0LWNvcmUtdGVzdC12YWlsYWJlbC52ZXJjZWwuYXBwIn19LHsibmFtZSI6InZhaWxhYmVsLXN0dWRpbyIsInJvb3REaXJlY3RvcnkiOiJhcHBzL3N0dWRpbyIsImluc3BlY3RvclVybCI6Imh0dHBzOi8vdmVyY2VsLmNvbS92YWlsYWJlbC92YWlsYWJlbC1zdHVkaW8vNTk4SDUzbnQyM3hiVGZNdjFCVG9VYnV1blJNVyIsInByZXZpZXdVcmwiOiJ2YWlsYWJlbC1zdHVkaW8tZ2l0LWZlYXQtY29yZS10ZXN0LXZhaWxhYmVsLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJsaXZlRmVlZGJhY2siOnsicmVzb2x2ZWQiOjAsInVucmVzb2x2ZWQiOjAsInRvdGFsIjowLCJsaW5rIjoidmFpbGFiZWwtc3R1ZGlvLWdpdC1mZWF0LWNvcmUtdGVzdC12YWlsYWJlbC52ZXJjZWwuYXBwIn19XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **vailabel** | ✅ Ready ([Inspect](https://vercel.com/vailabel/vailabel/BGaF2Bvhsi6pogrySpDibzeepLtd)) | [Visit Preview](https://vailabel-git-feat-core-test-vailabel.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/vailabel-git-feat-core-test-vailabel.vercel.app?via=pr-comment-feedback-link) | May 19, 2025 2:21am |
| **vailabel-studio** | ✅ Ready ([Inspect](https://vercel.com/vailabel/vailabel-studio/598H53nt23xbTfMv1BToUbuunRMW)) | [Visit Preview](https://vailabel-studio-git-feat-core-test-vailabel.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/vailabel-studio-git-feat-core-test-vailabel.vercel.app?via=pr-comment-feedback-link) | May 19, 2025 2:21am |



author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-05-19T02:22:23Z
body: ## [![Quality Gate Failed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-failed-20px.png 'Quality Gate Failed')](https://sonarcloud.io/dashboard?id=vailabel_vailabel-studio&pullRequest=97) **Quality Gate failed**  
Failed conditions  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/failed-16px.png '') [5.6% Coverage on New Code](https://sonarcloud.io/component_measures?id=vailabel_vailabel-studio&pullRequest=97&metric=new_coverage&view=list) (required ≥ 80%)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=vailabel_vailabel-studio&pullRequest=97)


author_association: NONE