Skip to content
Closed
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
3 changes: 2 additions & 1 deletion src/msg/core/executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ async def execute(self,request, *, entry='network'):
capacity_writes={'identity.register','identity.temporary','identity.custodial_create','content.topic_create',
'content.post_create','content.post_edit','content.file_put','content.attach',
'content.template_put','discussion.reply','discussion.quote','discussion.repost',
'transfer.part_put','git.create','git.push','git.receive','hosting.deploy','keystore.put'}
'transfer.part_put','git.create','git.push','git.receive','hosting.deploy','keystore.put',
'achievement.start'}
if spec.name in capacity_writes:
require(session.setting('runtime_config',{}).get('accept_writes',True),'writes_paused')
for rid,generation in request.expected_generations:
Expand Down
12 changes: 12 additions & 0 deletions src/msg/plugins/achievements.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,18 @@ async def list_grants(ctx, request, tx):
async def start(ctx, request, tx):
subject = _owner(ctx)
await tx.subject(subject)
grant = tx.one(
'SELECT 1 FROM achievement_grants WHERE subject=? AND achievement_id=? AND spec_version=?',
(subject, I_AM_NOT_HUMAN.id, I_AM_NOT_HUMAN.version),
)
require(grant is None, 'achievement_already_granted')
ceremonies = [loads(row[0]) for row in tx.rows(
'SELECT body FROM achievement_ceremonies WHERE subject=?', (subject,))]
require(not any(item['status'] == 'active' and ctx.now < parse_time(item['expires_at'])
for item in ceremonies), 'achievement_ceremony_active')
# A subject can retain at most one ceremony. Reclaim failed, completed, and
# expired attempts before allocating a replacement challenge.
tx.execute('DELETE FROM achievement_ceremonies WHERE subject=?', (subject,), write=True)
state = {'id': new_id('achc'), 'subject_id': subject, 'achievement_id': I_AM_NOT_HUMAN.id,
'spec_version': I_AM_NOT_HUMAN.version, 'status': 'active', 'round': 1,
'started_at': wire(ctx.now), 'expires_at': wire(ctx.now + timedelta(seconds=CEREMONY_TTL_SECONDS)),
Expand Down
30 changes: 30 additions & 0 deletions tests/test_achievements.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,36 @@ async def test_wrong_nonce_and_round_ttl_fail_closed(installed):
assert expired.data['reason'] == 'round_expired'


@pytest.mark.asyncio
async def test_start_keeps_only_one_ceremony_per_subject(installed):
app, _ = installed
key, subject, _ = await register(app, 'achievement-bounded')
started = await call(app, 'achievement.start', {}, key=key, subject=subject)
duplicate = await call(app, 'achievement.start', {}, key=key, subject=subject)
assert duplicate.status == 'error' and duplicate.error.code == 'achievement_ceremony_active'
async with app.metadata.transaction(write=False) as tx:
assert tx.one('SELECT COUNT(*) FROM achievement_ceremonies WHERE subject=?', (subject,))[0] == 1

app.executor.clock = lambda: NOW + timedelta(seconds=301)
replacement = await call(app, 'achievement.start', {}, key=key, subject=subject)
assert replacement.status == 'ok', wire(replacement)
assert replacement.data['challenge_id'] != started.data['challenge_id']
async with app.metadata.transaction(write=False) as tx:
assert tx.one('SELECT COUNT(*) FROM achievement_ceremonies WHERE subject=?', (subject,))[0] == 1


@pytest.mark.asyncio
async def test_start_obeys_capacity_pause(installed):
app, _ = installed
key, subject, _ = await register(app, 'achievement-paused')
async with app.metadata.transaction(write=True) as tx:
tx.set_setting('runtime_config', {'accept_writes': False, 'cleanup_enabled': True})
blocked = await call(app, 'achievement.start', {}, key=key, subject=subject)
assert blocked.status == 'error' and blocked.error.code == 'writes_paused'
async with app.metadata.transaction(write=False) as tx:
assert tx.one('SELECT COUNT(*) FROM achievement_ceremonies WHERE subject=?', (subject,))[0] == 0


@pytest.mark.asyncio
async def test_machine_payload_and_final_digest_are_required(installed):
app, _ = installed
Expand Down
Loading