Files
krow_talent_app/scripts/seed-fixture.mjs
Aravind 3ddacf269a fix(seed): emit the users array the backend seeder reads
The fixture generator was a version behind the seeder, and the gap was
the `users` key.

`internal/seeder` unmarshals `seed.json` into `{demoUser, users,
entities}` and writes every account in `users`, falling back to
`demoUser` alone when the key is absent. `fixtureFrom` never emitted it —
its comment still claimed the Go loader "ignores the rest", which stopped
being true when the seeder gained the field. So `npm run seed:check`
reported stale on every run, and that is the seventh suite failure, the
one that pre-dates the TypeScript migration.

What made this worth investigating rather than regenerating: the
committed fixture was NOT wrong. It carries two accounts — the demo
administrator and `employer@krow.app` — and running `npm run seed:fixture`
against the old generator would have written a fixture without them.
`krow-backend/docs/deploy-9d3192a.md` says so in as many words, and tells
anyone deploying not to run it.

The accounts are not deleted from a live database by that — the only
DELETE in the seeder is the shift-record prune, and `upsertUser`'s ON
CONFLICT deliberately leaves `password_hash` alone. The damage lands on
the next fresh environment, where `user_employer` would simply never be
created, leaving nobody to sign in as to reach the employer console —
which is the exact gap `seeder.go` records the account as having been
added to close.

The matching frontend half already existed, unmerged, on
`feat/krow-employee-pages` (7a4b61d). That branch is a 53-file console
restructure that renames the same files this migration renamed, so it is
not mergeable here. Only the three files that carry the contract are
taken: `EMPLOYER_USER`, `seedData.User` naming both accounts, and the
generator emitting `users`.

The proof is that nothing had to be written. With the generator corrected
its output is byte-identical to the fixture already committed in
krow-backend — 168,293 bytes, `cmp` clean — so `seed.json` was never
opened for writing, and its md5 and mtime are untouched.

  typecheck     0 errors
  seed:check    "seed.json is in step with src/api/seed.js", exit 0
  npm test      1685/1691, up from 1684 — the fixture check now reads
                "24 applications, byte-identical". The remaining 6 are
                exactly the documented dca1842 baseline failures.
  lint          exit 0, 0 errors, 289 warnings
  build         exit 0, bundle 74d17e2d… unchanged

The bundle being unchanged is itself a check: `demoUser.ts` IS in the
production bundle, and `EMPLOYER_USER` is a new export of it. Nothing in
the app imports it, so it is tree-shaken out and not one byte reaches the
shipped code.

No baseline artifact touched. krow-backend not modified.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
2026-09-19 23:54:36 +05:30

98 lines
4.8 KiB
JavaScript

/**
* Writes the backend's seed fixture from the frontend's seed module.
*
* node scripts/seed-fixture.mjs # check: exits non-zero if stale
* node scripts/seed-fixture.mjs --write # regenerate
*
* `src/api/seed.js` is the authored source: it has the helpers, the reasoning in
* comments, and the values a person edits. `seed/fixtures/seed.json` is what the
* Go seeder loads. They used to be maintained in parallel with nothing checking
* them, which is a duplication that only fails quietly — the demo and the API
* would answer the same question differently, and the first symptom would be a
* number on a page disagreeing with a number from an agent.
*
* `ShiftRecord` is deliberately not carried across. Its dates are anchored to
* the day the seeder runs rather than to this file's fixed calendar, so the Go
* side generates it (internal/seeder/shifts.go, a port of attendanceSeed.js).
* A frozen snapshot of it would read as permanently empty a fortnight later.
*/
import { createServer } from 'vite';
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { withSourceResolution } from './ssr-resolve.mjs';
export const FIXTURE_PATH = join(process.cwd(), '..', 'krow-backend', 'seed', 'fixtures', 'seed.json');
/** The fixture the frontend seed implies. Pure — takes the loaded module. */
export const GENERATED_NOTE =
'Generated from krow-demo/src/api/seed.js — do not edit by hand. ' +
'Regenerate with: npm run seed:fixture';
export function fixtureFrom(seedModule) {
const { ShiftRecord, ...entities } = seedModule.seedData;
/* `_generated` first, so it is the first thing anyone opening the file reads.
`users` is the channel the seeder actually reads — `internal/seeder`
unmarshals into `{demoUser, users, entities}` and writes every account in
`users`, falling back to `demoUser` alone when the key is absent. That
fallback is why `demoUser` stays beside it rather than being replaced: a
fixture written here still seeds correctly against a backend that predates
the list, it simply seeds one account instead of two.
Emitting only `demoUser` is what this generator used to do, and it was a
version behind: the fixture on disk carries the employer account, the
seeder reads it from `users`, and regenerating without this key would drop
`employer@krow.app` from every fresh seed — leaving nobody to sign in as
to reach the employer console, which is the exact gap the account was
added to close.
`entities.User` carries the same list. The seeder ignores it, and it is
emitted because `seedData` is copied wholesale; the two are the same array
rather than two places to keep in step. */
return {
_generated: GENERATED_NOTE,
demoUser: seedModule.DEMO_USER,
users: seedModule.seedData.User,
entities,
};
}
/** Serialised exactly as the committed file is: 2-space indent, no trailing newline. */
export const serialise = (fixture) => JSON.stringify(fixture, null, 2);
export async function buildFixture(server) {
return serialise(fixtureFrom(await server.ssrLoadModule('/src/api/seed.js')));
}
/* ── CLI ──────────────────────────────────────────────────────────────────── */
/* pathToFileURL, not string concatenation: this repo lives under a path with a
space in it, and import.meta.url percent-encodes it while `file://${path}`
does not — the guard silently never fires and the script does nothing. */
if (import.meta.url === pathToFileURL(process.argv[1]).href) {
const server = await createServer({
root: process.cwd(), server: { middlewareMode: true }, appType: 'custom', logLevel: 'error',
});
/* `buildFixture` loads `/src/api/seed.js`, which the TypeScript migration
will rename. The fixture it generates is unchanged either way. */
withSourceResolution(server, process.cwd());
const built = await buildFixture(server);
await server.close();
if (process.argv.includes('--write')) {
writeFileSync(FIXTURE_PATH, built);
const { entities } = JSON.parse(built);
const n = Object.values(entities).reduce((s, v) => s + v.length, 0);
console.log(`seed.json written — ${Object.keys(entities).length} collections, ${n} records.`);
} else if (!existsSync(FIXTURE_PATH)) {
console.error('seed.json is missing. Run: node scripts/seed-fixture.mjs --write');
process.exit(1);
} else if (readFileSync(FIXTURE_PATH, 'utf8') !== built) {
console.error('seed.json is stale — it no longer matches src/api/seed.js.');
console.error('Run: node scripts/seed-fixture.mjs --write');
process.exit(1);
} else {
console.log('seed.json is in step with src/api/seed.js.');
}
}