#!/usr/bin/env bats # EGB-1232: workspace discovery must not be npm-only. # # Two defects, both in the pattern-source layer: # 1. pnpm declares workspaces in pnpm-workspace.yaml, not package.json's # `workspaces` key — so both call sites came up empty on every pnpm # monorepo (silently, in the plain-push path). # 2. the jq filter `.workspaces // .workspaces.packages` short-circuits on # yarn's truthy object form, iterating the object's values and yielding # the pattern array itself instead of the patterns. # # bash 3.2 gotcha (see CLAUDE.md): every standalone [[ ]] assertion MUST end # with `|| false`, or a failing assertion does not fail the test. load test_helper # ─── fixtures ────────────────────────────────────────────────────────── # A monorepo root with two workspace packages, each holding a .env. # The workspace *declaration* is left to the caller — that is what varies. # NB: the workspace re-scan (_maybe_workspace_env_files) runs only for # projects that already have a .secrets.json — push's root-scan-only # behaviour on a first push is by design (EGB-677 E13), and EGB-1232 is # about the fallback that covers it never engaging. So every fixture # carries a manifest, matching the reported repro. make_monorepo() { local dir="$WORK_DIR/${1:-mono}" mkdir -p "$dir/apps/web" "$dir/apps/admin" echo "VITE_CLERK_PUBLISHABLE_KEY=pk_test_web" > "$dir/apps/web/.env" echo "VITE_CLERK_PUBLISHABLE_KEY=pk_test_admin" > "$dir/apps/admin/.env" printf '{\n "version": 2,\n "dotenv": [],\n "external": []\n}\n' > "$dir/.secrets.json" cd "$dir" } declare_pnpm() { cat > pnpm-workspace.yaml <<'YAML' packages: - "apps/*" - "packages/*" YAML echo '{"name":"mono","private":true}' > package.json } declare_npm() { echo '{"name":"mono","private":true,"workspaces":["apps/*"]}' > package.json } declare_yarn_object() { echo '{"name":"mono","private":true,"workspaces":{"packages":["apps/*"]}}' > package.json } # ─── the pnpm gap (the reported bug) ─────────────────────────────────── @test "EGB-1232: push discovers workspace env files in a pnpm monorepo" { init_with_remote make_monorepo pnpmrepo declare_pnpm run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" == *"apps/web/.env"* ]] || false [[ "$output" == *"apps/admin/.env"* ]] || false } @test "EGB-1232: push actually vaults pnpm workspace env files" { init_with_remote make_monorepo pnpmreal declare_pnpm run "$SECRETS_BIN" push [ "$status" -eq 0 ] rm -f apps/web/.env apps/admin/.env run "$SECRETS_BIN" pull [ "$status" -eq 0 ] [ -f apps/web/.env ] [ -f apps/admin/.env ] # The value that was empty in the .env.example — the actual impact. [[ "$(cat apps/web/.env)" == *"pk_test_web"* ]] || false } @test "EGB-1232: push -w works on a pnpm monorepo" { init_with_remote make_monorepo pnpmw declare_pnpm run "$SECRETS_BIN" push -w [ "$status" -eq 0 ] [[ "$output" != *"No workspaces field"* ]] || false } @test "EGB-1232: pnpm-workspace.yaml is read without requiring a package.json" { init_with_remote make_monorepo pnpmnopkg cat > pnpm-workspace.yaml <<'YAML' packages: - "apps/*" YAML run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" == *"apps/web/.env"* ]] || false } # ─── pnpm YAML parsing edge cases ────────────────────────────────────── @test "EGB-1232: pnpm parsing stops at the next top-level key" { init_with_remote make_monorepo pnpmkeys # pnpm 10+ carries sibling top-level keys. They must not leak in as globs. cat > pnpm-workspace.yaml <<'YAML' packages: - "apps/*" onlyBuiltDependencies: - esbuild - sharp catalog: react: ^18.0.0 YAML run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" == *"apps/web/.env"* ]] || false [[ "$output" != *"esbuild"* ]] || false [[ "$output" != *"sharp"* ]] || false } @test "EGB-1232: pnpm parsing handles quotes, bare entries, and comments" { init_with_remote make_monorepo pnpmquotes mkdir -p docs && echo "DOCS=1" > docs/.env cat > pnpm-workspace.yaml <<'YAML' # which packages belong to this workspace packages: - "apps/*" # double quoted, with a trailing comment - 'docs' # single quoted, no glob YAML run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" == *"apps/web/.env"* ]] || false [[ "$output" == *"docs/.env"* ]] || false } @test "EGB-1232: a pnpm-workspace.yaml with no packages key discovers nothing and says so" { init_with_remote make_monorepo pnpmempty cat > pnpm-workspace.yaml <<'YAML' onlyBuiltDependencies: - esbuild YAML run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" == *"pnpm-workspace.yaml"* ]] || false } # ─── the yarn object-form defect ─────────────────────────────────────── @test "EGB-1232: yarn's object workspaces form is expanded, not iterated" { init_with_remote make_monorepo yarnobj declare_yarn_object run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" == *"apps/web/.env"* ]] || false [[ "$output" == *"apps/admin/.env"* ]] || false } @test "EGB-1232: yarn object form works under -w" { init_with_remote make_monorepo yarnobjw declare_yarn_object run "$SECRETS_BIN" push -w [ "$status" -eq 0 ] [[ "$output" != *"No workspaces field"* ]] || false } # ─── npm regression guard ────────────────────────────────────────────── @test "EGB-1232: npm's array workspaces form still works" { init_with_remote make_monorepo npmarr declare_npm run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" == *"apps/web/.env"* ]] || false [[ "$output" == *"apps/admin/.env"* ]] || false } @test "EGB-1232: package.json workspaces wins when both sources are present" { init_with_remote make_monorepo bothsrc mkdir -p only-pnpm && echo "P=1" > only-pnpm/.env echo '{"name":"mono","workspaces":["apps/*"]}' > package.json cat > pnpm-workspace.yaml <<'YAML' packages: - "only-pnpm" YAML run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" == *"apps/web/.env"* ]] || false [[ "$output" != *"only-pnpm/.env"* ]] || false } # ─── diagnostics: the silence is the bug ─────────────────────────────── @test "EGB-1232: a monorepo-shaped root with no readable workspace source warns" { init_with_remote make_monorepo shaped # Looks like a monorepo (pnpm-workspace.yaml present) but declares nothing. echo "# nothing useful here" > pnpm-workspace.yaml # A root .env so the push SUCCEEDS — the dangerous case is a green push # that silently skipped every workspace, which is what bit thatsbait. echo "ROOT=1" > .env run "$SECRETS_BIN" push [ "$status" -eq 0 ] [[ "$output" == *"WARNING"* ]] || false [[ "$output" == *"workspace"* ]] || false } @test "EGB-1232: the warning tells you how to recover" { init_with_remote make_monorepo shapedfix echo "# nothing useful here" > pnpm-workspace.yaml echo "ROOT=1" > .env run "$SECRETS_BIN" push [ "$status" -eq 0 ] [[ "$output" == *"secrets add"* ]] || false } @test "EGB-1232: a plain single-package project never warns about workspaces" { init_with_remote create_project_dir plainproj printf '{\n "version": 2,\n "dotenv": [],\n "external": []\n}\n' > .secrets.json run "$SECRETS_BIN" push [ "$status" -eq 0 ] [[ "$output" != *"workspace"* ]] || false } @test "EGB-1232: push -w names pnpm-workspace.yaml when that is the file present" { init_with_remote make_monorepo wnames echo '{"name":"mono"}' > package.json echo "# no packages key" > pnpm-workspace.yaml run "$SECRETS_BIN" push -w [ "$status" -ne 0 ] [[ "$output" == *"pnpm-workspace.yaml"* ]] || false } @test "EGB-1232: push -w still names package.json when no pnpm file exists" { init_with_remote make_monorepo wnamespkg echo '{"name":"mono"}' > package.json run "$SECRETS_BIN" push -w [ "$status" -ne 0 ] [[ "$output" == *"package.json"* ]] || false } # ─── path safety ─────────────────────────────────────────────────────── @test "EGB-1232: a pnpm packages entry cannot escape the project root" { init_with_remote make_monorepo escapee echo "OUTSIDE=1" > "$WORK_DIR/outside.env" cat > pnpm-workspace.yaml <<'YAML' packages: - "../" - "/etc" YAML run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [[ "$output" != *"outside.env"* ]] || false [[ "$output" != *"/etc/"* ]] || false } @test "EGB-1232: a pnpm packages entry with shell metacharacters is refused" { init_with_remote make_monorepo metachars cat > pnpm-workspace.yaml <<'YAML' packages: - "apps/*; touch /tmp/egb1232-pwned" YAML run "$SECRETS_BIN" push --dry-run [ "$status" -eq 0 ] [ ! -f /tmp/egb1232-pwned ] }