Skip to content
Open
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
15 changes: 8 additions & 7 deletions spotify-releases/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
*
* Cron-only (no chat surface). Transport is configuration-driven (workforce#252):
* the persona gates `slack` on SLACK_USER and `telegram` on TELEGRAM_CHAT, so the
* unconfigured transport is pruned at deploy. The checkpoint (last-check date +
* notified-release set) advances ONLY after every configured transport delivered
* and no artist fetch failed — so a flaky send re-notifies next tick rather than
* silently dropping releases.
* unconfigured transport is pruned at deploy. Delivered releases are recorded
* after every configured transport succeeds, while the last-check date advances
* only after a complete artist scan. That avoids duplicate notifications without
* skipping artists whose fetch failed.
*/
import { defineAgent, isCronTickEvent, type WorkforceCtx } from '@agentworkforce/runtime';
import { slackClient } from '@relayfile/relay-helpers';
Expand Down Expand Up @@ -100,11 +100,12 @@ export async function checkReleases(
ctx.log?.('info', 'spotify-releases.nothing-new', { since });
}

if (delivered && releases.length > 0) {
await saveNotified(ctx, [...notified, ...releases.map(releaseKey)].slice(-500));
}

if (delivered && failed.length === 0) {
await saveLastCheck(ctx, today());
if (releases.length > 0) {
await saveNotified(ctx, [...notified, ...releases.map(releaseKey)].slice(-500));
}
} else {
ctx.log?.('warn', 'spotify-releases.checkpoint-not-advanced', {
since,
Expand Down
41 changes: 41 additions & 0 deletions tests/telegram-agents.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -313,6 +313,47 @@ test('spotify-releases (telegram): does not checkpoint after a Telegram no-recei
assert.equal((await ctx.memory.recall('x', { tags: ['spotify-releases:notified'] })).length, 0);
});

test('spotify-releases (telegram): persists delivered releases after a partial artist failure', async () => {
const originalFetch = globalThis.fetch;
const ctx = makeCtx();
const tg = makeTelegram();
await ctx.memory.save('2026-06-10', { tags: ['spotify-releases:last-check'], scope: 'workspace' });
globalThis.fetch = async (url) => {
const s = String(url);
if (s.includes('/me/following')) {
return Response.json({
artists: {
items: [
{ id: 'a1', name: 'Artist One' },
{ id: 'a2', name: 'Artist Two' }
]
}
});
}
if (s.includes('/artists/a2/')) return new Response('unavailable', { status: 503 });
return Response.json({
items: [{ name: 'Delivered Single', release_date: '2026-06-11', external_urls: { spotify: 'https://open.spotify.com/album/3' } }]
});
};
try {
const releaseCtx = {
...ctx,
persona: { inputs: { TELEGRAM_CHAT: '5', SPOTIFY_TOKEN: 'tok' }, inputSpecs: {} }
};
await checkReleases(releaseCtx, { telegram: tg });
await checkReleases(releaseCtx, { telegram: tg });
} finally {
globalThis.fetch = originalFetch;
}

assert.equal(tg.sends.length, 1);
assert.match(tg.sends[0].text, /Delivered Single/);
const notified = await ctx.memory.recall('x', { tags: ['spotify-releases:notified'] });
assert.deepEqual(JSON.parse(notified[0].content), ['https://open.spotify.com/album/3']);
const lastCheck = await ctx.memory.recall('x', { tags: ['spotify-releases:last-check'], limit: 1 });
assert.equal(lastCheck[0].content, '2026-06-10');
});

// ── Slack side of the unified agents (dual-transport dispatch + fan-out) ───────

/** A fake Slack client capturing post/reply/dm calls. */
Expand Down