feat(desktop): fill sponsor slots from direct house campaigns

When mediation had no programmatic fill, the banner and rewarded surfaces
collapsed to empty space. Direct house sponsors now serve their own copy and
click-through, with the same settlement accounting used by the mediated
network, and the mediation engine tests cover the added path.
This commit is contained in:
Yun Chan 2026-09-16 23:23:39 +09:00
parent 911c9f0229
commit c8d802d78f
5 changed files with 363 additions and 61 deletions

View file

@ -1,4 +1,4 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AdMediationConfig } from '@d3ro/core/types'
import { getAdMediationEngine } from '../../src/main/services/ads/AdMediationEngine'
import { getAdSettlementService } from '../../src/main/services/ads/AdSettlementService'
@ -23,7 +23,6 @@ const adapterCases = [
['InMobi', new InMobiAdapter()],
['PubMatic', new PubMaticAdapter()],
['Mintegral', new MintegralAdapter()],
['Direct sponsor', new DirectHouseSponsorAdapter()],
] as const
describe('desktop ad provider boundaries', () => {
@ -49,6 +48,96 @@ describe('desktop ad provider boundaries', () => {
})
})
describe('DirectHouseSponsorAdapter live decision endpoint', () => {
it('is fail-closed until an endpoint is configured', async () => {
const adapter = new DirectHouseSponsorAdapter()
await adapter.init()
const bid = await adapter.requestBid({ placement: 'bottom_dock_banner', format: 'banner_dock' })
expect(bid).toEqual({
hasBid: false,
bidEcpm: 0,
latencyMs: 0,
error: 'adapter_not_configured',
})
await expect(adapter.reportRewardCompletion('forged-ad-id')).resolves.toEqual({
success: false,
tokenReward: 0,
})
})
it('returns a validated live bid from a configured endpoint', async () => {
const adapter = new DirectHouseSponsorAdapter()
await adapter.init({ endpointUrl: 'https://ads.example.com', apiSecret: 'secret' })
const fetchMock = vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
bidEcpm: 9.5,
creative: {
id: 'creative-1',
title: 'Sponsor',
advertiserName: 'Example Inc',
clickUrl: 'https://example.com/offer',
format: 'banner_dock',
},
}),
{ status: 200 },
),
)
vi.stubGlobal('fetch', fetchMock)
try {
const bid = await adapter.requestBid({
placement: 'bottom_dock_banner',
format: 'banner_dock',
floorEcpm: 2,
})
expect(bid.hasBid).toBe(true)
expect(bid.bidEcpm).toBe(9.5)
expect(bid.creative?.networkId).toBe('direct_sponsor')
expect(bid.creative?.clickUrl).toBe('https://example.com/offer')
const [url, init] = fetchMock.mock.calls[0] as [string, RequestInit]
expect(String(url)).toBe('https://ads.example.com/bid')
expect(init.headers).toMatchObject({ Authorization: 'Bearer secret' })
} finally {
vi.unstubAllGlobals()
}
})
it('rejects a creative whose click target is not https', async () => {
const adapter = new DirectHouseSponsorAdapter()
await adapter.init({ endpointUrl: 'https://ads.example.com' })
vi.stubGlobal(
'fetch',
vi.fn().mockResolvedValue(
new Response(
JSON.stringify({
bidEcpm: 9.5,
creative: {
id: 'c',
title: 't',
advertiserName: 'a',
clickUrl: 'http://insecure.example.com',
format: 'banner_dock',
},
}),
{ status: 200 },
),
),
)
try {
const bid = await adapter.requestBid({
placement: 'bottom_dock_banner',
format: 'banner_dock',
})
expect(bid.hasBid).toBe(false)
expect(bid.error).toBe('invalid_creative')
} finally {
vi.unstubAllGlobals()
}
})
})
describe('AdMediationEngine fail-closed auction and telemetry', () => {
const engine = getAdMediationEngine()
const settlement = getAdSettlementService()