Improve character suggestions and serve pools from the API
This commit is contained in:
@@ -38,4 +38,4 @@
|
||||
## Words Words Words
|
||||
|
||||
- [x] **TLDR rules**. A one page summary sheet of the rulebook, for the impatient
|
||||
- [ ] **Suggestions.** Take a pass through all of the suggestions in suggestions.js, rewrite ones that are stiff, awkward, or don't fit the theme/setting as described in the rulebook. Move the suggestion pool from suggestions.js into a backend API endpoint.
|
||||
- [x] **Suggestions.** Take a pass through all of the suggestions in suggestions.js, rewrite ones that are stiff, awkward, or don't fit the theme/setting as described in the rulebook. Move the suggestion pool from suggestions.js into a backend API endpoint.
|
||||
|
||||
@@ -7,6 +7,17 @@
|
||||
|
||||
export let state;
|
||||
|
||||
let suggestionError = '';
|
||||
async function suggest(type, apply) {
|
||||
suggestionError = '';
|
||||
try {
|
||||
apply(await getSuggestion(type));
|
||||
} catch (error) {
|
||||
suggestionError = 'Could not load suggestions. Try again.';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Form states
|
||||
let basicDetails = {
|
||||
avatar_look: state.player.avatar_look || '',
|
||||
@@ -252,6 +263,8 @@
|
||||
|
||||
</script>
|
||||
|
||||
{#if suggestionError}<p role="alert">{suggestionError}</p>{/if}
|
||||
|
||||
<div class="character-creation-view">
|
||||
<div class="view-header text-center">
|
||||
<h2>Character Sheet Creation</h2>
|
||||
@@ -281,28 +294,28 @@
|
||||
<label for="avatar_look">What does your Pi-Rat look like?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="avatar_look" placeholder="e.g. A blue bandana, scarred snout" bind:value={basicDetails.avatar_look} required class="input-field">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_look = getSuggestion('look')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('look', value => basicDetails.avatar_look = value)}>Suggest</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="avatar_smell">What does your Pi-Rat smell like?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="avatar_smell" placeholder="e.g. Damp gunpowder, salty cheese" bind:value={basicDetails.avatar_smell} required class="input-field">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_smell = getSuggestion('smell')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('smell', value => basicDetails.avatar_smell = value)}>Suggest</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="first_words">What were your first words after transforming?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="first_words" placeholder="e.g. Where is my gat?!" bind:value={basicDetails.first_words} required class="input-field">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.first_words = getSuggestion('first_words')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('first_words', value => basicDetails.first_words = value)}>Suggest</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="good_at_math">Is your Pi-Rat good at Math?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="good_at_math" placeholder="e.g. Yes, but only geometry; No, thinks Pi is a dessert" bind:value={basicDetails.good_at_math} required class="input-field">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.good_at_math = getSuggestion('good_at_math')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('good_at_math', value => basicDetails.good_at_math = value)}>Suggest</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row mt-4">
|
||||
@@ -356,7 +369,7 @@
|
||||
</label>
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={inboxAnswers[`${p.id}:like`]} placeholder="e.g. They always share their cheese"/>
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${p.id}:like`] = getSuggestion('like')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('like', value => inboxAnswers[`${p.id}:like`] = value)}>Suggest</button>{/if}
|
||||
<button class="btn btn-primary" disabled={!(inboxAnswers[`${p.id}:like`] || '').trim()} on:click={() => submitDelegatedAnswer('like', p.id)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -369,7 +382,7 @@
|
||||
</label>
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={inboxAnswers[`${p.id}:hate`]} placeholder="e.g. They snore too loudly"/>
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${p.id}:hate`] = getSuggestion('hate')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('hate', value => inboxAnswers[`${p.id}:hate`] = value)}>Suggest</button>{/if}
|
||||
<button class="btn btn-primary" disabled={!(inboxAnswers[`${p.id}:hate`] || '').trim()} on:click={() => submitDelegatedAnswer('hate', p.id)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,17 @@
|
||||
|
||||
export let state;
|
||||
|
||||
let suggestionError = '';
|
||||
async function suggest(type, apply) {
|
||||
suggestionError = '';
|
||||
try {
|
||||
apply(await getSuggestion(type));
|
||||
} catch (error) {
|
||||
suggestionError = 'Could not load suggestions. Try again.';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function getPlayerName(id) {
|
||||
if (!id) return 'None';
|
||||
const p = state.players.find(x => x.id === id);
|
||||
@@ -137,6 +148,8 @@
|
||||
|
||||
</script>
|
||||
|
||||
{#if suggestionError}<p role="alert">{suggestionError}</p>{/if}
|
||||
|
||||
<div class="recruit-phase-view">
|
||||
<div class="view-header text-center">
|
||||
<h2>🐀 New Recruits</h2>
|
||||
@@ -162,28 +175,28 @@
|
||||
<label for="avatar_look">What does your Pi-Rat look like?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="avatar_look" placeholder="e.g. A blue bandana, scarred snout" bind:value={basicDetails.avatar_look} required class="input-field">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_look = getSuggestion('look')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('look', value => basicDetails.avatar_look = value)}>Suggest</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="avatar_smell">What does your Pi-Rat smell like? (Determines name)</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="avatar_smell" placeholder="e.g. Damp gunpowder, salty cheese" bind:value={basicDetails.avatar_smell} required class="input-field">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.avatar_smell = getSuggestion('smell')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('smell', value => basicDetails.avatar_smell = value)}>Suggest</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="first_words">What were your first words after transforming?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="first_words" placeholder="e.g. Where is my gat?!" bind:value={basicDetails.first_words} required class="input-field">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.first_words = getSuggestion('first_words')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('first_words', value => basicDetails.first_words = value)}>Suggest</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="good_at_math">Is your Pi-Rat good at Math?</label>
|
||||
<div class="input-row">
|
||||
<input type="text" id="good_at_math" placeholder="e.g. Thinks Pi is a dessert" bind:value={basicDetails.good_at_math} required class="input-field">
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => basicDetails.good_at_math = getSuggestion('good_at_math')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('good_at_math', value => basicDetails.good_at_math = value)}>Suggest</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row mt-4">
|
||||
@@ -264,7 +277,7 @@
|
||||
</label>
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={inboxAnswers[`${task.recruit.id}:like`]} placeholder="e.g. They always share their cheese"/>
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${task.recruit.id}:like`] = getSuggestion('like')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('like', value => inboxAnswers[`${task.recruit.id}:like`] = value)}>Suggest</button>{/if}
|
||||
<button class="btn btn-primary" disabled={!(inboxAnswers[`${task.recruit.id}:like`] || '').trim()} on:click={() => submitDelegatedAnswer('like', task.recruit.id)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -277,7 +290,7 @@
|
||||
</label>
|
||||
<div class="input-row">
|
||||
<input type="text" class="input-field" bind:value={inboxAnswers[`${task.recruit.id}:hate`]} placeholder="e.g. They snore too loudly"/>
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => inboxAnswers[`${task.recruit.id}:hate`] = getSuggestion('hate')}>Suggest</button>{/if}
|
||||
{#if devMode}<button type="button" class="btn btn-secondary btn-small" on:click={() => suggest('hate', value => inboxAnswers[`${task.recruit.id}:hate`] = value)}>Suggest</button>{/if}
|
||||
<button class="btn btn-primary" disabled={!(inboxAnswers[`${task.recruit.id}:hate`] || '').trim()} on:click={() => submitDelegatedAnswer('hate', task.recruit.id)}>Submit</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
// - Add a CHANGELOG entry only when a commit changes something players can
|
||||
// see. Skip refactors, tests, and tooling. Keep wording player-facing.
|
||||
|
||||
export const VERSION = 46;
|
||||
export const VERSION = 47;
|
||||
|
||||
// Newest first. Each entry: { version, date: 'YYYY-MM-DD', changes: [string, ...] }.
|
||||
export const CHANGELOG = [
|
||||
{ version: 47, date: '2026-09-04', changes: ['Rewrote character suggestions with scrappy pirate looks, distinctive smells, first words, and crew quirks that fit Yeld.'] },
|
||||
{ version: 46, date: '2026-09-04', changes: ['Added a printable TL;DR rules page, linked at the top of the full rulebook.'] },
|
||||
{ version: 44, date: '2026-09-04', changes: ['Crew Objectives opens as one connected panel, with the toggle and checklist sharing a border and background.'] },
|
||||
{ version: 43, date: '2026-09-04', changes: ['The crew roster keeps players in the same order when roles or ranks change, including when someone becomes the Deep.'] },
|
||||
|
||||
+19
-1229
File diff suppressed because it is too large
Load Diff
@@ -113,18 +113,18 @@
|
||||
async function skipCharacterCreation() {
|
||||
// Suggested values for everyone's free-text fields; the backend only
|
||||
// applies them to fields players haven't filled in themselves.
|
||||
try {
|
||||
const fills = {};
|
||||
for (const p of state.players) {
|
||||
fills[p.id] = {
|
||||
avatar_look: getSuggestion('look'),
|
||||
avatar_smell: getSuggestion('smell'),
|
||||
first_words: getSuggestion('first_words'),
|
||||
good_at_math: getSuggestion('good_at_math'),
|
||||
like: getSuggestion('like'),
|
||||
hate: getSuggestion('hate'),
|
||||
avatar_look: await getSuggestion('look'),
|
||||
avatar_smell: await getSuggestion('smell'),
|
||||
first_words: await getSuggestion('first_words'),
|
||||
good_at_math: await getSuggestion('good_at_math'),
|
||||
like: await getSuggestion('like'),
|
||||
hate: await getSuggestion('hate'),
|
||||
};
|
||||
}
|
||||
try {
|
||||
await apiRequest(`/game/${gameId}/player/${playerId}/skip-character-creation`, 'POST', {
|
||||
fills: JSON.stringify(fills)
|
||||
});
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { test } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
const asModule = source => `data:text/javascript;base64,${Buffer.from(source).toString('base64')}`;
|
||||
const api = asModule(await readFile(new URL('../src/lib/api.js', import.meta.url), 'utf8'));
|
||||
const source = (await readFile(new URL('../src/lib/suggestions.js', import.meta.url), 'utf8'))
|
||||
.replace("'./api'", JSON.stringify(api));
|
||||
|
||||
test('suggestions retry failed requests, share the pool, and respect exclusions', async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
let calls = 0;
|
||||
globalThis.fetch = async url => {
|
||||
calls++;
|
||||
if (calls === 1) throw new Error('offline');
|
||||
assert.equal(url, '/api/suggestions');
|
||||
return { ok: true, json: async () => ({ look: ['Bandana', 'Eyepatch'], smell: ['Tar'] }) };
|
||||
};
|
||||
try {
|
||||
const { getSuggestion } = await import(asModule(source));
|
||||
await assert.rejects(getSuggestion('look'), /offline/);
|
||||
assert.deepEqual(await Promise.all([
|
||||
getSuggestion('look', [' bandana ']), getSuggestion('smell')
|
||||
]), ['Eyepatch', 'Tar']);
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(await getSuggestion('unknown'), '');
|
||||
assert.ok(['Bandana', 'Eyepatch'].includes(await getSuggestion('look', ['Bandana', 'Eyepatch'])));
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -502,7 +502,7 @@ def skip_character_creation(db: Session, game: Game, fills: dict):
|
||||
creation for ALL players and advances to scene setup. `fills` maps
|
||||
player_id -> suggested values for the free-text fields (avatar_look,
|
||||
avatar_smell, first_words, good_at_math, like, hate), supplied by the
|
||||
admin's client from the frontend suggestion pools. Techniques come from
|
||||
admin's client from the backend suggestion API. Techniques come from
|
||||
the backend pool. Already-filled fields are left untouched.
|
||||
"""
|
||||
from .cards import TECHNIQUE_SUGGESTIONS
|
||||
|
||||
@@ -4,10 +4,16 @@ from sqlmodel import Session
|
||||
from .database import get_session
|
||||
from . import crud
|
||||
from .cards import TECHNIQUE_SUGGESTIONS
|
||||
from .suggestions import CHARACTER_SUGGESTIONS
|
||||
from .validation import sanitize_text, sanitize_name
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/suggestions")
|
||||
def character_suggestions():
|
||||
return CHARACTER_SUGGESTIONS
|
||||
|
||||
|
||||
# Secret Pirate Technique name pool (for the frontend's Suggest buttons)
|
||||
@router.get("/suggestions/techniques")
|
||||
def technique_suggestions():
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
"""Hand-written Rat Records prompts for the scrappy fantasy pirates of Yeld.
|
||||
|
||||
Smells also serve as temporary names; keep them short noun phrases. Math
|
||||
is a personality joke, not a power or a rule benefit.
|
||||
"""
|
||||
|
||||
CHARACTER_SUGGESTIONS = {'look': ['A red bandana with two holes chewed through for ears',
|
||||
'An eyepatch that keeps sliding onto the wrong eye',
|
||||
'A stolen waistcoat with pockets full of biscuit crumbs',
|
||||
'Whiskers braided with bits of sail thread',
|
||||
'A tail tied in a knot so nobody steps on it',
|
||||
'A tiny skull painted between the ears',
|
||||
"A coat made from the ship's old flag",
|
||||
'One gold tooth and a grin that shows it off',
|
||||
'A belt made of rope, with far too many knots',
|
||||
'A hat so wide it catches every gust of wind',
|
||||
'Soot-black paws and a suspiciously innocent expression',
|
||||
'A scar across the snout that they insist looks impressive',
|
||||
'A brass button worn as a medal',
|
||||
'A striped shirt still bearing the laundry tag',
|
||||
'A fork tucked behind one ear for emergencies',
|
||||
'A tail wrapped in a strip of stolen velvet',
|
||||
'A necklace of fish bones and one very nice pearl',
|
||||
"Fur slicked back with the cook's best butter",
|
||||
'A chalk handprint on the back of a borrowed coat',
|
||||
'An enormous hat held on with string'],
|
||||
'smell': ['Gunpowder and wet fur',
|
||||
'Old cheese and fresh tar',
|
||||
'Pickle brine',
|
||||
'Burnt toast',
|
||||
'Fish guts and cheap perfume',
|
||||
'Damp rope',
|
||||
'Sour grog',
|
||||
'Peppermint and bilge water',
|
||||
'Smoked herring',
|
||||
'Candle wax and singed whiskers',
|
||||
'Mouldy biscuits',
|
||||
'Salt pork and garlic',
|
||||
'Seaweed left in the sun',
|
||||
'Stolen jam',
|
||||
'Wet wool and black pepper',
|
||||
'Boot polish',
|
||||
'Rancid butter',
|
||||
'Lemon peel and cannon smoke',
|
||||
'Onions and damp wood',
|
||||
'Something dead behind the pantry'],
|
||||
'first_words': ["Who's been keeping all this cheese up here?",
|
||||
"I've got thumbs! Nobody panic!",
|
||||
'Right. Which end of the ship do we steal first?',
|
||||
"That hat's mine now.",
|
||||
'I liked the captain better when I could fit in his boot.',
|
||||
'I demand a bigger lunch.',
|
||||
'Can we eat the map?',
|
||||
'Nobody tell the cat.',
|
||||
"I can explain. Actually, no I can't.",
|
||||
'Why is the floor suddenly so far away?',
|
||||
'Hand over the biscuits and nobody gets bitten.',
|
||||
'I vote we keep the ship.',
|
||||
'Does this mean I have to wear trousers?',
|
||||
"I've always wanted to shout this: BOARD THEM!",
|
||||
'Somebody fetch me a dangerous stick.',
|
||||
'I know three words and two of them are threats.',
|
||||
'Was that your cheese? Unfortunate.',
|
||||
'I can count! We are outnumbered!',
|
||||
"Let's point the cannon at whoever owns it.",
|
||||
'I was already this clever. Ask anyone.'],
|
||||
'good_at_math': ['Yes, until someone starts watching.',
|
||||
'Only when dividing up stolen food.',
|
||||
'No, but very loud about the answer.',
|
||||
'Can count every coin in a purse by shaking it.',
|
||||
'Knows that two lunches are better than one.',
|
||||
"Counts on their claws, then borrows someone else's.",
|
||||
'Brilliant at sums. Cannot tell left from right.',
|
||||
'Insists every problem can be solved by adding more rats.',
|
||||
'Yes, and will prove it on your coat in chalk.',
|
||||
'Can divide loot fairly. Chooses not to.',
|
||||
'Thinks fractions are what happens when you bite a biscuit.',
|
||||
'Keeps losing count when their tail moves.',
|
||||
'No. Has appointed a beetle to handle the numbers.',
|
||||
'Can calculate a cannon shot but not the price of lunch.',
|
||||
'Gets the right answer for entirely the wrong reasons.',
|
||||
'Counts backwards whenever frightened.',
|
||||
'Only if the numbers are small enough to threaten.',
|
||||
'Knows pi to several places, all of them wrong.',
|
||||
"Refuses to subtract. That's how things go missing.",
|
||||
'Perfectly average, which they find deeply suspicious.'],
|
||||
'like': ['They always save you the least mouldy biscuit.',
|
||||
"They'll bite anyone who threatens the crew.",
|
||||
'They remember which hammock is yours.',
|
||||
'They take the blame when a good prank goes wrong.',
|
||||
'They can make a feast out of stolen scraps.',
|
||||
'They laugh at your jokes, even during a mutiny.',
|
||||
'They share their last dry match.',
|
||||
'They never leave a rat behind.',
|
||||
'They know when to stop talking and start chewing through ropes.',
|
||||
'They tell excellent lies on your behalf.',
|
||||
'They warm your paws when the night watch gets cold.',
|
||||
'They always volunteer to distract the cat.',
|
||||
'They can find food in an empty cupboard.',
|
||||
'They mend your clothes without being asked.',
|
||||
'They cheer loudest when you do something reckless.',
|
||||
'They keep a lookout while you sneak an extra ration.',
|
||||
'They turn every terrible plan into a team effort.',
|
||||
'They know a song for every disaster.',
|
||||
'They give the best apologies, usually with snacks.',
|
||||
'They trust you with the good end of the rope.'],
|
||||
'hate': ['They eat the evidence before anyone can examine it.',
|
||||
'They borrow your boots and return only one.',
|
||||
"They practice their captain voice while you're sleeping.",
|
||||
'They leave fish bones in your hammock.',
|
||||
'They insist every plan was their idea.',
|
||||
'They lick the best biscuit before offering to share.',
|
||||
"They chew through ropes without checking what they're holding up.",
|
||||
'They shout "Land ho!" whenever they\'re bored.',
|
||||
'They use your coat to wipe cannon soot off their paws.',
|
||||
'They keep a secret food stash that everyone can smell.',
|
||||
'They correct your sums in the middle of a fight.',
|
||||
'They drum on empty barrels during the night watch.',
|
||||
'They promise your share of the loot to strangers.',
|
||||
'They pick their teeth with your favourite knife.',
|
||||
'They give away the hiding place by giggling.',
|
||||
"They refuse to admit they can't read the map.",
|
||||
'They shake seawater onto everyone after a swim.',
|
||||
'They start a new verse just when the song seems over.',
|
||||
'They call every creature with fins their cousin.',
|
||||
"They always ask if you're going to finish that, mid-bite."]}
|
||||
@@ -0,0 +1,20 @@
|
||||
from fastapi.testclient import TestClient
|
||||
from pirats.main import app
|
||||
from pirats.validation import sanitize_name, sanitize_text
|
||||
|
||||
|
||||
def test_suggestions_api_covers_rat_records_without_truncation():
|
||||
# No lifespan needed: these public endpoints do not access the database.
|
||||
client = TestClient(app)
|
||||
response = client.get('/api/suggestions')
|
||||
assert response.status_code == 200
|
||||
pools = response.json()
|
||||
assert set(pools) == {'look', 'smell', 'first_words', 'good_at_math', 'like', 'hate'}
|
||||
for category, entries in pools.items():
|
||||
assert len(entries) >= 20
|
||||
assert len({entry.casefold() for entry in entries}) == len(entries)
|
||||
sanitize = sanitize_name if category == 'smell' else sanitize_text
|
||||
assert all(entry and sanitize(entry) == entry for entry in entries)
|
||||
techniques = client.get('/api/suggestions/techniques')
|
||||
assert techniques.status_code == 200
|
||||
assert techniques.json()['techniques']
|
||||
Reference in New Issue
Block a user