feat(api): documents[].reason + messages 对不存在会话回 404(安卓回执九两条;契约 v0.63)
- ① documents[] 元素增可选 reason(omitempty):装配期在给出 status 的**同一处**填充,取的**就是注入模型清单里那份原因文案**(同一字符串两处用,服务端不为客户端另编一句),只在 unreadable 时下发;随 S3.5 的回合后回写落进 document 列 ⇒ 历史接口 / 握手回放都有。文案只含原因、不含落盘路径/存储名以外内容/凭据(有独特目录名反证断言)。
- ② GET /sessions/{id}/messages:不存在/已删除 ⇒ 404 session_not_found;**存在但无消息仍 200 {data:[]}**(空会话绝不能被答成 404);参数形状校验仍先于 404;存在性判定复用既有窄视图(history.Store.SessionPersona,未知 id 报 ErrSessionNotFound),**未新增 SQL/存储方法**;判不出来(未接该视图)保持旧语义(判不出 ≠ 不存在),判定本身失败回 500 不静默。
- 契约 v0.63(三档:reason 纯增量 / messages 404 行为变更 / 无破坏性):api-client §1 行 6 + §1.2④ 的 10 条逐字文案 + §4.4 错误码 + changelog + banner;api-reference §1.2 行 6 + §3.4 + §3.8 DTO + §8.1 错误码表。
- 主会话独立核对的副作用(隔离会话 sess-* 现在也 404):查活库 chat_sessions 里**根本没有 sess-* 行**(只有 web 与 web:<uuid>),sess-backthink 消息数 0,前端唯一提到 sess-backthink 的是 graph_update 的内存桶测试 ⇒ **没有客户端按 id 读隔离会话的 messages**;且这与 POST /sessions/{id}/read(v0.37 起即 404)一致 ⇒ 接受不加 sess- 例外,并在回执里问安卓确认。
- 主会话独立验收:隔离 worktree(S9 的 13 文件)build/vet 绿、整树连跑 2 次 59 ok / 0 FAIL、reason/404 用例 8 PASS、双平台交叉编译绿;**变异亲自复现**(在调用点短路 404 守卫 ⇒ UnknownSessionIs404 与 DeletedSessionIs404 双红:status = 200, want 404)。
- 如实登记:验收过程中出现过**一次不可复现的子集 FAIL**(restore 之后那次 Session|Reason 子集跑;未记到用例名)—— 随后同一子集、那三条用例 ×5、以及整树 ×2 全绿,判为瞬时;已记入账本,后续留意。
This commit is contained in:
1 parent
c1fa1e9b9a
commit
902b6eecfd
13 files changed
+779
-21
No files matched your search
@@ -114,12 +114,15 @@ func TestMergeDocumentStatus(t *testing.T) {
|
||||
raw, err := history.EncodeDocuments([]history.DocumentRef{
|
||||
{Name: "aaa.txt", MIME: "text/plain", Size: 1},
|
||||
{Name: "bbb.pdf", MIME: "application/pdf", Size: 2},
|
||||
{Name: "ccc.txt", MIME: "text/plain", Size: 3},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
const scanReason = "无法抽取正文:这是图片型/无文本层 PDF,需要 OCR 才能读(当前未做)"
|
||||
merged, changed, err := mergeDocumentStatus(raw, []history.DocumentRef{
|
||||
{Name: "aaa.txt", Status: history.DocumentStatusExtracted, Chars: 42, Truncated: true},
|
||||
{Name: "bbb.pdf", Status: history.DocumentStatusUnreadable, Reason: scanReason},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("merge: %v", err)
|
||||
@@ -131,17 +134,26 @@ func TestMergeDocumentStatus(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("decode merged: %v", err)
|
||||
}
|
||||
if len(refs) != 2 {
|
||||
t.Fatalf("refs = %+v, want 2 条(没匹配到的原样保留)", refs)
|
||||
if len(refs) != 3 {
|
||||
t.Fatalf("refs = %+v, want 3 条(没匹配到的原样保留)", refs)
|
||||
}
|
||||
if refs[0].Status != history.DocumentStatusExtracted || refs[0].Chars != 42 || !refs[0].Truncated || refs[0].MIME != "text/plain" || refs[0].Size != 1 {
|
||||
t.Fatalf("refs[0] = %+v, want 结论补上且身份不变", refs[0])
|
||||
}
|
||||
if refs[1].Status != "" || refs[1].Name != "bbb.pdf" {
|
||||
t.Fatalf("refs[1] = %+v, want 原样保留(本回合没解析到它)", refs[1])
|
||||
// v0.63:reason 与 status 同批回写,客户端因此不会看到「unreadable 但没有理由」。
|
||||
if refs[1].Status != history.DocumentStatusUnreadable || refs[1].Reason != scanReason {
|
||||
t.Fatalf("refs[1] = %+v, want unreadable + 原因文案", refs[1])
|
||||
}
|
||||
if refs[2].Status != "" || refs[2].Reason != "" || refs[2].Name != "ccc.txt" {
|
||||
t.Fatalf("refs[2] = %+v, want 原样保留(本回合没解析到它)", refs[2])
|
||||
}
|
||||
|
||||
if _, changed, err := mergeDocumentStatus(merged, []history.DocumentRef{{Name: "aaa.txt", Status: history.DocumentStatusExtracted, Chars: 42, Truncated: true}}); err != nil || changed {
|
||||
// 结论与原因都补齐后,同一批再合一次必须报「无变化」——reason 也是变化判据的
|
||||
// 一部分(否则每回合都会因为空 reason 而重写一次)。
|
||||
if _, changed, err := mergeDocumentStatus(merged, []history.DocumentRef{
|
||||
{Name: "aaa.txt", Status: history.DocumentStatusExtracted, Chars: 42, Truncated: true},
|
||||
{Name: "bbb.pdf", Status: history.DocumentStatusUnreadable, Reason: scanReason},
|
||||
}); err != nil || changed {
|
||||
t.Fatalf("changed = %v, err = %v, want 无变化且无错(不必写库)", changed, err)
|
||||
}
|
||||
if _, _, err := mergeDocumentStatus("{not json", nil); !errors.Is(err, history.ErrBadDocuments) {
|
||||
|
||||
Reference in new issue
Block a user