vimonkey 0.2.2 → 0.2.4
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/dist/chaos/index.mjs +121 -0
- package/dist/fuzz/index.mjs +2 -0
- package/dist/index.mjs +37 -0
- package/dist/plugin.mjs +14 -0
- package/dist/test-fuzz-ZgFKEVq-.mjs +550 -0
- package/package.json +24 -9
- package/src/chaos/index.ts +0 -180
- package/src/env.ts +0 -63
- package/src/fuzz/context.ts +0 -59
- package/src/fuzz/gen.ts +0 -157
- package/src/fuzz/index.ts +0 -90
- package/src/fuzz/regression.ts +0 -115
- package/src/fuzz/shrink.ts +0 -115
- package/src/fuzz/test-fuzz.ts +0 -241
- package/src/index.ts +0 -37
- package/src/plugin.ts +0 -96
- package/src/random.ts +0 -181
package/src/fuzz/shrink.ts
DELETED
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shrinking - find minimal failing sequence via delta debugging
|
|
3
|
-
*
|
|
4
|
-
* Uses binary search to reduce a failing sequence to the smallest
|
|
5
|
-
* subsequence that still triggers the failure.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface ShrinkOptions {
|
|
9
|
-
/** Maximum shrinking attempts (default: 100) */
|
|
10
|
-
maxAttempts?: number
|
|
11
|
-
/** Minimum sequence length to try (default: 1) */
|
|
12
|
-
minLength?: number
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export interface ShrinkResult<T> {
|
|
16
|
-
/** Original sequence */
|
|
17
|
-
original: T[]
|
|
18
|
-
/** Shrunk (minimal) sequence */
|
|
19
|
-
shrunk: T[]
|
|
20
|
-
/** Number of shrinking attempts made */
|
|
21
|
-
attempts: number
|
|
22
|
-
/** Whether shrinking found a smaller sequence */
|
|
23
|
-
reduced: boolean
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Shrink a failing sequence to its minimal form
|
|
28
|
-
*
|
|
29
|
-
* Uses delta debugging algorithm:
|
|
30
|
-
* 1. Try removing first half - if still fails, keep only first half
|
|
31
|
-
* 2. Try removing second half - if still fails, keep only second half
|
|
32
|
-
* 3. Try removing individual elements from start/end
|
|
33
|
-
* 4. Repeat until no reduction possible
|
|
34
|
-
*
|
|
35
|
-
* @param sequence - The original failing sequence
|
|
36
|
-
* @param runTest - Function that returns true if sequence passes, false if fails
|
|
37
|
-
* @param options - Shrinking options
|
|
38
|
-
*/
|
|
39
|
-
export async function shrinkSequence<T>(
|
|
40
|
-
sequence: T[],
|
|
41
|
-
runTest: (seq: T[]) => Promise<boolean>,
|
|
42
|
-
options: ShrinkOptions = {},
|
|
43
|
-
): Promise<ShrinkResult<T>> {
|
|
44
|
-
const { maxAttempts = 100, minLength = 1 } = options
|
|
45
|
-
|
|
46
|
-
let current = [...sequence]
|
|
47
|
-
let attempts = 0
|
|
48
|
-
let changed = true
|
|
49
|
-
|
|
50
|
-
while (changed && attempts < maxAttempts && current.length > minLength) {
|
|
51
|
-
changed = false
|
|
52
|
-
|
|
53
|
-
// Try removing first half
|
|
54
|
-
if (current.length > 1) {
|
|
55
|
-
const half = Math.ceil(current.length / 2)
|
|
56
|
-
const secondHalf = current.slice(half)
|
|
57
|
-
attempts++
|
|
58
|
-
|
|
59
|
-
if (secondHalf.length >= minLength && !(await runTest(secondHalf))) {
|
|
60
|
-
// Second half alone still fails
|
|
61
|
-
current = secondHalf
|
|
62
|
-
changed = true
|
|
63
|
-
continue
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Try removing second half
|
|
68
|
-
if (current.length > 1) {
|
|
69
|
-
const half = Math.floor(current.length / 2)
|
|
70
|
-
const firstHalf = current.slice(0, half)
|
|
71
|
-
attempts++
|
|
72
|
-
|
|
73
|
-
if (firstHalf.length >= minLength && !(await runTest(firstHalf))) {
|
|
74
|
-
// First half alone still fails
|
|
75
|
-
current = firstHalf
|
|
76
|
-
changed = true
|
|
77
|
-
continue
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Try removing individual elements
|
|
82
|
-
for (let i = 0; i < current.length && attempts < maxAttempts; i++) {
|
|
83
|
-
const without = [...current.slice(0, i), ...current.slice(i + 1)]
|
|
84
|
-
attempts++
|
|
85
|
-
|
|
86
|
-
if (without.length >= minLength && !(await runTest(without))) {
|
|
87
|
-
// Removing element i still fails
|
|
88
|
-
current = without
|
|
89
|
-
changed = true
|
|
90
|
-
break
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return {
|
|
96
|
-
original: sequence,
|
|
97
|
-
shrunk: current,
|
|
98
|
-
attempts,
|
|
99
|
-
reduced: current.length < sequence.length,
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Format shrink result for display
|
|
105
|
-
*/
|
|
106
|
-
export function formatShrinkResult<T>(result: ShrinkResult<T>): string {
|
|
107
|
-
const reduction = result.original.length - result.shrunk.length
|
|
108
|
-
const percent = Math.round((reduction / result.original.length) * 100)
|
|
109
|
-
|
|
110
|
-
if (!result.reduced) {
|
|
111
|
-
return `Could not reduce sequence (${result.original.length} steps, ${result.attempts} attempts)`
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
return `Shrunk from ${result.original.length} to ${result.shrunk.length} steps (${percent}% reduction, ${result.attempts} attempts)`
|
|
115
|
-
}
|
package/src/fuzz/test-fuzz.ts
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* test.fuzz wrapper with auto-tracking, shrinking, and regression
|
|
3
|
-
*
|
|
4
|
-
* Usage:
|
|
5
|
-
* ```typescript
|
|
6
|
-
* import { test } from 'vimonkey/fuzz'
|
|
7
|
-
*
|
|
8
|
-
* test.fuzz('cursor invariants', async () => {
|
|
9
|
-
* const handle = await run(<Board />, { cols: 80, rows: 24 })
|
|
10
|
-
* for await (const key of take(gen(['j','k','h','l']), 100)) {
|
|
11
|
-
* await handle.press(key)
|
|
12
|
-
* expect(handle.locator('[data-cursor]').count()).toBe(1)
|
|
13
|
-
* }
|
|
14
|
-
* })
|
|
15
|
-
* ```
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { test as vitestTest, type TestOptions } from "vitest"
|
|
19
|
-
import { fuzzContext, createFuzzContext, createReplayContext, type FuzzContext } from "./context.js"
|
|
20
|
-
import { shrinkSequence, formatShrinkResult } from "./shrink.js"
|
|
21
|
-
import { saveCase, loadCasesForTest, type SavedCase } from "./regression.js"
|
|
22
|
-
import { parseSeed, parseRepeats, deriveSeeds } from "../random.js"
|
|
23
|
-
|
|
24
|
-
/** Options for test.fuzz */
|
|
25
|
-
export interface FuzzTestOptions extends TestOptions {
|
|
26
|
-
/** Seed for reproducibility (default: from FUZZ_SEED env or random) */
|
|
27
|
-
seed?: number
|
|
28
|
-
/** Whether to shrink failing sequences (default: true) */
|
|
29
|
-
shrink?: boolean
|
|
30
|
-
/** Whether to save failing sequences to __fuzz_cases__/ (default: true) */
|
|
31
|
-
save?: boolean
|
|
32
|
-
/** Whether to replay saved failing sequences first (default: true) */
|
|
33
|
-
replay?: boolean
|
|
34
|
-
/** Maximum shrinking attempts (default: 100) */
|
|
35
|
-
maxShrinkAttempts?: number
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Error with fuzz context attached */
|
|
39
|
-
export class FuzzError extends Error {
|
|
40
|
-
readonly sequence: unknown[]
|
|
41
|
-
readonly seed: number
|
|
42
|
-
readonly originalError: Error
|
|
43
|
-
|
|
44
|
-
constructor(
|
|
45
|
-
originalError: Error,
|
|
46
|
-
info: {
|
|
47
|
-
original: number
|
|
48
|
-
shrunk: number
|
|
49
|
-
sequence: unknown[]
|
|
50
|
-
seed: number
|
|
51
|
-
},
|
|
52
|
-
) {
|
|
53
|
-
const msg = `Fuzz test failed after ${info.original} steps (shrunk to ${info.shrunk})
|
|
54
|
-
Minimal failing sequence: ${JSON.stringify(info.sequence)}
|
|
55
|
-
Seed: ${info.seed} (reproduce with FUZZ_SEED=${info.seed})
|
|
56
|
-
|
|
57
|
-
Original error: ${originalError.message}`
|
|
58
|
-
|
|
59
|
-
super(msg)
|
|
60
|
-
this.name = "FuzzError"
|
|
61
|
-
this.sequence = info.sequence
|
|
62
|
-
this.seed = info.seed
|
|
63
|
-
this.originalError = originalError
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* Get the test file path from error stack
|
|
69
|
-
* This is a heuristic - it looks for .test.ts or .fuzz.ts files
|
|
70
|
-
*/
|
|
71
|
-
function getTestFilePath(): string {
|
|
72
|
-
const err = new Error()
|
|
73
|
-
const stack = err.stack?.split("\n") ?? []
|
|
74
|
-
|
|
75
|
-
for (const line of stack) {
|
|
76
|
-
// Look for test file patterns
|
|
77
|
-
const match = line.match(/\(([^)]+\.(test|fuzz)\.(ts|tsx|js|jsx)):\d+:\d+\)/)
|
|
78
|
-
if (match) return match[1]!
|
|
79
|
-
|
|
80
|
-
// Also try without parentheses
|
|
81
|
-
const match2 = line.match(/at\s+([^\s]+\.(test|fuzz)\.(ts|tsx|js|jsx)):\d+:\d+/)
|
|
82
|
-
if (match2) return match2[1]!
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Fallback to current working directory
|
|
86
|
-
return process.cwd() + "/unknown.test.ts"
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Run a single fuzz test body with a specific seed.
|
|
91
|
-
* Handles replay, shrinking, and saving of failing cases.
|
|
92
|
-
*/
|
|
93
|
-
async function runFuzzBody(
|
|
94
|
-
name: string,
|
|
95
|
-
fn: () => Promise<void>,
|
|
96
|
-
seed: number,
|
|
97
|
-
opts: { shrink: boolean; save: boolean; replay: boolean; maxShrinkAttempts: number },
|
|
98
|
-
) {
|
|
99
|
-
const testFilePath = getTestFilePath()
|
|
100
|
-
|
|
101
|
-
// Replay saved failing sequences first
|
|
102
|
-
if (opts.replay) {
|
|
103
|
-
const savedCases = loadCasesForTest(testFilePath, name)
|
|
104
|
-
for (const savedCase of savedCases) {
|
|
105
|
-
const replayCtx = createReplayContext(savedCase.sequence, savedCase.seed)
|
|
106
|
-
try {
|
|
107
|
-
await fuzzContext.run(replayCtx, fn)
|
|
108
|
-
// If replay passes, the bug might be fixed - but we still run the main test
|
|
109
|
-
} catch (error) {
|
|
110
|
-
// Replay still fails - throw with saved context
|
|
111
|
-
throw new FuzzError(error as Error, {
|
|
112
|
-
original: savedCase.originalLength ?? savedCase.sequence.length,
|
|
113
|
-
shrunk: savedCase.sequence.length,
|
|
114
|
-
sequence: savedCase.sequence,
|
|
115
|
-
seed: savedCase.seed,
|
|
116
|
-
})
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Run the main fuzz test
|
|
122
|
-
const ctx = createFuzzContext(seed)
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
await fuzzContext.run(ctx, fn)
|
|
126
|
-
} catch (error) {
|
|
127
|
-
// Test failed - attempt shrinking
|
|
128
|
-
if (ctx.history.length > 0) {
|
|
129
|
-
let minimalSequence = ctx.history
|
|
130
|
-
let shrinkResult
|
|
131
|
-
|
|
132
|
-
if (opts.shrink) {
|
|
133
|
-
// Define the test runner for shrinking
|
|
134
|
-
const runWithSequence = async (seq: unknown[]) => {
|
|
135
|
-
const replayCtx = createReplayContext(seq, seed)
|
|
136
|
-
try {
|
|
137
|
-
await fuzzContext.run(replayCtx, fn)
|
|
138
|
-
return true // passed
|
|
139
|
-
} catch {
|
|
140
|
-
return false // still fails
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
shrinkResult = await shrinkSequence(ctx.history, runWithSequence, {
|
|
145
|
-
maxAttempts: opts.maxShrinkAttempts,
|
|
146
|
-
})
|
|
147
|
-
minimalSequence = shrinkResult.shrunk
|
|
148
|
-
|
|
149
|
-
// Log shrink result
|
|
150
|
-
console.log(formatShrinkResult(shrinkResult))
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Save failing case
|
|
154
|
-
if (opts.save) {
|
|
155
|
-
const savedCase: SavedCase = {
|
|
156
|
-
test: name,
|
|
157
|
-
seed,
|
|
158
|
-
sequence: minimalSequence,
|
|
159
|
-
error: String(error),
|
|
160
|
-
timestamp: new Date().toISOString(),
|
|
161
|
-
originalLength: ctx.history.length,
|
|
162
|
-
}
|
|
163
|
-
const filepath = saveCase(testFilePath, name, savedCase)
|
|
164
|
-
console.log(`Saved failing case to: ${filepath}`)
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
throw new FuzzError(error as Error, {
|
|
168
|
-
original: ctx.history.length,
|
|
169
|
-
shrunk: minimalSequence.length,
|
|
170
|
-
sequence: minimalSequence,
|
|
171
|
-
seed,
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
throw error
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Create the test.fuzz wrapper
|
|
181
|
-
*
|
|
182
|
-
* When FUZZ_REPEATS > 1, registers multiple vitest tests — one per seed —
|
|
183
|
-
* so each gets its own result in the reporter and failures are independently
|
|
184
|
-
* visible. Seeds are deterministically derived from the base seed.
|
|
185
|
-
*/
|
|
186
|
-
function createFuzzTest(name: string, fn: () => Promise<void>, options: FuzzTestOptions = {}) {
|
|
187
|
-
const {
|
|
188
|
-
seed = parseSeed("env"),
|
|
189
|
-
shrink = true,
|
|
190
|
-
save = true,
|
|
191
|
-
replay = true,
|
|
192
|
-
maxShrinkAttempts = 100,
|
|
193
|
-
...testOptions
|
|
194
|
-
} = options
|
|
195
|
-
|
|
196
|
-
const repeats = parseRepeats()
|
|
197
|
-
const bodyOpts = { shrink, save, replay, maxShrinkAttempts }
|
|
198
|
-
|
|
199
|
-
if (repeats <= 1) {
|
|
200
|
-
// Single run (default) — original behavior
|
|
201
|
-
return vitestTest(name, testOptions, async () => {
|
|
202
|
-
await runFuzzBody(name, fn, seed, bodyOpts)
|
|
203
|
-
})
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Multiple runs — register one test per seed
|
|
207
|
-
const seeds = deriveSeeds(seed, repeats)
|
|
208
|
-
for (let i = 0; i < seeds.length; i++) {
|
|
209
|
-
const s = seeds[i]!
|
|
210
|
-
vitestTest(`${name} [seed=${s}]`, testOptions, async () => {
|
|
211
|
-
await runFuzzBody(name, fn, s, bodyOpts)
|
|
212
|
-
})
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
// Type for the fuzz function
|
|
217
|
-
type FuzzFn = {
|
|
218
|
-
(name: string, fn: () => Promise<void>, options?: FuzzTestOptions): void
|
|
219
|
-
(name: string, options: FuzzTestOptions, fn: () => Promise<void>): void
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Create the fuzz function with overloads
|
|
223
|
-
const fuzz: FuzzFn = (
|
|
224
|
-
name: string,
|
|
225
|
-
fnOrOptions: (() => Promise<void>) | FuzzTestOptions,
|
|
226
|
-
optionsOrFn?: FuzzTestOptions | (() => Promise<void>),
|
|
227
|
-
) => {
|
|
228
|
-
if (typeof fnOrOptions === "function") {
|
|
229
|
-
return createFuzzTest(name, fnOrOptions, optionsOrFn as FuzzTestOptions)
|
|
230
|
-
} else {
|
|
231
|
-
return createFuzzTest(name, optionsOrFn as () => Promise<void>, fnOrOptions)
|
|
232
|
-
}
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Extended test object with fuzz method
|
|
237
|
-
*/
|
|
238
|
-
export const test: typeof vitestTest & { fuzz: typeof fuzz } = Object.assign(vitestTest, { fuzz })
|
|
239
|
-
|
|
240
|
-
// Re-export vitest test types
|
|
241
|
-
export { describe, expect, it, beforeAll, afterAll, beforeEach, afterEach } from "vitest"
|
package/src/index.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* vimonkey — Fuzz testing and chaos streams for Vitest
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
// Fuzz API (primary)
|
|
6
|
-
export { gen, take, type Picker, type PickerContext } from "./fuzz/gen.js"
|
|
7
|
-
export { test, FuzzError, type FuzzTestOptions } from "./fuzz/test-fuzz.js"
|
|
8
|
-
export { describe, expect, it, beforeAll, afterAll, beforeEach, afterEach } from "./fuzz/test-fuzz.js"
|
|
9
|
-
export {
|
|
10
|
-
fuzzContext,
|
|
11
|
-
getFuzzContext,
|
|
12
|
-
isInFuzzContext,
|
|
13
|
-
createFuzzContext,
|
|
14
|
-
createReplayContext,
|
|
15
|
-
type FuzzContext,
|
|
16
|
-
} from "./fuzz/context.js"
|
|
17
|
-
export { shrinkSequence, formatShrinkResult, type ShrinkOptions, type ShrinkResult } from "./fuzz/shrink.js"
|
|
18
|
-
export {
|
|
19
|
-
saveCase,
|
|
20
|
-
loadCases,
|
|
21
|
-
loadCasesForTest,
|
|
22
|
-
deleteCase,
|
|
23
|
-
clearCases,
|
|
24
|
-
getFuzzCasesDir,
|
|
25
|
-
type SavedCase,
|
|
26
|
-
} from "./fuzz/regression.js"
|
|
27
|
-
|
|
28
|
-
// Utilities
|
|
29
|
-
export {
|
|
30
|
-
createSeededRandom,
|
|
31
|
-
weightedPickFromTuples,
|
|
32
|
-
parseSeed,
|
|
33
|
-
parseRepeats,
|
|
34
|
-
deriveSeeds,
|
|
35
|
-
type SeededRandom,
|
|
36
|
-
} from "./random.js"
|
|
37
|
-
export { getTestSys, type TestSys } from "./env.js"
|
package/src/plugin.ts
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @todo Planned vitest plugin — stub only. Implement config injection and
|
|
3
|
-
* custom CLI mode interception (vitest fuzz, vitest ai, vitest doc).
|
|
4
|
-
*
|
|
5
|
-
* Vitest plugin for vimonkey
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```typescript
|
|
9
|
-
* // vitest.config.ts
|
|
10
|
-
* import { defineConfig } from 'vitest/config'
|
|
11
|
-
* import { viMonkey } from 'vimonkey/plugin'
|
|
12
|
-
*
|
|
13
|
-
* export default defineConfig({
|
|
14
|
-
* plugins: [
|
|
15
|
-
* viMonkey({
|
|
16
|
-
* fuzz: { iterations: 100 },
|
|
17
|
-
* ai: { model: 'claude-sonnet' },
|
|
18
|
-
* doc: { pattern: '**\/*.test.md' },
|
|
19
|
-
* })
|
|
20
|
-
* ]
|
|
21
|
-
* })
|
|
22
|
-
* ```
|
|
23
|
-
*/
|
|
24
|
-
|
|
25
|
-
/** Minimal Vite plugin interface — avoids requiring vite as a dependency */
|
|
26
|
-
interface Plugin {
|
|
27
|
-
name: string
|
|
28
|
-
config?(): void
|
|
29
|
-
configureServer?(server: unknown): void
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface ViMonkeyFuzzOptions {
|
|
33
|
-
/** Number of actions per test run (default: 100) */
|
|
34
|
-
iterations?: number
|
|
35
|
-
/** Seed source: 'env' reads FUZZ_SEED, 'random' generates new (default: 'env') */
|
|
36
|
-
seed?: "env" | "random"
|
|
37
|
-
/** Stop after first failure (default: true) */
|
|
38
|
-
failFast?: boolean
|
|
39
|
-
/** Shrinking settings */
|
|
40
|
-
shrink?: {
|
|
41
|
-
enabled?: boolean
|
|
42
|
-
maxAttempts?: number
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export interface ViMonkeyAiOptions {
|
|
47
|
-
/** Model identifier (Vercel AI SDK format) */
|
|
48
|
-
model?: string
|
|
49
|
-
/** Temperature for LLM (0 = deterministic) */
|
|
50
|
-
temperature?: number
|
|
51
|
-
/** Maximum actions per exploration */
|
|
52
|
-
maxSteps?: number
|
|
53
|
-
/** Token budget per file */
|
|
54
|
-
maxTokens?: number
|
|
55
|
-
/** Directory to save discovered tests */
|
|
56
|
-
saveDir?: string
|
|
57
|
-
/** Use Claude Code provider */
|
|
58
|
-
provider?: "openai" | "anthropic" | "claude-code"
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export interface ViMonkeyDocOptions {
|
|
62
|
-
/** Glob pattern for mdtest files */
|
|
63
|
-
pattern?: string
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export interface ViMonkeyOptions {
|
|
67
|
-
/** Fuzz mode configuration */
|
|
68
|
-
fuzz?: ViMonkeyFuzzOptions
|
|
69
|
-
/** AI mode configuration */
|
|
70
|
-
ai?: ViMonkeyAiOptions
|
|
71
|
-
/** Doc mode configuration (mdtest) */
|
|
72
|
-
doc?: ViMonkeyDocOptions
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Creates the vimonkey plugin for Vitest
|
|
77
|
-
*/
|
|
78
|
-
export function viMonkey(options: ViMonkeyOptions = {}): Plugin {
|
|
79
|
-
const { fuzz = {}, ai = {}, doc = {} } = options
|
|
80
|
-
|
|
81
|
-
return {
|
|
82
|
-
name: "vimonkey",
|
|
83
|
-
|
|
84
|
-
config() {
|
|
85
|
-
// Configure vitest for custom modes
|
|
86
|
-
// Returns void for now - will be populated as modes are implemented
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
configureServer(server) {
|
|
90
|
-
// Handle custom CLI modes: vitest fuzz, vitest ai, vitest doc
|
|
91
|
-
// Implementation will intercept vitest CLI commands
|
|
92
|
-
},
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
export default viMonkey
|
package/src/random.ts
DELETED
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Seeded random number generator
|
|
3
|
-
*
|
|
4
|
-
* Uses a Linear Congruential Generator (LCG) for reproducible random sequences.
|
|
5
|
-
* Ported from km's chaos testing infrastructure.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export interface SeededRandom {
|
|
9
|
-
/** Get the current seed */
|
|
10
|
-
seed: number
|
|
11
|
-
|
|
12
|
-
/** Generate a random integer in [min, max] inclusive */
|
|
13
|
-
int(min: number, max: number): number
|
|
14
|
-
|
|
15
|
-
/** Generate a random float in [0, 1) */
|
|
16
|
-
float(): number
|
|
17
|
-
|
|
18
|
-
/** Pick a random element from an array */
|
|
19
|
-
pick<T>(array: readonly T[]): T
|
|
20
|
-
|
|
21
|
-
/** Pick a random element with weights */
|
|
22
|
-
weightedPick<T extends string>(items: readonly T[], weights: Partial<Record<T, number>>): T
|
|
23
|
-
|
|
24
|
-
/** Shuffle an array (returns new array) */
|
|
25
|
-
shuffle<T>(array: readonly T[]): T[]
|
|
26
|
-
|
|
27
|
-
/** Generate a random array */
|
|
28
|
-
array<T>(length: number, generator: () => T): T[]
|
|
29
|
-
|
|
30
|
-
/** Generate a random boolean with given probability of true */
|
|
31
|
-
bool(probability?: number): boolean
|
|
32
|
-
|
|
33
|
-
/** Fork the RNG (create independent stream) */
|
|
34
|
-
fork(): SeededRandom
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Linear Congruential Generator constants (same as glibc's rand()).
|
|
38
|
-
// Full period of 2^31. See Numerical Recipes §7.1 or Knuth TAOCP Vol 2 §3.2.1.
|
|
39
|
-
const LCG_A = 1103515245 // multiplier
|
|
40
|
-
const LCG_C = 12345 // increment
|
|
41
|
-
const LCG_M = 2 ** 31 // modulus (2^31)
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* Pick a random element from weighted tuples using a pre-generated random float.
|
|
45
|
-
*
|
|
46
|
-
* @param tuples - Array of [weight, value] pairs
|
|
47
|
-
* @param rand - Random float in [0, 1)
|
|
48
|
-
* @returns The selected value
|
|
49
|
-
*/
|
|
50
|
-
export function weightedPickFromTuples<T>(tuples: readonly (readonly [number, T])[], rand: number): T {
|
|
51
|
-
let total = 0
|
|
52
|
-
for (const [w] of tuples) total += w
|
|
53
|
-
let r = rand * total
|
|
54
|
-
for (const [weight, value] of tuples) {
|
|
55
|
-
r -= weight
|
|
56
|
-
if (r <= 0) return value
|
|
57
|
-
}
|
|
58
|
-
return tuples[tuples.length - 1]![1]
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Create a seeded random number generator
|
|
63
|
-
*
|
|
64
|
-
* @param seed - Initial seed (uses Date.now() if not provided)
|
|
65
|
-
* @returns Seeded random number generator
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
|
-
* ```typescript
|
|
69
|
-
* const random = createSeededRandom(12345)
|
|
70
|
-
*
|
|
71
|
-
* // Same seed = same sequence
|
|
72
|
-
* random.int(0, 100) // always 47 with seed 12345
|
|
73
|
-
* random.pick(['a', 'b', 'c']) // always 'b' with seed 12345
|
|
74
|
-
* ```
|
|
75
|
-
*/
|
|
76
|
-
export function createSeededRandom(seed?: number): SeededRandom {
|
|
77
|
-
let state = seed ?? Date.now()
|
|
78
|
-
|
|
79
|
-
function next(): number {
|
|
80
|
-
state = (LCG_A * state + LCG_C) % LCG_M
|
|
81
|
-
return state
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
const random: SeededRandom = {
|
|
85
|
-
get seed() {
|
|
86
|
-
return state
|
|
87
|
-
},
|
|
88
|
-
|
|
89
|
-
float(): number {
|
|
90
|
-
return next() / LCG_M
|
|
91
|
-
},
|
|
92
|
-
|
|
93
|
-
int(min: number, max: number): number {
|
|
94
|
-
return Math.floor(random.float() * (max - min + 1)) + min
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
pick<T>(array: readonly T[]): T {
|
|
98
|
-
if (array.length === 0) {
|
|
99
|
-
throw new Error("Cannot pick from empty array")
|
|
100
|
-
}
|
|
101
|
-
return array[random.int(0, array.length - 1)]!
|
|
102
|
-
},
|
|
103
|
-
|
|
104
|
-
weightedPick<T extends string>(items: readonly T[], weights: Partial<Record<T, number>>): T {
|
|
105
|
-
const tuples: [number, T][] = items.map((item) => [weights[item] ?? 1, item])
|
|
106
|
-
return weightedPickFromTuples(tuples, random.float())
|
|
107
|
-
},
|
|
108
|
-
|
|
109
|
-
shuffle<T>(array: readonly T[]): T[] {
|
|
110
|
-
const result = [...array]
|
|
111
|
-
for (let i = result.length - 1; i > 0; i--) {
|
|
112
|
-
const j = random.int(0, i)
|
|
113
|
-
;[result[i], result[j]] = [result[j]!, result[i]!]
|
|
114
|
-
}
|
|
115
|
-
return result
|
|
116
|
-
},
|
|
117
|
-
|
|
118
|
-
array<T>(length: number, generator: () => T): T[] {
|
|
119
|
-
return Array.from({ length }, generator)
|
|
120
|
-
},
|
|
121
|
-
|
|
122
|
-
bool(probability = 0.5): boolean {
|
|
123
|
-
return random.float() < probability
|
|
124
|
-
},
|
|
125
|
-
|
|
126
|
-
fork(): SeededRandom {
|
|
127
|
-
// Use current state to seed new generator
|
|
128
|
-
return createSeededRandom(next())
|
|
129
|
-
},
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
return random
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Parse seed from environment or generate random
|
|
137
|
-
*/
|
|
138
|
-
export function parseSeed(source: "env" | "random" = "env"): number {
|
|
139
|
-
if (source === "env") {
|
|
140
|
-
const envSeed = process.env.FUZZ_SEED
|
|
141
|
-
if (envSeed) {
|
|
142
|
-
const parsed = parseInt(envSeed, 10)
|
|
143
|
-
if (!isNaN(parsed)) {
|
|
144
|
-
return parsed
|
|
145
|
-
}
|
|
146
|
-
console.warn(`Invalid FUZZ_SEED: "${envSeed}", using random seed`)
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
return Date.now()
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Parse FUZZ_REPEATS from environment (default: 1)
|
|
154
|
-
*
|
|
155
|
-
* Controls how many times each test.fuzz() test runs with different seeds.
|
|
156
|
-
* Use FUZZ_REPEATS=10000 for CI nightly runs.
|
|
157
|
-
*/
|
|
158
|
-
export function parseRepeats(): number {
|
|
159
|
-
const env = process.env.FUZZ_REPEATS
|
|
160
|
-
if (env) {
|
|
161
|
-
const parsed = parseInt(env, 10)
|
|
162
|
-
if (!isNaN(parsed) && parsed > 0) {
|
|
163
|
-
return parsed
|
|
164
|
-
}
|
|
165
|
-
console.warn(`Invalid FUZZ_REPEATS: "${env}", using 1`)
|
|
166
|
-
}
|
|
167
|
-
return 1
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
/**
|
|
171
|
-
* Derive N unique seeds from a base seed using LCG
|
|
172
|
-
*/
|
|
173
|
-
export function deriveSeeds(baseSeed: number, count: number): number[] {
|
|
174
|
-
const seeds: number[] = []
|
|
175
|
-
let state = baseSeed
|
|
176
|
-
for (let i = 0; i < count; i++) {
|
|
177
|
-
state = (LCG_A * state + LCG_C) % LCG_M
|
|
178
|
-
seeds.push(state)
|
|
179
|
-
}
|
|
180
|
-
return seeds
|
|
181
|
-
}
|