test: close store handles in slow suites
This commit is contained in:
@@ -47,7 +47,7 @@ describe("TaskStore", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
store.stopWatching();
|
store.close();
|
||||||
await rm(rootDir, { recursive: true, force: true });
|
await rm(rootDir, { recursive: true, force: true });
|
||||||
await rm(globalDir, { recursive: true, force: true });
|
await rm(globalDir, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
@@ -1460,7 +1460,7 @@ describe("TaskStore", () => {
|
|||||||
expect(task.id).toBe("FN-001");
|
expect(task.id).toBe("FN-001");
|
||||||
expect(await freshStore.getTask(task.id)).toBeDefined();
|
expect(await freshStore.getTask(task.id)).toBeDefined();
|
||||||
|
|
||||||
freshStore.stopWatching();
|
freshStore.close();
|
||||||
await rm(freshRoot, { recursive: true, force: true });
|
await rm(freshRoot, { recursive: true, force: true });
|
||||||
await rm(freshGlobal, { recursive: true, force: true });
|
await rm(freshGlobal, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
@@ -4938,6 +4938,7 @@ Task with acceptance criteria
|
|||||||
expect(entries).toHaveLength(1);
|
expect(entries).toHaveLength(1);
|
||||||
expect(entries[0].id).toBe(task.id);
|
expect(entries[0].id).toBe(task.id);
|
||||||
expect(entries[0].description).toBe("Survival test");
|
expect(entries[0].description).toBe("Survival test");
|
||||||
|
newStore.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -5030,6 +5031,7 @@ Task with acceptance criteria
|
|||||||
const logs = await newStore.getActivityLog();
|
const logs = await newStore.getActivityLog();
|
||||||
expect(logs).toHaveLength(1);
|
expect(logs).toHaveLength(1);
|
||||||
expect(logs[0].taskId).toBe("FN-001");
|
expect(logs[0].taskId).toBe("FN-001");
|
||||||
|
newStore.close();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -6157,14 +6159,14 @@ Task with acceptance criteria
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Re-create store to simulate restart
|
// Re-create store to simulate restart
|
||||||
store.stopWatching();
|
store.close();
|
||||||
const store2 = new TaskStore(rootDir, globalDir);
|
const store2 = new TaskStore(rootDir, globalDir);
|
||||||
await store2.init();
|
await store2.init();
|
||||||
|
|
||||||
const reloaded = await store2.getTask(task.id);
|
const reloaded = await store2.getTask(task.id);
|
||||||
expect(reloaded.recoveryRetryCount).toBe(5);
|
expect(reloaded.recoveryRetryCount).toBe(5);
|
||||||
expect(reloaded.nextRecoveryAt).toBe(futureTime);
|
expect(reloaded.nextRecoveryAt).toBe(futureTime);
|
||||||
store2.stopWatching();
|
store2.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("schema migration: existing rows default to NULL (undefined) for recovery fields", async () => {
|
it("schema migration: existing rows default to NULL (undefined) for recovery fields", async () => {
|
||||||
@@ -6385,7 +6387,7 @@ Task with acceptance criteria
|
|||||||
if (existsSync(memoryPath)) {
|
if (existsSync(memoryPath)) {
|
||||||
await unlink(memoryPath);
|
await unlink(memoryPath);
|
||||||
}
|
}
|
||||||
localStore.stopWatching();
|
localStore.close();
|
||||||
|
|
||||||
// Re-init with memory disabled
|
// Re-init with memory disabled
|
||||||
const store2 = new TaskStore(localRoot, localGlobal);
|
const store2 = new TaskStore(localRoot, localGlobal);
|
||||||
@@ -6393,7 +6395,7 @@ Task with acceptance criteria
|
|||||||
await store2.init();
|
await store2.init();
|
||||||
await store2.updateSettings({ memoryEnabled: false } as any);
|
await store2.updateSettings({ memoryEnabled: false } as any);
|
||||||
// After setting false, verify we can init without creating
|
// After setting false, verify we can init without creating
|
||||||
store2.stopWatching();
|
store2.close();
|
||||||
|
|
||||||
// Create a third store with memory disabled in config
|
// Create a third store with memory disabled in config
|
||||||
const store3 = new TaskStore(localRoot, localGlobal);
|
const store3 = new TaskStore(localRoot, localGlobal);
|
||||||
@@ -6404,7 +6406,7 @@ Task with acceptance criteria
|
|||||||
// But init creates it by default, then we disabled it
|
// But init creates it by default, then we disabled it
|
||||||
// The key behavior is that when memoryEnabled is explicitly false,
|
// The key behavior is that when memoryEnabled is explicitly false,
|
||||||
// init() should not create the file
|
// init() should not create the file
|
||||||
store3.stopWatching();
|
store3.close();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(localRoot, { recursive: true, force: true });
|
await rm(localRoot, { recursive: true, force: true });
|
||||||
await rm(localGlobal, { recursive: true, force: true });
|
await rm(localGlobal, { recursive: true, force: true });
|
||||||
@@ -6435,7 +6437,7 @@ Task with acceptance criteria
|
|||||||
const content = await readFile(memoryPath, "utf-8");
|
const content = await readFile(memoryPath, "utf-8");
|
||||||
expect(content).toContain("# Project Memory");
|
expect(content).toContain("# Project Memory");
|
||||||
|
|
||||||
localStore.stopWatching();
|
localStore.close();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(localRoot, { recursive: true, force: true });
|
await rm(localRoot, { recursive: true, force: true });
|
||||||
await rm(localGlobal, { recursive: true, force: true });
|
await rm(localGlobal, { recursive: true, force: true });
|
||||||
@@ -6462,7 +6464,7 @@ Task with acceptance criteria
|
|||||||
const content = await readFile(memoryPath, "utf-8");
|
const content = await readFile(memoryPath, "utf-8");
|
||||||
expect(content).toBe(customContent);
|
expect(content).toBe(customContent);
|
||||||
|
|
||||||
localStore.stopWatching();
|
localStore.close();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(localRoot, { recursive: true, force: true });
|
await rm(localRoot, { recursive: true, force: true });
|
||||||
await rm(localGlobal, { recursive: true, force: true });
|
await rm(localGlobal, { recursive: true, force: true });
|
||||||
@@ -6664,7 +6666,7 @@ describe("RunMutationContext", () => {
|
|||||||
expect(lastEntry.action).toBe("Test action");
|
expect(lastEntry.action).toBe("Test action");
|
||||||
expect(lastEntry.outcome).toBe("Test outcome");
|
expect(lastEntry.outcome).toBe("Test outcome");
|
||||||
|
|
||||||
localStore.stopWatching();
|
localStore.close();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(localRoot, { recursive: true, force: true });
|
await rm(localRoot, { recursive: true, force: true });
|
||||||
await rm(localGlobal, { recursive: true, force: true });
|
await rm(localGlobal, { recursive: true, force: true });
|
||||||
@@ -6689,7 +6691,7 @@ describe("RunMutationContext", () => {
|
|||||||
expect(lastEntry.runContext).toBeUndefined();
|
expect(lastEntry.runContext).toBeUndefined();
|
||||||
expect(lastEntry.action).toBe("Test action");
|
expect(lastEntry.action).toBe("Test action");
|
||||||
|
|
||||||
localStore.stopWatching();
|
localStore.close();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(localRoot, { recursive: true, force: true });
|
await rm(localRoot, { recursive: true, force: true });
|
||||||
await rm(localGlobal, { recursive: true, force: true });
|
await rm(localGlobal, { recursive: true, force: true });
|
||||||
@@ -6717,7 +6719,7 @@ describe("RunMutationContext", () => {
|
|||||||
const lastEntry = updatedTask.log[updatedTask.log.length - 1];
|
const lastEntry = updatedTask.log[updatedTask.log.length - 1];
|
||||||
expect(lastEntry.runContext).toEqual(runContext);
|
expect(lastEntry.runContext).toEqual(runContext);
|
||||||
|
|
||||||
localStore.stopWatching();
|
localStore.close();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(localRoot, { recursive: true, force: true });
|
await rm(localRoot, { recursive: true, force: true });
|
||||||
await rm(localGlobal, { recursive: true, force: true });
|
await rm(localGlobal, { recursive: true, force: true });
|
||||||
@@ -6745,7 +6747,7 @@ describe("RunMutationContext", () => {
|
|||||||
const lastEntry = updatedTask.log[updatedTask.log.length - 1];
|
const lastEntry = updatedTask.log[updatedTask.log.length - 1];
|
||||||
expect(lastEntry.runContext).toEqual(runContext);
|
expect(lastEntry.runContext).toEqual(runContext);
|
||||||
|
|
||||||
localStore.stopWatching();
|
localStore.close();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(localRoot, { recursive: true, force: true });
|
await rm(localRoot, { recursive: true, force: true });
|
||||||
await rm(localGlobal, { recursive: true, force: true });
|
await rm(localGlobal, { recursive: true, force: true });
|
||||||
@@ -6776,7 +6778,7 @@ describe("RunMutationContext", () => {
|
|||||||
// Verify sorted by timestamp
|
// Verify sorted by timestamp
|
||||||
expect(new Date(mutations[0].timestamp).getTime()).toBeLessThan(new Date(mutations[1].timestamp).getTime());
|
expect(new Date(mutations[0].timestamp).getTime()).toBeLessThan(new Date(mutations[1].timestamp).getTime());
|
||||||
|
|
||||||
localStore.stopWatching();
|
localStore.close();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(localRoot, { recursive: true, force: true });
|
await rm(localRoot, { recursive: true, force: true });
|
||||||
await rm(localGlobal, { recursive: true, force: true });
|
await rm(localGlobal, { recursive: true, force: true });
|
||||||
@@ -6797,7 +6799,7 @@ describe("RunMutationContext", () => {
|
|||||||
|
|
||||||
expect(mutations).toEqual([]);
|
expect(mutations).toEqual([]);
|
||||||
|
|
||||||
localStore.stopWatching();
|
localStore.close();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(localRoot, { recursive: true, force: true });
|
await rm(localRoot, { recursive: true, force: true });
|
||||||
await rm(localGlobal, { recursive: true, force: true });
|
await rm(localGlobal, { recursive: true, force: true });
|
||||||
@@ -6824,7 +6826,7 @@ describe("RunMutationContext", () => {
|
|||||||
expect(mutations).toHaveLength(2);
|
expect(mutations).toHaveLength(2);
|
||||||
expect(mutations.map(m => m.action).sort()).toEqual(["Entry 1", "Entry 2"]);
|
expect(mutations.map(m => m.action).sort()).toEqual(["Entry 1", "Entry 2"]);
|
||||||
|
|
||||||
localStore.stopWatching();
|
localStore.close();
|
||||||
} finally {
|
} finally {
|
||||||
await rm(localRoot, { recursive: true, force: true });
|
await rm(localRoot, { recursive: true, force: true });
|
||||||
await rm(localGlobal, { recursive: true, force: true });
|
await rm(localGlobal, { recursive: true, force: true });
|
||||||
|
|||||||
@@ -189,6 +189,47 @@ function buildMultipart(fieldName: string, filename: string, contentType: string
|
|||||||
return { body, boundary };
|
return { body, boundary };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GitTestRepo = {
|
||||||
|
root: string;
|
||||||
|
repoDir: string;
|
||||||
|
headSha: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
let sharedGitTestRepo: GitTestRepo | null = null;
|
||||||
|
|
||||||
|
function getSharedGitTestRepo(): GitTestRepo {
|
||||||
|
if (sharedGitTestRepo) {
|
||||||
|
return sharedGitTestRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const root = mkdtempSync(join(tmpdir(), "kb-dashboard-git-"));
|
||||||
|
const remoteDir = join(root, "remote.git");
|
||||||
|
const repoDir = join(root, "repo");
|
||||||
|
|
||||||
|
mkdirSync(repoDir, { recursive: true });
|
||||||
|
execFileSync("git", ["init", "--bare", remoteDir], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["init", repoDir], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["-C", repoDir, "config", "user.email", "kb-tests@example.com"], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["-C", repoDir, "config", "user.name", "KB Tests"], { stdio: "pipe" });
|
||||||
|
writeFileSync(join(repoDir, "README.md"), "# Test Repo\n");
|
||||||
|
execFileSync("git", ["-C", repoDir, "add", "README.md"], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["-C", repoDir, "commit", "-m", "Initial commit"], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["-C", repoDir, "branch", "-M", "main"], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["-C", repoDir, "remote", "add", "origin", remoteDir], { stdio: "pipe" });
|
||||||
|
execFileSync("git", ["-C", repoDir, "push", "-u", "origin", "HEAD"], { stdio: "pipe" });
|
||||||
|
|
||||||
|
const headSha = execFileSync("git", ["-C", repoDir, "rev-parse", "HEAD"], { encoding: "utf-8", stdio: "pipe" }).trim();
|
||||||
|
sharedGitTestRepo = { root, repoDir, headSha };
|
||||||
|
return sharedGitTestRepo;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
if (sharedGitTestRepo) {
|
||||||
|
rmSync(sharedGitTestRepo.root, { recursive: true, force: true });
|
||||||
|
sharedGitTestRepo = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
describe("GET /tasks", () => {
|
describe("GET /tasks", () => {
|
||||||
let store: TaskStore;
|
let store: TaskStore;
|
||||||
|
|
||||||
@@ -6083,41 +6124,27 @@ describe("GET /tasks/:id/diff", () => {
|
|||||||
|
|
||||||
describe("done tasks with commit SHA", () => {
|
describe("done tasks with commit SHA", () => {
|
||||||
it("attempts git diff when commitSha is present", { timeout: 30_000 }, async () => {
|
it("attempts git diff when commitSha is present", { timeout: 30_000 }, async () => {
|
||||||
// Use a real git repo to test the commit-backed path
|
const gitRepo = getSharedGitTestRepo();
|
||||||
const testDir = mkdtempSync(join(tmpdir(), "kb-diff-test-"));
|
const localStore = createMockStore({
|
||||||
try {
|
getRootDir: vi.fn().mockReturnValue(gitRepo.repoDir),
|
||||||
execFileSync("git", ["init", testDir], { stdio: "pipe" });
|
});
|
||||||
execFileSync("git", ["-C", testDir, "config", "user.email", "test@test.com"], { stdio: "pipe" });
|
const doneTask = {
|
||||||
execFileSync("git", ["-C", testDir, "config", "user.name", "Test"], { stdio: "pipe" });
|
...FAKE_TASK_DETAIL,
|
||||||
writeFileSync(join(testDir, "a.txt"), "initial\n");
|
id: "FN-001",
|
||||||
execFileSync("git", ["-C", testDir, "add", "a.txt"], { stdio: "pipe" });
|
column: "done",
|
||||||
execFileSync("git", ["-C", testDir, "commit", "-m", "init"], { stdio: "pipe" });
|
mergeDetails: { commitSha: gitRepo.headSha },
|
||||||
|
};
|
||||||
|
(localStore.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
||||||
|
|
||||||
const headSha = execFileSync("git", ["-C", testDir, "rev-parse", "HEAD"], { encoding: "utf-8", stdio: "pipe" }).trim();
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use("/api", createApiRoutes(localStore));
|
||||||
|
|
||||||
const localStore = createMockStore({
|
const res = await GET(app, "/api/tasks/FN-001/diff");
|
||||||
getRootDir: vi.fn().mockReturnValue(testDir),
|
expect(res.status).toBe(200);
|
||||||
});
|
// The diff should be schema-compatible even if it returns empty
|
||||||
const doneTask = {
|
expect(Array.isArray(res.body.files)).toBe(true);
|
||||||
...FAKE_TASK_DETAIL,
|
expect(res.body.stats).toHaveProperty("filesChanged");
|
||||||
id: "FN-001",
|
|
||||||
column: "done",
|
|
||||||
mergeDetails: { commitSha: headSha },
|
|
||||||
};
|
|
||||||
(localStore.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
app.use(express.json());
|
|
||||||
app.use("/api", createApiRoutes(localStore));
|
|
||||||
|
|
||||||
const res = await GET(app, "/api/tasks/FN-001/diff");
|
|
||||||
expect(res.status).toBe(200);
|
|
||||||
// The diff should be schema-compatible even if it returns empty
|
|
||||||
expect(Array.isArray(res.body.files)).toBe(true);
|
|
||||||
expect(res.body.stats).toHaveProperty("filesChanged");
|
|
||||||
} finally {
|
|
||||||
rmSync(testDir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -6175,27 +6202,9 @@ describe("GET /tasks/:id/file-diffs", () => {
|
|||||||
describe("Git Management endpoints", () => {
|
describe("Git Management endpoints", () => {
|
||||||
let store: TaskStore;
|
let store: TaskStore;
|
||||||
let gitRepoDir: string;
|
let gitRepoDir: string;
|
||||||
let gitTestRoot: string;
|
|
||||||
|
|
||||||
function createGitTestRepo() {
|
|
||||||
gitTestRoot = mkdtempSync(join(tmpdir(), "kb-dashboard-git-"));
|
|
||||||
const remoteDir = join(gitTestRoot, "remote.git");
|
|
||||||
gitRepoDir = join(gitTestRoot, "repo");
|
|
||||||
|
|
||||||
mkdirSync(gitRepoDir, { recursive: true });
|
|
||||||
execFileSync("git", ["init", "--bare", remoteDir], { stdio: "pipe" });
|
|
||||||
execFileSync("git", ["init", gitRepoDir], { stdio: "pipe" });
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "config", "user.email", "kb-tests@example.com"], { stdio: "pipe" });
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "config", "user.name", "KB Tests"], { stdio: "pipe" });
|
|
||||||
writeFileSync(join(gitRepoDir, "README.md"), "# Test Repo\n");
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "add", "README.md"], { stdio: "pipe" });
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "commit", "-m", "Initial commit"], { stdio: "pipe" });
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "remote", "add", "origin", remoteDir], { stdio: "pipe" });
|
|
||||||
execFileSync("git", ["-C", gitRepoDir, "push", "-u", "origin", "HEAD"], { stdio: "pipe" });
|
|
||||||
}
|
|
||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
createGitTestRepo();
|
gitRepoDir = getSharedGitTestRepo().repoDir;
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -6204,14 +6213,6 @@ describe("Git Management endpoints", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
if (gitTestRoot) {
|
|
||||||
rmSync(gitTestRoot, { recursive: true, force: true });
|
|
||||||
gitTestRoot = "";
|
|
||||||
gitRepoDir = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function buildApp() {
|
function buildApp() {
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|||||||
Reference in New Issue
Block a user