177 lines
7.3 KiB
TypeScript
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(() => {});
|
|
}
|
|
});
|
|
});
|