feat(core): add Discord presence service and extract Jellyfin runtime composition

Introduce Discord presence runtime support and continue composition-root decomposition by moving Jellyfin wiring into dedicated composer modules. This keeps main runtime orchestration thinner while preserving behavior and test coverage across config, runtime, and docs updates.
This commit is contained in:
2026-02-22 14:53:10 -08:00
parent 43a8a37f5b
commit edfe6640ac
52 changed files with 2222 additions and 317 deletions

View File

@@ -17,7 +17,7 @@ test('jellyfin directPlayContainers are normalized', () => {
test('jellyfin legacy auth keys are ignored by resolver', () => {
const { context } = createResolveContext({
jellyfin: ({ accessToken: 'legacy-token', userId: 'legacy-user' } as unknown) as never,
jellyfin: { accessToken: 'legacy-token', userId: 'legacy-user' } as unknown as never,
});
applyIntegrationConfig(context);
@@ -25,3 +25,61 @@ test('jellyfin legacy auth keys are ignored by resolver', () => {
assert.equal('accessToken' in (context.resolved.jellyfin as Record<string, unknown>), false);
assert.equal('userId' in (context.resolved.jellyfin as Record<string, unknown>), false);
});
test('discordPresence fields are parsed and clamped', () => {
const { context } = createResolveContext({
discordPresence: {
enabled: true,
clientId: '123456789',
detailsTemplate: 'Watching {title}',
stateTemplate: 'Paused',
largeImageKey: 'subminer-logo',
largeImageText: 'SubMiner Runtime',
smallImageKey: 'pause',
smallImageText: 'Paused',
buttonLabel: 'Open Repo',
buttonUrl: 'https://github.com/sudacode/SubMiner',
updateIntervalMs: 500,
debounceMs: -100,
},
});
applyIntegrationConfig(context);
assert.equal(context.resolved.discordPresence.enabled, true);
assert.equal(context.resolved.discordPresence.clientId, '123456789');
assert.equal(context.resolved.discordPresence.detailsTemplate, 'Watching {title}');
assert.equal(context.resolved.discordPresence.stateTemplate, 'Paused');
assert.equal(context.resolved.discordPresence.largeImageKey, 'subminer-logo');
assert.equal(context.resolved.discordPresence.largeImageText, 'SubMiner Runtime');
assert.equal(context.resolved.discordPresence.smallImageKey, 'pause');
assert.equal(context.resolved.discordPresence.smallImageText, 'Paused');
assert.equal(context.resolved.discordPresence.buttonLabel, 'Open Repo');
assert.equal(context.resolved.discordPresence.buttonUrl, 'https://github.com/sudacode/SubMiner');
assert.equal(context.resolved.discordPresence.updateIntervalMs, 1000);
assert.equal(context.resolved.discordPresence.debounceMs, 0);
});
test('discordPresence invalid values warn and keep defaults', () => {
const { context, warnings } = createResolveContext({
discordPresence: {
enabled: 'true' as never,
clientId: 123 as never,
updateIntervalMs: 'fast' as never,
debounceMs: null as never,
},
});
applyIntegrationConfig(context);
assert.equal(context.resolved.discordPresence.enabled, false);
assert.equal(context.resolved.discordPresence.clientId, '');
assert.equal(context.resolved.discordPresence.updateIntervalMs, 15_000);
assert.equal(context.resolved.discordPresence.debounceMs, 750);
const warnedPaths = warnings.map((warning) => warning.path);
assert.ok(warnedPaths.includes('discordPresence.enabled'));
assert.ok(warnedPaths.includes('discordPresence.clientId'));
assert.ok(warnedPaths.includes('discordPresence.updateIntervalMs'));
assert.ok(warnedPaths.includes('discordPresence.debounceMs'));
});