|
| 1 | +# Coding test: Node.js |
| 2 | + |
| 3 | +We'd like to see how you build things, so here is a small exercise: one problem, and a repo with |
| 4 | +the boring parts already done. |
| 5 | + |
| 6 | +A word on what happens to it. What you send us starts a conversation, it isn't a score. We read it, |
| 7 | +then we ask you to walk us through it, and the walkthrough is where it counts. Aim for something you |
| 8 | +can defend end to end, not something large. |
| 9 | + |
| 10 | +--- |
| 11 | + |
| 12 | +## Before you start |
| 13 | + |
| 14 | +### Getting the repo |
| 15 | + |
| 16 | +Click **Use this template** at the top of this repo and work in the repo that gives you. If you'd |
| 17 | +prefer, clone it and push to a new repo of your own. Same result. |
| 18 | + |
| 19 | +Please don't fork it. Two reasons: |
| 20 | +- GitHub disables Actions on forks by default, so the CI run we read never happens. |
| 21 | +- Forks are listed publicly on the repo they came from, which would put your work in front of every other candidate. |
| 22 | + |
| 23 | +### Running it |
| 24 | + |
| 25 | +Node 24 (there's an `.nvmrc`) and npm. |
| 26 | + |
| 27 | +``` |
| 28 | +npm install |
| 29 | +npm run verify # lint, types, tests |
| 30 | +npm run dev # the server, on :3000 |
| 31 | +npm run report # what your service and your tool actually return |
| 32 | +``` |
| 33 | + |
| 34 | +One check is red on a fresh clone: `declares at least one argument`. Nothing is broken. An AI tool call with |
| 35 | +no arguments can't be told which order to price, so that check is waiting for you to resolve it. Everything else is green before you've written a single line. |
| 36 | + |
| 37 | +### The gate |
| 38 | + |
| 39 | +`npm run verify` is the bar and is required to be green when submitting your repo. It runs lint, types |
| 40 | +and tests on [the published floor](verification/README.md) and accross the codebase. |
| 41 | + |
| 42 | +The floor checks that your service answers in the agreed shape and that bad input doesn't crash it. |
| 43 | +It deliberately checks nothing about the discounts you calculate, or about how you represent money. |
| 44 | +Those are yours to decide and yours to explain. |
| 45 | + |
| 46 | +**`fixtures/`, `verification/` and `.github/workflows/` are ours. Please don't edit them.** They are |
| 47 | +the floor, and we check them on submission. If a floor test fails, change the code under `src/`. |
| 48 | + |
| 49 | +### What's already here |
| 50 | + |
| 51 | +A working Fastify skeleton with the discount service stubbed out. Dependencies flow one way: |
| 52 | + |
| 53 | +``` |
| 54 | +routes/ tools/ -> plugins/app/ -> plugins/external/ |
| 55 | +``` |
| 56 | + |
| 57 | +Files prefixed with `_` are private to their own module. This setup is enforced by `npm run lint` and the messages say which rule you hit and why. |
| 58 | + |
| 59 | +This is the boundary our own service uses. Treat it as a starting point. Change it if you can tell |
| 60 | +us why. |
| 61 | + |
| 62 | +### Agents |
| 63 | + |
| 64 | +You're welcome to use coding agents. We don't ask for anything about how you used coding agents. We expect you to be able to explain and own all submitted code. |
| 65 | + |
| 66 | +`AGENTS.md` in the root is a working aid for your agent: which paths not to touch, what the gate is, |
| 67 | +what the report does. We don't read it and we don't grade it, so edit or delete it as suits you. |
| 68 | + |
| 69 | +### Scope |
| 70 | + |
| 71 | +This exercise is deliberately kept small for fairness. We believe a candidate's available free time shouldn't be a factor in the submitted result. |
| 72 | + |
| 73 | +We'll ask you to walk us through your submission in about 30 minutes, and anything you can't explain |
| 74 | +in that conversation counts against you. Extra scope you weren't asked for will be ignored. |
| 75 | + |
| 76 | +--- |
| 77 | + |
| 78 | +## The problem: discounts |
| 79 | + |
| 80 | +We need you to build us a small service that calculates discounts for orders. |
| 81 | + |
| 82 | +### How discounts work |
| 83 | + |
| 84 | +For now, there are three possible ways of getting a discount: |
| 85 | + |
| 86 | +- A customer who has already bought for over € 1000 gets a discount of 10% on the whole order. |
| 87 | +- For every product of category "Switches" (id 2), when you buy five, you get a sixth for free. |
| 88 | +- If you buy two or more products of category "Tools" (id 1), you get a 20% discount on the |
| 89 | + cheapest product. |
| 90 | + |
| 91 | +By the way: there may be more ways of granting customers discounts in the future. |
| 92 | + |
| 93 | +### The data |
| 94 | + |
| 95 | +`fixtures/` holds everything the service has to work with. |
| 96 | + |
| 97 | +- `fixtures/customers.json` and `fixtures/products.json` are the customer and product data. Assume |
| 98 | + these arrive in the format of the real external API, because they do. |
| 99 | +- `fixtures/orders/` has example orders. These are the payloads we will send you. |
| 100 | + |
| 101 | +### What we ask you to build |
| 102 | + |
| 103 | +Four things, and only these four. |
| 104 | + |
| 105 | +1. **The three discount rules, with tests.** We should be able to see that the rules are right without starting the server. |
| 106 | +2. **`POST /discounts`.** It takes one of the example orders as its body and returns the discounts that apply, each with a reason a human can act on. The response must carry a `discountTotal` and a `discounts` array whose entries each have an `amount` and a `reason`. The shape of the values, including how you represent money, is yours. |
| 107 | +3. **The same logic reachable as a tool call for our product's AI assistant.** `src/tools/calculateDiscount.ts` is stubbed for you, against the descriptor our assistant's tool runtime expects. There is no new logic to write here. It reaches the same service the endpoint reaches. What it does need is a correct implementation for who is calling the tool (see below "Who is calling"). |
| 108 | +4. **A short README.** `README.md` is a placeholder. Replace it with yours: how to run it, how you structured it, and any assumptions you made along the way. |
| 109 | + |
| 110 | +Those four are the whole scope. We are not looking for more. |
| 111 | + |
| 112 | +### Who is calling |
| 113 | + |
| 114 | +**CHECK WITH TEAM FIREFLY HOW TO IMPROVE THIS PART** |
| 115 | + |
| 116 | +An assistant makes the tool call, never your own code, and there are four things worth knowing about |
| 117 | +it. No LLM experience is expected: every one of them is a backend concern. |
| 118 | + |
| 119 | +- **It cannot read your repo.** The tool's `description`, and the `description` on every property |
| 120 | + of its input schema, is the whole of its documentation. Anything you leave ambiguous, it guesses. |
| 121 | +- **It cannot catch an exception.** A failure has to come back as a value it can act on. That is |
| 122 | + what the `ToolResult` shape in the stub is for. Useful means it can tell what to do differently. |
| 123 | +- **Everything you return costs it something**, on every later turn of the conversation. Return the |
| 124 | + fields needed to answer the question, not everything you happen to have. |
| 125 | +- **It cannot be trusted with arithmetic.** Where the answer is a total, compute it and return it |
| 126 | + as a field. Don't leave a sum for the caller to work out. |
| 127 | + |
| 128 | +One more, and this one is about the human at the far end. A reason that says `category 2` sends the |
| 129 | +assistant, and then the user, off to work out what that means. What it takes to say `Switches` |
| 130 | +instead is already in `fixtures/`. |
| 131 | + |
| 132 | +`npm run report` invokes your tool the way a caller does when it gets things wrong, and prints what |
| 133 | +came back. It asserts nothing and cannot fail. Read it anyway. It is the only view you get of what |
| 134 | +the caller sees. |
| 135 | + |
| 136 | +### What we look at |
| 137 | + |
| 138 | +Not a checklist of techniques. These are outcomes, and there is more than one good way to reach each |
| 139 | +of them. |
| 140 | + |
| 141 | +- **Adding a fourth rule is cheap.** Someone should be able to add one without reading the other three first. |
| 142 | +- **Bad input fails in a way the caller can act on.** Not a stack trace, not a 500. |
| 143 | +- **We can follow your commits.** Small and self-contained, so your reasoning is legible after the fact. |
| 144 | +- **A stranger can run it.** Your README is the only support you get. |
| 145 | + |
| 146 | +--- |
| 147 | + |
| 148 | +## When you're done |
| 149 | + |
| 150 | +### Submitting |
| 151 | + |
| 152 | +Send us a link to the repository, public or private, whichever you prefer, as long as we can get to |
| 153 | +it. If you keep it private, add the GitHub account your recruiter gave you as a collaborator. |
| 154 | + |
| 155 | +Include the output of: |
| 156 | + |
| 157 | +``` |
| 158 | +npm run verify:integrity |
| 159 | +``` |
| 160 | + |
| 161 | +It compares `fixtures/`, `verification/` and `.github/workflows/` against this repo's first commit, |
| 162 | +the one we handed over, so it works however you got the repo and whatever you commit on top. If it |
| 163 | +prints a diff, we'll read the submission as not having passed the floor. It runs in CI too, so a |
| 164 | +slip shows up when you push, well before review. |
| 165 | + |
| 166 | +### Questions |
| 167 | + |
| 168 | +If something about the setup or the tooling isn't clear, ask. We'd rather you weren't stuck on our scaffolding. |
0 commit comments