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
81 changes: 81 additions & 0 deletions src/main/announcements-poller.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'

const logSpy = vi.fn()
vi.mock('./debug', () => ({
log: (...args: unknown[]) => logSpy(...args),
formatErr: (err: unknown) => (err instanceof Error ? err.message : String(err))
}))

import { fetchAnnouncementsFeed } from './announcements-poller'

const originalFetch = globalThis.fetch
Expand All @@ -16,6 +23,7 @@ function mockFetch(body: unknown, init: { ok?: boolean; status?: number } = {}):
describe('fetchAnnouncementsFeed', () => {
beforeEach(() => {
globalThis.fetch = originalFetch
logSpy.mockClear()
})
afterEach(() => {
globalThis.fetch = originalFetch
Expand Down Expand Up @@ -101,4 +109,77 @@ describe('fetchAnnouncementsFeed', () => {
mockFetch({}, { ok: false, status: 503 })
await expect(fetchAnnouncementsFeed('http://mock')).rejects.toThrow(/HTTP 503/)
})

it('includes summary when it is a non-empty string within the length cap', async () => {
mockFetch({
announcements: [
{
id: 'ok',
title: 'Hello',
href: 'https://x.example/y',
publishedAt: '2026-05-20T00:00:00Z',
summary: 'A short blurb about the post.'
}
]
})
const { items } = await fetchAnnouncementsFeed('http://mock')
expect(items).toHaveLength(1)
expect(items[0].summary).toBe('A short blurb about the post.')
})

it('ignores non-string summary and keeps the entry', async () => {
mockFetch({
announcements: [
{
id: 'ok',
title: 'Hello',
href: 'https://x.example/y',
publishedAt: '2026-05-20T00:00:00Z',
summary: 12345
}
]
})
const { items } = await fetchAnnouncementsFeed('http://mock')
expect(items).toHaveLength(1)
expect(items[0].summary).toBeUndefined()
})

it('omits an empty-string summary', async () => {
mockFetch({
announcements: [
{
id: 'ok',
title: 'Hello',
href: 'https://x.example/y',
publishedAt: '2026-05-20T00:00:00Z',
summary: ' '
}
]
})
const { items } = await fetchAnnouncementsFeed('http://mock')
expect(items).toHaveLength(1)
expect(items[0].summary).toBeUndefined()
})

it('drops a summary over 240 chars but keeps the entry, logging the case', async () => {
const longSummary = 'x'.repeat(300)
mockFetch({
announcements: [
{
id: 'ok',
title: 'Hello',
href: 'https://x.example/y',
publishedAt: '2026-05-20T00:00:00Z',
summary: longSummary
}
]
})
const { items } = await fetchAnnouncementsFeed('http://mock')
expect(items).toHaveLength(1)
expect(items[0].summary).toBeUndefined()
const summaryLogs = logSpy.mock.calls.filter(
(call) => call[0] === 'announcements' && String(call[1]).includes('summary')
)
expect(summaryLogs.length).toBeGreaterThan(0)
})
})
12 changes: 12 additions & 0 deletions src/main/announcements-poller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import type { Announcement } from '../shared/state/announcements'
const FEED_URL = 'https://harness.mikelyons.org/announcements.json'
const POLL_INTERVAL_MS = 6 * 60 * 60 * 1000
const FETCH_TIMEOUT_MS = 10_000
const MAX_SUMMARY_LEN = 240

interface RawAnnouncement {
id?: unknown
title?: unknown
href?: unknown
publishedAt?: unknown
summary?: unknown
expiresAt?: unknown
}

Expand Down Expand Up @@ -41,6 +43,16 @@ function validateEntry(raw: unknown): Announcement | null {
href: r.href,
publishedAt: r.publishedAt
}
if (typeof r.summary === 'string' && r.summary.trim()) {
if (r.summary.length > MAX_SUMMARY_LEN) {
log(
'announcements',
`summary on ${r.id} is ${r.summary.length} chars (max ${MAX_SUMMARY_LEN}) — dropping summary, keeping entry`
)
} else {
cleaned.summary = r.summary
}
}
if (typeof r.expiresAt === 'string' && !Number.isNaN(Date.parse(r.expiresAt))) {
cleaned.expiresAt = r.expiresAt
}
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1153,10 +1153,13 @@ const setQuestStep = useCallback((next: QuestStep) => {
<span className="text-accent text-sm flex-1">
<a
onClick={() => backend.openExternal(activeAnnouncement.href)}
className="underline hover:text-accent cursor-pointer no-drag"
className="font-semibold underline hover:text-accent cursor-pointer no-drag"
>
{activeAnnouncement.title}
</a>
{activeAnnouncement.summary && (
<span className="text-accent/80 ml-2">— {activeAnnouncement.summary}</span>
)}
</span>
<div className="relative no-drag self-stretch flex items-center">
<button
Expand Down
37 changes: 37 additions & 0 deletions src/shared/state/announcements.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,41 @@ describe('announcementsReducer', () => {
})
expect(next).not.toBe(initialAnnouncements)
})

it('preserves the summary field on loaded items', () => {
const next = announcementsReducer(initialAnnouncements, {
type: 'announcements/loaded',
payload: {
items: [
{
id: 'a',
title: 'A',
href: 'https://example.com/a',
publishedAt: '2026-05-20T00:00:00Z',
summary: 'A short blurb.'
}
],
fetchedAt: 1
}
})
expect(next.items[0].summary).toBe('A short blurb.')
})

it('items without a summary flow through unchanged', () => {
const next = announcementsReducer(initialAnnouncements, {
type: 'announcements/loaded',
payload: {
items: [
{
id: 'a',
title: 'A',
href: 'https://example.com/a',
publishedAt: '2026-05-20T00:00:00Z'
}
],
fetchedAt: 1
}
})
expect(next.items[0].summary).toBeUndefined()
})
})
1 change: 1 addition & 0 deletions src/shared/state/announcements.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export interface Announcement {
title: string
href: string
publishedAt: string
summary?: string
expiresAt?: string
}

Expand Down
Loading