{"owner":"loft-sh","repo":"vcluster","hasSkills":true,"totalSkillsCount":12,"totalTokensCount":14795,"categories":["anthropic-skill","claude-rule","plugin-manifest"],"hasMcp":false,"mcpConfig":null,"found":[".claude/references/e2e-examples.md",".claude/references/e2e-old-to-new-mapping.md",".claude/skills/e2e-migration-validator/SKILL.md","CLAUDE.md",".claude/references/e2e-framework-suite-deps.md",".claude/references/decomposition-heuristics.md",".claude/rules/e2e-conventions.md",".claude/rules/e2e-error-handling.md",".claude/rules/e2e-quality-checklist.md",".claude/rules/e2e-test-structure.md","vendor/github.com/google/gnostic-models/extensions/README.md",".claude/rules/scratchpad-programs.md"],"skills":{".claude/references/e2e-examples.md":"# E2E Test Examples — Annotated Patterns\n\nConcrete excerpts from production tests in `e2e/`. These are ground truth — read them before writing new tests.\n\n---\n\n## 1. DeferCleanup Pattern\n\nRendered YAML cleanup and vCluster teardown are handled by `setup/lazyvcluster.LazyVCluster` - you do not register them by hand. `SynchronizedBeforeSuite` only provisions the host kind cluster now.\n\n```go\n// suite_myfeature_test.go — lazy helper owns YAML cleanup + vCluster teardown\nfunc suiteMyFeature() {\n    Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n        cluster.Use(clusters.HostCluster),\n        func() {\n            BeforeAll(func(ctx context.Context) context.Context {\n                return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n            })\n            // specs...\n        },\n    )\n}\n```\n\nInside `It` blocks - register cleanup immediately after resource creation, before any further assertions:\n\n```go\nBy(\"creating source namespace on host\", func() {\n    _, err := hostClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{\n        ObjectMeta: metav1.ObjectMeta{Name: fromNS},\n    }, metav1.CreateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(func(ctx context.Context) {\n        Expect(hostClient.CoreV1().Namespaces().Delete(ctx, fromNS, metav1.DeleteOptions{})).To(Succeed())\n        Eventually(func(g Gomega) {\n            _, err := hostClient.CoreV1().Namespaces().Get(ctx, fromNS, metav1.GetOptions{})\n            g.Expect(kerrors.IsNotFound(err)).To(BeTrue())\n        }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n    })\n})\n```\n\n---\n\n## 2. Eventually + Polling\n\nSource: `e2e/test_core/sync/test_servicesync.go`, `e2e/test_deploy/test_helm_charts.go`\n\nUse the `g Gomega` parameter inside `Eventually`. Bare `Expect()` inside `Eventually` panics instead of retrying. Include failure context for debuggability.\n\n**Waiting for a resource to appear:**\n\n```go\nvar toService *corev1.Service\nBy(\"waiting for the replicated service to appear in vcluster\", func() {\n    Eventually(func(g Gomega) {\n        toService, err = vClusterClient.CoreV1().Services(toNS).Get(ctx, toName, metav1.GetOptions{})\n        g.Expect(err).NotTo(HaveOccurred())\n    }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n})\n```\n\n**Waiting for a resource to be deleted:**\n\n```go\nBy(\"waiting for the replicated service to be removed from vcluster\", func() {\n    Eventually(func(g Gomega) {\n        _, err := vClusterClient.CoreV1().Services(toNS).Get(ctx, toName, metav1.GetOptions{})\n        g.Expect(kerrors.IsNotFound(err)).To(BeTrue(), \"replicated service should be deleted after source is gone\")\n    }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n})\n```\n\n**Checking deploy status with descriptive failure messages:**\n\n```go\nEventually(func(g Gomega) {\n    cm, err := vClusterClient.CoreV1().ConfigMaps(deploy.VClusterDeployConfigMapNamespace).\n        Get(ctx, deploy.VClusterDeployConfigMap, metav1.GetOptions{})\n    g.Expect(err).NotTo(HaveOccurred(), \"Deploy configmap should exist\")\n    status := deploy.ParseStatus(cm)\n    g.Expect(status.Charts).To(HaveLen(2), \"Should have 2 charts configured\")\n    for _, chart := range status.Charts {\n        g.Expect(chart.Phase).To(\n            Equal(string(deploy.StatusSuccess)),\n            fmt.Sprintf(\"Chart %s is not in Success phase, got phase=%s, reason=%s, message=%s\",\n                chart.Name, chart.Phase, chart.Reason, chart.Message))\n    }\n}).\n    WithPolling(constants.PollingInterval).\n    WithTimeout(constants.PollingTimeout).\n    Should(Succeed(), \"Both charts should be successfully deployed\")\n```\n\n**Returning a value from Eventually for direct assertion:**\n\n```go\nEventually(func(g Gomega) []appsv1.Deployment {\n    deployList, err := vClusterClient.AppsV1().Deployments(ChartOCINamespace).List(ctx, metav1.ListOptions{\n        LabelSelector: k8slabels.SelectorFromSet(HelmOCIDeploymentLabels).String(),\n    })\n    g.Expect(err).NotTo(HaveOccurred(), \"Should be able to list deployments\")\n    return deployList.Items\n}).\n    WithPolling(constants.PollingInterval).\n    WithTimeout(constants.PollingTimeout).\n    Should(HaveLen(1), \"Should have exactly one fluent-bit deployment\")\n```\n\n---\n\n## 3. Cluster Client Usage\n\nSource: `e2e/test_core/sync/test_node.go`\n\nObtain host and vcluster clients from context. Use `cluster.KubeClientFrom` with the host cluster name for the host client, and `cluster.CurrentKubeClientFrom` for the current vcluster client.\n\n```go\nvar _ = Describe(\"Node sync\",\n    labels.Core,\n    labels.Sync,\n    cluster.Use(clusters.NodesVCluster),\n    cluster.Use(clusters.HostCluster),\n    func() {\n        var (\n            hostClient     kubernetes.Interface\n            vClusterClient kubernetes.Interface\n        )\n\n        BeforeEach(func(ctx context.Context) {\n            hostClient = cluster.KubeClientFrom(ctx, constants.GetHostClusterName())\n            Expect(hostClient).NotTo(BeNil())\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Sync nodes using label selector\", func(ctx context.Context) {\n            hostname := constants.GetHostClusterName() + \"-control-plane\"\n            Eventually(func(g Gomega) {\n                hostNodes, err := hostClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"Failed to list host nodes\")\n\n                virtualNodes, err := vClusterClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"Failed to list virtual nodes\")\n                g.Expect(virtualNodes.Items).ToNot(BeEmpty(), \"Virtual nodes list should not be empty\")\n\n                // ... assertions comparing host and virtual nodes\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Node sync should work correctly\")\n        })\n    })\n```\n\n---\n\n## 4. Ordered Usage\n\nSource: `test/e2e/snapshot/snapshot.go` (old suite — pattern, not import paths)\n\nUse `Ordered` when specs form a **lifecycle sequence** where later specs depend on state produced by earlier specs, **and** shared setup is genuinely expensive (resource creation, deployment readiness, data provisioning). The snapshot-with-volumes test is the canonical example: `BeforeAll` provisions a namespace, a PVC with written data, and several Kubernetes resources, then each `It` block advances through create → verify → delete → restore → verify-restored.\n\n```go\nDescribe(\"controller-based snapshot with volumes\", Ordered, func() {\n    const (\n        controllerTestNamespaceName = \"volume-snapshots-test\"\n        snapshotPath                = \"container:///snapshot-data/\" + controllerTestNamespaceName + \".tar.gz\"\n        pvcToRestoreName            = \"test-pvc-restore\"\n        testFileName                = controllerTestNamespaceName + \".txt\"\n        pvcData                     = \"Hello \" + controllerTestNamespaceName\n    )\n\n    // BeforeAll — expensive: creates namespace, PVC with data written via a pod,\n    // plus Deployments, Services, ConfigMaps, and Secrets.\n    BeforeAll(func(ctx context.Context) {\n        f = framework.DefaultFramework\n        deployTestNamespace(controllerTestNamespaceName)\n        // Creates PVC, spins up a writer pod, waits for pod readiness, writes data\n        createPVCWithData(ctx, f.VClusterClient, controllerTestNamespaceName,\n            pvcToRestoreName, testFileName, pvcData)\n        // Creates Deployment (nginx), Service, ConfigMap, Secret — waits for deploy ready\n        deployTestResources(controllerTestNamespaceName, true)\n    })\n\n    // Spec 1: triggers snapshot creation — later specs verify the snapshot exists\n    It(\"Creates the snapshot request\", func() {\n        createSnapshot(f, true, snapshotPath, true)\n    })\n\n    // Spec 2: depends on spec 1 — polls until snapshot completes\n    It(\"Creates the snapshot\", func(ctx context.Context) {\n        waitForSnapshotToBeCreated(ctx, f)\n    })\n\n    // Spec 3: depends on spec 2 — verifies VolumeSnapshot resources were cleaned up\n    It(\"Doesn't contain VolumeSnapshot and VolumeSnapshotContent\", func(ctx context.Context) {\n        // ... verifies cleanup of temporary snapshot objects\n    })\n\n    // Spec 4: depends on spec 2 — deletes PVC so restore can recreate it\n    It(\"Deletes the PVC with test data\", func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n    })\n\n    // checkTestResources embeds two It blocks:\n    //   \"Verify if only the resources in snapshot are available after restore\"\n    //   \"Verify if deleted resources are recreated after restore\"\n    // Both depend on prior specs having created and snapshotted resources.\n    checkTestResources(controllerTestNamespaceName, true, snapshotPath)\n\n    // Spec 7: depends on PVC being restored (without data) by checkTestResources\n    It(\"restores vCluster with volumes\", func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n        restoreVCluster(ctx, f, snapshotPath, true, true)\n    })\n\n    // Spec 8: depends on spec 7 — verifies PVC is bound after volume restore\n    It(\"has the restored PVC which is bound\", func(ctx context.Context) {\n        Eventually(func(g Gomega, ctx context.Context) corev1.PersistentVolumeClaimPhase {\n            restoredPVC, err := f.VClusterClient.CoreV1().PersistentVolumeClaims(\n                controllerTestNamespaceName).Get(ctx, pvcToRestoreName, metav1.GetOptions{})\n            g.Expect(err).NotTo(HaveOccurred())\n            return restoredPVC.Status.Phase\n        }).WithContext(ctx).\n            WithPolling(framework.PollInterval).\n            WithTimeout(framework.PollTimeoutLong).\n            Should(Equal(corev1.ClaimBound))\n    })\n\n    // Spec 9: depends on spec 8 — verifies data survived the snapshot/restore cycle\n    It(\"has the restored PVC with data from the volume snapshot\", func(ctx context.Context) {\n        checkPVCData(ctx, f.VClusterClient, controllerTestNamespaceName,\n            pvcToRestoreName, testFileName, pvcData)\n    })\n\n    AfterAll(func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n        cleanUpTestResources(ctx, true, controllerTestNamespaceName)\n    })\n})\n```\n\n**Why `Ordered` is justified here:** Each spec advances a lifecycle (provision → snapshot → cleanup → restore → verify) where later specs depend on side effects from earlier ones. The `BeforeAll` is genuinely expensive — it creates a namespace, provisions a PVC, spins up a writer pod to populate data, and deploys multiple resources with readiness checks. Contrast with the `BeforeEach` examples below, where specs are independent and only need client references.\n\n---\n\n## 5. BeforeEach Pattern\n\nSource: `e2e/test_deploy/test_helm_charts.go`, `e2e/test_deploy/test_init_manifests.go`\n\nUse `BeforeEach` when specs are independent and only need shared client setup. Each spec operates on pre-existing state (deployed charts, init manifests) and doesn't modify shared resources.\n\n```go\nvar _ = Describe(\"Helm charts (regular and OCI) are synced and applied as expected\",\n    labels.Deploy,\n    cluster.Use(clusters.HelmChartsVCluster),\n    func() {\n        var vClusterClient kubernetes.Interface\n\n        BeforeEach(func(ctx context.Context) {\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Test if configmap for both charts gets applied\", func(ctx context.Context) {\n            Eventually(func(g Gomega) {\n                // ... check deploy configmap status\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Both charts should be successfully deployed\")\n        })\n\n        It(\"Test nginx release secret existence in vcluster (regular chart)\", func(ctx context.Context) {\n            // ... independent verification\n        })\n    })\n```\n\n**BeforeEach returning context** (from `test_init_manifests.go`):\n\n```go\nvar _ = Describe(\"Init manifests are synced and applied as expected\",\n    labels.Deploy,\n    cluster.Use(clusters.InitManifestsVCluster),\n    func() {\n        var (\n            vClusterName   = clusters.InitManifestsVClusterName\n            vClusterClient kubernetes.Interface\n        )\n\n        BeforeEach(func(ctx context.Context) {\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Test if manifest template is synced with the vcluster\", func(ctx context.Context) {\n            Eventually(func(g Gomega) {\n                manifest, err := vClusterClient.CoreV1().ConfigMaps(TestManifestNamespace).\n                    Get(ctx, TestManifestName2, metav1.GetOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"ConfigMap should exist\")\n                g.Expect(manifest.Data[\"foo\"]).To(Equal(vClusterName),\n                    \"ConfigMap foo value should equal vcluster name\")\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Manifest template should be synced\")\n        })\n    },\n)\n```\n",".claude/references/e2e-old-to-new-mapping.md":"# Old → New Pattern Mapping (vcluster)\n\nTranslation table for migrating tests from `test/framework` to `e2e/`.\n\n## Client Accessors\n\n| Old Pattern (`test/framework`) | New Pattern (`e2e`) |\n|---|---|\n| `framework.DefaultFramework` global singleton | Context-based: `cluster.KubeClientFrom(ctx, name)`, `cluster.CurrentKubeClientFrom(ctx)` |\n| `f.HostClient` / `f.HostCRClient` | `cluster.KubeClientFrom(ctx, constants.GetHostClusterName())` |\n| `f.VClusterClient` / `f.VClusterCRClient` | `cluster.CurrentKubeClientFrom(ctx)` / `cluster.CurrentClusterClientFrom(ctx)` |\n| `f.VClusterName` / `f.VClusterNamespace` | Derived from cluster definition name in `clusters/` (e.g., `clusters.ServiceSyncVClusterName`) |\n| `os.Getenv(\"VCLUSTER_NAME\")` etc. | Cluster definitions with typed config in `constants/` |\n| `f.Suffix` / `translate.VClusterName` | `translate` package used directly where needed |\n\n## Setup and Lifecycle\n\n| Old Pattern | New Pattern |\n|---|---|\n| `framework.CreateFramework(ctx)` in `BeforeSuite` | `SynchronizedBeforeSuite` with `clusters.HostCluster.Setup` + `clusters.XVCluster.Setup` |\n| `f.Cleanup()` in `AfterSuite` | `SynchronizedAfterSuite` with `clusters.HostCluster.Teardown` |\n| `f.RefreshVirtualClient()` | `connectVCluster()` — see \"Reconnecting after destructive operations\" below |\n| Per-suite `values.yaml` | Per-cluster embedded YAML in `clusters/*.go` with `template.MustRender()` |\n| Single vcluster per suite | Multiple vclusters with different configs (DefaultVCluster, NodesVCluster, etc.) |\n\n## Test Execution\n\n| Old Pattern | New Pattern |\n|---|---|\n| `go test -v -ginkgo.v` | `ginkgo --label-filter=\"...\" ./e2e` |\n| `time.Sleep()` | `Eventually().WithPolling().WithTimeout()` |\n| Ad-hoc durations | `constants.PollingTimeout*` constants |\n| Env-based config (`VCLUSTER_SUFFIX`, etc.) | Flag-based config (`--vcluster-image`, `--cluster-name`) |\n\n## Timeouts\n\n| Old Constant | New Constant | Duration |\n|---|---|---|\n| `framework.PollInterval` (5s) | `constants.PollingInterval` | 2s |\n| `framework.PollTimeout` (1min) | `constants.PollingTimeout` | 60s |\n| `framework.PollTimeoutLong` (2min) | `constants.PollingTimeoutLong` | 120s |\n| N/A | `constants.PollingTimeoutVeryShort` | 5s |\n| N/A | `constants.PollingTimeoutShort` | 20s |\n| N/A | `constants.PollingTimeoutVeryLong` | 300s |\n\n## Cluster Definitions\n\nThe old framework uses a single vcluster per suite. The new framework defines multiple vclusters in `e2e/clusters/clusters.go`:\n\n```go\n// Host cluster (Kind)\nclusters.HostCluster = cluster.Define(\n    cluster.WithName(constants.GetHostClusterName()),\n    cluster.WithProvider(kind.NewProvider()),\n    cluster.WithConfigFile(\"e2e-kind.config.yaml\"),\n)\n\n// Virtual clusters — each with specific config\nclusters.DefaultVCluster = vcluster.Define(\n    vcluster.WithName(\"default-vcluster\"),\n    vcluster.WithVClusterYAML(DefaultVClusterYAML),\n    vcluster.WithOptions(DefaultVClusterOptions...),\n    vcluster.WithDependencies(HostCluster),\n)\n```\n\nUse `cluster.Use(clusters.XVCluster)` in `Describe` to wire a test to a specific cluster:\n\n```go\nvar _ = Describe(\"My test\",\n    labels.Core,\n    cluster.Use(clusters.DefaultVCluster),\n    cluster.Use(clusters.HostCluster),\n    func() { ... },\n)\n```\n\n## Reconnecting After Destructive Operations\n\nThe suite-level background proxy (established in `SynchronizedBeforeSuite`) does **not** survive\nvcluster pause/resume or cert rotation. `cluster.CurrentKubeClientFrom(ctx)` and\n`cluster.CurrentClusterFrom(ctx).KubernetesRestConfig()` both point to the suite proxy — if the\nvcluster has been paused, restarted, or had its certs rotated, these return dead connections.\n\nThe old framework's `f.RefreshVirtualClient()` re-established the connection. The e2e\nequivalent is to run `vcluster connect` programmatically with a background proxy:\n\n1. Create a temp file for the kubeconfig\n2. Build a `connectcmd.ConnectCmd` with `BackgroundProxy: true`, `KubeConfig` pointing to the temp file, and `BackgroundProxyImage` set to `constants.GetVClusterImage()` — the same image already loaded into Kind\n3. Call `cmd.Run(ctx, []string{vclusterName})`\n4. Poll with `Eventually` until the kubeconfig file is non-empty and `clientcmd.RESTConfigFromKubeConfig` succeeds\n5. Verify the connection by making a lightweight API call (e.g., `Get` the `default` ServiceAccount)\n6. Return the `*rest.Config` and a cleanup func that removes the temp file\n\n**Critical: use `constants.GetVClusterImage()` for `BackgroundProxyImage`**, not\n`vclusterconstants.DefaultBackgroundProxyImage(upgrade.GetVersion())`.\n\nKey imports: `connectcmd \"github.com/loft-sh/vcluster/cmd/vclusterctl/cmd\"`,\n`\"github.com/loft-sh/vcluster/pkg/cli\"`, `\"github.com/loft-sh/vcluster/pkg/cli/flags\"`,\n`loftlog \"github.com/loft-sh/log\"`,\n`\"github.com/spf13/cobra\"`, `\"k8s.io/client-go/tools/clientcmd\"`,\n`\"github.com/loft-sh/vcluster/e2e/constants\"`.\n",".claude/skills/e2e-migration-validator/SKILL.md":"---\nname: e2e-migration-validator\ndescription: >\n  Use this skill whenever a user wants to validate, review, or audit e2e tests that have been\n  migrated from an old framework (typically in a `e2e/` directory) to the new `e2e/`\n  framework. Triggers include: \"review my migrated test\", \"validate this e2e test\", \"check\n  my test migration\", \"did I migrate this correctly\", \"review e2e PR\", \"check test coverage\n  after migration\", or any request involving comparing old vs new e2e test files across the\n  vcluster, vcluster-pro, or loft-enterprise repos. Always use this skill when an e2e test\n  file or PR from one of these repos is provided for review — even if the user just says\n  \"take a look at this test\".\n---\n\n# E2E Test Migration Validator\n\nA skill for exhaustively reviewing e2e tests migrated from the old framework (`next/` directory)\nto the new framework (`e2e/` directory) across the `vcluster`, `vcluster-pro`, and\n`loft-enterprise` repositories.\n\n---\n\n## Inputs\n\nThe user will provide one or more of:\n- **Uploaded file(s)**: The new migrated test file, and optionally the original test file\n- **GitHub PR link**: A pull request URL containing the migrated test(s)\n- **Pasted code**: Snippets of old and/or new test code inline in the chat\n\nIf only the new test is provided (no original), you must locate the original yourself using\nthe local repo tools (see [Repo Access](#repo-access)).\n\n---\n\n## Repo Access\n\nUse the local repo MCP tools to inspect implementation details and locate source files.\nAll three repos may be relevant depending on the test subject:\n\n| Repo | Use when |\n|------|----------|\n| `vcluster` | Core vcluster functionality, open-source features |\n| `vcluster-pro` | Pro/enterprise vcluster features |\n| `loft-enterprise` | Loft platform, multi-tenancy, UI-level features |\n\nWhen reviewing a test, **always look up the actual implementation** being tested in the\nrelevant repo before drawing conclusions about correctness or coverage. Do not rely solely\non what the test itself asserts.\n\n---\n\n## Review Workflow\n\nFollow these steps in order for every migration review:\n\n### Step 1 — Locate the Original Test\n\nIf the original (`next/`) test was not provided:\n1. Search the appropriate repo(s) for the original test file by name or describe pattern\n2. Read it fully before proceeding\n3. Note the framework, helper utilities, and assertion style used\n\n### Step 2 — Locate the Implementation Under Test\n\nUsing the repo MCP tools:\n1. Find the source code that the test is exercising (controllers, handlers, CLI commands, API routes, UI components, etc.)\n2. Read the implementation thoroughly — **line by line** if needed\n3. Build a mental model of:\n   - All code paths (happy path, error paths, edge cases)\n   - All configurable inputs / flags / options\n   - Side effects and state mutations\n   - Interactions with other subsystems\n\n### Step 3 — Framework Migration Check\n\nVerify the structural correctness of the migration:\n\n- [ ] Test file is in the correct `e2e/` directory structure\n- [ ] Imports use the new framework's helpers/fixtures (not the old ones)\n- [ ] Test lifecycle hooks (`beforeAll`, `afterAll`, `beforeEach`, `afterEach`) are correctly\n      translated — no leftover old-framework teardown patterns\n- [ ] Setup and teardown create/delete resources correctly and completely\n- [ ] Cluster/namespace/resource provisioning uses the new framework's APIs\n- [ ] Any shared utilities (custom matchers, wait helpers, retry logic) are migrated or\n      replaced with new equivalents — not just copied verbatim\n- [ ] Test isolation is maintained (tests do not share mutable state across runs)\n- [ ] Framework-specific APIs (e.g. `page`, `context`, `request`) are used correctly\n\n### Step 4 — Correctness Check\n\nFor each test case in the migrated file:\n\n1. **Does the test actually test what it claims to test?**\n   - Read the test name / description and compare against what is asserted\n   - Flag any tests where the name and assertions are misaligned\n2. **Are the assertions meaningful?**\n   - Look for trivially-passing assertions (e.g. `expect(x).toBeDefined()` with no deeper check)\n   - Look for assertions that would pass even if the feature is broken\n   - Look for missing negative assertions (e.g. testing that a resource *was* created but not\n     that an invalid input *was rejected*)\n3. **Is the test deterministic?**\n   - Flag race conditions, missing `await`, missing retries on flaky operations\n   - Flag hardcoded timeouts that may not hold in CI\n4. **Is the test environment realistic?**\n   - Does the test reflect how the feature is actually used in production?\n   - Are there unrealistic shortcuts (e.g. bypassing auth, using superuser where a normal user\n     should be tested)?\n\n### Step 5 — Exhaustive Coverage Analysis\n\nThis is the most important step. Compare the implementation (Step 2) against the test suite.\n\nFor **every distinct behavior, code path, and configurable option** in the implementation:\n\n1. Check whether it is covered by at least one test\n2. If not covered, classify the gap:\n   - 🔴 **Critical gap** — a regression here would not be caught at all\n   - 🟡 **Important gap** — partial coverage, but a significant edge case is missed\n   - 🟢 **Minor gap** — low-risk or already covered by unit tests\n\nFor each gap, suggest a concrete additional test case including:\n- What scenario it covers\n- What inputs/state are needed\n- What assertion(s) should be made\n\n**Common gap categories to check explicitly:**\n- Error handling and invalid inputs\n- Permission / RBAC boundary conditions\n- Resource limit enforcement\n- Concurrent operations\n- Feature flags / configuration variants\n- Upgrade/downgrade paths (if applicable)\n- Interaction with other features (cross-feature integration)\n- Cleanup/teardown correctness (does the feature leave no orphaned resources?)\n\n### Step 6 — Code Quality Review\n\n- Naming: are test and variable names descriptive and consistent with the new framework's conventions?\n- DRY: is there duplicated setup that should be extracted into a shared fixture or helper?\n- Readability: would a new engineer understand what is being tested and why?\n- Comments: are non-obvious steps explained?\n- Dead code: are there commented-out blocks, unused imports, or leftover debug statements?\n\n---\n\n## Output Format\n\nProduce **all three** of the following output sections:\n\n---\n\n### 1. Inline Code Analysis\n\nGo through the migrated test file and annotate specific lines or blocks with numbered\ncomments. Format as a fenced code block with inline `// [N] ...` annotations, followed by\na numbered legend below it explaining each annotation.\n\nExample:\n```typescript\n// [1] Missing: should also assert that the old resource was deleted\nawait expect(newResource).toBeVisible();\n\n// [2] Race condition: no retry/wait before asserting pod readiness\nexpect(pod.status).toBe('Running');\n```\n\n**Legend:**\n1. 🔴 Critical gap — deletion is not verified; the old resource could persist undetected\n2. 🟡 Flakiness risk — pod status may not be `Running` immediately; use `waitForCondition`\n\n---\n\n### 2. Checklist Report\n\n```\n## Migration Checklist — <test file name>\n\n### Framework Migration\n- [x] Correct directory structure\n- [x] New framework imports\n- [ ] ⚠️  Teardown uses old-framework pattern on line 42\n\n### Correctness\n- [x] Assertions are meaningful\n- [ ] ⚠️  Test \"should reject invalid config\" passes even with valid config (line 87)\n\n### Coverage Gaps\n- 🔴 No test for RBAC: non-admin user attempting privileged operation\n- 🟡 No test for concurrent resource creation\n- 🟢 No test for audit log output (low risk)\n\n### Code Quality\n- [ ] ⚠️  Duplicate setup block (lines 12–25 and 61–74) — extract to beforeEach\n- [x] Naming is clear and consistent\n```\n\n---\n\n### 3. PR Review Comment\n\nA copy-paste-ready GitHub PR review comment in Markdown. Structure:\n\n```\n## E2E Migration Review\n\n**Overall:** ✅ Approved with suggestions / ⚠️ Changes requested / ❌ Major issues found\n\n### Summary\n<2–3 sentence high-level assessment>\n\n### Issues\n\n#### 🔴 Must Fix\n- ...\n\n#### 🟡 Should Fix\n- ...\n\n#### 🟢 Nice to Have\n- ...\n\n### Suggested Additional Tests\n- **Test:** `<descriptive test name>`\n  **Scenario:** <what to set up>\n  **Assert:** <what to check>\n\n<repeat for each suggested test>\n\n### Nits\n- ...\n```\n\n---\n\n## Framework Notes\n\n> **For the reviewer using this skill:** If you know the specific old and new frameworks\n> involved in this migration, add them here so Claude can apply framework-specific checks.\n>\n> Example entries:\n> - Old: `@vcluster/e2e` custom test runner → New: Playwright + `@vcluster/e2e-next` fixtures\n> - Old: Ginkgo/Gomega (Go) → New: Playwright (TypeScript)\n> - Old: Custom shell-script harness → New: Playwright with kubectl helpers\n>\n> Until this section is filled in, Claude will infer the frameworks from the provided files.\n\n---\n\n## Tips for High-Quality Reviews\n\n- **Always read the implementation first.** A test that looks complete may miss half the\n  code paths if you haven't read the source.\n- **Think adversarially.** For each assertion, ask: \"How could this pass even if the feature\n  is broken?\"\n- **Don't skip cleanup verification.** Many e2e bugs are leaked resources, not failed\n  assertions.\n- **Flag flakiness proactively.** A flaky test is worse than no test — it erodes trust in\n  the suite.\n- **Be specific in suggestions.** Don't say \"add more tests.\" Say exactly what scenario,\n  what inputs, and what assertion.\n","CLAUDE.md":"# vCluster — Agent Instructions\n\n## E2E TDD Workflow\n\nWhen doing test-driven development, ALWAYS follow the workflow in\n[.claude/e2e-tdd-workflow.md](.claude/e2e-tdd-workflow.md).\nUse the AGENT_SESSION env var to isolate your commands per session.\n\n## CI / GitHub Actions\n\nShared CI logic lives in [`loft-sh/github-actions`](https://github.com/loft-sh/github-actions);\nproduction repos reuse those actions rather than inlining shell. The\n`github-actions-developer` skill (from `vcluster-skills-engineering`) auto-loads\nwhen you touch `.github/` and carries the full convention.\n",".claude/references/e2e-framework-suite-deps.md":"# e2e-framework: External Infrastructure Patterns\n\nTwo distinct patterns depending on what needs provisioning. Pick the right one before writing the plan.\n\n## Service-level provisioning (Helm charts, external APIs)\n\nUse this for: ingress-nginx, fluent-bit, metrics-server, cert-manager, CSI drivers, or any service installed into the test cluster.\n\nInstall in `BeforeAll`, tear down in `AfterAll`. Never assume the service is pre-provisioned.\n\n```go\nBeforeAll(func(ctx context.Context) context.Context {\n    By(\"installing <service>\")\n    ctx, err := setuphelm.Upgrade(\n        setuphelm.WithHelmOptions(\n            constants.Get<Service>HelmOptions()...,\n            helm.WithWait(),\n        ),\n    )(ctx)\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(...)\n    return ctx\n})\n\nAfterAll(func(ctx context.Context) {\n    _, err := setuphelm.Uninstall(\n        setuphelm.WithHelmOptions(\n            helm.WithName(\"<release>\"),\n            helm.WithNamespace(\"<ns>\"),\n        ),\n    )(ctx)\n    Expect(err).NotTo(HaveOccurred())\n    _, err = namespace.Delete(\"<ns>\")(ctx)\n    Expect(err).NotTo(HaveOccurred())\n})\n```\n\n**Why `AfterAll` and not `DeferCleanup`**: `DeferCleanup` in `BeforeAll` runs after all specs, which is equivalent here — but `AfterAll` is explicit and survives `--setup-only` runs where you want the service to persist between dev iterations.\n\n**Chart constants**: Add a `e2e/constants/<service>.go` with `Get<Service>HelmOptions()` following the pattern in existing constants files.\n\n---\n\n## Cluster-level provisioning (Kind host, vClusters)\n\nUse this for: tests that need a full ephemeral Kubernetes cluster or vCluster, not a service inside an existing cluster.\n\n**Two-layer model:**\n\n1. **Host kind cluster** - eagerly provisioned once in `SynchronizedBeforeSuite` via the framework's `cluster.Define` + `cluster.Setup`. Defined in `e2e/clusters/registry.go` as `clusters.HostCluster`. Reused by every per-test vCluster.\n2. **Per-test vClusters** - created lazily, one per `suite_*_test.go`, in the suite's outer `BeforeAll` via `setup/lazyvcluster.LazyVCluster`. The helper is a thin wrapper over the framework's `vcluster.Create` (see `github.com/loft-sh/e2e-framework/pkg/setup/vcluster`). Peak concurrent vClusters is bounded by `ginkgo --procs`, not by the number of suite files.\n\n```go\n// e2e/suite_myfeature_test.go\n//go:embed vcluster-myfeature.yaml\nvar myFeatureYAML string\n\nconst myFeatureName = \"myfeature-vcluster\"\n\nfunc init() { suiteMyFeature() }\n\nfunc suiteMyFeature() {\n    Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n        cluster.Use(clusters.HostCluster),\n        func() {\n            BeforeAll(func(ctx context.Context) context.Context {\n                return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML,\n                    // optional: host-side prerequisites (CRDs, PVCs, Helm install)\n                    lazyvcluster.WithPreSetup(setup.MyPrereq()),\n                )\n            })\n\n            // spec functions...\n        },\n    )\n}\n```\n\nInside specs: `cluster.CurrentKubeClientFrom(ctx)` resolves to the suite's vCluster. `cluster.KubeClientFrom(ctx, constants.GetHostClusterName())` resolves to the host.\n\n**Failure-aware teardown**: on spec failure the framework keeps the vCluster alive and attaches diagnostics (rendered config, pods, events, syncer logs) to the failing spec's report entries via `AddReportEntry`. On pass, teardown destroys the vCluster.\n\n**Focus is automatic**: a label-filtered run only materializes vClusters whose outer suite Describe matches. Unmatched suites' `BeforeAll` never fires, so no vCluster is spun up for tests that do not need it.\n\n---\n\n## Identifying what needs provisioning\n\nWhen migrating an old test, check these files in order:\n\n1. **Old test's `values.yaml`** — Look for `controlPlane.helmRelease` entries (charts the vcluster deploys internally), `experimental.deploy.vcluster.helm` sections, and any inline manifests in `experimental.deploy.vcluster.manifests`.\n2. **`host-resources.yaml`** (if present) — PVCs for volume snapshots, CRDs, or other host-cluster prerequisites.\n3. **`.github/workflows/e2e.yaml`** — CI workflow steps that install external dependencies before running the test suite.\n\n### Known External Dependencies by Suite\n\n| Dependency | Suites | Notes |\n|---|---|---|\n| CSI volume snapshots | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Snapshot CRDs + controller |\n| ingress-nginx 4.14.2 | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Ingress controller for service exposure tests |\n| fluent-bit 3.1.13 (OCI) | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Deployed as Helm chart inside vcluster |\n| metrics-server | e2e_metrics_proxy | Required for metrics API proxy tests |\n| Plugin images | e2e_plugin | Custom plugin container images |\n\n### Dependency-Free Suites\n\nThese suites have no external service dependencies:\n- `e2e_certs`\n- `e2e_cli`\n- `e2e_ha`\n- `e2e_limit_classes`\n- `e2e_pause_resume`\n\n**How to read the old test setup**: Look at the suite's CI workflow step. Each `helm install` or `kubectl apply` in the workflow's pre-test phase becomes either a `setuphelm.Upgrade()` call in `BeforeAll` or a cluster-level dependency via `cluster.Define`/`vcluster.Define`. The vcluster's `values.yaml` shows what config was baked into the vcluster itself — this becomes embedded YAML in `clusters/*.go`.\n",".claude/references/decomposition-heuristics.md":"# Decomposition Heuristics\n\n## The Core Test: \"Is This One It()?\"\n\nA sub-problem is ready when you can express it as:\n\n```go\nIt(\"should {one observable behavior}\", func(ctx context.Context) {\n    By(\"{action 1}\")\n    By(\"{action 2}\")\n    By(\"{verification}\")\n    // At most 3 By() steps\n})\n```\n\nIf you need more than 3 `By()` steps, the sub-problem is too big. Split it.\n\n## Splitting Strategies\n\n### Strategy 1: Split by CRUD Operation\n\nA feature that creates, reads, updates, and deletes becomes 4 sub-problems:\n\n```\nSP-1: Create {resource} with valid input\nSP-2: Get {resource} returns expected fields\nSP-3: Update {resource} changes specific field\nSP-4: Delete {resource} cleans up dependents\n```\n\n### Strategy 2: Split by Actor\n\nWhen multiple roles interact with a feature:\n\n```\nSP-1: Admin can create {resource}\nSP-2: Project member can view {resource}\nSP-3: Non-member gets Forbidden\n```\n\n### Strategy 3: Split by State Transition\n\nFor lifecycle features (sleep mode, VCI phases):\n\n```\nSP-1: Resource starts in {initial state}\nSP-2: Trigger moves resource to {intermediate state}\nSP-3: Resource reaches {final state}\nSP-4: Error condition produces {error state}\n```\n\n### Strategy 4: Split by Integration Boundary\n\nWhen a feature touches multiple systems:\n\n```\nSP-1: Management API accepts the request (API layer)\nSP-2: Controller reconciles the resource (controller layer)\nSP-3: Agent reflects the change (agent layer)\n```\n\n### Strategy 5: Split by Input Variation\n\nWhen behavior depends on configuration:\n\n```\nSP-1: Default configuration produces {default behavior}\nSP-2: Custom configuration A produces {behavior A}\nSP-3: Invalid configuration is rejected with {error}\n```\n\n## Merging Rules\n\nMerge two sub-problems if:\n- They always modify the same lines of code\n- One is meaningless without the other (e.g., \"create\" and \"verify creation\" are one sub-problem)\n- The combined test would still have <= 3 `By()` steps\n\n## Ordering Rules (TDD)\n\n1. **Infrastructure first**: Types, stubs, code generation, new `setup/` builders, new labels, new constants — everything needed for tests to *compile* (not pass)\n2. **Test before implementation**: For each behavior, the test sub-problem is ordered before the implementation sub-problem\n3. **Happy-path test before error-path test**: Prove it works, then prove it fails correctly\n4. **Independent before dependent**: SP-3 that depends on SP-1 comes after SP-1\n5. **Mark parallel groups**: Sub-problems with no dependencies get the same group letter\n\n### The TDD Cycle Pattern\n\nEach behavior produces a test→impl pair:\n```\nSP-N: [test] Write test for behavior X           → RED  (test fails, behavior missing)\nSP-M: [impl] Implement behavior X                → GREEN (test passes)\n```\n\nInfrastructure sub-problems (SP-0) don't follow this cycle — they exist to make subsequent tests compilable. A stub handler that returns a zero value or error is infrastructure, not implementation.\n\nExample ordering:\n```\nSP-0: [infra] Types + stub handler + code generation      (group A)\nSP-1: [test]  Happy-path test: Create with defaults        (group B, depends: SP-0) → RED\nSP-2: [impl]  Implement Create handler                     (group B, depends: SP-1) → GREEN\nSP-3: [test]  Error-path test: Non-admin gets Forbidden    (group C, depends: SP-2) → RED\nSP-4: [impl]  Implement RBAC check                         (group C, depends: SP-3) → GREEN\nSP-5: [test]  Delete cleans up namespace                   (group D, depends: SP-2) → RED\nSP-6: [impl]  Implement Delete with cleanup                (group D, depends: SP-5) → GREEN\n```\n\n## Size Calibration\n\nTarget: each sub-problem takes 15-60 minutes to implement.\n\n| Too small (merge up) | Right size | Too big (split) |\n|----------------------|------------|-----------------|\n| \"Add an import\" | \"Create resource with validation\" | \"Implement full CRUD + RBAC\" |\n| \"Check one field\" | \"Verify state transition\" | \"Add feature end-to-end\" |\n| \"Set a label\" | \"Non-admin gets Forbidden\" | \"Multi-cluster replication\" |\n\n## The SP-0 Pattern\n\nWhen multiple sub-problems need shared infrastructure (a new builder, a new label, a helper function), extract it as SP-0:\n\n```\nSP-0: [infra] Create setup builder for {resource}\n  - New file: e2e/setup/{resource}/{resource}.go\n  - Options: WithGenerateName, With{Feature}\n  - CRUD: Create, Delete\n  - Context accessors: LastFrom, From\n```\n\nThis unblocks all downstream sub-problems and is itself testable (the builder compiles and the resource can be created/deleted).\n\n### SP-0 in TDD: Stubs and Scaffolding\n\nIn a TDD workflow, SP-0 often includes **stub implementations** — handlers that compile and register but return zero values or errors. This is intentional:\n\n```\nSP-0: [infra] Define API types + stub handler\n  - New type: VirtualClusterInstanceFoo with +subresource-request marker\n  - Run code generation (deepcopy, clientset, register)\n  - Stub REST handler: returns empty object or \"not implemented\" error\n  - Register stub in register.go\n  - Result: tests can compile and call the endpoint, but get wrong/empty results\n```\n\nThe stub exists solely so the test in SP-1 can compile and fail for the *right reason* (wrong response, not compile error). The real implementation comes in SP-2.\n\n## Red Flags in Decomposition\n\n- **\"And\" in the title**: \"Create resource and verify permissions\" → split\n- **Multiple actors**: \"Admin creates, user views\" → split by actor\n- **Multiple resources**: \"Create project with VCI and space\" → split by resource\n- **Conditional behavior**: \"If config X then A, else B\" → split by input variation\n- **\"End-to-end\"**: Almost always too big. What specific behavior are you testing?\n",".claude/rules/e2e-conventions.md":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-conventions-core.md -->\n\n# e2e Test Conventions\n\nGinkgo v2 + Gomega, running against vCluster instances on Kind. Read existing tests in `e2e/test_*/` for patterns before writing new ones.\n\n## Key Conventions\n\n1. **Random suffixes** — Always use `objectmeta.GenerateName` or `random.RandomString(6)` for resource names to avoid collisions in parallel runs. Try to share the suffix across a single test spec so all resources of a single test use the same suffix. This applies to **all** identifiers that could collide, not just Kubernetes object names — Helm release names, database names, etc. If an identifier is a package-level `const` and would conflict across parallel runs, make it dynamic.\n2. **Context propagation** — Setup helpers store objects in context; retrieve with `<resource>.From(ctx, name)`.\n3. **Eventually for async** — vCluster operations are eventually consistent. Always wrap API checks in `Eventually` with `constants.*` timeouts. Never hardcode durations.\n4. **Labels for filtering** — Tag `Describe` with resource labels and `labels.PR` if all specs in it should gate PRs. Only tag individual `It` blocks with `labels.PR` when some specs in the `Describe` should NOT gate PRs. Never duplicate a label on an `It` that is already present on its enclosing `Describe` or `Context` — Ginkgo inherits labels, so duplicates are redundant noise.\n5. **Ordered contexts** — See the decision table below. Default to `BeforeEach`; only use `Ordered` for true sequential dependencies.\n6. **Package registration** — Test packages self-register via `var _ = Describe(...)`. The suite imports them as blank imports in `e2e_suite_test.go`.\n7. **Error assertion** — Prefer `ginkgo.Expect(...).To(ginkgo.Succeed())` over `ginkgo.Expect(...).NotTo(ginkgo.HaveOccurred())`\n8. **gstruct usage** — Do not use `gstruct` for simple field assertions (use `Expect(obj.Field).To(Equal(...))` instead). Use `gstruct` with `ContainElement` or similar matchers when asserting on elements within a collection (slice, map) to avoid manual loops.\n\n## Setup Helpers\n\nvCluster's `e2e/` does not yet have a rich `setup/` builder library like loft-enterprise. During migration:\n\n- Use existing **cluster definitions** from `e2e/clusters/` for test environment setup. Browse `clusters/clusters.go` for available vclusters with different configurations.\n- Use the `setup/template` package (`template.MustRender()`) for rendering vcluster YAML with image overrides.\n- When a setup pattern repeats across multiple tests, create a shared setup helper as an `[infra]` sub-problem. Place it in `e2e/setup/` following the functional-options pattern.\n- For now, most test setup is done inline using the Kubernetes client directly (create namespace, create resource, DeferCleanup).\n\n### Scoping Constants and Helpers\n\nTest-specific constants, helper functions, and option builders that are only used by a single `test_*` package belong **in that package**, not in the shared `constants` or `setup` packages. Only promote to a shared package when a second consumer appears.\n\n### External Service Provisioning\n\nIf the old test installs Helm charts or external services (via `values.yaml` `controlPlane.helmRelease`, host resources, or CI workflow steps), flag this as an `[infra]` sub-problem during migration. The external service needs to be provisioned self-contained within the test using `setuphelm.Upgrade()` in `BeforeAll` or via the cluster dependency system.\n\n## vCluster Configuration Patterns\n\nEach vCluster lives next to the `suite_*_test.go` that uses it. `HostCluster` (the kind host) is the only eagerly-provisioned dependency in `SynchronizedBeforeSuite`; every per-test vCluster is created lazily in the suite's own `BeforeAll` via `setup/lazyvcluster.LazyVCluster`, which is a thin wrapper over the framework's `vcluster.Create`.\n\n### Embedded YAML Templates\n\nEmbed the `vcluster.yaml` in the suite file. Templates support `{{.Repository}}`, `{{.Tag}}`, `{{.HostClusterName}}` placeholders - the lazy helper renders them at `BeforeAll` time and registers temp-file cleanup automatically.\n\n### Defining a New vCluster Suite\n\n1. Create `e2e/vcluster-myfeature.yaml` with the `vcluster.yaml` config.\n2. Create `e2e/suite_myfeature_test.go`:\n   ```go\n   //go:embed vcluster-myfeature.yaml\n   var myFeatureYAML string\n\n   const myFeatureName = \"myfeature-vcluster\"\n\n   func init() { suiteMyFeature() }\n\n   func suiteMyFeature() {\n       Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n           cluster.Use(clusters.HostCluster),\n           func() {\n               BeforeAll(func(ctx context.Context) context.Context {\n                   return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n               })\n               // spec functions...\n           },\n       )\n   }\n   ```\n3. If a new filter label is needed, add it to `labels/labels.go`. `labels.PR` goes on the outer suite (never on specs).\n4. If the vCluster needs host-side prerequisites (CRDs, PVCs, Helm install), pass `lazyvcluster.WithPreSetup(fn)`. Reusable helpers live in `setup/` (`setup.SnapshotPreSetup`, `setup.MetricsServerPreSetup`).\n\nDo NOT add entries to `clusters/` - that package only holds `HostCluster` and `DefaultVClusterOptions`.\n\n## Client Accessors\n\n| Accessor | Type | Use for |\n|----------|------|---------|\n| `cluster.KubeClientFrom(ctx, clusterName)` | `kubernetes.Interface` | Host cluster typed client |\n| `cluster.CurrentKubeClientFrom(ctx)` | `kubernetes.Interface` | Current vcluster typed client |\n| `cluster.CurrentClusterClientFrom(ctx)` | `client.Client` (controller-runtime) | Current cluster CR client |\n\n## Background Proxy Limitations\n\nThe suite-level background proxy (started by `vcluster connect` during `SynchronizedBeforeSuite`)\nis a **one-shot process**. It dies when:\n\n- The vcluster pod is paused/resumed (e.g., `certs rotate`, `certs rotate-ca`, helm upgrade)\n- The vcluster's CA certificate changes\n- The vcluster pod restarts for any reason\n\nAfter any of these, `cluster.CurrentKubeClientFrom(ctx)` and\n`cluster.CurrentClusterFrom(ctx).KubernetesRestConfig()` return connections to a dead proxy.\n\n**If your test does destructive vcluster operations** (cert rotation, restart, config change),\nyou must establish a fresh connection using the `connectVCluster()` pattern documented in\n`.claude/references/e2e-old-to-new-mapping.md` § \"Reconnecting After Destructive Operations\".\n\n**Common pitfall:** When building `ConnectOptions`, always set `BackgroundProxyImage` to\n`constants.GetVClusterImage()` — **never** use `DefaultBackgroundProxyImage(upgrade.GetVersion())`.\nIn dev builds the latter produces an invalid Docker image ref (empty tag), Docker rejects it,\nand the code silently falls back to in-process port-forwarding that hangs the test process.\n\n## Cleanup\n\nFor non-Ordered contexts, return the enriched context from `BeforeEach`:\n\n```go\nBeforeEach(func(ctx context.Context) context.Context {\n    vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n    Expect(vClusterClient).NotTo(BeNil())\n    return ctx\n})\n```\n\nInside `It` blocks, register cleanup immediately after creation:\n\n```go\nIt(\"tests something\", func(ctx context.Context) {\n    _, err := vClusterClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{\n        ObjectMeta: metav1.ObjectMeta{Name: nsName},\n    }, metav1.CreateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(func(ctx context.Context) {\n        err := vClusterClient.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n        Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n    })\n\n    // ... test logic\n})\n```\n\n## Labels\n\nAttach labels to `Describe`, `Context`, or `It`. Check `e2e/labels/labels.go` for the full list. Key ones:\n- `labels.PR` — gate tests that run on every PR\n- `labels.Core`, `labels.Sync`, `labels.Deploy` — feature-area labels\n\n## Ordered vs. BeforeEach\n\nDefault to `BeforeEach`. The suite will be parallelized — every unnecessary `Ordered` is a bottleneck.\n\n| Signal | Pattern | Why |\n|---|---|---|\n| Specs are independent, just share setup | `BeforeEach` (no `Ordered`) | Parallelizable; no cascade failures |\n| Specs form a lifecycle sequence (create → mutate → verify → delete) | `Ordered` + `BeforeAll` | Specs depend on prior spec's side effects |\n| Setup is expensive (databases, vclusters with deployed services) | `BeforeEach` — pay the cost per spec | Parallel execution recovers wall time; isolation prevents false failures |\n| A spec deletes or mutates the shared resource | `BeforeEach` with per-spec resource | Other specs can't rely on it existing |\n| Specs must run in a fixed order but don't share mutable state | **Rethink** — probably independent specs | `Ordered` without true dependencies is a parallelization bottleneck |\n\n> **Hard rule:** If you use `Ordered`, add a comment explaining which spec depends on which prior spec's side effect. If you can't name one, drop `Ordered`.\n\n## Concrete Examples\n\nSee `.claude/references/e2e-examples.md` for annotated excerpts from production tests covering: DeferCleanup placement, `Eventually` with `g Gomega`, `Ordered` vs `BeforeEach` side-by-side, and cluster client usage.\n\n## No Plan Artifacts in Code\n\nNever include migration plan identifiers (e.g., `SP-0`, `SP-1`, `[migrate]`, `[infra]`, `[cleanup]`, `[consolidate]`) in code comments, `By()` text, or test descriptions. These are internal planning artifacts — the code should read as if no plan ever existed.\n\n```go\n// FAIL — plan artifact leaked into comment\n// SP-2: verify configmap is synced to host\nBy(\"SP-2: Checking configmap sync\", func() { /* ... */ })\n\n// PASS — describes intent without plan references\n// Verify the configmap is synced to the host cluster\nBy(\"Checking that the configmap is synced to the host cluster\", func() { /* ... */ })\n```\n\n## New Test Checklist\n\n1. Place the file in the appropriate `e2e/test_*` directory. Package name must match sibling files.\n2. If creating a **new** `test_*` package, add a blank import in `e2e_suite_test.go`.\n3. If testing a **new** resource type, add a label constant in `e2e/labels/labels.go`.\n4. Use an existing test file in the same package as your starting template.\n",".claude/rules/e2e-error-handling.md":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-error-handling.md (identical) -->\n\n# e2e Error Handling\n\n## Cleanup: only tolerate IsNotFound, assert everything else\n\nSingle-operation cleanup:\n\n```go\n// GOOD\nDeferCleanup(func(ctx context.Context) {\n    err := client.Delete(ctx, name, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n```\n\nMulti-step cleanup (get → mutate → delete):\n\n```go\n// GOOD — check IsNotFound on Get, assert all other errors at every step\nDeferCleanup(func(ctx context.Context) {\n    obj, err := client.Get(ctx, name, metav1.GetOptions{})\n    if kerrors.IsNotFound(err) {\n        return // already gone\n    }\n    Expect(err).NotTo(HaveOccurred())\n\n    delete(obj.Annotations, someAnnotation)\n    _, err = client.Update(ctx, obj, metav1.UpdateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n\n    err = client.Delete(ctx, name, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n```\n\n**When NOT to use `IgnoreNotFound`:** `IgnoreNotFound` is appropriate for resources that may have been deleted by cascade, by a prior spec, or by a controller. For resources the test **just created in the same `It` block** and expects to still exist at cleanup time, prefer a strict assertion — `Expect(err).NotTo(HaveOccurred())`. A `NotFound` in this case signals a test bug or unexpected controller behavior, not a benign race.\n\nNever use these patterns:\n- `_, _ =` to discard errors — silent failures mask regressions and leak resources\n- `if err \\!= nil { return }` to bail on any error — this swallows connectivity/RBAC failures, not just NotFound\n\n## Error assertions: assert the specific error, not just occurrence\n\n```go\n// BAD — passes on connectivity errors, RBAC failures, or any unrelated error\nExpect(err).To(HaveOccurred())\n\n// GOOD\nExpect(err).To(MatchError(ContainSubstring(\"already exists\")))\n```\n",".claude/rules/e2e-quality-checklist.md":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core (items 1-8): e2e-tdd-workflow plugin references/e2e-quality-checklist-core.md -->\n\n# E2E Test Quality Checklist (e2e)\n\nEvery item is **pass/fail**. A test must pass all 10 items.\n\n---\n\n## 1. Cleanup Tolerates Already-Deleted Resources\n\nCleanup code must not fail if the resource was already removed (e.g., cascade-deleted\nby a parent). Use `client.IgnoreNotFound` or equivalent.\n\n```go\n// PASS\nDeferCleanup(func(ctx context.Context) {\n    err := client.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n\n// FAIL — hard-fails if resource is already gone\nDeferCleanup(func(ctx context.Context) {\n    Expect(client.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})).To(Succeed())\n})\n```\n\n---\n\n## 2. DeferCleanup Registered Before Subsequent Assertions\n\n`DeferCleanup` must be the very next statement after verifying creation succeeded.\nIf an assertion between creation and cleanup registration fails, the resource leaks\nand can block teardown of the entire suite.\n\n```go\n// PASS — cleanup registered immediately, before any further assertions\n_, err := vClusterClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})\nExpect(err).NotTo(HaveOccurred())\nDeferCleanup(func(ctx context.Context) {\n    err := vClusterClient.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n\n// now safe to assert further\nExpect(ns.Name).To(Equal(expectedName))\n\n// FAIL — assertion before cleanup registration; if it fails, namespace leaks\n_, err := vClusterClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})\nExpect(err).NotTo(HaveOccurred())\nExpect(ns.Name).To(Equal(expectedName))\nDeferCleanup(func(ctx context.Context) { /* ... */ })\n```\n\n---\n\n## 3. Timeout Constants\n\nUse the predefined constants. Never invent ad-hoc durations.\n\n| Constant | Duration | Use for |\n|----------|----------|---------|\n| `PollingTimeoutVeryShort` | 5s | Immediate state checks |\n| `PollingTimeoutShort` | 20s | Quick API operations |\n| `PollingTimeout` | 60s | Standard operations |\n| `PollingTimeoutLong` | 120s | Resource creation |\n| `PollingTimeoutVeryLong` | 300s | vCluster startup, cluster creation |\n\n---\n\n## 4. No Cluster-Scoped Singletons\n\nTests must not assume they are the only consumer of a cluster-scoped resource\n(e.g., a ClusterRole with a fixed name). If a cluster-scoped resource is needed,\nits name must include the suffix.\n\n---\n\n## 5. Resource Naming for Debuggability\n\n### 5.1 Single suffix per `Ordered` container\n\nOne call to `random.RandomString(6)`, stored in a variable, reused for all resource\nnames in the container. Do not generate a separate suffix per resource.\n\n```go\n// PASS — single suffix, all resources traceable\nBeforeAll(func(ctx context.Context) context.Context {\n    suffix := random.RandomString(6)\n    nsName = \"test-sync-\" + suffix\n    svcName = \"test-svc-\" + suffix\n    // ...\n})\n\n// FAIL — different suffix per resource, can't correlate\nBeforeAll(func(ctx context.Context) context.Context {\n    nsName = \"test-sync-\" + random.RandomString(6)\n    svcName = \"test-svc-\" + random.RandomString(6)\n    // ...\n})\n```\n\n### 5.2 Descriptive resource name prefixes\n\nThe prefix before the suffix should identify the test or resource purpose.\n\n```go\n// PASS\nnsName = \"svc-sync-test-\" + suffix\n\n// FAIL — generic prefix, hard to trace in cluster\nnsName = \"ns-\" + suffix\n```\n\n---\n\n## 6. Use the Lazy vCluster Pattern\n\nPer-test vClusters live in `suite_*_test.go` and are created lazily in the suite's `BeforeAll` via `setup/lazyvcluster.LazyVCluster`. The only eager cluster is `clusters.HostCluster` (the kind host).\n\n```go\n// PASS — lazy creation inside the suite\nDescribe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n    cluster.Use(clusters.HostCluster),\n    func() {\n        BeforeAll(func(ctx context.Context) context.Context {\n            return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n        })\n        // specs...\n    },\n)\n\n// FAIL — eager registration in clusters/ + cluster.Use(vc) is the old pattern; do not reintroduce it.\n```\n\nDo not add definitions to `clusters/`. Pre-setup hooks (CRD install, PVC, Helm) go into `setup/` and are passed via `lazyvcluster.WithPreSetup`. See `e2e/README.md` for the full pattern.\n\n---\n\n## 7. Test Setup Does Not Patch System-Managed State\n\n`BeforeEach` and `BeforeAll` must not directly set fields or conditions that a controller or deployment configuration would normally own. If setup code bypasses the system's real initialization path to establish a condition, the test is not exercising production behavior.\n\n**FAIL**: `BeforeEach` patches a vcluster status field that the syncer reconciles into existence.\n\n**PASS**: The vcluster is bootstrapped with the correct YAML configuration so the system establishes the expected state through its normal code path.\n\nIf you find yourself writing setup that patches system-managed state, stop. Read all existing tests that share the same labels — they show how the environment should be configured.\n\n---\n\n## 8. `By()` Text Style and Closure\n\n`By()` must use a closure (see `e2e-test-structure.md`), and the text should be a human-readable sentence fragment describing *what* is happening, not *how*.\n\n```go\n// PASS — closure + descriptive text\nBy(\"Waiting for the replicated service to appear in vcluster\", func() {\n    // ... test logic\n})\n\n// FAIL — no closure; Ginkgo can't attribute failures to the step\nBy(\"Waiting for the replicated service to appear in vcluster\")\n\n// FAIL — too terse, sounds like a function name\nBy(\"WaitServiceSync\", func() { /* ... */ })\n\n// FAIL — describes implementation, not intent\nBy(\"Calling vClusterClient.CoreV1().Services().Get in a loop\", func() { /* ... */ })\n```\n\n---\n\n## 9. Lint Passes\n\n`just -f Justfile.agent lint ./e2e/...` runs clean. This runs the full `golangci-lint` suite (formatting, static analysis, 28+ linters) matching CI. Note: `compile-check` only runs `go build` + `go vet`, which is a subset — always run `lint` as well.\n\n---\n\n## 10. No Hardcoded Cluster or Context Names\n\nNever hardcode kind cluster names or kubectl context strings. Use `constants.GetHostClusterName()`\nfor the cluster name and derive the context from it. The cluster name is configurable via the\n`KIND_NAME` env var and the `--cluster-name` flag.\n\n```go\n// PASS — derived from configurable constant\nhostCluster := constants.GetHostClusterName()\nkubectlContext := \"kind-\" + hostCluster\n\n// FAIL — hardcoded, breaks when KIND_NAME is set\nkubectlContext := \"kind-kind-cluster\"\n```\n",".claude/rules/e2e-test-structure.md":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-test-structure-core.md -->\n\n# e2e Test Structure Rules\n\n| Do | Don't |\n|---|---|\n| Register `DeferCleanup` immediately after creating a resource, before any assertions. If an assertion fails before cleanup is registered, the resource leaks. | Register cleanup after assertions — a failed assertion skips the rest of the block and the resource leaks forever. |\n| Add failure context to assertions inside `Eventually`: `g.Expect(phase).To(Equal(\"Ready\"), \"reason: %v, message: %v\", obj.Status.Reason, obj.Status.Message)` | Write `g.Expect(phase).To(Equal(\"Ready\"))` inside `Eventually` with no context — when it times out you get no diagnostics. |\n| Use `By(\"description\", func() { ... })` with a closure so Ginkgo reports which step failed and how long it took. | Use bare `By(\"description\")` followed by loose statements — Ginkgo can't attribute failures to the step. |\n| Default to `BeforeEach`. Only use `Ordered` for true sequential dependencies. | Use `Ordered` for convenience — every `Ordered` context is a parallelization bottleneck. |\n| Tolerate `NotFound` in cleanup, assert everything else. See `e2e-error-handling.md` for patterns. | Use `_, _ =` to swallow errors in `DeferCleanup` — silent failures mask regressions and leak resources. |\n| Clean up only the specific resources you created, by name or label selector. | `List` all resources of a type in cleanup — parallel tests create the same types and you'll delete their resources. |\n| Assert specific error messages: `Expect(err).To(MatchError(ContainSubstring(\"...\")))` or `kerrs.Be(metav1.StatusReason...)` | Write `Expect(err).To(HaveOccurred())` — this passes on unrelated failures like connectivity errors. |\n| Use existing cluster definitions from `clusters/` and `setup/template` for YAML rendering. Browse available definitions before writing any direct client call in test setup or cleanup. | Write one-off raw client calls when a cluster definition or template helper exists — this duplicates error handling, skips context tracking, and diverges from the standard patterns over time. |\n","vendor/github.com/google/gnostic-models/extensions/README.md":"# Extensions\n\n**Extension Support is experimental.**\n\nThis directory contains support code for building Gnostic extensio handlers and\nassociated examples.\n\nExtension handlers can be used to compile vendor or specification extensions\ninto protocol buffer structures.\n\nLike plugins, extension handlers are built as separate executables. Extension\nbodies are written to extension handlers as serialized\nExtensionHandlerRequests.\n",".claude/rules/scratchpad-programs.md":"---\nglobs: [\"**/*.go\"]\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/scratchpad-programs.md (identical) -->\n\n## Verifying assumptions with scratchpad programs\n\nWhen you need to verify an assumption — about API behavior, data shape, timing,\nRBAC, sync logic, or anything else — write a small standalone program in\n`.agent-scratchpad/` that directly answers the question.\n\nThis is not a failure-recovery technique. Use it proactively any time you're\nabout to write code based on an assumption you haven't confirmed.\n\n### Examples of when to use this\n\n- \"Does this CRD have a status subresource?\"\n- \"What does the syncer actually return for a conflicting resource name?\"\n- \"Does client-go's patch merge or replace this nested field?\"\n- \"What order do these finalizers run in?\"\n- \"Is this label selector format valid?\"\n\n### How\n\n1. Create a subfolder in `.agent-scratchpad/` named after the question\n   (e.g., `.agent-scratchpad/check-patch-merge/main.go`). Each program gets\n   its own folder — never put multiple programs in the same folder.\n\n2. Run it:\n   ```bash\n   cd .agent-scratchpad/check-patch-merge && go run main.go --kubeconfig=\"${HOME}/.kube/config\" --context=kind-kind-cluster\n   ```\n   If no cluster is needed (pure logic, parsing, transforms), skip the kubeconfig.\n\n3. Read the output, answer the question, delete the folder.\n\n### Guidelines\n\n| Do | Don't |\n|----|-------|\n| One folder, one file, one question | Build a mini test framework |\n| Name the folder after the question | Use generic names like `test1/` |\n| Print the answer and exit | Add flags for \"reuse later\" |\n| Use `go run` directly | Create a separate go.mod |\n| Delete the folder after use | Leave folders accumulating |\n"},"files":{".claude/references/e2e-examples.md":"# E2E Test Examples — Annotated Patterns\n\nConcrete excerpts from production tests in `e2e/`. These are ground truth — read them before writing new tests.\n\n---\n\n## 1. DeferCleanup Pattern\n\nRendered YAML cleanup and vCluster teardown are handled by `setup/lazyvcluster.LazyVCluster` - you do not register them by hand. `SynchronizedBeforeSuite` only provisions the host kind cluster now.\n\n```go\n// suite_myfeature_test.go — lazy helper owns YAML cleanup + vCluster teardown\nfunc suiteMyFeature() {\n    Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n        cluster.Use(clusters.HostCluster),\n        func() {\n            BeforeAll(func(ctx context.Context) context.Context {\n                return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n            })\n            // specs...\n        },\n    )\n}\n```\n\nInside `It` blocks - register cleanup immediately after resource creation, before any further assertions:\n\n```go\nBy(\"creating source namespace on host\", func() {\n    _, err := hostClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{\n        ObjectMeta: metav1.ObjectMeta{Name: fromNS},\n    }, metav1.CreateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(func(ctx context.Context) {\n        Expect(hostClient.CoreV1().Namespaces().Delete(ctx, fromNS, metav1.DeleteOptions{})).To(Succeed())\n        Eventually(func(g Gomega) {\n            _, err := hostClient.CoreV1().Namespaces().Get(ctx, fromNS, metav1.GetOptions{})\n            g.Expect(kerrors.IsNotFound(err)).To(BeTrue())\n        }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n    })\n})\n```\n\n---\n\n## 2. Eventually + Polling\n\nSource: `e2e/test_core/sync/test_servicesync.go`, `e2e/test_deploy/test_helm_charts.go`\n\nUse the `g Gomega` parameter inside `Eventually`. Bare `Expect()` inside `Eventually` panics instead of retrying. Include failure context for debuggability.\n\n**Waiting for a resource to appear:**\n\n```go\nvar toService *corev1.Service\nBy(\"waiting for the replicated service to appear in vcluster\", func() {\n    Eventually(func(g Gomega) {\n        toService, err = vClusterClient.CoreV1().Services(toNS).Get(ctx, toName, metav1.GetOptions{})\n        g.Expect(err).NotTo(HaveOccurred())\n    }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n})\n```\n\n**Waiting for a resource to be deleted:**\n\n```go\nBy(\"waiting for the replicated service to be removed from vcluster\", func() {\n    Eventually(func(g Gomega) {\n        _, err := vClusterClient.CoreV1().Services(toNS).Get(ctx, toName, metav1.GetOptions{})\n        g.Expect(kerrors.IsNotFound(err)).To(BeTrue(), \"replicated service should be deleted after source is gone\")\n    }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n})\n```\n\n**Checking deploy status with descriptive failure messages:**\n\n```go\nEventually(func(g Gomega) {\n    cm, err := vClusterClient.CoreV1().ConfigMaps(deploy.VClusterDeployConfigMapNamespace).\n        Get(ctx, deploy.VClusterDeployConfigMap, metav1.GetOptions{})\n    g.Expect(err).NotTo(HaveOccurred(), \"Deploy configmap should exist\")\n    status := deploy.ParseStatus(cm)\n    g.Expect(status.Charts).To(HaveLen(2), \"Should have 2 charts configured\")\n    for _, chart := range status.Charts {\n        g.Expect(chart.Phase).To(\n            Equal(string(deploy.StatusSuccess)),\n            fmt.Sprintf(\"Chart %s is not in Success phase, got phase=%s, reason=%s, message=%s\",\n                chart.Name, chart.Phase, chart.Reason, chart.Message))\n    }\n}).\n    WithPolling(constants.PollingInterval).\n    WithTimeout(constants.PollingTimeout).\n    Should(Succeed(), \"Both charts should be successfully deployed\")\n```\n\n**Returning a value from Eventually for direct assertion:**\n\n```go\nEventually(func(g Gomega) []appsv1.Deployment {\n    deployList, err := vClusterClient.AppsV1().Deployments(ChartOCINamespace).List(ctx, metav1.ListOptions{\n        LabelSelector: k8slabels.SelectorFromSet(HelmOCIDeploymentLabels).String(),\n    })\n    g.Expect(err).NotTo(HaveOccurred(), \"Should be able to list deployments\")\n    return deployList.Items\n}).\n    WithPolling(constants.PollingInterval).\n    WithTimeout(constants.PollingTimeout).\n    Should(HaveLen(1), \"Should have exactly one fluent-bit deployment\")\n```\n\n---\n\n## 3. Cluster Client Usage\n\nSource: `e2e/test_core/sync/test_node.go`\n\nObtain host and vcluster clients from context. Use `cluster.KubeClientFrom` with the host cluster name for the host client, and `cluster.CurrentKubeClientFrom` for the current vcluster client.\n\n```go\nvar _ = Describe(\"Node sync\",\n    labels.Core,\n    labels.Sync,\n    cluster.Use(clusters.NodesVCluster),\n    cluster.Use(clusters.HostCluster),\n    func() {\n        var (\n            hostClient     kubernetes.Interface\n            vClusterClient kubernetes.Interface\n        )\n\n        BeforeEach(func(ctx context.Context) {\n            hostClient = cluster.KubeClientFrom(ctx, constants.GetHostClusterName())\n            Expect(hostClient).NotTo(BeNil())\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Sync nodes using label selector\", func(ctx context.Context) {\n            hostname := constants.GetHostClusterName() + \"-control-plane\"\n            Eventually(func(g Gomega) {\n                hostNodes, err := hostClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"Failed to list host nodes\")\n\n                virtualNodes, err := vClusterClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"Failed to list virtual nodes\")\n                g.Expect(virtualNodes.Items).ToNot(BeEmpty(), \"Virtual nodes list should not be empty\")\n\n                // ... assertions comparing host and virtual nodes\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Node sync should work correctly\")\n        })\n    })\n```\n\n---\n\n## 4. Ordered Usage\n\nSource: `test/e2e/snapshot/snapshot.go` (old suite — pattern, not import paths)\n\nUse `Ordered` when specs form a **lifecycle sequence** where later specs depend on state produced by earlier specs, **and** shared setup is genuinely expensive (resource creation, deployment readiness, data provisioning). The snapshot-with-volumes test is the canonical example: `BeforeAll` provisions a namespace, a PVC with written data, and several Kubernetes resources, then each `It` block advances through create → verify → delete → restore → verify-restored.\n\n```go\nDescribe(\"controller-based snapshot with volumes\", Ordered, func() {\n    const (\n        controllerTestNamespaceName = \"volume-snapshots-test\"\n        snapshotPath                = \"container:///snapshot-data/\" + controllerTestNamespaceName + \".tar.gz\"\n        pvcToRestoreName            = \"test-pvc-restore\"\n        testFileName                = controllerTestNamespaceName + \".txt\"\n        pvcData                     = \"Hello \" + controllerTestNamespaceName\n    )\n\n    // BeforeAll — expensive: creates namespace, PVC with data written via a pod,\n    // plus Deployments, Services, ConfigMaps, and Secrets.\n    BeforeAll(func(ctx context.Context) {\n        f = framework.DefaultFramework\n        deployTestNamespace(controllerTestNamespaceName)\n        // Creates PVC, spins up a writer pod, waits for pod readiness, writes data\n        createPVCWithData(ctx, f.VClusterClient, controllerTestNamespaceName,\n            pvcToRestoreName, testFileName, pvcData)\n        // Creates Deployment (nginx), Service, ConfigMap, Secret — waits for deploy ready\n        deployTestResources(controllerTestNamespaceName, true)\n    })\n\n    // Spec 1: triggers snapshot creation — later specs verify the snapshot exists\n    It(\"Creates the snapshot request\", func() {\n        createSnapshot(f, true, snapshotPath, true)\n    })\n\n    // Spec 2: depends on spec 1 — polls until snapshot completes\n    It(\"Creates the snapshot\", func(ctx context.Context) {\n        waitForSnapshotToBeCreated(ctx, f)\n    })\n\n    // Spec 3: depends on spec 2 — verifies VolumeSnapshot resources were cleaned up\n    It(\"Doesn't contain VolumeSnapshot and VolumeSnapshotContent\", func(ctx context.Context) {\n        // ... verifies cleanup of temporary snapshot objects\n    })\n\n    // Spec 4: depends on spec 2 — deletes PVC so restore can recreate it\n    It(\"Deletes the PVC with test data\", func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n    })\n\n    // checkTestResources embeds two It blocks:\n    //   \"Verify if only the resources in snapshot are available after restore\"\n    //   \"Verify if deleted resources are recreated after restore\"\n    // Both depend on prior specs having created and snapshotted resources.\n    checkTestResources(controllerTestNamespaceName, true, snapshotPath)\n\n    // Spec 7: depends on PVC being restored (without data) by checkTestResources\n    It(\"restores vCluster with volumes\", func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n        restoreVCluster(ctx, f, snapshotPath, true, true)\n    })\n\n    // Spec 8: depends on spec 7 — verifies PVC is bound after volume restore\n    It(\"has the restored PVC which is bound\", func(ctx context.Context) {\n        Eventually(func(g Gomega, ctx context.Context) corev1.PersistentVolumeClaimPhase {\n            restoredPVC, err := f.VClusterClient.CoreV1().PersistentVolumeClaims(\n                controllerTestNamespaceName).Get(ctx, pvcToRestoreName, metav1.GetOptions{})\n            g.Expect(err).NotTo(HaveOccurred())\n            return restoredPVC.Status.Phase\n        }).WithContext(ctx).\n            WithPolling(framework.PollInterval).\n            WithTimeout(framework.PollTimeoutLong).\n            Should(Equal(corev1.ClaimBound))\n    })\n\n    // Spec 9: depends on spec 8 — verifies data survived the snapshot/restore cycle\n    It(\"has the restored PVC with data from the volume snapshot\", func(ctx context.Context) {\n        checkPVCData(ctx, f.VClusterClient, controllerTestNamespaceName,\n            pvcToRestoreName, testFileName, pvcData)\n    })\n\n    AfterAll(func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n        cleanUpTestResources(ctx, true, controllerTestNamespaceName)\n    })\n})\n```\n\n**Why `Ordered` is justified here:** Each spec advances a lifecycle (provision → snapshot → cleanup → restore → verify) where later specs depend on side effects from earlier ones. The `BeforeAll` is genuinely expensive — it creates a namespace, provisions a PVC, spins up a writer pod to populate data, and deploys multiple resources with readiness checks. Contrast with the `BeforeEach` examples below, where specs are independent and only need client references.\n\n---\n\n## 5. BeforeEach Pattern\n\nSource: `e2e/test_deploy/test_helm_charts.go`, `e2e/test_deploy/test_init_manifests.go`\n\nUse `BeforeEach` when specs are independent and only need shared client setup. Each spec operates on pre-existing state (deployed charts, init manifests) and doesn't modify shared resources.\n\n```go\nvar _ = Describe(\"Helm charts (regular and OCI) are synced and applied as expected\",\n    labels.Deploy,\n    cluster.Use(clusters.HelmChartsVCluster),\n    func() {\n        var vClusterClient kubernetes.Interface\n\n        BeforeEach(func(ctx context.Context) {\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Test if configmap for both charts gets applied\", func(ctx context.Context) {\n            Eventually(func(g Gomega) {\n                // ... check deploy configmap status\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Both charts should be successfully deployed\")\n        })\n\n        It(\"Test nginx release secret existence in vcluster (regular chart)\", func(ctx context.Context) {\n            // ... independent verification\n        })\n    })\n```\n\n**BeforeEach returning context** (from `test_init_manifests.go`):\n\n```go\nvar _ = Describe(\"Init manifests are synced and applied as expected\",\n    labels.Deploy,\n    cluster.Use(clusters.InitManifestsVCluster),\n    func() {\n        var (\n            vClusterName   = clusters.InitManifestsVClusterName\n            vClusterClient kubernetes.Interface\n        )\n\n        BeforeEach(func(ctx context.Context) {\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Test if manifest template is synced with the vcluster\", func(ctx context.Context) {\n            Eventually(func(g Gomega) {\n                manifest, err := vClusterClient.CoreV1().ConfigMaps(TestManifestNamespace).\n                    Get(ctx, TestManifestName2, metav1.GetOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"ConfigMap should exist\")\n                g.Expect(manifest.Data[\"foo\"]).To(Equal(vClusterName),\n                    \"ConfigMap foo value should equal vcluster name\")\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Manifest template should be synced\")\n        })\n    },\n)\n```\n",".claude/references/e2e-old-to-new-mapping.md":"# Old → New Pattern Mapping (vcluster)\n\nTranslation table for migrating tests from `test/framework` to `e2e/`.\n\n## Client Accessors\n\n| Old Pattern (`test/framework`) | New Pattern (`e2e`) |\n|---|---|\n| `framework.DefaultFramework` global singleton | Context-based: `cluster.KubeClientFrom(ctx, name)`, `cluster.CurrentKubeClientFrom(ctx)` |\n| `f.HostClient` / `f.HostCRClient` | `cluster.KubeClientFrom(ctx, constants.GetHostClusterName())` |\n| `f.VClusterClient` / `f.VClusterCRClient` | `cluster.CurrentKubeClientFrom(ctx)` / `cluster.CurrentClusterClientFrom(ctx)` |\n| `f.VClusterName` / `f.VClusterNamespace` | Derived from cluster definition name in `clusters/` (e.g., `clusters.ServiceSyncVClusterName`) |\n| `os.Getenv(\"VCLUSTER_NAME\")` etc. | Cluster definitions with typed config in `constants/` |\n| `f.Suffix` / `translate.VClusterName` | `translate` package used directly where needed |\n\n## Setup and Lifecycle\n\n| Old Pattern | New Pattern |\n|---|---|\n| `framework.CreateFramework(ctx)` in `BeforeSuite` | `SynchronizedBeforeSuite` with `clusters.HostCluster.Setup` + `clusters.XVCluster.Setup` |\n| `f.Cleanup()` in `AfterSuite` | `SynchronizedAfterSuite` with `clusters.HostCluster.Teardown` |\n| `f.RefreshVirtualClient()` | `connectVCluster()` — see \"Reconnecting after destructive operations\" below |\n| Per-suite `values.yaml` | Per-cluster embedded YAML in `clusters/*.go` with `template.MustRender()` |\n| Single vcluster per suite | Multiple vclusters with different configs (DefaultVCluster, NodesVCluster, etc.) |\n\n## Test Execution\n\n| Old Pattern | New Pattern |\n|---|---|\n| `go test -v -ginkgo.v` | `ginkgo --label-filter=\"...\" ./e2e` |\n| `time.Sleep()` | `Eventually().WithPolling().WithTimeout()` |\n| Ad-hoc durations | `constants.PollingTimeout*` constants |\n| Env-based config (`VCLUSTER_SUFFIX`, etc.) | Flag-based config (`--vcluster-image`, `--cluster-name`) |\n\n## Timeouts\n\n| Old Constant | New Constant | Duration |\n|---|---|---|\n| `framework.PollInterval` (5s) | `constants.PollingInterval` | 2s |\n| `framework.PollTimeout` (1min) | `constants.PollingTimeout` | 60s |\n| `framework.PollTimeoutLong` (2min) | `constants.PollingTimeoutLong` | 120s |\n| N/A | `constants.PollingTimeoutVeryShort` | 5s |\n| N/A | `constants.PollingTimeoutShort` | 20s |\n| N/A | `constants.PollingTimeoutVeryLong` | 300s |\n\n## Cluster Definitions\n\nThe old framework uses a single vcluster per suite. The new framework defines multiple vclusters in `e2e/clusters/clusters.go`:\n\n```go\n// Host cluster (Kind)\nclusters.HostCluster = cluster.Define(\n    cluster.WithName(constants.GetHostClusterName()),\n    cluster.WithProvider(kind.NewProvider()),\n    cluster.WithConfigFile(\"e2e-kind.config.yaml\"),\n)\n\n// Virtual clusters — each with specific config\nclusters.DefaultVCluster = vcluster.Define(\n    vcluster.WithName(\"default-vcluster\"),\n    vcluster.WithVClusterYAML(DefaultVClusterYAML),\n    vcluster.WithOptions(DefaultVClusterOptions...),\n    vcluster.WithDependencies(HostCluster),\n)\n```\n\nUse `cluster.Use(clusters.XVCluster)` in `Describe` to wire a test to a specific cluster:\n\n```go\nvar _ = Describe(\"My test\",\n    labels.Core,\n    cluster.Use(clusters.DefaultVCluster),\n    cluster.Use(clusters.HostCluster),\n    func() { ... },\n)\n```\n\n## Reconnecting After Destructive Operations\n\nThe suite-level background proxy (established in `SynchronizedBeforeSuite`) does **not** survive\nvcluster pause/resume or cert rotation. `cluster.CurrentKubeClientFrom(ctx)` and\n`cluster.CurrentClusterFrom(ctx).KubernetesRestConfig()` both point to the suite proxy — if the\nvcluster has been paused, restarted, or had its certs rotated, these return dead connections.\n\nThe old framework's `f.RefreshVirtualClient()` re-established the connection. The e2e\nequivalent is to run `vcluster connect` programmatically with a background proxy:\n\n1. Create a temp file for the kubeconfig\n2. Build a `connectcmd.ConnectCmd` with `BackgroundProxy: true`, `KubeConfig` pointing to the temp file, and `BackgroundProxyImage` set to `constants.GetVClusterImage()` — the same image already loaded into Kind\n3. Call `cmd.Run(ctx, []string{vclusterName})`\n4. Poll with `Eventually` until the kubeconfig file is non-empty and `clientcmd.RESTConfigFromKubeConfig` succeeds\n5. Verify the connection by making a lightweight API call (e.g., `Get` the `default` ServiceAccount)\n6. Return the `*rest.Config` and a cleanup func that removes the temp file\n\n**Critical: use `constants.GetVClusterImage()` for `BackgroundProxyImage`**, not\n`vclusterconstants.DefaultBackgroundProxyImage(upgrade.GetVersion())`.\n\nKey imports: `connectcmd \"github.com/loft-sh/vcluster/cmd/vclusterctl/cmd\"`,\n`\"github.com/loft-sh/vcluster/pkg/cli\"`, `\"github.com/loft-sh/vcluster/pkg/cli/flags\"`,\n`loftlog \"github.com/loft-sh/log\"`,\n`\"github.com/spf13/cobra\"`, `\"k8s.io/client-go/tools/clientcmd\"`,\n`\"github.com/loft-sh/vcluster/e2e/constants\"`.\n",".claude/skills/e2e-migration-validator/SKILL.md":"---\nname: e2e-migration-validator\ndescription: >\n  Use this skill whenever a user wants to validate, review, or audit e2e tests that have been\n  migrated from an old framework (typically in a `e2e/` directory) to the new `e2e/`\n  framework. Triggers include: \"review my migrated test\", \"validate this e2e test\", \"check\n  my test migration\", \"did I migrate this correctly\", \"review e2e PR\", \"check test coverage\n  after migration\", or any request involving comparing old vs new e2e test files across the\n  vcluster, vcluster-pro, or loft-enterprise repos. Always use this skill when an e2e test\n  file or PR from one of these repos is provided for review — even if the user just says\n  \"take a look at this test\".\n---\n\n# E2E Test Migration Validator\n\nA skill for exhaustively reviewing e2e tests migrated from the old framework (`next/` directory)\nto the new framework (`e2e/` directory) across the `vcluster`, `vcluster-pro`, and\n`loft-enterprise` repositories.\n\n---\n\n## Inputs\n\nThe user will provide one or more of:\n- **Uploaded file(s)**: The new migrated test file, and optionally the original test file\n- **GitHub PR link**: A pull request URL containing the migrated test(s)\n- **Pasted code**: Snippets of old and/or new test code inline in the chat\n\nIf only the new test is provided (no original), you must locate the original yourself using\nthe local repo tools (see [Repo Access](#repo-access)).\n\n---\n\n## Repo Access\n\nUse the local repo MCP tools to inspect implementation details and locate source files.\nAll three repos may be relevant depending on the test subject:\n\n| Repo | Use when |\n|------|----------|\n| `vcluster` | Core vcluster functionality, open-source features |\n| `vcluster-pro` | Pro/enterprise vcluster features |\n| `loft-enterprise` | Loft platform, multi-tenancy, UI-level features |\n\nWhen reviewing a test, **always look up the actual implementation** being tested in the\nrelevant repo before drawing conclusions about correctness or coverage. Do not rely solely\non what the test itself asserts.\n\n---\n\n## Review Workflow\n\nFollow these steps in order for every migration review:\n\n### Step 1 — Locate the Original Test\n\nIf the original (`next/`) test was not provided:\n1. Search the appropriate repo(s) for the original test file by name or describe pattern\n2. Read it fully before proceeding\n3. Note the framework, helper utilities, and assertion style used\n\n### Step 2 — Locate the Implementation Under Test\n\nUsing the repo MCP tools:\n1. Find the source code that the test is exercising (controllers, handlers, CLI commands, API routes, UI components, etc.)\n2. Read the implementation thoroughly — **line by line** if needed\n3. Build a mental model of:\n   - All code paths (happy path, error paths, edge cases)\n   - All configurable inputs / flags / options\n   - Side effects and state mutations\n   - Interactions with other subsystems\n\n### Step 3 — Framework Migration Check\n\nVerify the structural correctness of the migration:\n\n- [ ] Test file is in the correct `e2e/` directory structure\n- [ ] Imports use the new framework's helpers/fixtures (not the old ones)\n- [ ] Test lifecycle hooks (`beforeAll`, `afterAll`, `beforeEach`, `afterEach`) are correctly\n      translated — no leftover old-framework teardown patterns\n- [ ] Setup and teardown create/delete resources correctly and completely\n- [ ] Cluster/namespace/resource provisioning uses the new framework's APIs\n- [ ] Any shared utilities (custom matchers, wait helpers, retry logic) are migrated or\n      replaced with new equivalents — not just copied verbatim\n- [ ] Test isolation is maintained (tests do not share mutable state across runs)\n- [ ] Framework-specific APIs (e.g. `page`, `context`, `request`) are used correctly\n\n### Step 4 — Correctness Check\n\nFor each test case in the migrated file:\n\n1. **Does the test actually test what it claims to test?**\n   - Read the test name / description and compare against what is asserted\n   - Flag any tests where the name and assertions are misaligned\n2. **Are the assertions meaningful?**\n   - Look for trivially-passing assertions (e.g. `expect(x).toBeDefined()` with no deeper check)\n   - Look for assertions that would pass even if the feature is broken\n   - Look for missing negative assertions (e.g. testing that a resource *was* created but not\n     that an invalid input *was rejected*)\n3. **Is the test deterministic?**\n   - Flag race conditions, missing `await`, missing retries on flaky operations\n   - Flag hardcoded timeouts that may not hold in CI\n4. **Is the test environment realistic?**\n   - Does the test reflect how the feature is actually used in production?\n   - Are there unrealistic shortcuts (e.g. bypassing auth, using superuser where a normal user\n     should be tested)?\n\n### Step 5 — Exhaustive Coverage Analysis\n\nThis is the most important step. Compare the implementation (Step 2) against the test suite.\n\nFor **every distinct behavior, code path, and configurable option** in the implementation:\n\n1. Check whether it is covered by at least one test\n2. If not covered, classify the gap:\n   - 🔴 **Critical gap** — a regression here would not be caught at all\n   - 🟡 **Important gap** — partial coverage, but a significant edge case is missed\n   - 🟢 **Minor gap** — low-risk or already covered by unit tests\n\nFor each gap, suggest a concrete additional test case including:\n- What scenario it covers\n- What inputs/state are needed\n- What assertion(s) should be made\n\n**Common gap categories to check explicitly:**\n- Error handling and invalid inputs\n- Permission / RBAC boundary conditions\n- Resource limit enforcement\n- Concurrent operations\n- Feature flags / configuration variants\n- Upgrade/downgrade paths (if applicable)\n- Interaction with other features (cross-feature integration)\n- Cleanup/teardown correctness (does the feature leave no orphaned resources?)\n\n### Step 6 — Code Quality Review\n\n- Naming: are test and variable names descriptive and consistent with the new framework's conventions?\n- DRY: is there duplicated setup that should be extracted into a shared fixture or helper?\n- Readability: would a new engineer understand what is being tested and why?\n- Comments: are non-obvious steps explained?\n- Dead code: are there commented-out blocks, unused imports, or leftover debug statements?\n\n---\n\n## Output Format\n\nProduce **all three** of the following output sections:\n\n---\n\n### 1. Inline Code Analysis\n\nGo through the migrated test file and annotate specific lines or blocks with numbered\ncomments. Format as a fenced code block with inline `// [N] ...` annotations, followed by\na numbered legend below it explaining each annotation.\n\nExample:\n```typescript\n// [1] Missing: should also assert that the old resource was deleted\nawait expect(newResource).toBeVisible();\n\n// [2] Race condition: no retry/wait before asserting pod readiness\nexpect(pod.status).toBe('Running');\n```\n\n**Legend:**\n1. 🔴 Critical gap — deletion is not verified; the old resource could persist undetected\n2. 🟡 Flakiness risk — pod status may not be `Running` immediately; use `waitForCondition`\n\n---\n\n### 2. Checklist Report\n\n```\n## Migration Checklist — <test file name>\n\n### Framework Migration\n- [x] Correct directory structure\n- [x] New framework imports\n- [ ] ⚠️  Teardown uses old-framework pattern on line 42\n\n### Correctness\n- [x] Assertions are meaningful\n- [ ] ⚠️  Test \"should reject invalid config\" passes even with valid config (line 87)\n\n### Coverage Gaps\n- 🔴 No test for RBAC: non-admin user attempting privileged operation\n- 🟡 No test for concurrent resource creation\n- 🟢 No test for audit log output (low risk)\n\n### Code Quality\n- [ ] ⚠️  Duplicate setup block (lines 12–25 and 61–74) — extract to beforeEach\n- [x] Naming is clear and consistent\n```\n\n---\n\n### 3. PR Review Comment\n\nA copy-paste-ready GitHub PR review comment in Markdown. Structure:\n\n```\n## E2E Migration Review\n\n**Overall:** ✅ Approved with suggestions / ⚠️ Changes requested / ❌ Major issues found\n\n### Summary\n<2–3 sentence high-level assessment>\n\n### Issues\n\n#### 🔴 Must Fix\n- ...\n\n#### 🟡 Should Fix\n- ...\n\n#### 🟢 Nice to Have\n- ...\n\n### Suggested Additional Tests\n- **Test:** `<descriptive test name>`\n  **Scenario:** <what to set up>\n  **Assert:** <what to check>\n\n<repeat for each suggested test>\n\n### Nits\n- ...\n```\n\n---\n\n## Framework Notes\n\n> **For the reviewer using this skill:** If you know the specific old and new frameworks\n> involved in this migration, add them here so Claude can apply framework-specific checks.\n>\n> Example entries:\n> - Old: `@vcluster/e2e` custom test runner → New: Playwright + `@vcluster/e2e-next` fixtures\n> - Old: Ginkgo/Gomega (Go) → New: Playwright (TypeScript)\n> - Old: Custom shell-script harness → New: Playwright with kubectl helpers\n>\n> Until this section is filled in, Claude will infer the frameworks from the provided files.\n\n---\n\n## Tips for High-Quality Reviews\n\n- **Always read the implementation first.** A test that looks complete may miss half the\n  code paths if you haven't read the source.\n- **Think adversarially.** For each assertion, ask: \"How could this pass even if the feature\n  is broken?\"\n- **Don't skip cleanup verification.** Many e2e bugs are leaked resources, not failed\n  assertions.\n- **Flag flakiness proactively.** A flaky test is worse than no test — it erodes trust in\n  the suite.\n- **Be specific in suggestions.** Don't say \"add more tests.\" Say exactly what scenario,\n  what inputs, and what assertion.\n","CLAUDE.md":"# vCluster — Agent Instructions\n\n## E2E TDD Workflow\n\nWhen doing test-driven development, ALWAYS follow the workflow in\n[.claude/e2e-tdd-workflow.md](.claude/e2e-tdd-workflow.md).\nUse the AGENT_SESSION env var to isolate your commands per session.\n\n## CI / GitHub Actions\n\nShared CI logic lives in [`loft-sh/github-actions`](https://github.com/loft-sh/github-actions);\nproduction repos reuse those actions rather than inlining shell. The\n`github-actions-developer` skill (from `vcluster-skills-engineering`) auto-loads\nwhen you touch `.github/` and carries the full convention.\n",".claude/references/e2e-framework-suite-deps.md":"# e2e-framework: External Infrastructure Patterns\n\nTwo distinct patterns depending on what needs provisioning. Pick the right one before writing the plan.\n\n## Service-level provisioning (Helm charts, external APIs)\n\nUse this for: ingress-nginx, fluent-bit, metrics-server, cert-manager, CSI drivers, or any service installed into the test cluster.\n\nInstall in `BeforeAll`, tear down in `AfterAll`. Never assume the service is pre-provisioned.\n\n```go\nBeforeAll(func(ctx context.Context) context.Context {\n    By(\"installing <service>\")\n    ctx, err := setuphelm.Upgrade(\n        setuphelm.WithHelmOptions(\n            constants.Get<Service>HelmOptions()...,\n            helm.WithWait(),\n        ),\n    )(ctx)\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(...)\n    return ctx\n})\n\nAfterAll(func(ctx context.Context) {\n    _, err := setuphelm.Uninstall(\n        setuphelm.WithHelmOptions(\n            helm.WithName(\"<release>\"),\n            helm.WithNamespace(\"<ns>\"),\n        ),\n    )(ctx)\n    Expect(err).NotTo(HaveOccurred())\n    _, err = namespace.Delete(\"<ns>\")(ctx)\n    Expect(err).NotTo(HaveOccurred())\n})\n```\n\n**Why `AfterAll` and not `DeferCleanup`**: `DeferCleanup` in `BeforeAll` runs after all specs, which is equivalent here — but `AfterAll` is explicit and survives `--setup-only` runs where you want the service to persist between dev iterations.\n\n**Chart constants**: Add a `e2e/constants/<service>.go` with `Get<Service>HelmOptions()` following the pattern in existing constants files.\n\n---\n\n## Cluster-level provisioning (Kind host, vClusters)\n\nUse this for: tests that need a full ephemeral Kubernetes cluster or vCluster, not a service inside an existing cluster.\n\n**Two-layer model:**\n\n1. **Host kind cluster** - eagerly provisioned once in `SynchronizedBeforeSuite` via the framework's `cluster.Define` + `cluster.Setup`. Defined in `e2e/clusters/registry.go` as `clusters.HostCluster`. Reused by every per-test vCluster.\n2. **Per-test vClusters** - created lazily, one per `suite_*_test.go`, in the suite's outer `BeforeAll` via `setup/lazyvcluster.LazyVCluster`. The helper is a thin wrapper over the framework's `vcluster.Create` (see `github.com/loft-sh/e2e-framework/pkg/setup/vcluster`). Peak concurrent vClusters is bounded by `ginkgo --procs`, not by the number of suite files.\n\n```go\n// e2e/suite_myfeature_test.go\n//go:embed vcluster-myfeature.yaml\nvar myFeatureYAML string\n\nconst myFeatureName = \"myfeature-vcluster\"\n\nfunc init() { suiteMyFeature() }\n\nfunc suiteMyFeature() {\n    Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n        cluster.Use(clusters.HostCluster),\n        func() {\n            BeforeAll(func(ctx context.Context) context.Context {\n                return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML,\n                    // optional: host-side prerequisites (CRDs, PVCs, Helm install)\n                    lazyvcluster.WithPreSetup(setup.MyPrereq()),\n                )\n            })\n\n            // spec functions...\n        },\n    )\n}\n```\n\nInside specs: `cluster.CurrentKubeClientFrom(ctx)` resolves to the suite's vCluster. `cluster.KubeClientFrom(ctx, constants.GetHostClusterName())` resolves to the host.\n\n**Failure-aware teardown**: on spec failure the framework keeps the vCluster alive and attaches diagnostics (rendered config, pods, events, syncer logs) to the failing spec's report entries via `AddReportEntry`. On pass, teardown destroys the vCluster.\n\n**Focus is automatic**: a label-filtered run only materializes vClusters whose outer suite Describe matches. Unmatched suites' `BeforeAll` never fires, so no vCluster is spun up for tests that do not need it.\n\n---\n\n## Identifying what needs provisioning\n\nWhen migrating an old test, check these files in order:\n\n1. **Old test's `values.yaml`** — Look for `controlPlane.helmRelease` entries (charts the vcluster deploys internally), `experimental.deploy.vcluster.helm` sections, and any inline manifests in `experimental.deploy.vcluster.manifests`.\n2. **`host-resources.yaml`** (if present) — PVCs for volume snapshots, CRDs, or other host-cluster prerequisites.\n3. **`.github/workflows/e2e.yaml`** — CI workflow steps that install external dependencies before running the test suite.\n\n### Known External Dependencies by Suite\n\n| Dependency | Suites | Notes |\n|---|---|---|\n| CSI volume snapshots | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Snapshot CRDs + controller |\n| ingress-nginx 4.14.2 | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Ingress controller for service exposure tests |\n| fluent-bit 3.1.13 (OCI) | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Deployed as Helm chart inside vcluster |\n| metrics-server | e2e_metrics_proxy | Required for metrics API proxy tests |\n| Plugin images | e2e_plugin | Custom plugin container images |\n\n### Dependency-Free Suites\n\nThese suites have no external service dependencies:\n- `e2e_certs`\n- `e2e_cli`\n- `e2e_ha`\n- `e2e_limit_classes`\n- `e2e_pause_resume`\n\n**How to read the old test setup**: Look at the suite's CI workflow step. Each `helm install` or `kubectl apply` in the workflow's pre-test phase becomes either a `setuphelm.Upgrade()` call in `BeforeAll` or a cluster-level dependency via `cluster.Define`/`vcluster.Define`. The vcluster's `values.yaml` shows what config was baked into the vcluster itself — this becomes embedded YAML in `clusters/*.go`.\n",".claude/references/decomposition-heuristics.md":"# Decomposition Heuristics\n\n## The Core Test: \"Is This One It()?\"\n\nA sub-problem is ready when you can express it as:\n\n```go\nIt(\"should {one observable behavior}\", func(ctx context.Context) {\n    By(\"{action 1}\")\n    By(\"{action 2}\")\n    By(\"{verification}\")\n    // At most 3 By() steps\n})\n```\n\nIf you need more than 3 `By()` steps, the sub-problem is too big. Split it.\n\n## Splitting Strategies\n\n### Strategy 1: Split by CRUD Operation\n\nA feature that creates, reads, updates, and deletes becomes 4 sub-problems:\n\n```\nSP-1: Create {resource} with valid input\nSP-2: Get {resource} returns expected fields\nSP-3: Update {resource} changes specific field\nSP-4: Delete {resource} cleans up dependents\n```\n\n### Strategy 2: Split by Actor\n\nWhen multiple roles interact with a feature:\n\n```\nSP-1: Admin can create {resource}\nSP-2: Project member can view {resource}\nSP-3: Non-member gets Forbidden\n```\n\n### Strategy 3: Split by State Transition\n\nFor lifecycle features (sleep mode, VCI phases):\n\n```\nSP-1: Resource starts in {initial state}\nSP-2: Trigger moves resource to {intermediate state}\nSP-3: Resource reaches {final state}\nSP-4: Error condition produces {error state}\n```\n\n### Strategy 4: Split by Integration Boundary\n\nWhen a feature touches multiple systems:\n\n```\nSP-1: Management API accepts the request (API layer)\nSP-2: Controller reconciles the resource (controller layer)\nSP-3: Agent reflects the change (agent layer)\n```\n\n### Strategy 5: Split by Input Variation\n\nWhen behavior depends on configuration:\n\n```\nSP-1: Default configuration produces {default behavior}\nSP-2: Custom configuration A produces {behavior A}\nSP-3: Invalid configuration is rejected with {error}\n```\n\n## Merging Rules\n\nMerge two sub-problems if:\n- They always modify the same lines of code\n- One is meaningless without the other (e.g., \"create\" and \"verify creation\" are one sub-problem)\n- The combined test would still have <= 3 `By()` steps\n\n## Ordering Rules (TDD)\n\n1. **Infrastructure first**: Types, stubs, code generation, new `setup/` builders, new labels, new constants — everything needed for tests to *compile* (not pass)\n2. **Test before implementation**: For each behavior, the test sub-problem is ordered before the implementation sub-problem\n3. **Happy-path test before error-path test**: Prove it works, then prove it fails correctly\n4. **Independent before dependent**: SP-3 that depends on SP-1 comes after SP-1\n5. **Mark parallel groups**: Sub-problems with no dependencies get the same group letter\n\n### The TDD Cycle Pattern\n\nEach behavior produces a test→impl pair:\n```\nSP-N: [test] Write test for behavior X           → RED  (test fails, behavior missing)\nSP-M: [impl] Implement behavior X                → GREEN (test passes)\n```\n\nInfrastructure sub-problems (SP-0) don't follow this cycle — they exist to make subsequent tests compilable. A stub handler that returns a zero value or error is infrastructure, not implementation.\n\nExample ordering:\n```\nSP-0: [infra] Types + stub handler + code generation      (group A)\nSP-1: [test]  Happy-path test: Create with defaults        (group B, depends: SP-0) → RED\nSP-2: [impl]  Implement Create handler                     (group B, depends: SP-1) → GREEN\nSP-3: [test]  Error-path test: Non-admin gets Forbidden    (group C, depends: SP-2) → RED\nSP-4: [impl]  Implement RBAC check                         (group C, depends: SP-3) → GREEN\nSP-5: [test]  Delete cleans up namespace                   (group D, depends: SP-2) → RED\nSP-6: [impl]  Implement Delete with cleanup                (group D, depends: SP-5) → GREEN\n```\n\n## Size Calibration\n\nTarget: each sub-problem takes 15-60 minutes to implement.\n\n| Too small (merge up) | Right size | Too big (split) |\n|----------------------|------------|-----------------|\n| \"Add an import\" | \"Create resource with validation\" | \"Implement full CRUD + RBAC\" |\n| \"Check one field\" | \"Verify state transition\" | \"Add feature end-to-end\" |\n| \"Set a label\" | \"Non-admin gets Forbidden\" | \"Multi-cluster replication\" |\n\n## The SP-0 Pattern\n\nWhen multiple sub-problems need shared infrastructure (a new builder, a new label, a helper function), extract it as SP-0:\n\n```\nSP-0: [infra] Create setup builder for {resource}\n  - New file: e2e/setup/{resource}/{resource}.go\n  - Options: WithGenerateName, With{Feature}\n  - CRUD: Create, Delete\n  - Context accessors: LastFrom, From\n```\n\nThis unblocks all downstream sub-problems and is itself testable (the builder compiles and the resource can be created/deleted).\n\n### SP-0 in TDD: Stubs and Scaffolding\n\nIn a TDD workflow, SP-0 often includes **stub implementations** — handlers that compile and register but return zero values or errors. This is intentional:\n\n```\nSP-0: [infra] Define API types + stub handler\n  - New type: VirtualClusterInstanceFoo with +subresource-request marker\n  - Run code generation (deepcopy, clientset, register)\n  - Stub REST handler: returns empty object or \"not implemented\" error\n  - Register stub in register.go\n  - Result: tests can compile and call the endpoint, but get wrong/empty results\n```\n\nThe stub exists solely so the test in SP-1 can compile and fail for the *right reason* (wrong response, not compile error). The real implementation comes in SP-2.\n\n## Red Flags in Decomposition\n\n- **\"And\" in the title**: \"Create resource and verify permissions\" → split\n- **Multiple actors**: \"Admin creates, user views\" → split by actor\n- **Multiple resources**: \"Create project with VCI and space\" → split by resource\n- **Conditional behavior**: \"If config X then A, else B\" → split by input variation\n- **\"End-to-end\"**: Almost always too big. What specific behavior are you testing?\n",".claude/rules/e2e-conventions.md":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-conventions-core.md -->\n\n# e2e Test Conventions\n\nGinkgo v2 + Gomega, running against vCluster instances on Kind. Read existing tests in `e2e/test_*/` for patterns before writing new ones.\n\n## Key Conventions\n\n1. **Random suffixes** — Always use `objectmeta.GenerateName` or `random.RandomString(6)` for resource names to avoid collisions in parallel runs. Try to share the suffix across a single test spec so all resources of a single test use the same suffix. This applies to **all** identifiers that could collide, not just Kubernetes object names — Helm release names, database names, etc. If an identifier is a package-level `const` and would conflict across parallel runs, make it dynamic.\n2. **Context propagation** — Setup helpers store objects in context; retrieve with `<resource>.From(ctx, name)`.\n3. **Eventually for async** — vCluster operations are eventually consistent. Always wrap API checks in `Eventually` with `constants.*` timeouts. Never hardcode durations.\n4. **Labels for filtering** — Tag `Describe` with resource labels and `labels.PR` if all specs in it should gate PRs. Only tag individual `It` blocks with `labels.PR` when some specs in the `Describe` should NOT gate PRs. Never duplicate a label on an `It` that is already present on its enclosing `Describe` or `Context` — Ginkgo inherits labels, so duplicates are redundant noise.\n5. **Ordered contexts** — See the decision table below. Default to `BeforeEach`; only use `Ordered` for true sequential dependencies.\n6. **Package registration** — Test packages self-register via `var _ = Describe(...)`. The suite imports them as blank imports in `e2e_suite_test.go`.\n7. **Error assertion** — Prefer `ginkgo.Expect(...).To(ginkgo.Succeed())` over `ginkgo.Expect(...).NotTo(ginkgo.HaveOccurred())`\n8. **gstruct usage** — Do not use `gstruct` for simple field assertions (use `Expect(obj.Field).To(Equal(...))` instead). Use `gstruct` with `ContainElement` or similar matchers when asserting on elements within a collection (slice, map) to avoid manual loops.\n\n## Setup Helpers\n\nvCluster's `e2e/` does not yet have a rich `setup/` builder library like loft-enterprise. During migration:\n\n- Use existing **cluster definitions** from `e2e/clusters/` for test environment setup. Browse `clusters/clusters.go` for available vclusters with different configurations.\n- Use the `setup/template` package (`template.MustRender()`) for rendering vcluster YAML with image overrides.\n- When a setup pattern repeats across multiple tests, create a shared setup helper as an `[infra]` sub-problem. Place it in `e2e/setup/` following the functional-options pattern.\n- For now, most test setup is done inline using the Kubernetes client directly (create namespace, create resource, DeferCleanup).\n\n### Scoping Constants and Helpers\n\nTest-specific constants, helper functions, and option builders that are only used by a single `test_*` package belong **in that package**, not in the shared `constants` or `setup` packages. Only promote to a shared package when a second consumer appears.\n\n### External Service Provisioning\n\nIf the old test installs Helm charts or external services (via `values.yaml` `controlPlane.helmRelease`, host resources, or CI workflow steps), flag this as an `[infra]` sub-problem during migration. The external service needs to be provisioned self-contained within the test using `setuphelm.Upgrade()` in `BeforeAll` or via the cluster dependency system.\n\n## vCluster Configuration Patterns\n\nEach vCluster lives next to the `suite_*_test.go` that uses it. `HostCluster` (the kind host) is the only eagerly-provisioned dependency in `SynchronizedBeforeSuite`; every per-test vCluster is created lazily in the suite's own `BeforeAll` via `setup/lazyvcluster.LazyVCluster`, which is a thin wrapper over the framework's `vcluster.Create`.\n\n### Embedded YAML Templates\n\nEmbed the `vcluster.yaml` in the suite file. Templates support `{{.Repository}}`, `{{.Tag}}`, `{{.HostClusterName}}` placeholders - the lazy helper renders them at `BeforeAll` time and registers temp-file cleanup automatically.\n\n### Defining a New vCluster Suite\n\n1. Create `e2e/vcluster-myfeature.yaml` with the `vcluster.yaml` config.\n2. Create `e2e/suite_myfeature_test.go`:\n   ```go\n   //go:embed vcluster-myfeature.yaml\n   var myFeatureYAML string\n\n   const myFeatureName = \"myfeature-vcluster\"\n\n   func init() { suiteMyFeature() }\n\n   func suiteMyFeature() {\n       Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n           cluster.Use(clusters.HostCluster),\n           func() {\n               BeforeAll(func(ctx context.Context) context.Context {\n                   return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n               })\n               // spec functions...\n           },\n       )\n   }\n   ```\n3. If a new filter label is needed, add it to `labels/labels.go`. `labels.PR` goes on the outer suite (never on specs).\n4. If the vCluster needs host-side prerequisites (CRDs, PVCs, Helm install), pass `lazyvcluster.WithPreSetup(fn)`. Reusable helpers live in `setup/` (`setup.SnapshotPreSetup`, `setup.MetricsServerPreSetup`).\n\nDo NOT add entries to `clusters/` - that package only holds `HostCluster` and `DefaultVClusterOptions`.\n\n## Client Accessors\n\n| Accessor | Type | Use for |\n|----------|------|---------|\n| `cluster.KubeClientFrom(ctx, clusterName)` | `kubernetes.Interface` | Host cluster typed client |\n| `cluster.CurrentKubeClientFrom(ctx)` | `kubernetes.Interface` | Current vcluster typed client |\n| `cluster.CurrentClusterClientFrom(ctx)` | `client.Client` (controller-runtime) | Current cluster CR client |\n\n## Background Proxy Limitations\n\nThe suite-level background proxy (started by `vcluster connect` during `SynchronizedBeforeSuite`)\nis a **one-shot process**. It dies when:\n\n- The vcluster pod is paused/resumed (e.g., `certs rotate`, `certs rotate-ca`, helm upgrade)\n- The vcluster's CA certificate changes\n- The vcluster pod restarts for any reason\n\nAfter any of these, `cluster.CurrentKubeClientFrom(ctx)` and\n`cluster.CurrentClusterFrom(ctx).KubernetesRestConfig()` return connections to a dead proxy.\n\n**If your test does destructive vcluster operations** (cert rotation, restart, config change),\nyou must establish a fresh connection using the `connectVCluster()` pattern documented in\n`.claude/references/e2e-old-to-new-mapping.md` § \"Reconnecting After Destructive Operations\".\n\n**Common pitfall:** When building `ConnectOptions`, always set `BackgroundProxyImage` to\n`constants.GetVClusterImage()` — **never** use `DefaultBackgroundProxyImage(upgrade.GetVersion())`.\nIn dev builds the latter produces an invalid Docker image ref (empty tag), Docker rejects it,\nand the code silently falls back to in-process port-forwarding that hangs the test process.\n\n## Cleanup\n\nFor non-Ordered contexts, return the enriched context from `BeforeEach`:\n\n```go\nBeforeEach(func(ctx context.Context) context.Context {\n    vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n    Expect(vClusterClient).NotTo(BeNil())\n    return ctx\n})\n```\n\nInside `It` blocks, register cleanup immediately after creation:\n\n```go\nIt(\"tests something\", func(ctx context.Context) {\n    _, err := vClusterClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{\n        ObjectMeta: metav1.ObjectMeta{Name: nsName},\n    }, metav1.CreateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(func(ctx context.Context) {\n        err := vClusterClient.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n        Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n    })\n\n    // ... test logic\n})\n```\n\n## Labels\n\nAttach labels to `Describe`, `Context`, or `It`. Check `e2e/labels/labels.go` for the full list. Key ones:\n- `labels.PR` — gate tests that run on every PR\n- `labels.Core`, `labels.Sync`, `labels.Deploy` — feature-area labels\n\n## Ordered vs. BeforeEach\n\nDefault to `BeforeEach`. The suite will be parallelized — every unnecessary `Ordered` is a bottleneck.\n\n| Signal | Pattern | Why |\n|---|---|---|\n| Specs are independent, just share setup | `BeforeEach` (no `Ordered`) | Parallelizable; no cascade failures |\n| Specs form a lifecycle sequence (create → mutate → verify → delete) | `Ordered` + `BeforeAll` | Specs depend on prior spec's side effects |\n| Setup is expensive (databases, vclusters with deployed services) | `BeforeEach` — pay the cost per spec | Parallel execution recovers wall time; isolation prevents false failures |\n| A spec deletes or mutates the shared resource | `BeforeEach` with per-spec resource | Other specs can't rely on it existing |\n| Specs must run in a fixed order but don't share mutable state | **Rethink** — probably independent specs | `Ordered` without true dependencies is a parallelization bottleneck |\n\n> **Hard rule:** If you use `Ordered`, add a comment explaining which spec depends on which prior spec's side effect. If you can't name one, drop `Ordered`.\n\n## Concrete Examples\n\nSee `.claude/references/e2e-examples.md` for annotated excerpts from production tests covering: DeferCleanup placement, `Eventually` with `g Gomega`, `Ordered` vs `BeforeEach` side-by-side, and cluster client usage.\n\n## No Plan Artifacts in Code\n\nNever include migration plan identifiers (e.g., `SP-0`, `SP-1`, `[migrate]`, `[infra]`, `[cleanup]`, `[consolidate]`) in code comments, `By()` text, or test descriptions. These are internal planning artifacts — the code should read as if no plan ever existed.\n\n```go\n// FAIL — plan artifact leaked into comment\n// SP-2: verify configmap is synced to host\nBy(\"SP-2: Checking configmap sync\", func() { /* ... */ })\n\n// PASS — describes intent without plan references\n// Verify the configmap is synced to the host cluster\nBy(\"Checking that the configmap is synced to the host cluster\", func() { /* ... */ })\n```\n\n## New Test Checklist\n\n1. Place the file in the appropriate `e2e/test_*` directory. Package name must match sibling files.\n2. If creating a **new** `test_*` package, add a blank import in `e2e_suite_test.go`.\n3. If testing a **new** resource type, add a label constant in `e2e/labels/labels.go`.\n4. Use an existing test file in the same package as your starting template.\n",".claude/rules/e2e-error-handling.md":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-error-handling.md (identical) -->\n\n# e2e Error Handling\n\n## Cleanup: only tolerate IsNotFound, assert everything else\n\nSingle-operation cleanup:\n\n```go\n// GOOD\nDeferCleanup(func(ctx context.Context) {\n    err := client.Delete(ctx, name, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n```\n\nMulti-step cleanup (get → mutate → delete):\n\n```go\n// GOOD — check IsNotFound on Get, assert all other errors at every step\nDeferCleanup(func(ctx context.Context) {\n    obj, err := client.Get(ctx, name, metav1.GetOptions{})\n    if kerrors.IsNotFound(err) {\n        return // already gone\n    }\n    Expect(err).NotTo(HaveOccurred())\n\n    delete(obj.Annotations, someAnnotation)\n    _, err = client.Update(ctx, obj, metav1.UpdateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n\n    err = client.Delete(ctx, name, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n```\n\n**When NOT to use `IgnoreNotFound`:** `IgnoreNotFound` is appropriate for resources that may have been deleted by cascade, by a prior spec, or by a controller. For resources the test **just created in the same `It` block** and expects to still exist at cleanup time, prefer a strict assertion — `Expect(err).NotTo(HaveOccurred())`. A `NotFound` in this case signals a test bug or unexpected controller behavior, not a benign race.\n\nNever use these patterns:\n- `_, _ =` to discard errors — silent failures mask regressions and leak resources\n- `if err \\!= nil { return }` to bail on any error — this swallows connectivity/RBAC failures, not just NotFound\n\n## Error assertions: assert the specific error, not just occurrence\n\n```go\n// BAD — passes on connectivity errors, RBAC failures, or any unrelated error\nExpect(err).To(HaveOccurred())\n\n// GOOD\nExpect(err).To(MatchError(ContainSubstring(\"already exists\")))\n```\n",".claude/rules/e2e-quality-checklist.md":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core (items 1-8): e2e-tdd-workflow plugin references/e2e-quality-checklist-core.md -->\n\n# E2E Test Quality Checklist (e2e)\n\nEvery item is **pass/fail**. A test must pass all 10 items.\n\n---\n\n## 1. Cleanup Tolerates Already-Deleted Resources\n\nCleanup code must not fail if the resource was already removed (e.g., cascade-deleted\nby a parent). Use `client.IgnoreNotFound` or equivalent.\n\n```go\n// PASS\nDeferCleanup(func(ctx context.Context) {\n    err := client.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n\n// FAIL — hard-fails if resource is already gone\nDeferCleanup(func(ctx context.Context) {\n    Expect(client.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})).To(Succeed())\n})\n```\n\n---\n\n## 2. DeferCleanup Registered Before Subsequent Assertions\n\n`DeferCleanup` must be the very next statement after verifying creation succeeded.\nIf an assertion between creation and cleanup registration fails, the resource leaks\nand can block teardown of the entire suite.\n\n```go\n// PASS — cleanup registered immediately, before any further assertions\n_, err := vClusterClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})\nExpect(err).NotTo(HaveOccurred())\nDeferCleanup(func(ctx context.Context) {\n    err := vClusterClient.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n\n// now safe to assert further\nExpect(ns.Name).To(Equal(expectedName))\n\n// FAIL — assertion before cleanup registration; if it fails, namespace leaks\n_, err := vClusterClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})\nExpect(err).NotTo(HaveOccurred())\nExpect(ns.Name).To(Equal(expectedName))\nDeferCleanup(func(ctx context.Context) { /* ... */ })\n```\n\n---\n\n## 3. Timeout Constants\n\nUse the predefined constants. Never invent ad-hoc durations.\n\n| Constant | Duration | Use for |\n|----------|----------|---------|\n| `PollingTimeoutVeryShort` | 5s | Immediate state checks |\n| `PollingTimeoutShort` | 20s | Quick API operations |\n| `PollingTimeout` | 60s | Standard operations |\n| `PollingTimeoutLong` | 120s | Resource creation |\n| `PollingTimeoutVeryLong` | 300s | vCluster startup, cluster creation |\n\n---\n\n## 4. No Cluster-Scoped Singletons\n\nTests must not assume they are the only consumer of a cluster-scoped resource\n(e.g., a ClusterRole with a fixed name). If a cluster-scoped resource is needed,\nits name must include the suffix.\n\n---\n\n## 5. Resource Naming for Debuggability\n\n### 5.1 Single suffix per `Ordered` container\n\nOne call to `random.RandomString(6)`, stored in a variable, reused for all resource\nnames in the container. Do not generate a separate suffix per resource.\n\n```go\n// PASS — single suffix, all resources traceable\nBeforeAll(func(ctx context.Context) context.Context {\n    suffix := random.RandomString(6)\n    nsName = \"test-sync-\" + suffix\n    svcName = \"test-svc-\" + suffix\n    // ...\n})\n\n// FAIL — different suffix per resource, can't correlate\nBeforeAll(func(ctx context.Context) context.Context {\n    nsName = \"test-sync-\" + random.RandomString(6)\n    svcName = \"test-svc-\" + random.RandomString(6)\n    // ...\n})\n```\n\n### 5.2 Descriptive resource name prefixes\n\nThe prefix before the suffix should identify the test or resource purpose.\n\n```go\n// PASS\nnsName = \"svc-sync-test-\" + suffix\n\n// FAIL — generic prefix, hard to trace in cluster\nnsName = \"ns-\" + suffix\n```\n\n---\n\n## 6. Use the Lazy vCluster Pattern\n\nPer-test vClusters live in `suite_*_test.go` and are created lazily in the suite's `BeforeAll` via `setup/lazyvcluster.LazyVCluster`. The only eager cluster is `clusters.HostCluster` (the kind host).\n\n```go\n// PASS — lazy creation inside the suite\nDescribe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n    cluster.Use(clusters.HostCluster),\n    func() {\n        BeforeAll(func(ctx context.Context) context.Context {\n            return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n        })\n        // specs...\n    },\n)\n\n// FAIL — eager registration in clusters/ + cluster.Use(vc) is the old pattern; do not reintroduce it.\n```\n\nDo not add definitions to `clusters/`. Pre-setup hooks (CRD install, PVC, Helm) go into `setup/` and are passed via `lazyvcluster.WithPreSetup`. See `e2e/README.md` for the full pattern.\n\n---\n\n## 7. Test Setup Does Not Patch System-Managed State\n\n`BeforeEach` and `BeforeAll` must not directly set fields or conditions that a controller or deployment configuration would normally own. If setup code bypasses the system's real initialization path to establish a condition, the test is not exercising production behavior.\n\n**FAIL**: `BeforeEach` patches a vcluster status field that the syncer reconciles into existence.\n\n**PASS**: The vcluster is bootstrapped with the correct YAML configuration so the system establishes the expected state through its normal code path.\n\nIf you find yourself writing setup that patches system-managed state, stop. Read all existing tests that share the same labels — they show how the environment should be configured.\n\n---\n\n## 8. `By()` Text Style and Closure\n\n`By()` must use a closure (see `e2e-test-structure.md`), and the text should be a human-readable sentence fragment describing *what* is happening, not *how*.\n\n```go\n// PASS — closure + descriptive text\nBy(\"Waiting for the replicated service to appear in vcluster\", func() {\n    // ... test logic\n})\n\n// FAIL — no closure; Ginkgo can't attribute failures to the step\nBy(\"Waiting for the replicated service to appear in vcluster\")\n\n// FAIL — too terse, sounds like a function name\nBy(\"WaitServiceSync\", func() { /* ... */ })\n\n// FAIL — describes implementation, not intent\nBy(\"Calling vClusterClient.CoreV1().Services().Get in a loop\", func() { /* ... */ })\n```\n\n---\n\n## 9. Lint Passes\n\n`just -f Justfile.agent lint ./e2e/...` runs clean. This runs the full `golangci-lint` suite (formatting, static analysis, 28+ linters) matching CI. Note: `compile-check` only runs `go build` + `go vet`, which is a subset — always run `lint` as well.\n\n---\n\n## 10. No Hardcoded Cluster or Context Names\n\nNever hardcode kind cluster names or kubectl context strings. Use `constants.GetHostClusterName()`\nfor the cluster name and derive the context from it. The cluster name is configurable via the\n`KIND_NAME` env var and the `--cluster-name` flag.\n\n```go\n// PASS — derived from configurable constant\nhostCluster := constants.GetHostClusterName()\nkubectlContext := \"kind-\" + hostCluster\n\n// FAIL — hardcoded, breaks when KIND_NAME is set\nkubectlContext := \"kind-kind-cluster\"\n```\n",".claude/rules/e2e-test-structure.md":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-test-structure-core.md -->\n\n# e2e Test Structure Rules\n\n| Do | Don't |\n|---|---|\n| Register `DeferCleanup` immediately after creating a resource, before any assertions. If an assertion fails before cleanup is registered, the resource leaks. | Register cleanup after assertions — a failed assertion skips the rest of the block and the resource leaks forever. |\n| Add failure context to assertions inside `Eventually`: `g.Expect(phase).To(Equal(\"Ready\"), \"reason: %v, message: %v\", obj.Status.Reason, obj.Status.Message)` | Write `g.Expect(phase).To(Equal(\"Ready\"))` inside `Eventually` with no context — when it times out you get no diagnostics. |\n| Use `By(\"description\", func() { ... })` with a closure so Ginkgo reports which step failed and how long it took. | Use bare `By(\"description\")` followed by loose statements — Ginkgo can't attribute failures to the step. |\n| Default to `BeforeEach`. Only use `Ordered` for true sequential dependencies. | Use `Ordered` for convenience — every `Ordered` context is a parallelization bottleneck. |\n| Tolerate `NotFound` in cleanup, assert everything else. See `e2e-error-handling.md` for patterns. | Use `_, _ =` to swallow errors in `DeferCleanup` — silent failures mask regressions and leak resources. |\n| Clean up only the specific resources you created, by name or label selector. | `List` all resources of a type in cleanup — parallel tests create the same types and you'll delete their resources. |\n| Assert specific error messages: `Expect(err).To(MatchError(ContainSubstring(\"...\")))` or `kerrs.Be(metav1.StatusReason...)` | Write `Expect(err).To(HaveOccurred())` — this passes on unrelated failures like connectivity errors. |\n| Use existing cluster definitions from `clusters/` and `setup/template` for YAML rendering. Browse available definitions before writing any direct client call in test setup or cleanup. | Write one-off raw client calls when a cluster definition or template helper exists — this duplicates error handling, skips context tracking, and diverges from the standard patterns over time. |\n","vendor/github.com/google/gnostic-models/extensions/README.md":"# Extensions\n\n**Extension Support is experimental.**\n\nThis directory contains support code for building Gnostic extensio handlers and\nassociated examples.\n\nExtension handlers can be used to compile vendor or specification extensions\ninto protocol buffer structures.\n\nLike plugins, extension handlers are built as separate executables. Extension\nbodies are written to extension handlers as serialized\nExtensionHandlerRequests.\n",".claude/rules/scratchpad-programs.md":"---\nglobs: [\"**/*.go\"]\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/scratchpad-programs.md (identical) -->\n\n## Verifying assumptions with scratchpad programs\n\nWhen you need to verify an assumption — about API behavior, data shape, timing,\nRBAC, sync logic, or anything else — write a small standalone program in\n`.agent-scratchpad/` that directly answers the question.\n\nThis is not a failure-recovery technique. Use it proactively any time you're\nabout to write code based on an assumption you haven't confirmed.\n\n### Examples of when to use this\n\n- \"Does this CRD have a status subresource?\"\n- \"What does the syncer actually return for a conflicting resource name?\"\n- \"Does client-go's patch merge or replace this nested field?\"\n- \"What order do these finalizers run in?\"\n- \"Is this label selector format valid?\"\n\n### How\n\n1. Create a subfolder in `.agent-scratchpad/` named after the question\n   (e.g., `.agent-scratchpad/check-patch-merge/main.go`). Each program gets\n   its own folder — never put multiple programs in the same folder.\n\n2. Run it:\n   ```bash\n   cd .agent-scratchpad/check-patch-merge && go run main.go --kubeconfig=\"${HOME}/.kube/config\" --context=kind-kind-cluster\n   ```\n   If no cluster is needed (pure logic, parsing, transforms), skip the kubeconfig.\n\n3. Read the output, answer the question, delete the folder.\n\n### Guidelines\n\n| Do | Don't |\n|----|-------|\n| One folder, one file, one question | Build a mini test framework |\n| Name the folder after the question | Use generic names like `test1/` |\n| Print the answer and exit | Add flags for \"reuse later\" |\n| Use `go run` directly | Create a separate go.mod |\n| Delete the folder after use | Leave folders accumulating |\n"},"items":[{"name":"decomposition-heuristics.md","path":".claude/references/decomposition-heuristics.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/.claude/references/decomposition-heuristics.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Decomposition Heuristics\n\n## The Core Test: \"Is This One It()?\"\n\nA sub-problem is ready when you can express it as:\n\n```go\nIt(\"should {one observable behavior}\", func(ctx context.Context) {\n    By(\"{action 1}\")\n    By(\"{action 2}\")\n    By(\"{verification}\")\n    // At most 3 By() steps\n})\n```\n\nIf you need more than 3 `By()` steps, the sub-problem is too big. Split it.\n\n## Splitting Strategies\n\n### Strategy 1: Split by CRUD Operation\n\nA feature that creates, reads, updates, and deletes becomes 4 sub-problems:\n\n```\nSP-1: Create {resource} with valid input\nSP-2: Get {resource} returns expected fields\nSP-3: Update {resource} changes specific field\nSP-4: Delete {resource} cleans up dependents\n```\n\n### Strategy 2: Split by Actor\n\nWhen multiple roles interact with a feature:\n\n```\nSP-1: Admin can create {resource}\nSP-2: Project member can view {resource}\nSP-3: Non-member gets Forbidden\n```\n\n### Strategy 3: Split by State Transition\n\nFor lifecycle features (sleep mode, VCI phases):\n\n```\nSP-1: Resource starts in {initial state}\nSP-2: Trigger moves resource to {intermediate state}\nSP-3: Resource reaches {final state}\nSP-4: Error condition produces {error state}\n```\n\n### Strategy 4: Split by Integration Boundary\n\nWhen a feature touches multiple systems:\n\n```\nSP-1: Management API accepts the request (API layer)\nSP-2: Controller reconciles the resource (controller layer)\nSP-3: Agent reflects the change (agent layer)\n```\n\n### Strategy 5: Split by Input Variation\n\nWhen behavior depends on configuration:\n\n```\nSP-1: Default configuration produces {default behavior}\nSP-2: Custom configuration A produces {behavior A}\nSP-3: Invalid configuration is rejected with {error}\n```\n\n## Merging Rules\n\nMerge two sub-problems if:\n- They always modify the same lines of code\n- One is meaningless without the other (e.g., \"create\" and \"verify creation\" are one sub-problem)\n- The combined test would still have <= 3 `By()` steps\n\n## Ordering Rules (TDD)\n\n1. **Infrastructure first**: Types, stubs, code generation, new `setup/` builders, new labels, new constants — everything needed for tests to *compile* (not pass)\n2. **Test before implementation**: For each behavior, the test sub-problem is ordered before the implementation sub-problem\n3. **Happy-path test before error-path test**: Prove it works, then prove it fails correctly\n4. **Independent before dependent**: SP-3 that depends on SP-1 comes after SP-1\n5. **Mark parallel groups**: Sub-problems with no dependencies get the same group letter\n\n### The TDD Cycle Pattern\n\nEach behavior produces a test→impl pair:\n```\nSP-N: [test] Write test for behavior X           → RED  (test fails, behavior missing)\nSP-M: [impl] Implement behavior X                → GREEN (test passes)\n```\n\nInfrastructure sub-problems (SP-0) don't follow this cycle — they exist to make subsequent tests compilable. A stub handler that returns a zero value or error is infrastructure, not implementation.\n\nExample ordering:\n```\nSP-0: [infra] Types + stub handler + code generation      (group A)\nSP-1: [test]  Happy-path test: Create with defaults        (group B, depends: SP-0) → RED\nSP-2: [impl]  Implement Create handler                     (group B, depends: SP-1) → GREEN\nSP-3: [test]  Error-path test: Non-admin gets Forbidden    (group C, depends: SP-2) → RED\nSP-4: [impl]  Implement RBAC check                         (group C, depends: SP-3) → GREEN\nSP-5: [test]  Delete cleans up namespace                   (group D, depends: SP-2) → RED\nSP-6: [impl]  Implement Delete with cleanup                (group D, depends: SP-5) → GREEN\n```\n\n## Size Calibration\n\nTarget: each sub-problem takes 15-60 minutes to implement.\n\n| Too small (merge up) | Right size | Too big (split) |\n|----------------------|------------|-----------------|\n| \"Add an import\" | \"Create resource with validation\" | \"Implement full CRUD + RBAC\" |\n| \"Check one field\" | \"Verify state transition\" | \"Add feature end-to-end\" |\n| \"Set a label\" | \"Non-admin gets Forbidden\" | \"Multi-cluster replication\" |\n\n## The SP-0 Pattern\n\nWhen multiple sub-problems need shared infrastructure (a new builder, a new label, a helper function), extract it as SP-0:\n\n```\nSP-0: [infra] Create setup builder for {resource}\n  - New file: e2e/setup/{resource}/{resource}.go\n  - Options: WithGenerateName, With{Feature}\n  - CRUD: Create, Delete\n  - Context accessors: LastFrom, From\n```\n\nThis unblocks all downstream sub-problems and is itself testable (the builder compiles and the resource can be created/deleted).\n\n### SP-0 in TDD: Stubs and Scaffolding\n\nIn a TDD workflow, SP-0 often includes **stub implementations** — handlers that compile and register but return zero values or errors. This is intentional:\n\n```\nSP-0: [infra] Define API types + stub handler\n  - New type: VirtualClusterInstanceFoo with +subresource-request marker\n  - Run code generation (deepcopy, clientset, register)\n  - Stub REST handler: returns empty object or \"not implemented\" error\n  - Register stub in register.go\n  - Result: tests can compile and call the endpoint, but get wrong/empty results\n```\n\nThe stub exists solely so the test in SP-1 can compile and fail for the *right reason* (wrong response, not compile error). The real implementation comes in SP-2.\n\n## Red Flags in Decomposition\n\n- **\"And\" in the title**: \"Create resource and verify permissions\" → split\n- **Multiple actors**: \"Admin creates, user views\" → split by actor\n- **Multiple resources**: \"Create project with VCI and space\" → split by resource\n- **Conditional behavior**: \"If config X then A, else B\" → split by input variation\n- **\"End-to-end\"**: Almost always too big. What specific behavior are you testing?\n","isInternal":false,"tokens":1422,"sizeBytes":5687},{"name":"e2e-examples.md","path":".claude/references/e2e-examples.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/.claude/references/e2e-examples.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# E2E Test Examples — Annotated Patterns\n\nConcrete excerpts from production tests in `e2e/`. These are ground truth — read them before writing new tests.\n\n---\n\n## 1. DeferCleanup Pattern\n\nRendered YAML cleanup and vCluster teardown are handled by `setup/lazyvcluster.LazyVCluster` - you do not register them by hand. `SynchronizedBeforeSuite` only provisions the host kind cluster now.\n\n```go\n// suite_myfeature_test.go — lazy helper owns YAML cleanup + vCluster teardown\nfunc suiteMyFeature() {\n    Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n        cluster.Use(clusters.HostCluster),\n        func() {\n            BeforeAll(func(ctx context.Context) context.Context {\n                return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n            })\n            // specs...\n        },\n    )\n}\n```\n\nInside `It` blocks - register cleanup immediately after resource creation, before any further assertions:\n\n```go\nBy(\"creating source namespace on host\", func() {\n    _, err := hostClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{\n        ObjectMeta: metav1.ObjectMeta{Name: fromNS},\n    }, metav1.CreateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(func(ctx context.Context) {\n        Expect(hostClient.CoreV1().Namespaces().Delete(ctx, fromNS, metav1.DeleteOptions{})).To(Succeed())\n        Eventually(func(g Gomega) {\n            _, err := hostClient.CoreV1().Namespaces().Get(ctx, fromNS, metav1.GetOptions{})\n            g.Expect(kerrors.IsNotFound(err)).To(BeTrue())\n        }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n    })\n})\n```\n\n---\n\n## 2. Eventually + Polling\n\nSource: `e2e/test_core/sync/test_servicesync.go`, `e2e/test_deploy/test_helm_charts.go`\n\nUse the `g Gomega` parameter inside `Eventually`. Bare `Expect()` inside `Eventually` panics instead of retrying. Include failure context for debuggability.\n\n**Waiting for a resource to appear:**\n\n```go\nvar toService *corev1.Service\nBy(\"waiting for the replicated service to appear in vcluster\", func() {\n    Eventually(func(g Gomega) {\n        toService, err = vClusterClient.CoreV1().Services(toNS).Get(ctx, toName, metav1.GetOptions{})\n        g.Expect(err).NotTo(HaveOccurred())\n    }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n})\n```\n\n**Waiting for a resource to be deleted:**\n\n```go\nBy(\"waiting for the replicated service to be removed from vcluster\", func() {\n    Eventually(func(g Gomega) {\n        _, err := vClusterClient.CoreV1().Services(toNS).Get(ctx, toName, metav1.GetOptions{})\n        g.Expect(kerrors.IsNotFound(err)).To(BeTrue(), \"replicated service should be deleted after source is gone\")\n    }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n})\n```\n\n**Checking deploy status with descriptive failure messages:**\n\n```go\nEventually(func(g Gomega) {\n    cm, err := vClusterClient.CoreV1().ConfigMaps(deploy.VClusterDeployConfigMapNamespace).\n        Get(ctx, deploy.VClusterDeployConfigMap, metav1.GetOptions{})\n    g.Expect(err).NotTo(HaveOccurred(), \"Deploy configmap should exist\")\n    status := deploy.ParseStatus(cm)\n    g.Expect(status.Charts).To(HaveLen(2), \"Should have 2 charts configured\")\n    for _, chart := range status.Charts {\n        g.Expect(chart.Phase).To(\n            Equal(string(deploy.StatusSuccess)),\n            fmt.Sprintf(\"Chart %s is not in Success phase, got phase=%s, reason=%s, message=%s\",\n                chart.Name, chart.Phase, chart.Reason, chart.Message))\n    }\n}).\n    WithPolling(constants.PollingInterval).\n    WithTimeout(constants.PollingTimeout).\n    Should(Succeed(), \"Both charts should be successfully deployed\")\n```\n\n**Returning a value from Eventually for direct assertion:**\n\n```go\nEventually(func(g Gomega) []appsv1.Deployment {\n    deployList, err := vClusterClient.AppsV1().Deployments(ChartOCINamespace).List(ctx, metav1.ListOptions{\n        LabelSelector: k8slabels.SelectorFromSet(HelmOCIDeploymentLabels).String(),\n    })\n    g.Expect(err).NotTo(HaveOccurred(), \"Should be able to list deployments\")\n    return deployList.Items\n}).\n    WithPolling(constants.PollingInterval).\n    WithTimeout(constants.PollingTimeout).\n    Should(HaveLen(1), \"Should have exactly one fluent-bit deployment\")\n```\n\n---\n\n## 3. Cluster Client Usage\n\nSource: `e2e/test_core/sync/test_node.go`\n\nObtain host and vcluster clients from context. Use `cluster.KubeClientFrom` with the host cluster name for the host client, and `cluster.CurrentKubeClientFrom` for the current vcluster client.\n\n```go\nvar _ = Describe(\"Node sync\",\n    labels.Core,\n    labels.Sync,\n    cluster.Use(clusters.NodesVCluster),\n    cluster.Use(clusters.HostCluster),\n    func() {\n        var (\n            hostClient     kubernetes.Interface\n            vClusterClient kubernetes.Interface\n        )\n\n        BeforeEach(func(ctx context.Context) {\n            hostClient = cluster.KubeClientFrom(ctx, constants.GetHostClusterName())\n            Expect(hostClient).NotTo(BeNil())\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Sync nodes using label selector\", func(ctx context.Context) {\n            hostname := constants.GetHostClusterName() + \"-control-plane\"\n            Eventually(func(g Gomega) {\n                hostNodes, err := hostClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"Failed to list host nodes\")\n\n                virtualNodes, err := vClusterClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"Failed to list virtual nodes\")\n                g.Expect(virtualNodes.Items).ToNot(BeEmpty(), \"Virtual nodes list should not be empty\")\n\n                // ... assertions comparing host and virtual nodes\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Node sync should work correctly\")\n        })\n    })\n```\n\n---\n\n## 4. Ordered Usage\n\nSource: `test/e2e/snapshot/snapshot.go` (old suite — pattern, not import paths)\n\nUse `Ordered` when specs form a **lifecycle sequence** where later specs depend on state produced by earlier specs, **and** shared setup is genuinely expensive (resource creation, deployment readiness, data provisioning). The snapshot-with-volumes test is the canonical example: `BeforeAll` provisions a namespace, a PVC with written data, and several Kubernetes resources, then each `It` block advances through create → verify → delete → restore → verify-restored.\n\n```go\nDescribe(\"controller-based snapshot with volumes\", Ordered, func() {\n    const (\n        controllerTestNamespaceName = \"volume-snapshots-test\"\n        snapshotPath                = \"container:///snapshot-data/\" + controllerTestNamespaceName + \".tar.gz\"\n        pvcToRestoreName            = \"test-pvc-restore\"\n        testFileName                = controllerTestNamespaceName + \".txt\"\n        pvcData                     = \"Hello \" + controllerTestNamespaceName\n    )\n\n    // BeforeAll — expensive: creates namespace, PVC with data written via a pod,\n    // plus Deployments, Services, ConfigMaps, and Secrets.\n    BeforeAll(func(ctx context.Context) {\n        f = framework.DefaultFramework\n        deployTestNamespace(controllerTestNamespaceName)\n        // Creates PVC, spins up a writer pod, waits for pod readiness, writes data\n        createPVCWithData(ctx, f.VClusterClient, controllerTestNamespaceName,\n            pvcToRestoreName, testFileName, pvcData)\n        // Creates Deployment (nginx), Service, ConfigMap, Secret — waits for deploy ready\n        deployTestResources(controllerTestNamespaceName, true)\n    })\n\n    // Spec 1: triggers snapshot creation — later specs verify the snapshot exists\n    It(\"Creates the snapshot request\", func() {\n        createSnapshot(f, true, snapshotPath, true)\n    })\n\n    // Spec 2: depends on spec 1 — polls until snapshot completes\n    It(\"Creates the snapshot\", func(ctx context.Context) {\n        waitForSnapshotToBeCreated(ctx, f)\n    })\n\n    // Spec 3: depends on spec 2 — verifies VolumeSnapshot resources were cleaned up\n    It(\"Doesn't contain VolumeSnapshot and VolumeSnapshotContent\", func(ctx context.Context) {\n        // ... verifies cleanup of temporary snapshot objects\n    })\n\n    // Spec 4: depends on spec 2 — deletes PVC so restore can recreate it\n    It(\"Deletes the PVC with test data\", func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n    })\n\n    // checkTestResources embeds two It blocks:\n    //   \"Verify if only the resources in snapshot are available after restore\"\n    //   \"Verify if deleted resources are recreated after restore\"\n    // Both depend on prior specs having created and snapshotted resources.\n    checkTestResources(controllerTestNamespaceName, true, snapshotPath)\n\n    // Spec 7: depends on PVC being restored (without data) by checkTestResources\n    It(\"restores vCluster with volumes\", func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n        restoreVCluster(ctx, f, snapshotPath, true, true)\n    })\n\n    // Spec 8: depends on spec 7 — verifies PVC is bound after volume restore\n    It(\"has the restored PVC which is bound\", func(ctx context.Context) {\n        Eventually(func(g Gomega, ctx context.Context) corev1.PersistentVolumeClaimPhase {\n            restoredPVC, err := f.VClusterClient.CoreV1().PersistentVolumeClaims(\n                controllerTestNamespaceName).Get(ctx, pvcToRestoreName, metav1.GetOptions{})\n            g.Expect(err).NotTo(HaveOccurred())\n            return restoredPVC.Status.Phase\n        }).WithContext(ctx).\n            WithPolling(framework.PollInterval).\n            WithTimeout(framework.PollTimeoutLong).\n            Should(Equal(corev1.ClaimBound))\n    })\n\n    // Spec 9: depends on spec 8 — verifies data survived the snapshot/restore cycle\n    It(\"has the restored PVC with data from the volume snapshot\", func(ctx context.Context) {\n        checkPVCData(ctx, f.VClusterClient, controllerTestNamespaceName,\n            pvcToRestoreName, testFileName, pvcData)\n    })\n\n    AfterAll(func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n        cleanUpTestResources(ctx, true, controllerTestNamespaceName)\n    })\n})\n```\n\n**Why `Ordered` is justified here:** Each spec advances a lifecycle (provision → snapshot → cleanup → restore → verify) where later specs depend on side effects from earlier ones. The `BeforeAll` is genuinely expensive — it creates a namespace, provisions a PVC, spins up a writer pod to populate data, and deploys multiple resources with readiness checks. Contrast with the `BeforeEach` examples below, where specs are independent and only need client references.\n\n---\n\n## 5. BeforeEach Pattern\n\nSource: `e2e/test_deploy/test_helm_charts.go`, `e2e/test_deploy/test_init_manifests.go`\n\nUse `BeforeEach` when specs are independent and only need shared client setup. Each spec operates on pre-existing state (deployed charts, init manifests) and doesn't modify shared resources.\n\n```go\nvar _ = Describe(\"Helm charts (regular and OCI) are synced and applied as expected\",\n    labels.Deploy,\n    cluster.Use(clusters.HelmChartsVCluster),\n    func() {\n        var vClusterClient kubernetes.Interface\n\n        BeforeEach(func(ctx context.Context) {\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Test if configmap for both charts gets applied\", func(ctx context.Context) {\n            Eventually(func(g Gomega) {\n                // ... check deploy configmap status\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Both charts should be successfully deployed\")\n        })\n\n        It(\"Test nginx release secret existence in vcluster (regular chart)\", func(ctx context.Context) {\n            // ... independent verification\n        })\n    })\n```\n\n**BeforeEach returning context** (from `test_init_manifests.go`):\n\n```go\nvar _ = Describe(\"Init manifests are synced and applied as expected\",\n    labels.Deploy,\n    cluster.Use(clusters.InitManifestsVCluster),\n    func() {\n        var (\n            vClusterName   = clusters.InitManifestsVClusterName\n            vClusterClient kubernetes.Interface\n        )\n\n        BeforeEach(func(ctx context.Context) {\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Test if manifest template is synced with the vcluster\", func(ctx context.Context) {\n            Eventually(func(g Gomega) {\n                manifest, err := vClusterClient.CoreV1().ConfigMaps(TestManifestNamespace).\n                    Get(ctx, TestManifestName2, metav1.GetOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"ConfigMap should exist\")\n                g.Expect(manifest.Data[\"foo\"]).To(Equal(vClusterName),\n                    \"ConfigMap foo value should equal vcluster name\")\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Manifest template should be synced\")\n        })\n    },\n)\n```\n","isInternal":false,"tokens":2983,"sizeBytes":13602},{"name":"e2e-framework-suite-deps.md","path":".claude/references/e2e-framework-suite-deps.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/.claude/references/e2e-framework-suite-deps.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# e2e-framework: External Infrastructure Patterns\n\nTwo distinct patterns depending on what needs provisioning. Pick the right one before writing the plan.\n\n## Service-level provisioning (Helm charts, external APIs)\n\nUse this for: ingress-nginx, fluent-bit, metrics-server, cert-manager, CSI drivers, or any service installed into the test cluster.\n\nInstall in `BeforeAll`, tear down in `AfterAll`. Never assume the service is pre-provisioned.\n\n```go\nBeforeAll(func(ctx context.Context) context.Context {\n    By(\"installing <service>\")\n    ctx, err := setuphelm.Upgrade(\n        setuphelm.WithHelmOptions(\n            constants.Get<Service>HelmOptions()...,\n            helm.WithWait(),\n        ),\n    )(ctx)\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(...)\n    return ctx\n})\n\nAfterAll(func(ctx context.Context) {\n    _, err := setuphelm.Uninstall(\n        setuphelm.WithHelmOptions(\n            helm.WithName(\"<release>\"),\n            helm.WithNamespace(\"<ns>\"),\n        ),\n    )(ctx)\n    Expect(err).NotTo(HaveOccurred())\n    _, err = namespace.Delete(\"<ns>\")(ctx)\n    Expect(err).NotTo(HaveOccurred())\n})\n```\n\n**Why `AfterAll` and not `DeferCleanup`**: `DeferCleanup` in `BeforeAll` runs after all specs, which is equivalent here — but `AfterAll` is explicit and survives `--setup-only` runs where you want the service to persist between dev iterations.\n\n**Chart constants**: Add a `e2e/constants/<service>.go` with `Get<Service>HelmOptions()` following the pattern in existing constants files.\n\n---\n\n## Cluster-level provisioning (Kind host, vClusters)\n\nUse this for: tests that need a full ephemeral Kubernetes cluster or vCluster, not a service inside an existing cluster.\n\n**Two-layer model:**\n\n1. **Host kind cluster** - eagerly provisioned once in `SynchronizedBeforeSuite` via the framework's `cluster.Define` + `cluster.Setup`. Defined in `e2e/clusters/registry.go` as `clusters.HostCluster`. Reused by every per-test vCluster.\n2. **Per-test vClusters** - created lazily, one per `suite_*_test.go`, in the suite's outer `BeforeAll` via `setup/lazyvcluster.LazyVCluster`. The helper is a thin wrapper over the framework's `vcluster.Create` (see `github.com/loft-sh/e2e-framework/pkg/setup/vcluster`). Peak concurrent vClusters is bounded by `ginkgo --procs`, not by the number of suite files.\n\n```go\n// e2e/suite_myfeature_test.go\n//go:embed vcluster-myfeature.yaml\nvar myFeatureYAML string\n\nconst myFeatureName = \"myfeature-vcluster\"\n\nfunc init() { suiteMyFeature() }\n\nfunc suiteMyFeature() {\n    Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n        cluster.Use(clusters.HostCluster),\n        func() {\n            BeforeAll(func(ctx context.Context) context.Context {\n                return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML,\n                    // optional: host-side prerequisites (CRDs, PVCs, Helm install)\n                    lazyvcluster.WithPreSetup(setup.MyPrereq()),\n                )\n            })\n\n            // spec functions...\n        },\n    )\n}\n```\n\nInside specs: `cluster.CurrentKubeClientFrom(ctx)` resolves to the suite's vCluster. `cluster.KubeClientFrom(ctx, constants.GetHostClusterName())` resolves to the host.\n\n**Failure-aware teardown**: on spec failure the framework keeps the vCluster alive and attaches diagnostics (rendered config, pods, events, syncer logs) to the failing spec's report entries via `AddReportEntry`. On pass, teardown destroys the vCluster.\n\n**Focus is automatic**: a label-filtered run only materializes vClusters whose outer suite Describe matches. Unmatched suites' `BeforeAll` never fires, so no vCluster is spun up for tests that do not need it.\n\n---\n\n## Identifying what needs provisioning\n\nWhen migrating an old test, check these files in order:\n\n1. **Old test's `values.yaml`** — Look for `controlPlane.helmRelease` entries (charts the vcluster deploys internally), `experimental.deploy.vcluster.helm` sections, and any inline manifests in `experimental.deploy.vcluster.manifests`.\n2. **`host-resources.yaml`** (if present) — PVCs for volume snapshots, CRDs, or other host-cluster prerequisites.\n3. **`.github/workflows/e2e.yaml`** — CI workflow steps that install external dependencies before running the test suite.\n\n### Known External Dependencies by Suite\n\n| Dependency | Suites | Notes |\n|---|---|---|\n| CSI volume snapshots | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Snapshot CRDs + controller |\n| ingress-nginx 4.14.2 | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Ingress controller for service exposure tests |\n| fluent-bit 3.1.13 (OCI) | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Deployed as Helm chart inside vcluster |\n| metrics-server | e2e_metrics_proxy | Required for metrics API proxy tests |\n| Plugin images | e2e_plugin | Custom plugin container images |\n\n### Dependency-Free Suites\n\nThese suites have no external service dependencies:\n- `e2e_certs`\n- `e2e_cli`\n- `e2e_ha`\n- `e2e_limit_classes`\n- `e2e_pause_resume`\n\n**How to read the old test setup**: Look at the suite's CI workflow step. Each `helm install` or `kubectl apply` in the workflow's pre-test phase becomes either a `setuphelm.Upgrade()` call in `BeforeAll` or a cluster-level dependency via `cluster.Define`/`vcluster.Define`. The vcluster's `values.yaml` shows what config was baked into the vcluster itself — this becomes embedded YAML in `clusters/*.go`.\n","isInternal":false,"tokens":1307,"sizeBytes":5434},{"name":"e2e-old-to-new-mapping.md","path":".claude/references/e2e-old-to-new-mapping.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/.claude/references/e2e-old-to-new-mapping.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Old → New Pattern Mapping (vcluster)\n\nTranslation table for migrating tests from `test/framework` to `e2e/`.\n\n## Client Accessors\n\n| Old Pattern (`test/framework`) | New Pattern (`e2e`) |\n|---|---|\n| `framework.DefaultFramework` global singleton | Context-based: `cluster.KubeClientFrom(ctx, name)`, `cluster.CurrentKubeClientFrom(ctx)` |\n| `f.HostClient` / `f.HostCRClient` | `cluster.KubeClientFrom(ctx, constants.GetHostClusterName())` |\n| `f.VClusterClient` / `f.VClusterCRClient` | `cluster.CurrentKubeClientFrom(ctx)` / `cluster.CurrentClusterClientFrom(ctx)` |\n| `f.VClusterName` / `f.VClusterNamespace` | Derived from cluster definition name in `clusters/` (e.g., `clusters.ServiceSyncVClusterName`) |\n| `os.Getenv(\"VCLUSTER_NAME\")` etc. | Cluster definitions with typed config in `constants/` |\n| `f.Suffix` / `translate.VClusterName` | `translate` package used directly where needed |\n\n## Setup and Lifecycle\n\n| Old Pattern | New Pattern |\n|---|---|\n| `framework.CreateFramework(ctx)` in `BeforeSuite` | `SynchronizedBeforeSuite` with `clusters.HostCluster.Setup` + `clusters.XVCluster.Setup` |\n| `f.Cleanup()` in `AfterSuite` | `SynchronizedAfterSuite` with `clusters.HostCluster.Teardown` |\n| `f.RefreshVirtualClient()` | `connectVCluster()` — see \"Reconnecting after destructive operations\" below |\n| Per-suite `values.yaml` | Per-cluster embedded YAML in `clusters/*.go` with `template.MustRender()` |\n| Single vcluster per suite | Multiple vclusters with different configs (DefaultVCluster, NodesVCluster, etc.) |\n\n## Test Execution\n\n| Old Pattern | New Pattern |\n|---|---|\n| `go test -v -ginkgo.v` | `ginkgo --label-filter=\"...\" ./e2e` |\n| `time.Sleep()` | `Eventually().WithPolling().WithTimeout()` |\n| Ad-hoc durations | `constants.PollingTimeout*` constants |\n| Env-based config (`VCLUSTER_SUFFIX`, etc.) | Flag-based config (`--vcluster-image`, `--cluster-name`) |\n\n## Timeouts\n\n| Old Constant | New Constant | Duration |\n|---|---|---|\n| `framework.PollInterval` (5s) | `constants.PollingInterval` | 2s |\n| `framework.PollTimeout` (1min) | `constants.PollingTimeout` | 60s |\n| `framework.PollTimeoutLong` (2min) | `constants.PollingTimeoutLong` | 120s |\n| N/A | `constants.PollingTimeoutVeryShort` | 5s |\n| N/A | `constants.PollingTimeoutShort` | 20s |\n| N/A | `constants.PollingTimeoutVeryLong` | 300s |\n\n## Cluster Definitions\n\nThe old framework uses a single vcluster per suite. The new framework defines multiple vclusters in `e2e/clusters/clusters.go`:\n\n```go\n// Host cluster (Kind)\nclusters.HostCluster = cluster.Define(\n    cluster.WithName(constants.GetHostClusterName()),\n    cluster.WithProvider(kind.NewProvider()),\n    cluster.WithConfigFile(\"e2e-kind.config.yaml\"),\n)\n\n// Virtual clusters — each with specific config\nclusters.DefaultVCluster = vcluster.Define(\n    vcluster.WithName(\"default-vcluster\"),\n    vcluster.WithVClusterYAML(DefaultVClusterYAML),\n    vcluster.WithOptions(DefaultVClusterOptions...),\n    vcluster.WithDependencies(HostCluster),\n)\n```\n\nUse `cluster.Use(clusters.XVCluster)` in `Describe` to wire a test to a specific cluster:\n\n```go\nvar _ = Describe(\"My test\",\n    labels.Core,\n    cluster.Use(clusters.DefaultVCluster),\n    cluster.Use(clusters.HostCluster),\n    func() { ... },\n)\n```\n\n## Reconnecting After Destructive Operations\n\nThe suite-level background proxy (established in `SynchronizedBeforeSuite`) does **not** survive\nvcluster pause/resume or cert rotation. `cluster.CurrentKubeClientFrom(ctx)` and\n`cluster.CurrentClusterFrom(ctx).KubernetesRestConfig()` both point to the suite proxy — if the\nvcluster has been paused, restarted, or had its certs rotated, these return dead connections.\n\nThe old framework's `f.RefreshVirtualClient()` re-established the connection. The e2e\nequivalent is to run `vcluster connect` programmatically with a background proxy:\n\n1. Create a temp file for the kubeconfig\n2. Build a `connectcmd.ConnectCmd` with `BackgroundProxy: true`, `KubeConfig` pointing to the temp file, and `BackgroundProxyImage` set to `constants.GetVClusterImage()` — the same image already loaded into Kind\n3. Call `cmd.Run(ctx, []string{vclusterName})`\n4. Poll with `Eventually` until the kubeconfig file is non-empty and `clientcmd.RESTConfigFromKubeConfig` succeeds\n5. Verify the connection by making a lightweight API call (e.g., `Get` the `default` ServiceAccount)\n6. Return the `*rest.Config` and a cleanup func that removes the temp file\n\n**Critical: use `constants.GetVClusterImage()` for `BackgroundProxyImage`**, not\n`vclusterconstants.DefaultBackgroundProxyImage(upgrade.GetVersion())`.\n\nKey imports: `connectcmd \"github.com/loft-sh/vcluster/cmd/vclusterctl/cmd\"`,\n`\"github.com/loft-sh/vcluster/pkg/cli\"`, `\"github.com/loft-sh/vcluster/pkg/cli/flags\"`,\n`loftlog \"github.com/loft-sh/log\"`,\n`\"github.com/spf13/cobra\"`, `\"k8s.io/client-go/tools/clientcmd\"`,\n`\"github.com/loft-sh/vcluster/e2e/constants\"`.\n","isInternal":false,"tokens":1273,"sizeBytes":4899},{"name":"e2e-conventions.md","path":".claude/rules/e2e-conventions.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/.claude/rules/e2e-conventions.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-conventions-core.md -->\n\n# e2e Test Conventions\n\nGinkgo v2 + Gomega, running against vCluster instances on Kind. Read existing tests in `e2e/test_*/` for patterns before writing new ones.\n\n## Key Conventions\n\n1. **Random suffixes** — Always use `objectmeta.GenerateName` or `random.RandomString(6)` for resource names to avoid collisions in parallel runs. Try to share the suffix across a single test spec so all resources of a single test use the same suffix. This applies to **all** identifiers that could collide, not just Kubernetes object names — Helm release names, database names, etc. If an identifier is a package-level `const` and would conflict across parallel runs, make it dynamic.\n2. **Context propagation** — Setup helpers store objects in context; retrieve with `<resource>.From(ctx, name)`.\n3. **Eventually for async** — vCluster operations are eventually consistent. Always wrap API checks in `Eventually` with `constants.*` timeouts. Never hardcode durations.\n4. **Labels for filtering** — Tag `Describe` with resource labels and `labels.PR` if all specs in it should gate PRs. Only tag individual `It` blocks with `labels.PR` when some specs in the `Describe` should NOT gate PRs. Never duplicate a label on an `It` that is already present on its enclosing `Describe` or `Context` — Ginkgo inherits labels, so duplicates are redundant noise.\n5. **Ordered contexts** — See the decision table below. Default to `BeforeEach`; only use `Ordered` for true sequential dependencies.\n6. **Package registration** — Test packages self-register via `var _ = Describe(...)`. The suite imports them as blank imports in `e2e_suite_test.go`.\n7. **Error assertion** — Prefer `ginkgo.Expect(...).To(ginkgo.Succeed())` over `ginkgo.Expect(...).NotTo(ginkgo.HaveOccurred())`\n8. **gstruct usage** — Do not use `gstruct` for simple field assertions (use `Expect(obj.Field).To(Equal(...))` instead). Use `gstruct` with `ContainElement` or similar matchers when asserting on elements within a collection (slice, map) to avoid manual loops.\n\n## Setup Helpers\n\nvCluster's `e2e/` does not yet have a rich `setup/` builder library like loft-enterprise. During migration:\n\n- Use existing **cluster definitions** from `e2e/clusters/` for test environment setup. Browse `clusters/clusters.go` for available vclusters with different configurations.\n- Use the `setup/template` package (`template.MustRender()`) for rendering vcluster YAML with image overrides.\n- When a setup pattern repeats across multiple tests, create a shared setup helper as an `[infra]` sub-problem. Place it in `e2e/setup/` following the functional-options pattern.\n- For now, most test setup is done inline using the Kubernetes client directly (create namespace, create resource, DeferCleanup).\n\n### Scoping Constants and Helpers\n\nTest-specific constants, helper functions, and option builders that are only used by a single `test_*` package belong **in that package**, not in the shared `constants` or `setup` packages. Only promote to a shared package when a second consumer appears.\n\n### External Service Provisioning\n\nIf the old test installs Helm charts or external services (via `values.yaml` `controlPlane.helmRelease`, host resources, or CI workflow steps), flag this as an `[infra]` sub-problem during migration. The external service needs to be provisioned self-contained within the test using `setuphelm.Upgrade()` in `BeforeAll` or via the cluster dependency system.\n\n## vCluster Configuration Patterns\n\nEach vCluster lives next to the `suite_*_test.go` that uses it. `HostCluster` (the kind host) is the only eagerly-provisioned dependency in `SynchronizedBeforeSuite`; every per-test vCluster is created lazily in the suite's own `BeforeAll` via `setup/lazyvcluster.LazyVCluster`, which is a thin wrapper over the framework's `vcluster.Create`.\n\n### Embedded YAML Templates\n\nEmbed the `vcluster.yaml` in the suite file. Templates support `{{.Repository}}`, `{{.Tag}}`, `{{.HostClusterName}}` placeholders - the lazy helper renders them at `BeforeAll` time and registers temp-file cleanup automatically.\n\n### Defining a New vCluster Suite\n\n1. Create `e2e/vcluster-myfeature.yaml` with the `vcluster.yaml` config.\n2. Create `e2e/suite_myfeature_test.go`:\n   ```go\n   //go:embed vcluster-myfeature.yaml\n   var myFeatureYAML string\n\n   const myFeatureName = \"myfeature-vcluster\"\n\n   func init() { suiteMyFeature() }\n\n   func suiteMyFeature() {\n       Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n           cluster.Use(clusters.HostCluster),\n           func() {\n               BeforeAll(func(ctx context.Context) context.Context {\n                   return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n               })\n               // spec functions...\n           },\n       )\n   }\n   ```\n3. If a new filter label is needed, add it to `labels/labels.go`. `labels.PR` goes on the outer suite (never on specs).\n4. If the vCluster needs host-side prerequisites (CRDs, PVCs, Helm install), pass `lazyvcluster.WithPreSetup(fn)`. Reusable helpers live in `setup/` (`setup.SnapshotPreSetup`, `setup.MetricsServerPreSetup`).\n\nDo NOT add entries to `clusters/` - that package only holds `HostCluster` and `DefaultVClusterOptions`.\n\n## Client Accessors\n\n| Accessor | Type | Use for |\n|----------|------|---------|\n| `cluster.KubeClientFrom(ctx, clusterName)` | `kubernetes.Interface` | Host cluster typed client |\n| `cluster.CurrentKubeClientFrom(ctx)` | `kubernetes.Interface` | Current vcluster typed client |\n| `cluster.CurrentClusterClientFrom(ctx)` | `client.Client` (controller-runtime) | Current cluster CR client |\n\n## Background Proxy Limitations\n\nThe suite-level background proxy (started by `vcluster connect` during `SynchronizedBeforeSuite`)\nis a **one-shot process**. It dies when:\n\n- The vcluster pod is paused/resumed (e.g., `certs rotate`, `certs rotate-ca`, helm upgrade)\n- The vcluster's CA certificate changes\n- The vcluster pod restarts for any reason\n\nAfter any of these, `cluster.CurrentKubeClientFrom(ctx)` and\n`cluster.CurrentClusterFrom(ctx).KubernetesRestConfig()` return connections to a dead proxy.\n\n**If your test does destructive vcluster operations** (cert rotation, restart, config change),\nyou must establish a fresh connection using the `connectVCluster()` pattern documented in\n`.claude/references/e2e-old-to-new-mapping.md` § \"Reconnecting After Destructive Operations\".\n\n**Common pitfall:** When building `ConnectOptions`, always set `BackgroundProxyImage` to\n`constants.GetVClusterImage()` — **never** use `DefaultBackgroundProxyImage(upgrade.GetVersion())`.\nIn dev builds the latter produces an invalid Docker image ref (empty tag), Docker rejects it,\nand the code silently falls back to in-process port-forwarding that hangs the test process.\n\n## Cleanup\n\nFor non-Ordered contexts, return the enriched context from `BeforeEach`:\n\n```go\nBeforeEach(func(ctx context.Context) context.Context {\n    vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n    Expect(vClusterClient).NotTo(BeNil())\n    return ctx\n})\n```\n\nInside `It` blocks, register cleanup immediately after creation:\n\n```go\nIt(\"tests something\", func(ctx context.Context) {\n    _, err := vClusterClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{\n        ObjectMeta: metav1.ObjectMeta{Name: nsName},\n    }, metav1.CreateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(func(ctx context.Context) {\n        err := vClusterClient.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n        Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n    })\n\n    // ... test logic\n})\n```\n\n## Labels\n\nAttach labels to `Describe`, `Context`, or `It`. Check `e2e/labels/labels.go` for the full list. Key ones:\n- `labels.PR` — gate tests that run on every PR\n- `labels.Core`, `labels.Sync`, `labels.Deploy` — feature-area labels\n\n## Ordered vs. BeforeEach\n\nDefault to `BeforeEach`. The suite will be parallelized — every unnecessary `Ordered` is a bottleneck.\n\n| Signal | Pattern | Why |\n|---|---|---|\n| Specs are independent, just share setup | `BeforeEach` (no `Ordered`) | Parallelizable; no cascade failures |\n| Specs form a lifecycle sequence (create → mutate → verify → delete) | `Ordered` + `BeforeAll` | Specs depend on prior spec's side effects |\n| Setup is expensive (databases, vclusters with deployed services) | `BeforeEach` — pay the cost per spec | Parallel execution recovers wall time; isolation prevents false failures |\n| A spec deletes or mutates the shared resource | `BeforeEach` with per-spec resource | Other specs can't rely on it existing |\n| Specs must run in a fixed order but don't share mutable state | **Rethink** — probably independent specs | `Ordered` without true dependencies is a parallelization bottleneck |\n\n> **Hard rule:** If you use `Ordered`, add a comment explaining which spec depends on which prior spec's side effect. If you can't name one, drop `Ordered`.\n\n## Concrete Examples\n\nSee `.claude/references/e2e-examples.md` for annotated excerpts from production tests covering: DeferCleanup placement, `Eventually` with `g Gomega`, `Ordered` vs `BeforeEach` side-by-side, and cluster client usage.\n\n## No Plan Artifacts in Code\n\nNever include migration plan identifiers (e.g., `SP-0`, `SP-1`, `[migrate]`, `[infra]`, `[cleanup]`, `[consolidate]`) in code comments, `By()` text, or test descriptions. These are internal planning artifacts — the code should read as if no plan ever existed.\n\n```go\n// FAIL — plan artifact leaked into comment\n// SP-2: verify configmap is synced to host\nBy(\"SP-2: Checking configmap sync\", func() { /* ... */ })\n\n// PASS — describes intent without plan references\n// Verify the configmap is synced to the host cluster\nBy(\"Checking that the configmap is synced to the host cluster\", func() { /* ... */ })\n```\n\n## New Test Checklist\n\n1. Place the file in the appropriate `e2e/test_*` directory. Package name must match sibling files.\n2. If creating a **new** `test_*` package, add a blank import in `e2e_suite_test.go`.\n3. If testing a **new** resource type, add a label constant in `e2e/labels/labels.go`.\n4. Use an existing test file in the same package as your starting template.\n","frontmatter":{"paths":["e2e/**/*.go"]},"isInternal":false,"tokens":2413,"sizeBytes":10311},{"name":"e2e-error-handling.md","path":".claude/rules/e2e-error-handling.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/.claude/rules/e2e-error-handling.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-error-handling.md (identical) -->\n\n# e2e Error Handling\n\n## Cleanup: only tolerate IsNotFound, assert everything else\n\nSingle-operation cleanup:\n\n```go\n// GOOD\nDeferCleanup(func(ctx context.Context) {\n    err := client.Delete(ctx, name, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n```\n\nMulti-step cleanup (get → mutate → delete):\n\n```go\n// GOOD — check IsNotFound on Get, assert all other errors at every step\nDeferCleanup(func(ctx context.Context) {\n    obj, err := client.Get(ctx, name, metav1.GetOptions{})\n    if kerrors.IsNotFound(err) {\n        return // already gone\n    }\n    Expect(err).NotTo(HaveOccurred())\n\n    delete(obj.Annotations, someAnnotation)\n    _, err = client.Update(ctx, obj, metav1.UpdateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n\n    err = client.Delete(ctx, name, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n```\n\n**When NOT to use `IgnoreNotFound`:** `IgnoreNotFound` is appropriate for resources that may have been deleted by cascade, by a prior spec, or by a controller. For resources the test **just created in the same `It` block** and expects to still exist at cleanup time, prefer a strict assertion — `Expect(err).NotTo(HaveOccurred())`. A `NotFound` in this case signals a test bug or unexpected controller behavior, not a benign race.\n\nNever use these patterns:\n- `_, _ =` to discard errors — silent failures mask regressions and leak resources\n- `if err \\!= nil { return }` to bail on any error — this swallows connectivity/RBAC failures, not just NotFound\n\n## Error assertions: assert the specific error, not just occurrence\n\n```go\n// BAD — passes on connectivity errors, RBAC failures, or any unrelated error\nExpect(err).To(HaveOccurred())\n\n// GOOD\nExpect(err).To(MatchError(ContainSubstring(\"already exists\")))\n```\n","frontmatter":{"paths":["e2e/**/*.go"]},"isInternal":false,"tokens":452,"sizeBytes":1945},{"name":"e2e-quality-checklist.md","path":".claude/rules/e2e-quality-checklist.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/.claude/rules/e2e-quality-checklist.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core (items 1-8): e2e-tdd-workflow plugin references/e2e-quality-checklist-core.md -->\n\n# E2E Test Quality Checklist (e2e)\n\nEvery item is **pass/fail**. A test must pass all 10 items.\n\n---\n\n## 1. Cleanup Tolerates Already-Deleted Resources\n\nCleanup code must not fail if the resource was already removed (e.g., cascade-deleted\nby a parent). Use `client.IgnoreNotFound` or equivalent.\n\n```go\n// PASS\nDeferCleanup(func(ctx context.Context) {\n    err := client.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n\n// FAIL — hard-fails if resource is already gone\nDeferCleanup(func(ctx context.Context) {\n    Expect(client.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})).To(Succeed())\n})\n```\n\n---\n\n## 2. DeferCleanup Registered Before Subsequent Assertions\n\n`DeferCleanup` must be the very next statement after verifying creation succeeded.\nIf an assertion between creation and cleanup registration fails, the resource leaks\nand can block teardown of the entire suite.\n\n```go\n// PASS — cleanup registered immediately, before any further assertions\n_, err := vClusterClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})\nExpect(err).NotTo(HaveOccurred())\nDeferCleanup(func(ctx context.Context) {\n    err := vClusterClient.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n\n// now safe to assert further\nExpect(ns.Name).To(Equal(expectedName))\n\n// FAIL — assertion before cleanup registration; if it fails, namespace leaks\n_, err := vClusterClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})\nExpect(err).NotTo(HaveOccurred())\nExpect(ns.Name).To(Equal(expectedName))\nDeferCleanup(func(ctx context.Context) { /* ... */ })\n```\n\n---\n\n## 3. Timeout Constants\n\nUse the predefined constants. Never invent ad-hoc durations.\n\n| Constant | Duration | Use for |\n|----------|----------|---------|\n| `PollingTimeoutVeryShort` | 5s | Immediate state checks |\n| `PollingTimeoutShort` | 20s | Quick API operations |\n| `PollingTimeout` | 60s | Standard operations |\n| `PollingTimeoutLong` | 120s | Resource creation |\n| `PollingTimeoutVeryLong` | 300s | vCluster startup, cluster creation |\n\n---\n\n## 4. No Cluster-Scoped Singletons\n\nTests must not assume they are the only consumer of a cluster-scoped resource\n(e.g., a ClusterRole with a fixed name). If a cluster-scoped resource is needed,\nits name must include the suffix.\n\n---\n\n## 5. Resource Naming for Debuggability\n\n### 5.1 Single suffix per `Ordered` container\n\nOne call to `random.RandomString(6)`, stored in a variable, reused for all resource\nnames in the container. Do not generate a separate suffix per resource.\n\n```go\n// PASS — single suffix, all resources traceable\nBeforeAll(func(ctx context.Context) context.Context {\n    suffix := random.RandomString(6)\n    nsName = \"test-sync-\" + suffix\n    svcName = \"test-svc-\" + suffix\n    // ...\n})\n\n// FAIL — different suffix per resource, can't correlate\nBeforeAll(func(ctx context.Context) context.Context {\n    nsName = \"test-sync-\" + random.RandomString(6)\n    svcName = \"test-svc-\" + random.RandomString(6)\n    // ...\n})\n```\n\n### 5.2 Descriptive resource name prefixes\n\nThe prefix before the suffix should identify the test or resource purpose.\n\n```go\n// PASS\nnsName = \"svc-sync-test-\" + suffix\n\n// FAIL — generic prefix, hard to trace in cluster\nnsName = \"ns-\" + suffix\n```\n\n---\n\n## 6. Use the Lazy vCluster Pattern\n\nPer-test vClusters live in `suite_*_test.go` and are created lazily in the suite's `BeforeAll` via `setup/lazyvcluster.LazyVCluster`. The only eager cluster is `clusters.HostCluster` (the kind host).\n\n```go\n// PASS — lazy creation inside the suite\nDescribe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n    cluster.Use(clusters.HostCluster),\n    func() {\n        BeforeAll(func(ctx context.Context) context.Context {\n            return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n        })\n        // specs...\n    },\n)\n\n// FAIL — eager registration in clusters/ + cluster.Use(vc) is the old pattern; do not reintroduce it.\n```\n\nDo not add definitions to `clusters/`. Pre-setup hooks (CRD install, PVC, Helm) go into `setup/` and are passed via `lazyvcluster.WithPreSetup`. See `e2e/README.md` for the full pattern.\n\n---\n\n## 7. Test Setup Does Not Patch System-Managed State\n\n`BeforeEach` and `BeforeAll` must not directly set fields or conditions that a controller or deployment configuration would normally own. If setup code bypasses the system's real initialization path to establish a condition, the test is not exercising production behavior.\n\n**FAIL**: `BeforeEach` patches a vcluster status field that the syncer reconciles into existence.\n\n**PASS**: The vcluster is bootstrapped with the correct YAML configuration so the system establishes the expected state through its normal code path.\n\nIf you find yourself writing setup that patches system-managed state, stop. Read all existing tests that share the same labels — they show how the environment should be configured.\n\n---\n\n## 8. `By()` Text Style and Closure\n\n`By()` must use a closure (see `e2e-test-structure.md`), and the text should be a human-readable sentence fragment describing *what* is happening, not *how*.\n\n```go\n// PASS — closure + descriptive text\nBy(\"Waiting for the replicated service to appear in vcluster\", func() {\n    // ... test logic\n})\n\n// FAIL — no closure; Ginkgo can't attribute failures to the step\nBy(\"Waiting for the replicated service to appear in vcluster\")\n\n// FAIL — too terse, sounds like a function name\nBy(\"WaitServiceSync\", func() { /* ... */ })\n\n// FAIL — describes implementation, not intent\nBy(\"Calling vClusterClient.CoreV1().Services().Get in a loop\", func() { /* ... */ })\n```\n\n---\n\n## 9. Lint Passes\n\n`just -f Justfile.agent lint ./e2e/...` runs clean. This runs the full `golangci-lint` suite (formatting, static analysis, 28+ linters) matching CI. Note: `compile-check` only runs `go build` + `go vet`, which is a subset — always run `lint` as well.\n\n---\n\n## 10. No Hardcoded Cluster or Context Names\n\nNever hardcode kind cluster names or kubectl context strings. Use `constants.GetHostClusterName()`\nfor the cluster name and derive the context from it. The cluster name is configurable via the\n`KIND_NAME` env var and the `--cluster-name` flag.\n\n```go\n// PASS — derived from configurable constant\nhostCluster := constants.GetHostClusterName()\nkubectlContext := \"kind-\" + hostCluster\n\n// FAIL — hardcoded, breaks when KIND_NAME is set\nkubectlContext := \"kind-kind-cluster\"\n```\n","frontmatter":{"paths":["e2e/**/*.go"]},"isInternal":false,"tokens":1614,"sizeBytes":6694},{"name":"e2e-test-structure.md","path":".claude/rules/e2e-test-structure.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/.claude/rules/e2e-test-structure.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-test-structure-core.md -->\n\n# e2e Test Structure Rules\n\n| Do | Don't |\n|---|---|\n| Register `DeferCleanup` immediately after creating a resource, before any assertions. If an assertion fails before cleanup is registered, the resource leaks. | Register cleanup after assertions — a failed assertion skips the rest of the block and the resource leaks forever. |\n| Add failure context to assertions inside `Eventually`: `g.Expect(phase).To(Equal(\"Ready\"), \"reason: %v, message: %v\", obj.Status.Reason, obj.Status.Message)` | Write `g.Expect(phase).To(Equal(\"Ready\"))` inside `Eventually` with no context — when it times out you get no diagnostics. |\n| Use `By(\"description\", func() { ... })` with a closure so Ginkgo reports which step failed and how long it took. | Use bare `By(\"description\")` followed by loose statements — Ginkgo can't attribute failures to the step. |\n| Default to `BeforeEach`. Only use `Ordered` for true sequential dependencies. | Use `Ordered` for convenience — every `Ordered` context is a parallelization bottleneck. |\n| Tolerate `NotFound` in cleanup, assert everything else. See `e2e-error-handling.md` for patterns. | Use `_, _ =` to swallow errors in `DeferCleanup` — silent failures mask regressions and leak resources. |\n| Clean up only the specific resources you created, by name or label selector. | `List` all resources of a type in cleanup — parallel tests create the same types and you'll delete their resources. |\n| Assert specific error messages: `Expect(err).To(MatchError(ContainSubstring(\"...\")))` or `kerrs.Be(metav1.StatusReason...)` | Write `Expect(err).To(HaveOccurred())` — this passes on unrelated failures like connectivity errors. |\n| Use existing cluster definitions from `clusters/` and `setup/template` for YAML rendering. Browse available definitions before writing any direct client call in test setup or cleanup. | Write one-off raw client calls when a cluster definition or template helper exists — this duplicates error handling, skips context tracking, and diverges from the standard patterns over time. |\n","frontmatter":{"paths":["e2e/**/*.go"]},"isInternal":false,"tokens":490,"sizeBytes":2171},{"name":"scratchpad-programs.md","path":".claude/rules/scratchpad-programs.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/.claude/rules/scratchpad-programs.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\nglobs: [\"**/*.go\"]\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/scratchpad-programs.md (identical) -->\n\n## Verifying assumptions with scratchpad programs\n\nWhen you need to verify an assumption — about API behavior, data shape, timing,\nRBAC, sync logic, or anything else — write a small standalone program in\n`.agent-scratchpad/` that directly answers the question.\n\nThis is not a failure-recovery technique. Use it proactively any time you're\nabout to write code based on an assumption you haven't confirmed.\n\n### Examples of when to use this\n\n- \"Does this CRD have a status subresource?\"\n- \"What does the syncer actually return for a conflicting resource name?\"\n- \"Does client-go's patch merge or replace this nested field?\"\n- \"What order do these finalizers run in?\"\n- \"Is this label selector format valid?\"\n\n### How\n\n1. Create a subfolder in `.agent-scratchpad/` named after the question\n   (e.g., `.agent-scratchpad/check-patch-merge/main.go`). Each program gets\n   its own folder — never put multiple programs in the same folder.\n\n2. Run it:\n   ```bash\n   cd .agent-scratchpad/check-patch-merge && go run main.go --kubeconfig=\"${HOME}/.kube/config\" --context=kind-kind-cluster\n   ```\n   If no cluster is needed (pure logic, parsing, transforms), skip the kubeconfig.\n\n3. Read the output, answer the question, delete the folder.\n\n### Guidelines\n\n| Do | Don't |\n|----|-------|\n| One folder, one file, one question | Build a mini test framework |\n| Name the folder after the question | Use generic names like `test1/` |\n| Print the answer and exit | Add flags for \"reuse later\" |\n| Use `go run` directly | Create a separate go.mod |\n| Delete the folder after use | Leave folders accumulating |\n","frontmatter":{"globs":["**/*.go"]},"isInternal":false,"tokens":414,"sizeBytes":1719},{"name":"SKILL.md","path":".claude/skills/e2e-migration-validator/SKILL.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/.claude/skills/e2e-migration-validator/SKILL.md","title":"e2e-migration-validator","category":"anthropic-skill","format":"markdown","content":"---\nname: e2e-migration-validator\ndescription: >\n  Use this skill whenever a user wants to validate, review, or audit e2e tests that have been\n  migrated from an old framework (typically in a `e2e/` directory) to the new `e2e/`\n  framework. Triggers include: \"review my migrated test\", \"validate this e2e test\", \"check\n  my test migration\", \"did I migrate this correctly\", \"review e2e PR\", \"check test coverage\n  after migration\", or any request involving comparing old vs new e2e test files across the\n  vcluster, vcluster-pro, or loft-enterprise repos. Always use this skill when an e2e test\n  file or PR from one of these repos is provided for review — even if the user just says\n  \"take a look at this test\".\n---\n\n# E2E Test Migration Validator\n\nA skill for exhaustively reviewing e2e tests migrated from the old framework (`next/` directory)\nto the new framework (`e2e/` directory) across the `vcluster`, `vcluster-pro`, and\n`loft-enterprise` repositories.\n\n---\n\n## Inputs\n\nThe user will provide one or more of:\n- **Uploaded file(s)**: The new migrated test file, and optionally the original test file\n- **GitHub PR link**: A pull request URL containing the migrated test(s)\n- **Pasted code**: Snippets of old and/or new test code inline in the chat\n\nIf only the new test is provided (no original), you must locate the original yourself using\nthe local repo tools (see [Repo Access](#repo-access)).\n\n---\n\n## Repo Access\n\nUse the local repo MCP tools to inspect implementation details and locate source files.\nAll three repos may be relevant depending on the test subject:\n\n| Repo | Use when |\n|------|----------|\n| `vcluster` | Core vcluster functionality, open-source features |\n| `vcluster-pro` | Pro/enterprise vcluster features |\n| `loft-enterprise` | Loft platform, multi-tenancy, UI-level features |\n\nWhen reviewing a test, **always look up the actual implementation** being tested in the\nrelevant repo before drawing conclusions about correctness or coverage. Do not rely solely\non what the test itself asserts.\n\n---\n\n## Review Workflow\n\nFollow these steps in order for every migration review:\n\n### Step 1 — Locate the Original Test\n\nIf the original (`next/`) test was not provided:\n1. Search the appropriate repo(s) for the original test file by name or describe pattern\n2. Read it fully before proceeding\n3. Note the framework, helper utilities, and assertion style used\n\n### Step 2 — Locate the Implementation Under Test\n\nUsing the repo MCP tools:\n1. Find the source code that the test is exercising (controllers, handlers, CLI commands, API routes, UI components, etc.)\n2. Read the implementation thoroughly — **line by line** if needed\n3. Build a mental model of:\n   - All code paths (happy path, error paths, edge cases)\n   - All configurable inputs / flags / options\n   - Side effects and state mutations\n   - Interactions with other subsystems\n\n### Step 3 — Framework Migration Check\n\nVerify the structural correctness of the migration:\n\n- [ ] Test file is in the correct `e2e/` directory structure\n- [ ] Imports use the new framework's helpers/fixtures (not the old ones)\n- [ ] Test lifecycle hooks (`beforeAll`, `afterAll`, `beforeEach`, `afterEach`) are correctly\n      translated — no leftover old-framework teardown patterns\n- [ ] Setup and teardown create/delete resources correctly and completely\n- [ ] Cluster/namespace/resource provisioning uses the new framework's APIs\n- [ ] Any shared utilities (custom matchers, wait helpers, retry logic) are migrated or\n      replaced with new equivalents — not just copied verbatim\n- [ ] Test isolation is maintained (tests do not share mutable state across runs)\n- [ ] Framework-specific APIs (e.g. `page`, `context`, `request`) are used correctly\n\n### Step 4 — Correctness Check\n\nFor each test case in the migrated file:\n\n1. **Does the test actually test what it claims to test?**\n   - Read the test name / description and compare against what is asserted\n   - Flag any tests where the name and assertions are misaligned\n2. **Are the assertions meaningful?**\n   - Look for trivially-passing assertions (e.g. `expect(x).toBeDefined()` with no deeper check)\n   - Look for assertions that would pass even if the feature is broken\n   - Look for missing negative assertions (e.g. testing that a resource *was* created but not\n     that an invalid input *was rejected*)\n3. **Is the test deterministic?**\n   - Flag race conditions, missing `await`, missing retries on flaky operations\n   - Flag hardcoded timeouts that may not hold in CI\n4. **Is the test environment realistic?**\n   - Does the test reflect how the feature is actually used in production?\n   - Are there unrealistic shortcuts (e.g. bypassing auth, using superuser where a normal user\n     should be tested)?\n\n### Step 5 — Exhaustive Coverage Analysis\n\nThis is the most important step. Compare the implementation (Step 2) against the test suite.\n\nFor **every distinct behavior, code path, and configurable option** in the implementation:\n\n1. Check whether it is covered by at least one test\n2. If not covered, classify the gap:\n   - 🔴 **Critical gap** — a regression here would not be caught at all\n   - 🟡 **Important gap** — partial coverage, but a significant edge case is missed\n   - 🟢 **Minor gap** — low-risk or already covered by unit tests\n\nFor each gap, suggest a concrete additional test case including:\n- What scenario it covers\n- What inputs/state are needed\n- What assertion(s) should be made\n\n**Common gap categories to check explicitly:**\n- Error handling and invalid inputs\n- Permission / RBAC boundary conditions\n- Resource limit enforcement\n- Concurrent operations\n- Feature flags / configuration variants\n- Upgrade/downgrade paths (if applicable)\n- Interaction with other features (cross-feature integration)\n- Cleanup/teardown correctness (does the feature leave no orphaned resources?)\n\n### Step 6 — Code Quality Review\n\n- Naming: are test and variable names descriptive and consistent with the new framework's conventions?\n- DRY: is there duplicated setup that should be extracted into a shared fixture or helper?\n- Readability: would a new engineer understand what is being tested and why?\n- Comments: are non-obvious steps explained?\n- Dead code: are there commented-out blocks, unused imports, or leftover debug statements?\n\n---\n\n## Output Format\n\nProduce **all three** of the following output sections:\n\n---\n\n### 1. Inline Code Analysis\n\nGo through the migrated test file and annotate specific lines or blocks with numbered\ncomments. Format as a fenced code block with inline `// [N] ...` annotations, followed by\na numbered legend below it explaining each annotation.\n\nExample:\n```typescript\n// [1] Missing: should also assert that the old resource was deleted\nawait expect(newResource).toBeVisible();\n\n// [2] Race condition: no retry/wait before asserting pod readiness\nexpect(pod.status).toBe('Running');\n```\n\n**Legend:**\n1. 🔴 Critical gap — deletion is not verified; the old resource could persist undetected\n2. 🟡 Flakiness risk — pod status may not be `Running` immediately; use `waitForCondition`\n\n---\n\n### 2. Checklist Report\n\n```\n## Migration Checklist — <test file name>\n\n### Framework Migration\n- [x] Correct directory structure\n- [x] New framework imports\n- [ ] ⚠️  Teardown uses old-framework pattern on line 42\n\n### Correctness\n- [x] Assertions are meaningful\n- [ ] ⚠️  Test \"should reject invalid config\" passes even with valid config (line 87)\n\n### Coverage Gaps\n- 🔴 No test for RBAC: non-admin user attempting privileged operation\n- 🟡 No test for concurrent resource creation\n- 🟢 No test for audit log output (low risk)\n\n### Code Quality\n- [ ] ⚠️  Duplicate setup block (lines 12–25 and 61–74) — extract to beforeEach\n- [x] Naming is clear and consistent\n```\n\n---\n\n### 3. PR Review Comment\n\nA copy-paste-ready GitHub PR review comment in Markdown. Structure:\n\n```\n## E2E Migration Review\n\n**Overall:** ✅ Approved with suggestions / ⚠️ Changes requested / ❌ Major issues found\n\n### Summary\n<2–3 sentence high-level assessment>\n\n### Issues\n\n#### 🔴 Must Fix\n- ...\n\n#### 🟡 Should Fix\n- ...\n\n#### 🟢 Nice to Have\n- ...\n\n### Suggested Additional Tests\n- **Test:** `<descriptive test name>`\n  **Scenario:** <what to set up>\n  **Assert:** <what to check>\n\n<repeat for each suggested test>\n\n### Nits\n- ...\n```\n\n---\n\n## Framework Notes\n\n> **For the reviewer using this skill:** If you know the specific old and new frameworks\n> involved in this migration, add them here so Claude can apply framework-specific checks.\n>\n> Example entries:\n> - Old: `@vcluster/e2e` custom test runner → New: Playwright + `@vcluster/e2e-next` fixtures\n> - Old: Ginkgo/Gomega (Go) → New: Playwright (TypeScript)\n> - Old: Custom shell-script harness → New: Playwright with kubectl helpers\n>\n> Until this section is filled in, Claude will infer the frameworks from the provided files.\n\n---\n\n## Tips for High-Quality Reviews\n\n- **Always read the implementation first.** A test that looks complete may miss half the\n  code paths if you haven't read the source.\n- **Think adversarially.** For each assertion, ask: \"How could this pass even if the feature\n  is broken?\"\n- **Don't skip cleanup verification.** Many e2e bugs are leaked resources, not failed\n  assertions.\n- **Flag flakiness proactively.** A flaky test is worse than no test — it erodes trust in\n  the suite.\n- **Be specific in suggestions.** Don't say \"add more tests.\" Say exactly what scenario,\n  what inputs, and what assertion.\n","frontmatter":{"name":"e2e-migration-validator","description":"Use this skill whenever a user wants to validate, review, or audit e2e tests that have been migrated from an old framework (typically in a `e2e/` directory) to the new `e2e/` framework. Triggers include: \"review my migrated test\", \"validate this e2e test\", \"check my test migration\", \"did I migrate this correctly\", \"review e2e PR\", \"check test coverage after migration\", or any request involving comparing old vs new e2e test files across the vcluster, vcluster-pro, or loft-enterprise repos. Always use this skill when an e2e test file or PR from one of these repos is provided for review — even if the user just says \"take a look at this test\".\n"},"isInternal":false,"tokens":2212,"sizeBytes":9530},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# vCluster — Agent Instructions\n\n## E2E TDD Workflow\n\nWhen doing test-driven development, ALWAYS follow the workflow in\n[.claude/e2e-tdd-workflow.md](.claude/e2e-tdd-workflow.md).\nUse the AGENT_SESSION env var to isolate your commands per session.\n\n## CI / GitHub Actions\n\nShared CI logic lives in [`loft-sh/github-actions`](https://github.com/loft-sh/github-actions);\nproduction repos reuse those actions rather than inlining shell. The\n`github-actions-developer` skill (from `vcluster-skills-engineering`) auto-loads\nwhen you touch `.github/` and carries the full convention.\n","isInternal":false,"tokens":142,"sizeBytes":580},{"name":"README.md","path":"vendor/github.com/google/gnostic-models/extensions/README.md","rawUrl":"https://raw.githubusercontent.com/loft-sh/vcluster/HEAD/vendor/github.com/google/gnostic-models/extensions/README.md","title":"extensions Documentation","category":"plugin-manifest","format":"markdown","content":"# Extensions\n\n**Extension Support is experimental.**\n\nThis directory contains support code for building Gnostic extensio handlers and\nassociated examples.\n\nExtension handlers can be used to compile vendor or specification extensions\ninto protocol buffer structures.\n\nLike plugins, extension handlers are built as separate executables. Extension\nbodies are written to extension handlers as serialized\nExtensionHandlerRequests.\n","isInternal":false,"tokens":73,"sizeBytes":426}],"systemPromptSnippet":"<yakaai_skills repo=\"loft-sh/vcluster\">\n<!-- File: .claude/references/decomposition-heuristics.md (Tokens: ~1422 | Category: anthropic-skill) -->\n# Decomposition Heuristics\n\n## The Core Test: \"Is This One It()?\"\n\nA sub-problem is ready when you can express it as:\n\n```go\nIt(\"should {one observable behavior}\", func(ctx context.Context) {\n    By(\"{action 1}\")\n    By(\"{action 2}\")\n    By(\"{verification}\")\n    // At most 3 By() steps\n})\n```\n\nIf you need more than 3 `By()` steps, the sub-problem is too big. Split it.\n\n## Splitting Strategies\n\n### Strategy 1: Split by CRUD Operation\n\nA feature that creates, reads, updates, and deletes becomes 4 sub-problems:\n\n```\nSP-1: Create {resource} with valid input\nSP-2: Get {resource} returns expected fields\nSP-3: Update {resource} changes specific field\nSP-4: Delete {resource} cleans up dependents\n```\n\n### Strategy 2: Split by Actor\n\nWhen multiple roles interact with a feature:\n\n```\nSP-1: Admin can create {resource}\nSP-2: Project member can view {resource}\nSP-3: Non-member gets Forbidden\n```\n\n### Strategy 3: Split by State Transition\n\nFor lifecycle features (sleep mode, VCI phases):\n\n```\nSP-1: Resource starts in {initial state}\nSP-2: Trigger moves resource to {intermediate state}\nSP-3: Resource reaches {final state}\nSP-4: Error condition produces {error state}\n```\n\n### Strategy 4: Split by Integration Boundary\n\nWhen a feature touches multiple systems:\n\n```\nSP-1: Management API accepts the request (API layer)\nSP-2: Controller reconciles the resource (controller layer)\nSP-3: Agent reflects the change (agent layer)\n```\n\n### Strategy 5: Split by Input Variation\n\nWhen behavior depends on configuration:\n\n```\nSP-1: Default configuration produces {default behavior}\nSP-2: Custom configuration A produces {behavior A}\nSP-3: Invalid configuration is rejected with {error}\n```\n\n## Merging Rules\n\nMerge two sub-problems if:\n- They always modify the same lines of code\n- One is meaningless without the other (e.g., \"create\" and \"verify creation\" are one sub-problem)\n- The combined test would still have <= 3 `By()` steps\n\n## Ordering Rules (TDD)\n\n1. **Infrastructure first**: Types, stubs, code generation, new `setup/` builders, new labels, new constants — everything needed for tests to *compile* (not pass)\n2. **Test before implementation**: For each behavior, the test sub-problem is ordered before the implementation sub-problem\n3. **Happy-path test before error-path test**: Prove it works, then prove it fails correctly\n4. **Independent before dependent**: SP-3 that depends on SP-1 comes after SP-1\n5. **Mark parallel groups**: Sub-problems with no dependencies get the same group letter\n\n### The TDD Cycle Pattern\n\nEach behavior produces a test→impl pair:\n```\nSP-N: [test] Write test for behavior X           → RED  (test fails, behavior missing)\nSP-M: [impl] Implement behavior X                → GREEN (test passes)\n```\n\nInfrastructure sub-problems (SP-0) don't follow this cycle — they exist to make subsequent tests compilable. A stub handler that returns a zero value or error is infrastructure, not implementation.\n\nExample ordering:\n```\nSP-0: [infra] Types + stub handler + code generation      (group A)\nSP-1: [test]  Happy-path test: Create with defaults        (group B, depends: SP-0) → RED\nSP-2: [impl]  Implement Create handler                     (group B, depends: SP-1) → GREEN\nSP-3: [test]  Error-path test: Non-admin gets Forbidden    (group C, depends: SP-2) → RED\nSP-4: [impl]  Implement RBAC check                         (group C, depends: SP-3) → GREEN\nSP-5: [test]  Delete cleans up namespace                   (group D, depends: SP-2) → RED\nSP-6: [impl]  Implement Delete with cleanup                (group D, depends: SP-5) → GREEN\n```\n\n## Size Calibration\n\nTarget: each sub-problem takes 15-60 minutes to implement.\n\n| Too small (merge up) | Right size | Too big (split) |\n|----------------------|------------|-----------------|\n| \"Add an import\" | \"Create resource with validation\" | \"Implement full CRUD + RBAC\" |\n| \"Check one field\" | \"Verify state transition\" | \"Add feature end-to-end\" |\n| \"Set a label\" | \"Non-admin gets Forbidden\" | \"Multi-cluster replication\" |\n\n## The SP-0 Pattern\n\nWhen multiple sub-problems need shared infrastructure (a new builder, a new label, a helper function), extract it as SP-0:\n\n```\nSP-0: [infra] Create setup builder for {resource}\n  - New file: e2e/setup/{resource}/{resource}.go\n  - Options: WithGenerateName, With{Feature}\n  - CRUD: Create, Delete\n  - Context accessors: LastFrom, From\n```\n\nThis unblocks all downstream sub-problems and is itself testable (the builder compiles and the resource can be created/deleted).\n\n### SP-0 in TDD: Stubs and Scaffolding\n\nIn a TDD workflow, SP-0 often includes **stub implementations** — handlers that compile and register but return zero values or errors. This is intentional:\n\n```\nSP-0: [infra] Define API types + stub handler\n  - New type: VirtualClusterInstanceFoo with +subresource-request marker\n  - Run code generation (deepcopy, clientset, register)\n  - Stub REST handler: returns empty object or \"not implemented\" error\n  - Register stub in register.go\n  - Result: tests can compile and call the endpoint, but get wrong/empty results\n```\n\nThe stub exists solely so the test in SP-1 can compile and fail for the *right reason* (wrong response, not compile error). The real implementation comes in SP-2.\n\n## Red Flags in Decomposition\n\n- **\"And\" in the title**: \"Create resource and verify permissions\" → split\n- **Multiple actors**: \"Admin creates, user views\" → split by actor\n- **Multiple resources**: \"Create project with VCI and space\" → split by resource\n- **Conditional behavior**: \"If config X then A, else B\" → split by input variation\n- **\"End-to-end\"**: Almost always too big. What specific behavior are you testing?\n\n<!-- File: .claude/references/e2e-examples.md (Tokens: ~2983 | Category: anthropic-skill) -->\n# E2E Test Examples — Annotated Patterns\n\nConcrete excerpts from production tests in `e2e/`. These are ground truth — read them before writing new tests.\n\n---\n\n## 1. DeferCleanup Pattern\n\nRendered YAML cleanup and vCluster teardown are handled by `setup/lazyvcluster.LazyVCluster` - you do not register them by hand. `SynchronizedBeforeSuite` only provisions the host kind cluster now.\n\n```go\n// suite_myfeature_test.go — lazy helper owns YAML cleanup + vCluster teardown\nfunc suiteMyFeature() {\n    Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n        cluster.Use(clusters.HostCluster),\n        func() {\n            BeforeAll(func(ctx context.Context) context.Context {\n                return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n            })\n            // specs...\n        },\n    )\n}\n```\n\nInside `It` blocks - register cleanup immediately after resource creation, before any further assertions:\n\n```go\nBy(\"creating source namespace on host\", func() {\n    _, err := hostClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{\n        ObjectMeta: metav1.ObjectMeta{Name: fromNS},\n    }, metav1.CreateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(func(ctx context.Context) {\n        Expect(hostClient.CoreV1().Namespaces().Delete(ctx, fromNS, metav1.DeleteOptions{})).To(Succeed())\n        Eventually(func(g Gomega) {\n            _, err := hostClient.CoreV1().Namespaces().Get(ctx, fromNS, metav1.GetOptions{})\n            g.Expect(kerrors.IsNotFound(err)).To(BeTrue())\n        }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n    })\n})\n```\n\n---\n\n## 2. Eventually + Polling\n\nSource: `e2e/test_core/sync/test_servicesync.go`, `e2e/test_deploy/test_helm_charts.go`\n\nUse the `g Gomega` parameter inside `Eventually`. Bare `Expect()` inside `Eventually` panics instead of retrying. Include failure context for debuggability.\n\n**Waiting for a resource to appear:**\n\n```go\nvar toService *corev1.Service\nBy(\"waiting for the replicated service to appear in vcluster\", func() {\n    Eventually(func(g Gomega) {\n        toService, err = vClusterClient.CoreV1().Services(toNS).Get(ctx, toName, metav1.GetOptions{})\n        g.Expect(err).NotTo(HaveOccurred())\n    }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n})\n```\n\n**Waiting for a resource to be deleted:**\n\n```go\nBy(\"waiting for the replicated service to be removed from vcluster\", func() {\n    Eventually(func(g Gomega) {\n        _, err := vClusterClient.CoreV1().Services(toNS).Get(ctx, toName, metav1.GetOptions{})\n        g.Expect(kerrors.IsNotFound(err)).To(BeTrue(), \"replicated service should be deleted after source is gone\")\n    }).WithPolling(constants.PollingInterval).WithTimeout(constants.PollingTimeoutLong).Should(Succeed())\n})\n```\n\n**Checking deploy status with descriptive failure messages:**\n\n```go\nEventually(func(g Gomega) {\n    cm, err := vClusterClient.CoreV1().ConfigMaps(deploy.VClusterDeployConfigMapNamespace).\n        Get(ctx, deploy.VClusterDeployConfigMap, metav1.GetOptions{})\n    g.Expect(err).NotTo(HaveOccurred(), \"Deploy configmap should exist\")\n    status := deploy.ParseStatus(cm)\n    g.Expect(status.Charts).To(HaveLen(2), \"Should have 2 charts configured\")\n    for _, chart := range status.Charts {\n        g.Expect(chart.Phase).To(\n            Equal(string(deploy.StatusSuccess)),\n            fmt.Sprintf(\"Chart %s is not in Success phase, got phase=%s, reason=%s, message=%s\",\n                chart.Name, chart.Phase, chart.Reason, chart.Message))\n    }\n}).\n    WithPolling(constants.PollingInterval).\n    WithTimeout(constants.PollingTimeout).\n    Should(Succeed(), \"Both charts should be successfully deployed\")\n```\n\n**Returning a value from Eventually for direct assertion:**\n\n```go\nEventually(func(g Gomega) []appsv1.Deployment {\n    deployList, err := vClusterClient.AppsV1().Deployments(ChartOCINamespace).List(ctx, metav1.ListOptions{\n        LabelSelector: k8slabels.SelectorFromSet(HelmOCIDeploymentLabels).String(),\n    })\n    g.Expect(err).NotTo(HaveOccurred(), \"Should be able to list deployments\")\n    return deployList.Items\n}).\n    WithPolling(constants.PollingInterval).\n    WithTimeout(constants.PollingTimeout).\n    Should(HaveLen(1), \"Should have exactly one fluent-bit deployment\")\n```\n\n---\n\n## 3. Cluster Client Usage\n\nSource: `e2e/test_core/sync/test_node.go`\n\nObtain host and vcluster clients from context. Use `cluster.KubeClientFrom` with the host cluster name for the host client, and `cluster.CurrentKubeClientFrom` for the current vcluster client.\n\n```go\nvar _ = Describe(\"Node sync\",\n    labels.Core,\n    labels.Sync,\n    cluster.Use(clusters.NodesVCluster),\n    cluster.Use(clusters.HostCluster),\n    func() {\n        var (\n            hostClient     kubernetes.Interface\n            vClusterClient kubernetes.Interface\n        )\n\n        BeforeEach(func(ctx context.Context) {\n            hostClient = cluster.KubeClientFrom(ctx, constants.GetHostClusterName())\n            Expect(hostClient).NotTo(BeNil())\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Sync nodes using label selector\", func(ctx context.Context) {\n            hostname := constants.GetHostClusterName() + \"-control-plane\"\n            Eventually(func(g Gomega) {\n                hostNodes, err := hostClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"Failed to list host nodes\")\n\n                virtualNodes, err := vClusterClient.CoreV1().Nodes().List(ctx, metav1.ListOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"Failed to list virtual nodes\")\n                g.Expect(virtualNodes.Items).ToNot(BeEmpty(), \"Virtual nodes list should not be empty\")\n\n                // ... assertions comparing host and virtual nodes\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Node sync should work correctly\")\n        })\n    })\n```\n\n---\n\n## 4. Ordered Usage\n\nSource: `test/e2e/snapshot/snapshot.go` (old suite — pattern, not import paths)\n\nUse `Ordered` when specs form a **lifecycle sequence** where later specs depend on state produced by earlier specs, **and** shared setup is genuinely expensive (resource creation, deployment readiness, data provisioning). The snapshot-with-volumes test is the canonical example: `BeforeAll` provisions a namespace, a PVC with written data, and several Kubernetes resources, then each `It` block advances through create → verify → delete → restore → verify-restored.\n\n```go\nDescribe(\"controller-based snapshot with volumes\", Ordered, func() {\n    const (\n        controllerTestNamespaceName = \"volume-snapshots-test\"\n        snapshotPath                = \"container:///snapshot-data/\" + controllerTestNamespaceName + \".tar.gz\"\n        pvcToRestoreName            = \"test-pvc-restore\"\n        testFileName                = controllerTestNamespaceName + \".txt\"\n        pvcData                     = \"Hello \" + controllerTestNamespaceName\n    )\n\n    // BeforeAll — expensive: creates namespace, PVC with data written via a pod,\n    // plus Deployments, Services, ConfigMaps, and Secrets.\n    BeforeAll(func(ctx context.Context) {\n        f = framework.DefaultFramework\n        deployTestNamespace(controllerTestNamespaceName)\n        // Creates PVC, spins up a writer pod, waits for pod readiness, writes data\n        createPVCWithData(ctx, f.VClusterClient, controllerTestNamespaceName,\n            pvcToRestoreName, testFileName, pvcData)\n        // Creates Deployment (nginx), Service, ConfigMap, Secret — waits for deploy ready\n        deployTestResources(controllerTestNamespaceName, true)\n    })\n\n    // Spec 1: triggers snapshot creation — later specs verify the snapshot exists\n    It(\"Creates the snapshot request\", func() {\n        createSnapshot(f, true, snapshotPath, true)\n    })\n\n    // Spec 2: depends on spec 1 — polls until snapshot completes\n    It(\"Creates the snapshot\", func(ctx context.Context) {\n        waitForSnapshotToBeCreated(ctx, f)\n    })\n\n    // Spec 3: depends on spec 2 — verifies VolumeSnapshot resources were cleaned up\n    It(\"Doesn't contain VolumeSnapshot and VolumeSnapshotContent\", func(ctx context.Context) {\n        // ... verifies cleanup of temporary snapshot objects\n    })\n\n    // Spec 4: depends on spec 2 — deletes PVC so restore can recreate it\n    It(\"Deletes the PVC with test data\", func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n    })\n\n    // checkTestResources embeds two It blocks:\n    //   \"Verify if only the resources in snapshot are available after restore\"\n    //   \"Verify if deleted resources are recreated after restore\"\n    // Both depend on prior specs having created and snapshotted resources.\n    checkTestResources(controllerTestNamespaceName, true, snapshotPath)\n\n    // Spec 7: depends on PVC being restored (without data) by checkTestResources\n    It(\"restores vCluster with volumes\", func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n        restoreVCluster(ctx, f, snapshotPath, true, true)\n    })\n\n    // Spec 8: depends on spec 7 — verifies PVC is bound after volume restore\n    It(\"has the restored PVC which is bound\", func(ctx context.Context) {\n        Eventually(func(g Gomega, ctx context.Context) corev1.PersistentVolumeClaimPhase {\n            restoredPVC, err := f.VClusterClient.CoreV1().PersistentVolumeClaims(\n                controllerTestNamespaceName).Get(ctx, pvcToRestoreName, metav1.GetOptions{})\n            g.Expect(err).NotTo(HaveOccurred())\n            return restoredPVC.Status.Phase\n        }).WithContext(ctx).\n            WithPolling(framework.PollInterval).\n            WithTimeout(framework.PollTimeoutLong).\n            Should(Equal(corev1.ClaimBound))\n    })\n\n    // Spec 9: depends on spec 8 — verifies data survived the snapshot/restore cycle\n    It(\"has the restored PVC with data from the volume snapshot\", func(ctx context.Context) {\n        checkPVCData(ctx, f.VClusterClient, controllerTestNamespaceName,\n            pvcToRestoreName, testFileName, pvcData)\n    })\n\n    AfterAll(func(ctx context.Context) {\n        deletePVC(ctx, f, controllerTestNamespaceName, pvcToRestoreName)\n        cleanUpTestResources(ctx, true, controllerTestNamespaceName)\n    })\n})\n```\n\n**Why `Ordered` is justified here:** Each spec advances a lifecycle (provision → snapshot → cleanup → restore → verify) where later specs depend on side effects from earlier ones. The `BeforeAll` is genuinely expensive — it creates a namespace, provisions a PVC, spins up a writer pod to populate data, and deploys multiple resources with readiness checks. Contrast with the `BeforeEach` examples below, where specs are independent and only need client references.\n\n---\n\n## 5. BeforeEach Pattern\n\nSource: `e2e/test_deploy/test_helm_charts.go`, `e2e/test_deploy/test_init_manifests.go`\n\nUse `BeforeEach` when specs are independent and only need shared client setup. Each spec operates on pre-existing state (deployed charts, init manifests) and doesn't modify shared resources.\n\n```go\nvar _ = Describe(\"Helm charts (regular and OCI) are synced and applied as expected\",\n    labels.Deploy,\n    cluster.Use(clusters.HelmChartsVCluster),\n    func() {\n        var vClusterClient kubernetes.Interface\n\n        BeforeEach(func(ctx context.Context) {\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Test if configmap for both charts gets applied\", func(ctx context.Context) {\n            Eventually(func(g Gomega) {\n                // ... check deploy configmap status\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Both charts should be successfully deployed\")\n        })\n\n        It(\"Test nginx release secret existence in vcluster (regular chart)\", func(ctx context.Context) {\n            // ... independent verification\n        })\n    })\n```\n\n**BeforeEach returning context** (from `test_init_manifests.go`):\n\n```go\nvar _ = Describe(\"Init manifests are synced and applied as expected\",\n    labels.Deploy,\n    cluster.Use(clusters.InitManifestsVCluster),\n    func() {\n        var (\n            vClusterName   = clusters.InitManifestsVClusterName\n            vClusterClient kubernetes.Interface\n        )\n\n        BeforeEach(func(ctx context.Context) {\n            vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n            Expect(vClusterClient).NotTo(BeNil())\n        })\n\n        It(\"Test if manifest template is synced with the vcluster\", func(ctx context.Context) {\n            Eventually(func(g Gomega) {\n                manifest, err := vClusterClient.CoreV1().ConfigMaps(TestManifestNamespace).\n                    Get(ctx, TestManifestName2, metav1.GetOptions{})\n                g.Expect(err).NotTo(HaveOccurred(), \"ConfigMap should exist\")\n                g.Expect(manifest.Data[\"foo\"]).To(Equal(vClusterName),\n                    \"ConfigMap foo value should equal vcluster name\")\n            }).\n                WithPolling(constants.PollingInterval).\n                WithTimeout(constants.PollingTimeout).\n                Should(Succeed(), \"Manifest template should be synced\")\n        })\n    },\n)\n```\n\n<!-- File: .claude/references/e2e-framework-suite-deps.md (Tokens: ~1307 | Category: anthropic-skill) -->\n# e2e-framework: External Infrastructure Patterns\n\nTwo distinct patterns depending on what needs provisioning. Pick the right one before writing the plan.\n\n## Service-level provisioning (Helm charts, external APIs)\n\nUse this for: ingress-nginx, fluent-bit, metrics-server, cert-manager, CSI drivers, or any service installed into the test cluster.\n\nInstall in `BeforeAll`, tear down in `AfterAll`. Never assume the service is pre-provisioned.\n\n```go\nBeforeAll(func(ctx context.Context) context.Context {\n    By(\"installing <service>\")\n    ctx, err := setuphelm.Upgrade(\n        setuphelm.WithHelmOptions(\n            constants.Get<Service>HelmOptions()...,\n            helm.WithWait(),\n        ),\n    )(ctx)\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(...)\n    return ctx\n})\n\nAfterAll(func(ctx context.Context) {\n    _, err := setuphelm.Uninstall(\n        setuphelm.WithHelmOptions(\n            helm.WithName(\"<release>\"),\n            helm.WithNamespace(\"<ns>\"),\n        ),\n    )(ctx)\n    Expect(err).NotTo(HaveOccurred())\n    _, err = namespace.Delete(\"<ns>\")(ctx)\n    Expect(err).NotTo(HaveOccurred())\n})\n```\n\n**Why `AfterAll` and not `DeferCleanup`**: `DeferCleanup` in `BeforeAll` runs after all specs, which is equivalent here — but `AfterAll` is explicit and survives `--setup-only` runs where you want the service to persist between dev iterations.\n\n**Chart constants**: Add a `e2e/constants/<service>.go` with `Get<Service>HelmOptions()` following the pattern in existing constants files.\n\n---\n\n## Cluster-level provisioning (Kind host, vClusters)\n\nUse this for: tests that need a full ephemeral Kubernetes cluster or vCluster, not a service inside an existing cluster.\n\n**Two-layer model:**\n\n1. **Host kind cluster** - eagerly provisioned once in `SynchronizedBeforeSuite` via the framework's `cluster.Define` + `cluster.Setup`. Defined in `e2e/clusters/registry.go` as `clusters.HostCluster`. Reused by every per-test vCluster.\n2. **Per-test vClusters** - created lazily, one per `suite_*_test.go`, in the suite's outer `BeforeAll` via `setup/lazyvcluster.LazyVCluster`. The helper is a thin wrapper over the framework's `vcluster.Create` (see `github.com/loft-sh/e2e-framework/pkg/setup/vcluster`). Peak concurrent vClusters is bounded by `ginkgo --procs`, not by the number of suite files.\n\n```go\n// e2e/suite_myfeature_test.go\n//go:embed vcluster-myfeature.yaml\nvar myFeatureYAML string\n\nconst myFeatureName = \"myfeature-vcluster\"\n\nfunc init() { suiteMyFeature() }\n\nfunc suiteMyFeature() {\n    Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n        cluster.Use(clusters.HostCluster),\n        func() {\n            BeforeAll(func(ctx context.Context) context.Context {\n                return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML,\n                    // optional: host-side prerequisites (CRDs, PVCs, Helm install)\n                    lazyvcluster.WithPreSetup(setup.MyPrereq()),\n                )\n            })\n\n            // spec functions...\n        },\n    )\n}\n```\n\nInside specs: `cluster.CurrentKubeClientFrom(ctx)` resolves to the suite's vCluster. `cluster.KubeClientFrom(ctx, constants.GetHostClusterName())` resolves to the host.\n\n**Failure-aware teardown**: on spec failure the framework keeps the vCluster alive and attaches diagnostics (rendered config, pods, events, syncer logs) to the failing spec's report entries via `AddReportEntry`. On pass, teardown destroys the vCluster.\n\n**Focus is automatic**: a label-filtered run only materializes vClusters whose outer suite Describe matches. Unmatched suites' `BeforeAll` never fires, so no vCluster is spun up for tests that do not need it.\n\n---\n\n## Identifying what needs provisioning\n\nWhen migrating an old test, check these files in order:\n\n1. **Old test's `values.yaml`** — Look for `controlPlane.helmRelease` entries (charts the vcluster deploys internally), `experimental.deploy.vcluster.helm` sections, and any inline manifests in `experimental.deploy.vcluster.manifests`.\n2. **`host-resources.yaml`** (if present) — PVCs for volume snapshots, CRDs, or other host-cluster prerequisites.\n3. **`.github/workflows/e2e.yaml`** — CI workflow steps that install external dependencies before running the test suite.\n\n### Known External Dependencies by Suite\n\n| Dependency | Suites | Notes |\n|---|---|---|\n| CSI volume snapshots | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Snapshot CRDs + controller |\n| ingress-nginx 4.14.2 | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Ingress controller for service exposure tests |\n| fluent-bit 3.1.13 (OCI) | e2e, e2e_node, e2e_scheduler, e2e_isolation_mode, e2e_rootless | Deployed as Helm chart inside vcluster |\n| metrics-server | e2e_metrics_proxy | Required for metrics API proxy tests |\n| Plugin images | e2e_plugin | Custom plugin container images |\n\n### Dependency-Free Suites\n\nThese suites have no external service dependencies:\n- `e2e_certs`\n- `e2e_cli`\n- `e2e_ha`\n- `e2e_limit_classes`\n- `e2e_pause_resume`\n\n**How to read the old test setup**: Look at the suite's CI workflow step. Each `helm install` or `kubectl apply` in the workflow's pre-test phase becomes either a `setuphelm.Upgrade()` call in `BeforeAll` or a cluster-level dependency via `cluster.Define`/`vcluster.Define`. The vcluster's `values.yaml` shows what config was baked into the vcluster itself — this becomes embedded YAML in `clusters/*.go`.\n\n<!-- File: .claude/references/e2e-old-to-new-mapping.md (Tokens: ~1273 | Category: anthropic-skill) -->\n# Old → New Pattern Mapping (vcluster)\n\nTranslation table for migrating tests from `test/framework` to `e2e/`.\n\n## Client Accessors\n\n| Old Pattern (`test/framework`) | New Pattern (`e2e`) |\n|---|---|\n| `framework.DefaultFramework` global singleton | Context-based: `cluster.KubeClientFrom(ctx, name)`, `cluster.CurrentKubeClientFrom(ctx)` |\n| `f.HostClient` / `f.HostCRClient` | `cluster.KubeClientFrom(ctx, constants.GetHostClusterName())` |\n| `f.VClusterClient` / `f.VClusterCRClient` | `cluster.CurrentKubeClientFrom(ctx)` / `cluster.CurrentClusterClientFrom(ctx)` |\n| `f.VClusterName` / `f.VClusterNamespace` | Derived from cluster definition name in `clusters/` (e.g., `clusters.ServiceSyncVClusterName`) |\n| `os.Getenv(\"VCLUSTER_NAME\")` etc. | Cluster definitions with typed config in `constants/` |\n| `f.Suffix` / `translate.VClusterName` | `translate` package used directly where needed |\n\n## Setup and Lifecycle\n\n| Old Pattern | New Pattern |\n|---|---|\n| `framework.CreateFramework(ctx)` in `BeforeSuite` | `SynchronizedBeforeSuite` with `clusters.HostCluster.Setup` + `clusters.XVCluster.Setup` |\n| `f.Cleanup()` in `AfterSuite` | `SynchronizedAfterSuite` with `clusters.HostCluster.Teardown` |\n| `f.RefreshVirtualClient()` | `connectVCluster()` — see \"Reconnecting after destructive operations\" below |\n| Per-suite `values.yaml` | Per-cluster embedded YAML in `clusters/*.go` with `template.MustRender()` |\n| Single vcluster per suite | Multiple vclusters with different configs (DefaultVCluster, NodesVCluster, etc.) |\n\n## Test Execution\n\n| Old Pattern | New Pattern |\n|---|---|\n| `go test -v -ginkgo.v` | `ginkgo --label-filter=\"...\" ./e2e` |\n| `time.Sleep()` | `Eventually().WithPolling().WithTimeout()` |\n| Ad-hoc durations | `constants.PollingTimeout*` constants |\n| Env-based config (`VCLUSTER_SUFFIX`, etc.) | Flag-based config (`--vcluster-image`, `--cluster-name`) |\n\n## Timeouts\n\n| Old Constant | New Constant | Duration |\n|---|---|---|\n| `framework.PollInterval` (5s) | `constants.PollingInterval` | 2s |\n| `framework.PollTimeout` (1min) | `constants.PollingTimeout` | 60s |\n| `framework.PollTimeoutLong` (2min) | `constants.PollingTimeoutLong` | 120s |\n| N/A | `constants.PollingTimeoutVeryShort` | 5s |\n| N/A | `constants.PollingTimeoutShort` | 20s |\n| N/A | `constants.PollingTimeoutVeryLong` | 300s |\n\n## Cluster Definitions\n\nThe old framework uses a single vcluster per suite. The new framework defines multiple vclusters in `e2e/clusters/clusters.go`:\n\n```go\n// Host cluster (Kind)\nclusters.HostCluster = cluster.Define(\n    cluster.WithName(constants.GetHostClusterName()),\n    cluster.WithProvider(kind.NewProvider()),\n    cluster.WithConfigFile(\"e2e-kind.config.yaml\"),\n)\n\n// Virtual clusters — each with specific config\nclusters.DefaultVCluster = vcluster.Define(\n    vcluster.WithName(\"default-vcluster\"),\n    vcluster.WithVClusterYAML(DefaultVClusterYAML),\n    vcluster.WithOptions(DefaultVClusterOptions...),\n    vcluster.WithDependencies(HostCluster),\n)\n```\n\nUse `cluster.Use(clusters.XVCluster)` in `Describe` to wire a test to a specific cluster:\n\n```go\nvar _ = Describe(\"My test\",\n    labels.Core,\n    cluster.Use(clusters.DefaultVCluster),\n    cluster.Use(clusters.HostCluster),\n    func() { ... },\n)\n```\n\n## Reconnecting After Destructive Operations\n\nThe suite-level background proxy (established in `SynchronizedBeforeSuite`) does **not** survive\nvcluster pause/resume or cert rotation. `cluster.CurrentKubeClientFrom(ctx)` and\n`cluster.CurrentClusterFrom(ctx).KubernetesRestConfig()` both point to the suite proxy — if the\nvcluster has been paused, restarted, or had its certs rotated, these return dead connections.\n\nThe old framework's `f.RefreshVirtualClient()` re-established the connection. The e2e\nequivalent is to run `vcluster connect` programmatically with a background proxy:\n\n1. Create a temp file for the kubeconfig\n2. Build a `connectcmd.ConnectCmd` with `BackgroundProxy: true`, `KubeConfig` pointing to the temp file, and `BackgroundProxyImage` set to `constants.GetVClusterImage()` — the same image already loaded into Kind\n3. Call `cmd.Run(ctx, []string{vclusterName})`\n4. Poll with `Eventually` until the kubeconfig file is non-empty and `clientcmd.RESTConfigFromKubeConfig` succeeds\n5. Verify the connection by making a lightweight API call (e.g., `Get` the `default` ServiceAccount)\n6. Return the `*rest.Config` and a cleanup func that removes the temp file\n\n**Critical: use `constants.GetVClusterImage()` for `BackgroundProxyImage`**, not\n`vclusterconstants.DefaultBackgroundProxyImage(upgrade.GetVersion())`.\n\nKey imports: `connectcmd \"github.com/loft-sh/vcluster/cmd/vclusterctl/cmd\"`,\n`\"github.com/loft-sh/vcluster/pkg/cli\"`, `\"github.com/loft-sh/vcluster/pkg/cli/flags\"`,\n`loftlog \"github.com/loft-sh/log\"`,\n`\"github.com/spf13/cobra\"`, `\"k8s.io/client-go/tools/clientcmd\"`,\n`\"github.com/loft-sh/vcluster/e2e/constants\"`.\n\n<!-- File: .claude/rules/e2e-conventions.md (Tokens: ~2413 | Category: anthropic-skill) -->\n---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-conventions-core.md -->\n\n# e2e Test Conventions\n\nGinkgo v2 + Gomega, running against vCluster instances on Kind. Read existing tests in `e2e/test_*/` for patterns before writing new ones.\n\n## Key Conventions\n\n1. **Random suffixes** — Always use `objectmeta.GenerateName` or `random.RandomString(6)` for resource names to avoid collisions in parallel runs. Try to share the suffix across a single test spec so all resources of a single test use the same suffix. This applies to **all** identifiers that could collide, not just Kubernetes object names — Helm release names, database names, etc. If an identifier is a package-level `const` and would conflict across parallel runs, make it dynamic.\n2. **Context propagation** — Setup helpers store objects in context; retrieve with `<resource>.From(ctx, name)`.\n3. **Eventually for async** — vCluster operations are eventually consistent. Always wrap API checks in `Eventually` with `constants.*` timeouts. Never hardcode durations.\n4. **Labels for filtering** — Tag `Describe` with resource labels and `labels.PR` if all specs in it should gate PRs. Only tag individual `It` blocks with `labels.PR` when some specs in the `Describe` should NOT gate PRs. Never duplicate a label on an `It` that is already present on its enclosing `Describe` or `Context` — Ginkgo inherits labels, so duplicates are redundant noise.\n5. **Ordered contexts** — See the decision table below. Default to `BeforeEach`; only use `Ordered` for true sequential dependencies.\n6. **Package registration** — Test packages self-register via `var _ = Describe(...)`. The suite imports them as blank imports in `e2e_suite_test.go`.\n7. **Error assertion** — Prefer `ginkgo.Expect(...).To(ginkgo.Succeed())` over `ginkgo.Expect(...).NotTo(ginkgo.HaveOccurred())`\n8. **gstruct usage** — Do not use `gstruct` for simple field assertions (use `Expect(obj.Field).To(Equal(...))` instead). Use `gstruct` with `ContainElement` or similar matchers when asserting on elements within a collection (slice, map) to avoid manual loops.\n\n## Setup Helpers\n\nvCluster's `e2e/` does not yet have a rich `setup/` builder library like loft-enterprise. During migration:\n\n- Use existing **cluster definitions** from `e2e/clusters/` for test environment setup. Browse `clusters/clusters.go` for available vclusters with different configurations.\n- Use the `setup/template` package (`template.MustRender()`) for rendering vcluster YAML with image overrides.\n- When a setup pattern repeats across multiple tests, create a shared setup helper as an `[infra]` sub-problem. Place it in `e2e/setup/` following the functional-options pattern.\n- For now, most test setup is done inline using the Kubernetes client directly (create namespace, create resource, DeferCleanup).\n\n### Scoping Constants and Helpers\n\nTest-specific constants, helper functions, and option builders that are only used by a single `test_*` package belong **in that package**, not in the shared `constants` or `setup` packages. Only promote to a shared package when a second consumer appears.\n\n### External Service Provisioning\n\nIf the old test installs Helm charts or external services (via `values.yaml` `controlPlane.helmRelease`, host resources, or CI workflow steps), flag this as an `[infra]` sub-problem during migration. The external service needs to be provisioned self-contained within the test using `setuphelm.Upgrade()` in `BeforeAll` or via the cluster dependency system.\n\n## vCluster Configuration Patterns\n\nEach vCluster lives next to the `suite_*_test.go` that uses it. `HostCluster` (the kind host) is the only eagerly-provisioned dependency in `SynchronizedBeforeSuite`; every per-test vCluster is created lazily in the suite's own `BeforeAll` via `setup/lazyvcluster.LazyVCluster`, which is a thin wrapper over the framework's `vcluster.Create`.\n\n### Embedded YAML Templates\n\nEmbed the `vcluster.yaml` in the suite file. Templates support `{{.Repository}}`, `{{.Tag}}`, `{{.HostClusterName}}` placeholders - the lazy helper renders them at `BeforeAll` time and registers temp-file cleanup automatically.\n\n### Defining a New vCluster Suite\n\n1. Create `e2e/vcluster-myfeature.yaml` with the `vcluster.yaml` config.\n2. Create `e2e/suite_myfeature_test.go`:\n   ```go\n   //go:embed vcluster-myfeature.yaml\n   var myFeatureYAML string\n\n   const myFeatureName = \"myfeature-vcluster\"\n\n   func init() { suiteMyFeature() }\n\n   func suiteMyFeature() {\n       Describe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n           cluster.Use(clusters.HostCluster),\n           func() {\n               BeforeAll(func(ctx context.Context) context.Context {\n                   return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n               })\n               // spec functions...\n           },\n       )\n   }\n   ```\n3. If a new filter label is needed, add it to `labels/labels.go`. `labels.PR` goes on the outer suite (never on specs).\n4. If the vCluster needs host-side prerequisites (CRDs, PVCs, Helm install), pass `lazyvcluster.WithPreSetup(fn)`. Reusable helpers live in `setup/` (`setup.SnapshotPreSetup`, `setup.MetricsServerPreSetup`).\n\nDo NOT add entries to `clusters/` - that package only holds `HostCluster` and `DefaultVClusterOptions`.\n\n## Client Accessors\n\n| Accessor | Type | Use for |\n|----------|------|---------|\n| `cluster.KubeClientFrom(ctx, clusterName)` | `kubernetes.Interface` | Host cluster typed client |\n| `cluster.CurrentKubeClientFrom(ctx)` | `kubernetes.Interface` | Current vcluster typed client |\n| `cluster.CurrentClusterClientFrom(ctx)` | `client.Client` (controller-runtime) | Current cluster CR client |\n\n## Background Proxy Limitations\n\nThe suite-level background proxy (started by `vcluster connect` during `SynchronizedBeforeSuite`)\nis a **one-shot process**. It dies when:\n\n- The vcluster pod is paused/resumed (e.g., `certs rotate`, `certs rotate-ca`, helm upgrade)\n- The vcluster's CA certificate changes\n- The vcluster pod restarts for any reason\n\nAfter any of these, `cluster.CurrentKubeClientFrom(ctx)` and\n`cluster.CurrentClusterFrom(ctx).KubernetesRestConfig()` return connections to a dead proxy.\n\n**If your test does destructive vcluster operations** (cert rotation, restart, config change),\nyou must establish a fresh connection using the `connectVCluster()` pattern documented in\n`.claude/references/e2e-old-to-new-mapping.md` § \"Reconnecting After Destructive Operations\".\n\n**Common pitfall:** When building `ConnectOptions`, always set `BackgroundProxyImage` to\n`constants.GetVClusterImage()` — **never** use `DefaultBackgroundProxyImage(upgrade.GetVersion())`.\nIn dev builds the latter produces an invalid Docker image ref (empty tag), Docker rejects it,\nand the code silently falls back to in-process port-forwarding that hangs the test process.\n\n## Cleanup\n\nFor non-Ordered contexts, return the enriched context from `BeforeEach`:\n\n```go\nBeforeEach(func(ctx context.Context) context.Context {\n    vClusterClient = cluster.CurrentKubeClientFrom(ctx)\n    Expect(vClusterClient).NotTo(BeNil())\n    return ctx\n})\n```\n\nInside `It` blocks, register cleanup immediately after creation:\n\n```go\nIt(\"tests something\", func(ctx context.Context) {\n    _, err := vClusterClient.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{\n        ObjectMeta: metav1.ObjectMeta{Name: nsName},\n    }, metav1.CreateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n    DeferCleanup(func(ctx context.Context) {\n        err := vClusterClient.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n        Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n    })\n\n    // ... test logic\n})\n```\n\n## Labels\n\nAttach labels to `Describe`, `Context`, or `It`. Check `e2e/labels/labels.go` for the full list. Key ones:\n- `labels.PR` — gate tests that run on every PR\n- `labels.Core`, `labels.Sync`, `labels.Deploy` — feature-area labels\n\n## Ordered vs. BeforeEach\n\nDefault to `BeforeEach`. The suite will be parallelized — every unnecessary `Ordered` is a bottleneck.\n\n| Signal | Pattern | Why |\n|---|---|---|\n| Specs are independent, just share setup | `BeforeEach` (no `Ordered`) | Parallelizable; no cascade failures |\n| Specs form a lifecycle sequence (create → mutate → verify → delete) | `Ordered` + `BeforeAll` | Specs depend on prior spec's side effects |\n| Setup is expensive (databases, vclusters with deployed services) | `BeforeEach` — pay the cost per spec | Parallel execution recovers wall time; isolation prevents false failures |\n| A spec deletes or mutates the shared resource | `BeforeEach` with per-spec resource | Other specs can't rely on it existing |\n| Specs must run in a fixed order but don't share mutable state | **Rethink** — probably independent specs | `Ordered` without true dependencies is a parallelization bottleneck |\n\n> **Hard rule:** If you use `Ordered`, add a comment explaining which spec depends on which prior spec's side effect. If you can't name one, drop `Ordered`.\n\n## Concrete Examples\n\nSee `.claude/references/e2e-examples.md` for annotated excerpts from production tests covering: DeferCleanup placement, `Eventually` with `g Gomega`, `Ordered` vs `BeforeEach` side-by-side, and cluster client usage.\n\n## No Plan Artifacts in Code\n\nNever include migration plan identifiers (e.g., `SP-0`, `SP-1`, `[migrate]`, `[infra]`, `[cleanup]`, `[consolidate]`) in code comments, `By()` text, or test descriptions. These are internal planning artifacts — the code should read as if no plan ever existed.\n\n```go\n// FAIL — plan artifact leaked into comment\n// SP-2: verify configmap is synced to host\nBy(\"SP-2: Checking configmap sync\", func() { /* ... */ })\n\n// PASS — describes intent without plan references\n// Verify the configmap is synced to the host cluster\nBy(\"Checking that the configmap is synced to the host cluster\", func() { /* ... */ })\n```\n\n## New Test Checklist\n\n1. Place the file in the appropriate `e2e/test_*` directory. Package name must match sibling files.\n2. If creating a **new** `test_*` package, add a blank import in `e2e_suite_test.go`.\n3. If testing a **new** resource type, add a label constant in `e2e/labels/labels.go`.\n4. Use an existing test file in the same package as your starting template.\n\n<!-- File: .claude/rules/e2e-error-handling.md (Tokens: ~452 | Category: anthropic-skill) -->\n---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-error-handling.md (identical) -->\n\n# e2e Error Handling\n\n## Cleanup: only tolerate IsNotFound, assert everything else\n\nSingle-operation cleanup:\n\n```go\n// GOOD\nDeferCleanup(func(ctx context.Context) {\n    err := client.Delete(ctx, name, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n```\n\nMulti-step cleanup (get → mutate → delete):\n\n```go\n// GOOD — check IsNotFound on Get, assert all other errors at every step\nDeferCleanup(func(ctx context.Context) {\n    obj, err := client.Get(ctx, name, metav1.GetOptions{})\n    if kerrors.IsNotFound(err) {\n        return // already gone\n    }\n    Expect(err).NotTo(HaveOccurred())\n\n    delete(obj.Annotations, someAnnotation)\n    _, err = client.Update(ctx, obj, metav1.UpdateOptions{})\n    Expect(err).NotTo(HaveOccurred())\n\n    err = client.Delete(ctx, name, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n```\n\n**When NOT to use `IgnoreNotFound`:** `IgnoreNotFound` is appropriate for resources that may have been deleted by cascade, by a prior spec, or by a controller. For resources the test **just created in the same `It` block** and expects to still exist at cleanup time, prefer a strict assertion — `Expect(err).NotTo(HaveOccurred())`. A `NotFound` in this case signals a test bug or unexpected controller behavior, not a benign race.\n\nNever use these patterns:\n- `_, _ =` to discard errors — silent failures mask regressions and leak resources\n- `if err \\!= nil { return }` to bail on any error — this swallows connectivity/RBAC failures, not just NotFound\n\n## Error assertions: assert the specific error, not just occurrence\n\n```go\n// BAD — passes on connectivity errors, RBAC failures, or any unrelated error\nExpect(err).To(HaveOccurred())\n\n// GOOD\nExpect(err).To(MatchError(ContainSubstring(\"already exists\")))\n```\n\n<!-- File: .claude/rules/e2e-quality-checklist.md (Tokens: ~1614 | Category: anthropic-skill) -->\n---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core (items 1-8): e2e-tdd-workflow plugin references/e2e-quality-checklist-core.md -->\n\n# E2E Test Quality Checklist (e2e)\n\nEvery item is **pass/fail**. A test must pass all 10 items.\n\n---\n\n## 1. Cleanup Tolerates Already-Deleted Resources\n\nCleanup code must not fail if the resource was already removed (e.g., cascade-deleted\nby a parent). Use `client.IgnoreNotFound` or equivalent.\n\n```go\n// PASS\nDeferCleanup(func(ctx context.Context) {\n    err := client.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n\n// FAIL — hard-fails if resource is already gone\nDeferCleanup(func(ctx context.Context) {\n    Expect(client.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})).To(Succeed())\n})\n```\n\n---\n\n## 2. DeferCleanup Registered Before Subsequent Assertions\n\n`DeferCleanup` must be the very next statement after verifying creation succeeded.\nIf an assertion between creation and cleanup registration fails, the resource leaks\nand can block teardown of the entire suite.\n\n```go\n// PASS — cleanup registered immediately, before any further assertions\n_, err := vClusterClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})\nExpect(err).NotTo(HaveOccurred())\nDeferCleanup(func(ctx context.Context) {\n    err := vClusterClient.CoreV1().Namespaces().Delete(ctx, nsName, metav1.DeleteOptions{})\n    Expect(clientpkg.IgnoreNotFound(err)).To(Succeed())\n})\n\n// now safe to assert further\nExpect(ns.Name).To(Equal(expectedName))\n\n// FAIL — assertion before cleanup registration; if it fails, namespace leaks\n_, err := vClusterClient.CoreV1().Namespaces().Create(ctx, ns, metav1.CreateOptions{})\nExpect(err).NotTo(HaveOccurred())\nExpect(ns.Name).To(Equal(expectedName))\nDeferCleanup(func(ctx context.Context) { /* ... */ })\n```\n\n---\n\n## 3. Timeout Constants\n\nUse the predefined constants. Never invent ad-hoc durations.\n\n| Constant | Duration | Use for |\n|----------|----------|---------|\n| `PollingTimeoutVeryShort` | 5s | Immediate state checks |\n| `PollingTimeoutShort` | 20s | Quick API operations |\n| `PollingTimeout` | 60s | Standard operations |\n| `PollingTimeoutLong` | 120s | Resource creation |\n| `PollingTimeoutVeryLong` | 300s | vCluster startup, cluster creation |\n\n---\n\n## 4. No Cluster-Scoped Singletons\n\nTests must not assume they are the only consumer of a cluster-scoped resource\n(e.g., a ClusterRole with a fixed name). If a cluster-scoped resource is needed,\nits name must include the suffix.\n\n---\n\n## 5. Resource Naming for Debuggability\n\n### 5.1 Single suffix per `Ordered` container\n\nOne call to `random.RandomString(6)`, stored in a variable, reused for all resource\nnames in the container. Do not generate a separate suffix per resource.\n\n```go\n// PASS — single suffix, all resources traceable\nBeforeAll(func(ctx context.Context) context.Context {\n    suffix := random.RandomString(6)\n    nsName = \"test-sync-\" + suffix\n    svcName = \"test-svc-\" + suffix\n    // ...\n})\n\n// FAIL — different suffix per resource, can't correlate\nBeforeAll(func(ctx context.Context) context.Context {\n    nsName = \"test-sync-\" + random.RandomString(6)\n    svcName = \"test-svc-\" + random.RandomString(6)\n    // ...\n})\n```\n\n### 5.2 Descriptive resource name prefixes\n\nThe prefix before the suffix should identify the test or resource purpose.\n\n```go\n// PASS\nnsName = \"svc-sync-test-\" + suffix\n\n// FAIL — generic prefix, hard to trace in cluster\nnsName = \"ns-\" + suffix\n```\n\n---\n\n## 6. Use the Lazy vCluster Pattern\n\nPer-test vClusters live in `suite_*_test.go` and are created lazily in the suite's `BeforeAll` via `setup/lazyvcluster.LazyVCluster`. The only eager cluster is `clusters.HostCluster` (the kind host).\n\n```go\n// PASS — lazy creation inside the suite\nDescribe(\"myfeature-vcluster\", labels.MyFeature, Ordered,\n    cluster.Use(clusters.HostCluster),\n    func() {\n        BeforeAll(func(ctx context.Context) context.Context {\n            return lazyvcluster.LazyVCluster(ctx, myFeatureName, myFeatureYAML)\n        })\n        // specs...\n    },\n)\n\n// FAIL — eager registration in clusters/ + cluster.Use(vc) is the old pattern; do not reintroduce it.\n```\n\nDo not add definitions to `clusters/`. Pre-setup hooks (CRD install, PVC, Helm) go into `setup/` and are passed via `lazyvcluster.WithPreSetup`. See `e2e/README.md` for the full pattern.\n\n---\n\n## 7. Test Setup Does Not Patch System-Managed State\n\n`BeforeEach` and `BeforeAll` must not directly set fields or conditions that a controller or deployment configuration would normally own. If setup code bypasses the system's real initialization path to establish a condition, the test is not exercising production behavior.\n\n**FAIL**: `BeforeEach` patches a vcluster status field that the syncer reconciles into existence.\n\n**PASS**: The vcluster is bootstrapped with the correct YAML configuration so the system establishes the expected state through its normal code path.\n\nIf you find yourself writing setup that patches system-managed state, stop. Read all existing tests that share the same labels — they show how the environment should be configured.\n\n---\n\n## 8. `By()` Text Style and Closure\n\n`By()` must use a closure (see `e2e-test-structure.md`), and the text should be a human-readable sentence fragment describing *what* is happening, not *how*.\n\n```go\n// PASS — closure + descriptive text\nBy(\"Waiting for the replicated service to appear in vcluster\", func() {\n    // ... test logic\n})\n\n// FAIL — no closure; Ginkgo can't attribute failures to the step\nBy(\"Waiting for the replicated service to appear in vcluster\")\n\n// FAIL — too terse, sounds like a function name\nBy(\"WaitServiceSync\", func() { /* ... */ })\n\n// FAIL — describes implementation, not intent\nBy(\"Calling vClusterClient.CoreV1().Services().Get in a loop\", func() { /* ... */ })\n```\n\n---\n\n## 9. Lint Passes\n\n`just -f Justfile.agent lint ./e2e/...` runs clean. This runs the full `golangci-lint` suite (formatting, static analysis, 28+ linters) matching CI. Note: `compile-check` only runs `go build` + `go vet`, which is a subset — always run `lint` as well.\n\n---\n\n## 10. No Hardcoded Cluster or Context Names\n\nNever hardcode kind cluster names or kubectl context strings. Use `constants.GetHostClusterName()`\nfor the cluster name and derive the context from it. The cluster name is configurable via the\n`KIND_NAME` env var and the `--cluster-name` flag.\n\n```go\n// PASS — derived from configurable constant\nhostCluster := constants.GetHostClusterName()\nkubectlContext := \"kind-\" + hostCluster\n\n// FAIL — hardcoded, breaks when KIND_NAME is set\nkubectlContext := \"kind-kind-cluster\"\n```\n\n<!-- File: .claude/rules/e2e-test-structure.md (Tokens: ~490 | Category: anthropic-skill) -->\n---\npaths:\n  - \"e2e/**/*.go\"\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/e2e-test-structure-core.md -->\n\n# e2e Test Structure Rules\n\n| Do | Don't |\n|---|---|\n| Register `DeferCleanup` immediately after creating a resource, before any assertions. If an assertion fails before cleanup is registered, the resource leaks. | Register cleanup after assertions — a failed assertion skips the rest of the block and the resource leaks forever. |\n| Add failure context to assertions inside `Eventually`: `g.Expect(phase).To(Equal(\"Ready\"), \"reason: %v, message: %v\", obj.Status.Reason, obj.Status.Message)` | Write `g.Expect(phase).To(Equal(\"Ready\"))` inside `Eventually` with no context — when it times out you get no diagnostics. |\n| Use `By(\"description\", func() { ... })` with a closure so Ginkgo reports which step failed and how long it took. | Use bare `By(\"description\")` followed by loose statements — Ginkgo can't attribute failures to the step. |\n| Default to `BeforeEach`. Only use `Ordered` for true sequential dependencies. | Use `Ordered` for convenience — every `Ordered` context is a parallelization bottleneck. |\n| Tolerate `NotFound` in cleanup, assert everything else. See `e2e-error-handling.md` for patterns. | Use `_, _ =` to swallow errors in `DeferCleanup` — silent failures mask regressions and leak resources. |\n| Clean up only the specific resources you created, by name or label selector. | `List` all resources of a type in cleanup — parallel tests create the same types and you'll delete their resources. |\n| Assert specific error messages: `Expect(err).To(MatchError(ContainSubstring(\"...\")))` or `kerrs.Be(metav1.StatusReason...)` | Write `Expect(err).To(HaveOccurred())` — this passes on unrelated failures like connectivity errors. |\n| Use existing cluster definitions from `clusters/` and `setup/template` for YAML rendering. Browse available definitions before writing any direct client call in test setup or cleanup. | Write one-off raw client calls when a cluster definition or template helper exists — this duplicates error handling, skips context tracking, and diverges from the standard patterns over time. |\n\n<!-- File: .claude/rules/scratchpad-programs.md (Tokens: ~414 | Category: anthropic-skill) -->\n---\nglobs: [\"**/*.go\"]\n---\n<!-- Generic core: e2e-tdd-workflow plugin references/scratchpad-programs.md (identical) -->\n\n## Verifying assumptions with scratchpad programs\n\nWhen you need to verify an assumption — about API behavior, data shape, timing,\nRBAC, sync logic, or anything else — write a small standalone program in\n`.agent-scratchpad/` that directly answers the question.\n\nThis is not a failure-recovery technique. Use it proactively any time you're\nabout to write code based on an assumption you haven't confirmed.\n\n### Examples of when to use this\n\n- \"Does this CRD have a status subresource?\"\n- \"What does the syncer actually return for a conflicting resource name?\"\n- \"Does client-go's patch merge or replace this nested field?\"\n- \"What order do these finalizers run in?\"\n- \"Is this label selector format valid?\"\n\n### How\n\n1. Create a subfolder in `.agent-scratchpad/` named after the question\n   (e.g., `.agent-scratchpad/check-patch-merge/main.go`). Each program gets\n   its own folder — never put multiple programs in the same folder.\n\n2. Run it:\n   ```bash\n   cd .agent-scratchpad/check-patch-merge && go run main.go --kubeconfig=\"${HOME}/.kube/config\" --context=kind-kind-cluster\n   ```\n   If no cluster is needed (pure logic, parsing, transforms), skip the kubeconfig.\n\n3. Read the output, answer the question, delete the folder.\n\n### Guidelines\n\n| Do | Don't |\n|----|-------|\n| One folder, one file, one question | Build a mini test framework |\n| Name the folder after the question | Use generic names like `test1/` |\n| Print the answer and exit | Add flags for \"reuse later\" |\n| Use `go run` directly | Create a separate go.mod |\n| Delete the folder after use | Leave folders accumulating |\n\n<!-- File: .claude/skills/e2e-migration-validator/SKILL.md (Tokens: ~2212 | Category: anthropic-skill) -->\n---\nname: e2e-migration-validator\ndescription: >\n  Use this skill whenever a user wants to validate, review, or audit e2e tests that have been\n  migrated from an old framework (typically in a `e2e/` directory) to the new `e2e/`\n  framework. Triggers include: \"review my migrated test\", \"validate this e2e test\", \"check\n  my test migration\", \"did I migrate this correctly\", \"review e2e PR\", \"check test coverage\n  after migration\", or any request involving comparing old vs new e2e test files across the\n  vcluster, vcluster-pro, or loft-enterprise repos. Always use this skill when an e2e test\n  file or PR from one of these repos is provided for review — even if the user just says\n  \"take a look at this test\".\n---\n\n# E2E Test Migration Validator\n\nA skill for exhaustively reviewing e2e tests migrated from the old framework (`next/` directory)\nto the new framework (`e2e/` directory) across the `vcluster`, `vcluster-pro`, and\n`loft-enterprise` repositories.\n\n---\n\n## Inputs\n\nThe user will provide one or more of:\n- **Uploaded file(s)**: The new migrated test file, and optionally the original test file\n- **GitHub PR link**: A pull request URL containing the migrated test(s)\n- **Pasted code**: Snippets of old and/or new test code inline in the chat\n\nIf only the new test is provided (no original), you must locate the original yourself using\nthe local repo tools (see [Repo Access](#repo-access)).\n\n---\n\n## Repo Access\n\nUse the local repo MCP tools to inspect implementation details and locate source files.\nAll three repos may be relevant depending on the test subject:\n\n| Repo | Use when |\n|------|----------|\n| `vcluster` | Core vcluster functionality, open-source features |\n| `vcluster-pro` | Pro/enterprise vcluster features |\n| `loft-enterprise` | Loft platform, multi-tenancy, UI-level features |\n\nWhen reviewing a test, **always look up the actual implementation** being tested in the\nrelevant repo before drawing conclusions about correctness or coverage. Do not rely solely\non what the test itself asserts.\n\n---\n\n## Review Workflow\n\nFollow these steps in order for every migration review:\n\n### Step 1 — Locate the Original Test\n\nIf the original (`next/`) test was not provided:\n1. Search the appropriate repo(s) for the original test file by name or describe pattern\n2. Read it fully before proceeding\n3. Note the framework, helper utilities, and assertion style used\n\n### Step 2 — Locate the Implementation Under Test\n\nUsing the repo MCP tools:\n1. Find the source code that the test is exercising (controllers, handlers, CLI commands, API routes, UI components, etc.)\n2. Read the implementation thoroughly — **line by line** if needed\n3. Build a mental model of:\n   - All code paths (happy path, error paths, edge cases)\n   - All configurable inputs / flags / options\n   - Side effects and state mutations\n   - Interactions with other subsystems\n\n### Step 3 — Framework Migration Check\n\nVerify the structural correctness of the migration:\n\n- [ ] Test file is in the correct `e2e/` directory structure\n- [ ] Imports use the new framework's helpers/fixtures (not the old ones)\n- [ ] Test lifecycle hooks (`beforeAll`, `afterAll`, `beforeEach`, `afterEach`) are correctly\n      translated — no leftover old-framework teardown patterns\n- [ ] Setup and teardown create/delete resources correctly and completely\n- [ ] Cluster/namespace/resource provisioning uses the new framework's APIs\n- [ ] Any shared utilities (custom matchers, wait helpers, retry logic) are migrated or\n      replaced with new equivalents — not just copied verbatim\n- [ ] Test isolation is maintained (tests do not share mutable state across runs)\n- [ ] Framework-specific APIs (e.g. `page`, `context`, `request`) are used correctly\n\n### Step 4 — Correctness Check\n\nFor each test case in the migrated file:\n\n1. **Does the test actually test what it claims to test?**\n   - Read the test name / description and compare against what is asserted\n   - Flag any tests where the name and assertions are misaligned\n2. **Are the assertions meaningful?**\n   - Look for trivially-passing assertions (e.g. `expect(x).toBeDefined()` with no deeper check)\n   - Look for assertions that would pass even if the feature is broken\n   - Look for missing negative assertions (e.g. testing that a resource *was* created but not\n     that an invalid input *was rejected*)\n3. **Is the test deterministic?**\n   - Flag race conditions, missing `await`, missing retries on flaky operations\n   - Flag hardcoded timeouts that may not hold in CI\n4. **Is the test environment realistic?**\n   - Does the test reflect how the feature is actually used in production?\n   - Are there unrealistic shortcuts (e.g. bypassing auth, using superuser where a normal user\n     should be tested)?\n\n### Step 5 — Exhaustive Coverage Analysis\n\nThis is the most important step. Compare the implementation (Step 2) against the test suite.\n\nFor **every distinct behavior, code path, and configurable option** in the implementation:\n\n1. Check whether it is covered by at least one test\n2. If not covered, classify the gap:\n   - 🔴 **Critical gap** — a regression here would not be caught at all\n   - 🟡 **Important gap** — partial coverage, but a significant edge case is missed\n   - 🟢 **Minor gap** — low-risk or already covered by unit tests\n\nFor each gap, suggest a concrete additional test case including:\n- What scenario it covers\n- What inputs/state are needed\n- What assertion(s) should be made\n\n**Common gap categories to check explicitly:**\n- Error handling and invalid inputs\n- Permission / RBAC boundary conditions\n- Resource limit enforcement\n- Concurrent operations\n- Feature flags / configuration variants\n- Upgrade/downgrade paths (if applicable)\n- Interaction with other features (cross-feature integration)\n- Cleanup/teardown correctness (does the feature leave no orphaned resources?)\n\n### Step 6 — Code Quality Review\n\n- Naming: are test and variable names descriptive and consistent with the new framework's conventions?\n- DRY: is there duplicated setup that should be extracted into a shared fixture or helper?\n- Readability: would a new engineer understand what is being tested and why?\n- Comments: are non-obvious steps explained?\n- Dead code: are there commented-out blocks, unused imports, or leftover debug statements?\n\n---\n\n## Output Format\n\nProduce **all three** of the following output sections:\n\n---\n\n### 1. Inline Code Analysis\n\nGo through the migrated test file and annotate specific lines or blocks with numbered\ncomments. Format as a fenced code block with inline `// [N] ...` annotations, followed by\na numbered legend below it explaining each annotation.\n\nExample:\n```typescript\n// [1] Missing: should also assert that the old resource was deleted\nawait expect(newResource).toBeVisible();\n\n// [2] Race condition: no retry/wait before asserting pod readiness\nexpect(pod.status).toBe('Running');\n```\n\n**Legend:**\n1. 🔴 Critical gap — deletion is not verified; the old resource could persist undetected\n2. 🟡 Flakiness risk — pod status may not be `Running` immediately; use `waitForCondition`\n\n---\n\n### 2. Checklist Report\n\n```\n## Migration Checklist — <test file name>\n\n### Framework Migration\n- [x] Correct directory structure\n- [x] New framework imports\n- [ ] ⚠️  Teardown uses old-framework pattern on line 42\n\n### Correctness\n- [x] Assertions are meaningful\n- [ ] ⚠️  Test \"should reject invalid config\" passes even with valid config (line 87)\n\n### Coverage Gaps\n- 🔴 No test for RBAC: non-admin user attempting privileged operation\n- 🟡 No test for concurrent resource creation\n- 🟢 No test for audit log output (low risk)\n\n### Code Quality\n- [ ] ⚠️  Duplicate setup block (lines 12–25 and 61–74) — extract to beforeEach\n- [x] Naming is clear and consistent\n```\n\n---\n\n### 3. PR Review Comment\n\nA copy-paste-ready GitHub PR review comment in Markdown. Structure:\n\n```\n## E2E Migration Review\n\n**Overall:** ✅ Approved with suggestions / ⚠️ Changes requested / ❌ Major issues found\n\n### Summary\n<2–3 sentence high-level assessment>\n\n### Issues\n\n#### 🔴 Must Fix\n- ...\n\n#### 🟡 Should Fix\n- ...\n\n#### 🟢 Nice to Have\n- ...\n\n### Suggested Additional Tests\n- **Test:** `<descriptive test name>`\n  **Scenario:** <what to set up>\n  **Assert:** <what to check>\n\n<repeat for each suggested test>\n\n### Nits\n- ...\n```\n\n---\n\n## Framework Notes\n\n> **For the reviewer using this skill:** If you know the specific old and new frameworks\n> involved in this migration, add them here so Claude can apply framework-specific checks.\n>\n> Example entries:\n> - Old: `@vcluster/e2e` custom test runner → New: Playwright + `@vcluster/e2e-next` fixtures\n> - Old: Ginkgo/Gomega (Go) → New: Playwright (TypeScript)\n> - Old: Custom shell-script harness → New: Playwright with kubectl helpers\n>\n> Until this section is filled in, Claude will infer the frameworks from the provided files.\n\n---\n\n## Tips for High-Quality Reviews\n\n- **Always read the implementation first.** A test that looks complete may miss half the\n  code paths if you haven't read the source.\n- **Think adversarially.** For each assertion, ask: \"How could this pass even if the feature\n  is broken?\"\n- **Don't skip cleanup verification.** Many e2e bugs are leaked resources, not failed\n  assertions.\n- **Flag flakiness proactively.** A flaky test is worse than no test — it erodes trust in\n  the suite.\n- **Be specific in suggestions.** Don't say \"add more tests.\" Say exactly what scenario,\n  what inputs, and what assertion.\n\n<!-- File: CLAUDE.md (Tokens: ~142 | Category: claude-rule) -->\n# vCluster — Agent Instructions\n\n## E2E TDD Workflow\n\nWhen doing test-driven development, ALWAYS follow the workflow in\n[.claude/e2e-tdd-workflow.md](.claude/e2e-tdd-workflow.md).\nUse the AGENT_SESSION env var to isolate your commands per session.\n\n## CI / GitHub Actions\n\nShared CI logic lives in [`loft-sh/github-actions`](https://github.com/loft-sh/github-actions);\nproduction repos reuse those actions rather than inlining shell. The\n`github-actions-developer` skill (from `vcluster-skills-engineering`) auto-loads\nwhen you touch `.github/` and carries the full convention.\n\n<!-- File: vendor/github.com/google/gnostic-models/extensions/README.md (Tokens: ~73 | Category: plugin-manifest) -->\n# Extensions\n\n**Extension Support is experimental.**\n\nThis directory contains support code for building Gnostic extensio handlers and\nassociated examples.\n\nExtension handlers can be used to compile vendor or specification extensions\ninto protocol buffer structures.\n\nLike plugins, extension handlers are built as separate executables. Extension\nbodies are written to extension handlers as serialized\nExtensionHandlerRequests.\n</yakaai_skills>"}