# Instructions

- Following Playwright test failed.
- Explain why, be concise, respect Playwright best practices.
- Provide a snippet of code with the fix, if possible.

# Test info

- Name: tests/ui/data-contracts.spec.ts >> graph data contracts >> Party exposes its FEC code
- Location: tests/ui/data-contracts.spec.ts:77:7

# Error details

```
Error: Party fields

expect(received).toEqual(expected) // deep equality

Expected: ArrayContaining ["party_code"]
Received: ["active_cycles", "candidate_count", "committee_count", "id", "party_name", "type", "candidatesAffiliatedWith", "candidatesAffiliatedWithConnection", "committeesAffiliatedWith", "committeesAffiliatedWithConnection"]
```

# Test source

```ts
  1   | import { test, expect, APIRequestContext } from '@playwright/test';
  2   | import * as fs from 'fs';
  3   | import * as path from 'path';
  4   | import { gql, getRandomItems } from './helpers';
  5   | 
  6   | /*
  7   |  * Data contracts between the graph the API serves and what the site relies on,
  8   |  * checked through the target's /api/graphql proxy. Each one broke or nearly broke
  9   |  * in the 2026-10 graph rebuild (electinfo/ops#514):
  10  |  *   - county GEOIDs: County.id became a UUID; the site joins counties to
  11  |  *     tracts/blocks/precincts on state_fips + county_fips;
  12  |  *   - entity id shapes the static pages and aliases key on;
  13  |  *   - entity counts: a partial load (Vendor 0.76x, Voter 0) went unnoticed
  14  |  *     until a manual parity check.
  15  |  */
  16  | 
  17  | const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
  18  | 
  19  | const baseline = JSON.parse(fs.readFileSync(path.join(__dirname, 'baseline-counts.json'), 'utf8')) as {
  20  |   floorRatio: number;
  21  |   counts: Record<string, number>;
  22  | };
  23  | 
  24  | /** True on the rebuilt graph (neo4j-cluster: Party.id is a UUID5), false on the legacy db-neo4j (id = FEC code). */
  25  | async function isRebuiltGraph(request: APIRequestContext): Promise<boolean> {
  26  |   const data = await gql<{ parties: { id: string }[] }>(request, '{ parties(limit: 1) { id } }');
  27  |   return UUID.test(data.parties[0]?.id ?? '');
  28  | }
  29  | 
  30  | test.describe('graph data contracts', () => {
  31  |   test('county state_fips + county_fips is the GEOID tracts and blocks join on', async ({ request }) => {
  32  |     const { counties } = await gql<{ counties: { name: string; state_fips: string; county_fips: string }[] }>(
  33  |       request,
  34  |       `{ counties(where: { state: { eq: "TX" } }, sort: [{ name: ASC }], limit: 300) { name state_fips county_fips } }`,
  35  |     );
  36  |     expect(counties.length, 'TX should have counties').toBeGreaterThan(200);
  37  |     for (const c of counties) {
  38  |       expect(c.state_fips, `${c.name} state_fips`).toMatch(/^\d{2}$/);
  39  |       expect(c.county_fips, `${c.name} county_fips`).toMatch(/^\d{3}$/);
  40  |     }
  41  |     for (const c of getRandomItems(counties, 3)) {
  42  |       const geoid = c.state_fips + c.county_fips;
  43  |       const data = await gql<{ tracts: unknown[]; blocks: unknown[] }>(
  44  |         request,
  45  |         `query($g: String!) {
  46  |            tracts(where: { county_fips: { eq: $g } }, limit: 1) { id }
  47  |            blocks(where: { geoid_county: { eq: $g } }, limit: 1) { id }
  48  |          }`,
  49  |         { g: geoid },
  50  |       );
  51  |       expect(data.tracts.length, `${c.name} (${geoid}) should have tracts`).toBeGreaterThan(0);
  52  |       expect(data.blocks.length, `${c.name} (${geoid}) should have blocks`).toBeGreaterThan(0);
  53  |     }
  54  |   });
  55  | 
  56  |   test('precincts join on the 5-digit county GEOID', async ({ request }) => {
  57  |     const data = await gql<{ precincts: unknown[] }>(
  58  |       request,
  59  |       `{ precincts(where: { county_fips: { eq: "48453" } }, limit: 1) { id } }`,
  60  |     );
  61  |     expect(data.precincts.length, 'Travis County (48453) should have precincts').toBeGreaterThan(0);
  62  |   });
  63  | 
  64  |   test('candidate ids are UUID5s and carry their FEC id', async ({ request }) => {
  65  |     const { candidates } = await gql<{ candidates: { id: string; fec_candidate_id: string | null }[] }>(
  66  |       request,
  67  |       // FEC-keyed candidates; the RDH ones without an FEC id are the schema-drift test's concern
  68  |       '{ candidates(where: { fec_candidate_id: { startsWith: "H" } }, limit: 50) { id fec_candidate_id } }',
  69  |     );
  70  |     expect(candidates.length).toBeGreaterThan(0);
  71  |     for (const c of candidates) {
  72  |       expect(c.id, `candidate ${c.fec_candidate_id} id`).toMatch(UUID);
  73  |       expect(c.fec_candidate_id ?? '', `candidate ${c.id} fec_candidate_id`).toMatch(/^[HSP][0-9A-Z]{8}$/);
  74  |     }
  75  |   });
  76  | 
  77  |   test('Party exposes its FEC code', async ({ request }) => {
  78  |     // On the rebuilt graph Party.id is a UUID5 and the code lives in
  79  |     // party_code / fec_party_id, but api.elect.info/schema.graphql was introspected
  80  |     // from the legacy graph and exposes neither. Remove test.fail once the schema
  81  |     // is regenerated from neo4j-cluster.
  82  |     test.fail(true, 'schema.graphql predates the rebuilt graph: Party has no party_code / fec_party_id field');
  83  |     const data = await gql<{ __type: { fields: { name: string }[] } }>(
  84  |       request,
  85  |       '{ __type(name: "Party") { fields { name } } }',
  86  |     );
  87  |     const fields = data.__type.fields.map(f => f.name);
> 88  |     expect(fields, 'Party fields').toEqual(expect.arrayContaining(['party_code']));
      |                                    ^ Error: Party fields
  89  |   });
  90  | 
  91  |   test('the graph honours every non-null scalar the schema declares (seeded sample per type)', async ({ request }) => {
  92  |     // schema.graphql is introspected from a graph and baked into the image. When the
  93  |     // served graph drifts (the 2026-10 rebuild), a declared non-null field that is
  94  |     // null ("Cannot return null for non-nullable field"), or a property whose Neo4j
  95  |     // type changed (a Date where the schema says String: "String cannot represent
  96  |     // value"), fails the whole query. Sample each node type at a seeded offset,
  97  |     // asking for all its non-null scalars. KNOWN lists drift already raised on
  98  |     // electinfo/ops#514 (cutover blockers until schema.graphql is regenerated from
  99  |     // neo4j-cluster); anything else fails.
  100 |     test.setTimeout(180000);
  101 |     const KNOWN: Record<string, string[]> = {
  102 |       Candidate: ['active_cycles', 'election_years', 'fec_candidate_id'], // null on 2,675 RDH candidates (no FEC id)
  103 |       Cycle: ['start_date', 'end_date'],                                  // Neo4j Date, schema String
  104 |       Individual: ['first_contribution', 'last_contribution'],            // Neo4j Date, schema String; null on ~0.02%
  105 |       Stage: ['election_year', 'stage_key'],                              // absent: nodes carry race_key
  106 |     };
  107 |     const SAMPLE = 500;
  108 |     type TypeRef = { kind: string; name: string | null; ofType: TypeRef | null };
  109 |     const unwrap = (t: TypeRef | null): { kinds: string[]; name: string | null } => {
  110 |       const kinds: string[] = [];
  111 |       let name: string | null = null;
  112 |       for (let x = t; x; x = x.ofType) { kinds.push(x.kind); name = x.name; }
  113 |       return { kinds, name };
  114 |     };
  115 |     const typeRef = 'type { kind name ofType { kind name ofType { kind name ofType { kind name } } } }';
  116 |     const { __schema } = await gql<{ __schema: { queryType: { fields: { name: string; type: TypeRef }[] } } }>(
  117 |       request, `{ __schema { queryType { fields { name ${typeRef} } } } }`);
  118 |     const roots = __schema.queryType.fields
  119 |       .map(f => ({ root: f.name, ...unwrap(f.type) }))
  120 |       .filter(f => f.kinds.join() === 'NON_NULL,LIST,NON_NULL,OBJECT' && !f.root.startsWith('curation'));
  121 | 
  122 |     const violations: string[] = [];
  123 |     const known: string[] = [];
  124 |     for (const { root, name } of roots) {
  125 |       const t = await gql<{ __type: { fields: { name: string; type: TypeRef; args: unknown[] }[] } }>(
  126 |         request, `query($n: String!) { __type(name: $n) { fields { name args { name } ${typeRef} } } }`, { n: name });
  127 |       const nonNullScalars = t.__type.fields
  128 |         .filter(f => f.args.length === 0)
  129 |         .map(f => ({ f: f.name, ...unwrap(f.type) }))
  130 |         .filter(x => x.kinds[0] === 'NON_NULL' && x.kinds[1] === 'SCALAR')
  131 |         .map(x => x.f);
  132 |       if (!nonNullScalars.length) continue;
  133 |       const offset = getRandomItems([0, 1000, 5000, 20000, 50000], 1, `nonnull:${name}`)[0];
  134 |       const res = await request.post('/api/graphql', {
  135 |         data: { query: `{ ${root}(limit: ${SAMPLE}, offset: ${offset}) { ${nonNullScalars.join(' ')} } }` },
  136 |       });
  137 |       const body = await res.json();
  138 |       for (const e of body.errors ?? []) {
  139 |         // path is [root, index, field] for field errors
  140 |         const field = Array.isArray(e.path) ? String(e.path[e.path.length - 1]) : '';
  141 |         const where = field ? `${name}.${field}` : `${name}: ${String(e.message).slice(0, 120)}`;
  142 |         if (field && KNOWN[name ?? '']?.includes(field)) known.push(where);
  143 |         else violations.push(`${where}: ${String(e.message).slice(0, 80)} (${root} offset ${offset})`);
  144 |       }
  145 |     }
  146 |     if (known.length) {
  147 |       test.info().annotations.push({ type: 'known schema/graph drift (ops#514)', description: [...new Set(known)].join(', ') });
  148 |     }
  149 |     expect([...new Set(violations)], 'non-null schema fields that are null in the graph').toEqual([]);
  150 |   });
  151 | 
  152 |   test('entity counts stay above the accepted baseline', async ({ request }) => {
  153 |     test.skip(!(await isRebuiltGraph(request)), 'legacy graph (db-neo4j): the baseline is the rebuilt graph');
  154 |     const keys = Object.keys(baseline.counts);
  155 |     const { entityCounts } = await gql<{ entityCounts: Record<string, number> }>(
  156 |       request,
  157 |       `{ entityCounts { ${keys.join(' ')} } }`,
  158 |     );
  159 |     const low = keys
  160 |       .map(k => ({ k, got: entityCounts[k], floor: Math.floor(baseline.counts[k] * baseline.floorRatio) }))
  161 |       .filter(({ got, floor }) => !(got >= floor));
  162 |     expect(
  163 |       low.map(({ k, got, floor }) => `${k} ${got} < floor ${floor} (baseline ${baseline.counts[k]})`),
  164 |       'entity counts below baseline floor (update baseline-counts.json only for an accepted drop)',
  165 |     ).toEqual([]);
  166 |   });
  167 | });
  168 | 
```