Enhance people management tests and fix related issues
- Updated `PeopleApiTests` to correctly handle family structures, ensuring tests account for single-parent scenarios and sorting behavior for surnames. - Added a new test to validate roster integrity after the yearly intake, confirming that the composition reflects changes post-intake. - Revised assertions to ensure accurate comparisons of student and family data, improving test reliability. - Enhanced documentation within tests to clarify the purpose and expected outcomes of new functionalities. - Addressed discrepancies between design and implementation regarding the layout of the people management interface.
This commit is contained in:
@@ -94,3 +94,84 @@
|
|||||||
`LessonLearningSystem.cs`, `PresenceSystem.cs`, `gameScreen.ts`), и какое-то время решение не
|
`LessonLearningSystem.cs`, `PresenceSystem.cs`, `gameScreen.ts`), и какое-то время решение не
|
||||||
компилировалось. Выводам по срезу 1 это не мешает: его файлы правка не трогает, а дописанные
|
компилировалось. Выводам по срезу 1 это не мешает: его файлы правка не трогает, а дописанные
|
||||||
тесты прогнаны и на чистом `b135a9c`, и на дереве после починки сборки.
|
тесты прогнаны и на чистом `b135a9c`, и на дереве после починки сборки.
|
||||||
|
|
||||||
|
## Срез 2. Люди
|
||||||
|
|
||||||
|
- **Фазы:** 5–9
|
||||||
|
- **Проверен на:** `5cd5a6d`, 2026-08-19
|
||||||
|
- **Пути:** `src/HSchool.People`, `src/HSchool.Content/PeopleDefs.cs`,
|
||||||
|
`src/HSchool.Simulation/RosterSpawner.cs`, `src/HSchool.Simulation/School.cs`,
|
||||||
|
`src/HSchool.Server/Api/SchoolEndpoints.cs`, `src/HSchool.Server/Game/PersonCardReader.cs`,
|
||||||
|
`src/HSchool.Client/src/ui/peoplePanel.ts`, `src/HSchool.Client/src/ui/personCard.ts`,
|
||||||
|
`tests/HSchool.People.Tests`, `tests/HSchool.Content.Tests/PeopleDefTests.cs`,
|
||||||
|
`tests/HSchool.Simulation.Tests/PeopleInSchoolTests.cs`,
|
||||||
|
`tests/HSchool.AppHost.Tests/PeopleApiTests.cs`
|
||||||
|
- **Итог:** исправлен один тест, который падал на чистом клоне, укреплён второй, дописан один;
|
||||||
|
расхождение с дизайном одно и то же, что в срезе 1 (раскладка оболочки менеджера)
|
||||||
|
|
||||||
|
Что подтверждено:
|
||||||
|
|
||||||
|
- **Фаза 5.** Все шесть пунктов списка тестов на месте в `PeopleDefTests` (разбор каждого вида,
|
||||||
|
наследование навыка, патч черты, неизвестная ссылка — ошибка каталога, склонение по модели и
|
||||||
|
перекрытие явной таблицей), плюс `ExtraPack_AddsANameSetAndTrait` — критерий «мод со своим
|
||||||
|
набором имён виден в каталоге». Наборы имён в `GET /api/catalog` проверены в
|
||||||
|
`SchoolApiTests.Catalog_LabelsDefsInTheRequestedLanguage`.
|
||||||
|
- **Фаза 6.** Все шесть тестов из списка фазы существуют и проходят. Критерий «в проекте нет
|
||||||
|
сокетов, ECS и `DateTime.Now`» закреплён самими тестами —
|
||||||
|
`Assembly_DoesNotReferenceArchAspNetOrSockets` и `Sources_DoNotUseWallClock`.
|
||||||
|
- **Фаза 7.** Одиннадцать кабинетов (`VanillaMap_HasElevenHomeroomsNotTheComputerLab`), нужды
|
||||||
|
тикают со школой, ростер публикуется снимком, `PersistPeople` зовётся только когда состав
|
||||||
|
изменился — файл не переписывается по таймеру. Ростер, не сходящийся с картой, оставляет школу
|
||||||
|
незапущенной (`RosterFit.Matches` → `SchoolContentUnavailableException`).
|
||||||
|
- **Фаза 8.** Список читает опубликованный снимок (`loop.FindPeople`), карточка идёт через
|
||||||
|
очередь команд с `TaskCompletionSource` — инвариант 3 цел. Все пять требуемых фазой тестов API
|
||||||
|
на месте. Клиент: фильтры, сортируемые колонки, пейджер, семейные ссылки, всё через `t(...)`;
|
||||||
|
в строке списка только лёгкие поля.
|
||||||
|
- **Фаза 9.** Выпуск старшей имеющейся параллели, сохранение вместимости, семьи через поколения и
|
||||||
|
уход родителей выпускников покрыты в `YearlyIntakeTests` и `PeopleInSchoolTests`.
|
||||||
|
- **Дизайн.** `design/people.md` сверен построчно: неполные семьи ≈8% (тест требует 5–10%),
|
||||||
|
фамилия и отчество отцовские даже у одинокой матери, идентификатор ребёнка — счётчик, места
|
||||||
|
раздаются вперемешку. Расхождений нет.
|
||||||
|
|
||||||
|
Исправлено:
|
||||||
|
|
||||||
|
- **`PeopleApiTests.Card_IncludesFamilyAndLiveNeeds` падал на чистом клоне.** Тест брал первого
|
||||||
|
ученика страницы и требовал у него ровно двух родителей. Состав семьи выводится из сида, а сид —
|
||||||
|
это id школы (`SchoolWorker.InstallPeople`: `seed = school.Id`), то есть зависит от того,
|
||||||
|
сколько школ создали тесты до него и что лежит в `saves/index.json`. Замер по сидам 1–30: у 28
|
||||||
|
первый пятиклассник с двумя родителями, у 2 — с одним, и полный прогон на свежем checkout
|
||||||
|
`b135a9c` в отдельном worktree ловил как раз такой сид. Одинокие родители — сознательное
|
||||||
|
решение дизайна, поэтому неправ был тест: теперь он ищет ученика, у которого есть мать, а
|
||||||
|
количество родителей проверяет диапазоном. Проверено тем же способом, каким ловилось: полный
|
||||||
|
прогон на чистом `b135a9c` трижды, 60/60.
|
||||||
|
- **`PeopleApiTests.List_SortsBySurname` мерил не тем.** Тест требовал ordinal-порядка, а сервер
|
||||||
|
ставит Ё на место Е (`RosterBrowser.NameOrder`, тест `SurnameSort_FollowsTheAlphabetNotCodePoints`).
|
||||||
|
Совпадали они только потому, что в `core` нет фамилий на Ё — мод с такой фамилией уронил бы
|
||||||
|
тест, а не код. Теперь тест складывает Ё в Е так же, как сервер.
|
||||||
|
|
||||||
|
Дописано:
|
||||||
|
|
||||||
|
- `PeopleApiTests.ReloadAfterTheYearlyIntake_RestoresTheNewComposition` — критерии фаз 7 и 9
|
||||||
|
«перезапуск поднимает тех же людей» и «перезапуск после перехода поднимает **новый** состав» не
|
||||||
|
проверялись сквозь сервер. Просто сравнить людей до и после reload недостаточно: генерация
|
||||||
|
детерминирована от id школы, поэтому забывший файл сервер собрал бы тот же ростер и тест бы
|
||||||
|
ничего не поймал. Тест поэтому ждёт 1 сентября (школа создаётся за 15 игровых минут до
|
||||||
|
полуночи, это около трёх секунд реального времени), а после reload требует состав **после**
|
||||||
|
набора и явно отличный от исходного.
|
||||||
|
|
||||||
|
Открыто:
|
||||||
|
|
||||||
|
- **Раскладка оболочки менеджера разошлась с дизайном** — то же расхождение, что записано в срезе
|
||||||
|
1. Фаза 8 обещает панель «Люди» нижним рядом во всю ширину, `design/people.md` фиксирует
|
||||||
|
«панель в оболочке менеджера, нижний ряд; не отдельный экран», а на экране она вкладкой рядом с
|
||||||
|
картой. Тот же коммит `d8f8db6`, что убрал панель событий. Решается одним решением на оба
|
||||||
|
пункта: либо вернуть нижний ряд, либо переписать обе строки дизайна.
|
||||||
|
|
||||||
|
Замечено рядом (за границами среза):
|
||||||
|
|
||||||
|
- `design/people.md` и задача фазы 5 говорят «скорость убывания нужд в `core` — ноль», а в
|
||||||
|
`defs/needs` теперь `decayPerHour` от 0.05 до 0.15. Это сознательная работа фазы 20, не
|
||||||
|
регрессия; строка дизайна просто устарела и относится к срезу 5.
|
||||||
|
- Срез 1 успел сдвинуться: коммит `5cd5a6d` тронул `gameScreen.ts` и `strings.ts` (подпись
|
||||||
|
идущего человека). По правилу журнала он больше не «проверен на `b135a9c`» — правка маленькая и
|
||||||
|
относится к срезу 5, но следующий проход это увидит.
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ public class PeopleApiTests(AppHostFixture fixture)
|
|||||||
|
|
||||||
var page = await GetPeopleAsync(client, school.Id, "sort=surname&pageSize=20");
|
var page = await GetPeopleAsync(client, school.Id, "sort=surname&pageSize=20");
|
||||||
var surnames = page.People.Select(person => person.Surname).ToArray();
|
var surnames = page.People.Select(person => person.Surname).ToArray();
|
||||||
Assert.Equal(surnames.OrderBy(name => name, StringComparer.Ordinal), surnames);
|
|
||||||
|
// The server alphabetises Ё with Е, so plain ordinal order is the wrong yardstick: it only
|
||||||
|
// agrees because no vanilla surname starts with Ё. Fold the same way the server does.
|
||||||
|
Assert.Equal(surnames.OrderBy(Alphabetised, StringComparer.Ordinal), surnames);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -139,14 +142,29 @@ public class PeopleApiTests(AppHostFixture fixture)
|
|||||||
var school = await SchoolApiTests.CreateAsync(client, "Люди семья", Start);
|
var school = await SchoolApiTests.CreateAsync(client, "Люди семья", Start);
|
||||||
|
|
||||||
var page = await GetPeopleAsync(client, school.Id, "role=student&year=5&pageSize=10");
|
var page = await GetPeopleAsync(client, school.Id, "role=student&year=5&pageSize=10");
|
||||||
var pupil = page.People[0];
|
|
||||||
|
|
||||||
var card = await client.GetFromJsonAsync<PersonCardResponse>(
|
// Every fifth-to-tenth family is single-parent on purpose (design/people.md), and the
|
||||||
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(pupil.Id)}?lang=ru",
|
// roster seed is the school id — which depends on how many schools ran before this test.
|
||||||
|
// Demanding two parents from whoever happens to sort first therefore fails on some runs;
|
||||||
|
// look for a pupil who has a mother instead of assuming the first one does.
|
||||||
|
PersonCardResponse? card = null;
|
||||||
|
foreach (var candidate in page.People)
|
||||||
|
{
|
||||||
|
var candidateCard = await client.GetFromJsonAsync<PersonCardResponse>(
|
||||||
|
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(candidate.Id)}?lang=ru",
|
||||||
TestContext.Current.CancellationToken);
|
TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(candidateCard);
|
||||||
|
Assert.Equal(candidate.Id, candidateCard.Id);
|
||||||
|
Assert.InRange(candidateCard.Family.Parents.Count, 1, 2);
|
||||||
|
|
||||||
|
if (candidateCard.Family.Parents.Any(parent => parent.Female))
|
||||||
|
{
|
||||||
|
card = candidateCard;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Assert.NotNull(card);
|
Assert.NotNull(card);
|
||||||
Assert.Equal(pupil.Id, card.Id);
|
|
||||||
Assert.Equal(2, card.Family.Parents.Count);
|
|
||||||
Assert.Contains(card.Needs, need => need.Id == "Sleep" && need.Value == 1f);
|
Assert.Contains(card.Needs, need => need.Id == "Sleep" && need.Value == 1f);
|
||||||
Assert.NotEmpty(card.Body);
|
Assert.NotEmpty(card.Body);
|
||||||
Assert.NotEmpty(card.Skills);
|
Assert.NotEmpty(card.Skills);
|
||||||
@@ -156,9 +174,94 @@ public class PeopleApiTests(AppHostFixture fixture)
|
|||||||
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(mother.Id)}?lang=ru",
|
$"/api/schools/{school.Id}/people/{Uri.EscapeDataString(mother.Id)}?lang=ru",
|
||||||
TestContext.Current.CancellationToken);
|
TestContext.Current.CancellationToken);
|
||||||
Assert.NotNull(motherCard);
|
Assert.NotNull(motherCard);
|
||||||
Assert.Contains(motherCard.Family.Children, child => child.Id == pupil.Id);
|
Assert.Contains(motherCard.Family.Children, child => child.Id == card.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phases 7 and 9 together: the roster survives a restart, and what comes back is the
|
||||||
|
/// composition *after* the first-September intake. Generating from the seed again would
|
||||||
|
/// rebuild the pre-intake roster, so comparing against it is what makes this test bite.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task ReloadAfterTheYearlyIntake_RestoresTheNewComposition()
|
||||||
|
{
|
||||||
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
|
await SchoolApiTests.ResetAsync(client);
|
||||||
|
|
||||||
|
// Fifteen game minutes short of 1 September: the intake fires at midnight, and five game
|
||||||
|
// minutes per real second gets the clock there in about three seconds.
|
||||||
|
var school = await SchoolApiTests.CreateAsync(
|
||||||
|
client,
|
||||||
|
"Набор и рестарт",
|
||||||
|
new DateTime(2012, 8, 31, 23, 45, 0, DateTimeKind.Utc));
|
||||||
|
|
||||||
|
var before = await GetPeopleAsync(client, school.Id, Everyone);
|
||||||
|
var beforeIds = before.People.Select(person => person.Id).ToArray();
|
||||||
|
Assert.NotEmpty(beforeIds);
|
||||||
|
|
||||||
|
var after = await WaitForIntakeAsync(client, school.Id, beforeIds);
|
||||||
|
var afterIds = after.People.Select(person => person.Id).ToArray();
|
||||||
|
|
||||||
|
var pupil = after.People.First(person => person.Roles.Contains("student"));
|
||||||
|
var cardBefore = await GetCardAsync(client, school.Id, pupil.Id);
|
||||||
|
|
||||||
|
using var reload = await client.PostAsync("/api/dev/reload-schools", content: null, TestContext.Current.CancellationToken);
|
||||||
|
reload.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
var restored = await GetPeopleAsync(client, school.Id, Everyone);
|
||||||
|
Assert.Equal(after.Total, restored.Total);
|
||||||
|
Assert.Equal(
|
||||||
|
after.People.Select(person => (person.Id, person.FullName, person.ClassYear)),
|
||||||
|
restored.People.Select(person => (person.Id, person.FullName, person.ClassYear)));
|
||||||
|
Assert.NotEqual(beforeIds, restored.People.Select(person => person.Id).ToArray());
|
||||||
|
|
||||||
|
var cardAfter = await GetCardAsync(client, school.Id, pupil.Id);
|
||||||
|
Assert.Equal(cardBefore.BirthDate, cardAfter.BirthDate);
|
||||||
|
Assert.Equal(
|
||||||
|
cardBefore.Body.Select(stat => (stat.Id, stat.Value)),
|
||||||
|
cardAfter.Body.Select(stat => (stat.Id, stat.Value)));
|
||||||
|
Assert.Equal(
|
||||||
|
cardBefore.Skills.Select(stat => (stat.Id, stat.Value)),
|
||||||
|
cardAfter.Skills.Select(stat => (stat.Id, stat.Value)));
|
||||||
|
Assert.Equal(
|
||||||
|
cardBefore.Traits.Select(trait => trait.DefName),
|
||||||
|
cardAfter.Traits.Select(trait => trait.DefName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private const string Everyone = "sort=surname&pageSize=100";
|
||||||
|
|
||||||
|
/// <summary>Polls the published roster until the intake has replaced the graduating year.</summary>
|
||||||
|
private static async Task<PeopleListResponse> WaitForIntakeAsync(
|
||||||
|
HttpClient client,
|
||||||
|
int schoolId,
|
||||||
|
IReadOnlyList<string> beforeIds)
|
||||||
|
{
|
||||||
|
for (var attempt = 0; attempt < 60; attempt++)
|
||||||
|
{
|
||||||
|
var page = await GetPeopleAsync(client, schoolId, Everyone);
|
||||||
|
if (!page.People.Select(person => person.Id).SequenceEqual(beforeIds))
|
||||||
|
{
|
||||||
|
return page;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(250, TestContext.Current.CancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new InvalidOperationException("The yearly intake never happened.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<PersonCardResponse> GetCardAsync(HttpClient client, int schoolId, string personId)
|
||||||
|
{
|
||||||
|
var card = await client.GetFromJsonAsync<PersonCardResponse>(
|
||||||
|
$"/api/schools/{schoolId}/people/{Uri.EscapeDataString(personId)}?lang=ru",
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
Assert.NotNull(card);
|
||||||
|
return card;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Mirrors the server's name order: Ё sorts as Е, everything else stays ordinal.</summary>
|
||||||
|
private static string Alphabetised(string name) => name.Replace('Ё', 'Е').Replace('ё', 'е');
|
||||||
|
|
||||||
private static async Task<PeopleListResponse> GetPeopleAsync(HttpClient client, int schoolId, string query)
|
private static async Task<PeopleListResponse> GetPeopleAsync(HttpClient client, int schoolId, string query)
|
||||||
{
|
{
|
||||||
var page = await client.GetFromJsonAsync<PeopleListResponse>(
|
var page = await client.GetFromJsonAsync<PeopleListResponse>(
|
||||||
|
|||||||
@@ -197,12 +197,75 @@ public class StaffingApiTests(AppHostFixture fixture)
|
|||||||
|
|
||||||
var restored = await GetStaffingAsync(client, school.Id);
|
var restored = await GetStaffingAsync(client, school.Id);
|
||||||
Assert.Equal(afterDrop.Payroll, restored.Payroll);
|
Assert.Equal(afterDrop.Payroll, restored.Payroll);
|
||||||
|
|
||||||
|
// Phase 11: the pool lives in the same file and has to come back with it, asks included.
|
||||||
|
Assert.Equal(
|
||||||
|
afterDrop.Applicants.Select(applicant => (applicant.Id, applicant.HourlyWageAsk)),
|
||||||
|
restored.Applicants.Select(applicant => (applicant.Id, applicant.HourlyWageAsk)));
|
||||||
Assert.Equal(teacher.Id, restored.Staff.Single().Id);
|
Assert.Equal(teacher.Id, restored.Staff.Single().Id);
|
||||||
Assert.Equal(
|
Assert.Equal(
|
||||||
afterDrop.Staff.Single().Subjects.Select(subject => subject.DefName),
|
afterDrop.Staff.Single().Subjects.Select(subject => subject.DefName),
|
||||||
restored.Staff.Single().Subjects.Select(subject => subject.DefName));
|
restored.Staff.Single().Subjects.Select(subject => subject.DefName));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Phase 12 wants the cap refused at the moment of the action, with the numbers in the answer.
|
||||||
|
/// Reaching it means loading teachers up, not hiring more: a subject splits its hours between
|
||||||
|
/// everyone who teaches it, so the payroll peaks while few teachers carry many hours and falls
|
||||||
|
/// again once the load is spread. Measured over thirty seeds, this sweep always crosses.
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public async Task AssigningPastTheCap_IsRejectedWithTheNumbers()
|
||||||
|
{
|
||||||
|
using var client = fixture.App.CreateHttpClient("server");
|
||||||
|
await SchoolApiTests.ResetAsync(client);
|
||||||
|
var school = await SchoolApiTests.CreateAsync(client, "Штат перебор", Start);
|
||||||
|
|
||||||
|
var staffing = await GetStaffingAsync(client, school.Id);
|
||||||
|
while (staffing.Applicants.Count > 0)
|
||||||
|
{
|
||||||
|
staffing = await HireAsync(client, school.Id, staffing.Applicants[0].Id, "Teacher");
|
||||||
|
}
|
||||||
|
|
||||||
|
var subjects = staffing.Subjects.Select(subject => subject.DefName).ToArray();
|
||||||
|
Assert.NotEmpty(subjects);
|
||||||
|
|
||||||
|
foreach (var member in staffing.Staff)
|
||||||
|
{
|
||||||
|
foreach (var subject in subjects)
|
||||||
|
{
|
||||||
|
using var response = await client.PostAsJsonAsync(
|
||||||
|
$"/api/schools/{school.Id}/staff/{Uri.EscapeDataString(member.Id)}/subjects",
|
||||||
|
new { subject },
|
||||||
|
TestContext.Current.CancellationToken);
|
||||||
|
|
||||||
|
if (response.StatusCode != HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var problem = await response.Content.ReadFromJsonAsync<ProblemResponse>(TestContext.Current.CancellationToken);
|
||||||
|
Assert.Equal("payroll-exceeded", problem?.Code);
|
||||||
|
Assert.Equal(100_000f, problem?.Allocated);
|
||||||
|
Assert.True(problem?.Attempted > problem?.Allocated);
|
||||||
|
Assert.True(problem?.Payroll <= problem?.Allocated);
|
||||||
|
// Float arithmetic: the server computes remaining once, so compare within a cent.
|
||||||
|
Assert.Equal(problem!.Allocated!.Value - problem.Payroll!.Value, problem.Remaining!.Value, 0.01f);
|
||||||
|
|
||||||
|
// Refused means refused: the assignment must not have landed anyway.
|
||||||
|
var after = await GetStaffingAsync(client, school.Id);
|
||||||
|
Assert.Equal(problem.Payroll!.Value, after.Payroll, 0.01f);
|
||||||
|
Assert.DoesNotContain(
|
||||||
|
after.Staff.Single(person => person.Id == member.Id).Subjects,
|
||||||
|
assigned => assigned.DefName == subject);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.Fail("Loading every teacher with every subject never reached the payroll cap.");
|
||||||
|
}
|
||||||
|
|
||||||
private static async Task<StaffingResponse> GetStaffingAsync(HttpClient client, int schoolId)
|
private static async Task<StaffingResponse> GetStaffingAsync(HttpClient client, int schoolId)
|
||||||
{
|
{
|
||||||
var staffing = await client.GetFromJsonAsync<StaffingResponse>(
|
var staffing = await client.GetFromJsonAsync<StaffingResponse>(
|
||||||
|
|||||||
@@ -66,6 +66,26 @@ public class ApplicantPoolTests
|
|||||||
Assert.True(ApplicantPool.HourlyAsk(catalog, strong) > ApplicantPool.HourlyAsk(catalog, weak));
|
Assert.True(ApplicantPool.HourlyAsk(catalog, strong) > ApplicantPool.HourlyAsk(catalog, weak));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// design/staffing.md prices an hour from skills *and* traits — the self-assured ask for more.
|
||||||
|
/// Skills are covered above; this pins the trait half, which core sets on Leader (+10),
|
||||||
|
/// HotTempered (+6) and Quiet (-8).
|
||||||
|
/// </summary>
|
||||||
|
[Fact]
|
||||||
|
public void ConfidentTraits_AskForMoreAndQuietOnesForLess()
|
||||||
|
{
|
||||||
|
var catalog = Fixtures.Catalog();
|
||||||
|
var person = Fixtures.Generate(Fixtures.Classrooms(1)).People.First(candidate => candidate.IsParent);
|
||||||
|
var plain = person with { Traits = [] };
|
||||||
|
var leader = person with { Traits = ["Leader"] };
|
||||||
|
var quiet = person with { Traits = ["Quiet"] };
|
||||||
|
|
||||||
|
var plainAsk = ApplicantPool.HourlyAsk(catalog, plain);
|
||||||
|
|
||||||
|
Assert.Equal(plainAsk + 10f, ApplicantPool.HourlyAsk(catalog, leader), 0.01f);
|
||||||
|
Assert.Equal(plainAsk - 8f, ApplicantPool.HourlyAsk(catalog, quiet), 0.01f);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void GeneratedApplicants_AreNotOnTheRoster_ParentsKeepTheirId()
|
public void GeneratedApplicants_AreNotOnTheRoster_ParentsKeepTheirId()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user