Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions internal/ai/deck_builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,8 @@ func isTransientAICommandError(err error, stdout, stderr string) bool {
"elocked",
"enotacquired",
"temporarily unavailable",
"internal_error",
"stream error",
"timeout",
"timed out",
"connection reset",
Expand Down
21 changes: 21 additions & 0 deletions internal/ai/deck_builder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,27 @@ func TestBuildDeckJSONRetriesTransientAILockError(t *testing.T) {
}
}

func TestBuildDeckJSONRetriesInternalStreamError(t *testing.T) {
runner := &sequenceRunner{calls: []runnerCall{
{stderr: "API Error: stream error: stream ID 1; INTERNAL_ERROR; received from peer", err: errors.New("exit status 1")},
{stdout: `{"pages":[]}`},
}}
b := Builder{Runner: runner}
b.SetCommand("ccs", []string{"codex"})
b.SetRetryDelays([]time.Duration{0})

got, err := b.BuildDeckJSON("# title")
if err != nil {
t.Fatalf("BuildDeckJSON() error = %v", err)
}
if got != `{"pages":[]}` {
t.Fatalf("BuildDeckJSON() = %q, want JSON from retry", got)
}
if runner.count != 2 {
t.Fatalf("runner calls = %d, want 2", runner.count)
}
}

func TestBuildDeckJSONUsesConfiguredRetryDelays(t *testing.T) {
runner := &sequenceRunner{calls: []runnerCall{
{stderr: "timeout", err: errors.New("exit status 1")},
Expand Down
10 changes: 6 additions & 4 deletions internal/app/card_manifest_deck.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func cardManifestItemPage(item cardManifestItem, pageNumber int, pageCount int)
if imageSrc != "" {
variant = "image-caption"
}
body := cardManifestItemBody(item, cardManifestBodyMaxRunes(variant))
body := cardManifestItemBody(item, cardManifestBodyMaxRunes(variant), variant == "image-caption")
if body == "" {
body = title
}
Expand Down Expand Up @@ -208,12 +208,14 @@ func cardManifestBodyMaxRunes(variant string) int {
return cardManifestTextCaptionBodyMaxRunes
}

func cardManifestItemBody(item cardManifestItem, maxRunes int) string {
func cardManifestItemBody(item cardManifestItem, maxRunes int, includeSource bool) string {
if body := cardManifestSectionsBody(item.Sections, maxRunes); body != "" {
return body
}
if source := cardManifestItemSourceText(item); source != "" {
return cardManifestFitSections(cardManifestNewsBodySections(item, source), maxRunes)
if includeSource {
if source := cardManifestItemSourceText(item); source != "" {
return cardManifestFitSections(cardManifestNewsBodySections(item, source), maxRunes)
}
}
summary, impact := cardManifestFitSummaryImpact(item.Summary, item.Impact, maxRunes)
parts := make([]string, 0, 2)
Expand Down
10 changes: 8 additions & 2 deletions internal/app/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,9 @@ func TestServiceGeneratePreviewBuildsCardManifestDeckWithoutAI(t *testing.T) {
if textPage.Variant != "text-caption" || textPage.Content.Title != "AI 眼镜更新" {
t.Fatalf("text page = %#v", textPage)
}
if textPage.Content.Body != "**摘要:** 新品强调续航。\n\n**影响:** 穿戴设备竞争加剧。" {
t.Fatalf("text page body = %q", textPage.Content.Body)
}
if textPage.Meta.CTA != "来源:Bloomberg / 2026-06-16 12:30" {
t.Fatalf("text page cta = %q", textPage.Meta.CTA)
}
Expand Down Expand Up @@ -601,8 +604,11 @@ func TestServiceGeneratePreviewFitsCardManifestTextForNewsCards(t *testing.T) {
if utf8.RuneCountInString(textBody) > cardManifestTextCaptionBodyMaxRunes {
t.Fatalf("text-caption body runes = %d, want <= %d", utf8.RuneCountInString(textBody), cardManifestTextCaptionBodyMaxRunes)
}
if !strings.Contains(textBody, "摘要:") || !strings.Contains(textBody, "影响:") || !strings.Contains(textBody, "来源:") || !strings.Contains(textBody, "Hacker News") || !strings.Contains(textBody, "…") {
t.Fatalf("text-caption body should preserve labels, source, and ellipsis: %q", textBody)
if !strings.Contains(textBody, "摘要:") || !strings.Contains(textBody, "影响:") || strings.Contains(textBody, "来源:") || strings.Contains(textBody, "Hacker News") || !strings.Contains(textBody, "…") {
t.Fatalf("text-caption body should preserve summary/impact labels and ellipsis without source: %q", textBody)
}
if r.rendered.Pages[2].Meta.CTA != "来源:Hacker News" {
t.Fatalf("text-caption cta = %q", r.rendered.Pages[2].Meta.CTA)
}
}

Expand Down