前端:失败终态载荷的 message 缺字段不再打断 reducer
directThreadChat.ts:`turn.completed.failure.message` 在生成类型里是必填 string,但跨 IPC 的载荷没有运行时校验,缺字段 / null 时 `.trim()` 会在 reducer 里抛错,把这条订阅之后的所有事件一起打断;改成与兄弟函数 directTurnFailureNoticeText 一致的 typeof 判据,取不到非空字符串就按"没有原因"收口。 directThreadChat.test.ts:补一条回归用例(message 为 undefined / null 时不抛错、不补空气泡、终态照样收口);变异验证:撤掉 typeof 判据后该用例变红。
This commit is contained in:
+16
-10
@@ -344,16 +344,22 @@ export function reduceDirectThreadEvent(
|
||||
// 失败终态带 `failure` 载荷:先把它落成本轮最后一条说明条目,再和正常终态走同一个收口
|
||||
// 函数。载荷在、原因非空才算一条说明;空原因不补一条空气泡(终态照样收口)。
|
||||
const failure = event.failure;
|
||||
const withNotice =
|
||||
failure && failure.message.trim()
|
||||
? upsertLiveEntry(state, {
|
||||
itemId: directTurnFailureItemId(eventUserItemId, eventAt),
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
text: directTurnFailureNoticeText(failure.message),
|
||||
at: eventAt,
|
||||
})
|
||||
: state;
|
||||
// `message` 在生成类型里是必填 string,但跨 IPC 的载荷没有运行时校验:缺字段 / `null`
|
||||
// 时直接 `.trim()` 会在 reducer 里抛错,把这一条订阅之后的全部事件一起打断。判据与兄弟
|
||||
// 函数 `directTurnFailureNoticeText` 保持一致,都是"不是非空字符串就当没有原因"。
|
||||
const failureText =
|
||||
failure && typeof failure.message === 'string'
|
||||
? failure.message.trim()
|
||||
: '';
|
||||
const withNotice = failureText
|
||||
? upsertLiveEntry(state, {
|
||||
itemId: directTurnFailureItemId(eventUserItemId, eventAt),
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
text: directTurnFailureNoticeText(failureText),
|
||||
at: eventAt,
|
||||
})
|
||||
: state;
|
||||
return finishDirectThreadTurn(withNotice, eventAt);
|
||||
}
|
||||
case 'item.delta':
|
||||
|
||||
@@ -417,6 +417,29 @@ describe('DirectProject 聊天 reducer', () => {
|
||||
expect(failed.history).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('载荷的 message 缺失或为 null 时不抛错,按"没有原因"收口', () => {
|
||||
// 跨 IPC 的载荷没有运行时校验:字段缺失 / `null` 都到得了 reducer。这里只要求
|
||||
// "不抛错 + 不补空气泡",终态照样收口——抛错会连带打断这条订阅之后的所有事件。
|
||||
for (const message of [undefined, null]) {
|
||||
const malformed = {
|
||||
type: 'turn.completed',
|
||||
status: 'failed',
|
||||
at: 2_000,
|
||||
failure: { kind: 'host-dropped', message },
|
||||
} as unknown as DirectThreadEvent;
|
||||
const failed = reduceDirectThreadEvents(emptyDirectThreadChatState(), [
|
||||
withUserItemId(
|
||||
event({ type: 'turn.started', at: 1_000 }),
|
||||
'direct-codex:turn-1:user',
|
||||
),
|
||||
malformed,
|
||||
]);
|
||||
expect(failed.turnRunning).toBe(false);
|
||||
expect(failed.turnEndedAt).toBe(2_000);
|
||||
expect(failed.history).toHaveLength(0);
|
||||
}
|
||||
});
|
||||
|
||||
it('没有身份时用事件时间派生说明身份,两轮失败不会合并成一条', () => {
|
||||
const first = reduceDirectThreadEvents(emptyDirectThreadChatState(), [
|
||||
event({ type: 'turn.started', at: 1_000 }),
|
||||
|
||||
Reference in New Issue
Block a user