Feature: The article title rule is the same in the request and the domain As the maintainer of RenewCMS I want PostArticle's title validation and article.CreateArticle's title validation to enforce the identical rule, counted the identical way So that a title the request layer accepts can never be rejected by the domain as a surprise 500, and a domain title rejection that does happen is always reported as a 400 with a client-facing apperr message instead of a raw 500 Background: Given internal/domain/article defines the sentinel article.ErrInvalidTitle And CreateArticleUseCase.CreateArticle rejects a title whose length, counted with utf8.RuneCountInString (runes, not bytes), is below 3 or above 200, returning article.ErrInvalidTitle and storing nothing And api/handlers/article/articleHandler.go's PostArticle.Title carries validate:"required,min=3,max=200", and go-playground/validator's min/max on a string already count runes with utf8.RuneCountInString, so the request tag and the domain check now agree on both the bound (3-200) and the unit (runes) And api/pkg/apperr defines ArticleTitleErrorMessage as "Article title must be between 3 and 200 characters.", a client-facing string distinct from both the raw validator error text and "Failed to create article" And Handler.PostArticle maps errors.Is(err, article.ErrInvalidTitle) from CreateArticleUseCase.CreateArticle to a 400 response with body apperr.ArticleTitleErrorMessage; any other error from CreateArticle still answers 500 with body "Failed to create article" And POST /v1/article/ sits behind the jwtauth Verifier and Authenticator group and requires a valid jwt cookie And UpdateArticleUseCase exposes only UpdateBody, AddImage and UpdateIsOnline; no route or use case updates an article's title after creation, so this feature only concerns article creation Scenario Outline: CreateArticle now measures title length in runes, not bytes Given an empty article repository fake When CreateArticle is called with a title made of repetitions of the 4-byte rune "😀" and a body "Some body text" Then the result is Examples: | rune_count | outcome | | 2 | ErrInvalidTitle, nothing stored | | 3 | created | | 200 | created | | 201 | ErrInvalidTitle, nothing stored | Scenario Outline: POST /v1/article/ enforces the same 3-200 rune boundary over HTTP Given a valid jwt cookie for a user with id 1 When the client sends POST /v1/article/ with that cookie, a title of ASCII characters and a body "Some body text" Then the response status is And Examples: | length | status | storage_effect | | 2 | 400 | not created | | 3 | 201 | created, is_online false | | 200 | 201 | created, is_online false | | 201 | 400 | not created | Scenario: A 200-rune multibyte title is accepted end to end over HTTP Given a valid jwt cookie for a user with id 1 When the client sends POST /v1/article/ with that cookie, a title made of 200 repetitions of the 4-byte rune "😀" (200 runes, 800 bytes) and a body "Some body text" Then the response status is 201 And the response body is the created article's JSON with that title Scenario: The handler maps a domain title rejection to 400, not 500 Given a valid jwt cookie for a user with id 1 And the article repository fake is configured so its next Create call returns article.ErrInvalidTitle directly, standing in for any layer beneath the use case's own check rejecting the title with that same sentinel When the client sends POST /v1/article/ with that cookie, title "Valid Title" and body "Some body text" (both already satisfying PostArticle's own validate tags) Then the response status is 400 And the response body is the text apperr.ArticleTitleErrorMessage And the response body is not "Failed to create article" Scenario: A non-title repository failure during creation still answers 500 Given a valid jwt cookie for a user with id 1 And the article repository fake is configured to fail on its next Create call with a generic repository error, not article.ErrInvalidTitle When the client sends POST /v1/article/ with that cookie, title "Valid Title" and body "Some body text" Then the response status is 500 And the response body is the text "Failed to create article"