/
raiden
/
obsidian-git-encrypt
Обзор
Документация
Войти
/
raiden
/
obsidian-git-encrypt
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
tests/sourceControl/sourceControl.integration.test.ts
211 строк
9 KB
Robert Kuzhin
fix: clear staging after manual revert
10 авг 2026, 07:35
10 авг 2026, 07:35
1de1382
Код
Авторство
О чём код?
import { mkdtemp, mkdir, readFile, readdir, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { CryptoService } from "../../src/crypto/index.js"; import { GitRevisionMirror, SystemGitAdapter } from "../../src/git/index.js"; import { ManifestStore } from "../../src/mirror/index.js"; import { FilesystemSessionBackend, SessionManager, type VaultWatcherPort, } from "../../src/session/index.js"; import { defaultSettings, type EncryptedGitSettings } from "../../src/settings/index.js"; import type { VaultEventGate } from "../../src/vault/index.js"; const encoder = new TextEncoder(); const firstText = "# Snapshot\n\nFIRST_STAGE_VERSION_19b0"; const secondText = "# Snapshot\n\nSECOND_WORKING_VERSION_25d1"; const revertBaselineText = "# Revert\n\nBASELINE_VERSION_3f7a"; const revertSelectedText = "# Revert\n\nSELECTED_VERSION_98c2"; const revertLaterText = "# Revert\n\nLATER_WORKING_VERSION_51ea"; let root: string; let vaultRoot: string; let mirrorRoot: string; let stateRoot: string; let settings: EncryptedGitSettings; let gate: VaultEventGate; let session: SessionManager; describe.sequential("manual encrypted source control", () => { beforeAll(async () => { root = await mkdtemp(path.join(tmpdir(), "encrypted-git-source-control-")); vaultRoot = path.join(root, "vault"); mirrorRoot = path.join(root, "mirror"); stateRoot = path.join(root, "state"); await Promise.all([mkdir(vaultRoot), mkdir(mirrorRoot)]); settings = { ...defaultSettings(vaultRoot), mirrorPath: mirrorRoot, statePath: stateRoot, }; const watcher: VaultWatcherPort = { start: vi.fn(), stop: vi.fn() }; session = new SessionManager(new FilesystemSessionBackend({ vaultRoot, getSettings: () => settings, createWatcher: (createdGate) => { gate = createdGate; return watcher; }, })); await session.createVault(encoder.encode("source control integration password")); }, 30_000); afterAll(async () => { if (session.getState().hasKey) await session.lock({ force: true }); await rm(root, { recursive: true, force: true }); }); it("commits the exact staged snapshot and leaves later edits unstaged", async () => { const notePath = path.join(vaultRoot, "Snapshot.md"); await writeFile(notePath, firstText); gate.notify({ type: "create", path: "Snapshot.md" }); const initial = await session.getSourceControlStatus(); expect(initial.staged).toEqual([]); expect(initial.changes).toEqual([ expect.objectContaining({ kind: "added", path: "Snapshot.md" }), ]); const fileId = initial.changes[0]!.fileId; await session.stage(fileId); const exactStagedEnvelope = new Uint8Array( await readFile(path.join(stateRoot, "staging-objects", `${fileId}.enc`)), ); expect((await session.getSourceControlStatus()).staged).toHaveLength(1); expect((await session.getDiff(fileId, "staged")).text).toContain(firstText.split("\n")[2]); await assertNoPlaintext(stateRoot, [firstText]); await writeFile(notePath, secondText); gate.notify({ type: "modify", path: "Snapshot.md" }); const afterEdit = await session.getSourceControlStatus(); expect(afterEdit.staged).toHaveLength(1); expect(afterEdit.changes).toEqual([ expect.objectContaining({ kind: "modified", path: "Snapshot.md" }), ]); const afterStagingDiff = await session.getDiff(fileId, "after-staging"); expect(afterStagingDiff.text).toContain("-FIRST_STAGE_VERSION_19b0"); expect(afterStagingDiff.text).toContain("+SECOND_WORKING_VERSION_25d1"); await session.commitSelected("Commit exact staged snapshot"); const pending = await session.getSourceControlStatus(); expect(pending.staged).toEqual([]); expect(pending.changes).toEqual([ expect.objectContaining({ kind: "modified", path: "Snapshot.md" }), ]); const git = await SystemGitAdapter.open(mirrorRoot, { branch: "main", retryDelaysMs: [] }); await git.init(); const head = (await git.getHeadCommit())!; const key = session.requireKeyHandle(); const crypto = new CryptoService(); const config = JSON.parse( await readFile(path.join(mirrorRoot, "secure-vault.json"), "utf8"), ) as { vaultId: string }; const committedManifest = await new ManifestStore( new GitRevisionMirror(git, head), crypto, config.vaultId, ).load(key); expect(committedManifest.files[fileId]).toBeDefined(); const envelope = await git.readFileAt(head, `objects/${fileId}.enc`); expect(envelope).toEqual(exactStagedEnvelope); const plaintext = await crypto.decryptObject({ key, fileId, envelope }); try { expect(new TextDecoder().decode(plaintext)).toBe(firstText); } finally { envelope.fill(0); plaintext.fill(0); } await session.stage(fileId); await session.commitSelected("Commit remaining working version"); const clean = await session.getSourceControlStatus(); expect(clean.staged).toEqual([]); expect(clean.changes).toEqual([]); }, 30_000); it("clears staging when the working file returns to HEAD", async () => { const notePath = path.join(vaultRoot, "Revert.md"); await writeFile(notePath, revertBaselineText); gate.notify({ type: "create", path: "Revert.md" }); const created = await session.getSourceControlStatus(); const fileId = created.changes.find((change) => change.path === "Revert.md")!.fileId; await session.stage(fileId); await session.commitSelected("Create revert baseline"); const baseline = await session.getSourceControlStatus(); expect(baseline.staged).toEqual([]); expect(baseline.changes).toEqual([]); await writeFile(notePath, revertSelectedText); gate.notify({ type: "modify", path: "Revert.md" }); const changed = await session.getSourceControlStatus(); expect(changed.staged).toEqual([]); expect(changed.changes).toEqual([ expect.objectContaining({ fileId, kind: "modified", path: "Revert.md" }), ]); await session.stage(fileId); const selected = await session.getSourceControlStatus(); expect(selected.staged).toEqual([ expect.objectContaining({ fileId, kind: "modified", path: "Revert.md" }), ]); expect(selected.changes).toEqual([]); await writeFile(notePath, revertLaterText); gate.notify({ type: "modify", path: "Revert.md" }); const editedAfterSelection = await session.getSourceControlStatus(); expect(editedAfterSelection.staged).toEqual([ expect.objectContaining({ fileId, kind: "modified", path: "Revert.md" }), ]); expect(editedAfterSelection.changes).toEqual([ expect.objectContaining({ fileId, kind: "modified", path: "Revert.md" }), ]); await writeFile(notePath, revertBaselineText); gate.notify({ type: "modify", path: "Revert.md" }); const reverted = await session.getSourceControlStatus(); expect(reverted.headCommit).toBe(baseline.headCommit); expect(reverted.staged).toEqual([]); expect(reverted.changes).toEqual([]); await expect(readFile(path.join(stateRoot, "staging-state.enc"))) .rejects.toMatchObject({ code: "ENOENT" }); await expect(readFile(path.join(stateRoot, "staging-objects", `${fileId}.enc`))) .rejects.toMatchObject({ code: "ENOENT" }); await session.pullRemote(); const afterPull = await session.getSourceControlStatus(); expect(afterPull.headCommit).toBe(baseline.headCommit); expect(afterPull.staged).toEqual([]); expect(afterPull.changes).toEqual([]); }, 30_000); it("restores encrypted staging after a forced session restart", async () => { await writeFile(path.join(vaultRoot, "Snapshot.md"), `${secondText}\nRestart staging`); gate.notify({ type: "modify", path: "Snapshot.md" }); const changed = await session.getSourceControlStatus(); const fileId = changed.changes[0]!.fileId; await session.stage(fileId); await session.lock({ force: true }); await session.unlock(encoder.encode("source control integration password")); const restored = await session.getSourceControlStatus(); expect(restored.staged).toEqual([ expect.objectContaining({ fileId, path: "Snapshot.md", kind: "modified" }), ]); const stagedPath = path.join(stateRoot, "staging-objects", `${fileId}.enc`); const tampered = new Uint8Array(await readFile(stagedPath)); tampered[tampered.length - 1] = tampered[tampered.length - 1]! ^ 1; await writeFile(stagedPath, tampered); await expect(session.commitSelected("Must reject tampered staging")) .rejects.toThrow(); await session.unstageAll(); }, 30_000); }); async function assertNoPlaintext(searchRoot: string, markers: readonly string[]): Promise<void> { for (const entry of await readdir(searchRoot, { recursive: true, withFileTypes: true })) { if (!entry.isFile()) continue; const bytes = await readFile(path.join(entry.parentPath, entry.name)); for (const marker of markers) expect(bytes.includes(Buffer.from(marker))).toBe(false); } }