ucode-agent 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,47 +1,85 @@
1
1
  ---
2
2
  name: write-tests
3
- description: Write tests that fail for the right reason behaviour over implementation, real edges, and no assertions that can never break.
3
+ description: Write tests that catch real regressions — the right level for each behaviour, real edge cases, deterministic setups, mocks only at the boundaries, and proof that each test can actually fail.
4
+ auto: write tests, add tests, add a test, unit test, unit tests, test coverage, integration test, e2e test, end to end test, testing, vitest, jest, pytest, playwright
4
5
  ---
5
6
 
6
7
  # Writing tests
7
8
 
8
- The test that matters is the one that fails the day someone breaks the thing it
9
+ The test that matters is the one that fails the day someone breaks what it
9
10
  covers. Every other test is overhead with a green tick on it.
10
11
 
11
- ## Test behaviour, not implementation
12
+ ## 1. Find how this project tests
12
13
 
13
- Assert on what a caller can observe: the return value, the state afterwards,
14
- the thing that was written. A test that checks a private helper was called
15
- locks the implementation in place and will need rewriting the first time
16
- anyone refactors while still not proving the feature works.
14
+ - Look for the runner and its config before writing anything: `vitest.config.*`,
15
+ `jest.config.*`, `playwright.config.*`, `pytest.ini`/`pyproject.toml`,
16
+ `go test`, the `test` script in `package.json`.
17
+ - Match the existing style: file location (`__tests__/`, `*.test.ts` beside the
18
+ source, `tests/`), naming, helpers, fixtures. Use what is there.
19
+ - If nothing exists, pick the standard for the stack — Vitest for Vite and
20
+ Next.js, pytest for Python, the built-in `go test` — and add the script.
17
21
 
18
- ## Cover the edges, not five versions of the middle
22
+ ## 2. Pick the right level for each behaviour
19
23
 
20
- For each unit, the ones that actually find bugs:
24
+ - **Unit** pure logic: parsing, scoring, validation, formatting, reducers.
25
+ Fast, many, no I/O.
26
+ - **Integration** — a route handler with its validation and error paths, a
27
+ component with its real children, a module against a real temporary
28
+ database or filesystem.
29
+ - **End to end** — the one or two core user journeys, in a real browser
30
+ (Playwright). Few, because they are slow and brittle.
31
+
32
+ Most value per minute is in unit tests of the logic that makes decisions and
33
+ integration tests of the boundaries where data comes in.
34
+
35
+ ## 3. Test behaviour, not implementation
36
+
37
+ Assert what a caller can observe — the return value, the rendered output, the
38
+ response, the state afterwards. A test that checks a private helper was called
39
+ breaks on every refactor while proving nothing about the feature.
40
+
41
+ For UI, query the way a user finds things: by role, label and text
42
+ (`getByRole('button', { name: /analyze/i })`), not by class names or test IDs
43
+ unless there is no accessible alternative.
44
+
45
+ ## 4. Cover the cases that find bugs
46
+
47
+ For every unit, go through:
21
48
 
22
49
  - empty, one, many
23
- - the boundary: 0, -1, the last index, the maximum
24
- - null, undefined, the missing field, the wrong type
25
- - the failure path: the dependency throws, the network times out, the file is
26
- gone
27
- - the second call: is it idempotent, is state left behind?
28
-
29
- ## Make each test readable on its own
30
-
31
- - The name says the behaviour: `returns null when the session file is missing`.
32
- - Arrange, act, assert, in that order and visibly separated.
33
- - One reason to fail per test. Six assertions in a row means the first failure
34
- hides the other five.
35
- - No shared mutable state between tests, and no dependence on the order they
36
- run in.
37
- - Real values over mocks wherever it is affordable. Mock the network and the
38
- clock; do not mock the thing you are testing.
39
-
40
- ## Prove the test works
41
-
42
- Break the code on purpose and watch the test fail, then put it back. A test
43
- that has never failed is a test you have no reason to trust. If a test cannot
44
- fail an assertion on a constant, a mock asserting itself — delete it.
45
-
46
- Then run the whole suite and report the real numbers, including anything that
47
- was already failing before you started.
50
+ - boundaries: 0, -1, the exact threshold, just over and just under it, the max
51
+ - missing: `null`, `undefined`, missing field, empty string, wrong type
52
+ - malformed input: invalid JSON, a string where a number was expected,
53
+ a reply wrapped in prose
54
+ - failure paths: the dependency throws, times out, returns an error status
55
+ - repetition: the second call, the same input twice, concurrent calls
56
+
57
+ A threshold rule ("sodium over 600mg is flagged") needs a test at 599, 600 and
58
+ 601. That is where the off-by-one lives.
59
+
60
+ ## 5. Keep tests deterministic and independent
61
+
62
+ - Control time (`vi.useFakeTimers()`, a fixed clock) and randomness (a seed).
63
+ - No real network. Mock at the boundary — the HTTP call, the SDK client — and
64
+ never mock the thing under test.
65
+ - Each test sets up what it needs and cleans up after. No reliance on order.
66
+ - Build test data with small factories so each test states only what matters
67
+ to it.
68
+ - One behaviour per test, named for it:
69
+ `flags sodium when it is over the daily threshold`.
70
+ - Arrange, act, assert visibly separated.
71
+ - Snapshots only for stable, small output; a 400-line snapshot gets approved
72
+ without being read.
73
+
74
+ ## 6. Prove each test can fail
75
+
76
+ Break the code on purpose — flip a comparison, delete a branch — and watch the
77
+ test fail, then restore it. A test that has never failed is a test you have no
78
+ reason to trust. Delete tests that cannot fail (asserting a constant, a mock
79
+ asserting itself).
80
+
81
+ ## 7. Run and report
82
+
83
+ Run the whole suite, not only the new file. Report the real numbers — passed,
84
+ failed, skipped — including anything that was already failing before you
85
+ started, and anything you could not cover and why.
package/src/core/loop.js CHANGED
@@ -1053,8 +1053,9 @@ export class Agent {
1053
1053
  .slice(-count);
1054
1054
 
1055
1055
  for (const m of tail) {
1056
- if (m.role === 'user') this.ui.write(`${blue('›')} ${dim(m.content.split('\n')[0])}`);
1057
- else this.ui.assistant(m.content);
1056
+ if (m.role !== 'user') this.ui.assistant(m.content);
1057
+ else if (this.ui.userMessage) this.ui.userMessage(m.content);
1058
+ else this.ui.write(`${blue('›')} ${dim(m.content.split('\n')[0])}`);
1058
1059
  }
1059
1060
  if (tail.length) this.ui.write(dim(' ── picking up here ──\n'));
1060
1061
  }
@@ -55,28 +55,28 @@ dotenv.config({ path: join(PACKAGE_ROOT, '.env'), quiet: true });
55
55
  */
56
56
  export const MODELS = {
57
57
  'nvidia/nemotron-3-ultra-550b-a55b:free': {
58
- name: 'Nemotron 3 Ultra (free)',
58
+ name: 'Nemotron 3 Ultra',
59
59
  context: 1_000_000,
60
60
  star: true,
61
61
  note: 'deepest reasoning, 1M context — the default',
62
62
  },
63
63
  'nvidia/nemotron-3.5-lightning:free': {
64
- name: 'Nemotron 3.5 Lightning (free)',
64
+ name: 'Nemotron 3.5 Lightning',
65
65
  context: 1_000_000,
66
66
  note: 'same huge window, answers much sooner',
67
67
  },
68
68
  'nvidia/nemotron-3-super-120b-a12b:free': {
69
- name: 'Nemotron 3 Super (free)',
69
+ name: 'Nemotron 3 Super',
70
70
  context: 262_144,
71
71
  note: 'strong all-rounder, quick to first token',
72
72
  },
73
73
  'nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free': {
74
- name: 'Nemotron 3 Nano Omni (free)',
74
+ name: 'Nemotron 3 Nano Omni',
75
75
  context: 256_000,
76
76
  note: 'small and fast, reasoning tuned',
77
77
  },
78
78
  'cohere/north-mini-code:free': {
79
- name: 'North Mini Code (free)',
79
+ name: 'North Mini Code',
80
80
  context: 256_000,
81
81
  star: true,
82
82
  note: 'code and UI specialist — reach for it on frontend work',
@@ -121,7 +121,7 @@ export function setModel(id) {
121
121
  return current;
122
122
  }
123
123
 
124
- /** The short name for a model id: "Nemotron 3 Ultra (free)". */
124
+ /** The short name for a model id: "Nemotron 3 Ultra". */
125
125
  export function modelName(id = current) {
126
126
  return MODELS[id]?.name ?? id;
127
127
  }
@@ -179,16 +179,18 @@ export function estimateConversation(messages) {
179
179
  * of first-run convenience is worth handing out a live credential.
180
180
  */
181
181
  function apiKey() {
182
- const key = (process.env.OPENROUTER_API_KEY ?? '').trim();
182
+ // UCODE_API_KEY is the documented name. The provider's own variable name is
183
+ // still read, so a key set up for another tool keeps working here.
184
+ const key = (process.env.UCODE_API_KEY || process.env.OPENROUTER_API_KEY || '').trim();
183
185
  if (!key) {
184
186
  throw new Failure({
185
187
  kind: 'no_api_key',
186
- attempted: 'connecting to OpenRouter',
187
- failed: 'OPENROUTER_API_KEY is not set in the environment or in any .env file.',
188
+ attempted: 'connecting to the model',
189
+ failed: 'No API key is set - UCODE_API_KEY is missing from the environment and from every .env file.',
188
190
  fix:
189
- `Put OPENROUTER_API_KEY=your-key in ${ENV_FILE} — that applies to every ` +
191
+ `Put UCODE_API_KEY=your-key in ${ENV_FILE} — that applies to every ` +
190
192
  'project on this machine — or in a .env file beside your code. ' +
191
- 'Keys are free at https://openrouter.ai/keys',
193
+ 'Free keys: https://openrouter.ai/keys',
192
194
  });
193
195
  }
194
196
  return key;
@@ -349,10 +351,10 @@ export function explain(err, id) {
349
351
  return new Failure({
350
352
  kind: 'invalid_api_key',
351
353
  attempted,
352
- failed: `OpenRouter rejected the API key (HTTP ${status ?? 401}).`,
354
+ failed: `The API key was rejected (HTTP ${status ?? 401}).`,
353
355
  fix:
354
- 'Check OPENROUTER_API_KEY for a typo or trailing space, and confirm the key ' +
355
- 'is still active at https://openrouter.ai/keys',
356
+ 'Check UCODE_API_KEY in ~/.ucode/.env for a typo or trailing space, and ' +
357
+ 'confirm the key is still active in your account.',
356
358
  cause: err,
357
359
  });
358
360
  }
@@ -380,8 +382,8 @@ export function explain(err, id) {
380
382
  ? `The free daily request cap for ${modelName(id)} is used up.`
381
383
  : `Too many requests for ${modelName(id)} just now${wait ? ` — clear in ${wait}` : ''}.`,
382
384
  fix: daily
383
- ? 'Free caps reset each day. /model switches to another one, or add credit at ' +
384
- 'https://openrouter.ai/credits to lift the ceiling.'
385
+ ? 'Free caps reset each day. /model switches to another one, or add credit to ' +
386
+ 'your account to lift the ceiling.'
385
387
  : 'ucode waits these out on its own. Free endpoints are shared, so it usually ' +
386
388
  'clears in seconds; /model moves to a quieter one.',
387
389
  detail: { retryAfter, daily },
@@ -406,7 +408,7 @@ export function explain(err, id) {
406
408
  return new Failure({
407
409
  kind: 'bad_model',
408
410
  attempted,
409
- failed: `OpenRouter has no model "${id}" available to this key.`,
411
+ failed: `No model "${id}" is available to this key.`,
410
412
  fix: `Run /model. ucode ships with: ${Object.keys(MODELS).join(', ')}`,
411
413
  cause: err,
412
414
  });
@@ -435,7 +437,7 @@ export function explain(err, id) {
435
437
  attempted,
436
438
  failed: noTools
437
439
  ? `${modelName(id)} cannot call tools, which ucode needs for every task.`
438
- : `OpenRouter rejected the request as malformed (HTTP 400): ${detail}`,
440
+ : `The request was rejected as malformed (HTTP 400): ${detail}`,
439
441
  fix: noTools
440
442
  ? 'Run /model and pick another one.'
441
443
  : 'Usually an oversized conversation. /new starts a fresh one.',
@@ -497,7 +499,7 @@ export function explain(err, id) {
497
499
  return new Failure({
498
500
  kind: 'network',
499
501
  attempted,
500
- failed: `The connection to OpenRouter dropped: ${raw}`,
502
+ failed: `The connection to the model dropped: ${raw}`,
501
503
  fix:
502
504
  'ucode retries this by itself. If it keeps happening, check your connection, ' +
503
505
  'VPN and any corporate proxy (HTTPS_PROXY) — or /model to a lighter one, since ' +
@@ -0,0 +1,16 @@
1
+ /**
2
+ * version.js — the version, read from package.json rather than typed twice.
3
+ *
4
+ * package.json always ships in an npm package, so this is right wherever ucode
5
+ * is installed, and it can never drift from what `npm publish` released.
6
+ */
7
+
8
+ import { readFileSync } from 'node:fs';
9
+
10
+ export const VERSION = (() => {
11
+ try {
12
+ return JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8')).version;
13
+ } catch {
14
+ return '';
15
+ }
16
+ })();