---
title: Cover the free agency period Discord announcements
type: task
status: draft
description: >-
  The 7-day advance and period-start Discord messages in announce-free-agency-period-start.mjs are
  built by module-private formatters, so no spec can reach them. Last two auction-adjacent messages
  with zero coverage.
base_uri: user:task/league/cover-free-agency-period-discord-announcements.md
created_at: '2026-09-02T20:13:39.840Z'
entity_id: 1efbeff6-b239-4954-8e59-3b521fdb9f23
owner_identity_uri: █████████████████████████
priority: Low
public_read: true
tags:
  - user:tag/league-xo-football.md
updated_at: '2026-10-03T03:42:51.445Z'
visibility_analyzed_at: '2026-11-05T23:48:40.619Z'
---

The free agency period announcements are the last two auction-adjacent Discord messages with no coverage, and they are unreachable by a spec as written.

`scripts/announce-free-agency-period-start.mjs` builds both through module-private consts:

- `format_free_agency_period_advance_message` (line ~159) — the 7-day advance notice
- `format_free_agency_period_start_message` (line ~177) — the period start

Neither is exported, so no spec can import either one. Across the whole suite, zero specs reference `sendNotifications`, and only `test/auction.blocks.spec.mjs` touches any auction message formatter.

## Why this is worth doing

This subsystem has now lost a Discord message the same way twice — the block convening announcement, and the settlement message. Both were one-line calls nothing could reach. The fix pattern both times was a seam plus a spec that mutates the CALL SITE rather than the message body.

These two are the same shape, one step further out. The sanctuary period start is the message that tells managers poaching claims are closed; a silent one is a rules change nobody was told about.

## Approach

Export both formatters, then spec their content directly — the cheap half, and it needs no seam because a script is not frozen runtime the way the auction socket is.

Whether the SEND is worth a seam is a judgment call for whoever picks this up. The announce path here is a script `main()` rather than a hot request path, so the argument for injection is weaker than it was for the settlement fan-out. Covering content only is a defensible stopping point if the seam is not warranted — but say so explicitly in the spec header rather than letting a future reader mistake content coverage for delivery coverage.

## Discipline

`docs/guides/test.md` and `docs/guides/auction.md` both carry the relevant lessons as of 2026-09-02:

- Mutating a message builder does not prove the builder is CALLED. Delete the call site.
- A poll bound built on `Date.now()` never expires under a frozen `MockDate`, turning a failing assertion into a full-budget mocha timeout that reads as infrastructure.

## Prior art

`test/auction.settlement-announcement.spec.mjs` is the worked example — a four-link chain (route reaches fan-out, fan-out calls announcer, default announcer is real, builder produces content), each link scored by its own mutant.
