fix(ticket): register the ticket schema inside the users sections array

/users/ticket loaded in the SPA but the form never did: the console showed
"未知的设置分组: ticket" from /settings/schema/users/ticket. The ticket section
was composed one array level too high in SchemaUsers::build() — as a sibling of
'sections' rather than inside it — so SchemaProvider::get_sections(), which reads
only $schema['sections'], never saw it and the REST handler 404'd.

Every other axis was wired correctly: the route, the menu entry, users/ticket.js
and SchemaUsersTicket itself. That is why only this page failed while sibling
/users/* pages worked. Move the entry inside sections.

The contract test could not catch this because it asserted only that the compose
call appears somewhere in the file. Replace it with a bracket-balanced scan that
locates the sections array bounds and asserts the compose call falls inside, so
nesting mistakes are visible.

Also update the two ticket contract tests that still read the trait from
src/Ticket/TicketRepository-traits/, its pre-flattening path. They assert on the
trait's contents, so they were silently skipping the very file the earlier fatal
was about.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Opus 5 committed 2026-10-26 14:16:25 +08:00
1 parent ffd8d7d1b7
commit d32bdc0581
4 files changed
+27 -5

No files matched your search

+1 -1
View File
@@ -611,8 +611,8 @@ class SchemaUsers {
]],
],
],
'ticket' => Users\SchemaUsersTicket::build(),
],
'ticket' => Users\SchemaUsersTicket::build(),
];
}
}
@@ -28,14 +28,14 @@ test('transition rules live in one place', () => {
assert.match(body, /if \(\$from === self::KEY_PENDING && \$to === self::KEY_CLOSED\) \{\s*\n\s*return false;/);
// 上层只调用 can_transition,不复制合法性判断。
for (const file of ['src/Ticket/TicketRepository-traits/TicketStatus.php', 'src/Ticket/TicketRestAdmin.php', 'src/Admin/Native/UsersMenusTicketsCenter.php']) {
for (const file of ['src/Ticket/TicketStatus.php', 'src/Ticket/TicketRestAdmin.php', 'src/Admin/Native/UsersMenusTicketsCenter.php']) {
const src = read(file);
assert.ok(src.includes('can_transition'), `${file} must delegate to can_transition`);
}
});
test('reply and receive drive the documented state changes', () => {
const status = read('src/Ticket/TicketRepository-traits/TicketStatus.php');
const status = read('src/Ticket/TicketStatus.php');
// 管理员回复 → 已回复;用户回复 → 待回复。
assert.match(status, /\$target = \$author_type === 'admin'\s*\n\s*\? TicketStatusService::KEY_ADMIN_REPLIED\s*\n\s*: TicketStatusService::KEY_USER_REPLIED;/);
@@ -52,7 +52,7 @@ test('reply and receive drive the documented state changes', () => {
});
test('closing notifies the counterparty exactly once', () => {
const status = read('src/Ticket/TicketRepository-traits/TicketStatus.php');
const status = read('src/Ticket/TicketStatus.php');
assert.match(status, /if \(\$to === TicketStatusService::KEY_CLOSED\) \{\s*\n\s*TicketNotifier::closed\(\$ticket, \$actor\);/);
const notifier = read('src/Ticket/TicketNotifier.php');
@@ -116,7 +116,7 @@ test('closed tickets refuse replies and cannot be re-closed', () => {
assert.match(admin, /if \(!empty\(\$ticket\['is_closed'\]\)\) \{\s*\n\s*return new \\WP_Error\('aksrt_ticket_closed'/);
// close_ticket 自身也挡已完结(双保险,避免前端绕过 UI 直接 POST)。
const status = read('src/Ticket/TicketRepository-traits/TicketStatus.php');
const status = read('src/Ticket/TicketStatus.php');
assert.match(status, /if \(TicketStatusService::is_closed\(\(string\) \(\$ticket\['status'\] \?\? ''\)\)\) \{\s*\n\s*return false;/);
});
@@ -84,6 +84,28 @@ test('ticket schema is registered as a schema page under users', () => {
// PHP 侧:分区由 SchemaUsers 组装,SchemaUsersTicket 提供内容。
const users = read('src/Admin/Schema/SchemaUsers.php');
assert.ok(users.includes("'ticket' => Users\\SchemaUsersTicket::build()"), 'PHP section not composed');
// 组装位置必须在 'sections' => [ ... ] 内部。SchemaProvider::get_sections() 只读
// $schema['sections'],分区挂在同级会让 /settings/schema/users/ticket 404
// (SPA 报「未知的设置分组」)。定位 sections 开括号后用配平扫描找闭括号,
// 确认 compose 调用落在区间内——单纯 includes() 查不出层级错误。
const sectionsAt = users.indexOf("'sections' => [");
assert.ok(sectionsAt !== -1, 'sections array not found');
const composeAt = users.indexOf("'ticket' => Users\\SchemaUsersTicket::build()");
assert.ok(composeAt > sectionsAt, 'ticket must be composed after sections opens');
let depth = 0;
let sectionsEnd = -1;
for (let i = users.indexOf('[', sectionsAt); i < users.length; i++) {
if (users[i] === '[') depth++;
else if (users[i] === ']') {
depth--;
if (depth === 0) {
sectionsEnd = i;
break;
}
}
}
assert.ok(sectionsEnd !== -1, 'unbalanced sections array');
assert.ok(composeAt < sectionsEnd, 'ticket section must be INSIDE the sections array');
const ticket = read('src/Admin/Schema/Users/SchemaUsersTicket.php');
assert.ok(ticket.includes("'key' => 'ticket'"), 'PHP section key missing');
assert.ok(ticket.includes("'option_key' => 'aksrt_settings'"), 'PHP option key missing');