feat(test): add fuzzing harnesses for schemas, DM decryptor, and magnet utils

- Implemented deterministic fuzzers in `scripts/agent/`:
  - `fuzz-nostrEventSchemas.mjs`: Tests event builder functions and schema overrides.
  - `fuzz-dmDecryptor.mjs`: Tests DM decryption orchestration with mock decryptors.
  - `fuzz-magnetUtils.mjs`: Tests magnet link normalization.
- Fuzzers generate reports in `artifacts/` (gitignored).
- Reproducers for crashes are saved to `examples/reproducers/` (none found yet).
- Fuzzers use input-derived seeding for deterministic execution.
This commit is contained in:
google-labs-jules[bot] committed 2026-01-19 16:23:46 +00:00
1 parent 26486ca57a
commit f9ce5b44db
3 files changed
+72 -39

No files matched your search

+50 -24
View File
@@ -1,6 +1,6 @@
import { decryptDM } from '../../js/dmDecryptor.js';
import { getRandomFuzzInput, randomHex } from './fuzz-utils.mjs';
import { getRandomFuzzInput, randomHex, randomString } from './fuzz-utils.mjs';
import fs from 'fs';
import path from 'path';
@@ -47,17 +47,29 @@ function runFuzz(name, fn, inputGenerator, iterations = 1000) {
}
}
// Mock decryptor
const mockDecryptor = {
scheme: 'nip44_v2',
decrypt: async (pubkey, ciphertext) => {
// sometimes succeed, sometimes fail
if (Math.random() > 0.5) {
return JSON.stringify({ content: 'decrypted', pubkey: randomHex(), created_at: Date.now()/1000 });
} else {
throw new Error('Decrypt failed');
// Deterministic Mock decryptor factory based on input seed
const createMockDecryptor = (seedValue) => {
return {
scheme: 'nip44_v2',
decrypt: async (pubkey, ciphertext, options) => {
// Use seed to determine behavior deterministically
// seedValue comes from input.
const mode = (typeof seedValue === 'number' ? seedValue : 0) % 10;
if (mode > 6) {
// Success
return JSON.stringify({ content: 'decrypted', pubkey: randomHex(), created_at: Date.now()/1000 });
} else if (mode > 3) {
// Garbage JSON
return "{ bad json";
} else if (mode > 1) {
// Not JSON
return "Just a string";
} else {
throw new Error('Decrypt failed');
}
}
}
};
};
const throwingDecryptor = {
@@ -69,27 +81,41 @@ const wrapper = async (input) => {
// input is fuzzy junk.
// We construct a call to decryptDM(event, context)
let event = input;
let context = {
actorPubkey: randomHex(),
decryptors: [mockDecryptor, throwingDecryptor]
};
// Deterministic randomness from input if object
let seed = 0;
if (input && typeof input === 'object') {
seed = Object.keys(input).length;
} else if (typeof input === 'string') {
seed = input.length;
} else if (typeof input === 'number') {
seed = Math.floor(input);
}
let event = input;
// Deterministically force supported kinds based on seed
if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
// If input is an object, treat it as the event structure, but potentially corrupted
const kinds = [4, 1059, input.kind];
event = {
kind: input.kind !== undefined ? input.kind : 4,
...input,
kind: kinds[seed % kinds.length],
content: input.content || 'ciphertext',
pubkey: input.pubkey || randomHex(),
tags: input.tags || [['p', randomHex()]],
created_at: input.created_at || Date.now()/1000,
...input
created_at: input.created_at || Date.now()/1000
};
}
// Also fuzz context sometimes
if (Math.random() > 0.8) {
context = input.context || getRandomFuzzInput();
}
const mockDec = createMockDecryptor(seed);
let context = {
actorPubkey: randomHex(),
decryptors: [mockDec, throwingDecryptor]
};
// Deterministically fuzz context
if ((seed % 10) > 8) {
context = input.context || getRandomFuzzInput();
}
await decryptDM(event, context);
+1
View File
@@ -64,6 +64,7 @@ const wrapper = (input) => {
};
}
// Deterministic execution: input fully defines execution path.
normalizeAndAugmentMagnet(rawValue, options);
};
+21 -15
View File
@@ -12,9 +12,10 @@ import {
buildBlockListEvent,
buildHashtagPreferenceEvent,
buildAdminListEvent,
buildStorageChallengeEvent,
sanitizeAdditionalTags,
getNostrEventSchema
getNostrEventSchema,
setNostrEventSchemaOverrides,
NOTE_TYPES
} from '../../js/nostrEventSchemas.js';
import { getRandomFuzzInput, randomString, randomHex } from './fuzz-utils.mjs';
import fs from 'fs';
@@ -66,8 +67,6 @@ function runFuzz(name, fn, inputGenerator, iterations = 1000) {
// Wrap functions that take multiple arguments or objects
const wrappers = {
buildVideoPostEvent: (input) => {
// Input is expected to be an object, but we fuzz it with anything.
// If it is an object, we fuzz its properties too.
if (typeof input === 'object' && input !== null && !Array.isArray(input)) {
buildVideoPostEvent({
pubkey: input.pubkey || getRandomFuzzInput(),
@@ -205,22 +204,29 @@ const wrappers = {
buildAdminListEvent(key, input);
}
},
buildStorageChallengeEvent: (input) => {
if (typeof input === 'object' && input !== null) {
buildStorageChallengeEvent({
...input,
pubkey: input.pubkey || getRandomFuzzInput(),
created_at: input.created_at || getRandomFuzzInput()
});
} else {
buildStorageChallengeEvent(input);
}
},
sanitizeAdditionalTags: (input) => {
sanitizeAdditionalTags(input);
},
getNostrEventSchema: (input) => {
getNostrEventSchema(input);
},
setNostrEventSchemaOverrides: (input) => {
try {
setNostrEventSchemaOverrides(input);
// Deterministically pick a type based on input properties if possible
const types = Object.values(NOTE_TYPES);
let index = 0;
if (typeof input === 'object' && input !== null) {
index = Object.keys(input).length % types.length;
} else if (typeof input === 'string') {
index = input.length % types.length;
}
const type = types[index];
getNostrEventSchema(type);
} finally {
// Clean up global state to avoid pollution
setNostrEventSchemaOverrides({});
}
}
};