Merge branch 'main' into fix/external-review-checkout

This commit is contained in:
gsxdsm
2026-06-29 17:33:03 -07:00
committed by GitHub
3 changed files with 85 additions and 1 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Wait for task store secret database handles to close before cleanup.
category: fix
dev: Awaits the async secrets store close path during TaskStore shutdown to avoid teardown races.

View File

@@ -261,6 +261,7 @@ vi.mock("@fusion/core", async (importOriginal) => {
const {
mockExec,
mockExecSync,
mockExecFileSync,
mockFindPrForBranch,
mockCreatePr,
mockGetPrMergeStatus,
@@ -282,6 +283,16 @@ const {
};
}),
mockExecSync: vi.fn(() => ""),
mockExecFileSync: vi.fn((file: string, args: readonly string[] = [], options?: { cwd?: string }) => {
if (file === "git" && args.join(" ") === "remote get-url origin") {
const effectiveCwd = options?.cwd ?? process.cwd();
if (effectiveCwd !== "/repo" && effectiveCwd !== process.cwd()) {
throw new Error(`unexpected repository cwd: ${options?.cwd ?? "<unset>"}`);
}
return "https://github.com/owner/repo.git\n";
}
return "";
}),
mockFindPrForBranch: vi.fn(),
mockCreatePr: vi.fn(),
mockGetPrMergeStatus: vi.fn(),
@@ -307,6 +318,7 @@ vi.mock("node:child_process", async (importOriginal) => {
exec: mockExec,
execSync: mockExecSync,
execFile: mockExecFile,
execFileSync: mockExecFileSync,
};
});
@@ -861,6 +873,18 @@ function resetGitHubMocks() {
mockCreatePr.mockReset();
mockGetPrMergeStatus.mockReset();
mockMergePr.mockReset();
mockExecFileSync.mockReset();
mockExecFileSync.mockImplementation((file: string, args: readonly string[] = [], options?: { cwd?: string }) => {
if (file === "git" && args.join(" ") === "remote get-url origin") {
const effectiveCwd = options?.cwd ?? process.cwd();
if (effectiveCwd !== "/repo" && effectiveCwd !== process.cwd()) {
throw new Error(`unexpected repository cwd: ${options?.cwd ?? "<unset>"}`);
}
return "https://github.com/owner/repo.git\n";
}
return "";
});
mockFindPrForBranch.mockResolvedValue(null);
mockCreatePr.mockResolvedValue({
@@ -1006,6 +1030,36 @@ describe("processPullRequestMergeTask", () => {
expect.objectContaining({ number: 42, status: "open" }),
);
expect(store.updateTask).toHaveBeenCalledWith("FN-093", { status: "awaiting-pr-checks" });
expect(mockExecFileSync).toHaveBeenCalledWith(
"git",
["remote", "get-url", "origin"],
expect.objectContaining({ cwd: "/repo" }),
);
});
it("keeps the production missing-repository error when no project remote is resolvable", async () => {
mockExecFileSync.mockImplementationOnce(() => {
throw new Error("not a git repository");
});
const store = makeMockStore();
store.getTask.mockResolvedValue({
id: "FN-093",
title: "Task",
description: "Description",
column: "in-review",
paused: false,
log: [],
});
await expect(processPullRequestMergeTask(store as any, "/repo", "FN-093", {
findPrForBranch: mockFindPrForBranch,
createPr: mockCreatePr,
getPrMergeStatus: mockGetPrMergeStatus,
mergePr: mockMergePr,
} as any, () => undefined)).rejects.toThrow("processPullRequestMergeTask: could not determine repository");
expect(mockCreatePr).not.toHaveBeenCalled();
expect(mockGetPrMergeStatus).not.toHaveBeenCalled();
});
it("links an existing PR instead of creating a duplicate", async () => {
@@ -1050,6 +1104,7 @@ describe("processPullRequestMergeTask", () => {
} as any, mockGetTaskMergeBlocker);
expect(mockCreatePr).not.toHaveBeenCalled();
expect(mockGetPrMergeStatus).toHaveBeenCalledWith("owner", "repo", 7);
expect(store.logEntry).toHaveBeenCalledWith(
"FN-093",
"Linked existing PR",
@@ -1114,6 +1169,7 @@ describe("processPullRequestMergeTask", () => {
} as any, mockGetTaskMergeBlocker);
expect(result).toBe("merged");
expect(mockGetPrMergeStatus).toHaveBeenCalledWith("owner", "repo", 42);
expect(mockMergePr).toHaveBeenCalledWith({ number: 42, method: "squash" });
expect(store.moveTask).toHaveBeenCalledWith("FN-093", "done");
// Check that exec was called with the expected commands (options object and callback may follow)
@@ -1177,6 +1233,7 @@ describe("processPullRequestMergeTask", () => {
} as any, mockGetTaskMergeBlocker);
expect(result).toBe("waiting");
expect(mockGetPrMergeStatus).toHaveBeenCalledWith("owner", "repo", 42);
expect(mockMergePr).not.toHaveBeenCalled();
expect(store.moveTask).not.toHaveBeenCalled();
expect(store.updateTask).toHaveBeenCalledWith("FN-093", { status: "awaiting-pr-checks" });
@@ -1234,6 +1291,11 @@ describe("runDashboard — PR-first auto-merge queue", () => {
head: "fusion/fn-093",
base: "main",
});
expect(mockExecFileSync).toHaveBeenCalledWith(
"git",
["remote", "get-url", "origin"],
expect.objectContaining({ cwd: process.cwd() }),
);
expect(aiMergeTask).not.toHaveBeenCalled();
});
@@ -1263,6 +1325,11 @@ describe("runDashboard — PR-first auto-merge queue", () => {
head: "fusion/fn-093",
base: "main",
});
expect(mockExecFileSync).toHaveBeenCalledWith(
"git",
["remote", "get-url", "origin"],
expect.objectContaining({ cwd: process.cwd() }),
);
expect(aiMergeTask).not.toHaveBeenCalled();
});
});

View File

@@ -16135,8 +16135,18 @@ ${stepsSection}`;
this._archiveDb = null;
}
if (this.secretsCentralCore) {
void this.secretsCentralCore.close();
/**
* FNXC:TaskStoreShutdown 2026-06-29-13:04:
* TaskStore.close() must deterministically await the cached secrets CentralCore close before temp-root cleanup and test teardown continue.
* CentralCore.close() is currently synchronous internally, but awaiting the async contract prevents unhandled rejections and preserves shutdown safety if the central secrets handle gains asynchronous cleanup.
*/
const secretsCentralCore = this.secretsCentralCore;
this.secretsCentralCore = null;
try {
await secretsCentralCore.close();
} catch (err) {
console.warn(`[fusion] Could not close secrets central core on TaskStore close:`, err);
}
}
this.secretsStore = null;
if (this.pluginStore) {