# Dedicated MySQL PrivateLink E2E Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Add a dedicated CDC MySQL PrivateLink E2E case and include it in weekly regression for AWS/GCP/Azure, with private-link-aware test-connection assembly.

**Architecture:** Implement a new dedicated changefeed case file that mirrors the Kafka private-link lifecycle (create sink endpoint, test connection, create changefeed, verify running, cleanup) but uses MySQL payload semantics. Add MySQL PrivateLink support in dedicated console test-connection parameter assembly so `TestConnectionWithPrivateLink` works for MySQL dependencies. Register three cloud-specific cases into dedicated weekly regression groups.

**Tech Stack:** Go, dataflow-service-e2e case framework (`BaseCase`), dedicated CDC wrappers (`cdcapi` + `consoleapi`), testify.

---

## Scope Check

This is one focused subsystem change (Dedicated CDC E2E + wrapper + registry wiring), so one implementation plan is sufficient.

## File Structure

1. **Create** `test/e2e/cases/dedicated/changefeed/mysql_private_link.go`
   - Responsibility: new Dedicated MySQL PrivateLink E2E lifecycle case.
2. **Create** `test/e2e/cases/dedicated/changefeed/mysql_private_link_test.go`
   - Responsibility: unit tests for case name and create-payload shape.
3. **Modify** `test/e2e/internal/tidbcloud/dedicated/consoleapi/cdc_client_wrapper.go`
   - Responsibility: add MySQL PrivateLink branch in test-connection param assembly.
4. **Modify** `test/e2e/internal/tidbcloud/dedicated/cdcapi/client_wrapper_test.go`
   - Responsibility: add failing/passing test for MySQL private-link `TestConnectionWithPrivateLink` behavior.
5. **Modify** `test/e2e/internal/case_driver/cases_registry_dedicated.go`
   - Responsibility: register AWS/GCP/Azure MySQL PrivateLink cases and add them to weekly regression groups.

### Task 1: Add failing unit tests for MySQL PrivateLink case payload

**Files:**
- Create: `test/e2e/cases/dedicated/changefeed/mysql_private_link_test.go`
- Test: `test/e2e/cases/dedicated/changefeed/mysql_private_link_test.go`

- [ ] **Step 1: Write failing tests for case name and payload builder**

```go
package changefeed

import (
	"testing"

	"github.com/stretchr/testify/require"
	. "github.com/tidbcloud/dataflow-service-e2e/internal/model"
	"github.com/tidbcloud/dataflow-service-e2e/internal/tidbcloud/dedicated/consoleapi/cdc/model"
)

func TestMySQLPrivateLinkCaseName(t *testing.T) {
	c := &mysqlPrivateLinkCase{cloudProvider: AWS}
	require.Equal(t, "awsMySQLPrivateLink", c.Name())
}

func TestGenCreateMySQLPrivateLinkSpec_UsesRequestedPayload(t *testing.T) {
	c := &mysqlPrivateLinkCase{cloudProvider: AWS}
	downstream := &MySQLEntity{
		MySQLDimensions: MySQLDimensions{
			NetworkType:   DataflowNetworkTypeNetworkTypePrivateLink,
			CloudProvider: AWS,
		},
		User:     "admin",
		Password: "dataflow.pingcap!!!",
		Ports:    []int64{3306},
	}

	spec := c.genCreateMySQLPrivateLinkSpec("awsMySQLPrivateLink", "2rcu", downstream, "sink-74")

	require.NotNil(t, spec.ConnectorType)
	require.Equal(t, model.DataflowConnectorTypeMYSQL, *spec.ConnectorType)
	require.Equal(t, "awsMySQLPrivateLink", spec.Name)
	require.Equal(t, "2rcu", spec.ChangefeedInstanceType)

	require.NotNil(t, spec.MysqlInfo)
	require.NotNil(t, spec.MysqlInfo.Connection)
	require.Equal(t, "admin", spec.MysqlInfo.Connection.Username)
	require.Equal(t, "dataflow.pingcap!!!", spec.MysqlInfo.Connection.Password)
	require.NotNil(t, spec.MysqlInfo.TableConfig)
	require.Equal(t, []string{"*.*"}, spec.MysqlInfo.TableConfig.FilterRule)
	require.Equal(t, 0, len(spec.MysqlInfo.TableConfig.EventFilterRule))
	require.False(t, spec.MysqlInfo.TableConfig.CaseSensitive)
	require.NotNil(t, spec.MysqlInfo.TableConfig.Mode)
	require.Equal(t, model.DataflowTableModeFORCESYNC, *spec.MysqlInfo.TableConfig.Mode)
	require.NotNil(t, spec.MysqlInfo.StartPosition)
	require.NotNil(t, spec.MysqlInfo.StartPosition.Mode)
	require.Equal(t, model.DataflowStartModeFROMNOW, *spec.MysqlInfo.StartPosition.Mode)

	require.NotNil(t, spec.NetworkInfo)
	require.NotNil(t, spec.NetworkInfo.NetworkType)
	require.Equal(t, model.DataflowNetworkTypeNetworkTypePrivateLink, *spec.NetworkInfo.NetworkType)
	require.Equal(t, []int64{3306}, spec.NetworkInfo.Ports)
	require.Equal(t, "sink-74", spec.NetworkInfo.SinkEndpointID)
}
```

- [ ] **Step 2: Run test to verify it fails**

Run:

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service/test/e2e
go test ./cases/dedicated/changefeed -run 'TestMySQLPrivateLinkCaseName|TestGenCreateMySQLPrivateLinkSpec_UsesRequestedPayload' -count=1
```

Expected: FAIL because `mysqlPrivateLinkCase` and `genCreateMySQLPrivateLinkSpec` do not exist yet.

### Task 2: Implement new dedicated MySQL PrivateLink case

**Files:**
- Create: `test/e2e/cases/dedicated/changefeed/mysql_private_link.go`
- Test: `test/e2e/cases/dedicated/changefeed/mysql_private_link_test.go`

- [ ] **Step 1: Create the case file with constructor, dependencies, and runtime flow**

```go
package changefeed

import (
	"fmt"
	"time"

	"github.com/aws/smithy-go/ptr"
	. "github.com/tidbcloud/dataflow-service-e2e/cases"
	"github.com/tidbcloud/dataflow-service-e2e/cases/dedicated/common"
	. "github.com/tidbcloud/dataflow-service-e2e/internal/config"
	. "github.com/tidbcloud/dataflow-service-e2e/internal/model"
	"github.com/tidbcloud/dataflow-service-e2e/internal/tidbcloud/dedicated/cdcapi"
	"github.com/tidbcloud/dataflow-service-e2e/internal/tidbcloud/dedicated/consoleapi"
	"github.com/tidbcloud/dataflow-service-e2e/internal/tidbcloud/dedicated/consoleapi/cdc/client/connector_service"
	"github.com/tidbcloud/dataflow-service-e2e/internal/tidbcloud/dedicated/consoleapi/cdc/client/sink_endpoint_service"
	"github.com/tidbcloud/dataflow-service-e2e/internal/tidbcloud/dedicated/consoleapi/cdc/model"
)

type mysqlPrivateLinkCase struct {
	BaseCase
	cdcApiWrapper          *cdcapi.CDCClientWrapper
	sinkEndpointApiWrapper *consoleapi.SinkEndpointClientWrapper
	cfValidator            *common.ChangfeedValidator
	cloudProvider          CloudProviderType
}

func NewMySQLPrivateLinkCase(cloudProvider CloudProviderType) E2ECase {
	return &mysqlPrivateLinkCase{cloudProvider: cloudProvider}
}

func (m *mysqlPrivateLinkCase) Name() string {
	return fmt.Sprintf("%s%s", m.cloudProvider, "MySQLPrivateLink")
}

func (m *mysqlPrivateLinkCase) DeclareDependencyRequirements() map[string]DependencyRequirement {
	return map[string]DependencyRequirement{
		"tidb": &TiDBDedicatedRequirement{
			Dedicated: false,
			TiDBDedicatedDimensions: TiDBDedicatedDimensions{
				CloudProvider: m.cloudProvider,
				Region:        GlobalConfig.DefaultRegionOf(m.cloudProvider),
			},
		},
		"mysql": &MySQLRequirement{
			MySQLDimensions: MySQLDimensions{
				NetworkType:   DataflowNetworkTypeNetworkTypePrivateLink,
				CloudProvider: m.cloudProvider,
			},
		},
	}
}

func (m *mysqlPrivateLinkCase) Run() {
	tidbCluster := m.TiDBDedicated("tidb")
	downstream := m.MySQL("mysql")
	changefeedName := m.Name()
	changefeedID := ""
	privateLinkID := ""
	var azs []string

	m.cdcApiWrapper = cdcapi.NewCDCClientWrapper(m.Logger())
	m.sinkEndpointApiWrapper = consoleapi.NewSinkEndpointClientWrapper(m.Logger())
	m.cfValidator = common.NewChangfeedValidator(m.Logger())

	defer func() {
		m.
			RetryDo("delete changefeeds", func() error {
				return m.cfValidator.DeleteChangefeedByName(tidbCluster, changefeedName)
			}, 3, time.Minute).
			Expect("changefeeds not in list", func() error {
				return m.cfValidator.AllChangefeedsNotInListByNames(tidbCluster, changefeedName)
			}, 3, 10*time.Second).
			RetryDo("delete private link", func() error {
				return m.sinkEndpointApiWrapper.DeleteSinkEndpoint(tidbCluster, privateLinkID)
			}, 3, time.Minute)
	}()

	m.
		RetryDo("list sink availability zones", func() (err error) {
			azs, err = m.sinkEndpointApiWrapper.ListSinkAvailabilityZones(tidbCluster)
			return err
		}, 3, time.Minute).
		RetryDo("create mysql private link sink endpoint", func() (err error) {
			privateLinkID, err = m.createSinkEndpoint(tidbCluster, downstream, azs)
			return err
		}, 3, time.Minute).
		Expect("sink endpoint is available", func() error {
			return m.sinkEndpointAvailable(tidbCluster, privateLinkID)
		}, 20, time.Minute).
		Expect("test mysql connection with private link", func() error {
			return m.cdcApiWrapper.TestConnectionWithPrivateLink(tidbCluster, downstream, privateLinkID)
		}, 20, time.Minute).
		RetryDo("create mysql private link changefeed", func() (err error) {
			spec := m.genCreateMySQLPrivateLinkSpec(changefeedName, "2rcu", downstream, privateLinkID)
			resp, err := m.cdcApiWrapper.CreateChangefeed(tidbCluster, spec)
			if err == nil {
				changefeedID = *resp.Payload.ConnectorID
			}
			return err
		}, 6, time.Minute).
		Expect("changefeed in list", func() error {
			return m.cfValidator.AreChangefeedsInList(tidbCluster, changefeedID)
		}, 3, 10*time.Second).
		Expect("changefeed is running", func() error {
			return m.cfValidator.AreChangefeedsRunning(tidbCluster, changefeedID)
		}, 20, time.Minute)
}

func (m *mysqlPrivateLinkCase) createSinkEndpoint(
	tidbCluster *TiDBDedicatedEntity,
	downstream *MySQLEntity,
	azs []string,
) (string, error) {
	params := &sink_endpoint_service.CreateSinkEndpointParams{
		OrgID:     tidbCluster.OrgID,
		ProjectID: tidbCluster.ProjectID,
		SinkEndpoint: &model.DataflowSinkEndpoint{
			EndpointName:        ptr.String(fmt.Sprintf("%s-mysql-pl-e2e", tidbCluster.ClusterID)),
			EndpointServiceName: ptr.String(downstream.PrivateLinkServiceName),
			Provider:            ptr.String(string(tidbCluster.CloudProvider)),
			Region:              ptr.String(tidbCluster.Region),
			EndpointZones:       azs,
		},
	}
	return m.sinkEndpointApiWrapper.CreateSinkEndpoint(params)
}

func (m *mysqlPrivateLinkCase) sinkEndpointAvailable(tidbCluster *TiDBDedicatedEntity, sinkEndpointID string) error {
	params := &sink_endpoint_service.GetSinkEndpointParams{
		OrgID:          tidbCluster.OrgID,
		ProjectID:      tidbCluster.ProjectID,
		SinkEndpointID: sinkEndpointID,
	}
	endpoint, err := m.sinkEndpointApiWrapper.GetSinkEndpoint(params)
	if err != nil {
		return err
	}
	if *endpoint.Status != model.DataflowSinkEndpointStatusSINKENDPOINTSTATUSAVAILABLE {
		return fmt.Errorf("sink endpoint %s not available, status=%s", sinkEndpointID, *endpoint.Status)
	}
	return nil
}

func (m *mysqlPrivateLinkCase) genCreateMySQLPrivateLinkSpec(
	name, instanceType string,
	downstream *MySQLEntity,
	sinkEndpointID string,
) connector_service.CreateConnectorBody {
	return connector_service.CreateConnectorBody{
		Name:                   name,
		ChangefeedInstanceType: instanceType,
		ConnectorType:          model.NewDataflowConnectorType(model.DataflowConnectorTypeMYSQL),
		MysqlInfo: &model.DataflowMySQLInfo{
			Connection: &model.DataflowConnection{
				Username: downstream.User,
				Password: downstream.Password,
			},
			TableConfig: &model.DataflowTableConfig{
				FilterRule:      []string{"*.*"},
				Mode:            model.NewDataflowTableMode(model.DataflowTableModeFORCESYNC),
				EventFilterRule: []*model.DataflowEventFilterRule{},
				CaseSensitive:   false,
			},
			StartPosition: &model.DataflowStartPosition{
				Mode: model.NewDataflowStartMode(model.DataflowStartModeFROMNOW),
			},
		},
		NetworkInfo: &model.DataflowNetworkInfo{
			NetworkType:    model.NewDataflowNetworkType(model.DataflowNetworkTypeNetworkTypePrivateLink),
			Ports:          downstream.Ports,
			SinkEndpointID: sinkEndpointID,
		},
	}
}
```

- [ ] **Step 2: Format files**

Run:

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service
gofmt -w test/e2e/cases/dedicated/changefeed/mysql_private_link.go \
         test/e2e/cases/dedicated/changefeed/mysql_private_link_test.go
```

Expected: no output; files rewritten in place.

- [ ] **Step 3: Run tests to verify pass**

Run:

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service/test/e2e
go test ./cases/dedicated/changefeed -run 'TestMySQLPrivateLinkCaseName|TestGenCreateMySQLPrivateLinkSpec_UsesRequestedPayload' -count=1
```

Expected: PASS.

- [ ] **Step 4: Commit**

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service
git add test/e2e/cases/dedicated/changefeed/mysql_private_link.go \
        test/e2e/cases/dedicated/changefeed/mysql_private_link_test.go
git commit -m "test(e2e): add dedicated mysql private link case"
```

### Task 3: Add failing cdcapi test for MySQL PrivateLink test-connection path

**Files:**
- Modify: `test/e2e/internal/tidbcloud/dedicated/cdcapi/client_wrapper_test.go`
- Test: `test/e2e/internal/tidbcloud/dedicated/cdcapi/client_wrapper_test.go`

- [ ] **Step 1: Extend fake console client to capture test params**

```go
type fakeConsoleClient struct {
	testCalls      int
	testErr        error
	lastTestParams *connector_service.TestConnectorParams
}

func (f *fakeConsoleClient) TestConnectionWithParams(params *connector_service.TestConnectorParams) error {
	f.testCalls++
	f.lastTestParams = params
	return f.testErr
}
```

- [ ] **Step 2: Add failing test for MySQL PrivateLink assembly**

```go
func TestTestConnectionWithPrivateLink_MySQLPrivateLink_UsesPrivateLinkNetwork(t *testing.T) {
	cluster := &TiDBDedicatedEntity{
		ClusterID: "cluster-1",
		OrgID:     "org-1",
		ProjectID: "project-1",
	}
	downstream := &MySQLEntity{
		MySQLDimensions: MySQLDimensions{
			NetworkType:   DataflowNetworkTypeNetworkTypePrivateLink,
			CloudProvider: AWS,
		},
		User:     "admin",
		Password: "dataflow.pingcap!!!",
		Ports:    []int64{3306},
	}

	console := &fakeConsoleClient{}
	openapi := &fakeOpenAPIClient{}
	wrapper := newCDCClientWrapperWithClients(noopLogger{}, console, openapi, nil)

	require.NotPanics(t, func() {
		err := wrapper.TestConnectionWithPrivateLink(cluster, downstream, "sink-74")
		require.NoError(t, err)
	})

	require.Equal(t, 1, console.testCalls)
	require.NotNil(t, console.lastTestParams)
	require.NotNil(t, console.lastTestParams.Body.NetworkInfo)
	require.NotNil(t, console.lastTestParams.Body.NetworkInfo.NetworkType)
	assert.Equal(t, console_model.DataflowNetworkTypeNetworkTypePrivateLink, *console.lastTestParams.Body.NetworkInfo.NetworkType)
	assert.Equal(t, []int64{3306}, console.lastTestParams.Body.NetworkInfo.Ports)
	assert.Equal(t, "sink-74", console.lastTestParams.Body.NetworkInfo.SinkEndpointID)
	require.NotNil(t, console.lastTestParams.Body.MysqlInfo)
	require.NotNil(t, console.lastTestParams.Body.MysqlInfo.Connection)
	assert.Equal(t, "admin", console.lastTestParams.Body.MysqlInfo.Connection.Username)
	assert.Equal(t, "dataflow.pingcap!!!", console.lastTestParams.Body.MysqlInfo.Connection.Password)
}
```

- [ ] **Step 3: Run test to verify it fails**

Run:

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service/test/e2e
go test ./internal/tidbcloud/dedicated/cdcapi -run TestTestConnectionWithPrivateLink_MySQLPrivateLink_UsesPrivateLinkNetwork -count=1
```

Expected: FAIL due panic from unsupported MySQL private-link network type in dedicated console param assembly.

### Task 4: Implement MySQL PrivateLink assembly in dedicated console wrapper

**Files:**
- Modify: `test/e2e/internal/tidbcloud/dedicated/consoleapi/cdc_client_wrapper.go`
- Modify: `test/e2e/internal/tidbcloud/dedicated/cdcapi/client_wrapper_test.go`
- Test: `test/e2e/internal/tidbcloud/dedicated/cdcapi/client_wrapper_test.go`

- [ ] **Step 1: Add private-link branch in `assembleMySQLTestConnectorParams`**

```go
func assembleMySQLTestConnectorParams(params *connector_service2.TestConnectorParams, downstream *MySQLEntity) {
	switch downstream.NetworkType {
	case DataflowNetworkTypeNetworkTypePublic:
		params.Body = genDefaultPublicMySQLTestConnectionBody()
		params.Body.MysqlInfo.Connection = &model2.DataflowConnection{
			Endpoint: downstream.Endpoint,
			Username: downstream.User,
			Password: downstream.Password,
		}
	case DataflowNetworkTypeNetworkTypePrivateLink:
		params.Body = genPrivateLinkMySQLTestConnectionBody(downstream)
	default:
		panic("unknown downstream.NetworkType: " + downstream.NetworkType)
	}
}
```

- [ ] **Step 2: Add helper body builder for MySQL private link**

```go
func genPrivateLinkMySQLTestConnectionBody(downstream *MySQLEntity) connector_service2.TestConnectorBody {
	return connector_service2.TestConnectorBody{
		ConnectorType: model2.NewDataflowConnectorType(model2.DataflowConnectorTypeMYSQL),
		MysqlInfo: &model2.DataflowMySQLInfo{
			Connection: &model2.DataflowConnection{
				Username: downstream.User,
				Password: downstream.Password,
			},
		},
		NetworkInfo: &model2.DataflowNetworkInfo{
			NetworkType: model2.NewDataflowNetworkType(model2.DataflowNetworkTypeNetworkTypePrivateLink),
			Ports:       downstream.Ports,
		},
	}
}
```

- [ ] **Step 3: Format files**

Run:

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service
gofmt -w test/e2e/internal/tidbcloud/dedicated/consoleapi/cdc_client_wrapper.go \
         test/e2e/internal/tidbcloud/dedicated/cdcapi/client_wrapper_test.go
```

Expected: no output; files rewritten in place.

- [ ] **Step 4: Re-run cdcapi targeted test**

Run:

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service/test/e2e
go test ./internal/tidbcloud/dedicated/cdcapi -run TestTestConnectionWithPrivateLink_MySQLPrivateLink_UsesPrivateLinkNetwork -count=1
```

Expected: PASS.

- [ ] **Step 5: Commit**

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service
git add test/e2e/internal/tidbcloud/dedicated/consoleapi/cdc_client_wrapper.go \
        test/e2e/internal/tidbcloud/dedicated/cdcapi/client_wrapper_test.go
git commit -m "test(e2e): support mysql private link test connection assembly"
```

### Task 5: Register MySQL PrivateLink case in dedicated weekly regression groups

**Files:**
- Modify: `test/e2e/internal/case_driver/cases_registry_dedicated.go`
- Test: `test/e2e/internal/case_driver/dependency_mgnt_test.go` (package compile/regression)

- [ ] **Step 1: Register AWS/GCP/Azure MySQL PrivateLink case constructors**

```go
awsMySQLPrivateLinkCase := changefeed.NewMySQLPrivateLinkCase(AWS)
gcpMySQLPrivateLinkCase := changefeed.NewMySQLPrivateLinkCase(GCP)
azureMySQLPrivateLinkCase := changefeed.NewMySQLPrivateLinkCase(AZURE)
addCases(awsMySQLPrivateLinkCase, gcpMySQLPrivateLinkCase, azureMySQLPrivateLinkCase)
```

- [ ] **Step 2: Add cases into cloud weekly regression groups**

```go
"awsWeeklyRegression": {
	awsKafkaChangefeedSmokeCase,
	awsMySQLChangefeedSmokeCase,
	awsPulsarChangefeedSmokeCase,
	awsS3ChangefeedSmokeCase,
	awsServerlessChangefeedSmokeCase,
	awsKafkaPrivateLink,
	awsMySQLPrivateLinkCase,
	awsBackupSettingSmokeCase,
	awsSnapshotBackupSmokeCase,
	awsSnapshotRestoreSmokeCase,
	awsPiTRBackupSmokeCase,
	awsPiTRRestoreSmokeCase,
	awsDualRegionBackupSmokeCase,
	awsBackupExportSmokeCase,
	awsCRRFEnableDisableSmokeCase,
	awsCRRFRestoreSmokeCase,
	awsDMFullMigrationSmokeCase,
	awsDMIncrementalMigrationSmokeCase,
	awsDMJobLifecycleSmokeCase,
	awsDMPrecheckSmokeCase,
	awsDMScaleSmokeCase,
	awsImportSmokeCase,
},
"gcpWeeklyRegression": {
	gcpKafkaChangefeedSmokeCase,
	gcpMySQLChangefeedSmokeCase,
	gcpPulsarChangefeedSmokeCase,
	gcpGcsChangefeedSmokeCase,
	gcpKafkaPrivateLink,
	gcpMySQLPrivateLinkCase,
	gcpBackupSettingSmokeCase,
	gcpSnapshotBackupSmokeCase,
	gcpSnapshotRestoreSmokeCase,
	gcpPiTRBackupSmokeCase,
	gcpPiTRRestoreSmokeCase,
	gcpDualRegionBackupSmokeCase,
	gcpBackupExportSmokeCase,
	gcpDMFullMigrationSmokeCase,
	gcpDMIncrementalMigrationSmokeCase,
	gcpDMJobLifecycleSmokeCase,
	gcpDMPrecheckSmokeCase,
	gcpDMScaleSmokeCase,
	gcpImportSmokeCase,
},
"azureWeeklyRegression": {
	azureKafkaChangefeedSmokeCase,
	azureMySQLChangefeedSmokeCase,
	azurePulsarChangefeedSmokeCase,
	azureKafkaPrivateLink,
	azureMySQLPrivateLinkCase,
	azureBackupSettingSmokeCase,
	azureSnapshotBackupSmokeCase,
	azureSnapshotRestoreSmokeCase,
	azurePiTRBackupSmokeCase,
	azurePiTRRestoreSmokeCase,
	azureDualRegionBackupSmokeCase,
	azureBackupExportSmokeCase,
	azureDMFullMigrationSmokeCase,
	azureDMIncrementalMigrationSmokeCase,
	azureDMJobLifecycleSmokeCase,
	azureDMPrecheckSmokeCase,
	azureDMScaleSmokeCase,
	azureImportSmokeCase,
},
```

- [ ] **Step 3: Verify wiring with source grep**

Run:

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service
rg "MySQLPrivateLink" test/e2e/internal/case_driver/cases_registry_dedicated.go -n
```

Expected: matches for constructor registration and all three `*WeeklyRegression` groups.

- [ ] **Step 4: Run case_driver package tests for compile regression**

Run:

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service/test/e2e
go test ./internal/case_driver -run TestAsyncDeps_ -count=1
```

Expected: PASS.

- [ ] **Step 5: Commit**

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service
git add test/e2e/internal/case_driver/cases_registry_dedicated.go
git commit -m "test(e2e): add mysql private link cases to dedicated weekly regression"
```

### Task 6: Final targeted verification

**Files:**
- Test: `test/e2e/cases/dedicated/changefeed/mysql_private_link*.go`
- Test: `test/e2e/internal/tidbcloud/dedicated/consoleapi/cdc_client_wrapper.go`
- Test: `test/e2e/internal/tidbcloud/dedicated/cdcapi/client_wrapper_test.go`
- Test: `test/e2e/internal/case_driver/cases_registry_dedicated.go`

- [ ] **Step 1: Run all affected package tests in one command**

Run:

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service/test/e2e
go test ./cases/dedicated/changefeed ./internal/tidbcloud/dedicated/cdcapi ./internal/case_driver -count=1
```

Expected: PASS for all selected packages.

- [ ] **Step 2: Check final working tree for only intended file changes**

Run:

```bash
cd /home/nil/pingcap/workbench/repos/dataflow-service
git --no-pager status --short
```

Expected: only files from this plan are listed.
```
