From be1a667fd988229b7903bb4e709869183e89d338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Kie=C3=9Fling?= Date: Thu, 5 Feb 2026 15:16:37 +0100 Subject: [PATCH 1/3] feat(workspace): human-friendly workspace branch naming - Add BranchNameGenerator (Domain) with format: -usermailATdomainDOTtld-SHORTWORKSPACEID - Require userEmail in dispatchSetupIfNeeded and SetupWorkspaceMessage - Thread userEmail through setup: controller (accountInfo->email), prefab subscriber (accountCoreId -> account email) - Unit tests for BranchNameGenerator (format, sanitization, short ID) - Update docs (project-workspace-conversation-workflow-high-level.md) - Wire BranchNameGeneratorInterface in config/services.yaml Fixes #67 --- config/services.yaml | 3 + ...kspace-conversation-workflow-high-level.md | 2 +- .../ChatBasedContentEditorController.php | 2 +- ...countCoreCreatedSymfonyEventSubscriber.php | 14 +++- .../Domain/Service/BranchNameGenerator.php | 38 +++++++++ .../Service/BranchNameGeneratorInterface.php | 16 ++++ .../Domain/Service/WorkspaceSetupService.php | 22 ++---- .../Facade/WorkspaceMgmtFacade.php | 4 +- .../Facade/WorkspaceMgmtFacadeInterface.php | 5 +- .../Handler/SetupWorkspaceHandler.php | 2 +- .../Message/SetupWorkspaceMessage.php | 3 +- .../WorkspaceMgmt/BranchNameGeneratorTest.php | 77 +++++++++++++++++++ 12 files changed, 164 insertions(+), 24 deletions(-) create mode 100644 src/WorkspaceMgmt/Domain/Service/BranchNameGenerator.php create mode 100644 src/WorkspaceMgmt/Domain/Service/BranchNameGeneratorInterface.php create mode 100644 tests/Unit/WorkspaceMgmt/BranchNameGeneratorTest.php diff --git a/config/services.yaml b/config/services.yaml index a07522d..aed7800 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -134,6 +134,9 @@ services: class: App\ChatBasedContentEditor\Facade\ChatBasedContentEditorFacade # Domain service bindings + App\WorkspaceMgmt\Domain\Service\BranchNameGeneratorInterface: + class: App\WorkspaceMgmt\Domain\Service\BranchNameGenerator + App\WorkspaceMgmt\Domain\Service\WorkspaceStatusGuardInterface: class: App\WorkspaceMgmt\Domain\Service\WorkspaceStatusGuard diff --git a/docs/plans/project-workspace-conversation-workflow-high-level.md b/docs/plans/project-workspace-conversation-workflow-high-level.md index cf98acc..29a8aeb 100644 --- a/docs/plans/project-workspace-conversation-workflow-high-level.md +++ b/docs/plans/project-workspace-conversation-workflow-high-level.md @@ -43,7 +43,7 @@ When a user wants to start a conversation on a project, and there is a workspace - The workspace is set into status IN_SETUP - The local workspace folder is removed - The git repository mapped to the project that is mapped to the workspace is cloned into the local workspace folder using the github token of the project -- A new branch, whose name has the format `-`, is created from the main branch +- A new branch, whose name has the format `-usermailATdomainDOTtld-SHORTWORKSPACEID` (timestamp, sanitized user email, short workspace ID), is created from the main branch - The workspace is set into status AVAILABLE_FOR_CONVERSATION When a user wants to start a conversation on a project, and succeeds to do so because its workspace is in status AVAILABLE_FOR_CONVERSATION, then diff --git a/src/ChatBasedContentEditor/Presentation/Controller/ChatBasedContentEditorController.php b/src/ChatBasedContentEditor/Presentation/Controller/ChatBasedContentEditorController.php index 5c1f4a3..09e0cd2 100644 --- a/src/ChatBasedContentEditor/Presentation/Controller/ChatBasedContentEditorController.php +++ b/src/ChatBasedContentEditor/Presentation/Controller/ChatBasedContentEditorController.php @@ -143,7 +143,7 @@ public function startConversation( try { // Dispatch async setup if needed - this will start setup in background - $workspace = $this->workspaceMgmtFacade->dispatchSetupIfNeeded($projectId); + $workspace = $this->workspaceMgmtFacade->dispatchSetupIfNeeded($projectId, $accountInfo->email); // If setup was dispatched (workspace is now IN_SETUP), show waiting page if ($workspace->status === WorkspaceStatus::IN_SETUP) { diff --git a/src/Organization/Domain/SymfonyEventSubscriber/AccountCoreCreatedSymfonyEventSubscriber.php b/src/Organization/Domain/SymfonyEventSubscriber/AccountCoreCreatedSymfonyEventSubscriber.php index de817fe..2eb537f 100644 --- a/src/Organization/Domain/SymfonyEventSubscriber/AccountCoreCreatedSymfonyEventSubscriber.php +++ b/src/Organization/Domain/SymfonyEventSubscriber/AccountCoreCreatedSymfonyEventSubscriber.php @@ -4,6 +4,7 @@ namespace App\Organization\Domain\SymfonyEventSubscriber; +use App\Account\Facade\AccountFacadeInterface; use App\Account\Facade\SymfonyEvent\AccountCoreCreatedSymfonyEvent; use App\Organization\Domain\Service\OrganizationDomainServiceInterface; use App\Organization\Facade\SymfonyEvent\CurrentlyActiveOrganizationChangedSymfonyEvent; @@ -12,6 +13,7 @@ use App\WorkspaceMgmt\Facade\WorkspaceMgmtFacadeInterface; use Exception; use Psr\Log\LoggerInterface; +use RuntimeException; use Symfony\Component\EventDispatcher\Attribute\AsEventListener; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Throwable; @@ -25,7 +27,8 @@ public function __construct( private PrefabFacadeInterface $prefabFacade, private ProjectMgmtFacadeInterface $projectMgmtFacade, private WorkspaceMgmtFacadeInterface $workspaceMgmtFacade, - private LoggerInterface $logger + private AccountFacadeInterface $accountFacade, + private LoggerInterface $logger, ) { } @@ -46,11 +49,18 @@ public function handle( ) ); + $accountInfo = $this->accountFacade->getAccountInfoById($event->accountCoreId); + if ($accountInfo === null) { + throw new RuntimeException( + 'Account not found for workspace setup; accountCoreId: ' . $event->accountCoreId + ); + } + $prefabs = $this->prefabFacade->loadPrefabs(); foreach ($prefabs as $prefab) { try { $projectId = $this->projectMgmtFacade->createProjectFromPrefab($organization->getId(), $prefab); - $this->workspaceMgmtFacade->dispatchSetupIfNeeded($projectId); + $this->workspaceMgmtFacade->dispatchSetupIfNeeded($projectId, $accountInfo->email); } catch (Throwable $e) { $this->logger->warning('Prefab project creation failed', [ 'organization_id' => $organization->getId(), diff --git a/src/WorkspaceMgmt/Domain/Service/BranchNameGenerator.php b/src/WorkspaceMgmt/Domain/Service/BranchNameGenerator.php new file mode 100644 index 0000000..b029fee --- /dev/null +++ b/src/WorkspaceMgmt/Domain/Service/BranchNameGenerator.php @@ -0,0 +1,38 @@ +-usermailATdomainDOTtld-SHORTWORKSPACEID + */ +final class BranchNameGenerator implements BranchNameGeneratorInterface +{ + public function generate(string $workspaceId, string $userEmail): string + { + $timestamp = DateAndTimeService::getDateTimeImmutable()->format('Y-m-d H:i:s'); + $sanitized = $this->sanitizeEmailForBranchName($userEmail); + $shortId = mb_substr($workspaceId, 0, 8); + + return $timestamp . '-' . $sanitized . '-' . $shortId; + } + + /** + * Sanitize email for use in branch name: @ → AT, . → DOT, lowercase. + */ + public function sanitizeEmailForBranchName(string $email): string + { + $lower = mb_strtolower($email, 'UTF-8'); + + return str_replace(['@', '.'], ['AT', 'DOT'], $lower); + } +} diff --git a/src/WorkspaceMgmt/Domain/Service/BranchNameGeneratorInterface.php b/src/WorkspaceMgmt/Domain/Service/BranchNameGeneratorInterface.php new file mode 100644 index 0000000..fcbe664 --- /dev/null +++ b/src/WorkspaceMgmt/Domain/Service/BranchNameGeneratorInterface.php @@ -0,0 +1,16 @@ +getId(); if ($workspaceId === null) { @@ -77,7 +77,7 @@ public function setup(Workspace $workspace): void } try { - $this->performSetup($workspace); + $this->performSetup($workspace, $userEmail); // Transition to AVAILABLE_FOR_CONVERSATION $workspace->setStatus(WorkspaceStatus::AVAILABLE_FOR_CONVERSATION); @@ -109,7 +109,7 @@ public function getWorkspacePath(Workspace $workspace): string return $this->workspaceRoot . '/' . $workspace->getId(); } - private function performSetup(Workspace $workspace): void + private function performSetup(Workspace $workspace, string $userEmail): void { $workspaceId = $workspace->getId(); $workspacePath = $this->getWorkspacePath($workspace); @@ -133,7 +133,7 @@ private function performSetup(Workspace $workspace): void $this->gitAdapter->clone($projectInfo->gitUrl, $workspacePath, $projectInfo->githubToken); // Step 3: Generate branch name and checkout - $branchName = $this->generateBranchName($workspaceId); + $branchName = $this->branchNameGenerator->generate($workspaceId, $userEmail); $this->logger->debug('Creating branch', ['branchName' => $branchName]); $this->gitAdapter->checkoutNewBranch($workspacePath, $branchName); @@ -168,12 +168,4 @@ private function runProjectSetupSteps(ProjectInfoDto $projectInfo, string $works 'projectType' => $projectInfo->projectType->value, ]); } - - private function generateBranchName(string $workspaceId): string - { - $shortId = mb_substr($workspaceId, 0, 8); - $timestamp = DateAndTimeService::getDateTimeImmutable()->format('Ymd-His'); - - return 'ws-' . $shortId . '-' . $timestamp; - } } diff --git a/src/WorkspaceMgmt/Facade/WorkspaceMgmtFacade.php b/src/WorkspaceMgmt/Facade/WorkspaceMgmtFacade.php index cc4de39..b80baae 100644 --- a/src/WorkspaceMgmt/Facade/WorkspaceMgmtFacade.php +++ b/src/WorkspaceMgmt/Facade/WorkspaceMgmtFacade.php @@ -66,7 +66,7 @@ public function getWorkspaceForProject(string $projectId): ?WorkspaceInfoDto return $this->toDto($workspace); } - public function dispatchSetupIfNeeded(string $projectId): WorkspaceInfoDto + public function dispatchSetupIfNeeded(string $projectId, string $userEmail): WorkspaceInfoDto { // Use transaction to prevent race conditions when creating workspace $this->entityManager->beginTransaction(); @@ -94,7 +94,7 @@ public function dispatchSetupIfNeeded(string $projectId): WorkspaceInfoDto $this->entityManager->flush(); // Dispatch async setup message - $this->messageBus->dispatch(new SetupWorkspaceMessage($workspaceId)); + $this->messageBus->dispatch(new SetupWorkspaceMessage($workspaceId, $userEmail)); } $this->entityManager->commit(); diff --git a/src/WorkspaceMgmt/Facade/WorkspaceMgmtFacadeInterface.php b/src/WorkspaceMgmt/Facade/WorkspaceMgmtFacadeInterface.php index d4a3999..23ee01b 100644 --- a/src/WorkspaceMgmt/Facade/WorkspaceMgmtFacadeInterface.php +++ b/src/WorkspaceMgmt/Facade/WorkspaceMgmtFacadeInterface.php @@ -27,9 +27,12 @@ public function getWorkspaceForProject(string $projectId): ?WorkspaceInfoDto; * Returns immediately with workspace info. Check workspace status * to determine if setup is in progress. * + * @param string $projectId the project ID + * @param string $userEmail email of the user triggering setup (used for human-friendly branch name) + * * @return WorkspaceInfoDto workspace info (status may be IN_SETUP if setup was dispatched) */ - public function dispatchSetupIfNeeded(string $projectId): WorkspaceInfoDto; + public function dispatchSetupIfNeeded(string $projectId, string $userEmail): WorkspaceInfoDto; /** * Transition workspace to IN_CONVERSATION status. diff --git a/src/WorkspaceMgmt/Infrastructure/Handler/SetupWorkspaceHandler.php b/src/WorkspaceMgmt/Infrastructure/Handler/SetupWorkspaceHandler.php index f83e0e0..905604d 100644 --- a/src/WorkspaceMgmt/Infrastructure/Handler/SetupWorkspaceHandler.php +++ b/src/WorkspaceMgmt/Infrastructure/Handler/SetupWorkspaceHandler.php @@ -43,7 +43,7 @@ public function __invoke(SetupWorkspaceMessage $message): void 'workspaceId' => $message->workspaceId, ]); - $this->setupService->setup($workspace); + $this->setupService->setup($workspace, $message->userEmail); $this->logger->info('Async workspace setup completed', [ 'workspaceId' => $message->workspaceId, diff --git a/src/WorkspaceMgmt/Infrastructure/Message/SetupWorkspaceMessage.php b/src/WorkspaceMgmt/Infrastructure/Message/SetupWorkspaceMessage.php index f5e53e0..0892b59 100644 --- a/src/WorkspaceMgmt/Infrastructure/Message/SetupWorkspaceMessage.php +++ b/src/WorkspaceMgmt/Infrastructure/Message/SetupWorkspaceMessage.php @@ -13,7 +13,8 @@ readonly class SetupWorkspaceMessage implements ImmediateSymfonyMessageInterface { public function __construct( - public string $workspaceId + public string $workspaceId, + public string $userEmail, ) { } } diff --git a/tests/Unit/WorkspaceMgmt/BranchNameGeneratorTest.php b/tests/Unit/WorkspaceMgmt/BranchNameGeneratorTest.php new file mode 100644 index 0000000..5c10613 --- /dev/null +++ b/tests/Unit/WorkspaceMgmt/BranchNameGeneratorTest.php @@ -0,0 +1,77 @@ +generate($workspaceId, $userEmail); + + // Timestamp: YYYY-MM-DD H:i:s + expect($result)->toMatch('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}-/'); + // Sanitized email segment + expect($result)->toContain('-userATexampleDOTcom-'); + // Short workspace ID (first 8 chars) + expect($result)->toEndWith('-aaaaaaaa'); + }); + + it('uses first 8 characters of workspace ID as short ID', function (): void { + $generator = new BranchNameGenerator(); + $workspaceId = 'deadbeef-1234-5678-abcd-ffffffffffff'; + $userEmail = 'a@b.co'; + + $result = $generator->generate($workspaceId, $userEmail); + + expect($result)->toEndWith('-deadbeef'); + }); + + it('matches full format regex and contains sanitized email and short workspace id', function (): void { + $generator = new BranchNameGenerator(); + $workspaceId = '12345678-aaaa-bbbb-cccc-dddddddddddd'; + $userEmail = 'foo@bar.baz'; + + $result = $generator->generate($workspaceId, $userEmail); + + // Full format: YYYY-MM-DD H:i:s-sanitizedEmail-shortId (shortId = 8 chars) + expect($result)->toMatch('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}-fooATbarDOTbaz-12345678$/'); + }); + }); + + describe('sanitizeEmailForBranchName', function (): void { + it('replaces @ with AT and . with DOT', function (): void { + $generator = new BranchNameGenerator(); + + expect($generator->sanitizeEmailForBranchName('user@example.com')) + ->toBe('userATexampleDOTcom'); + }); + + it('normalizes email to lowercase', function (): void { + $generator = new BranchNameGenerator(); + + expect($generator->sanitizeEmailForBranchName('User@Example.COM')) + ->toBe('userATexampleDOTcom'); + }); + + it('handles multiple dots in domain', function (): void { + $generator = new BranchNameGenerator(); + + expect($generator->sanitizeEmailForBranchName('a@b.c.d')) + ->toBe('aATbDOTcDOTd'); + }); + + it('handles subdomain-style email', function (): void { + $generator = new BranchNameGenerator(); + + expect($generator->sanitizeEmailForBranchName('manuel@kiessling.net')) + ->toBe('manuelATkiesslingDOTnet'); + }); + }); +}); From 62f93afbd154bed89f36f28b47c5572e2935c800 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Kie=C3=9Fling?= Date: Thu, 5 Feb 2026 18:56:38 +0100 Subject: [PATCH 2/3] Add JOBOO! GmbH sponsorship attribution to footer and app shell --- .../Resources/templates/base_appshell.html.twig | 8 ++++++++ .../Resources/templates/base_fullcontent.html.twig | 6 ++++++ 2 files changed, 14 insertions(+) diff --git a/src/Common/Presentation/Resources/templates/base_appshell.html.twig b/src/Common/Presentation/Resources/templates/base_appshell.html.twig index acdd6c2..c04fc0b 100644 --- a/src/Common/Presentation/Resources/templates/base_appshell.html.twig +++ b/src/Common/Presentation/Resources/templates/base_appshell.html.twig @@ -61,4 +61,12 @@
{% block content %}{% endblock %}
+
+

+ Sponsored by + + JOBOO! GmbH + +

+
{% endblock %} diff --git a/src/Common/Presentation/Resources/templates/base_fullcontent.html.twig b/src/Common/Presentation/Resources/templates/base_fullcontent.html.twig index 9ad881b..09fd887 100644 --- a/src/Common/Presentation/Resources/templates/base_fullcontent.html.twig +++ b/src/Common/Presentation/Resources/templates/base_fullcontent.html.twig @@ -10,4 +10,10 @@ · {% include '@common.presentation/_language_switcher.html.twig' %} +
+ A DX Tooling project, sponsored by + + JOBOO! GmbH + +
{% endblock %} From 909d4d88dbddf09157750d5d132cd22e2341a9fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20Kie=C3=9Fling?= Date: Fri, 6 Feb 2026 09:28:26 +0100 Subject: [PATCH 3/3] chore: URL/shell-safe branch name format + Symfony services rule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Branch name timestamp: Y-m-d H:i:s → Y-m-d_H-i-s (no space/colons) - Add .cursor/rules/08-symfony-services.mdc: do not add explicit interface bindings when autowiring resolves single implementation - Remove redundant BranchNameGeneratorInterface and WorkspaceStatusGuardInterface from config/services.yaml - Update docs and tests for new format Co-authored-by: Cursor --- .cursor/rules/08-symfony-services.mdc | 29 +++++++++++++++++++ config/services.yaml | 7 ----- ...kspace-conversation-workflow-high-level.md | 2 +- .../Domain/Service/BranchNameGenerator.php | 5 ++-- .../Service/BranchNameGeneratorInterface.php | 2 +- .../WorkspaceMgmt/BranchNameGeneratorTest.php | 10 +++---- 6 files changed, 39 insertions(+), 16 deletions(-) create mode 100644 .cursor/rules/08-symfony-services.mdc diff --git a/.cursor/rules/08-symfony-services.mdc b/.cursor/rules/08-symfony-services.mdc new file mode 100644 index 0000000..799512a --- /dev/null +++ b/.cursor/rules/08-symfony-services.mdc @@ -0,0 +1,29 @@ +--- +description: "Symfony service container, autowiring, config/services.yaml. Use when adding or changing services, interfaces, or DI configuration." +globs: ["config/services.yaml", "config/**/services*.yaml", "src/**/*.php"] +alwaysApply: false +--- + +# Symfony Services and Autowiring + +**Reference**: See [Symfony Autowiring](https://symfony.com/doc/current/service_container/autowiring.html). + +## Interface Bindings + +**Do not add explicit interface → implementation bindings in `config/services.yaml` when:** + +- The interface has **exactly one** concrete implementation in the application, and +- That implementation is in the autowired resource path (e.g. `App\` → `../src/`). + +Symfony's autowiring will automatically inject the single implementation when a type hint asks for the interface. Adding a redundant `InterfaceName: class: ImplementationName` entry is unnecessary and should be avoided. + +**Add explicit configuration only when:** + +- The implementation lives outside the autowired resource (e.g. in a third-party bundle or vendor), or +- You need to pass specific constructor arguments, or +- There are multiple implementations and you must choose one (or use tags/compiler passes), or +- You need an alias to a decorator or a specific service id. + +## When in Doubt + +Prefer omitting the binding and relying on autowiring; if the container fails with "multiple implementations" or "no service for interface", then add the minimal explicit configuration needed. diff --git a/config/services.yaml b/config/services.yaml index aed7800..c631da8 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -133,13 +133,6 @@ services: App\ChatBasedContentEditor\Facade\ChatBasedContentEditorFacadeInterface: class: App\ChatBasedContentEditor\Facade\ChatBasedContentEditorFacade - # Domain service bindings - App\WorkspaceMgmt\Domain\Service\BranchNameGeneratorInterface: - class: App\WorkspaceMgmt\Domain\Service\BranchNameGenerator - - App\WorkspaceMgmt\Domain\Service\WorkspaceStatusGuardInterface: - class: App\WorkspaceMgmt\Domain\Service\WorkspaceStatusGuard - # Infrastructure adapter bindings App\WorkspaceMgmt\Infrastructure\Adapter\GitAdapterInterface: class: App\WorkspaceMgmt\Infrastructure\Adapter\GitCliAdapter diff --git a/docs/plans/project-workspace-conversation-workflow-high-level.md b/docs/plans/project-workspace-conversation-workflow-high-level.md index 29a8aeb..88da0e4 100644 --- a/docs/plans/project-workspace-conversation-workflow-high-level.md +++ b/docs/plans/project-workspace-conversation-workflow-high-level.md @@ -43,7 +43,7 @@ When a user wants to start a conversation on a project, and there is a workspace - The workspace is set into status IN_SETUP - The local workspace folder is removed - The git repository mapped to the project that is mapped to the workspace is cloned into the local workspace folder using the github token of the project -- A new branch, whose name has the format `-usermailATdomainDOTtld-SHORTWORKSPACEID` (timestamp, sanitized user email, short workspace ID), is created from the main branch +- A new branch, whose name has the format `-usermailATdomainDOTtld-SHORTWORKSPACEID` (timestamp with underscore/hyphens for URL/shell safety, sanitized user email, short workspace ID), is created from the main branch - The workspace is set into status AVAILABLE_FOR_CONVERSATION When a user wants to start a conversation on a project, and succeeds to do so because its workspace is in status AVAILABLE_FOR_CONVERSATION, then diff --git a/src/WorkspaceMgmt/Domain/Service/BranchNameGenerator.php b/src/WorkspaceMgmt/Domain/Service/BranchNameGenerator.php index b029fee..0b449ac 100644 --- a/src/WorkspaceMgmt/Domain/Service/BranchNameGenerator.php +++ b/src/WorkspaceMgmt/Domain/Service/BranchNameGenerator.php @@ -13,13 +13,14 @@ /** * Generates human-friendly workspace branch names. * - * Format: -usermailATdomainDOTtld-SHORTWORKSPACEID + * Format: -usermailATdomainDOTtld-SHORTWORKSPACEID + * (underscore and hyphens only in timestamp for URL/shell safety) */ final class BranchNameGenerator implements BranchNameGeneratorInterface { public function generate(string $workspaceId, string $userEmail): string { - $timestamp = DateAndTimeService::getDateTimeImmutable()->format('Y-m-d H:i:s'); + $timestamp = DateAndTimeService::getDateTimeImmutable()->format('Y-m-d_H-i-s'); $sanitized = $this->sanitizeEmailForBranchName($userEmail); $shortId = mb_substr($workspaceId, 0, 8); diff --git a/src/WorkspaceMgmt/Domain/Service/BranchNameGeneratorInterface.php b/src/WorkspaceMgmt/Domain/Service/BranchNameGeneratorInterface.php index fcbe664..472fce2 100644 --- a/src/WorkspaceMgmt/Domain/Service/BranchNameGeneratorInterface.php +++ b/src/WorkspaceMgmt/Domain/Service/BranchNameGeneratorInterface.php @@ -10,7 +10,7 @@ interface BranchNameGeneratorInterface { /** - * Generate a branch name in format: <YYYY-MM-DD H:i:s>-usermailATdomainDOTtld-SHORTWORKSPACEID. + * Generate a branch name in format: <YYYY-MM-DD_HH-MM-SS>-usermailATdomainDOTtld-SHORTWORKSPACEID (URL/shell safe). */ public function generate(string $workspaceId, string $userEmail): string; } diff --git a/tests/Unit/WorkspaceMgmt/BranchNameGeneratorTest.php b/tests/Unit/WorkspaceMgmt/BranchNameGeneratorTest.php index 5c10613..71087b4 100644 --- a/tests/Unit/WorkspaceMgmt/BranchNameGeneratorTest.php +++ b/tests/Unit/WorkspaceMgmt/BranchNameGeneratorTest.php @@ -8,15 +8,15 @@ describe('BranchNameGenerator', function (): void { describe('generate', function (): void { - it('returns branch name in format YYYY-MM-DD H:i:s-sanitizedEmail-shortWorkspaceId', function (): void { + it('returns branch name in format YYYY-MM-DD_HH-MM-SS-sanitizedEmail-shortWorkspaceId (URL/shell safe)', function (): void { $generator = new BranchNameGenerator(); $workspaceId = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee'; $userEmail = 'user@example.com'; $result = $generator->generate($workspaceId, $userEmail); - // Timestamp: YYYY-MM-DD H:i:s - expect($result)->toMatch('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}-/'); + // Timestamp: YYYY-MM-DD_HH-MM-SS (no space, no colons) + expect($result)->toMatch('/^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-/'); // Sanitized email segment expect($result)->toContain('-userATexampleDOTcom-'); // Short workspace ID (first 8 chars) @@ -40,8 +40,8 @@ $result = $generator->generate($workspaceId, $userEmail); - // Full format: YYYY-MM-DD H:i:s-sanitizedEmail-shortId (shortId = 8 chars) - expect($result)->toMatch('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}-fooATbarDOTbaz-12345678$/'); + // Full format: YYYY-MM-DD_HH-MM-SS-sanitizedEmail-shortId (shortId = 8 chars, URL/shell safe) + expect($result)->toMatch('/^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}-fooATbarDOTbaz-12345678$/'); }); });