DEV Community

Daniel Pertu
Daniel Pertu

Posted on

A planner that invents nothing: 504 employers, 276 with a sourced test list, and 228 where it has to ask

The applications tracker on CogniPrep takes "I am
applying to this employer for this role" and produces a path: the stages the
hiring process usually runs through, what to practise at each one, and what to
read. 309 lines, no database, no request state.

The hard part was not building it. It was making sure it could not claim
anything. Here is the module's opening comment, which is also its spec:

 * Nothing here is new information. Every claim is already made somewhere else on
 * the site, and this module only joins those sources to an employer:
 *   - which tests an employer uses comes from the onboarding employer directory,
 *     which is itself derived from employer-assessments.ts and the provider
 *     company lists. That keeps the evidence rules in one place: an employer
 *     with no sourced association gets no tests here either.
 *   - the practice items are the games, interview and exercises we already sell.
 *   - the tips are existing guide and blog pages, looked up from their own
 *     registries so a renamed page cannot leave a stale title behind.
Enter fullscreen mode Exit fullscreen mode

A planner is a tempting place to be helpful. "Barclays probably uses X" is one
line of code and a claim we cannot source, printed to somebody who is about to
sit a real test. So the rule is that the planner is a join, never a source.

228 of 504 employers get no test list, on purpose

The employer directory a candidate picks from has 504 entries. 276 of them carry
a provider association that came with a citation. 228 do not, because the
directory deliberately mixes two different questions. Being in the list means
"you might be applying here". Carrying a provider means "we have a source saying
this employer uses that vendor", and only the first 276 pass that bar.

So the resolver is three-valued, and one of the values is an admission:

/**
 * Where `providers` came from. 'invite' when the candidate told us what their
 * invitation names, 'employer' when it is our sourced list for the employer,
 * 'unknown' when we have neither and the page should ask.
 */
providerSource: 'invite' | 'employer' | 'unknown';
Enter fullscreen mode Exit fullscreen mode
function resolveProviders(input: ApplicationPathInput) {
  if (input.reportedProvider && isAssessmentProvider(input.reportedProvider)) {
    return { providers: [input.reportedProvider], providerSource: 'invite' };
  }
  const employer = isTypedEmployerId(input.employerId) ? null : getEmployerById(input.employerId);
  if (employer && employer.providers.length > 0) {
    return { providers: [...employer.providers], providerSource: 'employer' };
  }
  return { providers: [], providerSource: 'unknown' };
}
Enter fullscreen mode Exit fullscreen mode

Note the order. The candidate's invitation outranks our own data, and the comment
says why:

 * The invitation wins when the candidate has told us what it names. Employers
 * change vendors and run different tests on different routes (Amazon's guide
 * says as much), so the email in the candidate's inbox is a better authority
 * for their sitting than our list for the employer as a whole.
Enter fullscreen mode Exit fullscreen mode

That is checkable: our public
Amazon assessment guide is one of the
pages that says a large employer runs more than one process. If our own content
admits the list is a generalisation, the code should not treat it as gospel.

A reported provider we do not simulate falls through to the employer list rather
than producing an empty plan, and a test pins that behaviour in both directions.

Every link it can emit is a page that already exists

Each stage gets reading, looked up from the registries that own those pages
rather than hard-coded as titles:

case 'final_interview':
  return [
    interviewGuide('competency-based-interview'),
    interviewGuide('strengths-based-interview'),
    blog('how-to-answer-behavioural-interview-questions'),
  ];
Enter fullscreen mode Exit fullscreen mode

blog(), interviewGuide() and acGuide() all return GuideLink | null, and
the nulls are dropped:

 * Lookups that miss are dropped rather than rendered as dead links; a
 * test pins every slug here to a real page, so a miss means a page was removed.
Enter fullscreen mode Exit fullscreen mode

The titles come from the page's own metadata, so renaming a guide renames it
here. And the test does not make HTTP calls, it asks the file system:

/** Whether an in-app href resolves to a page in the app router. */
function routeExists(href: string): boolean {
  return existsSync(path.join(process.cwd(), 'app', href, 'page.tsx'));
}
Enter fullscreen mode Exit fullscreen mode
it('gives every stage some reading, and every link resolves to a real page', () => {
  // ...
  expect(stage.guides.length, stage.stage).toBeGreaterThan(0);
  expect(routeExists(guide.href), guide.href).toBe(true);
});
Enter fullscreen mode Exit fullscreen mode

Two assertions doing different jobs: no stage may be empty, and no link may
point at nothing. You can check the second one from outside, since every one of
those pages is public. This is the full set of guide links the planner can emit,
and the status I got from each a few minutes ago:

Run them yourself with a loop and a -w '%{http_code}'. Twelve 200s is the
whole claim: a logged-in feature whose link graph a signed-out reader can audit.

On top of those, the online tests stage adds a provider hub per resolved
provider (the SHL hub is one), and the
researching stage adds the employer's own guide, but only where one exists:

const employerGuide: GuideLink | null = profile?.hasGuide
  ? { title: `${profile.name} assessment guide`, href: `/employers/${profile.slug}` }
  : null;
Enter fullscreen mode Exit fullscreen mode

Two filters on the practice list, pulling in opposite directions

 * The playable games for each provider, in library order. Coming-soon games are
 * left out: a plan task has to be something the candidate can open today.
 * Paid games stay in, because the play wall already explains the free budget
 * and the unlock, and hiding them would understate what the real test covers.
Enter fullscreen mode Exit fullscreen mode

Those two sentences are the same judgement applied twice. A plan is a to-do
list, so anything on it has to be openable, which removes the unbuilt items.
But a plan is also a description of what is coming in the real assessment, so
removing the paid items would quietly tell someone their test has four parts
when it has nine. One filter protects the list, the other protects the truth of
it.

Two small functions that keep the plan pointed at the right thing

Stage labels are described as typical rather than asserted, and the candidate can
move to the stage they are actually at, because employers skip and reorder
stages. Before the tests arrive, though, there is nothing to practise for the
current stage, so:

/**
 * The stage to prepare for while the candidate is at `stage`.
 *
 * Before the tests arrive there is nothing to practise for the current stage
 * itself, so researching and applied both look ahead to the online tests:
 * practising before the invitation lands is the whole point of starting early.
 */
export function focusStage(stage: ApplicationStage): ApplicationStage {
  return stage === 'researching' || stage === 'applied' ? 'online_tests' : stage;
}
Enter fullscreen mode Exit fullscreen mode

And the practice links carry the application's own sector, because the interview
and exercises pages otherwise preselect from the onboarding answer, which names
one employer and may well be a different one:

it("opens the interview and exercises on the application's field", () => {
  expect(interview.href).toBe('/dashboard/interview?sector=finance');
  expect(inTray.href).toBe('/dashboard/exercises?sector=finance&format=in-tray');
});

it('leaves the sector to the page when the role is unknown', () => {
  expect(items.find((item) => item.kind === 'interview')!.href).toBe('/dashboard/interview');
});
Enter fullscreen mode Exit fullscreen mode

Absent is not general. A missing sector omits the parameter entirely and lets
the page keep whatever the candidate last chose, which is a better default than
overwriting their choice with a guess. That is the same instinct as
providerSource: 'unknown', and it is the only idea in all 309 lines: when you
do not know, say so, in a shape the UI can act on.

See it for yourself: curl the twelve links above, then open
/employers/amazon and
/games/shl, which are the two kinds of page
the planner reaches for when it does have a source. The planner itself lives on
the applications page after you sign up.

Top comments (0)