chore(test-isolation): detect live engine lock + prune stale tests
Three coupled fixes to make `pnpm test:full` exit cleanly when the local `fn` dashboard is running: 1. scripts/check-test-isolation.mjs — replace timing-based "is the engine writing?" heuristic with a deterministic check: if `.fusion/engine.lock.lock/` exists (proper-lockfile's held-lock marker), the dir is engine-active and auto-skipped from violation reporting. The 2-second mutability probe is retained as a backstop for dirs with another external writer but no live lock. Also adds `engine.lock` / `engine.lock.lock/` to RUNTIME_IGNORE_PATTERNS so a mid-test engine start/stop doesn't trip the signature compare. 2. packages/dashboard/.../__tests__/GitManagerModal.test.tsx — prune the Status-panel Sync button + Recent-advances-events describe blocks. Their UI was removed in5d35b64bd("remove duplicate integration-advances UI") but the tests stayed and were timing out at 1s each. The Remotes-panel Sync describe is kept because the `remotes-sync-integration-tip-btn` still exists. 3. packages/engine/.../merge-reuse-task-worktree.slow.test.ts — update the happy-path assertion to reflect4c31e885b("merger auto-syncs project-root checkout after ref advance"). Before that change, the merger's `update-ref` advance left the project root's working tree stale, so `git status --porcelain` would differ after the merge. With auto-sync, the new file is tracked + clean at HEAD, so status doesn't change. Verify the file actually landed via `git ls-files` instead. After this, `pnpm test:full` exits 0 with the local dashboard running. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2991,180 +2991,6 @@ describe("GitManagerModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Sync Integration Tip button ────────────────────────────────
|
||||
|
||||
describe("Sync local tip button (Status panel)", () => {
|
||||
it("renders the Sync local tip button when integrationBranch is set", async () => {
|
||||
(fetchGitStatus as any).mockResolvedValue({
|
||||
branch: "main",
|
||||
commit: "abc1234",
|
||||
isDirty: false,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
integrationBranch: "main",
|
||||
isOnIntegrationBranch: true,
|
||||
});
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("sync-integration-tip-btn")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
it("the Sync local tip button is disabled when not on integration branch", async () => {
|
||||
(fetchGitStatus as any).mockResolvedValue({
|
||||
branch: "feature/foo",
|
||||
commit: "abc1234",
|
||||
isDirty: false,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
integrationBranch: "main",
|
||||
isOnIntegrationBranch: false,
|
||||
});
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
const btn = screen.getByTestId("sync-integration-tip-btn");
|
||||
expect(btn).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it("the Sync local tip button is disabled when no integrationBranch", async () => {
|
||||
(fetchGitStatus as any).mockResolvedValue({
|
||||
branch: "main",
|
||||
commit: "abc1234",
|
||||
isDirty: false,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
// no integrationBranch
|
||||
});
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Repository Status")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// Button should not render when there's no integrationBranch
|
||||
expect(screen.queryByTestId("sync-integration-tip-btn")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("calls POST /api/git/pull with correct body when Sync local tip is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
(fetchGitStatus as any).mockResolvedValue({
|
||||
branch: "main",
|
||||
commit: "abc1234",
|
||||
isDirty: false,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
integrationBranch: "main",
|
||||
isOnIntegrationBranch: true,
|
||||
});
|
||||
(fetchConfig as any).mockResolvedValue({ maxConcurrent: 4, rootDir: "/my/project" });
|
||||
(api as any).mockResolvedValue({ kind: "pull-clean", toSha: "deadbeef" });
|
||||
|
||||
render(
|
||||
<GitManagerModal
|
||||
isOpen={true}
|
||||
onClose={vi.fn()}
|
||||
tasks={mockTasks}
|
||||
addToast={mockAddToast}
|
||||
projectId="proj-123"
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("sync-integration-tip-btn")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByTestId("sync-integration-tip-btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(api).toHaveBeenCalledWith(
|
||||
expect.stringContaining("/git/pull"),
|
||||
expect.objectContaining({
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
worktreePath: "/my/project",
|
||||
integrationBranch: "main",
|
||||
taskId: undefined,
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("shows a success toast and refreshes status after sync succeeds", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
(fetchGitStatus as any).mockResolvedValue({
|
||||
branch: "main",
|
||||
commit: "abc1234",
|
||||
isDirty: false,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
integrationBranch: "main",
|
||||
isOnIntegrationBranch: true,
|
||||
});
|
||||
(fetchConfig as any).mockResolvedValue({ maxConcurrent: 4, rootDir: "/my/project" });
|
||||
(api as any).mockResolvedValue({ kind: "pull-clean", toSha: "deadbeef" });
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("sync-integration-tip-btn")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByTestId("sync-integration-tip-btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddToast).toHaveBeenCalledWith("Synced worktree to integration tip", "success");
|
||||
expect(fetchGitStatus).toHaveBeenCalledTimes(2); // initial load + post-sync refresh
|
||||
});
|
||||
});
|
||||
|
||||
it("shows error toast when sync fails", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
(fetchGitStatus as any).mockResolvedValue({
|
||||
branch: "main",
|
||||
commit: "abc1234",
|
||||
isDirty: false,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
integrationBranch: "main",
|
||||
isOnIntegrationBranch: true,
|
||||
});
|
||||
(fetchConfig as any).mockResolvedValue({ maxConcurrent: 4, rootDir: "/my/project" });
|
||||
(api as any).mockRejectedValue(new Error("merge conflict"));
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("sync-integration-tip-btn")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
await user.click(screen.getByTestId("sync-integration-tip-btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddToast).toHaveBeenCalledWith("merge conflict", "error");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sync local tip button (Remotes panel)", () => {
|
||||
it("renders the Sync local tip button in the remotes panel when integrationBranch is set", async () => {
|
||||
(fetchGitStatus as any).mockResolvedValue({
|
||||
@@ -3212,170 +3038,6 @@ describe("GitManagerModal", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ── Recent integration advance events panel ─────────────────────
|
||||
|
||||
describe("Recent integration advance events panel", () => {
|
||||
it("renders the events panel when there are succeeded events", async () => {
|
||||
(api as any).mockResolvedValue({
|
||||
events: [
|
||||
{
|
||||
taskId: "FN-101",
|
||||
integrationBranch: "main",
|
||||
toSha: "aabbccdd1234",
|
||||
fromSha: "00112233",
|
||||
advanceMode: "fast-forward",
|
||||
succeeded: true,
|
||||
advancedAt: new Date(Date.now() - 2 * 60 * 1000).toISOString(), // 2 minutes ago
|
||||
},
|
||||
{
|
||||
taskId: "FN-102",
|
||||
integrationBranch: "main",
|
||||
toSha: "eeff00112233",
|
||||
fromSha: "aabbccdd1234",
|
||||
advanceMode: "update-ref",
|
||||
succeeded: true,
|
||||
advancedAt: new Date(Date.now() - 10 * 60 * 1000).toISOString(), // 10 minutes ago
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("recent-advance-events")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getByText("FN-101")).toBeInTheDocument();
|
||||
expect(screen.getByText("FN-102")).toBeInTheDocument();
|
||||
// Short SHAs (first 8 chars)
|
||||
expect(screen.getByText("aabbccdd")).toBeInTheDocument();
|
||||
expect(screen.getByText("eeff0011")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows events in the order returned by the API (newest first)", async () => {
|
||||
(api as any).mockResolvedValue({
|
||||
events: [
|
||||
{
|
||||
taskId: "FN-200",
|
||||
integrationBranch: "main",
|
||||
toSha: "111111112222",
|
||||
fromSha: null,
|
||||
advanceMode: "fast-forward",
|
||||
succeeded: true,
|
||||
advancedAt: new Date(Date.now() - 1 * 60 * 1000).toISOString(),
|
||||
},
|
||||
{
|
||||
taskId: "FN-199",
|
||||
integrationBranch: "main",
|
||||
toSha: "222222223333",
|
||||
fromSha: null,
|
||||
advanceMode: "fast-forward",
|
||||
succeeded: true,
|
||||
advancedAt: new Date(Date.now() - 5 * 60 * 1000).toISOString(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("recent-advance-events")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const items = screen.getAllByRole("listitem");
|
||||
// FN-200 should appear before FN-199
|
||||
const firstText = items[0].textContent ?? "";
|
||||
const secondText = items[1].textContent ?? "";
|
||||
expect(firstText).toContain("FN-200");
|
||||
expect(secondText).toContain("FN-199");
|
||||
});
|
||||
|
||||
it("hides the events panel when there are no events", async () => {
|
||||
(api as any).mockResolvedValue({ events: [] });
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Repository Status")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("recent-advance-events")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides the events panel when only failed events are returned", async () => {
|
||||
(api as any).mockResolvedValue({
|
||||
events: [
|
||||
{
|
||||
taskId: "FN-300",
|
||||
integrationBranch: "main",
|
||||
toSha: "deadbeef1234",
|
||||
fromSha: null,
|
||||
advanceMode: "fast-forward",
|
||||
succeeded: false, // failed — should be filtered out
|
||||
advancedAt: new Date().toISOString(),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Repository Status")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.queryByTestId("recent-advance-events")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("refreshes events after a successful sync", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
(fetchGitStatus as any).mockResolvedValue({
|
||||
branch: "main",
|
||||
commit: "abc1234",
|
||||
isDirty: false,
|
||||
ahead: 0,
|
||||
behind: 0,
|
||||
integrationBranch: "main",
|
||||
isOnIntegrationBranch: true,
|
||||
});
|
||||
(fetchConfig as any).mockResolvedValue({ maxConcurrent: 4, rootDir: "/project" });
|
||||
|
||||
let apiCallCount = 0;
|
||||
(api as any).mockImplementation((path: string) => {
|
||||
if (path.includes("/tasks/merge-advance-events")) {
|
||||
apiCallCount++;
|
||||
return Promise.resolve({ events: [] });
|
||||
}
|
||||
return Promise.resolve({ kind: "pull-clean", toSha: "abc" });
|
||||
});
|
||||
|
||||
render(
|
||||
<GitManagerModal isOpen={true} onClose={vi.fn()} tasks={mockTasks} addToast={mockAddToast} />
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId("sync-integration-tip-btn")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
const callsBefore = apiCallCount;
|
||||
await user.click(screen.getByTestId("sync-integration-tip-btn"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddToast).toHaveBeenCalledWith("Synced worktree to integration tip", "success");
|
||||
});
|
||||
|
||||
// fetchMergeAdvanceEvents should have been called again after sync
|
||||
expect(apiCallCount).toBeGreaterThan(callsBefore);
|
||||
});
|
||||
});
|
||||
|
||||
describe("CSS regression coverage", () => {
|
||||
it("includes remotes layout selectors and mobile rules", () => {
|
||||
|
||||
Reference in New Issue
Block a user