Skip to content

Commit 88e05db

Browse files
Sync public snapshot from freebuff-private
Source: CodebuffAI/freebuff-private@6897ef215f7b0b07c2a6613ef1faef498dd66daa
1 parent d4ee9ca commit 88e05db

8 files changed

Lines changed: 1272 additions & 9 deletions

File tree

bun.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

common/src/__tests__/freebuff-placements.test.ts

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,16 @@ import {
1616
PLACEMENTS_CONSOLE_ENABLED,
1717
PLACEMENT_METRIC_LABELS,
1818
PLACEMENT_PREVIEW_WIDTHS,
19+
PLACEMENT_FORMATS,
1920
PLACEMENT_SLOTS,
21+
SPONSOR_BREAK_CREATIVE_LIMITS,
22+
SPONSOR_BREAK_FORMATS,
23+
SPONSOR_BREAK_HERO_SPEC,
2024
TRACKED_LINK_PLACEMENT_ID,
25+
isSponsorBreakPlacement,
26+
placementFormat,
2127
placementSlotLabel,
28+
placementSurface,
2229
PLACEMENT_STATUS_LABELS,
2330
PRIMARY_METRICS,
2431
UNDERSPEND_COPY,
@@ -221,12 +228,78 @@ describe('copy and configuration', () => {
221228
// eight `CLI-Chat-Inline-N` ids:
222229
// no shipping client requests those, so selling them would be selling a
223230
// decaying legacy path.
224-
expect(bySurface('cli_chat')).toBe(4)
231+
// Plus the three sponsor breaks, which are deliberately the same surface:
232+
// a break is a different RENDERER on Desktop chat, not a new surface, and
233+
// adding one costs a house creative and a pinned rollup row.
234+
expect(bySurface('cli_chat')).toBe(7)
225235
expect(bySurface('waiting_room')).toBe(4)
226236
expect(bySurface('freebuff_web_chat')).toBe(2)
227237
expect(bySurface('chat_assistant')).toBe(1)
228238
})
229239

240+
it('gives every slot a format, so no slot renders by guesswork', () => {
241+
// The whole point of the column: a renderer asks the catalog how to draw
242+
// a slot rather than pattern-matching its id. A slot with no format would
243+
// fall through `placementFormat`'s conservative default and silently
244+
// render inline, which for a break is a blank-looking card.
245+
for (const slot of PLACEMENT_SLOTS) {
246+
expect([slot.id, PLACEMENT_FORMATS.includes(slot.format)]).toEqual([
247+
slot.id,
248+
true,
249+
])
250+
}
251+
})
252+
253+
it('names the three sponsor breaks and leaves every other slot inline', () => {
254+
const breaks = PLACEMENT_SLOTS.filter((slot) =>
255+
isSponsorBreakPlacement(slot.id),
256+
).map((slot) => slot.id)
257+
expect(breaks).toEqual([
258+
'Desktop-Spotlight',
259+
'Desktop-Showcase',
260+
'Desktop-Intermission',
261+
])
262+
expect(placementFormat('Desktop-Spotlight')).toBe('spotlight')
263+
expect(placementFormat('Desktop-Showcase')).toBe('showcase')
264+
expect(placementFormat('Desktop-Intermission')).toBe('intermission')
265+
// Resolvable as slots, exactly like every other sellable id.
266+
for (const id of breaks) expect(placementSurface(id)).toBe('cli_chat')
267+
expect(SPONSOR_BREAK_FORMATS).toEqual([
268+
'showcase',
269+
'spotlight',
270+
'intermission',
271+
])
272+
})
273+
274+
it('answers inline for an id it does not know, never undefined', () => {
275+
// The conservative direction. An unknown id guessed as a break would turn
276+
// one typo into a full-screen interruption; guessed as inline it renders
277+
// as the ordinary card it almost certainly is.
278+
expect(placementFormat('CLI-Chat-Inline-3')).toBe('inline')
279+
expect(placementFormat('some-future-grain')).toBe('inline')
280+
expect(isSponsorBreakPlacement('some-future-grain')).toBe(false)
281+
})
282+
283+
it('bounds break copy far below the inline lengths', () => {
284+
// A break title renders at display size; inline-length copy does not
285+
// shrink to fit, it overflows or truncates mid-word.
286+
expect(SPONSOR_BREAK_CREATIVE_LIMITS).toEqual({
287+
titleMaxLength: 28,
288+
bodyMaxLength: 60,
289+
ctaMaxLength: 18,
290+
})
291+
expect(SPONSOR_BREAK_HERO_SPEC.minWidth).toBe(1024)
292+
expect(SPONSOR_BREAK_HERO_SPEC.minHeight).toBe(640)
293+
// The floor is itself on-ratio, so the minimum acceptable image passes
294+
// both gates rather than being rejected by the one it defines.
295+
expect(
296+
Math.abs(
297+
SPONSOR_BREAK_HERO_SPEC.minWidth / SPONSOR_BREAK_HERO_SPEC.minHeight -
298+
SPONSOR_BREAK_HERO_SPEC.aspectRatio,
299+
),
300+
).toBeLessThan(1e-9)
301+
})
302+
230303
it('gives every not-serving and underspend reason copy', () => {
231304
// Each of these is a state we can distinguish. Any reason without copy
232305
// would render an empty banner, which reads as "broken".
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { describe, expect, test } from 'bun:test'
2+
3+
import {
4+
ADS_BREAK_CLICKED_EVENT,
5+
ADS_BREAK_CLOSED_EVENT,
6+
ADS_BREAK_SHOWN_EVENT,
7+
buildSponsorBreakEvent,
8+
clampDwellMs,
9+
isSponsorBreakCloseMethod,
10+
readDwellMs,
11+
SPONSOR_BREAK_ACCIDENTAL_CLICK_MS,
12+
SPONSOR_BREAK_CLOSE_METHODS,
13+
SPONSOR_BREAK_DWELL_MAX_MS,
14+
} from '../sponsor-break-events'
15+
16+
import {
17+
ADS_CLIENT_EVENT_HYGIENE_FIELDS,
18+
ADS_SPONSOR_BREAK_FIELD_NAMES,
19+
getAxiomOnlyLogEvent,
20+
} from '../../util/axiom-only-log'
21+
22+
/**
23+
* COD-453 item 6. Two things are guarded here: the payload builder is TOTAL
24+
* (nothing a renderer sends can throw or reject), and the Axiom allowlist
25+
* discards everything not on the contract -- these events are emitted from
26+
* clients, so the allowlist is the only fence between a renderer and the
27+
* dataset.
28+
*/
29+
describe('the break close vocabulary', () => {
30+
test('is closed, and rejects anything outside it', () => {
31+
for (const method of SPONSOR_BREAK_CLOSE_METHODS) {
32+
expect(isSponsorBreakCloseMethod(method)).toBe(true)
33+
}
34+
for (const value of ['dismiss', 'X', '', null, undefined, 3, {}]) {
35+
expect(isSponsorBreakCloseMethod(value)).toBe(false)
36+
}
37+
})
38+
39+
/**
40+
* `escape` is deliberately not pooled with `x`: one is a considered exit and
41+
* one is a reflex, and telling them apart is how a format learns whether it
42+
* is being read or swatted.
43+
*/
44+
test('keeps the reflex exits separate from the considered ones', () => {
45+
expect(SPONSOR_BREAK_CLOSE_METHODS).toContain('x')
46+
expect(SPONSOR_BREAK_CLOSE_METHODS).toContain('escape')
47+
expect(SPONSOR_BREAK_CLOSE_METHODS).toContain('continue')
48+
expect(SPONSOR_BREAK_CLOSE_METHODS).toContain('timer_then_continue')
49+
expect(SPONSOR_BREAK_CLOSE_METHODS).toContain('thread_switch')
50+
})
51+
})
52+
53+
describe('clampDwellMs', () => {
54+
test('clamps rather than rejecting, exactly like the render delay', () => {
55+
expect(clampDwellMs(-5)).toBe(0)
56+
expect(clampDwellMs(0)).toBe(0)
57+
expect(clampDwellMs(1234.6)).toBe(1235)
58+
expect(clampDwellMs('2000')).toBe(2000)
59+
expect(clampDwellMs(9e9)).toBe(SPONSOR_BREAK_DWELL_MAX_MS)
60+
})
61+
62+
/**
63+
* Absent and unparseable are DELIBERATELY the same value, so nothing
64+
* downstream can reconstruct a dwell from timestamps to fill the gap.
65+
*/
66+
test('unknown is null, and is indistinguishable from absent', () => {
67+
for (const value of [
68+
undefined,
69+
null,
70+
'',
71+
' ',
72+
'abc',
73+
{},
74+
NaN,
75+
Infinity,
76+
]) {
77+
expect(clampDwellMs(value)).toBeNull()
78+
}
79+
expect(readDwellMs(undefined, null, 'abc')).toBeNull()
80+
})
81+
82+
test('reads the first candidate that clamps, so a header wins over a body', () => {
83+
expect(readDwellMs('900', 100)).toBe(900)
84+
expect(readDwellMs(null, 100)).toBe(100)
85+
})
86+
87+
/**
88+
* A one-hour ceiling rather than the render delay's day: a dwell of hours is
89+
* a laptop that slept with the break open, which is real and common and
90+
* whose value is meaningless. Clamping keeps the row in the denominator
91+
* while stopping one suspended machine from owning the p90.
92+
*/
93+
test('the dwell ceiling is far tighter than the render-delay ceiling', () => {
94+
expect(SPONSOR_BREAK_DWELL_MAX_MS).toBe(3_600_000)
95+
expect(SPONSOR_BREAK_ACCIDENTAL_CLICK_MS).toBe(300)
96+
})
97+
})
98+
99+
describe('buildSponsorBreakEvent', () => {
100+
const base = {
101+
placementId: 'Desktop-Spotlight',
102+
surface: 'cli_chat',
103+
format: 'spotlight',
104+
arm: 'reduced_spotlight',
105+
}
106+
107+
test('drops every unknown optional rather than emitting a sentinel', () => {
108+
expect(buildSponsorBreakEvent(base)).toEqual({
109+
placement_id: 'Desktop-Spotlight',
110+
surface: 'cli_chat',
111+
format: 'spotlight',
112+
sponsor_break_arm: 'reduced_spotlight',
113+
sample_rate: 1,
114+
})
115+
})
116+
117+
test('carries a full close event', () => {
118+
expect(
119+
buildSponsorBreakEvent({
120+
...base,
121+
method: 'timer_then_continue',
122+
dwellMs: 4200,
123+
timerMs: 3000,
124+
timerCompleted: true,
125+
clientEventId: 'evt-123',
126+
clientFamily: 'desktop',
127+
campaignLabel: 'pilot-b',
128+
creativeVersion: 3,
129+
opportunityId: 'opp_1',
130+
}),
131+
).toEqual({
132+
placement_id: 'Desktop-Spotlight',
133+
surface: 'cli_chat',
134+
format: 'spotlight',
135+
sponsor_break_arm: 'reduced_spotlight',
136+
campaign_label: 'pilot-b',
137+
creative_version: 3,
138+
opportunity_id: 'opp_1',
139+
method: 'timer_then_continue',
140+
dwell_ms: 4200,
141+
timer_ms: 3000,
142+
timer_completed: true,
143+
client_event_id: 'evt-123',
144+
client_family: 'desktop',
145+
sample_rate: 1,
146+
})
147+
})
148+
149+
test('a malformed method or event id is dropped, never emitted raw', () => {
150+
const built = buildSponsorBreakEvent({
151+
...base,
152+
method: 'made-up',
153+
clientEventId: 'has spaces and is not a token',
154+
dwellMs: 'nope',
155+
})
156+
expect(built.method).toBeUndefined()
157+
expect(built.client_event_id).toBeUndefined()
158+
expect(built.dwell_ms).toBeUndefined()
159+
})
160+
})
161+
162+
describe('the Axiom allowlist', () => {
163+
const events = [
164+
ADS_BREAK_SHOWN_EVENT,
165+
ADS_BREAK_CLOSED_EVENT,
166+
ADS_BREAK_CLICKED_EVENT,
167+
] as const
168+
169+
test('keeps the contract and discards everything else', () => {
170+
for (const axiomEvent of events) {
171+
expect(
172+
getAxiomOnlyLogEvent({
173+
axiomEvent,
174+
placement_id: 'Desktop-Intermission',
175+
surface: 'cli_chat',
176+
format: 'intermission',
177+
sponsor_break_arm: 'reduced_intermission',
178+
dwell_ms: 5000,
179+
method: 'continue',
180+
client_family: 'desktop',
181+
sample_rate: 1,
182+
// None of these may ship: an id, a url, and free copy.
183+
campaign_id: 'camp_secret',
184+
userId: 'user-123',
185+
landing_url: 'https://advertiser.example',
186+
title: 'Buy our thing',
187+
}),
188+
).toEqual({
189+
event: axiomEvent,
190+
data: {
191+
placement_id: 'Desktop-Intermission',
192+
surface: 'cli_chat',
193+
format: 'intermission',
194+
sponsor_break_arm: 'reduced_intermission',
195+
dwell_ms: 5000,
196+
method: 'continue',
197+
client_family: 'desktop',
198+
sample_rate: 1,
199+
},
200+
})
201+
}
202+
})
203+
204+
test('CI guard: the break allowlist carries the client hygiene fields', () => {
205+
for (const field of ADS_CLIENT_EVENT_HYGIENE_FIELDS) {
206+
expect(ADS_SPONSOR_BREAK_FIELD_NAMES).toContain(field)
207+
}
208+
})
209+
})

0 commit comments

Comments
 (0)