Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Package-specific guides:
| Build plugin | `cd nemoclaw && npm run build` |
| Watch mode | `cd nemoclaw && npm run dev` |
| Run all tests | `npm test` |
| Render behavior-oriented test tree | `npm run test:spec` |
| Run fast source tests | `npm run test:fast` |
| Run integration tests | `npm run test:integration` |
| Run package contracts | `npm run test:package` |
Expand Down Expand Up @@ -87,6 +88,7 @@ When writing tests:
- Plugin tests use TypeScript and are co-located with their source files
- Import CLI source from ordinary tests. Put genuine compiled-artifact assertions under `test/package-contract/`.
- Keep project globs disjoint; `npm run test:projects:check` derives membership from Vitest and rejects overlap.
- Write behavior-oriented titles, put local issue references in a final `(#1234)` suffix, and use `npm run test:spec` for the hierarchical specification view.
- Mock external dependencies; don't call real NVIDIA APIs in unit tests
- E2E tests run on ephemeral Brev cloud instances

Expand Down
13 changes: 13 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ These are the primary `make` and `npm` targets for day-to-day development:
| `make format` | Auto-format TypeScript and Python source |
| `npm run typecheck:cli` | Type-check CLI TypeScript using `tsconfig.cli.json` (`bin/`, `scripts/`, `src/`, `test/`, `nemoclaw-blueprint/scripts/`) |
| `npm test` | Build package artifacts and run every non-live Vitest project |
| `npm run test:spec` | Run every non-live test with hierarchical behavior-oriented output |
| `npm run test:fast` | Clean `dist/` and run source CLI, plugin, and E2E-support tests |
| `npm run test:integration` | Clean-build the CLI and run root integration and installer tests |
| `npm run test:package` | Clean-build CLI/plugin artifacts and run compiled-package contracts |
Expand All @@ -132,6 +133,18 @@ These are the primary `make` and `npm` targets for day-to-day development:
| `npm run docs:deps` | Print the pinned Fern CLI version used by docs commands |
| `npx prek run --all-files` | Run all hooks from `.pre-commit-config.yaml` — see below |

### Test Titles as Behavioral Documentation

Write `describe` and `it` titles so the Vitest tree reads as behavioral documentation. Start test
titles with behavior or context rather than issue numbers, flags, or scenario labels, and put local
issue references in a final suffix such as `(#1234)`. Prefer
`it("reticulates splines correctly (#1234)")` over
`it("#1234 fixes spline reticulation")`.

Run `npm run test:spec` to render the suite with Vitest's hierarchical tree reporter. Run
`npm run test:titles:check` to enforce the objective title-shape conventions without attempting to
lint subjective English grammar.

### Git hooks (prek)

All git hooks are managed by [prek](https://prek.j178.dev/), a fast, single-binary pre-commit hook runner installed as a devDependency (`@j178/prek`). The `npm install` step runs `prek install` automatically via the `prepare` script, which wires up the following hooks from [`.pre-commit-config.yaml`](.pre-commit-config.yaml):
Expand Down
4 changes: 2 additions & 2 deletions nemoclaw/src/blueprint/ssrf.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ describe("isPrivateIp – CIDR boundary precision", () => {
["128.0.0.0", false], // just above 127.0.0.0/8
["192.167.255.255", false], // just below 192.168.0.0/16
["192.169.0.0", false], // just above 192.168.0.0/16
])("boundary %s private=%s", (ip, expected) => {
])("classifies boundary address %s as private=%s", (ip, expected) => {
expect(isPrivateIp(ip)).toBe(expected);
});
});
Expand All @@ -277,7 +277,7 @@ describe("isPrivateIp – IPv6 edge cases", () => {
["fd00::0", true], // first address in fd00::/8 (within fc00::/7)
["fdff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", true], // last address in fc00::/7 ULA range
["fe00::1", false], // just above fc00::/7 (link-local starts at fe80::)
])("IPv6 %s private=%s", (ip, expected) => {
])("classifies IPv6 address %s as private=%s", (ip, expected) => {
expect(isPrivateIp(ip)).toBe(expected);
});

Expand Down
50 changes: 25 additions & 25 deletions nemoclaw/src/security/secret-scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,132 +29,132 @@ const FAKE = {
};

describe("scanForSecrets", () => {
describe("detects known secret patterns", () => {
it("NVIDIA API key", () => {
describe("known secret patterns", () => {
it("detects an NVIDIA API key", () => {
const matches = scanForSecrets(`my key is ${FAKE.nvidia}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("NVIDIA API key");
});

it("OpenAI API key", () => {
it("detects an OpenAI API key", () => {
const matches = scanForSecrets(`export OPENAI_API_KEY=${FAKE.openai}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("OpenAI API key");
});

it("OpenAI project API key", () => {
it("detects an OpenAI project API key", () => {
const matches = scanForSecrets(`export OPENAI_API_KEY=${FAKE.openaiProject}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("OpenAI API key");
});

it("GitHub personal access token", () => {
it("detects a GitHub personal access token", () => {
const matches = scanForSecrets(`token: ${FAKE.github}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("GitHub token");
});

it("AWS access key", () => {
it("detects an AWS access key", () => {
const matches = scanForSecrets(`aws_access_key_id = ${FAKE.aws}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("AWS access key");
});

it("Slack bot token", () => {
it("detects a Slack bot token", () => {
const matches = scanForSecrets(`SLACK_TOKEN=${FAKE.slack}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("Slack token");
});

it("Slack app token", () => {
it("detects a Slack app token", () => {
const matches = scanForSecrets(`SLACK_APP_TOKEN=${FAKE.slackApp}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("Slack token");
});

it("npm token", () => {
it("detects an npm token", () => {
const matches = scanForSecrets(`//registry.npmjs.org/:_authToken=${FAKE.npm}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("npm token");
});

it("private key (PEM RSA)", () => {
it("detects a PEM RSA private key", () => {
const matches = scanForSecrets(FAKE.pemRsa);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("Private key");
});

it("private key (OpenSSH)", () => {
it("detects an OpenSSH private key", () => {
const matches = scanForSecrets(FAKE.pemOpenssh);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("Private key");
});

it("Telegram bot token", () => {
it("detects a Telegram bot token", () => {
const matches = scanForSecrets(`bot token: ${FAKE.telegram}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("Telegram bot token");
});

it("Google API key", () => {
it("detects a Google API key", () => {
const matches = scanForSecrets(`GOOGLE_API_KEY=${FAKE.google}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("Google API key");
});

it("Anthropic API key", () => {
it("detects an Anthropic API key", () => {
const matches = scanForSecrets(`key: ${FAKE.anthropic}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("Anthropic API key");
});

it("HuggingFace token", () => {
it("detects a HuggingFace token", () => {
const matches = scanForSecrets(`HF_TOKEN=${FAKE.huggingface}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("HuggingFace token");
});

it("Discord bot token", () => {
it("detects a Discord bot token", () => {
const matches = scanForSecrets(`DISCORD_TOKEN=${FAKE.discord}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("Discord bot token");
});

it("AWS secret key", () => {
it("detects an AWS secret key", () => {
const matches = scanForSecrets(`aws_secret_access_key = ${FAKE.awsSecret}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("AWS secret key");
});

it("Authorization header", () => {
it("detects an Authorization header", () => {
const matches = scanForSecrets(`Authorization: Bearer ${FAKE.authHeader}`);
expect(matches).toHaveLength(1);
expect(matches[0].pattern).toBe("Authorization header");
});
});

describe("does not false-positive on safe content", () => {
it("normal markdown text", () => {
describe("safe content", () => {
it("allows normal markdown text", () => {
expect(scanForSecrets("# My Project\n\nThis is a regular markdown file.")).toHaveLength(0);
});

it("code blocks without secrets", () => {
it("allows code blocks without secrets", () => {
expect(scanForSecrets("```python\nprint('hello world')\n```")).toHaveLength(0);
});

it("short tokens that don't meet minimum length", () => {
it("ignores short tokens below the minimum length", () => {
expect(scanForSecrets("sk-short")).toHaveLength(0);
});

it("URLs with path segments", () => {
it("allows URLs with path segments", () => {
expect(scanForSecrets("https://github.com/NVIDIA/NemoClaw/pull/1121")).toHaveLength(0);
});

it("UUIDs", () => {
it("allows UUIDs", () => {
expect(scanForSecrets("id: 550e8400-e29b-41d4-a716-446655440000")).toHaveLength(0);
});

it("git commit hashes", () => {
it("allows git commit hashes", () => {
expect(scanForSecrets("commit 24a8b5a3f1e2d3c4b5a6f7e8d9c0b1a2")).toHaveLength(0);
});
});
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,14 @@
"scripts": {
"preinstall": "node scripts/check-node-version.js",
"test": "npm run clean:cli && npm --prefix nemoclaw run clean && npm run build:cli && npm --prefix nemoclaw run build && vitest run --project cli --project integration --project installer-integration --project package-contract --project plugin --project e2e-vitest-support",
"test:spec": "npm test -- --reporter=tree",
"test:fast": "npm run clean:cli && vitest run --project cli --project plugin --project e2e-vitest-support",
"test:integration": "npm run clean:cli && npm run build:cli && vitest run --project integration --project installer-integration",
"test:package": "npm run clean:cli && npm --prefix nemoclaw run clean && npm run build:cli && npm --prefix nemoclaw run build && vitest run --project package-contract",
"test:live-e2e": "NEMOCLAW_RUN_E2E_SCENARIOS=1 vitest run --project e2e-scenarios-live",
"test:imports:check": "tsx scripts/checks/no-test-dist-imports.ts",
"test:projects:check": "tsx scripts/checks/vitest-project-overlap.ts",
"test:titles:check": "tsx scripts/checks/test-title-style.ts",
"check": "npx prek run --all-files",
"checks": "tsx scripts/checks/run.ts",
"lint": "npx @biomejs/biome lint . && npm run checks",
Expand Down
5 changes: 5 additions & 0 deletions scripts/checks/run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ const CHECKS: readonly CheckCommand[] = [
command: TSX,
args: ["scripts/checks/vitest-project-overlap.ts"],
},
{
name: "test-title-style",
command: TSX,
args: ["scripts/checks/test-title-style.ts"],
},
];

function main(): void {
Expand Down
Loading
Loading