dsh-llm-verifier 0.1.13 → 0.1.16

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.
Files changed (90) hide show
  1. package/LICENSE +22 -22
  2. package/README.md +328 -301
  3. package/cordis.patch.yml +4 -4
  4. package/lib/caller-BWwSVNZf.js.map +1 -0
  5. package/lib/client.js +176 -14
  6. package/lib/client.js.map +1 -0
  7. package/lib/core.js.map +1 -0
  8. package/lib/index.js +400 -51
  9. package/lib/index.js.map +1 -0
  10. package/lib/types/auto.d.ts.map +1 -0
  11. package/lib/types/auto.js +8 -2
  12. package/lib/types/auto.js.map +1 -0
  13. package/lib/types/cache.d.ts.map +1 -0
  14. package/lib/types/cache.js.map +1 -0
  15. package/lib/types/caller.d.ts.map +1 -0
  16. package/lib/types/caller.js.map +1 -0
  17. package/lib/types/client-i18n.d.ts +27 -0
  18. package/lib/types/client-i18n.d.ts.map +1 -0
  19. package/lib/types/client-i18n.js +18 -0
  20. package/lib/types/client-i18n.js.map +1 -0
  21. package/lib/types/client.d.ts +15 -2
  22. package/lib/types/client.d.ts.map +1 -0
  23. package/lib/types/client.js +137 -10
  24. package/lib/types/client.js.map +1 -0
  25. package/lib/types/config.d.ts +4 -0
  26. package/lib/types/config.d.ts.map +1 -0
  27. package/lib/types/config.js +5 -3
  28. package/lib/types/config.js.map +1 -0
  29. package/lib/types/core.d.ts.map +1 -0
  30. package/lib/types/core.js.map +1 -0
  31. package/lib/types/engine.d.ts.map +1 -0
  32. package/lib/types/engine.js.map +1 -0
  33. package/lib/types/images.d.ts.map +1 -0
  34. package/lib/types/images.js.map +1 -0
  35. package/lib/types/index.d.ts +2 -0
  36. package/lib/types/index.d.ts.map +1 -0
  37. package/lib/types/index.js +162 -9
  38. package/lib/types/index.js.map +1 -0
  39. package/lib/types/plan-gate.d.ts +27 -0
  40. package/lib/types/plan-gate.d.ts.map +1 -0
  41. package/lib/types/plan-gate.js +60 -0
  42. package/lib/types/plan-gate.js.map +1 -0
  43. package/lib/types/router.d.ts +28 -32
  44. package/lib/types/router.d.ts.map +1 -0
  45. package/lib/types/router.js +41 -9
  46. package/lib/types/router.js.map +1 -0
  47. package/lib/types/session.d.ts +11 -0
  48. package/lib/types/session.d.ts.map +1 -0
  49. package/lib/types/session.js +41 -11
  50. package/lib/types/session.js.map +1 -0
  51. package/lib/types/statistics.d.ts.map +1 -0
  52. package/lib/types/statistics.js.map +1 -0
  53. package/lib/types/team-gate.d.ts +20 -0
  54. package/lib/types/team-gate.d.ts.map +1 -0
  55. package/lib/types/team-gate.js +59 -0
  56. package/lib/types/team-gate.js.map +1 -0
  57. package/lib/types/top-logprobs.d.ts.map +1 -0
  58. package/lib/types/top-logprobs.js.map +1 -0
  59. package/lib/types/topic-storage.d.ts.map +1 -0
  60. package/lib/types/topic-storage.js.map +1 -0
  61. package/package.json +114 -112
  62. package/src/auto.test.ts +95 -86
  63. package/src/auto.ts +140 -133
  64. package/src/cache.ts +115 -115
  65. package/src/caller.test.ts +34 -34
  66. package/src/caller.ts +151 -151
  67. package/src/client-i18n.ts +18 -0
  68. package/src/client.tsx +383 -213
  69. package/src/config.ts +189 -183
  70. package/src/core.test.ts +71 -71
  71. package/src/core.ts +205 -205
  72. package/src/engine.test.ts +136 -136
  73. package/src/engine.ts +140 -140
  74. package/src/images.ts +33 -33
  75. package/src/index.ts +394 -247
  76. package/src/parity.test.ts +85 -85
  77. package/src/plan-gate.test.ts +56 -0
  78. package/src/plan-gate.ts +66 -0
  79. package/src/router.test.ts +107 -89
  80. package/src/router.ts +361 -309
  81. package/src/session.test.ts +71 -36
  82. package/src/session.ts +132 -103
  83. package/src/statistics.test.ts +80 -80
  84. package/src/statistics.ts +328 -328
  85. package/src/team-gate.test.ts +92 -0
  86. package/src/team-gate.ts +80 -0
  87. package/src/top-logprobs.test.ts +84 -84
  88. package/src/top-logprobs.ts +153 -153
  89. package/src/topic-storage.test.ts +31 -31
  90. package/src/topic-storage.ts +42 -42
@@ -0,0 +1,92 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { Session } from '@deepseek-ai/dsh-session'
3
+ import { inspectTeamTasks, buildTeamTaskVerificationPrompt } from './team-gate.ts'
4
+
5
+ describe('team-gate', () => {
6
+ it('inspects team tasks and detects recently completed tasks', () => {
7
+ const session = Session.create('session-team-1' as never)
8
+ session.append('team/task' as never, { task: { id: 'task-1', revision: 1, subject: 'Write unit tests', status: 'in_progress' } } as never)
9
+ session.append('team/task' as never, { task: { id: 'task-2', revision: 1, subject: 'Update docs', status: 'pending' } } as never)
10
+ session.append('team/task' as never, { task: { id: 'task-1', revision: 2, subject: 'Write unit tests', description: 'Ensure 100% coverage', status: 'completed' } } as never)
11
+
12
+ const inspection = inspectTeamTasks(session.events, 0)
13
+ expect(inspection.hasRecentCompletedTask).toBe(true)
14
+ expect(inspection.latestCompletedTask?.id).toBe('task-1')
15
+ expect(inspection.latestCompletedTask?.status).toBe('completed')
16
+ expect(inspection.latestCompletedTask?.description).toBe('Ensure 100% coverage')
17
+ expect(inspection.activeTasks).toHaveLength(2)
18
+ expect(inspection.completedTasks).toHaveLength(1)
19
+ expect(inspection.completedTasks[0]?.seq).toBe(2)
20
+ })
21
+
22
+ it('lists every completed task once, oldest first', () => {
23
+ const session = Session.create('session-team-order' as never)
24
+ session.append('team/task' as never, { task: { id: 'task-a', revision: 1, subject: 'A', status: 'completed' } } as never)
25
+ session.append('team/task' as never, { task: { id: 'task-b', revision: 1, subject: 'B', status: 'in_progress' } } as never)
26
+ session.append('team/task' as never, { task: { id: 'task-b', revision: 2, subject: 'B', status: 'completed' } } as never)
27
+ session.append('team/task' as never, { task: { id: 'task-a', revision: 2, subject: 'A', status: 'in_progress' } } as never)
28
+ session.append('team/task' as never, { task: { id: 'task-a', revision: 3, subject: 'A', status: 'completed' } } as never)
29
+
30
+ const inspection = inspectTeamTasks(session.events, 0)
31
+ expect(inspection.completedTasks.map(entry => entry.task.id)).toEqual(['task-b', 'task-a'])
32
+ expect(inspection.completedTasks.map(entry => entry.seq)).toEqual([2, 4])
33
+ // A reopened task is verified once, at its newest completion.
34
+ expect(inspection.latestCompletedTask?.id).toBe('task-a')
35
+ expect(inspection.latestCompletedTask?.revision).toBe(3)
36
+ })
37
+
38
+ it('drops a completion that was reopened or deleted afterwards', () => {
39
+ const session = Session.create('session-team-ghost' as never)
40
+ session.append('team/task' as never, { task: { id: 'task-x', revision: 1, subject: 'X', status: 'completed' } } as never)
41
+ session.append('team/task' as never, { task: { id: 'task-x', revision: 2, subject: 'X', status: 'in_progress' } } as never)
42
+ const reopened = inspectTeamTasks(session.events, 0)
43
+ expect(reopened.completedTasks).toEqual([])
44
+ expect(reopened.latestCompletedTask).toBeUndefined()
45
+ expect(reopened.hasRecentCompletedTask).toBe(false)
46
+
47
+ session.append('team/task' as never, { task: { id: 'task-x', revision: 3, subject: 'X', status: 'deleted' } } as never)
48
+ expect(inspectTeamTasks(session.events, 0).completedTasks).toEqual([])
49
+
50
+ session.append('team/task' as never, { task: { id: 'task-x', revision: 4, subject: 'X', status: 'completed' } } as never)
51
+ expect(inspectTeamTasks(session.events, 0).completedTasks.map(entry => entry.seq)).toEqual([3])
52
+ })
53
+
54
+ it('filters deleted tasks from active tasks', () => {
55
+ const session = Session.create('session-team-2' as never)
56
+ session.append('team/task' as never, { task: { id: 'task-1', revision: 1, subject: 'Task A', status: 'in_progress' } } as never)
57
+ session.append('team/task' as never, { task: { id: 'task-2', revision: 1, subject: 'Task B', status: 'pending' } } as never)
58
+ session.append('team/task' as never, { task: { id: 'task-2', revision: 2, subject: 'Task B', status: 'deleted' } } as never)
59
+
60
+ const inspection = inspectTeamTasks(session.events, 0)
61
+ expect(inspection.activeTasks).toHaveLength(1)
62
+ expect(inspection.activeTasks[0]?.id).toBe('task-1')
63
+ expect(inspection.hasRecentCompletedTask).toBe(false)
64
+ expect(inspection.completedTasks).toEqual([])
65
+ })
66
+
67
+ it('respects fromSeq boundary for recent completed task', () => {
68
+ const session = Session.create('session-team-3' as never)
69
+ session.append('team/task' as never, { task: { id: 'task-1', revision: 1, subject: 'Initial setup', status: 'completed' } } as never)
70
+ // seq 0 is task-1 completed
71
+ session.append('team/task' as never, { task: { id: 'task-2', revision: 1, subject: 'Next step', status: 'in_progress' } } as never)
72
+
73
+ const inspection = inspectTeamTasks(session.events, 1)
74
+ expect(inspection.hasRecentCompletedTask).toBe(false)
75
+ expect(inspection.latestCompletedTask).toBeUndefined()
76
+ expect(inspection.completedTasks).toEqual([])
77
+ })
78
+
79
+ it('builds verification prompt containing task metadata and trace evidence', () => {
80
+ const prompt = buildTeamTaskVerificationPrompt(
81
+ { id: 'task-100', revision: 2, subject: 'Build Artifact Bundle', description: 'Produce dist/index.js and dist/client.js', status: 'completed' },
82
+ 'Executed pnpm build: completed in 1.2s with zero errors.',
83
+ 10000
84
+ )
85
+ expect(prompt).toContain('You are an expert independent technical verifier reviewing a completed Agent Teams task')
86
+ expect(prompt).toContain('ID: task-100')
87
+ expect(prompt).toContain('Subject: Build Artifact Bundle')
88
+ expect(prompt).toContain('Description: Produce dist/index.js')
89
+ expect(prompt).toContain('Executed pnpm build: completed in 1.2s')
90
+ expect(prompt).toContain('Verdict: <single uppercase letter A-T>')
91
+ })
92
+ })
@@ -0,0 +1,80 @@
1
+ import type { SessionEvent } from '@deepseek-ai/dsh-session'
2
+ import type { TeamTaskItem } from './router.ts'
3
+ import { sanitizeVerifierText } from './session.ts'
4
+
5
+ /** One completed task and the sequence number of its completion. */
6
+ export interface CompletedTeamTask {
7
+ task: TeamTaskItem
8
+ seq: number
9
+ }
10
+
11
+ export interface TeamTaskInspection {
12
+ /** Latest completion in the window; kept for callers that only need one. */
13
+ latestCompletedTask?: TeamTaskItem
14
+ completedSeq?: number
15
+ /** Every task completed in the window, one entry per task id, oldest first. */
16
+ completedTasks: CompletedTeamTask[]
17
+ /** Known non-deleted tasks, including completed ones. */
18
+ activeTasks: TeamTaskItem[]
19
+ hasRecentCompletedTask: boolean
20
+ }
21
+
22
+ export function inspectTeamTasks(events: readonly SessionEvent[], fromSeq = 0): TeamTaskInspection {
23
+ const taskMap = new Map<string, TeamTaskItem>()
24
+ // Sequence of the newest completion per task id. Reopening or deleting a task
25
+ // clears its entry, so a stale completion can never outlive the task status it
26
+ // came from; completing the same task again records the new sequence.
27
+ const completionSeq = new Map<string, number>()
28
+
29
+ for (const rawEvent of events) {
30
+ const event = rawEvent as unknown as { type: string; seq: number; data: any }
31
+ if (event.type === 'team/task') {
32
+ const data = event.data as { task?: TeamTaskItem }
33
+ if (data?.task) {
34
+ taskMap.set(data.task.id, { ...data.task })
35
+ if (data.task.status === 'completed') completionSeq.set(data.task.id, event.seq)
36
+ else completionSeq.delete(data.task.id)
37
+ }
38
+ }
39
+ }
40
+
41
+ const completedTasks: CompletedTeamTask[] = [...completionSeq.entries()]
42
+ .filter(([id, seq]) => seq >= fromSeq && taskMap.get(id)?.status === 'completed')
43
+ .map(([id, seq]) => ({ task: { ...(taskMap.get(id) as TeamTaskItem) }, seq }))
44
+ .sort((left, right) => left.seq - right.seq)
45
+ const latest = completedTasks.at(-1)
46
+
47
+ return {
48
+ latestCompletedTask: latest?.task,
49
+ completedSeq: latest?.seq,
50
+ completedTasks,
51
+ activeTasks: [...taskMap.values()].filter(t => t.status !== 'deleted'),
52
+ hasRecentCompletedTask: latest !== undefined,
53
+ }
54
+ }
55
+
56
+ export function buildTeamTaskVerificationPrompt(task: TeamTaskItem, executionTrace: string, maxChars = 20000): string {
57
+ return [
58
+ 'You are an expert independent technical verifier reviewing a completed Agent Teams task.',
59
+ 'Verify if the task goal has been satisfied by concrete evidence (code edits, test runs, successful commands).',
60
+ 'Task Details:',
61
+ 'ID: ' + task.id,
62
+ 'Subject: ' + task.subject,
63
+ task.description ? 'Description: ' + task.description : '',
64
+ '',
65
+ 'Execution Evidence & Trace:',
66
+ sanitizeVerifierText(executionTrace, maxChars),
67
+ '',
68
+ 'Evaluate whether the task is genuinely completed with verified proof.',
69
+ 'Scoring criteria:',
70
+ 'A: Completely fulfilled with solid execution/test evidence.',
71
+ 'B-J: Mostly complete, minor gaps or edge cases not fully asserted.',
72
+ 'K-S: Incomplete, missing key requirements or untested.',
73
+ 'T: Unfulfilled, failed, or contradictory evidence.',
74
+ '',
75
+ 'Output format:',
76
+ 'The first line must be exactly "Verdict: <single uppercase letter A-T>" — one letter, nothing else on the line.',
77
+ 'Line 2: Summary: <One sentence evaluation of task fulfillment>',
78
+ 'Line 3+: Remaining gaps or next steps for the team.',
79
+ ].filter(Boolean).join('\n\n')
80
+ }
@@ -1,84 +1,84 @@
1
- import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
- import { readFileSync } from 'node:fs'
3
- import { tmpdir } from 'node:os'
4
- import { join } from 'node:path'
5
- import { describe, expect, it } from 'vitest'
6
- import { CAPABILITY_TTL_MS, TopLogprobCapabilityCache, resolveCapabilityFile } from './top-logprobs.ts'
7
-
8
- describe('TopLogprobCapabilityCache persistence', () => {
9
- it('round-trips marks across instances through the capability file', async () => {
10
- const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-caps-'))
11
- try {
12
- const file = resolveCapabilityFile(dir)
13
- const first = new TopLogprobCapabilityCache(file)
14
- expect(first.isUnsupported('openai', 'gpt-5')).toBe(false)
15
- first.markUnsupported('openai', 'gpt-5')
16
- await first.flush()
17
- expect(existsSync(file)).toBe(true)
18
-
19
- const second = new TopLogprobCapabilityCache(file)
20
- await second.ensureLoaded()
21
- expect(second.isUnsupported('openai', 'gpt-5')).toBe(true)
22
- expect(second.isUnsupported('openai', 'other')).toBe(false)
23
-
24
- // A mark from a later instance must merge with hydrated entries, not clobber them.
25
- second.markUnsupported('anthropic', 'claude')
26
- await second.flush()
27
- const raw = JSON.parse(readFileSync(file, 'utf8')) as { version: number; entries: Record<string, number> }
28
- expect(raw.version).toBe(1)
29
- expect(Object.keys(raw.entries)).toHaveLength(2)
30
- } finally { rmSync(dir, { recursive: true, force: true }) }
31
- })
32
-
33
- it('drops marks older than the TTL on hydration', async () => {
34
- const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-caps-'))
35
- try {
36
- const file = join(dir, 'capabilities-v1.json')
37
- writeFileSync(file, JSON.stringify({ version: 1, entries: { 'openai\0gpt-5': Date.now() - CAPABILITY_TTL_MS - 1, 'openai\0fresh': Date.now() } }))
38
- const cache = new TopLogprobCapabilityCache(file)
39
- await cache.ensureLoaded()
40
- expect(cache.isUnsupported('openai', 'gpt-5')).toBe(false)
41
- expect(cache.isUnsupported('openai', 'fresh')).toBe(true)
42
- } finally { rmSync(dir, { recursive: true, force: true }) }
43
- })
44
-
45
- it('ignores missing and corrupt capability files', async () => {
46
- const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-caps-'))
47
- try {
48
- const corrupt = join(dir, 'corrupt.json')
49
- writeFileSync(corrupt, 'not json at all')
50
- const corrupted = new TopLogprobCapabilityCache(corrupt)
51
- await expect(corrupted.ensureLoaded()).resolves.toBeUndefined()
52
- expect(corrupted.isUnsupported('openai', 'gpt-5')).toBe(false)
53
-
54
- const absent = new TopLogprobCapabilityCache(join(dir, 'absent.json'))
55
- await expect(absent.ensureLoaded()).resolves.toBeUndefined()
56
- absent.markUnsupported('openai', 'gpt-5')
57
- await absent.flush()
58
- expect(absent.isUnsupported('openai', 'gpt-5')).toBe(true)
59
- } finally { rmSync(dir, { recursive: true, force: true }) }
60
- })
61
-
62
- it('keeps the freshest mark when hydration races an in-process probe', async () => {
63
- const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-caps-'))
64
- try {
65
- const file = join(dir, 'capabilities-v1.json')
66
- writeFileSync(file, JSON.stringify({ version: 1, entries: { 'openai\0gpt-5': Date.now() - 5_000 } }))
67
- const cache = new TopLogprobCapabilityCache(file)
68
- const floor = Date.now()
69
- cache.markUnsupported('openai', 'gpt-5')
70
- await cache.ensureLoaded()
71
- await cache.flush()
72
- const raw = JSON.parse(readFileSync(file, 'utf8')) as { entries: Record<string, number> }
73
- expect(raw.entries['openai\0gpt-5']).toBeGreaterThanOrEqual(floor)
74
- } finally { rmSync(dir, { recursive: true, force: true }) }
75
- })
76
-
77
- it('keeps the in-memory-only behavior when no file is configured', async () => {
78
- const cache = new TopLogprobCapabilityCache()
79
- await expect(cache.ensureLoaded()).resolves.toBeUndefined()
80
- cache.markUnsupported('p', 'm')
81
- expect(cache.isUnsupported('p', 'm')).toBe(true)
82
- expect(cache.isUnsupported('p', 'other')).toBe(false)
83
- })
84
- })
1
+ import { existsSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
2
+ import { readFileSync } from 'node:fs'
3
+ import { tmpdir } from 'node:os'
4
+ import { join } from 'node:path'
5
+ import { describe, expect, it } from 'vitest'
6
+ import { CAPABILITY_TTL_MS, TopLogprobCapabilityCache, resolveCapabilityFile } from './top-logprobs.ts'
7
+
8
+ describe('TopLogprobCapabilityCache persistence', () => {
9
+ it('round-trips marks across instances through the capability file', async () => {
10
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-caps-'))
11
+ try {
12
+ const file = resolveCapabilityFile(dir)
13
+ const first = new TopLogprobCapabilityCache(file)
14
+ expect(first.isUnsupported('openai', 'gpt-5')).toBe(false)
15
+ first.markUnsupported('openai', 'gpt-5')
16
+ await first.flush()
17
+ expect(existsSync(file)).toBe(true)
18
+
19
+ const second = new TopLogprobCapabilityCache(file)
20
+ await second.ensureLoaded()
21
+ expect(second.isUnsupported('openai', 'gpt-5')).toBe(true)
22
+ expect(second.isUnsupported('openai', 'other')).toBe(false)
23
+
24
+ // A mark from a later instance must merge with hydrated entries, not clobber them.
25
+ second.markUnsupported('anthropic', 'claude')
26
+ await second.flush()
27
+ const raw = JSON.parse(readFileSync(file, 'utf8')) as { version: number; entries: Record<string, number> }
28
+ expect(raw.version).toBe(1)
29
+ expect(Object.keys(raw.entries)).toHaveLength(2)
30
+ } finally { rmSync(dir, { recursive: true, force: true }) }
31
+ })
32
+
33
+ it('drops marks older than the TTL on hydration', async () => {
34
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-caps-'))
35
+ try {
36
+ const file = join(dir, 'capabilities-v1.json')
37
+ writeFileSync(file, JSON.stringify({ version: 1, entries: { 'openai\0gpt-5': Date.now() - CAPABILITY_TTL_MS - 1, 'openai\0fresh': Date.now() } }))
38
+ const cache = new TopLogprobCapabilityCache(file)
39
+ await cache.ensureLoaded()
40
+ expect(cache.isUnsupported('openai', 'gpt-5')).toBe(false)
41
+ expect(cache.isUnsupported('openai', 'fresh')).toBe(true)
42
+ } finally { rmSync(dir, { recursive: true, force: true }) }
43
+ })
44
+
45
+ it('ignores missing and corrupt capability files', async () => {
46
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-caps-'))
47
+ try {
48
+ const corrupt = join(dir, 'corrupt.json')
49
+ writeFileSync(corrupt, 'not json at all')
50
+ const corrupted = new TopLogprobCapabilityCache(corrupt)
51
+ await expect(corrupted.ensureLoaded()).resolves.toBeUndefined()
52
+ expect(corrupted.isUnsupported('openai', 'gpt-5')).toBe(false)
53
+
54
+ const absent = new TopLogprobCapabilityCache(join(dir, 'absent.json'))
55
+ await expect(absent.ensureLoaded()).resolves.toBeUndefined()
56
+ absent.markUnsupported('openai', 'gpt-5')
57
+ await absent.flush()
58
+ expect(absent.isUnsupported('openai', 'gpt-5')).toBe(true)
59
+ } finally { rmSync(dir, { recursive: true, force: true }) }
60
+ })
61
+
62
+ it('keeps the freshest mark when hydration races an in-process probe', async () => {
63
+ const dir = mkdtempSync(join(tmpdir(), 'dsh-verifier-caps-'))
64
+ try {
65
+ const file = join(dir, 'capabilities-v1.json')
66
+ writeFileSync(file, JSON.stringify({ version: 1, entries: { 'openai\0gpt-5': Date.now() - 5_000 } }))
67
+ const cache = new TopLogprobCapabilityCache(file)
68
+ const floor = Date.now()
69
+ cache.markUnsupported('openai', 'gpt-5')
70
+ await cache.ensureLoaded()
71
+ await cache.flush()
72
+ const raw = JSON.parse(readFileSync(file, 'utf8')) as { entries: Record<string, number> }
73
+ expect(raw.entries['openai\0gpt-5']).toBeGreaterThanOrEqual(floor)
74
+ } finally { rmSync(dir, { recursive: true, force: true }) }
75
+ })
76
+
77
+ it('keeps the in-memory-only behavior when no file is configured', async () => {
78
+ const cache = new TopLogprobCapabilityCache()
79
+ await expect(cache.ensureLoaded()).resolves.toBeUndefined()
80
+ cache.markUnsupported('p', 'm')
81
+ expect(cache.isUnsupported('p', 'm')).toBe(true)
82
+ expect(cache.isUnsupported('p', 'other')).toBe(false)
83
+ })
84
+ })