What happens
BoardModel is keyed by project id, so a freshly registered handler is attached to no board. Every action dispatched in that state answers with the bare string stoull — the what() of the std::invalid_argument that std::stoull("") throws — delivered as though it were a domain error.
send register typeId=BoardModel -> ok
execute GetBoardState -> err "stoull"
execute CreateColumn name="Todo" -> err "stoull"
execute CreateTask ... -> err "stoull"
execute MoveTaskPosition ... -> err "stoull"
execute AddComment ... -> err "stoull"
execute GetEventsSince lastEventId=0 -> err "stoull"
execute GetActivity -> err "stoull"
execute GetRules projectId=1 -> err "stoull"
The part that makes it worth an issue
Two of those actions already have a guard with exactly the right message. GetActivity:
// examples/kanban/src/models/board_model.cpp
GetActivityResult BoardModel::execute(const GetActivity& /*action*/) {
if (!_projectIdStr.has_value()) {
throw NotFound{"GetActivity: handler was never attached via OpenBoard"};
}
requireRole(Role::Viewer);
...
and GetRules has the same shape. Neither ever fires. On an unattached handler _projectIdStr is engaged with an empty string, so has_value() is true, control falls through to requireRole, and requireRole reaches std::stoull(*_projectIdStr).
That is worse than having no guard: reading the source suggests the good message exists, so the behaviour looks correct until someone drives it. polls::PollModel gets this right and answers "GetPollState: handler was never attached via OpenPoll", which is the message a client can act on and the one this rung appears to have been copying.
Why it matters to a client
stoull names nothing: not the action, not the model, not the mistake. A client cannot distinguish "I forgot to call OpenBoard" from an internal fault, and cannot present anything useful. It also leaks an implementation detail of the id representation into the wire contract.
Suggested fix
Make the sentinel unambiguous — either leave _projectIdStr disengaged until OpenBoard succeeds, or have requireRole (and the other stoull call sites, board_model.cpp:304, 333, 347, 390, 432, 490, …) check for an empty string and throw the same NotFound{"<Action>: handler was never attached via OpenBoard"} the two existing guards already spell. Whichever way, the two guards that exist today should start firing.
Where it is pinned
scripts/scenario/scenarios/kanban/a-board-must-be-opened-before-it-answers.scenario asserts the current replies per action, including that the two guarded ones do not mention OpenBoard, so a fix has to come through that file.
Found by the out-of-process scenario corpus. The in-process tests attach before acting, so no test had reached this state over a server.
What happens
BoardModelis keyed by project id, so a freshly registered handler is attached to no board. Every action dispatched in that state answers with the bare stringstoull— thewhat()of thestd::invalid_argumentthatstd::stoull("")throws — delivered as though it were a domain error.The part that makes it worth an issue
Two of those actions already have a guard with exactly the right message.
GetActivity:and
GetRuleshas the same shape. Neither ever fires. On an unattached handler_projectIdStris engaged with an empty string, sohas_value()is true, control falls through torequireRole, andrequireRolereachesstd::stoull(*_projectIdStr).That is worse than having no guard: reading the source suggests the good message exists, so the behaviour looks correct until someone drives it.
polls::PollModelgets this right and answers"GetPollState: handler was never attached via OpenPoll", which is the message a client can act on and the one this rung appears to have been copying.Why it matters to a client
stoullnames nothing: not the action, not the model, not the mistake. A client cannot distinguish "I forgot to callOpenBoard" from an internal fault, and cannot present anything useful. It also leaks an implementation detail of the id representation into the wire contract.Suggested fix
Make the sentinel unambiguous — either leave
_projectIdStrdisengaged untilOpenBoardsucceeds, or haverequireRole(and the otherstoullcall sites,board_model.cpp:304, 333, 347, 390, 432, 490, …) check for an empty string and throw the sameNotFound{"<Action>: handler was never attached via OpenBoard"}the two existing guards already spell. Whichever way, the two guards that exist today should start firing.Where it is pinned
scripts/scenario/scenarios/kanban/a-board-must-be-opened-before-it-answers.scenarioasserts the current replies per action, including that the two guarded ones do not mentionOpenBoard, so a fix has to come through that file.Found by the out-of-process scenario corpus. The in-process tests attach before acting, so no test had reached this state over a server.