diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index ed9f4c06c5..62243d6182 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -356,19 +356,23 @@ jobs: START=$(date +%s) deno task coverage:ci:shard -- --shard=${{ matrix.shard }}/4 --coverage-dir=coverage-shard-${{ matrix.shard }} echo "duration=$(($(date +%s) - START))s" >> "$GITHUB_OUTPUT" - - name: Include dependency history integration coverage + - name: Include CLI and dependency history integration coverage if: matrix.shard == 1 run: | - rm -rf coverage-history + rm -rf coverage-history coverage-cli deno task test:file --coverage=coverage-history \ tests/integration/semantic-unit-boundary/src/platform/adapters/fs/veryfront/dependency-metadata-history.test.ts \ tests/integration/semantic-unit-boundary/src/platform/adapters/veryfront-api-client/dependency-metadata-history.test.ts \ tests/integration/semantic-unit-boundary/src/transforms/esm/package-registry-metadata-history.test.ts + # The CLI regression changes cwd, so give it a separate process. + deno task test:file --coverage=coverage-cli \ + tests/integration/cli/merge-preview.integration.test.ts deno coverage coverage-history --include=src/ --include=cli/ --exclude=/tests/ --exclude=/__tests__/ --lcov > coverage-history.lcov + deno coverage coverage-cli --include=src/ --include=cli/ --exclude=/tests/ --exclude=/__tests__/ --lcov > coverage-cli.lcov deno eval ' import { mergeLcovReports } from "./scripts/test/coverage-ci.ts"; const target = "coverage-shard-1/lcov.info"; - const reports = await Promise.all([target, "coverage-history.lcov"].map(path => Deno.readTextFile(path))); + const reports = await Promise.all([target, "coverage-history.lcov", "coverage-cli.lcov"].map(path => Deno.readTextFile(path))); await Deno.writeTextFile(target, mergeLcovReports(reports)); ' - name: Upload unit coverage lcov diff --git a/cli/commands/merge/command-help.ts b/cli/commands/merge/command-help.ts index f457c6b954..b778eaefb3 100644 --- a/cli/commands/merge/command-help.ts +++ b/cli/commands/merge/command-help.ts @@ -28,6 +28,6 @@ export const mergeHelp: CommandHelp = { "Requires VERYFRONT_API_TOKEN env var or veryfront.json config", "A self-hosted apiUrl in veryfront.json or a project .env file needs a token from that same file, or VERYFRONT_API_URL set in your shell or CI environment to confirm the host", "Use --dry-run to preview which files would be merged", - "Conflicts are reported but must be resolved in Studio", + "--dry-run reports conflicting file paths; resolve those conflicts in Studio", ], }; diff --git a/cli/commands/merge/command.ts b/cli/commands/merge/command.ts index f31dd765a8..acb145ac19 100644 --- a/cli/commands/merge/command.ts +++ b/cli/commands/merge/command.ts @@ -71,8 +71,8 @@ interface MergeResponse { * Merge preview diff item */ interface MergePreviewDiff { - path: string; - has_conflict: boolean; + file_path: string; + has_conflicts: boolean; } /** @@ -190,13 +190,13 @@ export async function mergeCommand(options: MergeOptions): Promise { ); spinner.stop(); - const conflicts = diffs.filter((d) => d.has_conflict); + const conflicts = diffs.filter((d) => d.has_conflicts); logInfo(`Would merge ${diffs.length} files from "${branch}" into ${targetName}`); if (conflicts.length > 0) { cliLogger.warn(` ${conflicts.length} file(s) have conflicts`); for (const conflict of conflicts) { - cliLogger.warn(` - ${conflict.path}`); + cliLogger.warn(` - ${conflict.file_path}`); } } return; diff --git a/scripts/test/suites.ts b/scripts/test/suites.ts index 8b79e3731d..c83304c7d8 100644 --- a/scripts/test/suites.ts +++ b/scripts/test/suites.ts @@ -169,6 +169,7 @@ export const UNIT_CWD_FILES: readonly string[] = Object.freeze([ const CWD_MUTATING_TEST_FILES = new Set([ ...UNIT_CWD_FILES, "tests/integration/adapters/shell-adapter.test.ts", + "tests/integration/cli/merge-preview.integration.test.ts", "tests/integration/cli/mcp/standalone-auth-scaffold.test.ts", "tests/integration/semantic-unit-boundary/cli/scaffold/missing-parent-race.test.ts", ]); diff --git a/tests/integration/cli/merge-preview.integration.test.ts b/tests/integration/cli/merge-preview.integration.test.ts new file mode 100644 index 0000000000..42d0deaae1 --- /dev/null +++ b/tests/integration/cli/merge-preview.integration.test.ts @@ -0,0 +1,68 @@ +import "#veryfront/schemas/_test-setup.ts"; +import { assertEquals } from "#veryfront/testing/assert.ts"; +import { describe, it } from "#veryfront/testing/bdd.ts"; +import { withMockFetch } from "#veryfront/testing/mock-fetch.ts"; +import { makeTempDir } from "#veryfront/testing/deno-compat.ts"; +import { _resetEnvironmentConfig } from "#veryfront/config/environment-config.ts"; +import { cliLogger } from "../../../cli/utils/index.ts"; +import { mergeCommand } from "../../../cli/commands/merge/command.ts"; + +describe("merge dry-run REST contract", () => { + it("finds a branch on the next page and reports canonical conflict paths without merging", async () => { + const originalDirectory = Deno.cwd(); + const directory = await makeTempDir(); + const keys = ["VERYFRONT_API_TOKEN", "VERYFRONT_API_URL", "VERYFRONT_PROJECT_SLUG"]; + const previous = keys.map((key) => Deno.env.get(key)); + const originalWarn = cliLogger.warn; + const warnings: string[] = []; + const requests: string[] = []; + try { + Deno.chdir(directory); + Deno.env.set("VERYFRONT_API_TOKEN", ""); + Deno.env.set("VERYFRONT_API_URL", "https://control.example.test"); + Deno.env.set("VERYFRONT_PROJECT_SLUG", "fixture-project"); + _resetEnvironmentConfig(); + cliLogger.warn = (...args: unknown[]) => warnings.push(args.map(String).join(" ")); + await withMockFetch(async (input, init) => { + const request = input instanceof Request ? input : new Request(input, init); + const url = new URL(request.url); + requests.push(`${request.method} ${url.pathname}${url.search}`); + assertEquals(request.method, "GET"); + if (url.pathname === "/projects/fixture-project/branches") { + assertEquals(url.searchParams.get("search"), "fixture"); + assertEquals(url.searchParams.get("limit"), "100"); + return Response.json( + url.searchParams.has("cursor") + ? { + data: [{ id: "branch-id", name: "fixture", project_id: "project-id" }], + page_info: { next: null }, + } + : { + data: [{ id: "other-id", name: "fixture-other", project_id: "project-id" }], + page_info: { next: "page-two" }, + }, + ); + } + assertEquals(url.pathname, "/projects/fixture-project/branches/branch-id/merge-preview"); + return Response.json({ + diffs: [ + { file_path: "app/page.tsx", has_conflicts: true }, + { file_path: "app/layout.tsx", has_conflicts: false }, + ], + }); + }, () => mergeCommand({ branch: "fixture", dryRun: true, force: false })); + assertEquals(requests.length, 3); + assertEquals(requests[1]?.includes("cursor=page-two"), true); + assertEquals(warnings, [" 1 file(s) have conflicts", " - app/page.tsx"]); + } finally { + cliLogger.warn = originalWarn; + Deno.chdir(originalDirectory); + keys.forEach((key, i) => { + if (previous[i] === undefined) Deno.env.delete(key); + else Deno.env.set(key, previous[i]); + }); + _resetEnvironmentConfig(); + await Deno.remove(directory, { recursive: true }); + } + }); +});