Files
ss-tools/frontend/src/lib/models/__tests__/CommitModel.test.ts
busya 4fc3356312 refactor(frontend): migrate Svelte stores from .ts to .svelte.ts runes
- Delete legacy .ts stores (auth, activity, assistantChat, datasetReview, environmentContext, health, sidebar, taskDrawer, translationRun)
- Create new .svelte.ts runes-based stores using  reactive primitives
- Migrate i18n: /i18n → /i18n/index.svelte.js
- Migrate toasts: /toasts → /toasts.svelte.js
- Update all component imports across 180+ files: components, pages, routes, lib
- Remove fromStore() wrappers — use store.value directly with Svelte 5 runes
- Update test mocks for new import paths
- Add @DEPRECATED annotation to legacy  alias
2026-06-02 09:54:18 +03:00

331 lines
12 KiB
TypeScript

// frontend/src/lib/models/__tests__/CommitModel.test.ts
// #region CommitModelTests [C:3] [TYPE Module] [SEMANTICS test,model,commit]
// @BRIEF L1 unit tests for CommitModel — no DOM render, mocks API + toast + i18n.
// @RELATION BINDS_TO -> [CommitModel]
import { describe, it, expect, vi, beforeEach } from "vitest";
// ── Mocks ────────────────────────────────────────────────────────
vi.mock("../../../services/gitService.js", () => ({
gitService: {
getStatus: vi.fn(),
getDiff: vi.fn(),
commit: vi.fn(),
push: vi.fn(),
},
}));
vi.mock("$lib/api.js", () => ({
api: {
postApi: vi.fn(),
},
}));
vi.mock('$lib/toasts.svelte.js', () => ({
addToast: vi.fn(),
}));
vi.mock('$lib/i18n/index.svelte.js', () => ({
t: { subscribe: vi.fn() }, getT: vi.fn(() => ({})),
_: vi.fn(), getT: vi.fn(() => ({})),
}));
vi.mock("svelte/store", () => ({
get: vi.fn(() => ({ git: {} })),
}));
import { CommitModel } from "../CommitModel.svelte.ts";
import { gitService } from "../../../services/gitService.js";
import { api } from "$lib/api.js";
// ── Types ─────────────────────────────────────────────────────────
interface StatusFixture {
is_dirty: boolean;
current_branch: string;
staged_files: string[];
modified_files: string[];
untracked_files: string[];
[key: string]: unknown;
}
// ── Helpers ──────────────────────────────────────────────────────
/** Create a model with default constructor values. */
function createModel(overrides: Partial<{ dashboardId: string; envId: string | null; show: boolean }> = {}): CommitModel {
return new CommitModel({
dashboardId: "test-dashboard",
envId: "env-123",
show: true,
...overrides,
});
}
/** Minimal git status payload. */
function makeStatus(overrides: Partial<StatusFixture> = {}): StatusFixture {
return {
is_dirty: true,
current_branch: "main",
staged_files: ["file1.py"],
modified_files: [],
untracked_files: [],
...overrides,
};
}
// ── Suite ────────────────────────────────────────────────────────
describe("CommitModel — L1 invariants (no render)", () => {
let model: CommitModel;
beforeEach(() => {
vi.clearAllMocks();
model = createModel();
});
// ═══════════════════════════════════════════════════════════════
// @INVARIANT: loadStatus calls gitService
// ═══════════════════════════════════════════════════════════════
describe("loadStatus", () => {
it("fetches status and diff via gitService", async () => {
const status = makeStatus();
vi.mocked(gitService.getStatus).mockResolvedValueOnce(status);
vi.mocked(gitService.getDiff).mockResolvedValueOnce("staged diff content");
vi.mocked(gitService.getDiff).mockResolvedValueOnce("unstaged diff content");
await model.loadStatus();
expect(gitService.getStatus).toHaveBeenCalledWith("test-dashboard", "env-123");
expect(gitService.getDiff).toHaveBeenCalledWith("test-dashboard", null, true, "env-123");
expect(gitService.getDiff).toHaveBeenCalledWith("test-dashboard", null, false, "env-123");
expect(model.status).toEqual(status);
expect(model.loading).toBe(false);
});
it("combines staged and unstaged diff", async () => {
vi.mocked(gitService.getStatus).mockResolvedValueOnce(makeStatus());
vi.mocked(gitService.getDiff).mockResolvedValueOnce("staged content");
vi.mocked(gitService.getDiff).mockResolvedValueOnce("unstaged content");
await model.loadStatus();
expect(model.diff).toContain("STAGED CHANGES");
expect(model.diff).toContain("UNSTAGED CHANGES");
expect(model.diff).toContain("staged content");
expect(model.diff).toContain("unstaged content");
});
it("sets empty diff when there are no changes", async () => {
vi.mocked(gitService.getStatus).mockResolvedValueOnce(makeStatus());
vi.mocked(gitService.getDiff).mockResolvedValueOnce(null);
vi.mocked(gitService.getDiff).mockResolvedValueOnce(null);
await model.loadStatus();
expect(model.diff).toBe("");
});
it("sets error on API failure", async () => {
vi.mocked(gitService.getStatus).mockRejectedValueOnce(new Error("Network error"));
await model.loadStatus();
expect(model.status).toBeNull();
expect(model.diff).toBe("");
expect(model.loading).toBe(false);
});
it("does nothing when dashboardId is empty", async () => {
model.dashboardId = "";
await model.loadStatus();
expect(gitService.getStatus).not.toHaveBeenCalled();
});
it("does nothing when show is false", async () => {
model.show = false;
await model.loadStatus();
expect(gitService.getStatus).not.toHaveBeenCalled();
});
});
// ═══════════════════════════════════════════════════════════════
// @INVARIANT: loading lifecycle
// ═══════════════════════════════════════════════════════════════
describe("loading lifecycle", () => {
it("loadStatus sets loading true then false", async () => {
vi.mocked(gitService.getStatus).mockImplementation(() => new Promise((r) => setTimeout(r, 10)));
vi.mocked(gitService.getDiff).mockResolvedValueOnce("");
vi.mocked(gitService.getDiff).mockResolvedValueOnce("");
const promise = model.loadStatus();
expect(model.loading).toBe(true);
await promise;
expect(model.loading).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════
// @INVARIANT: handleGenerateMessage
// ═══════════════════════════════════════════════════════════════
describe("handleGenerateMessage", () => {
it("calls api.postApi and sets message", async () => {
vi.mocked(api.postApi).mockResolvedValueOnce({ message: "AI generated commit message" });
await model.handleGenerateMessage();
expect(api.postApi).toHaveBeenCalledWith(
expect.stringContaining("/git/repositories/test-dashboard/generate-message"),
undefined,
{ suppressToast: true },
);
expect(model.message).toBe("AI generated commit message");
expect(model.generatingMessage).toBe(false);
});
it("sets error on API failure", async () => {
vi.mocked(api.postApi).mockRejectedValueOnce(new Error("Generation failed"));
await model.handleGenerateMessage();
expect(model.generatingMessage).toBe(false);
});
it("appends env_id when envId is set", async () => {
vi.mocked(api.postApi).mockResolvedValueOnce({ message: "msg" });
await model.handleGenerateMessage();
expect(api.postApi).toHaveBeenCalledWith(
expect.stringContaining("env_id=env-123"),
undefined,
{ suppressToast: true },
);
});
});
// ═══════════════════════════════════════════════════════════════
// @INVARIANT: committing lifecycle
// ═══════════════════════════════════════════════════════════════
describe("committing lifecycle", () => {
it("sets committing true then false", async () => {
model.message = "fix: test";
model.status = makeStatus();
vi.mocked(gitService.commit).mockImplementation(() => new Promise((r) => setTimeout(r, 10)));
const promise = model.handleCommit();
expect(model.committing).toBe(true);
await promise;
expect(model.committing).toBe(false);
});
it("resets committing on failure", async () => {
model.message = "fix: test";
model.status = makeStatus();
vi.mocked(gitService.commit).mockRejectedValueOnce(new Error("Commit rejected"));
await model.handleCommit();
expect(model.committing).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════
// @INVARIANT: handleCommit calls gitService.commit
// ═══════════════════════════════════════════════════════════════
describe("handleCommit — API calls", () => {
it("calls gitService.commit with correct args", async () => {
model.message = "fix: resolve issue";
model.status = makeStatus();
model.autoPushAfterCommit = false;
vi.mocked(gitService.commit).mockResolvedValueOnce({});
await model.handleCommit();
expect(gitService.commit).toHaveBeenCalledWith(
"test-dashboard",
"fix: resolve issue",
[],
"env-123",
);
});
it("calls push when autoPushAfterCommit is true", async () => {
model.message = "fix: auto-push";
model.status = makeStatus();
model.autoPushAfterCommit = true;
vi.mocked(gitService.commit).mockResolvedValueOnce({});
vi.mocked(gitService.push).mockResolvedValueOnce({});
await model.handleCommit();
expect(gitService.push).toHaveBeenCalledWith("test-dashboard", "env-123");
});
it("does not call push when autoPushAfterCommit is false", async () => {
model.message = "fix: no-push";
model.status = makeStatus();
model.autoPushAfterCommit = false;
vi.mocked(gitService.commit).mockResolvedValueOnce({});
await model.handleCommit();
expect(gitService.push).not.toHaveBeenCalled();
});
it("clears message and closes modal on success", async () => {
model.message = "fix: done";
model.status = makeStatus();
model.autoPushAfterCommit = false;
vi.mocked(gitService.commit).mockResolvedValueOnce({});
await model.handleCommit();
expect(model.message).toBe("");
expect(model.show).toBe(false);
});
});
// ═══════════════════════════════════════════════════════════════
// @INVARIANT: canCommit derived
// ═══════════════════════════════════════════════════════════════
describe("canCommit derived", () => {
it("is false when loading", () => {
model.loading = true;
model.message = "fix";
model.status = makeStatus();
expect(model.canCommit).toBe(false);
});
it("is false when committing", () => {
model.committing = true;
model.message = "fix";
model.status = makeStatus();
expect(model.canCommit).toBe(false);
});
it("is false when message is empty", () => {
model.message = "";
model.status = makeStatus();
expect(model.canCommit).toBe(false);
});
it("is false when status is null", () => {
model.message = "fix";
model.status = null;
expect(model.canCommit).toBe(false);
});
it("is false when there are no changes", () => {
model.message = "fix";
model.status = makeStatus({ is_dirty: false, staged_files: [] });
expect(model.canCommit).toBe(false);
});
it("is true when ready to commit", () => {
model.message = "fix: ready";
model.status = makeStatus();
expect(model.canCommit).toBe(true);
});
});
});
// #endregion CommitModelTests