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.
- package/README.md +44 -21
- package/package.json +3 -4
- package/skills/ai-features/SKILL.md +140 -0
- package/skills/build-app/SKILL.md +137 -57
- package/skills/code-review/SKILL.md +69 -24
- package/skills/debug/SKILL.md +73 -31
- package/skills/performance/SKILL.md +84 -0
- package/skills/refactor/SKILL.md +72 -0
- package/skills/security/SKILL.md +110 -0
- package/skills/ui-ux/SKILL.md +256 -193
- package/skills/write-tests/SKILL.md +72 -34
- package/src/core/loop.js +3 -2
- package/src/core/provider.js +21 -19
- package/src/core/version.js +16 -0
- package/src/ui/screen.js +1187 -1067
- package/ucode.js +3 -4
|
@@ -1,47 +1,85 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: write-tests
|
|
3
|
-
description: Write tests that
|
|
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
|
|
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
|
-
##
|
|
12
|
+
## 1. Find how this project tests
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
##
|
|
22
|
+
## 2. Pick the right level for each behaviour
|
|
19
23
|
|
|
20
|
-
|
|
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
|
-
-
|
|
24
|
-
- null
|
|
25
|
-
-
|
|
26
|
-
|
|
27
|
-
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
1057
|
-
else this.ui.
|
|
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
|
}
|
package/src/core/provider.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
187
|
-
failed: '
|
|
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
|
|
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
|
-
'
|
|
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: `
|
|
354
|
+
failed: `The API key was rejected (HTTP ${status ?? 401}).`,
|
|
353
355
|
fix:
|
|
354
|
-
'Check
|
|
355
|
-
'is still active
|
|
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
|
|
384
|
-
'
|
|
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: `
|
|
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
|
-
: `
|
|
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
|
|
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
|
+
})();
|