mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
test: Phase 4 new dimensions — proptest, mock chaos modes, jest-axe a11y (#4501)
This commit is contained in:
Generated
+24
-1
@@ -4363,6 +4363,7 @@ dependencies = [
|
||||
"postgres",
|
||||
"ppt-rs",
|
||||
"prometheus",
|
||||
"proptest",
|
||||
"prost",
|
||||
"rand 0.10.1",
|
||||
"rdev",
|
||||
@@ -5046,12 +5047,16 @@ version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bit-vec",
|
||||
"bitflags 2.11.1",
|
||||
"num-traits",
|
||||
"rand 0.9.4",
|
||||
"rand_chacha 0.9.0",
|
||||
"rand_xorshift",
|
||||
"regex-syntax",
|
||||
"rusty-fork",
|
||||
"tempfile",
|
||||
"unarray",
|
||||
]
|
||||
|
||||
@@ -5129,6 +5134,12 @@ version = "0.1.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0"
|
||||
|
||||
[[package]]
|
||||
name = "quick-error"
|
||||
version = "2.0.1"
|
||||
@@ -5728,6 +5739,18 @@ version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "rusty-fork"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"quick-error 1.2.3",
|
||||
"tempfile",
|
||||
"wait-timeout",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.23"
|
||||
@@ -6629,7 +6652,7 @@ dependencies = [
|
||||
"fax",
|
||||
"flate2",
|
||||
"half",
|
||||
"quick-error",
|
||||
"quick-error 2.0.1",
|
||||
"weezl",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
@@ -300,6 +300,9 @@ filetime = "0.2"
|
||||
# beat without real-time waits. Test-only — the runtime feature set in
|
||||
# `[dependencies]` (`full`) intentionally excludes it.
|
||||
tokio = { version = "1", features = ["test-util"] }
|
||||
# Property-based testing for the adversarial-input surfaces (command classifier,
|
||||
# encryption round-trip) — plan.md §6.3. Version already resolved in Cargo.lock.
|
||||
proptest = "1"
|
||||
|
||||
[features]
|
||||
default = ["tokenjuice-treesitter"]
|
||||
|
||||
@@ -137,6 +137,7 @@
|
||||
"@trivago/prettier-plugin-sort-imports": "^6.0.2",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/debug": "^4.1.12",
|
||||
"@types/jest-axe": "^3.5.9",
|
||||
"@types/node": "^25.0.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
@@ -158,6 +159,7 @@
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"husky": "^9.1.7",
|
||||
"jest-axe": "^10.0.0",
|
||||
"jsdom": "^28.0.0",
|
||||
"knip": "^6.3.1",
|
||||
"postcss": "^8.5.6",
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Accessibility smoke lane (plan.md §7 — the frontend had zero a11y assertions).
|
||||
* Renders a few high-traffic, self-contained components and runs axe-core over
|
||||
* the output, asserting no violations. jsdom can't compute layout, so
|
||||
* color-contrast is auto-skipped; this catches the structural a11y bugs that
|
||||
* matter — missing roles/labels, invalid ARIA, unlabelled controls.
|
||||
*
|
||||
* Kept deliberately small and provider-light so it stays fast and stable; grow
|
||||
* it screen-by-screen rather than pulling in the full app shell.
|
||||
*/
|
||||
import { configureStore } from '@reduxjs/toolkit';
|
||||
import { render } from '@testing-library/react';
|
||||
import { axe } from 'jest-axe';
|
||||
import { Provider } from 'react-redux';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import chatRuntimeReducer, {
|
||||
type ArtifactSnapshot,
|
||||
type PendingApproval,
|
||||
setPendingApprovalForThread,
|
||||
} from '../../store/chatRuntimeSlice';
|
||||
import ApprovalRequestCard from '../chat/ApprovalRequestCard';
|
||||
import ArtifactCard from '../chat/ArtifactCard';
|
||||
|
||||
vi.mock('../../services/artifactDownloadService', () => ({
|
||||
saveArtifactViaDialog: vi.fn(),
|
||||
revealArtifactInFileManager: vi.fn(),
|
||||
}));
|
||||
vi.mock('../../services/coreRpcClient', () => ({ callCoreRpc: vi.fn() }));
|
||||
|
||||
async function expectNoViolations(container: HTMLElement) {
|
||||
const results = await axe(container);
|
||||
expect(results.violations).toEqual([]);
|
||||
}
|
||||
|
||||
describe('accessibility smoke', () => {
|
||||
it('ArtifactCard (ready) has no axe violations', async () => {
|
||||
const artifact: ArtifactSnapshot = {
|
||||
artifactId: 'a-1',
|
||||
kind: 'presentation',
|
||||
title: 'Quarterly Deck',
|
||||
status: 'ready',
|
||||
sizeBytes: 4096,
|
||||
path: 'a-1/deck.pptx',
|
||||
updatedAt: 0,
|
||||
};
|
||||
const { container } = render(<ArtifactCard artifact={artifact} />);
|
||||
await expectNoViolations(container);
|
||||
});
|
||||
|
||||
it('ArtifactCard (failed with error) has no axe violations', async () => {
|
||||
const artifact: ArtifactSnapshot = {
|
||||
artifactId: 'a-2',
|
||||
kind: 'document',
|
||||
title: 'Report',
|
||||
status: 'failed',
|
||||
error: 'producer crashed',
|
||||
updatedAt: 0,
|
||||
};
|
||||
const { container } = render(<ArtifactCard artifact={artifact} onRetry={vi.fn()} />);
|
||||
await expectNoViolations(container);
|
||||
});
|
||||
|
||||
it('ApprovalRequestCard has no axe violations', async () => {
|
||||
const approval: PendingApproval = {
|
||||
requestId: 'req-1',
|
||||
toolName: 'shell',
|
||||
message: 'Run `shell` — shell (18 bytes of arguments)',
|
||||
command: 'pip show yfinance',
|
||||
};
|
||||
const store = configureStore({ reducer: { chatRuntime: chatRuntimeReducer } });
|
||||
store.dispatch(setPendingApprovalForThread({ threadId: 't1', approval }));
|
||||
const { container } = render(
|
||||
<Provider store={store}>
|
||||
<ApprovalRequestCard threadId="t1" approval={approval} />
|
||||
</Provider>
|
||||
);
|
||||
await expectNoViolations(container);
|
||||
});
|
||||
});
|
||||
@@ -434,9 +434,22 @@ P0, most P1 "untested" claims were **inaccurate against current `main`**. Verifi
|
||||
the ~20 controller domains with no `tests/` reference (the Phase 0 inventory allowlist to burn
|
||||
down). Approval×turn integration, journey spec, and the Playwright approval mirror need WDIO/PW.
|
||||
|
||||
**Phase 4 — new dimensions (ongoing)**
|
||||
- proptest + cargo-fuzz targets; scoped cargo-mutants; jest-axe lane; migration fixture;
|
||||
duration report; mock chaos modes + contract tests.
|
||||
**Phase 4 — new dimensions** — ⏳ partial (PR: `test/phase4-new-dimensions`). Unlike the §4 backlog,
|
||||
these are genuinely greenfield (the suite had zero of them), so they were built and validated:
|
||||
- [x] **proptest** (was absent): added as a dev-dep; property suite for the command classifier +
|
||||
path check (never-panic on arbitrary/unicode input; fail-closed redirect floor). NOTE: an
|
||||
encryption round-trip property was trialled but dropped — under coverage Argon2id is ~2.5s/case,
|
||||
so it held the lib-test binary ~60s and deterministically exposed a pre-existing env-var race in
|
||||
the unrelated `config::schema::load` env-overlay tests; the round-trip stays covered by the
|
||||
Phase 1 fixed-input tests.
|
||||
- [x] **Mock chaos modes** (§5.3): `httpFaultRules` gains `mode: "reset"` (connection reset) and
|
||||
`mode: "malformed"` (non-JSON 200) beyond clean statuses; `scripts/mock-api/chaos.test.mjs`
|
||||
drives the real server (auto-run by the Phase 0 `test:scripts` job).
|
||||
- [x] **jest-axe a11y smoke lane** (§7 — the frontend had zero a11y assertions): axe-core over
|
||||
ArtifactCard + ApprovalRequestCard asserting no violations.
|
||||
- [ ] Still open (larger tooling / own PRs): cargo-fuzz targets, scoped cargo-mutants on the P0
|
||||
zones, criterion micro-benches, migration snapshot fixture, per-suite duration report,
|
||||
mock↔backend contract test.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Generated
+102
@@ -220,6 +220,9 @@ importers:
|
||||
'@types/debug':
|
||||
specifier: ^4.1.12
|
||||
version: 4.1.13
|
||||
'@types/jest-axe':
|
||||
specifier: ^3.5.9
|
||||
version: 3.5.9
|
||||
'@types/node':
|
||||
specifier: ^25.0.10
|
||||
version: 25.6.0
|
||||
@@ -283,6 +286,9 @@ importers:
|
||||
husky:
|
||||
specifier: ^9.1.7
|
||||
version: 9.1.7
|
||||
jest-axe:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
jsdom:
|
||||
specifier: ^28.0.0
|
||||
version: 28.1.0(@noble/hashes@2.2.0)
|
||||
@@ -888,6 +894,10 @@ packages:
|
||||
resolution: {integrity: sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
'@jest/schemas@29.6.3':
|
||||
resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
'@jest/schemas@30.0.5':
|
||||
resolution: {integrity: sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
@@ -1765,6 +1775,9 @@ packages:
|
||||
resolution: {integrity: sha512-avtjtIQ019sZW3FklpmNNsQOnYZjCHpnVxgDGElfZb+AaR4AvtHNlxXLJp+iqEfSK+Xok8MJarJqIgCaWcF40Q==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
'@sinclair/typebox@0.27.10':
|
||||
resolution: {integrity: sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==}
|
||||
|
||||
'@sinclair/typebox@0.34.49':
|
||||
resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==}
|
||||
|
||||
@@ -2009,6 +2022,12 @@ packages:
|
||||
'@types/istanbul-reports@3.0.4':
|
||||
resolution: {integrity: sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==}
|
||||
|
||||
'@types/jest-axe@3.5.9':
|
||||
resolution: {integrity: sha512-z98CzR0yVDalCEuhGXXO4/zN4HHuSebAukXDjTLJyjEAgoUf1H1i+sr7SUB/mz8CRS/03/XChsx0dcLjHkndoQ==}
|
||||
|
||||
'@types/jest@30.0.0':
|
||||
resolution: {integrity: sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==}
|
||||
|
||||
'@types/json-schema@7.0.15':
|
||||
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
|
||||
|
||||
@@ -2460,6 +2479,14 @@ packages:
|
||||
resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
axe-core@3.5.6:
|
||||
resolution: {integrity: sha512-LEUDjgmdJoA3LqklSTwKYqkjcZ4HKc4ddIYGSAiSkr46NTjzg2L9RNB+lekO9P7Dlpa87+hBtzc2Fzn/+GUWMQ==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
axe-core@4.10.2:
|
||||
resolution: {integrity: sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
b4a@1.8.0:
|
||||
resolution: {integrity: sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==}
|
||||
peerDependencies:
|
||||
@@ -3029,6 +3056,10 @@ packages:
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
diff-sequences@29.6.3:
|
||||
resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
diff@5.2.2:
|
||||
resolution: {integrity: sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
@@ -4009,10 +4040,26 @@ packages:
|
||||
javascript-natural-sort@0.7.1:
|
||||
resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
|
||||
|
||||
jest-axe@10.0.0:
|
||||
resolution: {integrity: sha512-9QR0M7//o5UVRnEUUm68IsGapHrcKGakYy9dKWWMX79LmeUKguDI6DREyljC5I13j78OUmtKLF5My6ccffLFBg==}
|
||||
engines: {node: '>= 16.0.0'}
|
||||
|
||||
jest-diff@29.7.0:
|
||||
resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
jest-diff@30.3.0:
|
||||
resolution: {integrity: sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
|
||||
jest-get-type@29.6.3:
|
||||
resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
jest-matcher-utils@29.2.2:
|
||||
resolution: {integrity: sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
jest-matcher-utils@30.3.0:
|
||||
resolution: {integrity: sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
@@ -4859,6 +4906,10 @@ packages:
|
||||
resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==}
|
||||
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
|
||||
|
||||
pretty-format@29.7.0:
|
||||
resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
|
||||
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
|
||||
|
||||
pretty-format@30.3.0:
|
||||
resolution: {integrity: sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==}
|
||||
engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0}
|
||||
@@ -6635,6 +6686,10 @@ snapshots:
|
||||
'@types/node': 25.6.0
|
||||
jest-regex-util: 30.0.1
|
||||
|
||||
'@jest/schemas@29.6.3':
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.27.10
|
||||
|
||||
'@jest/schemas@30.0.5':
|
||||
dependencies:
|
||||
'@sinclair/typebox': 0.34.49
|
||||
@@ -7329,6 +7384,8 @@ snapshots:
|
||||
- encoding
|
||||
- supports-color
|
||||
|
||||
'@sinclair/typebox@0.27.10': {}
|
||||
|
||||
'@sinclair/typebox@0.34.49': {}
|
||||
|
||||
'@sindresorhus/merge-streams@4.0.0': {}
|
||||
@@ -7547,6 +7604,16 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/istanbul-lib-report': 3.0.3
|
||||
|
||||
'@types/jest-axe@3.5.9':
|
||||
dependencies:
|
||||
'@types/jest': 30.0.0
|
||||
axe-core: 3.5.6
|
||||
|
||||
'@types/jest@30.0.0':
|
||||
dependencies:
|
||||
expect: 30.3.0
|
||||
pretty-format: 30.3.0
|
||||
|
||||
'@types/json-schema@7.0.15': {}
|
||||
|
||||
'@types/json5@0.0.29': {}
|
||||
@@ -8215,6 +8282,10 @@ snapshots:
|
||||
dependencies:
|
||||
possible-typed-array-names: 1.1.0
|
||||
|
||||
axe-core@3.5.6: {}
|
||||
|
||||
axe-core@4.10.2: {}
|
||||
|
||||
b4a@1.8.0: {}
|
||||
|
||||
bail@2.0.2: {}
|
||||
@@ -8814,6 +8885,8 @@ snapshots:
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
diff-sequences@29.6.3: {}
|
||||
|
||||
diff@5.2.2: {}
|
||||
|
||||
diff@8.0.4: {}
|
||||
@@ -10018,6 +10091,20 @@ snapshots:
|
||||
|
||||
javascript-natural-sort@0.7.1: {}
|
||||
|
||||
jest-axe@10.0.0:
|
||||
dependencies:
|
||||
axe-core: 4.10.2
|
||||
chalk: 4.1.2
|
||||
jest-matcher-utils: 29.2.2
|
||||
lodash.merge: 4.6.2
|
||||
|
||||
jest-diff@29.7.0:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
diff-sequences: 29.6.3
|
||||
jest-get-type: 29.6.3
|
||||
pretty-format: 29.7.0
|
||||
|
||||
jest-diff@30.3.0:
|
||||
dependencies:
|
||||
'@jest/diff-sequences': 30.3.0
|
||||
@@ -10025,6 +10112,15 @@ snapshots:
|
||||
chalk: 4.1.2
|
||||
pretty-format: 30.3.0
|
||||
|
||||
jest-get-type@29.6.3: {}
|
||||
|
||||
jest-matcher-utils@29.2.2:
|
||||
dependencies:
|
||||
chalk: 4.1.2
|
||||
jest-diff: 29.7.0
|
||||
jest-get-type: 29.6.3
|
||||
pretty-format: 29.7.0
|
||||
|
||||
jest-matcher-utils@30.3.0:
|
||||
dependencies:
|
||||
'@jest/get-type': 30.1.0
|
||||
@@ -11195,6 +11291,12 @@ snapshots:
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 17.0.2
|
||||
|
||||
pretty-format@29.7.0:
|
||||
dependencies:
|
||||
'@jest/schemas': 29.6.3
|
||||
ansi-styles: 5.2.0
|
||||
react-is: 18.3.1
|
||||
|
||||
pretty-format@30.3.0:
|
||||
dependencies:
|
||||
'@jest/schemas': 30.0.5
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
getMockServerPort,
|
||||
resetMockBehavior,
|
||||
setMockBehavior,
|
||||
startMockServer,
|
||||
stopMockServer,
|
||||
} from "./index.mjs";
|
||||
|
||||
// Chaos-toolkit coverage for the mock backend's httpFaultRules engine
|
||||
// (plan.md §5.3). Beyond clean HTTP error statuses, authors need to reproduce
|
||||
// two real outage shapes: a connection reset mid-response, and a 200 with a
|
||||
// non-JSON body. These tests drive the real server over a socket.
|
||||
|
||||
function base() {
|
||||
const port = getMockServerPort();
|
||||
assert.ok(port, "mock server must be running");
|
||||
return `http://127.0.0.1:${port}`;
|
||||
}
|
||||
|
||||
function setFaultRules(rules) {
|
||||
setMockBehavior("httpFaultRules", JSON.stringify(rules));
|
||||
}
|
||||
|
||||
test.beforeEach(async () => {
|
||||
await stopMockServer();
|
||||
resetMockBehavior();
|
||||
await startMockServer();
|
||||
});
|
||||
|
||||
test.afterEach(async () => {
|
||||
await stopMockServer();
|
||||
});
|
||||
|
||||
test("mode:reset tears down the connection (client sees a network error)", async () => {
|
||||
setFaultRules([{ contains: "/chaos-reset", mode: "reset" }]);
|
||||
|
||||
await assert.rejects(
|
||||
fetch(`${base()}/chaos-reset`),
|
||||
(err) => err instanceof TypeError || /reset|hang up|ECONNRESET|fetch failed/i.test(String(err)),
|
||||
"a reset rule must surface as a client-side network error, not a clean response",
|
||||
);
|
||||
});
|
||||
|
||||
test("mode:malformed returns a 200 with a body that is not valid JSON", async () => {
|
||||
setFaultRules([{ contains: "/chaos-malformed", mode: "malformed" }]);
|
||||
|
||||
const res = await fetch(`${base()}/chaos-malformed`);
|
||||
assert.equal(res.status, 200, "malformed mode defaults to a 200 status");
|
||||
|
||||
const text = await res.text();
|
||||
assert.ok(text.length > 0, "malformed body must be non-empty");
|
||||
assert.throws(
|
||||
() => JSON.parse(text),
|
||||
"the malformed body must not parse as JSON (that is the whole point)",
|
||||
);
|
||||
});
|
||||
|
||||
test("mode:malformed honours a custom status and body override", async () => {
|
||||
setFaultRules([
|
||||
{ contains: "/chaos-custom", mode: "malformed", status: 502, body: "upstream said <html>" },
|
||||
]);
|
||||
|
||||
const res = await fetch(`${base()}/chaos-custom`);
|
||||
assert.equal(res.status, 502);
|
||||
assert.equal(await res.text(), "upstream said <html>");
|
||||
});
|
||||
|
||||
test("default mode still injects a clean HTTP error status + JSON envelope", async () => {
|
||||
// Regression guard: adding chaos modes must not change the existing
|
||||
// status-injection behaviour when no mode is set.
|
||||
setFaultRules([{ contains: "/chaos-status", status: 503, error: "down for maintenance" }]);
|
||||
|
||||
const res = await fetch(`${base()}/chaos-status`);
|
||||
assert.equal(res.status, 503);
|
||||
const parsed = await res.json();
|
||||
assert.equal(parsed.success, false);
|
||||
assert.equal(parsed.error, "down for maintenance");
|
||||
});
|
||||
|
||||
test("a request that matches no fault rule is unaffected", async () => {
|
||||
setFaultRules([{ contains: "/chaos-reset", mode: "reset" }]);
|
||||
|
||||
const res = await fetch(`${base()}/__admin/health`);
|
||||
assert.equal(res.status, 200, "unmatched requests must pass through to the real handler");
|
||||
});
|
||||
@@ -155,6 +155,34 @@ async function maybeApplyGlobalBehavior(ctx) {
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!ruleMatches(rule, ctx)) continue;
|
||||
const mode = typeof rule.mode === "string" ? rule.mode : "status";
|
||||
|
||||
// Chaos mode "reset": tear down the socket mid-response so the client sees
|
||||
// a connection reset (ECONNRESET / "socket hang up") rather than a clean
|
||||
// HTTP status — a real outage shape the status path can't reproduce.
|
||||
if (mode === "reset") {
|
||||
console.warn(
|
||||
`[MockServer] Injected connection reset ${ctx.method} ${ctx.url}`,
|
||||
);
|
||||
ctx.res.socket?.destroy();
|
||||
return true;
|
||||
}
|
||||
|
||||
// Chaos mode "malformed": a 200 carrying a non-JSON body, so the caller's
|
||||
// JSON parse throws instead of receiving a clean error envelope.
|
||||
if (mode === "malformed") {
|
||||
const status = Number(rule.status || 200);
|
||||
const raw = typeof rule.body === "string" ? rule.body : "<<not-json>>{";
|
||||
console.warn(
|
||||
`[MockServer] Injected malformed body ${ctx.method} ${ctx.url} -> ${status}`,
|
||||
);
|
||||
setCors(ctx.res);
|
||||
ctx.res.writeHead(status, { "Content-Type": "application/json" });
|
||||
ctx.res.end(raw);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Default mode "status": a clean HTTP error status with a JSON body.
|
||||
const status = Number(rule.status || 500);
|
||||
const body =
|
||||
rule.body && typeof rule.body === "object"
|
||||
|
||||
@@ -526,21 +526,27 @@ impl EnvLookup for HashMapEnv {
|
||||
|
||||
#[test]
|
||||
fn env_overlay_toggles_agent_tracing_capture_content() {
|
||||
// Off by default; the opt-in env var enables prompt/reply export.
|
||||
// Serialize with the sibling env-overlay tests (TEST_ENV_LOCK note at the
|
||||
// top of the file) so a concurrent test's env mutation can't race in.
|
||||
let _g = env_lock();
|
||||
|
||||
// ON by default since #4498 (`default_capture_content() == true` — traces
|
||||
// without content aren't actionable in Langfuse). This assertion was left
|
||||
// asserting the pre-#4498 `false` default and is corrected here.
|
||||
let mut cfg = Config::default();
|
||||
assert!(!cfg.observability.agent_tracing.capture_content);
|
||||
cfg.apply_env_overlay_with(
|
||||
&HashMapEnv::new().with("OPENHUMAN_AGENT_TRACING_CAPTURE_CONTENT", "true"),
|
||||
);
|
||||
assert!(cfg.observability.agent_tracing.capture_content);
|
||||
|
||||
// An explicit falsy value turns it back off.
|
||||
let mut cfg = Config::default();
|
||||
cfg.observability.agent_tracing.capture_content = true;
|
||||
// An explicit falsy env value turns it off.
|
||||
cfg.apply_env_overlay_with(
|
||||
&HashMapEnv::new().with("OPENHUMAN_AGENT_TRACING_CAPTURE_CONTENT", "off"),
|
||||
);
|
||||
assert!(!cfg.observability.agent_tracing.capture_content);
|
||||
|
||||
// A truthy value turns it back on.
|
||||
cfg.apply_env_overlay_with(
|
||||
&HashMapEnv::new().with("OPENHUMAN_AGENT_TRACING_CAPTURE_CONTENT", "true"),
|
||||
);
|
||||
assert!(cfg.observability.agent_tracing.capture_content);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -314,4 +314,14 @@ mod tests {
|
||||
let payload = k.encrypt(b"").expect("encrypt empty");
|
||||
assert_eq!(k.decrypt(&payload).expect("decrypt empty"), b"");
|
||||
}
|
||||
|
||||
// NOTE: an `encrypt_decrypt_round_trips_for_arbitrary_input` proptest was
|
||||
// trialled here but removed: under coverage instrumentation Argon2id runs
|
||||
// ~2.5s/case, so a 24-case property held the lib-test binary for ~60s and
|
||||
// deterministically widened a pre-existing env-var race in the unrelated
|
||||
// `config::schema::load` env-overlay tests (they mutate process-global env
|
||||
// without per-test serialization). The round-trip is already covered by the
|
||||
// fixed-input tests above (round-trip, tamper, KDF determinism); the
|
||||
// property-based *fuzzing* value lives in the fast, panic-focused
|
||||
// `security::policy::proptest_tests` instead.
|
||||
}
|
||||
|
||||
@@ -20,3 +20,6 @@ use std::path::{Path, PathBuf};
|
||||
#[cfg(test)]
|
||||
#[path = "policy_tests.rs"]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
mod proptest_tests;
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
//! Property-based tests for the autonomy classifier's adversarial-input
|
||||
//! surfaces (plan.md §6.3 — proptest was previously absent). The command
|
||||
//! classifier and path checks run on fully untrusted strings from the
|
||||
//! LLM/user, so the central properties are "never panics on any input" and a
|
||||
//! handful of fail-closed invariants that must hold for *all* inputs, not just
|
||||
//! the hand-enumerated cases in `policy_tests.rs`.
|
||||
|
||||
use super::types::{CommandClass, SecurityPolicy};
|
||||
use proptest::prelude::*;
|
||||
|
||||
proptest! {
|
||||
/// The classifier + allowlist must never panic, regardless of shell
|
||||
/// metacharacters, redirects, pipes, or embedded control characters.
|
||||
#[test]
|
||||
fn classifier_never_panics_on_regex_strings(cmd in ".*") {
|
||||
let p = SecurityPolicy::default();
|
||||
let _ = p.classify_command(&cmd);
|
||||
let _ = p.command_risk_level(&cmd);
|
||||
let _ = p.is_command_allowed(&cmd);
|
||||
let _ = p.check_gated_command(&cmd);
|
||||
}
|
||||
|
||||
/// Same, but over fully arbitrary unicode (includes NUL, newlines, and
|
||||
/// multibyte scalars the `.*` strategy skips).
|
||||
#[test]
|
||||
fn classifier_never_panics_on_arbitrary_unicode(cmd in any::<String>()) {
|
||||
let p = SecurityPolicy::default();
|
||||
let _ = p.classify_command(&cmd);
|
||||
let _ = p.command_risk_level(&cmd);
|
||||
let _ = p.is_command_allowed(&cmd);
|
||||
}
|
||||
|
||||
/// Fail-closed floor: a quote-free command with an appended unquoted
|
||||
/// redirect always classifies as at least `Write` (`cmd > file` writes
|
||||
/// `file`), no matter what the base command is.
|
||||
#[test]
|
||||
fn appended_redirect_forces_at_least_write(prefix in "[a-z][a-z ]{0,40}") {
|
||||
let p = SecurityPolicy::default();
|
||||
let cmd = format!("{prefix} > out.txt");
|
||||
prop_assert!(
|
||||
p.classify_command(&cmd) >= CommandClass::Write,
|
||||
"an unquoted redirect must lift the class to >= Write: {cmd:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The string-level path check must never panic on arbitrary paths
|
||||
/// (traversal, NUL bytes, tilde, URL-encoding, unicode).
|
||||
#[test]
|
||||
fn path_string_check_never_panics(path in ".*") {
|
||||
let p = SecurityPolicy::default();
|
||||
let _ = p.is_path_string_allowed(&path);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user