Files
kandev/apps/web/e2e/tests/session/copyfiles-progress.spec.ts
T

177 lines
7.3 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import { test, expect } from "../../fixtures/test-base";
import { SessionPage } from "../../pages/session-page";
/**
* Verifies the "Copy N ignored files" step the env preparer surfaces in the
* prepare-progress panel whenever a worktree-mode repository has a non-empty
* `copy_files` spec that actually matches source files.
*
* Setup:
* - Writes `.env` + `config/local.yml` into the worker-scoped seed repo.
* - PATCHes `copy_files` on that repo to ".env, config/local.yml".
* - Restores both in a finally so this never leaks into sibling tests
* (the seedData repo is shared across the whole worker).
*
* Why an E2E (not just a unit test): the existing unit coverage exercises the
* step builder and the manager-level copy. This guards the full WS path —
* env preparer → prepare event → store → PrepareProgress panel — so a
* regression in any layer flips the panel back to silent.
*/
test.describe("Copy ignored files prepare step", () => {
test("surfaces 'Copy N ignored files' step when copy_files is configured", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
test.setTimeout(60_000);
const repoDir = path.join(backend.tmpDir, "repos", "e2e-repo");
const envPath = path.join(repoDir, ".env");
const configDir = path.join(repoDir, "config");
const configPath = path.join(configDir, "local.yml");
// Snapshot pre-existing state so teardown can restore it. The seed
// repo is worker-scoped — a previous test might have left fixtures
// in `config/`, and unconditionally rmSync'ing them would silently
// poison sibling tests with missing files. Pattern flagged by
// CodeRabbit on PR #950.
const hadConfigDir = fs.existsSync(configDir);
const hadConfigFile = fs.existsSync(configPath);
const previousConfig = hadConfigFile ? fs.readFileSync(configPath) : undefined;
const hadEnv = fs.existsSync(envPath);
const previousEnv = hadEnv ? fs.readFileSync(envPath) : undefined;
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(envPath, "E2E_SECRET=hunter2\n");
fs.writeFileSync(configPath, "debug: true\n");
await apiClient.updateRepository(seedData.repositoryId, {
copy_files: ".env, config/local.yml",
});
// Slow `git fetch` (worktree sync step) so the prepare panel is
// still streaming when the page mounts + WS subscribes. Without
// this shim prepare can complete before the client connects, the
// events broadcast into the void, and the panel renders with an
// empty steps list — making the "Copy 2 ignored files" assertion
// below race against the fallback completed-with-no-steps path.
// Same pattern as `setup-script-progress.spec.ts`.
const delayFile = path.join(backend.tmpDir, "git-delay-ms");
fs.writeFileSync(delayFile, "5000");
try {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Copy Files Prepare Step",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
executor_profile_id: seedData.worktreeExecutorProfileId,
},
);
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
const panel = testPage.getByTestId("prepare-progress-panel");
await expect(panel).toBeVisible({ timeout: 30_000 });
await expect(panel).toHaveAttribute("data-status", "completed", { timeout: 30_000 });
// Panel auto-collapses on a clean run — expand it so the step rows render.
await panel.getByTestId("prepare-progress-toggle").click();
await expect(panel).toHaveAttribute("data-expanded", "true");
// Pluralized count is what proves the backend wired in CopiedFiles —
// a regression in copyfiles.Copy's returned slice or the env preparer's
// step builder would land on "Copy ignored files" (no count) or skip
// the step entirely.
await expect(panel).toContainText("Copy 2 ignored files", { timeout: 5_000 });
} finally {
if (fs.existsSync(delayFile)) fs.unlinkSync(delayFile);
await apiClient.updateRepository(seedData.repositoryId, { copy_files: "" }).catch(() => {
// Test teardown is best-effort — a 404 here just means the repo
// was already cleaned up by a parallel teardown.
});
// Restore pre-existing state captured above so sibling tests
// sharing this worker repo don't lose any fixture files.
if (previousEnv !== undefined) {
fs.writeFileSync(envPath, previousEnv);
} else {
fs.rmSync(envPath, { force: true });
}
if (previousConfig !== undefined) {
fs.writeFileSync(configPath, previousConfig);
} else {
fs.rmSync(configPath, { force: true });
if (!hadConfigDir) {
fs.rmSync(configDir, { recursive: true, force: true });
}
}
}
});
test("renders the step with a warning when no files match", async ({
testPage,
apiClient,
seedData,
backend,
}) => {
test.setTimeout(60_000);
// Configure copy_files to a pattern that matches nothing in the repo —
// copyfiles.Copy returns zero copied + one "no matches" warning, but
// because the warning IS surfaced the step still appears. Use a pattern
// we know cannot match by giving it a path the seed repo never creates.
await apiClient.updateRepository(seedData.repositoryId, {
copy_files: "definitely-does-not-exist.never",
});
// Same git-fetch delay shim as the happy-path test above — gives the
// page time to mount + subscribe before the prepare-completed event
// broadcasts. Otherwise the panel falls back to the empty-steps
// completed state and the data-status / step-text assertions race.
const delayFile = path.join(backend.tmpDir, "git-delay-ms");
fs.writeFileSync(delayFile, "5000");
try {
const task = await apiClient.createTaskWithAgent(
seedData.workspaceId,
"Copy Files Zero Match",
seedData.agentProfileId,
{
description: "/e2e:simple-message",
workflow_id: seedData.workflowId,
workflow_step_id: seedData.startStepId,
repository_ids: [seedData.repositoryId],
executor_profile_id: seedData.worktreeExecutorProfileId,
},
);
await testPage.goto(`/t/${task.id}`);
const session = new SessionPage(testPage);
await session.waitForLoad();
const panel = testPage.getByTestId("prepare-progress-panel");
await expect(panel).toBeVisible({ timeout: 30_000 });
await expect(panel).toHaveAttribute("data-status", "completed_with_warnings", {
timeout: 30_000,
});
// Warnings keep the panel auto-expanded; the step renders with zero
// count ("Copy ignored files") and the no-match warning.
await expect(panel).toContainText("Copy ignored files");
await expect(panel).toContainText("definitely-does-not-exist.never");
} finally {
if (fs.existsSync(delayFile)) fs.unlinkSync(delayFile);
await apiClient.updateRepository(seedData.repositoryId, { copy_files: "" }).catch(() => {});
}
});
});