feat(FN-3986): remove PR lint pre-build and bun setup from pr-checks workfl

Removes the PR lint pre-build step from the GitHub Actions workflow and adds a new test suite (`ci-workflow.test.ts`) covering CI workflow behavior, with a small documentation update.

Fusion-Task-Id: FN-3986
This commit is contained in:
Fusion
2026-05-11 01:41:51 -07:00
committed by gsxdsm
parent fcbd9a4022
commit 0d7e24a72d
4 changed files with 74 additions and 24 deletions

View File

@@ -29,12 +29,6 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
- name: Install Bun
uses: oven-sh/setup-bun@v2
- name: Build plugins and core packages
run: pnpm build
- name: Lint - name: Lint
run: pnpm lint run: pnpm lint

View File

@@ -30,3 +30,4 @@ why it broke, how it was fixed, and what command verified the fix.
| TSH-012 | Warning filtering | The first SQLite warning filter also installed a `process.on("warning")` listener that could duplicate unrelated warnings. | Node still prints non-filtered warnings by default, so manually writing them again made future warning output noisier. | The shared setup now only wraps `process.emitWarning` for the known SQLite experimental warning, and the dashboard noisy-output marker list no longer suppresses generic trace-warning guidance. | `pnpm --filter @fusion/core typecheck`, `pnpm --filter @fusion/core exec vitest run src/__tests__/central-db.test.ts --silent=passed-only --reporter=dot`, and `pnpm test:full` | | TSH-012 | Warning filtering | The first SQLite warning filter also installed a `process.on("warning")` listener that could duplicate unrelated warnings. | Node still prints non-filtered warnings by default, so manually writing them again made future warning output noisier. | The shared setup now only wraps `process.emitWarning` for the known SQLite experimental warning, and the dashboard noisy-output marker list no longer suppresses generic trace-warning guidance. | `pnpm --filter @fusion/core typecheck`, `pnpm --filter @fusion/core exec vitest run src/__tests__/central-db.test.ts --silent=passed-only --reporter=dot`, and `pnpm test:full` |
| TSH-013 | Dashboard default runtime | The default dashboard package test still took about 9 minutes after the noise cleanup. | `pnpm --filter @fusion/dashboard test` continued to run every app/jsdom and API/node file, including exhaustive modal/view permutations and broad route matrices intended for deeper sweeps. | Added curated `dashboard-app-quality` and `dashboard-api-quality` Vitest projects for the default package gate, kept exhaustive coverage behind `test:deep`, `test:app`, and `test:api`, and documented when to run each lane. | `/usr/bin/time -p pnpm --filter @fusion/dashboard test` (148 files, 3642 tests, `real 91.73`), `/usr/bin/time -p pnpm --filter @fusion/dashboard test:deep` (419 files, 10747 tests, `real 335.47`), and `/usr/bin/time -p pnpm test:full` (`real 308.34`) | | TSH-013 | Dashboard default runtime | The default dashboard package test still took about 9 minutes after the noise cleanup. | `pnpm --filter @fusion/dashboard test` continued to run every app/jsdom and API/node file, including exhaustive modal/view permutations and broad route matrices intended for deeper sweeps. | Added curated `dashboard-app-quality` and `dashboard-api-quality` Vitest projects for the default package gate, kept exhaustive coverage behind `test:deep`, `test:app`, and `test:api`, and documented when to run each lane. | `/usr/bin/time -p pnpm --filter @fusion/dashboard test` (148 files, 3642 tests, `real 91.73`), `/usr/bin/time -p pnpm --filter @fusion/dashboard test:deep` (419 files, 10747 tests, `real 335.47`), and `/usr/bin/time -p pnpm test:full` (`real 308.34`) |
| TSH-014 | Core temp-dir cleanup | `kb-db-test-*` directories could leak after `db.test.ts` runs and trip isolation checks. | `afterEach(async)` and module-level `afterAll(async)` both cleared `createdTmpDirs` before async `rm(...)` completed, so hook timing races could hide leftovers from final teardown during worker shutdown. | Coordinated cleanup bookkeeping by deleting set entries only after per-dir removal, kept `afterEach` as async best-effort cleanup, and switched the final `afterAll` fallback to defensive synchronous `rmSync(..., { recursive: true, force: true })`. | `pnpm --filter @fusion/core test -- src/__tests__/db.test.ts` and `node scripts/check-test-isolation.mjs --before && pnpm --filter @fusion/core test && node scripts/check-test-isolation.mjs` | | TSH-014 | Core temp-dir cleanup | `kb-db-test-*` directories could leak after `db.test.ts` runs and trip isolation checks. | `afterEach(async)` and module-level `afterAll(async)` both cleared `createdTmpDirs` before async `rm(...)` completed, so hook timing races could hide leftovers from final teardown during worker shutdown. | Coordinated cleanup bookkeeping by deleting set entries only after per-dir removal, kept `afterEach` as async best-effort cleanup, and switched the final `afterAll` fallback to defensive synchronous `rmSync(..., { recursive: true, force: true })`. | `pnpm --filter @fusion/core test -- src/__tests__/db.test.ts` and `node scripts/check-test-isolation.mjs --before && pnpm --filter @fusion/core test && node scripts/check-test-isolation.mjs` |
| TSH-015 | PR lint workflow contract | PR `lint` spent extra CI minutes setting up Bun and running `pnpm build` before lint. | `pr-checks.yml` drifted from the CI pattern where lint should be the fastest install+lint gate and build should be enforced by the separate `build` job. | Removed Bun setup and pre-lint build from the PR `lint` job, and strengthened workflow contract tests to require `pnpm install --frozen-lockfile` + `pnpm lint` in lint while asserting Bun/build remain in the PR `build` job. | `pnpm --filter @runfusion/fusion exec vitest run src/__tests__/ci-workflow.test.ts --silent=passed-only --reporter=dot`, `pnpm lint`, `pnpm test`, `pnpm typecheck`, and `pnpm build` |

View File

@@ -178,8 +178,38 @@ describe("PR checks workflow (.github/workflows/pr-checks.yml)", () => {
expect(content).not.toContain("run: pnpm test\n"); expect(content).not.toContain("run: pnpm test\n");
}); });
it("keeps lint as install + lint only, without Bun/setup build coupling", () => {
const lintSteps = workflow.jobs?.lint?.steps ?? [];
expect(
lintSteps.some(
(step: any) =>
step.name === "Install dependencies" &&
typeof step.run === "string" &&
step.run.includes("pnpm install --frozen-lockfile"),
),
).toBe(true);
expect(
lintSteps.some((step: any) => step.name === "Lint" && typeof step.run === "string" && step.run.includes("pnpm lint")),
).toBe(true);
expect(
lintSteps.some(
(step: any) =>
step.name === "Install Bun" ||
(typeof step.uses === "string" && step.uses.includes("oven-sh/setup-bun")) ||
(typeof step.run === "string" && step.run.includes("pnpm build")),
),
).toBe(false);
});
it("keeps build coverage as an explicit PR gate", () => { it("keeps build coverage as an explicit PR gate", () => {
const buildSteps = workflow.jobs?.build?.steps ?? []; const buildSteps = workflow.jobs?.build?.steps ?? [];
expect(
buildSteps.some(
(step: any) =>
step.name === "Install Bun" ||
(typeof step.uses === "string" && step.uses.includes("oven-sh/setup-bun")),
),
).toBe(true);
expect( expect(
buildSteps.some( buildSteps.some(
(step: any) => step.name === "Build" && typeof step.run === "string" && step.run.includes("pnpm build"), (step: any) => step.name === "Build" && typeof step.run === "string" && step.run.includes("pnpm build"),

View File

@@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it, vi } from "vitest"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import type { ManagedDockerNode, MeshConnectionConfig } from "../types.js"; import type { ManagedDockerNode, MeshConnectionConfig } from "../types.js";
// ── Mocks ────────────────────────────────────────────────────────────────── // ── Mocks ──────────────────────────────────────────────────────────────────
@@ -224,6 +224,14 @@ describe("MeshConfigGenerator", () => {
// ── applyConfig ──────────────────────────────────────────────────────── // ── applyConfig ────────────────────────────────────────────────────────
describe("applyConfig", () => { describe("applyConfig", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
const config: MeshConnectionConfig = { const config: MeshConnectionConfig = {
nodeApiKey: "test-key", nodeApiKey: "test-key",
reachableUrl: "http://localhost:4041", reachableUrl: "http://localhost:4041",
@@ -249,7 +257,9 @@ describe("MeshConfigGenerator", () => {
containerId: "new-container-id", containerId: "new-container-id",
}); });
await generator.applyConfig("dn_test123", config, { host: undefined }); const applyPromise = generator.applyConfig("dn_test123", config, { host: undefined });
await vi.advanceTimersByTimeAsync(2_500);
await applyPromise;
// Status set to "recreating" first // Status set to "recreating" first
expect(mockCentral.updateManagedDockerNode).toHaveBeenCalledWith( expect(mockCentral.updateManagedDockerNode).toHaveBeenCalledWith(
@@ -377,17 +387,19 @@ describe("MeshConfigGenerator", () => {
// Use fake timers to speed up the timeout test // Use fake timers to speed up the timeout test
vi.useFakeTimers(); vi.useFakeTimers();
const resultPromise = generator.registerInMesh("dn_test123", config); try {
const resultPromise = generator.registerInMesh("dn_test123", config);
// Fast-forward through the polling // Fast-forward through the polling
await vi.advanceTimersByTimeAsync(35_000); await vi.advanceTimersByTimeAsync(35_000);
const result = await resultPromise; const result = await resultPromise;
expect(result.isHealthy).toBe(false); expect(result.isHealthy).toBe(false);
expect(result.error).toContain("did not reach online status"); expect(result.error).toContain("did not reach online status");
} finally {
vi.useRealTimers(); vi.useRealTimers();
}
}); });
it("re-throws when registration fails", async () => { it("re-throws when registration fails", async () => {
@@ -415,6 +427,14 @@ describe("MeshConfigGenerator", () => {
// ── provisionAndRegister ────────────────────────────────────────────── // ── provisionAndRegister ──────────────────────────────────────────────
describe("provisionAndRegister", () => { describe("provisionAndRegister", () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it("runs full end-to-end flow: generate → apply → register", async () => { it("runs full end-to-end flow: generate → apply → register", async () => {
const generator = createGenerator(); const generator = createGenerator();
const node = createManagedNode(); const node = createManagedNode();
@@ -429,7 +449,7 @@ describe("MeshConfigGenerator", () => {
mockCentral.linkManagedDockerNodeToNode.mockResolvedValue(node); mockCentral.linkManagedDockerNodeToNode.mockResolvedValue(node);
mockCentral.checkNodeHealth.mockResolvedValue("online"); mockCentral.checkNodeHealth.mockResolvedValue("online");
const result = await generator.provisionAndRegister({ const resultPromise = generator.provisionAndRegister({
managedNode: node, managedNode: node,
orchestratorUrl: "http://orchestrator:4040", orchestratorUrl: "http://orchestrator:4040",
orchestratorApiKey: "orch-key", orchestratorApiKey: "orch-key",
@@ -437,6 +457,9 @@ describe("MeshConfigGenerator", () => {
containerPort: 4041, containerPort: 4041,
}); });
await vi.advanceTimersByTimeAsync(2_500);
const result = await resultPromise;
expect(result.isHealthy).toBe(true); expect(result.isHealthy).toBe(true);
expect(result.config.nodeApiKey).toBe("my-key"); expect(result.config.nodeApiKey).toBe("my-key");
expect(result.config.envVars.FUSION_DAEMON_TOKEN).toBe("my-key"); expect(result.config.envVars.FUSION_DAEMON_TOKEN).toBe("my-key");
@@ -482,13 +505,15 @@ describe("MeshConfigGenerator", () => {
mockDockerClient.recreateContainer.mockResolvedValue("new-container-id"); mockDockerClient.recreateContainer.mockResolvedValue("new-container-id");
mockCentral.registerNode.mockRejectedValue(new Error("Registration failed")); mockCentral.registerNode.mockRejectedValue(new Error("Registration failed"));
await expect( const resultPromise = generator.provisionAndRegister({
generator.provisionAndRegister({ managedNode: node,
managedNode: node, orchestratorUrl: "http://orchestrator:4040",
orchestratorUrl: "http://orchestrator:4040", orchestratorApiKey: "orch-key",
orchestratorApiKey: "orch-key", });
}), const assertion = expect(resultPromise).rejects.toThrow("Registration failed");
).rejects.toThrow("Registration failed");
await vi.advanceTimersByTimeAsync(2_500);
await assertion;
expect(mockCentral.updateManagedDockerNode).toHaveBeenCalledWith( expect(mockCentral.updateManagedDockerNode).toHaveBeenCalledWith(
"dn_test123", "dn_test123",