Import the remade decks on stable ids and recommend one card per job, so a saved draft still loads after a chip is renamed.
The real-group profiles stay as a test, so a change that moves a pick fails. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
96ca15d8e3
commit
bcbc6f30d9
89 files changed
+36555
-2828
No files matched your search
@@ -105,9 +105,7 @@ describe("RuleStack Component", () => {
|
||||
expect(GOVERNANCE_TEMPLATE_CATALOG.length).toBeGreaterThan(
|
||||
homeFeatured.length,
|
||||
);
|
||||
expect(
|
||||
screen.queryByText("Solidarity Network"),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByText("Quadratic Governance")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test("renders with custom className", async () => {
|
||||
@@ -139,7 +137,7 @@ describe("RuleStack Component", () => {
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
/Voting cost is squared \(V²\), preventing a majority from steamrolling a passionate minority\./,
|
||||
/Power is held by autonomous "cells\." A central hub acts as a switchboard for resources but cannot dictate cell activities\./,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -7,14 +7,12 @@ import { methodSectionsPinsForHydratedSelections } from "../../lib/create/publis
|
||||
import coreValuesMessages from "../../messages/en/create/customRule/coreValues.json";
|
||||
|
||||
function coreValuePresetId(label: string): string {
|
||||
const values = coreValuesMessages.values as Array<
|
||||
string | { label: string }
|
||||
>;
|
||||
const idx = values.findIndex((v) => {
|
||||
const l = typeof v === "string" ? v : v.label;
|
||||
return l.toLowerCase() === label.toLowerCase();
|
||||
});
|
||||
return String(idx + 1);
|
||||
const values = coreValuesMessages.values as Array<{ id: string; label: string }>;
|
||||
const row = values.find(
|
||||
(value) => value.label.toLowerCase() === label.toLowerCase(),
|
||||
);
|
||||
if (!row) throw new Error(`No core value named ${label}`);
|
||||
return row.id;
|
||||
}
|
||||
|
||||
describe("buildTemplateCustomizePrefill", () => {
|
||||
@@ -76,7 +74,7 @@ describe("buildTemplateCustomizePrefill", () => {
|
||||
const snapshot = prefill.coreValuesChipsSnapshot ?? [];
|
||||
const selectedRows = snapshot.filter((r) => r.state === "selected");
|
||||
expect(selectedRows.map((r) => r.label).sort()).toEqual([
|
||||
"Community Care",
|
||||
"Community care",
|
||||
"Consensus",
|
||||
]);
|
||||
// Unmatched presets should still appear, as unselected, so the screen
|
||||
|
||||
@@ -20,19 +20,19 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
// Same shape `buildTemplateCustomizePrefill` emits via methodSlugFromTitle.
|
||||
const state: CreateFlowState = {
|
||||
selectedCommunicationMethodIds: ["signal", "in-person-meetings"],
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedMembershipMethodIds: ["open-admission"],
|
||||
selectedDecisionApproachIds: ["lazy-consensus"],
|
||||
selectedConflictManagementIds: ["peer-mediation"],
|
||||
selectedConflictManagementIds: ["conflict-ladder"],
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
const byName = new Map(rows.map((r) => [r.name, r.chips]));
|
||||
expect(byName.get("Communication")).toEqual([
|
||||
"Signal",
|
||||
"In-Person Meetings",
|
||||
"In-person meetings",
|
||||
]);
|
||||
expect(byName.get("Membership")).toEqual(["Open Access"]);
|
||||
expect(byName.get("Decision-making")).toEqual(["Lazy Consensus"]);
|
||||
expect(byName.get("Conflict management")).toEqual(["Peer Mediation"]);
|
||||
expect(byName.get("Membership")).toEqual(["Open admission"]);
|
||||
expect(byName.get("Decision-making")).toEqual(["Lazy consensus"]);
|
||||
expect(byName.get("Conflict management")).toEqual(["Conflict ladder"]);
|
||||
expect(byName.has("Values")).toBe(false);
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ describe("buildFinalReviewCategoriesFromState", () => {
|
||||
};
|
||||
const rows = buildFinalReviewCategoriesFromState(state, NAMES);
|
||||
expect(rows).toEqual([
|
||||
{ name: "Communication", chips: ["In-Person Meetings"] },
|
||||
{ name: "Communication", chips: ["In-person meetings"] },
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@@ -41,6 +41,7 @@ describe("buildPublishPayload", () => {
|
||||
},
|
||||
],
|
||||
coreValues: [],
|
||||
supermajorityShare: 75,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,6 +62,7 @@ describe("buildPublishPayload", () => {
|
||||
},
|
||||
],
|
||||
coreValues: [],
|
||||
supermajorityShare: 75,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -85,7 +87,7 @@ describe("buildPublishPayload", () => {
|
||||
const r = buildPublishPayload({ title: "T", sections });
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
expect(r.document).toEqual({ sections, coreValues: [] });
|
||||
expect(r.document).toEqual({ sections, coreValues: [], supermajorityShare: 75 });
|
||||
});
|
||||
|
||||
it("filters invalid section entries from state.sections", () => {
|
||||
@@ -101,6 +103,7 @@ describe("buildPublishPayload", () => {
|
||||
expect(r.document).toEqual({
|
||||
sections: [{ categoryName: "Values", entries: [{ title: "A", body: "B" }] }],
|
||||
coreValues: [],
|
||||
supermajorityShare: 75,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -331,9 +334,9 @@ describe("buildPublishPayload — methodSelections", () => {
|
||||
const r = buildPublishPayload({
|
||||
title: "T",
|
||||
selectedCommunicationMethodIds: ["signal"],
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedMembershipMethodIds: ["open-admission"],
|
||||
selectedDecisionApproachIds: ["lazy-consensus"],
|
||||
selectedConflictManagementIds: ["peer-mediation"],
|
||||
selectedConflictManagementIds: ["conflict-ladder"],
|
||||
});
|
||||
expect(r.ok).toBe(true);
|
||||
if (!r.ok) return;
|
||||
|
||||
@@ -4,9 +4,9 @@ import type { CommunityStructureChipSnapshotRow } from "../../app/(app)/create/t
|
||||
import { buildCoreValueChipOptionsFromDraft } from "../../lib/create/coreValueChipOptionsFromDraft";
|
||||
|
||||
const PRESETS = [
|
||||
{ label: "A", meaning: "am", signals: "as" },
|
||||
{ label: "B", meaning: "bm", signals: "bs" },
|
||||
{ label: "C", meaning: "cm", signals: "cs" },
|
||||
{ id: "1", label: "A", meaning: "am", signals: "as" },
|
||||
{ id: "2", label: "B", meaning: "bm", signals: "bs" },
|
||||
{ id: "3", label: "C", meaning: "cm", signals: "cs" },
|
||||
] as const;
|
||||
|
||||
describe("buildCoreValueChipOptionsFromDraft", () => {
|
||||
|
||||
@@ -100,27 +100,26 @@ describe("GET /api/create-flow/methods", () => {
|
||||
};
|
||||
expect(json.section).toBe("communication");
|
||||
expect(json.methods.length).toBe(communicationCatalog.length);
|
||||
expect(json.methods[0].slug).toBe("loomio");
|
||||
expect(json.methods[0].matches.score).toBe(2);
|
||||
expect(json.methods[1].slug).toBe("in-person-meetings");
|
||||
expect(json.methods[1].matches.score).toBe(1);
|
||||
expect(json.methods[0].matches.score).toBeGreaterThan(json.methods[1].matches.score);
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
expect(json.methods[0].label).toBeTruthy();
|
||||
});
|
||||
|
||||
it("returns full catalog without matches when the DB query throws", async () => {
|
||||
it("ranks from the deck files when the database is down", async () => {
|
||||
findManyMock.mockRejectedValueOnce(new Error("db down"));
|
||||
const res = await GET(
|
||||
makeReq(
|
||||
"https://x.test/api/create-flow/methods?section=communication&facet.size=oneMember",
|
||||
"https://x.test/api/create-flow/methods?section=communication&facet.size=oneMember&facet.orgType=workersCoop",
|
||||
),
|
||||
undefined,
|
||||
);
|
||||
expect(res.status).toBe(200);
|
||||
const json = (await res.json()) as {
|
||||
methods: Array<{ slug: string; matches?: unknown }>;
|
||||
methods: Array<{ slug: string; matches?: { score: number } }>;
|
||||
};
|
||||
expect(json.methods.length).toBe(communicationCatalog.length);
|
||||
expect(json.methods[0].matches).toBeUndefined();
|
||||
expect(json.methods[0].matches?.score).toBeGreaterThan(0);
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns all core values for section=coreValues", async () => {
|
||||
@@ -137,7 +136,7 @@ describe("GET /api/create-flow/methods", () => {
|
||||
};
|
||||
expect(json.section).toBe("coreValues");
|
||||
expect(json.methods.length).toBeGreaterThan(50);
|
||||
expect(json.methods[0].id).toBe("1");
|
||||
expect(json.methods[0].id).toBe("accessibility");
|
||||
expect(json.methods[0].label).toBeTruthy();
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -23,39 +23,42 @@ const emptyEntry = (): DecisionApproachDetailEntry => ({
|
||||
});
|
||||
|
||||
describe("decisionApproachKeyResources", () => {
|
||||
it("reads the four key-resource items from messages", () => {
|
||||
it("reads the decision checklist from messages", () => {
|
||||
expect(decisionApproachKeyResourceItems()).toEqual([
|
||||
{ id: "amend", label: "Amend your CommunityRule" },
|
||||
{ id: "finances", label: "Steward finances" },
|
||||
{ id: "project", label: "Project level decisions" },
|
||||
{ id: "discipline", label: "Discipline and member termination" },
|
||||
{ id: "everyday", label: "Everyday decisions" },
|
||||
{ id: "big", label: "Big decisions" },
|
||||
{ id: "amend", label: "Changing this Rule" },
|
||||
{ id: "emergency", label: "Emergencies" },
|
||||
{ id: "finances", label: "Shared money" },
|
||||
{ id: "spending", label: "Spending" },
|
||||
{ id: "roles", label: "Roles and working groups" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("selects checked key-resource labels without rewriting applicableScope", () => {
|
||||
const next = applyDecisionApproachKeyResources(emptyEntry(), [
|
||||
"Steward finances",
|
||||
"Shared money",
|
||||
]);
|
||||
expect(next.applicableScope).toEqual(["Daily Operations"]);
|
||||
expect(next.selectedApplicableScope).toEqual(["Steward finances"]);
|
||||
expect(next.selectedApplicableScope).toEqual(["Shared money"]);
|
||||
});
|
||||
|
||||
it("keeps non-key chip selections when syncing sidebar checks", () => {
|
||||
const next = applyDecisionApproachKeyResources(
|
||||
{
|
||||
...emptyEntry(),
|
||||
applicableScope: ["Steward finances", "Daily Operations"],
|
||||
applicableScope: ["Shared money", "Daily Operations"],
|
||||
selectedApplicableScope: ["Daily Operations"],
|
||||
},
|
||||
["Steward finances"],
|
||||
["Shared money"],
|
||||
);
|
||||
expect(next.applicableScope).toEqual([
|
||||
"Steward finances",
|
||||
"Shared money",
|
||||
"Daily Operations",
|
||||
]);
|
||||
expect(next.selectedApplicableScope).toEqual([
|
||||
"Daily Operations",
|
||||
"Steward finances",
|
||||
"Shared money",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -63,19 +66,19 @@ describe("decisionApproachKeyResources", () => {
|
||||
expect(
|
||||
decisionApproachScopeForPublish(
|
||||
["Daily Operations", "Minor Expenditures"],
|
||||
["Steward finances"],
|
||||
["Shared money"],
|
||||
),
|
||||
).toEqual([
|
||||
"Daily Operations",
|
||||
"Minor Expenditures",
|
||||
"Steward finances",
|
||||
"Shared money",
|
||||
]);
|
||||
expect(
|
||||
decisionApproachScopeForPublish(
|
||||
["Daily Operations", "Minor Expenditures"],
|
||||
["Daily Operations", "Steward finances"],
|
||||
["Daily Operations", "Shared money"],
|
||||
),
|
||||
).toEqual(["Daily Operations", "Steward finances"]);
|
||||
).toEqual(["Daily Operations", "Shared money"]);
|
||||
expect(
|
||||
decisionApproachScopeForPublish(
|
||||
["Daily Operations", "Minor Expenditures"],
|
||||
@@ -85,44 +88,44 @@ describe("decisionApproachKeyResources", () => {
|
||||
});
|
||||
|
||||
it("maps checkbox ids to labels and back", () => {
|
||||
expect(selectedKeyResourceLabelsFromCheckedIds(["amend", "project"])).toEqual(
|
||||
["Amend your CommunityRule", "Project level decisions"],
|
||||
expect(selectedKeyResourceLabelsFromCheckedIds(["amend", "big"])).toEqual(
|
||||
["Big decisions", "Changing this Rule"],
|
||||
);
|
||||
expect(
|
||||
decisionApproachKeyResourceIdsFromLabels([
|
||||
"Project level decisions",
|
||||
"Big decisions",
|
||||
"Unknown",
|
||||
"Amend your CommunityRule",
|
||||
"Changing this Rule",
|
||||
]),
|
||||
).toEqual(["amend", "project"]);
|
||||
).toEqual(["big", "amend"]);
|
||||
});
|
||||
|
||||
it("derives sidebar checks from the open draft or selected approaches", () => {
|
||||
const detailsById = {
|
||||
"lazy-consensus": {
|
||||
...emptyEntry(),
|
||||
selectedApplicableScope: ["Steward finances"],
|
||||
selectedApplicableScope: ["Shared money"],
|
||||
},
|
||||
"do-ocracy": {
|
||||
...emptyEntry(),
|
||||
selectedApplicableScope: ["Project level decisions"],
|
||||
selectedApplicableScope: ["Big decisions"],
|
||||
},
|
||||
};
|
||||
expect(
|
||||
decisionApproachKeyResourceCheckboxIds({
|
||||
detailsById,
|
||||
selectedApproachIds: ["lazy-consensus", "do-ocracy"],
|
||||
reminderIds: ["discipline"],
|
||||
reminderIds: ["spending"],
|
||||
}),
|
||||
).toEqual(["finances", "project", "discipline"]);
|
||||
).toEqual(["big", "finances", "spending"]);
|
||||
expect(
|
||||
decisionApproachKeyResourceCheckboxIds({
|
||||
detailsById,
|
||||
selectedApproachIds: ["lazy-consensus", "do-ocracy"],
|
||||
reminderIds: ["discipline"],
|
||||
reminderIds: ["spending"],
|
||||
openDraft: {
|
||||
...emptyEntry(),
|
||||
selectedApplicableScope: ["Amend your CommunityRule"],
|
||||
selectedApplicableScope: ["Changing this Rule"],
|
||||
},
|
||||
}),
|
||||
).toEqual(["amend"]);
|
||||
@@ -132,7 +135,7 @@ describe("decisionApproachKeyResources", () => {
|
||||
const next = syncDecisionApproachKeyResourceDetails(
|
||||
{ "lazy-consensus": emptyEntry() },
|
||||
["do-ocracy"],
|
||||
["Amend your CommunityRule"],
|
||||
["Changing this Rule"],
|
||||
(id) => ({
|
||||
...emptyEntry(),
|
||||
corePrinciple: id,
|
||||
@@ -140,19 +143,19 @@ describe("decisionApproachKeyResources", () => {
|
||||
}),
|
||||
);
|
||||
expect(next["lazy-consensus"]?.selectedApplicableScope).toEqual([
|
||||
"Amend your CommunityRule",
|
||||
"Changing this Rule",
|
||||
]);
|
||||
expect(next["do-ocracy"]?.corePrinciple).toBe("do-ocracy");
|
||||
expect(next["do-ocracy"]?.applicableScope).toEqual(["Volunteer Tasks"]);
|
||||
expect(next["do-ocracy"]?.selectedApplicableScope).toEqual([
|
||||
"Amend your CommunityRule",
|
||||
"Changing this Rule",
|
||||
]);
|
||||
});
|
||||
|
||||
it("compares string arrays by order", () => {
|
||||
expect(stringArraysEqual(["a", "b"], ["a", "b"])).toBe(true);
|
||||
expect(stringArraysEqual(["a", "b"], ["b", "a"])).toBe(false);
|
||||
expect(decisionApproachKeyResourceLabels()).toHaveLength(4);
|
||||
expect(decisionApproachKeyResourceLabels()).toHaveLength(7);
|
||||
});
|
||||
|
||||
it("unions key-resource labels onto an existing scope list", () => {
|
||||
@@ -160,10 +163,13 @@ describe("decisionApproachKeyResources", () => {
|
||||
withDecisionApproachKeyResourceScopes(["Daily Operations"]),
|
||||
).toEqual([
|
||||
"Daily Operations",
|
||||
"Amend your CommunityRule",
|
||||
"Steward finances",
|
||||
"Project level decisions",
|
||||
"Discipline and member termination",
|
||||
"Everyday decisions",
|
||||
"Big decisions",
|
||||
"Changing this Rule",
|
||||
"Emergencies",
|
||||
"Shared money",
|
||||
"Spending",
|
||||
"Roles and working groups",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,93 +1,62 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { deriveCompactCards } from "../../app/(app)/create/hooks/useFacetRecommendations";
|
||||
|
||||
const methods = [
|
||||
{ id: "alpha" },
|
||||
{ id: "bravo" },
|
||||
{ id: "charlie" },
|
||||
{ id: "delta" },
|
||||
{ id: "echo" },
|
||||
{ id: "foxtrot" },
|
||||
{ id: "golf" },
|
||||
] as const;
|
||||
|
||||
describe("deriveCompactCards", () => {
|
||||
it("falls back to authoring order with no badges when no facets selected", () => {
|
||||
const result = deriveCompactCards(methods, {}, false, 5);
|
||||
expect(result.compactCardIds).toEqual([
|
||||
"alpha",
|
||||
"bravo",
|
||||
"charlie",
|
||||
"delta",
|
||||
"echo",
|
||||
]);
|
||||
it("falls back to deck order with no tags when no facets are chosen", () => {
|
||||
const result = deriveCompactCards("decisionApproaches", {}, 5);
|
||||
expect(result.compactCardIds).toHaveLength(5);
|
||||
expect(result.recommendedIds.size).toBe(0);
|
||||
expect(result.orderedIds[0]).toBe(result.compactCardIds[0]);
|
||||
});
|
||||
|
||||
it("falls back to authoring order with no badges when facets selected but every score is zero", () => {
|
||||
const result = deriveCompactCards(methods, {}, true, 5);
|
||||
expect(result.compactCardIds).toEqual([
|
||||
"alpha",
|
||||
"bravo",
|
||||
"charlie",
|
||||
"delta",
|
||||
"echo",
|
||||
]);
|
||||
expect(result.recommendedIds.size).toBe(0);
|
||||
});
|
||||
|
||||
it("shows only recommended (matched) cards when fewer than the limit match", () => {
|
||||
it("shows the top five in deck order and leaves every-rule cards untagged", () => {
|
||||
const result = deriveCompactCards(
|
||||
methods,
|
||||
{ bravo: 2, delta: 1 },
|
||||
true,
|
||||
"decisionApproaches",
|
||||
{
|
||||
size: "thirteenToOneHundred",
|
||||
mainKind: "workersCoop",
|
||||
scale: ["local"],
|
||||
stage: "established",
|
||||
traits: { risky: true, sponsor: true, employer: true },
|
||||
},
|
||||
5,
|
||||
);
|
||||
// Caller is responsible for pre-ranking by score (rankMethodsByScore).
|
||||
// This test passes already-ranked input; the hook just respects ordering
|
||||
// and tags only the matched subset — no padding with unrecommended cards.
|
||||
expect(result.compactCardIds).toEqual(["bravo", "delta"]);
|
||||
expect([...result.recommendedIds].sort()).toEqual(["bravo", "delta"]);
|
||||
});
|
||||
|
||||
it("caps recommended cards at the limit when more than `limit` match", () => {
|
||||
const scores = {
|
||||
alpha: 4,
|
||||
bravo: 3,
|
||||
charlie: 3,
|
||||
delta: 2,
|
||||
echo: 1,
|
||||
foxtrot: 1,
|
||||
golf: 1,
|
||||
};
|
||||
const result = deriveCompactCards(methods, scores, true, 5);
|
||||
expect(result.compactCardIds).toEqual([
|
||||
"alpha",
|
||||
"bravo",
|
||||
"charlie",
|
||||
"delta",
|
||||
"echo",
|
||||
]);
|
||||
expect(result.recommendedIds.size).toBe(5);
|
||||
expect([...result.recommendedIds].sort()).toEqual([
|
||||
"alpha",
|
||||
"bravo",
|
||||
"charlie",
|
||||
"delta",
|
||||
"echo",
|
||||
"decision-speeds",
|
||||
"modified-consensus",
|
||||
"emergency-decisions",
|
||||
"on-site-lead",
|
||||
"how-we-are-covered",
|
||||
]);
|
||||
expect(result.recommendedIds.has("decision-speeds")).toBe(false);
|
||||
expect(result.recommendedIds.has("on-site-lead")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns a single card when only one method matches", () => {
|
||||
const result = deriveCompactCards(methods, { charlie: 4 }, true, 5);
|
||||
expect(result.compactCardIds).toEqual(["charlie"]);
|
||||
expect([...result.recommendedIds]).toEqual(["charlie"]);
|
||||
});
|
||||
|
||||
it("respects a smaller `limit` even when many methods match", () => {
|
||||
const scores = { alpha: 4, bravo: 3, charlie: 3, delta: 2 };
|
||||
const result = deriveCompactCards(methods, scores, true, 3);
|
||||
expect(result.compactCardIds).toEqual(["alpha", "bravo", "charlie"]);
|
||||
expect(result.recommendedIds.size).toBe(3);
|
||||
it("shows seven when the screen asks for seven, with spending behind see all", () => {
|
||||
const result = deriveCompactCards(
|
||||
"decisionApproaches",
|
||||
{
|
||||
size: "thirteenToOneHundred",
|
||||
mainKind: "workersCoop",
|
||||
scale: ["local"],
|
||||
stage: "established",
|
||||
traits: { risky: true, sponsor: true, employer: true },
|
||||
},
|
||||
7,
|
||||
);
|
||||
expect(result.compactCardIds).toEqual([
|
||||
"decision-speeds",
|
||||
"modified-consensus",
|
||||
"changing-this-rule",
|
||||
"emergency-decisions",
|
||||
"on-site-lead",
|
||||
"how-we-are-covered",
|
||||
"elected-board-of-directors",
|
||||
]);
|
||||
expect(result.orderedIds.slice(7, 10)).toEqual([
|
||||
"shared-money",
|
||||
"spending-limits",
|
||||
"pay-and-hiring",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -46,16 +46,16 @@ describe("governanceCatalog (CR-115)", () => {
|
||||
});
|
||||
}
|
||||
|
||||
it("core values: ids are 1-based positions with copy fields", () => {
|
||||
it("core values: ids come from the value id field", () => {
|
||||
const values = listCatalogCoreValues();
|
||||
const messages = readJson<{
|
||||
values: Array<string | { label: string }>;
|
||||
values: Array<{ id: string; label: string }>;
|
||||
}>("messages/en/create/customRule/coreValues.json");
|
||||
expect(values.length).toBe(messages.values.length);
|
||||
expect(values[0].id).toBe("1");
|
||||
expect(values[0].label).toBeTruthy();
|
||||
expect(values[0].id).toBe(messages.values[0].id);
|
||||
expect(values[0].label).toBe(messages.values[0].label);
|
||||
expect(typeof values[0].meaning).toBe("string");
|
||||
expect(typeof values[0].signals).toBe("string");
|
||||
expect(getCatalogCoreValue("1")).toEqual(values[0]);
|
||||
expect(getCatalogCoreValue(values[0].id)).toEqual(values[0]);
|
||||
});
|
||||
});
|
||||
@@ -45,7 +45,7 @@ describe("governanceTemplateCatalog (Figma 21764-16435)", () => {
|
||||
"consensus",
|
||||
"do-ocracy",
|
||||
"devolution",
|
||||
"quadratic-governance",
|
||||
"solidarity-network",
|
||||
]);
|
||||
expect(getGovernanceTemplatesForHome()).toHaveLength(4);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { migrateLegacyCreateFlowState } from "../../lib/create/migrateLegacyCreateFlowState";
|
||||
import { groupAnswersFromCreateFlowState } from "../../lib/create/groupAnswers";
|
||||
|
||||
describe("maturity and chip aliases", () => {
|
||||
it("scores one maturity answer and keeps a draft that stored several", () => {
|
||||
const draft = migrateLegacyCreateFlowState({
|
||||
selectedMaturityIds: ["1", "2", "4"],
|
||||
selectedOrganizationTypeIds: ["1"],
|
||||
});
|
||||
expect(draft.selectedMaturityIds).toEqual([
|
||||
"earlyStage",
|
||||
"growthStage",
|
||||
"established",
|
||||
]);
|
||||
expect(draft.selectedOrganizationTypeIds).toEqual(["workersCoop"]);
|
||||
const answers = groupAnswersFromCreateFlowState(draft);
|
||||
expect(answers.stage).toBe("earlyStage");
|
||||
expect(answers.mainKind).toBe("workersCoop");
|
||||
});
|
||||
|
||||
it("loads Enterprise and Larger organization as Established", () => {
|
||||
const fromId = migrateLegacyCreateFlowState({
|
||||
selectedMaturityIds: ["4"],
|
||||
communityStructureChipSnapshots: {
|
||||
maturity: [
|
||||
{ id: "1", label: "Early stage", state: "unselected" },
|
||||
{ id: "2", label: "Growth stage", state: "unselected" },
|
||||
{ id: "3", label: "Established", state: "unselected" },
|
||||
{ id: "4", label: "Enterprise", state: "selected" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(fromId.selectedMaturityIds).toEqual(["established"]);
|
||||
expect(
|
||||
fromId.communityStructureChipSnapshots?.maturity?.find(
|
||||
(row) => row.id === "established",
|
||||
)?.label,
|
||||
).toBe("Established");
|
||||
|
||||
const fromLabel = migrateLegacyCreateFlowState({
|
||||
selectedMaturityIds: ["larger-organization"],
|
||||
communityStructureChipSnapshots: {
|
||||
maturity: [
|
||||
{ id: "larger-organization", label: "Larger organization", state: "selected" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(fromLabel.selectedMaturityIds).toEqual(["established"]);
|
||||
});
|
||||
|
||||
it("keeps the worker co-op pick when the label was Worker cooperative", () => {
|
||||
const draft = migrateLegacyCreateFlowState({
|
||||
selectedOrganizationTypeIds: ["1"],
|
||||
communityStructureChipSnapshots: {
|
||||
organizationTypes: [
|
||||
{ id: "1", label: "Worker cooperative", state: "selected" },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(draft.selectedOrganizationTypeIds).toEqual(["workersCoop"]);
|
||||
expect(
|
||||
draft.communityStructureChipSnapshots?.organizationTypes?.find(
|
||||
(row) => row.id === "workersCoop",
|
||||
)?.label,
|
||||
).toBe("Worker co-op");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { migrateLegacyCreateFlowState } from "../../lib/create/migrateLegacyCreateFlowState";
|
||||
import { resolveCoreValueId, resolveMethodId } from "../../lib/create/idAliases";
|
||||
|
||||
describe("deck id aliases", () => {
|
||||
it("maps old card ids onto the remade decks", () => {
|
||||
expect(resolveMethodId("membership", "open-access")).toEqual({
|
||||
deck: "membership",
|
||||
id: "open-admission",
|
||||
});
|
||||
expect(resolveMethodId("conflictManagement", "peer-mediation")).toEqual({
|
||||
deck: "conflictManagement",
|
||||
id: "conflict-ladder",
|
||||
});
|
||||
expect(resolveMethodId("decisionApproaches", "lazy-consensus")).toEqual({
|
||||
deck: "decisionApproaches",
|
||||
id: "lazy-consensus",
|
||||
});
|
||||
});
|
||||
|
||||
it("moves a cut card to the card that now holds its content", () => {
|
||||
expect(resolveMethodId("decisionApproaches", "proof-of-work")).toEqual({
|
||||
deck: "decisionApproaches",
|
||||
id: "weighted-voting",
|
||||
});
|
||||
});
|
||||
|
||||
it("migrates a saved draft's cards and core values", () => {
|
||||
const draft = migrateLegacyCreateFlowState({
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedConflictManagementIds: ["peer-mediation"],
|
||||
selectedCoreValueIds: ["1", "5", "36"],
|
||||
coreValuesChipsSnapshot: [
|
||||
{ id: "1", label: "Accessibility", state: "selected" },
|
||||
{ id: "5", label: "Altruism", state: "selected" },
|
||||
{ id: "36", label: "Holocracy", state: "selected" },
|
||||
],
|
||||
});
|
||||
expect(draft.selectedMembershipMethodIds).toEqual(["open-admission"]);
|
||||
expect(draft.selectedConflictManagementIds).toEqual(["conflict-ladder"]);
|
||||
expect(draft.selectedCoreValueIds).toEqual(["accessibility", "generosity"]);
|
||||
expect(resolveCoreValueId("36")).toBeNull();
|
||||
expect(draft.coreValuesChipsSnapshot?.map((row) => row.id)).toEqual([
|
||||
"accessibility",
|
||||
"generosity",
|
||||
]);
|
||||
expect(draft.coreValuesChipsSnapshot?.[1]?.label).toBe("Generosity");
|
||||
});
|
||||
|
||||
it("is safe to run twice", () => {
|
||||
const once = migrateLegacyCreateFlowState({
|
||||
selectedMembershipMethodIds: ["open-access"],
|
||||
selectedCoreValueIds: ["2"],
|
||||
});
|
||||
const twice = migrateLegacyCreateFlowState(once);
|
||||
expect(twice.selectedMembershipMethodIds).toEqual(once.selectedMembershipMethodIds);
|
||||
expect(twice.selectedCoreValueIds).toEqual(once.selectedCoreValueIds);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,7 @@ describe("methodCardFacetMatchesPresetForId", () => {
|
||||
decisionApproachFacetMatchesPreset(
|
||||
{
|
||||
...p,
|
||||
selectedApplicableScope: ["Steward finances"],
|
||||
selectedApplicableScope: ["Shared money"],
|
||||
},
|
||||
uuid,
|
||||
),
|
||||
|
||||
@@ -66,13 +66,20 @@ describe("data/create/customRule/_facetGroups.json (CR-88)", () => {
|
||||
const [filePath, jsonPointer] = block.source.split("#");
|
||||
const file = readJson<Record<string, { label: string }[]>>(filePath);
|
||||
const arrayKey = jsonPointer.replace(/^\//, "");
|
||||
const arr = file[arrayKey];
|
||||
const arr = file[arrayKey] as { id?: string }[];
|
||||
expect(Array.isArray(arr), `${group}: ${block.source} → array`).toBe(true);
|
||||
const positions = new Set(arr.map((_, i) => String(i + 1)));
|
||||
const chipIds = new Set(
|
||||
arr.map((row, i) =>
|
||||
row && typeof row === "object" && "id" in row && typeof row.id === "string"
|
||||
? row.id
|
||||
: String(i + 1),
|
||||
),
|
||||
);
|
||||
for (const [valueId, { chipId }] of Object.entries(block.values)) {
|
||||
const retiredEnterprise = group === "maturity" && valueId === "enterprise";
|
||||
expect(
|
||||
positions.has(chipId),
|
||||
`${group}.${valueId} chipId ${chipId} should be a position in ${block.source} (have ${[...positions].join(",")})`,
|
||||
chipIds.has(chipId) || retiredEnterprise,
|
||||
`${group}.${valueId} chipId ${chipId} should be an option id in ${block.source}`,
|
||||
).toBe(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ beforeEach(() => {
|
||||
findManyMock.mockReset();
|
||||
});
|
||||
|
||||
describe("listMethodRecommendations (CR-88 §9.2)", () => {
|
||||
describe("listMethodRecommendations", () => {
|
||||
it("returns empty rankings when no facets are requested", async () => {
|
||||
const result = await listMethodRecommendations({
|
||||
section: "communication",
|
||||
@@ -33,55 +33,40 @@ describe("listMethodRecommendations (CR-88 §9.2)", () => {
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("scores methods by counting matched (group, value) pairs", async () => {
|
||||
findManyMock.mockResolvedValueOnce([
|
||||
{ slug: "loomio", group: "size", value: "thirteenToOneHundred" },
|
||||
{ slug: "loomio", group: "orgType", value: "workersCoop" },
|
||||
{ slug: "in-person", group: "size", value: "thirteenToOneHundred" },
|
||||
]);
|
||||
|
||||
const result = await listMethodRecommendations({
|
||||
section: "communication",
|
||||
it("ranks from the job rule and scores maturity once", async () => {
|
||||
const once = await listMethodRecommendations({
|
||||
section: "decisionApproaches",
|
||||
facets: {
|
||||
size: ["thirteenToOneHundred"],
|
||||
orgType: ["workersCoop"],
|
||||
maturity: ["established"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
rankedSlugs: ["loomio", "in-person"],
|
||||
matchesBySlug: {
|
||||
loomio: {
|
||||
score: 2,
|
||||
matchedFacets: ["size:thirteenToOneHundred", "orgType:workersCoop"],
|
||||
},
|
||||
"in-person": {
|
||||
score: 1,
|
||||
matchedFacets: ["size:thirteenToOneHundred"],
|
||||
},
|
||||
const several = await listMethodRecommendations({
|
||||
section: "decisionApproaches",
|
||||
facets: {
|
||||
size: ["thirteenToOneHundred"],
|
||||
orgType: ["workersCoop"],
|
||||
maturity: ["established", "earlyStage", "enterprise"],
|
||||
},
|
||||
});
|
||||
expect(once?.rankedSlugs).toEqual(several?.rankedSlugs);
|
||||
expect(once?.rankedSlugs[0]).toBeTruthy();
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns null on query failure (caller falls back to authoring order)", async () => {
|
||||
findManyMock.mockRejectedValueOnce(new Error("db down"));
|
||||
it("uses only the main kind for eligibility", async () => {
|
||||
const result = await listMethodRecommendations({
|
||||
section: "communication",
|
||||
facets: { size: ["oneMember"] },
|
||||
section: "decisionApproaches",
|
||||
facets: {
|
||||
size: ["thirteenToOneHundred"],
|
||||
orgType: ["workersCoop", "forProfit"],
|
||||
scale: ["local"],
|
||||
maturity: ["established"],
|
||||
},
|
||||
});
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("dedupes (group, value) so the same row never double-counts", async () => {
|
||||
findManyMock.mockResolvedValueOnce([
|
||||
{ slug: "loomio", group: "size", value: "twoToFive" },
|
||||
{ slug: "loomio", group: "size", value: "twoToFive" },
|
||||
]);
|
||||
const result = await listMethodRecommendations({
|
||||
section: "communication",
|
||||
facets: { size: ["twoToFive"] },
|
||||
});
|
||||
expect(result?.matchesBySlug["loomio"]?.score).toBe(1);
|
||||
expect(result?.rankedSlugs).toContain("consent-sociocracy");
|
||||
expect(findManyMock).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -276,7 +276,7 @@ describe("createFlowStateFromPublishedRule", () => {
|
||||
expect(partial.editingPublishedRuleId).toBe("rule-1");
|
||||
expect(partial.title).toBe("Oak");
|
||||
expect(partial.communityContext).toBe("River cleanup");
|
||||
expect(partial.selectedCoreValueIds).toEqual(["1"]);
|
||||
expect(partial.selectedCoreValueIds).toEqual(["accessibility"]);
|
||||
expect(partial.coreValuesChipsSnapshot?.[0]?.label).toBe("Ecology");
|
||||
expect(partial.selectedCommunicationMethodIds).toEqual(["slack"]);
|
||||
expect(partial.communicationMethodDetailsById?.slack?.corePrinciple).toBe(
|
||||
|
||||
@@ -32,7 +32,11 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
},
|
||||
};
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out.map((s) => s.categoryName)).toEqual(["Values", "Communication"]);
|
||||
expect(out.map((s) => s.categoryName)).toEqual([
|
||||
"Values",
|
||||
"Communication",
|
||||
"Shared terms",
|
||||
]);
|
||||
expect(out[0].entries[0].title).toBe("Ecology");
|
||||
expect(out[0].entries[0].body).toBe(
|
||||
"We protect water.\n\nLitter = violation.",
|
||||
@@ -50,7 +54,10 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
],
|
||||
};
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out.map((s) => s.categoryName)).toEqual(["Membership"]);
|
||||
expect(out.map((s) => s.categoryName)).toEqual([
|
||||
"Membership",
|
||||
"Shared terms",
|
||||
]);
|
||||
});
|
||||
|
||||
it("prefers document.coreValues over a parallel Values section in document.sections", () => {
|
||||
@@ -66,8 +73,10 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
],
|
||||
};
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out.length).toBe(1);
|
||||
expect(out[0].categoryName).toBe("Values");
|
||||
expect(out.map((section) => section.categoryName)).toEqual([
|
||||
"Values",
|
||||
"Shared terms",
|
||||
]);
|
||||
expect(out[0].entries[0].title).toBe("Should not duplicate");
|
||||
expect(out[0].entries[0].body).toBe("x\n\ny");
|
||||
});
|
||||
@@ -80,7 +89,7 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
],
|
||||
};
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out[0].entries[0].body).toMatch(/survival and success/);
|
||||
expect(out[0].entries[0].body).toMatch(/well-being is bound up/);
|
||||
});
|
||||
|
||||
it("replaces template placeholder bodies with preset copy, Values first", () => {
|
||||
@@ -99,7 +108,11 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
],
|
||||
};
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out.map((s) => s.categoryName)).toEqual(["Values", "Communication"]);
|
||||
expect(out.map((s) => s.categoryName)).toEqual([
|
||||
"Values",
|
||||
"Communication",
|
||||
"Shared terms",
|
||||
]);
|
||||
expect(out[0].entries[0].body).not.toContain("Suggested focus");
|
||||
expect(out[0].entries[0].body.length).toBeGreaterThan(20);
|
||||
expect(out[1].entries[0].blocks?.length).toBeGreaterThanOrEqual(1);
|
||||
@@ -112,9 +125,12 @@ describe("parsePublishedDocumentForCommunityRuleDisplay", () => {
|
||||
{ categoryName: "X", entries: [{ title: "t", body: "b" }] },
|
||||
],
|
||||
};
|
||||
expect(parsePublishedDocumentForCommunityRuleDisplay(doc)).toEqual(
|
||||
doc.sections,
|
||||
);
|
||||
const out = parsePublishedDocumentForCommunityRuleDisplay(doc);
|
||||
expect(out[0]).toEqual(doc.sections[0]);
|
||||
expect(out.map((section) => section.categoryName)).toEqual([
|
||||
"X",
|
||||
"Shared terms",
|
||||
]);
|
||||
});
|
||||
|
||||
it("replaces stale document.sections method category with full methodSelections (custom rules)", () => {
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
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, RecommendedCard } 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 compact(card: RecommendedCard) {
|
||||
const reason =
|
||||
card.reason.kind === "added"
|
||||
? `added:${card.reason.answer}`
|
||||
: card.reason.kind === "opened"
|
||||
? `opened:${card.reason.answer}`
|
||||
: card.reason.answer
|
||||
? `fit+${card.reason.answer}`
|
||||
: "fit";
|
||||
return {
|
||||
id: card.id,
|
||||
tag: card.tag,
|
||||
tier: card.tier,
|
||||
reason,
|
||||
...(card.decidedBy ? { decidedBy: card.decidedBy } : {}),
|
||||
...(card.alternative ? { alternative: card.alternative } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
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(compact),
|
||||
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<{ picks: unknown[] }>("tests/fixtures/profiles-picks.json");
|
||||
expect(picksFor([...profiles, ...founders])).toEqual(expected.picks);
|
||||
});
|
||||
|
||||
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 },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -120,7 +120,7 @@ describe("sectionFromDecision", () => {
|
||||
]);
|
||||
const blocks = section?.entries[0]?.blocks ?? [];
|
||||
expect(blocks).toContainEqual({
|
||||
label: "Consensus Level",
|
||||
label: "Consensus level",
|
||||
body: "100%",
|
||||
});
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user