fix: keep mail +watch fetch-failure output on one NDJSON line

The default json watch stream emits one enveloped JSON object per line via
PrintNdjson, but the fetch-message failure branch used PrintJson (multi-line
MarshalIndent) and bypassed the envelope, so a mid-stream failure injected a
multi-line {ok:false,...} blob that breaks line-based NDJSON parsers. Route
failures through watchFailureOutputValue: bare payload for --format data,
identity-tagged single line for the default json envelope.
This commit is contained in:
zhaojunlin.0405
2026-07-13 12:02:45 +08:00
parent fa66195826
commit a871d1a064
2 changed files with 62 additions and 1 deletions

View File

@@ -332,7 +332,7 @@ var MailWatch = common.Shortcut{
output.PrintError(errOut, fmt.Sprintf("failed to write event file: %v", writeErr))
}
}
output.PrintJson(out, failureData)
output.PrintNdjson(out, watchFailureOutputValue(outFormat, string(runtime.As()), failureData))
return
}
}
@@ -693,6 +693,22 @@ func watchOutputValue(outFormat, identity string, outputData interface{}) interf
return output.Envelope{OK: true, Identity: identity, Data: outputData}
}
// watchFailureOutputValue frames a fetch-failure payload for the watch stream,
// mirroring watchOutputValue so the default json stream stays one JSON object
// per line: bare payload for --format data, identity-tagged single line for the
// default json envelope. failureData already carries {ok:false, error, ...}.
func watchFailureOutputValue(outFormat, identity string, failureData map[string]interface{}) interface{} {
if outFormat == "data" || identity == "" {
return failureData
}
enriched := make(map[string]interface{}, len(failureData)+1)
for k, v := range failureData {
enriched[k] = v
}
enriched["identity"] = identity
return enriched
}
func watchFetchFailureValue(messageID, fetchFormat string, err error, eventBody map[string]interface{}) map[string]interface{} {
payload := map[string]interface{}{
"ok": false,

View File

@@ -941,3 +941,48 @@ func TestWatchOutputValueDefaultIsJSONEnvelope(t *testing.T) {
t.Fatalf("--format data payload should carry message, got %s", b2)
}
}
// P1: fetch 失败分支必须与成功分支同一 NDJSON 框架 —— 默认 json 输出一行
// ok:false + identity + error--format data 输出一行裸 failureData绝不能走
// PrintJson 的多行 pretty会破坏默认 json 的 NDJSON 流)。
func TestWatchFailureOutputValueDefaultIsJSONEnvelope(t *testing.T) {
failure := map[string]interface{}{
"ok": false,
"error": map[string]interface{}{"type": "fetch_message_failed", "message_id": "m1"},
}
// default (json) → identity-tagged single-line failure envelope, ok:false preserved
b, err := json.Marshal(watchFailureOutputValue("json", "user", failure))
if err != nil {
t.Fatalf("marshal json failure value: %v", err)
}
var env map[string]interface{}
if err := json.Unmarshal(b, &env); err != nil {
t.Fatalf("unmarshal json failure value: %v", err)
}
if env["ok"] != false {
t.Fatalf("failure json must keep ok:false, got %s", b)
}
if env["identity"] != "user" {
t.Fatalf("failure json must carry identity, got %s", b)
}
if _, ok := env["error"]; !ok {
t.Fatalf("failure json must carry error, got %s", b)
}
// --format data → bare failure payload, no identity injected
b2, err := json.Marshal(watchFailureOutputValue("data", "user", failure))
if err != nil {
t.Fatalf("marshal data failure value: %v", err)
}
var bare map[string]interface{}
if err := json.Unmarshal(b2, &bare); err != nil {
t.Fatalf("unmarshal data failure value: %v", err)
}
if _, hasIdentity := bare["identity"]; hasIdentity {
t.Fatalf("--format data failure must not inject identity, got %s", b2)
}
if bare["ok"] != false {
t.Fatalf("--format data failure should carry ok:false payload, got %s", b2)
}
}