FAILURE MAP
← Case archive

FA-34466 / Notification interfaces / Open access

Banner to inbox promotion: banner-exposure-end from saving · case 01

The banner-exposure-end event leaves a notification in gone instead of saving-without-banner.

Verified by executionVariant 1 · 36 checks per implementationDownload source bundle ↓JSON ↗

ROOT CAUSE

Exposure expiry cancels pending persistence.

VERIFIED REPAIR

Commit the saving / banner-exposure-end transition to saving-without-banner; preserve the other explicitly stipulated transitions.

Unsuccessful approach: Claiming inbox-only before persistence overstates durability.

Case contract

Banner to inbox promotion is a bounded visual-notification workflow with mutable policy state {'banner': True, 'inbox': [], 'item': 'n', 'save_revision': 2}. Its default transition relation is {('banner', 'retain-in-inbox'): 'saving', ('saving', 'save-success'): 'both', ('saving', 'save-failure'): 'save-error', ('both', 'banner-exposure-end'): 'inbox-only', ('save-error', 'retry-retention'): 'saving', ('saving', 'banner-exposure-end'): 'saving-without-banner', ('saving-without-banner', 'save-success'): 'inbox-only', ('saving-without-banner', 'save-failure'): 'recovery-banner', ('inbox-only', 'show-banner-again'): 'repromoting', ('repromoting', 'banner-mounted'): 'both'}; domain inputs can suppress or redirect transitions and update policy fields, as specified in solve. Different-generation events are inert. Batches apply in order. Optional observe returns the selected policy field alongside the phase.

Why this case matters

Visual notification lifecycle ordering can leave an inbox or toast showing the wrong actionable state even when transport succeeds.

1 / The failure

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(initial, events, generation, payload=None, observe=None):
    table = {('banner', 'retain-in-inbox'): 'saving', ('saving', 'save-success'): 'both', ('saving', 'save-failure'): 'save-error', ('both', 'banner-exposure-end'): 'inbox-only', ('save-error', 'retry-retention'): 'saving', ('saving', 'banner-exposure-end'): 'gone', ('saving-without-banner', 'save-success'): 'inbox-only', ('saving-without-banner', 'save-failure'): 'recovery-banner', ('inbox-only', 'show-banner-again'): 'repromoting', ('repromoting', 'banner-mounted'): 'both'}
    data = json.loads(json.dumps({'banner': True, 'inbox': [], 'item': 'n', 'save_revision': 2})) if payload is None else {**json.loads(json.dumps({'banner': True, 'inbox': [], 'item': 'n', 'save_revision': 2})), **json.loads(json.dumps(payload))}
    state = initial
    for delivery in events:
        event, event_generation = delivery[:2]
        arg = delivery[2] if len(delivery) > 2 else {}
        if event_generation == generation:
            previous = state
            if event in ('save-success','save-failure') and arg.get('revision',data['save_revision']) != data['save_revision']: event = 'unknown-event'
            state = table.get((state, event), state)
            if state in ('both','inbox-only') and data['item'] not in data['inbox']: data['inbox'].append(data['item'])
            data['banner'] = state in ('banner','both','saving','save-error','recovery-banner')
            if state == 'gone': data['inbox'] = [x for x in data['inbox'] if x != data['item']]
    return {'phase': state, 'value': data[observe]} if observe else state
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
check('banner / retain-in-inbox', solve('banner', [('retain-in-inbox', N)], N), 'saving')
check('saving / save-success', solve('saving', [('save-success', N)], N), 'both')
check('saving / save-failure', solve('saving', [('save-failure', N)], N), 'save-error')
check('both / banner-exposure-end', solve('both', [('banner-exposure-end', N)], N), 'inbox-only')
check('save-error / retry-retention', solve('save-error', [('retry-retention', N)], N), 'saving')
check('saving / banner-exposure-end', solve('saving', [('banner-exposure-end', N)], N), 'saving-without-banner')
check('saving-without-banner / save-success', solve('saving-without-banner', [('save-success', N)], N), 'inbox-only')
check('saving-without-banner / save-failure', solve('saving-without-banner', [('save-failure', N)], N), 'recovery-banner')
check('inbox-only / show-banner-again', solve('inbox-only', [('show-banner-again', N)], N), 'repromoting')
check('repromoting / banner-mounted', solve('repromoting', [('banner-mounted', N)], N), 'both')
check('older notification incarnation', solve('banner', [('retain-in-inbox', N - 1)], N), 'banner')
check('future notification incarnation', solve('banner', [('retain-in-inbox', N + 1)], N), 'banner')
check('unknown event is inert', solve('banner', [('unknown-event', N)], N), 'banner')
check('empty delivery batch', solve('banner', [], N), 'banner')
check('N stale deliveries before current delivery', solve('banner', [('retain-in-inbox', N - 1)] * N + [('retain-in-inbox', N)], N), 'saving')
check('trace retain-in-inbox then save-success', solve('banner', [('retain-in-inbox', N), ('save-success', N)], N), 'both')
check('trace retain-in-inbox then save-failure', solve('banner', [('retain-in-inbox', N), ('save-failure', N)], N), 'save-error')
check('trace retain-in-inbox then banner-exposure-end', solve('banner', [('retain-in-inbox', N), ('banner-exposure-end', N)], N), 'saving-without-banner')
check('trace save-success then banner-exposure-end', solve('saving', [('save-success', N), ('banner-exposure-end', N)], N), 'inbox-only')
check('trace save-failure then retry-retention', solve('saving', [('save-failure', N), ('retry-retention', N)], N), 'saving')
check('trace banner-exposure-end then show-banner-again', solve('both', [('banner-exposure-end', N), ('show-banner-again', N)], N), 'repromoting')
check('trace retry-retention then save-success', solve('save-error', [('retry-retention', N), ('save-success', N)], N), 'both')
check('trace retry-retention then save-failure', solve('save-error', [('retry-retention', N), ('save-failure', N)], N), 'save-error')
check('trace retry-retention then banner-exposure-end', solve('save-error', [('retry-retention', N), ('banner-exposure-end', N)], N), 'saving-without-banner')
check('trace banner-exposure-end then save-success', solve('saving', [('banner-exposure-end', N), ('save-success', N)], N), 'inbox-only')
check('trace banner-exposure-end then save-failure', solve('saving', [('banner-exposure-end', N), ('save-failure', N)], N), 'recovery-banner')
check('trace save-success then show-banner-again', solve('saving-without-banner', [('save-success', N), ('show-banner-again', N)], N), 'repromoting')
check('trace show-banner-again then banner-mounted', solve('inbox-only', [('show-banner-again', N), ('banner-mounted', N)], N), 'both')
check('trace banner-mounted then banner-exposure-end', solve('repromoting', [('banner-mounted', N), ('banner-exposure-end', N)], N), 'inbox-only')
check('domain state regression 1: inbox', solve('saving', [('save-success', N, {'revision': 1})], N, {}, 'inbox'), {'phase': 'saving', 'value': []})
check('domain state regression 2: inbox', solve('saving', [('save-success', N, {'revision': 2})], N, {'inbox': ['other']}, 'inbox'), {'phase': 'both', 'value': ['other', 'n']})
check('domain state regression 3: inbox', solve('both', [('banner-exposure-end', N, {})], N, {'inbox': ['n']}, 'inbox'), {'phase': 'inbox-only', 'value': ['n']})
check('domain state regression 4: banner', solve('both', [('banner-exposure-end', N, {})], N, {'inbox': ['n']}, 'banner'), {'phase': 'inbox-only', 'value': False})
check('domain state regression 5: banner', solve('saving-without-banner', [('save-failure', N, {})], N, {'banner': False}, 'banner'), {'phase': 'recovery-banner', 'value': True})
check('domain state regression 6: banner', solve('saving-without-banner', [('save-success', N, {})], N, {'banner': False}, 'banner'), {'phase': 'inbox-only', 'value': False})
check('domain state regression 7: banner', solve('banner', [('retain-in-inbox', N, {}), ('banner-exposure-end', N, {}), ('save-success', N, {})], N, {}, 'banner'), {'phase': 'inbox-only', 'value': False})
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
banner / retain-in-inboxsavingsavingPassed
saving / save-successbothbothPassed
saving / save-failuresave-errorsave-errorPassed
both / banner-exposure-endinbox-onlyinbox-onlyPassed
save-error / retry-retentionsavingsavingPassed
saving / banner-exposure-endgonesaving-without-bannerFailed
saving-without-banner / save-successinbox-onlyinbox-onlyPassed
saving-without-banner / save-failurerecovery-bannerrecovery-bannerPassed
inbox-only / show-banner-againrepromotingrepromotingPassed
repromoting / banner-mountedbothbothPassed
older notification incarnationbannerbannerPassed
future notification incarnationbannerbannerPassed
unknown event is inertbannerbannerPassed
empty delivery batchbannerbannerPassed
N stale deliveries before current deliverysavingsavingPassed
trace retain-in-inbox then save-successbothbothPassed
trace retain-in-inbox then save-failuresave-errorsave-errorPassed
trace retain-in-inbox then banner-exposure-endgonesaving-without-bannerFailed
trace save-success then banner-exposure-endinbox-onlyinbox-onlyPassed
trace save-failure then retry-retentionsavingsavingPassed
trace banner-exposure-end then show-banner-againrepromotingrepromotingPassed
trace retry-retention then save-successbothbothPassed
trace retry-retention then save-failuresave-errorsave-errorPassed
trace retry-retention then banner-exposure-endgonesaving-without-bannerFailed
trace banner-exposure-end then save-successgoneinbox-onlyFailed
trace banner-exposure-end then save-failuregonerecovery-bannerFailed
trace save-success then show-banner-againrepromotingrepromotingPassed
trace show-banner-again then banner-mountedbothbothPassed
trace banner-mounted then banner-exposure-endinbox-onlyinbox-onlyPassed
domain state regression 1: inbox{'phase': 'saving', 'value': []}{'phase': 'saving', 'value': []}Passed
domain state regression 2: inbox{'phase': 'both', 'value': ['other', 'n']}{'phase': 'both', 'value': ['other', 'n']}Passed
domain state regression 3: inbox{'phase': 'inbox-only', 'value': ['n']}{'phase': 'inbox-only', 'value': ['n']}Passed
domain state regression 4: banner{'phase': 'inbox-only', 'value': False}{'phase': 'inbox-only', 'value': False}Passed
domain state regression 5: banner{'phase': 'recovery-banner', 'value': True}{'phase': 'recovery-banner', 'value': True}Passed
domain state regression 6: banner{'phase': 'inbox-only', 'value': False}{'phase': 'inbox-only', 'value': False}Passed
domain state regression 7: banner{'phase': 'gone', 'value': False}{'phase': 'inbox-only', 'value': False}Failed

SHA-256 / 8fdabd8f479614bfce839217f4fd502501a49cd42ca6629799a40f09d9ff90d5

2 / The unsuccessful fix

Exit 1
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(initial, events, generation, payload=None, observe=None):
    table = {('banner', 'retain-in-inbox'): 'saving', ('saving', 'save-success'): 'both', ('saving', 'save-failure'): 'save-error', ('both', 'banner-exposure-end'): 'inbox-only', ('save-error', 'retry-retention'): 'saving', ('saving', 'banner-exposure-end'): 'inbox-only', ('saving-without-banner', 'save-success'): 'inbox-only', ('saving-without-banner', 'save-failure'): 'recovery-banner', ('inbox-only', 'show-banner-again'): 'repromoting', ('repromoting', 'banner-mounted'): 'both'}
    data = json.loads(json.dumps({'banner': True, 'inbox': [], 'item': 'n', 'save_revision': 2})) if payload is None else {**json.loads(json.dumps({'banner': True, 'inbox': [], 'item': 'n', 'save_revision': 2})), **json.loads(json.dumps(payload))}
    state = initial
    for delivery in events:
        event, event_generation = delivery[:2]
        arg = delivery[2] if len(delivery) > 2 else {}
        if event_generation == generation:
            previous = state
            if event in ('save-success','save-failure') and arg.get('revision',data['save_revision']) != data['save_revision']: event = 'unknown-event'
            state = table.get((state, event), state)
            if state in ('both','inbox-only') and data['item'] not in data['inbox']: data['inbox'].append(data['item'])
            data['banner'] = state in ('banner','both','saving','save-error','recovery-banner')
            if state == 'gone': data['inbox'] = [x for x in data['inbox'] if x != data['item']]
    return {'phase': state, 'value': data[observe]} if observe else state
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
check('banner / retain-in-inbox', solve('banner', [('retain-in-inbox', N)], N), 'saving')
check('saving / save-success', solve('saving', [('save-success', N)], N), 'both')
check('saving / save-failure', solve('saving', [('save-failure', N)], N), 'save-error')
check('both / banner-exposure-end', solve('both', [('banner-exposure-end', N)], N), 'inbox-only')
check('save-error / retry-retention', solve('save-error', [('retry-retention', N)], N), 'saving')
check('saving / banner-exposure-end', solve('saving', [('banner-exposure-end', N)], N), 'saving-without-banner')
check('saving-without-banner / save-success', solve('saving-without-banner', [('save-success', N)], N), 'inbox-only')
check('saving-without-banner / save-failure', solve('saving-without-banner', [('save-failure', N)], N), 'recovery-banner')
check('inbox-only / show-banner-again', solve('inbox-only', [('show-banner-again', N)], N), 'repromoting')
check('repromoting / banner-mounted', solve('repromoting', [('banner-mounted', N)], N), 'both')
check('older notification incarnation', solve('banner', [('retain-in-inbox', N - 1)], N), 'banner')
check('future notification incarnation', solve('banner', [('retain-in-inbox', N + 1)], N), 'banner')
check('unknown event is inert', solve('banner', [('unknown-event', N)], N), 'banner')
check('empty delivery batch', solve('banner', [], N), 'banner')
check('N stale deliveries before current delivery', solve('banner', [('retain-in-inbox', N - 1)] * N + [('retain-in-inbox', N)], N), 'saving')
check('trace retain-in-inbox then save-success', solve('banner', [('retain-in-inbox', N), ('save-success', N)], N), 'both')
check('trace retain-in-inbox then save-failure', solve('banner', [('retain-in-inbox', N), ('save-failure', N)], N), 'save-error')
check('trace retain-in-inbox then banner-exposure-end', solve('banner', [('retain-in-inbox', N), ('banner-exposure-end', N)], N), 'saving-without-banner')
check('trace save-success then banner-exposure-end', solve('saving', [('save-success', N), ('banner-exposure-end', N)], N), 'inbox-only')
check('trace save-failure then retry-retention', solve('saving', [('save-failure', N), ('retry-retention', N)], N), 'saving')
check('trace banner-exposure-end then show-banner-again', solve('both', [('banner-exposure-end', N), ('show-banner-again', N)], N), 'repromoting')
check('trace retry-retention then save-success', solve('save-error', [('retry-retention', N), ('save-success', N)], N), 'both')
check('trace retry-retention then save-failure', solve('save-error', [('retry-retention', N), ('save-failure', N)], N), 'save-error')
check('trace retry-retention then banner-exposure-end', solve('save-error', [('retry-retention', N), ('banner-exposure-end', N)], N), 'saving-without-banner')
check('trace banner-exposure-end then save-success', solve('saving', [('banner-exposure-end', N), ('save-success', N)], N), 'inbox-only')
check('trace banner-exposure-end then save-failure', solve('saving', [('banner-exposure-end', N), ('save-failure', N)], N), 'recovery-banner')
check('trace save-success then show-banner-again', solve('saving-without-banner', [('save-success', N), ('show-banner-again', N)], N), 'repromoting')
check('trace show-banner-again then banner-mounted', solve('inbox-only', [('show-banner-again', N), ('banner-mounted', N)], N), 'both')
check('trace banner-mounted then banner-exposure-end', solve('repromoting', [('banner-mounted', N), ('banner-exposure-end', N)], N), 'inbox-only')
check('domain state regression 1: inbox', solve('saving', [('save-success', N, {'revision': 1})], N, {}, 'inbox'), {'phase': 'saving', 'value': []})
check('domain state regression 2: inbox', solve('saving', [('save-success', N, {'revision': 2})], N, {'inbox': ['other']}, 'inbox'), {'phase': 'both', 'value': ['other', 'n']})
check('domain state regression 3: inbox', solve('both', [('banner-exposure-end', N, {})], N, {'inbox': ['n']}, 'inbox'), {'phase': 'inbox-only', 'value': ['n']})
check('domain state regression 4: banner', solve('both', [('banner-exposure-end', N, {})], N, {'inbox': ['n']}, 'banner'), {'phase': 'inbox-only', 'value': False})
check('domain state regression 5: banner', solve('saving-without-banner', [('save-failure', N, {})], N, {'banner': False}, 'banner'), {'phase': 'recovery-banner', 'value': True})
check('domain state regression 6: banner', solve('saving-without-banner', [('save-success', N, {})], N, {'banner': False}, 'banner'), {'phase': 'inbox-only', 'value': False})
check('domain state regression 7: banner', solve('banner', [('retain-in-inbox', N, {}), ('banner-exposure-end', N, {}), ('save-success', N, {})], N, {}, 'banner'), {'phase': 'inbox-only', 'value': False})
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
banner / retain-in-inboxsavingsavingPassed
saving / save-successbothbothPassed
saving / save-failuresave-errorsave-errorPassed
both / banner-exposure-endinbox-onlyinbox-onlyPassed
save-error / retry-retentionsavingsavingPassed
saving / banner-exposure-endinbox-onlysaving-without-bannerFailed
saving-without-banner / save-successinbox-onlyinbox-onlyPassed
saving-without-banner / save-failurerecovery-bannerrecovery-bannerPassed
inbox-only / show-banner-againrepromotingrepromotingPassed
repromoting / banner-mountedbothbothPassed
older notification incarnationbannerbannerPassed
future notification incarnationbannerbannerPassed
unknown event is inertbannerbannerPassed
empty delivery batchbannerbannerPassed
N stale deliveries before current deliverysavingsavingPassed
trace retain-in-inbox then save-successbothbothPassed
trace retain-in-inbox then save-failuresave-errorsave-errorPassed
trace retain-in-inbox then banner-exposure-endinbox-onlysaving-without-bannerFailed
trace save-success then banner-exposure-endinbox-onlyinbox-onlyPassed
trace save-failure then retry-retentionsavingsavingPassed
trace banner-exposure-end then show-banner-againrepromotingrepromotingPassed
trace retry-retention then save-successbothbothPassed
trace retry-retention then save-failuresave-errorsave-errorPassed
trace retry-retention then banner-exposure-endinbox-onlysaving-without-bannerFailed
trace banner-exposure-end then save-successinbox-onlyinbox-onlyPassed
trace banner-exposure-end then save-failureinbox-onlyrecovery-bannerFailed
trace save-success then show-banner-againrepromotingrepromotingPassed
trace show-banner-again then banner-mountedbothbothPassed
trace banner-mounted then banner-exposure-endinbox-onlyinbox-onlyPassed
domain state regression 1: inbox{'phase': 'saving', 'value': []}{'phase': 'saving', 'value': []}Passed
domain state regression 2: inbox{'phase': 'both', 'value': ['other', 'n']}{'phase': 'both', 'value': ['other', 'n']}Passed
domain state regression 3: inbox{'phase': 'inbox-only', 'value': ['n']}{'phase': 'inbox-only', 'value': ['n']}Passed
domain state regression 4: banner{'phase': 'inbox-only', 'value': False}{'phase': 'inbox-only', 'value': False}Passed
domain state regression 5: banner{'phase': 'recovery-banner', 'value': True}{'phase': 'recovery-banner', 'value': True}Passed
domain state regression 6: banner{'phase': 'inbox-only', 'value': False}{'phase': 'inbox-only', 'value': False}Passed
domain state regression 7: banner{'phase': 'inbox-only', 'value': False}{'phase': 'inbox-only', 'value': False}Passed

SHA-256 / bd5b0729ac57775c3964d86e9efe505b26b12a5cd218559d292d8fa9ab7d8e2f

3 / The verified repair

Exit 0
"""Failure Map reference implementation. Python standard library only."""
import json

N = 1
observations = []
def solve(initial, events, generation, payload=None, observe=None):
    table = {('banner', 'retain-in-inbox'): 'saving', ('saving', 'save-success'): 'both', ('saving', 'save-failure'): 'save-error', ('both', 'banner-exposure-end'): 'inbox-only', ('save-error', 'retry-retention'): 'saving', ('saving', 'banner-exposure-end'): 'saving-without-banner', ('saving-without-banner', 'save-success'): 'inbox-only', ('saving-without-banner', 'save-failure'): 'recovery-banner', ('inbox-only', 'show-banner-again'): 'repromoting', ('repromoting', 'banner-mounted'): 'both'}
    data = json.loads(json.dumps({'banner': True, 'inbox': [], 'item': 'n', 'save_revision': 2})) if payload is None else {**json.loads(json.dumps({'banner': True, 'inbox': [], 'item': 'n', 'save_revision': 2})), **json.loads(json.dumps(payload))}
    state = initial
    for delivery in events:
        event, event_generation = delivery[:2]
        arg = delivery[2] if len(delivery) > 2 else {}
        if event_generation == generation:
            previous = state
            if event in ('save-success','save-failure') and arg.get('revision',data['save_revision']) != data['save_revision']: event = 'unknown-event'
            state = table.get((state, event), state)
            if state in ('both','inbox-only') and data['item'] not in data['inbox']: data['inbox'].append(data['item'])
            data['banner'] = state in ('banner','both','saving','save-error','recovery-banner')
            if state == 'gone': data['inbox'] = [x for x in data['inbox'] if x != data['item']]
    return {'phase': state, 'value': data[observe]} if observe else state
def check(label, actual, expected):
    observations.append({"check": label, "actual": actual, "expected": expected, "passed": actual == expected})
check('banner / retain-in-inbox', solve('banner', [('retain-in-inbox', N)], N), 'saving')
check('saving / save-success', solve('saving', [('save-success', N)], N), 'both')
check('saving / save-failure', solve('saving', [('save-failure', N)], N), 'save-error')
check('both / banner-exposure-end', solve('both', [('banner-exposure-end', N)], N), 'inbox-only')
check('save-error / retry-retention', solve('save-error', [('retry-retention', N)], N), 'saving')
check('saving / banner-exposure-end', solve('saving', [('banner-exposure-end', N)], N), 'saving-without-banner')
check('saving-without-banner / save-success', solve('saving-without-banner', [('save-success', N)], N), 'inbox-only')
check('saving-without-banner / save-failure', solve('saving-without-banner', [('save-failure', N)], N), 'recovery-banner')
check('inbox-only / show-banner-again', solve('inbox-only', [('show-banner-again', N)], N), 'repromoting')
check('repromoting / banner-mounted', solve('repromoting', [('banner-mounted', N)], N), 'both')
check('older notification incarnation', solve('banner', [('retain-in-inbox', N - 1)], N), 'banner')
check('future notification incarnation', solve('banner', [('retain-in-inbox', N + 1)], N), 'banner')
check('unknown event is inert', solve('banner', [('unknown-event', N)], N), 'banner')
check('empty delivery batch', solve('banner', [], N), 'banner')
check('N stale deliveries before current delivery', solve('banner', [('retain-in-inbox', N - 1)] * N + [('retain-in-inbox', N)], N), 'saving')
check('trace retain-in-inbox then save-success', solve('banner', [('retain-in-inbox', N), ('save-success', N)], N), 'both')
check('trace retain-in-inbox then save-failure', solve('banner', [('retain-in-inbox', N), ('save-failure', N)], N), 'save-error')
check('trace retain-in-inbox then banner-exposure-end', solve('banner', [('retain-in-inbox', N), ('banner-exposure-end', N)], N), 'saving-without-banner')
check('trace save-success then banner-exposure-end', solve('saving', [('save-success', N), ('banner-exposure-end', N)], N), 'inbox-only')
check('trace save-failure then retry-retention', solve('saving', [('save-failure', N), ('retry-retention', N)], N), 'saving')
check('trace banner-exposure-end then show-banner-again', solve('both', [('banner-exposure-end', N), ('show-banner-again', N)], N), 'repromoting')
check('trace retry-retention then save-success', solve('save-error', [('retry-retention', N), ('save-success', N)], N), 'both')
check('trace retry-retention then save-failure', solve('save-error', [('retry-retention', N), ('save-failure', N)], N), 'save-error')
check('trace retry-retention then banner-exposure-end', solve('save-error', [('retry-retention', N), ('banner-exposure-end', N)], N), 'saving-without-banner')
check('trace banner-exposure-end then save-success', solve('saving', [('banner-exposure-end', N), ('save-success', N)], N), 'inbox-only')
check('trace banner-exposure-end then save-failure', solve('saving', [('banner-exposure-end', N), ('save-failure', N)], N), 'recovery-banner')
check('trace save-success then show-banner-again', solve('saving-without-banner', [('save-success', N), ('show-banner-again', N)], N), 'repromoting')
check('trace show-banner-again then banner-mounted', solve('inbox-only', [('show-banner-again', N), ('banner-mounted', N)], N), 'both')
check('trace banner-mounted then banner-exposure-end', solve('repromoting', [('banner-mounted', N), ('banner-exposure-end', N)], N), 'inbox-only')
check('domain state regression 1: inbox', solve('saving', [('save-success', N, {'revision': 1})], N, {}, 'inbox'), {'phase': 'saving', 'value': []})
check('domain state regression 2: inbox', solve('saving', [('save-success', N, {'revision': 2})], N, {'inbox': ['other']}, 'inbox'), {'phase': 'both', 'value': ['other', 'n']})
check('domain state regression 3: inbox', solve('both', [('banner-exposure-end', N, {})], N, {'inbox': ['n']}, 'inbox'), {'phase': 'inbox-only', 'value': ['n']})
check('domain state regression 4: banner', solve('both', [('banner-exposure-end', N, {})], N, {'inbox': ['n']}, 'banner'), {'phase': 'inbox-only', 'value': False})
check('domain state regression 5: banner', solve('saving-without-banner', [('save-failure', N, {})], N, {'banner': False}, 'banner'), {'phase': 'recovery-banner', 'value': True})
check('domain state regression 6: banner', solve('saving-without-banner', [('save-success', N, {})], N, {'banner': False}, 'banner'), {'phase': 'inbox-only', 'value': False})
check('domain state regression 7: banner', solve('banner', [('retain-in-inbox', N, {}), ('banner-exposure-end', N, {}), ('save-success', N, {})], N, {}, 'banner'), {'phase': 'inbox-only', 'value': False})
print(json.dumps({"observations": observations, "passed": all(x["passed"] for x in observations)}, ensure_ascii=False))
raise SystemExit(0 if all(x["passed"] for x in observations) else 1)
Boundary fixtureActualExpectedOutcome
banner / retain-in-inboxsavingsavingPassed
saving / save-successbothbothPassed
saving / save-failuresave-errorsave-errorPassed
both / banner-exposure-endinbox-onlyinbox-onlyPassed
save-error / retry-retentionsavingsavingPassed
saving / banner-exposure-endsaving-without-bannersaving-without-bannerPassed
saving-without-banner / save-successinbox-onlyinbox-onlyPassed
saving-without-banner / save-failurerecovery-bannerrecovery-bannerPassed
inbox-only / show-banner-againrepromotingrepromotingPassed
repromoting / banner-mountedbothbothPassed
older notification incarnationbannerbannerPassed
future notification incarnationbannerbannerPassed
unknown event is inertbannerbannerPassed
empty delivery batchbannerbannerPassed
N stale deliveries before current deliverysavingsavingPassed
trace retain-in-inbox then save-successbothbothPassed
trace retain-in-inbox then save-failuresave-errorsave-errorPassed
trace retain-in-inbox then banner-exposure-endsaving-without-bannersaving-without-bannerPassed
trace save-success then banner-exposure-endinbox-onlyinbox-onlyPassed
trace save-failure then retry-retentionsavingsavingPassed
trace banner-exposure-end then show-banner-againrepromotingrepromotingPassed
trace retry-retention then save-successbothbothPassed
trace retry-retention then save-failuresave-errorsave-errorPassed
trace retry-retention then banner-exposure-endsaving-without-bannersaving-without-bannerPassed
trace banner-exposure-end then save-successinbox-onlyinbox-onlyPassed
trace banner-exposure-end then save-failurerecovery-bannerrecovery-bannerPassed
trace save-success then show-banner-againrepromotingrepromotingPassed
trace show-banner-again then banner-mountedbothbothPassed
trace banner-mounted then banner-exposure-endinbox-onlyinbox-onlyPassed
domain state regression 1: inbox{'phase': 'saving', 'value': []}{'phase': 'saving', 'value': []}Passed
domain state regression 2: inbox{'phase': 'both', 'value': ['other', 'n']}{'phase': 'both', 'value': ['other', 'n']}Passed
domain state regression 3: inbox{'phase': 'inbox-only', 'value': ['n']}{'phase': 'inbox-only', 'value': ['n']}Passed
domain state regression 4: banner{'phase': 'inbox-only', 'value': False}{'phase': 'inbox-only', 'value': False}Passed
domain state regression 5: banner{'phase': 'recovery-banner', 'value': True}{'phase': 'recovery-banner', 'value': True}Passed
domain state regression 6: banner{'phase': 'inbox-only', 'value': False}{'phase': 'inbox-only', 'value': False}Passed
domain state regression 7: banner{'phase': 'inbox-only', 'value': False}{'phase': 'inbox-only', 'value': False}Passed

SHA-256 / 4f191d03be26b507cfac945ebf5c759da0d7f67ccce099c8d24d9db4e1576db1

Verification & scope

Stipulated single-notification state and policy model only. Event delivery and acknowledgements are explicit test inputs. No DOM, accessibility announcements, live timers, networking, actual rendering, or production-platform conformance is simulated. This reproducer isolates one failure mechanism. Results cover the supplied fixtures. Variants within a family share a test contract and should remain grouped when constructing evaluation splits. Related mechanisms with a shared evaluation_group must also remain together; these controlled models are not independent production incidents.

Observations recorded using Python 3.12.14 at 2026-09-29T14:42:32.170296+00:00.

Case digest / dfc49bc199817e25543d0028669d34d6eaa9f79d019a466f9edcbba198416204