From ee6d0e68b5557867389eb7ffc959cfd88a78cd29 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 12 May 2026 09:48:40 +0000 Subject: [PATCH 1/6] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/ProverCoderAI/docker-git/issues/278 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 00000000..f8bf83ed --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-05-12T09:48:40.302Z for PR creation at branch issue-278-d92a50df7e27 for issue https://github.com/ProverCoderAI/docker-git/issues/278 \ No newline at end of file From 87ede21d99ec83b04e758ac1791ad272f74d20e1 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 12 May 2026 10:17:28 +0000 Subject: [PATCH 2/6] feat(ci): add cross-platform final build checks --- .changeset/cross-platform-final-build.md | 6 +++ .github/actions/setup/action.yml | 1 + .github/workflows/final-build.yml | 46 +++++++++++++++++++ .gitkeep | 1 - package.json | 10 ++-- packages/app/package.json | 10 ++-- .../package-scripts-cross-platform.test.ts | 34 ++++++++++++++ packages/docker-git-session-sync/package.json | 2 +- scripts/mark-executable.mjs | 23 ++++++++++ 9 files changed, 121 insertions(+), 12 deletions(-) create mode 100644 .changeset/cross-platform-final-build.md create mode 100644 .github/workflows/final-build.yml delete mode 100644 .gitkeep create mode 100644 packages/app/tests/docker-git/package-scripts-cross-platform.test.ts create mode 100644 scripts/mark-executable.mjs diff --git a/.changeset/cross-platform-final-build.md b/.changeset/cross-platform-final-build.md new file mode 100644 index 00000000..260c76bb --- /dev/null +++ b/.changeset/cross-platform-final-build.md @@ -0,0 +1,6 @@ +--- +"@prover-coder-ai/docker-git": patch +"@prover-coder-ai/docker-git-session-sync": patch +--- + +Add portable launch/build scripts and CI final-build verification across Linux, macOS, and Windows. diff --git a/.github/actions/setup/action.yml b/.github/actions/setup/action.yml index cb33036e..0b19744d 100644 --- a/.github/actions/setup/action.yml +++ b/.github/actions/setup/action.yml @@ -22,6 +22,7 @@ runs: with: node-version: ${{ inputs.node-version }} - name: Install OpenSSH client + if: runner.os == 'Linux' shell: bash run: | if command -v ssh >/dev/null 2>&1 && command -v ssh-keygen >/dev/null 2>&1; then diff --git a/.github/workflows/final-build.yml b/.github/workflows/final-build.yml new file mode 100644 index 00000000..70e931f7 --- /dev/null +++ b/.github/workflows/final-build.yml @@ -0,0 +1,46 @@ +name: Final Build + +on: + workflow_dispatch: + pull_request: + branches: [main] + +permissions: + contents: read + +jobs: + final-build: + name: Final build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v6 + - name: Install dependencies + uses: ./.github/actions/setup + with: + bun-version: 1.3.11 + node-version: 24.14.0 + - name: Build final workspace packages + run: bun run build + - name: Verify docker-git CLI starts + run: bun ./packages/app/dist/src/docker-git/main.js --help + - name: Verify session sync CLI starts + run: bun ./packages/docker-git-session-sync/dist/docker-git-session-sync.js --help + - name: Prepare package artifacts directory + run: | + node -e "require('node:fs').mkdirSync('artifacts', { recursive: true })" + - name: Pack docker-git package + working-directory: packages/app + run: bun pm pack --quiet --ignore-scripts --destination ../../artifacts + - name: Pack session sync package + working-directory: packages/docker-git-session-sync + run: bun pm pack --quiet --ignore-scripts --destination ../../artifacts + - name: Upload final build artifacts + uses: actions/upload-artifact@v7 + with: + name: final-build-${{ matrix.os }} + path: artifacts/*.tgz diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index f8bf83ed..00000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-05-12T09:48:40.302Z for PR creation at branch issue-278-d92a50df7e27 for issue https://github.com/ProverCoderAI/docker-git/issues/278 \ No newline at end of file diff --git a/package.json b/package.json index 294c7e68..e8ea3862 100644 --- a/package.json +++ b/package.json @@ -23,9 +23,9 @@ "changeset": "changeset", "changeset-publish": "bun -e \"if (!process.env.NPM_TOKEN) { console.log('Skipping publish: NPM_TOKEN is not set'); process.exit(0); }\" && changeset publish", "changeset-version": "changeset version", - "clone": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js clone \"$@\"' --", - "open": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js open \"$@\"' --", - "docker-git": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js \"$@\"' --", + "clone": "bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js clone", + "open": "bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js open", + "docker-git": "bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js", "skiller:init": "git submodule update --init --checkout third_party/skiller-desktop-skills-manager && bun scripts/skiller-apply-docker-git-patches.mjs", "skiller:install": "bun install --cwd third_party/skiller-desktop-skills-manager --frozen-lockfile", "skiller:dev": "bun run --cwd third_party/skiller-desktop-skills-manager dev", @@ -36,7 +36,7 @@ "e2e:login-context": "bash scripts/e2e/login-context.sh", "e2e:runtime-volumes-ssh": "bash scripts/e2e/runtime-volumes-ssh.sh", "e2e:opencode-autoconnect": "bash scripts/e2e/opencode-autoconnect.sh", - "list": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js ps \"$@\"' --", + "list": "bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js ps", "dev": "bun run --cwd packages/app dev", "web:dev": "bun run --cwd packages/app dev:web", "web:build": "bun run --cwd packages/app build:web", @@ -47,7 +47,7 @@ "lint:effect": "bun run --filter @prover-coder-ai/docker-git lint:effect && bun run --filter @effect-template/lib lint:effect", "test": "bun run --filter @prover-coder-ai/docker-git-session-sync test && bun run --filter @prover-coder-ai/docker-git test && bun run --filter @effect-template/lib test", "typecheck": "bun run --filter @prover-coder-ai/docker-git-session-sync typecheck && bun run --filter @prover-coder-ai/docker-git typecheck && bun run --filter @effect-template/lib typecheck", - "start": "bash -lc 'bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js \"$@\"' --" + "start": "bun run --cwd packages/app build:docker-git && bun ./packages/app/dist/src/docker-git/main.js" }, "devDependencies": { "@changesets/changelog-github": "^0.7.0", diff --git a/packages/app/package.json b/packages/app/package.json index 85adcd9b..bd2d7ba6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -28,12 +28,12 @@ "prebuild:docker-git": "bun install --cwd ../.. && bun run --cwd ../docker-git-session-sync build && bun run --cwd ../lib build", "build:docker-git": "vite build --config vite.docker-git.config.ts", "check": "bun run typecheck", - "clone": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js clone \"$@\"' --", - "open": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js open \"$@\"' --", - "docker-git": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js \"$@\"' --", - "list": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js ps \"$@\"' --", + "clone": "bun run build:docker-git && bun dist/src/docker-git/main.js clone", + "open": "bun run build:docker-git && bun dist/src/docker-git/main.js open", + "docker-git": "bun run build:docker-git && bun dist/src/docker-git/main.js", + "list": "bun run build:docker-git && bun dist/src/docker-git/main.js ps", "preview:web": "vite preview --config vite.web.config.ts", - "start": "bash -lc 'bun run build:docker-git && bun dist/src/docker-git/main.js \"$@\"' --", + "start": "bun run build:docker-git && bun dist/src/docker-git/main.js", "pretest": "bun run --cwd ../docker-git-session-sync build && bun run --cwd ../lib build", "test": "bun run lint:tests && vitest run", "pretypecheck": "bun run --cwd ../docker-git-session-sync build && bun run --cwd ../lib build", diff --git a/packages/app/tests/docker-git/package-scripts-cross-platform.test.ts b/packages/app/tests/docker-git/package-scripts-cross-platform.test.ts new file mode 100644 index 00000000..4d4596af --- /dev/null +++ b/packages/app/tests/docker-git/package-scripts-cross-platform.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from "@effect/vitest" + +import rootPackage from "../../../../package.json" with { type: "json" } +import sessionSyncPackage from "../../../docker-git-session-sync/package.json" with { type: "json" } +import appPackage from "../../package.json" with { type: "json" } + +const launchScripts: ReadonlyArray> = [ + { packageName: "workspace", scriptName: "clone", script: rootPackage.scripts.clone }, + { packageName: "workspace", scriptName: "open", script: rootPackage.scripts.open }, + { packageName: "workspace", scriptName: "docker-git", script: rootPackage.scripts["docker-git"] }, + { packageName: "workspace", scriptName: "list", script: rootPackage.scripts.list }, + { packageName: "workspace", scriptName: "start", script: rootPackage.scripts.start }, + { packageName: "@prover-coder-ai/docker-git", scriptName: "clone", script: appPackage.scripts.clone }, + { packageName: "@prover-coder-ai/docker-git", scriptName: "open", script: appPackage.scripts.open }, + { + packageName: "@prover-coder-ai/docker-git", + scriptName: "docker-git", + script: appPackage.scripts["docker-git"] + }, + { packageName: "@prover-coder-ai/docker-git", scriptName: "list", script: appPackage.scripts.list }, + { packageName: "@prover-coder-ai/docker-git", scriptName: "start", script: appPackage.scripts.start } +] + +describe("package scripts cross-platform contract", () => { + it("keeps user-facing launch scripts independent from bash", () => { + for (const entry of launchScripts) { + expect(entry.script, `${entry.packageName}:${entry.scriptName}`).not.toMatch(/\bbash(?:\.exe)?\b/u) + } + }) + + it("keeps final package build independent from raw chmod", () => { + expect(sessionSyncPackage.scripts.build).not.toMatch(/\bchmod\s+/u) + }) +}) diff --git a/packages/docker-git-session-sync/package.json b/packages/docker-git-session-sync/package.json index 228ac33d..5b4b3725 100644 --- a/packages/docker-git-session-sync/package.json +++ b/packages/docker-git-session-sync/package.json @@ -10,7 +10,7 @@ "dist" ], "scripts": { - "build": "vite build && chmod +x dist/docker-git-session-sync.js", + "build": "vite build && bun ../../scripts/mark-executable.mjs dist/docker-git-session-sync.js", "check": "bun run typecheck", "prepack": "bun run build", "test": "vitest run --passWithNoTests", diff --git a/scripts/mark-executable.mjs b/scripts/mark-executable.mjs new file mode 100644 index 00000000..d7a3befa --- /dev/null +++ b/scripts/mark-executable.mjs @@ -0,0 +1,23 @@ +#!/usr/bin/env bun + +import { chmodSync } from "node:fs" +import { resolve } from "node:path" + +// CHANGE: centralize executable-bit handling for generated CLI files. +// WHY: POSIX chmod is not available on Windows, while Linux/macOS package builds require executable bins. +// QUOTE(TZ): "run conveniently on Windows and Linux" +// REF: issue-278 +// SOURCE: n/a +// FORMAT THEOREM: forall p in Paths: platform=win32 -> no_posix_chmod(p), platform!=win32 -> executable(p) +// PURITY: SHELL +// EFFECT: filesystem metadata update +// INVARIANT: missing target argument exits non-zero; Windows builds do not invoke POSIX chmod. +// COMPLEXITY: O(1)/O(1) +const target = process.argv[2] + +if (target === undefined || target.length === 0) { + process.stderr.write("Usage: mark-executable \n") + process.exitCode = 1 +} else if (process.platform !== "win32") { + chmodSync(resolve(process.cwd(), target), 0o755) +} From a872a79230edcea7405200a732154568f2051d81 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 12 May 2026 11:09:16 +0000 Subject: [PATCH 3/6] fix(ci): bound clone cache mirror refresh refs --- .../app/src/lib/core/templates-entrypoint/tasks.ts | 13 ++++++++++++- packages/lib/src/core/templates-entrypoint/tasks.ts | 13 ++++++++++++- packages/lib/tests/core/templates.test.ts | 11 +++++++++++ 3 files changed, 35 insertions(+), 2 deletions(-) diff --git a/packages/app/src/lib/core/templates-entrypoint/tasks.ts b/packages/app/src/lib/core/templates-entrypoint/tasks.ts index d6264455..1889eb05 100644 --- a/packages/app/src/lib/core/templates-entrypoint/tasks.ts +++ b/packages/app/src/lib/core/templates-entrypoint/tasks.ts @@ -117,6 +117,17 @@ const renderCloneAuthRepoUrl = (): string => AUTH_REPO_URL="$(printf "%s" "$REPO_URL" | sed "s#^https://#https://\${RESOLVED_GIT_AUTH_USER}:\${RESOLVED_GIT_AUTH_TOKEN}@#")" fi` +// CHANGE: restrict clone-cache mirror refresh to branch and tag refs +// WHY: broad refs include hosted forge PR refs and make cache reuse proportional to every remote ref +// QUOTE(ТЗ): "Для тестов можно реализовать CI/CD workflow для Linux, MAC, Windows" +// REF: issue-278-ci-check-clone-cache +// SOURCE: n/a +// FORMAT THEOREM: forall r in refreshedRefs: r in refs/heads/* union refs/tags/* +// PURITY: CORE +// INVARIANT: clone-cache refresh never requests refs/pull/* or refs/merge-requests/* +// COMPLEXITY: O(|heads| + |tags|) +const cloneCacheRefreshRefspecs = "'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'" + const renderCloneCacheInit = (config: TemplateConfig): string => ` CLONE_CACHE_ARGS="" CACHE_REPO_DIR="" @@ -135,7 +146,7 @@ const renderCloneCacheInit = (config: TemplateConfig): string => chown 1000:1000 "$CACHE_ROOT" || true if [[ -d "$CACHE_REPO_DIR" ]]; then if su - ${config.sshUser} -c "git --git-dir '$CACHE_REPO_DIR' rev-parse --is-bare-repository >/dev/null 2>&1"; then - if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' '+refs/*:refs/*'"; then + if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' ${cloneCacheRefreshRefspecs}"; then echo "[clone-cache] mirror refresh failed for $REPO_URL" fi CLONE_CACHE_ARGS="--reference-if-able '$CACHE_REPO_DIR' --dissociate" diff --git a/packages/lib/src/core/templates-entrypoint/tasks.ts b/packages/lib/src/core/templates-entrypoint/tasks.ts index d6264455..1889eb05 100644 --- a/packages/lib/src/core/templates-entrypoint/tasks.ts +++ b/packages/lib/src/core/templates-entrypoint/tasks.ts @@ -117,6 +117,17 @@ const renderCloneAuthRepoUrl = (): string => AUTH_REPO_URL="$(printf "%s" "$REPO_URL" | sed "s#^https://#https://\${RESOLVED_GIT_AUTH_USER}:\${RESOLVED_GIT_AUTH_TOKEN}@#")" fi` +// CHANGE: restrict clone-cache mirror refresh to branch and tag refs +// WHY: broad refs include hosted forge PR refs and make cache reuse proportional to every remote ref +// QUOTE(ТЗ): "Для тестов можно реализовать CI/CD workflow для Linux, MAC, Windows" +// REF: issue-278-ci-check-clone-cache +// SOURCE: n/a +// FORMAT THEOREM: forall r in refreshedRefs: r in refs/heads/* union refs/tags/* +// PURITY: CORE +// INVARIANT: clone-cache refresh never requests refs/pull/* or refs/merge-requests/* +// COMPLEXITY: O(|heads| + |tags|) +const cloneCacheRefreshRefspecs = "'+refs/heads/*:refs/heads/*' '+refs/tags/*:refs/tags/*'" + const renderCloneCacheInit = (config: TemplateConfig): string => ` CLONE_CACHE_ARGS="" CACHE_REPO_DIR="" @@ -135,7 +146,7 @@ const renderCloneCacheInit = (config: TemplateConfig): string => chown 1000:1000 "$CACHE_ROOT" || true if [[ -d "$CACHE_REPO_DIR" ]]; then if su - ${config.sshUser} -c "git --git-dir '$CACHE_REPO_DIR' rev-parse --is-bare-repository >/dev/null 2>&1"; then - if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' '+refs/*:refs/*'"; then + if ! su - ${config.sshUser} -c "GIT_TERMINAL_PROMPT=0 git --git-dir '$CACHE_REPO_DIR' fetch --progress --prune '$AUTH_REPO_URL' ${cloneCacheRefreshRefspecs}"; then echo "[clone-cache] mirror refresh failed for $REPO_URL" fi CLONE_CACHE_ARGS="--reference-if-able '$CACHE_REPO_DIR' --dissociate" diff --git a/packages/lib/tests/core/templates.test.ts b/packages/lib/tests/core/templates.test.ts index 4de72f9e..2092a1ba 100644 --- a/packages/lib/tests/core/templates.test.ts +++ b/packages/lib/tests/core/templates.test.ts @@ -74,6 +74,17 @@ describe("renderDockerfile", () => { }) }) +describe("renderEntrypoint clone cache", () => { + it("refreshes mirrors without broad remote refs", () => { + const entrypoint = renderEntrypoint(makeTemplateConfig()) + + expect(entrypoint).toContain("git --git-dir '$CACHE_REPO_DIR' fetch") + expect(entrypoint).toContain("'+refs/heads/*:refs/heads/*'") + expect(entrypoint).toContain("'+refs/tags/*:refs/tags/*'") + expect(entrypoint).not.toContain("'+refs/*:refs/*'") + }) +}) + describe("renderEntrypointGitHooks", () => { it("installs pre-push protection checks and a global git post-push runtime", () => { const hooks = renderEntrypointGitHooks() From 7cf5e2228ebbbbffca7e889fc33bcf24ef73a7fc Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Wed, 13 May 2026 12:49:44 +0000 Subject: [PATCH 4/6] ci(final-build): verify browser clone flow --- .github/workflows/final-build.yml | 5 + bun.lock | 5 +- packages/app/package.json | 1 + .../docker-git/actions-project-create.test.ts | 137 +++++++++++++++ .../tests/docker-git/app-ready-create.test.ts | 45 ++++- .../package-scripts-cross-platform.test.ts | 9 +- packages/lib/tests/core/templates.test.ts | 2 + scripts/final-build/browser-web-smoke.mjs | 158 ++++++++++++++++++ 8 files changed, 355 insertions(+), 7 deletions(-) create mode 100644 packages/app/tests/docker-git/actions-project-create.test.ts create mode 100644 scripts/final-build/browser-web-smoke.mjs diff --git a/.github/workflows/final-build.yml b/.github/workflows/final-build.yml index 70e931f7..ce24f93b 100644 --- a/.github/workflows/final-build.yml +++ b/.github/workflows/final-build.yml @@ -30,6 +30,11 @@ jobs: run: bun ./packages/app/dist/src/docker-git/main.js --help - name: Verify session sync CLI starts run: bun ./packages/docker-git-session-sync/dist/docker-git-session-sync.js --help + - name: Verify browser UI and menu clone smoke checks + run: | + bun run --cwd packages/app build:web + bun scripts/final-build/browser-web-smoke.mjs + bun run --cwd packages/app vitest run tests/docker-git/browser-frontend.test.ts tests/docker-git/app-ready-create.test.ts tests/docker-git/actions-project-create.test.ts - name: Prepare package artifacts directory run: | node -e "require('node:fs').mkdirSync('artifacts', { recursive: true })" diff --git a/bun.lock b/bun.lock index e0e2c218..d11dcc11 100644 --- a/bun.lock +++ b/bun.lock @@ -37,7 +37,7 @@ }, "packages/app": { "name": "@prover-coder-ai/docker-git", - "version": "1.1.1", + "version": "1.1.5", "bin": { "docker-git": "dist/src/docker-git/main.js", }, @@ -95,6 +95,7 @@ "eslint-plugin-sonarjs": "^4.0.3", "eslint-plugin-sort-destructure-keys": "^3.0.0", "eslint-plugin-unicorn": "^64.0.0", + "fast-check": "3.23.2", "globals": "^17.6.0", "jscpd": "^4.1.1", "typescript": "^6.0.3", @@ -106,7 +107,7 @@ }, "packages/docker-git-session-sync": { "name": "@prover-coder-ai/docker-git-session-sync", - "version": "1.0.5", + "version": "1.0.8", "bin": { "docker-git-session-sync": "dist/docker-git-session-sync.js", }, diff --git a/packages/app/package.json b/packages/app/package.json index eed3f938..3415cc1a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -114,6 +114,7 @@ "eslint-plugin-sonarjs": "^4.0.3", "eslint-plugin-sort-destructure-keys": "^3.0.0", "eslint-plugin-unicorn": "^64.0.0", + "fast-check": "3.23.2", "globals": "^17.6.0", "jscpd": "^4.1.1", "typescript": "^6.0.3", diff --git a/packages/app/tests/docker-git/actions-project-create.test.ts b/packages/app/tests/docker-git/actions-project-create.test.ts new file mode 100644 index 00000000..2c67470f --- /dev/null +++ b/packages/app/tests/docker-git/actions-project-create.test.ts @@ -0,0 +1,137 @@ +import { describe, expect, it } from "@effect/vitest" +import { Effect } from "effect" +import { beforeEach, vi } from "vitest" + +import type { CreateInputs } from "../../src/docker-git/menu-types.js" +import { submitCreateInputs } from "../../src/web/actions-project-create.js" +import type { ApiEvent, loadProjectDetails, ProjectDetails, startCreateProject } from "../../src/web/api.js" +import type { openProjectEventStream } from "../../src/web/project-events.js" +import { makeBrowserActionContext, waitForAssertion } from "./browser-action-context-fixture.js" + +const eventStreamCloseMock = vi.hoisted(() => vi.fn<() => void>()) +const loadProjectDetailsMock = vi.hoisted(() => vi.fn()) +const openProjectEventStreamMock = vi.hoisted(() => vi.fn()) +const startCreateProjectMock = vi.hoisted(() => vi.fn()) + +vi.mock("../../src/web/api.js", () => ({ + loadProjectDetails: loadProjectDetailsMock, + startCreateProject: startCreateProjectMock +})) + +vi.mock("../../src/web/project-events.js", () => ({ + openProjectEventStream: openProjectEventStreamMock +})) + +const createInputConfig = { + cpuLimit: "75%", + enableMcpPlaywright: true, + force: false, + forceEnv: false, + gpu: "none", + outDir: "/home/dev/.docker-git/octocat/Hello-World", + ramLimit: "1g", + repoRef: "main", + repoUrl: "https://github.com/octocat/Hello-World.git" +} satisfies Omit + +const createInputs: CreateInputs = { + ...createInputConfig, + runUp: true +} + +const expectedCreateDraft = { + ...createInputConfig, + up: createInputs.runUp +} + +const project = { + authorizedKeysExists: true, + authorizedKeysPath: "/home/dev/.docker-git/octocat/Hello-World/.ssh/authorized_keys", + clonedOnHostname: "runner", + codexAuthPath: "/home/dev/.docker-git/.orch/auth/codex", + codexHome: "/home/dev/.docker-git/.orch/codex", + containerName: "docker-git-octocat-hello-world", + displayName: "octocat/Hello-World", + envGlobalPath: "/home/dev/.docker-git/.orch/env/global.env", + envProjectPath: "/home/dev/.docker-git/octocat/Hello-World/.orch/env/project.env", + gpu: "none", + id: "project-1", + projectDir: "/home/dev/.docker-git/octocat/Hello-World", + projectKey: "octocat/Hello-World", + repoRef: "main", + repoUrl: "https://github.com/octocat/Hello-World.git", + serviceName: "app", + sshCommand: "ssh -p 2244 dev@127.0.0.1", + sshPort: 2244, + sshSessions: 0, + sshUser: "dev", + startedAtEpochMs: 1_777_000_000_000, + startedAtIso: "2026-05-13T00:00:00.000Z", + status: "running", + statusLabel: "running", + targetDir: "/home/dev/project" +} satisfies ProjectDetails + +const projectCreatedEvent: ApiEvent = { + at: "2026-05-13T00:00:01.000Z", + payload: { + project, + projectId: project.id + }, + projectId: project.id, + seq: 8, + type: "project.created" +} + +const readCreateEventHandler = () => { + const handler = openProjectEventStreamMock.mock.calls[0]?.[1]?.onEvent + if (handler === undefined) { + throw new Error("missing create event handler") + } + return handler +} + +describe("browser create project action", () => { + beforeEach(() => { + eventStreamCloseMock.mockReset() + loadProjectDetailsMock.mockReset() + openProjectEventStreamMock.mockReset() + startCreateProjectMock.mockReset() + startCreateProjectMock.mockImplementation(() => + Effect.succeed({ + accepted: true, + cursor: 7, + projectId: project.id + }) + ) + openProjectEventStreamMock.mockImplementation(() => ({ close: eventStreamCloseMock })) + }) + + it.effect("clones a project through the browser menu create flow", () => + Effect.gen(function*(_) { + const { context, output, reloadDashboard, setMessage } = makeBrowserActionContext() + + submitCreateInputs(createInputs, context) + + yield* _(waitForAssertion(() => { + expect(openProjectEventStreamMock).toHaveBeenCalledTimes(1) + })) + readCreateEventHandler()(projectCreatedEvent) + + yield* _(waitForAssertion(() => { + expect(context.setSelectedProject).toHaveBeenCalledWith(project) + })) + + expect(startCreateProjectMock).toHaveBeenCalledWith(expectedCreateDraft) + expect(openProjectEventStreamMock).toHaveBeenCalledWith(project.id, expect.objectContaining({ initialCursor: 7 })) + expect(eventStreamCloseMock).toHaveBeenCalledTimes(1) + expect(loadProjectDetailsMock).not.toHaveBeenCalled() + expect(reloadDashboard).toHaveBeenCalledTimes(1) + expect(context.setSelectedProjectId).toHaveBeenCalledWith(project.id) + expect(context.setSelectedMenuIndex).toHaveBeenCalledWith(1) + expect(setMessage).toHaveBeenLastCalledWith("Created octocat/Hello-World.") + expect(output()).toContain("[create] Project creation requested") + expect(output()).toContain("[create] Project accepted: project-1") + expect(output()).toContain("[create] Project created") + })) +}) diff --git a/packages/app/tests/docker-git/app-ready-create.test.ts b/packages/app/tests/docker-git/app-ready-create.test.ts index 80fc7900..ab497e73 100644 --- a/packages/app/tests/docker-git/app-ready-create.test.ts +++ b/packages/app/tests/docker-git/app-ready-create.test.ts @@ -1,15 +1,23 @@ import type { Dispatch, SetStateAction } from "react" -import { describe, expect, it, vi } from "vitest" +import { beforeEach, describe, expect, it, vi } from "vitest" import { type CreateFlowView, createInitialFlowView, resolveCreateFlowSteps } from "../../src/docker-git/menu-create-shared.js" +import type { CreateInputs } from "../../src/docker-git/menu-types.js" +import type { submitCreateInputs } from "../../src/web/actions-projects.js" import type { GithubAuthStatus } from "../../src/web/api.js" import { submitCreateView } from "../../src/web/app-ready-create.js" import { makeBrowserActionContext } from "./browser-action-context-fixture.js" +const submitCreateInputsMock = vi.hoisted(() => vi.fn()) + +vi.mock("../../src/web/actions-projects.js", () => ({ + submitCreateInputs: submitCreateInputsMock +})) + const validGithubStatus: GithubAuthStatus = { summary: "valid", tokens: [{ key: "default", label: "default", login: "octocat", status: "valid" }] @@ -30,15 +38,20 @@ const requireCreateViewValue = ( return value } -const submitCreateBuffer = (buffer: string) => { +const submitCreateBuffer = ( + buffer: string, + options: { readonly quickCreate?: boolean } = {} +) => { const { context } = makeBrowserActionContext({ githubStatus: validGithubStatus }) const { setCreateView, spy: setCreateViewSpy } = createSetCreateViewSpy() + const quickCreate = options.quickCreate === undefined ? {} : { quickCreate: options.quickCreate } submitCreateView({ context, controllerCwd: "/workspace", createView: createInitialFlowView(buffer), projectsRoot: "/home/dev/.docker-git", + ...quickCreate, setCreateView }) @@ -46,6 +59,10 @@ const submitCreateBuffer = (buffer: string) => { } describe("app-ready-create", () => { + beforeEach(() => { + submitCreateInputsMock.mockReset() + }) + it("advances to the next create field on Enter for a repo URL", () => { const { context, setCreateViewSpy } = submitCreateBuffer("https://github.com/org/repo/tree/feature-x --force") @@ -77,4 +94,28 @@ describe("app-ready-create", () => { expect(setCreateViewSpy).not.toHaveBeenCalled() expect(context.setMessage).toHaveBeenCalledWith("Missing value for option: --bogus") }) + + it("submits a quick create clone from the Create menu", () => { + const { setCreateViewSpy } = submitCreateBuffer( + "https://github.com/octocat/Hello-World/tree/feature-x", + { quickCreate: true } + ) + + expect(submitCreateInputsMock).toHaveBeenCalledTimes(1) + expect(submitCreateInputsMock.mock.calls[0]?.[0]).toEqual( + { + cpuLimit: "", + enableMcpPlaywright: false, + force: false, + forceEnv: false, + gpu: "none", + outDir: "/home/dev/.docker-git/octocat/hello-world", + ramLimit: "", + repoRef: "feature-x", + repoUrl: "https://github.com/octocat/Hello-World/tree/feature-x", + runUp: true + } satisfies CreateInputs + ) + expect(requireCreateViewValue(setCreateViewSpy.mock.calls[0]?.[0])).toEqual(createInitialFlowView()) + }) }) diff --git a/packages/app/tests/docker-git/package-scripts-cross-platform.test.ts b/packages/app/tests/docker-git/package-scripts-cross-platform.test.ts index 4d4596af..e73993d6 100644 --- a/packages/app/tests/docker-git/package-scripts-cross-platform.test.ts +++ b/packages/app/tests/docker-git/package-scripts-cross-platform.test.ts @@ -1,4 +1,5 @@ import { describe, expect, it } from "@effect/vitest" +import * as fc from "fast-check" import rootPackage from "../../../../package.json" with { type: "json" } import sessionSyncPackage from "../../../docker-git-session-sync/package.json" with { type: "json" } @@ -23,12 +24,14 @@ const launchScripts: ReadonlyArray { it("keeps user-facing launch scripts independent from bash", () => { - for (const entry of launchScripts) { + fc.assert(fc.property(fc.constantFrom(...launchScripts), (entry) => { expect(entry.script, `${entry.packageName}:${entry.scriptName}`).not.toMatch(/\bbash(?:\.exe)?\b/u) - } + })) }) it("keeps final package build independent from raw chmod", () => { - expect(sessionSyncPackage.scripts.build).not.toMatch(/\bchmod\s+/u) + fc.assert(fc.property(fc.constant(sessionSyncPackage.scripts.build), (script) => { + expect(script).not.toMatch(/\bchmod\s+/u) + })) }) }) diff --git a/packages/lib/tests/core/templates.test.ts b/packages/lib/tests/core/templates.test.ts index d584989a..e988469f 100644 --- a/packages/lib/tests/core/templates.test.ts +++ b/packages/lib/tests/core/templates.test.ts @@ -89,6 +89,8 @@ describe("renderEntrypoint clone cache", () => { expect(entrypoint).toContain("'+refs/heads/*:refs/heads/*'") expect(entrypoint).toContain("'+refs/tags/*:refs/tags/*'") expect(entrypoint).not.toContain("'+refs/*:refs/*'") + expect(entrypoint).not.toContain("'+refs/pull/*:refs/pull/*'") + expect(entrypoint).not.toContain("'+refs/merge-requests/*:refs/merge-requests/*'") }) }) diff --git a/scripts/final-build/browser-web-smoke.mjs b/scripts/final-build/browser-web-smoke.mjs new file mode 100644 index 00000000..85521919 --- /dev/null +++ b/scripts/final-build/browser-web-smoke.mjs @@ -0,0 +1,158 @@ +import { spawn } from "node:child_process" +import { createServer } from "node:http" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { fileURLToPath } from "node:url" + +const repoRoot = fileURLToPath(new URL("../..", import.meta.url)) +const requestTimeoutMs = 5000 +const startupTimeoutMs = 15000 + +const listen = (server) => + new Promise((resolve, reject) => { + server.once("error", reject) + server.listen(0, "127.0.0.1", () => { + server.off("error", reject) + resolve(server.address().port) + }) + }) + +const closeServer = (server) => + new Promise((resolve, reject) => { + server.close((error) => { + if (error === undefined) { + resolve() + return + } + reject(error) + }) + }) + +const delay = (ms) => + new Promise((resolve) => { + setTimeout(resolve, ms) + }) + +const fetchText = async (url) => { + const controller = new AbortController() + const timeout = setTimeout(() => { + controller.abort() + }, requestTimeoutMs) + try { + const response = await fetch(url, { signal: controller.signal }) + const body = await response.text() + return { body, status: response.status } + } finally { + clearTimeout(timeout) + } +} + +const waitForText = async (url, predicate) => { + const startedAt = Date.now() + let lastError = null + while (Date.now() - startedAt < startupTimeoutMs) { + try { + const response = await fetchText(url) + if (predicate(response)) { + return response + } + lastError = new Error(`Unexpected response ${response.status} from ${url}: ${response.body.slice(0, 160)}`) + } catch (error) { + lastError = error + } + await delay(250) + } + throw lastError ?? new Error(`Timed out waiting for ${url}`) +} + +const createApiServer = () => + createServer((request, response) => { + if (request.url === "/health") { + response.writeHead(200, { "content-type": "application/json; charset=utf-8" }) + response.end(JSON.stringify({ + cwd: "/tmp/docker-git-final-build-smoke", + ok: true, + projectsRoot: "/tmp/docker-git-final-build-smoke/projects", + revision: "final-build-smoke" + })) + return + } + response.writeHead(404, { "content-type": "text/plain; charset=utf-8" }) + response.end("not found") + }) + +const waitForExit = (child) => + new Promise((resolve) => { + child.once("exit", (code, signal) => { + resolve({ code, signal }) + }) + }) + +const terminate = async (child) => { + if (child.exitCode !== null || child.signalCode !== null) { + return + } + child.kill() + const result = await Promise.race([ + waitForExit(child), + delay(3000).then(() => null) + ]) + if (result === null) { + child.kill("SIGKILL") + await waitForExit(child) + } +} + +const main = async () => { + const apiServer = createApiServer() + const apiPort = await listen(apiServer) + const webPortServer = createServer() + const webPort = await listen(webPortServer) + await closeServer(webPortServer) + + const statePath = join(tmpdir(), `docker-git-web-smoke-${process.pid}.json`) + const child = spawn(process.execPath, ["packages/app/scripts/serve-dist-web.mjs"], { + cwd: repoRoot, + env: { + ...process.env, + DOCKER_GIT_API_URL: `http://127.0.0.1:${apiPort}`, + DOCKER_GIT_WEB_HOST: "127.0.0.1", + DOCKER_GIT_WEB_PORT: String(webPort), + DOCKER_GIT_WEB_REVISION: "final-build-smoke", + DOCKER_GIT_WEB_STATE_PATH: statePath + }, + stdio: ["ignore", "pipe", "pipe"] + }) + + let stdout = "" + let stderr = "" + child.stdout.setEncoding("utf8") + child.stderr.setEncoding("utf8") + child.stdout.on("data", (chunk) => { + stdout += chunk + }) + child.stderr.on("data", (chunk) => { + stderr += chunk + }) + + try { + await waitForText( + `http://127.0.0.1:${webPort}/`, + ({ body, status }) => status === 200 && body.includes("docker-git browser") + ) + await waitForText( + `http://127.0.0.1:${webPort}/api/health`, + ({ body, status }) => status === 200 && body.includes("\"ok\":true") + ) + console.log("browser web smoke passed") + } catch (error) { + console.error(stdout) + console.error(stderr) + throw error + } finally { + await terminate(child) + await closeServer(apiServer) + } +} + +await main() From feffc8b6dbef379d8d5e2c9dc639aae8b837c154 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Wed, 13 May 2026 13:20:53 +0000 Subject: [PATCH 5/6] test(app): add create flow property checks --- .../docker-git/actions-project-create.test.ts | 138 +++++++++++++----- .../tests/docker-git/app-ready-create.test.ts | 131 +++++++++++++++-- 2 files changed, 221 insertions(+), 48 deletions(-) diff --git a/packages/app/tests/docker-git/actions-project-create.test.ts b/packages/app/tests/docker-git/actions-project-create.test.ts index 2c67470f..5032d24a 100644 --- a/packages/app/tests/docker-git/actions-project-create.test.ts +++ b/packages/app/tests/docker-git/actions-project-create.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" +import * as fc from "fast-check" import { beforeEach, vi } from "vitest" import type { CreateInputs } from "../../src/docker-git/menu-types.js" @@ -72,16 +73,24 @@ const project = { targetDir: "/home/dev/project" } satisfies ProjectDetails -const projectCreatedEvent: ApiEvent = { +const projectDetailsWithId = (projectId: string) => + ({ + ...project, + id: projectId + }) satisfies ProjectDetails + +const projectCreatedEventFor = ( + createdProject: ReturnType +): ApiEvent => ({ at: "2026-05-13T00:00:01.000Z", payload: { - project, - projectId: project.id + project: createdProject, + projectId: createdProject.id }, - projectId: project.id, + projectId: createdProject.id, seq: 8, type: "project.created" -} +}) const readCreateEventHandler = () => { const handler = openProjectEventStreamMock.mock.calls[0]?.[1]?.onEvent @@ -91,47 +100,108 @@ const readCreateEventHandler = () => { return handler } +const resetCreateMocks = ( + projectId = project.id, + cursor = 7 +) => { + eventStreamCloseMock.mockReset() + loadProjectDetailsMock.mockReset() + openProjectEventStreamMock.mockReset() + startCreateProjectMock.mockReset() + startCreateProjectMock.mockImplementation(() => + Effect.succeed({ + accepted: true, + cursor, + projectId + }) + ) + openProjectEventStreamMock.mockImplementation(() => ({ close: eventStreamCloseMock })) +} + +const runCreateFlow = ( + createdProject: ReturnType +) => + Effect.gen(function*(_) { + const { context, output, reloadDashboard, setMessage } = makeBrowserActionContext() + + submitCreateInputs(createInputs, context) + + yield* _(waitForAssertion(() => { + expect(openProjectEventStreamMock).toHaveBeenCalledTimes(1) + })) + readCreateEventHandler()(projectCreatedEventFor(createdProject)) + + yield* _(waitForAssertion(() => { + expect(context.setSelectedProject).toHaveBeenCalledWith(createdProject) + })) + + return { context, createdProject, output, reloadDashboard, setMessage } + }) + +const expectCreateFlowInvariants = ( + { + context, + createdProject, + cursor, + reloadDashboard + }: { + readonly context: ReturnType["context"] + readonly createdProject: ReturnType + readonly cursor: number + readonly reloadDashboard: ReturnType["reloadDashboard"] + } +) => { + expect(openProjectEventStreamMock).toHaveBeenCalledWith( + createdProject.id, + expect.objectContaining({ initialCursor: cursor }) + ) + expect(eventStreamCloseMock).toHaveBeenCalledTimes(1) + expect(loadProjectDetailsMock).not.toHaveBeenCalled() + expect(reloadDashboard).toHaveBeenCalledTimes(1) + expect(context.setSelectedProjectId).toHaveBeenCalledWith(createdProject.id) + expect(context.setSelectedProject).toHaveBeenCalledWith(createdProject) +} + describe("browser create project action", () => { beforeEach(() => { - eventStreamCloseMock.mockReset() - loadProjectDetailsMock.mockReset() - openProjectEventStreamMock.mockReset() - startCreateProjectMock.mockReset() - startCreateProjectMock.mockImplementation(() => - Effect.succeed({ - accepted: true, - cursor: 7, - projectId: project.id - }) - ) - openProjectEventStreamMock.mockImplementation(() => ({ close: eventStreamCloseMock })) + resetCreateMocks() }) it.effect("clones a project through the browser menu create flow", () => Effect.gen(function*(_) { - const { context, output, reloadDashboard, setMessage } = makeBrowserActionContext() - - submitCreateInputs(createInputs, context) - - yield* _(waitForAssertion(() => { - expect(openProjectEventStreamMock).toHaveBeenCalledTimes(1) - })) - readCreateEventHandler()(projectCreatedEvent) - - yield* _(waitForAssertion(() => { - expect(context.setSelectedProject).toHaveBeenCalledWith(project) - })) + const { context, createdProject, output, reloadDashboard, setMessage } = yield* _( + runCreateFlow(projectDetailsWithId(project.id)) + ) expect(startCreateProjectMock).toHaveBeenCalledWith(expectedCreateDraft) - expect(openProjectEventStreamMock).toHaveBeenCalledWith(project.id, expect.objectContaining({ initialCursor: 7 })) - expect(eventStreamCloseMock).toHaveBeenCalledTimes(1) - expect(loadProjectDetailsMock).not.toHaveBeenCalled() - expect(reloadDashboard).toHaveBeenCalledTimes(1) - expect(context.setSelectedProjectId).toHaveBeenCalledWith(project.id) + expectCreateFlowInvariants({ context, createdProject, cursor: 7, reloadDashboard }) expect(context.setSelectedMenuIndex).toHaveBeenCalledWith(1) expect(setMessage).toHaveBeenLastCalledWith("Created octocat/Hello-World.") expect(output()).toContain("[create] Project creation requested") expect(output()).toContain("[create] Project accepted: project-1") expect(output()).toContain("[create] Project created") })) + + it.effect("preserves create event invariants for generated project ids and cursors", () => + Effect.tryPromise({ + catch: (error) => error, + try: () => + fc.assert( + fc.asyncProperty( + fc.uuid(), + fc.integer({ min: 0, max: 10_000 }), + (projectId, cursor) => + Effect.runPromise( + Effect.gen(function*(_) { + resetCreateMocks(projectId, cursor) + const createdProject = projectDetailsWithId(projectId) + const { context, reloadDashboard } = yield* _(runCreateFlow(createdProject)) + + expectCreateFlowInvariants({ context, createdProject, cursor, reloadDashboard }) + }) + ) + ), + { numRuns: 25 } + ) + })) }) diff --git a/packages/app/tests/docker-git/app-ready-create.test.ts b/packages/app/tests/docker-git/app-ready-create.test.ts index ab497e73..f0cfa62c 100644 --- a/packages/app/tests/docker-git/app-ready-create.test.ts +++ b/packages/app/tests/docker-git/app-ready-create.test.ts @@ -1,6 +1,8 @@ +import * as fc from "fast-check" import type { Dispatch, SetStateAction } from "react" import { beforeEach, describe, expect, it, vi } from "vitest" +import { deriveRepoPathParts, resolveRepoInput } from "../../src/docker-git/frontend-lib/core/domain.js" import { type CreateFlowView, createInitialFlowView, @@ -23,6 +25,72 @@ const validGithubStatus: GithubAuthStatus = { tokens: [{ key: "default", label: "default", login: "octocat", status: "valid" }] } +const githubNameChars = [ + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + "-" +] as const + +const githubSegmentArbitrary = fc + .array(fc.constantFrom(...githubNameChars), { minLength: 1, maxLength: 12 }) + .map((chars) => chars.join("")) + .filter((value) => !value.startsWith("-") && !value.endsWith("-")) + +const repositoryCreateInputArbitrary = fc.record({ + branch: fc.option(githubSegmentArbitrary, { nil: null }), + owner: githubSegmentArbitrary, + repo: githubSegmentArbitrary +}).map(({ branch, owner, repo }) => ({ + expectedRepoRef: branch ?? "main", + repoUrl: branch === null + ? `https://github.com/${owner}/${repo}` + : `https://github.com/${owner}/${repo}/tree/${branch}` +})) + +const defaultQuickCreateInputs = { + cpuLimit: "", + enableMcpPlaywright: false, + force: false, + forceEnv: false, + gpu: "none", + ramLimit: "", + runUp: true +} satisfies Omit + const createSetCreateViewSpy = () => { const spy = vi.fn<(value: SetStateAction) => void>() const setCreateView: Dispatch> = spy @@ -58,6 +126,32 @@ const submitCreateBuffer = ( return { context, setCreateViewSpy } } +const requireSubmittedCreateInputs = (): CreateInputs => { + const inputs = submitCreateInputsMock.mock.calls[0]?.[0] + if (inputs === undefined) { + throw new Error("Expected submitted CreateInputs.") + } + return inputs +} + +const expectQuickCreateInputs = ( + expected: Pick +) => { + expect(requireSubmittedCreateInputs()).toEqual( + { + ...defaultQuickCreateInputs, + ...expected + } satisfies CreateInputs + ) +} + +const expectCreateViewReset = (setCreateViewSpy: ReturnType["setCreateViewSpy"]) => { + expect(requireCreateViewValue(setCreateViewSpy.mock.calls[0]?.[0])).toEqual(createInitialFlowView()) +} + +const expectedOutDirForRepoUrl = (repoUrl: string): string => + `/home/dev/.docker-git/${deriveRepoPathParts(resolveRepoInput(repoUrl).repoUrl).pathParts.join("/")}` + describe("app-ready-create", () => { beforeEach(() => { submitCreateInputsMock.mockReset() @@ -102,20 +196,29 @@ describe("app-ready-create", () => { ) expect(submitCreateInputsMock).toHaveBeenCalledTimes(1) - expect(submitCreateInputsMock.mock.calls[0]?.[0]).toEqual( - { - cpuLimit: "", - enableMcpPlaywright: false, - force: false, - forceEnv: false, - gpu: "none", - outDir: "/home/dev/.docker-git/octocat/hello-world", - ramLimit: "", - repoRef: "feature-x", - repoUrl: "https://github.com/octocat/Hello-World/tree/feature-x", - runUp: true - } satisfies CreateInputs + expectQuickCreateInputs({ + outDir: "/home/dev/.docker-git/octocat/hello-world", + repoRef: "feature-x", + repoUrl: "https://github.com/octocat/Hello-World/tree/feature-x" + }) + expectCreateViewReset(setCreateViewSpy) + }) + + it("preserves quick create repo url to out dir invariants for generated GitHub repos", () => { + fc.assert( + fc.property(repositoryCreateInputArbitrary, ({ expectedRepoRef, repoUrl }) => { + submitCreateInputsMock.mockReset() + const { setCreateViewSpy } = submitCreateBuffer(repoUrl, { quickCreate: true }) + + expect(submitCreateInputsMock).toHaveBeenCalledTimes(1) + expectQuickCreateInputs({ + outDir: expectedOutDirForRepoUrl(repoUrl), + repoRef: expectedRepoRef, + repoUrl + }) + expectCreateViewReset(setCreateViewSpy) + }), + { numRuns: 50 } ) - expect(requireCreateViewValue(setCreateViewSpy.mock.calls[0]?.[0])).toEqual(createInitialFlowView()) }) }) From 203db754930f7c1815c250ff4255f3e3cc098598 Mon Sep 17 00:00:00 2001 From: skulidropek <66840575+skulidropek@users.noreply.github.com> Date: Wed, 13 May 2026 13:25:57 +0000 Subject: [PATCH 6/6] fix(test): satisfy effect lint for generators --- .../tests/docker-git/app-ready-create.test.ts | 45 +++---------------- 1 file changed, 5 insertions(+), 40 deletions(-) diff --git a/packages/app/tests/docker-git/app-ready-create.test.ts b/packages/app/tests/docker-git/app-ready-create.test.ts index f0cfa62c..8a3437cf 100644 --- a/packages/app/tests/docker-git/app-ready-create.test.ts +++ b/packages/app/tests/docker-git/app-ready-create.test.ts @@ -25,48 +25,13 @@ const validGithubStatus: GithubAuthStatus = { tokens: [{ key: "default", label: "default", login: "octocat", status: "valid" }] } -const githubNameChars = [ - "a", - "b", - "c", - "d", - "e", - "f", - "g", - "h", - "i", - "j", - "k", - "l", - "m", - "n", - "o", - "p", - "q", - "r", - "s", - "t", - "u", - "v", - "w", - "x", - "y", - "z", - "0", - "1", - "2", - "3", - "4", - "5", - "6", - "7", - "8", - "9", - "-" -] as const +const githubNameChars = "abcdefghijklmnopqrstuvwxyz0123456789-" +const githubNameCharArbitrary = fc + .integer({ min: 0, max: githubNameChars.length - 1 }) + .map((index) => githubNameChars[index] ?? "a") const githubSegmentArbitrary = fc - .array(fc.constantFrom(...githubNameChars), { minLength: 1, maxLength: 12 }) + .array(githubNameCharArbitrary, { minLength: 1, maxLength: 12 }) .map((chars) => chars.join("")) .filter((value) => !value.startsWith("-") && !value.endsWith("-"))