
























1+import fsSync from "node:fs";
12import fs from "node:fs/promises";
23import os from "node:os";
34import path from "node:path";
45import { DatabaseSync } from "node:sqlite";
5-import { afterAll, beforeAll, describe, expect, it } from "vitest";
6-import { openMemoryDatabaseAtPath } from "./manager-db.js";
6+import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest";
7+import {
8+cleanupAgedMemoryReindexTempFiles,
9+openMemoryDatabaseAtPath,
10+openMemoryReindexTempDatabaseAtPath,
11+} from "./manager-db.js";
12+import {
13+acquireMemoryReindexLock,
14+resolveMemoryReindexLockPath,
15+tryAcquireMemoryReindexLock,
16+} from "./manager-reindex-lock.js";
17+18+async function expectPathMissing(targetPath: string): Promise<void> {
19+await expect(fs.access(targetPath)).rejects.toThrow("ENOENT");
20+}
721822describe("openMemoryDatabaseAtPath readOnly probe", () => {
923let fixtureRoot = "";
@@ -17,6 +31,10 @@ describe("openMemoryDatabaseAtPath readOnly probe", () => {
1731await fs.rm(fixtureRoot, { recursive: true, force: true });
1832});
193334+afterEach(() => {
35+vi.restoreAllMocks();
36+});
37+2038it("allows opening when the database file exists", async () => {
2139const dbPath = path.join(fixtureRoot, `case-${caseId++}`, "index.sqlite");
2240const dir = path.dirname(dbPath);
@@ -41,6 +59,21 @@ describe("openMemoryDatabaseAtPath readOnly probe", () => {
4159expect(stat.size).toBeGreaterThan(0);
4260});
436162+it("refuses to create a missing live database while a safe reindex holds the lock", async () => {
63+const dbPath = path.join(fixtureRoot, `case-${caseId++}`, "index.sqlite");
64+await fs.mkdir(path.dirname(dbPath), { recursive: true });
65+const reindexLock = acquireMemoryReindexLock(dbPath);
66+67+expect(() => openMemoryDatabaseAtPath(dbPath, false, true)).toThrow(
68+/another reindex is active/,
69+);
70+await expectPathMissing(dbPath);
71+72+reindexLock.release();
73+const db = openMemoryDatabaseAtPath(dbPath, false, true);
74+db.close();
75+});
76+4477it("refuses to auto-create an empty database when allowCreate is false", async () => {
4578const dbPath = path.join(fixtureRoot, `case-${caseId++}`, "absent-index.sqlite");
4679@@ -54,12 +87,132 @@ describe("openMemoryDatabaseAtPath readOnly probe", () => {
5487it("allows open with allowCreate=true for temp database creation", async () => {
5588const dbPath = path.join(fixtureRoot, `case-${caseId++}`, "temp-index.sqlite");
568957-const db = openMemoryDatabaseAtPath(dbPath, false, true);
90+const db = openMemoryReindexTempDatabaseAtPath(dbPath, false);
5891db.exec("CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT)");
5992db.close();
93+await expectPathMissing(resolveMemoryReindexLockPath(dbPath));
60946195const reopen = openMemoryDatabaseAtPath(dbPath, false, false);
6296expect(reopen).toBeDefined();
6397reopen.close();
6498});
65-});
99+100+it("removes aged orphan reindex temp files before opening the live database", async () => {
101+const dbPath = path.join(fixtureRoot, `case-${caseId++}`, "index.sqlite");
102+const dir = path.dirname(dbPath);
103+await fs.mkdir(dir, { recursive: true });
104+const seed = new DatabaseSync(dbPath);
105+seed.exec("CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT)");
106+seed.close();
107+108+const orphanBase = `${dbPath}.tmp-11111111-2222-3333-4444-555555555555`;
109+for (const suffix of ["", "-wal", "-shm"]) {
110+const filePath = `${orphanBase}${suffix}`;
111+await fs.writeFile(filePath, "orphan");
112+const old = new Date(Date.now() - 48 * 60 * 60_000);
113+await fs.utimes(filePath, old, old);
114+}
115+116+const db = openMemoryDatabaseAtPath(dbPath, false);
117+db.close();
118+119+await expectPathMissing(orphanBase);
120+await expectPathMissing(`${orphanBase}-wal`);
121+await expectPathMissing(`${orphanBase}-shm`);
122+});
123+124+it("keeps young reindex temp files during live database startup", async () => {
125+const dbPath = path.join(fixtureRoot, `case-${caseId++}`, "index.sqlite");
126+const dir = path.dirname(dbPath);
127+await fs.mkdir(dir, { recursive: true });
128+const seed = new DatabaseSync(dbPath);
129+seed.exec("CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT)");
130+seed.close();
131+132+const activeBase = `${dbPath}.tmp-aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee`;
133+for (const suffix of ["", "-wal", "-shm"]) {
134+await fs.writeFile(`${activeBase}${suffix}`, "active");
135+}
136+137+const db = openMemoryDatabaseAtPath(dbPath, false);
138+db.close();
139+140+await expect(fs.access(activeBase)).resolves.toBeUndefined();
141+await expect(fs.access(`${activeBase}-wal`)).resolves.toBeUndefined();
142+await expect(fs.access(`${activeBase}-shm`)).resolves.toBeUndefined();
143+});
144+145+it("keeps aged reindex temp files while another process holds the reindex lock", async () => {
146+const dbPath = path.join(fixtureRoot, `case-${caseId++}`, "index.sqlite");
147+const dir = path.dirname(dbPath);
148+await fs.mkdir(dir, { recursive: true });
149+const seed = new DatabaseSync(dbPath);
150+seed.exec("CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT)");
151+seed.close();
152+153+const activeBase = `${dbPath}.tmp-99999999-aaaa-bbbb-cccc-dddddddddddd`;
154+for (const suffix of ["", "-wal", "-shm"]) {
155+const filePath = `${activeBase}${suffix}`;
156+await fs.writeFile(filePath, "active");
157+const old = new Date(Date.now() - 48 * 60 * 60_000);
158+await fs.utimes(filePath, old, old);
159+}
160+const reindexLock = acquireMemoryReindexLock(dbPath);
161+162+cleanupAgedMemoryReindexTempFiles(dbPath);
163+const db = openMemoryDatabaseAtPath(dbPath, false);
164+db.close();
165+166+await expect(fs.access(activeBase)).resolves.toBeUndefined();
167+await expect(fs.access(`${activeBase}-wal`)).resolves.toBeUndefined();
168+await expect(fs.access(`${activeBase}-shm`)).resolves.toBeUndefined();
169+reindexLock.release();
170+cleanupAgedMemoryReindexTempFiles(dbPath);
171+await expectPathMissing(activeBase);
172+await expectPathMissing(`${activeBase}-wal`);
173+await expectPathMissing(`${activeBase}-shm`);
174+});
175+176+it("keeps aged reindex temp files while the live database is absent", async () => {
177+const dbPath = path.join(fixtureRoot, `case-${caseId++}`, "index.sqlite");
178+await fs.mkdir(path.dirname(dbPath), { recursive: true });
179+const orphanBase = `${dbPath}.tmp-abcdef12-aaaa-bbbb-cccc-123456789abc`;
180+await fs.writeFile(orphanBase, "recovery candidate");
181+const old = new Date(Date.now() - 48 * 60 * 60_000);
182+await fs.utimes(orphanBase, old, old);
183+184+const db = openMemoryDatabaseAtPath(dbPath, false, true);
185+db.close();
186+187+await expect(fs.access(orphanBase)).resolves.toBeUndefined();
188+});
189+190+it("serializes safe reindexes and releases the lock for the next owner", async () => {
191+const dbPath = path.join(fixtureRoot, `case-${caseId++}`, "index.sqlite");
192+await fs.mkdir(path.dirname(dbPath), { recursive: true });
193+194+const first = acquireMemoryReindexLock(dbPath);
195+expect(tryAcquireMemoryReindexLock(dbPath)).toBeUndefined();
196+expect(() => acquireMemoryReindexLock(dbPath)).toThrow(/another reindex is active/);
197+198+first.release();
199+const second = tryAcquireMemoryReindexLock(dbPath);
200+expect(second).toBeDefined();
201+second?.release();
202+203+await expect(fs.access(resolveMemoryReindexLockPath(dbPath))).resolves.toBeUndefined();
204+});
205+206+it("does not block database startup when orphan discovery fails", async () => {
207+const dbPath = path.join(fixtureRoot, `case-${caseId++}`, "index.sqlite");
208+await fs.mkdir(path.dirname(dbPath), { recursive: true });
209+const seed = new DatabaseSync(dbPath);
210+seed.close();
211+vi.spyOn(fsSync, "readdirSync").mockImplementationOnce(() => {
212+throw Object.assign(new Error("scan failed"), { code: "EACCES" });
213+});
214+215+const db = openMemoryDatabaseAtPath(dbPath, false);
216+db.close();
217+});
218+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。