142 lines
5.6 KiB
TypeScript
142 lines
5.6 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import { DECK_IDS } from "../../lib/create/deckTypes";
|
|
import type { DeckId } from "../../lib/create/deckTypes";
|
|
import { deckData, recommendDeck } from "../../lib/create/deckRecommend";
|
|
import { variantsOf } from "../../lib/create/reviewProfiles";
|
|
import type { ReviewProfile } from "../../lib/create/reviewProfiles";
|
|
import { methodSlugFromTitle } from "../../lib/create/methodSlugFromTitle";
|
|
import { resolveMethodId } from "../../lib/create/idAliases";
|
|
import useCasesCompletedRules from "../../messages/en/pages/useCasesCompletedRules.json";
|
|
|
|
const ROOT = path.resolve(__dirname, "..", "..");
|
|
|
|
function readJson<T>(rel: string): T {
|
|
return JSON.parse(readFileSync(path.join(ROOT, rel), "utf8")) as T;
|
|
}
|
|
|
|
const profiles = readJson<ReviewProfile[]>("data/create/profiles/real-group-profiles.json");
|
|
const founders = readJson<ReviewProfile[]>("data/create/profiles/one-member-founders.json");
|
|
|
|
function picksFor(set: ReviewProfile[]) {
|
|
return set.flatMap(variantsOf).map((variant) => ({
|
|
profile: variant.profile,
|
|
variant: variant.variant,
|
|
decks: Object.fromEntries(
|
|
DECK_IDS.map((deck) => {
|
|
const result = recommendDeck(deckData(deck), variant.answers);
|
|
return [
|
|
deck,
|
|
{
|
|
shown: result.shown.map((card) => card.id),
|
|
waiting: result.waiting.map((card) => card.id),
|
|
},
|
|
];
|
|
}),
|
|
),
|
|
}));
|
|
}
|
|
|
|
function rowOrderSentence(set: ReviewProfile[]): string {
|
|
const base = set.flatMap(variantsOf).filter((variant) => variant.variant === "base");
|
|
const totals = { only: 0, score: 0, otherKinds: 0, rowOrder: 0 };
|
|
for (const deck of DECK_IDS) {
|
|
const jobs = deckData(deck).engine.jobs.filter((job) => job.displayTier !== "none");
|
|
for (const job of jobs) {
|
|
for (const variant of base) {
|
|
const winner = recommendDeck(deckData(deck), variant.answers).recommended.find(
|
|
(card) => card.job === job.id && card.tier !== "answers",
|
|
);
|
|
if (winner?.decidedBy) totals[winner.decidedBy] += 1;
|
|
}
|
|
}
|
|
}
|
|
const picks = totals.only + totals.score + totals.otherKinds + totals.rowOrder;
|
|
return `Of ${picks} picks for the 92 groups, row order settled ${totals.rowOrder}, other kinds ${totals.otherKinds} and fit ${totals.score}, and ${totals.only} had one eligible card.`;
|
|
}
|
|
|
|
const EXAMPLE_DECK_BY_CATEGORY: Record<string, DeckId> = {
|
|
Membership: "membership",
|
|
"Decision-Making": "decisionApproaches",
|
|
"Decision-making": "decisionApproaches",
|
|
"Conflict Management": "conflictManagement",
|
|
Communication: "communication",
|
|
};
|
|
|
|
function chosenCards(documentKey: string): { total: number; ids: string[] } {
|
|
const doc = (
|
|
useCasesCompletedRules as Record<
|
|
string,
|
|
{ document: { sections: { categoryName: string; entries: { title: string }[] }[] } }
|
|
>
|
|
)[documentKey];
|
|
const live = new Map(
|
|
DECK_IDS.map((deck) => [deck, new Set(Object.keys(deckData(deck).engine.cards))]),
|
|
);
|
|
let total = 0;
|
|
const ids: string[] = [];
|
|
for (const section of doc.document.sections) {
|
|
const deck = EXAMPLE_DECK_BY_CATEGORY[section.categoryName];
|
|
if (!deck) continue;
|
|
for (const entry of section.entries) {
|
|
total += 1;
|
|
const slug = methodSlugFromTitle(entry.title);
|
|
const resolved = resolveMethodId(deck, slug);
|
|
if (resolved && live.get(resolved.deck)?.has(resolved.id)) ids.push(resolved.id);
|
|
}
|
|
}
|
|
return { total, ids };
|
|
}
|
|
|
|
describe("real-group profiles", () => {
|
|
it("matches the reviewed 2 Nov package run", () => {
|
|
const expected = readJson<unknown[]>("tests/fixtures/profiles-picks.json");
|
|
expect(picksFor([...profiles, ...founders])).toEqual(expected);
|
|
});
|
|
|
|
it("reports how often row order settled a pick", () => {
|
|
const summary = readFileSync(
|
|
path.join(ROOT, "tests/fixtures/profiles-summary.md"),
|
|
"utf8",
|
|
);
|
|
expect(summary).toContain(rowOrderSentence(profiles));
|
|
expect(profiles).toHaveLength(92);
|
|
expect(founders.length).toBeGreaterThan(0);
|
|
expect(JSON.stringify(profiles)).not.toMatch(/[A-Z][a-z]+ [A-Z][a-z]+/);
|
|
expect(JSON.stringify(founders)).not.toMatch(/[A-Z][a-z]+ [A-Z][a-z]+/);
|
|
});
|
|
|
|
it("counts how many example-rule cards the engine recommends", () => {
|
|
const base = profiles.flatMap(variantsOf).filter((variant) => variant.variant === "base");
|
|
const report: Record<string, { chosen: number; resolved: number; recommendedOnMost: number }> = {};
|
|
for (const key of ["mutualAidColorado", "foodNotBombs", "boulderCountyStreetMedics"] as const) {
|
|
const chosen = chosenCards(key);
|
|
let best = 0;
|
|
for (const variant of base) {
|
|
const recommended = new Set<string>();
|
|
for (const deck of DECK_IDS) {
|
|
for (const card of recommendDeck(deckData(deck), variant.answers).recommended) {
|
|
recommended.add(card.id);
|
|
}
|
|
}
|
|
let hit = 0;
|
|
for (const id of chosen.ids) if (recommended.has(id)) hit += 1;
|
|
if (hit > best) best = hit;
|
|
}
|
|
report[key] = {
|
|
chosen: chosen.total,
|
|
resolved: chosen.ids.length,
|
|
recommendedOnMost: best,
|
|
};
|
|
}
|
|
// The team's own Rule is not in the repo, so this locks the three example
|
|
// Rules. A change that moves a recommendation changes recommendedOnMost.
|
|
expect(report).toEqual({
|
|
mutualAidColorado: { chosen: 7, resolved: 6, recommendedOnMost: 4 },
|
|
foodNotBombs: { chosen: 8, resolved: 8, recommendedOnMost: 5 },
|
|
boulderCountyStreetMedics: { chosen: 9, resolved: 8, recommendedOnMost: 5 },
|
|
});
|
|
});
|
|
});
|