From a0c5c7d0232b8c71545d5cb3d9d745a9c756e69f Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 03:31:02 -0700 Subject: [PATCH 001/121] fix: stabilize settings writes and enable fork PR scans (#2290) * fix: retire settings locks before cleanup and stabilize watcher coverage * ci: scan fork pull requests with CodeQL * test: allow full PowerShell permission parser retries --- .github/codeql/README.md | 102 ++++++++++++++++++ .github/workflows/codeql.yml | 54 ++++++++++ src/utils/permissions/permissions.test.ts | 4 +- .../settings/settings.transaction.test.ts | 35 ++++++ src/utils/settings/settingsFileTransaction.ts | 8 +- src/utils/skills/skillChangeDetector.test.ts | 15 ++- 6 files changed, 211 insertions(+), 7 deletions(-) create mode 100644 .github/codeql/README.md create mode 100644 .github/workflows/codeql.yml diff --git a/.github/codeql/README.md b/.github/codeql/README.md new file mode 100644 index 0000000000..839124da75 --- /dev/null +++ b/.github/codeql/README.md @@ -0,0 +1,102 @@ +# CodeQL analysis and merge protection + +The [CodeQL workflow](../workflows/codeql.yml) scans pull requests to `main`, +including forks, pushes to `main`, and a weekly schedule. It also supports manual +runs once the workflow exists on the default branch. JavaScript/TypeScript uses +the default security queries plus `code-quality`; GitHub Actions retains its +default security queries. Results are uploaded to **code scanning**. + +GitHub's CodeQL default setup [excludes fork pull requests](https://docs.github.com/en/code-security/concepts/code-scanning/setup-types#about-default-setup). +The separate managed Code Quality workflow has also failed to run on fork PRs in +this repository. Requiring its results leaves those PRs pending indefinitely. +Custom workflows cannot provide a supported replacement for the managed Code +Quality result: the [CodeQL action](https://github.com/github/codeql-action/blob/6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee/src/analyses.ts) +directs custom workflows to run quality queries through code scanning instead. + +## Permissions and activation + +Write access permits contributing this workflow. Changing CodeQL default setup +requires repository administration or the relevant security-management access; +editing the ruleset requires administration or a custom role with permission to +edit repository rules. Merging this file does not change either setting. + +An administrator must complete this rollout for `Twigpine/openclaude`: + +1. In **Settings → Advanced Security → CodeQL analysis**, disable default setup + and use advanced setup with this workflow. Default setup otherwise rejects + uploads from custom CodeQL workflows. The equivalent API change is: + + ```sh + gh api --method PATCH repos/Twigpine/openclaude/code-scanning/default-setup \ + -f state=not-configured + ``` + +2. Rerun the new **CodeQL** workflow on the introducing PR after changing the + setup. Verify that both `CodeQL (javascript-typescript)` and `CodeQL (actions)` + complete successfully, including their uploads. A successful analysis with + a failed upload is not sufficient. The `pull_request` trigger supports fork + uploads with the restricted token; no PAT, fork secret, `pull_request_target`, + or privileged artifact-upload workflow is needed. + +3. In **Settings → Rules → Rulesets → main branch protection** (ruleset + `14659193`), remove **Require code quality results** and add both CodeQL job + names as required status checks, selecting **GitHub Actions** as their source. + Keep every other rule, branch target, enforcement setting, bypass actor, and + existing required check. This temporary stage lets the introducing PR merge + with completed scans while the new configuration acquires a `main` baseline. + Continue the normal PR approval and CI process before merging. + +4. After merge, verify the new workflow's push run on `main` uploaded both + language analyses. Then enable **Require code scanning results**, select + **CodeQL**, and set **Alerts: Errors** and **Security alerts: None**. Retain the + two required status checks. The security threshold preserves the previous + ruleset's policy: it required quality errors but had no security-severity + merge threshold. Security findings remain visible for review. + +5. Verify a fork PR has both successful jobs and a completed CodeQL code-scanning + evaluation. Older PRs need a new push or a rerun that includes this workflow; + rerunning a historical workflow definition that lacks it cannot start it. + +The separate managed Code Quality feature may remain enabled for its dashboard. +Its **Require code quality results** rule must stay removed for this migration. +Do not disable the entire branch ruleset. + +## Prepared rule additions + +These are rule fragments for the existing ruleset, not a complete replacement +payload. Remove its `code_quality` rule, add the following `code_scanning` rule +after the baseline upload, and merge these checks into any existing +`required_status_checks` rule. Preserve all unrelated fields and rules from a +fresh administrator-readable export before saving an API update. + +```json +[ + { + "type": "code_scanning", + "parameters": { + "code_scanning_tools": [ + { + "tool": "CodeQL", + "alerts_threshold": "errors", + "security_alerts_threshold": "none" + } + ] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "required_status_checks": [ + { "context": "CodeQL (javascript-typescript)", "integration_id": 15368 }, + { "context": "CodeQL (actions)", "integration_id": 15368 } + ] + } + } +] +``` + +GitHub's [rules API](https://docs.github.com/en/rest/repos/rules#update-a-repository-ruleset) +documents these fields. Code-scanning merge protection needs results for both the +PR and its target branch, which is why activation is staged around the initial +`main` upload. diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml new file mode 100644 index 0000000000..a270408cee --- /dev/null +++ b/.github/workflows/codeql.yml @@ -0,0 +1,54 @@ +name: CodeQL + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + schedule: + - cron: "23 6 * * 2" + +permissions: + contents: read + +concurrency: + group: codeql-${{ github.event_name }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + analyze: + name: CodeQL (${{ matrix.language }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + security-events: write + strategy: + fail-fast: false + matrix: + include: + - language: javascript-typescript + queries: +code-quality + - language: actions + queries: "" + + steps: + # Keep the default PR merge checkout and pull_request event: code scanning + # supports uploads from fork PRs without a privileged fork-code workflow. + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Initialize CodeQL + uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4 + with: + languages: ${{ matrix.language }} + build-mode: none + queries: ${{ matrix.queries }} + + - name: Analyze and upload results + uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4 + with: + category: /language:${{ matrix.language }} diff --git a/src/utils/permissions/permissions.test.ts b/src/utils/permissions/permissions.test.ts index 3152ee0bfa..db49699850 100644 --- a/src/utils/permissions/permissions.test.ts +++ b/src/utils/permissions/permissions.test.ts @@ -320,6 +320,8 @@ describe('plan mode mechanical read-only policy', () => { }, ) + // Both the tool check and plan-mode guard can exhaust two 5s parse attempts. + // Allow the real parser's full retry budget before asserting the denial. test('denies mutating PowerShell', async () => { const result = await hasPermissionsToUseTool( PowerShellTool, @@ -329,7 +331,7 @@ describe('plan mode mechanical read-only policy', () => { 'tool-use-id', ) expect(result.behavior).toBe('deny') - }) + }, 25_000) test.each(['git diff', 'git status', 'ls', 'cat package.json'])( 'lets read-only Bash continue through normal permissions for %j', diff --git a/src/utils/settings/settings.transaction.test.ts b/src/utils/settings/settings.transaction.test.ts index 230d3a5613..bbcb7ab297 100644 --- a/src/utils/settings/settings.transaction.test.ts +++ b/src/utils/settings/settings.transaction.test.ts @@ -1,4 +1,5 @@ import { spawn, type ChildProcessByStdio } from 'node:child_process' +import * as fs from 'node:fs' import { existsSync, lstatSync, @@ -281,6 +282,40 @@ test( TEST_TIMEOUT_MS, ) +test('releasing a lock cannot remove a successor published during cleanup', async () => { + const root = mkdtempSync(join(tmpdir(), 'openclaude-settings-release-race-')) + const settingsPath = join(root, 'settings.json') + const originalRemove = fs.rmSync + const { withSettingsFileTransactionSync } = await import( + './settingsFileTransaction.js' + ) + let successorCompleted = false + let intercepted = false + const remove = spyOn(fs, 'rmSync').mockImplementation((path, options) => { + if (intercepted) return originalRemove(path, options) + intercepted = true + // Pause recursive cleanup after unlinking owner.json but before rmdir. + // Publishing a new non-empty directory can replace an empty directory. + originalRemove(join(String(path), 'owner.json')) + withSettingsFileTransactionSync(settingsPath, () => { + // Finish the old owner's cleanup while the successor holds its lock. + originalRemove(path, options) + }) + successorCompleted = true + }) + try { + expect(() => + withSettingsFileTransactionSync(settingsPath, () => undefined), + ).not.toThrow() + expect(intercepted).toBe(true) + expect(successorCompleted).toBe(true) + expect(readdirSync(root)).toEqual([]) + } finally { + remove.mockRestore() + originalRemove(root, { recursive: true, force: true }) + } +}) + test('reads the merge base from disk after ownership, not from warm caches', async () => { await withIsolatedUserSettings((_root, settingsPath) => { writeFileSync( diff --git a/src/utils/settings/settingsFileTransaction.ts b/src/utils/settings/settingsFileTransaction.ts index c33c8484ee..3bd3d3e698 100644 --- a/src/utils/settings/settingsFileTransaction.ts +++ b/src/utils/settings/settingsFileTransaction.ts @@ -405,7 +405,13 @@ function releaseOwnedLock( { code: 'ECOMPROMISED' }, ) } - removeLockSync(lockPath, { recursive: true }) + // Recursive removal briefly leaves an empty directory that a contender can + // replace with its own claim. Retire our non-empty lock atomically first so + // cleanup cannot remove that successor's lock. Our pending path was consumed + // by publication and is private to this ownership token. + const retiredPath = pendingLockPath(lockPath, owner) + renameLockSync(lockPath, retiredPath) + removeLockSync(retiredPath, { recursive: true }) } function describeCurrentOwner(lockPath: string): string { diff --git a/src/utils/skills/skillChangeDetector.test.ts b/src/utils/skills/skillChangeDetector.test.ts index d756696eaa..fa0236f7fa 100644 --- a/src/utils/skills/skillChangeDetector.test.ts +++ b/src/utils/skills/skillChangeDetector.test.ts @@ -362,12 +362,16 @@ describe('skillChangeDetector reload batching', () => { writeFileSync(siblingFile, 'sibling') const detector = await importFreshModule() let realWatcher: FSWatcher | undefined + const hookPaths: string[] = [] detector._setDependenciesForTesting({ getSkillsPath: () => own, getAdditionalDirectoriesForClaudeMd: () => [], getSiblingSkillsHomePaths: () => siblingSkillsHomePaths, getFsImplementation: () => ({ stat: async (path: string) => statSync(path) }), - executeConfigChangeHooks, + executeConfigChangeHooks: async (_source: string, path: string) => { + hookPaths.push(path) + return hookResults + }, clearCommandsCache, resetSentSkillNames, watch: (paths: string[], options: Parameters[1]) => { @@ -379,7 +383,7 @@ describe('skillChangeDetector reload batching', () => { stabilityThreshold: 10, pollInterval: 10, chokidarInterval: 10, reloadDebounce: 10, reloadCooldown: 10, }) - const hookCalls = () => executeConfigChangeHooks.mock.calls.length + const hookCalls = () => hookPaths.length // Settle until no hook has fired for a full window, so a late hook from an // earlier phase cannot be attributed to the next one. const waitForQuietHooks = async () => { @@ -399,14 +403,15 @@ describe('skillChangeDetector reload batching', () => { // initializing its polling baseline. Retry edits until one is observed. // A retry already in flight when the first hook lands can still produce // one more, so assert that the edit was seen rather than an exact count. - const before = hookCalls() + const start = hookPaths.length const deadline = Date.now() + 2000 let edit = 0 - while (hookCalls() === before && Date.now() < deadline) { + while (!hookPaths.slice(start).includes(path) && Date.now() < deadline) { writeFileSync(path, `${content} ${edit++}`) await sleep(50) } - expect(hookCalls()).toBeGreaterThan(before) + // This test checks source eligibility; batching has dedicated tests below. + expect(hookPaths.slice(start)).toContain(path) await waitForQuietHooks() } try { From 5a1fbe03e62f354a972871336cbf541f27cf4dd9 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 07:05:35 -0700 Subject: [PATCH 002/121] fix(tests): isolate skills homes and stabilize reload assertions (#2307) Co-authored-by: OpenClaude CI Maintenance --- src/skills/loadSkillsDir.test.ts | 57 ++++++++++++-- src/utils/skills/skillChangeDetector.test.ts | 82 +++++++++++++++----- 2 files changed, 112 insertions(+), 27 deletions(-) diff --git a/src/skills/loadSkillsDir.test.ts b/src/skills/loadSkillsDir.test.ts index 34b9c4d7fc..c9bdb41c62 100644 --- a/src/skills/loadSkillsDir.test.ts +++ b/src/skills/loadSkillsDir.test.ts @@ -5,6 +5,7 @@ import { mkdirSync, readdirSync, readFileSync, + realpathSync, rmSync, statSync, symlinkSync, @@ -131,17 +132,32 @@ function setRealFilesystemForTest(): ReturnType { return originalFs } -function setConfigDirEnv(configDir: string): void { +function setConfigDirEnv(configDir: string, homeDir = join(configDir, 'home')): void { + mkdirSync(homeDir, { recursive: true }) + process.env.HOME = homeDir + process.env.USERPROFILE = homeDir + // Bun caches os.homedir(), so changing HOME alone does not isolate siblings. + setSiblingSkillsHomeDirForTesting(homeDir) setClaudeConfigHomeDirForTesting(undefined) process.env.OPENCLAUDE_CONFIG_DIR = configDir delete process.env.CLAUDE_CONFIG_DIR } function restoreConfigDirEnv(original: { + home: string | undefined + userProfile: string | undefined openClaudeConfigDir: string | undefined claudeConfigDir: string | undefined configHomeOverride: string | undefined }): void { + for (const [key, value] of Object.entries({ + HOME: original.home, + USERPROFILE: original.userProfile, + })) { + if (value === undefined) delete process.env[key] + else process.env[key] = value + } + setSiblingSkillsHomeDirForTesting(undefined) setClaudeConfigHomeDirForTesting(original.configHomeOverride) if (original.openClaudeConfigDir === undefined) { @@ -162,6 +178,8 @@ test.serial('loads flat and nested skills with colon namespaces', async () => { const configDir = mkdtempSync(join(tmpdir(), 'openclaude-skills-')) const cwd = join(configDir, 'workspace') const originalConfigDir = { + home: process.env.HOME, + userProfile: process.env.USERPROFILE, openClaudeConfigDir: process.env.OPENCLAUDE_CONFIG_DIR, claudeConfigDir: process.env.CLAUDE_CONFIG_DIR, configHomeOverride: getClaudeConfigHomeDirOverrideForTesting(), @@ -179,6 +197,10 @@ test.serial('loads flat and nested skills with colon namespaces', async () => { clearSkillAndConfigCaches() const skills = await getSkillDirCommands(cwd) + assert.equal( + skills.some(skill => skill.type === 'prompt' && skill.siblingSkillsHome), + false, + ) const fixtureSkillsRoot = join(configDir, '.openclaude', 'skills') const promptSkills = skills.filter( ( @@ -227,6 +249,8 @@ test.serial('ignores legacy .claude project skills when .openclaude skills exist const configDir = mkdtempSync(join(tmpdir(), 'openclaude-skills-')) const cwd = join(configDir, 'workspace') const originalConfigDir = { + home: process.env.HOME, + userProfile: process.env.USERPROFILE, openClaudeConfigDir: process.env.OPENCLAUDE_CONFIG_DIR, claudeConfigDir: process.env.CLAUDE_CONFIG_DIR, configHomeOverride: getClaudeConfigHomeDirOverrideForTesting(), @@ -275,6 +299,8 @@ test.serial('loads persisted registry trust metadata from skill.json', async () const configDir = mkdtempSync(join(tmpdir(), 'openclaude-skills-')) const cwd = join(configDir, 'workspace') const originalConfigDir = { + home: process.env.HOME, + userProfile: process.env.USERPROFILE, openClaudeConfigDir: process.env.OPENCLAUDE_CONFIG_DIR, claudeConfigDir: process.env.CLAUDE_CONFIG_DIR, configHomeOverride: getClaudeConfigHomeDirOverrideForTesting(), @@ -323,6 +349,8 @@ test.serial('project skills are ordered before user skills with the same name', const configDir = mkdtempSync(join(tmpdir(), 'openclaude-skills-')) const cwd = join(configDir, 'workspace') const originalConfigDir = { + home: process.env.HOME, + userProfile: process.env.USERPROFILE, openClaudeConfigDir: process.env.OPENCLAUDE_CONFIG_DIR, claudeConfigDir: process.env.CLAUDE_CONFIG_DIR, configHomeOverride: getClaudeConfigHomeDirOverrideForTesting(), @@ -459,28 +487,31 @@ async function withSiblingHomeFixture( const rootDir = mkdtempSync(join(tmpdir(), 'openclaude-sibling-skills-')) const homeDir = join(rootDir, 'home') const configDir = join(homeDir, '.openclaude') - // Inside the fake home so project-dir traversal stops at it. + // Keep project and user skills in the same disposable fixture. const cwd = join(homeDir, 'workspace') const originalConfigDir = { + home: process.env.HOME, + userProfile: process.env.USERPROFILE, openClaudeConfigDir: process.env.OPENCLAUDE_CONFIG_DIR, claudeConfigDir: process.env.CLAUDE_CONFIG_DIR, configHomeOverride: getClaudeConfigHomeDirOverrideForTesting(), } const originalOriginalCwd = getOriginalCwd() + const originalCwd = process.cwd() const originalSettingsState = enableUserAndProjectSettingSources() const originalFs = setRealFilesystemForTest() try { mkdirSync(cwd, { recursive: true }) mkdirSync(configDir, { recursive: true }) - setSiblingSkillsHomeDirForTesting(homeDir) - setConfigDirEnv(configDir) + setConfigDirEnv(configDir, homeDir) + process.chdir(cwd) setOriginalCwd(cwd) clearSkillAndConfigCaches() await run({ homeDir, configDir, cwd }) } finally { try { - setSiblingSkillsHomeDirForTesting(undefined) + process.chdir(originalCwd) restoreConfigDirEnv(originalConfigDir) setOriginalCwd(originalOriginalCwd) setFsImplementation(originalFs) @@ -1103,6 +1134,7 @@ for (const home of ['.agents', '.claude'] as const) { const plainFile = join(writeSiblingSkill(homeDir, home, 'plain'), 'SKILL.md') symlinkSync(target, join(homeDir, home, 'skills', 'linked'), process.platform === 'win32' ? 'junction' : 'dir') const ready: Promise[] = [] + const changedPaths = new Set() await detector.resetForTesting({ stabilityThreshold: 10, pollInterval: 10, chokidarInterval: 10, reloadDebounce: 10, reloadCooldown: 10, @@ -1111,6 +1143,7 @@ for (const home of ['.agents', '.claude'] as const) { executeConfigChangeHooks: async () => [], watch: (paths, options) => { const watcher = chokidar.watch(paths, options) + watcher.on('change', path => changedPaths.add(realpathSync(path))) ready.push(new Promise(resolve => watcher.once('ready', resolve))) return watcher }, @@ -1127,12 +1160,20 @@ for (const home of ['.agents', '.claude'] as const) { const before = notifications const deadline = Date.now() + 2500 let edit = 0 - while (Date.now() < deadline) { + const watchedFile = realpathSync(file) + changedPaths.delete(watchedFile) + // Retry only until Chokidar observes the write. Retrying until reload + // can enqueue another edit or continually restart the reload debounce. + while (!changedPaths.has(watchedFile) && Date.now() < deadline) { writeFileSync(file, `---\ndescription: ${marker}\n---\nInstructions ${marker} ${edit++}\n`) await Bun.sleep(50) - if (notifications > before && (await invokedText(name)).includes(marker)) return } - assert.fail(`No refreshed invocation for ${name}: ${marker}`) + assert.ok(changedPaths.has(watchedFile), `No file event for ${name}: ${marker}`) + while (notifications === before && Date.now() < deadline) { + await Bun.sleep(10) + } + assert.ok(notifications > before, `No reload for ${name}: ${marker}`) + assert.ok((await invokedText(name)).includes(marker)) } try { await detector.initialize() diff --git a/src/utils/skills/skillChangeDetector.test.ts b/src/utils/skills/skillChangeDetector.test.ts index fa0236f7fa..cb3ffa234c 100644 --- a/src/utils/skills/skillChangeDetector.test.ts +++ b/src/utils/skills/skillChangeDetector.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterEach, beforeEach, describe, expect, jest, mock, test } from 'bun:test' import * as platformPath from 'path' import chokidar, { type FSWatcher } from 'chokidar' import { type Stats, mkdirSync, mkdtempSync, rmSync, statSync, writeFileSync } from 'fs' @@ -33,6 +33,9 @@ let chokidarWatch = mock(() => ({ close: mock(async () => {}), })) let activeDetector: SkillChangeDetectorModule | null = null +let testHome: string +let originalCwd: string +let originalEnv: Record function installMocks(): void { clearCommandsCache = mock(() => {}) @@ -91,8 +94,29 @@ function sleep(ms: number): Promise { return new Promise(resolve => setTimeout(resolve, ms)) } +async function advanceTime(ms: number): Promise { + await new Promise(resolve => setImmediate(resolve)) + jest.advanceTimersByTime(ms) + // Timer callbacks await hooks; let their promise continuations finish before + // assertions or the next advance (which may depend on the new cooldown). + await new Promise(resolve => setImmediate(resolve)) +} + beforeEach(async () => { await acquireSharedMutationLock('utils/skills/skillChangeDetector.test.ts') + originalCwd = process.cwd() + originalEnv = { + HOME: process.env.HOME, + USERPROFILE: process.env.USERPROFILE, + OPENCLAUDE_CONFIG_DIR: process.env.OPENCLAUDE_CONFIG_DIR, + CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR, + } + testHome = mkdtempSync(platformPath.join(tmpdir(), 'skill-detector-home-')) + process.env.HOME = testHome + process.env.USERPROFILE = testHome + process.env.OPENCLAUDE_CONFIG_DIR = platformPath.join(testHome, '.openclaude') + delete process.env.CLAUDE_CONFIG_DIR + process.chdir(testHome) installMocks() }) @@ -103,6 +127,13 @@ afterEach(async () => { activeDetector = null mock.restore() } finally { + jest.useRealTimers() + process.chdir(originalCwd) + for (const [key, value] of Object.entries(originalEnv)) { + if (value === undefined) delete process.env[key] + else process.env[key] = value + } + rmSync(testHome, { recursive: true, force: true }) releaseSharedMutationLock() } }) @@ -363,6 +394,7 @@ describe('skillChangeDetector reload batching', () => { const detector = await importFreshModule() let realWatcher: FSWatcher | undefined const hookPaths: string[] = [] + const changedPaths = new Set() detector._setDependenciesForTesting({ getSkillsPath: () => own, getAdditionalDirectoriesForClaudeMd: () => [], @@ -376,6 +408,7 @@ describe('skillChangeDetector reload batching', () => { resetSentSkillNames, watch: (paths: string[], options: Parameters[1]) => { realWatcher = chokidar.watch(paths, options) + realWatcher.on('change', path => changedPaths.add(path)) return realWatcher }, }) @@ -400,16 +433,20 @@ describe('skillChangeDetector reload batching', () => { } const editUntilHook = async (path: string, content: string) => { // Chokidar's ready marks traversal completion, but Bun may still be - // initializing its polling baseline. Retry edits until one is observed. - // A retry already in flight when the first hook lands can still produce - // one more, so assert that the edit was seen rather than an exact count. + // initializing its polling baseline. Stop retrying when the file event + // arrives, then wait for its hook without resetting the reload debounce. const start = hookPaths.length const deadline = Date.now() + 2000 let edit = 0 - while (!hookPaths.slice(start).includes(path) && Date.now() < deadline) { + changedPaths.delete(path) + while (!changedPaths.has(path) && Date.now() < deadline) { writeFileSync(path, `${content} ${edit++}`) await sleep(50) } + expect(changedPaths.has(path)).toBe(true) + while (!hookPaths.slice(start).includes(path) && Date.now() < deadline) { + await sleep(10) + } // This test checks source eligibility; batching has dedicated tests below. expect(hookPaths.slice(start)).toContain(path) await waitForQuietHooks() @@ -440,6 +477,7 @@ describe('skillChangeDetector reload batching', () => { test('batches rapid reload requests into one hook/cache clear/notification', async () => { const detector = await importFreshModule() + jest.useFakeTimers() await detector.resetForTesting({ reloadDebounce: 5, reloadCooldown: 20 }) let notifications = 0 @@ -451,7 +489,7 @@ describe('skillChangeDetector reload batching', () => { detector._scheduleReloadForTesting('/tmp/skills/b/SKILL.md') detector._scheduleReloadForTesting('/tmp/skills/c/SKILL.md') - await sleep(30) + await advanceTime(30) expect(executeConfigChangeHooks).toHaveBeenCalledTimes(1) expect(clearCommandsCache).toHaveBeenCalledTimes(1) @@ -465,6 +503,7 @@ describe('skillChangeDetector reload batching', () => { test('delays a second burst until the reload cooldown has elapsed', async () => { const detector = await importFreshModule() + jest.useFakeTimers() await detector.resetForTesting({ reloadDebounce: 5, reloadCooldown: 60 }) let notifications = 0 @@ -473,14 +512,14 @@ describe('skillChangeDetector reload batching', () => { }) detector._scheduleReloadForTesting('/tmp/skills/first/SKILL.md') - await sleep(20) + await advanceTime(20) expect(notifications).toBe(1) detector._scheduleReloadForTesting('/tmp/skills/second/SKILL.md') - await sleep(20) + await advanceTime(20) expect(notifications).toBe(1) - await sleep(60) + await advanceTime(60) expect(notifications).toBe(2) expect(executeConfigChangeHooks).toHaveBeenCalledTimes(2) @@ -490,6 +529,7 @@ describe('skillChangeDetector reload batching', () => { test('does not start cooldown when a ConfigChange hook blocks reload', async () => { const detector = await importFreshModule() + jest.useFakeTimers() await detector.resetForTesting({ reloadDebounce: 5, reloadCooldown: 500 }) let notifications = 0 @@ -499,13 +539,13 @@ describe('skillChangeDetector reload batching', () => { hookResults = [{ blocked: true }] detector._scheduleReloadForTesting('/tmp/skills/blocked/SKILL.md') - await sleep(20) + await advanceTime(20) expect(notifications).toBe(0) expect(clearCommandsCache).not.toHaveBeenCalled() hookResults = [] detector._scheduleReloadForTesting('/tmp/skills/allowed/SKILL.md') - await sleep(20) + await advanceTime(20) expect(notifications).toBe(1) expect(clearCommandsCache).toHaveBeenCalledTimes(1) @@ -516,6 +556,7 @@ describe('skillChangeDetector reload batching', () => { test('queues events during an in-flight reload before cache clear and notification', async () => { const detector = await importFreshModule() + jest.useFakeTimers() await detector.resetForTesting({ reloadDebounce: 5, reloadCooldown: 60 }) let releaseFirstHook: (() => void) | undefined @@ -541,21 +582,21 @@ describe('skillChangeDetector reload batching', () => { }) detector._scheduleReloadForTesting('/tmp/skills/first/SKILL.md') - await sleep(20) + await advanceTime(20) expect(executeConfigChangeHooks).toHaveBeenCalledTimes(1) detector._scheduleReloadForTesting('/tmp/skills/second/SKILL.md') - await sleep(30) + await advanceTime(30) expect(executeConfigChangeHooks).toHaveBeenCalledTimes(1) releaseFirstHook?.() - await sleep(20) + await advanceTime(20) expect(notifications).toBe(0) expect(clearCommandsCache).not.toHaveBeenCalled() expect(executeConfigChangeHooks).toHaveBeenCalledTimes(2) releaseSecondHook?.() - await sleep(30) + await advanceTime(30) expect(notifications).toBe(1) expect(executeConfigChangeHooks).toHaveBeenCalledTimes(2) @@ -565,6 +606,7 @@ describe('skillChangeDetector reload batching', () => { test('dispose prevents an in-flight reload from clearing caches after hook resolution', async () => { const detector = await importFreshModule() + jest.useFakeTimers() await detector.resetForTesting({ reloadDebounce: 5, reloadCooldown: 20 }) let releaseHook: (() => void) | undefined @@ -581,12 +623,12 @@ describe('skillChangeDetector reload batching', () => { }) detector._scheduleReloadForTesting('/tmp/skills/in-flight/SKILL.md') - await sleep(20) + await advanceTime(20) expect(executeConfigChangeHooks).toHaveBeenCalledTimes(1) await detector.dispose() releaseHook?.() - await sleep(20) + await advanceTime(20) expect(clearCommandsCache).not.toHaveBeenCalled() expect(resetSentSkillNames).not.toHaveBeenCalled() @@ -595,6 +637,7 @@ describe('skillChangeDetector reload batching', () => { test('dispose prevents later reload scheduling from running hooks', async () => { const detector = await importFreshModule() + jest.useFakeTimers() await detector.resetForTesting({ reloadDebounce: 5, reloadCooldown: 20 }) let notifications = 0 @@ -604,7 +647,7 @@ describe('skillChangeDetector reload batching', () => { await detector.dispose() detector._scheduleReloadForTesting('/tmp/skills/after-dispose/SKILL.md') - await sleep(20) + await advanceTime(20) expect(executeConfigChangeHooks).not.toHaveBeenCalled() expect(clearCommandsCache).not.toHaveBeenCalled() @@ -613,11 +656,12 @@ describe('skillChangeDetector reload batching', () => { test('resetForTesting clears pending reload timers', async () => { const detector = await importFreshModule() + jest.useFakeTimers() await detector.resetForTesting({ reloadDebounce: 20, reloadCooldown: 20 }) detector._scheduleReloadForTesting('/tmp/skills/pending/SKILL.md') await detector.resetForTesting({ reloadDebounce: 5, reloadCooldown: 5 }) - await sleep(30) + await advanceTime(30) expect(executeConfigChangeHooks).not.toHaveBeenCalled() expect(clearCommandsCache).not.toHaveBeenCalled() From 9254846b52d7d04028864cd32f7a5aa97c8cc24a Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 08:51:13 -0700 Subject: [PATCH 003/121] test(mcp): remove always-skipped CHICAGO_MCP toggle test (#2309) --- src/services/mcp/config.test.ts | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/services/mcp/config.test.ts b/src/services/mcp/config.test.ts index cb413d21e7..f958985fc5 100644 --- a/src/services/mcp/config.test.ts +++ b/src/services/mcp/config.test.ts @@ -1,4 +1,3 @@ -import { feature } from 'bun:bundle' import { afterEach, beforeEach, expect, test } from 'bun:test' import { acquireSharedMutationLock, @@ -105,14 +104,3 @@ test('enabling the last inherited server keeps an explicit empty override', () = expect(isMcpServerDisabled('a')).toBe(false) expect(getGlobalConfig().disabledMcpServers).toEqual(['a']) }) - -test.skipIf(feature('CHICAGO_MCP') ? false : true)('builtin toggles leave disabled lists unchanged', () => { - getGlobalConfig().disabledMcpServers = ['computer-use', 'other'] - expect(isMcpServerDisabled('computer-use')).toBe(true) - setMcpServerEnabled('computer-use', true) - expect(isMcpServerDisabled('computer-use')).toBe(false) - setMcpServerEnabled('computer-use', false) - expect(isMcpServerDisabled('computer-use')).toBe(true) - expect(getGlobalConfig().disabledMcpServers).toEqual(['computer-use', 'other']) - expect(getCurrentProjectConfig().disabledMcpServers).toBeUndefined() -}) From 76b4402057773e81e581c2b6ec9f360cd063f0be Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 09:44:33 -0700 Subject: [PATCH 004/121] ci: drop duplicate provider test steps from PR checks (#2308) * ci: drop duplicate provider test steps from PR checks * docs(contributing): drop provider suites from required preflight --- .github/workflows/pr-checks.yml | 5 ----- CONTRIBUTING.md | 2 -- 2 files changed, 7 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 4215c7896f..e92185b785 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -49,11 +49,6 @@ jobs: PR_SCAN_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || 'origin/main' }} PR_SCAN_HEAD: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'HEAD' }} run: bun run security:pr-scan -- --base "$PR_SCAN_BASE" --head "$PR_SCAN_HEAD" - - name: Provider tests - run: bun run test:provider - - - name: Provider recommendation tests - run: npm run test:provider-recommendation launcher-node-floor: runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 815dc4c8f6..76277dcf8d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -266,8 +266,6 @@ bun run check bun run typecheck bun run typecheck:type-tests node bin/openclaude --version -bun run test:provider -npm run test:provider-recommendation git fetch https://github.com/Twigpine/openclaude.git main bun run security:pr-scan -- --base FETCH_HEAD --head HEAD ``` From e81a91784cc7457d5e92fc5740c13e18dc8a57c1 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 10:36:00 -0700 Subject: [PATCH 005/121] test: include context efficiency feature tests in full check (#2311) Co-authored-by: Contributor --- CONTRIBUTING.md | 2 +- package.json | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76277dcf8d..625407c0ed 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -307,7 +307,7 @@ bun run web:build Notes on the local preflight: - `bun run check` already builds the CLI and includes smoke, deadcode, and the full unit pass (`test:full`) — do not run those separately, or you execute work twice. -- `bun run test` isolates each test file's globals and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups, then applies the same completion check to the conversation-arc pass. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files to avoid Bun’s isolated-stdio pipe registration bug; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI additionally caps each smoke/test job at 15 minutes and the other jobs at five minutes. +- `bun run test` isolates each test file's globals and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups, then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files to avoid Bun’s isolated-stdio pipe registration bug; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI additionally caps each smoke/test job at 15 minutes and the other jobs at five minutes. - Fetching upstream `main` by URL avoids assuming that a fork checkout's `origin` points at Twigpine/openclaude. `FETCH_HEAD` is the fetched upstream tip. The scan deliberately uses local `HEAD` so it includes commits that have not been pushed yet; CI uses the pushed PR head SHA after the push. - The web CI job remains unconditional as an integration backstop. Contributors do not need to run the web suite locally for changes that cannot affect the site. - This preflight covers the same command families as CI, but it does not reproduce CI exactly in one shell. CI runs the main checks under Node 22 and 24.11.x, separately builds and launches under exact Node 22.0.0, and executes every job on a clean runner. diff --git a/package.json b/package.json index ac6c2434b1..b14988938e 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,9 @@ "web:preview": "bun run --cwd web preview", "web:typecheck": "bun run --cwd web typecheck", "test": "bun run scripts/run-tests.ts --feature=UNATTENDED_RETRY --max-concurrency=1", - "test:full": "bun run scripts/run-tests.ts --shards=8 --feature=UNATTENDED_RETRY --max-concurrency=1 && bun run test:conversation-arc", + "test:full": "bun run scripts/run-tests.ts --shards=8 --feature=UNATTENDED_RETRY --max-concurrency=1 && bun run test:conversation-arc && bun run test:context-efficiency", "test:conversation-arc": "bun run scripts/run-tests.ts --feature=CONVERSATION_ARC --feature=MULTI_TURN_CONTEXT --max-concurrency=1 src/query.conversationArc.test.ts", + "test:context-efficiency": "bun run scripts/run-tests.ts --feature=UNATTENDED_RETRY --feature=HISTORY_SNIP --max-concurrency=1 src/utils/attachments.contextEfficiency.test.ts", "test:coverage": "bun test --feature=UNATTENDED_RETRY --coverage --coverage-reporter=lcov --coverage-dir=coverage --max-concurrency=1 && bun run scripts/render-coverage-heatmap.ts", "test:coverage:ui": "bun run scripts/render-coverage-heatmap.ts", "security:pr-scan": "bun run scripts/pr-intent-scan.ts", From ae9b3192a4409a0b81734a7e950d161243004ff9 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 11:16:37 -0700 Subject: [PATCH 006/121] ci: isolate launcher checks from the full suite (#2312) Co-authored-by: Contributor <> --- .github/workflows/pr-checks.yml | 44 ++++++++++++++++++++++++++------- CONTRIBUTING.md | 4 +-- 2 files changed, 37 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index e92185b785..ad3151892c 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -13,9 +13,6 @@ jobs: smoke-and-tests: runs-on: ubuntu-latest timeout-minutes: 15 - strategy: - matrix: - node-version: ["22", "24.11.x"] steps: - name: Check out repository @@ -26,7 +23,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: ${{ matrix.node-version }} + node-version: "22" - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -39,17 +36,46 @@ jobs: - name: Smoke and full unit test suite run: bun run check - - name: Launcher compatibility - run: | - node bin/openclaude --version - NODE_DISABLE_COMPILE_CACHE=1 node bin/openclaude --version - - name: Suspicious PR intent scan env: PR_SCAN_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || 'origin/main' }} PR_SCAN_HEAD: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'HEAD' }} run: bun run security:pr-scan -- --base "$PR_SCAN_BASE" --head "$PR_SCAN_HEAD" + launcher-compatibility: + runs-on: ubuntu-latest + timeout-minutes: 5 + strategy: + matrix: + node-version: ["22", "24.11.x"] + + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: ${{ matrix.node-version }} + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version-file: .bun-version + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build + run: bun run build + + - name: Launcher compatibility + run: | + node bin/openclaude --version + NODE_DISABLE_COMPILE_CACHE=1 node bin/openclaude --version + launcher-node-floor: runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 625407c0ed..e3154296bc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -307,10 +307,10 @@ bun run web:build Notes on the local preflight: - `bun run check` already builds the CLI and includes smoke, deadcode, and the full unit pass (`test:full`) — do not run those separately, or you execute work twice. -- `bun run test` isolates each test file's globals and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups, then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files to avoid Bun’s isolated-stdio pipe registration bug; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI additionally caps each smoke/test job at 15 minutes and the other jobs at five minutes. +- `bun run test` isolates each test file's globals and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups, then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files to avoid Bun’s isolated-stdio pipe registration bug; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI additionally caps the suite job at 15 minutes and the other jobs at five minutes. - Fetching upstream `main` by URL avoids assuming that a fork checkout's `origin` points at Twigpine/openclaude. `FETCH_HEAD` is the fetched upstream tip. The scan deliberately uses local `HEAD` so it includes commits that have not been pushed yet; CI uses the pushed PR head SHA after the push. - The web CI job remains unconditional as an integration backstop. Contributors do not need to run the web suite locally for changes that cannot affect the site. -- This preflight covers the same command families as CI, but it does not reproduce CI exactly in one shell. CI runs the main checks under Node 22 and 24.11.x, separately builds and launches under exact Node 22.0.0, and executes every job on a clean runner. +- This preflight covers the same command families as CI, but it does not reproduce CI exactly in one shell. CI runs the full suite once with Bun and Node 22 available for smoke checks, separately builds and checks launcher compatibility under Node 22 and 24.11.x, separately builds and launches under exact Node 22.0.0, and executes every job on a clean runner. If a local or GitHub CI check fails, first determine whether the PR owns the failure. Treat a failure as pre-existing only when you can reproduce it on the PR's current base (or link to the same failure on a current `main` CI run) and the PR neither changes the causal surface nor claims to fix that problem. From c5f53195ba5b693215fe271f186a57d645fc2124 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 12:45:10 -0700 Subject: [PATCH 007/121] ci: cache package downloads in PR checks (#2313) * ci: cache package downloads in PR checks * ci: cache package downloads in the suite job * ci: fix suite cache key and separate root and web cache prefixes --- .github/workflows/pr-checks.yml | 40 +++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index ad3151892c..3e6e0ad25c 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -30,6 +30,14 @@ jobs: with: bun-version-file: .bun-version + - name: Cache Bun packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-root-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-root- + - name: Install dependencies run: bun install --frozen-lockfile @@ -65,6 +73,14 @@ jobs: with: bun-version-file: .bun-version + - name: Cache Bun packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-root-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-root- + - name: Install dependencies run: bun install --frozen-lockfile @@ -96,6 +112,14 @@ jobs: with: bun-version-file: .bun-version + - name: Cache Bun packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-root-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-root- + - name: Install dependencies run: bun install --frozen-lockfile @@ -120,6 +144,14 @@ jobs: with: bun-version-file: .bun-version + - name: Cache Bun packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-root-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-root- + - name: Install dependencies run: bun install --frozen-lockfile @@ -147,6 +179,14 @@ jobs: with: bun-version-file: .bun-version + - name: Cache Bun packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-web-${{ hashFiles('web/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-web- + - name: Install web dependencies run: bun install --cwd web --frozen-lockfile From 0b8daaeb047abc6ed4c010f85c36fd8eab630dd3 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 13:19:52 -0700 Subject: [PATCH 008/121] ci: run unit test shards in parallel (#2314) * ci: run unit test shards in parallel * fix(ci): address shard-selection review feedback * ci: cache package downloads in the shard and dedicated test jobs --- .github/workflows/pr-checks.yml | 78 +++++++++++++++++++++++- CONTRIBUTING.md | 6 +- package.json | 3 +- scripts/run-tests.test.ts | 101 +++++++++++++++++++++++++++++++- scripts/run-tests.ts | 12 +++- 5 files changed, 189 insertions(+), 11 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 3e6e0ad25c..8cef8d78dd 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -41,8 +41,8 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Smoke and full unit test suite - run: bun run check + - name: Smoke and static checks + run: bun run check:static - name: Suspicious PR intent scan env: @@ -50,6 +50,80 @@ jobs: PR_SCAN_HEAD: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || 'HEAD' }} run: bun run security:pr-scan -- --base "$PR_SCAN_BASE" --head "$PR_SCAN_HEAD" + unit-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + strategy: + fail-fast: false + matrix: + shard: [1, 2, 3, 4, 5, 6, 7, 8] + + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22" + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version-file: .bun-version + + - name: Cache Bun packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-root-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-root- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Build + run: bun run build + + - name: Unit test shard + run: bun run scripts/run-tests.ts --shards=8 --shard-index=${{ matrix.shard }} --feature=UNATTENDED_RETRY --max-concurrency=1 + + dedicated-tests: + runs-on: ubuntu-latest + timeout-minutes: 15 + + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22" + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version-file: .bun-version + + - name: Cache Bun packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-root-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-root- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Conversation arc tests + run: bun run test:conversation-arc + + - name: Context efficiency tests + run: bun run test:context-efficiency + launcher-compatibility: runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e3154296bc..4c5ed3df64 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -306,11 +306,11 @@ bun run web:build Notes on the local preflight: -- `bun run check` already builds the CLI and includes smoke, deadcode, and the full unit pass (`test:full`) — do not run those separately, or you execute work twice. -- `bun run test` isolates each test file's globals and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups, then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files to avoid Bun’s isolated-stdio pipe registration bug; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI additionally caps the suite job at 15 minutes and the other jobs at five minutes. +- `bun run check` runs `check:static` (the any-usage budget, smoke build, and deadcode checks), then the full unit pass (`test:full`) — do not run those separately, or you execute work twice. +- `bun run test` isolates each test file's globals and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups. Use `bun run scripts/run-tests.ts --shards=N --shard-index=K` with the same test flags to run only shard K of N. `test:full` then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files to avoid Bun’s isolated-stdio pipe registration bug; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI runs `check:static` in `smoke-and-tests`, runs the eight unit shards as parallel `unit-tests` jobs, and runs each dedicated feature pass once in `dedicated-tests`. Each unit shard builds the CLI before testing. CI caps these jobs at 15 minutes each and the other jobs at five minutes. - Fetching upstream `main` by URL avoids assuming that a fork checkout's `origin` points at Twigpine/openclaude. `FETCH_HEAD` is the fetched upstream tip. The scan deliberately uses local `HEAD` so it includes commits that have not been pushed yet; CI uses the pushed PR head SHA after the push. - The web CI job remains unconditional as an integration backstop. Contributors do not need to run the web suite locally for changes that cannot affect the site. -- This preflight covers the same command families as CI, but it does not reproduce CI exactly in one shell. CI runs the full suite once with Bun and Node 22 available for smoke checks, separately builds and checks launcher compatibility under Node 22 and 24.11.x, separately builds and launches under exact Node 22.0.0, and executes every job on a clean runner. +- This preflight covers the same command families as CI, but it does not reproduce CI exactly in one shell. CI runs static checks with Node 22 available for smoke checks, runs the unit shards and dedicated feature passes under Node 22, separately builds and checks launcher compatibility under Node 22 and 24.11.x, separately builds and launches under exact Node 22.0.0, and executes every job on a clean runner. If a local or GitHub CI check fails, first determine whether the PR owns the failure. Treat a failure as pre-existing only when you can reproduce it on the PR's current base (or link to the same failure on a current `main` CI run) and the PR neither changes the causal surface nor claims to fix that problem. diff --git a/package.json b/package.json index b14988938e..09a6c8e607 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,8 @@ "deadcode": "knip --include files,dependencies", "lint": "eslint .", "lint:any-budget": "bun run scripts/any-usage-report.ts --check", - "check": "bun run lint:any-budget && bun run smoke && bun run deadcode && bun run test:full", + "check:static": "bun run lint:any-budget && bun run smoke && bun run deadcode", + "check": "bun run check:static && bun run test:full", "check:strict": "bun run typecheck && bun run lint && bun run lint:any-budget && bun run smoke && bun run deadcode && bun run test:full", "verify:privacy": "bun run scripts/verify-no-phone-home.ts", "install:verify": "bun run scripts/verify-clean-install.ts", diff --git a/scripts/run-tests.test.ts b/scripts/run-tests.test.ts index fd6e2b33da..c5579f7dc2 100644 --- a/scripts/run-tests.test.ts +++ b/scripts/run-tests.test.ts @@ -112,6 +112,102 @@ for (const shards of [1, 2]) { } } +test('selected shards partition the same files as the serial run exactly once', async () => { + const dir = await mkdtemp(join(tmpdir(), 'openclaude-runner-selection-')) + try { + const marker = join(dir, 'completed') + const fixtures = Array.from({ length: 8 }, (_, index) => join(dir, `fixture-${index + 1}.test.ts`)) + for (const [index, fixture] of fixtures.entries()) { + await writeFile(fixture, ` + import { test } from 'bun:test' + import { appendFileSync } from 'node:fs' + test('fixture ${index + 1}', () => appendFileSync(process.env.OC_TEST_MARKER!, '${index + 1}\\n')) + `) + } + const run = async (shardIndex?: number) => { + await writeFile(marker, '') + const child = Bun.spawn([ + process.execPath, runner, '--shards=8', + ...(shardIndex === undefined ? [] : [`--shard-index=${shardIndex}`]), + ...fixtures, + ], { stdout: 'pipe', stderr: 'pipe', env: { ...process.env, OC_TEST_MARKER: marker } }) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, new Response(child.stdout).text(), new Response(child.stderr).text(), + ]) + expect(exitCode, stdout + stderr).toBe(0) + expect(stderr).toContain(`Completed ${shardIndex === undefined ? '8/8' : '1/1'} test shards.`) + const executed = (await readFile(marker, 'utf8')).trim().split('\n').sort() + return { executed, stderr } + } + const serial = await run() + expect(serial.executed).toEqual(['1', '2', '3', '4', '5', '6', '7', '8']) + const selected: string[] = [] + for (let shardIndex = 1; shardIndex <= 8; shardIndex++) { + const result = await run(shardIndex) + expect(result.stderr.match(/Starting test shard \d+\/8/g)).toEqual([`Starting test shard ${shardIndex}/8`]) + const serialShard = serial.stderr.split(`Starting test shard ${shardIndex}/8`)[1]!.split('Finished test shard')[0]! + const assigned = [...serialShard.matchAll(/fixture-(\d+)\.test\.ts:/g)].map(match => match[1]!).sort() + expect(result.executed).toEqual(assigned) + selected.push(...result.executed) + } + expect(selected.sort()).toEqual(serial.executed) + } finally { + await rm(dir, { recursive: true, force: true }) + } +}, 15_000) + +for (const options of [ + ['--shard-index=0'], + ['--shard-index=-1'], + ['--shard-index=3'], + ['--shard-index=1.5'], + ['--shard-index=invalid'], + ['--shard-index=9007199254740992'], + ['--shard-index='], + ['--shard-index'], + ['--shard-index', '1'], + ['--shard-index=1', '--shard-index=2'], +]) { + test(`test runner rejects invalid selection ${options.join(' ')}`, async () => { + const child = Bun.spawn([process.execPath, runner, '--shards=2', ...options], { + stdout: 'pipe', stderr: 'pipe', + }) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, new Response(child.stdout).text(), new Response(child.stderr).text(), + ]) + expect(exitCode, stdout + stderr).not.toBe(0) + expect(stderr).toContain('Use one --shard-index=') + expect(stderr).not.toContain('Starting test shard') + }) +} + +for (const scenario of ['assertion', 'exit'] as const) { + test(`selected shard detects ${scenario} failure`, async () => { + const dir = await mkdtemp(join(tmpdir(), 'openclaude-runner-selected-failure-')) + try { + const fixtures = [join(dir, 'first.test.ts'), join(dir, 'later.test.ts')] + for (const fixture of fixtures) { + await writeFile(fixture, ` + import { test, expect } from 'bun:test' + test('failure', () => { ${scenario === 'exit' ? 'process.exit(0)' : 'expect(1).toBe(2)'} }) + `) + } + const child = Bun.spawn([process.execPath, runner, '--shards=2', '--shard-index=2', ...fixtures], { + stdout: 'pipe', stderr: 'pipe', + }) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, new Response(child.stdout).text(), new Response(child.stderr).text(), + ]) + expect(exitCode, stdout + stderr).toBe(1) + expect(stderr.match(/Starting test shard \d+\/2/g)).toEqual(['Starting test shard 2/2']) + expect(stderr).toContain(`Completed ${scenario === 'exit' ? 0 : 1}/1 test shards.`) + if (scenario === 'exit') expect(stderr).toContain('without a complete JUnit report') + } finally { + await rm(dir, { recursive: true, force: true }) + } + }) +} + for (const signal of [undefined, 'SIGINT', 'SIGTERM'] as const) { test.skipIf(process.platform === 'win32')(`test runner kills a stuck shard and its child (${signal ?? 'deadline'})`, async () => { const dir = await mkdtemp(join(tmpdir(), 'openclaude-runner-hang-')) @@ -135,7 +231,8 @@ for (const signal of [undefined, 'SIGINT', 'SIGTERM'] as const) { }, 200) `) const shardTimeout = signal ? 4000 : 1500 - child = Bun.spawn([process.execPath, runner, `--shard-timeout-ms=${shardTimeout}`, fixture], { + child = Bun.spawn([process.execPath, runner, `--shard-timeout-ms=${shardTimeout}`, + ...(signal ? [] : ['--shards=2', '--shard-index=1']), fixture], { stdout: 'pipe', stderr: 'pipe', timeout: 6000, killSignal: 'SIGKILL', }) const output = Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text()]) @@ -152,7 +249,7 @@ for (const signal of [undefined, 'SIGINT', 'SIGTERM'] as const) { const [, stderr] = await output expect(stderr).toContain('last diagnostic before hang') expect(exitCode, stderr).toBe(signal === 'SIGINT' ? 130 : signal === 'SIGTERM' ? 143 : 124) - if (!signal) expect(stderr).toContain('Test shard 1/1 timed out after 1500ms.') + if (!signal) expect(stderr).toContain('Test shard 1/2 timed out after 1500ms.') // An orphaned compiler must not survive a timeout or cancellation. for (let attempt = 0; attempt < 100; attempt++) { try { process.kill(descendant!, 0) } catch { break } diff --git a/scripts/run-tests.ts b/scripts/run-tests.ts index 1461a7fbd2..9dac73fa37 100644 --- a/scripts/run-tests.ts +++ b/scripts/run-tests.ts @@ -6,23 +6,29 @@ import { join } from 'node:path' const args = process.argv.slice(2) const shardOptions = args.filter(arg => arg.startsWith('--shards=')) const shards = shardOptions.length ? Number(shardOptions[0]!.slice('--shards='.length)) : 1 +const shardIndexOptions = args.filter(arg => arg === '--shard-index' || arg.startsWith('--shard-index=')) +const shardIndex = shardIndexOptions.length ? Number(shardIndexOptions[0]!.slice('--shard-index='.length)) : undefined const timeoutOptions = args.filter(arg => arg.startsWith('--shard-timeout-ms=')) const timeoutMs = timeoutOptions.length ? Number(timeoutOptions[0]!.split('=')[1]) : 600_000 if (shardOptions.length > 1 || !Number.isSafeInteger(shards) || shards < 1 || args.some(arg => arg === '--shard' || arg.startsWith('--shard='))) { throw new Error('Use one --shards= option; native --shard is not supported.') } +if (shardIndexOptions.length > 1 || (shardIndex !== undefined && + (!Number.isSafeInteger(shardIndex) || shardIndex < 1 || shardIndex > shards))) { + throw new Error('Use one --shard-index= option.') +} if (timeoutOptions.length > 1 || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 2_147_483_647) { throw new Error('Use one --shard-timeout-ms= option.') } -const testArgs = args.filter(arg => !arg.startsWith('--shards=') && !arg.startsWith('--shard-timeout-ms=')) +const testArgs = args.filter(arg => !arg.startsWith('--shards=') && !arg.startsWith('--shard-index=') && !arg.startsWith('--shard-timeout-ms=')) // Bun can exit zero without finishing its suite if a test calls process.exit(0). // Its JUnit report is written only when the runner finishes; require a fresh one. const reportDir = await mkdtemp(join(tmpdir(), 'openclaude-test-report-')) let exitCode = 0 let completed = 0 try { - for (let shard = 1; shard <= shards; shard++) { + for (let shard = shardIndex ?? 1; shard <= (shardIndex ?? shards); shard++) { const started = performance.now() console.error(`Starting test shard ${shard}/${shards} (deadline ${timeoutMs}ms).`) const reportPath = join(reportDir, `results-${shard}.xml`) @@ -138,7 +144,7 @@ try { } } } - if (shards > 1) console.error(`Completed ${completed}/${shards} test shards.`) + if (shards > 1) console.error(`Completed ${completed}/${shardIndex === undefined ? shards : 1} test shards.`) } finally { await rm(reportDir, { recursive: true, force: true }) } From 1f6f7473099ee2efb67d6dbbada047e67d82bd04 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 14:09:41 -0700 Subject: [PATCH 009/121] ci: skip unaffected pull request checks (#2315) * ci: skip unaffected pull request checks * fix(ci): run the PR intent scan on every pull request --------- Co-authored-by: Repository contributors --- .github/workflows/pr-checks.yml | 70 +++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 6 +-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 8cef8d78dd..7fd5949091 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -10,7 +10,34 @@ permissions: contents: read jobs: + changes: + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + pull-requests: read + outputs: + code: ${{ steps.filter.outputs.code }} + web: ${{ steps.filter.outputs.web }} + steps: + - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 + id: filter + if: ${{ github.event_name == 'pull_request' }} + with: + filters: | + code: + - '.github/**' + - '!{*.md,**/*.md,docs/**}' + web: + - 'web/**' + - 'package.json' + - 'bun.lock' + - '.bun-version' + - '.github/workflows/pr-checks.yml' + smoke-and-tests: + needs: changes + if: ${{ github.event_name == 'push' || needs.changes.outputs.code == 'true' }} runs-on: ubuntu-latest timeout-minutes: 15 @@ -44,6 +71,37 @@ jobs: - name: Smoke and static checks run: bun run check:static + pr-intent-scan: + runs-on: ubuntu-latest + timeout-minutes: 5 + + steps: + - name: Check out repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "22" + + - name: Set up Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + bun-version-file: .bun-version + + - name: Cache Bun packages + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-root-${{ hashFiles('bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-root- + + - name: Install dependencies + run: bun install --frozen-lockfile + - name: Suspicious PR intent scan env: PR_SCAN_BASE: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || 'origin/main' }} @@ -51,6 +109,8 @@ jobs: run: bun run security:pr-scan -- --base "$PR_SCAN_BASE" --head "$PR_SCAN_HEAD" unit-tests: + needs: changes + if: ${{ github.event_name == 'push' || needs.changes.outputs.code == 'true' }} runs-on: ubuntu-latest timeout-minutes: 15 strategy: @@ -90,6 +150,8 @@ jobs: run: bun run scripts/run-tests.ts --shards=8 --shard-index=${{ matrix.shard }} --feature=UNATTENDED_RETRY --max-concurrency=1 dedicated-tests: + needs: changes + if: ${{ github.event_name == 'push' || needs.changes.outputs.code == 'true' }} runs-on: ubuntu-latest timeout-minutes: 15 @@ -125,6 +187,8 @@ jobs: run: bun run test:context-efficiency launcher-compatibility: + needs: changes + if: ${{ github.event_name == 'push' || needs.changes.outputs.code == 'true' }} runs-on: ubuntu-latest timeout-minutes: 5 strategy: @@ -167,6 +231,8 @@ jobs: NODE_DISABLE_COMPILE_CACHE=1 node bin/openclaude --version launcher-node-floor: + needs: changes + if: ${{ github.event_name == 'push' || needs.changes.outputs.code == 'true' }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -204,6 +270,8 @@ jobs: run: node bin/openclaude --version typecheck: + needs: changes + if: ${{ github.event_name == 'push' || needs.changes.outputs.code == 'true' }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -236,6 +304,8 @@ jobs: run: bun run typecheck:type-tests web: + needs: changes + if: ${{ github.event_name == 'push' || needs.changes.outputs.web == 'true' }} runs-on: ubuntu-latest timeout-minutes: 5 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c5ed3df64..f2e2b1af16 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -244,7 +244,7 @@ bun run dev:profile ## Validation -CI runs a fixed set of checks on every PR (see `.github/workflows/pr-checks.yml`). This section is the **authoritative local pre-push validation contract** — `AGENTS.md` defers to it. **Run every locally applicable check before every push to an open PR, including follow-up pushes during review.** Do not wait for GitHub CI to discover failures you could have caught locally — wasted Actions minutes are a real cost on this repo. +PR Checks skips the static check, unit shard, dedicated test, launcher, and typecheck jobs when a PR changes only Markdown files or files under `docs/`, unless any change is under `.github/`. CI runs the PR intent scan on every PR, including documentation-only PRs. The web job is selected independently by website paths; every job runs on pushes to `main` (see `.github/workflows/pr-checks.yml`). This section is the **authoritative local pre-push validation contract** — `AGENTS.md` defers to it. **Run every locally applicable check before every push to an open PR, including follow-up pushes during review.** Do not wait for GitHub CI to discover failures you could have caught locally — wasted Actions minutes are a real cost on this repo. The security scan requires the commit ancestry needed to find the PR's merge base. Check whether your clone is shallow: @@ -309,8 +309,8 @@ Notes on the local preflight: - `bun run check` runs `check:static` (the any-usage budget, smoke build, and deadcode checks), then the full unit pass (`test:full`) — do not run those separately, or you execute work twice. - `bun run test` isolates each test file's globals and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups. Use `bun run scripts/run-tests.ts --shards=N --shard-index=K` with the same test flags to run only shard K of N. `test:full` then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files to avoid Bun’s isolated-stdio pipe registration bug; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI runs `check:static` in `smoke-and-tests`, runs the eight unit shards as parallel `unit-tests` jobs, and runs each dedicated feature pass once in `dedicated-tests`. Each unit shard builds the CLI before testing. CI caps these jobs at 15 minutes each and the other jobs at five minutes. - Fetching upstream `main` by URL avoids assuming that a fork checkout's `origin` points at Twigpine/openclaude. `FETCH_HEAD` is the fetched upstream tip. The scan deliberately uses local `HEAD` so it includes commits that have not been pushed yet; CI uses the pushed PR head SHA after the push. -- The web CI job remains unconditional as an integration backstop. Contributors do not need to run the web suite locally for changes that cannot affect the site. -- This preflight covers the same command families as CI, but it does not reproduce CI exactly in one shell. CI runs static checks with Node 22 available for smoke checks, runs the unit shards and dedicated feature passes under Node 22, separately builds and checks launcher compatibility under Node 22 and 24.11.x, separately builds and launches under exact Node 22.0.0, and executes every job on a clean runner. +- The web CI job runs on PRs that change `web/**`, root `package.json` or `bun.lock`, `.bun-version`, or `.github/workflows/pr-checks.yml`, and on every push to `main`. Contributors do not need to run the web suite locally for changes that cannot affect the site. +- This preflight covers the same command families as CI, but it does not reproduce CI exactly in one shell. When selected, CI runs static checks with Node 22 available for smoke checks, runs the unit shards and dedicated feature passes under Node 22, separately builds and checks launcher compatibility under Node 22 and 24.11.x, separately builds and launches under exact Node 22.0.0, and executes every job on a clean runner. If a local or GitHub CI check fails, first determine whether the PR owns the failure. Treat a failure as pre-existing only when you can reproduce it on the PR's current base (or link to the same failure on a current `main` CI run) and the PR neither changes the causal surface nor claims to fix that problem. From cb9f9ee0ca028dc1640f8f98b5c444949951be55 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 14:16:47 -0700 Subject: [PATCH 010/121] test: stabilize timing-sensitive tests (#2316) --- scripts/openclaude-bin-heap.test.ts | 9 ++++++--- src/cli/bgFinalizer.test.ts | 14 +++++++++----- src/cli/printMaxTurns.test.ts | 15 +-------------- src/components/PromptInput/utils.test.ts | 2 +- 4 files changed, 17 insertions(+), 23 deletions(-) diff --git a/scripts/openclaude-bin-heap.test.ts b/scripts/openclaude-bin-heap.test.ts index 51a0084553..65b8cceb55 100644 --- a/scripts/openclaude-bin-heap.test.ts +++ b/scripts/openclaude-bin-heap.test.ts @@ -72,6 +72,7 @@ describe('openclaude launcher heap guard', () => { expect(source).not.toMatch(/import\s*\{[^}]*enableCompileCache[^}]*\}\s*from\s*['"]node:module['"]/s) }) + // Real CLI startup has an 8s child deadline; allow time to assert its result. test('strips launcher-only percentage before Commander when native heap and expose-gc are present', () => { const env = { ...process.env, @@ -98,8 +99,9 @@ describe('openclaude launcher heap guard', () => { expect(output).not.toContain( "unknown option '--max-old-space-size-percentage=50'", ) - }) + }, 10_000) + // Real CLI startup has an 8s child deadline; allow time to assert its result. test('strips launcher-only percentage before Commander when heap relaunch is disabled', () => { const env = { ...process.env, @@ -120,8 +122,9 @@ describe('openclaude launcher heap guard', () => { expect(output).not.toContain( "unknown option '--max-old-space-size-percentage=50'", ) - }) + }, 10_000) + // Real CLI startup has an 8s child deadline; allow time to assert its result. test('strips launcher-only percentage when OPENCLAUDE_HEAP_RELAUNCHED is already set', () => { const env = { ...process.env, @@ -142,7 +145,7 @@ describe('openclaude launcher heap guard', () => { expect(output).not.toContain( "unknown option '--max-old-space-size-percentage=50'", ) - }) + }, 10_000) }) describe('heap-limit percentage resolution', () => { diff --git a/src/cli/bgFinalizer.test.ts b/src/cli/bgFinalizer.test.ts index fdc51937a1..5c16796480 100644 --- a/src/cli/bgFinalizer.test.ts +++ b/src/cli/bgFinalizer.test.ts @@ -633,8 +633,8 @@ describe('background session finalizer', () => { return { id, child, readyPath } } - async function waitForFile(path: string): Promise { - for (let attempt = 0; attempt < 500; attempt += 1) { + async function waitForFile(path: string, attempts: number = 500): Promise { + for (let attempt = 0; attempt < attempts; attempt += 1) { if (await Bun.file(path).exists()) return await new Promise(resolve => setTimeout(resolve, 10)) } @@ -967,6 +967,7 @@ describe('background session finalizer', () => { async function runWaitingDetachedLaunch( name: string, fixtureEnv: NodeJS.ProcessEnv = {}, + readinessAttempts?: number, ): Promise<{ id: string; childPid: number; readyPath: string }> { const readyPath = join(configDir, `${name}.ready`) const launcher = spawn( @@ -1017,7 +1018,7 @@ describe('background session finalizer', () => { if (!id || childPid === undefined) { throw new Error('detached wait launch omitted its session identity') } - await waitForFile(readyPath) + await waitForFile(readyPath, readinessAttempts) ownershipTransferred = true return { id, childPid, readyPath } } finally { @@ -1044,6 +1045,9 @@ describe('background session finalizer', () => { env: { OPENCLAUDE_BG_FINALIZER_FIXTURE_SKIP_READY: '1', }, + // Readiness is deliberately suppressed; only exercise the failure path. + readinessAttempts: 25, + expectedError: 'fixture readiness file was not created', }, ]) { it( @@ -1051,8 +1055,8 @@ describe('background session finalizer', () => { async () => { const name = `failed-handoff-${scenario.label.replaceAll(' ', '-')}` await expect( - runWaitingDetachedLaunch(name, scenario.env), - ).rejects.toThrow() + runWaitingDetachedLaunch(name, scenario.env, scenario.readinessAttempts), + ).rejects.toThrow(scenario.expectedError) const session = (await listBackgroundSessions()).find( candidate => candidate.name === name, ) diff --git a/src/cli/printMaxTurns.test.ts b/src/cli/printMaxTurns.test.ts index a18bc682b3..8f1855e1a5 100644 --- a/src/cli/printMaxTurns.test.ts +++ b/src/cli/printMaxTurns.test.ts @@ -135,18 +135,6 @@ function createHeadlessRunOptions(maxTurns: number | undefined) { } } -async function waitForAskCall( - askSpy: ReturnType>, -): Promise { - const deadline = Date.now() + 15_000 - while (askSpy.mock.calls.length === 0) { - if (Date.now() >= deadline) { - throw new Error('Timed out waiting for runHeadless to call ask()') - } - await new Promise(resolve => setTimeout(resolve, 25)) - } -} - describe('headless --print max-turns', () => { const savedSimple = process.env.CLAUDE_CODE_SIMPLE @@ -213,9 +201,8 @@ describe('headless --print max-turns', () => { createHeadlessRunOptions(0), ) - await waitForAskCall(askSpy) - expect(askSpy.mock.calls[0]?.[0]?.maxTurns).toBe(0) await runPromise + expect(askSpy.mock.calls[0]?.[0]?.maxTurns).toBe(0) expect(shutdownSpy).toHaveBeenCalledWith(0) } finally { await runPromise?.catch(() => {}) diff --git a/src/components/PromptInput/utils.test.ts b/src/components/PromptInput/utils.test.ts index 39f35d630d..0b078c629f 100644 --- a/src/components/PromptInput/utils.test.ts +++ b/src/components/PromptInput/utils.test.ts @@ -119,7 +119,7 @@ test.each([ /** Observes the real hook after mounting its AppState provider. */ function HookProbe() { observedBanner = useSwarmBanner() - React.useEffect(() => { notifyRendered() }, []) + React.useLayoutEffect(() => { notifyRendered() }, []) return null } From a13ed45b14a25f0afde43932ab1d53bf0c06909c Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 15:46:34 -0700 Subject: [PATCH 011/121] feat(gateway): add Merge Gateway provider (#2317) * feat(gateway): add Merge Gateway provider Co-authored-by: MatthewFeroz <136640686+MatthewFeroz@users.noreply.github.com> * fix(merge-gateway): preserve scoped credentials across profiles Co-authored-by: MatthewFeroz <136640686+MatthewFeroz@users.noreply.github.com> * fix: complete Merge Gateway credential and catalog handoffs Co-authored-by: MatthewFeroz <136640686+MatthewFeroz@users.noreply.github.com> * fix: constrain Merge catalog and admit provider env-file credentials Co-authored-by: MatthewFeroz <136640686+MatthewFeroz@users.noreply.github.com> * fix: preserve Merge credential ownership across profile and host settings Co-authored-by: MatthewFeroz <136640686+MatthewFeroz@users.noreply.github.com> * fix(merge-gateway): scope ambient key adoption and per-page discovery timeouts Co-authored-by: MatthewFeroz <136640686+MatthewFeroz@users.noreply.github.com> --------- Co-authored-by: MatthewFeroz <136640686+MatthewFeroz@users.noreply.github.com> --- README.md | 1 + docs/merge-gateway-setup.md | 47 ++ src/components/ProviderManager.test.tsx | 1 + src/integrations/compatibility.test.ts | 1 + src/integrations/discoveryService.test.ts | 192 ++++++++ src/integrations/discoveryService.ts | 15 +- .../gateways/merge-gateway.test.ts | 436 ++++++++++++++++++ src/integrations/gateways/merge-gateway.ts | 128 +++++ .../integrationArtifacts.generated.ts | 3 +- .../integrationManifest.generated.ts | 15 + src/integrations/index.test.ts | 1 + src/integrations/providerUiMetadata.ts | 1 + src/integrations/routeMetadata.ts | 49 ++ src/services/api/agentRouting.ts | 6 + .../api/openaiShim/requestExecutor.ts | 3 +- src/utils/envFile.test.ts | 26 ++ src/utils/envFile.ts | 1 + src/utils/managedEnvConstants.ts | 1 + src/utils/providerDiscovery.ts | 85 +++- src/utils/providerFlag.ts | 6 + src/utils/providerProfile.ts | 45 +- src/utils/providerProfiles.ts | 49 +- src/utils/providerValidation.test.ts | 42 ++ src/utils/providerValidation.ts | 4 + src/utils/swarm/spawnUtils.ts | 1 + web/src/data/providers.ts | 10 +- 26 files changed, 1155 insertions(+), 14 deletions(-) create mode 100644 docs/merge-gateway-setup.md create mode 100644 src/integrations/gateways/merge-gateway.test.ts create mode 100644 src/integrations/gateways/merge-gateway.ts diff --git a/README.md b/README.md index 0df86e51ee..29fdd50470 100644 --- a/README.md +++ b/README.md @@ -350,6 +350,7 @@ Advanced and source-build guides: | OpenAI-compatible | `/provider` or env vars | Works with OpenAI, OpenRouter, DeepSeek, Groq, Mistral, LM Studio, and other compatible `/v1` servers | | Z.AI GLM Coding Plan | `/provider` or OpenAI-compatible env vars | Uses `OPENAI_API_KEY` at `https://api.z.ai/api/coding/paas/v4`, defaults to `glm-5.2`, and offers the vision-capable `glm-5.3-flash` option | | AI/ML API | `/provider` or `AIMLAPI_API_KEY` ([setup guide](docs/aimlapi-setup.md)) | Uses `https://api.aimlapi.com/v1`, auto-detects the OpenAI-compatible route from `AIMLAPI_API_KEY`, sends OpenClaude attribution headers, and discovers chat-capable models from the public `/models` catalog | +| Merge Gateway | `/provider` or `MERGE_GATEWAY_API_KEY` ([setup guide](docs/merge-gateway-setup.md)) | Multi-provider model router at `https://api-gateway.merge.dev/v1/openai`; supports Chat Completions, Responses, authenticated model discovery, and routing policies | | Concentrate | `/provider` or `CONCENTRATE_API_KEY` | Unified OpenAI-compatible gateway at `https://api.concentrate.ai/v1`; defaults to `deepseek-v4-flash` and auto-discovers the chat model catalog | | LLMTR | `/provider` or OpenAI-compatible env vars | Multi-model gateway at `https://llmtr.com/v1`; `/provider` and `--provider llmtr` default to `deepseek/deepseek-v4-flash`, while raw env setup must set `OPENAI_BASE_URL=https://llmtr.com/v1` and `OPENAI_MODEL`; accepts `LLMTR_API_KEY` or `OPENAI_API_KEY` after the route is selected and discovers tool-capable Chat Completions models from the public catalog | | Command Code | `/provider` or OpenAI-compatible env vars | Hybrid OpenAI-compatible gateway at `https://api.commandcode.ai/provider/v1`; `/provider` and `--provider commandcode` default to `deepseek/deepseek-v4-flash`, while raw env setup must set `OPENAI_BASE_URL=https://api.commandcode.ai/provider/v1` and `OPENAI_MODEL`; requires `CMD_API_KEY`, `COMMANDCODE_API_KEY`, or the official `COMMAND_CODE_API_KEY` after the route is selected and discovers Chat Completions models from the public catalog. Claude models are unsupported by this Chat Completions integration | diff --git a/docs/merge-gateway-setup.md b/docs/merge-gateway-setup.md new file mode 100644 index 0000000000..91612b7846 --- /dev/null +++ b/docs/merge-gateway-setup.md @@ -0,0 +1,47 @@ +# Merge Gateway setup + +OpenClaude connects to [Merge Gateway](https://gateway.merge.dev) through its OpenAI-compatible endpoint at `https://api-gateway.merge.dev/v1/openai`. The route supports Merge's multi-provider model catalog and routing policies with a dedicated `MERGE_GATEWAY_API_KEY`. + +## Guided setup + +1. Create an API key in the [Merge Gateway dashboard](https://gateway.merge.dev/api-keys). +2. Start OpenClaude and run `/provider`. +3. Choose **Add provider**, then **Merge Gateway**. +4. Paste the API key when prompted. +5. Choose a discovered model, or select **Default routing policy** to let the applicable Merge routing policy choose the provider and model. +6. Choose **Chat Completions** or **Responses** for the API format supported by the selected model and route. + +The model picker keeps a cached catalog for one day and can be refreshed manually. If discovery is temporarily unavailable, the routing-policy and GPT-5.5 fallback entries remain selectable. + +## CLI setup + +```bash +export MERGE_GATEWAY_API_KEY="your-api-key" +openclaude --provider merge-gateway --model openai/gpt-5.5 +``` + +To delegate model selection to a Merge Gateway routing policy: + +```bash +openclaude --provider merge-gateway --model default_routing +``` + +The dedicated Merge credential is not replaced by `OPENAI_API_KEY`. Saved profiles retain it across restarts. It is used only with the canonical inference base above and the native catalog endpoint below; changing a Merge profile to a proxy or another path withholds the credential. Configure a custom OpenAI-compatible profile with its own credentials for a proxy. + +## Verify + +Inside OpenClaude: + +1. Run `/status` and confirm the active provider is **Merge Gateway** and the base URL is `https://api-gateway.merge.dev/v1/openai`. +2. Run `/model`, refresh the catalog, and confirm Merge Gateway models appear. +3. Send a short prompt and confirm the request appears in the [Gateway dashboard](https://gateway.merge.dev). + +For direct catalog troubleshooting, use the authenticated public model endpoint: + +```bash +curl --fail --silent --show-error \ + -H "Authorization: Bearer $MERGE_GATEWAY_API_KEY" \ + "https://api-gateway.merge.dev/v1/models?limit=5" +``` + +Never commit the API key or paste it into issue or pull-request text. diff --git a/src/components/ProviderManager.test.tsx b/src/components/ProviderManager.test.tsx index 4620d6bf9a..d99d04ac13 100644 --- a/src/components/ProviderManager.test.tsx +++ b/src/components/ProviderManager.test.tsx @@ -158,6 +158,7 @@ const PRESET_ORDER = [ 'Atomic Chat', 'Ollama', 'LongCat', + 'Merge Gateway', 'MiniMax', 'Mistral AI', 'Moonshot AI - API', diff --git a/src/integrations/compatibility.test.ts b/src/integrations/compatibility.test.ts index 016927076b..d1b98d26b1 100644 --- a/src/integrations/compatibility.test.ts +++ b/src/integrations/compatibility.test.ts @@ -32,6 +32,7 @@ const EXPECTED_PRESETS = [ 'together', 'groq', 'hicap', + 'merge-gateway', 'azure-openai', 'openrouter', 'lmstudio', diff --git a/src/integrations/discoveryService.test.ts b/src/integrations/discoveryService.test.ts index 8970b2a377..b81566e4c0 100644 --- a/src/integrations/discoveryService.test.ts +++ b/src/integrations/discoveryService.test.ts @@ -25,6 +25,7 @@ const originalEnv = { OPENAI_AUTH_HEADER_VALUE: process.env.OPENAI_AUTH_HEADER_VALUE, OPENAI_AUTH_SCHEME: process.env.OPENAI_AUTH_SCHEME, APISMART_API_KEY: process.env.APISMART_API_KEY, + MERGE_GATEWAY_API_KEY: process.env.MERGE_GATEWAY_API_KEY, ANTHROPIC_CUSTOM_HEADERS: process.env.ANTHROPIC_CUSTOM_HEADERS, CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, CLAUDE_CODE_USE_GEMINI: process.env.CLAUDE_CODE_USE_GEMINI, @@ -70,6 +71,7 @@ function clearProviderEnv(): void { delete process.env.OPENAI_AUTH_HEADER_VALUE delete process.env.OPENAI_AUTH_SCHEME delete process.env.APISMART_API_KEY + delete process.env.MERGE_GATEWAY_API_KEY delete process.env.ANTHROPIC_CUSTOM_HEADERS delete process.env.CLAUDE_CODE_USE_OPENAI delete process.env.CLAUDE_CODE_USE_GEMINI @@ -111,6 +113,7 @@ afterEach(() => { restoreEnvValue('OPENAI_AUTH_HEADER_VALUE') restoreEnvValue('OPENAI_AUTH_SCHEME') restoreEnvValue('APISMART_API_KEY') + restoreEnvValue('MERGE_GATEWAY_API_KEY') restoreEnvValue('ANTHROPIC_CUSTOM_HEADERS') restoreEnvValue('CLAUDE_CODE_USE_OPENAI') restoreEnvValue('CLAUDE_CODE_USE_GEMINI') @@ -128,6 +131,195 @@ afterEach(() => { }) describe('discoverModelsForRoute', () => { + test('uses Merge Gateway native discovery with its dedicated credential', async () => { + const { discoverModelsForRoute } = await loadDiscoveryServiceModule() + process.env.MERGE_GATEWAY_API_KEY = 'merge-key' + process.env.OPENAI_API_KEY = 'unrelated-openai-key' + + let requestUrl: string | null = null + let authorization: string | null = null + setMockFetch(mock((input: string | URL | Request, init?: RequestInit) => { + requestUrl = + typeof input === 'string' + ? input + : input instanceof URL + ? input.toString() + : input.url + authorization = new Headers(init?.headers).get('authorization') + return Promise.resolve( + new Response( + JSON.stringify({ + data: [ + { + model: 'anthropic/claude-sonnet-5', + display_name: 'Claude Sonnet 5', + }, + ], + }), + { status: 200, headers: { 'Content-Type': 'application/json' } }, + ), + ) + }) as unknown as typeof globalThis.fetch) + + const result = await discoverModelsForRoute('merge-gateway', { + forceRefresh: true, + }) + + expect(requestUrl).toBe( + 'https://api-gateway.merge.dev/v1/models', + ) + expect(authorization).toBe('Bearer merge-key') + expect(result).toMatchObject({ + routeId: 'merge-gateway', + source: 'network', + discoveredModelCount: 1, + }) + }) + + test('publishes only conversational Merge models to the catalog and cache', async () => { + const { discoverModelsForRoute } = await loadDiscoveryServiceModule() + process.env.MERGE_GATEWAY_API_KEY = 'merge-key' + const nativeModel = (model: string, input: string[], output: string[]) => ({ + model, vendors: { native: { capabilities: { input, output } } }, + }) + const models = [ + nativeModel('anthropic/claude-sonnet-5', ['text', 'image', 'document'], ['text', 'tool_use']), + nativeModel('provider/audio-chat', ['text', 'audio'], ['text', 'audio']), + { + model: 'provider/multi-vendor-chat', vendors: { + speech: { capabilities: { input: ['text'], output: ['audio'] } }, + chat: { capabilities: { input: ['text'], output: ['text'] } }, + }, + }, + // Modalities, rather than names, also exclude unknown media/model families. + nativeModel('provider/vector-v1', ['text'], ['embedding']), + nativeModel('provider/narrator-v1', ['text'], ['audio']), + nativeModel('provider/movie-v1', ['text', 'image'], ['video']), + nativeModel('provider/painter-v1', ['text'], ['image']), + nativeModel('provider/transcription-v1', ['audio'], ['text']), + nativeModel('provider/ranker-v1', ['text'], ['score']), + // Sparse native/compatible payloads still exclude known non-chat families. + { model: 'google/gemini-embedding-001' }, + { model: 'minimax/speech-2.6-turbo' }, + { id: 'google/veo-3.1-generate-001' }, + ] + const fetchMock = mock(() => Promise.resolve(new Response(JSON.stringify({ data: models, has_more: false })))) + setMockFetch(fetchMock as unknown as typeof globalThis.fetch) + const result = await discoverModelsForRoute('merge-gateway', { forceRefresh: true }) + expect(result).toMatchObject({ source: 'network', discoveredModelCount: 3, error: null }) + expect(result?.models.map(model => model.apiName)).toEqual([ + 'default_routing', 'openai/gpt-5.5', + 'anthropic/claude-sonnet-5', 'provider/audio-chat', 'provider/multi-vendor-chat', + ]) + const cached = await discoverModelsForRoute('merge-gateway') + expect(cached?.source).toBe('cache') + expect(cached?.models).toEqual(result?.models) + expect(fetchMock).toHaveBeenCalledTimes(1) + }) + + test('withholds explicit and ambient Merge keys from retargeted discovery', async () => { + const { discoverModelsForRoute } = await loadDiscoveryServiceModule() + process.env.MERGE_GATEWAY_API_KEY = 'merge-secret' + const authorizations: Array = [] + setMockFetch(mock((_input: string | URL | Request, init?: RequestInit) => { + authorizations.push(new Headers(init?.headers).get('authorization')) + return Promise.resolve(new Response(JSON.stringify({ data: [] }))) + }) as unknown as typeof globalThis.fetch) + for (const apiKey of [undefined, 'merge-secret']) { + await discoverModelsForRoute('merge-gateway', { + baseUrl: 'https://proxy.example/v1', apiKey, forceRefresh: true, + }) + } + expect(authorizations).toEqual([]) + }) + + test('caches the complete native Merge catalog after cursor continuation', async () => { + const { discoverModelsForRoute } = await loadDiscoveryServiceModule() + process.env.MERGE_GATEWAY_API_KEY = 'merge-key' + const urls: string[] = [] + setMockFetch(mock((input: string | URL | Request, init?: RequestInit) => { + const url = new URL(String(input)) + urls.push(url.toString()) + expect(new Headers(init?.headers).get('authorization')).toBe('Bearer merge-key') + expect(url.origin).toBe('https://api-gateway.merge.dev') + const second = url.searchParams.get('cursor') === 'page/2?opaque=1' + return Promise.resolve(new Response(JSON.stringify({ + data: second ? [{ model: 'provider/later' }, { model: 'provider/first' }] : [{ model: 'provider/first' }], + has_more: !second, next_cursor: second ? null : 'page/2?opaque=1', + }))) + }) as unknown as typeof globalThis.fetch) + const result = await discoverModelsForRoute('merge-gateway', { forceRefresh: true }) + expect(result).toMatchObject({ source: 'network', discoveredModelCount: 2, error: null }) + expect(result?.models.map(model => model.apiName)).toContain('provider/later') + expect(urls).toHaveLength(2) + expect((await discoverModelsForRoute('merge-gateway'))?.source).toBe('cache') + expect(urls).toHaveLength(2) + }) + + test('allows each Merge discovery page its own timeout', async () => { + const { discoverModelsForRoute } = await loadDiscoveryServiceModule() + process.env.MERGE_GATEWAY_API_KEY = 'merge-key' + let calls = 0 + const fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => { + const page = ++calls + const signal = init?.signal + await new Promise((resolve, reject) => { + const onAbort = () => { + clearTimeout(timer) + signal?.removeEventListener('abort', onAbort) + reject(signal?.reason) + } + const timer = setTimeout(() => { + signal?.removeEventListener('abort', onAbort) + resolve() + }, 2000) + signal?.addEventListener('abort', onAbort, { once: true }) + if (signal?.aborted) onAbort() + }) + return new Response(JSON.stringify({ + data: [{ model: `provider/page-${page}` }], + has_more: page < 3, next_cursor: page < 3 ? `page-${page + 1}` : null, + })) + }) + setMockFetch(fetchMock as unknown as typeof globalThis.fetch) + + const result = await discoverModelsForRoute('merge-gateway', { forceRefresh: true }) + expect(result).toMatchObject({ source: 'network', discoveredModelCount: 3, error: null }) + expect(result?.models.map(model => model.apiName).filter(name => name.startsWith('provider/page-'))).toEqual([ + 'provider/page-1', 'provider/page-2', 'provider/page-3', + ]) + const cached = await discoverModelsForRoute('merge-gateway') + expect(cached?.source).toBe('cache') + expect(cached?.models).toEqual(result?.models) + expect(fetchMock).toHaveBeenCalledTimes(3) + }, 15_000) + + test.each(['http-error', 'repeated-cursor', 'missing-cursor', 'page-bound'])('incomplete Merge discovery preserves fallback: %s', async failure => { + const { discoverModelsForRoute } = await loadDiscoveryServiceModule() + process.env.MERGE_GATEWAY_API_KEY = 'merge-key' + for (const withCache of [false, true]) { + if (withCache) { + setMockFetch(mock(() => Promise.resolve(new Response(JSON.stringify({data: [{model: 'provider/complete'}], has_more: false})))) as unknown as typeof globalThis.fetch) + await discoverModelsForRoute('merge-gateway', {forceRefresh: true}) + } + let calls = 0 + setMockFetch(mock(() => { + calls++ + if (failure === 'http-error' && calls > 1) return Promise.resolve(new Response('unavailable', { status: 503 })) + return Promise.resolve(new Response(JSON.stringify({ + data: [{ model: 'provider/partial' }], has_more: true, + next_cursor: failure === 'missing-cursor' ? null : failure === 'page-bound' ? 'page-' + calls : 'page-2', + }))) + }) as unknown as typeof globalThis.fetch) + const result = await discoverModelsForRoute('merge-gateway', {forceRefresh: true}) + expect(result?.error).not.toBeNull() + expect(result?.models.map(model => model.apiName)).not.toContain('provider/partial') + expect(result?.models.map(model => model.apiName)).toContain(withCache ? 'provider/complete' : 'default_routing') + expect(result?.source).toBe('stale-cache') + expect(calls).toBeLessThanOrEqual(100) + } + }) + test('does not send an ApiSmart key to an overridden discovery URL', async () => { const { discoverModelsForRoute } = await loadDiscoveryServiceModule() process.env.APISMART_API_KEY = 'apismart-secret' diff --git a/src/integrations/discoveryService.ts b/src/integrations/discoveryService.ts index 580b0db798..0fd6aa0d07 100644 --- a/src/integrations/discoveryService.ts +++ b/src/integrations/discoveryService.ts @@ -16,6 +16,7 @@ import { filterAvailableCatalogEntries } from './registry.js' import { getRouteDescriptor, isCanonicalApismartInferenceBaseUrl, + isCanonicalMergeGatewayInferenceBaseUrl, isCanonicalXaiInferenceBaseUrl, resolveActiveRouteIdFromEnv, resolveRouteCredentialValue, @@ -26,6 +27,7 @@ import type { } from '../utils/providerDiscovery.js' import { fetchOpenAICompatibleModelsRaw, + fetchMergeGatewayModelsRaw, listOpenAICompatibleModels, probeOllamaModelCatalog, probeAtomicChatReadiness, @@ -217,8 +219,10 @@ function getRouteDiscoveryApiKey( // URL. Apply the same exact inference-endpoint boundary used by requests and // profiles before considering either a caller-provided or ambient key. if ( - routeId === 'apismart' && - !isCanonicalApismartInferenceBaseUrl(baseUrl) + (routeId === 'apismart' && + !isCanonicalApismartInferenceBaseUrl(baseUrl)) || + (routeId === 'merge-gateway' && + !isCanonicalMergeGatewayInferenceBaseUrl(baseUrl)) ) { return undefined } @@ -427,10 +431,14 @@ async function runDiscovery( case 'openai-compatible': { if (discovery.mapModel) { - const rawModels = await fetchOpenAICompatibleModelsRaw({ + const fetchModels = routeId === 'merge-gateway' + ? fetchMergeGatewayModelsRaw + : fetchOpenAICompatibleModelsRaw + const rawModels = await fetchModels({ baseUrl: getRouteBaseUrl(routeId, options), apiKey: getRouteDiscoveryApiKey(routeId, options), headers: getRouteDiscoveryHeaders(routeId, options), + path: discovery.path, }) if (rawModels === null) { return null @@ -449,6 +457,7 @@ async function runDiscovery( baseUrl: getRouteBaseUrl(routeId, options), apiKey: getRouteDiscoveryApiKey(routeId, options), headers: getRouteDiscoveryHeaders(routeId, options), + path: discovery.path, }) return models?.map(model => toDiscoveredModelEntry(model)) ?? null } diff --git a/src/integrations/gateways/merge-gateway.test.ts b/src/integrations/gateways/merge-gateway.test.ts new file mode 100644 index 0000000000..c0858637a4 --- /dev/null +++ b/src/integrations/gateways/merge-gateway.test.ts @@ -0,0 +1,436 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { describe, expect, mock, test } from 'bun:test' + +import '../index.js' +import { + getRouteCredentialEnvVars, + resolveActiveRouteIdFromEnv, +} from '../routeMetadata.js' +import { clearManagedProfileEnv } from '../../utils/providerProfile.js' +import { applyProviderFlag } from '../../utils/providerFlag.js' +import { createOpenAIShimClient } from '../../services/api/openaiShim.js' +import { + acquireSharedMutationLock, + releaseSharedMutationLock, +} from '../../test/sharedMutationLock.js' +import { asMockFetch } from '../../test/typedMocks.js' +import mergeGateway from './merge-gateway.js' + +const mapModel = mergeGateway.catalog?.discovery?.mapModel +type OpenAIShimClient = { + beta: { + messages: { + create: (params: Record) => Promise + } + } +} + +describe('Merge Gateway', () => { + test('uses a dedicated OpenAI-compatible gateway route', () => { + expect(mergeGateway.id).toBe('merge-gateway') + expect(mergeGateway.defaultBaseUrl).toBe( + 'https://api-gateway.merge.dev/v1/openai', + ) + expect(mergeGateway.setup.credentialEnvVars).toEqual([ + 'MERGE_GATEWAY_API_KEY', + ]) + expect(mergeGateway.setup.dedicatedCredentialsOnly).toBe(true) + expect(mergeGateway.transportConfig.kind).toBe('openai-compatible') + expect( + mergeGateway.transportConfig.openaiShim?.supportsApiFormatSelection, + ).toBe(true) + }) + + test('keeps routing policy and concrete model fallbacks in the catalog', () => { + expect(mergeGateway.catalog?.source).toBe('hybrid') + expect(mergeGateway.catalog?.discovery?.requiresAuth).toBe(true) + expect(mergeGateway.catalog?.models?.map(model => model.apiName)).toEqual([ + 'default_routing', + 'openai/gpt-5.5', + ]) + }) + + test('--provider selects the route and uses only its dedicated credential', async () => { + await acquireSharedMutationLock('merge-gateway.test.ts') + expect(getRouteCredentialEnvVars('merge-gateway')).toEqual([ + 'MERGE_GATEWAY_API_KEY', + ]) + + const previousEnv = { ...process.env } + const originalFetch = globalThis.fetch + try { + process.env.MERGE_GATEWAY_API_KEY = 'merge-key' + process.env.OPENAI_API_KEY = 'unrelated-openai-key' + delete process.env.OPENAI_BASE_URL + process.env.OPENAI_AUTH_HEADER = 'X-Stale-Key' + process.env.OPENAI_AUTH_SCHEME = 'Token' + process.env.OPENAI_AUTH_HEADER_VALUE = 'stale-secret' + process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Stale-Secret: old-secret' + + expect( + applyProviderFlag('merge-gateway', [ + '--provider', + 'merge-gateway', + '--model', + 'default_routing', + ]), + ).toEqual({}) + expect(String(process.env.OPENAI_BASE_URL)).toBe( + 'https://api-gateway.merge.dev/v1/openai', + ) + expect(String(process.env.OPENAI_MODEL)).toBe('default_routing') + expect(resolveActiveRouteIdFromEnv(process.env)).toBe('merge-gateway') + + for (const name of ['OPENAI_AUTH_HEADER', 'OPENAI_AUTH_SCHEME', 'OPENAI_AUTH_HEADER_VALUE', 'ANTHROPIC_CUSTOM_HEADERS']) { + expect(process.env[name]).toBeUndefined() + } + // A direct canonical selection must also ignore stale settings at request time. + process.env.OPENAI_AUTH_HEADER = 'X-Stale-Key' + process.env.OPENAI_AUTH_SCHEME = 'Token' + process.env.OPENAI_AUTH_HEADER_VALUE = 'stale-secret' + process.env.ANTHROPIC_CUSTOM_HEADERS = 'X-Stale-Secret: old-secret' + let authorization: string | null = null + globalThis.fetch = asMockFetch(mock((_input, init) => { + const headers = new Headers(init?.headers) + expect(headers.get('x-stale-key')).toBeNull() + expect(headers.get('x-stale-secret')).toBeNull() + authorization = headers.get('authorization') + return Promise.resolve( + new Response( + JSON.stringify({ + id: 'chatcmpl-merge', + model: 'default_routing', + choices: [ + { + message: { role: 'assistant', content: 'ok' }, + finish_reason: 'stop', + }, + ], + }), + { headers: { 'Content-Type': 'application/json' } }, + ), + ) + })) + + const client = createOpenAIShimClient({ defaultHeaders: { 'X-Stale-Secret': 'old-secret' } }) as OpenAIShimClient + await client.beta.messages.create({ + model: 'default_routing', + messages: [{ role: 'user', content: 'hello' }], + max_tokens: 32, + stream: false, + }) + + expect(authorization).toBe('Bearer merge-key') + } finally { + mock.restore() + globalThis.fetch = originalFetch + for (const name of Object.keys(process.env)) { + if (!(name in previousEnv)) delete process.env[name] + } + Object.assign(process.env, previousEnv) + releaseSharedMutationLock() + } + }) + + test('maps native and OpenAI-compatible model list shapes', () => { + if (!mapModel) throw new Error('mapModel missing') + + expect( + mapModel({ + model: 'anthropic/claude-sonnet-5', + display_name: 'Claude Sonnet 5', + }), + ).toEqual({ + id: 'merge-gateway-anthropic/claude-sonnet-5', + apiName: 'anthropic/claude-sonnet-5', + label: 'Claude Sonnet 5 (via Merge Gateway)', + }) + expect(mapModel({ id: 'openai/gpt-5.5' })).toEqual({ + id: 'merge-gateway-openai/gpt-5.5', + apiName: 'openai/gpt-5.5', + label: 'openai/gpt-5.5 (via Merge Gateway)', + }) + expect(mapModel({})).toBeNull() + expect(mapModel(null)).toBeNull() + }) +}) + +// Use separate processes so profile creation cannot mask a missing startup key. +test.each(['merge-gateway', 'openai', 'custom'])('%s saved profiles authorize Merge inference and discovery after a fresh startup', async provider => { + const configDir = mkdtempSync(join(tmpdir(), 'merge-profile-')) + const env = { ...process.env } + clearManagedProfileEnv(env) + env.CLAUDE_CONFIG_DIR = configDir + env.OPENCLAUDE_CONFIG_DIR = configDir + env.NODE_ENV = 'production' + const run = async (source: string) => { + const child = Bun.spawn([process.execPath, '--eval', "import { enableConfigs } from './src/utils/config.ts'; enableConfigs();\n" + source], { + cwd: join(import.meta.dir, '../../..'), + env, + stdout: 'pipe', + stderr: 'pipe', + }) + const [stdout, stderr, exit] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + expect({ exit, stderr, stdout }).toEqual({ exit: 0, stderr: '', stdout: '' }) + } + try { + await run(` + import assert from 'node:assert/strict'; + import { addProviderProfile, applyProviderProfileToProcessEnv } from './src/utils/providerProfiles.ts'; + import { clearManagedProfileEnv } from './src/utils/providerProfile.ts'; + import { applyProviderFlag } from './src/utils/providerFlag.ts'; + globalThis.fetch = async () => new Response(JSON.stringify({ data: [] })); + const profile = addProviderProfile({ + provider: '${provider}', name: 'Merge', + baseUrl: 'https://api-gateway.merge.dev/v1/openai', + model: 'openai/gpt-5.5', apiKey: 'saved-merge-key', apiFormat: 'chat', + }); + assert.ok(profile); + assert.equal(process.env.MERGE_GATEWAY_API_KEY, 'saved-merge-key'); + applyProviderFlag('openai', ['--provider', 'openai']); + assert.equal(process.env.OPENAI_API_KEY, undefined); + for (const key of [undefined, 'undefined', 'null', 'SUA_CHAVE']) { + clearManagedProfileEnv(process.env); + applyProviderProfileToProcessEnv({ ...profile, apiKey: key }); + assert.equal(Object.hasOwn(process.env, 'MERGE_GATEWAY_API_KEY'), false); + } + process.env.MERGE_GATEWAY_API_KEY = 'ambient-merge-key'; + applyProviderProfileToProcessEnv({ ...profile, apiKey: undefined }); + assert.equal(process.env.MERGE_GATEWAY_API_KEY, '${provider}' === 'merge-gateway' ? 'ambient-merge-key' : undefined); + `) + for (const startup of ['legacy', 'plural']) await run(` + import assert from 'node:assert/strict'; + import { applyActiveProviderProfileFromConfig } from './src/utils/providerProfiles.ts'; + import { applyStartupEnvFromProfile } from './src/utils/providerProfile.ts'; + import { createOpenAIShimClient } from './src/services/api/openaiShim.ts'; + import { discoverModelsForRoute } from './src/integrations/discoveryService.ts'; + assert.equal(process.env.MERGE_GATEWAY_API_KEY, undefined); + if ('${startup}' === 'legacy') assert.equal(await applyStartupEnvFromProfile(), null); + else assert.ok(applyActiveProviderProfileFromConfig()); + const calls = []; + globalThis.fetch = async (url, init) => { + calls.push({ url: String(url), auth: new Headers(init?.headers).get('authorization') }); + return new Response(JSON.stringify(String(url).endsWith('/models') + ? { data: [{ id: 'openai/gpt-5.5' }] } + : { id: 'chatcmpl-merge', model: 'openai/gpt-5.5', choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }] }), + { headers: { 'Content-Type': 'application/json' } }); + }; + await createOpenAIShimClient({}).beta.messages.create({ + model: 'openai/gpt-5.5', messages: [{ role: 'user', content: 'hello' }], max_tokens: 32, stream: false, + }); + await discoverModelsForRoute('merge-gateway', { forceRefresh: true }); + assert.deepEqual(calls, [ + { url: 'https://api-gateway.merge.dev/v1/openai/chat/completions', auth: 'Bearer saved-merge-key' }, + { url: 'https://api-gateway.merge.dev/v1/models', auth: 'Bearer saved-merge-key' }, + ]); + `) + } finally { + rmSync(configDir, { recursive: true, force: true }) + } +}, 30_000) + +test.each(['commandcode', 'apismart', 'concentrate', 'api-route', 'llmtr', 'gmi-cloud'])('%s credentials are not repurposed when a profile is retargeted to Merge', async provider => { + const configDir = mkdtempSync(join(tmpdir(), 'merge-retarget-')) + const env = { ...process.env } + clearManagedProfileEnv(env) + env.CLAUDE_CONFIG_DIR = configDir + env.OPENCLAUDE_CONFIG_DIR = configDir + const source = ` + import assert from 'node:assert/strict'; + import { enableConfigs, getGlobalConfig } from './src/utils/config.ts'; + import { applyProviderProfileToProcessEnv, applyActiveProviderProfileFromConfig } from './src/utils/providerProfiles.ts'; + import { clearManagedProfileEnv } from './src/utils/providerProfile.ts'; + import { createOpenAIShimClient } from './src/services/api/openaiShim.ts'; + import { discoverModelsForRoute } from './src/integrations/discoveryService.ts'; + enableConfigs(); + const profile = { id: 'retargeted', name: 'Retargeted', provider: '${provider}', + baseUrl: 'https://api-gateway.merge.dev/v1/openai', model: 'openai/gpt-5.5', apiKey: 'source-provider-secret' }; + for (const startup of [false, true]) for (const ambientKey of [undefined, 'ambient-merge-key']) { + clearManagedProfileEnv(process.env); + process.env.OPENAI_API_KEY = 'unrelated-generic-key'; + if (ambientKey) process.env.MERGE_GATEWAY_API_KEY = ambientKey; + if (startup) applyActiveProviderProfileFromConfig({ ...getGlobalConfig(), + providerProfiles: [profile], activeProviderProfileId: profile.id }, { force: true }); + else applyProviderProfileToProcessEnv(profile); + const calls = []; + globalThis.fetch = async (url, init) => { + calls.push({ url: String(url), auth: new Headers(init?.headers).get('authorization') }); + return new Response(JSON.stringify(String(url).endsWith('/models') + ? { data: [{ id: 'openai/gpt-5.5' }] } + : { id: 'chatcmpl-merge', choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }] }), + { headers: { 'Content-Type': 'application/json' } }); + }; + await createOpenAIShimClient({}).beta.messages.create({ + model: 'openai/gpt-5.5', messages: [{ role: 'user', content: 'hello' }], max_tokens: 32, stream: false, + }); + await discoverModelsForRoute('merge-gateway', { forceRefresh: true }); + const auth = null; + assert.deepEqual(calls, [ + { url: 'https://api-gateway.merge.dev/v1/openai/chat/completions', auth }, + { url: 'https://api-gateway.merge.dev/v1/models', auth }, + ]); + } + ` + try { + const child = Bun.spawn([process.execPath, '--eval', source], { + cwd: join(import.meta.dir, '../../..'), env, stdout: 'pipe', stderr: 'pipe', + }) + expect(await Promise.all([child.exited, new Response(child.stderr).text(), new Response(child.stdout).text()])).toEqual([0, '', '']) + } finally { + rmSync(configDir, { recursive: true, force: true }) + } +}, 30_000) + +test('a retargeted profile cannot inherit the previous Merge session credential', async () => { + await acquireSharedMutationLock('merge-gateway.test.ts') + const previousEnv = { ...process.env } + try { + const { applyProviderProfileToProcessEnv } = await import('../../utils/providerProfiles.js') + if (!mergeGateway.defaultBaseUrl) throw new Error('defaultBaseUrl missing') + const profile = { + id: 'merge-session', name: 'Merge', provider: 'merge-gateway', + baseUrl: mergeGateway.defaultBaseUrl, model: 'default_routing', + apiKey: 'saved-merge-key', + } + applyProviderProfileToProcessEnv(profile) + expect(process.env.MERGE_GATEWAY_API_KEY).toBe('saved-merge-key') + + applyProviderProfileToProcessEnv({ + ...profile, id: 'retargeted', name: 'Retargeted', provider: 'apismart', + apiKey: 'source-provider-secret', + }) + expect(process.env.MERGE_GATEWAY_API_KEY).toBeUndefined() + expect(process.env.OPENAI_API_KEY).toBeUndefined() + } finally { + for (const name of Object.keys(process.env)) { + if (!(name in previousEnv)) delete process.env[name] + } + Object.assign(process.env, previousEnv) + releaseSharedMutationLock() + } +}) + +test('Merge credentials are withheld from retargeted inference and startup', async () => { + const { resolveRouteCredentialValue } = await import('../routeMetadata.js') + const { buildLaunchEnv, createProfileFile } = await import('../../utils/providerProfile.js') + for (const baseUrl of [ + 'https://proxy.example/v1', + 'http://api-gateway.merge.dev/v1/openai', + 'https://api-gateway.merge.dev:8443/v1/openai', + 'https://api-gateway.merge.dev/v1/openai?tenant=proxy', + 'https://api-gateway.merge.dev/v1/openai#proxy', + 'https://api-gateway.merge.dev/v1/other', + ]) { + expect(resolveRouteCredentialValue({ + routeId: 'merge-gateway', baseUrl, + processEnv: { MERGE_GATEWAY_API_KEY: 'merge-key' }, + })).toBeUndefined() + const env = await buildLaunchEnv({ + profile: 'openai', + goal: 'balanced', + persisted: createProfileFile('openai', { + CLAUDE_CODE_PROVIDER_ROUTE_ID: 'merge-gateway', + OPENAI_BASE_URL: baseUrl, OPENAI_MODEL: 'openai/gpt-5.5', + OPENAI_API_KEY: 'merge-key', MERGE_GATEWAY_API_KEY: 'merge-key', + }), + processEnv: { MERGE_GATEWAY_API_KEY: 'ambient-merge-key', OPENAI_API_KEY: 'ambient-merge-key' }, + }) + expect(env.MERGE_GATEWAY_API_KEY).toBeUndefined() + expect(env.OPENAI_API_KEY).toBeUndefined() + } +}) + +test('a startup URL override cannot inherit a saved Merge credential', async () => { + const { buildLaunchEnv, createProfileFile } = await import('../../utils/providerProfile.js') + const persisted = createProfileFile('openai', { + OPENAI_BASE_URL: 'https://api-gateway.merge.dev/v1/openai', + OPENAI_MODEL: 'openai/gpt-5.5', OPENAI_API_KEY: 'saved-merge-key', + MERGE_GATEWAY_API_KEY: 'saved-merge-key', + }) + for (const customKey of [undefined, 'custom-proxy-key']) { + const env = await buildLaunchEnv({ + profile: 'openai', goal: 'balanced', persisted, + processEnv: { OPENAI_BASE_URL: 'https://proxy.example/v1', OPENAI_API_KEY: customKey }, + }) + expect(env.MERGE_GATEWAY_API_KEY).toBeUndefined() + expect(env.OPENAI_API_KEY).toBe(customKey) + } +}) + + +test('configured and inherited Merge teammates authenticate in a fresh process', async () => { + const configDir = mkdtempSync(join(tmpdir(), 'merge-teammate-')) + const env = { ...process.env } + clearManagedProfileEnv(env) + env.CLAUDE_CONFIG_DIR = configDir + env.OPENCLAUDE_CONFIG_DIR = configDir + const source = ` + import assert from 'node:assert/strict'; + import { quote } from 'shell-quote'; + import { resolveOutOfProcessTeammateProvider, applyAgentProviderOverrideToEnv } from './src/services/api/agentRouting.ts'; + import { buildInheritedEnvVars } from './src/utils/swarm/spawnUtils.ts'; + const request = \` import assert from 'node:assert/strict'; + import { createOpenAIShimClient } from './src/services/api/openaiShim.ts'; + import { enableConfigs, saveGlobalConfig } from './src/utils/config.ts'; + import { applySafeConfigEnvironmentVariables, applyConfigEnvironmentVariables } from './src/utils/managedEnv.ts'; + enableConfigs(); + saveGlobalConfig(current => ({ ...current, env: { MERGE_GATEWAY_API_KEY: 'settings-merge-key' } })); + let auth; + globalThis.fetch = async (url, init) => { + auth = new Headers(init?.headers).get('authorization'); + return new Response(JSON.stringify({ id: 'chatcmpl-child', choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }] }), { headers: { 'Content-Type': 'application/json' } }); + }; + assert.equal(process.env.MERGE_GATEWAY_API_KEY, process.env.EXPECTED_KEY); + for (const applySettings of [applySafeConfigEnvironmentVariables, applyConfigEnvironmentVariables]) { + for (const hostManaged of [true, false]) { + process.env.CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST = hostManaged ? '1' : '0'; + process.env.MERGE_GATEWAY_API_KEY = process.env.EXPECTED_KEY; + applySettings(); + await createOpenAIShimClient({}).beta.messages.create({ model: 'openai/gpt-5.5', messages: [{role: 'user', content: 'hello'}], stream: false, max_tokens: 32 }); + assert.equal(auth, 'Bearer ' + (hostManaged ? process.env.EXPECTED_KEY : 'settings-merge-key')); + } + } + \`; + const run = async (command, childEnv) => { + const child = Bun.spawn(command, { env: childEnv, stdout: 'pipe', stderr: 'pipe' }); + assert.deepEqual(await Promise.all([child.exited, new Response(child.stderr).text(), new Response(child.stdout).text()]), [0, '', '']); + }; + for (const parentKey of [undefined, 'parent-key']) { + const childEnv = { ...process.env, EXPECTED_KEY: 'configured-agent-key' }; + if (parentKey) childEnv.MERGE_GATEWAY_API_KEY = parentKey; + const override = resolveOutOfProcessTeammateProvider({ + cliModel: 'openai/gpt-5.5', agentName: 'worker', settings: { + agentModels: { 'openai/gpt-5.5': { base_url: 'https://api-gateway.merge.dev/v1/openai', api_key: 'configured-agent-key' } }, + }, + }); + assert.ok(override); + applyAgentProviderOverrideToEnv(override, childEnv); + await run([process.execPath, '--eval', request], childEnv); + applyAgentProviderOverrideToEnv({ ...override, baseURL: 'https://proxy.example/v1', apiKey: 'custom-key' }, childEnv); + assert.equal(childEnv.MERGE_GATEWAY_API_KEY, undefined); + } + Object.assign(process.env, { CLAUDE_CODE_USE_OPENAI: '1', OPENAI_BASE_URL: 'https://api-gateway.merge.dev/v1/openai', OPENAI_MODEL: 'openai/gpt-5.5', OPENAI_API_KEY: 'parent-key', MERGE_GATEWAY_API_KEY: 'parent-key' }); + const forwarded = buildInheritedEnvVars(); + // A pane's shell may not have inherited any of the leader's credentials. + if (process.platform !== 'win32') { + await run(['/bin/sh', '-c', 'env ' + forwarded + ' ' + quote([process.execPath, '--eval', request])], { + PATH: process.env.PATH, EXPECTED_KEY: 'parent-key', + }); + } + ` + try { + const child = Bun.spawn([process.execPath, '--eval', source], { + cwd: join(import.meta.dir, '../../..'), env, stdout: 'pipe', stderr: 'pipe', + }) + expect(await Promise.all([child.exited, new Response(child.stderr).text(), new Response(child.stdout).text()])).toEqual([0, '', '']) + } finally { + rmSync(configDir, { recursive: true, force: true }) + } +}, 30_000) diff --git a/src/integrations/gateways/merge-gateway.ts b/src/integrations/gateways/merge-gateway.ts new file mode 100644 index 0000000000..8f1b87771c --- /dev/null +++ b/src/integrations/gateways/merge-gateway.ts @@ -0,0 +1,128 @@ +import { defineGateway } from '../define.js' +import { isKnownNonCodingModelId, isRecord } from '../modelMapping.js' + +/** Convert Merge's native catalog object into an OpenClaude route entry. */ +function mapMergeGatewayModel(raw: unknown) { + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { + return null + } + + const model = raw as Record + const apiName = + typeof model.id === 'string' + ? model.id.trim() + : typeof model.model === 'string' + ? model.model.trim() + : '' + if (!apiName) { + return null + } + + // Native entries describe modalities per vendor. Keep multimodal chat if + // any vendor supports text in and out; media/embedding endpoints cannot serve + // this conversation route. Sparse compatible entries use the existing ID filter. + const modalities = isRecord(model.vendors) + ? Object.values(model.vendors).flatMap(vendor => { + const capabilities = isRecord(vendor) ? vendor.capabilities : undefined + return isRecord(capabilities) && + Array.isArray(capabilities.input) && + Array.isArray(capabilities.output) + ? [{ input: capabilities.input, output: capabilities.output }] + : [] + }) + : [] + if ( + modalities.length > 0 + ? !modalities.some( + ({ input, output }) => input.includes('text') && output.includes('text'), + ) + : isKnownNonCodingModelId(apiName) || /(?:^|\/)veo[-_.]/i.test(apiName) + ) { + return null + } + + const label = + typeof model.display_name === 'string' && model.display_name.trim() + ? model.display_name.trim() + : apiName + + return { + id: `merge-gateway-${apiName}`, + apiName, + label: `${label} (via Merge Gateway)`, + } +} + +export default defineGateway({ + id: 'merge-gateway', + label: 'Merge Gateway', + category: 'aggregating', + defaultBaseUrl: 'https://api-gateway.merge.dev/v1/openai', + defaultModel: 'openai/gpt-5.5', + supportsModelRouting: true, + setup: { + requiresAuth: true, + authMode: 'api-key', + credentialEnvVars: ['MERGE_GATEWAY_API_KEY'], + dedicatedCredentialsOnly: true, + }, + startup: { + probeReadiness: 'openai-compatible-models', + }, + transportConfig: { + kind: 'openai-compatible', + openaiShim: { + supportsApiFormatSelection: true, + supportsAuthHeaders: false, + ui: { + showAuthHeader: false, + showAuthHeaderValue: false, + showCustomHeaders: false, + }, + }, + }, + preset: { + id: 'merge-gateway', + description: 'Merge Gateway multi-provider model router', + apiKeyEnvVars: ['MERGE_GATEWAY_API_KEY'], + modelEnvVars: ['OPENAI_MODEL'], + vendorId: 'openai', + }, + validation: { + kind: 'credential-env', + routing: { + matchDefaultBaseUrl: true, + matchBaseUrlHosts: ['api-gateway.merge.dev'], + }, + credentialEnvVars: ['MERGE_GATEWAY_API_KEY'], + missingCredentialMessage: + 'Merge Gateway auth is required. Set MERGE_GATEWAY_API_KEY.', + }, + catalog: { + source: 'hybrid', + discovery: { + kind: 'openai-compatible', + requiresAuth: true, + path: '/v1/models', + mapModel: mapMergeGatewayModel, + }, + discoveryCacheTtl: '1d', + discoveryRefreshMode: 'background-if-stale', + allowManualRefresh: true, + models: [ + { + id: 'merge-gateway-default-routing', + apiName: 'default_routing', + label: 'Default routing policy', + notes: 'Uses the applicable Merge routing policy.', + }, + { + id: 'merge-gateway-gpt-5.5', + apiName: 'openai/gpt-5.5', + label: 'GPT-5.5 (via Merge Gateway)', + modelDescriptorId: 'gpt-5.5', + }, + ], + }, + usage: { supported: false }, +}) diff --git a/src/integrations/generated/integrationArtifacts.generated.ts b/src/integrations/generated/integrationArtifacts.generated.ts index 61d9a90170..88c81cd810 100644 --- a/src/integrations/generated/integrationArtifacts.generated.ts +++ b/src/integrations/generated/integrationArtifacts.generated.ts @@ -42,6 +42,7 @@ import gatewayHicap from '../gateways/hicap.js' import gatewayKimiCode from '../gateways/kimi-code.js' import gatewayLlmtr from '../gateways/llmtr.js' import gatewayLmstudio from '../gateways/lmstudio.js' +import gatewayMergeGateway from '../gateways/merge-gateway.js' import gatewayMistral from '../gateways/mistral.js' import gatewayNvidiaNim from '../gateways/nvidia-nim.js' import gatewayOllama from '../gateways/ollama.js' @@ -95,7 +96,7 @@ import modelXai from '../models/xai.js' import modelXiaomiMimo from '../models/xiaomi-mimo.js' export const VENDOR_DESCRIPTORS = [vendorAnthropic, vendorBankr, vendorDeepseek, vendorFireworks, vendorGemini, vendorLongcat, vendorMinimax, vendorMoonshot, vendorNearai, vendorOpenai, vendorVenice, vendorXai, vendorXiaomiMimo, vendorZai] as const satisfies readonly VendorDescriptor[] -export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApiRoute, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCommandcode, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGmiCloud, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLlmtr, gatewayLmstudio, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[] +export const GATEWAY_DESCRIPTORS = [gatewayAimlapi, gatewayApiRoute, gatewayApismart, gatewayAtlasCloud, gatewayAtomicChat, gatewayAzureOpenai, gatewayBedrock, gatewayClinepass, gatewayCloudflare, gatewayCommandcode, gatewayConcentrate, gatewayCustom, gatewayDashscopeCn, gatewayDashscopeIntl, gatewayGithubEnterprise, gatewayGithub, gatewayGitlawbOpengateway, gatewayGmiCloud, gatewayGroq, gatewayHicap, gatewayKimiCode, gatewayLlmtr, gatewayLmstudio, gatewayMergeGateway, gatewayMistral, gatewayNvidiaNim, gatewayOllama, gatewayOpencodeGo, gatewayOpencode, gatewayOpenrouter, gatewayTogether, gatewayVertex, gatewayXiaomiMimoToken] as const satisfies readonly GatewayDescriptor[] export const ANTHROPIC_PROXY_DESCRIPTORS = [anthropicproxyCustom] as const satisfies readonly AnthropicProxyDescriptor[] export const BRAND_DESCRIPTORS = [brandClaude, brandDeepseek, brandFireworks, brandGemini, brandGlm, brandGpt, brandKimi, brandLing, brandLlama, brandLongcat, brandMacaron, brandMinimax, brandMistral, brandNearai, brandNemotron, brandOpenaiCompatibleAlias, brandQwen, brandTencent, brandXai, brandXiaomiMimo] as const satisfies readonly BrandDescriptor[] export const MODEL_DESCRIPTOR_GROUPS = [modelClaude, modelDeepseek, modelFireworksMerged, modelGemini, modelGlm, modelGpt, modelKimi, modelLing, modelLlama, modelLongcat, modelMacaron, modelMinimax, modelMistral, modelNearai, modelNemotron, modelOpenaiCompatibleAlias, modelOpencode, modelQwen, modelTencent, modelXai, modelXiaomiMimo] as const satisfies readonly (readonly ModelDescriptor[])[] diff --git a/src/integrations/generated/integrationManifest.generated.ts b/src/integrations/generated/integrationManifest.generated.ts index 47f720c89d..00df795cbc 100644 --- a/src/integrations/generated/integrationManifest.generated.ts +++ b/src/integrations/generated/integrationManifest.generated.ts @@ -361,6 +361,20 @@ export const PROVIDER_PRESET_MANIFEST = [ "OPENAI_MODEL" ] }, + { + "preset": "merge-gateway", + "routeKind": "gateway", + "routeId": "merge-gateway", + "vendorId": "openai", + "gatewayId": "merge-gateway", + "description": "Merge Gateway multi-provider model router", + "apiKeyEnvVars": [ + "MERGE_GATEWAY_API_KEY" + ], + "modelEnvVars": [ + "OPENAI_MODEL" + ] + }, { "preset": "minimax", "routeKind": "vendor", @@ -641,6 +655,7 @@ export const ORDERED_PROVIDER_PRESETS = [ "atomic-chat", "ollama", "longcat", + "merge-gateway", "minimax", "mistral", "moonshotai", diff --git a/src/integrations/index.test.ts b/src/integrations/index.test.ts index 0248f27a96..be562035f7 100644 --- a/src/integrations/index.test.ts +++ b/src/integrations/index.test.ts @@ -67,6 +67,7 @@ describe('loaded registry validation', () => { // Virtual model — the gateway's smart router resolves it server-side, // so there is no concrete model descriptor to reference. 'gitlawb-opengateway:opengateway-auto', + 'merge-gateway:merge-gateway-default-routing', // Cloudflare Workers AI serves provider-specific quantized builds // (`@cf/...`) with no shared cross-provider model descriptor, the same // situation as azure-deployment above. See gateways/cloudflare.ts (#1100). diff --git a/src/integrations/providerUiMetadata.ts b/src/integrations/providerUiMetadata.ts index 2340f45293..d32ea93686 100644 --- a/src/integrations/providerUiMetadata.ts +++ b/src/integrations/providerUiMetadata.ts @@ -45,6 +45,7 @@ function hasUsableEnvValue(envVar: string, value: string | undefined): boolean { envVar === 'OPENAI_API_KEYS' || envVar === 'OPENAI_API_KEY' || envVar === 'LLMTR_API_KEY' || + envVar === 'MERGE_GATEWAY_API_KEY' || envVar === 'CMD_API_KEY' || envVar === 'COMMANDCODE_API_KEY' || envVar === 'COMMAND_CODE_API_KEY' || diff --git a/src/integrations/routeMetadata.ts b/src/integrations/routeMetadata.ts index f249269b18..45db673eb4 100644 --- a/src/integrations/routeMetadata.ts +++ b/src/integrations/routeMetadata.ts @@ -244,6 +244,7 @@ function hasUsableEnvCredentialValue( envVar === 'APISMART_API_KEY' || envVar === 'CONCENTRATE_API_KEY' || envVar === 'LLMTR_API_KEY' || + envVar === 'MERGE_GATEWAY_API_KEY' || envVar === 'CMD_API_KEY' || envVar === 'COMMANDCODE_API_KEY' || envVar === 'COMMAND_CODE_API_KEY' || @@ -558,6 +559,36 @@ export function isCanonicalLlmtrInferenceBaseUrl( } } +const MERGE_GATEWAY_CANONICAL_INFERENCE_BASE_URL = 'https://api-gateway.merge.dev/v1/openai' + +export function isCanonicalMergeGatewayInferenceBaseUrl( + value: string | undefined, +): boolean { + const trimmed = value?.trim() + if (!trimmed) { + return false + } + + try { + const canonical = new URL(MERGE_GATEWAY_CANONICAL_INFERENCE_BASE_URL) + const candidate = new URL(trimmed) + const normalizePath = (pathname: string): string => + pathname.replace(/\/+$/, '') || '/' + return ( + candidate.protocol === 'https:' && + !candidate.port && + !candidate.username && + !candidate.password && + !candidate.search && + !candidate.hash && + candidate.hostname.toLowerCase() === canonical.hostname.toLowerCase() && + normalizePath(candidate.pathname) === normalizePath(canonical.pathname) + ) + } catch { + return false + } +} + const COMMANDCODE_CANONICAL_INFERENCE_BASE_URL = 'https://api.commandcode.ai/provider/v1' @@ -1319,6 +1350,13 @@ export function resolveRouteCredentialValue( ) { return undefined } + if ( + routeId === 'merge-gateway' && + options?.baseUrl !== undefined && + !isCanonicalMergeGatewayInferenceBaseUrl(options.baseUrl) + ) { + return undefined + } if ( routeId === 'commandcode' && options?.baseUrl !== undefined && @@ -1451,6 +1489,12 @@ export function resolveRouteIdFromBaseUrl( ) { continue } + if ( + route.id === 'merge-gateway' && + !isCanonicalMergeGatewayInferenceBaseUrl(baseUrl) + ) { + continue + } if ( route.id === 'commandcode' && !isCanonicalCommandcodeInferenceBaseUrl(baseUrl) @@ -1482,6 +1526,8 @@ export function resolveRouteIdFromBaseUrl( !isCanonicalConcentrateInferenceBaseUrl(baseUrl)) || (route.id === 'llmtr' && !isCanonicalLlmtrInferenceBaseUrl(baseUrl)) || + (route.id === 'merge-gateway' && + !isCanonicalMergeGatewayInferenceBaseUrl(baseUrl)) || (route.id === 'commandcode' && !isCanonicalCommandcodeInferenceBaseUrl(baseUrl)) || (route.id === 'api-route' && @@ -1528,6 +1574,9 @@ function profileRouteHonorsBaseUrlBoundary( if (routeId === 'llmtr') { return isCanonicalLlmtrInferenceBaseUrl(baseUrl) } + if (routeId === 'merge-gateway') { + return isCanonicalMergeGatewayInferenceBaseUrl(baseUrl) + } if (routeId === 'commandcode') { return isCanonicalCommandcodeInferenceBaseUrl(baseUrl) } diff --git a/src/services/api/agentRouting.ts b/src/services/api/agentRouting.ts index b5e46c1edb..fb900560b2 100644 --- a/src/services/api/agentRouting.ts +++ b/src/services/api/agentRouting.ts @@ -5,6 +5,7 @@ import { isModelAlias } from '../../utils/model/aliases.js' import { parseModelFlagValue } from '../../utils/cliArgs.js' import { argsBeforeModelOwningSubcommand } from '../../utils/printFlag.js' import { resolveRouteIdFromBaseUrl } from '../../integrations/routeMetadata.js' +import { sanitizeApiKey } from '../../utils/providerSecrets.js' /** * Provider override resolved from agent routing config. @@ -60,6 +61,7 @@ const PROVIDER_ENV_VARS_TO_CLEAR_FOR_OVERRIDE = [ 'OPENAI_AUTH_HEADER', 'OPENAI_AUTH_SCHEME', 'OPENAI_AUTH_HEADER_VALUE', + 'MERGE_GATEWAY_API_KEY', 'CMD_API_KEY', 'COMMANDCODE_API_KEY', 'COMMAND_CODE_API_KEY', @@ -368,6 +370,10 @@ export function applyAgentProviderOverrideToEnv( env.OPENAI_MODEL = providerOverride.model env.OPENAI_BASE_URL = providerOverride.baseURL env.OPENAI_API_KEY = providerOverride.apiKey + if (resolveRouteIdFromBaseUrl(providerOverride.baseURL) === 'merge-gateway') { + const key = sanitizeApiKey(providerOverride.apiKey) + if (key) env.MERGE_GATEWAY_API_KEY = key + } if (resolveRouteIdFromBaseUrl(providerOverride.baseURL) === 'commandcode') { env.CMD_API_KEY = providerOverride.apiKey } diff --git a/src/services/api/openaiShim/requestExecutor.ts b/src/services/api/openaiShim/requestExecutor.ts index 5a07a28fd1..4dd2475525 100644 --- a/src/services/api/openaiShim/requestExecutor.ts +++ b/src/services/api/openaiShim/requestExecutor.ts @@ -257,7 +257,8 @@ export async function executeOpenAIRequest( // behavior for unrelated providers or generic custom endpoints. const supportsConfiguredAuthHeaders = (runtimeShimContext.routeId !== 'llmtr' && - runtimeShimContext.routeId !== 'commandcode') || + runtimeShimContext.routeId !== 'commandcode' && + runtimeShimContext.routeId !== 'merge-gateway') || runtimeShimContext.openaiShimConfig.supportsAuthHeaders === true const unsupportedCustomHeaderNames = supportsConfiguredAuthHeaders ? null diff --git a/src/utils/envFile.test.ts b/src/utils/envFile.test.ts index 9d9febc408..5c696307c5 100644 --- a/src/utils/envFile.test.ts +++ b/src/utils/envFile.test.ts @@ -19,6 +19,7 @@ const TEST_ENV_KEYS = [ 'CODEX_AUTH_JSON_PATH', 'CODEX_HOME', 'LLMTR_API_KEY', + 'MERGE_GATEWAY_API_KEY', 'CMD_API_KEY', 'COMMANDCODE_API_KEY', 'COMMAND_CODE_API_KEY', @@ -343,6 +344,31 @@ describe('loadEnvFile', () => { expect(process.env.API_ROUTE_API_KEY).toBeUndefined() }) + it('loads a usable Merge credential with shell precedence and remembered replay', async () => { + const { getProviderValidationError } = await import('./providerValidation.js') + const filePath = writeTempEnvFile('MERGE_GATEWAY_API_KEY=file-merge-key') + const loaded = loadEnvFile(filePath) + expect(loaded).toEqual({ MERGE_GATEWAY_API_KEY: 'file-merge-key' }) + const validateMerge = () => getProviderValidationError({ + CLAUDE_CODE_USE_OPENAI: '1', + OPENAI_BASE_URL: 'https://api-gateway.merge.dev/v1/openai', + OPENAI_MODEL: 'openai/gpt-5.5', + MERGE_GATEWAY_API_KEY: process.env.MERGE_GATEWAY_API_KEY, + }) + expect(await validateMerge()).toBeNull() + + rememberLoadedEnvFileValues(loaded) + delete process.env.MERGE_GATEWAY_API_KEY + expect(await validateMerge()).toContain('MERGE_GATEWAY_API_KEY') + reapplyRememberedEnvFileValues() + expect(process.env.MERGE_GATEWAY_API_KEY).toBe('file-merge-key') + expect(await validateMerge()).toBeNull() + + process.env.MERGE_GATEWAY_API_KEY = 'shell-merge-key' + expect(loadEnvFile(filePath)).toEqual({}) + expect(process.env.MERGE_GATEWAY_API_KEY).toBe('shell-merge-key') + }) + it('loads the dedicated LLMTR credential without selecting a route', () => { const filePath = writeTempEnvFile('LLMTR_API_KEY=llmtr-key') diff --git a/src/utils/envFile.ts b/src/utils/envFile.ts index 456951ce43..531ba5ad20 100644 --- a/src/utils/envFile.ts +++ b/src/utils/envFile.ts @@ -89,6 +89,7 @@ const ALLOWED_ENV_FILE_KEYS = new Set([ 'COMMANDCODE_API_KEY', 'COMMAND_CODE_API_KEY', 'LONGCAT_API_KEY', + 'MERGE_GATEWAY_API_KEY', 'MINIMAX_API_KEY', 'MINIMAX_BASE_URL', 'MINIMAX_MODEL', diff --git a/src/utils/managedEnvConstants.ts b/src/utils/managedEnvConstants.ts index 629293b00b..e82eeae863 100644 --- a/src/utils/managedEnvConstants.ts +++ b/src/utils/managedEnvConstants.ts @@ -44,6 +44,7 @@ const PROVIDER_MANAGED_ENV_VARS = new Set([ 'COMMANDCODE_API_KEY', 'COMMAND_CODE_API_KEY', 'API_ROUTE_API_KEY', + 'MERGE_GATEWAY_API_KEY', 'OPENAI_API_KEY', 'OPENAI_API_KEYS', 'CLAUDE_CODE_SKIP_BEDROCK_AUTH', diff --git a/src/utils/providerDiscovery.ts b/src/utils/providerDiscovery.ts index 367f173f44..73505fbec5 100644 --- a/src/utils/providerDiscovery.ts +++ b/src/utils/providerDiscovery.ts @@ -2,6 +2,7 @@ import type { OllamaModelDescriptor } from './providerRecommendation.ts' import { DEFAULT_OPENAI_BASE_URL } from '../services/api/providerConfig.js' import { getRouteLabel, + isCanonicalMergeGatewayInferenceBaseUrl, resolveRouteIdFromBaseUrl, } from '../integrations/routeMetadata.js' @@ -158,6 +159,27 @@ export function getOpenAICompatibleModelsBaseUrl(baseUrl?: string): string { ).replace(/\/+$/, '') } +export function getOpenAICompatibleModelsUrl( + baseUrl?: string, + discoveryPath?: string, +): string { + const resolvedBaseUrl = getOpenAICompatibleModelsBaseUrl(baseUrl) + if (!discoveryPath?.trim()) { + return `${resolvedBaseUrl}/models` + } + + const parsedBaseUrl = new URL(resolvedBaseUrl) + const resolvedUrl = new URL( + discoveryPath.trim(), + `${resolvedBaseUrl}/`, + ) + if (resolvedUrl.origin !== parsedBaseUrl.origin) { + throw new Error('Model discovery path must stay on the provider origin') + } + + return resolvedUrl.toString() +} + export function getLocalOpenAICompatibleProviderLabel(baseUrl?: string): string { try { const parsed = new URL(getOpenAICompatibleModelsBaseUrl(baseUrl)) @@ -263,11 +285,15 @@ export async function listOpenAICompatibleModels(options?: { baseUrl?: string apiKey?: string headers?: Record + path?: string }): Promise { const { signal, clear } = withTimeoutSignal(5000) try { - const baseUrl = getOpenAICompatibleModelsBaseUrl(options?.baseUrl) - const isBankr = baseUrl.toLowerCase().includes('bankr') + const modelsUrl = getOpenAICompatibleModelsUrl( + options?.baseUrl, + options?.path, + ) + const isBankr = modelsUrl.toLowerCase().includes('bankr') const headers = { ...(options?.headers ?? {}), ...(options?.apiKey @@ -277,7 +303,7 @@ export async function listOpenAICompatibleModels(options?: { : {}), } const response = await fetch( - `${baseUrl}/models`, + modelsUrl, { method: 'GET', headers: Object.keys(headers).length > 0 ? headers : undefined, @@ -306,15 +332,62 @@ export async function listOpenAICompatibleModels(options?: { } } +// Merge's native catalog is paginated. Only a complete result may enter the +// shared discovery cache; an incomplete refresh retains its existing fallback. +export async function fetchMergeGatewayModelsRaw(options: { + baseUrl?: string + apiKey?: string +}): Promise[] | null> { + if (!isCanonicalMergeGatewayInferenceBaseUrl(options.baseUrl)) return null + try { + const url = new URL('/v1/models', options.baseUrl) + const models: Record[] = [] + const cursors = new Set() + for (let page = 0; page < 100; page++) { + const { signal, clear } = withTimeoutSignal(5000) + try { + const response = await fetch(url.toString(), { + method: 'GET', + headers: options.apiKey ? { Authorization: `Bearer ${options.apiKey}` } : undefined, + signal, + redirect: 'error', + }) + if (!response.ok) return null + const payload = await response.json() as { + data?: Record[] + has_more?: boolean + next_cursor?: string | null + } + if (!Array.isArray(payload.data)) return null + models.push(...payload.data) + if (!payload.has_more) return models + const cursor = payload.next_cursor + if (typeof cursor !== 'string' || !cursor || cursors.has(cursor)) return null + cursors.add(cursor) + url.searchParams.set('cursor', cursor) + } finally { + clear() + } + } + return null + } catch { + return null + } +} + export async function fetchOpenAICompatibleModelsRaw(options?: { baseUrl?: string apiKey?: string headers?: Record + path?: string }): Promise[] | null> { const { signal, clear } = withTimeoutSignal(5000) try { - const baseUrl = getOpenAICompatibleModelsBaseUrl(options?.baseUrl) - const isBankr = baseUrl.toLowerCase().includes('bankr') + const modelsUrl = getOpenAICompatibleModelsUrl( + options?.baseUrl, + options?.path, + ) + const isBankr = modelsUrl.toLowerCase().includes('bankr') const headers = { ...(options?.headers ?? {}), ...(options?.apiKey @@ -323,7 +396,7 @@ export async function fetchOpenAICompatibleModelsRaw(options?: { : { Authorization: `Bearer ${options.apiKey}` } : {}), } - const response = await fetch(`${baseUrl}/models`, { + const response = await fetch(modelsUrl, { method: 'GET', headers: Object.keys(headers).length > 0 ? headers : undefined, signal, diff --git a/src/utils/providerFlag.ts b/src/utils/providerFlag.ts index 641a8ff538..811e601a67 100644 --- a/src/utils/providerFlag.ts +++ b/src/utils/providerFlag.ts @@ -36,6 +36,7 @@ import { isCanonicalConcentrateInferenceBaseUrl, isCanonicalLlmtrInferenceBaseUrl, isCanonicalCommandcodeInferenceBaseUrl, + isCanonicalMergeGatewayInferenceBaseUrl, isGmiCloudBaseUrl, resolveActiveRouteIdFromEnv, } from '../integrations/routeMetadata.js' @@ -416,6 +417,9 @@ export function applyProviderFlag( : process.env.OPENAI_API_KEY !== undefined && process.env.OPENAI_API_KEY === process.env.LLMTR_API_KEY ? 'llmtr' + : process.env.OPENAI_API_KEY !== undefined && + process.env.OPENAI_API_KEY === process.env.MERGE_GATEWAY_API_KEY + ? 'merge-gateway' : process.env.OPENAI_API_KEY !== undefined && (process.env.OPENAI_API_KEY === process.env.CMD_API_KEY || process.env.OPENAI_API_KEY === @@ -933,6 +937,8 @@ export function applyProviderFlag( process.env.CLAUDE_CODE_USE_OPENAI = '1' applyOpenAIBaseUrlDefault(provider, defaultBaseUrl) if ( + (provider === 'merge-gateway' && + isCanonicalMergeGatewayInferenceBaseUrl(getConfiguredOpenAIBaseUrl())) || (provider === 'llmtr' && isCanonicalLlmtrInferenceBaseUrl(getConfiguredOpenAIBaseUrl())) || (provider === 'commandcode' && diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 665861e690..bf2c5cdae2 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -29,6 +29,7 @@ import { isCanonicalApismartInferenceBaseUrl, isCanonicalConcentrateInferenceBaseUrl, isCanonicalLlmtrInferenceBaseUrl, + isCanonicalMergeGatewayInferenceBaseUrl, isGmiCloudBaseUrl, isCanonicalCommandcodeInferenceBaseUrl, isLongcatBaseUrl, @@ -127,6 +128,7 @@ const PROFILE_ENV_KEYS = [ 'FIREWORKS_API_KEY', 'LONGCAT_API_KEY', 'LLMTR_API_KEY', + 'MERGE_GATEWAY_API_KEY', 'CMD_API_KEY', 'COMMANDCODE_API_KEY', 'COMMAND_CODE_API_KEY', @@ -231,6 +233,7 @@ export type ProfileEnv = { FIREWORKS_API_KEY?: string LONGCAT_API_KEY?: string LLMTR_API_KEY?: string + MERGE_GATEWAY_API_KEY?: string CMD_API_KEY?: string COMMANDCODE_API_KEY?: string COMMAND_CODE_API_KEY?: string @@ -2167,9 +2170,15 @@ export async function buildLaunchEnv(options: { } else { delete env.OPENAI_AUTH_HEADER_VALUE } + // A shell URL override must not carry a saved Merge key through its generic + // alias after route resolution drops the original provider identity. + const retargetedMergeProfile = + (persistedEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'merge-gateway' || + isCanonicalMergeGatewayInferenceBaseUrl(persistedOpenAIBaseUrl)) && + !isCanonicalMergeGatewayInferenceBaseUrl(env.OPENAI_BASE_URL) const openAICredential = resolveOpenAICredentialEnvOverride( processEnv, - persistedEnv, + retargetedMergeProfile ? {} : persistedEnv, ) if (openAICredential) { env[openAICredential.envVar] = openAICredential.value @@ -2230,6 +2239,10 @@ export async function buildLaunchEnv(options: { effectiveOpenAIRouteId === 'llmtr' && !!env.OPENAI_BASE_URL?.trim() && !isCanonicalLlmtrInferenceBaseUrl(env.OPENAI_BASE_URL) + const isNoncanonicalMergeGatewayLaunch = + effectiveOpenAIRouteId === 'merge-gateway' && + !!env.OPENAI_BASE_URL?.trim() && + !isCanonicalMergeGatewayInferenceBaseUrl(env.OPENAI_BASE_URL) const isNoncanonicalCommandcodeLaunch = effectiveOpenAIRouteId === 'commandcode' && !!env.OPENAI_BASE_URL?.trim() && @@ -2243,6 +2256,7 @@ export async function buildLaunchEnv(options: { isNoncanonicalApismartLaunch || isNoncanonicalConcentrateLaunch || isNoncanonicalLlmtrLaunch || + isNoncanonicalMergeGatewayLaunch || isNoncanonicalCommandcodeLaunch || isNoncanonicalApiRouteLaunch if (isNoncanonicalDedicatedOpenAILaunch) { @@ -2256,6 +2270,7 @@ export async function buildLaunchEnv(options: { if ( !isNoncanonicalConcentrateLaunch && !isNoncanonicalLlmtrLaunch && + !isNoncanonicalMergeGatewayLaunch && !isNoncanonicalCommandcodeLaunch ) { const persistedCredential = resolveOpenAICredentialEnvSelection(persistedEnv) @@ -2292,6 +2307,7 @@ export async function buildLaunchEnv(options: { 'APISMART_API_KEY', 'CONCENTRATE_API_KEY', 'LLMTR_API_KEY', + 'MERGE_GATEWAY_API_KEY', 'CMD_API_KEY', 'COMMANDCODE_API_KEY', 'COMMAND_CODE_API_KEY', @@ -2319,6 +2335,9 @@ export async function buildLaunchEnv(options: { if (dedicatedKey === 'LLMTR_API_KEY' && effectiveOpenAIRouteId !== 'llmtr') { continue } + if (dedicatedKey === 'MERGE_GATEWAY_API_KEY' && effectiveOpenAIRouteId !== 'merge-gateway') { + continue + } if ( isCommandcodeCredentialEnvVar(dedicatedKey) && effectiveOpenAIRouteId !== 'commandcode' @@ -2356,6 +2375,10 @@ export async function buildLaunchEnv(options: { dedicatedKey === 'LLMTR_API_KEY' && !!dedicatedBaseUrl && !isCanonicalLlmtrInferenceBaseUrl(dedicatedBaseUrl) + const withholdAmbientMergeGatewayKey = + dedicatedKey === 'MERGE_GATEWAY_API_KEY' && + !!dedicatedBaseUrl && + !isCanonicalMergeGatewayInferenceBaseUrl(dedicatedBaseUrl) const withholdAmbientCommandcodeKey = isCommandcodeCredentialEnvVar(dedicatedKey) && !!dedicatedBaseUrl && @@ -2365,6 +2388,7 @@ export async function buildLaunchEnv(options: { withholdAmbientApismartKey || withholdAmbientConcentrateKey || withholdAmbientLlmtrKey || + withholdAmbientMergeGatewayKey || withholdAmbientCommandcodeKey // Unlike the generic proxy-compatible routes above, Concentrate's // dedicated key is never valid outside its canonical inference endpoint. @@ -2374,6 +2398,7 @@ export async function buildLaunchEnv(options: { if ( withholdAmbientConcentrateKey || withholdAmbientLlmtrKey || + withholdAmbientMergeGatewayKey || withholdAmbientCommandcodeKey ) { continue @@ -2421,6 +2446,17 @@ export async function buildLaunchEnv(options: { ? sanitizeApiKey(persistedOpenAICredential.value) : undefined) : undefined + const persistedMergeGatewayProfileKey = + dedicatedKey === 'MERGE_GATEWAY_API_KEY' && + effectiveOpenAIRouteId === 'merge-gateway' && + isCanonicalMergeGatewayInferenceBaseUrl(persistedOpenAIBaseUrl) && + !!dedicatedBaseUrl && + isCanonicalMergeGatewayInferenceBaseUrl(dedicatedBaseUrl) + ? sanitizeApiKey(persistedEnv.MERGE_GATEWAY_API_KEY) || + (persistedOpenAICredential?.kind === 'usable' + ? sanitizeApiKey(persistedOpenAICredential.value) + : undefined) + : undefined // A selected canonical Command Code profile owns its saved credential. // Prefer persisted dedicated keys, then migrate startup files that only // mirrored the secret into OPENAI_API_KEY (LLMTR parity). Do not treat a @@ -2449,6 +2485,7 @@ export async function buildLaunchEnv(options: { ? sanitizeApiKey(persistedEnv[dedicatedKey]) : backfillDedicatedFromOpenAI || persistedLlmtrProfileKey || + persistedMergeGatewayProfileKey || persistedCommandcodeProfileKey || sanitizeApiKey(processEnv[dedicatedKey]) || sanitizeApiKey(persistedEnv[dedicatedKey]) || @@ -2564,6 +2601,11 @@ export async function buildStartupEnvFromProfile(options?: { persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'llmtr' && !!persisted.env.OPENAI_BASE_URL?.trim() && !isCanonicalLlmtrInferenceBaseUrl(persisted.env.OPENAI_BASE_URL) + const persistedMergeGatewayProxy = + persisted?.profile === 'openai' && + persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'merge-gateway' && + !!persisted.env.OPENAI_BASE_URL?.trim() && + !isCanonicalMergeGatewayInferenceBaseUrl(persisted.env.OPENAI_BASE_URL) const persistedCommandcodeProxy = persisted?.profile === 'openai' && persisted.env.CLAUDE_CODE_PROVIDER_ROUTE_ID === 'commandcode' && @@ -2579,6 +2621,7 @@ export async function buildStartupEnvFromProfile(options?: { !persistedApismartProxy && !persistedConcentrateProxy && !persistedLlmtrProxy && + !persistedMergeGatewayProxy && !persistedCommandcodeProxy && !persistedApiRouteProxy ) { diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index 9da19e212e..147d18dd72 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -60,6 +60,7 @@ import { isCanonicalApismartInferenceBaseUrl, isCanonicalConcentrateInferenceBaseUrl, isCanonicalLlmtrInferenceBaseUrl, + isCanonicalMergeGatewayInferenceBaseUrl, isCanonicalCommandcodeInferenceBaseUrl, isFireworksBaseUrl, isGmiCloudBaseUrl, @@ -205,6 +206,16 @@ function isLlmtrProfile(profile: ProviderProfile): boolean { return !baseUrl || isCanonicalLlmtrInferenceBaseUrl(baseUrl) } +function isMergeGatewayProfile(profile: ProviderProfile): boolean { + const { route, compatibilityMode } = resolveProfileCompatibility(profile.provider) + if (compatibilityMode !== 'openai') return false + const baseUrl = profile.baseUrl?.trim() + if (route.routeId === 'merge-gateway') { + return !baseUrl || isCanonicalMergeGatewayInferenceBaseUrl(baseUrl) + } + return Boolean(baseUrl && isCanonicalMergeGatewayInferenceBaseUrl(baseUrl)) +} + function isCommandcodeProfile(profile: ProviderProfile): boolean { const { route, compatibilityMode } = resolveProfileCompatibility( profile.provider, @@ -311,6 +322,7 @@ export function resolveProfileCapabilityRouteId( providerRouteId === 'concentrate' || providerRouteId === 'api-route' || providerRouteId === 'llmtr' || + providerRouteId === 'merge-gateway' || providerRouteId === 'commandcode') && baseUrl && !(providerRouteId === 'cloudflare' @@ -323,7 +335,9 @@ export function resolveProfileCapabilityRouteId( ? isCanonicalApiRouteInferenceBaseUrl(baseUrl) : providerRouteId === 'commandcode' ? isCanonicalCommandcodeInferenceBaseUrl(baseUrl) - : isCanonicalLlmtrInferenceBaseUrl(baseUrl)) + : providerRouteId === 'merge-gateway' + ? isCanonicalMergeGatewayInferenceBaseUrl(baseUrl) + : isCanonicalLlmtrInferenceBaseUrl(baseUrl)) ) { return 'custom' } @@ -1174,6 +1188,8 @@ export function applyProviderProfileToProcessEnv( route.routeId === 'api-route' && !isApiRouteProfile(profile) const withholdRetargetedLlmtrCredential = route.routeId === 'llmtr' && !isLlmtrProfile(profile) + const withholdRetargetedMergeGatewayCredential = + route.routeId === 'merge-gateway' && !isMergeGatewayProfile(profile) const withholdRetargetedCommandcodeCredential = route.routeId === 'commandcode' && !isCommandcodeProfile(profile) // A gmi-cloud profile's key belongs to GMI: never install it for another URL. @@ -1185,6 +1201,7 @@ export function applyProviderProfileToProcessEnv( !withholdRetargetedConcentrateCredential && !withholdRetargetedApiRouteCredential && !withholdRetargetedLlmtrCredential && + !withholdRetargetedMergeGatewayCredential && !withholdRetargetedCommandcodeCredential && !withholdRetargetedGmiCloudCredential ) { @@ -1335,6 +1352,19 @@ export function applyProviderProfileToProcessEnv( } } } + if (route.routeId === 'merge-gateway' || isMergeGatewayProfile(profile)) { + openAIProfileEnv.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'merge-gateway' + if (isMergeGatewayProfile(profile)) { + // Reuse the source-profile guards above: retargeting a protected + // provider to Merge does not transfer ownership of its saved key. + const mergeKey = + sanitizeApiKey(openAIProfileEnv.OPENAI_API_KEY) || + (route.routeId === 'merge-gateway' + ? sanitizeApiKey(process.env.MERGE_GATEWAY_API_KEY) + : undefined) + if (mergeKey) openAIProfileEnv.MERGE_GATEWAY_API_KEY = mergeKey + } + } // A keyless canonical LLMTR profile may use the dedicated credential from // the ambient environment. Profile application clears all managed provider // variables before installing this object, so adopt it here while the @@ -1665,6 +1695,8 @@ function buildOpenAICompatibleStartupEnv( activeProfileRouteId === 'api-route' && !isApiRouteProfile(activeProfile) const withholdRetargetedLlmtrCredential = activeProfileRouteId === 'llmtr' && !isLlmtrProfile(activeProfile) + const withholdRetargetedMergeGatewayCredential = + activeProfileRouteId === 'merge-gateway' && !isMergeGatewayProfile(activeProfile) const withholdRetargetedCommandcodeCredential = activeProfileRouteId === 'commandcode' && !isCommandcodeProfile(activeProfile) @@ -1674,6 +1706,7 @@ function buildOpenAICompatibleStartupEnv( const isConcentrateProfileFlag = isConcentrateProfile(activeProfile) const isApiRouteProfileFlag = isApiRouteProfile(activeProfile) const isLlmtrProfileFlag = isLlmtrProfile(activeProfile) + const isMergeGatewayProfileFlag = isMergeGatewayProfile(activeProfile) const isCommandcodeProfileFlag = isCommandcodeProfile(activeProfile) if ( @@ -1682,6 +1715,7 @@ function buildOpenAICompatibleStartupEnv( !withholdRetargetedConcentrateCredential && !withholdRetargetedApiRouteCredential && !withholdRetargetedLlmtrCredential && + !withholdRetargetedMergeGatewayCredential && !withholdRetargetedCommandcodeCredential ) { const strictEnv = buildOpenAIProfileEnv({ @@ -1735,6 +1769,9 @@ function buildOpenAICompatibleStartupEnv( if (isLlmtrProfileFlag) { strictEnv.LLMTR_API_KEY = activeProfile.apiKey } + if (isMergeGatewayProfileFlag) { + strictEnv.MERGE_GATEWAY_API_KEY = activeProfile.apiKey + } if (isCommandcodeProfileFlag) { strictEnv.CMD_API_KEY = activeProfile.apiKey } @@ -1805,6 +1842,9 @@ function buildOpenAICompatibleStartupEnv( if (activeProfileRouteId === 'llmtr') { env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'llmtr' } + if (activeProfileRouteId === 'merge-gateway') { + env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'merge-gateway' + } if (activeProfileRouteId === 'commandcode') { env.CLAUDE_CODE_PROVIDER_ROUTE_ID = 'commandcode' } @@ -1814,6 +1854,7 @@ function buildOpenAICompatibleStartupEnv( !withholdRetargetedConcentrateCredential && !withholdRetargetedApiRouteCredential && !withholdRetargetedLlmtrCredential && + !withholdRetargetedMergeGatewayCredential && !withholdRetargetedCommandcodeCredential ) { env.OPENAI_API_KEY = activeProfile.apiKey @@ -1853,6 +1894,9 @@ function buildOpenAICompatibleStartupEnv( if (isLlmtrProfileFlag) { env.LLMTR_API_KEY = activeProfile.apiKey } + if (isMergeGatewayProfileFlag) { + env.MERGE_GATEWAY_API_KEY = activeProfile.apiKey + } if (isCommandcodeProfileFlag) { env.CMD_API_KEY = activeProfile.apiKey } @@ -2135,6 +2179,9 @@ function triggerStartupDiscoveryRefreshForProfile( if (route.routeId === 'llmtr' && !isLlmtrProfile(profile)) { return } + if (route.routeId === 'merge-gateway' && !isMergeGatewayProfile(profile)) { + return + } if (route.routeId === 'commandcode' && !isCommandcodeProfile(profile)) { return } diff --git a/src/utils/providerValidation.test.ts b/src/utils/providerValidation.test.ts index fa32c11cf1..cc251e4f43 100644 --- a/src/utils/providerValidation.test.ts +++ b/src/utils/providerValidation.test.ts @@ -35,6 +35,7 @@ const ENV_KEYS = [ 'MINIMAX_API_KEY', 'LONGCAT_API_KEY', 'LLMTR_API_KEY', + 'MERGE_GATEWAY_API_KEY', 'CMD_API_KEY', 'COMMANDCODE_API_KEY', 'COMMAND_CODE_API_KEY', @@ -1032,3 +1033,44 @@ for (const baseEnv of ['ANTHROPIC_BASE_URL', 'OPENAI_BASE_URL', 'OPENAI_API_BASE expect(shouldExitForStartupProviderValidationError({ args: ['-p', 'hello'], stdoutIsTTY: false })).toBe(true) }) } + +test.each(['OPENAI_BASE_URL', 'OPENAI_API_BASE'])('Merge validation follows the runtime endpoint boundary through %s', async urlEnv => { + process.env.CLAUDE_CODE_USE_OPENAI = '1' + process.env.OPENAI_MODEL = 'openai/gpt-5.5' + process.env.OPENAI_API_KEY = 'custom-endpoint-key' + for (const baseUrl of [ + 'https://api-gateway.merge.dev/v1/custom', + 'http://api-gateway.merge.dev/v1/openai', + 'https://api-gateway.merge.dev:8443/v1/openai', + 'https://user@api-gateway.merge.dev/v1/openai', + 'https://api-gateway.merge.dev/v1/openai?tenant=proxy', + 'https://api-gateway.merge.dev/v1/openai#proxy', + 'https://api-gateway.merge.dev/v1/OpenAI', + 'https://proxy.example/v1', + ]) { + process.env[urlEnv] = baseUrl + expect(await getProviderValidationError(process.env)).toBeNull() + } + for (const suffix of ['', '/']) { + process.env[urlEnv] = 'https://api-gateway.merge.dev/v1/openai' + suffix + delete process.env.MERGE_GATEWAY_API_KEY + expect(await getProviderValidationError(process.env)).toContain('MERGE_GATEWAY_API_KEY') + process.env.MERGE_GATEWAY_API_KEY = 'merge-key' + expect(await getProviderValidationError(process.env)).toBeNull() + } +}) + +test.each([undefined, '', 'undefined', 'null', 'SUA_CHAVE', 'merge-key'])('Merge validation, picker and runtime agree on usable credentials: %s', async key => { + const { getProviderPresetUiMetadata } = await import('../integrations/providerUiMetadata.js') + const { resolveRouteCredentialValue } = await import('../integrations/routeMetadata.js') + const env = { + CLAUDE_CODE_USE_OPENAI: '1', OPENAI_MODEL: 'openai/gpt-5.5', + OPENAI_BASE_URL: 'https://api-gateway.merge.dev/v1/openai', + MERGE_GATEWAY_API_KEY: key, OPENAI_API_KEY: 'unrelated-openai-key', + } + const usable = key === 'merge-key' + const validation = await getProviderValidationError(env) + expect(validation === null).toBe(usable) + expect(getProviderPresetUiMetadata('merge-gateway', env).apiKey).toBe(usable ? key : '') + expect(resolveRouteCredentialValue({routeId: 'merge-gateway', baseUrl: env.OPENAI_BASE_URL, processEnv: env})).toBe(usable ? key : undefined) +}) diff --git a/src/utils/providerValidation.ts b/src/utils/providerValidation.ts index c5d49ba326..8ef05241ca 100644 --- a/src/utils/providerValidation.ts +++ b/src/utils/providerValidation.ts @@ -22,6 +22,7 @@ import { isCanonicalApismartInferenceBaseUrl, isCanonicalConcentrateInferenceBaseUrl, isCanonicalCommandcodeInferenceBaseUrl, + isCanonicalMergeGatewayInferenceBaseUrl, isCloudflareBaseUrl, isLongcatBaseUrl, matchHostnameAgainstRouteHosts, @@ -150,6 +151,7 @@ function hasUsableCredentialEnvValue( envVar === 'API_ROUTE_API_KEY' || envVar === 'CONCENTRATE_API_KEY' || envVar === 'LLMTR_API_KEY' || + envVar === 'MERGE_GATEWAY_API_KEY' || envVar === 'CMD_API_KEY' || envVar === 'COMMANDCODE_API_KEY' || envVar === 'COMMAND_CODE_API_KEY' @@ -313,6 +315,8 @@ function getRuntimeValidationTarget( !isCanonicalConcentrateInferenceBaseUrl(request.baseUrl)) || (target.descriptor.id === 'api-route' && !isCanonicalApiRouteInferenceBaseUrl(request.baseUrl)) || + (target.descriptor.id === 'merge-gateway' && + !isCanonicalMergeGatewayInferenceBaseUrl(request.baseUrl)) || (target.descriptor.id === 'commandcode' && !isCanonicalCommandcodeInferenceBaseUrl(request.baseUrl))) ) { diff --git a/src/utils/swarm/spawnUtils.ts b/src/utils/swarm/spawnUtils.ts index b6b32436b4..a46fa8be35 100644 --- a/src/utils/swarm/spawnUtils.ts +++ b/src/utils/swarm/spawnUtils.ts @@ -110,6 +110,7 @@ const TEAMMATE_ENV_VARS = [ 'OPENAI_API_KEYS', 'OPENAI_API_KEY', 'LLMTR_API_KEY', + 'MERGE_GATEWAY_API_KEY', 'CMD_API_KEY', 'COMMANDCODE_API_KEY', 'COMMAND_CODE_API_KEY', diff --git a/web/src/data/providers.ts b/web/src/data/providers.ts index 206e723fb4..1dfe68b8a1 100644 --- a/web/src/data/providers.ts +++ b/web/src/data/providers.ts @@ -1,5 +1,5 @@ // Mirrors the provider preset manifest in -// src/integrations/generated/integrationManifest.generated.ts (v0.26.0) +// src/integrations/generated/integrationManifest.generated.ts (v0.29.1) // plus the OAuth/special entries injected by the /provider picker. export type ProviderGroup = @@ -148,6 +148,14 @@ export const providers: Provider[] = [ envVars: ['LLMTR_API_KEY', 'OPENAI_API_KEY'], notes: '/provider and --provider llmtr default to deepseek/deepseek-v4-flash; raw env setup must set OPENAI_BASE_URL=https://llmtr.com/v1 and OPENAI_MODEL. Uses public discovery of tool-capable chat models.', }, + { + id: 'merge-gateway', + name: 'Merge Gateway', + group: 'gateways', + setup: '/provider or env vars', + envVars: ['MERGE_GATEWAY_API_KEY'], + notes: 'Multi-provider model router with Chat Completions, Responses, authenticated catalog discovery, and routing policies.', + }, { id: 'near-ai', name: 'NEAR AI', From 87e6ae6359c098f4f0fbc81e0d678e54526047a2 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 16:25:50 -0700 Subject: [PATCH 012/121] test: stabilize watchdog and settings timing (#2319) --- CONTRIBUTING.md | 1 + .../api/claude.streamWatchdog.test.ts | 54 ++++++++++--------- src/utils/settings/changeDetector.test.ts | 12 ++++- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f2e2b1af16..df4c8f2ab1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,6 +159,7 @@ Dependency changes need a clear project benefit — fixing a bug, addressing a s - Avoid mixing unrelated cleanup into the same change. - Preserve existing repo patterns unless the change is intentionally refactoring them. - Add or update tests when the change affects behavior. +- In timing-sensitive tests, drive timers with Bun's fake timers (`jest.useFakeTimers()` and `jest.advanceTimersByTime()`) instead of racing real delays, and restore real timers during cleanup, so the tests pass in a single-process whole-suite run as well as in the sharded runner. - Update docs when setup, commands, or user-facing behavior changes. - Website release notes live on GitHub Releases. Do not add manually maintained release-note data to the static site. diff --git a/src/services/api/claude.streamWatchdog.test.ts b/src/services/api/claude.streamWatchdog.test.ts index 38ecf5e50c..5cc9a74c56 100644 --- a/src/services/api/claude.streamWatchdog.test.ts +++ b/src/services/api/claude.streamWatchdog.test.ts @@ -3,6 +3,7 @@ import { beforeEach, describe, expect, + jest, spyOn, test, } from 'bun:test' @@ -279,14 +280,16 @@ async function collectStreamingMessages( return messages } -function delay(ms: number): Promise<'timeout'> { - return new Promise(resolve => { - setTimeout(() => resolve('timeout'), ms) - }) +function fireStreamWatchdog(signal: AbortSignal): void { + // Drive the idle deadline independently of event-loop latency. + jest.advanceTimersByTime(24) + expect(signal.aborted).toBe(false) + jest.advanceTimersByTime(1) + expect(signal.aborted).toBe(true) } async function settleForCleanup(promise: Promise): Promise { - await Promise.race([promise.catch(() => undefined), delay(50)]) + await promise.catch(() => undefined) } function setTestMacro(): void { @@ -315,6 +318,7 @@ beforeEach(async () => { process.env.CLAUDE_STREAM_IDLE_TIMEOUT_MS = '25' process.env.OPENCLAUDE_MAX_RETRIES = '0' process.env.VCR_RECORD = '1' + jest.useFakeTimers() }) afterEach(async () => { @@ -350,6 +354,7 @@ afterEach(async () => { fixturesRoot = undefined } } finally { + jest.useRealTimers() releaseSharedMutationLock() } }) @@ -380,8 +385,8 @@ describe('Claude stream watchdog', () => { await wedged.nextStarted try { - const result = await Promise.race([request, delay(250)]) - expect(result).not.toBe('timeout') + fireStreamWatchdog(wedged.abortSignal) + const result = await request expect(streamModes).toEqual([true, undefined]) expect(wedged.abortSignal.aborted).toBe(true) expect(wedged.returnCalled()).toBe(true) @@ -482,13 +487,15 @@ describe('Claude stream watchdog', () => { error instanceof Error ? error.name : String(error), ) await wedged.nextStarted - requestAbort(controller, undefined, { - source: 'cancel_keybinding', - controllerRole: 'query-root', - }) - try { - const result = await Promise.race([request, delay(150)]) + jest.advanceTimersByTime(249) + expect(wedged.abortSignal.aborted).toBe(false) + requestAbort(controller, undefined, { + source: 'cancel_keybinding', + controllerRole: 'query-root', + }) + + const result = await request expect(result).toBe('resolved') expect(streamModes).toEqual([true]) expect(wedged.abortSignal.aborted).toBe(true) @@ -536,7 +543,8 @@ describe('Claude stream watchdog', () => { ) await wedged.nextStarted try { - await Promise.race([request, delay(250)]) + fireStreamWatchdog(wedged.abortSignal) + await request const trace = __getInterruptionTraceSnapshotForTests() const started = trace.find( entry => entry.event === 'claude_stream.fallback_started', @@ -571,14 +579,11 @@ describe('Claude stream watchdog', () => { ) await wedged.nextStarted try { - const result = await Promise.race([ - request.then( - () => 'resolved', - error => error, - ), - delay(250), - ]) - expect(result).not.toBe('timeout') + fireStreamWatchdog(wedged.abortSignal) + await request.then( + () => 'resolved', + error => error, + ) const settlements = __getInterruptionTraceSnapshotForTests().filter( entry => entry.event === 'claude_stream.fallback_settled', ) @@ -649,8 +654,9 @@ describe('Claude stream watchdog', () => { await wedged.nextStarted try { - const result = await Promise.race([request, delay(250)]) - expect(result).not.toBe('timeout') + fireStreamWatchdog(wedged.abortSignal) + await request + jest.useRealTimers() wedged.rejectPendingNext(new Error('late iterator failure')) await new Promise(resolve => setTimeout(resolve, 0)) expect(unhandledRejections).toEqual([]) diff --git a/src/utils/settings/changeDetector.test.ts b/src/utils/settings/changeDetector.test.ts index b1f3906371..5cddd3d95a 100644 --- a/src/utils/settings/changeDetector.test.ts +++ b/src/utils/settings/changeDetector.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterEach, beforeEach, describe, expect, jest, mock, test } from 'bun:test' import { normalize } from 'path' import { acquireSharedMutationLock, @@ -70,6 +70,7 @@ afterEach(async () => { activeDetector = null mock.restore() } finally { + jest.useRealTimers() releaseSharedMutationLock() } }) @@ -103,6 +104,7 @@ describe('settings change detector fanout batching', () => { deletionGrace: 1, settingsDebounce: 5, }) + jest.useFakeTimers() const emitted: SettingSource[] = [] const unsubscribe = detector.subscribe(source => { @@ -112,7 +114,13 @@ describe('settings change detector fanout batching', () => { detector._handleDeleteForTesting(pathsBySource.userSettings!) detector._handleDeleteForTesting(pathsBySource.projectSettings!) - await sleep(30) + // Deletion hooks schedule a separate debounce after the grace period. + jest.advanceTimersByTime(1) + await Promise.resolve() + jest.advanceTimersByTime(4) + expect(resetSettingsCache).not.toHaveBeenCalled() + expect(emitted).toEqual([]) + jest.advanceTimersByTime(1) expect(resetSettingsCache).toHaveBeenCalledTimes(1) expect(emitted).toEqual(['userSettings', 'projectSettings']) From a4ed687b84d8276b2e868a9c9fe6c3cb8430013d Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 16:49:38 -0700 Subject: [PATCH 013/121] fix(agent): await per-agent output before completion (ORC-1337) (#2318) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: synchronize agent completion with output eviction Agent task completion could become externally observable before the output eviction and flush sequence completed, resulting in callers observing empty output files. Root Cause ---------- completeAgentTask() returned immediately while evictTaskOutput() continued asynchronously. Callers could observe task completion before the eviction/flush chain finished. Fix --- - Make completeAgentTask() async and await eviction - Await completeAsyncAgent() at both call sites - Clarify ORC-1337 vs gh-20236 in comments Testing ------- - AgentTool: 59/59 passing - LocalAgentTask: 6/6 passing Refs: ORC-1337, gh-20236 Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> * fix: move output barrier before terminal state transition Review follow-up for ORC-1337 addressing both P1 findings: 1. Ordering: updateTaskState('completed') ran before the eviction await, so TaskOutput(block=true) readers released by the terminal status could still read a flushing file. The durable-output barrier now precedes the state transition. 2. Sidechain transcript: local-agent output is a symlink to the agent transcript written by the session-storage queue (recordSidechainTranscript) — no DiskTaskOutput entry exists, so evictTaskOutput alone resolved without waiting. completeAgentTask now awaits flushSessionStorage() (drains the queue and in-flight tracked chains) before evictTaskOutput and the status flip. Also per review: - Remove ORC-1337-FIX.md from the PR (P3) - Add deterministic regression test gating both barriers (completionBarrier.test.ts) — verified to fail on the previous ordering - Update call-site comments to describe the corrected sequence Testing: AgentTool + LocalAgentTask 67/67 (incl. 2 new); tsc clean. Refs: ORC-1337, gh-20236 Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> * fix(agent): await scoped output writes before completion Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> * fix: cancel agent transcript waits without delaying cleanup Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> * fix: preserve agent output ownership across session changes Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> * fix: await agent output link replacement after clear Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> * fix: preserve agent output destination and finishing input admission Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> * fix(agent): preserve output destinations for background and resumed sessions Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> * fix: preserve agent output ownership across replacement and clear Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> * fix(agent): bind retained output links to the recording producer Record successful sidechain append destinations by agent ID and require retained output links to match that producer record. Preserve the record across session clears and project overrides for retained resumes. Cover swapped agent and project targets for captured and live writes. Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> --------- Co-authored-by: jatmn Co-authored-by: Zolow-kuni <285616838+Zolow-kuni@users.noreply.github.com> --- src/Tool.ts | 2 + src/commands/clear/conversation.ts | 23 +- src/screens/REPL.tsx | 15 +- src/tasks/LocalAgentTask/LocalAgentTask.tsx | 107 ++- .../LocalAgentTask/completionBarrier.test.ts | 199 +++++ src/tasks/LocalMainSessionTask.ts | 8 +- src/tools/AgentTool/AgentTool.tsx | 8 +- src/tools/AgentTool/agentToolUtils.ts | 8 +- src/tools/AgentTool/runAgent.output.test.ts | 835 ++++++++++++++++++ src/tools/AgentTool/runAgent.ts | 61 +- src/tools/SendMessageTool/SendMessageTool.ts | 22 +- src/utils/forkedAgent.ts | 37 +- src/utils/hooks.ts | 2 +- src/utils/hooks/execAgentHook.ts | 2 +- src/utils/sessionStorage.ts | 128 ++- 15 files changed, 1362 insertions(+), 95 deletions(-) create mode 100644 src/tasks/LocalAgentTask/completionBarrier.test.ts create mode 100644 src/tools/AgentTool/runAgent.output.test.ts diff --git a/src/Tool.ts b/src/Tool.ts index 54fb556084..c25c996014 100644 --- a/src/Tool.ts +++ b/src/Tool.ts @@ -271,6 +271,8 @@ export type ToolUseContext = { ) => void setConversationId?: (id: UUID) => void agentId?: AgentId // Only set for subagents; use getSessionId() for session ID. Hooks use this to distinguish subagent calls. + /** Last submitted transcript for this producer; undefined for other IDs. */ + getAgentTranscriptPath?: (agentId: AgentId) => string | undefined agentType?: string // Subagent type name. For the main thread's --agent type, hooks fall back to getMainThreadAgentType(). /** When true, canUseTool must always be called even when hooks auto-approve. * Used by speculation for overlay file path rewriting. */ diff --git a/src/commands/clear/conversation.ts b/src/commands/clear/conversation.ts index 87e08ad45e..a42aa115b0 100644 --- a/src/commands/clear/conversation.ts +++ b/src/commands/clear/conversation.ts @@ -18,6 +18,7 @@ import type { AppState } from '../../state/AppState.js' import { isInProcessTeammateTask } from '../../tasks/InProcessTeammateTask/types.js' import { isLocalAgentTask, + initAgentTaskOutput, type LocalAgentTaskState, } from '../../tasks/LocalAgentTask/LocalAgentTask.js' import { isLocalShellTask } from '../../tasks/LocalShellTask/guards.js' @@ -43,7 +44,6 @@ import { } from '../../utils/sessionStorage.js' import { evictTaskOutput, - initTaskOutputAsSymlink, } from '../../utils/task/diskOutput.js' import { getCurrentWorktreeSession } from '../../utils/worktree.js' import { clearSessionCaches } from './caches.js' @@ -221,18 +221,27 @@ export async function clearConversation({ // Preserved local_agent tasks had their TaskOutput symlink baked against the // old session ID at spawn time, but post-clear transcript writes land under - // the new session directory (appendEntry re-reads getSessionId()). Re-point + // the new session directory (new chains capture the current session). Re-point // the symlinks so TaskOutput reads the live file instead of a frozen pre-clear // snapshot. Only re-point running tasks — finished tasks will never write // again, so re-pointing would replace a valid symlink with a dangling one. // Main-session tasks use the same per-agent path (they write via // recordSidechainTranscript to getAgentTranscriptPath), so no special case. for (const task of preservedLocalAgents) { - if (task.status !== 'running') continue - void initTaskOutputAsSymlink( - task.id, - getAgentTranscriptPath(asAgentId(task.agentId)), - ) + // Re-read after clear's awaits: completion may have finished the producer + // since preservedLocalAgents was captured, without publishing success yet. + const currentTask = getAppState?.().tasks[task.id] + if (!isLocalAgentTask(currentTask) || currentTask.status !== 'running' || currentTask.producerFinished) continue + const destination = getAgentTranscriptPath(asAgentId(task.agentId)) + const outputReady = initAgentTaskOutput(task.id, destination) + void outputReady.catch(() => {}) + // Publish before yielding so completion joins this replacement. Once + // completion starts, producerFinished prevents any further retargeting. + setAppState?.(prev => { + const current = prev.tasks[task.id] + if (!isLocalAgentTask(current) || current.status !== 'running' || current.producerFinished) return prev + return { ...prev, tasks: { ...prev.tasks, [task.id]: { ...current, outputReady } } } + }) } // Re-persist mode and worktree state after the clear so future --resume diff --git a/src/screens/REPL.tsx b/src/screens/REPL.tsx index be7a3a4306..280d4b959a 100644 --- a/src/screens/REPL.tsx +++ b/src/screens/REPL.tsx @@ -4219,11 +4219,15 @@ export function REPL({ // Callback for when user submits input while viewing a teammate's transcript const onAgentSubmit = useCallback(async (input: string, task: InProcessTeammateTaskState | LocalAgentTaskState, helpers: PromptInputHelpers) => { if (isLocalAgentTask(task)) { - appendMessageToLocalAgent(task.id, createUserMessage({ - content: input - }), setAppState); if (task.status === 'running') { - queuePendingMessage(task.id, input, setAppState); + if (!queuePendingMessage(task.id, input, setAppState)) { + addNotification({ + key: `agent-finishing-${task.id}`, + jsx: Agent is finishing its output. Please retry after it completes., + priority: 'low' + }); + return; // Keep the typed input for retry; it has not been delivered. + } } else { void resumeAgentBackground({ agentId: task.id, @@ -4241,6 +4245,9 @@ export function REPL({ }); }); } + appendMessageToLocalAgent(task.id, createUserMessage({ + content: input + }), setAppState); } else { injectUserMessageToTeammate(task.id, input, setAppState); } diff --git a/src/tasks/LocalAgentTask/LocalAgentTask.tsx b/src/tasks/LocalAgentTask/LocalAgentTask.tsx index 747d18ea32..079c2bf721 100644 --- a/src/tasks/LocalAgentTask/LocalAgentTask.tsx +++ b/src/tasks/LocalAgentTask/LocalAgentTask.tsx @@ -12,6 +12,8 @@ import { SYNTHETIC_OUTPUT_TOOL_NAME } from '../../tools/SyntheticOutputTool/Synt import { asAgentId } from '../../types/ids.js'; import type { Message } from '../../types/message.js'; import { createAbortController, createChildAbortController } from '../../utils/abortController.js'; +import { raceAbort } from '../../utils/boundedAsync.js'; +import { AbortError } from '../../utils/errors.js'; import { registerCleanup } from '../../utils/cleanupRegistry.js'; import { getToolSearchOrReadInfo } from '../../utils/collapseReadSearch.js'; import { enqueuePendingNotification } from '../../utils/messageQueueManager.js'; @@ -122,6 +124,13 @@ export type LocalAgentTaskState = TaskStateBase & { agentType: string; model?: string; abortController?: AbortController; + // Stable across progress/terminal updates, fresh for each registration. + registration?: object; + outputReady?: Promise; + // Destination captured when the producer submitted its latest transcript. + outputDestination?: string; + // No more transcript writes; completion may still be waiting for output setup. + producerFinished?: boolean; unregisterCleanup?: () => void; error?: string; result?: AgentToolResult; @@ -160,11 +169,14 @@ export function isLocalAgentTask(task: unknown): task is LocalAgentTaskState { export function isPanelAgentTask(t: unknown): t is LocalAgentTaskState { return isLocalAgentTask(t) && t.agentType !== 'main-session'; } -export function queuePendingMessage(taskId: string, msg: string, setAppState: (f: (prev: AppState) => AppState) => void): void { - updateTaskState(taskId, setAppState, task => ({ - ...task, - pendingMessages: [...task.pendingMessages, msg] - })); +export function queuePendingMessage(taskId: string, msg: string, setAppState: (f: (prev: AppState) => AppState) => void): boolean { + let accepted = false; + updateTaskState(taskId, setAppState, task => { + if (task.status !== 'running' || task.producerFinished) return task; + accepted = true; + return { ...task, pendingMessages: [...task.pendingMessages, msg] }; + }); + return accepted; } /** @@ -307,6 +319,9 @@ export function killAsyncAgent(taskId: string, setAppState: SetAppState, trace: endTime: Date.now(), evictAfter: task.retain ? undefined : Date.now() + PANEL_GRACE_MS, abortController: undefined, + outputReady: undefined, + outputDestination: undefined, + producerFinished: undefined, unregisterCleanup: undefined, selectedAgent: undefined }; @@ -420,15 +435,74 @@ export function updateAgentSummary(taskId: string, summary: string, setAppState: } } +// Only pending link mutations are retained here. Task records may be killed or +// evicted while an uncancellable filesystem operation is still running. +const pendingOutputLinks = new Map>(); +export function initAgentTaskOutput(taskId: string, destination: string): Promise { + const previous = pendingOutputLinks.get(taskId); + const ready = previous + ? previous.catch(() => {}).then(() => initTaskOutputAsSymlink(taskId, destination)) + : initTaskOutputAsSymlink(taskId, destination); + pendingOutputLinks.set(taskId, ready); + void ready.finally(() => { + if (pendingOutputLinks.get(taskId) === ready) pendingOutputLinks.delete(taskId); + }).catch(() => {}); + return ready; +} + /** * Complete an agent task with result. */ -export function completeAgentTask(result: AgentToolResult, setAppState: SetAppState): void { +export async function completeAgentTask(result: AgentToolResult, setAppState: SetAppState): Promise { const taskId = result.agentId; + let runningTask: LocalAgentTaskState | undefined; updateTaskState(taskId, setAppState, task => { - if (task.status !== 'running') { + runningTask = task.status === 'running' && !task.abortController?.signal.aborted + ? { ...task, registration: task.registration ?? {}, producerFinished: true } + : task; + return runningTask; + }); + const signal = runningTask?.abortController?.signal; + if (signal?.aborted || runningTask?.status === 'killed') throw new AbortError(); + if (runningTask?.status !== 'running') return false; + + const registration = runningTask.registration; + const ownsRegistration = () => { + let owns = false; + updateTaskState(taskId, setAppState, task => { + owns = task.registration === registration; return task; + }); + return owns; + }; + // Enqueue reconciliation before yielding: a successor must join it even if + // cancellation releases this waiter or terminal eviction removes the task. + try { + let outputReady = runningTask.outputReady; + if (runningTask.outputDestination) { + outputReady = initAgentTaskOutput(taskId, runningTask.outputDestination); + const ready = outputReady; + updateTaskState(taskId, setAppState, task => + task.registration === registration ? { ...task, outputReady: ready } : task); } + if (outputReady) await raceAbort(outputReady, signal); + if (!ownsRegistration()) return false; + await raceAbort(evictTaskOutput(taskId), signal); + } catch (error) { + // Lifecycle error handlers act by ID. An obsolete waiter must not kill or + // fail a replacement, including one that has already reached terminal state. + if (!ownsRegistration()) return false; + if (signal?.aborted) throw new AbortError(); + throw error; + } + if (!ownsRegistration()) return false; + if (signal?.aborted) throw new AbortError(); + let completed = false; + updateTaskState(taskId, setAppState, task => { + if (task.status !== 'running' || task.registration !== registration) { + return task; + } + completed = true; task.unregisterCleanup?.(); return { ...task, @@ -437,12 +511,14 @@ export function completeAgentTask(result: AgentToolResult, setAppState: SetAppSt endTime: Date.now(), evictAfter: task.retain ? undefined : Date.now() + PANEL_GRACE_MS, abortController: undefined, + outputReady: undefined, + outputDestination: undefined, + producerFinished: undefined, unregisterCleanup: undefined, selectedAgent: undefined }; }); - void evictTaskOutput(taskId); - // Note: Notification is sent by AgentTool via enqueueAgentNotification + return completed; } /** @@ -461,6 +537,9 @@ export function failAgentTask(taskId: string, error: string, setAppState: SetApp endTime: Date.now(), evictAfter: task.retain ? undefined : Date.now() + PANEL_GRACE_MS, abortController: undefined, + outputReady: undefined, + outputDestination: undefined, + producerFinished: undefined, unregisterCleanup: undefined, selectedAgent: undefined }; @@ -494,7 +573,8 @@ export function registerAsyncAgent({ parentAbortController?: AbortController; toolUseId?: string; }): LocalAgentTaskState { - void initTaskOutputAsSymlink(agentId, getAgentTranscriptPath(asAgentId(agentId))); + const outputReady = initAgentTaskOutput(agentId, getAgentTranscriptPath(asAgentId(agentId))); + void outputReady.catch(() => {}); // Completion observes initialization failure. // Create abort controller - if parent provided, create child that auto-aborts with parent const abortController = parentAbortController @@ -513,6 +593,8 @@ export function registerAsyncAgent({ selectedAgent, agentType: selectedAgent.agentType ?? 'general-purpose', abortController, + registration: {}, + outputReady, retrieved: false, lastReportedToolCount: 0, lastReportedTokenCount: 0, @@ -564,7 +646,8 @@ export function registerAgentForeground({ backgroundSignal: Promise; cancelAutoBackground?: () => void; } { - void initTaskOutputAsSymlink(agentId, getAgentTranscriptPath(asAgentId(agentId))); + const outputReady = initAgentTaskOutput(agentId, getAgentTranscriptPath(asAgentId(agentId))); + void outputReady.catch(() => {}); // Completion observes initialization failure. const abortController = createAbortController(); const unregisterCleanup = registerCleanup(async () => { killAsyncAgent(agentId, setAppState); @@ -578,6 +661,8 @@ export function registerAgentForeground({ selectedAgent, agentType: selectedAgent.agentType ?? 'general-purpose', abortController, + registration: {}, + outputReady, unregisterCleanup, retrieved: false, lastReportedToolCount: 0, diff --git a/src/tasks/LocalAgentTask/completionBarrier.test.ts b/src/tasks/LocalAgentTask/completionBarrier.test.ts new file mode 100644 index 0000000000..7a115f7f7e --- /dev/null +++ b/src/tasks/LocalAgentTask/completionBarrier.test.ts @@ -0,0 +1,199 @@ +import { REAL_PROVIDER_TEST_CHILD_ENV, REAL_PROVIDER_TEST_TIMEOUT_MS, runTestFileWithRealProviders } from '../../test/providerModuleIsolation.js' +import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' + +import { readFileSync } from 'node:fs' +import ts from 'typescript' +import { asAgentId } from '../../types/ids.js' +import type { ToolUseContext } from '../../Tool.js' +import type { AppState } from '../../state/AppState.js' +import type { AgentToolResult } from '../../tools/AgentTool/agentToolUtils.js' +import type { AgentDefinition } from '../../tools/AgentTool/loadAgentsDir.js' +import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js' +import { AbortError } from '../../utils/errors.js' +import type { LocalAgentTaskState } from './LocalAgentTask.js' + +function deferred() { + let resolve!: () => void + const promise = new Promise(res => { resolve = res }) + return { promise, resolve } +} + +const actualDiskOutput = { ...await import('../../utils/task/diskOutput.js') } +const settle = () => new Promise(res => setTimeout(res, 0)) + +// Use the existing clean-child harness: other agent tests replace these +// production modules globally, and this test needs real registration/storage. +if (process.env[REAL_PROVIDER_TEST_CHILD_ENV] !== '1') { + test('agent output regression cases in a clean process', () => + runTestFileWithRealProviders(import.meta.path), REAL_PROVIDER_TEST_TIMEOUT_MS + 5_000) +} else { +beforeEach(() => acquireSharedMutationLock('tasks/LocalAgentTask/completionBarrier.test.ts')) +afterEach(() => { + mock.module('../../utils/task/diskOutput.js', () => actualDiskOutput) + releaseSharedMutationLock() +}) + +function state() { + let appState = { tasks: {}, agentNameRegistry: new Map() } as AppState + return { + getAppState: () => appState, + getTask: (id: string) => appState.tasks[id] as LocalAgentTaskState, + setAppState: (updater: (prev: AppState) => AppState) => { appState = updater(appState) }, + } +} + +describe('local-agent completion output barrier', () => { + for (const foreground of [false, true]) { + test(`waits for ${foreground ? 'foreground' : 'async'} output initialization and eviction`, async () => { + const link = deferred() + const eviction = deferred() + const evict = mock(() => eviction.promise) + mock.module('../../utils/task/diskOutput.js', () => ({ + ...actualDiskOutput, + initTaskOutputAsSymlink: () => link.promise.then(() => '/output'), + evictTaskOutput: evict, + })) + const { registerAsyncAgent, registerAgentForeground, completeAgentTask } = await import('./LocalAgentTask.js') + const { getTask, getAppState, setAppState } = state() + const agentId = `barrier-${foreground}` + const register = foreground ? registerAgentForeground : registerAsyncAgent + register({ agentId, description: 'test', prompt: 'test', selectedAgent: { agentType: 'test' } as AgentDefinition, setAppState }) + getAppState().agentNameRegistry.set('target', asAgentId(agentId)) + const { SendMessageTool } = await import('../../tools/SendMessageTool/SendMessageTool.js') + const send = (message: string) => SendMessageTool.call( + { to: 'target', message }, { getAppState, setAppState } as unknown as ToolUseContext, + async () => ({ behavior: 'allow' }), {} as never, + ) + const { drainPendingMessages } = await import('./LocalAgentTask.js') + expect((await send('live input')).data.success).toBe(true) + expect(drainPendingMessages(agentId, getAppState, setAppState)).toEqual(['live input']) + // Execute the actual REPL callback with simulated UI dependencies, avoiding + // a full terminal mount while checking the admission/clear-input decision. + const source = readFileSync(new URL('../../screens/REPL.tsx', import.meta.url), 'utf8') + const start = source.indexOf('const onAgentSubmit = useCallback') + const end = source.indexOf('// Handlers for auto-run', start) + const callback = ts.transpileModule(source.slice(start, end) + '\nreturn onAgentSubmit;', { + compilerOptions: { jsx: ts.JsxEmit.React, target: ts.ScriptTarget.ES2022 }, + }).outputText + const localAgents = await import('./LocalAgentTask.js') + const cleared = mock(() => {}) + const notified = mock(() => {}) + const resumed = mock(async () => {}) + const dependencies = { + ...localAgents, useCallback: (fn: unknown) => fn, setAppState, + setInputValue: cleared, getToolUseContext: () => ({}), canUseTool: () => {}, mainLoopModel: 'test', + addNotification: notified, resumeAgentBackground: resumed, messagesRef: { current: [] }, + createUserMessage: (args: unknown) => args, logForDebugging: () => {}, errorMessage: String, + injectUserMessageToTeammate: () => {}, React: { createElement: (...args: unknown[]) => args }, Text: 'Text', + } + const submit = new Function(...Object.keys(dependencies), callback)(...Object.values(dependencies)) + const assertRefusal = async () => { + const result = await send('FOLLOW-UP') + expect(result.data.success).toBe(false) + expect(result.data.message).toContain('retry') + await submit('typed follow-up', getTask(agentId), { setCursorOffset: cleared, clearBuffer: cleared }) + expect(notified).toHaveBeenCalled() + expect(cleared).not.toHaveBeenCalled() + expect(resumed).not.toHaveBeenCalled() + expect(getTask(agentId).pendingMessages).toEqual([]) + expect(getTask(agentId).messages ?? []).toEqual([]) + } + const result = { agentId, content: [] } as unknown as AgentToolResult + const completion = completeAgentTask(result, setAppState) + try { + await settle() + expect(getTask(agentId).status).toBe('running') + expect(evict).not.toHaveBeenCalled() + await assertRefusal() + link.resolve() + await settle() + expect(evict).toHaveBeenCalledTimes(1) + expect(getTask(agentId).status).toBe('running') + await assertRefusal() + eviction.resolve() + await completion + expect(getTask(agentId).status).toBe('completed') + expect(getTask(agentId).result).toBe(result) + expect(getTask(agentId).evictAfter).toBeGreaterThan(Date.now()) + await submit('retry after completion', getTask(agentId), { setCursorOffset: cleared, clearBuffer: cleared }) + expect(resumed).toHaveBeenCalledTimes(1) + expect(cleared).toHaveBeenCalled() + } finally { + link.resolve() + eviction.resolve() + await completion + } + }) + } + + test('cancellation interrupts pending output readiness instead of reporting success', async () => { + const link = deferred() + mock.module('../../utils/task/diskOutput.js', () => ({ + ...actualDiskOutput, + initTaskOutputAsSymlink: () => link.promise.then(() => '/output'), + evictTaskOutput: async () => {}, + })) + const { registerAsyncAgent, completeAgentTask, killAsyncAgent } = await import('./LocalAgentTask.js') + const { getTask, setAppState } = state() + const agentId = 'barrier-cancel' + registerAsyncAgent({ agentId, description: 'test', prompt: 'test', selectedAgent: { agentType: 'test' } as AgentDefinition, setAppState }) + const completion = completeAgentTask({ agentId, content: [] } as unknown as AgentToolResult, setAppState) + const outcome = completion.then(() => undefined, error => error) + try { + killAsyncAgent(agentId, setAppState) + await settle() + expect(getTask(agentId).status).toBe('killed') + expect(await Promise.race([outcome, Promise.resolve('still waiting')])).toBeInstanceOf(AbortError) + } finally { + link.resolve() + await outcome + } + }) + for (const foreground of [false, true]) { + for (const outcome of ['resolve', 'reject', 'abort'] as const) { + test(`obsolete ${foreground ? 'foreground' : 'async'} completion cannot own a replacement after ${outcome}`, async () => { + const link = deferred() + const failure = new Error('old initialization failed') + let count = 0 + mock.module('../../utils/task/diskOutput.js', () => ({ + ...actualDiskOutput, + initTaskOutputAsSymlink: () => ++count === 1 ? link.promise.then(() => { + if (outcome === 'reject') throw failure + return '/old' + }) : Promise.resolve('/new'), + evictTaskOutput: async () => {}, + })) + const { registerAsyncAgent, registerAgentForeground, completeAgentTask, killAsyncAgent } = await import('./LocalAgentTask.js') + const { getTask, setAppState } = state() + const agentId = `replace-${foreground}-${outcome}` + const register = foreground ? registerAgentForeground : registerAsyncAgent + const params = { agentId, description: 'old', prompt: 'old', selectedAgent: { agentType: 'test' } as AgentDefinition, setAppState } + register(params) + const old = getTask(agentId) + const completion = completeAgentTask({ agentId, content: [] } as unknown as AgentToolResult, setAppState).catch(error => error) + register({ ...params, description: 'replacement' }) + const replacement = getTask(agentId) + try { + if (outcome === 'abort') old.abortController!.abort() + link.resolve() + expect(await completion).toBe(false) + expect(getTask(agentId).status).toBe('running') + expect(getTask(agentId).result).toBeUndefined() + expect(getTask(agentId).abortController).toBe(replacement.abortController) + expect(getTask(agentId).unregisterCleanup).toBe(replacement.unregisterCleanup) + killAsyncAgent(agentId, setAppState) + expect(replacement.abortController!.signal.aborted).toBe(true) + } finally { + link.resolve() + await completion + await replacement.outputReady + old.unregisterCleanup?.() + killAsyncAgent(agentId, setAppState) + } + }) + } + } + +}) + +} diff --git a/src/tasks/LocalMainSessionTask.ts b/src/tasks/LocalMainSessionTask.ts index 0c87ab76d2..860e5e9dbe 100644 --- a/src/tasks/LocalMainSessionTask.ts +++ b/src/tasks/LocalMainSessionTask.ts @@ -475,8 +475,10 @@ export function startBackgroundSession({ // Persist the pre-backgrounding conversation to the task's isolated // transcript so TaskOutput shows context immediately. Subsequent - // messages are written incrementally below. - void recordSidechainTranscript(messages, taskId).catch(err => + // messages are written incrementally below. Unlike regular agents, this + // lifecycle follows clear's live output link and does not reconcile a + // submission-time destination on completion. Keep its late path selection. + void recordSidechainTranscript(messages, taskId, undefined, { captureDestination: false }).catch(err => logForDebugging(`bg-session initial transcript write failed: ${err}`), ) @@ -516,7 +518,7 @@ export function startBackgroundSession({ // Per-message write (matches runAgent.ts pattern) — gives live // TaskOutput progress and keeps the transcript file current even if // /clear re-links the symlink mid-run. - void recordSidechainTranscript([event], taskId, lastRecordedUuid).catch( + void recordSidechainTranscript([event], taskId, lastRecordedUuid, { captureDestination: false }).catch( err => logForDebugging(`bg-session transcript write failed: ${err}`), ) lastRecordedUuid = event.uuid diff --git a/src/tools/AgentTool/AgentTool.tsx b/src/tools/AgentTool/AgentTool.tsx index 069058254b..9a057944ab 100644 --- a/src/tools/AgentTool/AgentTool.tsx +++ b/src/tools/AgentTool/AgentTool.tsx @@ -1151,11 +1151,9 @@ export const AgentTool = buildTool({ } const agentResult = finalizeAgentTool(agentMessages, backgroundedTaskId, metadata); - // Mark task completed FIRST so TaskOutput(block=true) - // unblocks immediately. classifyHandoffIfNeeded and - // cleanupWorktreeIfNeeded can hang — they must not gate - // the status transition (gh-20236). - completeAsyncAgent(agentResult, rootSetAppState); + // Transcript writes are awaited by runAgent; completion joins the output link. + // Keep notification embellishments after the terminal state (gh-20236). + if (!await completeAsyncAgent(agentResult, rootSetAppState)) return; // Extract text from agent result content for the notification let finalMessage = extractTextContent(agentResult.content, '\n'); diff --git a/src/tools/AgentTool/agentToolUtils.ts b/src/tools/AgentTool/agentToolUtils.ts index 64e4a473d9..299e1c5872 100644 --- a/src/tools/AgentTool/agentToolUtils.ts +++ b/src/tools/AgentTool/agentToolUtils.ts @@ -615,11 +615,9 @@ export async function runAsyncAgentLifecycle({ const agentResult = finalizeAgentTool(agentMessages, taskId, metadata) - // Mark task completed FIRST so TaskOutput(block=true) unblocks - // immediately. classifyHandoffIfNeeded (API call) and getWorktreeResult - // (git exec) are notification embellishments that can hang — they must - // not gate the status transition (gh-20236). - completeAsyncAgent(agentResult, rootSetAppState) + // Transcript writes are awaited by runAgent; completion joins the output link. + // Keep notification embellishments after the terminal state (gh-20236). + if (!await completeAsyncAgent(agentResult, rootSetAppState)) return let finalMessage = extractTextContent(agentResult.content, '\n') diff --git a/src/tools/AgentTool/runAgent.output.test.ts b/src/tools/AgentTool/runAgent.output.test.ts new file mode 100644 index 0000000000..b5f9d782dc --- /dev/null +++ b/src/tools/AgentTool/runAgent.output.test.ts @@ -0,0 +1,835 @@ +import { REAL_PROVIDER_TEST_CHILD_ENV, REAL_PROVIDER_TEST_TIMEOUT_MS, runTestFileWithRealProviders } from '../../test/providerModuleIsolation.js' +import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test' +import { randomUUID } from 'node:crypto' +import { getSessionId, getSessionProjectDir, switchSession } from '../../bootstrap/state.js' +import { getDefaultAppState } from '../../state/AppStateStore.js' +import { clearConversation } from '../../commands/clear/conversation.js' +import { runForkedAgent } from '../../utils/forkedAgent.js' +import { AbortError } from '../../utils/errors.js' +import { spawn } from 'node:child_process' +import type { ShellCommand } from '../../utils/ShellCommand.js' +import { TaskOutput } from '../../utils/task/TaskOutput.js' +import { registerForeground } from '../../tasks/LocalShellTask/LocalShellTask.js' +import { mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' +import { dirname, join, sep } from 'node:path' +import { tmpdir } from 'node:os' +import type { AppState } from '../../state/AppState.js' +import type { ToolUseContext } from '../../Tool.js' +import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js' +import { asAgentId, asSessionId } from '../../types/ids.js' +import { createAssistantMessage, createUserMessage } from '../../utils/messages.js' +import { FileStateCache, createFileStateCacheWithSizeLimit, READ_FILE_STATE_CACHE_SIZE } from '../../utils/fileStateCache.js' +import { setClaudeConfigHomeDirForTesting } from '../../utils/envUtils.js' +import { resetSettingsCache } from '../../utils/settings/settingsCache.js' +import { getAgentTranscript, readAgentMetadata, writeAgentMetadata, setAgentTranscriptSubdir, getAgentTranscriptPath, getProjectDir, resetSessionFilePointer, recordSidechainTranscript, recordTranscript, setInternalEventWriter, resetProjectForTesting, flushSessionStorage } from '../../utils/sessionStorage.js' +import { withTranscriptFileLock } from '../../utils/transcriptFileLock.js' +import { registerAsyncAgent, registerAgentForeground, backgroundAgentTask, completeAgentTask, killAsyncAgent } from '../../tasks/LocalAgentTask/LocalAgentTask.js' +import { getTaskOutputPath, _clearOutputsForTest } from '../../utils/task/diskOutput.js' +import { clearPendingNotifications, getPendingNotificationsSnapshot } from '../../utils/messageQueueManager.js' +import { runAsyncAgentLifecycle } from './agentToolUtils.js' +import { runAgent } from './runAgent.js' +import { executeStopHooks } from '../../utils/hooks.js' +import { addSessionHook } from '../../utils/hooks/sessionHooks.js' +import { resumeAgentBackground } from './resumeAgent.js' +import { startBackgroundSession } from '../../tasks/LocalMainSessionTask.js' +import type { AgentDefinition } from './loadAgentsDir.js' + +const actualStorage = { ...await import('../../utils/sessionStorage.js') } +const actualDiskOutput = { ...await import('../../utils/task/diskOutput.js') } +const actualUuid = { ...await import('../../utils/uuid.js') } +let originalSession: ReturnType +let originalProjectDir: ReturnType +let oldSimple: string | undefined +const actualGit = { ...await import('../../utils/git.js') } +const actualQuery = { ...await import('../../query.js') } +let configDir: string +let oldPersistence: string | undefined + +function deferred() { + let resolve!: () => void + const promise = new Promise(res => { resolve = res }) + return { promise, resolve } +} + +async function waitFor(condition: () => boolean) { + const deadline = Date.now() + 2000 + while (!condition()) { + if (Date.now() > deadline) throw new Error('Timed out waiting for agent lifecycle checkpoint') + await new Promise(resolve => setTimeout(resolve, 5)) + } +} + +// Use the existing clean-child harness: other agent tests replace these +// production modules globally, and this test needs real registration/storage. +if (process.env[REAL_PROVIDER_TEST_CHILD_ENV] !== '1') { + test('agent output regression cases in a clean process', () => + runTestFileWithRealProviders(import.meta.path), REAL_PROVIDER_TEST_TIMEOUT_MS + 5_000) +} else { +beforeEach(async () => { + await acquireSharedMutationLock('tools/AgentTool/runAgent.output.test.ts') + originalSession = getSessionId() + originalProjectDir = getSessionProjectDir() + switchSession(originalSession, undefined) + oldSimple = process.env.CLAUDE_CODE_SIMPLE + process.env.CLAUDE_CODE_SIMPLE = '1' + oldPersistence = process.env.TEST_ENABLE_SESSION_PERSISTENCE + process.env.TEST_ENABLE_SESSION_PERSISTENCE = 'true' + configDir = await mkdtemp(join(tmpdir(), 'agent-output-')) + setClaudeConfigHomeDirForTesting(configDir) + resetSettingsCache() + resetProjectForTesting() + getProjectDir.cache.clear?.() + clearPendingNotifications() + expect(getAgentTranscriptPath(asAgentId('fixture-check')).startsWith(configDir + sep)).toBe(true) +}) +afterEach(async () => { + try { + mock.module('../../utils/sessionStorage.js', () => actualStorage) + mock.module('../../query.js', () => actualQuery) + mock.module('../../utils/git.js', () => actualGit) + mock.module('../../utils/uuid.js', () => actualUuid) + mock.module('../../utils/task/diskOutput.js', () => actualDiskOutput) + await flushSessionStorage() + await _clearOutputsForTest() + clearPendingNotifications() + resetProjectForTesting() + switchSession(originalSession, originalProjectDir) + getProjectDir.cache.clear?.() + setClaudeConfigHomeDirForTesting(undefined) + if (oldSimple === undefined) delete process.env.CLAUDE_CODE_SIMPLE + else process.env.CLAUDE_CODE_SIMPLE = oldSimple + resetSettingsCache() + if (oldPersistence === undefined) delete process.env.TEST_ENABLE_SESSION_PERSISTENCE + else process.env.TEST_ENABLE_SESSION_PERSISTENCE = oldPersistence + await rm(configDir, { recursive: true, force: true }) + } finally { releaseSharedMutationLock() } +}) + +for (const phase of ['success', 'initial', 'incremental'] as const) { + const cancel = phase !== 'success' + test(`agent ${cancel ? `stops during its ${phase}` : 'completes after its'} real transcript append while unrelated persistence is blocked`, async () => { + const remoteEntered = deferred() + const remoteRelease = deferred() + setInternalEventWriter(async () => { remoteEntered.resolve(); await remoteRelease.promise }) + const foreground = recordTranscript([createUserMessage({ content: 'unrelated foreground' })]) + await remoteEntered.promise + const agentId = asAgentId(`output-${phase}`) + const transcript = getAgentTranscriptPath(agentId) + await mkdir(dirname(transcript), { recursive: true }) + const locked = deferred() + const release = deferred() + const holdTranscript = () => withTranscriptFileLock(transcript, async () => { locked.resolve(); await release.promise }) + let holder: Promise | undefined + if (phase !== 'incremental') { + holder = holdTranscript() + await locked.promise + } + const submitted = deferred() + let branchCalls = 0 + mock.module('../../utils/git.js', () => ({ ...actualGit, getBranch: async () => { + // This checkpoint is inside the submitted storage chain, after metadata + // persistence. The held file lock still prevents its physical append. + if (++branchCalls === (phase === 'incremental' ? 2 : 1)) submitted.resolve() + return 'test' + } })) + let shell: ShellCommand | undefined + let childExit: Promise | undefined + const shellId = `shell-${phase}` + let queried = false + mock.module('../../query.js', () => ({ + ...actualQuery, + query: async function* () { + queried = true + if (phase === 'incremental') { + const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { stdio: 'ignore' }) + childExit = new Promise((resolve, reject) => { child.once('exit', () => resolve()); child.once('error', reject) }) + // A minimal shell adapter isolates this regression from ShellCommand's + // existing cleanup/exit race; the child and agent cleanup are real. + shell = { + status: 'backgrounded', + taskOutput: new TaskOutput(shellId, null), + background: () => true, + kill: () => { child.kill(); shell!.status = 'killed' }, + cleanup: () => {}, + result: childExit.then(() => ({ stdout: '', stderr: '', code: 0, interrupted: true })), + } + registerForeground({ command: 'test child', description: 'test child', shellCommand: shell, agentId }, setAppState) + holder = holdTranscript() + await locked.promise + } + yield createAssistantMessage({ content: 'finished output' }) + return { reason: 'completed' } + }, + })) + const selectedAgent = { agentType: 'general-purpose', source: 'built-in', baseDir: 'built-in', whenToUse: 'test', getSystemPrompt: () => 'test' } as AgentDefinition + let appState = { + tasks: {}, todos: { [agentId]: [] }, speculation: { status: 'idle' }, mainLoopModel: 'test-model', mainLoopModelForSession: 'test-model', + toolPermissionContext: { mode: 'default', additionalWorkingDirectories: new Map(), alwaysAllowRules: {}, alwaysDenyRules: {}, alwaysAskRules: {} }, + mcp: { clients: [], tools: [] }, + } as unknown as AppState + const setAppState = (updater: (prev: AppState) => AppState) => { appState = updater(appState) } + const task = registerAsyncAgent({ agentId, description: 'test output', prompt: 'initial prompt', selectedAgent, setAppState }) + const context = { + options: { commands: [], tools: [], mainLoopModel: 'test-model', thinkingConfig: { type: 'disabled' }, mcpClients: [], mcpResources: {}, agentDefinitions: { activeAgents: [], allAgents: [] } }, + abortController: task.abortController, + readFileState: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE), + messages: [], getAppState: () => appState, setAppState, + setInProgressToolUseIDs: () => {}, setResponseLength: () => {}, updateFileHistoryState: () => {}, updateAttributionState: () => {}, + } as unknown as ToolUseContext + const lifecycle = runAsyncAgentLifecycle({ + taskId: agentId, abortController: task.abortController!, + makeStream: () => runAgent({ + agentDefinition: selectedAgent, promptMessages: [createUserMessage({ content: 'initial prompt' }), createUserMessage({ content: 'remaining initial history' })], + toolUseContext: context, canUseTool: async () => ({ behavior: 'allow' }), + isAsync: true, querySource: 'agent:builtin:general-purpose', availableTools: [], + override: { agentId, abortController: task.abortController, userContext: {}, systemContext: {}, systemPrompt: ['test'] as never }, + }), + metadata: { prompt: 'initial prompt', resolvedAgentModel: 'test-model', isBuiltInAgent: true, startTime: Date.now(), agentType: 'general-purpose', isAsync: true }, + description: 'test output', toolUseContext: context, rootSetAppState: setAppState, + agentIdForCleanup: agentId, enableSummarization: false, getWorktreeResult: async () => ({}), + }) + let cancellationDeadline: ReturnType | undefined + try { + await locked.promise + await submitted.promise + expect(appState.tasks[agentId]?.status).toBe('running') + expect(queried).toBe(phase === 'incremental') + if (cancel) { + killAsyncAgent(agentId, setAppState) + expect(appState.tasks[agentId]?.status).toBe('killed') + // Keep storage locked until stopped handling and runner cleanup finish. + await Promise.race([ + lifecycle, + new Promise((_, reject) => { + cancellationDeadline = setTimeout(() => reject(new Error('cancellation is still waiting for transcript storage')), 2_000) + }), + ]) + expect(appState.todos).not.toHaveProperty(agentId) + if (phase === 'incremental') { + expect(appState.tasks[shellId]?.status).toBe('killed') + expect(shell?.status).toBe('killed') + await childExit + } + } else { + release.resolve() + await lifecycle + } + expect(appState.tasks[agentId]?.status).toBe(cancel ? 'killed' : 'completed') + const notifications = getPendingNotificationsSnapshot().map(item => item.value).join('\n') + expect(notifications).toContain(`${cancel ? 'killed' : 'completed'}`) + if (cancel) expect(notifications).not.toContain('completed') + else { + const output = await readFile(getTaskOutputPath(agentId), 'utf8') + expect(output).toContain('initial prompt') + expect(output).toContain('finished output') + expect(output.indexOf('initial prompt')).toBeLessThan(output.indexOf('finished output')) + } + } finally { + clearTimeout(cancellationDeadline) + release.resolve() + remoteRelease.resolve() + if (shell?.status === 'running' || shell?.status === 'backgrounded') { + shell.kill() + shell.cleanup() + } + await childExit + await holder + await foreground + await lifecycle + } + // Cancellation releases the runner, not the tracked producer or its messages. + await flushSessionStorage() + const persisted = await readFile(transcript, 'utf8') + expect(persisted).toContain('initial prompt') + expect(persisted).toContain('remaining initial history') + if (phase === 'incremental') expect(persisted).toContain('finished output') + }) +} + + +test('replacement runs join earlier transcript chains for the same agent only', async () => { + const entered = deferred() + const release = deferred() + const replacementEntered = deferred() + const replacementRelease = deferred() + let branchCalls = 0 + mock.module('../../utils/git.js', () => ({ + ...actualGit, + getBranch: async () => { + branchCalls++ + if (branchCalls === 1) { entered.resolve(); await release.promise } + if (branchCalls === 3) { replacementEntered.resolve(); await replacementRelease.promise } + return 'test' + }, + })) + const earlier = recordSidechainTranscript([createUserMessage({ content: 'earlier history' })], 'same-agent') + await entered.promise + let replacementFinished = false + const replacement = recordSidechainTranscript([createAssistantMessage({ content: 'replacement answer' })], 'same-agent') + .then(() => { replacementFinished = true }) + try { + await recordSidechainTranscript([createUserMessage({ content: 'other output' })], 'other-agent') + expect(await readFile(getAgentTranscriptPath(asAgentId('other-agent')), 'utf8')).toContain('other output') + await new Promise(resolve => setTimeout(resolve, 25)) + expect(replacementFinished).toBe(false) + let flushed = false + const flush = flushSessionStorage().then(() => { flushed = true }) + release.resolve() + await replacementEntered.promise + await new Promise(resolve => setTimeout(resolve, 0)) + expect(flushed).toBe(false) + replacementRelease.resolve() + await Promise.all([earlier, replacement, flush]) + const output = await readFile(getAgentTranscriptPath(asAgentId('same-agent')), 'utf8') + expect(output.indexOf('earlier history')).toBeLessThan(output.indexOf('replacement answer')) + } finally { + release.resolve() + replacementRelease.resolve() + await Promise.all([earlier, replacement]) + } +}) + +// Retain the standalone physical-readiness oracle in this real-module process. +test('sidechain write resolves only after its file is readable, without a session flush', async () => { + const agentId = asAgentId('durable-agent') + const path = getAgentTranscriptPath(agentId) + const release = deferred() + const locked = deferred() + let holder: Promise | undefined + let writing: Promise | undefined + try { + await mkdir(dirname(path), { recursive: true }) + holder = withTranscriptFileLock(path, async () => { locked.resolve(); await release.promise }) + await locked.promise + let settled = false + writing = recordSidechainTranscript([createUserMessage({ content: 'durable output' })], agentId) + .then(() => { settled = true }) + await new Promise(resolve => setTimeout(resolve, 150)) + expect(settled).toBe(false) + release.resolve() + await writing + expect(await readFile(path, 'utf8')).toContain('durable output') + } finally { + release.resolve() + await holder + await writing + } +}) + +test('live-destination sidechain chains preserve submission order', async () => { + const agentId = asAgentId('live-chain-order') + const entered = deferred() + const release = deferred() + let calls = 0 + mock.module('../../utils/git.js', () => ({ ...actualGit, getBranch: async () => { + if (++calls === 1) { entered.resolve(); await release.promise } + return 'test' + } })) + const first = createUserMessage({ content: 'first live chain' }) + const second = createAssistantMessage({ content: 'second live chain' }) + const writing = recordSidechainTranscript([first], agentId, null, { captureDestination: false }) + await entered.promise + const queued = recordSidechainTranscript([second], agentId, first.uuid, { captureDestination: false }) + try { + release.resolve() + await Promise.all([writing, queued]) + const entries = (await readFile(getAgentTranscriptPath(agentId), 'utf8')).trim().split('\n').map(line => JSON.parse(line)) + expect(entries.map(entry => entry.uuid)).toEqual([first.uuid, second.uuid]) + } finally { + release.resolve() + await Promise.all([writing, queued]) + } +}) + +test('queued sidechain chains keep their destination and session across clear pointer reset', async () => { + const agentId = asAgentId('session-boundary') + const sessionA = getSessionId() + const pathA = getAgentTranscriptPath(agentId) + const first = createUserMessage({ content: 'A initial' }) + const tail = createAssistantMessage({ content: 'A tail' }) + const answer = createAssistantMessage({ content: 'A queued answer' }) + const release = deferred() + const locked = deferred() + let holder: Promise | undefined + let initial: Promise | undefined + let queued: Promise | undefined + try { + await mkdir(dirname(pathA), { recursive: true }) + holder = withTranscriptFileLock(pathA, async () => { locked.resolve(); await release.promise }) + await locked.promise + initial = recordSidechainTranscript([first, tail], agentId) + queued = recordSidechainTranscript([answer], agentId, tail.uuid) + await new Promise(resolve => setTimeout(resolve, 150)) + switchSession(asSessionId(randomUUID())) + await resetSessionFilePointer() + const sessionB = getSessionId() + const pathB = getAgentTranscriptPath(agentId) + await recordSidechainTranscript([createUserMessage({ content: 'B new output' })], agentId) + expect(await readFile(pathB, 'utf8')).toContain('B new output') + release.resolve() + await Promise.all([initial, queued]) + const a = (await readFile(pathA, 'utf8')).trim().split('\n').map(line => JSON.parse(line)) + const b = (await readFile(pathB, 'utf8')).trim().split('\n').map(line => JSON.parse(line)) + expect(a.map(entry => entry.uuid)).toEqual([first.uuid, tail.uuid, answer.uuid]) + expect(a.map(entry => entry.sessionId)).toEqual([sessionA, sessionA, sessionA]) + expect(a.map(entry => entry.parentUuid)).toEqual([null, first.uuid, tail.uuid]) + expect(b).toHaveLength(1) + expect(b[0].sessionId).toBe(sessionB) + } finally { + release.resolve() + await Promise.all([holder, initial, queued]) + } +}) + +for (const foreground of [false, true]) { + test(`clear preserves finished ${foreground ? 'foreground' : 'async'} output while initialization is pending`, async () => { + let state = getDefaultAppState() + const setAppState = (update: (prev: AppState) => AppState) => { state = update(state) } + const agentId = asAgentId(`finished-${foreground}`) + const activeId = asAgentId(`active-${foreground}`) + const release = deferred() + const links: Promise[] = [] + const liveLinks: Promise[] = [] + const releaseRetarget = deferred() + const releaseInitial = deferred() + mock.module('../../utils/task/diskOutput.js', () => ({ + ...actualDiskOutput, + initTaskOutputAsSymlink: (id: string, path: string) => { + const ready = id === agentId ? (links[0] ?? release.promise) : liveLinks.length ? releaseRetarget.promise : foreground ? releaseInitial.promise : Promise.resolve() + const link = ready.then(() => actualDiskOutput.initTaskOutputAsSymlink(id, path)) + if (id === agentId) links.push(link) + else liveLinks.push(link) + return link + }, + })) + const selectedAgent = { agentType: 'test' } as AgentDefinition + const register = foreground ? registerAgentForeground : registerAsyncAgent + register({ agentId, description: 'finished', prompt: 'test', selectedAgent, setAppState }) + if (foreground) backgroundAgentTask(agentId, () => state, setAppState) + const active = registerAsyncAgent({ agentId: activeId, description: 'active', prompt: 'test', selectedAgent, setAppState }) + if (!foreground) await active.outputReady + await recordSidechainTranscript([createUserMessage({ content: 'finished readable output' })], agentId) + const finish = () => completeAgentTask({ agentId, content: [] } as never, setAppState) + let completion = foreground ? undefined : finish() + let activeCompletion: Promise | undefined + try { + expect(state.tasks[agentId]?.status).toBe('running') + await clearConversation({ setMessages: () => { completion ??= finish() }, readFileState: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE), getAppState: () => state, setAppState }) + release.resolve() + await completion + await Promise.all(links) + expect(state.tasks[agentId]?.status).toBe('completed') + expect(await readFile(getTaskOutputPath(agentId), 'utf8')).toContain('finished readable output') + expect(links).toHaveLength(1) + // A genuine live producer must still migrate and expose new-session output. + await recordSidechainTranscript([createUserMessage({ content: 'new active output' })], activeId) + activeCompletion = completeAgentTask({ agentId: activeId, content: [] } as never, setAppState) + await new Promise(resolve => setTimeout(resolve, 20)) + expect(state.tasks[activeId]?.status).toBe('running') + releaseRetarget.resolve() + if (foreground) { + await new Promise(resolve => setTimeout(resolve, 20)) + expect(state.tasks[activeId]?.status).toBe('running') + } + releaseInitial.resolve() + await activeCompletion + expect(state.tasks[activeId]?.status).toBe('completed') + expect(liveLinks).toHaveLength(2) + expect(await readFile(getTaskOutputPath(activeId), 'utf8')).toContain('new active output') + } finally { + release.resolve() + await completion + await Promise.all(links) + releaseRetarget.resolve() + releaseInitial.resolve() + await activeCompletion + await Promise.all(liveLinks) + killAsyncAgent(activeId, setAppState) + } + }) +} + +for (const foreground of [false, true]) { + for (const continuesInB of [false, true]) { + test(`final ${foreground ? 'foreground' : 'async'} write spanning clear stays readable, continued=${continuesInB}`, async () => { + const agentId = asAgentId(`final-clear-${foreground}-${continuesInB}`) + let state = getDefaultAppState() + const setAppState = (update: (prev: AppState) => AppState) => { state = update(state) } + const entered = deferred() + const release = deferred() + let holdNextBranch = false + mock.module('../../utils/git.js', () => ({ ...actualGit, getBranch: async () => { + if (holdNextBranch) { holdNextBranch = false; entered.resolve(); await release.promise } + return 'test' + } })) + let hookPath: string | undefined + let verifierPath: string | undefined + let verifierRules: readonly string[] | undefined + mock.module('../../query.js', () => ({ ...actualQuery, query: async function* (params: Parameters[0]) { + if (params.querySource === 'hook_agent') { + const text = JSON.stringify(params.messages) + const inputText = params.messages[0] + if (inputText?.type !== 'user' || typeof inputText.message.content !== 'string') throw new Error(`unexpected hook input ${text}`) + hookPath = JSON.parse(inputText.message.content).agent_transcript_path + verifierPath = params.systemPrompt.join('\n').match(/available at: ([^\n]+)/)?.[1] + verifierRules = params.toolUseContext.getAppState().toolPermissionContext.alwaysAllowRules.session + yield { type: 'attachment', attachment: { type: 'structured_output', data: { ok: true } }, uuid: randomUUID(), timestamp: new Date().toISOString() } as never + return { reason: 'completed' } + } + holdNextBranch = true + yield createAssistantMessage({ content: 'final A answer' }) + if (continuesInB) yield createAssistantMessage({ content: 'continued B answer' }) + addSessionHook(setAppState, agentId, 'SubagentStop', '', { type: 'agent', prompt: '$ARGUMENTS', model: 'test-model' }) + const simple = process.env.CLAUDE_CODE_SIMPLE + delete process.env.CLAUDE_CODE_SIMPLE + try { + for await (const _result of executeStopHooks(undefined, params.toolUseContext.abortController.signal, 1000, false, agentId, params.toolUseContext, params.messages)) { /* consume actual hook dispatch */ } + } finally { process.env.CLAUDE_CODE_SIMPLE = simple } + return { reason: 'completed' } + } })) + const selectedAgent = { agentType: continuesInB ? 'general-purpose' : 'retained-agent', source: continuesInB ? 'built-in' : 'userSettings', baseDir: 'built-in', whenToUse: 'test', getSystemPrompt: () => 'test' } as AgentDefinition + const transcriptSubdir = foreground && !continuesInB ? join('workflows', 'retained') : undefined + if (transcriptSubdir) { + switchSession(getSessionId(), getProjectDir(join(configDir, 'resumed-project'))) + setAgentTranscriptSubdir(agentId, transcriptSubdir) + } + const register = foreground ? registerAgentForeground : registerAsyncAgent + register({ agentId, description: 'clear output', prompt: 'initial prompt', selectedAgent, setAppState }) + const task = state.tasks[agentId] + if (task?.type !== 'local_agent') throw new Error('expected registered local agent') + if (foreground) backgroundAgentTask(agentId, () => state, setAppState) + const pathA = getAgentTranscriptPath(agentId) + const context = { + options: { commands: [], tools: [], mainLoopModel: 'test-model', thinkingConfig: { type: 'disabled' }, mcpClients: [], mcpResources: {}, agentDefinitions: { activeAgents: [], allAgents: [] } }, + abortController: task.abortController, readFileState: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE), + messages: [], getAppState: () => state, setAppState, + setInProgressToolUseIDs: () => {}, setResponseLength: () => {}, updateFileHistoryState: () => {}, updateAttributionState: () => {}, + } as unknown as ToolUseContext + const lifecycle = runAsyncAgentLifecycle({ + taskId: agentId, abortController: task.abortController!, + makeStream: () => runAgent({ + agentDefinition: selectedAgent, promptMessages: [createUserMessage({ content: 'initial prompt' })], + toolUseContext: context, canUseTool: async () => ({ behavior: 'allow' }), isAsync: true, + querySource: 'agent:builtin:general-purpose', availableTools: [], transcriptSubdir, + override: { agentId, abortController: task.abortController, userContext: {}, systemContext: {}, systemPrompt: ['test'] as never }, + }), + metadata: { prompt: 'initial prompt', resolvedAgentModel: 'test-model', isBuiltInAgent: true, startTime: Date.now(), agentType: 'general-purpose', isAsync: true }, + description: 'clear output', toolUseContext: context, rootSetAppState: setAppState, + agentIdForCleanup: agentId, enableSummarization: false, getWorktreeResult: async () => ({}), + }) + try { + await entered.promise + await clearConversation({ setMessages: () => {}, readFileState: context.readFileState, getAppState: () => state, setAppState }) + await recordTranscript([createUserMessage({ content: 'new main B conversation' })]) + const pathB = getAgentTranscriptPath(agentId) + expect(pathB).not.toBe(pathA) + release.resolve() + await lifecycle + expect(state.tasks[agentId]?.status).toBe('completed') + expect(await readFile(pathA, 'utf8')).toContain('final A answer') + const output = await readFile(getTaskOutputPath(agentId), 'utf8') + expect(output).toContain(continuesInB ? 'continued B answer' : 'final A answer') + expect(hookPath).toBe(continuesInB ? pathB : pathA) + expect(verifierPath).toBe(hookPath) + expect(verifierRules).toContain(`Read(/${hookPath})`) + expect(await readFile(verifierPath!, 'utf8')).toContain(continuesInB ? 'continued B answer' : 'final A answer') + expect(getPendingNotificationsSnapshot().map(item => item.value).join('\n')).toContain('completed') + const transcript = await getAgentTranscript(agentId) + expect(JSON.stringify(transcript?.messages)).toContain(continuesInB ? 'continued B answer' : 'final A answer') + if (!continuesInB) { + expect(await readAgentMetadata(agentId)).toMatchObject({ agentType: selectedAgent.agentType, source: selectedAgent.source }) + context.options.agentDefinitions.activeAgents = [] + const resume = () => resumeAgentBackground({ agentId, prompt: 'continue after clear', toolUseContext: context, canUseTool: async () => ({ behavior: 'allow' }) }) + await expect(resume()).rejects.toThrow('unavailable or disabled') + context.options.agentDefinitions.activeAgents = [{ ...selectedAgent, source: 'projectSettings' } as AgentDefinition] + await expect(resume()).rejects.toThrow('identity mismatch') + } + context.options.agentDefinitions.activeAgents = [selectedAgent] + const resumed = deferred() + let resumedMessages: unknown + mock.module('../../query.js', () => ({ ...actualQuery, query: async function* ({ messages }: { messages: unknown }) { + resumedMessages = structuredClone(messages) + yield createAssistantMessage({ content: 'resumed answer' }) + } })) + // Observe actual lifecycle completion, not only resume's admission result. + context.setAppState = update => { + setAppState(update) + if (state.tasks[agentId]?.status !== 'running') resumed.resolve() + } + const result = await resumeAgentBackground({ agentId, prompt: 'continue after clear', toolUseContext: context, canUseTool: async () => ({ behavior: 'allow' }) }) + await resumed.promise + expect(state.tasks[agentId]?.status).toBe('completed') + expect(JSON.stringify(resumedMessages)).toContain(continuesInB ? 'continued B answer' : 'final A answer') + expect(JSON.stringify(resumedMessages)).toContain('continue after clear') + expect(await readFile(result.outputFile, 'utf8')).toContain('resumed answer') + if (!foreground && !continuesInB) { + // Two real SendMessage resumes may both pass stopped-task admission + // before their disk reads finish. The older completion cannot own B. + const { SendMessageTool } = await import('../SendMessageTool/SendMessageTool.js') + const { TaskStopTool } = await import('../TaskStopTool/TaskStopTool.js') + const firstRead = deferred() + const secondRead = deferred() + const oldLink = deferred() + const secondQueryDone = deferred() + let reads = 0 + let links = 0 + let queries = 0 + mock.module('../../utils/sessionStorage.js', () => ({ ...actualStorage, readAgentMetadata: async (id: typeof agentId) => { + const read = ++reads + const meta = await actualStorage.readAgentMetadata(id) + await (read === 1 ? firstRead : secondRead).promise + return meta + } })) + mock.module('../../utils/task/diskOutput.js', () => ({ ...actualDiskOutput, initTaskOutputAsSymlink: (id: string, path: string) => + (++links === 1 ? oldLink.promise : Promise.resolve()).then(() => actualDiskOutput.initTaskOutputAsSymlink(id, path)), + })) + mock.module('../../query.js', () => ({ ...actualQuery, query: async function* (params: Parameters[0]) { + if (++queries === 1) { yield createAssistantMessage({ content: 'older resume answer' }); return { reason: 'completed' } } + try { + await new Promise(resolve => params.toolUseContext.abortController.signal.addEventListener('abort', () => resolve(), { once: true })) + } finally { secondQueryDone.resolve() } + return { reason: 'completed' } + } })) + state.agentNameRegistry.set('overlap-target', agentId) + const send = () => SendMessageTool.call({ to: 'overlap-target', message: 'resume overlap' }, context, async () => ({ behavior: 'allow' }), {} as never) + const firstResume = send() + await waitFor(() => reads === 1) + const secondResume = send() + try { + await waitFor(() => reads === 2) + firstRead.resolve() + expect((await firstResume).data.success).toBe(true) + await waitFor(() => state.tasks[agentId]?.type === 'local_agent' && state.tasks[agentId].producerFinished === true) + const first = state.tasks[agentId] + if (first?.type !== 'local_agent') throw new Error('missing first resume') + secondRead.resolve() + expect((await secondResume).data.success).toBe(true) + await waitFor(() => queries === 2) + const successor = state.tasks[agentId] + if (successor?.type !== 'local_agent') throw new Error('missing successor') + oldLink.resolve() + await first.outputReady + await actualDiskOutput._clearOutputsForTest() + await new Promise(resolve => setTimeout(resolve, 0)) + expect(state.tasks[agentId]?.status).toBe('running') + expect(await TaskStopTool.validateInput({ task_id: agentId }, context)).toEqual({ result: true }) + await TaskStopTool.call({ task_id: agentId }, context) + expect(successor.abortController!.signal.aborted).toBe(true) + await secondQueryDone.promise + await waitFor(() => getPendingNotificationsSnapshot().some(item => typeof item.value === 'string' && item.value.includes('killed'))) + } finally { + firstRead.resolve() + secondRead.resolve() + oldLink.resolve() + await Promise.all([firstResume, secondResume]) + killAsyncAgent(agentId, setAppState) + if (queries === 2) await secondQueryDone.promise + } + } + } finally { + release.resolve() + await lifecycle + } + }) + } +} + +for (const foreground of [false, true]) { + test(`cancelled reconciliation stays before ${foreground ? 'foreground' : 'async'} successor links after eviction`, async () => { + const agentId = asAgentId(`cancelled-link-${foreground}`) + let state = getDefaultAppState() + const setAppState = (update: (prev: AppState) => AppState) => { state = update(state) } + const params = { agentId, description: 'old', prompt: 'old', selectedAgent: { agentType: 'test' } as AgentDefinition, setAppState } + const old = registerAsyncAgent(params) + await old.outputReady + const pathA = getAgentTranscriptPath(agentId) + await recordSidechainTranscript([createUserMessage({ content: 'old A output' })], agentId) + state.tasks[agentId] = { ...old, outputDestination: pathA } + const entered = deferred() + const release = deferred() + const links: Promise[] = [] + mock.module('../../utils/task/diskOutput.js', () => ({ ...actualDiskOutput, initTaskOutputAsSymlink: (id: string, path: string) => { + const wait = path === pathA ? release.promise : Promise.resolve() + if (path === pathA) entered.resolve() + const link = wait.then(() => actualDiskOutput.initTaskOutputAsSymlink(id, path)) + links.push(link) + return link + } })) + const completionA = completeAgentTask({ agentId, content: [] } as never, setAppState).catch(error => error) + let completionB: Promise | undefined + try { + await entered.promise + killAsyncAgent(agentId, setAppState) + expect(await completionA).toBeInstanceOf(AbortError) + // Terminal records can be evicted before abandoned filesystem work settles. + setAppState(prev => { const { [agentId]: removed, ...tasks } = prev.tasks; return { ...prev, tasks } }) + await clearConversation({ setMessages: () => {}, readFileState: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE), getAppState: () => state, setAppState }) + const register = foreground ? registerAgentForeground : registerAsyncAgent + register({ ...params, description: 'successor' }) + await recordSidechainTranscript([createUserMessage({ content: 'new B output' })], agentId) + completionB = completeAgentTask({ agentId, content: [] } as never, setAppState) + await new Promise(resolve => setTimeout(resolve, 20)) + const statusWhileAWasPending = state.tasks[agentId]?.status + release.resolve() + await completionB + await Promise.all(links) + expect(await readFile(getTaskOutputPath(agentId), 'utf8')).toContain('new B output') + expect(statusWhileAWasPending).toBe('running') + } finally { + release.resolve() + await completionA + await completionB + await Promise.all(links) + killAsyncAgent(agentId, setAppState) + } + }) +} + +for (const captureDestination of [true, false]) { + for (const swappedTarget of ['agent', 'project']) { + test(`retained transcript lookup rejects another ${swappedTarget}'s output link, captured=${captureDestination}`, async () => { + const agentId = asAgentId('retained-producer') + const pathA = getAgentTranscriptPath(agentId) + const metadata = { agentType: 'retained-agent', source: 'userSettings' } + await writeAgentMetadata(agentId, metadata) + await recordSidechainTranscript([createUserMessage({ content: 'recorded producer history' })], agentId, undefined, { captureDestination }) + let target: string + if (swappedTarget === 'agent') { + const otherId = asAgentId('other-producer') + target = getAgentTranscriptPath(otherId) + await writeAgentMetadata(otherId, metadata) + await recordSidechainTranscript([createUserMessage({ content: 'other producer history' })], otherId, undefined, { captureDestination }) + } else { + target = join(getProjectDir(join(configDir, 'other-project')), getSessionId(), 'subagents', `agent-${agentId}.jsonl`) + await mkdir(dirname(target), { recursive: true }) + // Matching agent content and filename cannot establish producer ownership. + await writeFile(target, await readFile(pathA, 'utf8')) + await writeFile(target.replace(/\.jsonl$/, '.meta.json'), JSON.stringify(metadata)) + } + switchSession(asSessionId(randomUUID())) + await resetSessionFilePointer() + await actualDiskOutput.initTaskOutputAsSymlink(agentId, pathA) + expect(JSON.stringify((await getAgentTranscript(agentId))?.messages)).toContain('recorded producer history') + expect(await readAgentMetadata(agentId)).toEqual(metadata) + await actualDiskOutput.initTaskOutputAsSymlink(agentId, target) + expect(await getAgentTranscript(agentId)).toBeNull() + expect(await readAgentMetadata(agentId)).toBeNull() + }) + } +} + +test('retained transcript lookup rejects output links outside generated agent storage', async () => { + const agentId = asAgentId('retained-path-guard') + const pathA = getAgentTranscriptPath(agentId) + await writeAgentMetadata(agentId, { agentType: 'retained-agent', source: 'userSettings' }) + await recordSidechainTranscript([createUserMessage({ content: 'retained history' })], agentId) + const transcript = await readFile(pathA, 'utf8') + switchSession(asSessionId(randomUUID())) + for (const target of [ + join(configDir, `agent-${agentId}.jsonl`), + join(dirname(pathA), 'agent-other.jsonl'), + join(getProjectDir(configDir), 'invalid-session', 'subagents', `agent-${agentId}.jsonl`), + ]) { + await mkdir(dirname(target), { recursive: true }) + await writeFile(target, transcript) + await writeFile(target.replace(/\.jsonl$/, '.meta.json'), JSON.stringify({ agentType: 'retained-agent', source: 'userSettings' })) + await actualDiskOutput.initTaskOutputAsSymlink(agentId, target) + expect(await getAgentTranscript(agentId)).toBeNull() + expect(await readAgentMetadata(agentId)).toBeNull() + } +}) + +for (const continuesInB of [false, true]) { + test(`Ctrl+B final write spanning clear stays advertised, continued=${continuesInB}`, async () => { + let state = getDefaultAppState() + const setAppState = (update: (prev: AppState) => AppState) => { state = update(state) } + const entered = deferred() + const release = deferred() + const finish = deferred() + const settled = deferred() + let holdNextBranch = false + mock.module('../../utils/git.js', () => ({ ...actualGit, getBranch: async () => { + if (holdNextBranch) { holdNextBranch = false; entered.resolve(); await release.promise } + return 'test' + } })) + const handle = startBackgroundSession({ + description: 'main session clear output', setAppState, + prepare: async () => ({ messages: [createUserMessage({ content: 'main session initial' })], queryParams: {} as never }), + queryImpl: async function* () { + await flushSessionStorage() + holdNextBranch = true + yield createAssistantMessage({ content: 'main session final A answer' }) + await finish.promise + if (continuesInB) { + yield createAssistantMessage({ content: 'main session continued B answer' }) + await flushSessionStorage() + } + return { reason: 'completed' } + }, + onSettled: () => settled.resolve(), + }) + try { + await entered.promise + await clearConversation({ setMessages: () => {}, readFileState: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE), getAppState: () => state, setAppState }) + await recordTranscript([createUserMessage({ content: 'new foreground B' })]) + release.resolve() + await flushSessionStorage() + finish.resolve() + await settled.promise + await actualDiskOutput._clearOutputsForTest() + expect(state.tasks[handle.taskId]?.status).toBe('completed') + expect(getPendingNotificationsSnapshot().map(item => item.value).join('\n')).toContain(getTaskOutputPath(handle.taskId)) + expect(await readFile(getTaskOutputPath(handle.taskId), 'utf8')).toContain(continuesInB ? 'main session continued B answer' : 'main session final A answer') + } finally { + release.resolve() + finish.resolve() + await settled.promise + } + }) +} + +for (const phase of ['initial', 'incremental'] as const) { + test(`fork cancellation escapes a blocked ${phase} transcript and cleans its context`, async () => { + const agentId = asAgentId(`fork-${phase}`) + mock.module('../../utils/uuid.js', () => ({ ...actualUuid, createAgentId: () => agentId })) + const path = getAgentTranscriptPath(agentId) + const release = deferred() + const locked = deferred() + let holder: Promise | undefined + const hold = () => withTranscriptFileLock(path, async () => { locked.resolve(); await release.promise }) + await mkdir(dirname(path), { recursive: true }) + if (phase === 'initial') { holder = hold(); await locked.promise } + mock.module('../../query.js', () => ({ ...actualQuery, query: async function* ({ toolUseContext }: { toolUseContext: ToolUseContext }) { + if (toolUseContext.abortController.signal.aborted) throw new AbortError() + holder = hold() + await locked.promise + yield createAssistantMessage({ content: 'fork answer' }) + } })) + const controller = new AbortController() + const context = { abortController: controller, readFileState: createFileStateCacheWithSizeLimit(READ_FILE_STATE_CACHE_SIZE), getAppState: getDefaultAppState, options: {}, messages: [] } as unknown as ToolUseContext + const clear = spyOn(FileStateCache.prototype, 'clear') + const fork = runForkedAgent({ promptMessages: [createUserMessage({ content: 'fork initial' })], cacheSafeParams: { toolUseContext: context, forkContextMessages: [], systemPrompt: [] as never, userContext: {}, systemContext: {} }, canUseTool: async () => ({ behavior: 'allow' }), querySource: 'compact', forkLabel: 'compact' }) + const outcome = fork.then(() => 'finished', error => error) + let deadline: ReturnType | undefined + try { + await locked.promise + await new Promise(resolve => setTimeout(resolve, 150)) + controller.abort() + const result = await Promise.race([outcome, new Promise(resolve => { deadline = setTimeout(() => resolve('still blocked'), 2_000) })]) + expect(result).toBeInstanceOf(AbortError) + expect(clear).toHaveBeenCalled() + } finally { + clearTimeout(deadline) + release.resolve() + await holder + await outcome + clear.mockRestore() + } + await flushSessionStorage() + const output = await readFile(path, 'utf8') + expect(output).toContain('fork initial') + if (phase === 'incremental') expect(output).toContain('fork answer') + }) +} + +} diff --git a/src/tools/AgentTool/runAgent.ts b/src/tools/AgentTool/runAgent.ts index 72bb87e868..24831ad1cb 100644 --- a/src/tools/AgentTool/runAgent.ts +++ b/src/tools/AgentTool/runAgent.ts @@ -45,6 +45,7 @@ import type { } from '../../types/message.js' import { createAttachmentMessage } from '../../utils/attachments.js' import { AbortError } from '../../utils/errors.js' +import { raceAbort } from '../../utils/boundedAsync.js' import { getDisplayPath } from '../../utils/file.js' import { cloneFileStateCache, @@ -65,6 +66,7 @@ import { resolveAgentRunModelRouting, shouldEnforceModelAllowlist } from '../../ import { getInitialSettings } from '../../utils/settings/settings.js' import { clearAgentTranscriptSubdir, + getAgentTranscriptPath, recordSidechainTranscript, setAgentTranscriptSubdir, writeAgentMetadata, @@ -850,21 +852,44 @@ export async function* runAgent({ logForDebugging(`Failed to write agent metadata: ${_err}`) } - // Record initial messages before the query loop starts. - // Fire-and-forget — persistence failure shouldn't block the agent. - // Only write the transcript if identity metadata was successfully persisted, - // ensuring we never leave a transcript that would resume without its restricted identity. - if (metadataWritten) { - void recordSidechainTranscript(initialMessages, agentId).catch(_err => - logForDebugging(`Failed to record sidechain transcript: ${_err}`), - ) - } else { - logForDebugging('Skipping initial transcript write because identity metadata persistence failed') + let transcriptDestination: string | undefined + agentToolUseContext.getAgentTranscriptPath = id => id === agentId ? transcriptDestination : undefined + + function recordAgentTranscript(messages: Message[], parentUuid?: UUID | null) { + const outputDestination = getAgentTranscriptPath(agentId) + transcriptDestination = outputDestination + // Storage captures this same path synchronously. Publish submission order, + // so an older delayed write cannot replace a newer session's destination. + const write = recordSidechainTranscript(messages, agentId, parentUuid) + rootSetAppState(prev => { + const task = prev.tasks?.[agentId] + if (task?.type !== 'local_agent' || task.outputDestination === outputDestination) return prev + return { ...prev, tasks: { ...prev.tasks, [agentId]: { ...task, outputDestination } } } + }) + return write } - // Track the last recorded message UUID for parent chain continuity + + // Track the last recorded message UUID for parent chain continuity. let lastRecordedUuid: UUID | null = initialMessages.at(-1)?.uuid ?? null try { + // Serialize the initial transcript with subsequent writes. The producer + // resolves after the local append, without flushing unrelated sessions. + // Preserve the identity-metadata gate and best-effort persistence policy. + if (metadataWritten) { + // Storage owns a cleaned message array and tracks this producer even + // when cancellation releases the runner before its append finishes. + await raceAbort( + recordAgentTranscript(initialMessages), + agentAbortController.signal, + ).catch(err => { + if (agentAbortController.signal.aborted) throw new AbortError() + logForDebugging(`Failed to record sidechain transcript: ${err}`) + }) + } else { + logForDebugging('Skipping initial transcript write because identity metadata persistence failed') + } + if (agentAbortController.signal.aborted) throw new AbortError() let queryTerminal: Terminal | undefined const configuredMaxSteps = Number.isSafeInteger(maxSteps) && maxSteps! > 0 @@ -929,13 +954,13 @@ export async function* runAgent({ // Record only the new message with correct parent (O(1) per message) // Only write if identity metadata was successfully persisted. if (metadataWritten) { - await recordSidechainTranscript( - [message], - agentId, - lastRecordedUuid, - ).catch(err => - logForDebugging(`Failed to record sidechain transcript: ${err}`), - ) + await raceAbort( + recordAgentTranscript([message], lastRecordedUuid), + agentAbortController.signal, + ).catch(err => { + if (agentAbortController.signal.aborted) throw new AbortError() + logForDebugging(`Failed to record sidechain transcript: ${err}`) + }) } if (message.type !== 'progress') { lastRecordedUuid = message.uuid diff --git a/src/tools/SendMessageTool/SendMessageTool.ts b/src/tools/SendMessageTool/SendMessageTool.ts index a7e963511e..b949e9427d 100644 --- a/src/tools/SendMessageTool/SendMessageTool.ts +++ b/src/tools/SendMessageTool/SendMessageTool.ts @@ -812,11 +812,18 @@ export const SendMessageTool: Tool = const task = appState.tasks[agentId] if (isLocalAgentTask(task) && !isMainSessionTask(task)) { if (task.status === 'running') { - queuePendingMessage( + if (!queuePendingMessage( agentId, input.message, context.setAppStateForTasks ?? context.setAppState, - ) + )) { + return { + data: { + success: false, + message: `${input.to} is finishing its output. Please retry after it completes.`, + }, + } + } return { data: { success: true, @@ -831,11 +838,18 @@ export const SendMessageTool: Tool = // the task reference (the first resume changes status to 'running'). const freshTask = context.getAppState().tasks[agentId] if (isLocalAgentTask(freshTask) && freshTask.status === 'running') { - queuePendingMessage( + if (!queuePendingMessage( agentId, input.message, context.setAppStateForTasks ?? context.setAppState, - ) + )) { + return { + data: { + success: false, + message: `${input.to} is finishing its output. Please retry after it completes.`, + }, + } + } return { data: { success: true, diff --git a/src/utils/forkedAgent.ts b/src/utils/forkedAgent.ts index db74383450..f0ca2a7225 100644 --- a/src/utils/forkedAgent.ts +++ b/src/utils/forkedAgent.ts @@ -25,6 +25,8 @@ import type { AgentDefinition } from '../tools/AgentTool/loadAgentsDir.js' import type { AgentId } from '../types/ids.js' import type { Message } from '../types/message.js' import { createChildAbortController } from './abortController.js' +import { raceAbort } from './boundedAsync.js' +import { AbortError } from './errors.js' import { logForDebugging } from './debug.js' import { cloneFileStateCache } from './fileStateCache.js' import type { REPLHookContext } from './hooks/postSamplingHooks.js' @@ -559,21 +561,18 @@ export async function runForkedAgent({ // When skipTranscript is set, skip agent ID creation and all transcript I/O const agentId = skipTranscript ? undefined : createAgentId(forkLabel) let lastRecordedUuid: UUID | null = null - if (agentId) { - await recordSidechainTranscript(initialMessages, agentId).catch(err => - logForDebugging( - `Forked agent [${forkLabel}] failed to record initial transcript: ${err}`, - ), - ) - // Track the last recorded message UUID for parent chain continuity - lastRecordedUuid = - initialMessages.length > 0 - ? initialMessages[initialMessages.length - 1]!.uuid - : null - } - - // Run the query loop with isolated context (cache-safe params preserved) + const signal = isolatedToolUseContext.abortController.signal + // Initial persistence can now be interrupted too, so it belongs to cleanup. try { + if (agentId) { + await raceAbort(recordSidechainTranscript(initialMessages, agentId), signal).catch(err => { + if (signal.aborted) throw new AbortError() + logForDebugging(`Forked agent [${forkLabel}] failed to record initial transcript: ${err}`) + }) + lastRecordedUuid = initialMessages.at(-1)?.uuid ?? null + } + + // Run the query loop with isolated context (cache-safe params preserved) for await (const message of query({ messages: initialMessages, systemPrompt, @@ -626,12 +625,10 @@ export async function runForkedAgent({ msg.type === 'user' || msg.type === 'progress') ) { - await recordSidechainTranscript([msg], agentId, lastRecordedUuid).catch( - err => - logForDebugging( - `Forked agent [${forkLabel}] failed to record transcript: ${err}`, - ), - ) + await raceAbort(recordSidechainTranscript([msg], agentId, lastRecordedUuid), signal).catch(err => { + if (signal.aborted) throw new AbortError() + logForDebugging(`Forked agent [${forkLabel}] failed to record transcript: ${err}`) + }) if (msg.type !== 'progress') { lastRecordedUuid = msg.uuid } diff --git a/src/utils/hooks.ts b/src/utils/hooks.ts index 8408c742df..ad8112b322 100644 --- a/src/utils/hooks.ts +++ b/src/utils/hooks.ts @@ -3897,7 +3897,7 @@ export async function* executeStopHooks( hook_event_name: 'SubagentStop', stop_hook_active: stopHookActive, agent_id: subagentId, - agent_transcript_path: getAgentTranscriptPath(subagentId), + agent_transcript_path: toolUseContext?.getAgentTranscriptPath?.(subagentId) ?? getAgentTranscriptPath(subagentId), agent_type: agentType ?? '', last_assistant_message: lastAssistantText, } diff --git a/src/utils/hooks/execAgentHook.ts b/src/utils/hooks/execAgentHook.ts index 3282670fbc..28c082deb1 100644 --- a/src/utils/hooks/execAgentHook.ts +++ b/src/utils/hooks/execAgentHook.ts @@ -52,7 +52,7 @@ export async function execAgentHook( // Get transcript path from context const transcriptPath = toolUseContext.agentId - ? getAgentTranscriptPath(toolUseContext.agentId) + ? toolUseContext.getAgentTranscriptPath?.(toolUseContext.agentId) ?? getAgentTranscriptPath(toolUseContext.agentId) : getTranscriptPath() const hookStartTime = Date.now() try { diff --git a/src/utils/sessionStorage.ts b/src/utils/sessionStorage.ts index 4b02f119b7..a78cfc6a0f 100644 --- a/src/utils/sessionStorage.ts +++ b/src/utils/sessionStorage.ts @@ -11,12 +11,13 @@ import { mkdir, readdir, readFile, + readlink, stat, unlink, writeFile, } from 'fs/promises' import memoize from 'lodash-es/memoize.js' -import { basename, dirname, join, resolve as resolvePath } from 'path' +import { basename, dirname, join, relative, sep, isAbsolute, resolve as resolvePath } from 'path' import { type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, logEvent, @@ -79,7 +80,8 @@ import { getCwd } from './cwd.js' import { logForDebugging } from './debug.js' import { logForDiagnosticsNoPII } from './diagLogs.js' import { getClaudeConfigHomeDir, getProjectsDir, isEnvTruthy } from './envUtils.js' -import { isFsInaccessible } from './errors.js' +import { getErrnoCode, isFsInaccessible } from './errors.js' +import { getTaskOutputPath } from './task/diskOutput.js' import type { FileHistorySnapshot } from './fileHistory.js' import { formatFileSize } from './format.js' import { getFsImplementation } from './fsOperations.js' @@ -621,6 +623,35 @@ export function getAgentTranscriptPath(agentId: AgentId): string { return join(base, `agent-${agentId}.jsonl`) } +// Completion can retain a transcript submitted before /clear. Reuse that +// task's exact output link when no current-session transcript exists; do not +// search historical sessions. Metadata must follow the same selected file. +async function getAgentTranscriptReadPath(agentId: AgentId): Promise { + const currentPath = getAgentTranscriptPath(agentId) + try { + await stat(currentPath) + return currentPath + } catch (error) { + if (getErrnoCode(error) !== 'ENOENT') throw error + } + const outputPath = getTaskOutputPath(agentId) + try { + const target = resolvePath(dirname(outputPath), await readlink(outputPath)) + const relativePath = relative(getProjectsDir(), target) + const parts = relativePath.split(sep) + // Accept only generated project/session/subagents paths for this agent. + // Grouping directories and the resumed project override can be released + // before this read. Require a destination recorded by this agent's producer. + if (!isAbsolute(relativePath) && parts[0] !== '..' && parts.length >= 4 && + validateUuid(parts[1]) && parts[2] === 'subagents' && + parts.at(-1) === `agent-${agentId}.jsonl` && + getProject().sidechainProducerPaths.get(agentId)?.has(target)) return target + } catch (error) { + if (!isFsInaccessible(error) && getErrnoCode(error) !== 'EINVAL') throw error + } + return currentPath +} + function getAgentMetadataPath(agentId: AgentId): string { return getAgentTranscriptPath(agentId).replace(/\.jsonl$/, '.meta.json') } @@ -663,8 +694,8 @@ export async function writeAgentMetadata( export async function readAgentMetadata( agentId: AgentId, ): Promise { - const path = getAgentMetadataPath(agentId) try { + const path = (await getAgentTranscriptReadPath(agentId)).replace(/\.jsonl$/, '.meta.json') const raw = await readFile(path, 'utf-8') return JSON.parse(raw) as AgentMetadata } catch (e) { @@ -917,6 +948,8 @@ export function setRemoteIngressUrlForTesting(url: string): void { const REMOTE_FLUSH_INTERVAL_MS = 10 +type SidechainDestination = { path: string; sessionId: UUID } + class Project { // Minimal cache for current session only (not all sessions) currentSessionTag: string | undefined @@ -1631,12 +1664,59 @@ class Project { } } + private sidechainWrites = new Map>() + // Producer destinations outlive /clear and project/subdirectory overrides. + readonly sidechainProducerPaths = new Map>() + + private async appendSidechainEntry(agentId: string, path: string, data: string): Promise { + await this.appendToFile(path, data) + let paths = this.sidechainProducerPaths.get(agentId) + if (!paths) { + paths = new Set() + this.sidechainProducerPaths.set(agentId, paths) + } + paths.add(resolvePath(path)) + } + + insertSidechainTranscript( + messages: Transcript, + agentId?: string, + startingParentUuid?: UUID | null, + captureDestination = true, + ): Promise { + // Track the predecessor wait too: shutdown flushes must see queued chains. + return this.trackWrite(async () => { + if (!agentId) { + return this.insertMessageChain(messages, true, agentId, startingParentUuid) + } + // Handoff can restart the same agent before its old iterator has closed. + // Join whole chains; the file lock alone only serializes single appends. + const destination: SidechainDestination | undefined = captureDestination ? { + path: getAgentTranscriptPath(asAgentId(agentId)), + sessionId: getSessionId() as UUID, + } : undefined + // Live-path producers still serialize chains, without freezing a session. + const path = destination?.path ?? `live:${agentId}` + const previous = this.sidechainWrites.get(path) + const write = (previous?.catch(() => {}) ?? Promise.resolve()).then(() => + this.insertMessageChain(messages, true, agentId, startingParentUuid, undefined, destination), + ) + this.sidechainWrites.set(path, write) + try { + await write + } finally { + if (this.sidechainWrites.get(path) === write) this.sidechainWrites.delete(path) + } + }) + } + async insertMessageChain( messages: Transcript, isSidechain: boolean = false, agentId?: string, startingParentUuid?: UUID | null, teamInfo?: { teamName?: string; agentName?: string }, + sidechainDestination?: SidechainDestination, ) { return this.trackWrite(async () => { let parentUuid: UUID | null = startingParentUuid ?? null @@ -1644,7 +1724,10 @@ class Project { // First conversation message materializes the session file. // Hook progress/attachment messages and local-command bookkeeping // alone stay buffered. - if (this.sessionFile === null && messages.some(startsConversation)) { + if ( + this.sessionFile === null && messages.some(startsConversation) && + (!sidechainDestination || sidechainDestination.sessionId === getSessionId()) + ) { await this.materializeSessionFile() } @@ -1658,7 +1741,7 @@ class Project { } // Get slug if one exists for this session (used for plan files, etc.) - const sessionId = getSessionId() + const sessionId = sidechainDestination?.sessionId ?? getSessionId() const slug = getPlanSlugCache().get(sessionId) for (const message of messages) { @@ -1701,7 +1784,7 @@ class Project { gitBranch, slug, } - await this.appendEntry(transcriptMessage) + await this.appendEntry(transcriptMessage, sidechainDestination?.sessionId, sidechainDestination) const shouldTrackReplay = !this.shouldSkipPersistence() if (shouldTrackReplay && !isSidechain && message.type === 'user') { const content = getFirstMeaningfulUserMessageTextContent([message]) @@ -1819,11 +1902,22 @@ class Project { }) } - async appendEntry(entry: Entry, sessionId: UUID = getSessionId() as UUID) { + async appendEntry( + entry: Entry, + sessionId: UUID = getSessionId() as UUID, + sidechainDestination?: SidechainDestination, + ) { if (this.shouldSkipPersistence()) { return } + if (sidechainDestination && isTranscriptMessage(entry) && entry.isSidechain && entry.agentId !== undefined) { + // The queue key, session stamp and every append belong to the submission, + // even if /clear changes the active session while this chain is waiting. + await this.appendSidechainEntry(entry.agentId, sidechainDestination.path, jsonStringify(entry) + '\n') + return + } + const currentSessionId = getSessionId() as UUID const isCurrentSession = sessionId === currentSessionId @@ -1941,8 +2035,13 @@ class Project { // exhausts retries → gracefulShutdownSync(1). See inc-4718. const isNewUuid = !messageSet.has(entry.uuid) if (isAgentSidechain || isNewUuid) { - // Enqueue write — appendToFile handles ENOENT by creating directories - void this.enqueueWrite(targetFile, entry) + if (isAgentSidechain) { + // Agent completion awaits this producer. Use the existing per-file + // append/lock path so unrelated session queues cannot gate output. + await this.appendSidechainEntry(entry.agentId!, targetFile, jsonStringify(entry) + '\n') + } else { + void this.enqueueWrite(targetFile, entry) + } if (!isAgentSidechain) { // messageSet is main-file-authoritative. Sidechain entries go to a @@ -2202,12 +2301,10 @@ export async function recordSidechainTranscript( messages: Message[], agentId?: string, startingParentUuid?: UUID | null, + { captureDestination = true }: { captureDestination?: boolean } = {}, ) { - await getProject().insertMessageChain( - cleanMessagesForLogging(messages), - true, - agentId, - startingParentUuid, + await getProject().insertSidechainTranscript( + cleanMessagesForLogging(messages), agentId, startingParentUuid, captureDestination, ) } @@ -5308,9 +5405,8 @@ export async function getAgentTranscript(agentId: AgentId): Promise<{ messages: Message[] contentReplacements: ContentReplacementRecord[] } | null> { - const agentFile = getAgentTranscriptPath(agentId) - try { + const agentFile = await getAgentTranscriptReadPath(agentId) const { messages, agentContentReplacements } = await loadTranscriptFile(agentFile) From ae83f6e4cf9201ef65ececd09b16c2d0de267879 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 17:34:16 -0700 Subject: [PATCH 014/121] ci: use shallow checkout for smoke-and-tests (#2320) --- .github/workflows/pr-checks.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 7fd5949091..9cb37eb354 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -44,8 +44,6 @@ jobs: steps: - name: Check out repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - fetch-depth: 0 - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 From 1c42a10628df59cdaaf3a75a89978737f592721a Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 18:00:49 -0700 Subject: [PATCH 015/121] fix(shell): honor an abort that lands before the listener is attached (#2321) * fix(shell): honor an abort that lands before the listener is attached * test: spawn node timer instead of sleep for Windows portability --- src/utils/ShellCommand.test.ts | 40 ++++++++++++++++++++++++++++++++++ src/utils/ShellCommand.ts | 3 +++ 2 files changed, 43 insertions(+) diff --git a/src/utils/ShellCommand.test.ts b/src/utils/ShellCommand.test.ts index 03f5e87f87..306327bef0 100644 --- a/src/utils/ShellCommand.test.ts +++ b/src/utils/ShellCommand.test.ts @@ -1,4 +1,5 @@ import { EventEmitter } from 'events' +import { spawn } from 'child_process' import { expect, test } from 'bun:test' import { wrapSpawn } from './ShellCommand.js' import { TaskOutput } from './task/TaskOutput.js' @@ -123,3 +124,42 @@ test('interrupt does not kill keep-alive commands used by asyncRewake hooks', as expect(result.code).toBe(2) expect(result.interrupted).toBe(false) }) + +test('already-aborted user-cancel kills a real child promptly', async () => { + const controller = new AbortController() + controller.abort('user-cancel') + + const startedAt = performance.now() + const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1000)'], { + stdio: 'ignore', + }) + const childClosed = new Promise(resolve => { + child.once('close', () => resolve()) + }) + const command = wrapSpawn( + child, + controller.signal, + 1_000, + new TaskOutput('shellcommand-test-already-aborted', null), + ) + + try { + expect(child.pid).toBeDefined() + + const result = await command.result + const elapsedMs = performance.now() - startedAt + await childClosed + + expect(elapsedMs).toBeLessThan(2_000) + expect(result.isAbort).toBe(true) + expect(result.abortReason).toBe('user-abort') + expect(result.interrupted).toBe(true) + expect(result.signalAborted).toBe(true) + expect(command.status).toBe('killed') + } finally { + child.kill('SIGKILL') + await command.result + await childClosed + command.cleanup() + } +}) diff --git a/src/utils/ShellCommand.ts b/src/utils/ShellCommand.ts index 208226b396..96158db943 100644 --- a/src/utils/ShellCommand.ts +++ b/src/utils/ShellCommand.ts @@ -303,6 +303,9 @@ class ShellCommandImpl implements ShellCommand { this.#abortSignal.addEventListener('abort', this.#boundAbortHandler, { once: true, }) + if (this.#abortSignal.aborted) { + this.#boundAbortHandler() + } // Use 'exit' not 'close': 'close' waits for stdio to close, which includes // grandchild processes that inherit file descriptors (e.g. `sleep 30 &`). From 75c73ea3811f359a8bad23b073bd4713846ea833 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 18:08:10 -0700 Subject: [PATCH 016/121] docs: add CONTRIBUTING notes for CI caching and subprocess test timeouts (#2322) --- CONTRIBUTING.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index df4c8f2ab1..53737bac40 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -160,6 +160,8 @@ Dependency changes need a clear project benefit — fixing a bug, addressing a s - Preserve existing repo patterns unless the change is intentionally refactoring them. - Add or update tests when the change affects behavior. - In timing-sensitive tests, drive timers with Bun's fake timers (`jest.useFakeTimers()` and `jest.advanceTimersByTime()`) instead of racing real delays, and restore real timers during cleanup, so the tests pass in a single-process whole-suite run as well as in the sharded runner. +- Await completion promises or explicit readiness signals before asserting asynchronous results. For tests that deliberately suppress readiness, use a short, bounded wait and assert the expected failure. +- For tests that launch real subprocesses, set an explicit per-test timeout longer than the child-process deadline, leaving time for assertions and cleanup. The launcher heap tests use an 8-second child deadline and a 10-second test timeout. Document the reason for the timeout beside the test. - Update docs when setup, commands, or user-facing behavior changes. - Website release notes live on GitHub Releases. Do not add manually maintained release-note data to the static site. @@ -307,6 +309,7 @@ bun run web:build Notes on the local preflight: +- PR Checks caches Bun package downloads using OS-scoped keys derived from `bun.lock` for root installs and `web/bun.lock` for web installs, with separate restore prefixes. Dependency installs still run with `--frozen-lockfile` on cache hits. Keep the relevant lockfile in sync with dependency changes and run the required local preflight regardless of cache status. - `bun run check` runs `check:static` (the any-usage budget, smoke build, and deadcode checks), then the full unit pass (`test:full`) — do not run those separately, or you execute work twice. - `bun run test` isolates each test file's globals and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups. Use `bun run scripts/run-tests.ts --shards=N --shard-index=K` with the same test flags to run only shard K of N. `test:full` then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files to avoid Bun’s isolated-stdio pipe registration bug; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI runs `check:static` in `smoke-and-tests`, runs the eight unit shards as parallel `unit-tests` jobs, and runs each dedicated feature pass once in `dedicated-tests`. Each unit shard builds the CLI before testing. CI caps these jobs at 15 minutes each and the other jobs at five minutes. - Fetching upstream `main` by URL avoids assuming that a fork checkout's `origin` points at Twigpine/openclaude. `FETCH_HEAD` is the fetched upstream tip. The scan deliberately uses local `HEAD` so it includes commits that have not been pushed yet; CI uses the pushed PR head SHA after the push. From 78445348676d6c5797fc11d89eaaf0157d69de1b Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 18:18:19 -0700 Subject: [PATCH 017/121] test: stabilize QueryGuard keepalive interruption test (#2323) --- .../api/codexShim.interruption.test.ts | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/src/services/api/codexShim.interruption.test.ts b/src/services/api/codexShim.interruption.test.ts index cb12e0126a..db8829aa94 100644 --- a/src/services/api/codexShim.interruption.test.ts +++ b/src/services/api/codexShim.interruption.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { afterEach, beforeEach, describe, expect, jest, test } from 'bun:test' import { acquireSharedMutationLock, releaseSharedMutationLock, @@ -707,6 +707,10 @@ describe('issue #1830 Codex interruption ownership', () => { const originalTrace = process.env.OPENCLAUDE_INTERRUPT_TRACE process.env.OPENCLAUDE_INTERRUPT_TRACE = '1' __resetInterruptionTraceForTests() + const realSetImmediate = setImmediate + const nextTurn = () => new Promise(resolve => realSetImmediate(resolve)) + let consumption: ReturnType | undefined + jest.useFakeTimers() const timed = makeTimedStream( index => { if (index % 4 === 0) return ': keepalive\n\n' @@ -721,15 +725,35 @@ describe('issue #1830 Codex interruption ownership', () => { 25, ) try { - const outcome = await driveWithGuard(timed.response, { + let settled = false + consumption = driveWithGuard(timed.response, { idleTimeoutMs: 250, hardMaxQueryMs: 1_500, readerIdleTimeoutMs: 500, }) + void consumption.then( + () => { settled = true }, + () => { settled = true }, + ) + // Finish message_start and drain each frame before advancing the clock. + // Real interval callbacks can fall behind the guard deadline under load. + await nextTurn() + for (let elapsed = 25; elapsed < 250; elapsed += 25) { + jest.advanceTimersByTime(25) + await nextTurn() + } + jest.advanceTimersByTime(24) + await nextTurn() + expect(settled).toBe(false) + jest.advanceTimersByTime(1) + await nextTurn() + expect(settled).toBe(true) + const outcome = await consumption expect(outcome.timeout.reason satisfies QueryGuardTimeoutReason).toBe( 'idle', ) + expect(outcome.timeout.elapsedMs).toBe(250) expect(outcome.signalReason).toBe('query-timeout') expect(outcome.result.status).toBe('rejected') expect(outcome.events.map(event => event.type)).toEqual([ @@ -766,6 +790,8 @@ describe('issue #1830 Codex interruption ownership', () => { expect(timed.cancelReasons).toHaveLength(1) } finally { timed.stop() + jest.useRealTimers() + if (consumption) await bounded(consumption).catch(() => {}) await __waitForInterruptionTraceFlushForTests() __resetInterruptionTraceForTests() if (originalTrace === undefined) { From 6baae01922dd9392d17d359e8fdc5f71e5f4a1c5 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 20:59:55 -0700 Subject: [PATCH 018/121] test: consolidate vendor catalog tests into per-vendor contract tables (#2324) * test: consolidate vendor catalog tests into per-vendor contract tables * test: assert full descriptor limits and capabilities for nearai, longcat and xai --- CONTRIBUTING.md | 1 + src/integrations/gpt56Catalog.test.ts | 66 ++++++---- src/integrations/ling-tiny.test.ts | 78 +++++------ src/integrations/macaron.test.ts | 70 +++++----- src/integrations/nearai.test.ts | 63 +++++---- src/integrations/tencent.test.ts | 63 +++++---- src/integrations/vendors/longcat.test.ts | 52 ++++---- src/integrations/vendors/xai.test.ts | 161 ++++++++++------------- 8 files changed, 286 insertions(+), 268 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 53737bac40..a01ab2c091 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -159,6 +159,7 @@ Dependency changes need a clear project benefit — fixing a bug, addressing a s - Avoid mixing unrelated cleanup into the same change. - Preserve existing repo patterns unless the change is intentionally refactoring them. - Add or update tests when the change affects behavior. +- For vendor/integration catalog tests, cover each vendor with one table-driven test of its registry contract (route resolution, default base URL and model, credential env vars, catalog-to-descriptor linkage, and capability/limit values the runtime uses), plus a small number of behavioral tests such as model-discovery keep/drop parsing or availability windows. Do not copy full curated model lists or cache/refresh settings from vendor definitions into tests. - In timing-sensitive tests, drive timers with Bun's fake timers (`jest.useFakeTimers()` and `jest.advanceTimersByTime()`) instead of racing real delays, and restore real timers during cleanup, so the tests pass in a single-process whole-suite run as well as in the sharded runner. - Await completion promises or explicit readiness signals before asserting asynchronous results. For tests that deliberately suppress readiness, use a short, bounded wait and assert the expected failure. - For tests that launch real subprocesses, set an explicit per-test timeout longer than the child-process deadline, leaving time for assertions and cleanup. The launcher heap tests use an 8-second child deadline and a 10-second test timeout. Document the reason for the timeout beside the test. diff --git a/src/integrations/gpt56Catalog.test.ts b/src/integrations/gpt56Catalog.test.ts index 6212185e1a..2bbad628e7 100644 --- a/src/integrations/gpt56Catalog.test.ts +++ b/src/integrations/gpt56Catalog.test.ts @@ -1,32 +1,46 @@ -import { expect, test } from 'bun:test' +import { describe, expect, test } from 'bun:test' -import gptModels from './models/gpt.js' -import openaiVendor from './vendors/openai.js' +import { + getCatalogEntriesForRoute, + getModel, + getRouteDefaultBaseUrl, + getRouteDefaultModel, + getVendor, + resolveRouteIdFromBaseUrl, +} from './index.js' const GPT56_IDS = ['gpt-5.6-sol', 'gpt-5.6-terra', 'gpt-5.6-luna'] as const -test('gpt-5.6 model descriptors carry the verified limits', () => { - for (const id of GPT56_IDS) { - const descriptor = gptModels.find(model => model.id === id) - expect(descriptor).toBeDefined() - // Descriptor and catalog intentionally diverge: the descriptor is the - // fallback for catalog-less routes — notably the Codex transport - // (chatgpt.com/backend-api/codex), which enforces a ~272k effective - // input cap (issue #1118 precedent, same as gpt-5.5). The direct-OpenAI - // route reads the vendor catalog entry below, which keeps the true - // 1.05M window. - expect(descriptor?.contextWindow).toBe(272_000) - expect(descriptor?.maxOutputTokens).toBe(128_000) - } -}) +describe('OpenAI GPT-5.6 catalog', () => { + test('links descriptors while keeping route-specific limits and reasoning', () => { + const baseUrl = 'https://api.openai.com/v1' + expect(resolveRouteIdFromBaseUrl(baseUrl)).toBe('openai') + expect(getRouteDefaultBaseUrl('openai')).toBe(baseUrl) + expect(getRouteDefaultModel('openai')).toBe('gpt-5.4') + expect(getVendor('openai')?.setup.credentialEnvVars).toEqual([ + 'OPENAI_API_KEYS', + 'OPENAI_API_KEY', + ]) -test('openai vendor gpt-5.6 catalog entries carry limits and reasoning metadata', () => { - for (const id of GPT56_IDS) { - const entry = openaiVendor.catalog?.models?.find(model => model.id === id) - expect(entry).toBeDefined() - expect(entry?.contextWindow).toBe(1_050_000) - expect(entry?.maxOutputTokens).toBe(128_000) - expect(entry?.reasoning?.wireFormat).toBe('reasoning_effort') - expect(entry?.reasoning?.levels).toContain('xhigh') - } + for (const id of GPT56_IDS) { + const descriptor = getModel(id) + // Catalog-less routes (including Codex) use the 272k descriptor fallback; + // direct OpenAI requests use the catalog's 1.05M context window. + expect(descriptor).toMatchObject({ + id, + defaultModel: id, + contextWindow: 272_000, + maxOutputTokens: 128_000, + }) + const entry = getCatalogEntriesForRoute('openai').find(model => model.id === id) + expect(entry).toMatchObject({ + apiName: id, + modelDescriptorId: descriptor?.id, + contextWindow: 1_050_000, + maxOutputTokens: 128_000, + }) + expect(entry?.reasoning?.wireFormat).toBe('reasoning_effort') + expect(entry?.reasoning?.levels).toContain('xhigh') + } + }) }) diff --git a/src/integrations/ling-tiny.test.ts b/src/integrations/ling-tiny.test.ts index b947f9c001..0b203550a1 100644 --- a/src/integrations/ling-tiny.test.ts +++ b/src/integrations/ling-tiny.test.ts @@ -4,6 +4,8 @@ import { getCatalogEntriesForRoute, getModel, getModelsForBrand, + getRouteDefaultBaseUrl, + resolveRouteIdFromBaseUrl, } from './index.js' import { resolveModelRuntimeLimits } from './runtimeMetadata.js' @@ -16,46 +18,46 @@ const AT_CUTOFF = new Date('2026-08-13T10:00:00Z') const AFTER_WINDOW = new Date('2026-08-13T10:00:01Z') describe('Ling 3.0 Tiny :free descriptor', () => { - test('exposes the Tiny capabilities and limits to gateway catalogs', () => { - const model = getModel('inclusionai/ling-3.0-tiny:free') + test('links gateway wire models to descriptors and resolves limits by base URL', () => { + const routeId = 'gitlawb-opengateway' + const baseUrl = 'https://opengateway.gitlawb.com/v1' + expect(resolveRouteIdFromBaseUrl(baseUrl)).toBe(routeId) + expect(getRouteDefaultBaseUrl(routeId)).toBe(baseUrl) - expect(model).toBeDefined() - expect(model).toMatchObject({ - id: 'inclusionai/ling-3.0-tiny:free', - brandId: 'ling', - classification: ['chat', 'reasoning', 'coding'], - contextWindow: 262_144, - maxOutputTokens: 32_768, - capabilities: { - supportsVision: false, - supportsStreaming: true, - supportsFunctionCalling: true, - supportsJsonMode: false, - supportsReasoning: true, - supportsPreciseTokenCount: false, + for (const { id, catalogId, limits, classification, capabilities } of [ + { + id: 'inclusionai/ling-3.0-tiny:free', + catalogId: 'opengateway-ling-3.0-tiny-free', + limits: { contextWindow: 262_144, maxOutputTokens: 32_768 }, + classification: ['chat', 'reasoning', 'coding'], + capabilities: { + supportsVision: false, + supportsStreaming: true, + supportsFunctionCalling: true, + supportsJsonMode: false, + supportsReasoning: true, + supportsPreciseTokenCount: false, + }, }, - }) - expect(getModelsForBrand('ling').map(m => m.id)).toContain( - 'inclusionai/ling-3.0-tiny:free', - ) - - // The gateway entry must map BOTH the wire id (apiName) and the picker - // descriptor to the :free id — a mismatch would send a different model - // upstream than the picker advertises. - const catalogEntry = getCatalogEntriesForRoute( - 'gitlawb-opengateway', - DURING_WINDOW, - ).find(entry => entry.apiName === 'inclusionai/ling-3.0-tiny:free') - expect(catalogEntry?.id).toBe('opengateway-ling-3.0-tiny-free') - expect(catalogEntry?.modelDescriptorId).toBe(model?.id) - - expect( - resolveModelRuntimeLimits({ - model: 'inclusionai/ling-3.0-tiny:free', - baseUrl: 'https://opengateway.gitlawb.com/v1', - processEnv: {}, - }), - ).toEqual({ contextWindow: 262_144, maxOutputTokens: 32_768 }) + ]) { + const model = getModel(id) + expect(model).toMatchObject({ + id, + brandId: 'ling', + defaultModel: id, + ...limits, + classification, + capabilities, + }) + expect(getModelsForBrand('ling').map(m => m.id)).toContain(id) + const entry = getCatalogEntriesForRoute(routeId, DURING_WINDOW).find( + e => e.id === catalogId, + ) + expect(entry).toMatchObject({ apiName: id, modelDescriptorId: model?.id }) + expect( + resolveModelRuntimeLimits({ model: id, baseUrl, processEnv: {} }), + ).toEqual(limits) + } }) test('availableUntil drops the entry from catalog resolution after the window', () => { diff --git a/src/integrations/macaron.test.ts b/src/integrations/macaron.test.ts index cbb16127dc..e659956fbe 100644 --- a/src/integrations/macaron.test.ts +++ b/src/integrations/macaron.test.ts @@ -4,44 +4,48 @@ import { getCatalogEntriesForRoute, getModel, getModelsForBrand, + getRouteDefaultBaseUrl, + resolveRouteIdFromBaseUrl, } from './index.js' import { resolveModelRuntimeLimits } from './runtimeMetadata.js' describe('Macaron V1 Tall descriptor', () => { - test('exposes the verified Macaron capabilities and limits to gateway catalogs', () => { - const model = getModel('mindai/macaron-v1-tall') + test('links gateway wire models to descriptors and resolves limits by base URL', () => { + const routeId = 'gitlawb-opengateway' + const baseUrl = 'https://opengateway.gitlawb.com/v1' + expect(resolveRouteIdFromBaseUrl(baseUrl)).toBe(routeId) + expect(getRouteDefaultBaseUrl(routeId)).toBe(baseUrl) - expect(model).toBeDefined() - expect(model).toMatchObject({ - id: 'mindai/macaron-v1-tall', - brandId: 'macaron', - classification: ['chat', 'reasoning', 'coding'], - contextWindow: 262_144, - maxOutputTokens: 32_768, - capabilities: { - supportsVision: false, - supportsStreaming: true, - supportsFunctionCalling: true, - supportsJsonMode: false, - supportsReasoning: true, + for (const { id, catalogId, limits, classification, capabilities } of [ + { + id: 'mindai/macaron-v1-tall', + catalogId: 'opengateway-macaron-v1-tall', + limits: { contextWindow: 262_144, maxOutputTokens: 32_768 }, + classification: ['chat', 'reasoning', 'coding'], + capabilities: { + supportsVision: false, + supportsStreaming: true, + supportsFunctionCalling: true, + supportsJsonMode: false, + supportsReasoning: true, + }, }, - }) - expect(getModelsForBrand('macaron').map(m => m.id)).toContain( - 'mindai/macaron-v1-tall', - ) - - const catalogEntry = getCatalogEntriesForRoute('gitlawb-opengateway').find( - entry => entry.apiName === 'mindai/macaron-v1-tall', - ) - expect(catalogEntry?.id).toBe('opengateway-macaron-v1-tall') - expect(catalogEntry?.modelDescriptorId).toBe(model?.id) - - expect( - resolveModelRuntimeLimits({ - model: 'mindai/macaron-v1-tall', - baseUrl: 'https://opengateway.gitlawb.com/v1', - processEnv: {}, - }), - ).toEqual({ contextWindow: 262_144, maxOutputTokens: 32_768 }) + ]) { + const model = getModel(id) + expect(model).toMatchObject({ + id, + brandId: 'macaron', + defaultModel: id, + ...limits, + classification, + capabilities, + }) + expect(getModelsForBrand('macaron').map(m => m.id)).toContain(id) + const entry = getCatalogEntriesForRoute(routeId).find(e => e.id === catalogId) + expect(entry).toMatchObject({ apiName: id, modelDescriptorId: model?.id }) + expect( + resolveModelRuntimeLimits({ model: id, baseUrl, processEnv: {} }), + ).toEqual(limits) + } }) }) diff --git a/src/integrations/nearai.test.ts b/src/integrations/nearai.test.ts index 927059d553..59ae04a819 100644 --- a/src/integrations/nearai.test.ts +++ b/src/integrations/nearai.test.ts @@ -14,7 +14,12 @@ import { getCatalogEntriesForRoute, getModelsForVendor, } from './registry.js' -import { getRouteDefaultBaseUrl } from './routeMetadata.js' +import { + getRouteDefaultBaseUrl, + getRouteDefaultModel, + getRouteDescriptor, + resolveRouteIdFromBaseUrl, +} from './routeMetadata.js' import { acquireSharedMutationLock, releaseSharedMutationLock, @@ -33,30 +38,36 @@ afterEach(() => { }) describe('NearAI Claude Opus 4.8 route', () => { - test('catalog exposes the anthropic/claude-opus-4-8 entry', () => { - const entry = getCatalogEntriesForRoute('nearai').find( - e => e.id === OPUS_48_ID, - ) - expect(entry).toBeDefined() - expect(entry!.apiName).toBe(OPUS_48_ID) - // The entry must link to a model descriptor so the route resolves it. - expect(entry!.modelDescriptorId).toBe(OPUS_48_ID) - }) - - test('catalog entry resolves to a registered NearAI model descriptor', () => { - const model = getModelsForVendor('nearai').find(m => m.id === OPUS_48_ID) - // getModelsForVendor drops entries whose modelDescriptorId has no - // matching descriptor, so a defined result proves the route is wired. - expect(model).toBeDefined() - expect(model!.vendorId).toBe('nearai') - expect(model!.brandId).toBe('nearai') - expect(model!.defaultModel).toBe(OPUS_48_ID) - expect(model!.label).toBe('Claude Opus 4.8') - }) - - test('NearAI route points at the NearAI cloud gateway base URL', () => { - expect(getRouteDefaultBaseUrl('nearai')).toBe( - 'https://cloud-api.near.ai/v1', - ) + test('resolves route defaults, credentials, and catalog descriptor links', () => { + for (const { routeId, baseUrl, id } of [ + { routeId: 'nearai', baseUrl: 'https://cloud-api.near.ai/v1', id: OPUS_48_ID }, + ]) { + expect(resolveRouteIdFromBaseUrl(baseUrl)).toBe(routeId) + expect(getRouteDefaultBaseUrl(routeId)).toBe(baseUrl) + expect(getRouteDefaultModel(routeId)).toBe('anthropic/claude-sonnet-4-6') + expect(getRouteDescriptor(routeId)?.setup.credentialEnvVars).toEqual([ + 'NEARAI_API_KEY', + ]) + const entry = getCatalogEntriesForRoute(routeId).find(e => e.id === id) + expect(entry).toMatchObject({ apiName: id, modelDescriptorId: id }) + const model = getModelsForVendor(routeId).find(m => m.id === entry?.modelDescriptorId) + expect(model).toMatchObject({ + id, + vendorId: routeId, + brandId: 'nearai', + defaultModel: id, + label: 'Claude Opus 4.8', + contextWindow: 1_000_000, + maxOutputTokens: 65_536, + capabilities: { + supportsVision: false, + supportsStreaming: true, + supportsFunctionCalling: true, + supportsJsonMode: true, + supportsReasoning: false, + supportsPreciseTokenCount: false, + }, + }) + } }) }) diff --git a/src/integrations/tencent.test.ts b/src/integrations/tencent.test.ts index ed94753c7e..d55679fbb4 100644 --- a/src/integrations/tencent.test.ts +++ b/src/integrations/tencent.test.ts @@ -3,39 +3,46 @@ import { describe, expect, test } from 'bun:test' import { getCatalogEntriesForRoute, getModel, + getRouteDefaultBaseUrl, + resolveRouteIdFromBaseUrl, } from './index.js' import { resolveModelRuntimeLimits } from './runtimeMetadata.js' describe('Tencent HY3 descriptor', () => { - test('exposes the verified HY3 capabilities and limits to gateway catalogs', () => { - const model = getModel('tencent/hy3') + test('links gateway wire models to descriptors and resolves limits by base URL', () => { + const routeId = 'gitlawb-opengateway' + const baseUrl = 'https://opengateway.gitlawb.com/v1' + expect(resolveRouteIdFromBaseUrl(baseUrl)).toBe(routeId) + expect(getRouteDefaultBaseUrl(routeId)).toBe(baseUrl) - expect(model).toBeDefined() - expect(model).toMatchObject({ - id: 'tencent/hy3', - classification: ['chat', 'reasoning', 'coding'], - contextWindow: 262_144, - maxOutputTokens: 131_072, - capabilities: { - supportsVision: false, - supportsStreaming: true, - supportsFunctionCalling: true, - supportsJsonMode: true, - supportsReasoning: true, + for (const { id, catalogId, limits, classification, capabilities } of [ + { + id: 'tencent/hy3', + catalogId: 'opengateway-tencent-hy3', + limits: { contextWindow: 262_144, maxOutputTokens: 131_072 }, + classification: ['chat', 'reasoning', 'coding'], + capabilities: { + supportsVision: false, + supportsStreaming: true, + supportsFunctionCalling: true, + supportsJsonMode: true, + supportsReasoning: true, + }, }, - }) - - const catalogEntry = getCatalogEntriesForRoute('gitlawb-opengateway').find( - entry => entry.apiName === 'tencent/hy3', - ) - expect(catalogEntry?.modelDescriptorId).toBe(model?.id) - - expect( - resolveModelRuntimeLimits({ - model: 'tencent/hy3', - baseUrl: 'https://opengateway.gitlawb.com/v1', - processEnv: {}, - }), - ).toEqual({ contextWindow: 262_144, maxOutputTokens: 131_072 }) + ]) { + const model = getModel(id) + expect(model).toMatchObject({ + id, + defaultModel: id, + ...limits, + classification, + capabilities, + }) + const entry = getCatalogEntriesForRoute(routeId).find(e => e.id === catalogId) + expect(entry).toMatchObject({ apiName: id, modelDescriptorId: model?.id }) + expect( + resolveModelRuntimeLimits({ model: id, baseUrl, processEnv: {} }), + ).toEqual(limits) + } }) }) diff --git a/src/integrations/vendors/longcat.test.ts b/src/integrations/vendors/longcat.test.ts index eee2b6fdc7..f3ed056b40 100644 --- a/src/integrations/vendors/longcat.test.ts +++ b/src/integrations/vendors/longcat.test.ts @@ -17,10 +17,22 @@ describe('longcat vendor', () => { test('registers route defaults, catalog, and preset metadata', () => { ensureIntegrationsLoaded() + for (const baseUrl of [ + 'https://api.longcat.chat/openai', + 'https://api.longcat.chat/openai/v1', + ]) { + expect(resolveRouteIdFromBaseUrl(baseUrl)).toBe('longcat') + } + expect(getRouteDefaultBaseUrl('longcat')).toBe( + 'https://api.longcat.chat/openai/v1', + ) + expect(getRouteDefaultModel('longcat')).toBe('LongCat-2.0') + const vendor = getVendor('longcat') - expect(vendor).toBeDefined() - expect(vendor?.defaultBaseUrl).toBe('https://api.longcat.chat/openai/v1') - expect(vendor?.defaultModel).toBe('LongCat-2.0') + expect(vendor).toMatchObject({ + defaultBaseUrl: 'https://api.longcat.chat/openai/v1', + defaultModel: 'LongCat-2.0', + }) expect(vendor?.setup.credentialEnvVars).toEqual(['LONGCAT_API_KEY']) expect(vendor?.transportConfig.openaiShim?.thinkingRequestFormat).toBe( 'zai-compatible', @@ -32,27 +44,13 @@ describe('longcat vendor', () => { 'stream_options', ) - expect(getRouteDefaultBaseUrl('longcat')).toBe( - 'https://api.longcat.chat/openai/v1', - ) - expect(getRouteDefaultModel('longcat')).toBe('LongCat-2.0') - expect(resolveRouteIdFromBaseUrl('https://api.longcat.chat/openai')).toBe( - 'longcat', - ) - expect( - resolveRouteIdFromBaseUrl('https://api.longcat.chat/openai/v1'), - ).toBe('longcat') - - const catalog = getCatalogEntriesForRoute('longcat') - expect(catalog).toEqual( - expect.arrayContaining([ - expect.objectContaining({ - id: 'LongCat-2.0', - apiName: 'LongCat-2.0', - modelDescriptorId: 'LongCat-2.0', - }), - ]), + const entry = getCatalogEntriesForRoute('longcat').find( + entry => entry.id === 'LongCat-2.0', ) + expect(entry).toMatchObject({ + apiName: 'LongCat-2.0', + modelDescriptorId: 'LongCat-2.0', + }) const model = getModel('LongCat-2.0') expect(model).toMatchObject({ @@ -60,7 +58,13 @@ describe('longcat vendor', () => { vendorId: 'longcat', contextWindow: 1_048_576, maxOutputTokens: 131_072, - capabilities: expect.objectContaining({ supportsFunctionCalling: false }), + capabilities: { + supportsStreaming: true, + supportsFunctionCalling: false, + supportsJsonMode: false, + supportsReasoning: true, + supportsPreciseTokenCount: false, + }, }) const preset = getProviderPresetUiMetadata('longcat') diff --git a/src/integrations/vendors/xai.test.ts b/src/integrations/vendors/xai.test.ts index fd055f4153..2a88fcdbda 100644 --- a/src/integrations/vendors/xai.test.ts +++ b/src/integrations/vendors/xai.test.ts @@ -1,110 +1,85 @@ import { describe, expect, test } from 'bun:test' +import type { ModelCatalogEntry } from '../descriptors.js' +import { + getCatalogEntriesForRoute, + getModel, + getRouteDefaultBaseUrl, + getRouteDefaultModel, + getVendor, + resolveRouteIdFromBaseUrl, +} from '../index.js' import xai from './xai.js' const mapModel = xai.catalog?.discovery?.mapModel -function shape(id: string, extras: Record = {}) { - return { id, ...extras } -} - describe('xAI vendor hybrid catalog', () => { - test('uses hybrid discovery with curated Grok 4.6 as the default', () => { + test('registers route defaults, credentials, and descriptor links', () => { expect(mapModel).toBeDefined() expect(xai.catalog?.source).toBe('hybrid') expect(xai.catalog?.discovery?.kind).toBe('openai-compatible') - expect(xai.catalog?.discoveryCacheTtl).toBe('1d') - expect(xai.catalog?.discoveryRefreshMode).toBe('background-if-stale') - expect(xai.catalog?.allowManualRefresh).toBe(true) - expect(xai.defaultModel).toBe('grok-4.6') - expect(xai.catalog?.models?.map(model => model.apiName)).toEqual([ - 'grok-4.6', - 'grok-4.5', - 'grok-4.3', - 'grok-build-0.1', - 'grok-4.20-0309-reasoning', - 'grok-4.20-0309-non-reasoning', - ]) - }) - - test('keeps chat Grok IDs including later uncataloged releases', () => { - if (!mapModel) throw new Error('mapModel missing') - const keep = [ - 'grok-4.6', - 'grok-4.5', - 'grok-4.3', - 'grok-4.7', - 'grok-build-0.1', - 'grok-4.20-0309-reasoning', - ] - for (const id of keep) { - expect(mapModel(shape(id))).toEqual({ - id, - apiName: id, - label: id, - }) - } - }) - - test('drops Imagine, voice, STT/TTS, and embedding models', () => { - if (!mapModel) throw new Error('mapModel missing') - const drop = [ - 'grok-imagine-image', - 'grok-imagine-image-quality', - 'grok-imagine-video-1.5', - 'grok-voice-think-fast-1.0', - 'grok-voice-think-fast-2.0', - 'grok-stt-1.0', - 'grok-tts-1.0', - // This model requires xAI's Responses API and does not support the - // generic OpenAI-compatible chat transport used by live discovery. - 'grok-4.20-multi-agent-0309', - ] - for (const id of drop) { - expect(mapModel(shape(id))).toBeNull() + for (const { routeId, baseUrl, model } of [ + { routeId: 'xai', baseUrl: 'https://api.x.ai/v1', model: 'grok-4.6' }, + ]) { + expect(resolveRouteIdFromBaseUrl(baseUrl)).toBe(routeId) + expect(getRouteDefaultBaseUrl(routeId)).toBe(baseUrl) + expect(getRouteDefaultModel(routeId)).toBe(model) + expect(getVendor(routeId)?.setup.credentialEnvVars).toEqual(['XAI_API_KEY']) + const catalog = getCatalogEntriesForRoute(routeId) + expect(catalog.some(entry => entry.apiName === model)).toBe(true) + for (const entry of catalog) { + const descriptor = getModel(entry.modelDescriptorId ?? '') + expect(descriptor).toBeDefined() + expect(descriptor?.defaultModel).toBe(entry.apiName) + } } + expect(getModel('grok-4.6')).toMatchObject({ + contextWindow: 500_000, + capabilities: { + supportsVision: true, + supportsStreaming: true, + supportsFunctionCalling: true, + supportsJsonMode: true, + supportsReasoning: true, + supportsPreciseTokenCount: false, + }, + }) }) - test('drops curated aliases so hybrid merge does not duplicate them', () => { + test('maps chat discovery entries and rejects unsupported or invalid entries', () => { if (!mapModel) throw new Error('mapModel missing') - const drop = [ - 'latest', - 'grok-4.6-latest', - 'grok-4.5-latest', - 'grok-build-latest', - 'grok-latest', - 'grok-code-fast-1', + const chat = (id: string): ModelCatalogEntry => ({ id, apiName: id, label: id }) + const cases: Array<{ raw: unknown, expected: ModelCatalogEntry | null }> = [ + { raw: { id: 'grok-4.6' }, expected: chat('grok-4.6') }, + { raw: { id: 'grok-4.7' }, expected: chat('grok-4.7') }, + { raw: { id: 'grok-build-0.1' }, expected: chat('grok-build-0.1') }, + { raw: { id: 'grok-4.20-0309-reasoning' }, expected: chat('grok-4.20-0309-reasoning') }, + { raw: { id: 'grok-imagine-image' }, expected: null }, + { raw: { id: 'grok-imagine-video-1.5' }, expected: null }, + { raw: { id: 'grok-voice-think-fast-1.0' }, expected: null }, + { raw: { id: 'grok-stt-1.0' }, expected: null }, + { raw: { id: 'grok-tts-1.0' }, expected: null }, + { raw: { id: 'grok-embed-1.0' }, expected: null }, + { raw: { id: 'grok-4.20-multi-agent-0309' }, expected: null }, + { raw: { id: 'latest' }, expected: null }, + { raw: { id: 'grok-4.6-latest' }, expected: null }, + { raw: { id: 'grok-build-latest' }, expected: null }, + { raw: { id: 'grok-code-fast-1' }, expected: null }, + { raw: { id: 'grok-4.6', active: false }, expected: null }, + { raw: null, expected: null }, + { raw: {}, expected: null }, + { raw: { id: 1 }, expected: null }, + { raw: { id: '' }, expected: null }, + { raw: { id: 'other-model' }, expected: null }, + { raw: { id: 'grok-4.7', context_length: 0 }, expected: chat('grok-4.7') }, + { raw: { id: 'grok-4.7', context_length: -1 }, expected: chat('grok-4.7') }, + { raw: { id: 'grok-4.7', context_length: Number.NaN }, expected: chat('grok-4.7') }, + { + raw: { id: 'grok-4.6', context_length: 500_000 }, + expected: { ...chat('grok-4.6'), contextWindow: 500_000 }, + }, ] - for (const id of drop) { - expect(mapModel(shape(id))).toBeNull() + for (const { raw, expected } of cases) { + expect(mapModel(raw)).toEqual(expected) } }) - - test('drops inactive entries, missing ids, and non-positive context limits', () => { - if (!mapModel) throw new Error('mapModel missing') - expect(mapModel(shape('grok-4.6', { active: false }))).toBeNull() - expect(mapModel(null)).toBeNull() - expect(mapModel({})).toBeNull() - expect(mapModel({ id: 1 })).toBeNull() - expect(mapModel({ id: '' })).toBeNull() - expect(mapModel(shape('grok-4.7', { context_length: 0 }))).toEqual({ - id: 'grok-4.7', - apiName: 'grok-4.7', - label: 'grok-4.7', - }) - expect(mapModel(shape('grok-4.7', { context_length: Number.NaN }))).toEqual({ - id: 'grok-4.7', - apiName: 'grok-4.7', - label: 'grok-4.7', - }) - }) - - test('forwards the /v1/models context_length when present', () => { - if (!mapModel) throw new Error('mapModel missing') - expect(mapModel(shape('grok-4.6', { context_length: 500000 }))).toEqual({ - id: 'grok-4.6', - apiName: 'grok-4.6', - label: 'grok-4.6', - contextWindow: 500000, - }) - }) }) From 19bb39c8845a6920bb04ad8d70652ad520861b87 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 21:00:41 -0700 Subject: [PATCH 019/121] test: consolidate gateway catalog tests into per-gateway contract tables (#2325) * test: consolidate gateway catalog tests into per-gateway contract tables * test: assert opencode-go catalog excludes removed model ids --- src/integrations/gateways/api-route.test.ts | 63 +++-- src/integrations/gateways/apismart.test.ts | 41 +-- src/integrations/gateways/commandcode.test.ts | 92 +++---- src/integrations/gateways/concentrate.test.ts | 62 +++-- src/integrations/gateways/custom.test.ts | 29 +++ .../gateways/gitlawb-opengateway.test.ts | 27 +- src/integrations/gateways/gmi-cloud.test.ts | 62 +++-- src/integrations/gateways/llmtr.test.ts | 80 +++--- .../gateways/merge-gateway.test.ts | 52 ++-- src/integrations/gateways/nvidia-nim.test.ts | 25 +- src/integrations/gateways/opencode.test.ts | 241 +++++------------- src/integrations/gateways/openrouter.test.ts | 27 +- .../gateways/xiaomi-mimo-token.test.ts | 40 ++- src/test/gatewayCatalogTestHelpers.ts | 132 ++++++++++ 14 files changed, 557 insertions(+), 416 deletions(-) create mode 100644 src/test/gatewayCatalogTestHelpers.ts diff --git a/src/integrations/gateways/api-route.test.ts b/src/integrations/gateways/api-route.test.ts index 98b4824f50..3014ad26e3 100644 --- a/src/integrations/gateways/api-route.test.ts +++ b/src/integrations/gateways/api-route.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'bun:test' +import { describe, expect, test } from 'bun:test' import { routeForPreset } from '../compatibility.js' import { getProviderPresetUiMetadata } from '../providerUiMetadata.js' @@ -7,32 +7,47 @@ import { resolveRouteCredentialValue, resolveRouteIdFromBaseUrl, } from '../routeMetadata.js' -import gateway, { mapApiRouteModel } from './api-route.js' +import { mapApiRouteModel } from './api-route.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' -test('API Route uses a dedicated hybrid OpenAI-compatible gateway contract', () => { - expect(gateway.defaultBaseUrl).toBe('https://global.api-route.com/v1') - expect(gateway.defaultModel).toBe('claude-sonnet-4-6') - expect(gateway.setup.credentialEnvVars).toEqual(['API_ROUTE_API_KEY']) - expect(gateway.setup.dedicatedCredentialsOnly).toBe(false) - expect(gateway.preset?.apiKeyEnvVars).toEqual(['API_ROUTE_API_KEY']) - expect(gateway.validation).toMatchObject({ - kind: 'credential-env', - routing: { matchDefaultBaseUrl: true }, - credentialEnvVars: ['API_ROUTE_API_KEY', 'OPENAI_API_KEYS', 'OPENAI_API_KEY'], - }) - expect(gateway.transportConfig).toEqual({ - kind: 'openai-compatible', - openaiShim: { - supportsApiFormatSelection: false, - supportsAuthHeaders: false, - maxTokensField: 'max_tokens', +describe('api-route gateway', () => { + describe.each([ + { + routeId: 'api-route', + expected: { + baseUrl: 'https://global.api-route.com/v1', + defaultModel: 'claude-sonnet-4-6', + setup: { + credentialEnvVars: ['API_ROUTE_API_KEY'], + dedicatedCredentialsOnly: false, + }, + preset: { apiKeyEnvVars: ['API_ROUTE_API_KEY'] }, + validation: { + kind: 'credential-env', + routing: { matchDefaultBaseUrl: true }, + credentialEnvVars: [ + 'API_ROUTE_API_KEY', + 'OPENAI_API_KEYS', + 'OPENAI_API_KEY', + ], + }, + openaiShim: { + supportsApiFormatSelection: false, + supportsAuthHeaders: false, + maxTokensField: 'max_tokens', + }, + catalogSource: 'hybrid', + discovery: { requiresAuth: true, mapModel: mapApiRouteModel }, + } satisfies GatewayCatalogExpectations, }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + }) }) - expect(gateway.catalog?.source).toBe('hybrid') - expect(gateway.catalog?.discovery?.requiresAuth).toBe(true) - expect(gateway.catalog?.models?.map(model => model.apiName)).toEqual([ - 'claude-sonnet-4-6', - ]) }) test('API Route preset uses the existing generic profile path', () => { diff --git a/src/integrations/gateways/apismart.test.ts b/src/integrations/gateways/apismart.test.ts index 2086ddd17b..342d95ff32 100644 --- a/src/integrations/gateways/apismart.test.ts +++ b/src/integrations/gateways/apismart.test.ts @@ -1,29 +1,32 @@ import { describe, expect, test } from 'bun:test' import apismart from './apismart.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' const mapModel = apismart.catalog?.discovery?.mapModel describe('apismart gateway', () => { - test('uses hybrid discovery with dedicated credentials', () => { - expect(apismart.id).toBe('apismart') - expect(apismart.catalog?.source).toBe('hybrid') - expect(apismart.catalog?.discovery?.kind).toBe('openai-compatible') - expect(apismart.catalog?.discovery?.requiresAuth).toBe(true) - expect(apismart.setup.dedicatedCredentialsOnly).toBe(true) - expect(apismart.setup.credentialEnvVars).toEqual(['APISMART_API_KEY']) - expect(apismart.defaultBaseUrl).toBe('https://gw.apismart.ai/v1') - expect(apismart.defaultModel).toBe('DEEPSEEK_V4_FLASH') - expect(mapModel).toBeDefined() - }) - - test('curated catalog keeps ApiSmart case-sensitive model ids', () => { - const models = apismart.catalog?.models ?? [] - expect(models.some(model => model.apiName === 'DEEPSEEK_V4_FLASH')).toBe( - true, - ) - expect(models.some(model => model.apiName === 'GLM_5.2')).toBe(true) - expect(models.some(model => model.apiName === 'QWEN_3_7_MAX')).toBe(true) + describe.each([ + { + routeId: 'apismart', + expected: { + baseUrl: 'https://gw.apismart.ai/v1', + defaultModel: 'DEEPSEEK_V4_FLASH', + setup: { + credentialEnvVars: ['APISMART_API_KEY'], + dedicatedCredentialsOnly: true, + }, + catalogSource: 'hybrid', + discovery: { requiresAuth: true }, + } satisfies GatewayCatalogExpectations, + }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + }) }) test('mapModel keeps chat models and drops image/video ids', () => { diff --git a/src/integrations/gateways/commandcode.test.ts b/src/integrations/gateways/commandcode.test.ts index 8f7c2e0cce..a76cbe2886 100644 --- a/src/integrations/gateways/commandcode.test.ts +++ b/src/integrations/gateways/commandcode.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'bun:test' +import { describe, expect, test } from 'bun:test' import { routeForPreset } from '../compatibility.js' import { getProviderPresetUiMetadata } from '../providerUiMetadata.js' @@ -7,54 +7,60 @@ import { resolveRouteCredentialValue, resolveRouteIdFromBaseUrl, } from '../routeMetadata.js' -import gateway, { +import { getCommandcodeChatCompletionsModelError, mapCommandcodeModel, } from './commandcode.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' -test('Command Code uses a dedicated hybrid OpenAI-compatible gateway contract', () => { - expect(gateway.defaultBaseUrl).toBe( - 'https://api.commandcode.ai/provider/v1', - ) - expect(gateway.defaultModel).toBe('deepseek/deepseek-v4-flash') - expect(gateway.setup.credentialEnvVars).toEqual([ - 'CMD_API_KEY', - 'COMMANDCODE_API_KEY', - 'COMMAND_CODE_API_KEY', - ]) - expect(gateway.setup.dedicatedCredentialsOnly).toBe(true) - expect(gateway.preset?.apiKeyEnvVars).toEqual([ - 'CMD_API_KEY', - 'COMMANDCODE_API_KEY', - 'COMMAND_CODE_API_KEY', - ]) - expect(gateway.validation).toMatchObject({ - kind: 'credential-env', - routing: { matchDefaultBaseUrl: true }, - credentialEnvVars: [ - 'CMD_API_KEY', - 'COMMANDCODE_API_KEY', - 'COMMAND_CODE_API_KEY', - ], - }) - expect(gateway.transportConfig).toEqual({ - kind: 'openai-compatible', - openaiShim: { - requiredApiFormat: 'chat_completions', - supportsAuthHeaders: false, - maxTokensField: 'max_tokens', +describe('commandcode gateway', () => { + describe.each([ + { + routeId: 'commandcode', + expected: { + baseUrl: 'https://api.commandcode.ai/provider/v1', + defaultModel: 'deepseek/deepseek-v4-flash', + setup: { + credentialEnvVars: [ + 'CMD_API_KEY', + 'COMMANDCODE_API_KEY', + 'COMMAND_CODE_API_KEY', + ], + dedicatedCredentialsOnly: true, + }, + preset: { + apiKeyEnvVars: [ + 'CMD_API_KEY', + 'COMMANDCODE_API_KEY', + 'COMMAND_CODE_API_KEY', + ], + }, + validation: { + kind: 'credential-env', + routing: { matchDefaultBaseUrl: true }, + credentialEnvVars: [ + 'CMD_API_KEY', + 'COMMANDCODE_API_KEY', + 'COMMAND_CODE_API_KEY', + ], + }, + openaiShim: { + requiredApiFormat: 'chat_completions', + supportsAuthHeaders: false, + maxTokensField: 'max_tokens', + }, + catalogSource: 'hybrid', + discovery: { requiresAuth: false, mapModel: mapCommandcodeModel }, + } satisfies GatewayCatalogExpectations, }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + }) }) - expect(gateway.catalog?.source).toBe('hybrid') - expect(gateway.catalog?.discovery?.requiresAuth).toBe(false) - expect(gateway.catalog?.models?.map(model => model.apiName)).toEqual([ - 'deepseek/deepseek-v4-flash', - 'deepseek/deepseek-v4-pro', - 'gpt-5.6-sol', - 'moonshotai/Kimi-K2.5', - 'MiniMaxAI/MiniMax-M3', - 'z-ai/glm-5.3-flash', - ]) }) test('Command Code preset uses the existing generic profile path', () => { diff --git a/src/integrations/gateways/concentrate.test.ts b/src/integrations/gateways/concentrate.test.ts index 963c90ac9c..df325d497c 100644 --- a/src/integrations/gateways/concentrate.test.ts +++ b/src/integrations/gateways/concentrate.test.ts @@ -1,37 +1,45 @@ import { describe, expect, test } from 'bun:test' import concentrate from './concentrate.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' const mapModel = concentrate.catalog?.discovery?.mapModel describe('concentrate gateway', () => { - test('uses dynamic discovery and supports fallback auth', () => { - expect(concentrate.id).toBe('concentrate') - expect(concentrate.label).toBe('Concentrate') - expect(concentrate.category).toBe('aggregating') - expect(concentrate.defaultBaseUrl).toBe('https://api.concentrate.ai/v1') - expect(concentrate.defaultModel).toBe('deepseek-v4-flash') - expect(concentrate.supportsModelRouting).toBe(true) - - expect(concentrate.setup.requiresAuth).toBe(true) - expect(concentrate.setup.authMode).toBe('api-key') - expect(concentrate.setup.credentialEnvVars).toEqual(['CONCENTRATE_API_KEY']) - expect(concentrate.setup.dedicatedCredentialsOnly).toBeUndefined() - - expect(concentrate.catalog?.source).toBe('dynamic') - expect(concentrate.catalog?.discovery?.kind).toBe('openai-compatible') - expect(concentrate.catalog?.discovery?.requiresAuth).toBe(false) - - expect(concentrate.validation?.kind).toBe('credential-env') - expect( - (concentrate.validation as { credentialEnvVars: string[] }).credentialEnvVars, - ).toEqual([ - 'CONCENTRATE_API_KEY', - 'OPENAI_API_KEYS', - 'OPENAI_API_KEY', - ]) - - expect(mapModel).toBeDefined() + describe.each([ + { + routeId: 'concentrate', + expected: { + baseUrl: 'https://api.concentrate.ai/v1', + defaultModel: 'deepseek-v4-flash', + category: 'aggregating', + supportsModelRouting: true, + setup: { + requiresAuth: true, + authMode: 'api-key', + credentialEnvVars: ['CONCENTRATE_API_KEY'], + dedicatedCredentialsOnly: undefined, + }, + validation: { + kind: 'credential-env', + credentialEnvVars: [ + 'CONCENTRATE_API_KEY', + 'OPENAI_API_KEYS', + 'OPENAI_API_KEY', + ], + }, + catalogSource: 'dynamic', + discovery: { requiresAuth: false }, + allowEmptyCatalog: true, + } satisfies GatewayCatalogExpectations, + }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + }) }) test('mapModel keeps chat models and drops non-chat ids', () => { diff --git a/src/integrations/gateways/custom.test.ts b/src/integrations/gateways/custom.test.ts index 460e95f2eb..16b2111efa 100644 --- a/src/integrations/gateways/custom.test.ts +++ b/src/integrations/gateways/custom.test.ts @@ -1,5 +1,9 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' import { _clearRegistryForTesting, ensureIntegrationsLoaded, getCatalogForGateway } from '../index.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' describe('custom gateway', () => { beforeEach(() => { @@ -11,6 +15,31 @@ describe('custom gateway', () => { _clearRegistryForTesting() }) + describe.each([ + { + routeId: 'custom', + expected: { + baseUrl: undefined, + defaultModel: 'llama3.1:8b', + setup: { requiresAuth: false, credentialEnvVars: undefined }, + preset: { + fallbackBaseUrl: 'http://localhost:11434/v1', + apiKeyEnvVars: ['OPENAI_API_KEYS', 'OPENAI_API_KEY'], + }, + openaiShim: { + supportsApiFormatSelection: true, + supportsAuthHeaders: true, + }, + catalogSource: 'hybrid', + allowEmptyCatalog: true, + } satisfies GatewayCatalogExpectations, + }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + }) + }) + test('discovers /v1/models and maps context_length to contextWindow', async () => { const catalog = getCatalogForGateway('custom') expect(catalog?.source).toBe('hybrid') diff --git a/src/integrations/gateways/gitlawb-opengateway.test.ts b/src/integrations/gateways/gitlawb-opengateway.test.ts index e178758c04..665a31d6a1 100644 --- a/src/integrations/gateways/gitlawb-opengateway.test.ts +++ b/src/integrations/gateways/gitlawb-opengateway.test.ts @@ -1,17 +1,24 @@ import { describe, expect, test } from 'bun:test' import opengateway, { mapOpenGatewayModel } from './gitlawb-opengateway.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' describe('gitlawb-opengateway live model mapping', () => { - test('uses hybrid discovery against the public models list', () => { - expect(opengateway.catalog?.source).toBe('hybrid') - expect(opengateway.catalog?.discovery).toEqual( - expect.objectContaining({ - kind: 'openai-compatible', - requiresAuth: false, - }), - ) - expect(opengateway.catalog?.discovery?.mapModel).toBe(mapOpenGatewayModel) - expect(opengateway.startup?.probeReadiness).toBeUndefined() + describe.each([ + { + routeId: 'gitlawb-opengateway', + expected: { + catalogSource: 'hybrid', + discovery: { requiresAuth: false, mapModel: mapOpenGatewayModel }, + } satisfies GatewayCatalogExpectations, + }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + expect(opengateway.startup?.probeReadiness).toBeUndefined() + }) }) test('maps gateway routes including auto and free models', () => { diff --git a/src/integrations/gateways/gmi-cloud.test.ts b/src/integrations/gateways/gmi-cloud.test.ts index 8e43e16528..eb313c23c5 100644 --- a/src/integrations/gateways/gmi-cloud.test.ts +++ b/src/integrations/gateways/gmi-cloud.test.ts @@ -1,4 +1,4 @@ -import { expect, test } from 'bun:test' +import { describe, expect, test } from 'bun:test' import { routeForPreset } from '../compatibility.js' import { getProviderPresetUiMetadata } from '../providerUiMetadata.js' @@ -9,35 +9,43 @@ import { resolveRouteIdFromBaseUrl, } from '../routeMetadata.js' import { resolveModelRuntimeLimits } from '../runtimeMetadata.js' -import gateway, { mapGmiCloudModel } from './gmi-cloud.js' +import { mapGmiCloudModel } from './gmi-cloud.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' -test('GMI Cloud uses a dedicated-key hybrid OpenAI-compatible gateway contract', () => { - expect(gateway.defaultBaseUrl).toBe('https://api.gmi-serving.com/v1') - expect(gateway.defaultModel).toBe('Qwen/Qwen3.8-Max') - expect(gateway.setup.credentialEnvVars).toEqual(['GMI_API_KEY']) - expect(gateway.setup.dedicatedCredentialsOnly).toBe(true) - expect(gateway.preset?.apiKeyEnvVars).toEqual(['GMI_API_KEY']) - expect(gateway.validation).toMatchObject({ - kind: 'credential-env', - routing: { matchDefaultBaseUrl: true }, - credentialEnvVars: ['GMI_API_KEY'], - }) - expect(gateway.transportConfig).toEqual({ - kind: 'openai-compatible', - openaiShim: { - supportsApiFormatSelection: false, - supportsAuthHeaders: false, - maxTokensField: 'max_tokens', +describe('gmi-cloud gateway', () => { + describe.each([ + { + routeId: 'gmi-cloud', + expected: { + baseUrl: 'https://api.gmi-serving.com/v1', + defaultModel: 'Qwen/Qwen3.8-Max', + setup: { + credentialEnvVars: ['GMI_API_KEY'], + dedicatedCredentialsOnly: true, + }, + preset: { apiKeyEnvVars: ['GMI_API_KEY'] }, + validation: { + kind: 'credential-env', + routing: { matchDefaultBaseUrl: true }, + credentialEnvVars: ['GMI_API_KEY'], + }, + openaiShim: { + supportsApiFormatSelection: false, + supportsAuthHeaders: false, + maxTokensField: 'max_tokens', + }, + catalogSource: 'hybrid', + discovery: { requiresAuth: true }, + } satisfies GatewayCatalogExpectations, }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + }) }) - expect(gateway.catalog?.source).toBe('hybrid') - expect(gateway.catalog?.discovery?.requiresAuth).toBe(true) - expect(gateway.catalog?.models?.map(model => model.apiName)).toEqual([ - 'Qwen/Qwen3.8-Max', - 'Qwen/Qwen3.8-Flash', - 'deepseek-ai/DeepSeek-V4-Pro', - 'deepseek-ai/DeepSeek-V4-Flash', - ]) }) test('GMI Cloud preset resolves to its gateway route and reads GMI env vars', () => { diff --git a/src/integrations/gateways/llmtr.test.ts b/src/integrations/gateways/llmtr.test.ts index d41fb11b1f..5e53317dd7 100644 --- a/src/integrations/gateways/llmtr.test.ts +++ b/src/integrations/gateways/llmtr.test.ts @@ -1,5 +1,6 @@ -import { expect, test } from 'bun:test' +import { describe, expect, test } from 'bun:test' +import { getCatalogForGateway } from '../index.js' import { routeForPreset } from '../compatibility.js' import { getProviderPresetUiMetadata } from '../providerUiMetadata.js' import { @@ -9,39 +10,45 @@ import { } from '../routeMetadata.js' import catalog, { mapLlmtrModel } from './llmtr.models.js' import gateway from './llmtr.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' -test('LLMTR uses the standard OpenAI-compatible gateway contract', () => { - expect(gateway.defaultBaseUrl).toBe('https://llmtr.com/v1') - expect(gateway.defaultModel).toBe('deepseek/deepseek-v4-flash') - expect(gateway.setup.credentialEnvVars).toEqual([ - 'LLMTR_API_KEY', - 'OPENAI_API_KEY', - ]) - expect(gateway.setup.dedicatedCredentialsOnly).not.toBe(true) - expect(gateway.preset?.apiKeyEnvVars).toEqual([ - 'LLMTR_API_KEY', - 'OPENAI_API_KEY', - ]) - expect(gateway.validation).toMatchObject({ - kind: 'credential-env', - routing: { matchDefaultBaseUrl: true }, - credentialEnvVars: [ - 'LLMTR_API_KEY', - 'OPENAI_API_KEYS', - 'OPENAI_API_KEY', - ], - }) - expect(gateway.validation?.routing?.matchBaseUrlHosts).toBeUndefined() - expect(gateway.transportConfig).toEqual({ - kind: 'openai-compatible', - openaiShim: { - requiredApiFormat: 'chat_completions', - supportsAuthHeaders: false, - maxTokensField: 'max_tokens', +describe('llmtr gateway', () => { + describe.each([ + { + routeId: 'llmtr', + expected: { + baseUrl: 'https://llmtr.com/v1', + defaultModel: 'deepseek/deepseek-v4-flash', + setup: { credentialEnvVars: ['LLMTR_API_KEY', 'OPENAI_API_KEY'] }, + preset: { apiKeyEnvVars: ['LLMTR_API_KEY', 'OPENAI_API_KEY'] }, + validation: { + kind: 'credential-env', + routing: { matchDefaultBaseUrl: true, matchBaseUrlHosts: undefined }, + credentialEnvVars: [ + 'LLMTR_API_KEY', + 'OPENAI_API_KEYS', + 'OPENAI_API_KEY', + ], + }, + openaiShim: { + requiredApiFormat: 'chat_completions', + supportsAuthHeaders: false, + maxTokensField: 'max_tokens', + }, + catalogSource: 'hybrid', + discovery: { requiresAuth: false }, + } satisfies GatewayCatalogExpectations, }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + expect(gateway.setup.dedicatedCredentialsOnly).not.toBe(true) + expect(getCatalogForGateway(routeId)).toBe(catalog) + }) }) - expect(catalog.source).toBe('hybrid') - expect(catalog.discovery?.requiresAuth).toBe(false) }) test('LLMTR preset uses the existing generic profile path', () => { @@ -174,14 +181,3 @@ test.each([ }), ).toBeNull() }) - -test('LLMTR curated models are a small tool-capable fallback catalog', () => { - expect(catalog.models?.map(model => model.apiName)).toEqual([ - 'deepseek/deepseek-v4-flash', - 'anthropic/claude-sonnet-4.6', - 'openai/gpt-5.4', - 'google/gemini-3.1-pro-preview', - 'deepseek/deepseek-v4-pro', - 'zai/glm-5.2', - ]) -}) diff --git a/src/integrations/gateways/merge-gateway.test.ts b/src/integrations/gateways/merge-gateway.test.ts index c0858637a4..34ea58ddbb 100644 --- a/src/integrations/gateways/merge-gateway.test.ts +++ b/src/integrations/gateways/merge-gateway.test.ts @@ -4,7 +4,7 @@ import { join } from 'node:path' import { describe, expect, mock, test } from 'bun:test' -import '../index.js' +import { getCatalogForGateway } from '../index.js' import { getRouteCredentialEnvVars, resolveActiveRouteIdFromEnv, @@ -18,6 +18,10 @@ import { } from '../../test/sharedMutationLock.js' import { asMockFetch } from '../../test/typedMocks.js' import mergeGateway from './merge-gateway.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' const mapModel = mergeGateway.catalog?.discovery?.mapModel type OpenAIShimClient = { @@ -29,28 +33,30 @@ type OpenAIShimClient = { } describe('Merge Gateway', () => { - test('uses a dedicated OpenAI-compatible gateway route', () => { - expect(mergeGateway.id).toBe('merge-gateway') - expect(mergeGateway.defaultBaseUrl).toBe( - 'https://api-gateway.merge.dev/v1/openai', - ) - expect(mergeGateway.setup.credentialEnvVars).toEqual([ - 'MERGE_GATEWAY_API_KEY', - ]) - expect(mergeGateway.setup.dedicatedCredentialsOnly).toBe(true) - expect(mergeGateway.transportConfig.kind).toBe('openai-compatible') - expect( - mergeGateway.transportConfig.openaiShim?.supportsApiFormatSelection, - ).toBe(true) - }) - - test('keeps routing policy and concrete model fallbacks in the catalog', () => { - expect(mergeGateway.catalog?.source).toBe('hybrid') - expect(mergeGateway.catalog?.discovery?.requiresAuth).toBe(true) - expect(mergeGateway.catalog?.models?.map(model => model.apiName)).toEqual([ - 'default_routing', - 'openai/gpt-5.5', - ]) + describe.each([ + { + routeId: 'merge-gateway', + expected: { + baseUrl: 'https://api-gateway.merge.dev/v1/openai', + defaultModel: 'openai/gpt-5.5', + setup: { + credentialEnvVars: ['MERGE_GATEWAY_API_KEY'], + dedicatedCredentialsOnly: true, + }, + openaiShim: { supportsApiFormatSelection: true }, + catalogSource: 'hybrid', + discovery: { requiresAuth: true }, + } satisfies GatewayCatalogExpectations, + }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + expect( + getCatalogForGateway(routeId)?.models?.some( + model => model.apiName === 'default_routing', + ), + ).toBe(true) + }) }) test('--provider selects the route and uses only its dedicated credential', async () => { diff --git a/src/integrations/gateways/nvidia-nim.test.ts b/src/integrations/gateways/nvidia-nim.test.ts index bb7b396293..96d5a2a3d6 100644 --- a/src/integrations/gateways/nvidia-nim.test.ts +++ b/src/integrations/gateways/nvidia-nim.test.ts @@ -8,6 +8,10 @@ import { describe, expect, test } from 'bun:test' import nvidiaNim from './nvidia-nim.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' const mapModel = nvidiaNim.catalog?.discovery?.mapModel @@ -16,13 +20,20 @@ function shape(id: string, extras: Record = {}) { } describe('nvidia-nim gateway mapModel filter', () => { - test('mapModel is wired and gateway uses hybrid catalog', () => { - expect(mapModel).toBeDefined() - expect(nvidiaNim.catalog?.source).toBe('hybrid') - expect(nvidiaNim.catalog?.discovery?.kind).toBe('openai-compatible') - expect(nvidiaNim.catalog?.discoveryCacheTtl).toBe('1d') - expect(nvidiaNim.catalog?.discoveryRefreshMode).toBe('background-if-stale') - expect(nvidiaNim.catalog?.allowManualRefresh).toBe(true) + describe.each([ + { + routeId: 'nvidia-nim', + expected: { + catalogSource: 'hybrid', + discoveryRefreshMode: 'background-if-stale', + allowManualRefresh: true, + } satisfies GatewayCatalogExpectations, + }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + expect(nvidiaNim.catalog?.discoveryCacheTtl).toBeDefined() + }) }) test('keeps chat / instruct / reasoning / code models', () => { diff --git a/src/integrations/gateways/opencode.test.ts b/src/integrations/gateways/opencode.test.ts index aecf6829a5..e3aa08913b 100644 --- a/src/integrations/gateways/opencode.test.ts +++ b/src/integrations/gateways/opencode.test.ts @@ -12,6 +12,10 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' beforeEach(async () => { await acquireSharedMutationLock('integrations/gateways/opencode.test.ts') @@ -28,6 +32,55 @@ afterEach(() => { } }) +describe('OpenCode gateway catalogs', () => { + describe.each([ + { + routeId: 'opencode', + expected: { + baseUrl: 'https://opencode.ai/zen/v1', + defaultModel: 'gpt-5.4', + category: 'aggregating', + setup: { + requiresAuth: true, + authMode: 'api-key', + credentialEnvVars: ['OPENCODE_API_KEY'], + }, + preset: { + id: 'opencode', + vendorId: 'openai', + apiKeyEnvVars: ['OPENCODE_API_KEY'], + }, + validation: { kind: 'credential-env' }, + usage: { supported: false }, + catalogSource: 'static', + } satisfies GatewayCatalogExpectations, + }, + { + routeId: 'opencode-go', + expected: { + baseUrl: 'https://opencode.ai/zen/go/v1', + defaultModel: 'glm-5.1', + category: 'aggregating', + setup: { + requiresAuth: true, + authMode: 'api-key', + credentialEnvVars: ['OPENCODE_API_KEY'], + }, + preset: { + id: 'opencode-go', + vendorId: 'openai', + apiKeyEnvVars: ['OPENCODE_API_KEY'], + }, + catalogSource: 'static', + } satisfies GatewayCatalogExpectations, + }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + }) + }) +}) + // --------------------------------------------------------------------------- // Zen Gateway Descriptor Tests // --------------------------------------------------------------------------- @@ -39,77 +92,11 @@ describe('OpenCode Zen gateway descriptor', () => { expect(gateway!.id).toBe('opencode') }) - test('has correct label', () => { - const gateway = getGateway('opencode') - expect(gateway!.label).toBe('OpenCode Zen') - }) - - test('has aggregating category', () => { - const gateway = getGateway('opencode') - expect(gateway!.category).toBe('aggregating') - }) - - test('has correct default base URL', () => { - const gateway = getGateway('opencode') - expect(gateway!.defaultBaseUrl).toBe('https://opencode.ai/zen/v1') - }) - - test('has correct default model', () => { - const gateway = getGateway('opencode') - expect(gateway!.defaultModel).toBe('gpt-5.4') - }) - - test('requires auth', () => { - const gateway = getGateway('opencode') - expect(gateway!.setup.requiresAuth).toBe(true) - }) - - test('uses api-key auth mode', () => { - const gateway = getGateway('opencode') - expect(gateway!.setup.authMode).toBe('api-key') - }) - - test('has OPENCODE_API_KEY in credential env vars', () => { - const gateway = getGateway('opencode') - expect(gateway!.setup.credentialEnvVars).toContain('OPENCODE_API_KEY') - }) - - test('has openai-compatible transport kind', () => { - const gateway = getGateway('opencode') - expect(gateway!.transportConfig.kind).toBe('openai-compatible') - }) - - test('has preset metadata', () => { - const gateway = getGateway('opencode') - expect(gateway!.preset).toBeDefined() - expect(gateway!.preset!.id).toBe('opencode') - expect(gateway!.preset!.vendorId).toBe('openai') - expect(gateway!.preset!.apiKeyEnvVars).toContain('OPENCODE_API_KEY') - }) - - test('has validation metadata', () => { - const gateway = getGateway('opencode') - expect(gateway!.validation).toBeDefined() - expect(gateway!.validation!.kind).toBe('credential-env') - }) - - test('has catalog with static source', () => { - const gateway = getGateway('opencode') - expect(gateway!.catalog).toBeDefined() - expect(gateway!.catalog!.source).toBe('static') - }) - test('has static models in catalog', () => { const gateway = getGateway('opencode') expect(gateway!.catalog!.models).toBeDefined() expect(gateway!.catalog!.models!.length).toBeGreaterThan(0) }) - - test('has usage metadata', () => { - const gateway = getGateway('opencode') - expect(gateway!.usage).toBeDefined() - expect(gateway!.usage!.supported).toBe(false) - }) }) // --------------------------------------------------------------------------- @@ -123,60 +110,6 @@ describe('OpenCode Go gateway descriptor', () => { expect(gateway!.id).toBe('opencode-go') }) - test('has correct label', () => { - const gateway = getGateway('opencode-go') - expect(gateway!.label).toBe('OpenCode Go') - }) - - test('has aggregating category', () => { - const gateway = getGateway('opencode-go') - expect(gateway!.category).toBe('aggregating') - }) - - test('has correct default base URL', () => { - const gateway = getGateway('opencode-go') - expect(gateway!.defaultBaseUrl).toBe('https://opencode.ai/zen/go/v1') - }) - - test('has correct default model', () => { - const gateway = getGateway('opencode-go') - expect(gateway!.defaultModel).toBe('glm-5.1') - }) - - test('requires auth', () => { - const gateway = getGateway('opencode-go') - expect(gateway!.setup.requiresAuth).toBe(true) - }) - - test('uses api-key auth mode', () => { - const gateway = getGateway('opencode-go') - expect(gateway!.setup.authMode).toBe('api-key') - }) - - test('has OPENCODE_API_KEY in credential env vars', () => { - const gateway = getGateway('opencode-go') - expect(gateway!.setup.credentialEnvVars).toContain('OPENCODE_API_KEY') - }) - - test('has openai-compatible transport kind', () => { - const gateway = getGateway('opencode-go') - expect(gateway!.transportConfig.kind).toBe('openai-compatible') - }) - - test('has preset metadata with vendorId openai', () => { - const gateway = getGateway('opencode-go') - expect(gateway!.preset).toBeDefined() - expect(gateway!.preset!.id).toBe('opencode-go') - expect(gateway!.preset!.vendorId).toBe('openai') - expect(gateway!.preset!.apiKeyEnvVars).toContain('OPENCODE_API_KEY') - }) - - test('has catalog with static source', () => { - const gateway = getGateway('opencode-go') - expect(gateway!.catalog).toBeDefined() - expect(gateway!.catalog!.source).toBe('static') - }) - test('has static models in catalog', () => { const gateway = getGateway('opencode-go') expect(gateway!.catalog!.models).toBeDefined() @@ -282,57 +215,6 @@ describe('OpenCode model catalog', () => { } }) - test('zen model count matches expected', () => { - const models = getCatalogEntriesForRoute('opencode') - expect(models.length).toBe(48) - }) - - test('go model count matches expected', () => { - const models = getCatalogEntriesForRoute('opencode-go') - expect(models.length).toBe(13) - }) - - test('go model set matches opencode.ai/go catalog exactly', () => { - const models = getCatalogEntriesForRoute('opencode-go') - const expectedIds = new Set([ - 'opencode-go-glm-5.2', - 'opencode-go-qwen3.7-max', - 'opencode-go-kimi-k2.7-code', - 'opencode-go-mimo-v2.5-pro', - 'opencode-go-deepseek-v4-pro', - 'opencode-go-qwen3.7-plus', - 'opencode-go-minimax-m3', - 'opencode-go-mimo-v2.5', - 'opencode-go-deepseek-v4-flash', - 'opencode-go-glm-5.1', - 'opencode-go-kimi-k2.6', - 'opencode-go-qwen3.6-plus', - 'opencode-go-minimax-m2.7', - ]) - const actualIds = new Set(models.map(m => m.id)) - // All expected IDs present - for (const id of expectedIds) { - expect(actualIds.has(id)).toBe(true) - } - // No removed or unexpected IDs remain - for (const id of actualIds) { - expect(expectedIds.has(id)).toBe(true) - } - // Explicitly verify removed models are not included - const removedIds = [ - 'opencode-go-minimax-m2.5', - 'opencode-go-kimi-k2.5', - 'opencode-go-glm-5', - 'opencode-go-qwen3.5-plus', - 'opencode-go-mimo-v2-pro', - 'opencode-go-mimo-v2-omni', - 'opencode-go-hy3-preview', - ]; - for (const id of removedIds) { - expect(actualIds.has(id)).toBe(false); - } - }) - test('all zen gpt models have modelDescriptorId', () => { const models = getCatalogEntriesForRoute('opencode') const gptModels = models.filter(m => m.apiName.startsWith('gpt-')) @@ -359,6 +241,23 @@ describe('OpenCode model catalog', () => { } }) + test('go catalog excludes removed model ids', () => { + const models = getCatalogEntriesForRoute('opencode-go') + const ids = models.map(m => m.id) + const removedModelIds = [ + 'opencode-go-minimax-m2.5', + 'opencode-go-kimi-k2.5', + 'opencode-go-glm-5', + 'opencode-go-qwen3.5-plus', + 'opencode-go-mimo-v2-pro', + 'opencode-go-mimo-v2-omni', + 'opencode-go-hy3-preview', + ] + for (const id of removedModelIds) { + expect(ids).not.toContain(id) + } + }) + test('go Anthropic-format models use the messages endpoint with x-api-key auth', () => { const models = getCatalogEntriesForRoute('opencode-go') const messagesModelIds = [ diff --git a/src/integrations/gateways/openrouter.test.ts b/src/integrations/gateways/openrouter.test.ts index 70e6a9ee97..3bf2ffb124 100644 --- a/src/integrations/gateways/openrouter.test.ts +++ b/src/integrations/gateways/openrouter.test.ts @@ -1,16 +1,23 @@ import { describe, expect, test } from 'bun:test' -import openrouter, { mapOpenRouterModel } from './openrouter.js' +import { mapOpenRouterModel } from './openrouter.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' describe('openrouter gateway live model mapping', () => { - test('uses hybrid discovery against the public models list', () => { - expect(openrouter.catalog?.source).toBe('hybrid') - expect(openrouter.catalog?.discovery).toEqual( - expect.objectContaining({ - kind: 'openai-compatible', - requiresAuth: false, - }), - ) - expect(openrouter.catalog?.discovery?.mapModel).toBe(mapOpenRouterModel) + describe.each([ + { + routeId: 'openrouter', + expected: { + catalogSource: 'hybrid', + discovery: { requiresAuth: false, mapModel: mapOpenRouterModel }, + } satisfies GatewayCatalogExpectations, + }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + }) }) test('maps coding models with context length and free labels', () => { diff --git a/src/integrations/gateways/xiaomi-mimo-token.test.ts b/src/integrations/gateways/xiaomi-mimo-token.test.ts index 385f6960a9..5772021baa 100644 --- a/src/integrations/gateways/xiaomi-mimo-token.test.ts +++ b/src/integrations/gateways/xiaomi-mimo-token.test.ts @@ -6,24 +6,38 @@ import { getRouteDefaultModel, resolveRouteIdFromBaseUrl, } from '../routeMetadata.js' +import { + expectGatewayCatalog, + type GatewayCatalogExpectations, +} from '../../test/gatewayCatalogTestHelpers.js' describe('xiaomi-mimo-token gateway', () => { - test('default base URL is the token-plan SGP endpoint', () => { - expect(getRouteDefaultBaseUrl('xiaomi-mimo-token')).toBe( - 'https://token-plan-sgp.xiaomimimo.com/v1', - ) - }) - - test('default model is mimo-v2.5-pro', () => { - expect(getRouteDefaultModel('xiaomi-mimo-token')).toBe('mimo-v2.5-pro') + describe.each([ + { + routeId: 'xiaomi-mimo-token', + expected: { + baseUrl: 'https://token-plan-sgp.xiaomimimo.com/v1', + defaultModel: 'mimo-v2.5-pro', + setup: { credentialEnvVars: ['MIMO_API_KEY'] }, + preset: { apiKeyEnvVars: ['MIMO_API_KEY'] }, + validation: { + kind: 'credential-env', + credentialEnvVars: ['MIMO_API_KEY', 'OPENAI_API_KEY'], + }, + catalogSource: 'static', + } satisfies GatewayCatalogExpectations, + }, + ])('$routeId catalog', ({ routeId, expected }) => { + test('satisfies the registered gateway catalog contract', () => { + expectGatewayCatalog(routeId, expected) + expect(getRouteDefaultBaseUrl(routeId)).toBe(expected.baseUrl) + expect(getRouteDefaultModel(routeId)).toBe(expected.defaultModel) + }) }) - test('catalog is limited to supported v2.5 chat models with OpenAI-compatible reasoning effort', () => { + test('catalog models use OpenAI-compatible reasoning effort', () => { const entries = getCatalogEntriesForRoute('xiaomi-mimo-token') - expect(entries.map(model => model.apiName)).toEqual([ - 'mimo-v2.5-pro', - 'mimo-v2.5', - ]) + expect(entries.length).toBeGreaterThan(0) for (const entry of entries) { expect(entry.reasoning).toEqual({ mode: 'levels', diff --git a/src/test/gatewayCatalogTestHelpers.ts b/src/test/gatewayCatalogTestHelpers.ts new file mode 100644 index 0000000000..d30ca8cac5 --- /dev/null +++ b/src/test/gatewayCatalogTestHelpers.ts @@ -0,0 +1,132 @@ +import { expect } from 'bun:test' + +import type { + GatewayDescriptor, + ModelCatalogConfig, + ModelDiscoveryConfig, + OpenAIShimTransportConfig, + ValidationMetadata, + ValidationRoutingMetadata, +} from '../integrations/descriptors.js' +import { parseDurationString } from '../integrations/discoveryCache.js' +import { + ensureIntegrationsLoaded, + getCatalogForGateway, + getGateway, + getModel, +} from '../integrations/index.js' + +type CredentialValidation = Extract + +export type GatewayCatalogExpectations = { + baseUrl?: GatewayDescriptor['defaultBaseUrl'] + defaultModel?: GatewayDescriptor['defaultModel'] + category?: GatewayDescriptor['category'] + supportsModelRouting?: GatewayDescriptor['supportsModelRouting'] + setup?: Partial + preset?: Partial> + validation?: Omit, 'routing'> & { + routing?: Partial + } + openaiShim?: Partial + discovery?: Partial + catalogSource: ModelCatalogConfig['source'] + discoveryRefreshMode?: ModelCatalogConfig['discoveryRefreshMode'] + allowManualRefresh?: ModelCatalogConfig['allowManualRefresh'] + usage?: Partial> + allowEmptyCatalog?: boolean +} + +// Iterating supplied keys preserves explicit undefined expectations. +function expectFields( + actual: T | undefined, + expected: Partial, +): void { + expect(actual).toBeDefined() + if (!actual) throw new Error('Expected gateway metadata to exist') + for (const key of Object.keys(expected) as (keyof T)[]) { + expect(actual[key]).toEqual(expected[key]) + } +} + +export function expectGatewayCatalog( + routeId: string, + expected: GatewayCatalogExpectations, +): void { + ensureIntegrationsLoaded() + const gateway = getGateway(routeId) + const catalog = getCatalogForGateway(routeId) + expect(gateway).toBeDefined() + expect(catalog).toBeDefined() + if (!gateway || !catalog) throw new Error(`Missing gateway catalog: ${routeId}`) + + expect(gateway.id).toBe(routeId) + expect(gateway.label.trim().length).toBeGreaterThan(0) + if ('baseUrl' in expected) expect(gateway.defaultBaseUrl).toBe(expected.baseUrl) + if ('defaultModel' in expected) { + expect(gateway.defaultModel).toBe(expected.defaultModel) + } + if ('category' in expected) expect(gateway.category).toBe(expected.category) + if ('supportsModelRouting' in expected) { + expect(gateway.supportsModelRouting).toBe(expected.supportsModelRouting) + } + if (expected.setup) expectFields(gateway.setup, expected.setup) + if (expected.preset) expectFields(gateway.preset, expected.preset) + if (expected.validation) { + const { routing, ...validation } = expected.validation + expectFields(gateway.validation, validation) + if (routing) expectFields(gateway.validation?.routing, routing) + } + if (expected.usage) expectFields(gateway.usage, expected.usage) + expect(gateway.transportConfig.kind).toBe('openai-compatible') + if (expected.openaiShim) { + expectFields(gateway.transportConfig.openaiShim, expected.openaiShim) + } + + expect(catalog.source).toBe(expected.catalogSource) + if (catalog.source === 'dynamic' || catalog.source === 'hybrid') { + expect(catalog.discovery?.kind).toBe('openai-compatible') + expect(typeof catalog.discovery?.mapModel).toBe('function') + } + if (expected.discovery) { + const { mapModel, ...discovery } = expected.discovery + expectFields(catalog.discovery, discovery) + if ('mapModel' in expected.discovery) { + expect(catalog.discovery?.mapModel).toBe(mapModel) + } + } + if (catalog.discoveryCacheTtl !== undefined) { + const ttl = parseDurationString(catalog.discoveryCacheTtl) + expect(Number.isFinite(ttl)).toBe(true) + expect(ttl).toBeGreaterThan(0) + } + if ('discoveryRefreshMode' in expected) { + expect(catalog.discoveryRefreshMode).toBe(expected.discoveryRefreshMode) + } + if ('allowManualRefresh' in expected) { + expect(catalog.allowManualRefresh).toBe(expected.allowManualRefresh) + } + + // Raw entries include hidden and expired models filtered out by route lookup. + const entries = catalog.models ?? [] + if (!expected.allowEmptyCatalog) { + expect(entries.length).toBeGreaterThan(0) + if (gateway.defaultModel !== undefined) { + expect( + entries.some(entry => entry.apiName === gateway.defaultModel), + ).toBe(true) + } + } + expect(new Set(entries.map(entry => entry.id)).size).toBe(entries.length) + expect(new Set(entries.map(entry => entry.apiName)).size).toBe(entries.length) + for (const entry of entries) { + expect(entry.id.trim().length).toBeGreaterThan(0) + expect(entry.apiName.trim().length).toBeGreaterThan(0) + if (entry.label !== undefined) { + expect(entry.label.trim().length).toBeGreaterThan(0) + } + if (entry.modelDescriptorId !== undefined) { + expect(getModel(entry.modelDescriptorId)).toBeDefined() + } + } +} From a8b9fa9affd13b398c0f56885a1014b44fa92868 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 21:24:45 -0700 Subject: [PATCH 020/121] test: consolidate Z.AI GLM-5.3 registry catalog tests into a golden table (#2326) --- src/integrations/runtimeMetadata.test.ts | 173 ++++++++++++----------- 1 file changed, 89 insertions(+), 84 deletions(-) diff --git a/src/integrations/runtimeMetadata.test.ts b/src/integrations/runtimeMetadata.test.ts index 58b5ba5861..517563ab23 100644 --- a/src/integrations/runtimeMetadata.test.ts +++ b/src/integrations/runtimeMetadata.test.ts @@ -26,93 +26,98 @@ import glmModels from './models/glm.js' import zaiVendor from './vendors/zai.js' describe('Z.AI GLM-5.3 descriptor contract', () => { - it('wires the verified GLM-5.3-Flash descriptor and direct catalog contract', () => { - const model = glmModels.find(candidate => candidate.id === 'glm-5.3-flash') - expect(model).toMatchObject({ - id: 'glm-5.3-flash', - label: 'GLM 5.3 Flash', - vendorId: 'zai', - brandId: 'glm', - classification: ['chat', 'reasoning', 'vision', 'coding'], - defaultModel: 'glm-5.3-flash', - contextWindow: 1_000_000, - maxOutputTokens: 131_072, - runtimeMetadataScope: 'catalog', - capabilities: { - supportsVision: true, - supportsStreaming: true, - supportsFunctionCalling: true, - supportsJsonMode: true, - supportsReasoning: true, - supportsPreciseTokenCount: false, - }, - }) + const golden = { + zai: { + defaultModel: 'glm-5.2', + catalogSource: 'static' as const, + models: [ + { + id: 'glm-5.3-flash', + label: 'GLM 5.3 Flash', + catalogLabel: 'GLM-5.3-Flash', + vendorId: 'zai', + brandId: 'glm', + classification: ['chat', 'reasoning', 'vision', 'coding'], + defaultModel: 'glm-5.3-flash', + contextWindow: 1_000_000, + maxOutputTokens: 131_072, + runtimeMetadataScope: 'catalog', + capabilities: { + supportsVision: true, + supportsStreaming: true, + supportsFunctionCalling: true, + supportsJsonMode: true, + supportsReasoning: true, + supportsPreciseTokenCount: false, + }, + }, + { + id: 'glm-5.3', + label: 'GLM 5.3', + catalogLabel: 'GLM-5.3', + vendorId: 'zai', + brandId: 'glm', + classification: ['chat', 'reasoning', 'coding'], + defaultModel: 'glm-5.3', + contextWindow: 1_000_000, + maxOutputTokens: 131_072, + runtimeMetadataScope: 'catalog', + capabilities: { + supportsVision: false, + supportsStreaming: true, + supportsFunctionCalling: true, + supportsJsonMode: true, + supportsReasoning: true, + supportsPreciseTokenCount: false, + }, + }, + ], + }, + } - expect(glmBrand.modelIds?.[0]).toBe('glm-5.3-flash') - expect(glmBrand.modelIds?.[1]).toBe('glm-5.3') - - const catalogEntries = zaiVendor.catalog?.models ?? [] - const flashEntries = catalogEntries.filter(entry => entry.id === 'glm-5.3-flash') - expect(flashEntries).toHaveLength(1) - expect(catalogEntries[0]).toMatchObject({ - id: 'glm-5.3-flash', - apiName: 'glm-5.3-flash', - label: 'GLM-5.3-Flash', - modelDescriptorId: 'glm-5.3-flash', - reasoning: { - mode: 'levels', - levels: ['low', 'high', 'xhigh'], - wireFormat: 'zai_compatible', - }, - transportOverrides: { - openaiShim: { enableToolStreaming: true }, - }, - }) - expect(catalogEntries[1]?.id).toBe('glm-5.3') - expect(zaiVendor.catalog?.source).toBe('static') - expect(zaiVendor.defaultModel).toBe('glm-5.2') - }) + for (const [routeId, route] of Object.entries(golden)) { + it(`preserves the golden model descriptor and catalog contracts for ${routeId}`, () => { + expect(zaiVendor.id).toBe(routeId) + expect(zaiVendor.defaultModel).toBe(route.defaultModel) + expect(zaiVendor.catalog?.source).toBe(route.catalogSource) - it('wires the verified shared model, brand, and direct catalog entry without changing the default', () => { - const model = glmModels.find(candidate => candidate.id === 'glm-5.3') - expect(model).toMatchObject({ - id: 'glm-5.3', - label: 'GLM 5.3', - vendorId: 'zai', - brandId: 'glm', - classification: ['chat', 'reasoning', 'coding'], - defaultModel: 'glm-5.3', - contextWindow: 1_000_000, - maxOutputTokens: 131_072, - runtimeMetadataScope: 'catalog', - capabilities: { - supportsVision: false, - supportsStreaming: true, - supportsFunctionCalling: true, - supportsJsonMode: true, - supportsReasoning: true, - supportsPreciseTokenCount: false, - }, - }) - expect(glmBrand.modelIds).toContain('glm-5.3') - - const catalogEntry = zaiVendor.catalog?.models?.find(entry => entry.id === 'glm-5.3') - expect(catalogEntry).toMatchObject({ - id: 'glm-5.3', - apiName: 'glm-5.3', - label: 'GLM-5.3', - modelDescriptorId: 'glm-5.3', - reasoning: { - mode: 'levels', - levels: ['low', 'high', 'xhigh'], - wireFormat: 'zai_compatible', - }, - transportOverrides: { - openaiShim: { enableToolStreaming: true }, - }, + for (const expected of route.models) { + const { catalogLabel, ...descriptor } = expected + const model = glmModels.find(candidate => candidate.id === expected.id) + expect(model).toMatchObject(descriptor) + expect(glmBrand.modelIds).toContain(expected.id) + + const catalogEntry = zaiVendor.catalog?.models?.find( + entry => entry.id === expected.id, + ) + expect(catalogEntry).toMatchObject({ + id: expected.id, + apiName: expected.id, + label: catalogLabel, + modelDescriptorId: expected.id, + reasoning: { + mode: 'levels', + levels: ['low', 'high', 'xhigh'], + wireFormat: 'zai_compatible', + }, + transportOverrides: { + openaiShim: { enableToolStreaming: true }, + }, + }) + + expect( + resolveModelRuntimeLimits({ + model: expected.id, + baseUrl: zaiVendor.defaultBaseUrl, + processEnv: { CLAUDE_CODE_USE_OPENAI: '1' }, + }), + ).toEqual({ + contextWindow: expected.contextWindow, + maxOutputTokens: expected.maxOutputTokens, + }) + } }) - expect(zaiVendor.defaultModel).toBe('glm-5.2') - }) + } }) // The discovery cache resolves its path through getClaudeConfigHomeDir(), which From 3a299281bacfb14bcf73a8c7d871477fbe2c6f49 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 21:28:33 -0700 Subject: [PATCH 021/121] test(aimlapi): consolidate malformed lease-receipt tests into one table (#2327) --- src/integrations/aimlapi/topupState.test.ts | 93 ++++++--------------- 1 file changed, 25 insertions(+), 68 deletions(-) diff --git a/src/integrations/aimlapi/topupState.test.ts b/src/integrations/aimlapi/topupState.test.ts index e20fdc871f..90483e4925 100644 --- a/src/integrations/aimlapi/topupState.test.ts +++ b/src/integrations/aimlapi/topupState.test.ts @@ -953,8 +953,18 @@ test('a receipt that fails schema validation fails closed instead of being claim ) }) -for (const leasePairField of ['exchangeLease', 'keyMintLease'] as const) { - test(`a receipt with a malformed ${leasePairField}Owner fails closed instead of being claimed over as empty`, () => { +test.each([ + ['exchangeLease / malformed owner', 'exchangeLease', 12345, 'now'], + ['exchangeLease / malformed timestamp', 'exchangeLease', 'some-owner', 'not-a-number'], + ['exchangeLease / missing timestamp', 'exchangeLease', 'some-owner', undefined], + ['exchangeLease / missing owner', 'exchangeLease', undefined, 'now'], + ['keyMintLease / malformed owner', 'keyMintLease', 12345, 'now'], + ['keyMintLease / malformed timestamp', 'keyMintLease', 'some-owner', 'not-a-number'], + ['keyMintLease / missing timestamp', 'keyMintLease', 'some-owner', undefined], + ['keyMintLease / missing owner', 'keyMintLease', undefined, 'now'], +] as const)( + 'malformed lease pairs fail closed: %s', + (_label, leasePairField, owner, timestamp) => { const directory = useTemporaryConfig() const claimed = claimAimlapiTopupState(intent) saveAimlapiTopupState({ @@ -964,77 +974,24 @@ for (const leasePairField of ['exchangeLease', 'keyMintLease'] as const) { }) const statePath = join(directory, 'aimlapi-topup.json') const seeded = JSON.parse(readFileSync(statePath, 'utf8')) - // A non-string owner: isLeaseLive would read this as merely "not - // currently live" on its own, but the whole receipt must still be - // rejected as untrustworthy rather than silently claimed over. - seeded[`${leasePairField}Owner`] = 12345 - seeded[`${leasePairField}At`] = Date.now() + const now = Date.now() + if (owner === undefined) { + delete seeded[`${leasePairField}Owner`] + } else { + seeded[`${leasePairField}Owner`] = owner + } + if (timestamp === undefined) { + delete seeded[`${leasePairField}At`] + } else { + seeded[`${leasePairField}At`] = timestamp === 'now' ? now : timestamp + } writeFileSync(statePath, JSON.stringify(seeded)) expect(() => claimAimlapiTopupState({ ...intent, amountUsdMinor: 5000 })).toThrow( /does not match the expected format/, ) - }) - - test(`a receipt with a malformed ${leasePairField}At fails closed instead of being claimed over as empty`, () => { - const directory = useTemporaryConfig() - const claimed = claimAimlapiTopupState(intent) - saveAimlapiTopupState({ - ...intent, - paymentSessionId: claimed.paymentSessionId, - resumeSessionToken: 'live-resume-token', - }) - const statePath = join(directory, 'aimlapi-topup.json') - const seeded = JSON.parse(readFileSync(statePath, 'utf8')) - seeded[`${leasePairField}Owner`] = 'some-owner' - seeded[`${leasePairField}At`] = 'not-a-number' - writeFileSync(statePath, JSON.stringify(seeded)) - - expect(() => claimAimlapiTopupState({ ...intent, amountUsdMinor: 5000 })).toThrow( - /does not match the expected format/, - ) - }) - - test(`a receipt with a one-sided ${leasePairField} pair (owner with no timestamp) fails closed`, () => { - const directory = useTemporaryConfig() - const claimed = claimAimlapiTopupState(intent) - saveAimlapiTopupState({ - ...intent, - paymentSessionId: claimed.paymentSessionId, - resumeSessionToken: 'live-resume-token', - }) - const statePath = join(directory, 'aimlapi-topup.json') - const seeded = JSON.parse(readFileSync(statePath, 'utf8')) - seeded[`${leasePairField}Owner`] = 'some-owner' - // No matching *At field — the old, independent-field validation would - // have accepted this (each field was only checked in isolation). - delete seeded[`${leasePairField}At`] - writeFileSync(statePath, JSON.stringify(seeded)) - - expect(() => claimAimlapiTopupState({ ...intent, amountUsdMinor: 5000 })).toThrow( - /does not match the expected format/, - ) - }) - - test(`a receipt with a one-sided ${leasePairField} pair (timestamp with no owner) fails closed`, () => { - const directory = useTemporaryConfig() - const claimed = claimAimlapiTopupState(intent) - saveAimlapiTopupState({ - ...intent, - paymentSessionId: claimed.paymentSessionId, - resumeSessionToken: 'live-resume-token', - }) - const statePath = join(directory, 'aimlapi-topup.json') - const seeded = JSON.parse(readFileSync(statePath, 'utf8')) - delete seeded[`${leasePairField}Owner`] - seeded[`${leasePairField}At`] = Date.now() - writeFileSync(statePath, JSON.stringify(seeded)) - - expect(() => claimAimlapiTopupState({ ...intent, amountUsdMinor: 5000 })).toThrow( - /does not match the expected format/, - ) - }) -} + }, +) // By-key checkouts (a saved profile or an env-sourced credential) have no // passwordless account email to key the intent on, so the intent's `email` From 227fa02e3b1eedf5071df4b9e7e578aad13370fa Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 22:21:43 -0700 Subject: [PATCH 022/121] test: consolidate stub walls in compact and extractMemories.abort tests (#2329) --- src/services/compact/compact.test.ts | 183 +++++++++++++--- .../extractMemories.abort.test.ts | 202 ++++++------------ 2 files changed, 215 insertions(+), 170 deletions(-) diff --git a/src/services/compact/compact.test.ts b/src/services/compact/compact.test.ts index 5b391c8210..668be01257 100644 --- a/src/services/compact/compact.test.ts +++ b/src/services/compact/compact.test.ts @@ -6,6 +6,7 @@ import { describe, expect, mock, + spyOn, test, vi, } from 'bun:test' @@ -363,9 +364,23 @@ export type CompactMockOptions = { * mocks that the provider gate test specifically depends on; all others are * DEFENSIVE (prevent transitive import / side-effect / I/O failures). */ -function registerCommonCompactStubs(options: CompactMockOptions = {}) { +async function registerCommonCompactStubs(options: CompactMockOptions = {}) { mock.restore() + const [config, path, settings, model] = await Promise.all([ + import('../../utils/config.js'), + import('../../utils/path.js'), + import('../../utils/settings/settings.js'), + import('../../utils/model/model.js'), + ]) + spyOn(config, 'getMemoryPath').mockReturnValue('/tmp/memory') + if (options.getGlobalConfig) { + spyOn(config, 'getGlobalConfig').mockImplementation(options.getGlobalConfig) + } + spyOn(path, 'expandPath').mockImplementation((p: string) => p) + spyOn(settings, 'getInitialSettings').mockReturnValue({}) + spyOn(model, 'getCanonicalName').mockImplementation((m: string) => m) + // --- Provider gate control --- // The isAnthropicProvider() gate is exercised via environment variables // (e.g. CLAUDE_CODE_USE_OPENAI=1) instead of mock.module() on betas.ts. @@ -510,15 +525,6 @@ function registerCommonCompactStubs(options: CompactMockOptions = {}) { groupMessagesByApiRound: mock((msgs: Message[]) => [msgs]), })) - // --- Config (DEFENSIVE) --- - mock.module('../../utils/config.js', () => ({ - ..._realConfigModule, - getMemoryPath: mock(() => '/tmp/memory'), - ...(options.getGlobalConfig - ? { getGlobalConfig: options.getGlobalConfig } - : {}), - })) - // --- File state cache (DEFENSIVE) --- mock.module('../../utils/fileStateCache.js', () => ({ cacheToObject: mock(() => ({})), @@ -555,12 +561,6 @@ function registerCommonCompactStubs(options: CompactMockOptions = {}) { getPlanFilePath: mock(() => '/tmp/plan'), })) - // --- Path (DEFENSIVE) --- - mock.module('../../utils/path.js', () => ({ - ..._realPathModule, - expandPath: mock((p: string) => p), - })) - // --- Sleep (DEFENSIVE) --- mock.module('../../utils/sleep.js', () => ({ sleep: mock(async () => {}), @@ -646,18 +646,6 @@ function registerCommonCompactStubs(options: CompactMockOptions = {}) { get3PModelCapabilityOverride: mock(() => undefined), })) - // --- Settings (DEFENSIVE) --- - mock.module('../../utils/settings/settings.js', () => ({ - ..._realSettingsModule, - getInitialSettings: mock(() => ({})), - })) - - // --- Model (DEFENSIVE) --- - mock.module('../../utils/model/model.js', () => ({ - ..._realModelModule, - getCanonicalName: mock((m: string) => m), - })) - return { runForkedAgent } } @@ -682,7 +670,7 @@ function registerCommonCompactStubs(options: CompactMockOptions = {}) { * trigger hundreds of failed transitive resolution steps. */ async function importCompact(options: CompactMockOptions = {}) { - const { runForkedAgent } = registerCommonCompactStubs(options) + const { runForkedAgent } = await registerCommonCompactStubs(options) // Dynamic import with cache-busting so each test gets fresh module state const nonce = `${Date.now()}-${Math.random()}` @@ -820,6 +808,143 @@ afterAll(async () => { } }) +describe('compact media stripping', () => { + test('strips top-level images and documents without mutating user messages', async () => { + const { stripImagesFromMessages } = await import( + `./compact.ts?test=${Date.now()}-${Math.random()}` + ) + const message = { + type: 'user', + uuid: randomUUID(), + timestamp: '2026-10-21T00:00:00.000Z', + origin: { kind: 'human' }, + isMeta: true, + message: { + role: 'user', + content: [ + { type: 'text', text: 'Summarize these attachments.' }, + { + type: 'image', + source: { type: 'base64', media_type: 'image/png', data: 'aW1hZ2U=' }, + }, + { + type: 'document', + source: { type: 'text', media_type: 'text/plain', data: 'document' }, + }, + ], + }, + } satisfies Message + const original = structuredClone(message) + + expect(stripImagesFromMessages([message])).toEqual([ + { + ...original, + message: { + ...original.message, + content: [ + { type: 'text', text: 'Summarize these attachments.' }, + { type: 'text', text: '[image]' }, + { type: 'text', text: '[document]' }, + ], + }, + }, + ]) + expect(message).toEqual(original) + expect(stripImagesFromMessages([])).toEqual([]) + + const stringMessage = userMessage('plain text') + const textOnlyMessage = { + ...message, + message: { role: 'user', content: [{ type: 'text', text: 'only text' }] }, + } satisfies Message + const assistant = assistantMessage('assistant text') + const unaffected = [stringMessage, textOnlyMessage, assistant] + const stripped = stripImagesFromMessages(unaffected) + expect(stripped).toEqual(unaffected) + for (const [index, unchanged] of unaffected.entries()) { + expect(stripped[index]).toBe(unchanged) + } + }) + + test('strips media inside tool results while preserving tool metadata and text', async () => { + const { stripImagesFromMessages } = await import( + `./compact.ts?test=${Date.now()}-${Math.random()}` + ) + const message = { + type: 'user', + uuid: randomUUID(), + timestamp: '2026-10-21T00:00:00.000Z', + sourceToolUseID: 'toolu_media', + toolUseResult: { status: 'failed' }, + message: { + role: 'user', + content: [ + { type: 'text', text: 'before the tool result' }, + { + type: 'tool_result', + tool_use_id: 'toolu_media', + is_error: true, + content: [ + { type: 'text', text: 'tool text' }, + { + type: 'image', + source: { type: 'base64', media_type: 'image/png', data: 'aW1hZ2U=' }, + }, + { + type: 'document', + source: { type: 'text', media_type: 'text/plain', data: 'document' }, + }, + { type: 'text', text: 'trailing tool text' }, + ], + }, + { type: 'tool_result', tool_use_id: 'toolu_string', content: 'string result' }, + { type: 'tool_result', tool_use_id: 'toolu_empty', content: [] }, + { + type: 'tool_result', + tool_use_id: 'toolu_text', + content: [{ type: 'text', text: 'text-only result' }], + }, + { type: 'text', text: 'after the tool result' }, + ], + }, + } satisfies Message + const original = structuredClone(message) + const stripped = stripImagesFromMessages([message])[0] as typeof message + + expect(stripped).toEqual({ + ...original, + message: { + ...original.message, + content: [ + original.message.content[0], + { + type: 'tool_result', + tool_use_id: 'toolu_media', + is_error: true, + content: [ + { type: 'text', text: 'tool text' }, + { type: 'text', text: '[image]' }, + { type: 'text', text: '[document]' }, + { type: 'text', text: 'trailing tool text' }, + ], + }, + ...original.message.content.slice(2), + ], + }, + }) + expect(message).toEqual(original) + for (const index of [0, 2, 3, 4, 5]) { + expect(stripped.message.content[index]).toBe(message.message.content[index]) + } + + const unchanged = { + ...message, + message: { ...message.message, content: message.message.content.slice(2) }, + } + expect(stripImagesFromMessages([unchanged])[0]).toBe(unchanged) + }) +}) + describe('compactConversation provider gate', () => { test('attributes cache-sharing timeout aborts to the compact fork', async () => { const originalTrace = process.env.OPENCLAUDE_INTERRUPT_TRACE diff --git a/src/services/extractMemories/extractMemories.abort.test.ts b/src/services/extractMemories/extractMemories.abort.test.ts index 86b38c435b..2b8c003432 100644 --- a/src/services/extractMemories/extractMemories.abort.test.ts +++ b/src/services/extractMemories/extractMemories.abort.test.ts @@ -42,6 +42,64 @@ function createContext(messageUuids: string | string[]): any { } } +function registerExtractMemoriesStubs({ + runForkedAgent, + debugLog, + events, +}: { + runForkedAgent: ReturnType + debugLog: ReturnType void>> + events: string[] +}) { + mock.module('../../bootstrap/state.js', () => ({ + ...realBootstrapState, + getIsRemoteMode: () => false, + })) + mock.module('../../memdir/memoryScan.js', () => ({ + ...realMemoryScan, + formatMemoryManifest: () => 'No memories yet.', + scanMemoryFiles: async () => [], + })) + mock.module('../../memdir/paths.js', () => ({ + ...realPaths, + getAutoMemPath: () => '/tmp/memory-extraction-test', + isAutoMemoryEnabled: () => true, + isAutoMemPath: () => false, + })) + mock.module('../../utils/governancePolicy.js', () => ({ + ...realGovernance, + isMemoryWriteApprovalRequired: () => false, + })) + mock.module('../../utils/debug.js', () => ({ + ...realDebug, + logForDebugging: debugLog, + })) + mock.module('../../utils/forkedAgent.js', () => ({ + ...realForkedAgent, + createCacheSafeParams: (context: any) => ({ + systemPrompt: context.systemPrompt, + userContext: context.userContext, + systemContext: context.systemContext, + toolUseContext: context.toolUseContext, + forkContextMessages: context.messages, + }), + runForkedAgent, + })) + mock.module('../analytics/growthbook.js', () => ({ + ...realGrowthbook, + getFeatureValue_CACHED_MAY_BE_STALE: ( + key: string, + defaultValue: unknown, + ) => (key === 'tengu_passport_quail' ? true : defaultValue), + })) + mock.module('../analytics/index.js', () => ({ + ...realAnalytics, + logEvent: (name: string) => { + events.push(name) + }, + })) +} + describe('extractMemories cancellation', () => { beforeEach(async () => { await acquireSharedMutationLock('extractMemories.abort.test.ts') @@ -94,53 +152,7 @@ describe('extractMemories cancellation', () => { }) }) - mock.module('../../bootstrap/state.js', () => ({ - ...realBootstrapState, - getIsRemoteMode: () => false, - })) - mock.module('../../memdir/memoryScan.js', () => ({ - ...realMemoryScan, - formatMemoryManifest: () => 'No memories yet.', - scanMemoryFiles: async () => [], - })) - mock.module('../../memdir/paths.js', () => ({ - ...realPaths, - getAutoMemPath: () => '/tmp/memory-extraction-test', - isAutoMemoryEnabled: () => true, - isAutoMemPath: () => false, - })) - mock.module('../../utils/governancePolicy.js', () => ({ - ...realGovernance, - isMemoryWriteApprovalRequired: () => false, - })) - mock.module('../../utils/debug.js', () => ({ - ...realDebug, - logForDebugging: debugLog, - })) - mock.module('../../utils/forkedAgent.js', () => ({ - ...realForkedAgent, - createCacheSafeParams: (context: any) => ({ - systemPrompt: context.systemPrompt, - userContext: context.userContext, - systemContext: context.systemContext, - toolUseContext: context.toolUseContext, - forkContextMessages: context.messages, - }), - runForkedAgent, - })) - mock.module('../analytics/growthbook.js', () => ({ - ...realGrowthbook, - getFeatureValue_CACHED_MAY_BE_STALE: ( - key: string, - defaultValue: unknown, - ) => (key === 'tengu_passport_quail' ? true : defaultValue), - })) - mock.module('../analytics/index.js', () => ({ - ...realAnalytics, - logEvent: (name: string) => { - events.push(name) - }, - })) + registerExtractMemoriesStubs({ runForkedAgent, debugLog, events }) const { executeExtractMemories, initExtractMemories } = await import( `./extractMemories.js?ts=${Date.now()}-${Math.random()}` @@ -202,53 +214,7 @@ describe('extractMemories cancellation', () => { }) }) - mock.module('../../bootstrap/state.js', () => ({ - ...realBootstrapState, - getIsRemoteMode: () => false, - })) - mock.module('../../memdir/memoryScan.js', () => ({ - ...realMemoryScan, - formatMemoryManifest: () => 'No memories yet.', - scanMemoryFiles: async () => [], - })) - mock.module('../../memdir/paths.js', () => ({ - ...realPaths, - getAutoMemPath: () => '/tmp/memory-extraction-test', - isAutoMemoryEnabled: () => true, - isAutoMemPath: () => false, - })) - mock.module('../../utils/governancePolicy.js', () => ({ - ...realGovernance, - isMemoryWriteApprovalRequired: () => false, - })) - mock.module('../../utils/debug.js', () => ({ - ...realDebug, - logForDebugging: debugLog, - })) - mock.module('../../utils/forkedAgent.js', () => ({ - ...realForkedAgent, - createCacheSafeParams: (context: any) => ({ - systemPrompt: context.systemPrompt, - userContext: context.userContext, - systemContext: context.systemContext, - toolUseContext: context.toolUseContext, - forkContextMessages: context.messages, - }), - runForkedAgent, - })) - mock.module('../analytics/growthbook.js', () => ({ - ...realGrowthbook, - getFeatureValue_CACHED_MAY_BE_STALE: ( - key: string, - defaultValue: unknown, - ) => (key === 'tengu_passport_quail' ? true : defaultValue), - })) - mock.module('../analytics/index.js', () => ({ - ...realAnalytics, - logEvent: (name: string) => { - events.push(name) - }, - })) + registerExtractMemoriesStubs({ runForkedAgent, debugLog, events }) const { executeExtractMemories, initExtractMemories } = await import( `./extractMemories.js?ts=${Date.now()}-${Math.random()}` @@ -307,53 +273,7 @@ describe('extractMemories cancellation', () => { }) }) - mock.module('../../bootstrap/state.js', () => ({ - ...realBootstrapState, - getIsRemoteMode: () => false, - })) - mock.module('../../memdir/memoryScan.js', () => ({ - ...realMemoryScan, - formatMemoryManifest: () => 'No memories yet.', - scanMemoryFiles: async () => [], - })) - mock.module('../../memdir/paths.js', () => ({ - ...realPaths, - getAutoMemPath: () => '/tmp/memory-extraction-test', - isAutoMemoryEnabled: () => true, - isAutoMemPath: () => false, - })) - mock.module('../../utils/governancePolicy.js', () => ({ - ...realGovernance, - isMemoryWriteApprovalRequired: () => false, - })) - mock.module('../../utils/debug.js', () => ({ - ...realDebug, - logForDebugging: debugLog, - })) - mock.module('../../utils/forkedAgent.js', () => ({ - ...realForkedAgent, - createCacheSafeParams: (context: any) => ({ - systemPrompt: context.systemPrompt, - userContext: context.userContext, - systemContext: context.systemContext, - toolUseContext: context.toolUseContext, - forkContextMessages: context.messages, - }), - runForkedAgent, - })) - mock.module('../analytics/growthbook.js', () => ({ - ...realGrowthbook, - getFeatureValue_CACHED_MAY_BE_STALE: ( - key: string, - defaultValue: unknown, - ) => (key === 'tengu_passport_quail' ? true : defaultValue), - })) - mock.module('../analytics/index.js', () => ({ - ...realAnalytics, - logEvent: (name: string) => { - events.push(name) - }, - })) + registerExtractMemoriesStubs({ runForkedAgent, debugLog, events }) const { executeExtractMemories, initExtractMemories } = await import( `./extractMemories.js?ts=${Date.now()}-${Math.random()}` From dea4d393b331cc8c2fa45d140fb17dba9b16b249 Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 22:29:27 -0700 Subject: [PATCH 023/121] test: restore leaked provider mocks and Concentrate env in single-process runs (#2328) Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com> --- src/utils/envFile.test.ts | 3 +++ src/utils/status.test.ts | 7 +++++++ src/utils/thinking.test.ts | 10 ++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/utils/envFile.test.ts b/src/utils/envFile.test.ts index 5c696307c5..3354a715da 100644 --- a/src/utils/envFile.test.ts +++ b/src/utils/envFile.test.ts @@ -23,6 +23,9 @@ const TEST_ENV_KEYS = [ 'CMD_API_KEY', 'COMMANDCODE_API_KEY', 'COMMAND_CODE_API_KEY', + 'CONCENTRATE_API_KEY', + 'CONCENTRATE_BASE_URL', + 'CONCENTRATE_MODEL', 'APISMART_API_KEY', 'APISMART_MODEL', 'API_ROUTE_API_KEY', diff --git a/src/utils/status.test.ts b/src/utils/status.test.ts index 234077935a..85cdfb5f5f 100644 --- a/src/utils/status.test.ts +++ b/src/utils/status.test.ts @@ -6,6 +6,11 @@ import { releaseSharedMutationLock, } from '../test/sharedMutationLock.js' +const realProvidersModule = await import( + `./model/providers.js?real=${Date.now()}-${Math.random()}` +) +const realProviders = { ...realProvidersModule } + const ORIGINAL_ENV = { ...process.env } function restoreEnv(): void { @@ -81,6 +86,8 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + // mock.restore() does not unregister Bun's process-global module mocks. + mock.module('./model/providers.js', () => realProviders) restoreEnv() } finally { releaseSharedMutationLock() diff --git a/src/utils/thinking.test.ts b/src/utils/thinking.test.ts index 585e68a790..2051f2b43e 100644 --- a/src/utils/thinking.test.ts +++ b/src/utils/thinking.test.ts @@ -5,6 +5,11 @@ import { } from '../test/sharedMutationLock.js' import { resetSettingsCache } from './settings/settingsCache.js' +const realProvidersModule = await import( + `./model/providers.js?real=${Date.now()}-${Math.random()}` +) +const realProviders = { ...realProvidersModule } + const ENV_KEYS = [ 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_GEMINI', @@ -43,6 +48,8 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + // mock.restore() does not unregister Bun's process-global module mocks. + mock.module('./model/providers.js', () => realProviders) for (const key of ENV_KEYS) { if (originalEnv[key] === undefined) { delete process.env[key] @@ -58,10 +65,9 @@ afterEach(() => { async function importFreshThinkingModule() { mock.restore() - const originalProviders = await import('./model/providers.js') mock.module('./model/providers.js', () => { return { - ...originalProviders, + ...realProviders, getAPIProvider: () => 'openai', } }) From 3a646df82182c5d63bff2587e2df070aed7209ad Mon Sep 17 00:00:00 2001 From: JATMN Date: Wed, 21 Oct 2026 22:29:48 -0700 Subject: [PATCH 024/121] test(gateways): assert OpenGateway route default model and credentials (#2330) --- src/integrations/gateways/gitlawb-opengateway.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/integrations/gateways/gitlawb-opengateway.test.ts b/src/integrations/gateways/gitlawb-opengateway.test.ts index 665a31d6a1..fb22f1cfbd 100644 --- a/src/integrations/gateways/gitlawb-opengateway.test.ts +++ b/src/integrations/gateways/gitlawb-opengateway.test.ts @@ -10,6 +10,14 @@ describe('gitlawb-opengateway live model mapping', () => { { routeId: 'gitlawb-opengateway', expected: { + defaultModel: 'mimo-v2.5-pro', + setup: { + credentialEnvVars: [ + 'OPENGATEWAY_API_KEY', + 'OPENAI_API_KEYS', + 'OPENAI_API_KEY', + ], + }, catalogSource: 'hybrid', discovery: { requiresAuth: false, mapModel: mapOpenGatewayModel }, } satisfies GatewayCatalogExpectations, From 92d71e6b87ea43c2baa77ed7561c91e979b6d706 Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 05:44:44 -0700 Subject: [PATCH 025/121] test: restore leaked settings, config and path state across test files (TA-001 slice 3) (#2331) * test: restore leaked settings, config-home and path state in single-process runs (TA-001 slice 3) * test(printMaxTurns): release shared mutation lock if setup throws --- src/cli/printMaxTurns.test.ts | 89 ++++++++++------- src/components/TrustDialog/utils.test.ts | 30 +++--- src/services/mcp/config.test.ts | 95 +++++++++++-------- src/utils/auth.test.ts | 25 ++--- src/utils/hookChains.integration.test.ts | 9 +- src/utils/modelCost.modelGate.test.ts | 11 ++- src/utils/openclaudePaths.test.ts | 73 ++++++-------- src/utils/providerProfile.test.ts | 8 ++ .../sessionStorage.commandOnlySession.test.ts | 51 ++++++---- .../sessionStorage.deletedSession.test.ts | 50 ++++++---- 10 files changed, 251 insertions(+), 190 deletions(-) diff --git a/src/cli/printMaxTurns.test.ts b/src/cli/printMaxTurns.test.ts index 8f1855e1a5..919ca07456 100644 --- a/src/cli/printMaxTurns.test.ts +++ b/src/cli/printMaxTurns.test.ts @@ -1,5 +1,10 @@ import { afterEach, describe, expect, spyOn, test } from 'bun:test' +import { + acquireSharedMutationLock, + releaseSharedMutationLock, +} from '../test/sharedMutationLock.js' +import { settingsChangeDetector } from '../utils/settings/changeDetector.js' import type { Command } from '../commands.js' import type { SDKMessage } from '../entrypoints/agentSdkTypes.js' import * as queryEngineModule from '../QueryEngine.js' @@ -170,45 +175,61 @@ describe('headless --print max-turns', () => { }) test('forwards maxTurns 0 through runHeadless into ask()', async () => { - process.env.CLAUDE_CODE_SIMPLE = '1' + await acquireSharedMutationLock('cli/printMaxTurns.test.ts') + try { + const unsubscribes: Array<() => void> = [] + const realSubscribe = settingsChangeDetector.subscribe + const subscribeSpy = spyOn(settingsChangeDetector, 'subscribe').mockImplementation( + listener => { + const unsubscribe = realSubscribe(listener) + unsubscribes.push(unsubscribe) + return unsubscribe + }, + ) + process.env.CLAUDE_CODE_SIMPLE = '1' - const shutdownSpy = spyOn(shutdownModule, 'gracefulShutdownSync').mockImplementation(() => {}) - const stdoutSpy = spyOn(processModule, 'writeToStdout').mockImplementation( - () => {}, - ) - const askSpy = spyOn(queryEngineModule, 'ask').mockImplementation( - async function* () { - yield createHeadlessSuccessResult() - }, - ) + const shutdownSpy = spyOn(shutdownModule, 'gracefulShutdownSync').mockImplementation(() => {}) + const stdoutSpy = spyOn(processModule, 'writeToStdout').mockImplementation( + () => {}, + ) + const askSpy = spyOn(queryEngineModule, 'ask').mockImplementation( + async function* () { + yield createHeadlessSuccessResult() + }, + ) - let state = getDefaultAppState() - const getAppState = () => state - const setAppState = (update: (previous: AppState) => AppState) => { - state = update(state) - } + let state = getDefaultAppState() + const getAppState = () => state + const setAppState = (update: (previous: AppState) => AppState) => { + state = update(state) + } - let runPromise: Promise | undefined - try { - runPromise = runHeadless( - 'headless prompt', - getAppState, - setAppState, - [] as Command[], - [] as Tools, - {}, - [], - createHeadlessRunOptions(0), - ) + let runPromise: Promise | undefined + try { + runPromise = runHeadless( + 'headless prompt', + getAppState, + setAppState, + [] as Command[], + [] as Tools, + {}, + [], + createHeadlessRunOptions(0), + ) - await runPromise - expect(askSpy.mock.calls[0]?.[0]?.maxTurns).toBe(0) - expect(shutdownSpy).toHaveBeenCalledWith(0) + await runPromise + expect(askSpy.mock.calls[0]?.[0]?.maxTurns).toBe(0) + expect(shutdownSpy).toHaveBeenCalledWith(0) + } finally { + await runPromise?.catch(() => {}) + for (const unsubscribe of unsubscribes) unsubscribe() + subscribeSpy.mockRestore() + askSpy.mockRestore() + stdoutSpy.mockRestore() + shutdownSpy.mockRestore() + } } finally { - await runPromise?.catch(() => {}) - askSpy.mockRestore() - stdoutSpy.mockRestore() - shutdownSpy.mockRestore() + releaseSharedMutationLock() } }) }) diff --git a/src/components/TrustDialog/utils.test.ts b/src/components/TrustDialog/utils.test.ts index 8ef3c9b28f..aa5446a213 100644 --- a/src/components/TrustDialog/utils.test.ts +++ b/src/components/TrustDialog/utils.test.ts @@ -1,10 +1,13 @@ -import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterAll, beforeEach, describe, expect, spyOn, test } from 'bun:test' import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js' import { BASH_TOOL_NAME } from '../../tools/BashTool/toolName.js' import type { PermissionRule } from '../../utils/permissions/PermissionRule.js' -import { getRelativeSettingsFilePathForSource as REAL_canonicalPath } from '../../utils/settings/settings.js' +import * as settingsModule from '../../utils/settings/settings.js' +import * as permissionsLoader from '../../utils/permissions/permissionsLoader.js' import type { SettingsJson } from '../../utils/settings/types.js' +const REAL_canonicalPath = settingsModule.getRelativeSettingsFilePathForSource + // TrustDialog/utils.ts emits file-path strings shown to users in the trust // dialog. These MUST come from getRelativeSettingsFilePathForSource — the // single source of truth — not hardcoded literals. The previous code hardcoded @@ -52,24 +55,17 @@ const permissionRulesState: { localSettings: [], } -mock.module('../../utils/settings/settings.js', () => ({ - // Stateful — needs mocking to control test inputs. - getSettingsForSource: (source: 'projectSettings' | 'localSettings') => - settingsState[source], - // Pure — pass the REAL function through. The mock exists only because - // utils.ts imports both names from the same module; we must provide both. - getRelativeSettingsFilePathForSource: REAL_canonicalPath, -})) - -mock.module('../../utils/permissions/permissionsLoader.js', () => ({ - getPermissionRulesForSource: ( - source: 'projectSettings' | 'localSettings', - ): PermissionRule[] => permissionRulesState[source], -})) +const settingsSpy = spyOn(settingsModule, 'getSettingsForSource').mockImplementation( + source => settingsState[source as keyof typeof settingsState], +) +const permissionRulesSpy = spyOn(permissionsLoader, 'getPermissionRulesForSource').mockImplementation( + source => permissionRulesState[source as keyof typeof permissionRulesState], +) afterAll(() => { try { - mock.restore() + settingsSpy.mockRestore() + permissionRulesSpy.mockRestore() } finally { releaseSharedMutationLock() } diff --git a/src/services/mcp/config.test.ts b/src/services/mcp/config.test.ts index f958985fc5..679292de8b 100644 --- a/src/services/mcp/config.test.ts +++ b/src/services/mcp/config.test.ts @@ -1,30 +1,47 @@ -import { afterEach, beforeEach, expect, test } from 'bun:test' +import { afterAll, afterEach, beforeAll, beforeEach, expect, mock, test } from 'bun:test' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' -import { - getCurrentProjectConfig, - getGlobalConfig, - saveCurrentProjectConfig, - saveGlobalConfig, -} from '../../utils/config.js' -import { isMcpServerDisabled, setMcpServerEnabled } from './config.js' +type ConfigModule = typeof import('../../utils/config.js') +type McpConfigModule = typeof import('./config.js') + +let realConfig: ConfigModule +let incomingConfig: ConfigModule +let isMcpServerDisabled: McpConfigModule['isMcpServerDisabled'] +let setMcpServerEnabled: McpConfigModule['setMcpServerEnabled'] + +beforeAll(async () => { + await acquireSharedMutationLock('services/mcp/config.test.ts') + incomingConfig = { ...await import('../../utils/config.js') } + realConfig = { ...await import(`../../utils/config.js?real=${Date.now()}-${Math.random()}`) } + mock.module('../../utils/config.js', () => realConfig) + const mcpConfig = await import(`./config.js?test=${Date.now()}-${Math.random()}`) + isMcpServerDisabled = mcpConfig.isMcpServerDisabled + setMcpServerEnabled = mcpConfig.setMcpServerEnabled +}) + +afterAll(() => { + try { + mock.module('../../utils/config.js', () => incomingConfig) + } finally { + releaseSharedMutationLock() + } +}) let savedNodeEnv: string | undefined let savedGlobalDisabled: string[] | undefined let savedProjectDisabled: string[] | undefined let savedProjectEnabled: string[] | undefined -beforeEach(async () => { - await acquireSharedMutationLock('services/mcp/config.test.ts') +beforeEach(() => { savedNodeEnv = process.env.NODE_ENV process.env.NODE_ENV = 'test' - savedGlobalDisabled = getGlobalConfig().disabledMcpServers - savedProjectDisabled = getCurrentProjectConfig().disabledMcpServers - savedProjectEnabled = getCurrentProjectConfig().enabledMcpServers - saveGlobalConfig(c => ({ ...c, disabledMcpServers: undefined })) - saveCurrentProjectConfig(c => ({ + savedGlobalDisabled = realConfig.getGlobalConfig().disabledMcpServers + savedProjectDisabled = realConfig.getCurrentProjectConfig().disabledMcpServers + savedProjectEnabled = realConfig.getCurrentProjectConfig().enabledMcpServers + realConfig.saveGlobalConfig(c => ({ ...c, disabledMcpServers: undefined })) + realConfig.saveCurrentProjectConfig(c => ({ ...c, disabledMcpServers: undefined, enabledMcpServers: undefined, @@ -32,18 +49,14 @@ beforeEach(async () => { }) afterEach(() => { - try { - saveGlobalConfig(c => ({ ...c, disabledMcpServers: savedGlobalDisabled })) - saveCurrentProjectConfig(c => ({ - ...c, - disabledMcpServers: savedProjectDisabled, - enabledMcpServers: savedProjectEnabled, - })) - if (savedNodeEnv === undefined) delete process.env.NODE_ENV - else process.env.NODE_ENV = savedNodeEnv - } finally { - releaseSharedMutationLock() - } + realConfig.saveGlobalConfig(c => ({ ...c, disabledMcpServers: savedGlobalDisabled })) + realConfig.saveCurrentProjectConfig(c => ({ + ...c, + disabledMcpServers: savedProjectDisabled, + enabledMcpServers: savedProjectEnabled, + })) + if (savedNodeEnv === undefined) delete process.env.NODE_ENV + else process.env.NODE_ENV = savedNodeEnv }) test('without project or global lists, servers are enabled', () => { @@ -52,32 +65,32 @@ test('without project or global lists, servers are enabled', () => { }) test('an undefined project list inherits only global disables', () => { - getGlobalConfig().disabledMcpServers = ['a', 'b'] + realConfig.getGlobalConfig().disabledMcpServers = ['a', 'b'] expect(isMcpServerDisabled('a')).toBe(true) expect(isMcpServerDisabled('b')).toBe(true) expect(isMcpServerDisabled('other')).toBe(false) }) test.each([{ list: [] }, { list: ['local'] }])('an explicit project list %j overrides global disables', ({ list }) => { - getGlobalConfig().disabledMcpServers = ['global'] - saveCurrentProjectConfig(c => ({ ...c, disabledMcpServers: [...list] })) + realConfig.getGlobalConfig().disabledMcpServers = ['global'] + realConfig.saveCurrentProjectConfig(c => ({ ...c, disabledMcpServers: [...list] })) expect(isMcpServerDisabled('global')).toBe(false) expect(isMcpServerDisabled('local')).toBe(list.length > 0) }) test('disabling preserves inherited entries and propagates globally without duplicates', () => { - getGlobalConfig().disabledMcpServers = ['a'] + realConfig.getGlobalConfig().disabledMcpServers = ['a'] setMcpServerEnabled('b', false) setMcpServerEnabled('b', false) expect(isMcpServerDisabled('a')).toBe(true) expect(isMcpServerDisabled('b')).toBe(true) - expect(getCurrentProjectConfig().disabledMcpServers).toEqual(['a', 'b']) - expect(getGlobalConfig().disabledMcpServers).toEqual(['a', 'b']) + expect(realConfig.getCurrentProjectConfig().disabledMcpServers).toEqual(['a', 'b']) + expect(realConfig.getGlobalConfig().disabledMcpServers).toEqual(['a', 'b']) }) test('toggling an explicit project list preserves its overrides and enables locally', () => { - getGlobalConfig().disabledMcpServers = ['global'] - saveCurrentProjectConfig(c => ({ ...c, disabledMcpServers: ['local'] })) + realConfig.getGlobalConfig().disabledMcpServers = ['global'] + realConfig.saveCurrentProjectConfig(c => ({ ...c, disabledMcpServers: ['local'] })) setMcpServerEnabled('target', false) expect(isMcpServerDisabled('target')).toBe(true) setMcpServerEnabled('target', true) @@ -85,22 +98,22 @@ test('toggling an explicit project list preserves its overrides and enables loca expect(isMcpServerDisabled('target')).toBe(false) expect(isMcpServerDisabled('local')).toBe(true) expect(isMcpServerDisabled('global')).toBe(false) - expect(getGlobalConfig().disabledMcpServers).toEqual(['global', 'target']) + expect(realConfig.getGlobalConfig().disabledMcpServers).toEqual(['global', 'target']) }) test('enabling one inherited server preserves other disables and stays local', () => { - getGlobalConfig().disabledMcpServers = ['a', 'b'] + realConfig.getGlobalConfig().disabledMcpServers = ['a', 'b'] setMcpServerEnabled('a', true) setMcpServerEnabled('a', true) expect(isMcpServerDisabled('a')).toBe(false) expect(isMcpServerDisabled('b')).toBe(true) - expect(getGlobalConfig().disabledMcpServers).toEqual(['a', 'b']) + expect(realConfig.getGlobalConfig().disabledMcpServers).toEqual(['a', 'b']) }) test('enabling the last inherited server keeps an explicit empty override', () => { - getGlobalConfig().disabledMcpServers = ['a'] + realConfig.getGlobalConfig().disabledMcpServers = ['a'] setMcpServerEnabled('a', true) - expect(getCurrentProjectConfig().disabledMcpServers).toEqual([]) + expect(realConfig.getCurrentProjectConfig().disabledMcpServers).toEqual([]) expect(isMcpServerDisabled('a')).toBe(false) - expect(getGlobalConfig().disabledMcpServers).toEqual(['a']) + expect(realConfig.getGlobalConfig().disabledMcpServers).toEqual(['a']) }) diff --git a/src/utils/auth.test.ts b/src/utils/auth.test.ts index 02cc24478d..63caaf18ac 100644 --- a/src/utils/auth.test.ts +++ b/src/utils/auth.test.ts @@ -1,5 +1,6 @@ -import { afterEach, expect, mock, test } from 'bun:test' +import { afterEach, expect, spyOn, test } from 'bun:test' import * as originalSettings from './settings/settings.js' +import type { SettingsJson } from './settings/types.js' type MockSource = | 'userSettings' @@ -14,11 +15,10 @@ async function importAuthFresh() { return await import(`./auth.js?ts=${Date.now()}-${Math.random()}`) } -// Restore Bun's module mocks after each test so leaked settings behavior -// cannot influence later auth/settings tests in the same process. -// Addresses jatmn's P3 on #1731: test isolation for mock.module(). +const settingsSpies: Array<{ mockRestore(): void }> = [] + afterEach(() => { - mock.restore() + for (const spy of settingsSpies.splice(0)) spy.mockRestore() }) // Helper: mock settings to return the given subscriptionType from a specific @@ -26,17 +26,18 @@ afterEach(() => { // from user settings only — project, local, flag, and policy settings must // NOT propagate subscriptionType. function mockSettings( - subscriptionType: string | undefined, + subscriptionType: SettingsJson['subscriptionType'], source: MockSource = 'userSettings', ) { - mock.module('./settings/settings.js', () => ({ - ...originalSettings, - getSettings_DEPRECATED: () => (source === 'none' ? {} : { subscriptionType }), - getSettingsForSource: (s: string) => { + settingsSpies.push( + spyOn(originalSettings, 'getSettings_DEPRECATED').mockImplementation( + () => (source === 'none' ? {} : { subscriptionType }), + ), + spyOn(originalSettings, 'getSettingsForSource').mockImplementation(s => { if (source === 'none') return null return s === source ? { subscriptionType } : null - }, - })) + }), + ) } async function withControlledAuthEnv( diff --git a/src/utils/hookChains.integration.test.ts b/src/utils/hookChains.integration.test.ts index 12553779d2..be50a86033 100644 --- a/src/utils/hookChains.integration.test.ts +++ b/src/utils/hookChains.integration.test.ts @@ -7,6 +7,10 @@ import { releaseSharedMutationLock, } from '../test/sharedMutationLock.js' +import * as realTeammate from './teammate.js' + +const realTeammateSnapshot = { ...realTeammate } + type HookChainsModule = typeof import('./hookChains.js') type ImportHarnessOptions = { allowRemoteSessions?: boolean @@ -45,7 +49,6 @@ async function importRealModules() { policyLimits: await import(`../services/policyLimits/index.js?real=${ts}-${rand}`) as typeof import('../services/policyLimits/index.js'), teamHelpers: await import(`./swarm/teamHelpers.js?real=${ts}-${rand}`) as typeof import('./swarm/teamHelpers.js'), teammateMailbox: await import(`./teammateMailbox.js?real=${ts}-${rand}`) as typeof import('./teammateMailbox.js'), - teammate: await import(`./teammate.js?real=${ts}-${rand}`) as typeof import('./teammate.js'), replBridge: await import(`../bridge/replBridgeHandle.js?real=${ts}-${rand}`) as typeof import('../bridge/replBridgeHandle.js'), agentTool: await import(`../tools/AgentTool/AgentTool.js?real=${ts}-${rand}`) as typeof import('../tools/AgentTool/AgentTool.js'), } @@ -58,7 +61,7 @@ async function restorePersistentModuleMocks(): Promise { mock.module('../services/policyLimits/index.js', () => real.policyLimits) mock.module('./swarm/teamHelpers.js', () => real.teamHelpers) mock.module('./teammateMailbox.js', () => real.teammateMailbox) - mock.module('./teammate.js', () => real.teammate) + mock.module('./teammate.js', () => realTeammateSnapshot) mock.module('../bridge/replBridgeHandle.js', () => real.replBridge) mock.module('../tools/AgentTool/AgentTool.js', () => real.agentTool) } @@ -112,7 +115,7 @@ async function importHookChainsHarness( })) mock.module('./teammate.js', () => ({ - ...real.teammate, + ...realTeammateSnapshot, getAgentName: () => senderName, getTeamName: () => teamName, getTeammateColor: () => 'blue', diff --git a/src/utils/modelCost.modelGate.test.ts b/src/utils/modelCost.modelGate.test.ts index 659c098883..15718c82ec 100644 --- a/src/utils/modelCost.modelGate.test.ts +++ b/src/utils/modelCost.modelGate.test.ts @@ -17,6 +17,7 @@ import * as realFastMode from './fastMode.js' import * as realModel from './model/model.js' import { resetSettingsCache } from './settings/settingsCache.js' +const realFastModeSnapshot = { ...realFastMode } const realModelSnapshot = { ...realModel } type PricingOverride = { @@ -38,6 +39,7 @@ async function importFreshModelCost() { beforeEach(async () => { await acquireSharedMutationLock('utils/modelCost.modelGate.test.ts') + resetCostState() originalSources = [...getAllowedSettingSources()] originalFlagPath = getFlagSettingsPath() originalFlagInline = getFlagSettingsInline() @@ -50,8 +52,9 @@ beforeEach(async () => { afterEach(() => { try { + resetCostState() mock.restore() - mock.module('./fastMode.js', () => realFastMode) + mock.module('./fastMode.js', () => realFastModeSnapshot) mock.module('./model/model.js', () => realModelSnapshot) setAllowedSettingSources(originalSources) setFlagSettingsPath(originalFlagPath) @@ -90,7 +93,7 @@ test('unknown models do not inherit the configured default model price', async ( // billed at the normal rate while the picker advertised the fast-mode price. test('fast-mode Opus 4.8 is charged the elevated fast-mode tier, normal otherwise', async () => { mock.module('./fastMode.js', () => ({ - ...realFastMode, + ...realFastModeSnapshot, isFastModeEnabled: () => true, })) const { getModelCosts, COST_TIER_30_150, COST_TIER_5_25 } = @@ -199,7 +202,7 @@ test('all-zero exact override is authoritative for unknown and fast-mode known m pricingByModel['nvidia/free-model'] = zero pricingByModel['claude-opus-4-8'] = zero mock.module('./fastMode.js', () => ({ - ...realFastMode, + ...realFastModeSnapshot, isFastModeEnabled: () => true, })) const { calculateUSDCost, getModelCosts, getModelPricingString } = @@ -223,7 +226,7 @@ test('all-zero exact override is authoritative for unknown and fast-mode known m test('custom pricing matches exact resolved ids only, including unusual own keys', async () => { mock.restore() - mock.module('./fastMode.js', () => realFastMode) + mock.module('./fastMode.js', () => realFastModeSnapshot) mock.module('./model/model.js', () => realModelSnapshot) const configured = { inputTokens: 7, diff --git a/src/utils/openclaudePaths.test.ts b/src/utils/openclaudePaths.test.ts index 4eeb210050..707fac4844 100644 --- a/src/utils/openclaudePaths.test.ts +++ b/src/utils/openclaudePaths.test.ts @@ -1,4 +1,4 @@ -import { afterEach, describe, expect, mock, test } from 'bun:test' +import { afterEach, beforeEach, describe, expect, spyOn, test } from 'bun:test' import { existsSync, mkdirSync, @@ -7,12 +7,29 @@ import { writeFileSync, } from 'fs' import * as fsPromises from 'fs/promises' +import * as os from 'os' import { homedir, tmpdir } from 'os' import { join } from 'path' import { acquireEnvMutex, releaseEnvMutex } from '../entrypoints/sdk/shared.js' - -const originalEnv = { ...process.env } -const originalArgv = [...process.argv] +import { + getClaudeConfigHomeDir, + getClaudeConfigHomeDirOverrideForTesting, + setClaudeConfigHomeDirForTesting, +} from './envUtils.js' + +let originalEnv: NodeJS.ProcessEnv +let originalArgv: string[] +let originalConfigOverride: string | undefined +const pathSpies: Array<{ mockRestore(): void }> = [] + +beforeEach(async () => { + await acquireEnvMutex() + originalEnv = { ...process.env } + originalArgv = [...process.argv] + originalConfigOverride = getClaudeConfigHomeDirOverrideForTesting() + setClaudeConfigHomeDirForTesting(undefined) + getClaudeConfigHomeDir.cache.clear() +}) async function importFreshEnvUtils() { return import(`./envUtils.ts?ts=${Date.now()}-${Math.random()}`) @@ -34,7 +51,9 @@ afterEach(() => { try { process.env = { ...originalEnv } process.argv = [...originalArgv] - mock.restore() + for (const spy of pathSpies.splice(0)) spy.mockRestore() + setClaudeConfigHomeDirForTesting(originalConfigOverride) + getClaudeConfigHomeDir.cache.clear() } finally { releaseEnvMutex() } @@ -42,7 +61,6 @@ afterEach(() => { describe('OpenClaude paths', () => { test('defaults user config home to ~/.openclaude', async () => { - await acquireEnvMutex() delete process.env.OPENCLAUDE_CONFIG_DIR delete process.env.CLAUDE_CONFIG_DIR const { resolveClaudeConfigHomeDir } = await importFreshEnvUtils() @@ -55,7 +73,6 @@ describe('OpenClaude paths', () => { }) test('hard-cuts user config home to ~/.openclaude by default', async () => { - await acquireEnvMutex() delete process.env.OPENCLAUDE_CONFIG_DIR delete process.env.CLAUDE_CONFIG_DIR const { resolveClaudeConfigHomeDir } = await importFreshEnvUtils() @@ -68,7 +85,6 @@ describe('OpenClaude paths', () => { }) test('does not migrate legacy .claude config into .openclaude', async () => { - await acquireEnvMutex() const tempHome = mkdtempSync(join(tmpdir(), 'openclaude-paths-test-')) try { mkdirSync(join(tempHome, '.claude', 'skills', 'legacy-skill'), { @@ -91,15 +107,11 @@ describe('OpenClaude paths', () => { }) test('config home does not fall back to legacy .claude', async () => { - await acquireEnvMutex() const tempHome = mkdtempSync(join(tmpdir(), 'openclaude-paths-test-')) try { writeFileSync(join(tempHome, '.openclaude'), 'not a directory') mkdirSync(join(tempHome, '.claude'), { recursive: true }) - mock.module('os', () => ({ - homedir: () => tempHome, - tmpdir, - })) + pathSpies.push(spyOn(os, 'homedir').mockReturnValue(tempHome)) delete process.env.OPENCLAUDE_CONFIG_DIR delete process.env.CLAUDE_CONFIG_DIR @@ -112,7 +124,6 @@ describe('OpenClaude paths', () => { }) test('default plans directory uses ~/.openclaude/plans', async () => { - await acquireEnvMutex() delete process.env.OPENCLAUDE_CONFIG_DIR delete process.env.CLAUDE_CONFIG_DIR const { getDefaultPlansDirectory } = await importFreshPlans() @@ -123,7 +134,6 @@ describe('OpenClaude paths', () => { }) test('default plans directory respects explicit configDirEnv argument', async () => { - await acquireEnvMutex() const { getDefaultPlansDirectory } = await importFreshPlans() expect( @@ -132,7 +142,6 @@ describe('OpenClaude paths', () => { }) test('default plans directory respects OPENCLAUDE_CONFIG_DIR', async () => { - await acquireEnvMutex() process.env.OPENCLAUDE_CONFIG_DIR = '/tmp/preferred-openclaude' delete process.env.CLAUDE_CONFIG_DIR const { getDefaultPlansDirectory } = await importFreshPlans() @@ -143,7 +152,6 @@ describe('OpenClaude paths', () => { }) test('OPENCLAUDE_CONFIG_DIR wins for default plans directory', async () => { - await acquireEnvMutex() process.env.OPENCLAUDE_CONFIG_DIR = '/tmp/preferred-openclaude' process.env.CLAUDE_CONFIG_DIR = '/tmp/legacy-openclaude' const { getDefaultPlansDirectory } = await importFreshPlans() @@ -154,7 +162,6 @@ describe('OpenClaude paths', () => { }) test('default plans directory normalizes generated path to NFC', async () => { - await acquireEnvMutex() delete process.env.OPENCLAUDE_CONFIG_DIR const { getDefaultPlansDirectory } = await importFreshPlans() @@ -164,7 +171,6 @@ describe('OpenClaude paths', () => { }) test('default plans directory normalizes explicit configDirEnv argument to NFC', async () => { - await acquireEnvMutex() const { getDefaultPlansDirectory } = await importFreshPlans() expect( @@ -173,20 +179,15 @@ describe('OpenClaude paths', () => { }) test('ignores CLAUDE_CONFIG_DIR override when provided', async () => { - await acquireEnvMutex() delete process.env.OPENCLAUDE_CONFIG_DIR process.env.CLAUDE_CONFIG_DIR = '/tmp/custom-openclaude' - mock.module('os', () => ({ - homedir: () => '/tmp/home', - tmpdir, - })) + pathSpies.push(spyOn(os, 'homedir').mockReturnValue('/tmp/home')) const { getClaudeConfigHomeDir } = await importFreshEnvUtils() expect(getClaudeConfigHomeDir()).toBe('/tmp/home/.openclaude') }) test('OPENCLAUDE_CONFIG_DIR overrides the default (issue #454)', async () => { - await acquireEnvMutex() delete process.env.CLAUDE_CONFIG_DIR process.env.OPENCLAUDE_CONFIG_DIR = '/tmp/oc-config-only' const { getClaudeConfigHomeDir } = await importFreshEnvUtils() @@ -195,7 +196,6 @@ describe('OpenClaude paths', () => { }) test('OPENCLAUDE_CONFIG_DIR wins when both env vars are set with different values', async () => { - await acquireEnvMutex() process.env.OPENCLAUDE_CONFIG_DIR = '/tmp/oc-wins' process.env.CLAUDE_CONFIG_DIR = '/tmp/legacy-loses' const { getClaudeConfigHomeDir } = await importFreshEnvUtils() @@ -204,7 +204,6 @@ describe('OpenClaude paths', () => { }) test('CLAUDE_CONFIG_DIR is ignored when OPENCLAUDE_CONFIG_DIR is unset', async () => { - await acquireEnvMutex() delete process.env.OPENCLAUDE_CONFIG_DIR process.env.CLAUDE_CONFIG_DIR = '/tmp/legacy-only' const { getClaudeConfigHomeDir } = await importFreshEnvUtils() @@ -213,7 +212,6 @@ describe('OpenClaude paths', () => { }) test('empty OPENCLAUDE_CONFIG_DIR does not fall through to CLAUDE_CONFIG_DIR', async () => { - await acquireEnvMutex() process.env.OPENCLAUDE_CONFIG_DIR = '' process.env.CLAUDE_CONFIG_DIR = '/tmp/legacy-fallback' const { getClaudeConfigHomeDir } = await importFreshEnvUtils() @@ -222,7 +220,6 @@ describe('OpenClaude paths', () => { }) test('resolveConfigDirEnv ignores CLAUDE_CONFIG_DIR without warning', async () => { - await acquireEnvMutex() const { resolveConfigDirEnv, __resetConfigDirEnvWarningForTesting } = await importFreshEnvUtils() __resetConfigDirEnvWarningForTesting() @@ -246,7 +243,6 @@ describe('OpenClaude paths', () => { }) test('resolveConfigDirEnv ignores legacy env for silent and warning callers', async () => { - await acquireEnvMutex() const { resolveConfigDirEnv, __resetConfigDirEnvWarningForTesting } = await importFreshEnvUtils() __resetConfigDirEnvWarningForTesting() @@ -270,7 +266,6 @@ describe('OpenClaude paths', () => { }) test('resolveConfigDirEnv does not warn when both env vars agree', async () => { - await acquireEnvMutex() const { resolveConfigDirEnv, __resetConfigDirEnvWarningForTesting } = await importFreshEnvUtils() __resetConfigDirEnvWarningForTesting() @@ -287,7 +282,6 @@ describe('OpenClaude paths', () => { }) test('resolveConfigDirEnv returns undefined when neither env var is set', async () => { - await acquireEnvMutex() const { resolveConfigDirEnv } = await importFreshEnvUtils() expect( @@ -299,7 +293,6 @@ describe('OpenClaude paths', () => { }) test('project and local settings paths use .openclaude', async () => { - await acquireEnvMutex() const { getRelativeSettingsFilePathForSource } = await importFreshSettings() expect(getRelativeSettingsFilePathForSource('projectSettings')).toBe( @@ -311,7 +304,6 @@ describe('OpenClaude paths', () => { }) test('local installer uses openclaude wrapper path', async () => { - await acquireEnvMutex() process.env.OPENCLAUDE_CONFIG_DIR = join(homedir(), '.openclaude') delete process.env.CLAUDE_CONFIG_DIR const { getLocalClaudePath } = await importFreshLocalInstaller() @@ -322,7 +314,6 @@ describe('OpenClaude paths', () => { }) test('local installation detection matches .openclaude path', async () => { - await acquireEnvMutex() const { isManagedLocalInstallationPath } = await importFreshLocalInstaller() @@ -334,7 +325,6 @@ describe('OpenClaude paths', () => { }) test('local installation detection ignores legacy .claude path', async () => { - await acquireEnvMutex() const { isManagedLocalInstallationPath } = await importFreshLocalInstaller() @@ -346,7 +336,6 @@ describe('OpenClaude paths', () => { }) test('candidate local install dirs include only openclaude path', async () => { - await acquireEnvMutex() const { getCandidateLocalInstallDirs } = await importFreshLocalInstaller() expect( @@ -359,18 +348,16 @@ describe('OpenClaude paths', () => { }) test('legacy local installs are ignored even when they expose the claude binary', async () => { - await acquireEnvMutex() - mock.module('fs/promises', () => ({ - ...fsPromises, - access: async (path: string) => { + pathSpies.push( + spyOn(fsPromises, 'access').mockImplementation(async path => { if ( path === join(homedir(), '.claude', 'local', 'node_modules', '.bin', 'claude') ) { return } throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' }) - }, - })) + }), + ) const { getDetectedLocalInstallDir, localInstallationExists } = await importFreshLocalInstaller() diff --git a/src/utils/providerProfile.test.ts b/src/utils/providerProfile.test.ts index 675f308d49..623e535739 100644 --- a/src/utils/providerProfile.test.ts +++ b/src/utils/providerProfile.test.ts @@ -10,6 +10,10 @@ import { resolveRouteCredentialValue, } from '../integrations/routeMetadata.js' import { DEFAULT_CODEX_BASE_URL } from '../services/api/providerConfig.js' +import { + getClaudeConfigHomeDirOverrideForTesting, + setClaudeConfigHomeDirForTesting, +} from './envUtils.js' import { getProviderValidationError } from './providerValidation.js' import { applySavedProfileToCurrentSession, @@ -2002,8 +2006,10 @@ test('deleteProfileFile clears the default profile and legacy workspace fallback const configDir = mkdtempSync(join(tmpdir(), 'openclaude-delete-config-profile-')) const previousConfigDir = process.env.OPENCLAUDE_CONFIG_DIR const previousCwd = process.cwd() + const previousConfigOverride = getClaudeConfigHomeDirOverrideForTesting() try { + setClaudeConfigHomeDirForTesting(undefined) process.env.OPENCLAUDE_CONFIG_DIR = configDir process.chdir(cwd) @@ -2015,6 +2021,7 @@ test('deleteProfileFile clears the default profile and legacy workspace fallback OPENAI_MODEL: 'llama3.1:8b', }) + assert.equal(getDefaultProfileFilePath(), join(configDir, PROFILE_FILE_NAME)) saveProfileFile(configProfile) assert.equal(existsSync(join(configDir, PROFILE_FILE_NAME)), true) writeFileSync( @@ -2029,6 +2036,7 @@ test('deleteProfileFile clears the default profile and legacy workspace fallback assert.equal(existsSync(join(cwd, PROFILE_FILE_NAME)), false) assert.equal(loadProfileFile(), null) } finally { + setClaudeConfigHomeDirForTesting(previousConfigOverride) process.chdir(previousCwd) if (previousConfigDir === undefined) { delete process.env.OPENCLAUDE_CONFIG_DIR diff --git a/src/utils/sessionStorage.commandOnlySession.test.ts b/src/utils/sessionStorage.commandOnlySession.test.ts index fc1babcc25..9087913cef 100644 --- a/src/utils/sessionStorage.commandOnlySession.test.ts +++ b/src/utils/sessionStorage.commandOnlySession.test.ts @@ -3,7 +3,7 @@ import { type UUID } from 'node:crypto' import { existsSync } from 'node:fs' import { mkdtemp, readFile, rm } from 'node:fs/promises' import { tmpdir } from 'node:os' -import { join } from 'node:path' +import { join, sep } from 'node:path' import { acquireSharedMutationLock, @@ -16,7 +16,10 @@ import { switchSession, } from '../bootstrap/state.js' import type { Message } from '../types/message.js' -import { setClaudeConfigHomeDirForTesting } from './envUtils.js' +import { + getClaudeConfigHomeDirOverrideForTesting, + setClaudeConfigHomeDirForTesting, +} from './envUtils.js' import { createAssistantMessage, createUserMessage, @@ -24,6 +27,7 @@ import { } from './messages.js' import { flushSessionStorage, + getProjectDir, getTranscriptPathForSession, recordTranscript, resetProjectForTesting, @@ -34,16 +38,19 @@ import { const SESSION = '00000000-0000-4000-8000-00000000c001' as UUID const OTHER = '00000000-0000-4000-8000-00000000c002' as UUID const tempDirs: string[] = [] -const savedEnv = { - NODE_ENV: process.env.NODE_ENV, - TEST_ENABLE_SESSION_PERSISTENCE: process.env.TEST_ENABLE_SESSION_PERSISTENCE, - ENABLE_SESSION_PERSISTENCE: process.env.ENABLE_SESSION_PERSISTENCE, -} +let savedEnv: Record +let originalConfigOverride: string | undefined let originalSessionId: string let originalPersistenceDisabled: boolean beforeEach(async () => { await acquireSharedMutationLock('utils/sessionStorage.commandOnlySession.test.ts') + savedEnv = { + NODE_ENV: process.env.NODE_ENV, + TEST_ENABLE_SESSION_PERSISTENCE: process.env.TEST_ENABLE_SESSION_PERSISTENCE, + ENABLE_SESSION_PERSISTENCE: process.env.ENABLE_SESSION_PERSISTENCE, + } + originalConfigOverride = getClaudeConfigHomeDirOverrideForTesting() originalSessionId = getSessionId() originalPersistenceDisabled = isSessionPersistenceDisabled() process.env.NODE_ENV = 'development' @@ -53,25 +60,33 @@ beforeEach(async () => { const configDir = await mkdtemp(join(tmpdir(), 'openclaude-command-only-')) tempDirs.push(configDir) setClaudeConfigHomeDirForTesting(configDir) + getProjectDir.cache.clear() resetProjectForTesting() switchSession(SESSION as never) await resetSessionFilePointer() + expect(getTranscriptPathForSession(SESSION).startsWith(`${configDir}${sep}`)).toBe(true) }) afterEach(async () => { try { - for (const [key, value] of Object.entries(savedEnv)) { - if (value === undefined) delete process.env[key] - else process.env[key] = value - } - setSessionPersistenceDisabled(originalPersistenceDisabled) - switchSession(originalSessionId as never) - resetProjectForTesting() - await Promise.all( - tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })), - ) + await flushSessionStorage() } finally { - releaseSharedMutationLock() + try { + for (const [key, value] of Object.entries(savedEnv)) { + if (value === undefined) delete process.env[key] + else process.env[key] = value + } + setSessionPersistenceDisabled(originalPersistenceDisabled) + switchSession(originalSessionId as never) + resetProjectForTesting() + setClaudeConfigHomeDirForTesting(originalConfigOverride) + getProjectDir.cache.clear() + await Promise.all( + tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })), + ) + } finally { + releaseSharedMutationLock() + } } }) diff --git a/src/utils/sessionStorage.deletedSession.test.ts b/src/utils/sessionStorage.deletedSession.test.ts index b927a246bd..ff070ac0b1 100644 --- a/src/utils/sessionStorage.deletedSession.test.ts +++ b/src/utils/sessionStorage.deletedSession.test.ts @@ -17,11 +17,15 @@ import { switchSession, } from '../bootstrap/state.js' import { createGoalState } from '../services/goal/state.js' -import { setClaudeConfigHomeDirForTesting } from './envUtils.js' +import { + getClaudeConfigHomeDirOverrideForTesting, + setClaudeConfigHomeDirForTesting, +} from './envUtils.js' import { deleteSessionFiles } from './sessionDeletion.js' import * as sessionStorage from './sessionStorage.js' import { flushSessionStorage, + getProjectDir, getTranscriptPathForSession, recordGoalState, resetProjectForTesting, @@ -34,16 +38,19 @@ import { const DELETED = '00000000-0000-4000-8000-00000000d001' as UUID const ACTIVE = '00000000-0000-4000-8000-00000000d002' as UUID const tempDirs: string[] = [] -const savedEnv = { - NODE_ENV: process.env.NODE_ENV, - TEST_ENABLE_SESSION_PERSISTENCE: process.env.TEST_ENABLE_SESSION_PERSISTENCE, - ENABLE_SESSION_PERSISTENCE: process.env.ENABLE_SESSION_PERSISTENCE, -} +let savedEnv: Record +let originalConfigOverride: string | undefined let originalSessionId: string let originalPersistenceDisabled: boolean beforeEach(async () => { await acquireSharedMutationLock('utils/sessionStorage.deletedSession.test.ts') + savedEnv = { + NODE_ENV: process.env.NODE_ENV, + TEST_ENABLE_SESSION_PERSISTENCE: process.env.TEST_ENABLE_SESSION_PERSISTENCE, + ENABLE_SESSION_PERSISTENCE: process.env.ENABLE_SESSION_PERSISTENCE, + } + originalConfigOverride = getClaudeConfigHomeDirOverrideForTesting() originalSessionId = getSessionId() originalPersistenceDisabled = isSessionPersistenceDisabled() process.env.NODE_ENV = 'development' @@ -53,24 +60,31 @@ beforeEach(async () => { const configDir = await mkdtemp(join(tmpdir(), 'openclaude-deleted-session-')) tempDirs.push(configDir) setClaudeConfigHomeDirForTesting(configDir) + getProjectDir.cache.clear() resetProjectForTesting() }) afterEach(async () => { try { - mock.restore() - for (const [key, value] of Object.entries(savedEnv)) { - if (value === undefined) delete process.env[key] - else process.env[key] = value - } - setSessionPersistenceDisabled(originalPersistenceDisabled) - switchSession(originalSessionId as never) - resetProjectForTesting() - await Promise.all( - tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })), - ) + await flushSessionStorage() } finally { - releaseSharedMutationLock() + try { + mock.restore() + for (const [key, value] of Object.entries(savedEnv)) { + if (value === undefined) delete process.env[key] + else process.env[key] = value + } + setSessionPersistenceDisabled(originalPersistenceDisabled) + switchSession(originalSessionId as never) + resetProjectForTesting() + setClaudeConfigHomeDirForTesting(originalConfigOverride) + getProjectDir.cache.clear() + await Promise.all( + tempDirs.splice(0).map(dir => rm(dir, { recursive: true, force: true })), + ) + } finally { + releaseSharedMutationLock() + } } }) From 0d85ac1be50a8e58f7b95ac5d8cc9926dd87bd6e Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 05:45:16 -0700 Subject: [PATCH 026/121] test: restore leaked provider, betas, sessionTitle and discovery module mocks (TA-001 slice 2) (#2332) * test: restore leaked provider, betas, sessionTitle and discovery module mocks (TA-001 slice 2) * test(model): restore real providerProfiles module after model command tests --------- Co-authored-by: jatmn <12479882.jatmn@users.noreply.github.com> --- src/commands/effort/effort.test.tsx | 17 ++++- src/commands/model/model.test.tsx | 69 ++++++++++++++----- .../ModelPicker.switchProfile.test.ts | 17 ++++- src/utils/apiPreconnect.test.ts | 14 +++- src/utils/attribution.test.ts | 23 +++++-- src/utils/conversationRecovery.hooks.test.ts | 18 ++++- src/utils/conversationRecovery.test.ts | 18 ++++- src/utils/model/agent.test.ts | 16 ++++- .../model/modelOptions.catalogDedup.test.ts | 17 ++++- .../model/modelOptions.codexRecovery.test.ts | 14 +++- .../model/modelOptions.crossProfile.test.ts | 17 ++++- src/utils/model/modelOptions.gateways.test.ts | 14 +++- src/utils/model/modelOptions.github.test.ts | 14 +++- src/utils/model/modelOptions.hicap.test.ts | 14 +++- src/utils/model/openaiModelDiscovery.test.ts | 14 +++- src/utils/sessionTitle.test.ts | 37 +++++++++- .../statusNoticeDefinitions.safety.test.tsx | 26 ++++++- src/utils/swarm/teammateModel.test.ts | 14 +++- 18 files changed, 321 insertions(+), 52 deletions(-) diff --git a/src/commands/effort/effort.test.tsx b/src/commands/effort/effort.test.tsx index 735e2e57b4..8455d6218c 100644 --- a/src/commands/effort/effort.test.tsx +++ b/src/commands/effort/effort.test.tsx @@ -1,6 +1,6 @@ import { PassThrough } from 'node:stream' -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import React from 'react' import { render } from '../../ink.js' @@ -11,11 +11,24 @@ import { } from '../../test/sharedMutationLock.js' import * as actualAuth from '../../utils/auth.js' import * as actualModelSupportOverrides from '../../utils/model/modelSupportOverrides.js' -import * as actualProviders from '../../utils/model/providers.js' import * as actualSettings from '../../utils/settings/settings.js' import * as actualThinking from '../../utils/thinking.js' import * as actualGrowthbook from '../../services/analytics/growthbook.js' +const providerSpecifier = '../../utils/model/providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + +const actualProviders = realProvidersForCleanup + const originalEffortEnv = process.env.CLAUDE_CODE_EFFORT_LEVEL beforeEach(async () => { diff --git a/src/commands/model/model.test.tsx b/src/commands/model/model.test.tsx index 6f05c5ad39..0e983403c3 100644 --- a/src/commands/model/model.test.tsx +++ b/src/commands/model/model.test.tsx @@ -1,6 +1,6 @@ import { PassThrough } from 'node:stream' -import { afterEach, beforeEach, expect, mock, setSystemTime, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, setSystemTime, test } from 'bun:test' import React from 'react' import { getAdditionalModelOptionsCacheScope } from '../../services/api/providerConfig.js' @@ -22,20 +22,47 @@ import { getRouteDescriptor, } from '../../integrations/index.js' import { mergeRouteCatalogEntries } from '../../utils/model/routeCatalogOptions.js' -import * as actualFastModeForModelTest from '../../utils/fastMode.js' -import * as actualExtraUsageForModelTest from '../../utils/extraUsage.js' -import * as actualDiscoveryServiceForModelTest from '../../integrations/discoveryService.js' -import * as actualDiscoveryCacheForModelTest from '../../integrations/discoveryCache.js' -const REAL_DISCOVERY_SERVICE_FOR_MODEL_TEST = { ...actualDiscoveryServiceForModelTest } -const REAL_DISCOVERY_CACHE_FOR_MODEL_TEST = { ...actualDiscoveryCacheForModelTest } +const nonce = `${Date.now()}-${Math.random()}` +const REAL_DISCOVERY_SERVICE_FOR_MODEL_TEST = { + ...await import(`../../integrations/discoveryService.js?real=${nonce}`), +} +const REAL_DISCOVERY_CACHE_FOR_MODEL_TEST = { + ...await import(`../../integrations/discoveryCache.js?real=${nonce}`), +} // Snapshot the real fast-mode module up front so the cross-profile switch test // can mock it with a full surface and restore the real one afterwards. -const REAL_FAST_MODE_FOR_MODEL_TEST = { ...actualFastModeForModelTest } +const REAL_FAST_MODE_FOR_MODEL_TEST = { + ...await import(`../../utils/fastMode.js?real=${nonce}`), +} // Same for extraUsage, so the cross-profile confirmation test can force the // `Billed as extra usage` branch and restore the real surface afterwards. -const REAL_EXTRA_USAGE_FOR_MODEL_TEST = { ...actualExtraUsageForModelTest } +const REAL_EXTRA_USAGE_FOR_MODEL_TEST = { + ...await import(`../../utils/extraUsage.js?real=${nonce}`), +} +const REAL_PROVIDER_PROFILES_FOR_MODEL_TEST = { + ...await import(`../../utils/providerProfiles.js?real=${nonce}`), +} + +function restoreDiscoveryModules(): void { + mock.module( + '../../integrations/discoveryService.js', + () => REAL_DISCOVERY_SERVICE_FOR_MODEL_TEST, + ) + mock.module( + '../../integrations/discoveryCache.js', + () => REAL_DISCOVERY_CACHE_FOR_MODEL_TEST, + ) +} + +afterAll(() => { + mock.restore() + mock.module('../../utils/providerProfiles.js', () => REAL_PROVIDER_PROFILES_FOR_MODEL_TEST) + restoreDiscoveryModules() + mock.module('../../utils/fastMode.js', () => REAL_FAST_MODE_FOR_MODEL_TEST) + mock.module('../../utils/extraUsage.js', () => REAL_EXTRA_USAGE_FOR_MODEL_TEST) +}) type SettingsModule = typeof import('../../utils/settings/settings.js') @@ -3429,18 +3456,22 @@ test('/model does not auto-refresh descriptor models when nonessential traffic i discoverModelsForRoute, })) - mockProviderProfiles({ - getActiveOpenAIModelOptionsCache: () => [], - getActiveProviderProfile: () => undefined, - getProfileModelOptions: () => [], - setActiveOpenAIModelOptionsCache: () => {}, - }) + try { + mockProviderProfiles({ + getActiveOpenAIModelOptionsCache: () => [], + getActiveProviderProfile: () => undefined, + getProfileModelOptions: () => [], + setActiveOpenAIModelOptionsCache: () => {}, + }) - const { call } = await importFreshModelModule('descriptor-privacy-open') - const result = await call(() => {}, {} as never, '') + const { call } = await importFreshModelModule('descriptor-privacy-open') + const result = await call(() => {}, {} as never, '') - expect(result).toBeTruthy() - expect(discoverModelsForRoute).not.toHaveBeenCalled() + expect(result).toBeTruthy() + expect(discoverModelsForRoute).not.toHaveBeenCalled() + } finally { + restoreDiscoveryModules() + } }) test('cross-profile /model switch drops latched fast mode before activating an unsupported target profile (#1119)', async () => { diff --git a/src/components/ModelPicker.switchProfile.test.ts b/src/components/ModelPicker.switchProfile.test.ts index 3634601470..d3ce35f22e 100644 --- a/src/components/ModelPicker.switchProfile.test.ts +++ b/src/components/ModelPicker.switchProfile.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { resetModelStringsForTestingOnly } from '../bootstrap/state.js' import { acquireEnvMutex, releaseEnvMutex } from '../entrypoints/sdk/shared.js' @@ -10,16 +10,27 @@ import { import * as actualAuth from '../utils/auth.js' import * as actualModelOptions from '../utils/model/modelOptions.js' import * as actualProviderProfiles from '../utils/providerProfiles.js' -import * as actualProviders from '../utils/model/providers.js' import type { ProviderProfile } from '../utils/config.js' +const providerSpecifier = '../utils/model/providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + // Snapshot the real modules before any mock.module runs. bun live-repoints the // `actual*` namespaces to the active mock, so these plain-object copies are the // stable handle on the genuine implementations (2026-04-30 mock-leak lesson). const realAuth = { ...actualAuth } const realModelOptions = { ...actualModelOptions } const realProviderProfiles = { ...actualProviderProfiles } -const realProviders = { ...actualProviders } +const realProviders = realProvidersForCleanup // bun's mock.module is process-wide and mock.restore() does NOT undo it, so // each mock is gated and falls through to the real implementation whenever diff --git a/src/utils/apiPreconnect.test.ts b/src/utils/apiPreconnect.test.ts index 0666f5a860..021a1aa25c 100644 --- a/src/utils/apiPreconnect.test.ts +++ b/src/utils/apiPreconnect.test.ts @@ -1,10 +1,22 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' import { asMockFetch } from '../test/typedMocks.js' +const providerSpecifier = './model/providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + const originalEnv = { ...process.env } const originalFetch = globalThis.fetch diff --git a/src/utils/attribution.test.ts b/src/utils/attribution.test.ts index edfcb8dc7e..64bc569e72 100644 --- a/src/utils/attribution.test.ts +++ b/src/utils/attribution.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test' +import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from 'bun:test' import { getClientType, getMainLoopModelOverride, @@ -6,16 +6,29 @@ import { setClientType, setMainLoopModelOverride, } from '../bootstrap/state.js' -import * as actualModel from './model/model.js' -import * as actualProviders from './model/providers.js' import { resetSettingsCache, setSessionSettingsCache, } from './settings/settingsCache.js' -import * as realSettings from './settings/settings.js' import type { SettingsJson } from './settings/types.js' -const actualSettings = { ...realSettings } +const nonce = `${Date.now()}-${Math.random()}` +const actualProviders = { + ...await import(`./model/providers.js?real=${nonce}`), +} +const actualModel = { + ...await import(`./model/model.js?real=${nonce}`), +} +const actualSettings = { + ...await import(`./settings/settings.js?real=${nonce}`), +} + +afterAll(() => { + mock.restore() + mock.module('./model/providers.js', () => actualProviders) + mock.module('./model/model.js', () => actualModel) + mock.module('./settings/settings.js', () => actualSettings) +}) let getAttributionTexts: (typeof import('./attribution.js'))['getAttributionTexts'] let getDefaultCommitCoAuthorEmail: (typeof import('./attribution.js'))[ diff --git a/src/utils/conversationRecovery.hooks.test.ts b/src/utils/conversationRecovery.hooks.test.ts index ee57b8355c..4738629f80 100644 --- a/src/utils/conversationRecovery.hooks.test.ts +++ b/src/utils/conversationRecovery.hooks.test.ts @@ -3,7 +3,7 @@ * conversationRecovery so Bun's mock.module can replace sessionStart before * that module is first loaded. */ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -11,8 +11,20 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as realProviders from './model/providers.js' -import * as realSessionStart from './sessionStart.js' + +const nonce = `${Date.now()}-${Math.random()}` +const realProviders = { + ...await import(`./model/providers.js?real=${nonce}`), +} +const realSessionStart = { + ...await import(`./sessionStart.js?real=${nonce}`), +} + +afterAll(() => { + mock.restore() + mock.module('./model/providers.js', () => realProviders) + mock.module('./sessionStart.js', () => realSessionStart) +}) const tempDirs: string[] = [] const originalEnv = { ...process.env } diff --git a/src/utils/conversationRecovery.test.ts b/src/utils/conversationRecovery.test.ts index c20a956fb4..d364d9ac0b 100644 --- a/src/utils/conversationRecovery.test.ts +++ b/src/utils/conversationRecovery.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -10,10 +10,22 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as realUdsClient from './udsClient.js' -import * as realProviders from './model/providers.js' import type { NormalizedMessage } from '../types/message.js' +const nonce = `${Date.now()}-${Math.random()}` +const realProviders = { + ...await import(`./model/providers.js?real=${nonce}`), +} +const realUdsClient = { + ...await import(`./udsClient.js?real=${nonce}`), +} + +afterAll(() => { + mock.restore() + mock.module('./model/providers.js', () => realProviders) + mock.module('./udsClient.js', () => realUdsClient) +}) + // Typed fixture for the thinking-strip gate tests. The full NormalizedMessage // shape carries fields these tests don't exercise, so the cast is centralized // here once rather than re-spelled as `as any` at each call site. diff --git a/src/utils/model/agent.test.ts b/src/utils/model/agent.test.ts index c6a89da955..1bce5aea1c 100644 --- a/src/utils/model/agent.test.ts +++ b/src/utils/model/agent.test.ts @@ -1,10 +1,22 @@ -import { describe, test, expect, beforeEach, afterEach, mock } from 'bun:test' +import { afterAll, describe, test, expect, beforeEach, afterEach, mock } from 'bun:test' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' -const actualProviders = { ...await import('./providers.js') } +const providerSpecifier = './providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + +const actualProviders = realProvidersForCleanup const originalSubagentModel = process.env.CLAUDE_CODE_SUBAGENT_MODEL const originalOpenAIModel = process.env.OPENAI_MODEL diff --git a/src/utils/model/modelOptions.catalogDedup.test.ts b/src/utils/model/modelOptions.catalogDedup.test.ts index 0913ab2ece..08616af917 100644 --- a/src/utils/model/modelOptions.catalogDedup.test.ts +++ b/src/utils/model/modelOptions.catalogDedup.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js' import { acquireEnvMutex, releaseEnvMutex } from '../../entrypoints/sdk/shared.js' @@ -11,10 +11,21 @@ import { import * as actualIndex from '../../integrations/index.js' import * as actualProviderConfig from '../../services/api/providerConfig.js' import * as actualProviderProfiles from '../providerProfiles.js' -import * as actualProviders from './providers.js' import * as actualModel from './model.js' import type { ModelOption } from './modelOptions.js' +const providerSpecifier = './providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + // Snapshot the real modules before any mock.module runs (same lesson as the // 2026-04-30 mock-leak note in lessons_learned.md — bun live-repoints the // `actual*` namespaces to the active mock, so these copies are the stable @@ -22,7 +33,7 @@ import type { ModelOption } from './modelOptions.js' const realIndex = { ...actualIndex } const realProviderConfig = { ...actualProviderConfig } const realProviderProfiles = { ...actualProviderProfiles } -const realProviders = { ...actualProviders } +const realProviders = realProvidersForCleanup const realModel = { ...actualModel } // bun's mock.module is process-wide and mock.restore() does NOT undo it, so diff --git a/src/utils/model/modelOptions.codexRecovery.test.ts b/src/utils/model/modelOptions.codexRecovery.test.ts index 2b850ec1af..00449014e4 100644 --- a/src/utils/model/modelOptions.codexRecovery.test.ts +++ b/src/utils/model/modelOptions.codexRecovery.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js' import { acquireEnvMutex, releaseEnvMutex } from '../../entrypoints/sdk/shared.js' @@ -7,6 +7,18 @@ import { setSessionSettingsCache, } from '../settings/settingsCache.js' +const providerSpecifier = './providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + // Picker recovery for persisted Codex models while a NON-Codex provider is // active (the curated Codex options are only appended for the openai/codex // providers): the persisted value must surface the curated label/description diff --git a/src/utils/model/modelOptions.crossProfile.test.ts b/src/utils/model/modelOptions.crossProfile.test.ts index 008d2317d6..cf1a4bd663 100644 --- a/src/utils/model/modelOptions.crossProfile.test.ts +++ b/src/utils/model/modelOptions.crossProfile.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test' import { mock } from 'bun:test' import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js' @@ -15,7 +15,6 @@ import { // the canonical pattern; this is the same lesson as the 2026-04-30 mock-leak // note in lessons_learned.md. import * as actualProviderProfiles from '../providerProfiles.js' -import * as actualProviders from './providers.js' import * as actualAuth from '../auth.js' import * as actualProviderConfig from '../../services/api/providerConfig.js' import * as actualSettings from '../settings/settings.js' @@ -28,11 +27,23 @@ import type { ModelOption } from './modelOptions.js' import type { ProviderProfile } from '../config.js' import type { SettingsJson } from '../settings/types.js' +const providerSpecifier = './providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + // Snapshot the real modules before any mock.module runs. bun live-repoints the // `actual*` namespaces to the active mock, so these plain-object copies are the // stable handle on the genuine implementations. const realProviderProfiles = { ...actualProviderProfiles } -const realProviders = { ...actualProviders } +const realProviders = realProvidersForCleanup const realAuth = { ...actualAuth } const realProviderConfig = { ...actualProviderConfig } const realSettings = { ...actualSettings } diff --git a/src/utils/model/modelOptions.gateways.test.ts b/src/utils/model/modelOptions.gateways.test.ts index a04b43348b..284156c400 100644 --- a/src/utils/model/modelOptions.gateways.test.ts +++ b/src/utils/model/modelOptions.gateways.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js' import { acquireEnvMutex, releaseEnvMutex } from '../../entrypoints/sdk/shared.js' @@ -9,6 +9,18 @@ import { setSessionSettingsCache, } from '../settings/settingsCache.js' +const providerSpecifier = './providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + async function importFreshModelOptionsModule( provider = 'openai', isFirstPartyAnthropicBaseUrl = false, diff --git a/src/utils/model/modelOptions.github.test.ts b/src/utils/model/modelOptions.github.test.ts index 5b7414cd84..768ff7f586 100644 --- a/src/utils/model/modelOptions.github.test.ts +++ b/src/utils/model/modelOptions.github.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, test } from 'bun:test' import { mock } from 'bun:test' import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js' @@ -12,6 +12,18 @@ import { setSessionSettingsCache, } from '../settings/settingsCache.js' +const providerSpecifier = './providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + async function importFreshModelOptionsModule() { mock.restore() mock.module('./providers.js', () => ({ diff --git a/src/utils/model/modelOptions.hicap.test.ts b/src/utils/model/modelOptions.hicap.test.ts index 0ba3908d64..2d4647eb6c 100644 --- a/src/utils/model/modelOptions.hicap.test.ts +++ b/src/utils/model/modelOptions.hicap.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js' import { acquireEnvMutex, releaseEnvMutex } from '../../entrypoints/sdk/shared.js' @@ -8,6 +8,18 @@ import { setSessionSettingsCache, } from '../settings/settingsCache.js' +const providerSpecifier = './providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + async function importFreshModelOptionsModule() { mock.restore() mock.module('./providers.js', () => ({ diff --git a/src/utils/model/openaiModelDiscovery.test.ts b/src/utils/model/openaiModelDiscovery.test.ts index 5399812e81..7623b9c461 100644 --- a/src/utils/model/openaiModelDiscovery.test.ts +++ b/src/utils/model/openaiModelDiscovery.test.ts @@ -1,10 +1,22 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import axios from 'axios' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' +const providerSpecifier = './providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + const originalAxiosGet = axios.get const originalEnv = { CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: diff --git a/src/utils/sessionTitle.test.ts b/src/utils/sessionTitle.test.ts index a114606e1f..a3c1513d6c 100644 --- a/src/utils/sessionTitle.test.ts +++ b/src/utils/sessionTitle.test.ts @@ -1,4 +1,38 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' + +const nonce = `${Date.now()}-${Math.random()}` +const realAnalytics = { + ...await import(`../services/analytics/index.js?real=${nonce}`), +} +const realClaude = { + ...await import(`../services/api/claude.js?real=${nonce}`), +} +const realBetas = { + ...await import(`./betas.js?real=${nonce}`), +} +const realCombinedAbortSignal = { + ...await import(`./combinedAbortSignal.js?real=${nonce}`), +} +const realDebug = { + ...await import(`./debug.js?real=${nonce}`), +} +const realModel = { + ...await import(`./model/model.js?real=${nonce}`), +} +const realProviders = { + ...await import(`./model/providers.js?real=${nonce}`), +} + +afterAll(() => { + mock.restore() + mock.module('../services/analytics/index.js', () => realAnalytics) + mock.module('../services/api/claude.js', () => realClaude) + mock.module('./betas.js', () => realBetas) + mock.module('./combinedAbortSignal.js', () => realCombinedAbortSignal) + mock.module('./debug.js', () => realDebug) + mock.module('./model/model.js', () => realModel) + mock.module('./model/providers.js', () => realProviders) +}) type QueryHaikuArgs = { outputFormat?: unknown @@ -70,6 +104,7 @@ async function importSubject() { }, })) mock.module('../services/api/claude.js', () => ({ + ...realClaude, queryHaiku: async (args: QueryHaikuArgs) => { queryHaikuCalls.push(args) return queryHaikuImpl(args) diff --git a/src/utils/statusNoticeDefinitions.safety.test.tsx b/src/utils/statusNoticeDefinitions.safety.test.tsx index cffa7d6662..26d76d772f 100644 --- a/src/utils/statusNoticeDefinitions.safety.test.tsx +++ b/src/utils/statusNoticeDefinitions.safety.test.tsx @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' import figures from 'figures' import type { StatusNoticeContext } from './statusNoticeDefinitions.js' import { @@ -7,6 +7,20 @@ import { } from './statusNoticeDefinitions.js' import { renderToString } from './staticRender.js' +const nonce = `${Date.now()}-${Math.random()}` +const realProviders = { + ...await import(`./model/providers.js?real=${nonce}`), +} +const realBetas = { + ...await import(`./betas.js?real=${nonce}`), +} + +afterAll(() => { + mock.restore() + mock.module('./model/providers.js', () => realProviders) + mock.module('./betas.js', () => realBetas) +}) + // Regression coverage for issue #244 — the two safety-related status notices // that warn 3P users when they are running without the AI classifier or with // `--dangerously-skip-permissions` outside a sandbox. @@ -65,9 +79,11 @@ afterEach(() => { describe('third-party permissive mode notice (#244 finding 1)', () => { test('fires when 3P + acceptEdits + classifier-off model', async () => { mock.module('./model/providers.js', () => ({ + ...realProviders, getAPIProvider: () => 'openai', })) mock.module('./betas.js', () => ({ + ...realBetas, modelSupportsAutoMode: () => false, })) const { getActiveNotices: freshGetActiveNotices } = await import( @@ -80,9 +96,11 @@ describe('third-party permissive mode notice (#244 finding 1)', () => { test('fires when 3P + bypassPermissions', async () => { mock.module('./model/providers.js', () => ({ + ...realProviders, getAPIProvider: () => 'openai', })) mock.module('./betas.js', () => ({ + ...realBetas, modelSupportsAutoMode: () => false, })) const { getActiveNotices: freshGetActiveNotices } = await import( @@ -95,9 +113,11 @@ describe('third-party permissive mode notice (#244 finding 1)', () => { test('suppressed in default mode even on 3P', async () => { mock.module('./model/providers.js', () => ({ + ...realProviders, getAPIProvider: () => 'openai', })) mock.module('./betas.js', () => ({ + ...realBetas, modelSupportsAutoMode: () => false, })) const { getActiveNotices: freshGetActiveNotices } = await import( @@ -110,9 +130,11 @@ describe('third-party permissive mode notice (#244 finding 1)', () => { test('suppressed on firstParty Anthropic in acceptEdits', async () => { mock.module('./model/providers.js', () => ({ + ...realProviders, getAPIProvider: () => 'firstParty', })) mock.module('./betas.js', () => ({ + ...realBetas, modelSupportsAutoMode: () => true, })) const { getActiveNotices: freshGetActiveNotices } = await import( @@ -125,9 +147,11 @@ describe('third-party permissive mode notice (#244 finding 1)', () => { test('suppressed when classifier supports the model (defensive)', async () => { mock.module('./model/providers.js', () => ({ + ...realProviders, getAPIProvider: () => 'openai', })) mock.module('./betas.js', () => ({ + ...realBetas, modelSupportsAutoMode: () => true, })) const { getActiveNotices: freshGetActiveNotices } = await import( diff --git a/src/utils/swarm/teammateModel.test.ts b/src/utils/swarm/teammateModel.test.ts index a7492fbb22..4d45cda0f4 100644 --- a/src/utils/swarm/teammateModel.test.ts +++ b/src/utils/swarm/teammateModel.test.ts @@ -1,9 +1,21 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' +const providerSpecifier = '../model/providers.js' +const realProvidersForCleanup = { + ...await import( + `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + ), +} + +afterAll(() => { + mock.restore() + mock.module(providerSpecifier, () => realProvidersForCleanup) +}) + beforeEach(async () => { await acquireSharedMutationLock('utils/swarm/teammateModel.test.ts') }) From c84411b9dd783a6154838537a0561705c70dc0ab Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 05:46:03 -0700 Subject: [PATCH 027/121] test: run attribution suites in a child when auth, beta or routing exports are polluted (TA-001 slice 5) (#2334) --- .../api/authRouting.attribution.test.ts | 328 +++++---- .../providerModuleIsolation.fixture.ts | 107 +++ src/test/providerModuleIsolation.test.ts | 107 +++ src/test/providerModuleIsolation.ts | 56 ++ src/utils/sideQuery.attribution.test.ts | 623 +++++++++--------- 5 files changed, 728 insertions(+), 493 deletions(-) create mode 100644 src/test/fixtures/providerModuleIsolation.fixture.ts create mode 100644 src/test/providerModuleIsolation.test.ts diff --git a/src/services/api/authRouting.attribution.test.ts b/src/services/api/authRouting.attribution.test.ts index 4dba7b97d7..c38ac77be2 100644 --- a/src/services/api/authRouting.attribution.test.ts +++ b/src/services/api/authRouting.attribution.test.ts @@ -1,182 +1,185 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' -import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js' -import { applyAnthropicAttributionPolicy } from '../../utils/anthropicAttribution.js' import { - providerModuleIsMocked, - REAL_PROVIDER_TEST_CHILD_ENV, + attributionModulesNeedIsolation, REAL_PROVIDER_TEST_TIMEOUT_MS, runTestFileWithRealProviders, } from '../../test/providerModuleIsolation.js' -// Bun keeps mock.module() registrations process-global across test files. -// Bind request modules only when the canonical provider module is real. When a -// prior file replaced it, run this file in a clean child process instead. -const _realProvidersModule = await import( - `../../utils/model/providers.js?attributionReal=${Date.now()}-${Math.random()}` -) -const _loadedProvidersModule = await import('src/utils/model/providers.js') -const runInProviderIsolatedChild = - process.env[REAL_PROVIDER_TEST_CHILD_ENV] !== '1' && - providerModuleIsMocked(_loadedProvidersModule, _realProvidersModule) -type AuthRoutingModule = typeof import('./authRouting.js') -let resolveAnthropicAttributionAuthFromSources!: AuthRoutingModule['resolveAnthropicAttributionAuthFromSources'] -let resolveCurrentAnthropicAttributionPolicy!: AuthRoutingModule['resolveCurrentAnthropicAttributionPolicy'] -if (!runInProviderIsolatedChild) { - ;({ +// Inspect canonical dependencies before binding the subject or suite setup. +const needsChild = await attributionModulesNeedIsolation() +if (needsChild) { + test('runs attribution route cases with the real provider module', async () => { + await runTestFileWithRealProviders(import.meta.path) + }, { timeout: REAL_PROVIDER_TEST_TIMEOUT_MS + 5_000 }) +} else { + const { acquireSharedMutationLock, releaseSharedMutationLock } = + await import('../../test/sharedMutationLock.js') + const { applyAnthropicAttributionPolicy } = + await import('../../utils/anthropicAttribution.js') + const { resolveAnthropicAttributionAuthFromSources, resolveCurrentAnthropicAttributionPolicy, - } = await import( + }: typeof import('./authRouting.js') = await import( `./authRouting.js?attributionReal=${Date.now()}-${Math.random()}` - )) -} + ) -const routeEnvKeys = [ - 'ANTHROPIC_API_KEY', - 'ANTHROPIC_AUTH_TOKEN', - 'ANTHROPIC_BASE_URL', - 'ANTHROPIC_MODEL', - 'CLAUDE_CODE_ENTRYPOINT', - 'CLAUDE_CODE_OAUTH_TOKEN', - 'CLAUDE_CODE_REMOTE', - 'CLAUDE_CODE_USE_BEDROCK', - 'CLAUDE_CODE_USE_FOUNDRY', - 'CLAUDE_CODE_USE_GEMINI', - 'CLAUDE_CODE_USE_GITHUB', - 'CLAUDE_CODE_USE_MISTRAL', - 'CLAUDE_CODE_USE_OPENAI', - 'CLAUDE_CODE_USE_VERTEX', - 'MINIMAX_API_KEY', - 'OPENAI_BASE_URL', - 'OPENAI_MODEL', -] as const -const originalEnv = { ...process.env } - -beforeEach(async () => { - await acquireSharedMutationLock('authRouting.attribution.test.ts') - for (const key of routeEnvKeys) delete process.env[key] - process.env.ANTHROPIC_API_KEY = 'sk-test-attribution-routing' -}) - -afterEach(() => { - try { - for (const key of routeEnvKeys) { - if (originalEnv[key] === undefined) { - delete process.env[key] - } else { - process.env[key] = originalEnv[key] + const routeEnvKeys = [ + 'ANTHROPIC_API_KEY', + 'ANTHROPIC_AUTH_TOKEN', + 'ANTHROPIC_BASE_URL', + 'ANTHROPIC_MODEL', + 'CLAUDE_CODE_ENTRYPOINT', + 'CLAUDE_CODE_OAUTH_TOKEN', + 'CLAUDE_CODE_REMOTE', + 'CLAUDE_CODE_USE_BEDROCK', + 'CLAUDE_CODE_USE_FOUNDRY', + 'CLAUDE_CODE_USE_GEMINI', + 'CLAUDE_CODE_USE_GITHUB', + 'CLAUDE_CODE_USE_MISTRAL', + 'CLAUDE_CODE_USE_OPENAI', + 'CLAUDE_CODE_USE_VERTEX', + 'MINIMAX_API_KEY', + 'OPENAI_BASE_URL', + 'OPENAI_MODEL', + ] as const + const originalEnv = { ...process.env } + + beforeEach(async () => { + await acquireSharedMutationLock('authRouting.attribution.test.ts') + for (const key of routeEnvKeys) delete process.env[key] + process.env.ANTHROPIC_API_KEY = 'sk-test-attribution-routing' + }) + + afterEach(() => { + try { + for (const key of routeEnvKeys) { + if (originalEnv[key] === undefined) { + delete process.env[key] + } else { + process.env[key] = originalEnv[key] + } } + } finally { + releaseSharedMutationLock() } - } finally { - releaseSharedMutationLock() - } -}) + }) -if (runInProviderIsolatedChild) { - test('runs attribution route cases with the real provider module', async () => { - await runTestFileWithRealProviders(import.meta.path) - }, { timeout: REAL_PROVIDER_TEST_TIMEOUT_MS + 5_000 }) -} + describe('current Anthropic attribution route resolution', () => { + for (const apiKeySource of [ + 'ANTHROPIC_API_KEY', + 'apiKeyHelper', + ] as const) { + test(`ignores ${apiKeySource} when managed OAuth is effective`, () => { + expect( + resolveAnthropicAttributionAuthFromSources({ + apiKeySource, + authTokenSource: 'CLAUDE_CODE_OAUTH_TOKEN', + bareMode: false, + isSubscriber: true, + managedOAuthContext: true, + }), + ).toBe('oauth_subscription') + }) + } -const describeAttribution = runInProviderIsolatedChild - ? describe.skip - : describe + for (const [apiKeySource, authTokenSource] of [ + ['ANTHROPIC_API_KEY', 'none'], + ['apiKeyHelper', 'apiKeyHelper'], + ] as const) { + test(`preserves bare-mode ${apiKeySource} in a managed context`, () => { + expect( + resolveAnthropicAttributionAuthFromSources({ + apiKeySource, + authTokenSource, + bareMode: true, + isSubscriber: false, + managedOAuthContext: true, + }), + ).toBe('api_key') + }) + } -describeAttribution('current Anthropic attribution route resolution', () => { - for (const apiKeySource of [ - 'ANTHROPIC_API_KEY', - 'apiKeyHelper', - ] as const) { - test(`ignores ${apiKeySource} when managed OAuth is effective`, () => { + test('preserves a managed API key when managed OAuth is not effective', () => { expect( resolveAnthropicAttributionAuthFromSources({ - apiKeySource, - authTokenSource: 'CLAUDE_CODE_OAUTH_TOKEN', + apiKeySource: '/login managed key', + authTokenSource: 'none', bareMode: false, - isSubscriber: true, + isSubscriber: false, managedOAuthContext: true, }), - ).toBe('oauth_subscription') + ).toBe('api_key') }) - } - for (const [apiKeySource, authTokenSource] of [ - ['ANTHROPIC_API_KEY', 'none'], - ['apiKeyHelper', 'apiKeyHelper'], - ] as const) { - test(`preserves bare-mode ${apiKeySource} in a managed context`, () => { + test('does not force managed OAuth past an explicit non-subscriber result', () => { expect( resolveAnthropicAttributionAuthFromSources({ - apiKeySource, - authTokenSource, - bareMode: true, + apiKeySource: 'none', + authTokenSource: 'CLAUDE_CODE_OAUTH_TOKEN', + bareMode: false, isSubscriber: false, managedOAuthContext: true, }), - ).toBe('api_key') + ).toBe('unknown') }) - } - - test('preserves a managed API key when managed OAuth is not effective', () => { - expect( - resolveAnthropicAttributionAuthFromSources({ - apiKeySource: '/login managed key', - authTokenSource: 'none', - bareMode: false, - isSubscriber: false, - managedOAuthContext: true, - }), - ).toBe('api_key') - }) - - test('does not force managed OAuth past an explicit non-subscriber result', () => { - expect( - resolveAnthropicAttributionAuthFromSources({ - apiKeySource: 'none', - authTokenSource: 'CLAUDE_CODE_OAUTH_TOKEN', - bareMode: false, - isSubscriber: false, - managedOAuthContext: true, - }), - ).toBe('unknown') - }) - - for (const [label, envKey, envValue] of [ - ['remote', 'CLAUDE_CODE_REMOTE', '1'], - ['Claude Desktop', 'CLAUDE_CODE_ENTRYPOINT', 'claude-desktop'], - ] as const) { - test(`uses the effective OAuth credential for ${label} sessions`, () => { - process.env[envKey] = envValue - process.env.CLAUDE_CODE_OAUTH_TOKEN = 'oauth-test-token' - expect( - resolveCurrentAnthropicAttributionPolicy({ - attributionEnabled: false, - }), - ).toEqual({ - generate: true, - retain: true, - reason: 'official_oauth_required', + for (const [label, envKey, envValue] of [ + ['remote', 'CLAUDE_CODE_REMOTE', '1'], + ['Claude Desktop', 'CLAUDE_CODE_ENTRYPOINT', 'claude-desktop'], + ] as const) { + test(`uses the effective OAuth credential for ${label} sessions`, () => { + process.env[envKey] = envValue + process.env.CLAUDE_CODE_OAUTH_TOKEN = 'oauth-test-token' + + expect( + resolveCurrentAnthropicAttributionPolicy({ + attributionEnabled: false, + }), + ).toEqual({ + generate: true, + retain: true, + reason: 'official_oauth_required', + }) }) - }) - } + } - for (const [label, envKey] of [ - ['Bedrock', 'CLAUDE_CODE_USE_BEDROCK'], - ['Vertex', 'CLAUDE_CODE_USE_VERTEX'], - ['Foundry', 'CLAUDE_CODE_USE_FOUNDRY'], - ] as const) { - test(`does not emit first-party metadata on ${label}`, () => { - process.env[envKey] = '1' + for (const [label, envKey] of [ + ['Bedrock', 'CLAUDE_CODE_USE_BEDROCK'], + ['Vertex', 'CLAUDE_CODE_USE_VERTEX'], + ['Foundry', 'CLAUDE_CODE_USE_FOUNDRY'], + ] as const) { + test(`does not emit first-party metadata on ${label}`, () => { + process.env[envKey] = '1' + + const policy = resolveCurrentAnthropicAttributionPolicy({ + attributionEnabled: true, + }) + + expect(policy).toEqual({ + generate: false, + retain: false, + reason: 'non_official_route', + }) + expect( + applyAnthropicAttributionPolicy( + ['x-anthropic-billing-header: stale', 'stable route prompt'], + policy, + ), + ).toEqual(['stable route prompt']) + }) + } + test('does not emit through a provider override', () => { const policy = resolveCurrentAnthropicAttributionPolicy({ attributionEnabled: true, + providerOverride: { + model: 'third-party-model', + baseURL: 'https://provider.example/v1', + apiKey: 'provider-test-key', + }, }) - expect(policy).toEqual({ - generate: false, - retain: false, - reason: 'non_official_route', - }) + expect(policy).toMatchObject({ generate: false, retain: false }) expect( applyAnthropicAttributionPolicy( ['x-anthropic-billing-header: stale', 'stable route prompt'], @@ -184,35 +187,16 @@ describeAttribution('current Anthropic attribution route resolution', () => { ), ).toEqual(['stable route prompt']) }) - } - - test('does not emit through a provider override', () => { - const policy = resolveCurrentAnthropicAttributionPolicy({ - attributionEnabled: true, - providerOverride: { - model: 'third-party-model', - baseURL: 'https://provider.example/v1', - apiKey: 'provider-test-key', - }, - }) - expect(policy).toMatchObject({ generate: false, retain: false }) - expect( - applyAnthropicAttributionPolicy( - ['x-anthropic-billing-header: stale', 'stable route prompt'], - policy, - ), - ).toEqual(['stable route prompt']) - }) - - test('treats a custom Anthropic Messages route as non-official', () => { - process.env.ANTHROPIC_BASE_URL = 'https://messages.example/v1' - process.env.ANTHROPIC_MODEL = 'claude-compatible' + test('treats a custom Anthropic Messages route as non-official', () => { + process.env.ANTHROPIC_BASE_URL = 'https://messages.example/v1' + process.env.ANTHROPIC_MODEL = 'claude-compatible' - expect( - resolveCurrentAnthropicAttributionPolicy({ - attributionEnabled: true, - }), - ).toMatchObject({ generate: false, retain: false }) + expect( + resolveCurrentAnthropicAttributionPolicy({ + attributionEnabled: true, + }), + ).toMatchObject({ generate: false, retain: false }) + }) }) -}) +} diff --git a/src/test/fixtures/providerModuleIsolation.fixture.ts b/src/test/fixtures/providerModuleIsolation.fixture.ts new file mode 100644 index 0000000000..0304ac2ffa --- /dev/null +++ b/src/test/fixtures/providerModuleIsolation.fixture.ts @@ -0,0 +1,107 @@ +// Imported only by disposable test entrypoints; never discovered as a suite. +import { expect, mock, test } from 'bun:test' +import { + attributionModulesNeedIsolation, + providerModuleIsMocked, + REAL_PROVIDER_TEST_CHILD_ENV, +} from '../providerModuleIsolation.js' + +const scenario = process.env.ISOLATION_FIXTURE_SCENARIO +const inChild = process.env[REAL_PROVIDER_TEST_CHILD_ENV] === '1' + +if (scenario === 'assertion-failure') { + test('child assertion failure sentinel', () => { + expect('actual child value').toBe('expected child value') + }) +} else if (scenario?.startsWith('suite-')) { + if (!inChild) { + const auth = await import('../../utils/auth.js') + mock.module('../../utils/auth.js', () => ({ + ...auth, + isClaudeAISubscriber: () => false, + })) + // Any real-suite hook in the parent must fail, including cleanup hooks. + mock.module('../sharedMutationLock.js', () => ({ + acquireSharedMutationLock: () => { + throw new Error('parent setup sentinel') + }, + releaseSharedMutationLock: () => { + throw new Error('parent cleanup sentinel') + }, + })) + } + if (scenario === 'suite-sideQuery') { + await import('../../utils/sideQuery.attribution.test.js') + } else { + await import('../../services/api/authRouting.attribution.test.js') + } +} else { + if (scenario === 'auth') { + const auth = await import('../../utils/auth.js') + mock.module('../../utils/auth.js', () => ({ + ...auth, + isClaudeAISubscriber: () => false, + })) + // Provider restoration must not hide independent auth pollution. + const realProviders = await import( + `../../utils/model/providers.js?fixtureReal=${Math.random()}` + ) + mock.module('../../utils/model/providers.js', () => ({ ...realProviders })) + const providers = await import('../../utils/model/providers.js') + expect(providerModuleIsMocked(providers, realProviders)).toBe(false) + } else if (scenario === 'betas') { + const betas = await import('../../utils/betas.js') + mock.module('../../utils/betas.js', () => ({ + ...betas, + shouldUseGlobalCacheScope: () => false, + })) + } else if (scenario === 'routing') { + const routing = await import('../../services/api/authRouting.js') + mock.module('../../services/api/authRouting.js', () => ({ + ...routing, + shouldUseCustomAnthropicBearerAuth: () => true, + })) + } else if (scenario === 'metadata') { + const metadata = await import('../../integrations/routeMetadata.js') + mock.module('../../integrations/routeMetadata.js', () => ({ + ...metadata, + getRouteLabel: () => 'polluted route', + })) + } else if (scenario === 'client') { + const client = await import('../../services/api/client.js') + mock.module('../../services/api/client.js', () => ({ + ...client, + CLIENT_REQUEST_ID_HEADER: 'polluted-header', + })) + } else if (scenario === 'missing') { + // Register before loading: Bun retains omitted exports when replacing an + // already-loaded namespace, which would not exercise a missing surface. + mock.module('../../utils/model/providers.js', () => ({})) + const loaded = await import('../../utils/model/providers.js') + expect(Object.keys(loaded)).toHaveLength(0) + expect(loaded.isFirstPartyAnthropicBaseUrl).toBeUndefined() + } else if (scenario === 'unsupported' || scenario === 'native-function') { + const providers = await import('../../utils/model/providers.js') + mock.module('../../utils/model/providers.js', () => ({ + ...providers, + getAPIProvider: scenario === 'unsupported' + ? {} + : providers.getAPIProvider.bind(null), + })) + } else if (scenario === 'import-failure') { + mock.module('../../utils/model/providers.js', () => { + throw new Error('module inspection sentinel') + }) + } else if (scenario === 'child-marker') { + process.env[REAL_PROVIDER_TEST_CHILD_ENV] = '1' + mock.module('../../utils/model/providers.js', () => { + throw new Error('child detector imported providers') + }) + } + + test(`attribution isolation detector: ${scenario}`, async () => { + expect(await attributionModulesNeedIsolation()).toBe( + scenario !== 'clean' && scenario !== 'child-marker', + ) + }) +} diff --git a/src/test/providerModuleIsolation.test.ts b/src/test/providerModuleIsolation.test.ts new file mode 100644 index 0000000000..575877879d --- /dev/null +++ b/src/test/providerModuleIsolation.test.ts @@ -0,0 +1,107 @@ +import { expect, test } from 'bun:test' +import { mkdtempSync, rmSync, writeFileSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' +import { + REAL_PROVIDER_TEST_CHILD_ENV, + REAL_PROVIDER_TEST_TIMEOUT_MS, +} from './providerModuleIsolation.js' + +async function withFixture( + scenario: string, + run: (entry: string, env: NodeJS.ProcessEnv) => Promise, +): Promise { + const root = mkdtempSync(join(tmpdir(), 'provider-isolation-')) + const entry = join(root, 'isolation.test.ts') + const fixture = join(import.meta.dir, 'fixtures/providerModuleIsolation.fixture.ts') + writeFileSync(entry, `await import(${JSON.stringify(fixture)})\n`) + try { + await run(entry, { + ...process.env, + [REAL_PROVIDER_TEST_CHILD_ENV]: undefined, + ISOLATION_FIXTURE_SCENARIO: scenario, + CLAUDE_CONFIG_DIR: root, + OPENCLAUDE_CONFIG_DIR: root, + }) + } finally { + rmSync(root, { recursive: true, force: true }) + } +} + +// The fixture may itself run a child with a 30-second deadline. Give the outer +// process and test additional time for diagnostics and cleanup. +const fixtureTimeout = REAL_PROVIDER_TEST_TIMEOUT_MS + 10_000 +const testTimeout = fixtureTimeout + 5_000 + +for (const scenario of [ + 'clean', + 'auth', + 'betas', + 'routing', + 'metadata', + 'client', + 'missing', + 'unsupported', + 'native-function', + 'import-failure', + 'child-marker', + 'suite-sideQuery', + 'suite-authRouting', +]) { + test(`isolates attribution modules: ${scenario}`, async () => { + await withFixture(scenario, async (entry, env) => { + const child = Bun.spawn({ + cmd: [process.execPath, 'test', entry], + cwd: process.cwd(), + env, + stdout: 'pipe', + stderr: 'pipe', + timeout: fixtureTimeout, + killSignal: process.platform === 'win32' ? undefined : 'SIGKILL', + }) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]) + const output = `${stdout}\n${stderr}` + expect(output).not.toContain('parent setup sentinel') + expect(output).not.toContain('parent cleanup sentinel') + expect(output).toMatch(/^\s*1 pass$/m) + expect(output).toMatch(/^\s*0 fail$/m) + expect(output).not.toMatch(/\d+ skip/) + expect(exitCode).toBe(0) + }) + }, { timeout: testTimeout }) +} + +test('propagates child assertion failures through the launcher', async () => { + await withFixture('assertion-failure', async (entry, env) => { + // Run the real launcher in a disposable process so coordinator env and mocks + // remain untouched even when other suites share this process. + const runner = join(entry, '..', 'launcher.ts') + writeFileSync(runner, ` + import { runTestFileWithRealProviders } from ${JSON.stringify( + join(import.meta.dir, 'providerModuleIsolation.ts'), + )} + await runTestFileWithRealProviders(${JSON.stringify(entry)}) + `) + const child = Bun.spawn({ + cmd: [process.execPath, runner], + env, + stdout: 'pipe', + stderr: 'pipe', + timeout: fixtureTimeout, + killSignal: process.platform === 'win32' ? undefined : 'SIGKILL', + }) + const [exitCode, stdout, stderr] = await Promise.all([ + child.exited, + new Response(child.stdout).text(), + new Response(child.stderr).text(), + ]) + expect(exitCode).not.toBe(0) + expect(`${stdout}\n${stderr}`).toContain('provider-isolated test process failed') + expect(`${stdout}\n${stderr}`).toContain('child assertion failure sentinel') + expect(`${stdout}\n${stderr}`).toContain('expected child value') + }) +}, { timeout: testTimeout }) diff --git a/src/test/providerModuleIsolation.ts b/src/test/providerModuleIsolation.ts index e7a329a183..938812350e 100644 --- a/src/test/providerModuleIsolation.ts +++ b/src/test/providerModuleIsolation.ts @@ -32,6 +32,62 @@ export function providerModuleIsMocked( ) } +function exportSurfacesDiffer(loaded: object, real: object): boolean { + const loadedKeys = Reflect.ownKeys(loaded) + const realKeys = Reflect.ownKeys(real) + if ( + loadedKeys.length !== realKeys.length || + realKeys.some(key => !Object.hasOwn(loaded, key)) + ) return true + + return realKeys.some(key => { + const actual: unknown = Reflect.get(loaded, key) + const expected: unknown = Reflect.get(real, key) + if (typeof actual !== typeof expected) return true + if (typeof actual === 'function' && typeof expected === 'function') { + const actualSource = Function.prototype.toString.call(actual) + const expectedSource = Function.prototype.toString.call(expected) + // Native/bound functions cannot be inspected reliably. + return actualSource.includes('[native code]') || + expectedSource.includes('[native code]') || actualSource !== expectedSource + } + // These modules currently export functions and primitives. Unknown mutable + // objects (including symbols) cannot establish a clean comparison. + if ((typeof actual === 'object' && actual !== null) || + typeof actual === 'symbol') return true + return !Object.is(actual, expected) + }) +} + +export async function attributionModulesNeedIsolation(): Promise { + // Check before any imports: a clean child must never recurse. + if (process.env[REAL_PROVIDER_TEST_CHILD_ENV] === '1') return false + + // This detects replaced exports, not arbitrary stale dependency caches. + // Auth is checked independently even when routing has unchanged source. + const modulePaths = [ + '../utils/model/providers.js', + '../utils/auth.js', + '../utils/betas.js', + '../services/api/authRouting.js', + '../integrations/routeMetadata.js', + '../services/api/client.js', + ] + try { + for (const path of modulePaths) { + const loaded = await import(path) + const real = await import( + `${path}?attributionReal=${Date.now()}-${Math.random()}` + ) + if (exportSurfacesDiffer(loaded, real)) return true + } + return false + } catch { + // Missing exports, throwing getters, and failed imports require isolation. + return true + } +} + export async function runTestFileWithRealProviders( testFile: string, ): Promise { diff --git a/src/utils/sideQuery.attribution.test.ts b/src/utils/sideQuery.attribution.test.ts index b9a0ea98ba..17ca9b845b 100644 --- a/src/utils/sideQuery.attribution.test.ts +++ b/src/utils/sideQuery.attribution.test.ts @@ -1,270 +1,281 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' -import { mkdtempSync, rmSync, writeFileSync } from 'fs' -import { tmpdir } from 'os' -import { join } from 'path' import { - setAllowedSettingSources, - setFlagSettingsInline, - setFlagSettingsPath, -} from '../bootstrap/state.js' -import { resetGrowthBook } from '../services/analytics/growthbook.js' -import { acquireSharedMutationLock, releaseSharedMutationLock } from '../test/sharedMutationLock.js' -import { getClaudeAIOAuthTokens } from './auth.js' -import { enableConfigs } from './config.js' -import { SETTING_SOURCES } from './settings/constants.js' -import { resetSettingsCache } from './settings/settingsCache.js' -import { - providerModuleIsMocked, - REAL_PROVIDER_TEST_CHILD_ENV, + attributionModulesNeedIsolation, REAL_PROVIDER_TEST_TIMEOUT_MS, runTestFileWithRealProviders, } from '../test/providerModuleIsolation.js' -// Bun keeps mock.module() registrations process-global across test files. -// Bind sideQuery only when the canonical provider module is real. When a prior -// file replaced it, run this file in a clean child process instead. -const _realProvidersModule = await import( - `./model/providers.js?attributionReal=${Date.now()}-${Math.random()}` -) -const _loadedProvidersModule = await import('src/utils/model/providers.js') -const runInProviderIsolatedChild = - process.env[REAL_PROVIDER_TEST_CHILD_ENV] !== '1' && - providerModuleIsMocked(_loadedProvidersModule, _realProvidersModule) -type SideQueryModule = typeof import('./sideQuery.js') -let sideQuery!: SideQueryModule['sideQuery'] -if (!runInProviderIsolatedChild) { - ;({ sideQuery } = await import( +// Inspect canonical dependencies before binding the subject or suite setup. +const needsChild = await attributionModulesNeedIsolation() +if (needsChild) { + test('runs side-query attribution cases with the real provider module', async () => { + await runTestFileWithRealProviders(import.meta.path) + }, { timeout: REAL_PROVIDER_TEST_TIMEOUT_MS + 5_000 }) +} else { + const { mkdtempSync, rmSync, writeFileSync } = await import('fs') + const { tmpdir } = await import('os') + const { join } = await import('path') + const { setAllowedSettingSources, setFlagSettingsInline, setFlagSettingsPath } = + await import('../bootstrap/state.js') + const { resetGrowthBook } = await import('../services/analytics/growthbook.js') + const { acquireSharedMutationLock, releaseSharedMutationLock } = + await import('../test/sharedMutationLock.js') + const { getClaudeAIOAuthTokens } = await import('./auth.js') + const { enableConfigs } = await import('./config.js') + const { SETTING_SOURCES } = await import('./settings/constants.js') + const { resetSettingsCache } = await import('./settings/settingsCache.js') + const { sideQuery }: typeof import('./sideQuery.js') = await import( `./sideQuery.js?attributionReal=${Date.now()}-${Math.random()}` - )) -} + ) -const envKeys = [ - 'ANTHROPIC_API_KEY', - 'ANTHROPIC_AUTH_TOKEN', - 'ANTHROPIC_BASE_URL', - 'ANTHROPIC_MODEL', - 'ANTHROPIC_UNIX_SOCKET', - 'CLAUDE_CODE_ATTRIBUTION_HEADER', - 'CLAUDE_CODE_ENTRYPOINT', - 'CLAUDE_CODE_OAUTH_TOKEN', - 'CLAUDE_CODE_REMOTE', - 'CLAUDE_CODE_USE_BEDROCK', - 'CLAUDE_CODE_USE_FOUNDRY', - 'CLAUDE_CODE_USE_GEMINI', - 'CLAUDE_CODE_USE_GITHUB', - 'CLAUDE_CODE_USE_MISTRAL', - 'CLAUDE_CODE_USE_OPENAI', - 'CLAUDE_CODE_USE_VERTEX', - 'CLAUDE_FEATURE_FLAGS_FILE', - 'OPENAI_API_BASE', - 'OPENAI_API_KEY', - 'OPENAI_BASE_URL', - 'OPENAI_MODEL', - 'OPENCLAUDE_CONFIG_DIR', -] as const -const originalEnv = { ...process.env } -const originalFetch = globalThis.fetch -const hadSavedMacro = Object.hasOwn(globalThis, 'MACRO') -const savedMacro = (globalThis as Record).MACRO -const originalNodeEnv = process.env.NODE_ENV -let configRoot: string | undefined + const envKeys = [ + 'ANTHROPIC_API_KEY', + 'ANTHROPIC_AUTH_TOKEN', + 'ANTHROPIC_BASE_URL', + 'ANTHROPIC_MODEL', + 'ANTHROPIC_UNIX_SOCKET', + 'CLAUDE_CODE_ATTRIBUTION_HEADER', + 'CLAUDE_CODE_ENTRYPOINT', + 'CLAUDE_CODE_OAUTH_TOKEN', + 'CLAUDE_CODE_REMOTE', + 'CLAUDE_CODE_USE_BEDROCK', + 'CLAUDE_CODE_USE_FOUNDRY', + 'CLAUDE_CODE_USE_GEMINI', + 'CLAUDE_CODE_USE_GITHUB', + 'CLAUDE_CODE_USE_MISTRAL', + 'CLAUDE_CODE_USE_OPENAI', + 'CLAUDE_CODE_USE_VERTEX', + 'CLAUDE_FEATURE_FLAGS_FILE', + 'OPENAI_API_BASE', + 'OPENAI_API_KEY', + 'OPENAI_BASE_URL', + 'OPENAI_MODEL', + 'OPENCLAUDE_CONFIG_DIR', + ] as const + const originalEnv = { ...process.env } + const originalFetch = globalThis.fetch + const hadSavedMacro = Object.hasOwn(globalThis, 'MACRO') + const savedMacro = (globalThis as Record).MACRO + const originalNodeEnv = process.env.NODE_ENV + let configRoot: string | undefined -function makeMessageResponse(): Response { - return new Response( - JSON.stringify({ - id: 'msg-side-attribution-test', - type: 'message', - role: 'assistant', - model: 'claude-side-attribution-test', - content: [], - container: null, - context_management: null, - stop_details: null, - stop_reason: 'end_turn', - stop_sequence: null, - usage: { - input_tokens: 1, - output_tokens: 1, - cache_creation_input_tokens: 0, - cache_read_input_tokens: 0, - }, - }), - { - headers: { - 'content-type': 'application/json', - 'request-id': 'req-side-attribution-test', + function makeMessageResponse(): Response { + return new Response( + JSON.stringify({ + id: 'msg-side-attribution-test', + type: 'message', + role: 'assistant', + model: 'claude-side-attribution-test', + content: [], + container: null, + context_management: null, + stop_details: null, + stop_reason: 'end_turn', + stop_sequence: null, + usage: { + input_tokens: 1, + output_tokens: 1, + cache_creation_input_tokens: 0, + cache_read_input_tokens: 0, + }, + }), + { + headers: { + 'content-type': 'application/json', + 'request-id': 'req-side-attribution-test', + }, }, - }, - ) -} + ) + } -async function captureSideQueryRequest( - system: - | string - | Array<{ - type: 'text' - text: string - cache_control?: { type: 'ephemeral'; scope?: 'global' | 'org' } - }> = 'stable side prompt', -): Promise<{ - system: Array> - headers: Headers -}> { - let requestBody: Record | undefined - let requestHeaders: Headers | undefined - globalThis.fetch = (async (input, init) => { - const request = - input instanceof Request - ? input.clone() - : new Request(input as RequestInfo, init) - const body = await request.text() - if (body) requestBody = JSON.parse(body) as Record - requestHeaders = new Headers(request.headers) - return makeMessageResponse() - }) as typeof fetch + async function captureSideQueryRequest( + system: + | string + | Array<{ + type: 'text' + text: string + cache_control?: { type: 'ephemeral'; scope?: 'global' | 'org' } + }> = 'stable side prompt', + ): Promise<{ + system: Array> + headers: Headers + }> { + let requestBody: Record | undefined + let requestHeaders: Headers | undefined + globalThis.fetch = (async (input, init) => { + const request = + input instanceof Request + ? input.clone() + : new Request(input as RequestInfo, init) + const body = await request.text() + if (body) requestBody = JSON.parse(body) as Record + requestHeaders = new Headers(request.headers) + return makeMessageResponse() + }) as typeof fetch - await sideQuery({ - querySource: 'model_validation', - model: 'claude-side-attribution-test', - system, - messages: [{ role: 'user', content: 'hello' }], - }) + await sideQuery({ + querySource: 'model_validation', + model: 'claude-side-attribution-test', + system, + messages: [{ role: 'user', content: 'hello' }], + }) - if (!Array.isArray(requestBody?.system)) { - throw new Error('expected captured side-query system blocks') - } - if (!requestHeaders) { - throw new Error('expected captured side-query request headers') - } - return { - system: requestBody.system as Array>, - headers: requestHeaders, + if (!Array.isArray(requestBody?.system)) { + throw new Error('expected captured side-query system blocks') + } + if (!requestHeaders) { + throw new Error('expected captured side-query request headers') + } + return { + system: requestBody.system as Array>, + headers: requestHeaders, + } } -} -async function captureSideQuerySystem( - system: - | string - | Array<{ - type: 'text' - text: string - cache_control?: { type: 'ephemeral'; scope?: 'global' | 'org' } - }> = 'stable side prompt', -): Promise>> { - return (await captureSideQueryRequest(system)).system -} - -function blockTexts(blocks: Array>): string[] { - return blocks.flatMap(block => - typeof block.text === 'string' ? [block.text] : [], - ) -} - -beforeEach(async () => { - await acquireSharedMutationLock('sideQuery.attribution.test.ts') - setFlagSettingsPath(undefined) - setFlagSettingsInline(null) - setAllowedSettingSources([...SETTING_SOURCES]) - resetSettingsCache() - configRoot = mkdtempSync(join(tmpdir(), 'side-query-attribution-')) - for (const key of envKeys) delete process.env[key] - process.env.ANTHROPIC_API_KEY = 'sk-test-side-query' - process.env.OPENCLAUDE_CONFIG_DIR = configRoot - process.env.CLAUDE_FEATURE_FLAGS_FILE = join( - configRoot, - 'feature-flags.json', - ) - ;(globalThis as Record).MACRO = { - VERSION: '0.0.0-test', - DISPLAY_VERSION: '0.0.0-test', - BUILD_TIME: 'test', - ISSUES_EXPLAINER: 'test', - PACKAGE_URL: 'test', - NATIVE_PACKAGE_URL: undefined, + async function captureSideQuerySystem( + system: + | string + | Array<{ + type: 'text' + text: string + cache_control?: { type: 'ephemeral'; scope?: 'global' | 'org' } + }> = 'stable side prompt', + ): Promise>> { + return (await captureSideQueryRequest(system)).system } - getClaudeAIOAuthTokens.cache?.clear?.() - resetGrowthBook() -}) -function setIgnoredApiKeyHelper(): void { - delete process.env.ANTHROPIC_API_KEY - setFlagSettingsPath(undefined) - setFlagSettingsInline({ apiKeyHelper: 'ignored-test-helper' }) - setAllowedSettingSources(['flagSettings']) - resetSettingsCache() - enableConfigs() - process.env.NODE_ENV = 'development' -} + function blockTexts(blocks: Array>): string[] { + return blocks.flatMap(block => + typeof block.text === 'string' ? [block.text] : [], + ) + } -afterEach(() => { - try { - for (const key of envKeys) { - if (originalEnv[key] === undefined) { - delete process.env[key] - } else { - process.env[key] = originalEnv[key] - } - } - globalThis.fetch = originalFetch - if (originalNodeEnv === undefined) { - delete process.env.NODE_ENV - } else { - process.env.NODE_ENV = originalNodeEnv - } + beforeEach(async () => { + await acquireSharedMutationLock('sideQuery.attribution.test.ts') setFlagSettingsPath(undefined) setFlagSettingsInline(null) setAllowedSettingSources([...SETTING_SOURCES]) resetSettingsCache() - if (hadSavedMacro) { - ;(globalThis as Record).MACRO = savedMacro - } else { - delete (globalThis as Record).MACRO + configRoot = mkdtempSync(join(tmpdir(), 'side-query-attribution-')) + for (const key of envKeys) delete process.env[key] + process.env.ANTHROPIC_API_KEY = 'sk-test-side-query' + process.env.OPENCLAUDE_CONFIG_DIR = configRoot + process.env.CLAUDE_FEATURE_FLAGS_FILE = join( + configRoot, + 'feature-flags.json', + ) + ;(globalThis as Record).MACRO = { + VERSION: '0.0.0-test', + DISPLAY_VERSION: '0.0.0-test', + BUILD_TIME: 'test', + ISSUES_EXPLAINER: 'test', + PACKAGE_URL: 'test', + NATIVE_PACKAGE_URL: undefined, } getClaudeAIOAuthTokens.cache?.clear?.() resetGrowthBook() - if (configRoot) { - rmSync(configRoot, { recursive: true, force: true }) - configRoot = undefined - } - } finally { - releaseSharedMutationLock() + }) + + function setIgnoredApiKeyHelper(): void { + delete process.env.ANTHROPIC_API_KEY + setFlagSettingsPath(undefined) + setFlagSettingsInline({ apiKeyHelper: 'ignored-test-helper' }) + setAllowedSettingSources(['flagSettings']) + resetSettingsCache() + enableConfigs() + process.env.NODE_ENV = 'development' } -}) -if (runInProviderIsolatedChild) { - test('runs side-query attribution cases with the real provider module', async () => { - await runTestFileWithRealProviders(import.meta.path) - }, { timeout: REAL_PROVIDER_TEST_TIMEOUT_MS + 5_000 }) -} + afterEach(() => { + try { + for (const key of envKeys) { + if (originalEnv[key] === undefined) { + delete process.env[key] + } else { + process.env[key] = originalEnv[key] + } + } + globalThis.fetch = originalFetch + if (originalNodeEnv === undefined) { + delete process.env.NODE_ENV + } else { + process.env.NODE_ENV = originalNodeEnv + } + setFlagSettingsPath(undefined) + setFlagSettingsInline(null) + setAllowedSettingSources([...SETTING_SOURCES]) + resetSettingsCache() + if (hadSavedMacro) { + ;(globalThis as Record).MACRO = savedMacro + } else { + delete (globalThis as Record).MACRO + } + getClaudeAIOAuthTokens.cache?.clear?.() + resetGrowthBook() + if (configRoot) { + rmSync(configRoot, { recursive: true, force: true }) + configRoot = undefined + } + } finally { + releaseSharedMutationLock() + } + }) -const describeAttribution = runInProviderIsolatedChild - ? describe.skip - : describe + describe('sideQuery Anthropic attribution', () => { + for (const [label, envKey, envValue, ambientAuth] of [ + ['remote', 'CLAUDE_CODE_REMOTE', '1', 'api-key'], + [ + 'Claude Desktop', + 'CLAUDE_CODE_ENTRYPOINT', + 'claude-desktop', + 'api-key-helper', + ], + [ + 'Unix-socket OAuth proxy', + 'ANTHROPIC_UNIX_SOCKET', + '/tmp/openclaude-auth-test.sock', + 'auth-token', + ], + ] as const) { + test(`uses OAuth headers and attribution in managed ${label} sessions`, async () => { + process.env[envKey] = envValue + process.env.CLAUDE_CODE_OAUTH_TOKEN = 'oauth-test-token' + process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0' + if (ambientAuth === 'api-key-helper') setIgnoredApiKeyHelper() + if (ambientAuth === 'auth-token') { + process.env.ANTHROPIC_AUTH_TOKEN = 'ignored-test-auth-token' + } + getClaudeAIOAuthTokens.cache?.clear?.() -describeAttribution('sideQuery Anthropic attribution', () => { - for (const [label, envKey, envValue, ambientAuth] of [ - ['remote', 'CLAUDE_CODE_REMOTE', '1', 'api-key'], - [ - 'Claude Desktop', - 'CLAUDE_CODE_ENTRYPOINT', - 'claude-desktop', - 'api-key-helper', - ], - [ - 'Unix-socket OAuth proxy', - 'ANTHROPIC_UNIX_SOCKET', - '/tmp/openclaude-auth-test.sock', - 'auth-token', - ], - ] as const) { - test(`uses OAuth headers and attribution in managed ${label} sessions`, async () => { - process.env[envKey] = envValue - process.env.CLAUDE_CODE_OAUTH_TOKEN = 'oauth-test-token' + const request = await captureSideQueryRequest() + const texts = blockTexts(request.system) + + expect( + texts.some(text => text.startsWith('x-anthropic-billing-header')), + ).toBe(true) + expect(request.headers.get('authorization')).toBe( + 'Bearer oauth-test-token', + ) + expect(request.headers.has('x-api-key')).toBe(false) + }) + } + + test('keeps Unix-socket API-key auth when the OAuth placeholder is absent', async () => { + writeFileSync( + join(configRoot!, '.credentials.json'), + JSON.stringify({ + claudeAiOauth: { + accessToken: 'stored-test-oauth-token', + refreshToken: null, + expiresAt: null, + scopes: ['user:inference'], + subscriptionType: null, + rateLimitTier: null, + }, + }), + ) + process.env.ANTHROPIC_UNIX_SOCKET = '/tmp/openclaude-auth-test.sock' process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0' - if (ambientAuth === 'api-key-helper') setIgnoredApiKeyHelper() - if (ambientAuth === 'auth-token') { - process.env.ANTHROPIC_AUTH_TOKEN = 'ignored-test-auth-token' - } getClaudeAIOAuthTokens.cache?.clear?.() const request = await captureSideQueryRequest() @@ -272,101 +283,71 @@ describeAttribution('sideQuery Anthropic attribution', () => { expect( texts.some(text => text.startsWith('x-anthropic-billing-header')), - ).toBe(true) - expect(request.headers.get('authorization')).toBe( - 'Bearer oauth-test-token', - ) - expect(request.headers.has('x-api-key')).toBe(false) + ).toBe(false) + expect(request.headers.get('x-api-key')).toBe('sk-test-side-query') + expect(request.headers.has('authorization')).toBe(false) }) - } - test('keeps Unix-socket API-key auth when the OAuth placeholder is absent', async () => { - writeFileSync( - join(configRoot!, '.credentials.json'), - JSON.stringify({ - claudeAiOauth: { - accessToken: 'stored-test-oauth-token', - refreshToken: null, - expiresAt: null, - scopes: ['user:inference'], - subscriptionType: null, - rateLimitTier: null, - }, - }), - ) - process.env.ANTHROPIC_UNIX_SOCKET = '/tmp/openclaude-auth-test.sock' - process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0' - getClaudeAIOAuthTokens.cache?.clear?.() + test('strips the block from a custom native endpoint', async () => { + process.env.ANTHROPIC_BASE_URL = 'https://custom-anthropic.example/v1' - const request = await captureSideQueryRequest() - const texts = blockTexts(request.system) - - expect( - texts.some(text => text.startsWith('x-anthropic-billing-header')), - ).toBe(false) - expect(request.headers.get('x-api-key')).toBe('sk-test-side-query') - expect(request.headers.has('authorization')).toBe(false) - }) - - test('strips the block from a custom native endpoint', async () => { - process.env.ANTHROPIC_BASE_URL = 'https://custom-anthropic.example/v1' - - const texts = blockTexts( - await captureSideQuerySystem([ - { type: 'text', text: 'x-anthropic-billing-header: stale' }, - { type: 'text', text: 'stable side prompt' }, - ]), - ) + const texts = blockTexts( + await captureSideQuerySystem([ + { type: 'text', text: 'x-anthropic-billing-header: stale' }, + { type: 'text', text: 'stable side prompt' }, + ]), + ) - expect( - texts.some(text => text.startsWith('x-anthropic-billing-header')), - ).toBe(false) - expect(texts).toContain('stable side prompt') - }) + expect( + texts.some(text => text.startsWith('x-anthropic-billing-header')), + ).toBe(false) + expect(texts).toContain('stable side prompt') + }) - test('honors the disabled setting for an official API key', async () => { - process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0' + test('honors the disabled setting for an official API key', async () => { + process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0' - const texts = blockTexts(await captureSideQuerySystem()) + const texts = blockTexts(await captureSideQuerySystem()) - expect( - texts.some(text => text.startsWith('x-anthropic-billing-header')), - ).toBe(false) - expect(texts).toContain('stable side prompt') - }) + expect( + texts.some(text => text.startsWith('x-anthropic-billing-header')), + ).toBe(false) + expect(texts).toContain('stable side prompt') + }) - test('keeps the block for official OAuth when globally disabled', async () => { - delete process.env.ANTHROPIC_API_KEY - process.env.CLAUDE_CODE_OAUTH_TOKEN = 'oauth-test-token' - process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0' - getClaudeAIOAuthTokens.cache?.clear?.() + test('keeps the block for official OAuth when globally disabled', async () => { + delete process.env.ANTHROPIC_API_KEY + process.env.CLAUDE_CODE_OAUTH_TOKEN = 'oauth-test-token' + process.env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0' + getClaudeAIOAuthTokens.cache?.clear?.() - const texts = blockTexts(await captureSideQuerySystem()) + const texts = blockTexts(await captureSideQuerySystem()) - expect( - texts.some(text => text.startsWith('x-anthropic-billing-header')), - ).toBe(true) - expect(texts).toContain('stable side prompt') - }) + expect( + texts.some(text => text.startsWith('x-anthropic-billing-header')), + ).toBe(true) + expect(texts).toContain('stable side prompt') + }) - test('keeps one generated block and preserves cache metadata order', async () => { - const blocks = await captureSideQuerySystem([ - { type: 'text', text: 'x-anthropic-billing-header: stale' }, - { - type: 'text', - text: 'stable cached side prompt', - cache_control: { type: 'ephemeral', scope: 'org' }, - }, - ]) + test('keeps one generated block and preserves cache metadata order', async () => { + const blocks = await captureSideQuerySystem([ + { type: 'text', text: 'x-anthropic-billing-header: stale' }, + { + type: 'text', + text: 'stable cached side prompt', + cache_control: { type: 'ephemeral', scope: 'org' }, + }, + ]) - const texts = blockTexts(blocks) - expect( - texts.filter(text => text.startsWith('x-anthropic-billing-header')), - ).toHaveLength(1) - expect(texts[1]).toBe('stable cached side prompt') - expect(blocks[1]?.cache_control).toEqual({ - type: 'ephemeral', - scope: 'org', + const texts = blockTexts(blocks) + expect( + texts.filter(text => text.startsWith('x-anthropic-billing-header')), + ).toHaveLength(1) + expect(texts[1]).toBe('stable cached side prompt') + expect(blocks[1]?.cache_control).toEqual({ + type: 'ephemeral', + scope: 'org', + }) }) }) -}) +} From c823efe56ae380e5c479855da22300d829451d48 Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 07:30:08 -0700 Subject: [PATCH 028/121] test: add snapshotEnv helper and restore leaked env and auth state (TA-001 slice 4) (#2335) --- CONTRIBUTING.md | 1 + src/hooks/useApiKeyVerification.test.tsx | 12 ++- .../openaiShim.ollamaTextToolCalls.test.ts | 33 ++++--- .../api/openaiShim/xmlToolCallParsing.test.ts | 6 +- src/test/envSnapshot.test.ts | 97 +++++++++++++++++++ src/test/envSnapshot.ts | 24 +++++ src/utils/fastMode.test.ts | 12 ++- src/utils/providerFlag.test.ts | 68 +------------ 8 files changed, 169 insertions(+), 84 deletions(-) create mode 100644 src/test/envSnapshot.test.ts create mode 100644 src/test/envSnapshot.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a01ab2c091..a199e60540 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -160,6 +160,7 @@ Dependency changes need a clear project benefit — fixing a bug, addressing a s - Preserve existing repo patterns unless the change is intentionally refactoring them. - Add or update tests when the change affects behavior. - For vendor/integration catalog tests, cover each vendor with one table-driven test of its registry contract (route resolution, default base URL and model, credential env vars, catalog-to-descriptor linkage, and capability/limit values the runtime uses), plus a small number of behavioral tests such as model-discovery keep/drop parsing or availability windows. Do not copy full curated model lists or cache/refresh settings from vendor definitions into tests. +- When tests set or delete environment variables, import `snapshotEnv` from `src/test/envSnapshot.ts` using the appropriate relative `.js` path. In `beforeEach`, save the named variables before changing them with `restoreEnv = snapshotEnv(keys)`; pass `{ clear: true }` when setup needs those variables unset. Call `restoreEnv()` in `afterEach`, or use `finally` for changes scoped to one test. Restoration preserves previous values, including empty strings, and deletes variables that were previously unset. List only the variables the test changes or deliberately clears; do not replace `process.env` or blanket-clear unrelated variables. When using the shared mutation lock, snapshot after acquiring it and restore before releasing it. - In timing-sensitive tests, drive timers with Bun's fake timers (`jest.useFakeTimers()` and `jest.advanceTimersByTime()`) instead of racing real delays, and restore real timers during cleanup, so the tests pass in a single-process whole-suite run as well as in the sharded runner. - Await completion promises or explicit readiness signals before asserting asynchronous results. For tests that deliberately suppress readiness, use a short, bounded wait and assert the expected failure. - For tests that launch real subprocesses, set an explicit per-test timeout longer than the child-process deadline, leaving time for assertions and cleanup. The launcher heap tests use an 8-second child deadline and a 10-second test timeout. Document the reason for the timeout beside the test. diff --git a/src/hooks/useApiKeyVerification.test.tsx b/src/hooks/useApiKeyVerification.test.tsx index 334e640728..3b41fe5761 100644 --- a/src/hooks/useApiKeyVerification.test.tsx +++ b/src/hooks/useApiKeyVerification.test.tsx @@ -1,6 +1,6 @@ import { PassThrough } from 'node:stream' -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import React from 'react' import { createRoot, Text } from '../ink.js' import * as realState from '../bootstrap/state.js' @@ -9,7 +9,11 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as realAuth from '../utils/auth.js' + +const nonce = `${Date.now()}-${Math.random()}` +const realAuth = { + ...await import(`../utils/auth.js?real=${nonce}`), +} type AuthState = { anthropicAuthEnabled: boolean @@ -75,6 +79,10 @@ afterEach(() => { } }) +afterAll(() => { + mock.module('../utils/auth.js', () => realAuth) +}) + test('useApiKeyVerification resets stale missing status when the session switches to a third-party provider', async () => { const authState: AuthState = { anthropicAuthEnabled: true, diff --git a/src/services/api/openaiShim.ollamaTextToolCalls.test.ts b/src/services/api/openaiShim.ollamaTextToolCalls.test.ts index 53c3f4f42c..4d8faec0be 100644 --- a/src/services/api/openaiShim.ollamaTextToolCalls.test.ts +++ b/src/services/api/openaiShim.ollamaTextToolCalls.test.ts @@ -10,9 +10,12 @@ * 5. P1 context guard — bare JSON in explanatory prose is skipped */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { snapshotEnv } from '../../test/envSnapshot.js' import { createOpenAIShimClient } from './openaiShim.js' import { parseTextToolCalls } from './openaiShim/rawToolCallParsing.js' +const ENV_KEYS = ['OLLAMA_BASE_URL', 'OPENAI_API_KEY', 'OPENAI_BASE_URL'] as const + type FetchType = typeof globalThis.fetch type OpenAIShimClient = { @@ -224,17 +227,17 @@ const ollamaToolChunk = (toolCalls: unknown[], finishReason?: string) => ({ describe('Ollama streaming — think-tag filtering on text-tool fallback (P1)', () => { let originalFetch: FetchType + let restoreEnv: () => void beforeEach(() => { originalFetch = globalThis.fetch + restoreEnv = snapshotEnv(ENV_KEYS) process.env.OLLAMA_BASE_URL = 'http://localhost:11434' process.env.OPENAI_API_KEY = 'test-key' process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' }) afterEach(() => { globalThis.fetch = originalFetch - delete process.env.OLLAMA_BASE_URL - delete process.env.OPENAI_API_KEY - delete process.env.OPENAI_BASE_URL + restoreEnv() }) test(' content is NOT emitted as assistant text when text-tool fallback fires', async () => { @@ -278,17 +281,17 @@ describe('Ollama streaming — think-tag filtering on text-tool fallback (P1)', describe('Ollama streaming — plain text response with no tool calls', () => { let originalFetch: FetchType + let restoreEnv: () => void beforeEach(() => { originalFetch = globalThis.fetch + restoreEnv = snapshotEnv(ENV_KEYS) process.env.OLLAMA_BASE_URL = 'http://localhost:11434' process.env.OPENAI_API_KEY = 'test-key' process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' }) afterEach(() => { globalThis.fetch = originalFetch - delete process.env.OLLAMA_BASE_URL - delete process.env.OPENAI_API_KEY - delete process.env.OPENAI_BASE_URL + restoreEnv() }) test('plain text in two chunks (content then stop) is emitted as text_delta', async () => { @@ -385,17 +388,17 @@ describe('Ollama streaming — plain text response with no tool calls', () => { describe('Ollama streaming — visible text before real structured tool_calls (P2)', () => { let originalFetch: FetchType + let restoreEnv: () => void beforeEach(() => { originalFetch = globalThis.fetch + restoreEnv = snapshotEnv(ENV_KEYS) process.env.OLLAMA_BASE_URL = 'http://localhost:11434' process.env.OPENAI_API_KEY = 'test-key' process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' }) afterEach(() => { globalThis.fetch = originalFetch - delete process.env.OLLAMA_BASE_URL - delete process.env.OPENAI_API_KEY - delete process.env.OPENAI_BASE_URL + restoreEnv() }) test('visible assistant text is preserved when real delta.tool_calls follow it', async () => { @@ -441,17 +444,17 @@ describe('Ollama streaming — visible text before real structured tool_calls (P describe('Ollama streaming — visible prose before text-based tool-call fallback (P2 buffered path)', () => { let originalFetch: FetchType + let restoreEnv: () => void beforeEach(() => { originalFetch = globalThis.fetch + restoreEnv = snapshotEnv(ENV_KEYS) process.env.OLLAMA_BASE_URL = 'http://localhost:11434' process.env.OPENAI_API_KEY = 'test-key' process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' }) afterEach(() => { globalThis.fetch = originalFetch - delete process.env.OLLAMA_BASE_URL - delete process.env.OPENAI_API_KEY - delete process.env.OPENAI_BASE_URL + restoreEnv() }) test('visible prose before text-based JSON tool call is preserved in emitted text_delta', async () => { @@ -513,17 +516,17 @@ describe('parseTextToolCalls — pretty-printed bare JSON detection', () => { describe('Ollama streaming — non-stop terminal finish reasons flush buffer', () => { let originalFetch: FetchType + let restoreEnv: () => void beforeEach(() => { originalFetch = globalThis.fetch + restoreEnv = snapshotEnv(ENV_KEYS) process.env.OLLAMA_BASE_URL = 'http://localhost:11434' process.env.OPENAI_API_KEY = 'test-key' process.env.OPENAI_BASE_URL = 'http://localhost:11434/v1' }) afterEach(() => { globalThis.fetch = originalFetch - delete process.env.OLLAMA_BASE_URL - delete process.env.OPENAI_API_KEY - delete process.env.OPENAI_BASE_URL + restoreEnv() }) test('buffered text is flushed when finish_reason is "length"', async () => { diff --git a/src/services/api/openaiShim/xmlToolCallParsing.test.ts b/src/services/api/openaiShim/xmlToolCallParsing.test.ts index 52ae3a7a55..15418f0800 100644 --- a/src/services/api/openaiShim/xmlToolCallParsing.test.ts +++ b/src/services/api/openaiShim/xmlToolCallParsing.test.ts @@ -15,6 +15,7 @@ * D. NAMEVALUE… */ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { snapshotEnv } from '../../../test/envSnapshot.js' import { createOpenAIShimClient } from '../openaiShim.js' import { parseXmlToolCalls as parseXmlToolCallsModule, @@ -283,16 +284,17 @@ describe('parseXmlToolCalls', () => { describe('GLM streaming — XML tool calls', () => { let originalFetch: FetchType + let restoreEnv: () => void beforeEach(() => { originalFetch = globalThis.fetch + restoreEnv = snapshotEnv(['OPENAI_API_KEY', 'OPENAI_BASE_URL']) process.env.OPENAI_API_KEY = 'test-key' // Deliberately NOT an Ollama endpoint, so isOllama=false. process.env.OPENAI_BASE_URL = 'https://gateway.example.com/v1' }) afterEach(() => { globalThis.fetch = originalFetch - delete process.env.OPENAI_API_KEY - delete process.env.OPENAI_BASE_URL + restoreEnv() }) async function run( diff --git a/src/test/envSnapshot.test.ts b/src/test/envSnapshot.test.ts new file mode 100644 index 0000000000..11934e97e4 --- /dev/null +++ b/src/test/envSnapshot.test.ts @@ -0,0 +1,97 @@ +import { describe, expect, test } from 'bun:test' +import { snapshotEnv } from './envSnapshot.js' + +const KEYS = [ + 'TA001_ENV_SNAPSHOT_TEST_POPULATED', + 'TA001_ENV_SNAPSHOT_TEST_EMPTY', + 'TA001_ENV_SNAPSHOT_TEST_UNDEFINED', + 'TA001_ENV_SNAPSHOT_TEST_ABSENT', + 'TA001_ENV_SNAPSHOT_TEST_UNLISTED', +] as const + +function withTestEnv(run: () => void): void { + const incoming = KEYS.map(key => [key, process.env[key]] as const) + try { + run() + } finally { + for (const [key, value] of incoming) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } + } +} + +describe('snapshotEnv', () => { + test('leaves values unchanged by default and restores exact strings', () => { + withTestEnv(() => { + const [populated, empty, literalUndefined] = KEYS + process.env[populated] = 'original' + process.env[empty] = '' + process.env[literalUndefined] = 'undefined' + + const restoreEnv = snapshotEnv([populated, empty, literalUndefined]) + expect(process.env[populated]).toBe('original') + expect(process.env[empty]).toBe('') + expect(process.env[literalUndefined]).toBe('undefined') + + process.env[populated] = 'changed' + process.env[empty] = 'changed' + delete process.env[literalUndefined] + restoreEnv() + + expect(process.env[populated]).toBe('original') + expect(process.env[empty]).toBe('') + expect(process.env[literalUndefined]).toBe('undefined') + }) + }) + + test('clears only named keys and saves their values before clearing', () => { + withTestEnv(() => { + const [populated, empty, , , unlisted] = KEYS + process.env[populated] = 'original' + process.env[empty] = '' + process.env[unlisted] = 'untouched' + + const restoreEnv = snapshotEnv([populated, empty], { clear: true }) + expect(Object.hasOwn(process.env, populated)).toBe(false) + expect(Object.hasOwn(process.env, empty)).toBe(false) + expect(process.env[unlisted]).toBe('untouched') + + restoreEnv() + expect(process.env[populated]).toBe('original') + expect(process.env[empty]).toBe('') + expect(process.env[unlisted]).toBe('untouched') + }) + }) + + test('deletes keys that were previously absent', () => { + withTestEnv(() => { + const absent = KEYS[3] + delete process.env[absent] + const restoreEnv = snapshotEnv([absent]) + process.env[absent] = 'temporary' + + restoreEnv() + expect(process.env[absent]).toBeUndefined() + expect(Object.hasOwn(process.env, absent)).toBe(false) + }) + }) + + test('preserves changes to unlisted variables during restoration', () => { + withTestEnv(() => { + const [populated, , , , unlisted] = KEYS + process.env[populated] = 'original' + process.env[unlisted] = 'original unlisted' + const restoreEnv = snapshotEnv([populated]) + process.env[populated] = 'changed' + process.env[unlisted] = 'changed unlisted' + + restoreEnv() + expect(process.env[populated]).toBe('original') + expect(process.env[unlisted]).toBe('changed unlisted') + }) + }) +}) diff --git a/src/test/envSnapshot.ts b/src/test/envSnapshot.ts new file mode 100644 index 0000000000..aabec450f0 --- /dev/null +++ b/src/test/envSnapshot.ts @@ -0,0 +1,24 @@ +export function snapshotEnv( + keys: readonly string[], + options: { clear?: boolean } = {}, +): () => void { + const saved = new Map( + keys.map(key => [key, process.env[key]] as const), + ) + + if (options.clear) { + for (const key of saved.keys()) { + delete process.env[key] + } + } + + return () => { + for (const [key, value] of saved) { + if (value === undefined) { + delete process.env[key] + } else { + process.env[key] = value + } + } + } +} diff --git a/src/utils/fastMode.test.ts b/src/utils/fastMode.test.ts index 882f9c7f13..0cd7cbc67c 100644 --- a/src/utils/fastMode.test.ts +++ b/src/utils/fastMode.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' import * as realAxios from 'axios' import * as realOauthConstants from 'src/constants/oauth.js' import * as realGrowthbook from 'src/services/analytics/growthbook.js' @@ -6,11 +6,15 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as realAuth from './auth.js' import * as realModel from './model/model.js' type ProvidersModule = typeof import('./model/providers.js') type AxiosModule = typeof import('axios') +const nonce = `${Date.now()}-${Math.random()}` +const realAuth = { + ...await import(`./auth.js?real=${nonce}`), +} + const originalEnv = { ...process.env } let originalProvidersModule: ProvidersModule | undefined let originalAxiosModule: AxiosModule | undefined @@ -243,6 +247,10 @@ afterEach(async () => { } }) +afterAll(() => { + mock.module('./auth.js', () => realAuth) +}) + describe('isFastModeSupportedByModel — Opus model gate (#1769)', () => { test('supports the current default Opus (now 4.8), matching the /fast UI', async () => { forceFirstPartyProviderEnv() diff --git a/src/utils/providerFlag.test.ts b/src/utils/providerFlag.test.ts index 33260b30ae..c841e349e0 100644 --- a/src/utils/providerFlag.test.ts +++ b/src/utils/providerFlag.test.ts @@ -14,8 +14,10 @@ import { VALID_PROVIDERS, } from './providerFlag.js' import { resolveActiveRouteIdFromEnv } from '../integrations/routeMetadata.js' +import { snapshotEnv } from '../test/envSnapshot.js' const ENV_KEYS = [ + 'CLAUDE_CODE_PROVIDER_ROUTE_ID', 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_GEMINI', 'CLAUDE_CODE_USE_GITHUB', @@ -67,81 +69,21 @@ const ENV_KEYS = [ 'USER_TYPE', ] -const originalEnv: Record = {} +let restoreEnv: () => void beforeEach(async () => { await acquireSharedMutationLock('utils/providerFlag.test.ts') - for (const key of ENV_KEYS) { - originalEnv[key] = process.env[key] - delete process.env[key] - } + restoreEnv = snapshotEnv(ENV_KEYS, { clear: true }) }) -const RESET_KEYS = [ - 'CLAUDE_CODE_USE_OPENAI', - 'CLAUDE_CODE_USE_GEMINI', - 'CLAUDE_CODE_USE_GITHUB', - 'CLAUDE_CODE_USE_MISTRAL', - 'CLAUDE_CODE_USE_BEDROCK', - 'CLAUDE_CODE_USE_VERTEX', - 'CLAUDE_CODE_USE_FOUNDRY', - 'OPENAI_BASE_URL', - 'OPENAI_API_BASE', - 'OPENAI_API_KEY', - 'OPENAI_MODEL', - 'OPENAI_API_FORMAT', - 'OPENAI_AZURE_STYLE', - 'OPENAI_AUTH_HEADER', - 'OPENAI_AUTH_SCHEME', - 'OPENAI_AUTH_HEADER_VALUE', - 'GEMINI_MODEL', - 'NVIDIA_API_KEY', - 'NVIDIA_NIM', - 'BNKR_API_KEY', - 'XAI_API_KEY', - 'MINIMAX_API_KEY', - 'VENICE_API_KEY', - 'MIMO_API_KEY', - 'ATLAS_CLOUD_API_KEY', - 'GMI_API_KEY', - 'GMI_MODEL', - 'APISMART_API_KEY', - 'APISMART_MODEL', - 'API_ROUTE_API_KEY', - 'API_ROUTE_MODEL', - 'CONCENTRATE_API_KEY', - 'CONCENTRATE_BASE_URL', - 'CONCENTRATE_MODEL', - 'LONGCAT_API_KEY', - 'OPENGATEWAY_API_KEY', - 'OPENGATEWAY_BASE_URL', - 'CLOUDFLARE_API_TOKEN', - 'MISTRAL_MODEL', - 'ANTHROPIC_MODEL', - 'ANTHROPIC_BASE_URL', - 'ANTHROPIC_API_KEY', - 'ANTHROPIC_AUTH_TOKEN', - 'ANTHROPIC_CUSTOM_HEADERS', - 'USER_TYPE', -] as const - beforeEach(() => { clearRememberedProviderFlagForTests() - for (const key of RESET_KEYS) { - delete process.env[key] - } }) afterEach(() => { try { clearRememberedProviderFlagForTests() - for (const key of ENV_KEYS) { - if (originalEnv[key] === undefined) { - delete process.env[key] - } else { - process.env[key] = originalEnv[key] - } - } + restoreEnv() } finally { releaseSharedMutationLock() } From 5c9b3c92dc844700e41ef612ff0d31e8c9bb99a0 Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 08:59:41 -0700 Subject: [PATCH 029/121] test: stub stdin ref/unref in cli premature-exit regression test (#2337) --- src/entrypoints/cli.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/entrypoints/cli.test.ts b/src/entrypoints/cli.test.ts index 7570173332..d980ec3ec5 100644 --- a/src/entrypoints/cli.test.ts +++ b/src/entrypoints/cli.test.ts @@ -1128,6 +1128,8 @@ describe('Node 24 premature exit regression (issue #1678)', () => { process.stdout.isTTY = true; process.stdin.isTTY = true; process.stdin.setRawMode = () => {}; + process.stdin.ref = () => process.stdin; + process.stdin.unref = () => process.stdin; process.env.OPENCLAUDE_DISABLE_TELEMETRY = '1'; process.env.OPENGATEWAY_API_KEY = 'dummy'; From 4eae36abd6b282166daa7359c914c749e762ed6f Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 09:03:45 -0700 Subject: [PATCH 030/121] test: restore Shell and GrowthBook module mocks after BashTool sandboxAnalysis tests (#2336) --- .../BashTool/BashTool.sandboxAnalysis.test.ts | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/tools/BashTool/BashTool.sandboxAnalysis.test.ts b/src/tools/BashTool/BashTool.sandboxAnalysis.test.ts index 7a9b4fff0f..ec8f2c44e0 100644 --- a/src/tools/BashTool/BashTool.sandboxAnalysis.test.ts +++ b/src/tools/BashTool/BashTool.sandboxAnalysis.test.ts @@ -1,14 +1,28 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' -import * as realGrowthbook from '../../services/analytics/growthbook.js' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import { getEmptyToolPermissionContext } from '../../Tool.js' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' -import * as realShell from '../../utils/Shell.js' import type { ExecResult, ShellCommand } from '../../utils/ShellCommand.js' import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js' +const nonce = `${Date.now()}-${Math.random()}` +const REAL_SHELL = { + ...await import(`../../utils/Shell.js?real=${nonce}`), +} +const REAL_GROWTHBOOK = { + ...await import(`../../services/analytics/growthbook.js?real=${nonce}`), +} + +afterAll(() => { + mock.restore() + mock.module('../../utils/Shell.js', () => REAL_SHELL) + mock.module('src/utils/Shell.js', () => REAL_SHELL) + mock.module('../../services/analytics/growthbook.js', () => REAL_GROWTHBOOK) + mock.module('src/services/analytics/growthbook.js', () => REAL_GROWTHBOOK) +}) + const originalSandboxMethods = { isSandboxingEnabled: SandboxManager.isSandboxingEnabled, areUnsandboxedCommandsAllowed: SandboxManager.areUnsandboxedCommandsAllowed, @@ -107,19 +121,19 @@ async function importBashToolWithExecutionMocks() { ) mock.module('../../utils/Shell.js', () => ({ - ...realShell, + ...REAL_SHELL, exec: execMock, })) mock.module('src/utils/Shell.js', () => ({ - ...realShell, + ...REAL_SHELL, exec: execMock, })) mock.module('../../services/analytics/growthbook.js', () => ({ - ...realGrowthbook, + ...REAL_GROWTHBOOK, getFeatureValue_CACHED_MAY_BE_STALE, })) mock.module('src/services/analytics/growthbook.js', () => ({ - ...realGrowthbook, + ...REAL_GROWTHBOOK, getFeatureValue_CACHED_MAY_BE_STALE, })) @@ -137,11 +151,11 @@ async function importSandboxPresentationWithMocks() { ) mock.module('../../services/analytics/growthbook.js', () => ({ - ...realGrowthbook, + ...REAL_GROWTHBOOK, getFeatureValue_CACHED_MAY_BE_STALE, })) mock.module('src/services/analytics/growthbook.js', () => ({ - ...realGrowthbook, + ...REAL_GROWTHBOOK, getFeatureValue_CACHED_MAY_BE_STALE, })) From a63ad4a9e0173137de203cafbc18b3bbcd3a4f9e Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 10:08:10 -0700 Subject: [PATCH 031/121] test: restore real secureStorage exports after githubModelsCredentials hydrate tests (TA-001 round 2) (#2338) --- .../githubModelsCredentials.hydrate.test.ts | 38 ++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/utils/githubModelsCredentials.hydrate.test.ts b/src/utils/githubModelsCredentials.hydrate.test.ts index 4385e8a499..fc3bdbc8d4 100644 --- a/src/utils/githubModelsCredentials.hydrate.test.ts +++ b/src/utils/githubModelsCredentials.hydrate.test.ts @@ -3,12 +3,22 @@ * githubModelsCredentials so Bun's mock.module can replace secureStorage * before that module is first loaded. */ -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { snapshotEnv } from '../test/envSnapshot.js' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' +const nonce = `${Date.now()}-${Math.random()}` +const realSecureStorage = { + ...await import(`./secureStorage/index.js?real=${nonce}`), +} + +afterAll(() => { + mock.module('./secureStorage/index.js', () => realSecureStorage) +}) + type GithubModelsCredentialsModule = typeof import('./githubModelsCredentials.js') @@ -25,30 +35,24 @@ function getEnvValue(name: string): string | undefined { } describe('hydrateGithubModelsTokenFromSecureStorage', () => { - const orig = { - CLAUDE_CODE_USE_GITHUB: process.env.CLAUDE_CODE_USE_GITHUB, - GITHUB_COPILOT_KEY: process.env.GITHUB_COPILOT_KEY, - GITHUB_TOKEN: process.env.GITHUB_TOKEN, - GH_TOKEN: process.env.GH_TOKEN, - CLAUDE_CODE_GITHUB_TOKEN_HYDRATED: - process.env.CLAUDE_CODE_GITHUB_TOKEN_HYDRATED, - CLAUDE_CODE_SIMPLE: process.env.CLAUDE_CODE_SIMPLE, - } + let restoreEnv: () => void = () => {} beforeEach(async () => { await acquireSharedMutationLock('utils/githubModelsCredentials.hydrate.test.ts') + restoreEnv = snapshotEnv([ + 'CLAUDE_CODE_USE_GITHUB', + 'GITHUB_COPILOT_KEY', + 'GITHUB_TOKEN', + 'GH_TOKEN', + 'CLAUDE_CODE_GITHUB_TOKEN_HYDRATED', + 'CLAUDE_CODE_SIMPLE', + ]) }) afterEach(() => { try { mock.restore() - for (const [k, v] of Object.entries(orig)) { - if (v === undefined) { - delete process.env[k as keyof typeof orig] - } else { - process.env[k as keyof typeof orig] = v - } - } + restoreEnv() } finally { releaseSharedMutationLock() } From 90b34cbd9b2e3c4902e6eca6d1113d5790982dda Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 11:49:18 -0700 Subject: [PATCH 032/121] test: restore leaked settings, sleep and fast-mode mocks and bound loadSkillsDir readiness waits (TA-001) (#2339) --- src/services/api/withRetry.test.ts | 14 +++++- src/services/tips/sponsoredTips.test.ts | 15 ++++++- src/skills/loadSkillsDir.test.ts | 60 +++++++++++++++++++++++-- 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/src/services/api/withRetry.test.ts b/src/services/api/withRetry.test.ts index 78cc503249..66f080aa25 100644 --- a/src/services/api/withRetry.test.ts +++ b/src/services/api/withRetry.test.ts @@ -3,8 +3,10 @@ import type Anthropic from '@anthropic-ai/sdk' import { APIError, APIUserAbortError } from '@anthropic-ai/sdk' import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js' import * as debugNs from '../../utils/debug.js' +import * as sleepNs from '../../utils/sleep.js' import { markOpenAIRequestNonReplayable } from './openaiErrorClassification.js' type ProvidersModule = typeof import('../../utils/model/providers.js') +type FastModeModule = typeof import('../../utils/fastMode.js') // Helper to build a mock APIError with specific headers function makeError(headers: Record): APIError { @@ -20,7 +22,9 @@ function makeError(headers: Record): APIError { // Save/restore env vars between tests const originalEnv = { ...process.env } const originalDebugModule = { ...debugNs } +const originalSleepModule = { ...sleepNs } let originalProvidersModule: ProvidersModule | undefined +let originalFastModeModule: FastModeModule | undefined const envKeys = [ 'CLAUDE_CODE_USE_OPENAI', @@ -56,6 +60,10 @@ afterEach(() => { mock.module('src/utils/model/providers.js', () => originalProvidersModule!) } mock.module('src/utils/debug.js', () => originalDebugModule) + mock.module('src/utils/sleep.js', () => originalSleepModule) + if (originalFastModeModule) { + mock.module('src/utils/fastMode.js', () => originalFastModeModule!) + } } finally { releaseSharedMutationLock() } @@ -102,9 +110,11 @@ async function importFreshWithRetryModule( usesAnthropicAccountFlow: () => false, })) if (options.forceFastMode) { - const realFastMode = await import('../../utils/fastMode.js') + originalFastModeModule ??= { + ...(await import('../../utils/fastMode.js')), + } mock.module('src/utils/fastMode.js', () => ({ - ...realFastMode, + ...originalFastModeModule!, isFastModeEnabled: () => true, })) } diff --git a/src/services/tips/sponsoredTips.test.ts b/src/services/tips/sponsoredTips.test.ts index ab6859b133..266ac9d3ea 100644 --- a/src/services/tips/sponsoredTips.test.ts +++ b/src/services/tips/sponsoredTips.test.ts @@ -20,6 +20,10 @@ const configRef: { // mock.module is process-global — install once, then mutate the refs per test. await acquireSharedMutationLock('services/tips/sponsoredTips.test.ts') +// Bun repoints module namespaces when mocked; preserve the original exports. +const realSettings = { ...actualSettings } +const realConfig = { ...actualConfig } + mock.module('../../utils/settings/settings.js', () => ({ getSettings_DEPRECATED: () => settingsRef.value, getInitialSettings: () => settingsRef.value, @@ -36,8 +40,15 @@ mock.module('../../utils/config.js', () => ({ afterAll(() => { try { mock.restore() - mock.module('../../utils/settings/settings.js', () => actualSettings) - mock.module('../../utils/config.js', () => actualConfig) + mock.module('../../utils/settings/settings.js', () => realSettings) + mock.module('../../utils/config.js', () => realConfig) + expect(actualSettings.getSettings_DEPRECATED).toBe( + realSettings.getSettings_DEPRECATED, + ) + expect(actualSettings.getInitialSettings).toBe(realSettings.getInitialSettings) + expect(actualSettings.getSettingsForSource).toBe(realSettings.getSettingsForSource) + expect(actualConfig.getGlobalConfig).toBe(realConfig.getGlobalConfig) + expect(actualConfig.saveGlobalConfig).toBe(realConfig.saveGlobalConfig) } finally { releaseSharedMutationLock() } diff --git a/src/skills/loadSkillsDir.test.ts b/src/skills/loadSkillsDir.test.ts index c9bdb41c62..974bad9909 100644 --- a/src/skills/loadSkillsDir.test.ts +++ b/src/skills/loadSkillsDir.test.ts @@ -480,6 +480,28 @@ function promptSkillsNamed( ) } +async function waitForReadiness( + readiness: Promise, + description: string, + timeoutMs = 1000, +): Promise { + let timer: ReturnType | undefined + try { + return await Promise.race([ + readiness, + new Promise((_, reject) => { + timer = setTimeout(() => { + reject(new Error( + `Timed out after ${timeoutMs}ms waiting for ${description}`, + )) + }, timeoutMs) + }), + ]) + } finally { + if (timer !== undefined) clearTimeout(timer) + } +} + async function withSiblingHomeFixture( run: (fixture: SiblingHomeFixture) => Promise, ): Promise { @@ -525,6 +547,27 @@ async function withSiblingHomeFixture( } } +test.serial('releases the sibling home fixture lock when readiness times out', async () => { + await assert.rejects( + withSiblingHomeFixture(async () => { + await waitForReadiness( + new Promise(() => {}), + 'missing sibling skill readiness', + 50, + ) + }), + { message: 'Timed out after 50ms waiting for missing sibling skill readiness' }, + ) + + let lockAcquired = false + try { + await acquireSharedMutationLock('loadSkillsDir.test.ts readiness cleanup', 250) + lockAcquired = true + } finally { + if (lockAcquired) releaseSharedMutationLock() + } +}) + test.serial('loads ~/.agents/skills by default and keeps ~/.claude/skills opt-in', async () => { await withSiblingHomeFixture(async ({ homeDir }) => { writeSiblingSkill(homeDir, '.agents', 'agent-skill') @@ -730,7 +773,10 @@ for (const home of ['.agents', '.claude'] as const) { } }) const oldScan = getSkillDirCommands(cwd) try { - await reading + await waitForReadiness( + reading, + `both SKILL.md reads for ${home} before ${invalidation}`, + ) if (invalidation === 'disable') { writeFileSync(join(configDir, 'settings.json'), JSON.stringify({ agentsSkillsEnabled: false, claudeCodeSkillsEnabled: false, @@ -1177,7 +1223,11 @@ for (const home of ['.agents', '.claude'] as const) { } try { await detector.initialize() - await Promise.all(ready) + await waitForReadiness( + Promise.all(ready), + `initial watcher readiness for ${home}`, + 3_000, + ) assert.ok((await invokedText('linked')).includes('Instructions initial linked')) await editUntilRefreshed(plainFile, 'plain', 'plain control') await editUntilRefreshed(targetFile, 'linked', 'revised linked') @@ -1194,7 +1244,11 @@ for (const home of ['.agents', '.claude'] as const) { writeFileSync(join(configDir, 'settings.json'), JSON.stringify({ [settingKey]: true })) resetSettingsCache() await detector.reloadAfterSkillSourceSettingsChange() - await Promise.all(ready) + await waitForReadiness( + Promise.all(ready), + `watcher readiness for ${home} after re-enabling`, + 3_000, + ) await editUntilRefreshed(targetFile, 'linked', 're-enabled linked') await detector.dispose() const disposedCount = notifications From 56da2c4b4068642c5be00bfa4b2093db1911122f Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 12:55:21 -0700 Subject: [PATCH 033/121] test: restore real ColorPicker and CustomSelect exports after wizard step tests (TA-001 round 2) (#2340) --- .../wizard-steps/wizardSteps.test.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/agents/new-agent-creation/wizard-steps/wizardSteps.test.tsx b/src/components/agents/new-agent-creation/wizard-steps/wizardSteps.test.tsx index 889ea6dd6d..6ff94147c5 100644 --- a/src/components/agents/new-agent-creation/wizard-steps/wizardSteps.test.tsx +++ b/src/components/agents/new-agent-creation/wizard-steps/wizardSteps.test.tsx @@ -1,6 +1,6 @@ import { PassThrough } from 'node:stream' -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import React, { useEffect } from 'react' import stripAnsi from 'strip-ansi' @@ -8,6 +8,15 @@ import { Box, createRoot, Text } from '../../../../ink.js' import { WizardProvider, useWizard } from '../../../wizard/index.js' import type { AgentWizardData } from '../types.js' +// Copy real exports before mock.module can replace live module bindings. +const nonce = `${Date.now()}-${Math.random()}` +const realColorPicker = { + ...await import(`../../ColorPicker.js?real=${nonce}`), +} +const realSelect = { + ...await import(`../../../CustomSelect/select.js?real=${nonce}`), +} + type ColorPickerProps = { onConfirm: (color: string | undefined) => void } @@ -160,6 +169,12 @@ afterEach(() => { } }) +afterAll(() => { + // mock.restore() does not remove Bun module mock registrations. + mock.module('../../ColorPicker.js', () => realColorPicker) + mock.module('../../../CustomSelect/select.js', () => realSelect) +}) + test('ColorStep does not create a final agent when required wizard data is missing', async () => { const { ColorStep } = await importFreshStep('./ColorStep.js') From 0724ca8783a87f3da5e3221e666eee2a291fafeb Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 14:47:51 -0700 Subject: [PATCH 034/121] test: restore leaked module mocks behind the remaining non-isolated shard failures (TA-001 round 2) (#2341) * test: restore leaked module mocks behind the remaining non-isolated shard failures (TA-001 round 2) * test: restore real sessionStorage after resume tests (TA-001 round 2) --- .../setupGitHubActions.test.ts | 7 +++++ src/commands/replay/replay.test.tsx | 30 ++++++++++++++----- src/commands/resume/resume.test.tsx | 5 ++++ src/hooks/useApiKeyVerification.test.tsx | 8 +++-- .../AgentTool/runAgent.persistence.test.ts | 11 +++++-- .../BashTool/bashCommandAnalysis.test.ts | 6 +++- 6 files changed, 55 insertions(+), 12 deletions(-) diff --git a/src/commands/install-github-app/setupGitHubActions.test.ts b/src/commands/install-github-app/setupGitHubActions.test.ts index b7e114170c..212a411adb 100644 --- a/src/commands/install-github-app/setupGitHubActions.test.ts +++ b/src/commands/install-github-app/setupGitHubActions.test.ts @@ -6,6 +6,7 @@ import { } from '../../test/sharedMutationLock.js' type BrowserModule = typeof import('../../utils/browser.js') +type ConfigModule = typeof import('../../utils/config.js') type ExecFileNoThrowModule = typeof import('../../utils/execFileNoThrow.js') type ExecCall = { @@ -15,6 +16,7 @@ type ExecCall = { type RealModules = { browser: BrowserModule + config: ConfigModule execFileNoThrow: ExecFileNoThrowModule } @@ -44,6 +46,9 @@ async function importRealModules(): Promise { browser: (await import( `../../utils/browser.ts?setup-actions-real-${cacheKey}` )) as BrowserModule, + config: { + ...await import(`../../utils/config.js?real=${cacheKey}`), + } as ConfigModule, execFileNoThrow: (await import( `../../utils/execFileNoThrow.ts?setup-actions-real-${cacheKey}` )) as ExecFileNoThrowModule, @@ -212,6 +217,8 @@ afterEach(() => { try { mock.restore() if (realModules) { + mock.module('src/utils/config.js', () => realModules!.config) + mock.module('../../utils/config.js', () => realModules!.config) mock.module( '../../utils/execFileNoThrow.js', () => realModules!.execFileNoThrow, diff --git a/src/commands/replay/replay.test.tsx b/src/commands/replay/replay.test.tsx index 7bfa6e17cc..af55570e35 100644 --- a/src/commands/replay/replay.test.tsx +++ b/src/commands/replay/replay.test.tsx @@ -10,13 +10,29 @@ import { AppStateProvider } from '../../state/AppState.js' import type { LogOption, ReplayIndex } from '../../types/logs.js' import { renderToString } from '../../utils/staticRender.js' import * as realBootstrapState from '../../bootstrap/state.js' -import * as realGetWorktreePaths from '../../utils/getWorktreePaths.js' -import * as realSessionStorage from '../../utils/sessionStorage.js' -import * as realReplayIndex from '../../utils/replayIndex.js' -import * as realUseTerminalSize from '../../hooks/useTerminalSize.js' -import * as realModalContext from '../../context/modalContext.js' -import * as realLogSelector from '../../components/LogSelector.js' -import * as realReplayTimeline from './ReplayTimeline.js' + +const nonce = `${Date.now()}-${Math.random()}` +const realGetWorktreePaths = { + ...await import(`../../utils/getWorktreePaths.js?real=${nonce}`), +} +const realSessionStorage = { + ...await import(`../../utils/sessionStorage.js?real=${nonce}`), +} +const realReplayIndex = { + ...await import(`../../utils/replayIndex.js?real=${nonce}`), +} +const realUseTerminalSize = { + ...await import(`../../hooks/useTerminalSize.js?real=${nonce}`), +} +const realModalContext = { + ...await import(`../../context/modalContext.js?real=${nonce}`), +} +const realLogSelector = { + ...await import(`../../components/LogSelector.js?real=${nonce}`), +} +const realReplayTimeline = { + ...await import(`./ReplayTimeline.js?real=${nonce}`), +} type LogSelectorProps = { logs: LogOption[] diff --git a/src/commands/resume/resume.test.tsx b/src/commands/resume/resume.test.tsx index fc8e91c4bd..a71849c8e2 100644 --- a/src/commands/resume/resume.test.tsx +++ b/src/commands/resume/resume.test.tsx @@ -18,6 +18,10 @@ import type { LocalJSXCommandOnDone, } from '../../types/command.js' +const realSessionStorage = { + ...await import(`../../utils/sessionStorage.js?real=${Date.now()}-${Math.random()}`), +} + type OnDoneArgs = Parameters let saveGoalStateMock: ReturnType @@ -196,6 +200,7 @@ describe('/resume and /continue unified command', () => { afterEach(() => { mock.restore() + mock.module('../../utils/sessionStorage.js', () => realSessionStorage) }) test('/continue continues an active goal', async () => { diff --git a/src/hooks/useApiKeyVerification.test.tsx b/src/hooks/useApiKeyVerification.test.tsx index 3b41fe5761..0c21b7e3e2 100644 --- a/src/hooks/useApiKeyVerification.test.tsx +++ b/src/hooks/useApiKeyVerification.test.tsx @@ -3,8 +3,6 @@ import { PassThrough } from 'node:stream' import { afterAll, afterEach, beforeEach, expect, mock, test } from 'bun:test' import React from 'react' import { createRoot, Text } from '../ink.js' -import * as realState from '../bootstrap/state.js' -import * as realClaudeApi from '../services/api/claude.js' import { acquireSharedMutationLock, releaseSharedMutationLock, @@ -14,6 +12,12 @@ const nonce = `${Date.now()}-${Math.random()}` const realAuth = { ...await import(`../utils/auth.js?real=${nonce}`), } +const realState = { + ...await import(`../bootstrap/state.js?real=${nonce}`), +} +const realClaudeApi = { + ...await import(`../services/api/claude.js?real=${nonce}`), +} type AuthState = { anthropicAuthEnabled: boolean diff --git a/src/tools/AgentTool/runAgent.persistence.test.ts b/src/tools/AgentTool/runAgent.persistence.test.ts index 6f4d260f40..afa1fcae9d 100644 --- a/src/tools/AgentTool/runAgent.persistence.test.ts +++ b/src/tools/AgentTool/runAgent.persistence.test.ts @@ -2,8 +2,13 @@ import { afterEach, beforeEach, expect, mock, test } from 'bun:test' import type { AgentDefinition } from './loadAgentsDir.js' import type { ToolUseContext } from '../../Tool.js' -const actualSessionStorage = { ...await import('../../utils/sessionStorage.js') } -const actualQuery = { ...await import('../../query.js') } +const nonce = `${Date.now()}-${Math.random()}` +const actualSessionStorage = { + ...await import(`../../utils/sessionStorage.js?real=${nonce}`), +} +const actualQuery = { + ...await import(`../../query.js?real=${nonce}`), +} // Track how many times each storage function is called let metadataWriteCount = 0 @@ -16,6 +21,8 @@ beforeEach(() => { afterEach(() => { mock.restore() + mock.module('../../utils/sessionStorage.js', () => actualSessionStorage) + mock.module('../../query.js', () => actualQuery) }) /** Load runAgent with a failing metadata writer while retaining unrelated module exports. */ diff --git a/src/tools/BashTool/bashCommandAnalysis.test.ts b/src/tools/BashTool/bashCommandAnalysis.test.ts index c1e61efcc2..3e50a9f65a 100644 --- a/src/tools/BashTool/bashCommandAnalysis.test.ts +++ b/src/tools/BashTool/bashCommandAnalysis.test.ts @@ -5,9 +5,13 @@ import { } from '../../test/sharedMutationLock.js' import { parseForSecurityFromAst } from '../../utils/bash/ast.js' import { PARSE_ABORTED } from '../../utils/bash/parser.js' -import * as realDebug from '../../utils/debug.js' import { analyzeBashCommand } from './bashCommandAnalysis.js' +const nonce = `${Date.now()}-${Math.random()}` +const realDebug = { + ...await import(`../../utils/debug.js?real=${nonce}`), +} + let importCounter = 0 beforeEach(async () => { From 0f8b4cbc87c18a410eea39a472c186d4579e6b1a Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 17:10:33 -0700 Subject: [PATCH 035/121] test: restore leaked mock.module registrations behind randomized-order failures (TA-001 round 2) (#2342) * test: restore real modules after modelOptions crossProfile and catalogDedup mocks (TA-001 round 2) * test: restore real modules after leaked mock.module registrations (TA-001 round 2 batch 1) * test: restore resume mocks without overlapping sessionStorage restore * test: drop withRetry snapshot change from batch 1 * test: restore leaked mocks in batch 2 polluter files (TA-001 round 2 batch 1) * test: keep discovery cache and retry tests isolated from restored mocks (TA-001 round 2 batch 1) --------- Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com> --- src/commands/effort/effort.test.tsx | 32 +++++++++-- .../model/model.fastModeSwitch.test.ts | 6 ++- src/commands/model/model.test.tsx | 21 ++++++++ src/commands/provider/provider.test.tsx | 6 +++ src/commands/resume/resume.test.tsx | 14 +++++ .../ModelPicker.switchProfile.test.ts | 27 ++++++---- src/components/ModelPicker.test.tsx | 12 ++++- src/components/ProviderManager.test.tsx | 34 ++++++++++++ src/integrations/discoveryCache.test.ts | 8 +-- src/services/api/errors.opencodeGo.test.ts | 2 + ...iderConfig.runtimeCodexCredentials.test.ts | 6 +++ src/services/compact/autoCompact.test.ts | 6 ++- .../compact/resumeCompactPrompt.test.ts | 9 ++++ .../contextCollapse/spawnCtxAgent.test.ts | 36 ++++++++----- src/services/mcp/channelNotification.test.ts | 12 ++--- src/utils/effort.codex.test.ts | 18 +++++-- src/utils/effort.test.ts | 11 +++- src/utils/extraUsage.test.ts | 6 ++- src/utils/fastMode.test.ts | 24 ++++++--- src/utils/file.test.ts | 5 +- src/utils/mcpValidation.test.ts | 18 +++++-- .../model/modelOptions.catalogDedup.test.ts | 33 ++++++------ .../model/modelOptions.codexRecovery.test.ts | 6 +++ .../model/modelOptions.crossProfile.test.ts | 53 ++++++++++--------- src/utils/model/modelOptions.gateways.test.ts | 6 +++ src/utils/modelCost.modelGate.test.ts | 11 ++-- .../dangerousModePromptRuntime.test.ts | 5 ++ src/utils/plugins/lspRecommendation.test.ts | 20 +++++-- .../officialMarketplaceStartupCheck.test.ts | 52 ++++++++++++------ src/utils/providerProfiles.test.ts | 6 +++ src/utils/status.test.ts | 6 +++ src/utils/toolSearch.test.ts | 5 ++ src/utils/user.test.ts | 26 +++++---- 33 files changed, 407 insertions(+), 135 deletions(-) diff --git a/src/commands/effort/effort.test.tsx b/src/commands/effort/effort.test.tsx index 8455d6218c..9532211fe3 100644 --- a/src/commands/effort/effort.test.tsx +++ b/src/commands/effort/effort.test.tsx @@ -9,11 +9,26 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' -import * as actualAuth from '../../utils/auth.js' -import * as actualModelSupportOverrides from '../../utils/model/modelSupportOverrides.js' -import * as actualSettings from '../../utils/settings/settings.js' -import * as actualThinking from '../../utils/thinking.js' -import * as actualGrowthbook from '../../services/analytics/growthbook.js' + +const nonce = `${Date.now()}-${Math.random()}` +const actualAuth = { + ...await import(`../../utils/auth.js?real=${nonce}`), +} as typeof import('../../utils/auth.js') +const actualModelSupportOverrides = { + ...await import(`../../utils/model/modelSupportOverrides.js?real=${nonce}`), +} as typeof import('../../utils/model/modelSupportOverrides.js') +const actualSettings = { + ...await import(`../../utils/settings/settings.js?real=${nonce}`), +} as typeof import('../../utils/settings/settings.js') +const actualThinking = { + ...await import(`../../utils/thinking.js?real=${nonce}`), +} as typeof import('../../utils/thinking.js') +const actualGrowthbook = { + ...await import(`../../services/analytics/growthbook.js?real=${nonce}`), +} as typeof import('../../services/analytics/growthbook.js') +const realEffortPicker = { + ...await import(`../../components/EffortPicker.js?real=${nonce}`), +} as typeof import('../../components/EffortPicker.js') const providerSpecifier = '../../utils/model/providers.js' const realProvidersForCleanup = { @@ -39,6 +54,13 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + mock.module('../../utils/auth.js', () => actualAuth) + mock.module('../../utils/model/modelSupportOverrides.js', () => actualModelSupportOverrides) + mock.module('../../utils/settings/settings.js', () => actualSettings) + mock.module('../../utils/thinking.js', () => actualThinking) + mock.module('../../services/analytics/growthbook.js', () => actualGrowthbook) + mock.module('../../utils/model/providers.js', () => realProvidersForCleanup) + mock.module('../../components/EffortPicker.js', () => realEffortPicker) if (originalEffortEnv === undefined) { delete process.env.CLAUDE_CODE_EFFORT_LEVEL } else { diff --git a/src/commands/model/model.fastModeSwitch.test.ts b/src/commands/model/model.fastModeSwitch.test.ts index 04809d31cf..8857b52f82 100644 --- a/src/commands/model/model.fastModeSwitch.test.ts +++ b/src/commands/model/model.fastModeSwitch.test.ts @@ -1,6 +1,9 @@ import { afterEach, expect, mock, test } from 'bun:test' -import * as actualFastMode from '../../utils/fastMode.js' +const nonce = `${Date.now()}-${Math.random()}` +const actualFastMode = { + ...await import(`../../utils/fastMode.js?real=${nonce}`), +} as typeof import('../../utils/fastMode.js') // Regression for #1119 / jatmn review on PR #1164 — the cross-profile // `/model` branch must run the same fast-mode reconciliation as the regular @@ -9,6 +12,7 @@ import * as actualFastMode from '../../utils/fastMode.js' afterEach(() => { mock.restore() + mock.module('../../utils/fastMode.js', () => actualFastMode) }) function mockFastMode( diff --git a/src/commands/model/model.test.tsx b/src/commands/model/model.test.tsx index 0e983403c3..37edda8463 100644 --- a/src/commands/model/model.test.tsx +++ b/src/commands/model/model.test.tsx @@ -24,6 +24,22 @@ import { import { mergeRouteCatalogEntries } from '../../utils/model/routeCatalogOptions.js' const nonce = `${Date.now()}-${Math.random()}` +const realSettings = { + ...await import(`../../utils/settings/settings.js?real=${nonce}`), +} as typeof import('../../utils/settings/settings.js') +const realModelAllowlist = { + ...await import(`../../utils/model/modelAllowlist.js?real=${nonce}`), +} as typeof import('../../utils/model/modelAllowlist.js') +const realConfig = { + ...await import(`../../utils/config.js?real=${nonce}`), +} as typeof import('../../utils/config.js') +const realModelPicker = { + ...await import(`../../components/ModelPicker.js?real=${nonce}`), +} as typeof import('../../components/ModelPicker.js') +const realOpenAIModelDiscovery = { + ...await import(`../../utils/model/openaiModelDiscovery.js?real=${nonce}`), +} as typeof import('../../utils/model/openaiModelDiscovery.js') + const REAL_DISCOVERY_SERVICE_FOR_MODEL_TEST = { ...await import(`../../integrations/discoveryService.js?real=${nonce}`), } @@ -253,6 +269,11 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + mock.module('../../utils/settings/settings.js', () => realSettings) + mock.module('../../utils/model/modelAllowlist.js', () => realModelAllowlist) + mock.module('../../utils/config.js', () => realConfig) + mock.module('../../components/ModelPicker.js', () => realModelPicker) + mock.module('../../utils/model/openaiModelDiscovery.js', () => realOpenAIModelDiscovery) resetSettingsCache() settingsForTest = {} restoreEnv('CLAUDE_CODE_USE_OPENAI', originalEnv.CLAUDE_CODE_USE_OPENAI) diff --git a/src/commands/provider/provider.test.tsx b/src/commands/provider/provider.test.tsx index d3d3d3183f..8c20018232 100644 --- a/src/commands/provider/provider.test.tsx +++ b/src/commands/provider/provider.test.tsx @@ -23,6 +23,11 @@ import { } from '../../test/sharedMutationLock.js' import { createProfileFile } from '../../utils/providerProfile.js' +const nonce = `${Date.now()}-${Math.random()}` +const realProviderConfig = { + ...await import(`../../services/api/providerConfig.js?real=${nonce}`), +} as typeof import('../../services/api/providerConfig.js') + const SYNC_START = '\x1B[?2026h' const SYNC_END = '\x1B[?2026l' const ORIGINAL_SIMPLE_ENV = process.env.CLAUDE_CODE_SIMPLE @@ -169,6 +174,7 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + mock.module('../../services/api/providerConfig.js', () => realProviderConfig) if (ORIGINAL_SIMPLE_ENV === undefined) { delete process.env.CLAUDE_CODE_SIMPLE diff --git a/src/commands/resume/resume.test.tsx b/src/commands/resume/resume.test.tsx index a71849c8e2..0c1635f385 100644 --- a/src/commands/resume/resume.test.tsx +++ b/src/commands/resume/resume.test.tsx @@ -21,6 +21,15 @@ import type { const realSessionStorage = { ...await import(`../../utils/sessionStorage.js?real=${Date.now()}-${Math.random()}`), } +const realGoalPersistence = { + ...await import(`../../services/goal/persistence.js?real=${Date.now()}-${Math.random()}`), +} +const realGetWorktreePaths = { + ...await import(`../../utils/getWorktreePaths.js?real=${Date.now()}-${Math.random()}`), +} +const realReplayIndex = { + ...await import(`../../utils/replayIndex.js?real=${Date.now()}-${Math.random()}`), +} type OnDoneArgs = Parameters @@ -194,6 +203,8 @@ describe('/resume and /continue unified command', () => { searchSessionsByCustomTitle: searchSessionsByCustomTitleMock, })) mock.module('../../utils/replayIndex.js', () => ({ + // Bun caches the initial mock's export names, including after restoration. + ...realReplayIndex, loadReplayIndex: loadReplayIndexMock, })) }) @@ -201,6 +212,9 @@ describe('/resume and /continue unified command', () => { afterEach(() => { mock.restore() mock.module('../../utils/sessionStorage.js', () => realSessionStorage) + mock.module('../../services/goal/persistence.js', () => realGoalPersistence) + mock.module('../../utils/getWorktreePaths.js', () => realGetWorktreePaths) + mock.module('../../utils/replayIndex.js', () => realReplayIndex) }) test('/continue continues an active goal', async () => { diff --git a/src/components/ModelPicker.switchProfile.test.ts b/src/components/ModelPicker.switchProfile.test.ts index d3ce35f22e..0f40676727 100644 --- a/src/components/ModelPicker.switchProfile.test.ts +++ b/src/components/ModelPicker.switchProfile.test.ts @@ -7,11 +7,19 @@ import { resetSettingsCache, setSessionSettingsCache, } from '../utils/settings/settingsCache.js' -import * as actualAuth from '../utils/auth.js' -import * as actualModelOptions from '../utils/model/modelOptions.js' -import * as actualProviderProfiles from '../utils/providerProfiles.js' import type { ProviderProfile } from '../utils/config.js' +const nonce = `${Date.now()}-${Math.random()}` +const realAuth = { + ...await import(`../utils/auth.js?real=${nonce}`), +} as typeof import('../utils/auth.js') +const realModelOptions = { + ...await import(`../utils/model/modelOptions.js?real=${nonce}`), +} as typeof import('../utils/model/modelOptions.js') +const realProviderProfiles = { + ...await import(`../utils/providerProfiles.js?real=${nonce}`), +} as typeof import('../utils/providerProfiles.js') + const providerSpecifier = '../utils/model/providers.js' const realProvidersForCleanup = { ...await import( @@ -22,21 +30,18 @@ const realProvidersForCleanup = { afterAll(() => { mock.restore() mock.module(providerSpecifier, () => realProvidersForCleanup) + mock.module('../utils/auth.js', () => realAuth) + mock.module('../utils/model/modelOptions.js', () => realModelOptions) + mock.module('../utils/providerProfiles.js', () => realProviderProfiles) }) -// Snapshot the real modules before any mock.module runs. bun live-repoints the -// `actual*` namespaces to the active mock, so these plain-object copies are the -// stable handle on the genuine implementations (2026-04-30 mock-leak lesson). -const realAuth = { ...actualAuth } -const realModelOptions = { ...actualModelOptions } -const realProviderProfiles = { ...actualProviderProfiles } const realProviders = realProvidersForCleanup // bun's mock.module is process-wide and mock.restore() does NOT undo it, so // each mock is gated and falls through to the real implementation whenever // activeProfilesOverride is null — a transparent passthrough for every other // suite in the same `bun test` run. -let activeProfilesOverride: Partial | null = null +let activeProfilesOverride: Partial | null = null mock.module('../utils/model/providers.js', () => ({ ...realProviders, @@ -91,7 +96,7 @@ mock.module('../utils/providerProfiles.js', () => ({ // mock modelOptions for the fresh ModelPicker import so its isGenuineSwitch- // ProfileValue is bound to the fresh instance. async function importFreshModelPicker( - profilesMock: Partial, + profilesMock: Partial, options: { trackGetModelOptions?: boolean } = {}, diff --git a/src/components/ModelPicker.test.tsx b/src/components/ModelPicker.test.tsx index 4f49f9c5ed..1aaff0d687 100644 --- a/src/components/ModelPicker.test.tsx +++ b/src/components/ModelPicker.test.tsx @@ -17,9 +17,17 @@ import { } from '../utils/settings/settingsCache.js' import type { SettingsJson } from '../utils/settings/types.js' +const nonce = `${Date.now()}-${Math.random()}` +const realSettings = { + ...await import(`../utils/settings/settings.js?real=${nonce}`), +} as typeof import('../utils/settings/settings.js') +const realModelAllowlist = { + ...await import(`../utils/model/modelAllowlist.js?real=${nonce}`), +} as typeof import('../utils/model/modelAllowlist.js') + type SettingsModule = typeof import('../utils/settings/settings.js') -let actualSettingsModule: SettingsModule | undefined +let actualSettingsModule: SettingsModule | undefined = realSettings let settingsForTest: SettingsJson = {} function useSettings(settings: SettingsJson): void { @@ -83,6 +91,8 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + mock.module('../utils/settings/settings.js', () => realSettings) + mock.module('../utils/model/modelAllowlist.js', () => realModelAllowlist) resetSettingsCache() settingsForTest = {} } finally { diff --git a/src/components/ProviderManager.test.tsx b/src/components/ProviderManager.test.tsx index d99d04ac13..5b9a4f2e0a 100644 --- a/src/components/ProviderManager.test.tsx +++ b/src/components/ProviderManager.test.tsx @@ -14,6 +14,32 @@ import { releaseSharedMutationLock, } from '../test/sharedMutationLock.js' +const nonce = `${Date.now()}-${Math.random()}` +const realProviderProfiles = { + ...await import(`../utils/providerProfiles.js?real=${nonce}`), +} as typeof import('../utils/providerProfiles.js') +const realProviderDiscovery = { + ...await import(`../utils/providerDiscovery.js?real=${nonce}`), +} as typeof import('../utils/providerDiscovery.js') +const realDiscoveryService = { + ...await import(`../integrations/discoveryService.js?real=${nonce}`), +} as typeof import('../integrations/discoveryService.js') +const realGithubModelsCredentials = { + ...await import(`../utils/githubModelsCredentials.js?real=${nonce}`), +} as typeof import('../utils/githubModelsCredentials.js') +const realCodexCredentials = { + ...await import(`../utils/codexCredentials.js?real=${nonce}`), +} as typeof import('../utils/codexCredentials.js') +const realProviderProfile = { + ...await import(`../utils/providerProfile.js?real=${nonce}`), +} as typeof import('../utils/providerProfile.js') +const realProviderManagerAimlapi = { + ...await import(`./providerManagerAimlapi.js?real=${nonce}`), +} as typeof import('./providerManagerAimlapi.js') +const realUseCodexOAuthFlow = { + ...await import(`./useCodexOAuthFlow.js?real=${nonce}`), +} as typeof import('./useCodexOAuthFlow.js') + type SettingsModule = typeof import('../utils/settings/settings.js') type ProviderStartupOverridesModule = typeof import('../utils/providerStartupOverrides.js') @@ -829,6 +855,14 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + mock.module('../utils/providerProfiles.js', () => realProviderProfiles) + mock.module('../utils/providerDiscovery.js', () => realProviderDiscovery) + mock.module('../integrations/discoveryService.js', () => realDiscoveryService) + mock.module('../utils/githubModelsCredentials.js', () => realGithubModelsCredentials) + mock.module('../utils/codexCredentials.js', () => realCodexCredentials) + mock.module('../utils/providerProfile.js', () => realProviderProfile) + mock.module('./providerManagerAimlapi.js', () => realProviderManagerAimlapi) + mock.module('./useCodexOAuthFlow.js', () => realUseCodexOAuthFlow) mock.module('../utils/settings/settings.js', () => actualSettingsModule) mock.module('../utils/providerStartupOverrides.js', () => actualProviderStartupOverridesModule) diff --git a/src/integrations/discoveryCache.test.ts b/src/integrations/discoveryCache.test.ts index a07a0b4cb0..c5f7af9f1b 100644 --- a/src/integrations/discoveryCache.test.ts +++ b/src/integrations/discoveryCache.test.ts @@ -22,7 +22,7 @@ import { releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -const originalConfigDir = process.env.CLAUDE_CONFIG_DIR +const originalConfigDir = process.env.OPENCLAUDE_CONFIG_DIR let tempDir: string @@ -37,7 +37,7 @@ function createModel(id: string): ModelCatalogEntry { beforeEach(async () => { await acquireSharedMutationLock('discoveryCache.test.ts') tempDir = mkdtempSync(join(tmpdir(), 'openclaude-discovery-cache-test-')) - process.env.CLAUDE_CONFIG_DIR = tempDir + process.env.OPENCLAUDE_CONFIG_DIR = tempDir setOriginalFsImplementation() await clearDiscoveryCache() }) @@ -46,9 +46,9 @@ afterEach(() => { try { setOriginalFsImplementation() if (originalConfigDir === undefined) { - delete process.env.CLAUDE_CONFIG_DIR + delete process.env.OPENCLAUDE_CONFIG_DIR } else { - process.env.CLAUDE_CONFIG_DIR = originalConfigDir + process.env.OPENCLAUDE_CONFIG_DIR = originalConfigDir } rmSync(tempDir, { recursive: true, force: true }) } finally { diff --git a/src/services/api/errors.opencodeGo.test.ts b/src/services/api/errors.opencodeGo.test.ts index c18cf68cd2..945e5e97aa 100644 --- a/src/services/api/errors.opencodeGo.test.ts +++ b/src/services/api/errors.opencodeGo.test.ts @@ -4,6 +4,7 @@ import { join } from 'node:path' import { APIError } from '@anthropic-ai/sdk' import { afterEach, beforeEach, expect, test } from 'bun:test' +import { clearOAuthTokenCache } from '../../utils/auth.js' import { setClaudeConfigHomeDirForTesting } from '../../utils/envUtils.js' import { classifyAPIError, @@ -27,6 +28,7 @@ let tempDir: string beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), 'openclaude-opencode-go-error-test-')) setClaudeConfigHomeDirForTesting(tempDir) + clearOAuthTokenCache() }) afterEach(() => { diff --git a/src/services/api/providerConfig.runtimeCodexCredentials.test.ts b/src/services/api/providerConfig.runtimeCodexCredentials.test.ts index 14b62b2966..5eeaeeccb4 100644 --- a/src/services/api/providerConfig.runtimeCodexCredentials.test.ts +++ b/src/services/api/providerConfig.runtimeCodexCredentials.test.ts @@ -9,6 +9,11 @@ import { releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' +const nonce = `${Date.now()}-${Math.random()}` +const realCodexCredentials = { + ...await import(`../../utils/codexCredentials.js?real=${nonce}`), +} as typeof import('../../utils/codexCredentials.js') + type ProviderConfigModule = typeof import('./providerConfig.js') function importFreshProviderConfig( @@ -26,6 +31,7 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + mock.module('../../utils/codexCredentials.js', () => realCodexCredentials) } finally { releaseSharedMutationLock() } diff --git a/src/services/compact/autoCompact.test.ts b/src/services/compact/autoCompact.test.ts index bbb09ca0c1..65d8e316b6 100644 --- a/src/services/compact/autoCompact.test.ts +++ b/src/services/compact/autoCompact.test.ts @@ -12,7 +12,11 @@ import { releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' import type { Message } from '../../types/message.js' -import * as realConfig from '../../utils/config.js' + +const nonce = `${Date.now()}-${Math.random()}` +const realConfig = { + ...await import(`../../utils/config.js?real=${nonce}`), +} as typeof import('../../utils/config.js') const realContext = await import( `../../utils/context.js?real=${Date.now()}-${Math.random()}` diff --git a/src/services/compact/resumeCompactPrompt.test.ts b/src/services/compact/resumeCompactPrompt.test.ts index eb32bec466..14a2de8e99 100644 --- a/src/services/compact/resumeCompactPrompt.test.ts +++ b/src/services/compact/resumeCompactPrompt.test.ts @@ -1,5 +1,12 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +const realTokens = { + ...await import(`../../utils/tokens.js?real=${Date.now()}-${Math.random()}`), +} +const realAutoCompact = { + ...await import(`./autoCompact.js?real=${Date.now()}-${Math.random()}`), +} + async function importResumeCompactPrompt() { mock.restore() const nonce = `${Date.now()}-${Math.random()}` @@ -15,6 +22,8 @@ describe('shouldPromptCompactOnResume', () => { afterEach(() => { mock.restore() + mock.module('../../utils/tokens.js', () => realTokens) + mock.module('./autoCompact.js', () => realAutoCompact) }) test('returns false for empty messages', async () => { diff --git a/src/services/contextCollapse/spawnCtxAgent.test.ts b/src/services/contextCollapse/spawnCtxAgent.test.ts index a7af0e852f..792a12df4d 100644 --- a/src/services/contextCollapse/spawnCtxAgent.test.ts +++ b/src/services/contextCollapse/spawnCtxAgent.test.ts @@ -14,21 +14,29 @@ import type { Message } from '../../types/message.js' // which imports that function and uses it to size truncation. Restoring by // specifier is safe: autoCompact.test.ts re-imports autoCompact fresh via a // cache-busting nonce (a different specifier), so it is unaffected. -import * as spanSelectionNs from './spanSelection.js' -import * as forkedAgentNs from '../../utils/forkedAgent.js' -import * as tokensNs from '../../utils/tokens.js' -import * as autoCompactNs from '../compact/autoCompact.js' -import * as analyticsNs from '../../services/analytics/index.js' -import * as logNs from '../../utils/log.js' -import * as messagesNs from '../../utils/messages.js' -const realSpanSelection = { ...spanSelectionNs } -const realForkedAgent = { ...forkedAgentNs } -const realTokens = { ...tokensNs } -const realAutoCompact = { ...autoCompactNs } -const realAnalytics = { ...analyticsNs } -const realLog = { ...logNs } -const realMessages = { ...messagesNs } +const nonce = `${Date.now()}-${Math.random()}` +const realSpanSelection = { + ...await import(`./spanSelection.js?real=${nonce}`), +} as typeof import('./spanSelection.js') +const realForkedAgent = { + ...await import(`../../utils/forkedAgent.js?real=${nonce}`), +} as typeof import('../../utils/forkedAgent.js') +const realTokens = { + ...await import(`../../utils/tokens.js?real=${nonce}`), +} as typeof import('../../utils/tokens.js') +const realAutoCompact = { + ...await import(`../compact/autoCompact.js?real=${nonce}`), +} as typeof import('../compact/autoCompact.js') +const realAnalytics = { + ...await import(`../../services/analytics/index.js?real=${nonce}`), +} as typeof import('../../services/analytics/index.js') +const realLog = { + ...await import(`../../utils/log.js?real=${nonce}`), +} as typeof import('../../utils/log.js') +const realMessages = { + ...await import(`../../utils/messages.js?real=${nonce}`), +} as typeof import('../../utils/messages.js') // Build a transcript big enough to yield a collapsible span. function bigTranscript(): Message[] { diff --git a/src/services/mcp/channelNotification.test.ts b/src/services/mcp/channelNotification.test.ts index cd7cc6e4a7..5da1b6b708 100644 --- a/src/services/mcp/channelNotification.test.ts +++ b/src/services/mcp/channelNotification.test.ts @@ -26,14 +26,14 @@ import { parsePluginIdentifier } from '../../utils/plugins/pluginIdentifier.js' // module-level mock.module() overrides in bun (the registry is process-global), // so without this, neighboring test files that import the real module would // fail with "Export named 'getChannelAllowlist' not found". -const _realChannelAllowlist = await import( - `./channelAllowlist.js?real=${Date.now()}-${Math.random()}` -) +const _realChannelAllowlist = { + ...await import(`./channelAllowlist.js?real=${Date.now()}-${Math.random()}`), +} // Real auth module — captured before mocking so afterAll can restore it. -const _realAuth = await import( - `../../utils/auth.js?real=${Date.now()}-${Math.random()}` -) +const _realAuth = { + ...await import(`../../utils/auth.js?real=${Date.now()}-${Math.random()}`), +} // Module-level mocks for the GrowthBook-backed helpers. The gate // reads these on every call; resetting between tests keeps the diff --git a/src/utils/effort.codex.test.ts b/src/utils/effort.codex.test.ts index 6e47a7856d..ed0a35ec49 100644 --- a/src/utils/effort.codex.test.ts +++ b/src/utils/effort.codex.test.ts @@ -9,12 +9,22 @@ import { // any module we mock here needs to keep the full original export shape — or // downstream tests that load it via openaiShim/client/codexShim crash with // "Export named 'X' not found in module". -import * as actualAuth from './auth.js' -import * as actualThinking from './thinking.js' -import * as actualGrowthbook from 'src/services/analytics/growthbook.js' -import * as actualModelSupportOverrides from './model/modelSupportOverrides.js' import type { APIProvider } from './model/providers.js' +const nonce = `${Date.now()}-${Math.random()}` +const actualAuth = { + ...await import(`./auth.js?real=${nonce}`), +} as typeof import('./auth.js') +const actualThinking = { + ...await import(`./thinking.js?real=${nonce}`), +} as typeof import('./thinking.js') +const actualGrowthbook = { + ...await import(`../services/analytics/growthbook.js?real=${nonce}`), +} as typeof import('src/services/analytics/growthbook.js') +const actualModelSupportOverrides = { + ...await import(`./model/modelSupportOverrides.js?real=${nonce}`), +} as typeof import('./model/modelSupportOverrides.js') + type MockedThirdPartyCapability = 'effort' | 'max_effort' | 'xhigh_effort' const originalEnv = { ...process.env } diff --git a/src/utils/effort.test.ts b/src/utils/effort.test.ts index ea7b9089da..549fcca481 100644 --- a/src/utils/effort.test.ts +++ b/src/utils/effort.test.ts @@ -7,8 +7,15 @@ import { ensureIntegrationsLoaded, getCatalogEntriesForRoute, } from '../integrations/index.js' -import * as realAuth from './auth.js' -import * as realThinking from './thinking.js' + +const nonce = `${Date.now()}-${Math.random()}` +const realAuth = { + ...await import(`./auth.js?real=${nonce}`), +} as typeof import('./auth.js') +const realThinking = { + ...await import(`./thinking.js?real=${nonce}`), +} as typeof import('./thinking.js') + const realModelSupportOverridesModule = await import( `./model/modelSupportOverrides.js?real=${Date.now()}-${Math.random()}`, ) diff --git a/src/utils/extraUsage.test.ts b/src/utils/extraUsage.test.ts index 2c0b886687..76e15268ff 100644 --- a/src/utils/extraUsage.test.ts +++ b/src/utils/extraUsage.test.ts @@ -3,9 +3,13 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as realAuth from './auth.js' import { isBilledAsExtraUsage } from './extraUsage.js' +const nonce = `${Date.now()}-${Math.random()}` +const realAuth = { + ...await import(`./auth.js?real=${nonce}`), +} as typeof import('./auth.js') + beforeEach(async () => { await acquireSharedMutationLock('utils/extraUsage.test.ts') delete process.env.CLAUDE_CODE_DISABLE_1M_CONTEXT diff --git a/src/utils/fastMode.test.ts b/src/utils/fastMode.test.ts index 0cd7cbc67c..c8f0763fe4 100644 --- a/src/utils/fastMode.test.ts +++ b/src/utils/fastMode.test.ts @@ -1,23 +1,35 @@ import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' -import * as realAxios from 'axios' -import * as realOauthConstants from 'src/constants/oauth.js' -import * as realGrowthbook from 'src/services/analytics/growthbook.js' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as realModel from './model/model.js' type ProvidersModule = typeof import('./model/providers.js') type AxiosModule = typeof import('axios') const nonce = `${Date.now()}-${Math.random()}` +const realAxios = { + ...await import(`${import.meta.resolve('axios')}?real=${nonce}`), +} as typeof import('axios') +const realOauthConstants = { + ...await import(`../constants/oauth.js?real=${nonce}`), +} as typeof import('src/constants/oauth.js') +const realGrowthbook = { + ...await import(`../services/analytics/growthbook.js?real=${nonce}`), +} as typeof import('src/services/analytics/growthbook.js') +const realModel = { + ...await import(`./model/model.js?real=${nonce}`), +} as typeof import('./model/model.js') +const realProviders = { + ...await import(`./model/providers.js?real=${nonce}`), +} as typeof import('./model/providers.js') + const realAuth = { ...await import(`./auth.js?real=${nonce}`), } const originalEnv = { ...process.env } -let originalProvidersModule: ProvidersModule | undefined -let originalAxiosModule: AxiosModule | undefined +let originalProvidersModule: ProvidersModule | undefined = realProviders +let originalAxiosModule: AxiosModule | undefined = realAxios async function importFreshFastModeModule() { return import(`./fastMode.ts?ts=${Date.now()}-${Math.random()}`) diff --git a/src/utils/file.test.ts b/src/utils/file.test.ts index 097874efe2..510de2ed6f 100644 --- a/src/utils/file.test.ts +++ b/src/utils/file.test.ts @@ -3,7 +3,10 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as actualGrowthbook from '../services/analytics/growthbook.js' + +const actualGrowthbook = { + ...await import(`../services/analytics/growthbook.js?real=${Date.now()}-${Math.random()}`), +} async function importFileModuleWithKillswitchEnabled( killswitchEnabled: boolean, diff --git a/src/utils/mcpValidation.test.ts b/src/utils/mcpValidation.test.ts index c844569d9c..4ae5ac2bf7 100644 --- a/src/utils/mcpValidation.test.ts +++ b/src/utils/mcpValidation.test.ts @@ -1,9 +1,19 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' import { mcpContentNeedsTruncation, truncateMcpContent } from './mcpValidation.js' -import * as realGrowthbook from '../services/analytics/growthbook.js' -import * as realTokenEstimation from '../services/tokenEstimation.js' -import * as realImageResizer from './imageResizer.js' -import * as realLog from './log.js' + +const nonce = `${Date.now()}-${Math.random()}` +const realGrowthbook = { + ...await import(`../services/analytics/growthbook.js?real=${nonce}`), +} as typeof import('../services/analytics/growthbook.js') +const realTokenEstimation = { + ...await import(`../services/tokenEstimation.js?real=${nonce}`), +} as typeof import('../services/tokenEstimation.js') +const realImageResizer = { + ...await import(`./imageResizer.js?real=${nonce}`), +} as typeof import('./imageResizer.js') +const realLog = { + ...await import(`./log.js?real=${nonce}`), +} as typeof import('./log.js') // 60_000-char inputs: real roughTokenCountEstimation returns 15_000, which exceeds // DEFAULT_MAX_MCP_OUTPUT_TOKENS * MCP_TOKEN_COUNT_THRESHOLD_FACTOR (25_000 * 0.5 = 12_500), diff --git a/src/utils/model/modelOptions.catalogDedup.test.ts b/src/utils/model/modelOptions.catalogDedup.test.ts index 08616af917..5d71f0979c 100644 --- a/src/utils/model/modelOptions.catalogDedup.test.ts +++ b/src/utils/model/modelOptions.catalogDedup.test.ts @@ -8,41 +8,42 @@ import { resetSettingsCache, setSessionSettingsCache, } from '../settings/settingsCache.js' -import * as actualIndex from '../../integrations/index.js' -import * as actualProviderConfig from '../../services/api/providerConfig.js' -import * as actualProviderProfiles from '../providerProfiles.js' -import * as actualModel from './model.js' import type { ModelOption } from './modelOptions.js' const providerSpecifier = './providers.js' +const realNonce = `${Date.now()}-${Math.random()}` const realProvidersForCleanup = { ...await import( - `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + `${providerSpecifier}?real=${realNonce}` ), } +// Snapshot the real modules with cache-busted imports before any mock.module +// runs (same lesson as the 2026-04-30 mock-leak note in lessons_learned.md — bun +// live-repoints bare-import namespaces to the active mock, so these copies +// bypass earlier mocks and keep the genuine implementations). +const realIndex = { ...await import(`../../integrations/index.js?real=${realNonce}`) } +const realProviderConfig = { ...await import(`../../services/api/providerConfig.js?real=${realNonce}`) } +const realProviderProfiles = { ...await import(`../providerProfiles.js?real=${realNonce}`) } +const realProviders = realProvidersForCleanup +const realModel = { ...await import(`./model.js?real=${realNonce}`) } + afterAll(() => { mock.restore() mock.module(providerSpecifier, () => realProvidersForCleanup) + mock.module('../../integrations/index.js', () => realIndex) + mock.module('../../services/api/providerConfig.js', () => realProviderConfig) + mock.module('../providerProfiles.js', () => realProviderProfiles) + mock.module('./model.js', () => realModel) }) -// Snapshot the real modules before any mock.module runs (same lesson as the -// 2026-04-30 mock-leak note in lessons_learned.md — bun live-repoints the -// `actual*` namespaces to the active mock, so these copies are the stable -// handle on the genuine implementations). -const realIndex = { ...actualIndex } -const realProviderConfig = { ...actualProviderConfig } -const realProviderProfiles = { ...actualProviderProfiles } -const realProviders = realProvidersForCleanup -const realModel = { ...actualModel } - // bun's mock.module is process-wide and mock.restore() does NOT undo it, so // each mock installed here is gated and falls through to the real // implementation whenever its gate is null — a transparent passthrough for // every other suite in the same `bun test` run. let activeCatalogEntries: ModelCatalogEntry[] | null = null let activeCacheScopeOverride: string | null = null -let activeProfileOverride: Partial | null = null +let activeProfileOverride: Partial | null = null let activeOpenAIProvider = false let activeModelOverride: typeof realModel | null = null let modelMockRegistered = false diff --git a/src/utils/model/modelOptions.codexRecovery.test.ts b/src/utils/model/modelOptions.codexRecovery.test.ts index 00449014e4..56ef615e05 100644 --- a/src/utils/model/modelOptions.codexRecovery.test.ts +++ b/src/utils/model/modelOptions.codexRecovery.test.ts @@ -7,6 +7,11 @@ import { setSessionSettingsCache, } from '../settings/settingsCache.js' +const nonce = `${Date.now()}-${Math.random()}` +const realModel = { + ...await import(`./model.js?real=${nonce}`), +} as typeof import('./model.js') + const providerSpecifier = './providers.js' const realProvidersForCleanup = { ...await import( @@ -16,6 +21,7 @@ const realProvidersForCleanup = { afterAll(() => { mock.restore() + mock.module('./model.js', () => realModel) mock.module(providerSpecifier, () => realProvidersForCleanup) }) diff --git a/src/utils/model/modelOptions.crossProfile.test.ts b/src/utils/model/modelOptions.crossProfile.test.ts index cf1a4bd663..643b825f03 100644 --- a/src/utils/model/modelOptions.crossProfile.test.ts +++ b/src/utils/model/modelOptions.crossProfile.test.ts @@ -14,45 +14,46 @@ import { // loaded in the same `bun test` invocation. See `src/utils/user.test.ts` for // the canonical pattern; this is the same lesson as the 2026-04-30 mock-leak // note in lessons_learned.md. -import * as actualProviderProfiles from '../providerProfiles.js' -import * as actualAuth from '../auth.js' -import * as actualProviderConfig from '../../services/api/providerConfig.js' -import * as actualSettings from '../settings/settings.js' -import * as actualModelAllowlist from './modelAllowlist.js' -import * as actualOllamaModels from './ollamaModels.js' -import * as actualNvidiaModels from './nvidiaNimModels.js' -import * as actualMiniMaxModels from './minimaxModels.js' -import * as actualXiaomiModels from './xiaomi-mimoModels.js' import type { ModelOption } from './modelOptions.js' import type { ProviderProfile } from '../config.js' import type { SettingsJson } from '../settings/types.js' const providerSpecifier = './providers.js' +const realNonce = `${Date.now()}-${Math.random()}` const realProvidersForCleanup = { ...await import( - `${providerSpecifier}?real=${Date.now()}-${Math.random()}` + `${providerSpecifier}?real=${realNonce}` ), } +// Snapshot the real modules with cache-busted imports before any mock.module +// runs. bun live-repoints bare-import namespaces to the active mock, so these +// plain-object copies bypass earlier mocks and keep the genuine implementations. +const realProviderProfiles = { ...await import(`../providerProfiles.js?real=${realNonce}`) } +const realProviders = realProvidersForCleanup +const realAuth = { ...await import(`../auth.js?real=${realNonce}`) } +const realProviderConfig = { ...await import(`../../services/api/providerConfig.js?real=${realNonce}`) } +const realSettings = { ...await import(`../settings/settings.js?real=${realNonce}`) } +const realModelAllowlist = { ...await import(`./modelAllowlist.js?real=${realNonce}`) } +const realOllamaModels = { ...await import(`./ollamaModels.js?real=${realNonce}`) } +const realNvidiaModels = { ...await import(`./nvidiaNimModels.js?real=${realNonce}`) } +const realMiniMaxModels = { ...await import(`./minimaxModels.js?real=${realNonce}`) } +const realXiaomiModels = { ...await import(`./xiaomi-mimoModels.js?real=${realNonce}`) } + afterAll(() => { mock.restore() mock.module(providerSpecifier, () => realProvidersForCleanup) + mock.module('../providerProfiles.js', () => realProviderProfiles) + mock.module('../auth.js', () => realAuth) + mock.module('../../services/api/providerConfig.js', () => realProviderConfig) + mock.module('../settings/settings.js', () => realSettings) + mock.module('./modelAllowlist.js', () => realModelAllowlist) + mock.module('./ollamaModels.js', () => realOllamaModels) + mock.module('./nvidiaNimModels.js', () => realNvidiaModels) + mock.module('./minimaxModels.js', () => realMiniMaxModels) + mock.module('./xiaomi-mimoModels.js', () => realXiaomiModels) }) -// Snapshot the real modules before any mock.module runs. bun live-repoints the -// `actual*` namespaces to the active mock, so these plain-object copies are the -// stable handle on the genuine implementations. -const realProviderProfiles = { ...actualProviderProfiles } -const realProviders = realProvidersForCleanup -const realAuth = { ...actualAuth } -const realProviderConfig = { ...actualProviderConfig } -const realSettings = { ...actualSettings } -const realModelAllowlist = { ...actualModelAllowlist } -const realOllamaModels = { ...actualOllamaModels } -const realNvidiaModels = { ...actualNvidiaModels } -const realMiniMaxModels = { ...actualMiniMaxModels } -const realXiaomiModels = { ...actualXiaomiModels } - // bun's mock.module is process-wide and mock.restore() does NOT undo it, so // per-test mocks installed in a harness would persist and leak into later // suites (e.g. providerConfig's cache-scope tests reading a pinned @@ -61,7 +62,7 @@ const realXiaomiModels = { ...actualXiaomiModels } // actively running. afterEach clears the flag, so the persisted mock becomes a // transparent passthrough for every other file. Same pattern as the // cross-spawn/file-suggestions leak fix (#1667). -let activeProfilesOverride: Partial | null = null +let activeProfilesOverride: Partial | null = null // Overrides getAdditionalModelOptionsCacheScope (used by getModelOptions to pick // the openai-scope branch) only while a test sets it. Keeps the full // providerConfig surface so the persisted mock doesn't strip resolveProviderRequest @@ -225,7 +226,7 @@ function buildProviderProfileFixture( } async function importFreshModelOptionsModule( - providerProfilesMock: Partial, + providerProfilesMock: Partial, ) { activeProfilesOverride = providerProfilesMock const nonce = `${Date.now()}-${Math.random()}` diff --git a/src/utils/model/modelOptions.gateways.test.ts b/src/utils/model/modelOptions.gateways.test.ts index 284156c400..56e3426605 100644 --- a/src/utils/model/modelOptions.gateways.test.ts +++ b/src/utils/model/modelOptions.gateways.test.ts @@ -9,6 +9,11 @@ import { setSessionSettingsCache, } from '../settings/settingsCache.js' +const nonce = `${Date.now()}-${Math.random()}` +const realModel = { + ...await import(`./model.js?real=${nonce}`), +} as typeof import('./model.js') + const providerSpecifier = './providers.js' const realProvidersForCleanup = { ...await import( @@ -18,6 +23,7 @@ const realProvidersForCleanup = { afterAll(() => { mock.restore() + mock.module('./model.js', () => realModel) mock.module(providerSpecifier, () => realProvidersForCleanup) }) diff --git a/src/utils/modelCost.modelGate.test.ts b/src/utils/modelCost.modelGate.test.ts index 15718c82ec..87dbd0f591 100644 --- a/src/utils/modelCost.modelGate.test.ts +++ b/src/utils/modelCost.modelGate.test.ts @@ -13,12 +13,15 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as realFastMode from './fastMode.js' -import * as realModel from './model/model.js' import { resetSettingsCache } from './settings/settingsCache.js' -const realFastModeSnapshot = { ...realFastMode } -const realModelSnapshot = { ...realModel } +const nonce = `${Date.now()}-${Math.random()}` +const realFastModeSnapshot = { + ...await import(`./fastMode.js?real=${nonce}`), +} as typeof import('./fastMode.js') +const realModelSnapshot = { + ...await import(`./model/model.js?real=${nonce}`), +} as typeof import('./model/model.js') type PricingOverride = { inputTokens: number diff --git a/src/utils/permissions/dangerousModePromptRuntime.test.ts b/src/utils/permissions/dangerousModePromptRuntime.test.ts index 395bd532bc..821ba13164 100644 --- a/src/utils/permissions/dangerousModePromptRuntime.test.ts +++ b/src/utils/permissions/dangerousModePromptRuntime.test.ts @@ -1,7 +1,12 @@ import { afterEach, describe, expect, mock, test } from 'bun:test' +const realSettings = { + ...await import(`../settings/settings.js?real=${Date.now()}-${Math.random()}`), +} + afterEach(() => { mock.restore() + mock.module('../settings/settings.js', () => realSettings) }) describe('dangerousModePromptRuntime', () => { diff --git a/src/utils/plugins/lspRecommendation.test.ts b/src/utils/plugins/lspRecommendation.test.ts index 02c58ad4ab..99ed036dbe 100644 --- a/src/utils/plugins/lspRecommendation.test.ts +++ b/src/utils/plugins/lspRecommendation.test.ts @@ -4,6 +4,19 @@ import { releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' +const realMarketplaceManager = { + ...await import(`./marketplaceManager.js?real=${Date.now()}-${Math.random()}`), +} +const realBinaryCheck = { + ...await import(`../binaryCheck.js?real=${Date.now()}-${Math.random()}`), +} +const realInstalledPluginsManager = { + ...await import(`./installedPluginsManager.js?real=${Date.now()}-${Math.random()}`), +} +const realConfig = { + ...await import(`../config.js?real=${Date.now()}-${Math.random()}`), +} + type MarketplaceEntry = { name: string description?: string @@ -89,9 +102,6 @@ mock.module('./installedPluginsManager.js', () => ({ // any downstream test that re-imports the mocked path in the same Bun // process. The `?real=...` cache-bust ensures the import bypasses this // file's own mock.module() registration for the same path. -const realConfig = await import( - `../config.js?real=${Date.now()}-${Math.random()}` -) mock.module('../config.js', () => ({ ...realConfig, checkHasTrustDialogAccepted: () => true, @@ -154,6 +164,10 @@ listLspPluginCandidates = mod.listLspPluginCandidates afterAll(() => { try { mock.restore() + mock.module('./marketplaceManager.js', () => realMarketplaceManager) + mock.module('../binaryCheck.js', () => realBinaryCheck) + mock.module('./installedPluginsManager.js', () => realInstalledPluginsManager) + mock.module('../config.js', () => realConfig) } finally { releaseSharedMutationLock() } diff --git a/src/utils/plugins/officialMarketplaceStartupCheck.test.ts b/src/utils/plugins/officialMarketplaceStartupCheck.test.ts index 8a6a609f32..4cebb94790 100644 --- a/src/utils/plugins/officialMarketplaceStartupCheck.test.ts +++ b/src/utils/plugins/officialMarketplaceStartupCheck.test.ts @@ -4,6 +4,34 @@ import { releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' +const realGrowthbook = { + ...await import(`../../services/analytics/growthbook.js?real=${Date.now()}-${Math.random()}`), +} +const realAnalytics = { + ...await import(`../../services/analytics/index.js?real=${Date.now()}-${Math.random()}`), +} +const realConfig = { + ...await import(`../config.js?real=${Date.now()}-${Math.random()}`), +} +const realDebug = { + ...await import(`../debug.js?real=${Date.now()}-${Math.random()}`), +} +const realLog = { + ...await import(`../log.js?real=${Date.now()}-${Math.random()}`), +} +const realGitAvailability = { + ...await import(`./gitAvailability.js?real=${Date.now()}-${Math.random()}`), +} +const realMarketplaceHelpers = { + ...await import(`./marketplaceHelpers.js?real=${Date.now()}-${Math.random()}`), +} +const realMarketplaceManager = { + ...await import(`./marketplaceManager.js?real=${Date.now()}-${Math.random()}`), +} +const realOfficialMarketplaceGcs = { + ...await import(`./officialMarketplaceGcs.js?real=${Date.now()}-${Math.random()}`), +} + type TestGlobalConfig = { officialMarketplaceAutoInstallAttempted?: boolean officialMarketplaceAutoInstalled?: boolean @@ -38,9 +66,6 @@ const addMarketplaceSource = mock(async () => ({ await acquireSharedMutationLock('utils/plugins/officialMarketplaceStartupCheck.test.ts') -const realGrowthbook = await import( - `../../services/analytics/growthbook.js?real=${Date.now()}-${Math.random()}` -) mock.module('../../services/analytics/growthbook.js', () => ({ ...realGrowthbook, getFeatureValue_CACHED_MAY_BE_STALE: () => true, @@ -56,9 +81,6 @@ mock.module('../../services/analytics/index.js', () => ({ // any downstream test that re-imports the mocked path in the same Bun // process. The `?real=...` cache-bust ensures the import bypasses this // file's own mock.module() registration for the same path. -const realConfig = await import( - `../config.js?real=${Date.now()}-${Math.random()}` -) mock.module('../config.js', () => ({ ...realConfig, checkHasTrustDialogAccepted: () => true, @@ -92,9 +114,6 @@ mock.module('../config.js', () => ({ // downstream test that re-imports the mocked path in the same Bun // process. Pre-imported here (not inline) because mock.module's factory // is sync — see the growthbook pattern at line 41 above. -const realDebug = await import( - `../debug.js?real=${Date.now()}-${Math.random()}` -) mock.module('../debug.js', () => ({ ...realDebug, logForDebugging: mock(() => {}), @@ -103,9 +122,6 @@ mock.module('../debug.js', () => ({ // Spread the real log module to preserve all exports. The marketplace // module body transitively imports many of these; missing entries break // any downstream test that re-imports the mocked path. -const realLog = await import( - `../log.js?real=${Date.now()}-${Math.random()}` -) mock.module('../log.js', () => ({ ...realLog, logError: mock(() => {}), @@ -123,9 +139,6 @@ mock.module('./gitAvailability.js', () => ({ // process (e.g. marketplaceManager.test.ts running after this file). // Using `?real=...` cache-bust ensures the import bypasses this file's // own mock.module() registration for the same path. -const realMarketplaceHelpers = await import( - `./marketplaceHelpers.js?real=${Date.now()}-${Math.random()}` -) mock.module('./marketplaceHelpers.js', () => ({ ...realMarketplaceHelpers, isSourceAllowedByPolicy: () => true, @@ -156,6 +169,15 @@ checkAndInstallOfficialMarketplace = mod.checkAndInstallOfficialMarketplace afterAll(() => { try { mock.restore() + mock.module('../../services/analytics/growthbook.js', () => realGrowthbook) + mock.module('../../services/analytics/index.js', () => realAnalytics) + mock.module('../config.js', () => realConfig) + mock.module('../debug.js', () => realDebug) + mock.module('../log.js', () => realLog) + mock.module('./gitAvailability.js', () => realGitAvailability) + mock.module('./marketplaceHelpers.js', () => realMarketplaceHelpers) + mock.module('./marketplaceManager.js', () => realMarketplaceManager) + mock.module('./officialMarketplaceGcs.js', () => realOfficialMarketplaceGcs) } finally { releaseSharedMutationLock() } diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index 3f5a71374d..eeaa3ba44d 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -8,6 +8,11 @@ import { acquireEnvMutex, releaseEnvMutex } from '../entrypoints/sdk/shared.js' import { resolveRouteCredentialValue } from '../integrations/routeMetadata.js' import type { ProviderProfile } from './config.js' +const nonce = `${Date.now()}-${Math.random()}` +const realConfig = { + ...await import(`./config.js?real=${nonce}`), +} as typeof import('./config.js') + async function importFreshProvidersModule() { return import(`./model/providers.ts?ts=${Date.now()}-${Math.random()}`) } @@ -140,6 +145,7 @@ afterEach(() => { } mock.restore() + mock.module('./config.js', () => realConfig) globalThis.fetch = originalFetch mockConfigState = createMockConfigState() process.chdir(originalCwd) diff --git a/src/utils/status.test.ts b/src/utils/status.test.ts index 85cdfb5f5f..7e45753994 100644 --- a/src/utils/status.test.ts +++ b/src/utils/status.test.ts @@ -6,6 +6,11 @@ import { releaseSharedMutationLock, } from '../test/sharedMutationLock.js' +const nonce = `${Date.now()}-${Math.random()}` +const realMTLS = { + ...await import(`./mtls.js?real=${nonce}`), +} as typeof import('./mtls.js') + const realProvidersModule = await import( `./model/providers.js?real=${Date.now()}-${Math.random()}` ) @@ -86,6 +91,7 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + mock.module('./mtls.js', () => realMTLS) // mock.restore() does not unregister Bun's process-global module mocks. mock.module('./model/providers.js', () => realProviders) restoreEnv() diff --git a/src/utils/toolSearch.test.ts b/src/utils/toolSearch.test.ts index 00da01c2dc..17300d66a2 100644 --- a/src/utils/toolSearch.test.ts +++ b/src/utils/toolSearch.test.ts @@ -7,8 +7,13 @@ import { import { TaskCreateTool } from '../tools/TaskCreateTool/TaskCreateTool.js' import { ToolSearchTool } from '../tools/ToolSearchTool/ToolSearchTool.js' +const realGrowthbook = { + ...await import(`../services/analytics/growthbook.js?real=${Date.now()}-${Math.random()}`), +} + afterEach(() => { mock.restore() + mock.module('../services/analytics/growthbook.js', () => realGrowthbook) }) describe('resolveToolSearchMode', () => { diff --git a/src/utils/user.test.ts b/src/utils/user.test.ts index 3b040da62d..d9ff514764 100644 --- a/src/utils/user.test.ts +++ b/src/utils/user.test.ts @@ -1,13 +1,8 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' -import * as realExeca from 'execa' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as realAuth from './auth.js' -import * as realConfig from './config.js' -import * as realEnv from './env.js' -import * as realEnvUtils from './envUtils.js' // Snapshot each real module surface into a plain object BEFORE any // mock.module call. `import * as` gives a live namespace: mock.module @@ -15,11 +10,22 @@ import * as realEnvUtils from './envUtils.js' // rather than the real module -- permanently, since mock.module lasts for the // life of the process. That is how this suite's stderr-less `execa` stub used // to escape into every later suite, where `result.stderr.trim()` then threw. -const realAuthSnapshot = { ...realAuth } -const realConfigSnapshot = { ...realConfig } -const realEnvSnapshot = { ...realEnv } -const realEnvUtilsSnapshot = { ...realEnvUtils } -const realExecaSnapshot = { ...realExeca } +const nonce = `${Date.now()}-${Math.random()}` +const realAuthSnapshot = { + ...await import(`./auth.js?real=${nonce}`), +} as typeof import('./auth.js') +const realConfigSnapshot = { + ...await import(`./config.js?real=${nonce}`), +} as typeof import('./config.js') +const realEnvSnapshot = { + ...await import(`./env.js?real=${nonce}`), +} as typeof import('./env.js') +const realEnvUtilsSnapshot = { + ...await import(`./envUtils.js?real=${nonce}`), +} as typeof import('./envUtils.js') +const realExecaSnapshot = { + ...await import(`${import.meta.resolve('execa')}?real=${nonce}`), +} as typeof import('execa') const originalEnv = { ...process.env } const originalMacro = (globalThis as Record).MACRO From 357b8da08eb3623603e9bf44e8800dc649fb09e7 Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 18:01:26 -0700 Subject: [PATCH 036/121] test: restore module exports after mocks (TA-001 round 2) (#2343) --- src/ink/termio/osc.test.ts | 7 +++++++ src/services/tips/tipScheduler.test.ts | 6 +++++- src/tools/AgentTool/AgentTool.teammateModel.test.ts | 8 +++++--- src/tools/AgentTool/runAgent.routing.test.ts | 8 +++++--- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/ink/termio/osc.test.ts b/src/ink/termio/osc.test.ts index da55021aab..657756d730 100644 --- a/src/ink/termio/osc.test.ts +++ b/src/ink/termio/osc.test.ts @@ -5,6 +5,11 @@ import { releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' +const nonce = `${Date.now()}-${Math.random()}` +const actualExecFileNoThrow = { + ...await import(`../../utils/execFileNoThrow.js?real=${nonce}`), +} + const originalEnv = { ...process.env } const originalPlatform = process.platform const mockedClipboardPath = join(process.cwd(), 'openclaude-clipboard.txt') @@ -70,6 +75,7 @@ describe('Windows clipboard fallback', () => { afterEach(() => { try { mock.restore() + mock.module('../../utils/execFileNoThrow.js', () => actualExecFileNoThrow) process.env = { ...originalEnv } Object.defineProperty(process, 'platform', { value: originalPlatform }) } finally { @@ -125,6 +131,7 @@ describe('clipboard path behavior remains stable', () => { afterEach(() => { try { mock.restore() + mock.module('../../utils/execFileNoThrow.js', () => actualExecFileNoThrow) process.env = { ...originalEnv } Object.defineProperty(process, 'platform', { value: originalPlatform }) } finally { diff --git a/src/services/tips/tipScheduler.test.ts b/src/services/tips/tipScheduler.test.ts index f7c7d53711..8a096f7791 100644 --- a/src/services/tips/tipScheduler.test.ts +++ b/src/services/tips/tipScheduler.test.ts @@ -5,10 +5,14 @@ import { } from '../../test/sharedMutationLock.js' import * as actualAnalytics from '../analytics/index.js' import * as actualConfig from '../../utils/config.js' -import * as actualSettings from '../../utils/settings/settings.js' import type { Tip } from './types.js' import * as actualTipRegistry from './tipRegistry.js' +const nonce = `${Date.now()}-${Math.random()}` +const actualSettings = { + ...await import(`../../utils/settings/settings.js?real=${nonce}`), +} + const settingsRef: { value: { sponsoredTipsEnabled?: boolean diff --git a/src/tools/AgentTool/AgentTool.teammateModel.test.ts b/src/tools/AgentTool/AgentTool.teammateModel.test.ts index 4d7a472138..33967b7461 100644 --- a/src/tools/AgentTool/AgentTool.teammateModel.test.ts +++ b/src/tools/AgentTool/AgentTool.teammateModel.test.ts @@ -77,9 +77,11 @@ async function importActualModelAllowlist(): Promise { } async function importActualSettings(): Promise { - return import( - `../../utils/settings/settings.ts?agentToolActual=${Date.now()}-${Math.random()}` - ) + return { + ...await import( + `../../utils/settings/settings.ts?agentToolActual=${Date.now()}-${Math.random()}` + ), + } } async function importActualSpawnMultiAgent(): Promise { diff --git a/src/tools/AgentTool/runAgent.routing.test.ts b/src/tools/AgentTool/runAgent.routing.test.ts index becea0b18a..499be515dc 100644 --- a/src/tools/AgentTool/runAgent.routing.test.ts +++ b/src/tools/AgentTool/runAgent.routing.test.ts @@ -44,9 +44,11 @@ describe('runAgent provider routing', () => { beforeEach(async () => { await acquireSharedMutationLock('tools/AgentTool/runAgent.routing.test.ts') resetSettingsCache() - actualSettingsModule ??= await import( - `../../utils/settings/settings.ts?runAgentRoutingSettingsActual=${Date.now()}-${Math.random()}` - ) + actualSettingsModule ??= { + ...await import( + `../../utils/settings/settings.ts?runAgentRoutingSettingsActual=${Date.now()}-${Math.random()}` + ), + } settingsForTest = {} mock.module('../../utils/settings/settings.js', () => ({ ...actualSettingsModule!, From 11c6afe8d0fc5ba4c46d1cc388c02ee865a6da96 Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 18:06:42 -0700 Subject: [PATCH 037/121] test: isolate env, process and global state leaks across randomized seeds (TA-001 round 2 batch 2) (#2344) * test: isolate env, process and global state leaks across randomized seeds (TA-001 round 2 batch 2) * test: keep batch 2 state isolation from leaking into later files (TA-001 round 2 batch 2) Drop the standalone processExitCode helper self-test. Adding a discovered test file changed native shard assignments and seeded file order, exposing existing cross-file mock contamination. The existing finalizer and skills integration tests retain exit-code assertions and continue to exercise the helper. --- src/cli/bgFinalizer.test.ts | 11 +++++++-- src/cli/handlers/skills.test.ts | 17 +++++++++----- src/cli/handlers/skillsVerify.test.ts | 30 ++++++++++++++++--------- src/components/ThemePicker.test.tsx | 2 +- src/services/api/codexOAuth.test.ts | 2 ++ src/test/processExitCode.ts | 23 +++++++++++++++++++ src/utils/providerProfiles.test.ts | 1 + tests/sdk/sdk-context-isolation.test.ts | 23 +++++++++++++------ 8 files changed, 82 insertions(+), 27 deletions(-) create mode 100644 src/test/processExitCode.ts diff --git a/src/cli/bgFinalizer.test.ts b/src/cli/bgFinalizer.test.ts index 5c16796480..f7dc750006 100644 --- a/src/cli/bgFinalizer.test.ts +++ b/src/cli/bgFinalizer.test.ts @@ -6,6 +6,7 @@ import { tmpdir } from 'node:os' import { basename, join } from 'node:path' import { afterEach, beforeEach, describe, expect, it } from 'bun:test' import { isProcessRunning } from '../utils/genericProcessUtils.js' +import { isolateProcessExitCode } from '../test/processExitCode.js' import { BACKGROUND_SESSION_ID_ENV, BACKGROUND_SESSION_LAUNCHER_PID_ENV, @@ -37,16 +38,22 @@ const FIXTURE_OUTPUT_LIMIT_BYTES = 64 * 1024 describe('background session finalizer', () => { let configDir: string let sessionsRoot: string + let restoreProcess: () => void beforeEach(async () => { + restoreProcess = isolateProcessExitCode() configDir = await mkdtemp(join(tmpdir(), 'openclaude-bg-finalizer-')) sessionsRoot = join(configDir, 'bg-sessions') _setBackgroundSessionsRootForTesting(sessionsRoot) }) afterEach(async () => { - _setBackgroundSessionsRootForTesting(undefined) - await rm(configDir, { recursive: true, force: true }) + try { + _setBackgroundSessionsRootForTesting(undefined) + await rm(configDir, { recursive: true, force: true }) + } finally { + restoreProcess() + } }) function ownedSession(id: string, pid: number): BackgroundSession { diff --git a/src/cli/handlers/skills.test.ts b/src/cli/handlers/skills.test.ts index ff08a47841..c7895633df 100644 --- a/src/cli/handlers/skills.test.ts +++ b/src/cli/handlers/skills.test.ts @@ -13,6 +13,7 @@ import { import { tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'bun:test' +import { isolateProcessExitCode } from '../../test/processExitCode.js' import { acquireSharedMutationLock, @@ -227,16 +228,20 @@ async function withTempDir(fn: (tempDir: string) => Promise): Promise { // process so each fixture reads its own list, and hand it back afterwards. const inheritedRevocationsUrl = process.env.OPENCLAUDE_SKILLS_REVOCATIONS_URL delete process.env.OPENCLAUDE_SKILLS_REVOCATIONS_URL + const restoreProcess = isolateProcessExitCode(0) try { return await fn(tempDir) } finally { - process.exitCode = 0 - if (inheritedRevocationsUrl === undefined) { - delete process.env.OPENCLAUDE_SKILLS_REVOCATIONS_URL - } else { - process.env.OPENCLAUDE_SKILLS_REVOCATIONS_URL = inheritedRevocationsUrl + try { + if (inheritedRevocationsUrl === undefined) { + delete process.env.OPENCLAUDE_SKILLS_REVOCATIONS_URL + } else { + process.env.OPENCLAUDE_SKILLS_REVOCATIONS_URL = inheritedRevocationsUrl + } + rmSync(tempDir, { recursive: true, force: true }) + } finally { + restoreProcess() } - rmSync(tempDir, { recursive: true, force: true }) } } diff --git a/src/cli/handlers/skillsVerify.test.ts b/src/cli/handlers/skillsVerify.test.ts index b61e5930c6..ded1a8cefe 100644 --- a/src/cli/handlers/skillsVerify.test.ts +++ b/src/cli/handlers/skillsVerify.test.ts @@ -3,6 +3,7 @@ import { chmodSync, mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } import { tmpdir } from 'node:os' import { delimiter, join } from 'node:path' import { test } from 'bun:test' +import { isolateProcessExitCode } from '../../test/processExitCode.js' import { acquireSharedMutationLock, @@ -40,6 +41,7 @@ async function withVerifyFixture( }) => Promise, ): Promise { await acquireSharedMutationLock('skillsVerifyHandler') + const restoreProcess = isolateProcessExitCode(0) const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-skill-verify-test-')) const projectDir = join(tempDir, 'project') const projectSkills = join(projectDir, '.openclaude', 'skills') @@ -77,17 +79,23 @@ async function withVerifyFixture( try { await fn({ tempDir, projectDir, userSkills, projectSkills, registryDir, captured }) } finally { - console.log = saved.log - console.error = saved.error - setClaudeConfigHomeDirForTesting(undefined) - setEyebrowRunnerForTesting(undefined) - process.exitCode = 0 - restoreEnv('PATH', saved.path) - restoreEnv('OPENCLAUDE_EYEBROW_BIN', saved.bin) - restoreEnv('OPENCLAUDE_SKILLS_REVOCATIONS_URL', saved.revocationsUrl) - restoreEnv('OPENCLAUDE_SKILLS_REGISTRY_URL', saved.registryUrl) - rmSync(tempDir, { recursive: true, force: true }) - releaseSharedMutationLock() + try { + console.log = saved.log + console.error = saved.error + setClaudeConfigHomeDirForTesting(undefined) + setEyebrowRunnerForTesting(undefined) + restoreEnv('PATH', saved.path) + restoreEnv('OPENCLAUDE_EYEBROW_BIN', saved.bin) + restoreEnv('OPENCLAUDE_SKILLS_REVOCATIONS_URL', saved.revocationsUrl) + restoreEnv('OPENCLAUDE_SKILLS_REGISTRY_URL', saved.registryUrl) + rmSync(tempDir, { recursive: true, force: true }) + } finally { + try { + restoreProcess() + } finally { + releaseSharedMutationLock() + } + } } } diff --git a/src/components/ThemePicker.test.tsx b/src/components/ThemePicker.test.tsx index dd05ab0c4d..9f32fb1588 100644 --- a/src/components/ThemePicker.test.tsx +++ b/src/components/ThemePicker.test.tsx @@ -130,7 +130,7 @@ afterAll(() => { }) test('updates the preview when keyboard focus moves to another theme', async () => { - const { ThemePicker } = await import('./ThemePicker.js') + const { ThemePicker } = await import(`./ThemePicker.js?theme-picker=${Date.now()}-${Math.random()}`) const { stdout, stdin, getOutput } = createTestStreams() const root = await createRoot({ stdout: stdout as unknown as NodeJS.WriteStream, diff --git a/src/services/api/codexOAuth.test.ts b/src/services/api/codexOAuth.test.ts index 7990d31404..6bb852ae16 100644 --- a/src/services/api/codexOAuth.test.ts +++ b/src/services/api/codexOAuth.test.ts @@ -156,6 +156,8 @@ function createFakeAuthCodeListener(callbackPath: string): FakeAuthCodeListenerI async function acquireCodexOAuthTestIsolation(): Promise { const result = await acquireEnvMutex() expect(result.acquired).toBe(true) + nextFakePort = 41000 + fakeListenerInstance = null activeSnapshot = { fetch: globalThis.fetch, diff --git a/src/test/processExitCode.ts b/src/test/processExitCode.ts new file mode 100644 index 0000000000..82bcdc2b25 --- /dev/null +++ b/src/test/processExitCode.ts @@ -0,0 +1,23 @@ +export function isolateProcessExitCode(initialCode = process.exitCode): () => void { + const originalProcess = globalThis.process + let exitCode = initialCode + + globalThis.process = new Proxy(originalProcess, { + get(target, property) { + return property === 'exitCode' + ? exitCode + : Reflect.get(target, property, target) + }, + set(target, property, value) { + if (property === 'exitCode') { + exitCode = value + return true + } + return Reflect.set(target, property, value, target) + }, + }) + + return () => { + globalThis.process = originalProcess + } +} diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index eeaa3ba44d..8a855bd3e0 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -44,6 +44,7 @@ const RESTORED_KEYS = [ 'OPENAI_AUTH_HEADER_VALUE', 'OPENAI_API_KEYS', 'OPENAI_API_KEY', + 'FIREWORKS_API_KEY', 'GITHUB_COPILOT_KEY', 'GITHUB_ENTERPRISE_URL', 'CODEX_API_KEY', diff --git a/tests/sdk/sdk-context-isolation.test.ts b/tests/sdk/sdk-context-isolation.test.ts index 593175aed6..64453f684b 100644 --- a/tests/sdk/sdk-context-isolation.test.ts +++ b/tests/sdk/sdk-context-isolation.test.ts @@ -1,5 +1,11 @@ import { describe, test, expect, beforeEach, afterEach } from 'bun:test' import { + acquireSharedMutationLock, + releaseSharedMutationLock, +} from '../../src/test/sharedMutationLock.js' +import type { SessionId } from '../../src/entrypoints/agentSdkTypes.js' + +const { bindSdkContextToAsyncGenerator, runWithSdkContext, runOutsideSdkContext, @@ -12,12 +18,10 @@ import { getOriginalCwd, setOriginalCwd, getParentSessionId, -} from '../../src/bootstrap/state.js' -import { - acquireSharedMutationLock, - releaseSharedMutationLock, -} from '../../src/test/sharedMutationLock.js' -import type { SessionId } from '../../src/entrypoints/agentSdkTypes.js' + resetStateForTests, +} = await import( + `../../src/bootstrap/state.js?sdk-context-isolation=${Date.now()}-${Math.random()}` +) as typeof import('../../src/bootstrap/state.js') // Snapshot global state before each test so we can restore it let originalSessionId: SessionId @@ -28,6 +32,7 @@ let originalSessionProjectDir: string | null describe('SDK context isolation', () => { beforeEach(async () => { await acquireSharedMutationLock('sdk-context-isolation') + resetStateForTests() originalSessionId = getSessionId() originalCwd = getCwdState() originalOriginalCwd = getOriginalCwd() @@ -41,7 +46,11 @@ describe('SDK context isolation', () => { setCwdState(originalCwd) setOriginalCwd(originalOriginalCwd) } finally { - releaseSharedMutationLock() + try { + resetStateForTests() + } finally { + releaseSharedMutationLock() + } } }) From ada7f0fe383e9f9b7b4130a1626878928e76ce21 Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 22:22:59 -0700 Subject: [PATCH 038/121] test: clean up remaining randomized-order leaks (TA-001 round 2) (#2345) * test: clean up remaining randomized-order leaks (TA-001 round 2) * test: keep file suggestion abort test deterministic after module restore (TA-001 round 2) * test: bound lifecycle trace cleanup and avoid Bun isolate crash (TA-001 round 2) * test: reset lifecycle trace state without crashing isolated runs (TA-001 round 2) --- src/cli/handlers/xaiAuth.test.ts | 5 +++++ src/cli/transports/HybridTransport.test.ts | 10 +++++++++- ...tPlanModePermissionRequest.render.test.tsx | 19 ++++++++++++++++++- src/hooks/fileSuggestions.test.ts | 16 +++++++++++----- ...dTaskNavigation.interruptionTrace.test.tsx | 2 ++ src/services/api/claude.lifecycle.test.ts | 14 +++++++++++++- src/services/mcp/officialRegistry.test.ts | 8 +++++--- src/services/tips/tipScheduler.test.ts | 12 +++++++++--- .../ExitPlanModeV2Tool.test.ts | 7 +++++-- src/utils/plugins/marketplaceManager.test.ts | 10 ++++++++-- .../secureStorage/platformStorage.test.ts | 5 ++++- 11 files changed, 89 insertions(+), 19 deletions(-) diff --git a/src/cli/handlers/xaiAuth.test.ts b/src/cli/handlers/xaiAuth.test.ts index 9723efbc97..7f3ed1bcf1 100644 --- a/src/cli/handlers/xaiAuth.test.ts +++ b/src/cli/handlers/xaiAuth.test.ts @@ -3,6 +3,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'no import { tmpdir } from 'node:os' import { join } from 'node:path' +import { getCwdState, setCwdState } from '../../bootstrap/state.js' import { acquireSharedMutationLock, releaseSharedMutationLock, @@ -19,10 +20,13 @@ const SAVED_ENV = { let tempConfigDir = '' let tempCwd = '' +let originalSessionCwd: string const originalCwd = process.cwd() beforeEach(async () => { await acquireSharedMutationLock('cli/handlers/xaiAuth.test.ts') + // Initialize session state before lazy handler imports see the temporary cwd. + originalSessionCwd = getCwdState() tempConfigDir = mkdtempSync(join(tmpdir(), 'openclaude-xai-cli-config-')) tempCwd = mkdtempSync(join(tmpdir(), 'openclaude-xai-cli-cwd-')) process.chdir(tempCwd) @@ -39,6 +43,7 @@ afterEach(() => { try { setClaudeConfigHomeDirForTesting(undefined) process.chdir(originalCwd) + setCwdState(originalSessionCwd) rmSync(tempConfigDir, { recursive: true, force: true }) rmSync(tempCwd, { recursive: true, force: true }) if (SAVED_ENV.CLAUDE_CONFIG_DIR === undefined) { diff --git a/src/cli/transports/HybridTransport.test.ts b/src/cli/transports/HybridTransport.test.ts index 9148d3f850..a94f3a0c77 100644 --- a/src/cli/transports/HybridTransport.test.ts +++ b/src/cli/transports/HybridTransport.test.ts @@ -1,6 +1,14 @@ -import { afterEach, beforeEach, describe, expect, jest, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, describe, expect, jest, mock, test } from 'bun:test' import type { StdoutMessage } from 'src/entrypoints/sdk/controlTypes.js' +const realAxios = { + ...await import(`${import.meta.resolve('axios')}?real=${Date.now()}-${Math.random()}`), +} as typeof import('axios') + +afterAll(() => { + mock.module('axios', () => realAxios) +}) + type AxiosPost = ( url: string, data?: unknown, diff --git a/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.render.test.tsx b/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.render.test.tsx index 5cff8f15ff..28b9a7c0cd 100644 --- a/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.render.test.tsx +++ b/src/components/permissions/ExitPlanModePermissionRequest/ExitPlanModePermissionRequest.render.test.tsx @@ -22,6 +22,20 @@ import type { Notification } from '../../../context/notifications.js' import { getDefaultAppState } from '../../../state/AppStateStore.js' import { EXIT_PLAN_MODE_V2_TOOL_NAME } from '../../../tools/ExitPlanModeTool/constants.js' +const nonce = `${Date.now()}-${Math.random()}` +const realPlans = { + ...await import(`../../../utils/plans.js?real=${nonce}`), +} +const realPermissionRuleExplanation = { + ...await import(`../PermissionRuleExplanation.js?real=${nonce}`), +} +const realMarkdown = { + ...await import(`../../Markdown.js?real=${nonce}`), +} +const realNotifications = { + ...await import(`../../../context/notifications.js?real=${nonce}`), +} + // A real path whose parent directory does not exist, so writeFile rejects with // a genuine ENOENT — no fs/promises mock needed. const unwritablePlanPath = join( @@ -142,7 +156,6 @@ beforeEach(async () => { useNotifications: () => ({ addNotification, removeNotification: () => {} }), })) // V2 plan-file path points at a missing parent dir → real ENOENT on write. - const realPlans = await import('../../../utils/plans.ts') mock.module('../../../utils/plans.js', () => ({ ...realPlans, getPlanFilePath: () => unwritablePlanPath, @@ -154,6 +167,10 @@ beforeEach(async () => { afterEach(() => { try { mock.restore() + mock.module('../../../utils/plans.js', () => realPlans) + mock.module('../PermissionRuleExplanation.js', () => realPermissionRuleExplanation) + mock.module('../../Markdown.js', () => realMarkdown) + mock.module('src/context/notifications.js', () => realNotifications) } finally { releaseSharedMutationLock() } diff --git a/src/hooks/fileSuggestions.test.ts b/src/hooks/fileSuggestions.test.ts index d2ec8546aa..df2dfb8f57 100644 --- a/src/hooks/fileSuggestions.test.ts +++ b/src/hooks/fileSuggestions.test.ts @@ -188,16 +188,22 @@ function installFileSuggestionsDependencyMocks(options: LoadModuleOptions = {}): async function loadFileSuggestionsModule(options: LoadModuleOptions = {}) { activeSpawnScenario = options.spawnScenario - actualCrossSpawnModule ??= await import('cross-spawn') - actualRipgrepModule ??= await import('../utils/ripgrep.js') - actualFileIndexModule ??= await import('../native-ts/file-index/index.js') + const nonce = `${Date.now()}-${Math.random()}` + actualCrossSpawnModule ??= { + ...await import(`${import.meta.resolve('cross-spawn')}?real=${nonce}`), + } + actualRipgrepModule ??= { + ...await import(`../utils/ripgrep.js?real=${nonce}`), + } + actualFileIndexModule ??= { + ...await import(`../native-ts/file-index/index.js?real=${nonce}`), + } actualMarkdownConfigLoaderModule ??= await import( '../utils/markdownConfigLoader.js' ) installFileSuggestionsDependencyMocks(options) - const nonce = `${Date.now()}-${Math.random()}` const module = await import(`./fileSuggestions.ts?ts=${nonce}`) - restoreFileSuggestionsDependencyMocks() + // Restore in afterEach so live imports stay mocked while the test runs. return module } diff --git a/src/hooks/useBackgroundTaskNavigation.interruptionTrace.test.tsx b/src/hooks/useBackgroundTaskNavigation.interruptionTrace.test.tsx index 46b1a66212..5d8f5a4d63 100644 --- a/src/hooks/useBackgroundTaskNavigation.interruptionTrace.test.tsx +++ b/src/hooks/useBackgroundTaskNavigation.interruptionTrace.test.tsx @@ -22,6 +22,8 @@ beforeEach(async () => { await acquireSharedMutationLock( 'hooks/useBackgroundTaskNavigation.interruptionTrace.test.tsx', ) + // Earlier files can leave trace entries behind in non-isolated runs. + __resetInterruptionTraceForTests() }) afterEach(() => { diff --git a/src/services/api/claude.lifecycle.test.ts b/src/services/api/claude.lifecycle.test.ts index 61f12c72e7..4f7d215d02 100644 --- a/src/services/api/claude.lifecycle.test.ts +++ b/src/services/api/claude.lifecycle.test.ts @@ -22,6 +22,7 @@ import { QueryLifecycleOperationTracker } from '../../utils/queryLifecycle.js' import { __getInterruptionTraceSnapshotForTests, __resetInterruptionTraceForTests, + __waitForInterruptionTraceFlushForTests, } from '../../utils/interruptionTrace.js' import { asSystemPrompt } from '../../utils/systemPromptType.js' import { getClaudeAIOAuthTokens } from '../../utils/auth.js' @@ -509,8 +510,18 @@ beforeEach(async () => { getClaudeAIOAuthTokens.cache?.clear?.() }) -afterEach(() => { +afterEach(async () => { + let flushTimeout: ReturnType | undefined try { + // Match graceful shutdown's budget so a stuck append cannot hold the lock. + await Promise.race([ + __waitForInterruptionTraceFlushForTests(), + new Promise(resolve => { + flushTimeout = setTimeout(resolve, 500) + }), + ]) + // Trace-enabled cases reset their snapshots before use. Resetting here + // makes Bun 1.3.13 crash while running subsequent isolated test files. for (const key of envKeys) { if (originalEnv[key] === undefined) { delete process.env[key] @@ -540,6 +551,7 @@ afterEach(() => { fixturesRoot = undefined } } finally { + if (flushTimeout !== undefined) clearTimeout(flushTimeout) releaseSharedMutationLock() } }) diff --git a/src/services/mcp/officialRegistry.test.ts b/src/services/mcp/officialRegistry.test.ts index 261e5ec20f..05ebbb9d1f 100644 --- a/src/services/mcp/officialRegistry.test.ts +++ b/src/services/mcp/officialRegistry.test.ts @@ -34,9 +34,11 @@ afterEach(() => { }) async function importActualProviders(): Promise { - return import( - `../../utils/model/providers.ts?officialRegistryActual=${Date.now()}-${Math.random()}` - ) + return { + ...await import( + `../../utils/model/providers.ts?officialRegistryActual=${Date.now()}-${Math.random()}` + ), + } } async function mockApiProvider(provider: string): Promise { diff --git a/src/services/tips/tipScheduler.test.ts b/src/services/tips/tipScheduler.test.ts index 8a096f7791..fb54149a08 100644 --- a/src/services/tips/tipScheduler.test.ts +++ b/src/services/tips/tipScheduler.test.ts @@ -3,12 +3,18 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' -import * as actualAnalytics from '../analytics/index.js' -import * as actualConfig from '../../utils/config.js' import type { Tip } from './types.js' -import * as actualTipRegistry from './tipRegistry.js' const nonce = `${Date.now()}-${Math.random()}` +const actualAnalytics = { + ...await import(`../analytics/index.js?real=${nonce}`), +} +const actualConfig = { + ...await import(`../../utils/config.js?real=${nonce}`), +} +const actualTipRegistry = { + ...await import(`./tipRegistry.js?real=${nonce}`), +} const actualSettings = { ...await import(`../../utils/settings/settings.js?real=${nonce}`), } diff --git a/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.test.ts b/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.test.ts index 7db7c16508..c679cab1c3 100644 --- a/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.test.ts +++ b/src/tools/ExitPlanModeTool/ExitPlanModeV2Tool.test.ts @@ -35,8 +35,11 @@ beforeAll(async () => { 'tools/ExitPlanModeTool/ExitPlanModeV2Tool.test.ts', ) - actualPlans = await import('../../utils/plans.ts') - actualTeammateMailbox = await import('../../utils/teammateMailbox.ts') + const nonce = `${Date.now()}-${Math.random()}` + actualPlans = { ...await import(`../../utils/plans.js?real=${nonce}`) } + actualTeammateMailbox = { + ...await import(`../../utils/teammateMailbox.js?real=${nonce}`), + } mock.module('../../utils/plans.js', () => ({ ...actualPlans, diff --git a/src/utils/plugins/marketplaceManager.test.ts b/src/utils/plugins/marketplaceManager.test.ts index 98d41f33b5..95a9e14bb7 100644 --- a/src/utils/plugins/marketplaceManager.test.ts +++ b/src/utils/plugins/marketplaceManager.test.ts @@ -9,11 +9,9 @@ import { type Mock, test, } from 'bun:test' -import * as realAxios from 'axios' import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' -import * as realExecFileNoThrow from '../execFileNoThrow.js' import { getFsImplementation, NodeFsOperations, @@ -23,6 +21,14 @@ import { import type { MarketplaceSource } from './schemas.js' +const nonce = `${Date.now()}-${Math.random()}` +const realAxios = { + ...await import(`${import.meta.resolve('axios')}?real=${nonce}`), +} as typeof import('axios') +const realExecFileNoThrow = { + ...await import(`../execFileNoThrow.js?real=${nonce}`), +} as typeof import('../execFileNoThrow.js') + // @ts-expect-error -- query-string cache-buster: the import specifier ends with `?bust=...` // so TypeScript cannot resolve the bare path. The runtime import works under Bun (treats the // query string as a distinct module id that bypasses other test files' mock.module registrations). diff --git a/src/utils/secureStorage/platformStorage.test.ts b/src/utils/secureStorage/platformStorage.test.ts index 46f2eddc5e..cffdc065a0 100644 --- a/src/utils/secureStorage/platformStorage.test.ts +++ b/src/utils/secureStorage/platformStorage.test.ts @@ -1,5 +1,4 @@ import { expect, test, mock, describe, beforeEach, afterEach, afterAll, beforeAll } from "bun:test"; -import * as realExeca from "execa"; import { homedir } from "os"; import { join } from "path"; import { @@ -11,6 +10,10 @@ import type * as MacOsKeychainHelpers from "./macOsKeychainHelpers.js"; import type { linuxSecretStorage as LinuxSecretStorage } from "./linuxSecretStorage.js"; import type { windowsCredentialStorage as WindowsCredentialStorage } from "./windowsCredentialStorage.js"; +const realExeca = { + ...await import(`${import.meta.resolve("execa")}?real=${Date.now()}-${Math.random()}`), +} as typeof import("execa"); + type MockExecaOptions = { input?: string; reject?: boolean; From 4772c395b2b04c37e9be4f7bfdf1d70a60c38ee6 Mon Sep 17 00:00:00 2001 From: JATMN Date: Thu, 22 Oct 2026 23:32:21 -0700 Subject: [PATCH 039/121] test: run the unit suite without --isolate (TA-001 round 2) (#2346) * test: run the unit suite without --isolate (TA-001 round 2) * test: clean up leaks exposed by running without --isolate (TA-001 round 2) * test: clean up remaining leaks in shared-process runs (TA-001 round 2) * test: reset query engine trace state before each test (TA-001 round 2) --- CONTRIBUTING.md | 3 ++- scripts/run-tests.ts | 1 - src/QueryEngine.interruptionTrace.test.ts | 2 ++ .../setupGitHubActions.test.ts | 12 ++++++------ src/commands/replay/replay.test.tsx | 2 ++ .../api/promptCacheBreakDetection.test.ts | 18 ++++++++++++++++-- src/services/api/withRetry.test.ts | 9 ++++++--- src/services/tips/tipScheduler.test.ts | 1 + .../AgentTool/AgentTool.teammateModel.test.ts | 9 ++++++--- 9 files changed, 41 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a199e60540..9fd3b80a92 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -160,6 +160,7 @@ Dependency changes need a clear project benefit — fixing a bug, addressing a s - Preserve existing repo patterns unless the change is intentionally refactoring them. - Add or update tests when the change affects behavior. - For vendor/integration catalog tests, cover each vendor with one table-driven test of its registry contract (route resolution, default base URL and model, credential env vars, catalog-to-descriptor linkage, and capability/limit values the runtime uses), plus a small number of behavioral tests such as model-discovery keep/drop parsing or availability windows. Do not copy full curated model lists or cache/refresh settings from vendor definitions into tests. +- Test files share a process within each run or shard; restore `mock.module` registrations, environment variables, globals, and process state in `afterEach`/`afterAll`. - When tests set or delete environment variables, import `snapshotEnv` from `src/test/envSnapshot.ts` using the appropriate relative `.js` path. In `beforeEach`, save the named variables before changing them with `restoreEnv = snapshotEnv(keys)`; pass `{ clear: true }` when setup needs those variables unset. Call `restoreEnv()` in `afterEach`, or use `finally` for changes scoped to one test. Restoration preserves previous values, including empty strings, and deletes variables that were previously unset. List only the variables the test changes or deliberately clears; do not replace `process.env` or blanket-clear unrelated variables. When using the shared mutation lock, snapshot after acquiring it and restore before releasing it. - In timing-sensitive tests, drive timers with Bun's fake timers (`jest.useFakeTimers()` and `jest.advanceTimersByTime()`) instead of racing real delays, and restore real timers during cleanup, so the tests pass in a single-process whole-suite run as well as in the sharded runner. - Await completion promises or explicit readiness signals before asserting asynchronous results. For tests that deliberately suppress readiness, use a short, bounded wait and assert the expected failure. @@ -313,7 +314,7 @@ Notes on the local preflight: - PR Checks caches Bun package downloads using OS-scoped keys derived from `bun.lock` for root installs and `web/bun.lock` for web installs, with separate restore prefixes. Dependency installs still run with `--frozen-lockfile` on cache hits. Keep the relevant lockfile in sync with dependency changes and run the required local preflight regardless of cache status. - `bun run check` runs `check:static` (the any-usage budget, smoke build, and deadcode checks), then the full unit pass (`test:full`) — do not run those separately, or you execute work twice. -- `bun run test` isolates each test file's globals and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups. Use `bun run scripts/run-tests.ts --shards=N --shard-index=K` with the same test flags to run only shard K of N. `test:full` then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files to avoid Bun’s isolated-stdio pipe registration bug; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI runs `check:static` in `smoke-and-tests`, runs the eight unit shards as parallel `unit-tests` jobs, and runs each dedicated feature pass once in `dedicated-tests`. Each unit shard builds the CLI before testing. CI caps these jobs at 15 minutes each and the other jobs at five minutes. +- `bun run test` runs test files in a shared process and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups. Use `bun run scripts/run-tests.ts --shards=N --shard-index=K` with the same test flags to run only shard K of N. `test:full` then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI runs `check:static` in `smoke-and-tests`, runs the eight unit shards as parallel `unit-tests` jobs, and runs each dedicated feature pass once in `dedicated-tests`. Each unit shard builds the CLI before testing. CI caps these jobs at 15 minutes each and the other jobs at five minutes. - Fetching upstream `main` by URL avoids assuming that a fork checkout's `origin` points at Twigpine/openclaude. `FETCH_HEAD` is the fetched upstream tip. The scan deliberately uses local `HEAD` so it includes commits that have not been pushed yet; CI uses the pushed PR head SHA after the push. - The web CI job runs on PRs that change `web/**`, root `package.json` or `bun.lock`, `.bun-version`, or `.github/workflows/pr-checks.yml`, and on every push to `main`. Contributors do not need to run the web suite locally for changes that cannot affect the site. - This preflight covers the same command families as CI, but it does not reproduce CI exactly in one shell. When selected, CI runs static checks with Node 22 available for smoke checks, runs the unit shards and dedicated feature passes under Node 22, separately builds and checks launcher compatibility under Node 22 and 24.11.x, separately builds and launches under exact Node 22.0.0, and executes every job on a clean runner. diff --git a/scripts/run-tests.ts b/scripts/run-tests.ts index 9dac73fa37..90c1ceb35a 100644 --- a/scripts/run-tests.ts +++ b/scripts/run-tests.ts @@ -61,7 +61,6 @@ try { const child = Bun.spawn([ process.execPath, 'test', - '--isolate', ...testArgs, ...(shards > 1 ? [`--shard=${shard}/${shards}`] : []), '--reporter=junit', diff --git a/src/QueryEngine.interruptionTrace.test.ts b/src/QueryEngine.interruptionTrace.test.ts index 5e4a231f96..376cc1af97 100644 --- a/src/QueryEngine.interruptionTrace.test.ts +++ b/src/QueryEngine.interruptionTrace.test.ts @@ -15,6 +15,8 @@ const originalTrace = process.env.OPENCLAUDE_INTERRUPT_TRACE beforeEach(async () => { await acquireSharedMutationLock('QueryEngine.interruptionTrace.test.ts') + // Earlier files can leave trace entries behind in non-isolated runs. + __resetInterruptionTraceForTests() }) afterEach(async () => { diff --git a/src/commands/install-github-app/setupGitHubActions.test.ts b/src/commands/install-github-app/setupGitHubActions.test.ts index 212a411adb..15e2fb4a67 100644 --- a/src/commands/install-github-app/setupGitHubActions.test.ts +++ b/src/commands/install-github-app/setupGitHubActions.test.ts @@ -43,15 +43,15 @@ async function importRealModules(): Promise { const cacheKey = `${Date.now()}-${Math.random()}` realModules = { - browser: (await import( - `../../utils/browser.ts?setup-actions-real-${cacheKey}` - )) as BrowserModule, + browser: { + ...await import(`../../utils/browser.ts?setup-actions-real-${cacheKey}`), + } as BrowserModule, config: { ...await import(`../../utils/config.js?real=${cacheKey}`), } as ConfigModule, - execFileNoThrow: (await import( - `../../utils/execFileNoThrow.ts?setup-actions-real-${cacheKey}` - )) as ExecFileNoThrowModule, + execFileNoThrow: { + ...await import(`../../utils/execFileNoThrow.ts?setup-actions-real-${cacheKey}`), + } as ExecFileNoThrowModule, } return realModules } diff --git a/src/commands/replay/replay.test.tsx b/src/commands/replay/replay.test.tsx index af55570e35..8c1a4093fe 100644 --- a/src/commands/replay/replay.test.tsx +++ b/src/commands/replay/replay.test.tsx @@ -146,6 +146,7 @@ describe('/replay command', () => { loadSameRepoMessageLogs: loadSameRepoMessageLogsMock, })) mock.module('../../utils/replayIndex.js', () => ({ + ...realReplayIndex, loadReplayIndex: loadReplayIndexMock, })) mock.module('../../hooks/useTerminalSize.js', () => ({ @@ -155,6 +156,7 @@ describe('/replay command', () => { useIsInsideModal: () => false, })) mock.module('../../components/LogSelector.js', () => ({ + ...realLogSelector, LogSelector: (props: LogSelectorProps) => { lastLogSelectorProps = props return {`LogSelector:${props.logs.length}`} diff --git a/src/services/api/promptCacheBreakDetection.test.ts b/src/services/api/promptCacheBreakDetection.test.ts index 5ad9423e3e..0a98927dd8 100644 --- a/src/services/api/promptCacheBreakDetection.test.ts +++ b/src/services/api/promptCacheBreakDetection.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' import type { BetaToolUnion } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' import type { TextBlockParam } from '@anthropic-ai/sdk/resources/index.mjs' import type { QuerySource } from '../../constants/querySource.js' @@ -30,9 +30,23 @@ const logForDebuggingMock = mock( debugCalls.push({ message, options }) }, ) -const actualDebugModule = await import('../../utils/debug.js') +const nonce = `${Date.now()}-${Math.random()}` +const actualDebugModule = { + ...await import(`../../utils/debug.js?real=${nonce}`), +} +const actualAnalyticsModule = { + ...await import(`../analytics/index.js?real=${nonce}`), +} + +afterAll(() => { + mock.restore() + mock.module('../analytics/index.js', () => actualAnalyticsModule) + mock.module('src/utils/debug.js', () => actualDebugModule) + mock.module('../../utils/debug.js', () => actualDebugModule) +}) mock.module('../analytics/index.js', () => ({ + ...actualAnalyticsModule, logEvent: logEventMock, })) diff --git a/src/services/api/withRetry.test.ts b/src/services/api/withRetry.test.ts index 66f080aa25..b1059d329e 100644 --- a/src/services/api/withRetry.test.ts +++ b/src/services/api/withRetry.test.ts @@ -70,9 +70,12 @@ afterEach(() => { }) async function importActualProviders(): Promise { - return import( - `../../utils/model/providers.ts?withRetryActual=${Date.now()}-${Math.random()}` - ) + // Bun repoints module namespaces when mocked; keep the original exports. + return { + ...await import( + `../../utils/model/providers.ts?withRetryActual=${Date.now()}-${Math.random()}` + ), + } } async function importFreshWithRetryModule( diff --git a/src/services/tips/tipScheduler.test.ts b/src/services/tips/tipScheduler.test.ts index fb54149a08..0f13e17fab 100644 --- a/src/services/tips/tipScheduler.test.ts +++ b/src/services/tips/tipScheduler.test.ts @@ -53,6 +53,7 @@ mock.module('../../utils/config.js', () => ({ })) mock.module('./tipRegistry.js', () => ({ + ...actualTipRegistry, getRelevantTips: async () => relevantTipsRef.value, })) diff --git a/src/tools/AgentTool/AgentTool.teammateModel.test.ts b/src/tools/AgentTool/AgentTool.teammateModel.test.ts index 33967b7461..52545012ab 100644 --- a/src/tools/AgentTool/AgentTool.teammateModel.test.ts +++ b/src/tools/AgentTool/AgentTool.teammateModel.test.ts @@ -71,9 +71,12 @@ function restoreEnv(key: keyof typeof originalEnv): void { } async function importActualModelAllowlist(): Promise { - return import( - `../../utils/model/modelAllowlist.ts?agentToolActual=${Date.now()}-${Math.random()}` - ) + // Bun repoints module namespaces when mocked; keep the original exports. + return { + ...await import( + `../../utils/model/modelAllowlist.ts?agentToolActual=${Date.now()}-${Math.random()}` + ), + } } async function importActualSettings(): Promise { From 804e331c53007e66ba2c5cc7a2a2acb25ff59fd4 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 05:05:50 -0700 Subject: [PATCH 040/121] test: keep gitSettings tests independent of the temp dir location (#2347) --- src/utils/gitSettings.test.ts | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/src/utils/gitSettings.test.ts b/src/utils/gitSettings.test.ts index 07ae574eda..f68c50fe4b 100644 --- a/src/utils/gitSettings.test.ts +++ b/src/utils/gitSettings.test.ts @@ -1,8 +1,10 @@ -import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test' +import * as fs from 'fs' import { mkdirSync, mkdtempSync, rmSync } from 'fs' import { tmpdir } from 'os' -import { join } from 'path' +import { dirname, join } from 'path' import { getCwdState, setCwdState } from '../bootstrap/state.js' +import { snapshotEnv } from '../test/envSnapshot.js' import { shouldIncludeGitInstructions } from './gitSettings.js' const realSettings = (await import( @@ -10,15 +12,28 @@ const realSettings = (await import( )) as typeof import('./settings/settings.js') let originalCwdState: string -let originalEnv: string | undefined +let restoreEnv: () => void let tempRoot: string let settingsFixture: Record = {} beforeEach(() => { originalCwdState = getCwdState() - originalEnv = process.env.CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS - delete process.env.CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS + restoreEnv = snapshotEnv(['CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS'], { clear: true }) tempRoot = mkdtempSync(join(tmpdir(), 'gitsettings-test-')) + // findGitRoot walks the filesystem itself, ignoring GIT_CEILING_DIRECTORIES. + // Hide ancestor repositories when tmpdir() happens to be inside a checkout. + const ancestorGitPaths = new Set() + for (let ancestor = dirname(tempRoot); ; ancestor = dirname(ancestor)) { + ancestorGitPaths.add(join(ancestor, '.git')) + if (dirname(ancestor) === ancestor) break + } + const statSync = fs.statSync + spyOn(fs, 'statSync').mockImplementation(((path, options) => { + if (ancestorGitPaths.has(String(path))) { + throw new Error('Outside the test fixture') + } + return statSync(path, options) + }) as typeof fs.statSync) settingsFixture = {} mock.module('./settings/settings.js', () => ({ ...realSettings, @@ -28,11 +43,7 @@ beforeEach(() => { afterEach(() => { setCwdState(originalCwdState) - if (originalEnv === undefined) { - delete process.env.CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS - } else { - process.env.CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS = originalEnv - } + restoreEnv() rmSync(tempRoot, { recursive: true, force: true }) mock.module('./settings/settings.js', () => ({ ...realSettings })) mock.restore() From 36b7ffd69aca678d0dbe1f6a4c67d6f8a914837e Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 05:08:10 -0700 Subject: [PATCH 041/121] build: prepare tsconfig and relay types for TypeScript 6 (#2348) --- src/upstreamproxy/relay.ts | 2 +- tsconfig.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/upstreamproxy/relay.ts b/src/upstreamproxy/relay.ts index cd17f73291..837f06a1ae 100644 --- a/src/upstreamproxy/relay.ts +++ b/src/upstreamproxy/relay.ts @@ -63,7 +63,7 @@ const PING_INTERVAL_MS = 30_000 * protobufjs would be the general answer; for a single-field bytes message * the hand encoding is 10 lines and avoids a runtime dep in the hot path. */ -export function encodeChunk(data: Uint8Array): Uint8Array { +export function encodeChunk(data: Uint8Array): Uint8Array { const len = data.length // varint encoding of length — most chunks fit in 1–3 length bytes const varint: number[] = [] diff --git a/tsconfig.json b/tsconfig.json index eb9ed65a5d..6a95a78e57 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,7 @@ "declaration": false, "outDir": "./dist", "rootDir": "./src", - "baseUrl": ".", + "types": ["bun", "node"], "paths": { "src/*": ["./src/*"] } From 2e415e79554ec562ffe8c6f3396904a171a31155 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 05:46:15 -0700 Subject: [PATCH 042/121] test: clear remembered provider routing after CLI tests (#2349) --- src/entrypoints/cli.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/entrypoints/cli.test.ts b/src/entrypoints/cli.test.ts index d980ec3ec5..d901360bfe 100644 --- a/src/entrypoints/cli.test.ts +++ b/src/entrypoints/cli.test.ts @@ -487,6 +487,8 @@ describe('cli.tsx — background routing behavior', () => { afterEach(() => { process.argv = [...savedArgv] + // Later SDK initialization must not replay a test's provider selection. + clearRememberedProviderFlagForTests() }) it('dispatches background management commands before startup work', async () => { From 51226f0c8969d7a5c801e310648e2c7b1d5a0482 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 06:08:50 -0700 Subject: [PATCH 043/121] test: reset teammate interruption trace state (#2350) --- src/utils/swarm/spawnInProcess.interruptionTrace.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/utils/swarm/spawnInProcess.interruptionTrace.test.ts b/src/utils/swarm/spawnInProcess.interruptionTrace.test.ts index 0b4a72a68d..1d4f46ecd6 100644 --- a/src/utils/swarm/spawnInProcess.interruptionTrace.test.ts +++ b/src/utils/swarm/spawnInProcess.interruptionTrace.test.ts @@ -18,6 +18,8 @@ beforeEach(async () => { await acquireSharedMutationLock( 'utils/swarm/spawnInProcess.interruptionTrace.test.ts', ) + // Earlier files can leave trace entries behind in non-isolated runs. + __resetInterruptionTraceForTests() }) afterEach(() => { From 7141ba29f3b85769491da65ab42567a1cee1d1d1 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 08:28:24 -0700 Subject: [PATCH 044/121] test: restore registry after custom gateway tests (#2352) --- src/integrations/gateways/custom.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/integrations/gateways/custom.test.ts b/src/integrations/gateways/custom.test.ts index 16b2111efa..688e8c35b8 100644 --- a/src/integrations/gateways/custom.test.ts +++ b/src/integrations/gateways/custom.test.ts @@ -13,6 +13,7 @@ describe('custom gateway', () => { afterEach(() => { _clearRegistryForTesting() + ensureIntegrationsLoaded() }) describe.each([ From 40297b49792f4b4cbde34d9b3b08d56fcf0e92c4 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 09:22:42 -0700 Subject: [PATCH 045/121] chore: upgrade Bun to 1.4.4 (#2351) * chore: upgrade Bun to 1.4.3 * chore: upgrade Bun to 1.4.4 --- .bun-version | 2 +- ANDROID_INSTALL.md | 7 +++++-- CONTRIBUTING.md | 2 ++ README.md | 2 +- bun.lock | 6 +++--- docs/advanced-setup.md | 2 +- package.json | 2 +- 7 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.bun-version b/.bun-version index 7962dcfdb6..1c99cf0e80 100644 --- a/.bun-version +++ b/.bun-version @@ -1 +1 @@ -1.3.13 +1.4.4 diff --git a/ANDROID_INSTALL.md b/ANDROID_INSTALL.md index 2e77fb8cea..357f19eab3 100644 --- a/ANDROID_INSTALL.md +++ b/ANDROID_INSTALL.md @@ -60,9 +60,12 @@ This downloads ~200–400MB. Wait for it to complete. ```bash proot-distro login ubuntu -curl -fsSL https://bun.sh/install | bash +bun_installer="$(mktemp)" +curl -fsSL https://bun.sh/install -o "$bun_installer" +bash "$bun_installer" bun-v1.4.4 +rm -f "$bun_installer" source ~/.bashrc -bun --version # should show 1.3.13+ +bun --version # should show 1.4.4 ``` ### Step 6 — Build OpenClaude diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9fd3b80a92..2236d182b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -211,6 +211,8 @@ When submitting provider changes: ## Local Setup +Use Node.js `>=22.0.0` and Bun `1.4.4` (tracked in `.bun-version`). + Install dependencies: ```bash diff --git a/README.md b/README.md index 29fdd50470..e3dad94f35 100644 --- a/README.md +++ b/README.md @@ -489,7 +489,7 @@ and client generation from `src/proto/openclaude.proto`. ## Development -Use Node.js `>=22.0.0` and Bun `1.3.13` or newer for source builds. +Use Node.js `>=22.0.0` and Bun `1.4.4` for source builds. ```bash bun install diff --git a/bun.lock b/bun.lock index 8785f5262d..cda29e8466 100644 --- a/bun.lock +++ b/bun.lock @@ -26,7 +26,7 @@ "@sentry/node": "^10.70.0", "@smithy/core": "3.24.3", "@smithy/node-http-handler": "4.7.3", - "@types/bun": "1.3.11", + "@types/bun": "1.4.4", "@types/node": "25.5.0", "@types/react": "19.2.14", "@typescript-eslint/eslint-plugin": "^8.69.0", @@ -533,7 +533,7 @@ "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], - "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + "@types/bun": ["@types/bun@1.4.4", "", { "dependencies": { "bun-types": "1.4.4" } }, "sha512-9wkRDXUXK124pEnAutE6n/l18j+i9GC5IAHK8TmX/2nlySETmky0OU6yL5U8pW1AoKh++AN9auDCr647BfurjQ=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], @@ -637,7 +637,7 @@ "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], - "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + "bun-types": ["bun-types@1.4.4", "", { "dependencies": { "@types/node": "*", "undici-types": "*" } }, "sha512-yjrJh5oGX7mMPKGd19YXyIjmEuj4G4zNC30bs7PM9Az7NQ3ndF3j6hbeRdvJTRQEhJ/FYQwW0a2moweEi2hMxQ=="], "bundle-name": ["bundle-name@4.1.0", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="], diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 6d23c8622d..56781de038 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -15,7 +15,7 @@ npm install -g @twigpine/openclaude@latest ### Option B: From source with Bun -Use Bun `1.3.13` or newer for source builds. Older Bun versions can fail during `bun run build`. +Use Bun `1.4.4` for source builds. Older Bun versions can fail during `bun run build`. ```bash git clone https://github.com/Twigpine/openclaude.git diff --git a/package.json b/package.json index 09a6c8e607..ac7975635e 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "@sentry/node": "^10.70.0", "@smithy/core": "3.24.3", "@smithy/node-http-handler": "4.7.3", - "@types/bun": "1.3.11", + "@types/bun": "1.4.4", "@types/node": "25.5.0", "@types/react": "19.2.14", "@typescript-eslint/eslint-plugin": "^8.69.0", From 0876fb2de0ffe8321298bcdee215ee7739c67ec8 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 10:00:10 -0700 Subject: [PATCH 046/121] chore: upgrade TypeScript to 6.0.3 (#2353) --- bun.lock | 4 ++-- package.json | 2 +- web/bun.lock | 4 ++-- web/package.json | 2 +- web/src/env.d.ts | 2 ++ 5 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 web/src/env.d.ts diff --git a/bun.lock b/bun.lock index cda29e8466..95fb5a32f4 100644 --- a/bun.lock +++ b/bun.lock @@ -79,7 +79,7 @@ "tree-sitter-wasms": "0.1.13", "turndown": "7.2.2", "type-fest": "4.41.0", - "typescript": "5.9.3", + "typescript": "6.0.3", "undici": "7.28.0", "usehooks-ts": "3.1.1", "vscode-languageserver-protocol": "3.17.5", @@ -1213,7 +1213,7 @@ "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "unbash": ["unbash@3.0.0", "", {}, "sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA=="], diff --git a/package.json b/package.json index ac7975635e..20f75b55d6 100644 --- a/package.json +++ b/package.json @@ -158,7 +158,7 @@ "tree-sitter-wasms": "0.1.13", "turndown": "7.2.2", "type-fest": "4.41.0", - "typescript": "5.9.3", + "typescript": "6.0.3", "undici": "7.28.0", "usehooks-ts": "3.1.1", "vscode-languageserver-protocol": "3.17.5", diff --git a/web/bun.lock b/web/bun.lock index 86d0c4d0aa..3869f0a101 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -9,7 +9,7 @@ "@astrojs/sitemap": "3.7.3", "@fontsource-variable/geist-mono": "^5.2.5", "astro": "6.4.6", - "typescript": "^5.8.3", + "typescript": "^6.0.3", }, }, }, @@ -704,7 +704,7 @@ "typesafe-path": ["typesafe-path@0.2.2", "", {}, "sha512-OJabfkAg1WLZSqJAJ0Z6Sdt3utnbzr/jh+NAHoyWHJe8CMSy79Gm085094M9nvTPy22KzTVn5Zq5mbapCI/hPA=="], - "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "typescript-auto-import-cache": ["typescript-auto-import-cache@0.3.6", "", { "dependencies": { "semver": "^7.3.8" } }, "sha512-RpuHXrknHdVdK7wv/8ug3Fr0WNsNi5l5aB8MYYuXhq2UH5lnEB1htJ1smhtD5VeCsGr2p8mUDtd83LCQDFVgjQ=="], diff --git a/web/package.json b/web/package.json index 0b1e6e5c66..2ba002d2f3 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,6 @@ "@astrojs/sitemap": "3.7.3", "@fontsource-variable/geist-mono": "^5.2.5", "astro": "6.4.6", - "typescript": "^5.8.3" + "typescript": "^6.0.3" } } diff --git a/web/src/env.d.ts b/web/src/env.d.ts new file mode 100644 index 0000000000..9fa0dc33e7 --- /dev/null +++ b/web/src/env.d.ts @@ -0,0 +1,2 @@ +// The package exports CSS without type declarations for its side-effect import. +declare module '@fontsource-variable/geist-mono' {} From 03652ca8a24bd21e224f786b2f937d4b55a8dcd7 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 10:58:25 -0700 Subject: [PATCH 047/121] test: make background termination race deterministic (#2355) --- src/cli/bgRegistry.test.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/cli/bgRegistry.test.ts b/src/cli/bgRegistry.test.ts index f7d67b7e9c..3f6a523d84 100644 --- a/src/cli/bgRegistry.test.ts +++ b/src/cli/bgRegistry.test.ts @@ -2527,14 +2527,20 @@ describe('background session registry', () => { sessionId: 'conversation-kill-natural-race', }) - await Promise.all([ - recordBackgroundSessionNaturalTermination( - 'bg-kill-natural-race', - { exitCode: 0 }, - { ownerPid: 345 }, - ), - markBackgroundSessionKilled('bg-kill-natural-race'), - ]) + // Let kill observe a running session, then finish naturally before its write. + // Force this interleaving without depending on lock retry timing. + await markBackgroundSessionKilled('bg-kill-natural-race', { + _beforeMarkWriteForTesting: async session => { + expect(session.status).toBe('running') + expect( + await recordBackgroundSessionNaturalTermination( + session.id, + { exitCode: 0 }, + { ownerPid: session.pid }, + ), + ).toMatchObject({ status: 'exited', exitCode: 0 }) + }, + }) expect(await resolveBackgroundSession('bg-kill-natural-race')).toMatchObject( { From 5840d73b2a4e673b975cbb616d66684505b4bf46 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 11:14:35 -0700 Subject: [PATCH 048/121] test: restore diagnostic mocks and reset trace state (#2354) --- .../lsp/LSPDiagnosticRegistry.test.ts | 17 ++++++-- ...mmateViewHelpers.interruptionTrace.test.ts | 11 +++-- src/utils/attachments.lspDiagnostics.test.ts | 40 +++++++++---------- 3 files changed, 37 insertions(+), 31 deletions(-) diff --git a/src/services/lsp/LSPDiagnosticRegistry.test.ts b/src/services/lsp/LSPDiagnosticRegistry.test.ts index 97fe882ad0..4bcd05803f 100644 --- a/src/services/lsp/LSPDiagnosticRegistry.test.ts +++ b/src/services/lsp/LSPDiagnosticRegistry.test.ts @@ -1,11 +1,19 @@ -import { beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterAll, beforeEach, describe, expect, mock, test } from 'bun:test' import type { Diagnostic, DiagnosticFile } from '../diagnosticTracking.js' const debugMessages: string[] = [] -const realDebugModule = await import( - `../../utils/debug.js?real=${Date.now()}-${Math.random()}`, -) +const realDebugModule = { + ...await import(`../../utils/debug.js?real=${Date.now()}-${Math.random()}`), +} +const realSlowOperations = { + ...await import(`../../utils/slowOperations.js?real=${Date.now()}-${Math.random()}`), +} + +afterAll(() => { + mock.module('../../utils/debug.js', () => realDebugModule) + mock.module('../../utils/slowOperations.js', () => realSlowOperations) +}) mock.module('../../utils/debug.js', () => ({ ...realDebugModule, @@ -16,6 +24,7 @@ mock.module('../../utils/debug.js', () => ({ // Other tests mock slowOperations process-wide; restore the real serializer so // diagnostic keys keep message/range/code entropy under full-suite ordering. mock.module('../../utils/slowOperations.js', () => ({ + ...realSlowOperations, jsonStringify: JSON.stringify, })) diff --git a/src/state/teammateViewHelpers.interruptionTrace.test.ts b/src/state/teammateViewHelpers.interruptionTrace.test.ts index 928635ac4c..7c1d9a9939 100644 --- a/src/state/teammateViewHelpers.interruptionTrace.test.ts +++ b/src/state/teammateViewHelpers.interruptionTrace.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, expect, test } from 'bun:test' +import { snapshotEnv } from '../test/envSnapshot.js' import type { AppState } from './AppState.js' import { getDefaultAppState } from './AppStateStore.js' import { stopOrDismissAgent } from './teammateViewHelpers.js' @@ -12,22 +13,20 @@ import { releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -const originalTrace = process.env.OPENCLAUDE_INTERRUPT_TRACE +let restoreEnv: () => void beforeEach(async () => { await acquireSharedMutationLock( 'state/teammateViewHelpers.interruptionTrace.test.ts', ) + restoreEnv = snapshotEnv(['OPENCLAUDE_INTERRUPT_TRACE']) + __resetInterruptionTraceForTests() }) afterEach(() => { try { __resetInterruptionTraceForTests() - if (originalTrace === undefined) { - delete process.env.OPENCLAUDE_INTERRUPT_TRACE - } else { - process.env.OPENCLAUDE_INTERRUPT_TRACE = originalTrace - } + restoreEnv() } finally { releaseSharedMutationLock() } diff --git a/src/utils/attachments.lspDiagnostics.test.ts b/src/utils/attachments.lspDiagnostics.test.ts index e7985e1e16..255a0aec03 100644 --- a/src/utils/attachments.lspDiagnostics.test.ts +++ b/src/utils/attachments.lspDiagnostics.test.ts @@ -1,16 +1,22 @@ -import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, beforeEach, describe, expect, mock, test } from 'bun:test' +import { snapshotEnv } from '../test/envSnapshot.js' import type { ToolUseContext, Tools } from '../Tool.js' import { BASH_TOOL_NAME } from '../tools/BashTool/toolName.js' import type { DiagnosticFile } from '../services/diagnosticTracking.js' import type { Attachment } from './attachments.js' const debugMessages: string[] = [] -const realDebugModule = await import( - `./debug.js?real=${Date.now()}-${Math.random()}`, -) -const realLSPRegistry = await import( - `../services/lsp/LSPDiagnosticRegistry.js?real=${Date.now()}-${Math.random()}`, -) +const realDebugModule = { + ...await import(`./debug.js?real=${Date.now()}-${Math.random()}`), +} +const realLSPRegistry = { + ...await import(`../services/lsp/LSPDiagnosticRegistry.js?real=${Date.now()}-${Math.random()}`), +} + +afterAll(() => { + mock.module('./debug.js', () => realDebugModule) + mock.module('../services/lsp/LSPDiagnosticRegistry.js', () => realLSPRegistry) +}) let diagnosticSets: Array<{ serverName: string; files: DiagnosticFile[] }> = [] let nextDeliveryDelay: number | null = null @@ -48,8 +54,7 @@ const { getAttachmentMessages, __test } = await import( `./attachments.ts?test=${Date.now()}-${Math.random()}` ) -const SAVED_SIMPLE = process.env.CLAUDE_CODE_SIMPLE -const SAVED_DISABLE_ATTACHMENTS = process.env.CLAUDE_CODE_DISABLE_ATTACHMENTS +let restoreEnv: () => void type DiagnosticsAttachment = Extract @@ -137,8 +142,10 @@ async function collectLSPDiagnosticAttachments(): Promise< describe('LSP diagnostic attachment filtering', () => { beforeEach(() => { - delete process.env.CLAUDE_CODE_SIMPLE - delete process.env.CLAUDE_CODE_DISABLE_ATTACHMENTS + restoreEnv = snapshotEnv( + ['CLAUDE_CODE_SIMPLE', 'CLAUDE_CODE_DISABLE_ATTACHMENTS'], + { clear: true }, + ) diagnosticSets = [] nextDeliveryDelay = null checkForLSPDiagnosticsOptions.length = 0 @@ -150,16 +157,7 @@ describe('LSP diagnostic attachment filtering', () => { }) afterEach(() => { - if (SAVED_SIMPLE === undefined) { - delete process.env.CLAUDE_CODE_SIMPLE - } else { - process.env.CLAUDE_CODE_SIMPLE = SAVED_SIMPLE - } - if (SAVED_DISABLE_ATTACHMENTS === undefined) { - delete process.env.CLAUDE_CODE_DISABLE_ATTACHMENTS - } else { - process.env.CLAUDE_CODE_DISABLE_ATTACHMENTS = SAVED_DISABLE_ATTACHMENTS - } + restoreEnv() }) test('does not return a diagnostics attachment for an empty final LSP payload', async () => { From 8cfdb43df973a8e3e51d6423e0b5c6d936abd7fc Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 11:16:36 -0700 Subject: [PATCH 049/121] ci: add bun check fast typecheck alongside tsc (#2356) --- .github/workflows/pr-checks.yml | 3 +++ AGENTS.md | 3 +++ CONTRIBUTING.md | 2 ++ package.json | 1 + scripts/typecheck-fast.ts | 13 +++++++++++++ 5 files changed, 22 insertions(+) create mode 100644 scripts/typecheck-fast.ts diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index 9cb37eb354..f8d3178709 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -298,6 +298,9 @@ jobs: - name: Typecheck run: bun run typecheck + - name: Fast typecheck (Bun builtin) + run: bun run typecheck:fast + - name: Type tests run: bun run typecheck:type-tests diff --git a/AGENTS.md b/AGENTS.md index bcdd5aaded..5f590353de 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,9 +58,12 @@ bun run build bun run smoke bun run check bun run typecheck +bun run typecheck:fast bun run typecheck:type-tests ``` +`bun run typecheck` (tsc) remains authoritative. `bun run typecheck:fast` adds Bun's builtin type checker locally and in CI; use the wrapper because the repository's `check` script shadows `bun check`. + Focused checks: ```bash diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2236d182b8..63e3506d61 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -272,6 +272,7 @@ Required local preflight: bun install --frozen-lockfile bun run check bun run typecheck +bun run typecheck:fast bun run typecheck:type-tests node bin/openclaude --version git fetch https://github.com/Twigpine/openclaude.git main @@ -314,6 +315,7 @@ bun run web:build Notes on the local preflight: +- `bun run typecheck` uses TypeScript's `tsc` and remains the authoritative typecheck. `bun run typecheck:fast` runs Bun's builtin type checker against the same `tsconfig.json` as an additional local and CI check. Its wrapper resolves the config relative to the script and runs from outside the repository because the package's `check` script shadows `bun check` inside the project. Use `bun run typecheck:fast` from the root or a subdirectory; from elsewhere, use `bun run --cwd /path/to/openclaude typecheck:fast`. - PR Checks caches Bun package downloads using OS-scoped keys derived from `bun.lock` for root installs and `web/bun.lock` for web installs, with separate restore prefixes. Dependency installs still run with `--frozen-lockfile` on cache hits. Keep the relevant lockfile in sync with dependency changes and run the required local preflight regardless of cache status. - `bun run check` runs `check:static` (the any-usage budget, smoke build, and deadcode checks), then the full unit pass (`test:full`) — do not run those separately, or you execute work twice. - `bun run test` runs test files in a shared process and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups. Use `bun run scripts/run-tests.ts --shards=N --shard-index=K` with the same test flags to run only shard K of N. `test:full` then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI runs `check:static` in `smoke-and-tests`, runs the eight unit shards as parallel `unit-tests` jobs, and runs each dedicated feature pass once in `dedicated-tests`. Each unit shard builds the CLI before testing. CI caps these jobs at 15 minutes each and the other jobs at five minutes. diff --git a/package.json b/package.json index 20f75b55d6..0b81d98d08 100644 --- a/package.json +++ b/package.json @@ -63,6 +63,7 @@ "security:pr-scan": "bun run scripts/pr-intent-scan.ts", "test:provider-recommendation": "bun test src/utils/providerRecommendation.test.ts src/utils/providerProfile.test.ts", "typecheck": "tsc --noEmit", + "typecheck:fast": "bun run scripts/typecheck-fast.ts", "typecheck:type-tests": "bun run scripts/typecheck-type-tests.ts", "smoke": "bun run build && node dist/cli.mjs --version", "deadcode": "knip --include files,dependencies", diff --git a/scripts/typecheck-fast.ts b/scripts/typecheck-fast.ts new file mode 100644 index 0000000000..7c88ec259b --- /dev/null +++ b/scripts/typecheck-fast.ts @@ -0,0 +1,13 @@ +import { parse, resolve } from 'node:path' + +const tsconfigPath = resolve(import.meta.dir, '../tsconfig.json') +// Outside the project, `bun check` invokes the builtin instead of our check script. +const result = Bun.spawnSync([process.execPath, 'check', '-p', tsconfigPath], { + cwd: parse(tsconfigPath).root, + stdin: 'inherit', + stdout: 'inherit', + stderr: 'inherit', +}) + +if (!result.success) process.exit(result.exitCode ?? 1) +process.exit(0) From a838b6700650acc30ca1e263816ad867a2b6a00d Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 11:48:27 -0700 Subject: [PATCH 050/121] test: extend attribution isolation detector timeout (#2358) --- src/test/fixtures/providerModuleIsolation.fixture.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/fixtures/providerModuleIsolation.fixture.ts b/src/test/fixtures/providerModuleIsolation.fixture.ts index 0304ac2ffa..378fecb794 100644 --- a/src/test/fixtures/providerModuleIsolation.fixture.ts +++ b/src/test/fixtures/providerModuleIsolation.fixture.ts @@ -4,6 +4,7 @@ import { attributionModulesNeedIsolation, providerModuleIsMocked, REAL_PROVIDER_TEST_CHILD_ENV, + REAL_PROVIDER_TEST_TIMEOUT_MS, } from '../providerModuleIsolation.js' const scenario = process.env.ISOLATION_FIXTURE_SCENARIO @@ -99,9 +100,11 @@ if (scenario === 'assertion-failure') { }) } + // Cold attribution imports can exceed the default test timeout under shard + // load. Use the isolation budget, below the outer fixture process deadline. test(`attribution isolation detector: ${scenario}`, async () => { expect(await attributionModulesNeedIsolation()).toBe( scenario !== 'clean' && scenario !== 'child-marker', ) - }) + }, { timeout: REAL_PROVIDER_TEST_TIMEOUT_MS }) } From e36580576278c27223b66ed3e9581186ea699161 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 12:04:50 -0700 Subject: [PATCH 051/121] test: make concurrent natural finalizers deterministic (#2357) --- src/cli/bgRegistry.test.ts | 60 ++++++++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 18 deletions(-) diff --git a/src/cli/bgRegistry.test.ts b/src/cli/bgRegistry.test.ts index 3f6a523d84..11b839de4d 100644 --- a/src/cli/bgRegistry.test.ts +++ b/src/cli/bgRegistry.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, it } from 'bun:test' import { createHash } from 'node:crypto' +import { readFileSync } from 'node:fs' import { chmod, mkdir, @@ -1621,7 +1622,7 @@ describe('background session registry', () => { }) it('converges concurrent natural finalizers on one immutable fact', async () => { - await createBackgroundSession({ + const session = await createBackgroundSession({ id: 'bg-concurrent-natural', pid: 346, cwd: '/repo', @@ -1635,24 +1636,41 @@ describe('background session registry', () => { processMarker: TEST_PROCESS_MARKER, }) - const results = await Promise.all([ - recordBackgroundSessionNaturalTermination( - 'bg-concurrent-natural', - { exitCode: 31 }, - { - ownerPid: 346, - now: new Date('2026-06-15T08:10:00.000Z'), - }, - ), - recordBackgroundSessionNaturalTermination( - 'bg-concurrent-natural', - { exitCode: 0 }, - { - ownerPid: 346, - now: new Date('2026-06-15T08:11:00.000Z'), + const factPath = terminalFactPath( + session.id, + 'natural', + TEST_PROCESS_MARKER, + ) + let concurrentResult: Promise | undefined + let firstFact: string | undefined + const result = await recordBackgroundSessionNaturalTermination( + session.id, + { exitCode: 31 }, + { + ownerPid: session.pid, + get now() { + // The async finalizer holds the metadata lock and has read running + // state. Force the exit-event finalizer to install its fact before + // the async writer, without depending on the lock retry window. + recordBackgroundSessionNaturalTerminationSync( + session.id, + { exitCode: 0 }, + { + ownerPid: session.pid, + expectedSession: session, + now: new Date('2026-06-15T08:11:00.000Z'), + }, + ) + firstFact = readFileSync(factPath, 'utf8') + concurrentResult = resolveBackgroundSession(session.id) + return new Date('2026-06-15T08:10:00.000Z') }, - ), - ]) + }, + ) + if (!concurrentResult || firstFact === undefined) { + throw new Error('concurrent finalizer did not run') + } + const results = [result, await concurrentResult] const facts = results.map(result => { if (result.finishedAt === undefined || result.exitCode === undefined) { @@ -1665,6 +1683,11 @@ describe('background session registry', () => { } }) expect(new Set(facts.map(fact => JSON.stringify(fact))).size).toBe(1) + expect(facts[0]).toEqual({ + status: 'exited', + finishedAt: '2026-06-15T08:11:00.000Z', + exitCode: 0, + }) expect([ { status: 'failed', @@ -1680,6 +1703,7 @@ describe('background session registry', () => { expect(await resolveBackgroundSession('bg-concurrent-natural')).toMatchObject( facts[0]!, ) + expect(await readFile(factPath, 'utf8')).toBe(firstFact) }) it('corrects a stale guess with a later exact-owner natural fact', async () => { From 3e81882b97b0ea5ea50e488c094e297fe439d4f8 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 13:36:44 -0700 Subject: [PATCH 052/121] test: clear provider model cache after request tests (#2359) --- src/services/api/client.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/services/api/client.test.ts b/src/services/api/client.test.ts index 28a75d1372..1f43a2756a 100644 --- a/src/services/api/client.test.ts +++ b/src/services/api/client.test.ts @@ -7,6 +7,7 @@ import { registerGateway, } from '../../integrations/index.js' import { publicBuildVersion } from '../../utils/version.js' +import { resetModelStringsForTestingOnly } from '../../bootstrap/state.js' // bun:test keeps mock.module() registrations process-global across test files. // Load and re-register the real module before importing the client so a prior @@ -328,6 +329,7 @@ afterEach(() => { originalEnv.CLAUDE_CODE_PROVIDER_PROFILE_ENV_APPLIED_ID, ) globalThis.fetch = originalFetch + resetModelStringsForTestingOnly() } finally { releaseSharedMutationLock() } From 6c195819854a361e8df4503b746f7460f41b13a2 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 13:49:48 -0700 Subject: [PATCH 053/121] ci: use bun check as the CI typecheck (#2363) --- .github/workflows/pr-checks.yml | 5 +---- AGENTS.md | 3 +-- CONTRIBUTING.md | 9 +++++++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index f8d3178709..cef0880560 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -295,10 +295,7 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile - - name: Typecheck - run: bun run typecheck - - - name: Fast typecheck (Bun builtin) + - name: Typecheck (Bun builtin) run: bun run typecheck:fast - name: Type tests diff --git a/AGENTS.md b/AGENTS.md index 5f590353de..28417ffc0b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -57,12 +57,11 @@ bun install bun run build bun run smoke bun run check -bun run typecheck bun run typecheck:fast bun run typecheck:type-tests ``` -`bun run typecheck` (tsc) remains authoritative. `bun run typecheck:fast` adds Bun's builtin type checker locally and in CI; use the wrapper because the repository's `check` script shadows `bun check`. +`bun run typecheck:fast` runs Bun's builtin type checker locally and is the CI typecheck; use the wrapper because the repository's `check` script shadows `bun check`. `bun run typecheck:type-tests` also runs in CI for focused TypeScript assertions. `bun run typecheck` (tsc) is available as an additional local check. Keep the `typescript` devDependency for ESLint, `@astrojs/check`, and the focused type tests. Focused checks: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 63e3506d61..379b255bef 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -271,7 +271,6 @@ Required local preflight: ```bash bun install --frozen-lockfile bun run check -bun run typecheck bun run typecheck:fast bun run typecheck:type-tests node bin/openclaude --version @@ -315,7 +314,7 @@ bun run web:build Notes on the local preflight: -- `bun run typecheck` uses TypeScript's `tsc` and remains the authoritative typecheck. `bun run typecheck:fast` runs Bun's builtin type checker against the same `tsconfig.json` as an additional local and CI check. Its wrapper resolves the config relative to the script and runs from outside the repository because the package's `check` script shadows `bun check` inside the project. Use `bun run typecheck:fast` from the root or a subdirectory; from elsewhere, use `bun run --cwd /path/to/openclaude typecheck:fast`. +- `bun run typecheck:fast` runs Bun's builtin type checker against `tsconfig.json` and is the CI typecheck. Its wrapper resolves the config relative to the script and runs from outside the repository because the package's `check` script shadows `bun check` inside the project. Use `bun run typecheck:fast` from the root or a subdirectory; from elsewhere, use `bun run --cwd /path/to/openclaude typecheck:fast`. `bun run typecheck:type-tests` also runs in CI and uses TypeScript for focused type assertions. Keep the `typescript` devDependency for these assertions, ESLint, and `@astrojs/check`. - PR Checks caches Bun package downloads using OS-scoped keys derived from `bun.lock` for root installs and `web/bun.lock` for web installs, with separate restore prefixes. Dependency installs still run with `--frozen-lockfile` on cache hits. Keep the relevant lockfile in sync with dependency changes and run the required local preflight regardless of cache status. - `bun run check` runs `check:static` (the any-usage budget, smoke build, and deadcode checks), then the full unit pass (`test:full`) — do not run those separately, or you execute work twice. - `bun run test` runs test files in a shared process and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups. Use `bun run scripts/run-tests.ts --shards=N --shard-index=K` with the same test flags to run only shard K of N. `test:full` then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI runs `check:static` in `smoke-and-tests`, runs the eight unit shards as parallel `unit-tests` jobs, and runs each dedicated feature pass once in `dedicated-tests`. Each unit shard builds the CLI before testing. CI caps these jobs at 15 minutes each and the other jobs at five minutes. @@ -345,6 +344,12 @@ Focused tests: bun test ./path/to/test-file.test.ts ``` +Additional local TypeScript check against the same `tsconfig.json`: + +```bash +bun run typecheck +``` + Provider/runtime diagnostics: ```bash From 1bccad9c0179a2bc9b6836fc7e9f79c8fed40fc2 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 14:03:21 -0700 Subject: [PATCH 054/121] test: restore thinking exports after attachment tests (#2361) --- src/utils/attachments.ultrathink.test.ts | 33 +++++++++--------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/utils/attachments.ultrathink.test.ts b/src/utils/attachments.ultrathink.test.ts index f597074897..3ce0168376 100644 --- a/src/utils/attachments.ultrathink.test.ts +++ b/src/utils/attachments.ultrathink.test.ts @@ -1,24 +1,24 @@ import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { snapshotEnv } from '../test/envSnapshot.js' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as realThinking from './thinking.js' -let getUltrathinkEffortAttachment: typeof import('./attachments.js').getUltrathinkEffortAttachment -let savedEnv: { - disableAttachments: string | undefined - simple: string | undefined +// Keep a copy: mock.module also repoints an imported module namespace. +const realThinking = { + ...await import(`./thinking.js?real=${Date.now()}-${Math.random()}`), } +let getUltrathinkEffortAttachment: typeof import('./attachments.js').getUltrathinkEffortAttachment +let restoreEnv: () => void + beforeEach(async () => { await acquireSharedMutationLock('utils/attachments.ultrathink.test.ts') - savedEnv = { - disableAttachments: process.env.CLAUDE_CODE_DISABLE_ATTACHMENTS, - simple: process.env.CLAUDE_CODE_SIMPLE, - } - delete process.env.CLAUDE_CODE_DISABLE_ATTACHMENTS - delete process.env.CLAUDE_CODE_SIMPLE + restoreEnv = snapshotEnv([ + 'CLAUDE_CODE_DISABLE_ATTACHMENTS', + 'CLAUDE_CODE_SIMPLE', + ], { clear: true }) mock.module('./thinking.js', () => ({ ...realThinking, isUltrathinkEnabled: () => true, @@ -30,16 +30,7 @@ beforeEach(async () => { afterEach(() => { try { - if (savedEnv.disableAttachments === undefined) { - delete process.env.CLAUDE_CODE_DISABLE_ATTACHMENTS - } else { - process.env.CLAUDE_CODE_DISABLE_ATTACHMENTS = savedEnv.disableAttachments - } - if (savedEnv.simple === undefined) { - delete process.env.CLAUDE_CODE_SIMPLE - } else { - process.env.CLAUDE_CODE_SIMPLE = savedEnv.simple - } + restoreEnv() mock.restore() // Bun's mock.restore() does not unregister module mocks. mock.module('./thinking.js', () => realThinking) From 81ef40e27bf1824f5acc108a1efa858c951ba3a2 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 14:38:29 -0700 Subject: [PATCH 055/121] test: isolate diagnostic logger spy history (#2362) --- src/ink/output.test.ts | 1 + src/utils/context.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/src/ink/output.test.ts b/src/ink/output.test.ts index ad4a397b1b..b060dbe229 100644 --- a/src/ink/output.test.ts +++ b/src/ink/output.test.ts @@ -71,6 +71,7 @@ function writeFullFrame(output: Output, width: number, height: number): void { beforeEach(() => { logSpy = spyOn(debug, 'logForDebugging').mockImplementation(() => {}) + logSpy.mockClear() }) afterEach(() => { diff --git a/src/utils/context.test.ts b/src/utils/context.test.ts index d86406c948..343278fabd 100644 --- a/src/utils/context.test.ts +++ b/src/utils/context.test.ts @@ -575,6 +575,7 @@ test('unknown openai-compatible model fallback logs one debug warning and no con actualDebugModule, 'logForDebugging', ).mockImplementation((_message, _options) => {}) + logForDebugging.mockClear() const originalConsoleError = console.error const consoleError = mock(() => {}) From 0b39dfec098421e7d0b4be65b9376c87ebde6bdc Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 16:01:37 -0700 Subject: [PATCH 056/121] chore: require Node 24.22.1+ and default to Node 26 (#2365) Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com> --- .github/workflows/install-hygiene.yml | 2 +- .github/workflows/pr-checks.yml | 14 +++---- .github/workflows/release.yml | 10 ++--- .node-version | 2 +- .nvmrc | 2 +- AGENTS.md | 2 +- ANDROID_INSTALL.md | 2 +- CONTRIBUTING.md | 4 +- Dockerfile | 4 +- README.md | 9 ++--- bun.lock | 8 ++-- docs/advanced-setup.md | 8 ++-- docs/non-technical-setup.md | 2 +- docs/quick-start-mac-linux.md | 2 +- docs/quick-start-windows.md | 2 +- package.json | 4 +- scripts/benchmark-openclaude-startup.mjs | 2 +- scripts/externalsValidation.test.ts | 2 +- scripts/externalsValidation.ts | 2 +- scripts/system-check.test.ts | 33 ++++++++++++---- src/bootstrap/state.ts | 2 +- src/entrypoints/cli.tsx | 2 +- src/utils/interruptionTrace.ts | 7 +--- src/utils/nodeRuntime.test.ts | 49 +++++++++++++++++------- src/utils/nodeRuntime.ts | 10 +++-- src/utils/readFileInRange.ts | 3 +- src/utils/withResolvers.ts | 2 +- web/public/llms.txt | 2 +- web/src/pages/docs/installation.astro | 2 +- web/src/pages/index.astro | 2 +- 30 files changed, 119 insertions(+), 78 deletions(-) diff --git a/.github/workflows/install-hygiene.yml b/.github/workflows/install-hygiene.yml index 3d90185024..9d06877e2d 100644 --- a/.github/workflows/install-hygiene.yml +++ b/.github/workflows/install-hygiene.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - node-version: [22, 24] + node-version: ["24.22.1", "26"] steps: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index cef0880560..e3d29b8c76 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -48,7 +48,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "22" + node-version-file: .node-version - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -82,7 +82,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "22" + node-version-file: .node-version - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -123,7 +123,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "22" + node-version-file: .node-version - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -160,7 +160,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "22" + node-version-file: .node-version - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -191,7 +191,7 @@ jobs: timeout-minutes: 5 strategy: matrix: - node-version: ["22", "24.11.x"] + node-version: ["24.22.1", "26"] steps: - name: Check out repository @@ -243,7 +243,7 @@ jobs: - name: Set up minimum supported Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: "22.0.0" + node-version: "24.22.1" - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 @@ -314,7 +314,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 22 + node-version-file: .node-version - name: Set up Bun uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 195116dffe..4e92c55d92 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,8 +30,8 @@ jobs: release-type: node # Zero-warning install gate: pack the release tag and install it globally in - # a sandbox (cold + upgrade scenarios) on the supported Node floor (22 → - # npm 10) and current (24 → npm 11), whose warning output differs. Publish + # a sandbox (cold + upgrade scenarios) on the supported Node floor (24.22.1) + # and current (26), both shipping npm 11. Publish # is blocked unless the real `npm install -g` experience is clean. install-verify: name: Verify clean npm install (Node ${{ matrix.node-version }}) @@ -43,7 +43,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [22, 24] + node-version: ["24.22.1", "26"] steps: - name: Checkout release tag uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 @@ -89,7 +89,7 @@ jobs: - name: Set up Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: - node-version: 24 + node-version-file: .node-version registry-url: https://registry.npmjs.org - name: Set up Bun @@ -110,7 +110,7 @@ jobs: run: bun run smoke # Final install-hygiene gate on the publishing machine itself (the - # install-verify job covered Node 22/24 already); packs with + # install-verify job covered Node 24.22.1/26 already); packs with # --ignore-scripts to reuse the dist/ built above. Supersedes the old # `npm pack --dry-run` (contents are asserted inside the verify). - name: Verify zero-warning global install diff --git a/.node-version b/.node-version index 2bd5a0a98a..6f4247a625 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -22 +26 diff --git a/.nvmrc b/.nvmrc index 2bd5a0a98a..6f4247a625 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -22 +26 diff --git a/AGENTS.md b/AGENTS.md index 28417ffc0b..3828c5c774 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ This guide is for AI coding agents working in the OpenClaude repository. Read it OpenClaude is a coding-agent CLI for cloud and local model providers. It supports OpenAI-compatible APIs, Anthropic, Gemini, DeepSeek, Ollama, MCP, local backends, slash commands, tools, agents, and a React/Ink terminal UI. -The installed CLI runs on Node.js `>=22.0.0`. Bun is used for source builds, scripts, dependency management, and tests. +The installed CLI runs on Node.js `>=24.22.1` (Node 26 recommended/default). Bun is used for source builds, scripts, dependency management, and tests. ## Work Style diff --git a/ANDROID_INSTALL.md b/ANDROID_INSTALL.md index 357f19eab3..f658688016 100644 --- a/ANDROID_INSTALL.md +++ b/ANDROID_INSTALL.md @@ -36,7 +36,7 @@ pkg install nodejs-lts git proot-distro Verify Node.js: ```bash -node --version # should be v20+ +node --version # requires v24.22.1 or newer; Node 26 recommended/default ``` ### Step 3 — Clone OpenClaude diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 379b255bef..e8a12ce6b5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -211,7 +211,7 @@ When submitting provider changes: ## Local Setup -Use Node.js `>=22.0.0` and Bun `1.4.4` (tracked in `.bun-version`). +Use Node.js `>=24.22.1` (Node 26 recommended/default) and Bun `1.4.4` (tracked in `.bun-version`). Install dependencies: @@ -320,7 +320,7 @@ Notes on the local preflight: - `bun run test` runs test files in a shared process and requires Bun's completed JUnit report as well as a successful exit. `test:full` runs all eight native Bun shards serially to release memory between groups. Use `bun run scripts/run-tests.ts --shards=N --shard-index=K` with the same test flags to run only shard K of N. `test:full` then applies the same completion check to the conversation-arc pass and the context-efficiency pass with `HISTORY_SNIP` enabled. Every shard must produce a complete report; a failed shard does not prevent the remaining shards from running. A test that terminates the runner early fails the command, even if it exits with code zero. Each shard has a ten-minute deadline (override with `--shard-timeout-ms=`), reports its start/end and elapsed time, and kills its process group on timeout or cancellation. A timeout stops the remaining shards and exits nonzero. On Linux, shard output is relayed from temporary regular files; diagnostics remain visible during execution. Temporary reports and logs are removed after the check. CI runs `check:static` in `smoke-and-tests`, runs the eight unit shards as parallel `unit-tests` jobs, and runs each dedicated feature pass once in `dedicated-tests`. Each unit shard builds the CLI before testing. CI caps these jobs at 15 minutes each and the other jobs at five minutes. - Fetching upstream `main` by URL avoids assuming that a fork checkout's `origin` points at Twigpine/openclaude. `FETCH_HEAD` is the fetched upstream tip. The scan deliberately uses local `HEAD` so it includes commits that have not been pushed yet; CI uses the pushed PR head SHA after the push. - The web CI job runs on PRs that change `web/**`, root `package.json` or `bun.lock`, `.bun-version`, or `.github/workflows/pr-checks.yml`, and on every push to `main`. Contributors do not need to run the web suite locally for changes that cannot affect the site. -- This preflight covers the same command families as CI, but it does not reproduce CI exactly in one shell. When selected, CI runs static checks with Node 22 available for smoke checks, runs the unit shards and dedicated feature passes under Node 22, separately builds and checks launcher compatibility under Node 22 and 24.11.x, separately builds and launches under exact Node 22.0.0, and executes every job on a clean runner. +- This preflight covers the same command families as CI, but it does not reproduce CI exactly in one shell. When selected, CI runs static checks with Node 26 available for smoke checks, runs the unit shards and dedicated feature passes under Node 26, separately builds and checks launcher compatibility under Node 24.22.1 and 26, separately builds and launches under exact Node 24.22.1, and executes every job on a clean runner. If a local or GitHub CI check fails, first determine whether the PR owns the failure. Treat a failure as pre-existing only when you can reproduce it on the PR's current base (or link to the same failure on a current `main` CI run) and the PR neither changes the causal surface nor claims to fix that problem. diff --git a/Dockerfile b/Dockerfile index 11c797eb49..e12ba30b67 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ---- build stage ---- -FROM node:22-slim AS build +FROM node:26-slim AS build WORKDIR /app @@ -28,7 +28,7 @@ RUN bun run build RUN rm -rf node_modules && bun install --frozen-lockfile --production # ---- runtime stage ---- -FROM node:22-slim +FROM node:26-slim WORKDIR /app diff --git a/README.md b/README.md index e3dad94f35..18022b6f29 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ OpenClaude is also mirrored to Twigpine: ### Install -OpenClaude requires Node.js `>=22.0.0` for npm installs and runtime. Bun is +OpenClaude requires Node.js `>=24.22.1` for npm installs and runtime (Node 26 recommended/default). Bun is only needed for source builds and local development. ```bash @@ -489,7 +489,7 @@ and client generation from `src/proto/openclaude.proto`. ## Development -Use Node.js `>=22.0.0` and Bun `1.4.4` for source builds. +Use Node.js `>=24.22.1` (Node 26 recommended/default) and Bun `1.4.4` for source builds. ```bash bun install @@ -515,9 +515,8 @@ bun run build bun run benchmark:startup ``` -The benchmark requires Node `>=22.8.0`, where the compile-cache API was added; -the built OpenClaude launcher continues to support the declared Node `>=22.0.0` -runtime range. +The benchmark and built OpenClaude launcher require Node `>=24.22.1`; +Node 26 is recommended/default. The benchmark defaults to 30 separate-process warm runs and 10 isolated empty-cache runs. It reports the median, IQR, MAD, first cache-populating run, diff --git a/bun.lock b/bun.lock index 95fb5a32f4..eac1554fd6 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,7 @@ "@smithy/core": "3.24.3", "@smithy/node-http-handler": "4.7.3", "@types/bun": "1.4.4", - "@types/node": "25.5.0", + "@types/node": "^24.19.2", "@types/react": "19.2.14", "@typescript-eslint/eslint-plugin": "^8.69.0", "@typescript-eslint/parser": "^8.69.0", @@ -539,7 +539,7 @@ "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="], - "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + "@types/node": ["@types/node@24.19.2", "", { "dependencies": { "undici-types": ">=7.24.0 <7.24.7" } }, "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ=="], "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], @@ -1219,7 +1219,7 @@ "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], - "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], @@ -1415,6 +1415,8 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "bun-types/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], "cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], diff --git a/docs/advanced-setup.md b/docs/advanced-setup.md index 56781de038..cf300c1324 100644 --- a/docs/advanced-setup.md +++ b/docs/advanced-setup.md @@ -4,7 +4,7 @@ This guide is for users who want source builds, Bun workflows, provider profiles ## Install Options -OpenClaude requires Node.js `>=22.0.0` for npm installs and runtime. Bun is +OpenClaude requires Node.js `>=24.22.1` for npm installs and runtime (Node 26 recommended/default). Bun is only required when building or running from source. ### Option A: npm @@ -424,7 +424,7 @@ the percentage through the OpenClaude launcher. OpenClaude converts `--max-old-space-size-percentage` and `OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_PERCENTAGE` to an explicit `--max-old-space-size` in megabytes so the same command works on every supported -Node version, including `22.0.0`: +Node version, including `24.22.1`: ```bash openclaude --max-old-space-size-percentage=50 @@ -441,7 +441,7 @@ Heap size precedence, earlier wins: Putting the native flag in `NODE_OPTIONS` or `node --max-old-space-size-percentage=…` only works on Node versions that accept -that option; Node 22.0.0 rejects it before OpenClaude starts. When a supporting +that option. When a supporting Node already applied that native flag, OpenClaude leaves it in place and does not append `8192`. Launcher-only flags (`--max-old-space-size-percentage`, `--max-memory=`) are still stripped so Commander can start. That native-flag @@ -507,7 +507,7 @@ host. Without this variable the behavior is unchanged. | `OLLAMA_BASE_URL` | Ollama only | Optional Ollama server base URL used for model discovery and the signed-in local Web Search proxy. The Ollama provider profile normally supplies the equivalent `OPENAI_BASE_URL`. | | `OLLAMA_API_KEY` | Ollama Web Search only | Enables hosted Ollama search at `https://ollama.com/api/web_search` and acts as a fallback when the configured local Ollama search endpoint is unavailable. | | `OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_MB` | No | Explicit V8 old-space heap cap in megabytes for the Node launcher. Used only when `--max-memory`, `--max-old-space-size-percentage`, and `OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_PERCENTAGE` are all unset. Default `8192`. | -| `OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_PERCENTAGE` | No | Size the V8 old-space heap as a percentage (greater than 0, up to 100) of constrained/container memory when Node reports it, otherwise total system RAM. Converted to `--max-old-space-size` so Node 22.0.0 can start. Precedence, earlier wins: `--max-memory` → `--max-old-space-size-percentage` → this variable → `OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_MB` → default `8192`. | +| `OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_PERCENTAGE` | No | Size the V8 old-space heap as a percentage (greater than 0, up to 100) of constrained/container memory when Node reports it, otherwise total system RAM. Converted to `--max-old-space-size` for compatibility across supported Node versions. Precedence, earlier wins: `--max-memory` → `--max-old-space-size-percentage` → this variable → `OPENCLAUDE_NODE_MAX_OLD_SPACE_SIZE_MB` → default `8192`. | | `CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS` | No | JSON map of OpenAI-compatible model names to context windows, such as `{"custom-model":1000000}`. Use this when a custom provider does not expose context metadata from `/v1/models`. | | `CLAUDE_CODE_OPENAI_MAX_OUTPUT_TOKENS` | No | JSON map of OpenAI-compatible model names to max output tokens, such as `{"custom-model":32768}`. Use this when a custom provider does not expose output-limit metadata from `/v1/models`. | | `OPENCODE_API_KEY` | OpenCode Zen / Go | Shared API key for OpenCode Zen (pay-as-you-go) and OpenCode Go (subscription); get yours from https://opencode.ai | diff --git a/docs/non-technical-setup.md b/docs/non-technical-setup.md index a5eff255ea..ae55f3c4b4 100644 --- a/docs/non-technical-setup.md +++ b/docs/non-technical-setup.md @@ -22,7 +22,7 @@ For most first-time users, OpenAI is the easiest option. You need: -1. Node.js 22 LTS or newer installed +1. Node.js 24.22.1 or newer (Node 26 recommended/default) installed 2. A terminal window 3. An API key from your provider, unless you are using a local model like Ollama diff --git a/docs/quick-start-mac-linux.md b/docs/quick-start-mac-linux.md index 03f80264ed..bd065322ac 100644 --- a/docs/quick-start-mac-linux.md +++ b/docs/quick-start-mac-linux.md @@ -4,7 +4,7 @@ This guide uses a standard shell such as Terminal, iTerm, bash, or zsh. ## 1. Install Node.js -Install Node.js 22 LTS or newer from: +Install Node.js 24.22.1 or newer (Node 26 recommended/default) from: - `https://nodejs.org/` diff --git a/docs/quick-start-windows.md b/docs/quick-start-windows.md index ca5bd84901..545d81a6b9 100644 --- a/docs/quick-start-windows.md +++ b/docs/quick-start-windows.md @@ -4,7 +4,7 @@ This guide uses Windows PowerShell. ## 1. Install Node.js -Install Node.js 22 LTS or newer from: +Install Node.js 24.22.1 or newer (Node 26 recommended/default) from: - `https://nodejs.org/` diff --git a/package.json b/package.json index 0b81d98d08..6b21190c5b 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@smithy/core": "3.24.3", "@smithy/node-http-handler": "4.7.3", "@types/bun": "1.4.4", - "@types/node": "25.5.0", + "@types/node": "^24.19.2", "@types/react": "19.2.14", "@typescript-eslint/eslint-plugin": "^8.69.0", "@typescript-eslint/parser": "^8.69.0", @@ -191,7 +191,7 @@ } }, "engines": { - "node": ">=22.0.0" + "node": ">=24.22.1" }, "repository": { "type": "git", diff --git a/scripts/benchmark-openclaude-startup.mjs b/scripts/benchmark-openclaude-startup.mjs index e6ac2d05b0..a1ea9357d9 100644 --- a/scripts/benchmark-openclaude-startup.mjs +++ b/scripts/benchmark-openclaude-startup.mjs @@ -34,7 +34,7 @@ if (warmRuns < MIN_WARM_RUNS) { throw new Error(`--warm-runs must be at least ${MIN_WARM_RUNS}`) } if (typeof nodeModule.enableCompileCache !== 'function') { - throw new Error(`Node ${process.version} does not expose module.enableCompileCache; the benchmark requires Node >=22.8.0`) + throw new Error(`Node ${process.version} does not expose module.enableCompileCache; the benchmark requires Node >=24.22.1`) } if (!existsSync(BUNDLE_PATH) || !statSync(BUNDLE_PATH).isFile()) { throw new Error('dist/cli.mjs is missing; run `bun run build` first') diff --git a/scripts/externalsValidation.test.ts b/scripts/externalsValidation.test.ts index 685d8f2ab4..70eb136243 100644 --- a/scripts/externalsValidation.test.ts +++ b/scripts/externalsValidation.test.ts @@ -304,7 +304,7 @@ describe('validateRuntimeDependencyContract', () => { }) describe('validateInstallHygieneFields', () => { - const CLEAN = { engines: { node: '>=22.0.0' } } + const CLEAN = { engines: { node: '>=24.22.1' } } test('passes for a clean manifest', () => { expect(validateInstallHygieneFields(CLEAN).ok).toBe(true) diff --git a/scripts/externalsValidation.ts b/scripts/externalsValidation.ts index 6a969704a1..0ebc0fa794 100644 --- a/scripts/externalsValidation.ts +++ b/scripts/externalsValidation.ts @@ -40,7 +40,7 @@ export const RUNTIME_DEPENDENCY_CONTRACT: Readonly> = { } /** Node range advertised to installers; changing it changes who gets EBADENGINE. */ -export const ENGINES_NODE_CONTRACT = '>=22.0.0' +export const ENGINES_NODE_CONTRACT = '>=24.22.1' const EXACT_VERSION_RE = /^\d+\.\d+\.\d+(?:-[\w.]+)?$/ diff --git a/scripts/system-check.test.ts b/scripts/system-check.test.ts index dead9e1cd4..45e8ed6f95 100644 --- a/scripts/system-check.test.ts +++ b/scripts/system-check.test.ts @@ -1048,14 +1048,14 @@ describe('checkNodeVersion', () => { test('reads the Node.js version from the node executable output', () => { const probe = readNodeExecutableVersion(() => ({ status: 0, - stdout: 'v22.0.0\n', + stdout: 'v24.22.1\n', stderr: '', error: undefined, })) expect(probe).toEqual({ ok: true, - version: 'v22.0.0', + version: 'v24.22.1', }) }) @@ -1064,7 +1064,7 @@ describe('checkNodeVersion', () => { ok: false, label: 'Node.js version', detail: - 'Detected 20.11.1. OpenClaude requires Node.js >=22.0.0. Install Node 22 LTS or newer, then reinstall/re-run OpenClaude.', + 'Detected 20.11.1. OpenClaude requires Node.js >=24.22.1. Install Node 24.22.1 or newer, then reinstall/re-run OpenClaude.', }) }) @@ -1080,7 +1080,7 @@ describe('checkNodeVersion', () => { ok: false, label: 'Node.js version', detail: - 'Unable to run `node --version`: spawn node ENOENT. OpenClaude requires Node.js >=22.0.0 on PATH.', + 'Unable to run `node --version`: spawn node ENOENT. OpenClaude requires Node.js >=24.22.1 on PATH.', }) }) @@ -1089,17 +1089,36 @@ describe('checkNodeVersion', () => { ok: false, label: 'Node.js version', detail: - 'Detected 20.11.1. OpenClaude requires Node.js >=22.0.0. Install Node 22 LTS or newer, then reinstall/re-run OpenClaude.', + 'Detected 20.11.1. OpenClaude requires Node.js >=24.22.1. Install Node 24.22.1 or newer, then reinstall/re-run OpenClaude.', }) }) test('passes supported Node.js versions', () => { - expect(checkNodeVersion('22.0.0')).toEqual({ + expect(checkNodeVersion('24.22.1')).toEqual({ ok: true, label: 'Node.js version', - detail: '22.0.0', + detail: '24.22.1', }) }) + + test('rejects the patch immediately below the Node.js minimum', () => { + expect(checkNodeVersion({ ok: true, version: 'v24.22.0' })).toEqual({ + ok: false, + label: 'Node.js version', + detail: + 'Detected 24.22.0. OpenClaude requires Node.js >=24.22.1. Install Node 24.22.1 or newer, then reinstall/re-run OpenClaude.', + }) + }) + + test.each(['v24.22.1', 'v26.12.1'])( + 'passes the supported probed version %s', version => { + expect(checkNodeVersion({ ok: true, version })).toEqual({ + ok: true, + label: 'Node.js version', + detail: version.slice(1), + }) + }, + ) }) describe('sandbox runtime diagnostics', () => { diff --git a/src/bootstrap/state.ts b/src/bootstrap/state.ts index 28486e5b45..c4c3a3cb9b 100644 --- a/src/bootstrap/state.ts +++ b/src/bootstrap/state.ts @@ -407,7 +407,7 @@ const STATE: State = getInitialState() * * **Runtime Requirement:** Uses Node.js `async_hooks.AsyncLocalStorage`. * Not available in browsers or non-Node JavaScript environments. - * SDK consumers must run in OpenClaude's supported Node.js runtime (>=22.0.0). + * SDK consumers must run in OpenClaude's supported Node.js runtime (>=24.22.1). */ type SdkContext = { sessionId: SessionId diff --git a/src/entrypoints/cli.tsx b/src/entrypoints/cli.tsx index 27fd715b35..4f0ec5833d 100644 --- a/src/entrypoints/cli.tsx +++ b/src/entrypoints/cli.tsx @@ -11,7 +11,7 @@ import { } from '../utils/printFlag.js' // Defensive compatibility guard for environments where globalThis.File is -// unexpectedly absent. OpenClaude's supported runtime is Node >=22; this is +// unexpectedly absent. OpenClaude's supported runtime is Node >=24.22.1; this is // not a Node 18 support guarantee. The guard is harmless on supported Node // versions and prevents undici's module evaluation from throwing in unusual // embedded/runtime setups. diff --git a/src/utils/interruptionTrace.ts b/src/utils/interruptionTrace.ts index 3999505d98..1c148ad1aa 100644 --- a/src/utils/interruptionTrace.ts +++ b/src/utils/interruptionTrace.ts @@ -1,9 +1,6 @@ import { createHash, randomUUID } from 'node:crypto' import { isAbsolute, posix, win32 } from 'node:path' -import { - monitorEventLoopDelay, - type IntervalHistogram, -} from 'node:perf_hooks' +import { monitorEventLoopDelay } from 'node:perf_hooks' import { normalizeAbortReason } from './abortReasons.js' import { appendDiagnosticsNoPII } from './diagLogs.js' import { redactHomePath, redactLikelySecrets } from './redaction.js' @@ -117,7 +114,7 @@ let signalAbortEventIds = new WeakMap() let signalAbortSources = new WeakMap() let signalStates = new WeakMap() let errorCausalEventIds = new WeakMap() -let eventLoopDelay: IntervalHistogram | undefined +let eventLoopDelay: ReturnType | undefined let flushQueue: Promise = Promise.resolve() let retryBatch: | { diff --git a/src/utils/nodeRuntime.test.ts b/src/utils/nodeRuntime.test.ts index 79b04f6a44..674954295c 100644 --- a/src/utils/nodeRuntime.test.ts +++ b/src/utils/nodeRuntime.test.ts @@ -13,27 +13,27 @@ describe('node runtime contract', () => { new URL('../../package.json', import.meta.url), ).json() - expect(MIN_NODE_MAJOR).toBe(22) - expect(MIN_NODE_VERSION).toBe('22.0.0') - expect(MIN_NODE_ENGINE_RANGE).toBe('>=22.0.0') + expect(MIN_NODE_MAJOR).toBe(24) + expect(MIN_NODE_VERSION).toBe('24.22.1') + expect(MIN_NODE_ENGINE_RANGE).toBe('>=24.22.1') expect(packageJson.engines.node).toBe(MIN_NODE_ENGINE_RANGE) }) test('accepts supported Node versions', () => { - expect(checkSupportedNodeVersion('v22.0.0')).toEqual({ + expect(checkSupportedNodeVersion('v24.22.1')).toEqual({ ok: true, - version: '22.0.0', - major: 22, + version: '24.22.1', + major: 24, }) - expect(checkSupportedNodeVersion('22.0.0')).toEqual({ + expect(checkSupportedNodeVersion('24.22.1')).toEqual({ ok: true, - version: '22.0.0', - major: 22, + version: '24.22.1', + major: 24, }) - expect(checkSupportedNodeVersion('25.5.0')).toEqual({ + expect(checkSupportedNodeVersion('26.12.1')).toEqual({ ok: true, - version: '25.5.0', - major: 25, + version: '26.12.1', + major: 26, }) }) @@ -43,17 +43,38 @@ describe('node runtime contract', () => { version: '20.11.1', major: 20, message: - 'Detected 20.11.1. OpenClaude requires Node.js >=22.0.0. Install Node 22 LTS or newer, then reinstall/re-run OpenClaude.', + 'Detected 20.11.1. OpenClaude requires Node.js >=24.22.1. Install Node 24.22.1 or newer, then reinstall/re-run OpenClaude.', }) }) + test.each(['23.12.0', '24.21.9', '24.22.0', '24.22.1-rc.1'])( + 'rejects %s below the exact minimum', version => { + expect(checkSupportedNodeVersion(version)).toEqual({ + ok: false, + version, + major: Number(version.split('.')[0]), + message: `Detected ${version}. OpenClaude requires Node.js >=24.22.1. Install Node 24.22.1 or newer, then reinstall/re-run OpenClaude.`, + }) + }, + ) + + test.each(['24.22.1', '24.22.2', '24.23.0', '26.0.0', '26.12.1'])( + 'accepts %s at or above the exact minimum', version => { + expect(checkSupportedNodeVersion(version)).toEqual({ + ok: true, + version, + major: Number(version.split('.')[0]), + }) + }, + ) + test('rejects malformed Node versions with the same required minimum', () => { expect(checkSupportedNodeVersion('nightly')).toEqual({ ok: false, version: 'nightly', major: null, message: - 'Could not parse Node.js version: nightly. OpenClaude requires Node.js >=22.0.0.', + 'Could not parse Node.js version: nightly. OpenClaude requires Node.js >=24.22.1.', }) }) }) diff --git a/src/utils/nodeRuntime.ts b/src/utils/nodeRuntime.ts index be1fb6eba4..eba3c03fdb 100644 --- a/src/utils/nodeRuntime.ts +++ b/src/utils/nodeRuntime.ts @@ -1,5 +1,7 @@ -export const MIN_NODE_MAJOR = 22 -export const MIN_NODE_VERSION = '22.0.0' +import { satisfies } from './semver.js' + +export const MIN_NODE_MAJOR = 24 +export const MIN_NODE_VERSION = '24.22.1' export const MIN_NODE_ENGINE_RANGE = `>=${MIN_NODE_VERSION}` export type NodeVersionCheckResult = @@ -39,12 +41,12 @@ export function checkSupportedNodeVersion( } } - if (major < MIN_NODE_MAJOR) { + if (!satisfies(version, MIN_NODE_ENGINE_RANGE)) { return { ok: false, version, major, - message: `Detected ${version}. OpenClaude requires Node.js ${MIN_NODE_ENGINE_RANGE}. Install Node ${MIN_NODE_MAJOR} LTS or newer, then reinstall/re-run OpenClaude.`, + message: `Detected ${version}. OpenClaude requires Node.js ${MIN_NODE_ENGINE_RANGE}. Install Node ${MIN_NODE_VERSION} or newer, then reinstall/re-run OpenClaude.`, } } diff --git a/src/utils/readFileInRange.ts b/src/utils/readFileInRange.ts index 5268ab792f..c5a5b7619b 100644 --- a/src/utils/readFileInRange.ts +++ b/src/utils/readFileInRange.ts @@ -237,7 +237,8 @@ function streamOnOpen(this: StreamState, fd: number): void { }) } -function streamOnData(this: StreamState, chunk: string): void { +function streamOnData(this: StreamState, chunk: string | Buffer): void { + chunk = typeof chunk === 'string' ? chunk : chunk.toString('utf8') if (this.isFirstChunk) { this.isFirstChunk = false if (chunk.charCodeAt(0) === 0xfeff) { diff --git a/src/utils/withResolvers.ts b/src/utils/withResolvers.ts index e7f261ee65..181df3a2ae 100644 --- a/src/utils/withResolvers.ts +++ b/src/utils/withResolvers.ts @@ -1,6 +1,6 @@ /** * Polyfill for Promise.withResolvers() (ES2024, Node 22+). - * Native on every supported runtime now that engines.node is >=22.0.0; kept as a + * Native on every supported runtime (engines.node >=24.22.1); kept as a * thin wrapper so call sites stay stable and downstream embeds running older Node * (despite the warning) don't crash. */ diff --git a/web/public/llms.txt b/web/public/llms.txt index 6c3725bf83..ec38db6cc2 100644 --- a/web/public/llms.txt +++ b/web/public/llms.txt @@ -18,7 +18,7 @@ OpenClaude originated from the Claude Code codebase and has since been substanti - [Quick start (Windows)](https://github.com/Twigpine/openclaude/blob/main/docs/quick-start-windows.md) - [Non-technical setup](https://github.com/Twigpine/openclaude/blob/main/docs/non-technical-setup.md) - [Android install](https://github.com/Twigpine/openclaude/blob/main/ANDROID_INSTALL.md) -- [npm install](https://www.npmjs.com/package/@twigpine/openclaude): `npm install -g @twigpine/openclaude@latest` (requires Node >= 22) +- [npm install](https://www.npmjs.com/package/@twigpine/openclaude): `npm install -g @twigpine/openclaude@latest` (requires Node >= 24.22.1; Node 26 recommended/default) - [AUR package: openclaude](https://aur.archlinux.org/packages/openclaude) ## Documentation diff --git a/web/src/pages/docs/installation.astro b/web/src/pages/docs/installation.astro index 656d078ac2..36cab72634 100644 --- a/web/src/pages/docs/installation.astro +++ b/web/src/pages/docs/installation.astro @@ -22,7 +22,7 @@ const toc = [ >

requirements

    -
  • Node.js 22 or newer — grab it from nodejs.org and confirm with node --version.
  • +
  • Node.js 24.22.1 or newer (Node 26 recommended/default) — grab it from nodejs.org and confirm with node --version.
  • A terminal: Terminal/iTerm on macOS, any shell on Linux, PowerShell or Windows Terminal on Windows.
  • ripgrep is bundled; if startup reports ripgrep not found, install it system-wide and confirm rg --version works.
diff --git a/web/src/pages/index.astro b/web/src/pages/index.astro index f4450d1140..755c6dc2b9 100644 --- a/web/src/pages/index.astro +++ b/web/src/pages/index.astro @@ -216,7 +216,7 @@ const jsonLd = [ 01
install -

requires node ≥ 22. platform guides →

+

requires node ≥ 24.22.1; node 26 recommended/default. platform guides →

  • From 9c41a4fab0eb873b385aef641c2f762532d40c29 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 18:32:17 -0700 Subject: [PATCH 057/121] chore(deps): apply patch updates and fix grpc and fflate advisories (#2366) --- bun.lock | 234 +++++++++++++++++++++++++++++++++++++---------- package.json | 34 +++---- web/bun.lock | 120 +++++++++++++++--------- web/package.json | 8 +- 4 files changed, 282 insertions(+), 114 deletions(-) diff --git a/bun.lock b/bun.lock index eac1554fd6..12921fa910 100644 --- a/bun.lock +++ b/bun.lock @@ -10,18 +10,18 @@ "@vscode/ripgrep": "1.18.0", }, "devDependencies": { - "@alcalzone/ansi-tokenize": "0.3.0", - "@anthropic-ai/bedrock-sdk": "0.29.1", - "@anthropic-ai/foundry-sdk": "0.2.3", + "@alcalzone/ansi-tokenize": "0.3.1", + "@anthropic-ai/bedrock-sdk": "0.29.2", + "@anthropic-ai/foundry-sdk": "0.2.4", "@anthropic-ai/sandbox-runtime": "0.0.55", "@anthropic-ai/sdk": "0.94.0", "@aws-sdk/client-bedrock": "3.1047.0", "@aws-sdk/client-sts": "3.1047.0", - "@aws-sdk/credential-provider-node": "3.972.41", - "@azure/identity": "^4.13.1", + "@aws-sdk/credential-provider-node": "3.972.88", + "@azure/identity": "4.13.3", "@commander-js/extra-typings": "12.1.0", - "@grpc/grpc-js": "^1.14.3", - "@grpc/proto-loader": "^0.8.0", + "@grpc/grpc-js": "1.14.6", + "@grpc/proto-loader": "0.8.1", "@modelcontextprotocol/sdk": "1.29.0", "@sentry/node": "^10.70.0", "@smithy/core": "3.24.3", @@ -41,21 +41,21 @@ "cli-highlight": "2.1.11", "commander": "12.1.0", "cross-spawn": "7.0.6", - "diff": "8.0.3", + "diff": "8.0.4", "duck-duck-scrape": "^2.2.7", "emoji-regex": "10.6.0", "env-paths": "3.0.0", - "eslint": "9", + "eslint": "9.39.5", "execa": "9.6.1", - "fflate": "0.8.2", + "fflate": "0.8.3", "figures": "6.1.0", "fuse.js": "7.1.0", "get-east-asian-width": "1.5.0", "google-auth-library": "10.6.2", "graphology": "0.26.0", - "graphology-metrics": "2.4.0", + "graphology-metrics": "2.4.2", "https-proxy-agent": "7.0.6", - "ignore": "7.0.5", + "ignore": "7.0.13", "indent-string": "5.0.0", "js-tiktoken": "1.0.21", "jsonc-parser": "3.3.1", @@ -63,8 +63,8 @@ "lodash-es": "4.18.1", "lru-cache": "11.2.7", "marked": "15.0.12", - "p-map": "7.0.4", - "picomatch": "4.0.4", + "p-map": "7.0.8", + "picomatch": "4.0.7", "proper-lockfile": "4.1.2", "qrcode": "1.5.4", "react": "19.2.4", @@ -77,7 +77,7 @@ "supports-hyperlinks": "3.2.0", "tree-kill": "1.2.2", "tree-sitter-wasms": "0.1.13", - "turndown": "7.2.2", + "turndown": "7.2.4", "type-fest": "4.41.0", "typescript": "6.0.3", "undici": "7.28.0", @@ -85,9 +85,9 @@ "vscode-languageserver-protocol": "3.17.5", "web-tree-sitter": "0.25.10", "wrap-ansi": "9.0.2", - "ws": "8.21.0", - "xss": "1.0.15", - "yaml": "2.8.3", + "ws": "8.21.3", + "xss": "1.0.16", + "yaml": "2.8.4", "zod": "3.25.76", }, "peerDependencies": { @@ -111,11 +111,11 @@ "node-domexception": "file:vendor/node-domexception-shim", }, "packages": { - "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-p+CMKJ93HFmLkjXKlXiVGlMQEuRb6H0MokBSwUsX+S6BRX8eV5naFZpQJFfJHjRZY0Hmnqy1/r6UWl3x+19zYA=="], + "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.1", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-cUD0PT0YX+rImRhfn7BfMRduP/tX/Zqt/N0dHlAyn2RlcJssCb5KsCJkhln61eh9vuJDkH8wKn3L8zJzzWXPFg=="], - "@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.29.1", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-eV3vqNvMEy9C5nBgZMlKFewgXoBcIuV/PERRQJIlot3Vd5lU8dVUS7YqkkbXLw8p/gkDmam3PPoPNYsEAmqivw=="], + "@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.29.2", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-QqaECBd/kRMIqIVHaRT/7y7uvaAPb2ZY4geI8igIcE8JTov38qXzfv55jnG+5Lu5wEbyg1FB0n9KVSIz4HPb4Q=="], - "@anthropic-ai/foundry-sdk": ["@anthropic-ai/foundry-sdk@0.2.3", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1" } }, "sha512-pD5yYnAeem5s8wDLbdf8/N8CejF/edRd9TJV+0PrT9tLKv6ggQimnr7d05pQn6FrIYACPmty9hekCo2JgepP0w=="], + "@anthropic-ai/foundry-sdk": ["@anthropic-ai/foundry-sdk@0.2.4", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1" } }, "sha512-RlM2UnOU31ahEUFSXN4v5TzLoF+cR66JU22wZhbLEpY//LzNJeepZii2wXsYkdnoUI8EXUrVFySEoOcjfOkX+Q=="], "@anthropic-ai/sandbox-runtime": ["@anthropic-ai/sandbox-runtime@0.0.55", "", { "dependencies": { "@pondwader/socks5-server": "^1.0.10", "commander": "^12.1.0", "node-forge": "^1.4.0", "shell-quote": "^1.8.4", "zod": "^3.24.1" }, "bin": { "srt": "dist/cli.js" } }, "sha512-XGTLbzitn0y6OCdmqP5aYrBR0HlNi87ue++m9vMPxcNwaQeOj9i3f9CSWmXua8mv+Q8E55k4ZGlzJmpDHzQFBw=="], @@ -149,21 +149,21 @@ "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.33", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-yPrIm0tgMN44utkOdKe1Bkvt/NRxcWJFdhbC98tTWxWGEBAzB3lksPhGuB8TtKC0/XyfXcGrBPzx/OKXXexa4w=="], - "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.36", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gE+CGuPZD1eqUWGSrM8CXDjlwuPujIuwI+IlorD1wE2RcANKKT4jscB9GY1nTJbjmXzD18sycsYbgCG5m3n4/g=="], + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-YxmL1Ar7bV9ZkJvtv+lj+MF6le76NnZnbmgySyZuxpZaCv0/1lZ3KxKy6RIj1h9ui2hWnVjZxbmTMeO0y/NrQA=="], - "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/fetch-http-handler": "^5.4.1", "@smithy/node-http-handler": "^4.7.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-cHZo3bV6zN9joDQ2AYVctfzHTKStxWKwnGu0z7GwCUC+DAtB3qL/+26l+a63RbmFbVvb1JK+0vJKodN3hRMwyw=="], + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.78", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-3VHknDRngoebsaqyT1iR75b5cBT2zfEtODK6yYmYltljkK1HEAQLs2FEd1IpWqAeOYdMGaG9wikngWLTufY7RA=="], - "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.40", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/credential-provider-env": "^3.972.36", "@aws-sdk/credential-provider-http": "^3.972.38", "@aws-sdk/credential-provider-login": "^3.972.40", "@aws-sdk/credential-provider-process": "^3.972.36", "@aws-sdk/credential-provider-sso": "^3.972.40", "@aws-sdk/credential-provider-web-identity": "^3.972.40", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/credential-provider-imds": "^4.3.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0NFGS9I3PD2yMveQqqpwpRdyZVStzgk0Yr2rZHh80kV/QNqQCK5lSrksvU3nBcRNSUF5Uk8rL3Xk0EVR+UVAnA=="], + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.21", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/credential-provider-env": "^3.972.76", "@aws-sdk/credential-provider-http": "^3.972.78", "@aws-sdk/credential-provider-login": "^3.972.83", "@aws-sdk/credential-provider-process": "^3.972.76", "@aws-sdk/credential-provider-sso": "^3.973.20", "@aws-sdk/credential-provider-web-identity": "^3.972.82", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-aLx9hbCX6mGAyv4ZZd8UagJ4RASHVJw6BFDSIyuKmLkqNL6H47EjBPK6qaJ4B5TDV6mc+vMa0SJFYyo4mkvVQg=="], "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.40", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-IEIl+UQnrEjZP53TSl91e8LBephi4i1Mt9WZrMgN8pOg6xPOLZdkN1GhsEzjkMD1TQy4Fp2dwWA/9ToTQFOlLA=="], - "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.41", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.36", "@aws-sdk/credential-provider-http": "^3.972.38", "@aws-sdk/credential-provider-ini": "^3.972.40", "@aws-sdk/credential-provider-process": "^3.972.36", "@aws-sdk/credential-provider-sso": "^3.972.40", "@aws-sdk/credential-provider-web-identity": "^3.972.40", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/credential-provider-imds": "^4.3.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-h6BlclpsPGkx7Pv7ukr8oKVqN3jvxnH5n9ZIUQa8focr1ZkKd2MYiPJ2Nv9GI97dohJVJBfZAsTp/qoZL5R1pw=="], + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.88", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.76", "@aws-sdk/credential-provider-http": "^3.972.78", "@aws-sdk/credential-provider-ini": "^3.973.21", "@aws-sdk/credential-provider-process": "^3.972.76", "@aws-sdk/credential-provider-sso": "^3.973.20", "@aws-sdk/credential-provider-web-identity": "^3.972.82", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/credential-provider-imds": "^4.5.2", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-AG0KID5qhQPzrFkh5b0Cws1Amytvzo0L0Xj6hMgJXEwD6ZXAqninDhSNAWJiN5zU4YVVZnuidnqwPwYJ04IamA=="], - "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.36", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-eDQ6X7clTAOxXegOx4rGT1hyfusGEYdJGCGo0Ym2+CKeMQBjk+SJSxSVev11NJew5xJHJ/c3hryl2awKaxuSEA=="], + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-pdhKFTXv1zPYhh92MTlp0jVbLWvUEV9v+rziuEkoskGGQ2iJskxHTO38nA8UAbT/N4IM3n8OZJ9rIY5IBO15/A=="], - "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.40", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/token-providers": "3.1047.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-jaABbsoOkGlKg5kaHetYmUV6mWM57H89ia0Yksom1XxC847mfjmEVb4p7VijS1sjPbXjUii4cftJuwsl4MXkRg=="], + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.20", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/token-providers": "3.1155.0", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-DlxrAB3MnjNLx/da7b89W+3DyJQYso/q7Ns15MsST2IV2nkAYQOYTUCaffNSjgrpMP99Dm2mV2jxzcbyF94ZfA=="], - "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.40", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-bfIrM8IIzbRtXRQWx/vNEUBLTImLZyX5uKk8uSdeSAZ4Mj3Yi4UnRJLK4FkQLWErbM3McpVLQ1DaM6XO66Ed5g=="], + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.82", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-LUo46IqPTJ4xKBsrLuoaxiv/D2GTBCcFDMD1y3jKIqPR/p7Rw14RJFZ0iRWftqZtyLPQX9JMGnSi3mBu4y/pNA=="], "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1047.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1047.0", "@aws-sdk/core": "^3.974.10", "@aws-sdk/credential-provider-cognito-identity": "^3.972.33", "@aws-sdk/credential-provider-env": "^3.972.36", "@aws-sdk/credential-provider-http": "^3.972.38", "@aws-sdk/credential-provider-ini": "^3.972.40", "@aws-sdk/credential-provider-login": "^3.972.40", "@aws-sdk/credential-provider-node": "^3.972.41", "@aws-sdk/credential-provider-process": "^3.972.36", "@aws-sdk/credential-provider-sso": "^3.972.40", "@aws-sdk/credential-provider-web-identity": "^3.972.40", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/credential-provider-imds": "^4.3.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Fgx+rIstqocZSgHG2zqE/bjVr0TXBvhof0LEf6eQI+mJK5S6Qehkv0tpA0ln2w9nIa08tlaoXq3qXdsNOU8qmw=="], @@ -211,13 +211,15 @@ "@azure/core-client": ["@azure/core-client@1.10.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-1D2LpsU7y9xrqKjdIbsB7PlrRePw0xsVV8p+AKTlzITrWmscajryfJCdDJB/oGwvDI5HmRo04eMMADB67uwAwQ=="], + "@azure/core-process": ["@azure/core-process@1.0.0", "", {}, "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg=="], + "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.24.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.4", "tslib": "^2.6.2" } }, "sha512-PpLsoDQ3AMmKZ0VU+0GrmqMxgp/sExjlVm4R+nLWngeoEGAzOIPVifaxKGU5gMv+nWELUoHfvrolWD+ZS/nFJg=="], "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="], - "@azure/identity": ["@azure/identity@4.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^5.1.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw=="], + "@azure/identity": ["@azure/identity@4.13.3", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-process": "^1.0.0", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^6.0.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A=="], "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], @@ -225,7 +227,7 @@ "@azure/msal-common": ["@azure/msal-common@16.11.0", "", {}, "sha512-UikJOtMwkFpZNzTH6Dqk8UTUPbow15zH3e0UjGYZy69lYENW/S05gMLhbxI2eonz66uALhIljvhsSMEb6+O30g=="], - "@azure/msal-node": ["@azure/msal-node@5.3.1", "", { "dependencies": { "@azure/msal-common": "16.11.0", "jsonwebtoken": "^9.0.0" } }, "sha512-sqqv3L1UOI4KDXonNtbxPYUgbSWVXqxvmmb6BUw9n4P/UXgG+cVur3dLWQN4Cz7qQ+UJROCCxMXlksm7gIq0Sw=="], + "@azure/msal-node": ["@azure/msal-node@6.0.1", "", { "dependencies": { "@azure/msal-common": "16.14.1", "jsonwebtoken": "^9.0.0" } }, "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ=="], "@babel/runtime": ["@babel/runtime@7.29.2", "", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], @@ -255,7 +257,7 @@ "@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA=="], - "@grpc/grpc-js": ["@grpc/grpc-js@1.14.3", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA=="], + "@grpc/grpc-js": ["@grpc/grpc-js@1.14.6", "", { "dependencies": { "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-SgC/B3/VZ8lTyOZ4Cwk6f1skWhmkNAaVfiX90qd5u1rRXGKk1//8FTUEN9GvOUq7rfWIqAYkA3CoyumYVpOS6g=="], "@grpc/proto-loader": ["@grpc/proto-loader@0.8.1", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.5.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg=="], @@ -477,7 +479,7 @@ "@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="], - "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w=="], + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], "@smithy/eventstream-codec": ["@smithy/eventstream-codec@2.2.0", "", { "dependencies": { "@aws-crypto/crc32": "3.0.0", "@smithy/types": "^2.12.0", "@smithy/util-hex-encoding": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-8janZoJw85nJmQZc4L8TuePp2pk1nxLgkxIR0TUjKJ5Dkj5oelB9WtiSSGXCQvNsJl0VSTvK/2ueMXxvpa9GVw=="], @@ -709,7 +711,7 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], @@ -795,7 +797,7 @@ "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], - "fflate": ["fflate@0.8.2", "", {}, "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A=="], + "fflate": ["fflate@0.8.3", "", {}, "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA=="], "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], @@ -857,7 +859,7 @@ "graphology-indices": ["graphology-indices@0.17.0", "", { "dependencies": { "graphology-utils": "^2.4.2", "mnemonist": "^0.39.0" }, "peerDependencies": { "graphology-types": ">=0.20.0" } }, "sha512-A7RXuKQvdqSWOpn7ZVQo4S33O0vCfPBnUSf7FwE0zNCasqwZVUaCXePuWo5HBpWw68KJcwObZDHpFk6HKH6MYQ=="], - "graphology-metrics": ["graphology-metrics@2.4.0", "", { "dependencies": { "graphology-indices": "^0.17.0", "graphology-shortest-path": "^2.0.0", "graphology-utils": "^2.4.4", "mnemonist": "^0.39.0", "pandemonium": "2.4.1" }, "peerDependencies": { "graphology-types": ">=0.20.0" } }, "sha512-7WOfOP+mFLCaTJx55Qg4eY+211vr1/b3D/R3biz3SXGhAaCVcWYkfabnmO4O4WBNWANEHtVnFrGgJ0kj6MM6xw=="], + "graphology-metrics": ["graphology-metrics@2.4.2", "", { "dependencies": { "graphology-indices": "^0.17.0", "graphology-shortest-path": "^2.0.0", "graphology-utils": "^2.4.4", "mnemonist": "^0.39.0", "pandemonium": "2.4.1" }, "peerDependencies": { "graphology-types": ">=0.20.0" } }, "sha512-6/ykELkmU2RuicM5zzUaIIvHPwfE94uyhpPkpQm+a9UvHU/2hkJHHIOkj4CFYKyuXNfzMw/xHZiuQfOr3nWLUw=="], "graphology-shortest-path": ["graphology-shortest-path@2.1.0", "", { "dependencies": { "@yomguithereal/helpers": "^1.1.1", "graphology-indices": "^0.17.0", "graphology-utils": "^2.4.3", "mnemonist": "^0.39.0" }, "peerDependencies": { "graphology-types": ">=0.20.0" } }, "sha512-KbT9CTkP/u72vGEJzyRr24xFC7usI9Es3LMmCPHGwQ1KTsoZjxwA9lMKxfU0syvT/w+7fZUdB/Hu2wWYcJBm6Q=="], @@ -889,7 +891,7 @@ "iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="], - "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "ignore": ["ignore@7.0.13", "", {}, "sha512-2E8Q6rh+1RRSq/YdaGsNfXsEZy9FVqv6BSSrkmZJP7BnaoPnRfKL+YaofPvP+5rzdk144qSm6i4+hvPgUcbV4Q=="], "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="], @@ -1049,7 +1051,7 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], + "p-map": ["p-map@7.0.8", "", {}, "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], @@ -1073,7 +1075,7 @@ "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], - "picomatch": ["picomatch@4.0.4", "", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], @@ -1205,7 +1207,7 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "turndown": ["turndown@7.2.2", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ=="], + "turndown": ["turndown@7.2.4", "", { "dependencies": { "@mixmark-io/domino": "^2.2.0" } }, "sha512-I8yFsfRzmzK0WV1pNNOA4A7y4RDfFxPRxb3t+e3ui14qSGOxGtiSP6GjeX+Y6CHb7HYaFj7ECUD7VE5kQMZWGQ=="], "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], @@ -1253,17 +1255,17 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], - "xss": ["xss@1.0.15", "", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-FVdlVVC67WOIPvfOwhoMETV72f6GbW7aOabBC3WxN/oUdoEMDyLz4OgRv5/gck2ZeNqEQu+Tb0kloovXOfpYVg=="], + "xss": ["xss@1.0.16", "", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-CsEvZ0QNH9vVgf3qldg+mJmmIwreZZEu/JS1SgldJrJqZgwyMrmIfENt1BJlkj/cPxO0c+R80kB5OMRtUPgqpQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -1313,21 +1315,89 @@ "@aws-sdk/credential-provider-cognito-identity/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - "@aws-sdk/credential-provider-env/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + + "@aws-sdk/credential-provider-env/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + + "@aws-sdk/credential-provider-env/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + + "@aws-sdk/credential-provider-http/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + + "@aws-sdk/credential-provider-http/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + + "@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], - "@aws-sdk/credential-provider-http/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/credential-provider-http/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], - "@aws-sdk/credential-provider-ini/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.83", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-9bJU6FjRacF4FK/QHf8Dyxhur7B1grRj8C1Yh4IkpoFW2exQflPdCv0VrbemLW9ZctLNle+AE7TETs9KNvuLYg=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + + "@aws-sdk/credential-provider-ini/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + + "@aws-sdk/credential-provider-ini/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], "@aws-sdk/credential-provider-login/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - "@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/credential-provider-node/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + + "@aws-sdk/credential-provider-node/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + + "@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + + "@aws-sdk/credential-provider-process/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + + "@aws-sdk/credential-provider-process/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1155.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-iIcQnYLhWuq9qOLQbIWFrxF3PF0svVeZvHnUS4DUnO4zUFmFHIyGyjqrntXG+e/5PUgfavV0pYfDIKHxTzN2LA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + + "@aws-sdk/credential-provider-sso/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + + "@aws-sdk/credential-provider-sso/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + + "@aws-sdk/credential-provider-web-identity/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + + "@aws-sdk/credential-provider-web-identity/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.36", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gE+CGuPZD1eqUWGSrM8CXDjlwuPujIuwI+IlorD1wE2RcANKKT4jscB9GY1nTJbjmXzD18sycsYbgCG5m3n4/g=="], + + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/fetch-http-handler": "^5.4.1", "@smithy/node-http-handler": "^4.7.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-cHZo3bV6zN9joDQ2AYVctfzHTKStxWKwnGu0z7GwCUC+DAtB3qL/+26l+a63RbmFbVvb1JK+0vJKodN3hRMwyw=="], + + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.40", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/credential-provider-env": "^3.972.36", "@aws-sdk/credential-provider-http": "^3.972.38", "@aws-sdk/credential-provider-login": "^3.972.40", "@aws-sdk/credential-provider-process": "^3.972.36", "@aws-sdk/credential-provider-sso": "^3.972.40", "@aws-sdk/credential-provider-web-identity": "^3.972.40", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/credential-provider-imds": "^4.3.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-0NFGS9I3PD2yMveQqqpwpRdyZVStzgk0Yr2rZHh80kV/QNqQCK5lSrksvU3nBcRNSUF5Uk8rL3Xk0EVR+UVAnA=="], - "@aws-sdk/credential-provider-process/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.36", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-eDQ6X7clTAOxXegOx4rGT1hyfusGEYdJGCGo0Ym2+CKeMQBjk+SJSxSVev11NJew5xJHJ/c3hryl2awKaxuSEA=="], - "@aws-sdk/credential-provider-sso/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.40", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/token-providers": "3.1047.0", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-jaABbsoOkGlKg5kaHetYmUV6mWM57H89ia0Yksom1XxC847mfjmEVb4p7VijS1sjPbXjUii4cftJuwsl4MXkRg=="], - "@aws-sdk/credential-provider-web-identity/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.40", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-bfIrM8IIzbRtXRQWx/vNEUBLTImLZyX5uKk8uSdeSAZ4Mj3Yi4UnRJLK4FkQLWErbM3McpVLQ1DaM6XO66Ed5g=="], + + "@aws-sdk/credential-providers/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w=="], "@aws-sdk/credential-providers/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], @@ -1369,6 +1439,8 @@ "@aws-sdk/xml-builder/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@azure/msal-node/@azure/msal-common": ["@azure/msal-common@16.14.1", "", {}, "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], @@ -1379,7 +1451,9 @@ "@smithy/core/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - "@smithy/credential-provider-imds/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@smithy/credential-provider-imds/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + + "@smithy/credential-provider-imds/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], "@smithy/eventstream-codec/@aws-crypto/crc32": ["@aws-crypto/crc32@3.0.0", "", { "dependencies": { "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", "tslib": "^1.11.1" } }, "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA=="], @@ -1467,6 +1541,60 @@ "@aws-sdk/client-sts/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.49", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.49", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.49", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], + "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], @@ -1523,6 +1651,12 @@ "@aws-sdk/client-sts/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], diff --git a/package.json b/package.json index 6b21190c5b..941e313c2f 100644 --- a/package.json +++ b/package.json @@ -90,18 +90,18 @@ "@vscode/ripgrep": "1.18.0" }, "devDependencies": { - "@alcalzone/ansi-tokenize": "0.3.0", - "@anthropic-ai/bedrock-sdk": "0.29.1", - "@anthropic-ai/foundry-sdk": "0.2.3", + "@alcalzone/ansi-tokenize": "0.3.1", + "@anthropic-ai/bedrock-sdk": "0.29.2", + "@anthropic-ai/foundry-sdk": "0.2.4", "@anthropic-ai/sandbox-runtime": "0.0.55", "@anthropic-ai/sdk": "0.94.0", "@aws-sdk/client-bedrock": "3.1047.0", "@aws-sdk/client-sts": "3.1047.0", - "@aws-sdk/credential-provider-node": "3.972.41", - "@azure/identity": "^4.13.1", + "@aws-sdk/credential-provider-node": "3.972.88", + "@azure/identity": "4.13.3", "@commander-js/extra-typings": "12.1.0", - "@grpc/grpc-js": "^1.14.3", - "@grpc/proto-loader": "^0.8.0", + "@grpc/grpc-js": "1.14.6", + "@grpc/proto-loader": "0.8.1", "@modelcontextprotocol/sdk": "1.29.0", "@sentry/node": "^10.70.0", "@smithy/core": "3.24.3", @@ -121,21 +121,21 @@ "cli-highlight": "2.1.11", "commander": "12.1.0", "cross-spawn": "7.0.6", - "diff": "8.0.3", + "diff": "8.0.4", "duck-duck-scrape": "^2.2.7", "emoji-regex": "10.6.0", "env-paths": "3.0.0", "eslint": "9.39.5", "execa": "9.6.1", - "fflate": "0.8.2", + "fflate": "0.8.3", "figures": "6.1.0", "fuse.js": "7.1.0", "get-east-asian-width": "1.5.0", "google-auth-library": "10.6.2", "graphology": "0.26.0", - "graphology-metrics": "2.4.0", + "graphology-metrics": "2.4.2", "https-proxy-agent": "7.0.6", - "ignore": "7.0.5", + "ignore": "7.0.13", "indent-string": "5.0.0", "js-tiktoken": "1.0.21", "jsonc-parser": "3.3.1", @@ -143,8 +143,8 @@ "lodash-es": "4.18.1", "lru-cache": "11.2.7", "marked": "15.0.12", - "p-map": "7.0.4", - "picomatch": "4.0.4", + "p-map": "7.0.8", + "picomatch": "4.0.7", "proper-lockfile": "4.1.2", "qrcode": "1.5.4", "react": "19.2.4", @@ -157,7 +157,7 @@ "supports-hyperlinks": "3.2.0", "tree-kill": "1.2.2", "tree-sitter-wasms": "0.1.13", - "turndown": "7.2.2", + "turndown": "7.2.4", "type-fest": "4.41.0", "typescript": "6.0.3", "undici": "7.28.0", @@ -165,9 +165,9 @@ "vscode-languageserver-protocol": "3.17.5", "web-tree-sitter": "0.25.10", "wrap-ansi": "9.0.2", - "ws": "8.21.0", - "xss": "1.0.15", - "yaml": "2.8.3", + "ws": "8.21.3", + "xss": "1.0.16", + "yaml": "2.8.4", "zod": "3.25.76" }, "peerDependencies": { diff --git a/web/bun.lock b/web/bun.lock index 3869f0a101..5450acf508 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -5,28 +5,30 @@ "": { "name": "openclaude-web", "dependencies": { - "@astrojs/check": "0.9.9", - "@astrojs/sitemap": "3.7.3", - "@fontsource-variable/geist-mono": "^5.2.5", - "astro": "6.4.6", + "@astrojs/check": "0.9.12", + "@astrojs/sitemap": "3.7.4", + "@fontsource-variable/geist-mono": "5.2.8", + "astro": "6.4.8", "typescript": "^6.0.3", }, }, }, "packages": { - "@astrojs/check": ["@astrojs/check@0.9.9", "", { "dependencies": { "@astrojs/language-server": "^2.16.7", "chokidar": "^4.0.3", "kleur": "^4.1.5", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "astro-check": "bin/astro-check.js" } }, "sha512-A5UW8uIuErLWEoRQvzgXpO1gTjUFtK8r7nU2Z7GewAMxUb7bPvpk11qaKKgxqXlHJWlAvaaxy+Xg28A6bmQ1Tg=="], + "@astrojs/astro2tsx": ["@astrojs/astro2tsx@0.1.3", "", { "dependencies": { "@emnapi/core": "1.11.3", "@napi-rs/wasm-runtime": "1.2.4" } }, "sha512-GMDC7Eo0myBUA/QLBl0u+OcegJ8LiBhccmNZR34KmprQNP8gQUJlWlOOZWUsxNA2rNS6GuPYcGDyLoPnjmn4/Q=="], + + "@astrojs/check": ["@astrojs/check@0.9.12", "", { "dependencies": { "@astrojs/language-server": "^2.17.3", "chokidar": "^5.0.0", "kleur": "^4.1.5", "yargs": "^18.2.0" }, "peerDependencies": { "typescript": "^5.0.0 || ^6.0.0" }, "bin": { "astro-check": "./bin/astro-check.js" } }, "sha512-hazpqxIfZWj2VhyPyR0pOEQ/IhFDM/2nIApL96zMXABdD8NrUKYDmhxTy7h+drTDl9MQG/Sd1ET/4amadVJUUA=="], "@astrojs/compiler": ["@astrojs/compiler@4.0.0", "", {}, "sha512-eouss7G8ygdZqHuke033VMcVw5HTZUu+PXd/h06DGDUg/jt5btPYPqh66ENWw/mU78rBrf/oeC4oqoBwMtDMNA=="], "@astrojs/internal-helpers": ["@astrojs/internal-helpers@0.10.0", "", { "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", "js-yaml": "^4.1.1", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "unified": "^11.0.5" } }, "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw=="], - "@astrojs/language-server": ["@astrojs/language-server@2.16.10", "", { "dependencies": { "@astrojs/compiler": "^2.13.1", "@astrojs/yaml2ts": "^0.2.4", "@jridgewell/sourcemap-codec": "^1.5.5", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.16", "volar-service-css": "0.0.70", "volar-service-emmet": "0.0.70", "volar-service-html": "0.0.70", "volar-service-prettier": "0.0.70", "volar-service-typescript": "0.0.70", "volar-service-typescript-twoslash-queries": "0.0.70", "volar-service-yaml": "0.0.70", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.1.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "./bin/nodeServer.js" } }, "sha512-87VQ/5GSdHlRnUA+hGuerYyIGAj+9RbZmATyuKLEUePinUXhQ5YkRnRrHhOD9sSi5JOErLjrLkHnfZFEvGrV8w=="], + "@astrojs/language-server": ["@astrojs/language-server@2.17.3", "", { "dependencies": { "@astrojs/astro2tsx": "^0.1.2", "@astrojs/yaml2ts": "^0.2.4", "@volar/kit": "~2.4.28", "@volar/language-core": "~2.4.28", "@volar/language-server": "~2.4.28", "@volar/language-service": "~2.4.28", "muggle-string": "^0.4.1", "tinyglobby": "^0.2.17", "volar-service-css": "0.0.71", "volar-service-emmet": "0.0.71", "volar-service-html": "0.0.71", "volar-service-prettier": "0.0.71", "volar-service-typescript": "0.0.71", "volar-service-typescript-twoslash-queries": "0.0.71", "volar-service-yaml": "0.0.71", "vscode-html-languageservice": "^5.6.2", "vscode-uri": "^3.2.0" }, "peerDependencies": { "prettier": "^3.0.0", "prettier-plugin-astro": ">=0.11.0" }, "optionalPeers": ["prettier", "prettier-plugin-astro"], "bin": { "astro-ls": "./bin/nodeServer.js" } }, "sha512-QIAFu1e/4quJMTPdmBOm+ZPXc5NDGZE4/YQjgR3EgjhGLl/tdidaWJDr5WmO2lesVc+BW1qN/xNBURmuePDG2A=="], "@astrojs/markdown-remark": ["@astrojs/markdown-remark@7.2.0", "", { "dependencies": { "@astrojs/internal-helpers": "0.10.0", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", "hast-util-to-text": "^4.0.2", "mdast-util-definitions": "^6.0.0", "rehype-raw": "^7.0.0", "rehype-stringify": "^10.0.1", "remark-gfm": "^4.0.1", "remark-parse": "^11.0.0", "remark-rehype": "^11.1.2", "remark-smartypants": "^3.0.2", "unified": "^11.0.5", "unist-util-remove-position": "^5.0.0", "unist-util-visit": "^5.1.0", "unist-util-visit-parents": "^6.0.2", "vfile": "^6.0.3" } }, "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw=="], "@astrojs/prism": ["@astrojs/prism@4.0.2", "", { "dependencies": { "prismjs": "^1.30.0" } }, "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA=="], - "@astrojs/sitemap": ["@astrojs/sitemap@3.7.3", "", { "dependencies": { "sitemap": "^9.0.0", "stream-replace-string": "^2.0.0", "zod": "^4.3.6" } }, "sha512-f8euLVsyeAmAkSm/1M2Kb8sL8byQmfgbvBNaHFItCheTj/IpiJYSEWVcqDHZ/yEHxiS7+w87mQkzwZaPHmk5GA=="], + "@astrojs/sitemap": ["@astrojs/sitemap@3.7.4", "", { "dependencies": { "sitemap": "^9.0.0", "zod": "^4.3.6" } }, "sha512-LbKNC24bdUWcQf/pThB6qLlSqHojxGjZDURIzFocY8rlWnAn2t74nnhnK6S5x0NHriHoAduLEpVjRykmeGiVvA=="], "@astrojs/telemetry": ["@astrojs/telemetry@3.3.2", "", { "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", "is-wsl": "^3.1.1", "which-pm-runs": "^1.1.0" } }, "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ=="], @@ -60,8 +62,12 @@ "@emmetio/stream-reader-utils": ["@emmetio/stream-reader-utils@0.1.0", "", {}, "sha512-ZsZ2I9Vzso3Ho/pjZFsmmZ++FWeEd/txqybHTm4OgaZzdS8V9V/YYWQwg5TC38Z7uLWUV1vavpLLbjJtKubR1A=="], + "@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" } }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], + "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g=="], + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], @@ -168,6 +174,8 @@ "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.4", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g=="], + "@oslojs/encoding": ["@oslojs/encoding@1.1.0", "", {}, "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ=="], "@rollup/pluginutils": ["@rollup/pluginutils@5.4.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg=="], @@ -238,6 +246,8 @@ "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], + "@types/debug": ["@types/debug@4.1.13", "", { "dependencies": { "@types/ms": "*" } }, "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw=="], "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], @@ -278,9 +288,11 @@ "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], - "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "ajv-i18n": ["ajv-i18n@4.2.0", "", { "peerDependencies": { "ajv": "^8.0.0-beta.0" } }, "sha512-v/ei2UkCEeuKNXh8RToiFsUclmU+G57LO1Oo22OagNMENIw+Yb8eMwvHu7Vn9fmkjJyv6XclhJ8TbuigSglPkg=="], + + "ansi-regex": ["ansi-regex@6.4.0", "", {}, "sha512-KzTVk2tCWAHtYrvvvaP8bJKJq2pVinhLcGEQdtLIYPbmNGNyYe8QwNaTUYQp2J7/vIsUKt5QCqAfUkYyG9DkOw=="], - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], "anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="], @@ -292,7 +304,7 @@ "array-iterate": ["array-iterate@2.0.1", "", {}, "sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg=="], - "astro": ["astro@6.4.6", "", { "dependencies": { "@astrojs/compiler": "^4.0.0", "@astrojs/internal-helpers": "0.10.0", "@astrojs/markdown-remark": "7.2.0", "@astrojs/telemetry": "3.3.2", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", "vfile": "^6.0.3", "vite": "^7.3.2", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "./bin/astro.mjs" } }, "sha512-48OBTBKR9ctbf+DQxpOuxGl8ebfn59zTuNQMBzptmG/Mi/H8IdfMSbJgGuX1I/4U6g9yazG1p4BHlf4+2hWU4Q=="], + "astro": ["astro@6.4.8", "", { "dependencies": { "@astrojs/compiler": "^4.0.0", "@astrojs/internal-helpers": "0.10.0", "@astrojs/markdown-remark": "7.2.0", "@astrojs/telemetry": "3.3.2", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", "@rollup/pluginutils": "^5.3.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", "cookie": "^1.1.1", "devalue": "^5.8.1", "diff": "^8.0.3", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.27.3", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", "js-yaml": "^4.1.1", "jsonc-parser": "^3.3.1", "magic-string": "^0.30.21", "magicast": "^0.5.2", "mrmime": "^2.0.1", "neotraverse": "^0.6.18", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", "package-manager-detector": "^1.6.0", "piccolore": "^0.1.3", "picomatch": "^4.0.4", "rehype": "^13.0.2", "semver": "^7.7.4", "shiki": "^4.0.2", "smol-toml": "^1.6.0", "svgo": "^4.0.1", "tinyclip": "^0.1.12", "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", "unifont": "~0.7.4", "unist-util-visit": "^5.1.0", "unstorage": "^1.17.5", "vfile": "^6.0.3", "vite": "^7.3.2", "vitefu": "^1.1.2", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", "zod": "^4.3.6" }, "optionalDependencies": { "sharp": "^0.34.0" }, "bin": { "astro": "./bin/astro.mjs" } }, "sha512-KK5lX90uU9EeVaTjINyj3sy9/NFXVa59aowaqbWBDDKLXZh4rr7GwIaCFYVetE22MJtsCNFerQXn0vlCLmpP/Q=="], "axobject-query": ["axobject-query@4.1.0", "", {}, "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ=="], @@ -308,18 +320,14 @@ "character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], "ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - "comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="], "commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], @@ -370,7 +378,7 @@ "emmet": ["emmet@2.4.11", "", { "dependencies": { "@emmetio/abbreviation": "^2.3.3", "@emmetio/css-abbreviation": "^2.1.8" } }, "sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], "entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], @@ -410,6 +418,8 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="], + "get-tsconfig": ["get-tsconfig@5.0.0-beta.4", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ=="], "github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="], @@ -446,8 +456,6 @@ "is-docker": ["is-docker@4.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA=="], - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], @@ -616,7 +624,7 @@ "radix3": ["radix3@1.1.2", "", {}, "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA=="], - "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], "regex": ["regex@6.1.0", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg=="], @@ -644,8 +652,6 @@ "request-light": ["request-light@0.7.0", "", {}, "sha512-lMbBMrDoxgsyO+yB3sDcrDuX85yYt7sS8BfQd11jtbW/z5ZWgLZRcEGLsLoYw7I0WSUGQBs8CC8ScIxkTX1+6Q=="], - "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], @@ -678,13 +684,11 @@ "space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="], - "stream-replace-string": ["stream-replace-string@2.0.0", "", {}, "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], "stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="], - "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], "svgo": ["svgo@4.0.1", "", { "dependencies": { "commander": "^11.1.0", "css-select": "^5.1.0", "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", "picocolors": "^1.1.1", "sax": "^1.5.0" }, "bin": "./bin/svgo.js" }, "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w=="], @@ -750,19 +754,19 @@ "vitefu": ["vitefu@1.1.3", "", { "peerDependencies": { "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["vite"] }, "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg=="], - "volar-service-css": ["volar-service-css@0.0.70", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-K1qyOvBpE3rzdAv3e4/6Rv5yizrYPy5R/ne3IWCAzLBuMO4qBMV3kSqWzj6KUVe6S0AnN6wxF7cRkiaKfYMYJw=="], + "volar-service-css": ["volar-service-css@0.0.71", "", { "dependencies": { "vscode-css-languageservice": "^6.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-wRRFt9BpjMKCazcgOh67MSjUjiWUCAh99DyYSDIOTuxaRjEtDC7PpB0k1Y1wbJIW/pVtMUSVbpPo3UGSm0Byxw=="], - "volar-service-emmet": ["volar-service-emmet@0.0.70", "", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-xi5bC4m/VyE3zy/n2CXspKeDZs3qA41tHLTw275/7dNWM/RqE2z3BnDICQybHIVp/6G1iOQj5c1qXMgQC08TNg=="], + "volar-service-emmet": ["volar-service-emmet@0.0.71", "", { "dependencies": { "@emmetio/css-parser": "^0.4.1", "@emmetio/html-matcher": "^1.3.0", "@vscode/emmet-helper": "^2.9.3", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-zqjzt6bN95e3CUstBm0PBFAJnrfz0ZAARka87fart46/gNCLLuP3Vujy8V/J8HEziTFLnfkgIASLFYPUhonJcA=="], - "volar-service-html": ["volar-service-html@0.0.70", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-eR6vCgMdmYAo4n+gcT7DSyBQbwB8S3HZZvSagTf0sxNaD4WppMCFfpqWnkrlGStPKMZvMiejRRVmqsX9dYcTvQ=="], + "volar-service-html": ["volar-service-html@0.0.71", "", { "dependencies": { "vscode-html-languageservice": "^5.3.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-e8tHPhgQ7ooLfudAEIku+kgd9pWkq3SSz8RbnQDI1+Eb8wbenkLGHqoirLqz5ORLV6wIMr2Iv08RWBG5eOcgpw=="], - "volar-service-prettier": ["volar-service-prettier@0.0.70", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-Z6BCFSpGVCd8BPAsZ785Kce1BGlWd5ODqmqZGVuB14MJvrR4+CYz6cDy4F+igmE1gMifqfvMhdgT8Aud4M5ngg=="], + "volar-service-prettier": ["volar-service-prettier@0.0.71", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0", "prettier": "^2.2 || ^3.0" }, "optionalPeers": ["@volar/language-service", "prettier"] }, "sha512-Rz7JVH3qD108UCdmIEiZvOBNljMt2nLFdbN8AXcDfn7xD9F5I2aCIsDVqBbXw21PsnxG0b7MfwtNF+zPS/NKUg=="], - "volar-service-typescript": ["volar-service-typescript@0.0.70", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-l46Bx4cokkUedTd74ojO5H/zqHZJ8SUuyZ0IB8JN4jfRqUM3bQFBHoOwlZCyZmOeO0A3RQNkMnFclxO4c++gsg=="], + "volar-service-typescript": ["volar-service-typescript@0.0.71", "", { "dependencies": { "path-browserify": "^1.0.1", "semver": "^7.6.2", "typescript-auto-import-cache": "^0.3.5", "vscode-languageserver-textdocument": "^1.0.11", "vscode-nls": "^5.2.0", "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-yTtM/BVT6hoyEYnDtaCyAtNhdNeS/mhTTABlBOdw3NNiRBUin3IznFJpgfjer4c6RYopiPjjQjc9VFhxVl1mLw=="], - "volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.70", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-IdD13Z9N2Bu8EM6CM0fDV1E69olEYGHDU25X51YXmq8Y0CmJ2LNj6gOiBJgpS5JGUqFzECVhMNBW7R0sPdRTMQ=="], + "volar-service-typescript-twoslash-queries": ["volar-service-typescript-twoslash-queries@0.0.71", "", { "dependencies": { "vscode-uri": "^3.0.8" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-9K2k72s4n7rV9s4bX0MyjbX9iBribvKZbBJKuEmTCZfeWJXs6Yh7bGpY4eoc7UufAjvpheBqwyZCOIPBvxCv0A=="], - "volar-service-yaml": ["volar-service-yaml@0.0.70", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.20.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-0c8bXDBeoATF9F6iPIlOuYTuZAC4c+yi0siQo920u7eiBJk8oQmUmg9cDUbR4+Gl++bvGP4plj3fErbJuPqdcQ=="], + "volar-service-yaml": ["volar-service-yaml@0.0.71", "", { "dependencies": { "vscode-uri": "^3.0.8", "yaml-language-server": "~1.23.0" }, "peerDependencies": { "@volar/language-service": "~2.4.0" }, "optionalPeers": ["@volar/language-service"] }, "sha512-qYGWGuVpUTnZGu5P/CR4KLK4aIR8RrcVnmfZ2eRcj9q/I8VZCoC5yy9FtEvfNvnDp4MU17yhdJcvpQPIqhJS2Q=="], "vscode-css-languageservice": ["vscode-css-languageservice@6.3.10", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "vscode-languageserver-textdocument": "^1.0.12", "vscode-languageserver-types": "3.17.5", "vscode-uri": "^3.1.0" } }, "sha512-eq5N9Er3fC4vA9zd9EFhyBG90wtCCuXgRSpAndaOgXMh1Wgep5lBgRIeDgjZBW9pa+332yC9+49cZMW8jcL3MA=="], @@ -782,13 +786,13 @@ "vscode-nls": ["vscode-nls@5.2.0", "", {}, "sha512-RAaHx7B14ZU04EU31pT+rKz2/zSl7xMsfIZuo8pd+KZO6PXtQmpevpq3vxvWNcrGbdmhM/rr5Uw5Mz+NBfhVng=="], - "vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "vscode-uri": ["vscode-uri@3.2.0", "", {}, "sha512-m2gXo3bn0G1kT9InzMf07fTbqMbGtyckj3bH5ktLO+1Ssv+yiATZ4dhwaQv9UZWxJh6E9IFGnQyjgWVDWVBDrg=="], "web-namespaces": ["web-namespaces@2.0.1", "", {}, "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ=="], "which-pm-runs": ["which-pm-runs@1.1.0", "", {}, "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA=="], - "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], "xxhash-wasm": ["xxhash-wasm@1.1.0", "", {}, "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA=="], @@ -796,9 +800,9 @@ "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "yaml-language-server": ["yaml-language-server@1.20.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "prettier": "^3.5.0", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.7.1" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-qhjK/bzSRZ6HtTvgeFvjNPJGWdZ0+x5NREV/9XZWFjIGezew2b4r5JPy66IfOhd5OA7KeFwk1JfmEbnTvev0cA=="], + "yaml-language-server": ["yaml-language-server@1.23.0", "", { "dependencies": { "@vscode/l10n": "^0.0.18", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-i18n": "^4.2.0", "prettier": "^3.8.1", "request-light": "^0.5.7", "vscode-json-languageservice": "4.1.8", "vscode-languageserver": "^9.0.0", "vscode-languageserver-textdocument": "^1.0.1", "vscode-languageserver-types": "^3.16.0", "vscode-uri": "^3.0.2", "yaml": "2.8.3" }, "bin": { "yaml-language-server": "bin/yaml-language-server" } }, "sha512-3qVyCOexLCWw06PQa5kRPwvMWMZ/eZeCRWUvgD6a0OkqL/4iCnxy2WumbWifa937Uo5xhyWJ0uxlU39ljhNh7A=="], - "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + "yargs": ["yargs@18.2.0", "", { "dependencies": { "cliui": "^9.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "string-width": "^8.2.1", "y18n": "^5.0.5", "yargs-parser": "^22.0.0" } }, "sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w=="], "yargs-parser": ["yargs-parser@22.0.0", "", {}, "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw=="], @@ -808,34 +812,64 @@ "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], - "@astrojs/language-server/@astrojs/compiler": ["@astrojs/compiler@2.13.1", "", {}, "sha512-f3FN83d2G/v32ipNClRKgYv30onQlMZX1vCeZMjPsMMPl1mDpmbl0+N5BYo4S/ofzqJyS5hvwacEo0CCVDn/Qg=="], + "@astrojs/language-server/tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "@volar/kit/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "@volar/language-server/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "@volar/language-service/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "@volar/typescript/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "@vscode/emmet-helper/jsonc-parser": ["jsonc-parser@2.3.1", "", {}, "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg=="], + "@vscode/emmet-helper/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "anymatch/picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], + "cliui/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "csso/css-tree": ["css-tree@2.2.1", "", { "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" } }, "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA=="], "dom-serializer/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "is-inside-container/is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - "unstorage/chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "volar-service-css/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "volar-service-emmet/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "volar-service-html/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "volar-service-prettier/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "volar-service-typescript/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "volar-service-typescript-twoslash-queries/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "volar-service-yaml/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], "vscode-css-languageservice/vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + "vscode-css-languageservice/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "vscode-html-languageservice/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + + "vscode-json-languageservice/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], + "vscode-languageserver/vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + "wrap-ansi/string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "yaml-language-server/request-light": ["request-light@0.5.8", "", {}, "sha512-3Zjgh+8b5fhRJBQZoy+zbVKpAQGLyka0MPgW3zruTF4dFFJ8Fqcfu9YsAvi/rvdcaTeWG3MkbZv4WKxAn/84Lg=="], - "yaml-language-server/yaml": ["yaml@2.7.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ=="], + "yaml-language-server/vscode-uri": ["vscode-uri@3.1.0", "", {}, "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ=="], - "yargs/yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yaml-language-server/yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], "csso/css-tree/mdn-data": ["mdn-data@2.0.28", "", {}, "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g=="], - "unstorage/chokidar/readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], - "vscode-languageserver/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], "vscode-languageserver/vscode-languageserver-protocol/vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], diff --git a/web/package.json b/web/package.json index 2ba002d2f3..0203392b24 100644 --- a/web/package.json +++ b/web/package.json @@ -10,10 +10,10 @@ "typecheck": "astro check" }, "dependencies": { - "@astrojs/check": "0.9.9", - "@astrojs/sitemap": "3.7.3", - "@fontsource-variable/geist-mono": "^5.2.5", - "astro": "6.4.6", + "@astrojs/check": "0.9.12", + "@astrojs/sitemap": "3.7.4", + "@fontsource-variable/geist-mono": "5.2.8", + "astro": "6.4.8", "typescript": "^6.0.3" } } From c699c41b0eb4de74d78a77c24b73e074a2ef2c9d Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 19:36:46 -0700 Subject: [PATCH 058/121] chore(deps): update utility minors and HTTP security fixes (#2367) * chore(deps): update utility minors and HTTP security fixes * test(lsp): build platform-native diagnostic path in passiveFeedback test --- bun.lock | 82 ++++++++++++++---------- package.json | 28 ++++---- src/services/lsp/LSPClient.ts | 4 +- src/services/lsp/passiveFeedback.test.ts | 36 +++++++++++ src/services/lsp/passiveFeedback.ts | 13 +--- src/utils/bash/commands.ts | 6 +- web/bun.lock | 4 +- web/package.json | 2 +- 8 files changed, 107 insertions(+), 68 deletions(-) create mode 100644 src/services/lsp/passiveFeedback.test.ts diff --git a/bun.lock b/bun.lock index 12921fa910..faf169fd70 100644 --- a/bun.lock +++ b/bun.lock @@ -18,7 +18,7 @@ "@aws-sdk/client-bedrock": "3.1047.0", "@aws-sdk/client-sts": "3.1047.0", "@aws-sdk/credential-provider-node": "3.972.88", - "@azure/identity": "4.13.3", + "@azure/identity": "4.14.0", "@commander-js/extra-typings": "12.1.0", "@grpc/grpc-js": "1.14.6", "@grpc/proto-loader": "0.8.1", @@ -31,10 +31,10 @@ "@types/react": "19.2.14", "@typescript-eslint/eslint-plugin": "^8.69.0", "@typescript-eslint/parser": "^8.69.0", - "ajv": "8.18.0", + "ajv": "8.20.0", "auto-bind": "5.0.1", - "axios": "1.16.0", - "bidi-js": "1.0.3", + "axios": "1.20.0", + "bidi-js": "1.1.0", "chalk": "5.6.2", "chokidar": "4.0.3", "cli-boxes": "3.0.0", @@ -49,8 +49,8 @@ "execa": "9.6.1", "fflate": "0.8.3", "figures": "6.1.0", - "fuse.js": "7.1.0", - "get-east-asian-width": "1.5.0", + "fuse.js": "7.5.0", + "get-east-asian-width": "1.7.0", "google-auth-library": "10.6.2", "graphology": "0.26.0", "graphology-metrics": "2.4.2", @@ -61,18 +61,18 @@ "jsonc-parser": "3.3.1", "knip": "^6.16.1", "lodash-es": "4.18.1", - "lru-cache": "11.2.7", + "lru-cache": "11.5.3", "marked": "15.0.12", - "p-map": "7.0.8", + "p-map": "7.1.1", "picomatch": "4.0.7", "proper-lockfile": "4.1.2", "qrcode": "1.5.4", "react": "19.2.4", "react-compiler-runtime": "1.0.0", "react-reconciler": "0.33.0", - "semver": "7.7.4", + "semver": "7.8.5", "sharp": "^0.34.5", - "shell-quote": "1.8.4", + "shell-quote": "1.12.0", "signal-exit": "4.1.0", "supports-hyperlinks": "3.2.0", "tree-kill": "1.2.2", @@ -80,14 +80,14 @@ "turndown": "7.2.4", "type-fest": "4.41.0", "typescript": "6.0.3", - "undici": "7.28.0", + "undici": "7.30.1", "usehooks-ts": "3.1.1", - "vscode-languageserver-protocol": "3.17.5", + "vscode-languageserver-protocol": "3.18.5", "web-tree-sitter": "0.25.10", "wrap-ansi": "9.0.2", - "ws": "8.21.3", + "ws": "8.22.0", "xss": "1.0.16", - "yaml": "2.8.4", + "yaml": "2.9.1", "zod": "3.25.76", }, "peerDependencies": { @@ -219,7 +219,7 @@ "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="], - "@azure/identity": ["@azure/identity@4.13.3", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-process": "^1.0.0", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^6.0.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A=="], + "@azure/identity": ["@azure/identity@4.14.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-process": "^1.0.0", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^6.0.0", "open": "^11.0.0", "tslib": "^2.2.0" } }, "sha512-8utCLdTZn14CiAe0HW4JbtzUMsc0mdDwnjUvStFMrR4IxQP8S2eL9zVAdplpfORVJGYmidZElpeXRYjJQ4UyUw=="], "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], @@ -603,7 +603,7 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], @@ -621,13 +621,13 @@ "auto-bind": ["auto-bind@5.0.1", "", {}, "sha512-ooviqdwwgfIfNmDwo94wlshcdzfO64XV0Cg6oDsDYBJfITDz1EngD2z7DkbvCWn+XIMsIqW27sEVF6qcpJrRcg=="], - "axios": ["axios@1.16.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w=="], + "axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="], "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], - "bidi-js": ["bidi-js@1.0.3", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="], + "bidi-js": ["bidi-js@1.1.0", "", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-fX1Onk0tdVPC7obPWB5EbJ1z7NVhLq4m2xZLq2YXBkxzMXIGRpNMU88n0EPgWseKl12J7zXs7qrDxPK4sRs2fg=="], "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], @@ -699,7 +699,7 @@ "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], - "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], + "default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], @@ -813,7 +813,7 @@ "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], @@ -825,7 +825,7 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], - "fuse.js": ["fuse.js@7.1.0", "", {}, "sha512-trLf4SzuuUxfusZADLINj+dE8clK1frKdmqiJNb1Es75fmI5oY6X2mxLVUciLLjxqw/xr72Dhy+lER6dGd02FQ=="], + "fuse.js": ["fuse.js@7.5.0", "", {}, "sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow=="], "gaxios": ["gaxios@7.1.5", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg=="], @@ -833,7 +833,7 @@ "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "get-east-asian-width": ["get-east-asian-width@1.5.0", "", {}, "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA=="], + "get-east-asian-width": ["get-east-asian-width@1.7.0", "", {}, "sha512-XjH1AECxf0giL2V1aU8vKyRR2ppRUb5c0EvT7zuJTokQ74bNo52zOtghqdWIqrhUD79fo3x0WfKZdOqxF6LG1Q=="], "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], @@ -873,7 +873,7 @@ "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], @@ -915,6 +915,8 @@ "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="], + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], @@ -989,7 +991,7 @@ "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], - "lru-cache": ["lru-cache@11.2.7", "", {}, "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA=="], + "lru-cache": ["lru-cache@11.5.3", "", {}, "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], @@ -1039,7 +1041,7 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], + "open": ["open@11.0.4", "", { "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.1", "wsl-utils": "^1.0.0" } }, "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg=="], "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], @@ -1051,7 +1053,7 @@ "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], - "p-map": ["p-map@7.0.8", "", {}, "sha512-MitaVsCuCFIvOLLPIU7NnfrZvS9H9h7kwMUkDo+T2pEISaJD48IV9S8iIdXB7PsvvdxyYcsSTTrr90XKsbulNw=="], + "p-map": ["p-map@7.1.1", "", {}, "sha512-6Lh3jQPLthYujSrlw6ccWXBGZTkoTCz27Hd92g79LH/kjAbgoEu0t442cCCNbegPSJsA0lwDX8z0bcKtkpA32g=="], "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], @@ -1081,6 +1083,8 @@ "pngjs": ["pngjs@5.0.0", "", {}, "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw=="], + "powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="], + "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], @@ -1139,7 +1143,7 @@ "semifies": ["semifies@1.0.0", "", {}, "sha512-xXR3KGeoxTNWPD4aBvL5NUpMTT7WMANr3EWnaS190QVkY52lqqcVRD7Q05UVbBhiWDGWMlJEUam9m7uFFGVScw=="], - "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "send": ["send@1.2.1", "", { "dependencies": { "debug": "^4.4.3", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.1", "mime-types": "^3.0.2", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.2" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], @@ -1157,7 +1161,7 @@ "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], - "shell-quote": ["shell-quote@1.8.4", "", {}, "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ=="], + "shell-quote": ["shell-quote@1.12.0", "", {}, "sha512-PcByqNyT/38F2kDNi006HAMRJaULuBzq/FOsw3qdZvX/GA9W/jamDaRskgHjubHiftXK5sIFxLNkvrXUwcof6Q=="], "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], @@ -1219,7 +1223,7 @@ "unbash": ["unbash@3.0.0", "", {}, "sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA=="], - "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], + "undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="], "undici-types": ["undici-types@7.24.6", "", {}, "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg=="], @@ -1233,11 +1237,11 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], - "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], + "vscode-jsonrpc": ["vscode-jsonrpc@10.0.0", "", {}, "sha512-USSfxg0vDIpTQqlQBGxF3mBARyoS8cU7LHLpbYNh6dOJt32dlskxPWB0Irrh5TllTmyf7oNLAvqDqzklbwwnpA=="], - "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.17.5", "", { "dependencies": { "vscode-jsonrpc": "8.2.0", "vscode-languageserver-types": "3.17.5" } }, "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg=="], + "vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.18.5", "", { "dependencies": { "vscode-jsonrpc": "10.0.0", "vscode-languageserver-types": "3.18.5" } }, "sha512-FW4fBMzZ99N8xeob4py0l050wgCoRgTpEq5Xcsb9lM1bc29UKGR/wSV+4knaqFxssFHajy7OzNiKP/m3U1ZEuA=="], - "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], + "vscode-languageserver-types": ["vscode-languageserver-types@3.18.5", "", {}, "sha512-kPd6+JQNz3HMJvjKlqUxERSlZHAntl/aGpsk7ImwIh0ewDKDcjgAEyVSYciPiOJD3ftbK9IrfiqUeTZLSbCizw=="], "walk-up-path": ["walk-up-path@4.0.0", "", {}, "sha512-3hu+tD8YzSLGuFYtPRb48vdhKMi0KQV5sn+uWr8+7dMEq/2G/dtLrdDinkLjqq5TIbIBjYJ4Ax/n3YiaW7QM8A=="], @@ -1255,9 +1259,9 @@ "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "ws": ["ws@8.22.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Ydggc987+RO0AnWtZ/7Wq9FtNvcrL1b/RO0ud9mWjUPgDrsAAwQSF51sm2hm1XofbU/4jkpGEsLFsZZxU+1DOg=="], - "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], + "wsl-utils": ["wsl-utils@1.0.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.2.0" } }, "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ=="], "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], @@ -1265,7 +1269,7 @@ "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], - "yaml": ["yaml@2.8.4", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="], + "yaml": ["yaml@2.9.1", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw=="], "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], @@ -1489,6 +1493,8 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "bun-types/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -1501,6 +1507,8 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "es-set-tostringtag/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "eslint/ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="], "eslint/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -1511,6 +1519,8 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "get-intrinsic/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "knip/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "knip/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], @@ -1605,6 +1615,8 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], + "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], diff --git a/package.json b/package.json index 941e313c2f..b7128af08c 100644 --- a/package.json +++ b/package.json @@ -98,7 +98,7 @@ "@aws-sdk/client-bedrock": "3.1047.0", "@aws-sdk/client-sts": "3.1047.0", "@aws-sdk/credential-provider-node": "3.972.88", - "@azure/identity": "4.13.3", + "@azure/identity": "4.14.0", "@commander-js/extra-typings": "12.1.0", "@grpc/grpc-js": "1.14.6", "@grpc/proto-loader": "0.8.1", @@ -111,10 +111,10 @@ "@types/react": "19.2.14", "@typescript-eslint/eslint-plugin": "^8.69.0", "@typescript-eslint/parser": "^8.69.0", - "ajv": "8.18.0", + "ajv": "8.20.0", "auto-bind": "5.0.1", - "axios": "1.16.0", - "bidi-js": "1.0.3", + "axios": "1.20.0", + "bidi-js": "1.1.0", "chalk": "5.6.2", "chokidar": "4.0.3", "cli-boxes": "3.0.0", @@ -129,8 +129,8 @@ "execa": "9.6.1", "fflate": "0.8.3", "figures": "6.1.0", - "fuse.js": "7.1.0", - "get-east-asian-width": "1.5.0", + "fuse.js": "7.5.0", + "get-east-asian-width": "1.7.0", "google-auth-library": "10.6.2", "graphology": "0.26.0", "graphology-metrics": "2.4.2", @@ -141,18 +141,18 @@ "jsonc-parser": "3.3.1", "knip": "^6.16.1", "lodash-es": "4.18.1", - "lru-cache": "11.2.7", + "lru-cache": "11.5.3", "marked": "15.0.12", - "p-map": "7.0.8", + "p-map": "7.1.1", "picomatch": "4.0.7", "proper-lockfile": "4.1.2", "qrcode": "1.5.4", "react": "19.2.4", "react-compiler-runtime": "1.0.0", "react-reconciler": "0.33.0", - "semver": "7.7.4", + "semver": "7.8.5", "sharp": "^0.34.5", - "shell-quote": "1.8.4", + "shell-quote": "1.12.0", "signal-exit": "4.1.0", "supports-hyperlinks": "3.2.0", "tree-kill": "1.2.2", @@ -160,14 +160,14 @@ "turndown": "7.2.4", "type-fest": "4.41.0", "typescript": "6.0.3", - "undici": "7.28.0", + "undici": "7.30.1", "usehooks-ts": "3.1.1", - "vscode-languageserver-protocol": "3.17.5", + "vscode-languageserver-protocol": "3.18.5", "web-tree-sitter": "0.25.10", "wrap-ansi": "9.0.2", - "ws": "8.21.3", + "ws": "8.22.0", "xss": "1.0.16", - "yaml": "2.8.4", + "yaml": "2.9.1", "zod": "3.25.76" }, "peerDependencies": { diff --git a/src/services/lsp/LSPClient.ts b/src/services/lsp/LSPClient.ts index 5ac8509cb9..c6ba53ef6e 100644 --- a/src/services/lsp/LSPClient.ts +++ b/src/services/lsp/LSPClient.ts @@ -5,7 +5,7 @@ import { StreamMessageReader, StreamMessageWriter, Trace, -} from 'vscode-jsonrpc/node.js' +} from 'vscode-jsonrpc/node' import type { InitializeParams, InitializeResult, @@ -367,7 +367,7 @@ export function createLSPClient( checkStartFailed() - connection.onRequest(method, handler) + connection.onRequest(method, handler) }, async stop(): Promise { diff --git a/src/services/lsp/passiveFeedback.test.ts b/src/services/lsp/passiveFeedback.test.ts new file mode 100644 index 0000000000..30987c406c --- /dev/null +++ b/src/services/lsp/passiveFeedback.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, test } from 'bun:test' +import { resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import type { Diagnostic } from 'vscode-languageserver-protocol' +import { formatDiagnosticsForAttachment } from './passiveFeedback.js' + +describe('formatDiagnosticsForAttachment', () => { + test.each([ + ['plain text', 'Missing import'], + ['markdown', { kind: 'markdown', value: '**Missing import**' }], + ['plaintext markup', { kind: 'plaintext', value: 'Missing import' }], + ] satisfies [string, Diagnostic['message']][])( + 'preserves diagnostic text and metadata for %s messages', + (_label, message) => { + const uri = pathToFileURL(resolve('example.ts')).href + const range = { + start: { line: 2, character: 3 }, + end: { line: 2, character: 9 }, + } + + expect(formatDiagnosticsForAttachment({ + uri, + diagnostics: [{ message, range, severity: 2, source: 'typescript', code: 2304 }], + })).toEqual([{ + uri: fileURLToPath(uri), + diagnostics: [{ + message: typeof message === 'string' ? message : message.value, + range, + severity: 'Warning', + source: 'typescript', + code: '2304', + }], + }]) + }, + ) +}) diff --git a/src/services/lsp/passiveFeedback.ts b/src/services/lsp/passiveFeedback.ts index 53de258f67..edc5163c65 100644 --- a/src/services/lsp/passiveFeedback.ts +++ b/src/services/lsp/passiveFeedback.ts @@ -61,17 +61,8 @@ export function formatDiagnosticsForAttachment( } const diagnostics = params.diagnostics.map( - (diag: { - message: string - severity?: number - range: { - start: { line: number; character: number } - end: { line: number; character: number } - } - source?: string - code?: string | number - }) => ({ - message: diag.message, + diag => ({ + message: typeof diag.message === 'string' ? diag.message : diag.message.value, severity: mapLSPSeverity(diag.severity), range: { start: { diff --git a/src/utils/bash/commands.ts b/src/utils/bash/commands.ts index 8c2d0ef16b..f4467ac017 100644 --- a/src/utils/bash/commands.ts +++ b/src/utils/bash/commands.ts @@ -252,7 +252,7 @@ export function filterControlOperators( commandsAndOperators: string[], ): string[] { return commandsAndOperators.filter( - part => !(ALL_SUPPORTED_CONTROL_OPERATORS as Set).has(part), + part => !ALL_SUPPORTED_CONTROL_OPERATORS.has(part), ) } @@ -520,7 +520,7 @@ export function clearCommandPrefixCaches(): void { getCommandSubcommandPrefix.cache.clear() } -const COMMAND_LIST_SEPARATORS = new Set([ +const COMMAND_LIST_SEPARATORS: ReadonlySet = new Set([ '&&', '||', ';', @@ -528,7 +528,7 @@ const COMMAND_LIST_SEPARATORS = new Set([ '|', ]) -const ALL_SUPPORTED_CONTROL_OPERATORS = new Set([ +const ALL_SUPPORTED_CONTROL_OPERATORS: ReadonlySet = new Set([ ...COMMAND_LIST_SEPARATORS, '>&', '>', diff --git a/web/bun.lock b/web/bun.lock index 5450acf508..817f24d3d9 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -7,7 +7,7 @@ "dependencies": { "@astrojs/check": "0.9.12", "@astrojs/sitemap": "3.7.4", - "@fontsource-variable/geist-mono": "5.2.8", + "@fontsource-variable/geist-mono": "5.3.0", "astro": "6.4.8", "typescript": "^6.0.3", }, @@ -120,7 +120,7 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], - "@fontsource-variable/geist-mono": ["@fontsource-variable/geist-mono@5.2.8", "", {}, "sha512-KI5bj+hkkRiHttYHmccotUZ80ZuZyai+RwI1d7UId0clkx/jXxlo8qYK8j54WzmpBjtMoEMPyllV7faDcj+6RA=="], + "@fontsource-variable/geist-mono": ["@fontsource-variable/geist-mono@5.3.0", "", {}, "sha512-vBbuwDEo9AkrqADMXOrlAR3DFcJi4/JxeuU43FoiQERnNwsfXNnvxvReZG02cQKmyk4DZkZdBZX3oTDvy2zBAw=="], "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], diff --git a/web/package.json b/web/package.json index 0203392b24..87508b8f0f 100644 --- a/web/package.json +++ b/web/package.json @@ -12,7 +12,7 @@ "dependencies": { "@astrojs/check": "0.9.12", "@astrojs/sitemap": "3.7.4", - "@fontsource-variable/geist-mono": "5.2.8", + "@fontsource-variable/geist-mono": "5.3.0", "astro": "6.4.8", "typescript": "^6.0.3" } From a0d0a964bee9799c03f7e4d3d789693ab3b4982f Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 20:13:25 -0700 Subject: [PATCH 059/121] ci: update checkout, setup-node and codeql-action pins (#2368) --- .github/workflows/codeql.yml | 6 +++--- .github/workflows/install-hygiene.yml | 4 ++-- .github/workflows/pr-checks.yml | 30 +++++++++++++-------------- .github/workflows/release.yml | 10 ++++----- 4 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a270408cee..b7c7316a96 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -37,18 +37,18 @@ jobs: # Keep the default PR merge checkout and pull_request event: code scanning # supports uploads from fork PRs without a privileged fork-code workflow. - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Initialize CodeQL - uses: github/codeql-action/init@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4 + uses: github/codeql-action/init@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: languages: ${{ matrix.language }} build-mode: none queries: ${{ matrix.queries }} - name: Analyze and upload results - uses: github/codeql-action/analyze@6f2ac81f5cd81fe351a197ba0b00d91d03cd2aee # v4 + uses: github/codeql-action/analyze@9b3254085494b55a0f7c4c2b3d2d52cc712ce370 # v4.39.1 with: category: /language:${{ matrix.language }} diff --git a/.github/workflows/install-hygiene.yml b/.github/workflows/install-hygiene.yml index 9d06877e2d..f5b4e2d0eb 100644 --- a/.github/workflows/install-hygiene.yml +++ b/.github/workflows/install-hygiene.yml @@ -31,12 +31,12 @@ jobs: node-version: ["24.22.1", "26"] steps: - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node-version }} diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index e3d29b8c76..cb7c82dd3b 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -43,10 +43,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version @@ -75,12 +75,12 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version @@ -118,10 +118,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version @@ -155,10 +155,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version @@ -195,12 +195,12 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node-version }} @@ -236,12 +236,12 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Set up minimum supported Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: "24.22.1" @@ -275,7 +275,7 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false @@ -309,10 +309,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4e92c55d92..6d1dab5225 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,13 +46,13 @@ jobs: node-version: ["24.22.1", "26"] steps: - name: Checkout release tag - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.release-please.outputs.tag_name }} persist-credentials: false - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version: ${{ matrix.node-version }} @@ -81,13 +81,13 @@ jobs: id-token: write steps: - name: Checkout release tag - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.release-please.outputs.tag_name }} fetch-depth: 0 - name: Set up Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: .node-version registry-url: https://registry.npmjs.org @@ -195,7 +195,7 @@ jobs: packages: write steps: - name: Checkout release tag - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.release-please.outputs.tag_name }} From 714533a2fef61ffd3a0006ae9459b3273a1c9719 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 20:34:58 -0700 Subject: [PATCH 060/121] ci: disable persisted checkout credentials in CI and release jobs (#2369) --- .github/workflows/pr-checks.yml | 9 +++++++++ .github/workflows/release.yml | 2 ++ 2 files changed, 11 insertions(+) diff --git a/.github/workflows/pr-checks.yml b/.github/workflows/pr-checks.yml index cb7c82dd3b..ba72513f9b 100644 --- a/.github/workflows/pr-checks.yml +++ b/.github/workflows/pr-checks.yml @@ -44,6 +44,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 @@ -78,6 +80,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 @@ -119,6 +122,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 @@ -156,6 +161,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 @@ -310,6 +317,8 @@ jobs: steps: - name: Check out repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d1dab5225..9393d7b356 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,6 +85,7 @@ jobs: with: ref: ${{ needs.release-please.outputs.tag_name }} fetch-depth: 0 + persist-credentials: false - name: Set up Node.js uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 @@ -198,6 +199,7 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ needs.release-please.outputs.tag_name }} + persist-credentials: false - name: Set up Docker Buildx uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 From e7a3a17e00e993710935209a91b95c4941a7e1ad Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 21:25:39 -0700 Subject: [PATCH 061/121] chore(deps): update AWS SDK and Smithy group (#2370) --- bun.lock | 326 ++++++++++++++++++--------------------------------- package.json | 8 +- 2 files changed, 121 insertions(+), 213 deletions(-) diff --git a/bun.lock b/bun.lock index faf169fd70..61cc989e7d 100644 --- a/bun.lock +++ b/bun.lock @@ -15,8 +15,8 @@ "@anthropic-ai/foundry-sdk": "0.2.4", "@anthropic-ai/sandbox-runtime": "0.0.55", "@anthropic-ai/sdk": "0.94.0", - "@aws-sdk/client-bedrock": "3.1047.0", - "@aws-sdk/client-sts": "3.1047.0", + "@aws-sdk/client-bedrock": "3.1156.0", + "@aws-sdk/client-sts": "3.1156.0", "@aws-sdk/credential-provider-node": "3.972.88", "@azure/identity": "4.14.0", "@commander-js/extra-typings": "12.1.0", @@ -24,8 +24,8 @@ "@grpc/proto-loader": "0.8.1", "@modelcontextprotocol/sdk": "1.29.0", "@sentry/node": "^10.70.0", - "@smithy/core": "3.24.3", - "@smithy/node-http-handler": "4.7.3", + "@smithy/core": "3.36.2", + "@smithy/node-http-handler": "4.12.1", "@types/bun": "1.4.4", "@types/node": "^24.19.2", "@types/react": "19.2.14", @@ -127,7 +127,7 @@ "@apm-js-collab/tracing-hooks": ["@apm-js-collab/tracing-hooks@0.13.0", "", { "dependencies": { "@apm-js-collab/code-transformer": "^0.18.0", "debug": "^4.4.1", "module-details-from-path": "^1.0.4" } }, "sha512-mTvWz9rnQwx1U3h0XPTHaX7bgfkpipLLTQyjlC2cdhQpQEuoLT0AGzoydeoq2NxfEVv6fWOOETcSbb2nptleyw=="], - "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + "@aws-crypto/crc32": ["@aws-crypto/crc32@3.0.0", "", { "dependencies": { "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", "tslib": "^1.11.1" } }, "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA=="], "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], @@ -137,15 +137,15 @@ "@aws-crypto/util": ["@aws-crypto/util@4.0.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-2EnmPy2gsFZ6m8bwUQN4jq+IyXV3quHAcwPOS6ZA3k+geujiqI8aRokO2kFJe+idJ/P3v4qWI186rVMo0+zLDQ=="], - "@aws-sdk/client-bedrock": ["@aws-sdk/client-bedrock@3.1047.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.10", "@aws-sdk/credential-provider-node": "^3.972.41", "@aws-sdk/middleware-host-header": "^3.972.11", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.12", "@aws-sdk/middleware-user-agent": "^3.972.40", "@aws-sdk/region-config-resolver": "^3.972.14", "@aws-sdk/token-providers": "3.1047.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.9", "@aws-sdk/util-user-agent-browser": "^3.972.11", "@aws-sdk/util-user-agent-node": "^3.973.26", "@smithy/core": "^3.24.1", "@smithy/fetch-http-handler": "^5.4.1", "@smithy/node-http-handler": "^4.7.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-t1nSLT/FOv9KCKcrq1/KemqrPCwJUQS2gsN/6C3quZjLMAty16rqiDYLaBYfSQ43uciEi3BRC128vggXSn0YLw=="], + "@aws-sdk/client-bedrock": ["@aws-sdk/client-bedrock@3.1156.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/credential-provider-node": "^3.972.88", "@aws-sdk/token-providers": "3.1156.0", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-SJTtm0PfBtvijFlflXBkoGDBdTBeOgIBiF7yUakd9XQq3h8u7haoCVA58W0s5JSSQmYxQuYXREl3ypitwq0ZQw=="], "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1047.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.10", "@aws-sdk/credential-provider-node": "^3.972.41", "@aws-sdk/eventstream-handler-node": "^3.972.15", "@aws-sdk/middleware-eventstream": "^3.972.11", "@aws-sdk/middleware-host-header": "^3.972.11", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.12", "@aws-sdk/middleware-user-agent": "^3.972.40", "@aws-sdk/middleware-websocket": "^3.972.18", "@aws-sdk/region-config-resolver": "^3.972.14", "@aws-sdk/token-providers": "3.1047.0", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.9", "@aws-sdk/util-user-agent-browser": "^3.972.11", "@aws-sdk/util-user-agent-node": "^3.973.26", "@smithy/core": "^3.24.1", "@smithy/fetch-http-handler": "^5.4.1", "@smithy/node-http-handler": "^4.7.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-b9yUBRqYQ9ADb1ta8nLXKby9pWo0lKozsauPeAT3IemBfReoY7PG7bSqGCoXkljL/H2mjBQXAbMinPaw6xhigA=="], "@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1047.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.10", "@aws-sdk/credential-provider-node": "^3.972.41", "@aws-sdk/middleware-host-header": "^3.972.11", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.12", "@aws-sdk/middleware-user-agent": "^3.972.40", "@aws-sdk/region-config-resolver": "^3.972.14", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.9", "@aws-sdk/util-user-agent-browser": "^3.972.11", "@aws-sdk/util-user-agent-node": "^3.973.26", "@smithy/core": "^3.24.1", "@smithy/fetch-http-handler": "^5.4.1", "@smithy/node-http-handler": "^4.7.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-Uu8cmia5ePWyimT5vXbViF2fP8/RT8261zlfz0dzgAELjCpdyT71B6JObY4IZ5tcKa/IiuKrC5ntRVfNbJFh9w=="], - "@aws-sdk/client-sts": ["@aws-sdk/client-sts@3.1047.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.10", "@aws-sdk/credential-provider-node": "^3.972.41", "@aws-sdk/middleware-host-header": "^3.972.11", "@aws-sdk/middleware-logger": "^3.972.10", "@aws-sdk/middleware-recursion-detection": "^3.972.12", "@aws-sdk/middleware-user-agent": "^3.972.40", "@aws-sdk/region-config-resolver": "^3.972.14", "@aws-sdk/signature-v4-multi-region": "^3.996.26", "@aws-sdk/types": "^3.973.8", "@aws-sdk/util-endpoints": "^3.996.9", "@aws-sdk/util-user-agent-browser": "^3.972.11", "@aws-sdk/util-user-agent-node": "^3.973.26", "@smithy/core": "^3.24.1", "@smithy/fetch-http-handler": "^5.4.1", "@smithy/node-http-handler": "^4.7.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-LLeyHPEYlo16wfteaAkYKZx8BrvF+ZqSkYSWodLmtk8xxCAONLOkeHyAofOswQrSetz3BWiu+sd9WNXEvucoPg=="], + "@aws-sdk/client-sts": ["@aws-sdk/client-sts@3.1156.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/credential-provider-node": "^3.972.88", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-emJli57W8F9wPykDPhYRvXTpiI13/IsZOqXnCOoODhBpY/El/HoZDWIoQNzN9N9UOMILVh3T86EOjfI/6bqumA=="], - "@aws-sdk/core": ["@aws-sdk/core@3.974.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@aws-sdk/xml-builder": "^3.972.24", "@smithy/core": "^3.24.1", "@smithy/signature-v4": "^5.4.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-ZGFFlYynBR78Y/F8b/7y4i4sgW/iGwJSjoM7AZo5Et6vyr4/L0bunN+uzKMsvecCZyqcPp4RRK7Rs17l0kMujg=="], + "@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.33", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-yPrIm0tgMN44utkOdKe1Bkvt/NRxcWJFdhbC98tTWxWGEBAzB3lksPhGuB8TtKC0/XyfXcGrBPzx/OKXXexa4w=="], @@ -185,11 +185,11 @@ "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.14", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-VuLXVmm7+lKVxqFcOItPkXhjbJ02iUfxkxheRu41SfWf6/xrZup2A2SwHZos/LeQGu3SBHeqTQht80Uo3ienPA=="], - "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.26", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/signature-v4": "^5.4.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag=="], + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.49", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w=="], - "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1047.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-GwJUeMijpeO2SOGGLRg4q2Nj9foBUBd7hTALYVId+m8fQmA4P2hITp5dmrZFd4AjEkSVmt2eFqmk3TttF7HZeQ=="], + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1156.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-/dS+LLiTM+63ZE1gEOmlXwQ0yrkyT+Bk1xUalfCXPo8w5RUvJXFhXUTAoe7vI83h1YjhFQz1CQtB6ZP6rn8g6g=="], - "@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.996.9", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-ibx8Vd73rCTHekNGeXX8cpGWoBKbNAlwKHL3yjSxxttu5QnNDaSAM7/0MFYDjU31/F4lyrPoQcGirT0ew61xcg=="], @@ -201,9 +201,9 @@ "@aws-sdk/util-utf8-browser": ["@aws-sdk/util-utf8-browser@3.259.0", "", { "dependencies": { "tslib": "^2.3.1" } }, "sha512-UvFa/vR+e19XookZF8RzFZBrw2EUkQWxiBW0yYQAhvk3C+QVGl0H3ouca8LDBlBfQKXwmW3huo/59H8rwb1wJw=="], - "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.24", "", { "dependencies": { "@nodable/entities": "2.1.0", "@smithy/types": "^4.14.1", "fast-xml-parser": "5.7.3", "tslib": "^2.6.2" } }, "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw=="], + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], - "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], @@ -335,8 +335,6 @@ "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], - "@nodable/entities": ["@nodable/entities@2.1.0", "", {}, "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA=="], - "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.220.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w=="], @@ -477,7 +475,7 @@ "@smithy/abort-controller": ["@smithy/abort-controller@2.2.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-wRlta7GuLWpTqtFfGo+nZyOO1vEvewdNR1R4rTxpC8XU6vG/NDyrFBhwLZsqg1NUoR1noVaXJPC/7ZK47QCySw=="], - "@smithy/core": ["@smithy/core@3.24.3", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg=="], + "@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], @@ -487,7 +485,7 @@ "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@2.2.0", "", { "dependencies": { "@smithy/eventstream-codec": "^2.2.0", "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-pvoe/vvJY0mOpuF84BEtyZoYfbehiFj8KKWk1ds2AT0mTLYFVs+7sBJZmioOFdBXKd48lfrx1vumdPdmGlCLxA=="], - "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A=="], + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], "@smithy/is-array-buffer": ["@smithy/is-array-buffer@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-+Fsu6Q6C4RSJiy81Y8eApjEB5gVtM+oFKTffg+jSuwtvomJJrhUJBu2zS8wjXSgH/g1MKEWrzyChTBe6clb5FQ=="], @@ -499,7 +497,7 @@ "@smithy/node-config-provider": ["@smithy/node-config-provider@2.3.0", "", { "dependencies": { "@smithy/property-provider": "^2.2.0", "@smithy/shared-ini-file-loader": "^2.4.0", "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-0elK5/03a1JPWMDPaS726Iw6LpQg80gFut1tNpPfxFuChEEklo2yL823V94SpTZTxmKlXFtFgsP55uh3dErnIg=="], - "@smithy/node-http-handler": ["@smithy/node-http-handler@4.7.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA=="], + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], "@smithy/property-provider": ["@smithy/property-provider@2.2.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-+xiil2lFhtTRzXkx8F053AV46QnIw6e7MV8od5Mi68E1ICOjCeCHw2XfLnDEUHnT9WGUIkwcqavXjfwuJbGlpg=="], @@ -511,17 +509,17 @@ "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@2.4.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-WyujUJL8e1B6Z4PBfAqC/aGY1+C7T0w20Gih3yrvJSk97gpiVfB+y7c46T4Nunk+ZngLq0rOIdeVeIklk0R3OA=="], - "@smithy/signature-v4": ["@smithy/signature-v4@3.1.2", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA=="], + "@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], "@smithy/smithy-client": ["@smithy/smithy-client@2.5.1", "", { "dependencies": { "@smithy/middleware-endpoint": "^2.5.1", "@smithy/middleware-stack": "^2.2.0", "@smithy/protocol-http": "^3.3.0", "@smithy/types": "^2.12.0", "@smithy/util-stream": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-jrbSQrYCho0yDaaf92qWgd+7nAeap5LtHTI51KXqmpIFCceKU3K9+vIVTUH72bOJngBMqa4kyu1VJhRcSrk/CQ=="], - "@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + "@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], "@smithy/url-parser": ["@smithy/url-parser@2.2.0", "", { "dependencies": { "@smithy/querystring-parser": "^2.2.0", "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-hoA4zm61q1mNTpksiSWp2nEl1dt3j726HdRhiNgVJQMj7mLp7dprtF57mOB6JvEk/x9d2bsuL5hlqZbBuHQylQ=="], "@smithy/util-base64": ["@smithy/util-base64@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "@smithy/util-utf8": "^2.3.0", "tslib": "^2.6.2" } }, "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw=="], - "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@3.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA=="], "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ=="], @@ -787,10 +785,6 @@ "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], - "fast-xml-builder": ["fast-xml-builder@1.2.0", "", { "dependencies": { "path-expression-matcher": "^1.5.0", "xml-naming": "^0.1.0" } }, "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q=="], - - "fast-xml-parser": ["fast-xml-parser@5.7.3", "", { "dependencies": { "@nodable/entities": "^2.1.0", "fast-xml-builder": "^1.1.7", "path-expression-matcher": "^1.5.0", "strnum": "^2.2.3" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg=="], - "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], @@ -1071,8 +1065,6 @@ "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], - "path-expression-matcher": ["path-expression-matcher@1.5.0", "", {}, "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], "path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], @@ -1187,8 +1179,6 @@ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - "strnum": ["strnum@2.3.0", "", {}, "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="], @@ -1263,8 +1253,6 @@ "wsl-utils": ["wsl-utils@1.0.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.2.0" } }, "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ=="], - "xml-naming": ["xml-naming@0.1.0", "", {}, "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw=="], - "xss": ["xss@1.0.16", "", { "dependencies": { "commander": "^2.20.3", "cssfilter": "0.0.10" }, "bin": { "xss": "bin/xss" } }, "sha512-CsEvZ0QNH9vVgf3qldg+mJmmIwreZZEu/JS1SgldJrJqZgwyMrmIfENt1BJlkj/cPxO0c+R80kB5OMRtUPgqpQ=="], "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -1283,112 +1271,58 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@anthropic-ai/sandbox-runtime/shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - - "@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - - "@aws-crypto/sha256-browser/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], - - "@aws-crypto/sha256-browser/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - - "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - - "@aws-crypto/sha256-js/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - - "@aws-crypto/util/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - - "@aws-sdk/client-bedrock/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], - - "@aws-sdk/client-bedrock/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - - "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + "@anthropic-ai/bedrock-sdk/@smithy/signature-v4": ["@smithy/signature-v4@3.1.2", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA=="], - "@aws-sdk/client-bedrock-runtime/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@anthropic-ai/bedrock-sdk/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + "@anthropic-ai/sandbox-runtime/shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "@aws-sdk/client-cognito-identity/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@3.0.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w=="], - "@aws-sdk/client-sts/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/client-sts/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-crypto/crc32/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g=="], + "@aws-crypto/sha256-browser/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], - "@aws-sdk/core/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-crypto/sha256-browser/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/credential-provider-cognito-identity/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-crypto/sha256-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/credential-provider-env/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-sdk/credential-provider-env/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + "@aws-crypto/sha256-js/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/credential-provider-env/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + "@aws-crypto/sha256-js/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "@aws-sdk/credential-provider-env/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-crypto/util/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/credential-provider-http/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + "@aws-crypto/util/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], - "@aws-sdk/credential-provider-http/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], + "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], - "@aws-sdk/credential-provider-http/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + "@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1047.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-GwJUeMijpeO2SOGGLRg4q2Nj9foBUBd7hTALYVId+m8fQmA4P2hITp5dmrZFd4AjEkSVmt2eFqmk3TttF7HZeQ=="], - "@aws-sdk/credential-provider-http/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + "@aws-sdk/client-bedrock-runtime/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/credential-provider-http/@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], + "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], - "@aws-sdk/credential-provider-http/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], "@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.83", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-9bJU6FjRacF4FK/QHf8Dyxhur7B1grRj8C1Yh4IkpoFW2exQflPdCv0VrbemLW9ZctLNle+AE7TETs9KNvuLYg=="], "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], - - "@aws-sdk/credential-provider-ini/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], - - "@aws-sdk/credential-provider-ini/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], - - "@aws-sdk/credential-provider-login/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - - "@aws-sdk/credential-provider-node/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], - - "@aws-sdk/credential-provider-node/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], - - "@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], - - "@aws-sdk/credential-provider-process/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], - - "@aws-sdk/credential-provider-process/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], - - "@aws-sdk/credential-provider-process/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], - - "@aws-sdk/credential-provider-process/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], + "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1155.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-iIcQnYLhWuq9qOLQbIWFrxF3PF0svVeZvHnUS4DUnO4zUFmFHIyGyjqrntXG+e/5PUgfavV0pYfDIKHxTzN2LA=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], - - "@aws-sdk/credential-provider-sso/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], - - "@aws-sdk/credential-provider-sso/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core": ["@aws-sdk/core@3.978.5", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@aws-sdk/xml-builder": "^3.972.41", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.36.1", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-ruI0AWqcrfL20XDS6SwZIJFPOBZ6N0ADa9nLzWlBhxk1aoUjbUX3JkTR9vEmk7o35XpAHi68ahZ4T6RE2U9XyA=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/types": ["@aws-sdk/types@3.975.0", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-WQlhs6juByzWOzj5fwxOx0mPJaEqMcG/sDpl3Id61B3XhKE63xY+0QWcnG+sWR/sCfprmreyUo1lM7IKZmHMzw=="], - - "@aws-sdk/credential-provider-web-identity/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], - - "@aws-sdk/credential-provider-web-identity/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], - "@aws-sdk/credential-providers/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.36", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gE+CGuPZD1eqUWGSrM8CXDjlwuPujIuwI+IlorD1wE2RcANKKT4jscB9GY1nTJbjmXzD18sycsYbgCG5m3n4/g=="], "@aws-sdk/credential-providers/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/fetch-http-handler": "^5.4.1", "@smithy/node-http-handler": "^4.7.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-cHZo3bV6zN9joDQ2AYVctfzHTKStxWKwnGu0z7GwCUC+DAtB3qL/+26l+a63RbmFbVvb1JK+0vJKodN3hRMwyw=="], @@ -1401,47 +1335,39 @@ "@aws-sdk/credential-providers/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.40", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-bfIrM8IIzbRtXRQWx/vNEUBLTImLZyX5uKk8uSdeSAZ4Mj3Yi4UnRJLK4FkQLWErbM3McpVLQ1DaM6XO66Ed5g=="], - "@aws-sdk/credential-providers/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.3.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w=="], + "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/credential-providers/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/eventstream-handler-node/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/eventstream-handler-node/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/middleware-eventstream/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/middleware-eventstream/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/middleware-host-header/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/middleware-host-header/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/middleware-logger/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/middleware-logger/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/middleware-recursion-detection/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/middleware-recursion-detection/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/middleware-recursion-detection/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], - "@aws-sdk/middleware-user-agent/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/middleware-user-agent/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/middleware-websocket/@smithy/signature-v4": ["@smithy/signature-v4@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g=="], - - "@aws-sdk/middleware-websocket/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/middleware-websocket/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], "@aws-sdk/nested-clients/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], - "@aws-sdk/nested-clients/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - - "@aws-sdk/region-config-resolver/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - - "@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.4.3", "", { "dependencies": { "@smithy/core": "^3.24.3", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g=="], + "@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.26", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/signature-v4": "^5.4.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag=="], - "@aws-sdk/signature-v4-multi-region/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/token-providers/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/region-config-resolver/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/types/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], - "@aws-sdk/util-endpoints/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/util-endpoints/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/util-user-agent-browser/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/util-user-agent-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], - "@aws-sdk/util-user-agent-node/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], - - "@aws-sdk/xml-builder/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@aws-sdk/util-user-agent-node/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], "@azure/msal-node/@azure/msal-common": ["@azure/msal-common@16.14.1", "", {}, "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ=="], @@ -1453,29 +1379,45 @@ "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "@smithy/core/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@smithy/abort-controller/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - "@smithy/credential-provider-imds/@smithy/core": ["@smithy/core@3.36.2", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-oRw0rB9QSUiCZfEs23G5F4Af+b2rQt1lItIICWu5IejFNd+uPM8SWqiKLMpQ6bX1Q7slsLbjuoE2niErFtkVPQ=="], + "@smithy/eventstream-codec/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - "@smithy/credential-provider-imds/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ=="], - "@smithy/eventstream-codec/@aws-crypto/crc32": ["@aws-crypto/crc32@3.0.0", "", { "dependencies": { "@aws-crypto/util": "^3.0.0", "@aws-sdk/types": "^3.222.0", "tslib": "^1.11.1" } }, "sha512-IzSgsrxUcsrejQbPVilIKy16kAT52EwB6zSaI+M3xxIhKh5+aldEyvI+z6erM7TCLB2BJsFrtHjp6/4/sr+3dA=="], + "@smithy/eventstream-serde-node/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - "@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ=="], + "@smithy/eventstream-serde-universal/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - "@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@smithy/middleware-endpoint/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], "@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/util-middleware@2.2.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw=="], - "@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.14.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw=="], + "@smithy/middleware-serde/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + + "@smithy/middleware-stack/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + + "@smithy/node-config-provider/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + + "@smithy/property-provider/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + + "@smithy/protocol-http/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + + "@smithy/querystring-builder/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], "@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA=="], - "@smithy/signature-v4/@smithy/types": ["@smithy/types@3.7.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg=="], + "@smithy/querystring-parser/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - "@smithy/util-base64/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@smithy/shared-ini-file-loader/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + + "@smithy/smithy-client/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - "@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@smithy/url-parser/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + + "@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@smithy/util-base64/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@smithy/util-middleware/@smithy/types": ["@smithy/types@3.7.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg=="], @@ -1483,12 +1425,14 @@ "@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@2.5.0", "", { "dependencies": { "@smithy/abort-controller": "^2.2.0", "@smithy/protocol-http": "^3.3.0", "@smithy/querystring-builder": "^2.2.0", "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA=="], + "@smithy/util-stream/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + + "@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ=="], "@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@3.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA=="], - "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], @@ -1541,77 +1485,29 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@aws-crypto/crc32/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - - "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - - "@aws-sdk/client-bedrock/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - - "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - - "@aws-sdk/client-sts/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - - "@aws-sdk/credential-provider-env/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - - "@aws-sdk/credential-provider-http/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - - "@aws-sdk/credential-provider-ini/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + "@anthropic-ai/bedrock-sdk/@smithy/signature-v4/@smithy/types": ["@smithy/types@3.7.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.49", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w=="], + "@aws-crypto/crc32/@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], + "@aws-crypto/sha256-js/@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], + "@aws-crypto/util/@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@aws-sdk/credential-provider-process/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - - "@aws-sdk/credential-provider-process/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.49", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], - - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.41", "", { "dependencies": { "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-ctjVSyCMegrWfXlx6VqzSBFI6UqmQ5ZlnfMhdLIiWmhoH8UAQxSCP5N3OpG7X3k4LnS7ou74C4mt20+bfTW2aQ=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], - - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.49", "", { "dependencies": { "@aws-sdk/types": "^3.975.0", "@smithy/signature-v4": "^5.7.4", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nvuAZNRrRRVCiARRKsYlQ0VnXiEQiq7vXPH+UA+LSo+kN3Djl9eo/V2r0udkw85PnsNshmv2Em1clld5M3j67w=="], + "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1047.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-GwJUeMijpeO2SOGGLRg4q2Nj9foBUBd7hTALYVId+m8fQmA4P2hITp5dmrZFd4AjEkSVmt2eFqmk3TttF7HZeQ=="], "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - "@smithy/eventstream-codec/@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@3.0.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w=="], + "@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@smithy/eventstream-codec/@aws-crypto/crc32/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], @@ -1655,21 +1551,19 @@ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-crypto/sha256-browser/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@aws-sdk/client-bedrock/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - - "@aws-sdk/client-sts/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + "@smithy/util-base64/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@smithy/util-stream/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], @@ -1697,6 +1591,14 @@ "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@aws-crypto/sha256-browser/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -1711,6 +1613,12 @@ "qrcode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], } } diff --git a/package.json b/package.json index b7128af08c..197ba3b605 100644 --- a/package.json +++ b/package.json @@ -95,8 +95,8 @@ "@anthropic-ai/foundry-sdk": "0.2.4", "@anthropic-ai/sandbox-runtime": "0.0.55", "@anthropic-ai/sdk": "0.94.0", - "@aws-sdk/client-bedrock": "3.1047.0", - "@aws-sdk/client-sts": "3.1047.0", + "@aws-sdk/client-bedrock": "3.1156.0", + "@aws-sdk/client-sts": "3.1156.0", "@aws-sdk/credential-provider-node": "3.972.88", "@azure/identity": "4.14.0", "@commander-js/extra-typings": "12.1.0", @@ -104,8 +104,8 @@ "@grpc/proto-loader": "0.8.1", "@modelcontextprotocol/sdk": "1.29.0", "@sentry/node": "^10.70.0", - "@smithy/core": "3.24.3", - "@smithy/node-http-handler": "4.7.3", + "@smithy/core": "3.36.2", + "@smithy/node-http-handler": "4.12.1", "@types/bun": "1.4.4", "@types/node": "^24.19.2", "@types/react": "19.2.14", From 9086822bb69b394c481865a004340dbb034f5cc1 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 22:03:30 -0700 Subject: [PATCH 062/121] chore(deps): update typescript-eslint, knip and MCP SDK (#2371) --- bun.lock | 296 +++++++++++++++++++++++++++------------------------ package.json | 10 +- 2 files changed, 164 insertions(+), 142 deletions(-) diff --git a/bun.lock b/bun.lock index 61cc989e7d..4f0eccd839 100644 --- a/bun.lock +++ b/bun.lock @@ -22,15 +22,15 @@ "@commander-js/extra-typings": "12.1.0", "@grpc/grpc-js": "1.14.6", "@grpc/proto-loader": "0.8.1", - "@modelcontextprotocol/sdk": "1.29.0", + "@modelcontextprotocol/sdk": "1.32.1", "@sentry/node": "^10.70.0", "@smithy/core": "3.36.2", "@smithy/node-http-handler": "4.12.1", "@types/bun": "1.4.4", "@types/node": "^24.19.2", "@types/react": "19.2.14", - "@typescript-eslint/eslint-plugin": "^8.69.0", - "@typescript-eslint/parser": "^8.69.0", + "@typescript-eslint/eslint-plugin": "8.72.0", + "@typescript-eslint/parser": "8.72.0", "ajv": "8.20.0", "auto-bind": "5.0.1", "axios": "1.20.0", @@ -59,7 +59,7 @@ "indent-string": "5.0.0", "js-tiktoken": "1.0.21", "jsonc-parser": "3.3.1", - "knip": "^6.16.1", + "knip": "6.42.1", "lodash-es": "4.18.1", "lru-cache": "11.5.3", "marked": "15.0.12", @@ -92,7 +92,7 @@ }, "peerDependencies": { "@anthropic-ai/sdk": "^0.94.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.32.1", "react": "^19.0.0", "react-reconciler": "^0.33.0", }, @@ -233,11 +233,11 @@ "@commander-js/extra-typings": ["@commander-js/extra-typings@12.1.0", "", { "peerDependencies": { "commander": "~12.1.0" } }, "sha512-wf/lwQvWAA0goIghcb91dQYpkLBcyhOhQNqG/VgWhnKzgt+UOMvra7EX/2fv70arm5RW+PUHoQHHDa6/p77Eqg=="], - "@emnapi/core": ["@emnapi/core@1.10.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="], + "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], - "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.10.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg=="], @@ -329,11 +329,11 @@ "@mixmark-io/domino": ["@mixmark-io/domino@2.2.0", "", {}, "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw=="], - "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.29.0", "", { "dependencies": { "@hono/node-server": "^1.19.9", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.32.1", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-2DdE+SJDtzLEEWzY1ZjY7Q+VcPhcV1KisD3zI4u0XZyktsjHum1mwbMI+JaulUBi2OZk+KJAi2uPXzxichPkdw=="], "@msgpack/msgpack": ["@msgpack/msgpack@3.1.3", "", {}, "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA=="], - "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.2" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.2.5", "", { "dependencies": { "@tybys/wasm-util": "^0.10.3" }, "peerDependencies": { "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA=="], "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], @@ -355,85 +355,83 @@ "@orama/plugin-data-persistence": ["@orama/plugin-data-persistence@3.1.18", "", { "dependencies": { "@msgpack/msgpack": "^3.1.2", "@orama/orama": "3.1.18", "dpack": "^0.6.22", "seqproto": "^0.2.3" } }, "sha512-pfBbpK96VRW/7IkdMHn2HaW3/+4k2C9Uwyup0IONNuz5bG3L1orCNFZPBmu+zcokOU2YH+IAVuQz6MlvqOe3iw=="], - "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.133.0", "", { "os": "android", "cpu": "arm" }, "sha512-l/44caGse+VpnY9gx0yvvc5QnnG3yG1FO3KZgYvNL1GZrfK86zIwAOgGEVlxDyRymzrU/KHiblPFpevKOmJmUA=="], + "@oxc-parser/binding-android-arm-eabi": ["@oxc-parser/binding-android-arm-eabi@0.154.0", "", { "os": "android", "cpu": "arm" }, "sha512-sDdEtDl7vKdCvf54j72RJbowE3jCmyRz/+oOB6IAwQxAHZ0eI+Dcj4hvVnm0b9Dy31plUkdmfds9C+/Zt895Tg=="], - "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.133.0", "", { "os": "android", "cpu": "arm64" }, "sha512-KUHmPMziLBp4u+zbrLdB7iWS7KshuZe+RAp7ELnY9SI9nNXBZ+dp8fiBqWOxhXqn+FQg3a4UcQhwmsJOKV8Jjg=="], + "@oxc-parser/binding-android-arm64": ["@oxc-parser/binding-android-arm64@0.154.0", "", { "os": "android", "cpu": "arm64" }, "sha512-UHrDPELzvWZoVkIDdSycj4bzcTMCJG3IbN++mSUgVBaUtNtWHjQ98U6Vz5bpF/1Ic3FYJyYhMr74huO5tVvpZw=="], - "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.133.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-q8dWmnU/8ea2tga9w2f1PinQ5rcMPDUGkF64T189b65YMjUomET4oy5oRldOr4AwOQkneOG/Zttnz1Dvrc62wg=="], + "@oxc-parser/binding-darwin-arm64": ["@oxc-parser/binding-darwin-arm64@0.154.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mokYzajOSXxtSRDWCdHARGdntrKH+rZZ24Jzo8AtafyNzz0NARmpMm26Tc5B6qiGrr6UliWibHCOif1qRN2vaA=="], - "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.133.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-cOKeIELIB2bJnCKwqx4Rdj+1Lss/U6uCbLxRySZrhyOOQa1flKhwZFjEHRHxk8fU1NKmhK5OnTdPQ4CpjuFuVw=="], + "@oxc-parser/binding-darwin-x64": ["@oxc-parser/binding-darwin-x64@0.154.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-WbtUBrclOVItWfLRSvs+Z/WLtXxJ2Ctv50PyuX894HzDN5/e0qzxVpTNPIVMzOerBhgKtFsSMcfxlYIAbvdVgg=="], - "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.133.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-OpaSv4pW3KgFrMYQxTaS0aOE4T1DQF3qZE/4B6uqqv1KgPWWd4UQhJALi8PJPX1RRV5K7ThKXRfF7qGg2+3l1A=="], + "@oxc-parser/binding-freebsd-x64": ["@oxc-parser/binding-freebsd-x64@0.154.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-YooHZSLawOe2fqm+lDdG8oAaQh2NYV/RMYTYzWRLy72stm2BgE9O/8Al6kFENjkmzyyBccDLLBirGxVtpzQfrw=="], - "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.133.0", "", { "os": "linux", "cpu": "arm" }, "sha512-JGK1wlGrGwxBIlVSF7KWTX1/ru6BEtf28fRROztDRkLfiW+Kxa4onnriezMIiogfn9hVw2KzYcKiLjkLR2ns8A=="], + "@oxc-parser/binding-linux-arm-gnueabihf": ["@oxc-parser/binding-linux-arm-gnueabihf@0.154.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hKOwhZbrXdTwc4bGVfbLCXl9f2swl6zWdISocLQSNX9MgPmwHE3qGzOUkCrkuGpwdCVHPnkfOwsqTuYsrP5uNg=="], - "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.133.0", "", { "os": "linux", "cpu": "arm" }, "sha512-yuZO533Ftonxn/iyoqQzURzLQHMspvsIyfiCSNi1t/ER4eIQaR0SsmUOUm5b/lmSig7IWIUa5/BrbEkAPwcilQ=="], + "@oxc-parser/binding-linux-arm-musleabihf": ["@oxc-parser/binding-linux-arm-musleabihf@0.154.0", "", { "os": "linux", "cpu": "arm" }, "sha512-tnjiooXEU013349O/yxKbF5Cusp1CfHWQ8kYR8tGeRzR/P4sOpQGQo21fsS71M0fMD6ESLq+lbvXFWi43PuKeQ=="], - "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.133.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-hvpbqT5pN2rR+3+xtWeizwfR/aZ0vGceg6TqYMl+ToxMpk9/tmnX7kSvQnfEUkoua8mhogzvIKsAkn0wxgblBA=="], + "@oxc-parser/binding-linux-arm64-gnu": ["@oxc-parser/binding-linux-arm64-gnu@0.154.0", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-Cao2kj+8d7BT6XJU1gV9eVQjV+Cr+e8vQm3tddDRZiM2hHrMDxwy3j6t+ofaSl0idyuPPCZ4AlpmQoj8yOT1QA=="], - "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.133.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wJQGamIosQBoJHW9+S5XxrtKRo3eyJxsnS1XCPrqN0LHi8uw1pTqqTfn3t/NVuvbBg7Pumn4ez9Eidgcn0xbEg=="], + "@oxc-parser/binding-linux-arm64-musl": ["@oxc-parser/binding-linux-arm64-musl@0.154.0", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-uiwA0V3q8llWA7/8r+a9BBjk4j5YMDmi6wfOsyQfGOJMQSLHEiZ3HI8ulea766ykUa9wJFWk9XWEl4uJrZLQqw=="], - "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.133.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Koaz32/O5+abIfrNGdyndgRvdOZ9jEf5/z3Ep9h3h2QWpdDiUQpVwgH0OcMXCs+l9aXxPLtkupqyVig9W6FDKw=="], + "@oxc-parser/binding-linux-ppc64-gnu": ["@oxc-parser/binding-linux-ppc64-gnu@0.154.0", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-LICfN0Xs4Zdko+bJy6eOAEQojDuIR26suF2JtOsMaXjgbCrpRSH7/NCvJhY13YvYBx12V6hnwrL8n0lk0rbNJQ=="], - "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.133.0", "", { "os": "linux", "cpu": "none" }, "sha512-R4vOjWzxhnNWHnVLeiB6jNuIifdy9vcMXZGPc7StXcxBovI+U2zg1QhZ9o8OjV80oGivs1lX5NfPLzk4IPqlRA=="], + "@oxc-parser/binding-linux-riscv64-gnu": ["@oxc-parser/binding-linux-riscv64-gnu@0.154.0", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-gbbSYxLF8ssRbQ2qQPy+2EhGNwNH1bAoRdkk6cnWaKHqmCikpSIPtpXjFbCCtjTi0uojqOM3ghFQwQPbDQsU0Q=="], - "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.133.0", "", { "os": "linux", "cpu": "none" }, "sha512-iwgBNUTHiMdxARLYuM0SBlnYeb19iw1Ea5M+4ERZupCsBMLArti6FyZ6UfFjJxIiTDr2oW2DGQFxlQVQ/dW9rA=="], + "@oxc-parser/binding-linux-riscv64-musl": ["@oxc-parser/binding-linux-riscv64-musl@0.154.0", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-nSnRYYDLCcRVjSQzF3g6rKqCNYwo5o+aQgnn8BBXfe+ofCgMGlGqkfRyvXm6ptZvoxNkkWtLlqM8W7Xc3j6ZnQ=="], - "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.133.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-ZwZNo8FZmB/gVfboQl+wXilBigGl+6nQQs+nITOeAP/HcAOjiHl6XZJL9F/KXNEspODQcbjAiyjUbeCJd9a0fA=="], + "@oxc-parser/binding-linux-s390x-gnu": ["@oxc-parser/binding-linux-s390x-gnu@0.154.0", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-BxBQoJKtzRQgZEKQIt6CDUm8RGKC5T8Qo4576yDa5/xgc10X3fBDItMbRabzLtbUBfTqKLl+8AVr/H6CF7W22g=="], - "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.133.0", "", { "os": "linux", "cpu": "x64" }, "sha512-govCvWx1dBlED3uu4qXctxpRcouu9I8Kn+DBktGCl760JtlGJzc9l/OmPJKlYWSbrRqKkMZehNeZ/4Wfma7uSA=="], + "@oxc-parser/binding-linux-x64-gnu": ["@oxc-parser/binding-linux-x64-gnu@0.154.0", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-/whkust2vYy+GjRUVL5cWaJmEu6jkdFfo5mkxgckyeOX61eMwo4iBoiwzeJZeMWAdzb/XOtQyLApHhFht3sOWA=="], - "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.133.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ssTlpXD5Mq9uCssDJPzlRWqBt4Y7Zzd9i+XZhWmK/9Y6KUIuAxVYTYiI8lxcGWi0+3/Cz4A8q9UrD4NK9Y2j7g=="], + "@oxc-parser/binding-linux-x64-musl": ["@oxc-parser/binding-linux-x64-musl@0.154.0", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-EXURTyW9riw4B/kZnnE+58emAtw6+3VEszQfXUZkRqGaKIcmWDYkZtKB+xY/blHvuZ+Qy/fDclc31nmUCgtkSA=="], - "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.133.0", "", { "os": "none", "cpu": "arm64" }, "sha512-51aByfXhPtLEdWG4a2Ihdw6cPWV1ei1AarALpFdDP8MLWDLE2NuUMgbo3DERR2Kt8fT/ok1GUvBiLxVGke9uUQ=="], + "@oxc-parser/binding-openharmony-arm64": ["@oxc-parser/binding-openharmony-arm64@0.154.0", "", { "os": "none", "cpu": "arm64" }, "sha512-ZRFooyH3qsR6vuR4jFGlG3XM36490SvvXtNaUahBK4zHpooMdTHNaOMtGGJI0EW38m5hi4rOco9kw7ZwCIp28w=="], - "@oxc-parser/binding-wasm32-wasi": ["@oxc-parser/binding-wasm32-wasi@0.133.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-2e16tkKp+wDO2GTAmXfxbBcCmGEaFPIJEIRBBmVKNVXSc8/fJsSIaBGyFTPHM9ST5GNWgJcYIt94rDTks+PLwA=="], + "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.154.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-U0Vp+4lvwUa/PnOCJTZOjydj1rX2+VLHz4Y2vSFqlN8/7LTiURgD7z4uQ9yNv9O/NlU3mELBnPDlu7aaSYG1zQ=="], - "@oxc-parser/binding-win32-arm64-msvc": ["@oxc-parser/binding-win32-arm64-msvc@0.133.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-KPTNDKbxH1cglrqTyVeXHb4Pk4oksz8EcE1/v8zqU7N4UXbiHfA/IwtXZ2U77fnRAWBbgVkl/lZbL7o3hRdejg=="], + "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.154.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-Hc8QgsFY5eUUP25we8TISUruibG2cDt0H4IcPG9RKdabb6odkCzIqjV6Gk9beGWGuOwMVCr81H/Su1q363LCCg=="], - "@oxc-parser/binding-win32-ia32-msvc": ["@oxc-parser/binding-win32-ia32-msvc@0.133.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-Una1bNYv9zCavQrfnDR9wuZVB3itLjCEH4Oz7i6CwAJN/Xq9b+zbbcxmvdkKvvJt4Ngc/MBmIYlbLo3zS4TQ0A=="], + "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.154.0", "", { "os": "win32", "cpu": "x64" }, "sha512-hD7judkUMA/hZPoamvKADs6+an1smp8UTK0aMYRijkvi00K/1vl7vEtmyn2JyQ9S+vibtnfcNzxqTLGyJUZCRQ=="], - "@oxc-parser/binding-win32-x64-msvc": ["@oxc-parser/binding-win32-x64-msvc@0.133.0", "", { "os": "win32", "cpu": "x64" }, "sha512-kjBhCiOGSYTwDJQuuZa7a94JbP8htWu7J0X1KwH74kV2K5eYf6eyJRYmkpCDvr0XEL8tMxYI4WU1VekblFCLgg=="], + "@oxc-project/types": ["@oxc-project/types@0.154.0", "", {}, "sha512-KYOUGQFlLWR+s4QMV2Q9ko38TZm7EdO87Qm7rvZLqk/webse/cDjtyxKhg/47l0byCbs/k46LT8qc4PKjAikzA=="], - "@oxc-project/types": ["@oxc-project/types@0.133.0", "", {}, "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA=="], + "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.24.2", "", { "os": "android", "cpu": "arm" }, "sha512-y09e0L0SRI2OA2tUIrjBgoV3eH5hvUKXNkJqXmNo5V2WxIjyC7I7aJfRLMEVpA8yi95f90gFDvO0VMgrDw+vwA=="], - "@oxc-resolver/binding-android-arm-eabi": ["@oxc-resolver/binding-android-arm-eabi@11.20.0", "", { "os": "android", "cpu": "arm" }, "sha512-IjfWOXRgJFNdORDl+Uf1aibNgZY2guOD3zmOhx1BGVb/MIiqlFTdmjpQNplSN58lhWehnX4UNqC3QwpUo8pjJg=="], + "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.24.2", "", { "os": "android", "cpu": "arm64" }, "sha512-cl4icWaZFnLdg8m6qtnh5rBMuGbxc/ptStFHLeCNwr+2cZjkjNwQu/jYRS0CHlnPecOJMpuS5M6/BH+0J/YkEg=="], - "@oxc-resolver/binding-android-arm64": ["@oxc-resolver/binding-android-arm64@11.20.0", "", { "os": "android", "cpu": "arm64" }, "sha512-QqslZAuFQG8Q9xm7JuIn8JUbvywhSBMVhuQHtYW+auirZJloS41oxUUaBXk7uUhZJgp44c5zQLeVvmFaDQB+2Q=="], + "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.24.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-At29QEMF6HajbQvgY8K6OXnHD1x9rad74xBEfmCB6ZqCGsdq75aK7tOYcTbOanMy8qdIBrfL3SMr3p/lfSlb9w=="], - "@oxc-resolver/binding-darwin-arm64": ["@oxc-resolver/binding-darwin-arm64@11.20.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MUcavykj2ewlR+kc5arpg4tC2RvzJkUxWtNv74pf7lcNk00GpIpN43vXMj+j6r4eMmfZhlb8hueKoIb8e9kAGQ=="], + "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.24.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-A5Kqr1EUj4oIL5CF4WRssq/o5P0Y11cwoFouMRmQ7YnC/A8V93nv1nb7aSU8HwcgmXropjLNkVTl4MN87cu28Q=="], - "@oxc-resolver/binding-darwin-x64": ["@oxc-resolver/binding-darwin-x64@11.20.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-BGB16nRUK5Etiv//ihPyzj8Lj1px0mhh4YIfe0FDf045ywknfSm0GEbiRESpr6Q4K82AvnyaRIhhluHByvS4bg=="], + "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.24.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-R5xkRBRRz7ceH/P5Jrc6G7FmdUdgpLYyESFAUDVTNQ9K0sGPxcp4ljiwEwEqsvNcQ4sYbMRrWcHHBCu7ksAJVw=="], - "@oxc-resolver/binding-freebsd-x64": ["@oxc-resolver/binding-freebsd-x64@11.20.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JZgtePaqj3qmD5XFHJaSLWzHRxQu0LaPkdoM1KJXYADvAaa83ijXHclV3ej3CueeW0wxfIAbGCZVP45J0CA7uQ=="], + "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-k/RuYL4L/R58IBn3wT5ma3Wh4k62bp1eYCFRWCmMsasUOqL+H6sW0VGFadEzKWXFFlz+2uIMoeMk9ySSZJHgbg=="], - "@oxc-resolver/binding-linux-arm-gnueabihf": ["@oxc-resolver/binding-linux-arm-gnueabihf@11.20.0", "", { "os": "linux", "cpu": "arm" }, "sha512-hOQ/p3ry3v3SchUBXicrrnszaI/UmYzM4wtS4RGfwgVUX7a+HbyQSzJ5aOzu+o6XZkFkS3ZXN4PZAzhOb77OSg=="], + "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.24.2", "", { "os": "linux", "cpu": "arm" }, "sha512-bnHAak3ujYfH5pKk4NieFNbvYvernfoQDgwLddbZ3OtMYrem87/qjlA+u+aKG0oZcqSLGCful/6/CEA+aeAgaA=="], - "@oxc-resolver/binding-linux-arm-musleabihf": ["@oxc-resolver/binding-linux-arm-musleabihf@11.20.0", "", { "os": "linux", "cpu": "arm" }, "sha512-2ArPksaw0AqeuGBfoS715VF+JvJQAhD2niWgjE5hVO+L+nAfikVQopvngCMX9x4BD8itWoQ3dnikrQyl5Ho5Jg=="], + "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.24.2", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-vDT3KHgzYp47gmtNOqL2VNhCyl5Zv643eyxm//A68J8DeUGXrvD1pZFiaT4jSfe+RInfnn1R2yVHye4enx6RnA=="], - "@oxc-resolver/binding-linux-arm64-gnu": ["@oxc-resolver/binding-linux-arm64-gnu@11.20.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0bJnmYFp62JdZ4nVMDUZ/C58BCZOCcqgKtnUlp7L9Ojf/czIN+3j72YlLPeWLkzlr6SlYvIQA4SGV/HyO0d+qg=="], + "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.24.2", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-+kMlQvbzfyEYtu5FcjE4p+ttBLpKW4d/AsAsuE69BxV6V4twZJeIQZFfD8gh/wqglY0MkPSezWXQH0jBV13MUw=="], - "@oxc-resolver/binding-linux-arm64-musl": ["@oxc-resolver/binding-linux-arm64-musl@11.20.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-wKHHzPKZo7Ufhv/Bt6yxT7FOgnIgW4gwXcJUipkShGp68W3wGVqvr1Sr0fY65lN0Oy6y41+g2kIDvkgZaMMUkw=="], + "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.24.2", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-shjfMhmZ3gq9fv/w7bi3PnZlgOPG+2QAOFf0BJF0EgBSIGZ6PMLN2zbGEblTUYB/NKVDRyYhE2ff3dJ1QqNPkA=="], - "@oxc-resolver/binding-linux-ppc64-gnu": ["@oxc-resolver/binding-linux-ppc64-gnu@11.20.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-RN8goF7Ie0B79L4i4G6OeBocTgSC56vJbQ65VJje+oXnldVpLnOU7j/AQ/dP94TcCS+Yh6WG8u3Qt4ETteXFNQ=="], + "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.24.2", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-zGelwFR5oRo+b69k8Lrzun86DyUHzfKN6cnjbR9l7Z7NIRznOE/2ZvPa1IUKqAL2PzAXOdwkfVqNvO1H2RlpAw=="], - "@oxc-resolver/binding-linux-riscv64-gnu": ["@oxc-resolver/binding-linux-riscv64-gnu@11.20.0", "", { "os": "linux", "cpu": "none" }, "sha512-5l1yU6/xQEqLZRzxqmMxJfWPslpwCmBsdDGaBvABPehxquCXDC7dd7oraNdKSJUMDXSM7VvVj8H2D2FTjU7oWw=="], + "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.24.2", "", { "os": "linux", "cpu": "none", "libc": "musl" }, "sha512-qxZ1SWCXJY0eyhAlP6Lmo9F2Nrtx7EkYj9oCgL8apDPCwXwCEDA2U697bbT81JIc2IrVjxO4KX6WU2N+oN9Z4w=="], - "@oxc-resolver/binding-linux-riscv64-musl": ["@oxc-resolver/binding-linux-riscv64-musl@11.20.0", "", { "os": "linux", "cpu": "none" }, "sha512-xHEvkbgz6UC+A3JOyDQy76LkUaxsNSfIr3/GV8slwZsnuooJiIB34gzJfsyvR4JdCYNUUPsRJc/w/oWkODu+hg=="], + "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.24.2", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-sGCecF3cx2DFlH4t/z7ApnOnXqN48p5p5mlHDEnHTAukQa2P+qMVE4CwyWE9W+q/m3QJ7kKfGrIjax31f44oFQ=="], - "@oxc-resolver/binding-linux-s390x-gnu": ["@oxc-resolver/binding-linux-s390x-gnu@11.20.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-aWPDUUmSeyHvlW+SoEUd+JIJsQhVhu6a5tBpDRMu058naPAchTgAVGCFy35zjbnFlt0i8hLWziff6HX0D3LU4g=="], + "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.24.2", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-k/VlMMcSzMlahb3/fENM4rTlsJ0s3fFROA0KXPBmKggqmTSaE383sl8F3KCOXPLmVsYfW6hCitMhXCEtNeZxxg=="], - "@oxc-resolver/binding-linux-x64-gnu": ["@oxc-resolver/binding-linux-x64-gnu@11.20.0", "", { "os": "linux", "cpu": "x64" }, "sha512-x2YeSimvhJjKLVD8KSu8f/rqU1potcdEMkApIPJqjZWN7c2Fpt4g2X32WDg1p+XDAmyT7nuQGe0vnhvXeLbH+g=="], + "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.24.2", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-8hbnZyNi97b/8wapYaIF9+t9GmZKBW2vunaOc3h9HGJptH7b7XpvZqOTBSm/MpTjr7H497BlgOaSfLUdhmy2bw=="], - "@oxc-resolver/binding-linux-x64-musl": ["@oxc-resolver/binding-linux-x64-musl@11.20.0", "", { "os": "linux", "cpu": "x64" }, "sha512-kcRLEIxpZefeYfLChjpgFf3ilBzRDZ+yobMrpRsQlSrxuFGtm3U6PMU7AaEpMqo3NfDGVyJJseAjnRLzMFHjwQ=="], + "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.24.2", "", { "os": "none", "cpu": "arm64" }, "sha512-MvyGik3a6pVgZ0t/kWlbmFxFLmXQJwgLsY2eYFHLpy0wGwRbfzeIGgDwQ3kXqE30z+kSXennRkCrT7TUvkptNg=="], - "@oxc-resolver/binding-openharmony-arm64": ["@oxc-resolver/binding-openharmony-arm64@11.20.0", "", { "os": "none", "cpu": "arm64" }, "sha512-HHcfnApSZGtKhTiHqe8OZruOZe5XuFQH5/E0Yhj3u8fnFvzkM4/k6WjacUf4SvA0SPEAbfbgYmVPuo0VX/fIBQ=="], + "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.24.2", "", { "dependencies": { "@emnapi/core": "1.11.2", "@emnapi/runtime": "1.11.2", "@napi-rs/wasm-runtime": "^1.1.6" }, "cpu": "none" }, "sha512-vHcssMPwO08RTvj/c0iOBz90attxyG3wQJ0dTcyEQK43LRpcdLWZlV5feBhv6Isn6ahbQIzHbCgfa81+RiML0Q=="], - "@oxc-resolver/binding-wasm32-wasi": ["@oxc-resolver/binding-wasm32-wasi@11.20.0", "", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-Tn0y1XOFYHNfK1wp1Z5QK8Rcld/bsOwRISQXfqAZ5IBpv8Gz1IvV39fUWNprqNdRizgcvFhOzWwFun2zkJsyBg=="], + "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.24.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-uokJqro2iBqkFvJdKQLP7d8/BUmFwESQFVmIJUQKj1Xn1a/LysJoe1vmeECLF5b3jsV8CAL5sEMJXX6SdK9Nhg=="], - "@oxc-resolver/binding-win32-arm64-msvc": ["@oxc-resolver/binding-win32-arm64-msvc@11.20.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-qPi25YNPe4YenS8MgsQU2+bIFHxxpLx1LVna2444cEHqNPhNjvWf9zqj4aWE43H9LpAsTmkkAlA3eL5ElBU3mA=="], - - "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.20.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Wb14jWEW8huH6It9F6sXd9vrYmIS7pMrgkU6sxpLxkP+9z+wRgs71hUEhRpcn8FOXAFa27FVWfY2tRpbfTzfLw=="], + "@oxc-resolver/binding-win32-x64-msvc": ["@oxc-resolver/binding-win32-x64-msvc@11.24.2", "", { "os": "win32", "cpu": "x64" }, "sha512-UqGPmo56KDfLlfXFAFIrNflHT8tFxWGEivWg3Zeyp4Uy2NlKN1FGPr6/BxcLGG3+kZ6Wp14g5Uj+n71boqZfiw=="], "@pondwader/socks5-server": ["@pondwader/socks5-server@1.0.10", "", {}, "sha512-bQY06wzzR8D2+vVCUoBsr5QS2U6UgPUQRmErNwtsuI6vLcyRKkafjkr3KxbtGFf9aBBIV2mcvlsKD1UYaIV+sg=="], @@ -509,17 +507,17 @@ "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@2.4.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-WyujUJL8e1B6Z4PBfAqC/aGY1+C7T0w20Gih3yrvJSk97gpiVfB+y7c46T4Nunk+ZngLq0rOIdeVeIklk0R3OA=="], - "@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + "@smithy/signature-v4": ["@smithy/signature-v4@3.1.2", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA=="], "@smithy/smithy-client": ["@smithy/smithy-client@2.5.1", "", { "dependencies": { "@smithy/middleware-endpoint": "^2.5.1", "@smithy/middleware-stack": "^2.2.0", "@smithy/protocol-http": "^3.3.0", "@smithy/types": "^2.12.0", "@smithy/util-stream": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-jrbSQrYCho0yDaaf92qWgd+7nAeap5LtHTI51KXqmpIFCceKU3K9+vIVTUH72bOJngBMqa4kyu1VJhRcSrk/CQ=="], - "@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], "@smithy/url-parser": ["@smithy/url-parser@2.2.0", "", { "dependencies": { "@smithy/querystring-parser": "^2.2.0", "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-hoA4zm61q1mNTpksiSWp2nEl1dt3j726HdRhiNgVJQMj7mLp7dprtF57mOB6JvEk/x9d2bsuL5hlqZbBuHQylQ=="], "@smithy/util-base64": ["@smithy/util-base64@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "@smithy/util-utf8": "^2.3.0", "tslib": "^2.6.2" } }, "sha512-s3+eVwNeJuXUwuMbusncZNViuhv2LjVJ1nMwTqSA0XAC7gjKhqqxRdJPhR8+YrkoZ9IiIbFk/yK6ACe/xlF+hw=="], - "@smithy/util-buffer-from": ["@smithy/util-buffer-from@3.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA=="], + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@3.0.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-eFndh1WEK5YMUYvy3lPlVmYY/fZcQE1D8oSf41Id2vCeIkKJXPcYDCZD+4+xViI6b1XSd7tE+s5AmXzz5ilabQ=="], @@ -531,7 +529,7 @@ "@smithy/util-utf8": ["@smithy/util-utf8@3.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA=="], - "@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], "@types/bun": ["@types/bun@1.4.4", "", { "dependencies": { "bun-types": "1.4.4" } }, "sha512-9wkRDXUXK124pEnAutE6n/l18j+i9GC5IAHK8TmX/2nlySETmky0OU6yL5U8pW1AoKh++AN9auDCr647BfurjQ=="], @@ -543,25 +541,25 @@ "@types/react": ["@types/react@19.2.14", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], - "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.69.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/type-utils": "8.69.0", "@typescript-eslint/utils": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.69.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-t5jQTKPIgVW1PE6dR6H6Qz5gm8zjMlX5/2gRaOGd9eO6V7J+tQc6iWKukEe7dY8u9HyYasQ0yfF0/FSSTEO2gA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.72.0", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.72.0", "@typescript-eslint/type-utils": "8.72.0", "@typescript-eslint/utils": "8.72.0", "@typescript-eslint/visitor-keys": "8.72.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.72.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.69.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-l4b0DhWioGg6Gt2ebGlvfkFMOjRsauxtsnDRwUSRX1qHq3HdTfQHV8wW9zEXeciai6HfeaKOedQn2Zoofx3WBw=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.72.0", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.72.0", "@typescript-eslint/types": "8.72.0", "@typescript-eslint/typescript-estree": "8.72.0", "@typescript-eslint/visitor-keys": "8.72.0", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.69.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.69.0", "@typescript-eslint/types": "^8.69.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-yi4obFrHMmnsesWehHbkg9zMA7Jt8cXT+mKM08G999pH1yT6nqgsHx7MYm0uY1wAj8CqiBXYRJ7WAT0QdQHQXg=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.72.0", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.72.0", "@typescript-eslint/types": "^8.72.0", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ=="], - "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0" } }, "sha512-ewfspqWvSxKSOaplqAUNbaSFO0eB6w1EtQ+esfYFRm3614Ty4uNtExkcbgd6nWsXphbqKyf9ZYdbZdv2xEoWEQ=="], + "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.72.0", "", { "dependencies": { "@typescript-eslint/types": "8.72.0", "@typescript-eslint/visitor-keys": "8.72.0" } }, "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.69.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-xNqK7YTDZsLniQMV/4rpFR8Z5JlqeRvVjuG1YgF/mdPVH84HSD19L8CczMA0qg2RfwEV231GHH3VnToJDo4MfQ=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.72.0", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA=="], - "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0", "@typescript-eslint/utils": "8.69.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ZfoJAVg3JZndQEpEl9petVlxau3lRuElc4HRMuAlLCf8to04/iHz692RUSNmXKDjEuJmIL+KZ2/BsOcBc16dsA=="], + "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.72.0", "", { "dependencies": { "@typescript-eslint/types": "8.72.0", "@typescript-eslint/typescript-estree": "8.72.0", "@typescript-eslint/utils": "8.72.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.69.0", "", {}, "sha512-K3VrubUPhlo9VDBS6QdI8YB5j7ClpqLRdefcz6PFrhnwicehBweqQ9Evhl4l+FYz0HdDmMqIiSX0aldGRYtDCA=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.72.0", "", {}, "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.69.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.69.0", "@typescript-eslint/tsconfig-utils": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/visitor-keys": "8.69.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-AdFkgqck3Vudb/kWnxlyafU/4aBhHrbQ9locP2N4psXTy5mOBg0SHJumnLvx7r6g1gV4DKvUFwV2nJZBoqOD8w=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.72.0", "", { "dependencies": { "@typescript-eslint/project-service": "8.72.0", "@typescript-eslint/tsconfig-utils": "8.72.0", "@typescript-eslint/types": "8.72.0", "@typescript-eslint/visitor-keys": "8.72.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.69.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.69.0", "@typescript-eslint/types": "8.69.0", "@typescript-eslint/typescript-estree": "8.69.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-tUbx60BBqQa31kXF5MCsOOLL5E/WzUuxIn7YpAvq+eaUlqvk8/NXnXMBNAdLCr0icjkzem7iUA5QqWHe/hJ1aw=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.72.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.72.0", "@typescript-eslint/types": "8.72.0", "@typescript-eslint/typescript-estree": "8.72.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg=="], - "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.69.0", "", { "dependencies": { "@typescript-eslint/types": "8.69.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-+rmdgPA+EXkNgKYvHvFfhrs35utXbwaC5PGpDquSXcoXQDKUA5UjV0LmTucG/4JXkM31BTu4TilHtrN8IVBe8w=="], + "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.72.0", "", { "dependencies": { "@typescript-eslint/types": "8.72.0", "eslint-visitor-keys": "^5.0.0" } }, "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA=="], "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.6", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-jIXhD0eWQ1JA6ln/5Dltyx22UxWNrw0hZmhy2rlv6m6KgF7kplHx3g0fzi09lNmTJQRR91OlemYp3xFnvDK9og=="], @@ -809,7 +807,7 @@ "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], - "formatly": ["formatly@0.3.0", "", { "dependencies": { "fd-package-json": "^2.0.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-9XNj/o4wrRFyhSMJOvsuyMwy8aUfBaZ1VrqHVfohyXf0Sw0e+yfKG+xZaY3arGCOMdwFsqObtzVOc1gU9KiT9w=="], + "formatly": ["formatly@0.11.4", "", { "dependencies": { "fd-package-json": "^2.0.0", "package-manager-detector": "^1.8.0" }, "bin": { "formatly": "bin/index.mjs" } }, "sha512-TeCC10lP8KCgoX+Fe5lryILIT0/Dy+nP0OSVnsuB5CwXoLuOdqxCYlPFF1inR8S/zTy1ec7TjhS2TeRKWzzImA=="], "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], @@ -835,7 +833,7 @@ "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], - "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + "get-tsconfig": ["get-tsconfig@4.14.3", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA=="], "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], @@ -955,7 +953,7 @@ "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], - "knip": ["knip@6.16.1", "", { "dependencies": { "fdir": "^6.5.0", "formatly": "^0.3.0", "get-tsconfig": "4.14.0", "jiti": "^2.7.0", "oxc-parser": "^0.133.0", "oxc-resolver": "^11.20.0", "picomatch": "^4.0.4", "smol-toml": "^1.6.1", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.16", "unbash": "^3.0.0", "yaml": "^2.9.0", "zod": "^4.1.11" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-TKMn1rxgH6h9vXR9Y0B+Cq7AdPTr9EI02IwoT65NzqYUkvoDQAaJ/aPybiFpAhZ1px6cNYYwXf86iHkBgzCo9w=="], + "knip": ["knip@6.42.1", "", { "dependencies": { "formatly": "^0.11.4", "get-tsconfig": "4.14.3", "jiti": "^2.7.0", "oxc-parser": "^0.154.0", "oxc-resolver": "11.24.2", "picomatch": "^4.0.7", "smol-toml": "^1.9.0", "strip-json-comments": "5.0.3", "tinyglobby": "^0.2.17", "unbash": "^6.0.0", "yaml": "^2.9.1", "zod": "^4.6.5" }, "bin": { "knip": "bin/knip.js", "knip-bun": "bin/knip-bun.js" } }, "sha512-gKJVsiT4LJQ0lfc8uSHOCzkOY8adAXNFGarIeODZbJOkBnQRVsG3e0UZFnO4LMwjA82A5tBWX/VgoeTYcjdKiQ=="], "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="], @@ -1039,9 +1037,9 @@ "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], - "oxc-parser": ["oxc-parser@0.133.0", "", { "dependencies": { "@oxc-project/types": "^0.133.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.133.0", "@oxc-parser/binding-android-arm64": "0.133.0", "@oxc-parser/binding-darwin-arm64": "0.133.0", "@oxc-parser/binding-darwin-x64": "0.133.0", "@oxc-parser/binding-freebsd-x64": "0.133.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.133.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.133.0", "@oxc-parser/binding-linux-arm64-gnu": "0.133.0", "@oxc-parser/binding-linux-arm64-musl": "0.133.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.133.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.133.0", "@oxc-parser/binding-linux-riscv64-musl": "0.133.0", "@oxc-parser/binding-linux-s390x-gnu": "0.133.0", "@oxc-parser/binding-linux-x64-gnu": "0.133.0", "@oxc-parser/binding-linux-x64-musl": "0.133.0", "@oxc-parser/binding-openharmony-arm64": "0.133.0", "@oxc-parser/binding-wasm32-wasi": "0.133.0", "@oxc-parser/binding-win32-arm64-msvc": "0.133.0", "@oxc-parser/binding-win32-ia32-msvc": "0.133.0", "@oxc-parser/binding-win32-x64-msvc": "0.133.0" } }, "sha512-661RSx+ZcjBmjBYid+Fpp/2F5EbtildpeoZh5HdgnGs+jZ03nqQEQW8yGkt4BGyOC3OMPDQQRl8M5kqD2/g6jw=="], + "oxc-parser": ["oxc-parser@0.154.0", "", { "dependencies": { "@oxc-project/types": "^0.154.0" }, "optionalDependencies": { "@oxc-parser/binding-android-arm-eabi": "0.154.0", "@oxc-parser/binding-android-arm64": "0.154.0", "@oxc-parser/binding-darwin-arm64": "0.154.0", "@oxc-parser/binding-darwin-x64": "0.154.0", "@oxc-parser/binding-freebsd-x64": "0.154.0", "@oxc-parser/binding-linux-arm-gnueabihf": "0.154.0", "@oxc-parser/binding-linux-arm-musleabihf": "0.154.0", "@oxc-parser/binding-linux-arm64-gnu": "0.154.0", "@oxc-parser/binding-linux-arm64-musl": "0.154.0", "@oxc-parser/binding-linux-ppc64-gnu": "0.154.0", "@oxc-parser/binding-linux-riscv64-gnu": "0.154.0", "@oxc-parser/binding-linux-riscv64-musl": "0.154.0", "@oxc-parser/binding-linux-s390x-gnu": "0.154.0", "@oxc-parser/binding-linux-x64-gnu": "0.154.0", "@oxc-parser/binding-linux-x64-musl": "0.154.0", "@oxc-parser/binding-openharmony-arm64": "0.154.0", "@oxc-parser/binding-win32-arm64-msvc": "0.154.0", "@oxc-parser/binding-win32-ia32-msvc": "0.154.0", "@oxc-parser/binding-win32-x64-msvc": "0.154.0" } }, "sha512-Gr/6fkjZwi937c4Tsz8y0iO4U0FLPkZI6JSVucLAIvQyqh379px7RS8ORIc0HOORbFjXZ6zyqWLl53qxhSeofw=="], - "oxc-resolver": ["oxc-resolver@11.20.0", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.20.0", "@oxc-resolver/binding-android-arm64": "11.20.0", "@oxc-resolver/binding-darwin-arm64": "11.20.0", "@oxc-resolver/binding-darwin-x64": "11.20.0", "@oxc-resolver/binding-freebsd-x64": "11.20.0", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.20.0", "@oxc-resolver/binding-linux-arm-musleabihf": "11.20.0", "@oxc-resolver/binding-linux-arm64-gnu": "11.20.0", "@oxc-resolver/binding-linux-arm64-musl": "11.20.0", "@oxc-resolver/binding-linux-ppc64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-gnu": "11.20.0", "@oxc-resolver/binding-linux-riscv64-musl": "11.20.0", "@oxc-resolver/binding-linux-s390x-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-gnu": "11.20.0", "@oxc-resolver/binding-linux-x64-musl": "11.20.0", "@oxc-resolver/binding-openharmony-arm64": "11.20.0", "@oxc-resolver/binding-wasm32-wasi": "11.20.0", "@oxc-resolver/binding-win32-arm64-msvc": "11.20.0", "@oxc-resolver/binding-win32-x64-msvc": "11.20.0" } }, "sha512-CblytBiV/a/ZXY34dsVU2NxhIOxMXst8CvDCtyBelVITgd7PLrKzbEbA6oKLdPjvDKDzCiW48qzmzZ+mYaqn+g=="], + "oxc-resolver": ["oxc-resolver@11.24.2", "", { "optionalDependencies": { "@oxc-resolver/binding-android-arm-eabi": "11.24.2", "@oxc-resolver/binding-android-arm64": "11.24.2", "@oxc-resolver/binding-darwin-arm64": "11.24.2", "@oxc-resolver/binding-darwin-x64": "11.24.2", "@oxc-resolver/binding-freebsd-x64": "11.24.2", "@oxc-resolver/binding-linux-arm-gnueabihf": "11.24.2", "@oxc-resolver/binding-linux-arm-musleabihf": "11.24.2", "@oxc-resolver/binding-linux-arm64-gnu": "11.24.2", "@oxc-resolver/binding-linux-arm64-musl": "11.24.2", "@oxc-resolver/binding-linux-ppc64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-gnu": "11.24.2", "@oxc-resolver/binding-linux-riscv64-musl": "11.24.2", "@oxc-resolver/binding-linux-s390x-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-gnu": "11.24.2", "@oxc-resolver/binding-linux-x64-musl": "11.24.2", "@oxc-resolver/binding-openharmony-arm64": "11.24.2", "@oxc-resolver/binding-wasm32-wasi": "11.24.2", "@oxc-resolver/binding-win32-arm64-msvc": "11.24.2", "@oxc-resolver/binding-win32-x64-msvc": "11.24.2" } }, "sha512-FY91FiDBj7ls5MsFS9jN3tjz2o0/zsdSsymlakySaBwVJZorHhkWyICLZMKxlu1R9vYo+sd3z1jwb4J8x7bNDw=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -1051,6 +1049,8 @@ "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + "package-manager-detector": ["package-manager-detector@1.9.0", "", {}, "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA=="], + "pandemonium": ["pandemonium@2.4.1", "", { "dependencies": { "mnemonist": "^0.39.2" } }, "sha512-wRqjisUyiUfXowgm7MFH2rwJzKIr20rca5FsHXCMNm1W5YPP1hCtrZfgmQ62kP7OZ7Xt+cR858aB28lu5NX55g=="], "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], @@ -1165,7 +1165,7 @@ "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - "smol-toml": ["smol-toml@1.6.1", "", {}, "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg=="], + "smol-toml": ["smol-toml@1.9.1", "", {}, "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ=="], "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], @@ -1211,7 +1211,7 @@ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], - "unbash": ["unbash@3.0.0", "", {}, "sha512-FeFPZ/WFT0mbRCuydiZzpPFlrYN8ZUpphQKoq4EeElVIYjYyGzPMxQR/simUwCOJIyVhpFk4RbtyO7RuMpMnHA=="], + "unbash": ["unbash@6.0.1", "", {}, "sha512-0db9eS5iqybux41zLkX69nk90KTci7F7eZ+gXzEG/Q7E0tAqEBhRh5fkZvk1N7jT7QkGWqq34i8SFGPFEe1V8A=="], "undici": ["undici@7.30.1", "", {}, "sha512-03KrMbjIFDWQR5RNPrSw3bd1r12JpyFQl9/yxjUbJtqkjmllqC5lFjZh6ujeuJCKr8EOQvRW872Oy/az+nLYxg=="], @@ -1271,10 +1271,6 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@anthropic-ai/bedrock-sdk/@smithy/signature-v4": ["@smithy/signature-v4@3.1.2", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "@smithy/types": "^3.3.0", "@smithy/util-hex-encoding": "^3.0.0", "@smithy/util-middleware": "^3.0.3", "@smithy/util-uri-escape": "^3.0.0", "@smithy/util-utf8": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-3BcPylEsYtD0esM4Hoyml/+s7WP2LFhcM3J2AGdcL2vx9O60TtfpDOL72gjb4lU8NeRPeKAwR77YNyyGvMbuEA=="], - - "@anthropic-ai/bedrock-sdk/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - "@anthropic-ai/sandbox-runtime/shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], "@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@3.0.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w=="], @@ -1299,30 +1295,60 @@ "@aws-crypto/util/tslib": ["tslib@1.14.1", "", {}, "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg=="], + "@aws-sdk/client-bedrock/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], "@aws-sdk/client-bedrock-runtime/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1047.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/nested-clients": "^3.997.8", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-GwJUeMijpeO2SOGGLRg4q2Nj9foBUBd7hTALYVId+m8fQmA4P2hITp5dmrZFd4AjEkSVmt2eFqmk3TttF7HZeQ=="], "@aws-sdk/client-bedrock-runtime/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/client-bedrock-runtime/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], "@aws-sdk/client-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/client-cognito-identity/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/client-sts/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/core/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/credential-provider-cognito-identity/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/credential-provider-cognito-identity/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/credential-provider-env/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/credential-provider-http/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.83", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-9bJU6FjRacF4FK/QHf8Dyxhur7B1grRj8C1Yh4IkpoFW2exQflPdCv0VrbemLW9ZctLNle+AE7TETs9KNvuLYg=="], "@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], + "@aws-sdk/credential-provider-ini/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/credential-provider-login/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/credential-provider-login/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/credential-provider-process/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], "@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1155.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/nested-clients": "^3.997.50", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-iIcQnYLhWuq9qOLQbIWFrxF3PF0svVeZvHnUS4DUnO4zUFmFHIyGyjqrntXG+e/5PUgfavV0pYfDIKHxTzN2LA=="], + "@aws-sdk/credential-provider-sso/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], + "@aws-sdk/credential-provider-web-identity/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/credential-providers/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.36", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gE+CGuPZD1eqUWGSrM8CXDjlwuPujIuwI+IlorD1wE2RcANKKT4jscB9GY1nTJbjmXzD18sycsYbgCG5m3n4/g=="], "@aws-sdk/credential-providers/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.38", "", { "dependencies": { "@aws-sdk/core": "^3.974.10", "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/fetch-http-handler": "^5.4.1", "@smithy/node-http-handler": "^4.7.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-cHZo3bV6zN9joDQ2AYVctfzHTKStxWKwnGu0z7GwCUC+DAtB3qL/+26l+a63RbmFbVvb1JK+0vJKodN3hRMwyw=="], @@ -1337,38 +1363,76 @@ "@aws-sdk/credential-providers/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/credential-providers/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/eventstream-handler-node/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/eventstream-handler-node/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/middleware-eventstream/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/middleware-eventstream/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/middleware-host-header/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/middleware-host-header/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/middleware-logger/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/middleware-logger/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/middleware-recursion-detection/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], "@aws-sdk/middleware-recursion-detection/@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.4", "", {}, "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ=="], + "@aws-sdk/middleware-recursion-detection/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/middleware-user-agent/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/middleware-user-agent/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/middleware-websocket/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/middleware-websocket/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/middleware-websocket/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/nested-clients/@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], "@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.26", "", { "dependencies": { "@aws-sdk/types": "^3.973.8", "@smithy/core": "^3.24.1", "@smithy/signature-v4": "^5.4.1", "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-2N62veqdMZBCwQUHsbhtnaovOFjOa5Dn3dAD1nRqFTUXR4QmirT3HZnfus/L1DS08Vm5CkoKmL0iMVt6YbqEag=="], "@aws-sdk/nested-clients/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/nested-clients/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/region-config-resolver/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/region-config-resolver/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], + + "@aws-sdk/signature-v4-multi-region/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/token-providers/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.50", "", { "dependencies": { "@aws-sdk/core": "^3.978.5", "@aws-sdk/signature-v4-multi-region": "^3.996.49", "@aws-sdk/types": "^3.975.0", "@smithy/core": "^3.36.1", "@smithy/fetch-http-handler": "^5.8.0", "@smithy/node-http-handler": "^4.12.1", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-nKb8qu4QsnpZTq/6JIXc72mzuzfh7BK4sUQkpQznPec2z4W7Ob2xYkcM4i5e1yGTBnbJVeFv/0sf3jcz85hclA=="], + "@aws-sdk/token-providers/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/types/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/util-endpoints/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/util-endpoints/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/util-user-agent-browser/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/util-user-agent-browser/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-sdk/util-user-agent-node/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], + "@aws-sdk/util-user-agent-node/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-sdk/xml-builder/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@azure/msal-node/@azure/msal-common": ["@azure/msal-common@16.14.1", "", {}, "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ=="], "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], @@ -1379,60 +1443,40 @@ "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "@smithy/abort-controller/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], - "@smithy/eventstream-codec/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + "@smithy/core/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], - "@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ=="], + "@smithy/credential-provider-imds/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], - "@smithy/eventstream-serde-node/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/eventstream-serde-universal/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + "@smithy/eventstream-codec/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ=="], - "@smithy/middleware-endpoint/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + "@smithy/fetch-http-handler/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], "@smithy/middleware-endpoint/@smithy/util-middleware": ["@smithy/util-middleware@2.2.0", "", { "dependencies": { "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-L1qpleXf9QD6LwLCJ5jddGkgWyuSvWBkJwWAZ6kFkdifdso+sk3L3O1HdmPvCdnCK3IS4qWyPxev01QMnfHSBw=="], - "@smithy/middleware-serde/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/middleware-stack/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/node-config-provider/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/property-provider/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/protocol-http/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/querystring-builder/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], + "@smithy/node-http-handler/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], "@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-jtmJMyt1xMD/d8OtbVJ2gFZOSKc+ueYJZPW20ULW1GOp/q/YIM0wNh+u8ZFao9UaIGz4WoPW8hC64qlWLIfoDA=="], - "@smithy/querystring-parser/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/shared-ini-file-loader/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/smithy-client/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/url-parser/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/util-base64/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@smithy/signature-v4/@smithy/types": ["@smithy/types@3.7.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg=="], "@smithy/util-base64/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@smithy/util-middleware/@smithy/types": ["@smithy/types@3.7.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg=="], "@smithy/util-stream/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@2.5.0", "", { "dependencies": { "@smithy/protocol-http": "^3.3.0", "@smithy/querystring-builder": "^2.2.0", "@smithy/types": "^2.12.0", "@smithy/util-base64": "^2.3.0", "tslib": "^2.6.2" } }, "sha512-BOWEBeppWhLn/no/JxUL/ghTfANTjT7kg3Ww2rPqTUY9R4yHPXxJ9JhMe3Z03LN3aPwiwlpDIUcVw1xDyHqEhw=="], "@smithy/util-stream/@smithy/node-http-handler": ["@smithy/node-http-handler@2.5.0", "", { "dependencies": { "@smithy/abort-controller": "^2.2.0", "@smithy/protocol-http": "^3.3.0", "@smithy/querystring-builder": "^2.2.0", "@smithy/types": "^2.12.0", "tslib": "^2.6.2" } }, "sha512-mVGyPBzkkGQsPoxQUbxlEfRjrj6FPyA3u3u2VXGr9hT8wilsoQdZdvKpMBFMB8Crfhv5dNkKHIW0Yyuc7eABqA=="], - "@smithy/util-stream/@smithy/types": ["@smithy/types@2.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw=="], - - "@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "@smithy/util-stream/@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-7iKXR+/4TpLK194pVjKiasIyqMtTYJsgKgM242Y9uzt5dhHnUDvMNb+3xIhRJ9QhvqGii/5cRUt4fJn3dtXNHQ=="], "@smithy/util-stream/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + "@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@3.0.0", "", { "dependencies": { "@smithy/is-array-buffer": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-aEOHCgq5RWFbP+UDPvPot26EJHjOC+bRgse5A8V3FSShqd5E5UN4qc7zkwsvJPPAVsf73QwYcHN1/gt/rtLwQA=="], + "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], @@ -1465,9 +1509,7 @@ "get-intrinsic/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], - "knip/yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - - "knip/zod": ["zod@4.4.3", "", {}, "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ=="], + "knip/zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], "needle/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -1485,14 +1527,18 @@ "yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - "@anthropic-ai/bedrock-sdk/@smithy/signature-v4/@smithy/types": ["@smithy/types@3.7.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-bNwBYYmN8Eh9RyjS1p2gW6MIhSO2rl7X9QeLM8iTdcGRP+eDiIWDt66c9IysCc22gefKszZv+ubV9qZc7hdESg=="], + "@aws-crypto/crc32/@aws-sdk/types/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], "@aws-crypto/crc32/@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + "@aws-crypto/sha256-browser/@aws-sdk/types/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + + "@aws-crypto/sha256-js/@aws-sdk/types/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], "@aws-crypto/sha256-js/@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "@aws-crypto/util/@aws-sdk/types/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], + "@aws-crypto/util/@aws-sdk/types/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], @@ -1503,11 +1549,9 @@ "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], - "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], - - "@smithy/util-base64/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@aws-sdk/nested-clients/@aws-sdk/signature-v4-multi-region/@smithy/signature-v4": ["@smithy/signature-v4@5.7.4", "", { "dependencies": { "@smithy/core": "^3.35.0", "@smithy/types": "^4.19.0", "tslib": "^2.6.2" } }, "sha512-tHy0K0VtqNd5Y7Y41h0a0Lhh0L1GzC08dTWg0F7vRJWFtTENg7IZikf3wQkanYIRdb7ngoIPMTmqgUi401fEeQ=="], - "@smithy/util-stream/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + "@eslint/eslintrc/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="], "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], @@ -1551,20 +1595,12 @@ "yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "@aws-crypto/sha256-browser/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - - "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], - "@smithy/util-base64/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - - "@smithy/util-stream/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1591,14 +1627,6 @@ "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@aws-crypto/sha256-browser/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - - "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - - "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - - "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], - "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -1613,12 +1641,6 @@ "qrcode/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@aws-sdk/client-bedrock-runtime/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - - "@aws-sdk/client-cognito-identity/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - - "@aws-sdk/nested-clients/@aws-crypto/sha256-js/@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], - "qrcode/yargs/find-up/locate-path/p-locate/p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], } } diff --git a/package.json b/package.json index 197ba3b605..a74f5705f2 100644 --- a/package.json +++ b/package.json @@ -102,15 +102,15 @@ "@commander-js/extra-typings": "12.1.0", "@grpc/grpc-js": "1.14.6", "@grpc/proto-loader": "0.8.1", - "@modelcontextprotocol/sdk": "1.29.0", + "@modelcontextprotocol/sdk": "1.32.1", "@sentry/node": "^10.70.0", "@smithy/core": "3.36.2", "@smithy/node-http-handler": "4.12.1", "@types/bun": "1.4.4", "@types/node": "^24.19.2", "@types/react": "19.2.14", - "@typescript-eslint/eslint-plugin": "^8.69.0", - "@typescript-eslint/parser": "^8.69.0", + "@typescript-eslint/eslint-plugin": "8.72.0", + "@typescript-eslint/parser": "8.72.0", "ajv": "8.20.0", "auto-bind": "5.0.1", "axios": "1.20.0", @@ -139,7 +139,7 @@ "indent-string": "5.0.0", "js-tiktoken": "1.0.21", "jsonc-parser": "3.3.1", - "knip": "^6.16.1", + "knip": "6.42.1", "lodash-es": "4.18.1", "lru-cache": "11.5.3", "marked": "15.0.12", @@ -172,7 +172,7 @@ }, "peerDependencies": { "@anthropic-ai/sdk": "^0.94.0", - "@modelcontextprotocol/sdk": "^1.29.0", + "@modelcontextprotocol/sdk": "^1.32.1", "react": "^19.0.0", "react-reconciler": "^0.33.0" }, From 67549c6da298ffc2c1068c8536b5a45e14966cc4 Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 23:08:43 -0700 Subject: [PATCH 063/121] ci: bump Docker actions to current majors (#2373) --- .github/workflows/release.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9393d7b356..edc6e0c3a5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -202,10 +202,10 @@ jobs: persist-credentials: false - name: Set up Docker Buildx - uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - name: Log in to GitHub Container Registry - uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0 + uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -213,7 +213,7 @@ jobs: - name: Extract metadata id: meta - uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0 + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 with: images: ghcr.io/${{ github.repository }} tags: | @@ -222,7 +222,7 @@ jobs: type=raw,value=latest - name: Build and load locally - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . load: true @@ -233,7 +233,7 @@ jobs: run: docker run --rm openclaude:smoke --version - name: Build and push - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . push: true From 402478c97e585a5fc7a1c4f775360301841a225c Mon Sep 17 00:00:00 2001 From: JATMN Date: Fri, 23 Oct 2026 23:13:23 -0700 Subject: [PATCH 064/121] chore(deps): update small ESM utility majors (#2372) --- bun.lock | 42 +++++++++++++++++++++++++++--------------- package.json | 12 ++++++------ 2 files changed, 33 insertions(+), 21 deletions(-) diff --git a/bun.lock b/bun.lock index 4f0eccd839..dcf315f886 100644 --- a/bun.lock +++ b/bun.lock @@ -37,14 +37,14 @@ "bidi-js": "1.1.0", "chalk": "5.6.2", "chokidar": "4.0.3", - "cli-boxes": "3.0.0", + "cli-boxes": "4.0.1", "cli-highlight": "2.1.11", "commander": "12.1.0", "cross-spawn": "7.0.6", "diff": "8.0.4", "duck-duck-scrape": "^2.2.7", - "emoji-regex": "10.6.0", - "env-paths": "3.0.0", + "emoji-regex": "11.0.0", + "env-paths": "4.0.0", "eslint": "9.39.5", "execa": "9.6.1", "fflate": "0.8.3", @@ -74,17 +74,17 @@ "sharp": "^0.34.5", "shell-quote": "1.12.0", "signal-exit": "4.1.0", - "supports-hyperlinks": "3.2.0", + "supports-hyperlinks": "4.6.0", "tree-kill": "1.2.2", "tree-sitter-wasms": "0.1.13", "turndown": "7.2.4", - "type-fest": "4.41.0", + "type-fest": "5.10.0", "typescript": "6.0.3", "undici": "7.30.1", "usehooks-ts": "3.1.1", "vscode-languageserver-protocol": "3.18.5", "web-tree-sitter": "0.25.10", - "wrap-ansi": "9.0.2", + "wrap-ansi": "10.0.2", "ws": "8.22.0", "xss": "1.0.16", "yaml": "2.9.1", @@ -655,7 +655,7 @@ "cjs-module-lexer": ["cjs-module-lexer@2.2.1", "", {}, "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q=="], - "cli-boxes": ["cli-boxes@3.0.0", "", {}, "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g=="], + "cli-boxes": ["cli-boxes@4.0.1", "", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="], "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], @@ -721,11 +721,11 @@ "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], - "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], + "emoji-regex": ["emoji-regex@11.0.0", "", {}, "sha512-2qQtc7syPcJmvF7BfKxBvwgvhzDth6u3q23+Jr4JOQUcmMGgmj7B4/ePZ2DmOhrrb77S8kMNHq2kilVASysfqA=="], "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], - "env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="], + "env-paths": ["env-paths@4.0.0", "", { "dependencies": { "is-safe-filename": "^0.1.0" } }, "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -859,7 +859,7 @@ "graphology-utils": ["graphology-utils@2.5.2", "", { "peerDependencies": { "graphology-types": ">=0.23.0" } }, "sha512-ckHg8MXrXJkOARk56ZaSCM1g1Wihe2d6iTmz1enGOz4W/l831MBCKSayeFQfowgF8wd+PQ4rlch/56Vs/VZLDQ=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-flag": ["has-flag@5.0.1", "", {}, "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], @@ -915,6 +915,8 @@ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-safe-filename": ["is-safe-filename@0.1.1", "", {}, "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g=="], + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], @@ -1171,7 +1173,7 @@ "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], - "string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="], + "string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], @@ -1179,9 +1181,11 @@ "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "supports-hyperlinks": ["supports-hyperlinks@4.6.0", "", { "dependencies": { "has-flag": "^5.0.1", "supports-color": "^10.2.2" } }, "sha512-FL/q/YePS4tFFTNiBOR+zPk2ZrKheqNtmTJ/9OUZ4e1wKHJofDbGQSmSsxS0XNnabSkqexryCaXGvEKz1vf9UQ=="], - "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], @@ -1205,7 +1209,7 @@ "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "type-fest": ["type-fest@5.10.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA=="], "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], @@ -1245,7 +1249,7 @@ "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], - "wrap-ansi": ["wrap-ansi@9.0.2", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="], + "wrap-ansi": ["wrap-ansi@10.0.2", "", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0" } }, "sha512-6OLRNZqRntVGBm26ghZ5eZhCuWWyOkRCSn8XsOIuq5stpCE/tjLbdozd93naILZig0lmD8ANfRwnhn0DLeXGlg=="], "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], @@ -1559,6 +1563,8 @@ "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], "cli-highlight/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], @@ -1577,6 +1583,8 @@ "eslint/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "eslint/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], @@ -1603,6 +1611,8 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + "cli-highlight/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "cli-highlight/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], @@ -1613,6 +1623,8 @@ "cli-highlight/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "eslint/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], "qrcode/yargs/cliui/wrap-ansi": ["wrap-ansi@6.2.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], diff --git a/package.json b/package.json index a74f5705f2..24160ea39f 100644 --- a/package.json +++ b/package.json @@ -117,14 +117,14 @@ "bidi-js": "1.1.0", "chalk": "5.6.2", "chokidar": "4.0.3", - "cli-boxes": "3.0.0", + "cli-boxes": "4.0.1", "cli-highlight": "2.1.11", "commander": "12.1.0", "cross-spawn": "7.0.6", "diff": "8.0.4", "duck-duck-scrape": "^2.2.7", - "emoji-regex": "10.6.0", - "env-paths": "3.0.0", + "emoji-regex": "11.0.0", + "env-paths": "4.0.0", "eslint": "9.39.5", "execa": "9.6.1", "fflate": "0.8.3", @@ -154,17 +154,17 @@ "sharp": "^0.34.5", "shell-quote": "1.12.0", "signal-exit": "4.1.0", - "supports-hyperlinks": "3.2.0", + "supports-hyperlinks": "4.6.0", "tree-kill": "1.2.2", "tree-sitter-wasms": "0.1.13", "turndown": "7.2.4", - "type-fest": "4.41.0", + "type-fest": "5.10.0", "typescript": "6.0.3", "undici": "7.30.1", "usehooks-ts": "3.1.1", "vscode-languageserver-protocol": "3.18.5", "web-tree-sitter": "0.25.10", - "wrap-ansi": "9.0.2", + "wrap-ansi": "10.0.2", "ws": "8.22.0", "xss": "1.0.16", "yaml": "2.9.1", From a5a68675cc4ac0075fdc13a9f28addd7db46c6c0 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 01:00:32 -0700 Subject: [PATCH 065/121] deps: pin @types/node to 24.19.2 (M-1) (#2375) Co-authored-by: jatmn <12479882+jatmn@users.noreply.github.com> --- CONTRIBUTING.md | 2 ++ bun.lock | 2 +- package.json | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e8a12ce6b5..058a112106 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -213,6 +213,8 @@ When submitting provider changes: Use Node.js `>=24.22.1` (Node 26 recommended/default) and Bun `1.4.4` (tracked in `.bun-version`). +Keep `@types/node` on the Node 24 major to match the minimum supported runtime, even when developing on Node 26. Pin it to an exact published 24.x version; the type package version does not need to match the Node.js runtime patch version. + Install dependencies: ```bash diff --git a/bun.lock b/bun.lock index dcf315f886..e799041373 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,7 @@ "@smithy/core": "3.36.2", "@smithy/node-http-handler": "4.12.1", "@types/bun": "1.4.4", - "@types/node": "^24.19.2", + "@types/node": "24.19.2", "@types/react": "19.2.14", "@typescript-eslint/eslint-plugin": "8.72.0", "@typescript-eslint/parser": "8.72.0", diff --git a/package.json b/package.json index 24160ea39f..342d23d82c 100644 --- a/package.json +++ b/package.json @@ -107,7 +107,7 @@ "@smithy/core": "3.36.2", "@smithy/node-http-handler": "4.12.1", "@types/bun": "1.4.4", - "@types/node": "^24.19.2", + "@types/node": "24.19.2", "@types/react": "19.2.14", "@typescript-eslint/eslint-plugin": "8.72.0", "@typescript-eslint/parser": "8.72.0", From 17cd64c54e6edac434f92ed1062e4e7dd81df6c6 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 01:09:41 -0700 Subject: [PATCH 066/121] deps: upgrade diff to 9.0.0 (M-3) (#2377) --- CONTRIBUTING.md | 8 ++++++++ bun.lock | 4 ++-- package.json | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 058a112106..3a11ece112 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -193,6 +193,14 @@ In particular: link release-notes navigation to GitHub Releases rather than addi - Do not reformat unrelated files just because they are nearby. - Keep comments useful and concise. +When working on diff generation or rendering, import public APIs and types from +`diff`; the package supplies its own TypeScript declarations, so do not add +`@types/diff` or import internal library paths. Use `diffWordsWithSpace` when +rendering must preserve whitespace. Keep patch filenames separate from hunk +content: serialized headers may quote or escape filenames, and parsed patches +can omit filenames. Check the diff and edit-snippet tests when changing these +paths. + ## Provider Changes OpenClaude supports multiple provider paths. Before contributing provider changes, review the relevant documentation to ensure your implementation follows the expected patterns: diff --git a/bun.lock b/bun.lock index e799041373..889b97660b 100644 --- a/bun.lock +++ b/bun.lock @@ -41,7 +41,7 @@ "cli-highlight": "2.1.11", "commander": "12.1.0", "cross-spawn": "7.0.6", - "diff": "8.0.4", + "diff": "9.0.0", "duck-duck-scrape": "^2.2.7", "emoji-regex": "11.0.0", "env-paths": "4.0.0", @@ -707,7 +707,7 @@ "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "diff": ["diff@8.0.4", "", {}, "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw=="], + "diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="], "dijkstrajs": ["dijkstrajs@1.0.3", "", {}, "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA=="], diff --git a/package.json b/package.json index 342d23d82c..baa8c29fa2 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,7 @@ "cli-highlight": "2.1.11", "commander": "12.1.0", "cross-spawn": "7.0.6", - "diff": "8.0.4", + "diff": "9.0.0", "duck-duck-scrape": "^2.2.7", "emoji-regex": "11.0.0", "env-paths": "4.0.0", From fea268bca6d81903a2d84198c27ad612f8aa708e Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 01:25:11 -0700 Subject: [PATCH 067/121] test: synchronize process outcome checks (#2378) --- src/cli/bgFinalizer.test.ts | 49 ++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/src/cli/bgFinalizer.test.ts b/src/cli/bgFinalizer.test.ts index f7dc750006..af86a02638 100644 --- a/src/cli/bgFinalizer.test.ts +++ b/src/cli/bgFinalizer.test.ts @@ -614,6 +614,7 @@ describe('background session finalizer', () => { id: string child: ReturnType readyPath: string + exited: Promise<[number | null, NodeJS.Signals | null]> }> { const id = `bg-fixture-${mode}` const readyPath = join(configDir, `${id}.ready`) @@ -631,13 +632,17 @@ describe('background session finalizer', () => { stdio: ['ignore', 'ignore', 'ignore'], }) if (!child.pid) throw new Error('fixture did not start') + // Subscribe before registry I/O can yield past a short-lived child's exit. + const exited = once(child, 'exit') as Promise< + [number | null, NodeJS.Signals | null] + > await mkdir(join(sessionsRoot, 'sessions'), { recursive: true }) await writeFile( join(sessionsRoot, 'sessions', `${id}.json`), JSON.stringify(ownedSession(id, child.pid)), ) - return { id, child, readyPath } + return { id, child, readyPath, exited } } async function waitForFile(path: string, attempts: number = 500): Promise { @@ -914,6 +919,9 @@ describe('background session finalizer', () => { }) if (!child.pid) throw new Error('built CLI fixture did not start') + // Observe termination before yielding to registration I/O: a fast child + // may otherwise exit before the listener is installed. + const exited = once(child, 'exit') as Promise<[number]> await mkdir(join(sessionsRoot, 'sessions'), { recursive: true }) await writeFile( join(sessionsRoot, 'sessions', `${id}.json`), @@ -923,7 +931,7 @@ describe('background session finalizer', () => { command: [childConfig.command, ...childConfig.args], }), ) - const [code] = (await once(child, 'exit')) as [number] + const [code] = await exited return code } @@ -1208,8 +1216,8 @@ describe('background session finalizer', () => { { mode: 'throw' as const, status: 'failed', exitCode: 1 }, ]) { it(`records a real ${expectation.mode} child outcome`, async () => { - const { id, child } = await runFixture(expectation.mode) - const [code] = (await once(child, 'exit')) as [number] + const { id, exited } = await runFixture(expectation.mode) + const [code] = await exited expect(code).toBe(expectation.exitCode) expect((await listBackgroundSessions())[0]).toMatchObject({ @@ -1294,13 +1302,11 @@ describe('background session finalizer', () => { 'keeps a handled uncaught exception live until the actual exit', async () => { const releasePath = join(configDir, 'handled-throw.release') - const { id, child, readyPath } = await runFixture( + const { id, child, readyPath, exited } = await runFixture( 'handled-throw', releasePath, ) - const childExit = once(child, 'exit') as Promise< - [number | null, NodeJS.Signals | null] - > + const childExit = exited const forceTimer = setTimeout(() => child.kill('SIGKILL'), 10_000) try { await waitForFile(readyPath) @@ -1352,13 +1358,12 @@ describe('background session finalizer', () => { it.skipIf(process.platform === 'win32')( `records an observed ${expectation.signal} as a failed signal fact`, async () => { - const { id, child, readyPath } = await runFixture(expectation.mode) + const { id, child, readyPath, exited } = await runFixture( + expectation.mode, + ) await waitForFile(readyPath) child.kill(expectation.signal) - const [code, signal] = (await once(child, 'exit')) as [ - number, - NodeJS.Signals | null, - ] + const [code, signal] = await exited expect(code).toBe(expectation.childExitCode) expect(signal).toBeNull() expect((await listBackgroundSessions())[0]).toMatchObject({ @@ -1677,15 +1682,19 @@ describe('background session finalizer', () => { ) it('keeps the installed launcher PID stable and shows outcomes truthfully in ps', async () => { - expect(await runBuiltCliSession('bg-built-success', ['--version'])).toBe(0) - expect( - await runBuiltCliSession('bg-built-failure', [ + // These launches have independent registry IDs. Synchronize on both exit + // events instead of spending the test's budget on serial CLI startups. + const [successCode, failureCode] = await Promise.all([ + runBuiltCliSession('bg-built-success', ['--version']), + runBuiltCliSession('bg-built-failure', [ '--provider-env-file', join(configDir, 'missing.env'), '--print', 'noop', ]), - ).toBe(1) + ]) + expect(successCode).toBe(0) + expect(failureCode).toBe(1) const ps = spawn('node', [installedLauncherPath, 'ps'], { env: { ...process.env, OPENCLAUDE_CONFIG_DIR: configDir }, @@ -1701,7 +1710,7 @@ describe('background session finalizer', () => { ps.stderr?.on('data', chunk => { stderr += chunk }) - const [psCode] = (await once(ps, 'exit')) as [number] + const [psCode] = (await once(ps, 'close')) as [number] expect(psCode).toBe(0) expect(stderr).toBe('') @@ -1718,10 +1727,10 @@ describe('background session finalizer', () => { it.skipIf(process.platform === 'win32')( 'does not invent success when a fixture is forcibly destroyed', async () => { - const { child, readyPath } = await runFixture('wait') + const { child, readyPath, exited } = await runFixture('wait') await waitForFile(readyPath) child.kill('SIGKILL') - await once(child, 'exit') + await exited const refreshed = await refreshBackgroundSessionStatuses({ isProcessAlive: () => false, From 38ac01881797c06030426347eb1e4d686f4fd60a Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 01:26:02 -0700 Subject: [PATCH 068/121] test: control paginated discovery deadlines (#2379) --- src/integrations/discoveryService.test.ts | 43 +++++++++++++++++------ 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/integrations/discoveryService.test.ts b/src/integrations/discoveryService.test.ts index b81566e4c0..afaa9cf0cf 100644 --- a/src/integrations/discoveryService.test.ts +++ b/src/integrations/discoveryService.test.ts @@ -1,4 +1,4 @@ -import { afterEach, beforeEach, describe, expect, mock, spyOn, test } from 'bun:test' +import { afterEach, beforeEach, describe, expect, jest, mock, spyOn, test } from 'bun:test' import { mkdtempSync, rmSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' @@ -97,6 +97,7 @@ beforeEach(async () => { afterEach(() => { try { + jest.useRealTimers() mock.restore() globalThis.fetch = originalFetch rmSync(tempDir, { recursive: true, force: true }) @@ -259,6 +260,7 @@ describe('discoverModelsForRoute', () => { test('allows each Merge discovery page its own timeout', async () => { const { discoverModelsForRoute } = await loadDiscoveryServiceModule() process.env.MERGE_GATEWAY_API_KEY = 'merge-key' + const pages = Array.from({ length: 3 }, () => Promise.withResolvers()) let calls = 0 const fetchMock = mock(async (_input: string | URL | Request, init?: RequestInit) => { const page = ++calls @@ -275,6 +277,7 @@ describe('discoverModelsForRoute', () => { }, 2000) signal?.addEventListener('abort', onAbort, { once: true }) if (signal?.aborted) onAbort() + pages[page - 1]!.resolve() }) return new Response(JSON.stringify({ data: [{ model: `provider/page-${page}` }], @@ -283,16 +286,34 @@ describe('discoverModelsForRoute', () => { }) setMockFetch(fetchMock as unknown as typeof globalThis.fetch) - const result = await discoverModelsForRoute('merge-gateway', { forceRefresh: true }) - expect(result).toMatchObject({ source: 'network', discoveredModelCount: 3, error: null }) - expect(result?.models.map(model => model.apiName).filter(name => name.startsWith('provider/page-'))).toEqual([ - 'provider/page-1', 'provider/page-2', 'provider/page-3', - ]) - const cached = await discoverModelsForRoute('merge-gateway') - expect(cached?.source).toBe('cache') - expect(cached?.models).toEqual(result?.models) - expect(fetchMock).toHaveBeenCalledTimes(3) - }, 15_000) + jest.useFakeTimers() + try { + const discovery = discoverModelsForRoute('merge-gateway', { forceRefresh: true }) + const completed = discovery.then(() => 'completed' as const) + // Each page completes within its own five-second deadline, but the + // complete request exceeds one shared deadline. Wait for the next fetch + // to install both timers before advancing the clock again. + for (const page of pages) { + const next = await Promise.race([ + page.promise.then(() => 'page' as const), + completed, + ]) + expect(next).toBe('page') + jest.advanceTimersByTime(2000) + } + const result = await discovery + expect(result).toMatchObject({ source: 'network', discoveredModelCount: 3, error: null }) + expect(result?.models.map(model => model.apiName).filter(name => name.startsWith('provider/page-'))).toEqual([ + 'provider/page-1', 'provider/page-2', 'provider/page-3', + ]) + const cached = await discoverModelsForRoute('merge-gateway') + expect(cached?.source).toBe('cache') + expect(cached?.models).toEqual(result?.models) + expect(fetchMock).toHaveBeenCalledTimes(3) + } finally { + jest.useRealTimers() + } + }) test.each(['http-error', 'repeated-cursor', 'missing-cursor', 'page-bound'])('incomplete Merge discovery preserves fallback: %s', async failure => { const { discoverModelsForRoute } = await loadDiscoveryServiceModule() From f5d5c69544d2ac94abab4ed22d7f5bc3aa6919cd Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 01:31:56 -0700 Subject: [PATCH 069/121] test: synchronize retention policy phases (#2380) * test: synchronize retention policy phases * test: verify cleanup policy hook ordering Record the rejecting policy hook and threshold phase in the cleanup fixture, and assert that the hook runs first while the throttle lock is held. Preserve existing policy outcome and snapshot assertions. * test: prove cleanup reruns remove newly completed sessions --- src/utils/cleanup.test.ts | 67 +++++++------ .../cleanupBackgroundSessions.fixture.ts | 99 ++++++++++++++++++- 2 files changed, 129 insertions(+), 37 deletions(-) diff --git a/src/utils/cleanup.test.ts b/src/utils/cleanup.test.ts index 3306f63453..b89b1d0667 100644 --- a/src/utils/cleanup.test.ts +++ b/src/utils/cleanup.test.ts @@ -67,6 +67,7 @@ async function runCleanupFixture( mode: | 'once' | 'pass-outcome' + | 'policy-outcome' | 'marker-failure' | 'periodic-policy-recheck' | 'periodic-recovery' @@ -400,6 +401,11 @@ describe('cleanupOldMessageFilesInBackground', () => { expect(await Bun.file(targetPath).exists()).toBe(true) await rm(join(root, '.recovery-pass'), { force: true }) + const freshPath = await createCompletedBackgroundSession( + configDir, + 'bg-progress-fresh', + new Date(Date.now() - 60_000), + ) expect( await runCleanupFixture( configDir, @@ -407,6 +413,7 @@ describe('cleanupOldMessageFilesInBackground', () => { { OPENCLAUDE_CLEANUP_RECOVERY_LIMIT: '2' }, ), ).toEqual({ result: 'ran' }) + expect(await Bun.file(freshPath).exists()).toBe(false) expect(await Bun.file(targetPath).exists()).toBe(false) }, 30_000, @@ -447,41 +454,37 @@ describe('cleanupOldMessageFilesInBackground', () => { ) const journal = join(root, '.recovery-journal') const before = await readFile(journal, 'utf8') - expect( - await runCleanupFixture(configDir, 'pass-outcome', { - OPENCLAUDE_CLEANUP_TRIGGER: trigger, - OPENCLAUDE_CLEANUP_REJECT_AFTER_LOCK: - timing === 'after' ? '1' : '0', - }), - ).toEqual({ result: 'invalid-policy' }) - expect(await Bun.file(metadataPath).exists()).toBe(true) - expect(await Bun.file(marker).exists()).toBe(false) - expect( - await Bun.file(join(root, '.recovery-cursor.json')).exists(), - ).toBe(false) - expect(await readFile(journal, 'utf8')).toBe(before) - - await writeFile(settingsPath, validPolicy) - expect( - await runCleanupFixture(configDir, 'pass-outcome', { - OPENCLAUDE_CLEANUP_TRIGGER: trigger, - }), - ).toEqual({ result: 'ran' }) + const outcomes = await runCleanupFixture(configDir, 'policy-outcome', { + OPENCLAUDE_CLEANUP_TRIGGER: trigger, + OPENCLAUDE_CLEANUP_REJECT_AFTER_LOCK: + timing === 'after' ? '1' : '0', + }) + expect(outcomes).toEqual({ + phases: timing === 'after' ? ['policy-hook', 'threshold'] : ['threshold'], + policyHookLockHeld: timing === 'after' ? true : null, + rejected: { + result: 'invalid-policy', + metadataPresent: true, + markerPresent: false, + cursorPresent: false, + journal: before, + }, + retried: { + result: 'ran', + metadataPresent: false, + journal: '', + markerPresent: true, + }, + throttled: 'skipped', + rerun: { + result: 'ran', + metadataPresent: false, + markerPresent: true, + }, + }) expect(await Bun.file(metadataPath).exists()).toBe(false) expect(await readFile(journal, 'utf8')).toBe('') expect(await Bun.file(marker).exists()).toBe(true) - expect( - await runCleanupFixture(configDir, 'pass-outcome', { - OPENCLAUDE_CLEANUP_TRIGGER: trigger, - }), - ).toEqual({ result: 'skipped' }) - await rm(marker) - expect( - await runCleanupFixture(configDir, 'pass-outcome', { - OPENCLAUDE_CLEANUP_TRIGGER: trigger, - }), - ).toEqual({ result: 'ran' }) - expect(await Bun.file(marker).exists()).toBe(true) }, 30_000) } diff --git a/src/utils/cleanupBackgroundSessions.fixture.ts b/src/utils/cleanupBackgroundSessions.fixture.ts index a9b0a838cc..f6b3efe2e3 100644 --- a/src/utils/cleanupBackgroundSessions.fixture.ts +++ b/src/utils/cleanupBackgroundSessions.fixture.ts @@ -1,5 +1,5 @@ import { createHash } from 'node:crypto' -import { mkdir, utimes } from 'node:fs/promises' +import { mkdir, readFile, rm, stat, utimes } from 'node:fs/promises' import { dirname, join } from 'node:path' import { createBackgroundSession, @@ -8,6 +8,7 @@ import { import { killHandler } from '../cli/bg.js' import { enableConfigs } from './config.js' import { eagerLoadSettingsFromArgs } from './settings/flagSettings.js' +import { resetSettingsCache } from './settings/settingsCache.js' import { cleanupBackgroundSessionsAfterFinalization, cleanupBackgroundSessionsInBackground, @@ -17,7 +18,7 @@ import { try { const mode = process.env.OPENCLAUDE_CLEANUP_FIXTURE_MODE - if (mode === 'pass-outcome') { + if (mode === 'pass-outcome' || mode === 'policy-outcome') { enableConfigs() const settingsPath = join( process.env.OPENCLAUDE_CONFIG_DIR!, @@ -32,15 +33,27 @@ try { process.env.OPENCLAUDE_CLEANUP_TRIGGER === 'periodic-retention' ? 'periodic-retention' : 'periodic-recovery' + let rejectAfterLock = + process.env.OPENCLAUDE_CLEANUP_REJECT_AFTER_LOCK === '1' + const phases: string[] = [] + let policyHookLockHeld: boolean | null = null const afterLock = async () => { - if (process.env.OPENCLAUDE_CLEANUP_REJECT_AFTER_LOCK === '1') { + if (rejectAfterLock) { + phases.push('policy-hook') + policyHookLockHeld = await stat(join( + process.env.OPENCLAUDE_CONFIG_DIR!, + 'bg-sessions', + trigger === 'periodic-recovery' + ? '.recovery-pass.lock' + : '.retention-pass.lock', + )).then(lock => lock.isDirectory(), () => false) await Bun.write( settingsPath, JSON.stringify({ cleanupPeriodDays: 'invalid' }), ) } } - const result = await runBackgroundSessionRetention( + const runPass = () => runBackgroundSessionRetention( trigger === 'periodic-recovery' ? { trigger, _afterThrottleLockForTesting: afterLock } : { @@ -74,7 +87,83 @@ try { }, }, ) - process.stdout.write(`${JSON.stringify({ result })}\n`) + if (mode === 'policy-outcome') { + const root = join(process.env.OPENCLAUDE_CONFIG_DIR!, 'bg-sessions') + const marker = join( + root, + trigger === 'periodic-recovery' ? '.recovery-pass' : '.retention-pass', + ) + const snapshot = async (result: Awaited>) => ({ + result, + metadataPresent: await Bun.file( + join(root, 'sessions', 'bg-rejected-policy.json'), + ).exists(), + markerPresent: await Bun.file(marker).exists(), + cursorPresent: await Bun.file( + join(root, '.recovery-cursor.json'), + ).exists(), + journal: await readFile(join(root, '.recovery-journal'), 'utf8'), + }) + const freshPass = () => { + // Match a fresh process's settings read at each phase boundary. + resetSettingsCache() + return runPass() + } + const originalDateNow = Date.now + const now = Date.now() + try { + // Scheduling must not age the recovery marker between these phases. + Date.now = () => now + const rejected = await snapshot(await freshPass()) + rejectAfterLock = false + await Bun.write(settingsPath, JSON.stringify({ cleanupPeriodDays: 0 })) + const retriedState = await snapshot(await freshPass()) + const retried = { + result: retriedState.result, + metadataPresent: retriedState.metadataPresent, + journal: retriedState.journal, + markerPresent: retriedState.markerPresent, + } + // Anchor the throttle check to the published marker's time. + const markerNow = Math.max(now, Math.ceil((await stat(marker)).mtimeMs)) + Date.now = () => markerNow + phases.push('threshold') + const throttled = await freshPass() + await rm(marker) + const rerunSession = await createBackgroundSession({ + id: 'bg-policy-rerun', + pid: process.pid, + cwd: process.cwd(), + command: ['openclaude', '--print', 'fixture'], + sessionId: 'bg-policy-rerun-conversation', + }) + recordBackgroundSessionNaturalTerminationSync( + rerunSession.id, + { exitCode: 0 }, + { + ownerPid: process.pid, + expectedSession: rerunSession, + now: new Date(markerNow - 1_000), + }, + ) + const rerunState = await snapshot(await freshPass()) + const rerun = { + result: rerunState.result, + metadataPresent: await Bun.file( + join(root, 'sessions', `${rerunSession.id}.json`), + ).exists(), + markerPresent: rerunState.markerPresent, + } + process.stdout.write( + `${JSON.stringify({ rejected, retried, throttled, rerun, phases, policyHookLockHeld })}\n`, + ) + } finally { + Date.now = originalDateNow + } + } else { + const result = await runPass() + process.stdout.write(`${JSON.stringify({ result })}\n`) + } } else if ( mode === 'periodic-retention' || mode === 'periodic-retention-policy-recheck' From 9160012753db590dccb72b8184a031cf84e88e0f Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 01:44:47 -0700 Subject: [PATCH 070/121] test: run provider recommendation credential checks in process (#2376) --- scripts/provider-recommend.test.ts | 72 +++++++++++++++++------------- scripts/provider-recommend.ts | 2 +- 2 files changed, 42 insertions(+), 32 deletions(-) diff --git a/scripts/provider-recommend.test.ts b/scripts/provider-recommend.test.ts index b42f49018e..1251eab3f5 100644 --- a/scripts/provider-recommend.test.ts +++ b/scripts/provider-recommend.test.ts @@ -1,5 +1,10 @@ -import { describe, expect, test } from 'bun:test' -import { getOpenAIConfigurationState } from './provider-recommend.ts' +import { describe, expect, spyOn, test } from 'bun:test' +import { snapshotEnv } from '../src/test/envSnapshot.js' +import { + acquireSharedMutationLock, + releaseSharedMutationLock, +} from '../src/test/sharedMutationLock.js' +import { getOpenAIConfigurationState, main } from './provider-recommend.ts' function envWithOpenAI( patch: Record, @@ -13,10 +18,6 @@ function envWithOpenAI( return env } -async function readStream(stream: ReadableStream): Promise { - return await new Response(stream).text() -} - describe('provider-recommend OpenAI credential detection', () => { test('detects valid pooled OpenAI credentials', () => { expect( @@ -61,36 +62,45 @@ describe('provider-recommend OpenAI credential detection', () => { }) test('profile:recommend openai path runs with pooled credentials', async () => { - const env = envWithOpenAI({ - OPENAI_API_KEYS: 'key-a,key-b', - OPENAI_API_KEY: undefined, - NO_COLOR: '1', + await acquireSharedMutationLock('scripts/provider-recommend.test.ts') + const restoreEnv = snapshotEnv(['OPENAI_API_KEYS', 'OPENAI_API_KEY', 'NO_COLOR']) + const originalArgv = process.argv + const stdout = spyOn(console, 'log').mockImplementation(() => {}) + const stderr = spyOn(console, 'error').mockImplementation(() => {}) + const exit = spyOn(process, 'exit').mockImplementation(code => { + throw new Error(`Unexpected process.exit(${code})`) + }) + const fetch = spyOn(globalThis, 'fetch').mockImplementation(() => { + throw new Error('Explicit OpenAI recommendations must not use discovery') }) - const proc = Bun.spawn( - [ + + try { + process.env.OPENAI_API_KEYS = 'key-a,key-b' + delete process.env.OPENAI_API_KEY + process.env.NO_COLOR = '1' + process.argv = [ process.execPath, - 'run', 'scripts/provider-recommend.ts', '--provider', 'openai', '--json', - ], - { - cwd: process.cwd(), - env, - stdout: 'pipe', - stderr: 'pipe', - }, - ) - - const [stdout, stderr, exitCode] = await Promise.all([ - readStream(proc.stdout), - readStream(proc.stderr), - proc.exited, - ]) + ] - expect(stderr).toBe('') - expect(exitCode).toBe(0) - expect(JSON.parse(stdout).openAIConfigured).toBe(true) + // Exercise the real CLI path without racing child startup and cold imports. + await expect(main()).resolves.toBeUndefined() + expect(stderr).not.toHaveBeenCalled() + expect(exit).not.toHaveBeenCalled() + expect(fetch).not.toHaveBeenCalled() + expect(stdout).toHaveBeenCalledTimes(1) + expect(JSON.parse(stdout.mock.calls[0]![0]).openAIConfigured).toBe(true) + } finally { + fetch.mockRestore() + exit.mockRestore() + stderr.mockRestore() + stdout.mockRestore() + process.argv = originalArgv + restoreEnv() + releaseSharedMutationLock() + } }) -}) \ No newline at end of file +}) diff --git a/scripts/provider-recommend.ts b/scripts/provider-recommend.ts index 5cb3dbf7ff..3b2e590816 100644 --- a/scripts/provider-recommend.ts +++ b/scripts/provider-recommend.ts @@ -162,7 +162,7 @@ async function maybeApplyProfile( return true } -async function main(): Promise { +export async function main(): Promise { const options = parseOptions(process.argv.slice(2)) const ollamaAvailable = options.provider !== 'openai' && From 95e96e9c16e1d563f09355b19645b3a00a7ec003 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 01:51:00 -0700 Subject: [PATCH 071/121] test: restore credential refresh module mocks (#2381) Capture detached real module exports before mocking and restore secure storage and device-flow modules after each refresh-on-401 test. This prevents shared-process credential mocks from breaking provider setup and saved-profile startup tests in randomized serial runs. --- src/utils/githubModelsCredentials.refresh.test.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/utils/githubModelsCredentials.refresh.test.ts b/src/utils/githubModelsCredentials.refresh.test.ts index 12dbff7e72..d8dd14929d 100644 --- a/src/utils/githubModelsCredentials.refresh.test.ts +++ b/src/utils/githubModelsCredentials.refresh.test.ts @@ -3,8 +3,16 @@ import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../test/sharedMutationLock.js' -import * as realDeviceFlow from '../services/github/deviceFlow.js' -import * as realSecureStorage from './secureStorage/index.js' + +// Module namespaces have live bindings: keep detached exports so a module mock +// cannot replace the implementations that cleanup needs to restore. +const nonce = `${Date.now()}-${Math.random()}` +const realDeviceFlow = { + ...await import(`../services/github/deviceFlow.js?real=${nonce}`), +} +const realSecureStorage = { + ...await import(`./secureStorage/index.js?real=${nonce}`), +} async function importFreshModule() { mock.restore() @@ -174,6 +182,8 @@ describe('refreshCopilotTokenOn401', () => { afterEach(() => { try { mock.restore() + mock.module('./secureStorage/index.js', () => realSecureStorage) + mock.module('../services/github/deviceFlow.js', () => realDeviceFlow) for (const [k, v] of Object.entries(orig)) { if (v === undefined) { delete process.env[k as keyof typeof orig] From d62eb70da179352e3f1889ffaed7cdd80b99cd59 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 02:52:27 -0700 Subject: [PATCH 072/121] chore(deps): update marked to 18.1.1 (#2382) --- CONTRIBUTING.md | 23 +++++ bun.lock | 4 +- package.json | 2 +- src/commands/copy/copy.tsx | 4 +- src/components/Markdown.tsx | 4 +- .../fixtures/markdownRendering.baseline.json | 88 +++++++++++++++++++ .../fixtures/markdownRendering.fixture.tsx | 68 ++++++++++++++ src/utils/claudemd.ts | 10 ++- src/utils/markdown.test.ts | 45 ++++++++++ src/utils/markdown.ts | 38 ++++++-- 10 files changed, 272 insertions(+), 14 deletions(-) create mode 100644 src/test/fixtures/markdownRendering.baseline.json create mode 100644 src/test/fixtures/markdownRendering.fixture.tsx create mode 100644 src/utils/markdown.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a11ece112..9a33cb91d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -180,6 +180,29 @@ Before submitting, run multiple rounds of review on generated code: Self-review up front saves everyone time and reduces back-and-forth during maintainer review. +### Markdown parsing and rendering + +Use the exact `marked` version in `package.json` and its bundled TypeScript types; +import `marked`, `Lexer`, `Token`, or `Tokens` from the package's ESM entry point. +Do not add `@types/marked` or a separate rendering extension. Use +`configureMarked()` before terminal lexing and `formatToken()` / `applyMarkdown()` +for terminal output; strikethrough parsing is deliberately disabled. + +List items can contain `paragraph` children and `checkbox` tokens. Preserve list +markers for paragraph children and omit checkbox glyphs in terminal output. Pass +the previous sibling to `formatToken()` when rendering block sequences so separate +`space` tokens do not duplicate spacing after headings, tables, or indented code. +Indented code has trailing newlines removed for terminal display and `/copy`; +fenced code retains its content. Keep streaming boundary calculations on the +complete lexer token stream, including whitespace and each token's `raw` length. + +CLAUDE.md parsing uses its own `new Lexer({ gfm: false })` to preserve include +paths. When removing a comment-only HTML block, consume its following whitespace +token too; preserve whitespace with visible comment residue and leave code blocks +untouched. When updating `marked`, compare representative terminal, table, list, +code, link, streaming, and memory-file output and run the markdown regression tests. +Regenerate the exact dependency pin and lockfile with `bun add --exact marked@`. + ## AI Agent Guidelines If you are an AI agent (Copilot, Cursor, Claude, etc.) working on this codebase, refer to [AGENTS.md](AGENTS.md) for project-specific coding guidelines, conventions, and validation commands. Following these guidelines will help your contributions align with the project's patterns and reduce review friction. diff --git a/bun.lock b/bun.lock index 889b97660b..1cffe990c6 100644 --- a/bun.lock +++ b/bun.lock @@ -62,7 +62,7 @@ "knip": "6.42.1", "lodash-es": "4.18.1", "lru-cache": "11.5.3", - "marked": "15.0.12", + "marked": "18.1.1", "p-map": "7.1.1", "picomatch": "4.0.7", "proper-lockfile": "4.1.2", @@ -989,7 +989,7 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + "marked": ["marked@18.1.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-zEGgrtUG8crk+TrMacJSVABU4DYDDkS8uqgU3ohrn/2fW9C2AEfUrO3m6IhpIpQpl7YcVpiXXC9cSMWDANT3dg=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], diff --git a/package.json b/package.json index baa8c29fa2..040b6200ae 100644 --- a/package.json +++ b/package.json @@ -142,7 +142,7 @@ "knip": "6.42.1", "lodash-es": "4.18.1", "lru-cache": "11.5.3", - "marked": "15.0.12", + "marked": "18.1.1", "p-map": "7.1.1", "picomatch": "4.0.7", "proper-lockfile": "4.1.2", diff --git a/src/commands/copy/copy.tsx b/src/commands/copy/copy.tsx index d540cdff2c..88d99e5f7a 100644 --- a/src/commands/copy/copy.tsx +++ b/src/commands/copy/copy.tsx @@ -34,7 +34,9 @@ function extractCodeBlocks(markdown: string): CodeBlock[] { if (token.type === 'code') { const codeToken = token as Tokens.Code; blocks.push({ - code: codeToken.text, + code: codeToken.codeBlockStyle === 'indented' + ? codeToken.text.replace(/\n+$/, '') + : codeToken.text, lang: codeToken.lang }); } diff --git a/src/components/Markdown.tsx b/src/components/Markdown.tsx index 0dadd055f0..defe1a063b 100644 --- a/src/components/Markdown.tsx +++ b/src/components/Markdown.tsx @@ -140,12 +140,12 @@ function MarkdownBody(t0) { nonTableContent = ""; } }; - for (const token of tokens) { + for (const [index, token] of tokens.entries()) { if (token.type === "table") { flushNonTableContent(); elements.push(); } else { - nonTableContent = nonTableContent + formatToken(token, theme, 0, null, null, highlight); + nonTableContent = nonTableContent + formatToken(token, theme, 0, null, null, highlight, tokens[index - 1]); nonTableContent; } } diff --git a/src/test/fixtures/markdownRendering.baseline.json b/src/test/fixtures/markdownRendering.baseline.json new file mode 100644 index 0000000000..fa0b016056 --- /dev/null +++ b/src/test/fixtures/markdownRendering.baseline.json @@ -0,0 +1,88 @@ +{ + "inline": { + "input": "Plain **bold**, *italic*, `code`, ~~approximate~~ and \\*literal\\*.", + "terminal": "Plain bold, italic, code, ~~approximate~~ and *literal*.", + "codes": [] + }, + "headings": { + "input": "# Heading\n\nBody\n\n## Subheading\n\nNext", + "terminal": "Heading\n\nBody\n\nSubheading\n\nNext", + "codes": [] + }, + "blocks": { + "input": "> A quote\n> with **bold**\n\n---\n\nAfter", + "terminal": "▎ A quote\n▎ with bold\n\n---\nAfter", + "codes": [] + }, + "code": { + "input": "Before\n\n```ts\nconst n = 1\nconsole.log(n)\n```\n\nAfter\n\n indented()\n", + "terminal": "Before\n\nconst n = 1\nconsole.log(n)\n\nAfter\n\nindented()", + "codes": [ + { + "code": "const n = 1\nconsole.log(n)", + "lang": "ts" + }, + { + "code": "indented()" + } + ] + }, + "links": { + "input": "[Guide](https://example.com/guide) and and ![alt](image.png)", + "terminal": "https://example.com/guide and dev@example.com and image.png", + "codes": [] + }, + "tight": { + "input": "- First **bold**\n- Second\n continued\n 1. Nested one\n 2. Nested two\n- Last", + "terminal": "- First bold\n- Second\ncontinued\n a. Nested one\n b. Nested two\n- Last", + "codes": [] + }, + "loose": { + "input": "3. First **bold**\n\n4. Second\n\n Another paragraph\n\n5. Last", + "terminal": "3. First bold\n4. Second\n\n4. Another paragraph\n5. Last", + "codes": [] + }, + "tasks": { + "input": "- [ ] Pending\n- [x] Done\n\n- [ ] Loose task\n\n More text", + "terminal": "- Pending\n- Done\n- Loose task\n\n- More text", + "codes": [] + }, + "table": { + "input": "| Name | Value |\n| :--- | ---: |\n| **one** | `two` |\n| 三 | 42 |\n\nAfter", + "terminal": "| Name | Value |\n|------|-------|\n| one | two |\n| 三 | 42 |\n\nAfter", + "codes": [] + }, + "spacing": { + "input": "# Title\n\n\nBody\n\n```\ncode\n```\n\n\nAfter\n", + "terminal": "Title\n\nBody\n\ncode\n\nAfter", + "codes": [ + { + "code": "code", + "lang": "" + } + ] + }, + "listBlocks": { + "input": "- Intro\n\n ```\n code\n ```\n\n > quote\n\n- End", + "terminal": "- Intro\n\ncode\n\n▎ quote\n- End", + "codes": [] + }, + "tableComponent": "┌──────┬───────┐\n│ Name │ Value │\n├──────┼───────┤\n│ one │ two │\n├──────┼───────┤\n│ 三 │ 42 │\n└──────┴───────┘", + "Markdown": "Heading\n\nBody\n\nSubheading\n\nNext\n\n- First bold\n- Second\ncontinued\n a. Nested one\n b. Nested two\n- Last\n\n┌──────┬───────┐\n│ Name │ Value │\n├──────┼───────┤\n│ one │ two │\n├──────┼───────┤\n│ 三 │ 42 │\n└──────┴───────┘\n\nAfter", + "StreamingMarkdown": "Heading\n\nBody\n\nSubheading\n\nNext\n\n- First bold\n- Second\ncontinued\n a. Nested one\n b. Nested two\n- Last\n\n┌──────┬───────┐\n│ Name │ Value │\n├──────┼───────┤\n│ one │ two │\n├──────┼───────┤\n│ 三 │ 42 │\n└──────┴───────┘\n\nAfter", + "memory": { + "input": "# Rules\n\n\n\nUse **Bun**.\n\n```md\n\n@./ignored.md\n```\n\n@./included.md\n", + "content": "# Rules\n\nUse **Bun**.\n\n```md\n\n@./ignored.md\n```\n\n@./included.md\n", + "stripped": true + }, + "memoryFiles": [ + { + "name": "CLAUDE.md", + "content": "# Rules\n\nUse **Bun**.\n\n```md\n\n@./ignored.md\n```\n\n@./included.md\n" + }, + { + "name": "included.md", + "content": "Included rule\n" + } + ] +} diff --git a/src/test/fixtures/markdownRendering.fixture.tsx b/src/test/fixtures/markdownRendering.fixture.tsx new file mode 100644 index 0000000000..ef82112cdf --- /dev/null +++ b/src/test/fixtures/markdownRendering.fixture.tsx @@ -0,0 +1,68 @@ +import { mkdir, writeFile } from 'node:fs/promises' +import { join } from 'node:path' +import { stripVTControlCharacters as stripAnsi } from 'node:util' +import { marked, type Tokens } from 'marked' +import React from 'react' +import { call as copy } from '../../commands/copy/copy.js' +import { Markdown, StreamingMarkdown } from '../../components/Markdown.js' +import { MarkdownTable } from '../../components/MarkdownTable.js' +import { AppStateProvider, getDefaultAppState } from '../../state/AppState.js' +import { processMemoryFile, stripHtmlComments } from '../../utils/claudemd.js' +import { enableConfigs } from '../../utils/config.js' +import { applyMarkdown } from '../../utils/markdown.js' +import { renderToString } from '../../utils/staticRender.js' +import baseline from './markdownRendering.baseline.json' + +// Run in a fresh process: exercise real rendering/configuration without shared +// module mocks or settings mutations from the rest of the unit suite. +enableConfigs() +const output: Record = {} +for (const [name, fixture] of Object.entries(baseline)) { + if (typeof fixture !== 'object' || !('terminal' in fixture)) continue + let codes: { code: string; lang?: string }[] = [] + if (fixture.codes.length) { + const picker = await copy( + () => { throw new Error('Expected the code block picker') }, + { + messages: [{ + type: 'assistant', + message: { content: [{ type: 'text', text: fixture.input }] }, + }], + } as unknown as Parameters[1], + '', + ) + if (!React.isValidElement<{ codeBlocks: typeof codes }>(picker)) { + throw new Error('Missing code block picker') + } + codes = picker.props.codeBlocks + } + output[name] = { + input: fixture.input, + terminal: stripAnsi(applyMarkdown(fixture.input, 'dark')), + codes, + } +} +const table = marked.lexer(baseline.table.input).find(t => t.type === 'table') as Tokens.Table +output.tableComponent = await renderToString( + , 80, +) +const defaultState = getDefaultAppState() +const state = { + ...defaultState, + settings: { ...defaultState.settings, syntaxHighlightingDisabled: true }, +} +const combined = [baseline.headings.input, baseline.tight.input, baseline.table.input].join('\n\n') +for (const Component of [Markdown, StreamingMarkdown]) { + output[Component.name] = await renderToString( + {combined}, 80, + ) +} +output.memory = { input: baseline.memory.input, ...stripHtmlComments(baseline.memory.input) } +const dir = process.argv[2]! +await mkdir(dir, { recursive: true }) +await writeFile(join(dir, 'CLAUDE.md'), baseline.memory.input) +await writeFile(join(dir, 'included.md'), 'Included rule\n') +output.memoryFiles = ( + await processMemoryFile(join(dir, 'CLAUDE.md'), 'Project', new Set(), true) +).map(f => ({ name: f.path.split(/[\\/]/).at(-1), content: f.content })) +console.log(JSON.stringify(output)) diff --git a/src/utils/claudemd.ts b/src/utils/claudemd.ts index a332a3cf07..9cb1990fdb 100644 --- a/src/utils/claudemd.ts +++ b/src/utils/claudemd.ts @@ -312,14 +312,20 @@ function stripHtmlCommentsFromTokens(tokens: ReturnType): { let result = '' let stripped = false - for (const token of tokens) { + for (let index = 0; index < tokens.length; index++) { + const token = tokens[index]! if (token.type === 'html') { const trimmed = token.raw.trimStart() if (trimmed.startsWith('')) { // Per CommonMark, a type-2 HTML block ends at the *line* containing // `-->`, so text after `-->` on that line is part of this token. // Strip only the comment spans and keep any residual content. - const residue = stripHtmlCommentSpans(token.raw) + // marked emits trailing blank lines as a separate token. Remove them + // with a comment-only block, or retain them with its visible residue. + const next = tokens[index + 1] + const trailingSpace = next?.type === 'space' ? next.raw : '' + if (trailingSpace) index++ + const residue = stripHtmlCommentSpans(token.raw + trailingSpace) stripped = true if (residue.trim().length > 0) { // Residual content exists (e.g. ` Use bun`): keep it. diff --git a/src/utils/markdown.test.ts b/src/utils/markdown.test.ts new file mode 100644 index 0000000000..f500b698e8 --- /dev/null +++ b/src/utils/markdown.test.ts @@ -0,0 +1,45 @@ +import { afterAll, beforeAll, describe, expect, test } from 'bun:test' +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import baseline from '../test/fixtures/markdownRendering.baseline.json' + +let directory: string +let actual: Record + +beforeAll(() => { + directory = mkdtempSync(join(tmpdir(), 'openclaude-markdown-')) + const child = Bun.spawnSync([ + process.execPath, 'run', + join(import.meta.dir, '../test/fixtures/markdownRendering.fixture.tsx'), + directory, + ], { + env: { + ...process.env, + CLAUDE_CONFIG_DIR: join(directory, 'config'), + OPENCLAUDE_CONFIG_DIR: join(directory, 'config'), + }, + stdout: 'pipe', + stderr: 'pipe', + timeout: 15_000, + }) + expect(child.exitCode, child.stderr.toString()).toBe(0) + expect(child.stderr.toString()).not.toContain('[Ink:uncaught]') + actual = JSON.parse(child.stdout.toString()) + // The child has a 15-second deadline; allow another five seconds for setup + // and assertions when the test suite is sharing a busy runner. +}, 20_000) + +afterAll(() => { + if (directory) rmSync(directory, { recursive: true, force: true }) +}) + +// Captured through the same production paths using marked 15.0.12 before the +// dependency change. Assert rendered output, not the lexer's internal shape. +describe('markdown output compatibility', () => { + for (const [name, expected] of Object.entries(baseline)) { + test(name, () => { + expect(actual[name]).toEqual(expected) + }) + } +}) diff --git a/src/utils/markdown.ts b/src/utils/markdown.ts index 50e9928dd4..a41f479f25 100644 --- a/src/utils/markdown.ts +++ b/src/utils/markdown.ts @@ -41,7 +41,9 @@ export function applyMarkdown( configureMarked() return marked .lexer(stripPromptXMLTags(content)) - .map(_ => formatToken(_, theme, 0, null, null, highlight)) + .map((token, index, tokens) => + formatToken(token, theme, 0, null, null, highlight, tokens[index - 1]), + ) .join('') .trim() } @@ -53,11 +55,14 @@ export function formatToken( orderedListNumber: number | null = null, parent: Token | null = null, highlight: CliHighlight | null = null, + previousToken?: Token, ): string { switch (token.type) { case 'blockquote': { const inner = (token.tokens ?? []) - .map(_ => formatToken(_, theme, 0, null, null, highlight)) + .map((child, index, tokens) => + formatToken(child, theme, 0, null, null, highlight, tokens[index - 1]), + ) .join('') // Prefix each line with a dim vertical bar. Keep text italic but at // normal brightness — chalk.dim is nearly invisible on dark themes. @@ -70,8 +75,12 @@ export function formatToken( .join(EOL) } case 'code': { + const text = + token.codeBlockStyle === 'indented' + ? token.text.replace(/\n+$/, '') + : token.text if (!highlight) { - return token.text + EOL + return text + EOL } let language = 'plaintext' if (token.lang) { @@ -83,7 +92,7 @@ export function formatToken( ) } } - return highlight.highlight(token.text, { language }) + EOL + return highlight.highlight(text, { language }) + EOL } case 'codespan': { // inline code @@ -176,17 +185,34 @@ export function formatToken( case 'list_item': return (token.tokens ?? []) .map( - _ => - `${' '.repeat(listDepth)}${formatToken(_, theme, listDepth + 1, orderedListNumber, token, highlight)}`, + (child, index, tokens) => + child.type === 'checkbox' + ? '' + : `${' '.repeat(listDepth)}${formatToken(child, theme, listDepth + 1, orderedListNumber, token, highlight, tokens[index - 1])}`, ) .join('') case 'paragraph': + // Loose list children are paragraphs; retain the list marker path. + if (parent?.type === 'list_item') { + return formatToken( + { ...token, type: 'text' }, theme, listDepth, + orderedListNumber, parent, highlight, + ) + } return ( (token.tokens ?? []) .map(_ => formatToken(_, theme, 0, null, null, highlight)) .join('') + EOL ) case 'space': + // These blocks already supply their spacing. Their trailing blank lines + // are separate space tokens in marked 18. + if ( + previousToken && ( + ['heading', 'table', 'html', 'def'].includes(previousToken.type) || + (previousToken.type === 'code' && previousToken.codeBlockStyle === 'indented') + ) + ) return '' return EOL case 'br': return EOL From 2976dbc0b0799610ed95a20dac4fb48ecea95f5d Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 04:16:15 -0700 Subject: [PATCH 073/121] test: reset cached model strings in claude lifecycle tests (#2384) The custom-gateway lifecycle case initializes the process-global model string table with OpenAI aliases. Teardown did not clear it, so later getAgentModel haiku and fast-mode Opus tests in the same serial process resolved stale gpt-4o aliases. Reset the table in lifecycle setup and teardown. --- src/services/api/claude.lifecycle.test.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/services/api/claude.lifecycle.test.ts b/src/services/api/claude.lifecycle.test.ts index 4f7d215d02..9a014282a7 100644 --- a/src/services/api/claude.lifecycle.test.ts +++ b/src/services/api/claude.lifecycle.test.ts @@ -7,6 +7,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'fs' import { tmpdir } from 'os' import { join } from 'path' import { + resetModelStringsForTestingOnly, setAllowedSettingSources, setFlagSettingsInline, setFlagSettingsPath, @@ -503,6 +504,7 @@ function setIgnoredApiKeyHelper(): void { beforeEach(async () => { await acquireSharedMutationLock('claude.lifecycle.test.ts') + resetModelStringsForTestingOnly() setFlagSettingsPath(undefined) setFlagSettingsInline(null) setAllowedSettingSources([...SETTING_SOURCES]) @@ -543,6 +545,7 @@ afterEach(async () => { setFlagSettingsPath(undefined) setFlagSettingsInline(null) setAllowedSettingSources([...SETTING_SOURCES]) + resetModelStringsForTestingOnly() resetSettingsCache() getClaudeAIOAuthTokens.cache?.clear?.() resetGrowthBook() From 196c5981a8e2ff3591db95d7f9e11d7d1a6c507a Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 04:31:06 -0700 Subject: [PATCH 074/121] chore(deps): update execa to 10.x and chalk to 6.x (#2385) --- CONTRIBUTING.md | 25 ++++++++++++ bun.lock | 14 ++++--- package.json | 4 +- src/ink/colorize.test.ts | 46 +++++++++++++++++++++++ src/utils/execFileNoThrowPortable.test.ts | 26 +++++++++++++ src/utils/execFileNoThrowPortable.ts | 4 +- 6 files changed, 109 insertions(+), 10 deletions(-) create mode 100644 src/ink/colorize.test.ts create mode 100644 src/utils/execFileNoThrowPortable.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9a33cb91d0..4c88efedab 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -203,6 +203,31 @@ untouched. When updating `marked`, compare representative terminal, table, list, code, link, streaming, and memory-file output and run the markdown regression tests. Regenerate the exact dependency pin and lockfile with `bun add --exact marked@`. +### Subprocesses and terminal colors + +Use the exact `execa` and `chalk` pins in `package.json` and their public ESM +exports and bundled types. Regenerate both pins and the lockfile with +`bun add --exact execa@ chalk@`. + +Prefer asynchronous `execa(file, args, options)`; use `shell: true` only for +commands that need shell syntax. With `reject: false`, check `failed`, +`timedOut` and `exitCode` before consuming `stdout` or `stderr`. Use +`SyncOptions` for synchronous options. The subprocess is a promise; access +Node child-process event and lifecycle APIs through `subprocess.nodeChildProcess`. +Use template strings or `parseCommandString()` for command splitting, and +`ipc: true` for IPC. Explicit `input` takes precedence over inherited stdin; +set stdin deliberately, and bound subprocess output with `maxBuffer` and +execution time with `timeout`. + +Use the shared default `chalk` instance for terminal styles and the existing +`colorize()` and theme helpers for theme colors. Keep the VS Code color boost +and tmux clamp in `src/ink/colorize.ts`. Numeric `FORCE_COLOR` selects an exact +level (0 disables color, 1 selects 16 colors, 2 selects 256, 3 selects truecolor); +`ansi256()` and `bgAnsi256()` downsample at level 1. Use `new Chalk({ level })` +only when a separate capability level is intentional, as for Apple Terminal +charts. Tests that change `chalk.level` must restore it, and environment-driven +color detection tests must import in a fresh process. + ## AI Agent Guidelines If you are an AI agent (Copilot, Cursor, Claude, etc.) working on this codebase, refer to [AGENTS.md](AGENTS.md) for project-specific coding guidelines, conventions, and validation commands. Following these guidelines will help your contributions align with the project's patterns and reduce review friction. diff --git a/bun.lock b/bun.lock index 1cffe990c6..c42072af5a 100644 --- a/bun.lock +++ b/bun.lock @@ -35,7 +35,7 @@ "auto-bind": "5.0.1", "axios": "1.20.0", "bidi-js": "1.1.0", - "chalk": "5.6.2", + "chalk": "6.0.1", "chokidar": "4.0.3", "cli-boxes": "4.0.1", "cli-highlight": "2.1.11", @@ -46,7 +46,7 @@ "emoji-regex": "11.0.0", "env-paths": "4.0.0", "eslint": "9.39.5", - "execa": "9.6.1", + "execa": "10.2.1", "fflate": "0.8.3", "figures": "6.1.0", "fuse.js": "7.5.0", @@ -649,7 +649,7 @@ "camelcase": ["camelcase@5.3.1", "", {}, "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg=="], - "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + "chalk": ["chalk@6.0.1", "", {}, "sha512-/Ce6KNm3vIbWdMlNna6RVIZ/ICQxnJxCicet5LBKK9ZffBkqzDw0xh9EiKSljdRtiIQ1S1z4YgcscUUGzNCWrA=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], @@ -767,7 +767,7 @@ "eventsource-parser": ["eventsource-parser@3.0.8", "", {}, "sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ=="], - "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "execa": ["execa@10.2.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "figures": "^6.1.0", "get-stream": "^9.0.1", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.3.1", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "which-command": "^0.1.0", "yoctocolors": "^2.2.0" } }, "sha512-/Bofl3ZBh2Q4/PG0W63qFzP5vDAypi8Zc90UWTBzaDNIYiX7apPOd0N6L8hG8CojUdq4V51adu8BObP+iVsSTw=="], "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], @@ -1081,7 +1081,7 @@ "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "pretty-ms": ["pretty-ms@9.3.1", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA=="], "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], @@ -1245,6 +1245,8 @@ "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "which-command": ["which-command@0.1.0", "", { "bin": { "which-command": "cli.js" } }, "sha512-XZyoF5/5hZtXitIwzrU4NKK+Wtbb9aB9CezUEw2Q0wlYK8NUYQxC1rRXgNueYLtBAJwXIb+/tFVk4dozciNJMA=="], + "which-module": ["which-module@2.0.1", "", {}, "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ=="], "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], @@ -1269,7 +1271,7 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "yoctocolors": ["yoctocolors@2.2.0", "", {}, "sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg=="], "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], diff --git a/package.json b/package.json index 040b6200ae..7659b5a9b4 100644 --- a/package.json +++ b/package.json @@ -115,7 +115,7 @@ "auto-bind": "5.0.1", "axios": "1.20.0", "bidi-js": "1.1.0", - "chalk": "5.6.2", + "chalk": "6.0.1", "chokidar": "4.0.3", "cli-boxes": "4.0.1", "cli-highlight": "2.1.11", @@ -126,7 +126,7 @@ "emoji-regex": "11.0.0", "env-paths": "4.0.0", "eslint": "9.39.5", - "execa": "9.6.1", + "execa": "10.2.1", "fflate": "0.8.3", "figures": "6.1.0", "fuse.js": "7.5.0", diff --git a/src/ink/colorize.test.ts b/src/ink/colorize.test.ts new file mode 100644 index 0000000000..bfbbc59728 --- /dev/null +++ b/src/ink/colorize.test.ts @@ -0,0 +1,46 @@ +import { expect, test } from 'bun:test' +import { spawnSync } from 'node:child_process' + +const colorizeUrl = new URL('./colorize.ts', import.meta.url).href + +// Each process imports chalk and colorize after its terminal environment is set. +// Leave two seconds for assertions beyond the 8-second subprocess deadline. +test.each([ + { force: '0', terminal: '', tmux: '', level: 0, color: 'X' }, + { force: '1', terminal: '', tmux: '', level: 1, color: '\x1b[91mX\x1b[39m' }, + { force: '2', terminal: '', tmux: '', level: 2, color: '\x1b[38;5;196mX\x1b[39m' }, + { force: '3', terminal: '', tmux: '', level: 3, color: '\x1b[38;5;196mX\x1b[39m' }, + { force: '0', terminal: 'vscode', tmux: '', level: 0, color: 'X' }, + { force: '2', terminal: 'vscode', tmux: '', level: 3, color: '\x1b[38;5;196mX\x1b[39m' }, + { force: '3', terminal: 'vscode', tmux: 'test', level: 2, color: '\x1b[38;5;196mX\x1b[39m' }, +])('colorize respects FORCE_COLOR=$force, terminal=$terminal, tmux=$tmux', ({ force, terminal, tmux, level, color }) => { + const script = ` + import chalk from 'chalk'; + import { colorize } from ${JSON.stringify(colorizeUrl)}; + process.stdout.write(JSON.stringify({ + level: chalk.level, + foreground: colorize('X', 'ansi256(196)', 'foreground'), + background: colorize('X', 'ansi256(196)', 'background'), + })); + ` + const result = spawnSync(process.execPath, ['--eval', script], { + env: { + ...process.env, + FORCE_COLOR: force, + TERM_PROGRAM: terminal, + TMUX: tmux, + CLAUDE_CODE_TMUX_TRUECOLOR: '', + COLORTERM: '', + }, + encoding: 'utf8', + timeout: 8_000, + }) + expect(result.error).toBeUndefined() + expect(result.status).toBe(0) + const rendered = JSON.parse(result.stdout) + expect(rendered.level).toBe(level) + expect(rendered.foreground).toBe(color) + expect(rendered.background).toBe(level === 0 ? 'X' : level === 1 + ? '\x1b[101mX\x1b[49m' + : '\x1b[48;5;196mX\x1b[49m') +}, 10_000) diff --git a/src/utils/execFileNoThrowPortable.test.ts b/src/utils/execFileNoThrowPortable.test.ts new file mode 100644 index 0000000000..0ac632b3ff --- /dev/null +++ b/src/utils/execFileNoThrowPortable.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from 'bun:test' +import { execSyncWithDefaults_DEPRECATED } from './execFileNoThrowPortable.js' + +// Allow assertions and cleanup beyond the subprocess's 8-second deadline. +const options = { timeout: 8_000 } + +test('portable sync execution trims captured stdout', () => { + expect(execSyncWithDefaults_DEPRECATED('echo hello', options)).toBe('hello') +}, 10_000) + +test('portable sync execution preserves stdout on nonzero exit', () => { + expect(execSyncWithDefaults_DEPRECATED('echo partial && exit 7', options)).toBe('partial') +}, 10_000) + +test('portable sync execution returns null without stdout', () => { + expect(execSyncWithDefaults_DEPRECATED('exit 7', options)).toBeNull() +}, 10_000) + +test('portable sync execution forwards explicit stdin', () => { + const command = `"${process.execPath}" -e "process.stdin.pipe(process.stdout)"` + expect(execSyncWithDefaults_DEPRECATED(command, { + ...options, + input: 'payload\n', + stdio: ['pipe', 'pipe', 'pipe'], + })).toBe('payload') +}, 10_000) diff --git a/src/utils/execFileNoThrowPortable.ts b/src/utils/execFileNoThrowPortable.ts index 674653382c..e6af9ed43d 100644 --- a/src/utils/execFileNoThrowPortable.ts +++ b/src/utils/execFileNoThrowPortable.ts @@ -9,8 +9,8 @@ type ExecSyncOptions = { abortSignal?: AbortSignal timeout?: number input?: string - // Sync variant: execaSync rejects async-only stdio values like 'overlapped' - stdio?: ExecaSyncOptions['stdio'] + // This wrapper uses UTF-8 output, so exclude binary-only stdio transforms. + stdio?: Extract['stdio'] } /** From 58d8a9d67fcce4a6514f849799f3eb39d7619fc0 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 04:58:01 -0700 Subject: [PATCH 075/121] chore(deps): update https-proxy-agent to 9.1.0 (#2383) * chore(deps): update https-proxy-agent to 9.1.0 * test(proxy): isolate CONNECT test from leaked axios mocks * test(proxy): avoid global axios module registration * test(proxy): exercise createAxiosInstance in CONNECT test --- CONTRIBUTING.md | 28 +++++ bun.lock | 18 ++- package.json | 2 +- src/utils/proxy.connect.test.ts | 195 ++++++++++++++++++++++++++++++++ 4 files changed, 239 insertions(+), 4 deletions(-) create mode 100644 src/utils/proxy.connect.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4c88efedab..93b736491e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -249,6 +249,34 @@ content: serialized headers may quote or escape filenames, and parsed patches can omit filenames. Check the diff and edit-snippet tests when changing these paths. +### Proxy agents + +Use the shared helpers in `src/utils/proxy.ts`: `createAxiosInstance()` for +Axios, `getWebSocketProxyAgent(url)` for Node WebSockets, and `getProxyFetchOptions()` +for fetch. The Axios and WebSocket helpers apply `NO_PROXY` bypasses before +selecting an agent. `getWebSocketProxyUrl(url)` supplies Bun's native WebSocket proxy +option. Lowercase proxy environment variables take precedence over uppercase +variants on the Axios and WebSocket paths. + +`https-proxy-agent` is pinned exactly to `9.1.0` and remains an external runtime +module in `scripts/externals.ts`. Use its public named ESM exports, such as +`import { HttpsProxyAgent } from 'https-proxy-agent'`, and the constructor +`new HttpsProxyAgent(proxyUrl, options)`. Both it and `agent-base` are ESM +packages requiring Node.js 20 or later; use OpenClaude's supported Node.js +`>=24.22.1` runtime. Keep dependency updates exact and generate `bun.lock` with +`bun add --exact https-proxy-agent@`. + +Keep TLS verification enabled. Configure `NODE_EXTRA_CA_CERTS` before starting +the CLI when a custom CA is needed. For runtime certificate configuration, the +shared CA loader reads that file and the TLS helpers supply explicit options; +clear the CA and mTLS caches after configuration changes. Proxy-agent +constructor options configure the connection to the proxy. Supply target TLS +options on the HTTPS request, using `getWebSocketTLSOptions()` where applicable; +do not assume constructor `ca` options also trust the target inside a CONNECT +tunnel. Run `bun test src/utils/proxy.connect.test.ts src/utils/proxy.test.ts` +when changing these paths to verify HTTPS CONNECT routing and `NO_PROXY` +bypass with certificate validation enabled. + ## Provider Changes OpenClaude supports multiple provider paths. Before contributing provider changes, review the relevant documentation to ensure your implementation follows the expected patterns: diff --git a/bun.lock b/bun.lock index c42072af5a..443906786b 100644 --- a/bun.lock +++ b/bun.lock @@ -54,7 +54,7 @@ "google-auth-library": "10.6.2", "graphology": "0.26.0", "graphology-metrics": "2.4.2", - "https-proxy-agent": "7.0.6", + "https-proxy-agent": "9.1.0", "ignore": "7.0.13", "indent-string": "5.0.0", "js-tiktoken": "1.0.21", @@ -597,7 +597,7 @@ "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], - "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "agent-base": ["agent-base@9.0.0", "", {}, "sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA=="], "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -877,7 +877,7 @@ "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], - "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "https-proxy-agent": ["https-proxy-agent@9.1.0", "", { "dependencies": { "agent-base": "9.0.0", "debug": "^4.3.4", "proxy-agent-negotiate": "1.1.0" } }, "sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA=="], "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], @@ -1089,6 +1089,8 @@ "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-agent-negotiate": ["proxy-agent-negotiate@1.1.0", "", { "peerDependencies": { "kerberos": "^2.0.0" }, "optionalPeers": ["kerberos"] }, "sha512-N8IBcM3UgCVzz2L2Lqv8DVntDnnC8/hiV4nEDUPkqq72TPUgYWjQc+bdZlBPZK9LzPAvOY//gAt0S0DApoOXWQ=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], @@ -1487,6 +1489,8 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "bun-types/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], @@ -1513,8 +1517,12 @@ "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "gaxios/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "get-intrinsic/hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + "http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "knip/zod": ["zod@4.6.7", "", {}, "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg=="], "needle/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], @@ -1561,6 +1569,8 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="], + "@typespec/ts-http-runtime/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], @@ -1589,6 +1599,8 @@ "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "gaxios/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "qrcode/yargs/cliui": ["cliui@6.0.0", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^6.2.0" } }, "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ=="], "qrcode/yargs/find-up": ["find-up@4.1.0", "", { "dependencies": { "locate-path": "^5.0.0", "path-exists": "^4.0.0" } }, "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw=="], diff --git a/package.json b/package.json index 7659b5a9b4..4c19f5c18a 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "google-auth-library": "10.6.2", "graphology": "0.26.0", "graphology-metrics": "2.4.2", - "https-proxy-agent": "7.0.6", + "https-proxy-agent": "9.1.0", "ignore": "7.0.13", "indent-string": "5.0.0", "js-tiktoken": "1.0.21", diff --git a/src/utils/proxy.connect.test.ts b/src/utils/proxy.connect.test.ts new file mode 100644 index 0000000000..596acd37b4 --- /dev/null +++ b/src/utils/proxy.connect.test.ts @@ -0,0 +1,195 @@ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { once } from 'node:events' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { createServer as createHttpServer, type IncomingMessage } from 'node:http' +import { + createServer as createHttpsServer, + get, + request, + type RequestOptions, +} from 'node:https' +import { connect, type AddressInfo } from 'node:net' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import type { Duplex } from 'node:stream' +import { snapshotEnv } from '../test/envSnapshot.js' +import { clearCACertsCache } from './caCerts.js' +import { clearMTLSCache, getMTLSAgent, getWebSocketTLSOptions } from './mtls.js' + +// Cache-busting proxy.ts alone still imports any leaked global Axios mock. +// Build a fresh copy of the production module with only its Axios import +// redirected to the real CommonJS entry. Keep the other imports external so +// CA/mTLS caches stay shared, without changing any global mock registration. +const nonce = `${Date.now()}-${Math.random()}` +const proxyBuild = await Bun.build({ + entrypoints: [join(import.meta.dir, 'proxy.ts')], + target: 'bun', + plugins: [{ + name: 'connect-test-real-axios', + setup(build) { + build.onResolve({ filter: /.*/ }, args => { + if (!args.importer) return + return { + path: args.path === 'axios' + ? `${require.resolve('axios')}?real=${nonce}` + : Bun.resolveSync(args.path, args.resolveDir), + external: true, + } + }) + }, + }], +}) +if (!proxyBuild.success) { + throw new AggregateError(proxyBuild.logs, 'Could not build proxy.ts for CONNECT tests') +} +const { createAxiosInstance, getWebSocketProxyAgent } = await import( + `data:text/javascript;base64,${Buffer.from(await proxyBuild.outputs[0].text()).toString('base64')}` +) as typeof import('./proxy.js') + +// Self-signed test certificate and key, valid only for the loopback target. +const cert = `-----BEGIN CERTIFICATE----- +MIIBmzCCAUGgAwIBAgIUAtOYg4xV7C9catkY/bwZc7XywMgwCgYIKoZIzj0EAwIw +FDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MTAyNDA4NTUxNloYDzIxMjYwOTMw +MDg1NTE2WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjO +PQMBBwNCAAS5HiqEES/b1Oe+NOKT+Cg3Gh89Rk0O7bSu0aDJpwdMzOxgOT2jTBmK +2H5TUBnDWHjLktPtbmQQ4nMAb1kD4oKWo28wbTAdBgNVHQ4EFgQUu+xqnGfB/gu8 +DbNNHlVonV69Y64wHwYDVR0jBBgwFoAUu+xqnGfB/gu8DbNNHlVonV69Y64wDwYD +VR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwCgYIKoZI +zj0EAwIDSAAwRQIgPy5jbT1ECH7LFy5r5JrV7xfsOkhwtbuwqVeidDBynFACIQCp +sTyTz0llkGsTStOnpDBTzTUQzcDT8z6ZgqqxbY29nw== +-----END CERTIFICATE-----` +const key = `-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXc/iw7bBMrZkWHsr +tDvMDJ6qPLsspu6IH8lHZ3ccGvihRANCAAS5HiqEES/b1Oe+NOKT+Cg3Gh89Rk0O +7bSu0aDJpwdMzOxgOT2jTBmK2H5TUBnDWHjLktPtbmQQ4nMAb1kD4oKW +-----END PRIVATE KEY-----` + +describe('HTTPS_PROXY CONNECT transport', () => { + let restoreEnv: () => void + let directory: string + const sockets = new Set() + let target: ReturnType + let proxy: ReturnType + let targetUrl: string + let connects: string[] + + beforeEach(async () => { + restoreEnv = snapshotEnv([ + 'HTTPS_PROXY', + 'https_proxy', + 'HTTP_PROXY', + 'http_proxy', + 'NO_PROXY', + 'no_proxy', + 'NODE_EXTRA_CA_CERTS', + 'NODE_OPTIONS', + 'NODE_TLS_REJECT_UNAUTHORIZED', + 'CLAUDE_CODE_CLIENT_CERT', + 'CLAUDE_CODE_CLIENT_KEY', + 'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE', + 'CLAUDE_CODE_PROXY_RESOLVES_HOSTS', + ], { clear: true }) + directory = mkdtempSync(join(tmpdir(), 'proxy-connect-')) + const caPath = join(directory, 'ca.pem') + writeFileSync(caPath, cert) + process.env.NODE_EXTRA_CA_CERTS = caPath + clearCACertsCache() + clearMTLSCache() + connects = [] + + target = createHttpsServer({ key, cert }, (_request, response) => { + response.end('secure response') + }) + target.listen(0, '127.0.0.1') + await once(target, 'listening') + const targetPort = (target.address() as AddressInfo).port + targetUrl = `https://127.0.0.1:${targetPort}/test` + + proxy = createHttpServer() + proxy.on('connect', (request, client, head) => { + connects.push(request.url!) + // Forward only to this test's target; assert the CONNECT authority below. + const upstream = connect(targetPort, '127.0.0.1', () => { + client.write('HTTP/1.1 200 Connection Established\r\n\r\n') + if (head.length) upstream.write(head) + client.pipe(upstream) + upstream.pipe(client) + }) + sockets.add(client) + sockets.add(upstream) + client.on('error', () => upstream.destroy()) + upstream.on('error', () => client.destroy()) + }) + proxy.listen(0, '127.0.0.1') + await once(proxy, 'listening') + process.env.HTTPS_PROXY = `http://127.0.0.1:${(proxy.address() as AddressInfo).port}` + }) + + afterEach(async () => { + getMTLSAgent()?.destroy() + for (const socket of sockets) socket.destroy() + sockets.clear() + target?.closeAllConnections() + await Promise.all( + [target, proxy].filter(Boolean).map(server => + new Promise((resolve, reject) => { + server.close(error => error ? reject(error) : resolve()) + }), + ), + ) + restoreEnv() + clearCACertsCache() + clearMTLSCache() + rmSync(directory, { recursive: true, force: true }) + }) + + test('Axios tunnels HTTPS through HTTPS_PROXY and bypasses it for NO_PROXY', async () => { + const client = createAxiosInstance() + // Native TLS reads NODE_EXTRA_CA_CERTS only at startup. Pass the runtime + // CA loader's result to the target request; agent options configure the proxy. + client.defaults.transport = { + request: ( + options: RequestOptions, + callback: (response: IncomingMessage) => void, + ) => request({ ...options, ...getWebSocketTLSOptions() }, callback), + } + const response = await client.get(targetUrl, { timeout: 3000 }) + expect(response.data).toBe('secure response') + expect(connects).toEqual([new URL(targetUrl).host]) + + process.env.NO_PROXY = '127.0.0.1' + const direct = await client.get(targetUrl, { timeout: 3000 }) + expect(direct.data).toBe('secure response') + expect(connects).toHaveLength(1) + }) + + test('WebSocket agent tunnels HTTPS and is omitted for NO_PROXY', async () => { + const agent = getWebSocketProxyAgent(targetUrl.replace('https:', 'wss:')) + expect(agent).toBeDefined() + try { + const body = await new Promise((resolve, reject) => { + const request = get( + targetUrl, + { agent, ...getWebSocketTLSOptions() }, + response => { + response.setEncoding('utf8') + let data = '' + response.on('data', chunk => { data += chunk }) + response.on('end', () => resolve(data)) + response.on('error', reject) + }, + ) + request.on('error', reject) + request.setTimeout(3000, () => + request.destroy(new Error('HTTPS request timed out')), + ) + }) + expect(body).toBe('secure response') + expect(connects).toEqual([new URL(targetUrl).host]) + process.env.NO_PROXY = '127.0.0.1' + expect(getWebSocketProxyAgent(targetUrl.replace('https:', 'wss:'))).toBeUndefined() + } finally { + agent?.destroy() + } + }) +}) From df184fbafcad18d1feada4a4da38fecb9ce25c49 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 06:11:04 -0700 Subject: [PATCH 076/121] fix(proxy): pass runtime CA and mTLS options to the target TLS connection behind CONNECT proxies (#2386) * fix(proxy): pass runtime CA and mTLS options to the target TLS connection behind CONNECT proxies * test(proxy): isolate CONNECT tests from leaked axios mocks --- src/utils/proxy.test.ts | 143 ++++++++++++++++++++++++++++++++++++++++ src/utils/proxy.ts | 28 +++++++- 2 files changed, 170 insertions(+), 1 deletion(-) diff --git a/src/utils/proxy.test.ts b/src/utils/proxy.test.ts index a64a0b9b05..9f90bcff5e 100644 --- a/src/utils/proxy.test.ts +++ b/src/utils/proxy.test.ts @@ -1,6 +1,149 @@ import { describe, expect, test } from 'bun:test' +import { execFileSync } from 'node:child_process' +import { mkdtempSync, readFileSync, rmSync } from 'node:fs' +import { createServer as createHttpServer } from 'node:http' +import { createServer as createHttpsServer } from 'node:https' +import { connect, type AddressInfo, type Socket } from 'node:net' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { getGlobalDispatcher, setGlobalDispatcher } from 'undici' +import { snapshotEnv } from '../test/envSnapshot.js' +import { clearCACertsCache } from './caCerts.js' +import { clearMTLSCache } from './mtls.js' import { shouldBypassProxy } from './proxy.js' +// Other suites can leave a process-wide axios mock behind. Give this copy of +// proxy.ts and the global-client test the same real axios, without registering +// a global mock. Keep other imports external so CA/mTLS caches stay shared. +const axiosPath = `${require.resolve('axios')}?connect=${Date.now()}-${Math.random()}` +const { default: axios } = await import(axiosPath) as typeof import('axios') +const proxyBuild = await Bun.build({ + entrypoints: [join(import.meta.dir, 'proxy.ts')], + target: 'bun', + plugins: [{ + name: 'connect-test-real-axios', + setup(build) { + build.onResolve({ filter: /.*/ }, args => { + if (!args.importer) return + return { + path: args.path === 'axios' + ? axiosPath + : Bun.resolveSync(args.path, args.resolveDir), + external: true, + } + }) + }, + }], +}) +if (!proxyBuild.success) { + throw new AggregateError(proxyBuild.logs, 'Could not build proxy.ts for CONNECT tests') +} +const { clearProxyCache, configureGlobalAgents, createAxiosInstance } = await import( + `data:text/javascript;base64,${Buffer.from(await proxyBuild.outputs[0].text()).toString('base64')}` +) as typeof import('./proxy.js') + +describe('runtime CA certificates through CONNECT', () => { + test.each(['instance', 'global'] as const)('%s axios trusts a runtime CA', async path => { + const restoreEnv = snapshotEnv([ + 'HTTPS_PROXY', 'https_proxy', 'HTTP_PROXY', 'http_proxy', + 'NO_PROXY', 'no_proxy', 'NODE_EXTRA_CA_CERTS', 'NODE_OPTIONS', + 'CLAUDE_CODE_CLIENT_CERT', 'CLAUDE_CODE_CLIENT_KEY', + 'CLAUDE_CODE_CLIENT_KEY_PASSPHRASE', 'CLAUDE_CODE_PROXY_RESOLVES_HOSTS', + 'NODE_TLS_REJECT_UNAUTHORIZED', + ], { clear: true }) + const defaults = { + proxy: axios.defaults.proxy, + httpAgent: axios.defaults.httpAgent, + httpsAgent: axios.defaults.httpsAgent, + } + const dispatcher = getGlobalDispatcher() + const directory = mkdtempSync(join(tmpdir(), 'proxy-ca-')) + const sockets = new Set() + const proxy = createHttpServer() + let target: ReturnType | undefined + let connectCount = 0 + let instance: ReturnType | undefined + + try { + const certFile = join(directory, 'cert.pem') + const keyFile = join(directory, 'key.pem') + execFileSync('openssl', [ + 'req', '-x509', '-newkey', 'rsa:2048', '-nodes', '-days', '1', + '-subj', '/CN=localhost', + '-addext', 'subjectAltName=DNS:localhost,IP:127.0.0.1', + '-keyout', keyFile, '-out', certFile, + ], { stdio: 'ignore', timeout: 5000 }) + target = createHttpsServer({ + cert: readFileSync(certFile), + key: readFileSync(keyFile), + }, (_req, res) => res.end('trusted target')) + for (const server of [proxy, target]) { + server.on('connection', socket => { + sockets.add(socket) + socket.on('close', () => sockets.delete(socket)) + }) + } + await new Promise(resolve => target!.listen(0, '127.0.0.1', resolve)) + const targetPort = (target.address() as AddressInfo).port + proxy.on('connect', (req, client, head) => { + connectCount++ + const upstream = connect(targetPort, '127.0.0.1', () => { + client.write('HTTP/1.1 200 Connection Established\r\n\r\n') + upstream.write(head) + client.pipe(upstream).pipe(client) + }) + sockets.add(upstream) + upstream.on('close', () => { + sockets.delete(upstream) + client.destroy() + }) + client.on('close', () => upstream.destroy()) + client.on('error', () => upstream.destroy()) + upstream.on('error', () => client.destroy()) + expect(req.url).toBe(`localhost:${targetPort}`) + }) + await new Promise(resolve => proxy.listen(0, '127.0.0.1', resolve)) + process.env.HTTPS_PROXY = `http://127.0.0.1:${(proxy.address() as AddressInfo).port}` + // Applied after boot, just like settings loaded by caCertsConfig.ts. + process.env.NODE_EXTRA_CA_CERTS = certFile + clearCACertsCache() + clearMTLSCache() + clearProxyCache() + + if (path === 'global') configureGlobalAgents() + else instance = createAxiosInstance() + const response = await (instance ?? axios).get(`https://localhost:${targetPort}/`, { + adapter: 'http', + timeout: 3000, + }) + expect(response.status).toBe(200) + expect(response.data).toBe('trusted target') + expect(connectCount).toBe(1) + } finally { + instance?.defaults.httpsAgent?.destroy() + for (const socket of sockets) socket.destroy() + await Promise.all([proxy, ...(target ? [target] : [])].map(server => new Promise(resolve => { + server.close(() => resolve()) + }))) + const testDispatcher = getGlobalDispatcher() + delete process.env.HTTPS_PROXY + delete process.env.NODE_EXTRA_CA_CERTS + clearCACertsCache() + clearMTLSCache() + clearProxyCache() + configureGlobalAgents() // Eject the test's global axios interceptor. + Object.assign(axios.defaults, defaults) + setGlobalDispatcher(dispatcher) + if (testDispatcher !== dispatcher) await testDispatcher.close?.() + restoreEnv() + clearCACertsCache() + clearMTLSCache() + rmSync(directory, { recursive: true, force: true }) + } + // Allow time for the 5s OpenSSL deadline plus requests and cleanup. + }, 15000) +}) + describe('shouldBypassProxy — NO_PROXY matching', () => { test('bare domain bypasses the domain and its subdomains', () => { // Regression: a bare NO_PROXY entry matched the host exactly only, so a diff --git a/src/utils/proxy.ts b/src/utils/proxy.ts index 833ed8615e..608c5074c2 100644 --- a/src/utils/proxy.ts +++ b/src/utils/proxy.ts @@ -7,6 +7,7 @@ import type { LookupOptions } from 'dns' import type { Agent } from 'http' import { HttpsProxyAgent, type HttpsProxyAgentOptions } from 'https-proxy-agent' import memoize from 'lodash-es/memoize.js' +import type { ConnectionOptions } from 'tls' import type * as undici from 'undici' import { getCACertificates } from './caCerts.js' import { logForDebugging } from './debug.js' @@ -154,6 +155,31 @@ export function shouldBypassProxy( } } +/** + * Keep target TLS defaults separate from the proxy connection options. + * HttpsProxyAgent's constructor options configure only the socket to the proxy; + * the target TLS upgrade takes its options from connect(). + */ +class TargetTLSHttpsProxyAgent extends HttpsProxyAgent { + private readonly targetTLSOptions: Pick< + ConnectionOptions, + 'ca' | 'cert' | 'key' | 'passphrase' + > + + constructor( + proxyUrl: string, + options: HttpsProxyAgentOptions & ConnectionOptions, + ) { + super(proxyUrl, options) + const { ca, cert, key, passphrase } = options + this.targetTLSOptions = { ca, cert, key, passphrase } + } + + override connect(...[req, opts]: Parameters['connect']>) { + return super.connect(req, { ...this.targetTLSOptions, ...opts }) + } +} + /** * Create an HttpsProxyAgent with optional mTLS configuration * Skips local DNS resolution to let the proxy handle it @@ -183,7 +209,7 @@ function createHttpsProxyAgent( } } - return new HttpsProxyAgent(proxyUrl, { ...agentOptions, ...extra }) + return new TargetTLSHttpsProxyAgent(proxyUrl, { ...agentOptions, ...extra }) } /** From 94e22e19d3ac1570abc267b24bf3ff165ab281f7 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 06:22:19 -0700 Subject: [PATCH 077/121] build(deps): bump google-auth-library to 11.3.1 (#2387) --- bun.lock | 12 +- package.json | 4 +- .../api/googleAuth.integration.test.ts | 168 ++++++++++++++++++ 3 files changed, 176 insertions(+), 8 deletions(-) create mode 100644 src/services/api/googleAuth.integration.test.ts diff --git a/bun.lock b/bun.lock index 443906786b..834ae44131 100644 --- a/bun.lock +++ b/bun.lock @@ -51,7 +51,7 @@ "figures": "6.1.0", "fuse.js": "7.5.0", "get-east-asian-width": "1.7.0", - "google-auth-library": "10.6.2", + "google-auth-library": "11.3.1", "graphology": "0.26.0", "graphology-metrics": "2.4.2", "https-proxy-agent": "9.1.0", @@ -105,7 +105,7 @@ }, }, "overrides": { - "google-auth-library": "10.6.2", + "google-auth-library": "11.3.1", "ip-address": "10.2.0", "lodash-es": "4.18.1", "node-domexception": "file:vendor/node-domexception-shim", @@ -819,9 +819,9 @@ "fuse.js": ["fuse.js@7.5.0", "", {}, "sha512-sQtrEfA+ez/3G0cCZecF70oqpCRttCexYUG4mUrtWL49ULUzUyxokt5kyqwtKzj1270RaKih+hcP3qLcumccow=="], - "gaxios": ["gaxios@7.1.5", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg=="], + "gaxios": ["gaxios@8.3.2", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-7VsaTExxjbDv2ZK1YuC4jbgfN33NI/6/7zdYWUHB6mcg51v4tCBZe+xzjqPzOpVzI+C5k1Kk9OQ80WVrXAHOqw=="], - "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "gcp-metadata": ["gcp-metadata@9.2.2", "", { "dependencies": { "gaxios": "^8.3.0", "google-logging-utils": "^2.0.0", "json-bigint": "^1.0.0" } }, "sha512-hlaHpCBoNCzoUjMnBZq0LbUiYqPAhXFaAgyNwQqwWU44TZIYJK3RD3o2wkNdvPs8nnUXqJjTudnodLJkLMBjow=="], "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], @@ -839,9 +839,9 @@ "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], - "google-auth-library": ["google-auth-library@10.6.2", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw=="], + "google-auth-library": ["google-auth-library@11.3.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^8.3.0", "gcp-metadata": "^9.0.0", "google-logging-utils": "^2.0.0", "jws": "^4.0.0" } }, "sha512-mQS0R2Ev/DKkzuWSYtDeq15uhDeaWuT2qzLYqJ41LmooxVKHmn5ut3pc6dlHY6wfHW/D7FBmOYjAtkjaD1A7qA=="], - "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "google-logging-utils": ["google-logging-utils@2.1.0", "", {}, "sha512-1R6uiq6XhXW2ZrcGtZpGgVDsYGzSmveVOn8Pp37qe1fbThHZ34e3t3eXj5mVu6PFSVx9fvB1KNVnYgVtq/BRcA=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], diff --git a/package.json b/package.json index 4c19f5c18a..fa8fb73a23 100644 --- a/package.json +++ b/package.json @@ -131,7 +131,7 @@ "figures": "6.1.0", "fuse.js": "7.5.0", "get-east-asian-width": "1.7.0", - "google-auth-library": "10.6.2", + "google-auth-library": "11.3.1", "graphology": "0.26.0", "graphology-metrics": "2.4.2", "https-proxy-agent": "9.1.0", @@ -213,7 +213,7 @@ }, "overrides": { "ip-address": "10.2.0", - "google-auth-library": "10.6.2", + "google-auth-library": "11.3.1", "lodash-es": "4.18.1", "node-domexception": "file:vendor/node-domexception-shim" }, diff --git a/src/services/api/googleAuth.integration.test.ts b/src/services/api/googleAuth.integration.test.ts new file mode 100644 index 0000000000..70edde8275 --- /dev/null +++ b/src/services/api/googleAuth.integration.test.ts @@ -0,0 +1,168 @@ +import { afterEach, beforeEach, expect, mock, spyOn, test } from 'bun:test' +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { GoogleAuth, UserRefreshClient } from 'google-auth-library' + +import { snapshotEnv } from '../../test/envSnapshot.js' +import { + acquireSharedMutationLock, + releaseSharedMutationLock, +} from '../../test/sharedMutationLock.js' +import { checkGcpCredentialsValid } from '../../utils/auth.js' +import { resolveGeminiCredential } from '../../utils/geminiAuth.js' +import { getAnthropicClient } from './client.js' + +const originalGetClient = GoogleAuth.prototype.getClient +const envKeys = [ + 'GOOGLE_APPLICATION_CREDENTIALS', 'GOOGLE_CLOUD_PROJECT', 'GCLOUD_PROJECT', + 'GOOGLE_CLOUD_QUOTA_PROJECT', 'GOOGLE_PROJECT_ID', + 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_SKIP_VERTEX_AUTH', + 'ANTHROPIC_VERTEX_PROJECT_ID', 'ANTHROPIC_VERTEX_BASE_URL', 'CLOUD_ML_REGION', + 'CLAUDE_CODE_USE_GEMINI', 'GEMINI_AUTH_MODE', 'GEMINI_API_KEY', 'GOOGLE_API_KEY', + 'GEMINI_ACCESS_TOKEN', 'GEMINI_BASE_URL', 'GEMINI_MODEL', + 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_FOUNDRY', + 'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_MISTRAL', 'ANTHROPIC_CUSTOM_HEADERS', +] as const + +let restoreEnv: () => void +let fixtureDir: string +let originalMacro: PropertyDescriptor | undefined +let authInstances: GoogleAuth[] +let tokenRequests: URLSearchParams[] +let apiHeaders: Headers | undefined +let apiUrl: string | undefined + +const apiFetch = (async (input, init) => { + apiUrl = String(input) + apiHeaders = new Headers(init?.headers) + const vertex = process.env.CLAUDE_CODE_USE_VERTEX === '1' + return new Response(JSON.stringify(vertex ? { + id: 'msg_auth', type: 'message', role: 'assistant', + model: 'claude-sonnet-4-6', content: [{ type: 'text', text: 'ok' }], + stop_reason: 'end_turn', stop_sequence: null, + usage: { input_tokens: 1, output_tokens: 1 }, + } : { + id: 'chatcmpl_auth', model: 'gemini-2.5-flash', + choices: [{ message: { role: 'assistant', content: 'ok' }, finish_reason: 'stop' }], + usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 }, + }), { headers: { 'Content-Type': 'application/json' } }) +}) as typeof fetch + +beforeEach(async () => { + await acquireSharedMutationLock('services/api/googleAuth.integration.test.ts') + restoreEnv = snapshotEnv(envKeys, { clear: true }) + originalMacro = Object.getOwnPropertyDescriptor(globalThis, 'MACRO') + ;(globalThis as Record).MACRO = { VERSION: 'test-version' } + fixtureDir = mkdtempSync(join(tmpdir(), 'openclaude-google-auth-')) + const credentialsPath = join(fixtureDir, 'adc.json') + writeFileSync(credentialsPath, JSON.stringify({ + type: 'authorized_user', + client_id: 'fixture-client', + client_secret: 'fixture-secret', + refresh_token: 'fixture-refresh', + quota_project_id: 'fixture-quota', + })) + process.env.GOOGLE_APPLICATION_CREDENTIALS = credentialsPath + process.env.GOOGLE_CLOUD_PROJECT = 'fixture-project' + authInstances = [] + tokenRequests = [] + apiHeaders = undefined + apiUrl = undefined + + // Keep the production constructor, ADC file loading, getClient, and token/header + // methods real. Replace only the HTTP transport of the resulting auth client. + spyOn(GoogleAuth.prototype, 'getClient').mockImplementation(async function (this: GoogleAuth) { + const client = await originalGetClient.call(this) + expect(client).toBeInstanceOf(UserRefreshClient) + authInstances.push(this) + client.transporter.defaults.fetchImplementation = async (url, init) => { + expect(String(url)).toBe('https://oauth2.googleapis.com/token') + expect(init?.method).toBe('POST') + tokenRequests.push(new URLSearchParams(String(init?.body))) + return new Response(JSON.stringify({ + access_token: 'fixture-access-token', token_type: 'Bearer', expires_in: 3600, + }), { headers: { 'Content-Type': 'application/json' } }) + } + return client + }) +}) + +afterEach(() => { + try { + mock.restore() + restoreEnv() + if (originalMacro) { + Object.defineProperty(globalThis, 'MACRO', originalMacro) + } else { + delete (globalThis as Record).MACRO + } + rmSync(fixtureDir, { recursive: true, force: true }) + } finally { + releaseSharedMutationLock() + } +}) + +function expectRealAuthFlow(): void { + expect(authInstances).toHaveLength(1) + expect(tokenRequests).toHaveLength(1) + expect(Object.fromEntries(tokenRequests[0]!)).toEqual({ + client_id: 'fixture-client', + client_secret: 'fixture-secret', + refresh_token: 'fixture-refresh', + grant_type: 'refresh_token', + }) +} + +test('Vertex loads ADC and forwards real Google auth Headers to rawPredict', async () => { + process.env.CLAUDE_CODE_USE_VERTEX = '1' + process.env.CLOUD_ML_REGION = 'us-east5' + process.env.ANTHROPIC_VERTEX_PROJECT_ID = 'fixture-target' + const client = await getAnthropicClient({ + maxRetries: 0, model: 'claude-sonnet-4-6', fetchOverride: apiFetch, + }) + await client.messages.create({ + model: 'claude-sonnet-4-6', max_tokens: 16, + messages: [{ role: 'user', content: 'hello' }], + }) + + expectRealAuthFlow() + expect(apiUrl).toContain('/projects/fixture-target/locations/us-east5/') + expect(apiUrl).toEndWith(':rawPredict') + expect(apiHeaders?.get('authorization')).toBe('Bearer fixture-access-token') + expect(apiHeaders?.get('x-goog-user-project')).toBe('fixture-quota') +}) + +test('Gemini loads ADC and normalizes the real getAccessToken result for requests', async () => { + // The OpenAI shim uses the shared fetch transport rather than fetchOverride. + spyOn(globalThis, 'fetch').mockImplementation(apiFetch) + process.env.CLAUDE_CODE_USE_GEMINI = '1' + process.env.GEMINI_AUTH_MODE = 'adc' + process.env.GEMINI_MODEL = 'gemini-2.5-flash' + process.env.GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta/openai' + const client = await getAnthropicClient({ + maxRetries: 0, model: 'gemini-2.5-flash', fetchOverride: apiFetch, + }) + await client.messages.create({ + model: 'gemini-2.5-flash', max_tokens: 16, + messages: [{ role: 'user', content: 'hello' }], + }) + + expectRealAuthFlow() + expect(apiUrl).toBe('https://generativelanguage.googleapis.com/v1beta/openai/chat/completions') + expect(apiHeaders?.get('authorization')).toBe('Bearer fixture-access-token') + expect(apiHeaders?.get('x-goog-user-project')).toBe('fixture-project') +}) + +test('Vertex credential validation loads ADC and retrieves a real access token', async () => { + await expect(checkGcpCredentialsValid()).resolves.toBe(true) + expectRealAuthFlow() +}) + +test('Vertex validation and Gemini ADC reject malformed credential files', async () => { + writeFileSync(process.env.GOOGLE_APPLICATION_CREDENTIALS!, '{invalid json') + process.env.GEMINI_AUTH_MODE = 'adc' + await expect(checkGcpCredentialsValid()).resolves.toBe(false) + await expect(resolveGeminiCredential()).resolves.toEqual({ kind: 'none' }) + expect(tokenRequests).toHaveLength(0) +}) From d97567b44a37128ed6aef1bc61553812f10c50f9 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 07:23:47 -0700 Subject: [PATCH 078/121] test(proxy): generate TLS test fixtures at runtime (#2389) Generate ephemeral TLS fixtures once per test file. Preserve transport assertions and add certificate validation coverage. --- src/test/tlsFixture.ts | 77 +++++++++++++++++++++++++++++++++ src/utils/proxy.connect.test.ts | 32 +++++++------- 2 files changed, 92 insertions(+), 17 deletions(-) create mode 100644 src/test/tlsFixture.ts diff --git a/src/test/tlsFixture.ts b/src/test/tlsFixture.ts new file mode 100644 index 0000000000..e7dc9da4a1 --- /dev/null +++ b/src/test/tlsFixture.ts @@ -0,0 +1,77 @@ +import { generateKeyPairSync, sign, X509Certificate } from 'node:crypto' + +// Minimal DER encoding for this test certificate, including long-form lengths. +function der(tag: number, ...parts: Buffer[]): Buffer { + const content = Buffer.concat(parts) + const length: number[] = [] + if (content.length < 128) { + length.push(content.length) + } else { + for (let remaining = content.length; remaining > 0; remaining >>>= 8) { + length.unshift(remaining & 0xff) + } + length.unshift(0x80 | length.length) + } + return Buffer.concat([Buffer.from([tag, ...length]), content]) +} + +const sequence = (...parts: Buffer[]) => der(0x30, ...parts) +const oid = (hex: string) => der(0x06, Buffer.from(hex, 'hex')) +const booleanTrue = der(0x01, Buffer.from([0xff])) + +function certificateTime(date: Date): Buffer { + const digits = date.toISOString().replace(/[-:T]/g, '').replace(/\.\d{3}Z$/, 'Z') + // X.509 uses UTCTime for 1950–2049 and GeneralizedTime otherwise. + const utc = date.getUTCFullYear() >= 1950 && date.getUTCFullYear() < 2050 + return der(utc ? 0x17 : 0x18, Buffer.from(utc ? digits.slice(2) : digits)) +} + +function extension(oidHex: string, value: Buffer, critical = false): Buffer { + return sequence( + oid(oidHex), + ...(critical ? [booleanTrue] : []), + der(0x04, value), + ) +} + +export function createTestTlsFixture() { + const { privateKey, publicKey } = generateKeyPairSync('ec', { + namedCurve: 'prime256v1', + }) + // AlgorithmIdentifier for ecdsa-with-SHA256 (1.2.840.10045.4.3.2). + const algorithm = sequence(oid('2a8648ce3d040302')) + const name = sequence(der(0x31, sequence( + oid('550403'), // commonName (2.5.4.3) + der(0x0c, Buffer.from('localhost')), + ))) + const now = Date.now() + const day = 24 * 60 * 60 * 1000 + const tbsCertificate = sequence( + der(0xa0, der(0x02, Buffer.from([2]))), // explicit v3 version + der(0x02, Buffer.from([1])), // positive serial, unique for this fresh issuer + algorithm, + name, // issuer and subject are identical for a self-signed certificate + sequence(certificateTime(new Date(now - day)), certificateTime(new Date(now + day))), + name, + publicKey.export({ type: 'spki', format: 'der' }), + der(0xa3, sequence( + extension('551d13', sequence(booleanTrue), true), // basicConstraints: CA + // keyUsage: digitalSignature + keyCertSign; two unused bits. + extension('551d0f', der(0x03, Buffer.from([2, 0x84])), true), + extension('551d11', sequence( // subjectAltName + der(0x82, Buffer.from('localhost')), + der(0x87, Buffer.from([127, 0, 0, 1])), + )), + )), + ) + const certificate = sequence( + tbsCertificate, + algorithm, + der(0x03, Buffer.from([0]), sign('sha256', tbsCertificate, privateKey)), + ) + return { + key: privateKey.export({ type: 'pkcs8', format: 'pem' }), + cert: new X509Certificate(certificate).toString(), + publicKey, + } +} diff --git a/src/utils/proxy.connect.test.ts b/src/utils/proxy.connect.test.ts index 596acd37b4..f45adc93ef 100644 --- a/src/utils/proxy.connect.test.ts +++ b/src/utils/proxy.connect.test.ts @@ -1,4 +1,5 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import { X509Certificate } from 'node:crypto' import { once } from 'node:events' import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' import { createServer as createHttpServer, type IncomingMessage } from 'node:http' @@ -13,6 +14,7 @@ import { tmpdir } from 'node:os' import { join } from 'node:path' import type { Duplex } from 'node:stream' import { snapshotEnv } from '../test/envSnapshot.js' +import { createTestTlsFixture } from '../test/tlsFixture.js' import { clearCACertsCache } from './caCerts.js' import { clearMTLSCache, getMTLSAgent, getWebSocketTLSOptions } from './mtls.js' @@ -46,23 +48,19 @@ const { createAxiosInstance, getWebSocketProxyAgent } = await import( `data:text/javascript;base64,${Buffer.from(await proxyBuild.outputs[0].text()).toString('base64')}` ) as typeof import('./proxy.js') -// Self-signed test certificate and key, valid only for the loopback target. -const cert = `-----BEGIN CERTIFICATE----- -MIIBmzCCAUGgAwIBAgIUAtOYg4xV7C9catkY/bwZc7XywMgwCgYIKoZIzj0EAwIw -FDESMBAGA1UEAwwJbG9jYWxob3N0MCAXDTI2MTAyNDA4NTUxNloYDzIxMjYwOTMw -MDg1NTE2WjAUMRIwEAYDVQQDDAlsb2NhbGhvc3QwWTATBgcqhkjOPQIBBggqhkjO -PQMBBwNCAAS5HiqEES/b1Oe+NOKT+Cg3Gh89Rk0O7bSu0aDJpwdMzOxgOT2jTBmK -2H5TUBnDWHjLktPtbmQQ4nMAb1kD4oKWo28wbTAdBgNVHQ4EFgQUu+xqnGfB/gu8 -DbNNHlVonV69Y64wHwYDVR0jBBgwFoAUu+xqnGfB/gu8DbNNHlVonV69Y64wDwYD -VR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2NhbGhvc3SHBH8AAAEwCgYIKoZI -zj0EAwIDSAAwRQIgPy5jbT1ECH7LFy5r5JrV7xfsOkhwtbuwqVeidDBynFACIQCp -sTyTz0llkGsTStOnpDBTzTUQzcDT8z6ZgqqxbY29nw== ------END CERTIFICATE-----` -const key = `-----BEGIN PRIVATE KEY----- -MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgXc/iw7bBMrZkWHsr -tDvMDJ6qPLsspu6IH8lHZ3ccGvihRANCAAS5HiqEES/b1Oe+NOKT+Cg3Gh89Rk0O -7bSu0aDJpwdMzOxgOT2jTBmK2H5TUBnDWHjLktPtbmQQ4nMAb1kD4oKW ------END PRIVATE KEY-----` +const { key, cert, publicKey } = createTestTlsFixture() + +test('runtime TLS fixture parses, verifies and covers the loopback target', () => { + const certificate = new X509Certificate(cert) + expect(certificate.verify(publicKey)).toBe(true) + expect(certificate.ca).toBe(true) + expect(certificate.subject).toBe('CN=localhost') + expect(certificate.subjectAltName).toBe('DNS:localhost, IP Address:127.0.0.1') + expect(certificate.checkHost('localhost')).toBe('localhost') + expect(certificate.checkIP('127.0.0.1')).toBe('127.0.0.1') + expect(Date.parse(certificate.validFrom)).toBeLessThan(Date.now()) + expect(Date.parse(certificate.validTo)).toBeGreaterThan(Date.now()) +}) describe('HTTPS_PROXY CONNECT transport', () => { let restoreEnv: () => void From 0dbfe067def08875b18d8a3b1279d75ba6036dea Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 08:35:57 -0700 Subject: [PATCH 079/121] chore(deps): finish D-3/D-4 exact pins and raise ip-address override to 10.7.4 (#2392) --- CONTRIBUTING.md | 19 +++++++++++++++++++ bun.lock | 8 ++++---- package.json | 6 +++--- web/bun.lock | 2 +- web/package.json | 2 +- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 93b736491e..e79c4effbc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -228,6 +228,25 @@ only when a separate capability level is intentional, as for Apple Terminal charts. Tests that change `chalk.level` must restore it, and environment-driven color detection tests must import in a fresh process. +### Dependency pins and overrides + +Keep all direct dependencies and devDependencies pinned to exact versions in the +root and web manifests. The existing `sharp` range is the temporary exception +while its separate upgrade is in progress. Regenerate the relevant Bun lockfile +when changing a pin. + +The root `overrides` block also controls transitive resolutions: + +- `ip-address`: `10.7.4` sets the security floor for the MCP SDK's + `express-rate-limit` dependency path. +- `google-auth-library`: `11.3.1` keeps Google authentication on the same version + as the direct dependency. The override accompanies the replacement of the + legacy Vertex SDK to remove its deprecated UUID dependency path. +- `lodash-es`: `4.18.1` keeps transitive copies on the security fixes for template + code injection and prototype pollution, avoiding the deprecated `4.18.0` release. +- `node-domexception`: the existing shim re-exports the platform's native + `DOMException` for `fetch-blob`, replacing the deprecated polyfill. + ## AI Agent Guidelines If you are an AI agent (Copilot, Cursor, Claude, etc.) working on this codebase, refer to [AGENTS.md](AGENTS.md) for project-specific coding guidelines, conventions, and validation commands. Following these guidelines will help your contributions align with the project's patterns and reduce review friction. diff --git a/bun.lock b/bun.lock index 834ae44131..31acbae924 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ "@grpc/grpc-js": "1.14.6", "@grpc/proto-loader": "0.8.1", "@modelcontextprotocol/sdk": "1.32.1", - "@sentry/node": "^10.70.0", + "@sentry/node": "10.70.0", "@smithy/core": "3.36.2", "@smithy/node-http-handler": "4.12.1", "@types/bun": "1.4.4", @@ -42,7 +42,7 @@ "commander": "12.1.0", "cross-spawn": "7.0.6", "diff": "9.0.0", - "duck-duck-scrape": "^2.2.7", + "duck-duck-scrape": "2.2.7", "emoji-regex": "11.0.0", "env-paths": "4.0.0", "eslint": "9.39.5", @@ -106,7 +106,7 @@ }, "overrides": { "google-auth-library": "11.3.1", - "ip-address": "10.2.0", + "ip-address": "10.7.4", "lodash-es": "4.18.1", "node-domexception": "file:vendor/node-domexception-shim", }, @@ -895,7 +895,7 @@ "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - "ip-address": ["ip-address@10.2.0", "", {}, "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA=="], + "ip-address": ["ip-address@10.7.4", "", {}, "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg=="], "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], diff --git a/package.json b/package.json index fa8fb73a23..372adcf31e 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "@grpc/grpc-js": "1.14.6", "@grpc/proto-loader": "0.8.1", "@modelcontextprotocol/sdk": "1.32.1", - "@sentry/node": "^10.70.0", + "@sentry/node": "10.70.0", "@smithy/core": "3.36.2", "@smithy/node-http-handler": "4.12.1", "@types/bun": "1.4.4", @@ -122,7 +122,7 @@ "commander": "12.1.0", "cross-spawn": "7.0.6", "diff": "9.0.0", - "duck-duck-scrape": "^2.2.7", + "duck-duck-scrape": "2.2.7", "emoji-regex": "11.0.0", "env-paths": "4.0.0", "eslint": "9.39.5", @@ -212,7 +212,7 @@ "access": "public" }, "overrides": { - "ip-address": "10.2.0", + "ip-address": "10.7.4", "google-auth-library": "11.3.1", "lodash-es": "4.18.1", "node-domexception": "file:vendor/node-domexception-shim" diff --git a/web/bun.lock b/web/bun.lock index 817f24d3d9..a93cf898eb 100644 --- a/web/bun.lock +++ b/web/bun.lock @@ -9,7 +9,7 @@ "@astrojs/sitemap": "3.7.4", "@fontsource-variable/geist-mono": "5.3.0", "astro": "6.4.8", - "typescript": "^6.0.3", + "typescript": "6.0.3", }, }, }, diff --git a/web/package.json b/web/package.json index 87508b8f0f..84d7630f2a 100644 --- a/web/package.json +++ b/web/package.json @@ -14,6 +14,6 @@ "@astrojs/sitemap": "3.7.4", "@fontsource-variable/geist-mono": "5.3.0", "astro": "6.4.8", - "typescript": "^6.0.3" + "typescript": "6.0.3" } } From 9ccd6aeaac255a39532f8a6ca4619c355fc675c3 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 08:47:37 -0700 Subject: [PATCH 080/121] build(deps): bump @anthropic-ai/sdk to 0.110.0 and bedrock-sdk to 0.30.2 (#2391) --- bun.lock | 16 ++-- package.json | 6 +- src/services/api/claude.lifecycle.test.ts | 1 + .../api/claude.streamWatchdog.test.ts | 31 ++++++- src/services/api/claude.ts | 7 ++ src/services/api/claude.usage.test.ts | 47 ++++++++++ .../api/client.bedrockStreaming.test.ts | 90 +++++++++++++++++++ src/services/api/emptyUsage.ts | 1 + src/tools/AgentTool/UI.tsx | 1 + src/tools/AgentTool/UI.usage.test.ts | 33 +++++++ src/utils/messages/factories.test.ts | 12 +++ src/utils/messages/factories.ts | 1 + 12 files changed, 236 insertions(+), 10 deletions(-) create mode 100644 src/services/api/claude.usage.test.ts create mode 100644 src/services/api/client.bedrockStreaming.test.ts create mode 100644 src/tools/AgentTool/UI.usage.test.ts diff --git a/bun.lock b/bun.lock index 31acbae924..7e8b7ea2e9 100644 --- a/bun.lock +++ b/bun.lock @@ -11,10 +11,10 @@ }, "devDependencies": { "@alcalzone/ansi-tokenize": "0.3.1", - "@anthropic-ai/bedrock-sdk": "0.29.2", + "@anthropic-ai/bedrock-sdk": "0.30.2", "@anthropic-ai/foundry-sdk": "0.2.4", "@anthropic-ai/sandbox-runtime": "0.0.55", - "@anthropic-ai/sdk": "0.94.0", + "@anthropic-ai/sdk": "0.110.0", "@aws-sdk/client-bedrock": "3.1156.0", "@aws-sdk/client-sts": "3.1156.0", "@aws-sdk/credential-provider-node": "3.972.88", @@ -91,7 +91,7 @@ "zod": "3.25.76", }, "peerDependencies": { - "@anthropic-ai/sdk": "^0.94.0", + "@anthropic-ai/sdk": "^0.110.0", "@modelcontextprotocol/sdk": "^1.32.1", "react": "^19.0.0", "react-reconciler": "^0.33.0", @@ -113,13 +113,13 @@ "packages": { "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.1", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-cUD0PT0YX+rImRhfn7BfMRduP/tX/Zqt/N0dHlAyn2RlcJssCb5KsCJkhln61eh9vuJDkH8wKn3L8zJzzWXPFg=="], - "@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.29.2", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-QqaECBd/kRMIqIVHaRT/7y7uvaAPb2ZY4geI8igIcE8JTov38qXzfv55jnG+5Lu5wEbyg1FB0n9KVSIz4HPb4Q=="], + "@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.30.2", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-4CSqbJN9UfKxPCut2y8PyNP32Pj2omUE6//lVVpiVRgYgml8QRm/Q+HknOSJgcr7b+YKXW8/+oVuKA93f08ShQ=="], "@anthropic-ai/foundry-sdk": ["@anthropic-ai/foundry-sdk@0.2.4", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1" } }, "sha512-RlM2UnOU31ahEUFSXN4v5TzLoF+cR66JU22wZhbLEpY//LzNJeepZii2wXsYkdnoUI8EXUrVFySEoOcjfOkX+Q=="], "@anthropic-ai/sandbox-runtime": ["@anthropic-ai/sandbox-runtime@0.0.55", "", { "dependencies": { "@pondwader/socks5-server": "^1.0.10", "commander": "^12.1.0", "node-forge": "^1.4.0", "shell-quote": "^1.8.4", "zod": "^3.24.1" }, "bin": { "srt": "dist/cli.js" } }, "sha512-XGTLbzitn0y6OCdmqP5aYrBR0HlNi87ue++m9vMPxcNwaQeOj9i3f9CSWmXua8mv+Q8E55k4ZGlzJmpDHzQFBw=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.94.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-OVlCttk5MyeTGtrWX5+F3MJOfEMDuEjK8+rm9aQMDfRPWndVMbhk37QG8WLnVbcc7huyUGngVMjT7iMN2llySA=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.110.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-hOP4bNYXDFHDxxiEgzlILXrxZIYCDnhe8sry0RDRKD/QnsEpvZcQpablCdm9X/WuD/YgOiSIkkqsL1mLLlTqJw=="], "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.18.1", "", { "dependencies": { "@types/estree": "^1.0.8", "astring": "^1.9.0", "esquery": "^1.7.0", "meriyah": "^6.1.4", "semifies": "^1.0.0", "source-map": "^0.6.0" }, "bin": { "code-transformer": "cli.js" } }, "sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ=="], @@ -529,6 +529,8 @@ "@smithy/util-utf8": ["@smithy/util-utf8@3.0.0", "", { "dependencies": { "@smithy/util-buffer-from": "^3.0.0", "tslib": "^2.6.2" } }, "sha512-rUeT12bxFnplYDe815GXbq/oixEGHfRFFtcTF3YdDi/JaENIM6aSYYLJydG83UNzLXeRI5K8abYd/8Sp/QM0kA=="], + "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.4", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A=="], "@types/bun": ["@types/bun@1.4.4", "", { "dependencies": { "bun-types": "1.4.4" } }, "sha512-9wkRDXUXK124pEnAutE6n/l18j+i9GC5IAHK8TmX/2nlySETmky0OU6yL5U8pW1AoKh++AN9auDCr647BfurjQ=="], @@ -781,6 +783,8 @@ "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="], + "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], "fd-package-json": ["fd-package-json@2.0.0", "", { "dependencies": { "walk-up-path": "^4.0.0" } }, "sha512-jKmm9YtsNXN789RS/0mSzOC1NUq9mkVd65vbSSVsKdjGvYXBuE4oWe2QOEoFeRmJg+lPuZxpmrfFclNhoRMneQ=="], @@ -1173,6 +1177,8 @@ "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + "standardwebhooks": ["standardwebhooks@1.1.1", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], "string-width": ["string-width@8.3.0", "", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-ZbmZM0JCihQN91dWnxoipT2KOEyHqEyfRXUyjuRhW8b/xnqPDoq4gWEVApTVa9db2wN8mmoikgFBbjh71+cGeQ=="], diff --git a/package.json b/package.json index 372adcf31e..9e1097b611 100644 --- a/package.json +++ b/package.json @@ -91,10 +91,10 @@ }, "devDependencies": { "@alcalzone/ansi-tokenize": "0.3.1", - "@anthropic-ai/bedrock-sdk": "0.29.2", + "@anthropic-ai/bedrock-sdk": "0.30.2", "@anthropic-ai/foundry-sdk": "0.2.4", "@anthropic-ai/sandbox-runtime": "0.0.55", - "@anthropic-ai/sdk": "0.94.0", + "@anthropic-ai/sdk": "0.110.0", "@aws-sdk/client-bedrock": "3.1156.0", "@aws-sdk/client-sts": "3.1156.0", "@aws-sdk/credential-provider-node": "3.972.88", @@ -171,7 +171,7 @@ "zod": "3.25.76" }, "peerDependencies": { - "@anthropic-ai/sdk": "^0.94.0", + "@anthropic-ai/sdk": "^0.110.0", "@modelcontextprotocol/sdk": "^1.32.1", "react": "^19.0.0", "react-reconciler": "^0.33.0" diff --git a/src/services/api/claude.lifecycle.test.ts b/src/services/api/claude.lifecycle.test.ts index 9a014282a7..afa9fab3f5 100644 --- a/src/services/api/claude.lifecycle.test.ts +++ b/src/services/api/claude.lifecycle.test.ts @@ -145,6 +145,7 @@ function makeBetaMessage(): BetaMessage { content: [], container: null, context_management: null, + diagnostics: null, stop_details: null, stop_reason: 'end_turn', stop_sequence: null, diff --git a/src/services/api/claude.streamWatchdog.test.ts b/src/services/api/claude.streamWatchdog.test.ts index 5cc9a74c56..ed43e63fed 100644 --- a/src/services/api/claude.streamWatchdog.test.ts +++ b/src/services/api/claude.streamWatchdog.test.ts @@ -92,6 +92,7 @@ function makeBetaMessage( content, container: null, context_management: null, + diagnostics: null, stop_details: null, stop_reason: 'end_turn', stop_sequence: null, @@ -139,6 +140,7 @@ function makeCompleteStreamEvents(): BetaRawMessageStreamEvent[] { input_tokens: null, iterations: null, output_tokens: 1, + output_tokens_details: null, server_tool_use: null, }, }, @@ -202,9 +204,8 @@ function makeWedgedStream(): { } } -function makeCompleteStream(): Stream { +function makeCompleteStream(events = makeCompleteStreamEvents()): Stream { const controller = new AbortController() - const events = makeCompleteStreamEvents() const iterator: AsyncIterator = { next() { const value = events.shift() @@ -360,6 +361,32 @@ afterEach(async () => { }) describe('Claude stream watchdog', () => { + test('streamed assistant usage retains cumulative thinking token details', async () => { + const events = makeCompleteStreamEvents() + for (const event of events) { + if (event.type === 'message_start') { + event.message.usage.output_tokens_details = { thinking_tokens: 2 } + } else if (event.type === 'message_delta') { + event.usage.output_tokens = 7 + event.usage.output_tokens_details = { thinking_tokens: 5 } + } + } + createHandler = () => makeWithResponse(makeCompleteStream(events)) + const messages = await collectStreamingMessages( + new AbortController().signal, + makeOptions(), + ) + expect(messages).toContainEqual(expect.objectContaining({ + type: 'assistant', + message: expect.objectContaining({ + usage: expect.objectContaining({ + output_tokens: 7, + output_tokens_details: { thinking_tokens: 5 }, + }), + }), + })) + }) + test('falls back when the top-level stream iterator never settles', async () => { const traceFile = join(fixturesRoot!, 'interruption-trace.jsonl') process.env.OPENCLAUDE_INTERRUPT_TRACE = '1' diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index ed12d01cab..0134da54c7 100644 --- a/src/services/api/claude.ts +++ b/src/services/api/claude.ts @@ -3508,6 +3508,8 @@ export function updateUsage( ? partUsage.cache_read_input_tokens : usage.cache_read_input_tokens, output_tokens: partUsage.output_tokens ?? usage.output_tokens, + output_tokens_details: + partUsage.output_tokens_details ?? usage.output_tokens_details, server_tool_use: { web_search_requests: partUsage.server_tool_use?.web_search_requests ?? @@ -3566,6 +3568,11 @@ export function accumulateUsage( cache_read_input_tokens: totalUsage.cache_read_input_tokens + messageUsage.cache_read_input_tokens, output_tokens: totalUsage.output_tokens + messageUsage.output_tokens, + output_tokens_details: { + thinking_tokens: + totalUsage.output_tokens_details.thinking_tokens + + messageUsage.output_tokens_details.thinking_tokens, + }, server_tool_use: { web_search_requests: totalUsage.server_tool_use.web_search_requests + diff --git a/src/services/api/claude.usage.test.ts b/src/services/api/claude.usage.test.ts new file mode 100644 index 0000000000..85a5d95b1d --- /dev/null +++ b/src/services/api/claude.usage.test.ts @@ -0,0 +1,47 @@ +import { expect, test } from 'bun:test' +import { accumulateUsage, updateUsage } from './claude.js' +import { EMPTY_USAGE } from './emptyUsage.js' + +test('stream usage preserves cumulative thinking details through null and legacy updates', () => { + const start = updateUsage(EMPTY_USAGE, { + ...EMPTY_USAGE, + input_tokens: 10, + output_tokens: 4, + output_tokens_details: { thinking_tokens: 3 }, + }) + const delta = updateUsage(start, { + ...EMPTY_USAGE, + output_tokens: 9, + output_tokens_details: { thinking_tokens: 6 }, + }) + expect(delta.output_tokens_details).toEqual({ thinking_tokens: 6 }) + expect(delta.output_tokens).toBe(9) + expect(delta.input_tokens).toBe(10) + + const nullable = { ...EMPTY_USAGE, output_tokens: 10, output_tokens_details: null } + expect(updateUsage(delta, nullable).output_tokens_details).toEqual({ thinking_tokens: 6 }) + const { output_tokens_details: _details, ...legacy } = nullable + expect(updateUsage(delta, legacy as typeof nullable).output_tokens_details).toEqual({ thinking_tokens: 6 }) + expect(updateUsage(delta, undefined)).toEqual(delta) + expect(updateUsage(delta, { + ...nullable, + output_tokens_details: { thinking_tokens: 0 }, + }).output_tokens_details).toEqual({ thinking_tokens: 0 }) +}) + +test('turn accumulation sums thinking details without adding them to billed output', () => { + const first = updateUsage(EMPTY_USAGE, { + ...EMPTY_USAGE, + output_tokens: 9, + output_tokens_details: { thinking_tokens: 6 }, + }) + const second = updateUsage(EMPTY_USAGE, { + ...EMPTY_USAGE, + output_tokens: 5, + output_tokens_details: { thinking_tokens: 2 }, + }) + const total = accumulateUsage(accumulateUsage(EMPTY_USAGE, first), second) + expect(total.output_tokens_details).toEqual({ thinking_tokens: 8 }) + expect(total.output_tokens).toBe(14) + expect(EMPTY_USAGE.output_tokens_details).toEqual({ thinking_tokens: 0 }) +}) diff --git a/src/services/api/client.bedrockStreaming.test.ts b/src/services/api/client.bedrockStreaming.test.ts new file mode 100644 index 0000000000..ba24930f44 --- /dev/null +++ b/src/services/api/client.bedrockStreaming.test.ts @@ -0,0 +1,90 @@ +import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { crc32 } from 'node:zlib' +import { snapshotEnv } from '../../test/envSnapshot.js' +import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js' +import { asMockFetch } from '../../test/typedMocks.js' + +const envKeys = [ + 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_SKIP_BEDROCK_AUTH', + 'CLAUDE_CODE_USE_FOUNDRY', 'CLAUDE_CODE_USE_VERTEX', + 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_GEMINI', + 'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_MISTRAL', + 'AWS_BEARER_TOKEN_BEDROCK', 'ANTHROPIC_BASE_URL', + 'ANTHROPIC_BEDROCK_BASE_URL', 'AWS_REGION', +] as const +let restoreEnv: () => void +let originalMacro: PropertyDescriptor | undefined + +beforeEach(async () => { + await acquireSharedMutationLock('client.bedrockStreaming.test.ts') + restoreEnv = snapshotEnv(envKeys, { clear: true }) + originalMacro = Object.getOwnPropertyDescriptor(globalThis, 'MACRO') + ;(globalThis as Record).MACRO = { VERSION: 'test-version' } + process.env.CLAUDE_CODE_USE_BEDROCK = '1' + process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH = '1' + process.env.AWS_REGION = 'us-east-1' + process.env.ANTHROPIC_BEDROCK_BASE_URL = 'https://bedrock.example.test' +}) + +afterEach(() => { + try { + restoreEnv() + if (originalMacro) Object.defineProperty(globalThis, 'MACRO', originalMacro) + else delete (globalThis as Record).MACRO + } finally { + releaseSharedMutationLock() + } +}) + +// Encode the public AWS event-stream wire format, including both CRC checks. +// The real Bedrock SDK must decode these frames before the Anthropic SSE parser. +function bedrockFrame(event: object): Buffer { + const headers = Buffer.concat(Object.entries({ + ':message-type': 'event', ':event-type': 'chunk', + ':content-type': 'application/json', + }).map(([name, value]) => { + const key = Buffer.from(name) + const text = Buffer.from(value) + const prefix = Buffer.from([key.length, ...key, 7, 0, 0]) + prefix.writeUInt16BE(text.length, prefix.length - 2) + return Buffer.concat([prefix, text]) + })) + const body = Buffer.from(JSON.stringify({ + bytes: Buffer.from(JSON.stringify(event)).toString('base64'), + })) + const frame = Buffer.alloc(16 + headers.length + body.length) + frame.writeUInt32BE(frame.length, 0) + frame.writeUInt32BE(headers.length, 4) + frame.writeUInt32BE(crc32(frame.subarray(0, 8)), 8) + headers.copy(frame, 12) + body.copy(frame, 12 + headers.length) + frame.writeUInt32BE(crc32(frame.subarray(0, -4)), frame.length - 4) + return frame +} + +test('the real Bedrock client decodes AWS streaming frames with the installed Anthropic SDK', async () => { + const events = [ + { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'hello' } }, + { type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 7, output_tokens_details: { thinking_tokens: 5 } } }, + { type: 'message_stop' }, + ] + const fetch = mock(async () => new Response(Buffer.concat(events.map(bedrockFrame)), { + headers: { 'content-type': 'application/vnd.amazon.eventstream' }, + })) + const { getAnthropicClient } = await import( + `./client.js?bedrock-stream=${Date.now()}-${Math.random()}` + ) as typeof import('./client.js') + const client = await getAnthropicClient({ + maxRetries: 0, + model: 'anthropic.claude-sonnet-4-6', + fetchOverride: asMockFetch(fetch), + }) + expect(client.constructor.name).toBe('AnthropicBedrock') + const received: unknown[] = [] + for await (const event of await client.beta.messages.create({ + model: 'anthropic.claude-sonnet-4-6', max_tokens: 16, + messages: [{ role: 'user', content: 'hello' }], stream: true, + })) received.push(event) + expect(fetch).toHaveBeenCalledTimes(1) + expect(received).toEqual(events) +}) diff --git a/src/services/api/emptyUsage.ts b/src/services/api/emptyUsage.ts index ad8c25ffd5..abceeaee1a 100644 --- a/src/services/api/emptyUsage.ts +++ b/src/services/api/emptyUsage.ts @@ -10,6 +10,7 @@ export const EMPTY_USAGE: Readonly = { cache_creation_input_tokens: 0, cache_read_input_tokens: 0, output_tokens: 0, + output_tokens_details: { thinking_tokens: 0 }, server_tool_use: { web_search_requests: 0, web_fetch_requests: 0 }, service_tier: 'standard', cache_creation: { diff --git a/src/tools/AgentTool/UI.tsx b/src/tools/AgentTool/UI.tsx index bedcbd5ef4..e8bae2b1d6 100644 --- a/src/tools/AgentTool/UI.tsx +++ b/src/tools/AgentTool/UI.tsx @@ -280,6 +280,7 @@ export function renderToolResultMessage(data: Output, progressMessagesForMessage content: completionMessage, usage: { ...usage, + output_tokens_details: null, inference_geo: null, iterations: null, speed: null diff --git a/src/tools/AgentTool/UI.usage.test.ts b/src/tools/AgentTool/UI.usage.test.ts new file mode 100644 index 0000000000..5041558f98 --- /dev/null +++ b/src/tools/AgentTool/UI.usage.test.ts @@ -0,0 +1,33 @@ +import { expect, test } from 'bun:test' +import { Children, isValidElement, type ReactNode } from 'react' +import { Message } from '../../components/Message.js' +import { EMPTY_USAGE } from '../../services/api/emptyUsage.js' +import type { AssistantMessage } from '../../types/message.js' +import { renderToolResultMessage } from './UI.js' + +function findAssistantMessage(node: ReactNode): AssistantMessage | undefined { + for (const child of Children.toArray(node)) { + if (!isValidElement<{ children?: ReactNode; message?: AssistantMessage }>(child)) continue + if (child.type === Message) return child.props.message + const message = findAssistantMessage(child.props.children) + if (message) return message + } +} + +test('completed agent summaries supply null details when tool-result usage omits them', () => { + const { output_tokens_details: _details, ...usage } = EMPTY_USAGE + const tree = renderToolResultMessage({ + status: 'completed', + agentId: 'test-agent', + content: [], + prompt: '', + totalToolUseCount: 1, + totalDurationMs: 100, + totalTokens: 7, + usage: { ...usage, input_tokens: 3, output_tokens: 4 }, + }, [], { tools: [], verbose: false, theme: 'dark' }) + const message = findAssistantMessage(tree) + expect(message).toBeDefined() + expect(message?.message.usage.output_tokens_details).toBeNull() + expect(message?.message.usage.output_tokens).toBe(4) +}) diff --git a/src/utils/messages/factories.test.ts b/src/utils/messages/factories.test.ts index 1898629c1d..f93aa8046f 100644 --- a/src/utils/messages/factories.test.ts +++ b/src/utils/messages/factories.test.ts @@ -26,12 +26,24 @@ test('createAssistantMessage builds a synthetic assistant message', () => { const message = createAssistantMessage({ content: 'hello' }) expect(message.type).toBe('assistant') + expect(message.message.usage.output_tokens_details).toBeNull() expect(message.message.content[0]).toMatchObject({ type: 'text', text: 'hello', }) }) +test('createAssistantMessage preserves supplied output token details', () => { + const usage = { + ...createAssistantMessage({ content: 'default' }).message.usage, + output_tokens: 7, + output_tokens_details: { thinking_tokens: 4 }, + } + const message = createAssistantMessage({ content: 'hello', usage }) + expect(message.message.usage.output_tokens_details).toEqual({ thinking_tokens: 4 }) + expect(message.message.usage.output_tokens).toBe(7) +}) + test('createAssistantAPIErrorMessage builds a synthetic API error message', () => { const message = createAssistantAPIErrorMessage({ content: '', diff --git a/src/utils/messages/factories.ts b/src/utils/messages/factories.ts index dc145ad6d0..b6e000356c 100644 --- a/src/utils/messages/factories.ts +++ b/src/utils/messages/factories.ts @@ -98,6 +98,7 @@ function baseCreateAssistantMessage({ usage = { input_tokens: 0, output_tokens: 0, + output_tokens_details: null, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, server_tool_use: { web_search_requests: 0, web_fetch_requests: 0 }, From 8fd997c45ae6138cf85749567dc5a700d5cb57dc Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 09:04:58 -0700 Subject: [PATCH 081/121] chore(deps): update sharp to 0.35.5 (#2390) * chore(deps): update sharp to 0.35.5 * ci(install-hygiene): cancel superseded pull request runs --- .github/workflows/install-hygiene.yml | 15 +++++++ bun.lock | 60 ++++++++++++++------------ package.json | 5 +-- src/utils/imageResizer.native.test.ts | 62 +++++++++++++++++++++++++++ 4 files changed, 111 insertions(+), 31 deletions(-) create mode 100644 src/utils/imageResizer.native.test.ts diff --git a/.github/workflows/install-hygiene.yml b/.github/workflows/install-hygiene.yml index f5b4e2d0eb..451262e674 100644 --- a/.github/workflows/install-hygiene.yml +++ b/.github/workflows/install-hygiene.yml @@ -13,6 +13,12 @@ name: Install hygiene on: + pull_request: + paths: + - 'package.json' + - 'bun.lock' + - 'src/utils/imageResizer.native.test.ts' + - '.github/workflows/install-hygiene.yml' schedule: - cron: '17 6 * * *' # daily, off the top-of-hour rush workflow_dispatch: @@ -20,6 +26,12 @@ on: permissions: contents: read +# Supersede stale runs for the same pull request. Scheduled and manual runs +# get a per-run group and are never cancelled. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: verify-published-install: name: ${{ matrix.os }} / Node ${{ matrix.node-version }} @@ -48,5 +60,8 @@ jobs: - name: Install dependencies run: bun install --frozen-lockfile + - name: Verify native image processing + run: bun test src/utils/imageResizer.native.test.ts + - name: Verify published package installs clean run: bun run scripts/verify-clean-install.ts --published diff --git a/bun.lock b/bun.lock index 7e8b7ea2e9..975c389548 100644 --- a/bun.lock +++ b/bun.lock @@ -71,7 +71,7 @@ "react-compiler-runtime": "1.0.0", "react-reconciler": "0.33.0", "semver": "7.8.5", - "sharp": "^0.34.5", + "sharp": "0.35.5", "shell-quote": "1.12.0", "signal-exit": "4.1.0", "supports-hyperlinks": "4.6.0", @@ -235,7 +235,7 @@ "@emnapi/core": ["@emnapi/core@1.11.2", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA=="], - "@emnapi/runtime": ["@emnapi/runtime@1.10.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="], + "@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA=="], @@ -275,53 +275,57 @@ "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], - "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg=="], - "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="], + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.4" }, "os": "darwin", "cpu": "x64" }, "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw=="], - "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="], + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "os": "freebsd" }, "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw=="], - "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="], + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw=="], - "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="], + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ=="], - "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="], + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.4", "", { "os": "linux", "cpu": "arm", "libc": "glibc" }, "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw=="], - "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="], + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A=="], - "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="], + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.4", "", { "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ=="], - "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="], + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.4", "", { "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q=="], - "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="], + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.4", "", { "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ=="], - "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="], + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.4", "", { "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw=="], - "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="], + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.4", "", { "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA=="], - "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="], + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.4", "", { "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg=="], - "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="], + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.4" }, "os": "linux", "cpu": "arm", "libc": "glibc" }, "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ=="], - "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="], + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64", "libc": "glibc" }, "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ=="], - "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="], + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.4" }, "os": "linux", "cpu": "ppc64", "libc": "glibc" }, "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA=="], - "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="], + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.4" }, "os": "linux", "cpu": "none", "libc": "glibc" }, "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ=="], - "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="], + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.4" }, "os": "linux", "cpu": "s390x", "libc": "glibc" }, "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA=="], - "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="], + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.4" }, "os": "linux", "cpu": "x64", "libc": "glibc" }, "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog=="], - "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="], + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" }, "os": "linux", "cpu": "arm64", "libc": "musl" }, "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg=="], - "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="], + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.4" }, "os": "linux", "cpu": "x64", "libc": "musl" }, "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A=="], - "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="], + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.5", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ=="], - "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="], + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.5", "", { "dependencies": { "@img/sharp-wasm32": "0.35.5" }, "cpu": "none" }, "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA=="], - "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="], + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.5", "", { "os": "win32", "cpu": "x64" }, "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg=="], "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], @@ -1155,7 +1159,7 @@ "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], - "sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="], + "sharp": ["sharp@0.35.5", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.5", "@img/sharp-darwin-x64": "0.35.5", "@img/sharp-freebsd-wasm32": "0.35.5", "@img/sharp-libvips-darwin-arm64": "1.3.4", "@img/sharp-libvips-darwin-x64": "1.3.4", "@img/sharp-libvips-linux-arm": "1.3.4", "@img/sharp-libvips-linux-arm64": "1.3.4", "@img/sharp-libvips-linux-ppc64": "1.3.4", "@img/sharp-libvips-linux-riscv64": "1.3.4", "@img/sharp-libvips-linux-s390x": "1.3.4", "@img/sharp-libvips-linux-x64": "1.3.4", "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", "@img/sharp-libvips-linuxmusl-x64": "1.3.4", "@img/sharp-linux-arm": "0.35.5", "@img/sharp-linux-arm64": "0.35.5", "@img/sharp-linux-ppc64": "0.35.5", "@img/sharp-linux-riscv64": "0.35.5", "@img/sharp-linux-s390x": "0.35.5", "@img/sharp-linux-x64": "0.35.5", "@img/sharp-linuxmusl-arm64": "0.35.5", "@img/sharp-linuxmusl-x64": "0.35.5", "@img/sharp-webcontainers-wasm32": "0.35.5", "@img/sharp-win32-arm64": "0.35.5", "@img/sharp-win32-ia32": "0.35.5", "@img/sharp-win32-x64": "0.35.5" }, "peerDependencies": { "@types/node": "*" }, "optionalPeers": ["@types/node"] }, "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1457,7 +1461,7 @@ "@eslint/eslintrc/strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], - "@oxc-resolver/binding-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.11.2", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA=="], + "@img/sharp-wasm32/@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], "@smithy/core/@smithy/types": ["@smithy/types@4.19.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-r7jh49VJxGerfAcTQA6gXcKc+98zOp/tqRwzYjgOE+iSQsP6cEU1hq2QzbuipmP68QtYdY9wKEhiCQZIzHgZ4Q=="], diff --git a/package.json b/package.json index 9e1097b611..6aa73231d0 100644 --- a/package.json +++ b/package.json @@ -151,7 +151,7 @@ "react-compiler-runtime": "1.0.0", "react-reconciler": "0.33.0", "semver": "7.8.5", - "sharp": "^0.34.5", + "sharp": "0.35.5", "shell-quote": "1.12.0", "signal-exit": "4.1.0", "supports-hyperlinks": "4.6.0", @@ -218,7 +218,6 @@ "node-domexception": "file:vendor/node-domexception-shim" }, "allowScripts": { - "protobufjs@7.6.4": true, - "sharp@0.34.5": true + "protobufjs@7.6.4": true } } diff --git a/src/utils/imageResizer.native.test.ts b/src/utils/imageResizer.native.test.ts new file mode 100644 index 0000000000..0ad6683f89 --- /dev/null +++ b/src/utils/imageResizer.native.test.ts @@ -0,0 +1,62 @@ +import { expect, test } from 'bun:test' + +// A fresh process keeps the real native addon independent of shared-process +// image-processor mocks, and keeps the missing-sharp simulation local. +function runImageCheck(source: string): void { + const result = Bun.spawnSync([process.execPath, '--eval', source], { + cwd: import.meta.dir, + stdout: 'pipe', + stderr: 'pipe', + timeout: 15_000, + }) + expect(result.stderr.toString()).not.toContain('AssertionError') + expect(result.exitCode).toBe(0) + expect(result.stdout.toString()).toContain('image check passed') +} + +// Leave time beyond the child deadline for failure reporting and cleanup. +test('native sharp creates, resizes and decodes images and rejects invalid input', () => { + runImageCheck(` + import assert from 'node:assert/strict' + import { getImageCreator, getImageProcessor } from '../tools/FileReadTool/imageProcessor.ts' + import { maybeResizeAndDownsampleImageBuffer } from './imageResizer.ts' + + const create = await getImageCreator() + const processImage = await getImageProcessor() + for (const format of ['png', 'jpeg', 'webp']) { + const input = await create({ create: { + width: 2400, height: 1200, channels: 3, + background: { r: 30, g: 100, b: 180 }, + } })[format]().toBuffer() + const result = await maybeResizeAndDownsampleImageBuffer(input, input.length, format) + const metadata = await processImage(result.buffer).metadata() + assert.equal(result.mediaType, format) + assert.equal(metadata.format, format) + assert.equal(metadata.width, 1568) + assert.equal(metadata.height, 784) + assert.equal(result.dimensions.originalWidth, 2400) + assert.equal(result.dimensions.originalHeight, 1200) + assert.equal(result.dimensions.displayWidth, metadata.width) + assert.equal(result.dimensions.displayHeight, metadata.height) + } + await assert.rejects(processImage(Buffer.from('invalid image')).metadata()) + await assert.rejects(maybeResizeAndDownsampleImageBuffer(Buffer.alloc(0), 0, 'png'), /empty/) + console.log('image check passed') + `) +}, 20_000) + +test('missing sharp preserves the in-budget image fallback', () => { + runImageCheck(` + import assert from 'node:assert/strict' + import { mock } from 'bun:test' + mock.module('sharp', () => ({ __stub: true })) + const { getImageProcessor, ImageProcessorUnavailableError } = await import('../tools/FileReadTool/imageProcessor.ts') + const { maybeResizeAndDownsampleImageBuffer } = await import('./imageResizer.ts') + await assert.rejects(getImageProcessor(), ImageProcessorUnavailableError) + const input = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+jRZkAAAAASUVORK5CYII=', 'base64') + const result = await maybeResizeAndDownsampleImageBuffer(input, input.length, 'png') + assert.deepEqual(result.buffer, input) + assert.equal(result.mediaType, 'png') + console.log('image check passed') + `) +}, 20_000) From cd536b25907e7e1ba42cc5a3684d11725fbda012 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 09:35:51 -0700 Subject: [PATCH 082/121] test(skills): isolate staged install temp directory assertions (#2394) --- src/cli/handlers/skills.test.ts | 116 ++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 6 deletions(-) diff --git a/src/cli/handlers/skills.test.ts b/src/cli/handlers/skills.test.ts index c7895633df..e417593198 100644 --- a/src/cli/handlers/skills.test.ts +++ b/src/cli/handlers/skills.test.ts @@ -13,6 +13,7 @@ import { import { tmpdir } from 'node:os' import { join } from 'node:path' import { test } from 'bun:test' +import { snapshotEnv } from '../../test/envSnapshot.js' import { isolateProcessExitCode } from '../../test/processExitCode.js' import { @@ -222,21 +223,33 @@ async function withMockedFetch( async function withTempDir(fn: (tempDir: string) => Promise): Promise { const tempDir = mkdtempSync(join(tmpdir(), 'openclaude-skill-install-test-')) + const tmpKeys = ['TMPDIR', 'TMP', 'TEMP'] as const + const restoreEnv = snapshotEnv([ + ...tmpKeys, 'OPENCLAUDE_SKILLS_REVOCATIONS_URL', + ]) + const nativeTmpEnv = tmpKeys.map(key => [key, Bun.env[key]] as const) + // Both the installer and the leak assertions must use this fixture's own + // directory, so other runs cannot change the staged-root snapshots. + // Bun's tmpdir reads its native environment even when another suite has + // replaced process.env, so update and restore both environment objects. + for (const key of tmpKeys) { + process.env[key] = tempDir + Bun.env[key] = tempDir + } // The revocation override is process-global input that the fixtures below // resolve against, and production gives it precedence over the sibling // revocations.json. Own it here: clear any value inherited from the host // process so each fixture reads its own list, and hand it back afterwards. - const inheritedRevocationsUrl = process.env.OPENCLAUDE_SKILLS_REVOCATIONS_URL delete process.env.OPENCLAUDE_SKILLS_REVOCATIONS_URL const restoreProcess = isolateProcessExitCode(0) try { return await fn(tempDir) } finally { try { - if (inheritedRevocationsUrl === undefined) { - delete process.env.OPENCLAUDE_SKILLS_REVOCATIONS_URL - } else { - process.env.OPENCLAUDE_SKILLS_REVOCATIONS_URL = inheritedRevocationsUrl + restoreEnv() + for (const [key, value] of nativeTmpEnv) { + if (value === undefined) delete Bun.env[key] + else Bun.env[key] = value } rmSync(tempDir, { recursive: true, force: true }) } finally { @@ -245,6 +258,61 @@ async function withTempDir(fn: (tempDir: string) => Promise): Promise { } } +test.serial('restores fixture environment and removes its temp root when a callback throws', async () => { + const keys = ['TMPDIR', 'TMP', 'TEMP', 'OPENCLAUDE_SKILLS_REVOCATIONS_URL'] + const inherited = keys.map(key => process.env[key]) + const inheritedNativeTmp = keys.slice(0, 3).map(key => Bun.env[key]) + const failure = new Error('fixture callback failed') + let fixtureRoot: string | undefined + await assert.rejects(withTempDir(async tempDir => { + fixtureRoot = tempDir + assert.equal(tmpdir(), tempDir) + throw failure + }), error => error === failure) + + assert.deepEqual(keys.map(key => process.env[key]), inherited) + assert.deepEqual(keys.slice(0, 3).map(key => Bun.env[key]), inheritedNativeTmp) + assert.ok(fixtureRoot) + assert.equal(existsSync(fixtureRoot), false) +}) + +for (const change of ['appears', 'disappears'] as const) { + test.serial(`ignores a foreign staged temp root that ${change} during install`, async () => { + const sharedTmpdir = tmpdir() + let foreignRoot: string | undefined + try { + if (change === 'disappears') { + foreignRoot = mkdtempSync(join(sharedTmpdir, 'openclaude-skill-install-test-')) + } + await withTempDir(async tempDir => { + const cwd = join(tempDir, 'project') + const source = writeSkillDir(join(tempDir, 'source')) + mkdirSync(cwd, { recursive: true }) + + const stagedBefore = stagedInstallTempDirs() + // Model another run creating a fixture, or finishing its cleanup, + // between this install's snapshots without relying on timing. + if (change === 'appears') { + foreignRoot = mkdtempSync(join(sharedTmpdir, 'openclaude-skill-install-test-')) + } else { + assert.ok(foreignRoot) + rmSync(foreignRoot, { recursive: true, force: true }) + } + await skillsInstallHandler(join(source, 'SKILL.md'), { projectDir: cwd }) + + assert.equal(process.exitCode, 0) + assert.equal( + readFileSync(join(cwd, '.openclaude', 'skills', 'sample-skill', 'SKILL.md'), 'utf8'), + VALID_SKILL, + ) + assertNoNewStagedInstallDirs(stagedBefore) + }) + } finally { + if (foreignRoot) rmSync(foreignRoot, { recursive: true, force: true }) + } + }) +} + test('formats skills list as an aligned human table', () => { const output = formatSkillsListForDisplay( [ @@ -1218,7 +1286,7 @@ test.serial('rejects direct HTTP URL installs without a sha256 pin', async () => }) }) -test.serial('cleans staged temp roots when markdown staging fails', async () => { +test.serial('rejects oversized skill names before staging', async () => { await withTempDir(async tempDir => { const cwd = join(tempDir, 'project') const source = join(tempDir, 'source', 'SKILL.md') @@ -1239,6 +1307,42 @@ test.serial('cleans staged temp roots when markdown staging fails', async () => }) }) +test.serial('cleans staged temp roots when markdown staging fails', async () => { + await acquireSharedMutationLock('skillsInstallHandler staging failure') + const originalFs = getFsImplementation() + try { + await withTempDir(async tempDir => { + const cwd = join(tempDir, 'project') + const source = writeSkillDir(join(tempDir, 'source')) + mkdirSync(cwd, { recursive: true }) + const stagedBefore = stagedInstallTempDirs() + let stagingWriteAttempted = false + setFsImplementation({ + ...originalFs, + writeFile: async () => { + // A valid skill must reach staging, then fail after mkdtemp. An + // invalid name is rejected before allocation and cannot test cleanup. + assert.equal(stagedInstallTempDirs().length, stagedBefore.length + 1) + stagingWriteAttempted = true + throw new Error('staging write failed') + }, + }) + try { + await skillsInstallHandler(join(source, 'SKILL.md'), { projectDir: cwd }) + + assert.ok(stagingWriteAttempted) + assert.equal(process.exitCode, 1) + assert.equal(existsSync(join(cwd, '.openclaude', 'skills')), false) + assertNoNewStagedInstallDirs(stagedBefore) + } finally { + setFsImplementation(originalFs) + } + }) + } finally { + releaseSharedMutationLock() + } +}) + test.serial('removes only the targeted project skill directory', async () => { await acquireSharedMutationLock('skillsRemoveHandler') const originalFs = getFsImplementation() From 1a79295216d07e7a4f43576c585626c076a1624f Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 09:51:53 -0700 Subject: [PATCH 083/121] build(deps): bump Anthropic SDK family to 0.137.0 (#2395) --- bun.lock | 14 +- package.json | 8 +- src/entrypoints/sdk/sdkUtilityTypes.ts | 9 +- .../api/claude.streamWatchdog.test.ts | 1 + src/services/api/claude.ts | 2 + src/services/api/claude.usage.test.ts | 32 +++++ .../api/client.bedrockStreaming.test.ts | 80 +++++++---- .../api/client.foundryStreaming.test.ts | 126 ++++++++++++++++++ src/services/api/emptyUsage.ts | 1 + src/services/tokenEstimation.test.ts | 21 ++- src/services/tokenEstimation.ts | 11 +- src/tools/AgentTool/UI.tsx | 1 + src/utils/messages/factories.test.ts | 10 ++ src/utils/messages/factories.ts | 1 + 14 files changed, 272 insertions(+), 45 deletions(-) create mode 100644 src/services/api/client.foundryStreaming.test.ts diff --git a/bun.lock b/bun.lock index 975c389548..8440806126 100644 --- a/bun.lock +++ b/bun.lock @@ -11,10 +11,10 @@ }, "devDependencies": { "@alcalzone/ansi-tokenize": "0.3.1", - "@anthropic-ai/bedrock-sdk": "0.30.2", - "@anthropic-ai/foundry-sdk": "0.2.4", + "@anthropic-ai/bedrock-sdk": "0.36.0", + "@anthropic-ai/foundry-sdk": "0.6.0", "@anthropic-ai/sandbox-runtime": "0.0.55", - "@anthropic-ai/sdk": "0.110.0", + "@anthropic-ai/sdk": "0.137.0", "@aws-sdk/client-bedrock": "3.1156.0", "@aws-sdk/client-sts": "3.1156.0", "@aws-sdk/credential-provider-node": "3.972.88", @@ -91,7 +91,7 @@ "zod": "3.25.76", }, "peerDependencies": { - "@anthropic-ai/sdk": "^0.110.0", + "@anthropic-ai/sdk": "^0.137.0", "@modelcontextprotocol/sdk": "^1.32.1", "react": "^19.0.0", "react-reconciler": "^0.33.0", @@ -113,13 +113,13 @@ "packages": { "@alcalzone/ansi-tokenize": ["@alcalzone/ansi-tokenize@0.3.1", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-cUD0PT0YX+rImRhfn7BfMRduP/tX/Zqt/N0dHlAyn2RlcJssCb5KsCJkhln61eh9vuJDkH8wKn3L8zJzzWXPFg=="], - "@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.30.2", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-4CSqbJN9UfKxPCut2y8PyNP32Pj2omUE6//lVVpiVRgYgml8QRm/Q+HknOSJgcr7b+YKXW8/+oVuKA93f08ShQ=="], + "@anthropic-ai/bedrock-sdk": ["@anthropic-ai/bedrock-sdk@0.36.0", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.115.1 <1", "@aws-crypto/sha256-js": "^4.0.0", "@aws-sdk/client-bedrock-runtime": "^3.797.0", "@aws-sdk/credential-providers": "^3.796.0", "@opentelemetry/api": "^1.4.1", "@smithy/eventstream-serde-node": "^2.0.10", "@smithy/fetch-http-handler": "^5.0.4", "@smithy/protocol-http": "^3.0.6", "@smithy/signature-v4": "^3.1.1", "@smithy/smithy-client": "^2.1.9", "@smithy/types": "^2.3.4", "@smithy/util-base64": "^2.0.0" } }, "sha512-NgodHkNxLiqmP5aDiHg7YrtXp0q0cLUTSD/GmdfBv7LH/y8wD0hdax+eyy67CBZ/inw9ak0r+7I9+lbMZVb1/g=="], - "@anthropic-ai/foundry-sdk": ["@anthropic-ai/foundry-sdk@0.2.4", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.50.3 <1" } }, "sha512-RlM2UnOU31ahEUFSXN4v5TzLoF+cR66JU22wZhbLEpY//LzNJeepZii2wXsYkdnoUI8EXUrVFySEoOcjfOkX+Q=="], + "@anthropic-ai/foundry-sdk": ["@anthropic-ai/foundry-sdk@0.6.0", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.115.1 <1", "@opentelemetry/api": "^1.4.1" } }, "sha512-6f3kUuZT9aAaAkdJq4wZRKd2KgVaOcWkbY6KwK0Z86mfn3LWJI86ZVWZhh5CQZyg9VC3T+8jJk4vpXIth6bqUg=="], "@anthropic-ai/sandbox-runtime": ["@anthropic-ai/sandbox-runtime@0.0.55", "", { "dependencies": { "@pondwader/socks5-server": "^1.0.10", "commander": "^12.1.0", "node-forge": "^1.4.0", "shell-quote": "^1.8.4", "zod": "^3.24.1" }, "bin": { "srt": "dist/cli.js" } }, "sha512-XGTLbzitn0y6OCdmqP5aYrBR0HlNi87ue++m9vMPxcNwaQeOj9i3f9CSWmXua8mv+Q8E55k4ZGlzJmpDHzQFBw=="], - "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.110.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-hOP4bNYXDFHDxxiEgzlILXrxZIYCDnhe8sry0RDRKD/QnsEpvZcQpablCdm9X/WuD/YgOiSIkkqsL1mLLlTqJw=="], + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.137.0", "", { "dependencies": { "@opentelemetry/api": "^1.4.1", "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-YpEnbPz8kWF1BWthwUCnRhci7OvDLSUO8NFKoevoPkjNkK9CJeibZITgxr9vhqOP8Le61e5tNDHFTxw1dc6Png=="], "@apm-js-collab/code-transformer": ["@apm-js-collab/code-transformer@0.18.1", "", { "dependencies": { "@types/estree": "^1.0.8", "astring": "^1.9.0", "esquery": "^1.7.0", "meriyah": "^6.1.4", "semifies": "^1.0.0", "source-map": "^0.6.0" }, "bin": { "code-transformer": "cli.js" } }, "sha512-u1Hb6bHjWtkSpiprwVP6YaHC1DTN4RAU3zYkUDUe7WMnJwdyU1pwTL9dFKiSJB9IiLue/EQovmyx6xhU7FFtAQ=="], diff --git a/package.json b/package.json index 6aa73231d0..3652baa8f1 100644 --- a/package.json +++ b/package.json @@ -91,10 +91,10 @@ }, "devDependencies": { "@alcalzone/ansi-tokenize": "0.3.1", - "@anthropic-ai/bedrock-sdk": "0.30.2", - "@anthropic-ai/foundry-sdk": "0.2.4", + "@anthropic-ai/bedrock-sdk": "0.36.0", + "@anthropic-ai/foundry-sdk": "0.6.0", "@anthropic-ai/sandbox-runtime": "0.0.55", - "@anthropic-ai/sdk": "0.110.0", + "@anthropic-ai/sdk": "0.137.0", "@aws-sdk/client-bedrock": "3.1156.0", "@aws-sdk/client-sts": "3.1156.0", "@aws-sdk/credential-provider-node": "3.972.88", @@ -171,7 +171,7 @@ "zod": "3.25.76" }, "peerDependencies": { - "@anthropic-ai/sdk": "^0.110.0", + "@anthropic-ai/sdk": "^0.137.0", "@modelcontextprotocol/sdk": "^1.32.1", "react": "^19.0.0", "react-reconciler": "^0.33.0" diff --git a/src/entrypoints/sdk/sdkUtilityTypes.ts b/src/entrypoints/sdk/sdkUtilityTypes.ts index c6731107c6..26b34433ad 100644 --- a/src/entrypoints/sdk/sdkUtilityTypes.ts +++ b/src/entrypoints/sdk/sdkUtilityTypes.ts @@ -9,12 +9,13 @@ import type { BetaUsage as Usage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' /** - * API usage with every nullable field made required and non-null. + * API usage with counters made required and non-null. * * The Anthropic API types most usage fields as `T | null`; internally the * CLI zero-initializes them (see EMPTY_USAGE in services/api/emptyUsage.ts) - * so accumulation code doesn't have to null-check on every add. + * so accumulation code doesn't have to null-check on every add. Fallback-credit + * metadata stays nullable because absence is distinct from a redemption outcome. */ export type NonNullableUsage = { - [K in keyof Usage]: NonNullable -} + [K in keyof Omit]: NonNullable +} & Pick diff --git a/src/services/api/claude.streamWatchdog.test.ts b/src/services/api/claude.streamWatchdog.test.ts index ed43e63fed..c403a2c26f 100644 --- a/src/services/api/claude.streamWatchdog.test.ts +++ b/src/services/api/claude.streamWatchdog.test.ts @@ -141,6 +141,7 @@ function makeCompleteStreamEvents(): BetaRawMessageStreamEvent[] { iterations: null, output_tokens: 1, output_tokens_details: null, + fallback_credit: null, server_tool_use: null, }, }, diff --git a/src/services/api/claude.ts b/src/services/api/claude.ts index 0134da54c7..745e560801 100644 --- a/src/services/api/claude.ts +++ b/src/services/api/claude.ts @@ -3510,6 +3510,7 @@ export function updateUsage( output_tokens: partUsage.output_tokens ?? usage.output_tokens, output_tokens_details: partUsage.output_tokens_details ?? usage.output_tokens_details, + fallback_credit: partUsage.fallback_credit ?? usage.fallback_credit, server_tool_use: { web_search_requests: partUsage.server_tool_use?.web_search_requests ?? @@ -3582,6 +3583,7 @@ export function accumulateUsage( messageUsage.server_tool_use.web_fetch_requests, }, service_tier: messageUsage.service_tier, // Use the most recent service tier + fallback_credit: messageUsage.fallback_credit, // Use the most recent outcome cache_creation: { ephemeral_1h_input_tokens: totalUsage.cache_creation.ephemeral_1h_input_tokens + diff --git a/src/services/api/claude.usage.test.ts b/src/services/api/claude.usage.test.ts index 85a5d95b1d..1e12b42e85 100644 --- a/src/services/api/claude.usage.test.ts +++ b/src/services/api/claude.usage.test.ts @@ -1,6 +1,38 @@ import { expect, test } from 'bun:test' import { accumulateUsage, updateUsage } from './claude.js' import { EMPTY_USAGE } from './emptyUsage.js' +import type { BetaFallbackCreditUsage } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' + +test('stream usage preserves fallback-credit outcomes across cumulative updates and legacy events', () => { + const redeemed: BetaFallbackCreditUsage = { status: { type: 'redeemed' } } + const declined: BetaFallbackCreditUsage = { + status: { type: 'not_applied', reason: 'expired' }, + } + const start = updateUsage(EMPTY_USAGE, { + ...EMPTY_USAGE, output_tokens: 4, fallback_credit: redeemed, + }) + expect(EMPTY_USAGE.fallback_credit).toBeNull() + expect(start.fallback_credit).toEqual(redeemed) + const delta = updateUsage(start, { + ...EMPTY_USAGE, output_tokens: 7, fallback_credit: null, + }) + expect(delta.fallback_credit).toEqual(redeemed) + const { fallback_credit: _credit, ...legacy } = { ...EMPTY_USAGE, output_tokens: 8 } + expect(updateUsage(delta, legacy as typeof EMPTY_USAGE).fallback_credit).toEqual(redeemed) + expect(updateUsage(delta, { + ...EMPTY_USAGE, output_tokens: 9, fallback_credit: declined, + }).fallback_credit).toEqual(declined) + expect(delta.output_tokens).toBe(7) +}) + +test('turn accumulation keeps the latest fallback-credit metadata without changing billed tokens', () => { + const first = { ...EMPTY_USAGE, output_tokens: 7, fallback_credit: { status: { type: 'redeemed' as const } } } + const second = { ...EMPTY_USAGE, output_tokens: 4 } + const total = accumulateUsage(first, second) + expect(total.fallback_credit).toBeNull() + expect(total.output_tokens).toBe(11) + expect(accumulateUsage(second, first).fallback_credit).toEqual(first.fallback_credit) +}) test('stream usage preserves cumulative thinking details through null and legacy updates', () => { const start = updateUsage(EMPTY_USAGE, { diff --git a/src/services/api/client.bedrockStreaming.test.ts b/src/services/api/client.bedrockStreaming.test.ts index ba24930f44..a2d78c69a7 100644 --- a/src/services/api/client.bedrockStreaming.test.ts +++ b/src/services/api/client.bedrockStreaming.test.ts @@ -11,6 +11,8 @@ const envKeys = [ 'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_MISTRAL', 'AWS_BEARER_TOKEN_BEDROCK', 'ANTHROPIC_BASE_URL', 'ANTHROPIC_BEDROCK_BASE_URL', 'AWS_REGION', + 'AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY', 'AWS_SESSION_TOKEN', + 'ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION', 'ANTHROPIC_CUSTOM_HEADERS', ] as const let restoreEnv: () => void let originalMacro: PropertyDescriptor | undefined @@ -62,29 +64,57 @@ function bedrockFrame(event: object): Buffer { return frame } -test('the real Bedrock client decodes AWS streaming frames with the installed Anthropic SDK', async () => { - const events = [ - { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'hello' } }, - { type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 7, output_tokens_details: { thinking_tokens: 5 } } }, - { type: 'message_stop' }, - ] - const fetch = mock(async () => new Response(Buffer.concat(events.map(bedrockFrame)), { - headers: { 'content-type': 'application/vnd.amazon.eventstream' }, - })) - const { getAnthropicClient } = await import( - `./client.js?bedrock-stream=${Date.now()}-${Math.random()}` - ) as typeof import('./client.js') - const client = await getAnthropicClient({ - maxRetries: 0, - model: 'anthropic.claude-sonnet-4-6', - fetchOverride: asMockFetch(fetch), +for (const auth of ['skip', 'bearer', 'sigv4'] as const) { + test(`the real Bedrock client decodes AWS streaming frames with ${auth} auth`, async () => { + if (auth === 'bearer') process.env.AWS_BEARER_TOKEN_BEDROCK = 'bedrock-test-token' + if (auth === 'sigv4') { + delete process.env.CLAUDE_CODE_SKIP_BEDROCK_AUTH + process.env.AWS_ACCESS_KEY_ID = 'test-access-key' + process.env.AWS_SECRET_ACCESS_KEY = 'test-secret-key' + process.env.AWS_SESSION_TOKEN = 'test-session-token' + } + const events = [ + { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'hello' } }, + { type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 7, output_tokens_details: { thinking_tokens: 5 } } }, + { type: 'message_stop' }, + ] + let requestUrl = '' + let requestHeaders = new Headers() + let requestBody: unknown + const fetch = mock(async (input: RequestInfo | URL, init?: RequestInit) => { + requestUrl = String(input) + requestHeaders = new Headers(init?.headers) + requestBody = JSON.parse(String(init?.body)) + return new Response(Buffer.concat(events.map(bedrockFrame)), { + headers: { 'content-type': 'application/vnd.amazon.eventstream' }, + }) + }) + const { getAnthropicClient } = await import( + `./client.js?bedrock-stream=${Date.now()}-${Math.random()}` + ) as typeof import('./client.js') + const client = await getAnthropicClient({ + maxRetries: 0, + model: 'anthropic.claude-sonnet-4-6', + fetchOverride: asMockFetch(fetch), + }) + expect(client.constructor.name).toBe('AnthropicBedrock') + const received: unknown[] = [] + for await (const event of await client.beta.messages.create({ + model: 'anthropic.claude-sonnet-4-6', max_tokens: 16, + messages: [{ role: 'user', content: 'hello' }], stream: true, + })) received.push(event) + expect(fetch).toHaveBeenCalledTimes(1) + expect(received).toEqual(events) + expect(requestUrl).toBe('https://bedrock.example.test/model/anthropic.claude-sonnet-4-6/invoke-with-response-stream') + expect(requestBody).toMatchObject({ + anthropic_version: 'bedrock-2023-05-31', max_tokens: 16, + messages: [{ role: 'user', content: 'hello' }], + }) + if (auth === 'bearer') expect(requestHeaders.get('authorization')).toBe('Bearer bedrock-test-token') + else if (auth === 'sigv4') { + expect(requestHeaders.get('authorization')).toContain('AWS4-HMAC-SHA256 Credential=test-access-key/') + expect(requestHeaders.get('authorization')).toContain('/us-east-1/bedrock/aws4_request') + expect(requestHeaders.get('x-amz-security-token')).toBe('test-session-token') + } else expect(requestHeaders.get('authorization')).toBeNull() }) - expect(client.constructor.name).toBe('AnthropicBedrock') - const received: unknown[] = [] - for await (const event of await client.beta.messages.create({ - model: 'anthropic.claude-sonnet-4-6', max_tokens: 16, - messages: [{ role: 'user', content: 'hello' }], stream: true, - })) received.push(event) - expect(fetch).toHaveBeenCalledTimes(1) - expect(received).toEqual(events) -}) +} diff --git a/src/services/api/client.foundryStreaming.test.ts b/src/services/api/client.foundryStreaming.test.ts new file mode 100644 index 0000000000..2f09279384 --- /dev/null +++ b/src/services/api/client.foundryStreaming.test.ts @@ -0,0 +1,126 @@ +import { afterEach, beforeEach, expect, mock, test } from 'bun:test' +import { snapshotEnv } from '../../test/envSnapshot.js' +import { acquireSharedMutationLock, releaseSharedMutationLock } from '../../test/sharedMutationLock.js' +import { asMockFetch } from '../../test/typedMocks.js' +import { importOptionalRuntimeModule } from '../../utils/optionalRuntimeModule.js' +import { updateUsage } from './claude.js' +import { EMPTY_USAGE } from './emptyUsage.js' +import type { BetaMessage, BetaRawMessageStreamEvent } from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs' + +const envKeys = [ + 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_FOUNDRY', 'CLAUDE_CODE_USE_VERTEX', + 'CLAUDE_CODE_USE_OPENAI', 'CLAUDE_CODE_USE_GEMINI', + 'CLAUDE_CODE_USE_GITHUB', 'CLAUDE_CODE_USE_MISTRAL', + 'CLAUDE_CODE_SKIP_FOUNDRY_AUTH', 'ANTHROPIC_FOUNDRY_API_KEY', + 'ANTHROPIC_FOUNDRY_BASE_URL', 'ANTHROPIC_FOUNDRY_RESOURCE', + 'ANTHROPIC_BASE_URL', 'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', + 'ANTHROPIC_CUSTOM_HEADERS', +] as const +let restoreEnv: () => void +let originalMacro: PropertyDescriptor | undefined + +beforeEach(async () => { + await acquireSharedMutationLock('client.foundryStreaming.test.ts') + restoreEnv = snapshotEnv(envKeys, { clear: true }) + originalMacro = Object.getOwnPropertyDescriptor(globalThis, 'MACRO') + ;(globalThis as Record).MACRO = { VERSION: 'test-version' } + process.env.CLAUDE_CODE_USE_FOUNDRY = '1' + process.env.ANTHROPIC_FOUNDRY_RESOURCE = 'test-resource' +}) + +afterEach(() => { + try { + restoreEnv() + if (originalMacro) Object.defineProperty(globalThis, 'MACRO', originalMacro) + else delete (globalThis as Record).MACRO + } finally { + releaseSharedMutationLock() + } +}) + +for (const auth of ['api-key', 'entra'] as const) { + test(`the real Foundry client constructs and decodes beta streams with ${auth} auth`, async () => { + if (auth === 'api-key') process.env.ANTHROPIC_FOUNDRY_API_KEY = 'foundry-test-key' + else { + delete process.env.ANTHROPIC_FOUNDRY_RESOURCE + process.env.ANTHROPIC_FOUNDRY_BASE_URL = 'https://foundry.example.test/anthropic/' + } + const message: BetaMessage = { + id: 'msg_foundry', type: 'message', role: 'assistant', + model: 'claude-sonnet-4-6', content: [], + usage: { ...EMPTY_USAGE, input_tokens: 4 }, + stop_reason: null, stop_sequence: null, container: null, + context_management: null, diagnostics: null, stop_details: null, + } + const events: BetaRawMessageStreamEvent[] = [ + { type: 'message_start', message }, + { type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'tool_lookup', name: 'lookup', input: {} } }, + { type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: '{"query":' } }, + { type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: '"hello"}' } }, + { type: 'content_block_stop', index: 0 }, + { + type: 'message_delta', context_management: null, + delta: { container: null, stop_reason: 'tool_use', stop_sequence: null, stop_details: null }, + usage: { ...message.usage, output_tokens: 7, output_tokens_details: { thinking_tokens: 3 }, fallback_credit: { status: { type: 'redeemed' } } }, + }, + { type: 'message_stop' }, + ] + const requests: { url: string; headers: Headers; body: unknown }[] = [] + const fetch = mock(async (input: RequestInfo | URL, init?: RequestInit) => { + requests.push({ url: String(input), headers: new Headers(init?.headers), body: JSON.parse(String(init?.body)) }) + return new Response(events.map(event => `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`).join(''), { + headers: { 'content-type': 'text/event-stream' }, + }) + }) + const tokenProvider = mock(async () => `entra-test-token-${tokenProvider.mock.calls.length}`) + const getBearerTokenProvider = mock((_credential: unknown, _scope: string) => tokenProvider) + const { getAnthropicClient, _setOptionalRuntimeModuleImporterForTesting } = await import( + `./client.js?foundry-stream=${Date.now()}-${Math.random()}` + ) as typeof import('./client.js') + // Keep the installed Foundry SDK and its stream implementation real; replace + // only Azure credential acquisition so no cloud account is needed. + _setOptionalRuntimeModuleImporterForTesting(async (specifier: string, feature: string): Promise => { + if (specifier === '@azure/identity') return { + DefaultAzureCredential: class {}, getBearerTokenProvider, + } as T + return importOptionalRuntimeModule(specifier, feature) + }) + try { + const client = await getAnthropicClient({ maxRetries: 0, model: 'claude-sonnet-4-6', fetchOverride: asMockFetch(fetch) }) + expect(client.constructor.name).toBe('AnthropicFoundry') + // Two requests prove the Entra token callback is invoked for each request. + for (let i = 0; i < 2; i++) { + const received: unknown[] = [] + let usage = { ...EMPTY_USAGE } + for await (const event of await client.beta.messages.create({ + model: 'claude-sonnet-4-6', max_tokens: 16, stream: true, + messages: [{ role: 'user', content: 'hello' }], + tools: [{ name: 'lookup', input_schema: { type: 'object' } }], + })) { + received.push(event) + if (event.type === 'message_delta') usage = updateUsage(usage, event.usage) + } + expect(received).toEqual(events) + expect(usage).toMatchObject({ + output_tokens: 7, output_tokens_details: { thinking_tokens: 3 }, + fallback_credit: { status: { type: 'redeemed' } }, + }) + } + expect(requests).toHaveLength(2) + for (const [i, request] of requests.entries()) { + expect(request.url).toBe(auth === 'api-key' + ? 'https://test-resource.services.ai.azure.com/anthropic/v1/messages?beta=true' + : 'https://foundry.example.test/anthropic/v1/messages?beta=true') + expect(request.body).toMatchObject({ model: 'claude-sonnet-4-6', max_tokens: 16, stream: true, tools: [{ name: 'lookup' }] }) + expect(request.headers.get('x-api-key')).toBe(auth === 'api-key' ? 'foundry-test-key' : null) + expect(request.headers.get('authorization')).toBe(auth === 'entra' ? `Bearer entra-test-token-${i + 1}` : null) + } + if (auth === 'entra') { + expect(getBearerTokenProvider).toHaveBeenCalledWith(expect.anything(), 'https://cognitiveservices.azure.com/.default') + expect(tokenProvider).toHaveBeenCalledTimes(2) + } else expect(getBearerTokenProvider).not.toHaveBeenCalled() + } finally { + _setOptionalRuntimeModuleImporterForTesting() + } + }) +} diff --git a/src/services/api/emptyUsage.ts b/src/services/api/emptyUsage.ts index abceeaee1a..852b32af4c 100644 --- a/src/services/api/emptyUsage.ts +++ b/src/services/api/emptyUsage.ts @@ -11,6 +11,7 @@ export const EMPTY_USAGE: Readonly = { cache_read_input_tokens: 0, output_tokens: 0, output_tokens_details: { thinking_tokens: 0 }, + fallback_credit: null, server_tool_use: { web_search_requests: 0, web_fetch_requests: 0 }, service_tier: 'standard', cache_creation: { diff --git a/src/services/tokenEstimation.test.ts b/src/services/tokenEstimation.test.ts index 3a2bfb2c4e..6cd43a793f 100644 --- a/src/services/tokenEstimation.test.ts +++ b/src/services/tokenEstimation.test.ts @@ -1,7 +1,26 @@ import type { Anthropic } from '@anthropic-ai/sdk' import { expect, mock, test } from 'bun:test' import { jsonStringify } from '../utils/slowOperations.js' -import { __test, roughTokenCountEstimation } from './tokenEstimation.js' +import { __test, roughTokenCountEstimation, roughTokenCountEstimationForMessage } from './tokenEstimation.js' + +test('rough estimation handles browser state alongside text, images and tool references in tool results', () => { + const browserState: Anthropic.BrowserStateBlockParam = { type: 'browser_state', tabs: [] } + const reference: Anthropic.ToolReferenceBlockParam = { type: 'tool_reference', tool_name: 'lookup_docs' } + const toolResult: Anthropic.ToolResultBlockParam = { + type: 'tool_result', tool_use_id: 'tool_browser', + content: [ + { type: 'text', text: 'page loaded' }, browserState, reference, + { type: 'image', source: { type: 'base64', media_type: 'image/png', data: 'A'.repeat(10000) } }, + ], + } + expect(roughTokenCountEstimationForMessage({ + type: 'user', message: { content: [toolResult] }, + })).toBe( + roughTokenCountEstimation('page loaded') + + roughTokenCountEstimation(jsonStringify(browserState)) + + roughTokenCountEstimation(jsonStringify(reference)) + 2000, + ) +}) function createTextTool(): Anthropic.Beta.Messages.BetaToolUnion { return { diff --git a/src/services/tokenEstimation.ts b/src/services/tokenEstimation.ts index 2aeac095df..d5f61fc5c3 100644 --- a/src/services/tokenEstimation.ts +++ b/src/services/tokenEstimation.ts @@ -582,13 +582,16 @@ export function roughTokenCountEstimationForMessage(message: { return 0 } +type ToolResultContentBlock = Exclude< + Anthropic.ToolResultBlockParam['content'], string | undefined +>[number] + function roughTokenCountEstimationForContent( content: | string | Array - // ToolReferenceBlockParam appears nested in tool_result content but is - // not part of the top-level ContentBlockParam union. - | Array + // Nested tool results can include blocks absent from the top-level union. + | Array | undefined, ): number { if (!content) { @@ -610,7 +613,7 @@ function roughTokenCountEstimationForBlock( | Anthropic.ContentBlock | Anthropic.ContentBlockParam // Nested tool_result content block; falls through to the stringify path. - | Anthropic.ToolReferenceBlockParam, + | ToolResultContentBlock, ): number { if (typeof block === 'string') { return roughTokenCountEstimation(block) diff --git a/src/tools/AgentTool/UI.tsx b/src/tools/AgentTool/UI.tsx index e8bae2b1d6..41a35d446c 100644 --- a/src/tools/AgentTool/UI.tsx +++ b/src/tools/AgentTool/UI.tsx @@ -281,6 +281,7 @@ export function renderToolResultMessage(data: Output, progressMessagesForMessage usage: { ...usage, output_tokens_details: null, + fallback_credit: null, inference_geo: null, iterations: null, speed: null diff --git a/src/utils/messages/factories.test.ts b/src/utils/messages/factories.test.ts index f93aa8046f..65f2bea621 100644 --- a/src/utils/messages/factories.test.ts +++ b/src/utils/messages/factories.test.ts @@ -27,6 +27,7 @@ test('createAssistantMessage builds a synthetic assistant message', () => { expect(message.type).toBe('assistant') expect(message.message.usage.output_tokens_details).toBeNull() + expect(message.message.usage.fallback_credit).toBeNull() expect(message.message.content[0]).toMatchObject({ type: 'text', text: 'hello', @@ -44,6 +45,15 @@ test('createAssistantMessage preserves supplied output token details', () => { expect(message.message.usage.output_tokens).toBe(7) }) +test('createAssistantMessage preserves supplied fallback-credit outcomes', () => { + const usage = { + ...createAssistantMessage({ content: 'default' }).message.usage, + fallback_credit: { status: { type: 'redeemed' as const } }, + } + const message = createAssistantMessage({ content: 'hello', usage }) + expect(message.message.usage.fallback_credit).toEqual(usage.fallback_credit) +}) + test('createAssistantAPIErrorMessage builds a synthetic API error message', () => { const message = createAssistantAPIErrorMessage({ content: '', diff --git a/src/utils/messages/factories.ts b/src/utils/messages/factories.ts index b6e000356c..4b0ba42e5e 100644 --- a/src/utils/messages/factories.ts +++ b/src/utils/messages/factories.ts @@ -99,6 +99,7 @@ function baseCreateAssistantMessage({ input_tokens: 0, output_tokens: 0, output_tokens_details: null, + fallback_credit: null, cache_creation_input_tokens: 0, cache_read_input_tokens: 0, server_tool_use: { web_search_requests: 0, web_fetch_requests: 0 }, From cd350528f6f412a0ff19e779a6bd12d0fd0f52a5 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 09:53:31 -0700 Subject: [PATCH 084/121] test(aimlapi): drive polling retries with fake timers (#2393) * test(aimlapi): drive polling retries with fake timers * test(aimlapi): await rate-limit retry timer registration --- src/integrations/aimlapi/topup.test.ts | 35 ++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/integrations/aimlapi/topup.test.ts b/src/integrations/aimlapi/topup.test.ts index 8baaa6d174..e6e0ac8d96 100644 --- a/src/integrations/aimlapi/topup.test.ts +++ b/src/integrations/aimlapi/topup.test.ts @@ -1,4 +1,4 @@ -import { afterAll, afterEach, expect, mock, test } from 'bun:test' +import { afterAll, afterEach, expect, jest, mock, test } from 'bun:test' import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -78,6 +78,7 @@ afterAll(() => { }) afterEach(() => { + jest.useRealTimers() globalThis.fetch = originalFetch lastSavedProfileEnv = undefined setClaudeConfigHomeDirForTesting(undefined) @@ -1950,6 +1951,7 @@ test('a terminal poll error awaits onSession before rejecting, not racing ahead }) test('polling retries a transient transport failure', async () => { + jest.useFakeTimers() process.env.AIMLAPI_APP_URL = 'https://app.example.test' let attempts = 0 globalThis.fetch = mock(async () => { @@ -1965,13 +1967,22 @@ test('polling retries a transient transport failure', async () => { verificationBaseUrl: 'https://front.example.test', }) - await expect(pollUntilPaid(client, 'session')).resolves.toEqual( + const polling = pollUntilPaid(client, 'session') + // Let the failed request schedule its retry before advancing the poll timer. + await new Promise(resolve => setImmediate(resolve)) + jest.advanceTimersByTime(2999) + await new Promise(resolve => setImmediate(resolve)) + expect(attempts).toBe(1) + jest.advanceTimersByTime(1) + + await expect(polling).resolves.toEqual( expect.objectContaining({ status: 'paid' }), ) expect(attempts).toBe(2) }) test('polling retains and retries the same session after a rate limit', async () => { + jest.useFakeTimers() process.env.AIMLAPI_APP_URL = 'https://app.example.test' let attempts = 0 globalThis.fetch = mock(async () => { @@ -1988,9 +1999,23 @@ test('polling retains and retries the same session after a rate limit', async () }) const sessions: string[] = [] - await expect( - pollUntilPaid(client, 'session', undefined, value => { sessions.push(value) }), - ).resolves.toEqual(expect.objectContaining({ status: 'paid' })) + const setTimeoutSpy = jest.spyOn(globalThis, 'setTimeout') + const polling = pollUntilPaid(client, 'session', undefined, value => { sessions.push(value) }) + try { + // Reading the 429 body may take multiple turns before the retry is scheduled. + for (let tick = 0; tick < 100 && !setTimeoutSpy.mock.calls.some(call => call[1] === 3000); tick++) { + await new Promise(resolve => setImmediate(resolve)) + } + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), 3000) + } finally { + setTimeoutSpy.mockRestore() + } + jest.advanceTimersByTime(2999) + await new Promise(resolve => setImmediate(resolve)) + expect(attempts).toBe(1) + jest.advanceTimersByTime(1) + + await expect(polling).resolves.toEqual(expect.objectContaining({ status: 'paid' })) expect(attempts).toBe(2) expect(sessions).toEqual([]) }) From de1bafcf6b1e9f94955444a97d8b4f7b16f54d23 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 11:19:50 -0700 Subject: [PATCH 085/121] build(deps): bump @anthropic-ai/sandbox-runtime to 0.0.81 (#2397) * build(deps): bump @anthropic-ai/sandbox-runtime to 0.0.81 * test(sandbox): restore base sandbox config between adapter tests --- bun.lock | 6 +- package.json | 2 +- src/utils/sandbox/sandbox-adapter.test.ts | 266 ++++++++++++++++++++-- 3 files changed, 254 insertions(+), 20 deletions(-) diff --git a/bun.lock b/bun.lock index 8440806126..0fe3c8953c 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ "@alcalzone/ansi-tokenize": "0.3.1", "@anthropic-ai/bedrock-sdk": "0.36.0", "@anthropic-ai/foundry-sdk": "0.6.0", - "@anthropic-ai/sandbox-runtime": "0.0.55", + "@anthropic-ai/sandbox-runtime": "0.0.81", "@anthropic-ai/sdk": "0.137.0", "@aws-sdk/client-bedrock": "3.1156.0", "@aws-sdk/client-sts": "3.1156.0", @@ -117,7 +117,7 @@ "@anthropic-ai/foundry-sdk": ["@anthropic-ai/foundry-sdk@0.6.0", "", { "dependencies": { "@anthropic-ai/sdk": ">=0.115.1 <1", "@opentelemetry/api": "^1.4.1" } }, "sha512-6f3kUuZT9aAaAkdJq4wZRKd2KgVaOcWkbY6KwK0Z86mfn3LWJI86ZVWZhh5CQZyg9VC3T+8jJk4vpXIth6bqUg=="], - "@anthropic-ai/sandbox-runtime": ["@anthropic-ai/sandbox-runtime@0.0.55", "", { "dependencies": { "@pondwader/socks5-server": "^1.0.10", "commander": "^12.1.0", "node-forge": "^1.4.0", "shell-quote": "^1.8.4", "zod": "^3.24.1" }, "bin": { "srt": "dist/cli.js" } }, "sha512-XGTLbzitn0y6OCdmqP5aYrBR0HlNi87ue++m9vMPxcNwaQeOj9i3f9CSWmXua8mv+Q8E55k4ZGlzJmpDHzQFBw=="], + "@anthropic-ai/sandbox-runtime": ["@anthropic-ai/sandbox-runtime@0.0.81", "", { "dependencies": { "@pondwader/socks5-server": "^1.0.10", "commander": "^12.1.0", "node-forge": "^1.4.0", "zod": "^3.25.0" }, "bin": { "srt": "dist/cli.js" } }, "sha512-bjHDu87gjIR7+Oxy9jPiK3v8CvSELCzfpXa3fjqfSKdvlZET2iF51xcXILWPayTpowB3xtZeQVf2JR916nVW/w=="], "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.137.0", "", { "dependencies": { "@opentelemetry/api": "^1.4.1", "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-YpEnbPz8kWF1BWthwUCnRhci7OvDLSUO8NFKoevoPkjNkK9CJeibZITgxr9vhqOP8Le61e5tNDHFTxw1dc6Png=="], @@ -1289,8 +1289,6 @@ "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], - "@anthropic-ai/sandbox-runtime/shell-quote": ["shell-quote@1.8.3", "", {}, "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw=="], - "@aws-crypto/crc32/@aws-crypto/util": ["@aws-crypto/util@3.0.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-utf8-browser": "^3.0.0", "tslib": "^1.11.1" } }, "sha512-2OJlpeJpCR48CC8r+uKVChzs9Iungj9wkZrl8Z041DWEWvyIHILYKCPNzJghKsivj+S3mLo6BVc7mBNzdxA46w=="], "@aws-crypto/crc32/@aws-sdk/types": ["@aws-sdk/types@3.973.8", "", { "dependencies": { "@smithy/types": "^4.14.1", "tslib": "^2.6.2" } }, "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw=="], diff --git a/package.json b/package.json index 3652baa8f1..ba0cfac106 100644 --- a/package.json +++ b/package.json @@ -93,7 +93,7 @@ "@alcalzone/ansi-tokenize": "0.3.1", "@anthropic-ai/bedrock-sdk": "0.36.0", "@anthropic-ai/foundry-sdk": "0.6.0", - "@anthropic-ai/sandbox-runtime": "0.0.55", + "@anthropic-ai/sandbox-runtime": "0.0.81", "@anthropic-ai/sdk": "0.137.0", "@aws-sdk/client-bedrock": "3.1156.0", "@aws-sdk/client-sts": "3.1156.0", diff --git a/src/utils/sandbox/sandbox-adapter.test.ts b/src/utils/sandbox/sandbox-adapter.test.ts index c054d6aee9..cc6565b318 100644 --- a/src/utils/sandbox/sandbox-adapter.test.ts +++ b/src/utils/sandbox/sandbox-adapter.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, describe, expect, test } from 'bun:test' -import { mkdtemp, rm } from 'fs/promises' +import { SandboxManager as BaseSandboxManager } from '@anthropic-ai/sandbox-runtime' +import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises' import { tmpdir } from 'os' import { join, resolve, sep } from 'path' import { @@ -8,25 +9,86 @@ import { setCwdState, setOriginalCwd, } from '../../bootstrap/state.js' +import { snapshotEnv } from '../../test/envSnapshot.js' import { acquireSharedMutationLock, releaseSharedMutationLock, } from '../../test/sharedMutationLock.js' -import { resetSettingsCache } from '../settings/settingsCache.js' -import type { SettingsJson } from '../settings/types.js' -import { convertToSandboxRuntimeConfig } from './sandbox-adapter.js' +import { getSettingsForSource } from '../settings/settings.js' +import { + resetSettingsCache, + setCachedSettingsForSource, +} from '../settings/settingsCache.js' +import { SettingsSchema, type SettingsJson } from '../settings/types.js' +import { + convertToSandboxRuntimeConfig, + SandboxManager, + type SandboxRuntimeConfig, + SandboxRuntimeConfigSchema, +} from './sandbox-adapter.js' + +const policyGetters = [ + 'getFsReadConfig', + 'getFsWriteConfig', + 'getNetworkRestrictionConfig', + 'getAllowUnixSockets', + 'getAllowLocalBinding', + 'getIgnoreViolations', + 'getEnableWeakerNestedSandbox', +] as const + +type RuntimePolicy = { + [K in (typeof policyGetters)[number]]: ReturnType<(typeof SandboxManager)[K]> +} + +// reset() retains config and the public API cannot clear it. Inspect policies +// in a child so an initially unconfigured shared manager stays unconfigured. +async function inspectRuntimePolicy(config: SandboxRuntimeConfig): Promise { + const previousConfig = BaseSandboxManager.getConfig() + const adapterUrl = new URL('./sandbox-adapter.ts', import.meta.url).href + const child = Bun.spawn([process.execPath, '--eval', ` + import { SandboxManager as BaseSandboxManager } from '@anthropic-ai/sandbox-runtime' + import { SandboxManager } from ${JSON.stringify(adapterUrl)} + const initiallyUnconfigured = BaseSandboxManager.getConfig() === undefined + BaseSandboxManager.updateConfig(await Bun.stdin.json()) + const policy = Object.fromEntries(${JSON.stringify(policyGetters)}.map( + getter => [getter, SandboxManager[getter]()], + )) + console.log(JSON.stringify({ initiallyUnconfigured, policy })) + `], { + stdin: new Blob([JSON.stringify(config)]), + stdout: 'pipe', + stderr: 'pipe', + timeout: 8_000, + }) + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(child.stdout).text(), + new Response(child.stderr).text(), + child.exited, + ]) + expect(exitCode, stderr).toBe(0) + const result = JSON.parse(stdout) as { + initiallyUnconfigured: boolean + policy: RuntimePolicy + } + expect(result.initiallyUnconfigured).toBe(true) + expect(BaseSandboxManager.getConfig()).toBe(previousConfig) + return result.policy +} describe('convertToSandboxRuntimeConfig', () => { - let previousConfigDir: string | undefined + let restoreEnv: () => void let previousOriginalCwd: string let previousCwd: string let tempRoot: string let activeCwd: string + let previousRuntimeConfig: ReturnType beforeEach(async () => { await acquireSharedMutationLock('utils/sandbox/sandbox-adapter.test.ts') - previousConfigDir = process.env.CLAUDE_CONFIG_DIR + previousRuntimeConfig = BaseSandboxManager.getConfig() + restoreEnv = snapshotEnv(['CLAUDE_CONFIG_DIR', 'OPENCLAUDE_CONFIG_DIR']) previousOriginalCwd = getOriginalCwd() previousCwd = getCwdState() @@ -35,6 +97,7 @@ describe('convertToSandboxRuntimeConfig', () => { activeCwd = join(tempRoot, 'active-project') process.env.CLAUDE_CONFIG_DIR = join(tempRoot, 'config') + process.env.OPENCLAUDE_CONFIG_DIR = join(tempRoot, 'openclaude-config') resetSettingsCache() setOriginalCwd(originalCwd) setCwdState(activeCwd) @@ -42,17 +105,21 @@ describe('convertToSandboxRuntimeConfig', () => { afterEach(async () => { try { - if (previousConfigDir === undefined) { - delete process.env.CLAUDE_CONFIG_DIR - } else { - process.env.CLAUDE_CONFIG_DIR = previousConfigDir + await SandboxManager.reset() + if (previousRuntimeConfig !== undefined) { + BaseSandboxManager.updateConfig(previousRuntimeConfig) } - setOriginalCwd(previousOriginalCwd) - setCwdState(previousCwd) - resetSettingsCache() - await rm(tempRoot, { recursive: true, force: true }) + expect(BaseSandboxManager.getConfig()).toEqual(previousRuntimeConfig) } finally { - releaseSharedMutationLock() + try { + restoreEnv() + setOriginalCwd(previousOriginalCwd) + setCwdState(previousCwd) + resetSettingsCache() + await rm(tempRoot, { recursive: true, force: true }) + } finally { + releaseSharedMutationLock() + } } }) @@ -104,4 +171,173 @@ describe('convertToSandboxRuntimeConfig', () => { ).toBe(true) } }) + + // Each child has an 8s deadline; allow 2s more for assertions and teardown. + test('maps validated settings to the runtime network and process policy', async () => { + const settings = SettingsSchema().parse({ + permissions: { + allow: ['WebFetch(domain:api.example.com:443)'], + deny: ['WebFetch(domain:blocked.example.com)', 'WebFetch(domain:*:22)'], + }, + sandbox: { + network: { + allowedDomains: ['*.example.org'], + allowUnixSockets: ['/var/run/example.sock'], + allowAllUnixSockets: false, + allowLocalBinding: false, + httpProxyPort: 8080, + socksProxyPort: 1080, + }, + ignoreViolations: { '*': ['/dev/example'] }, + enableWeakerNestedSandbox: false, + enableWeakerNetworkIsolation: false, + ripgrep: { command: 'rg', args: ['--hidden'] }, + }, + }) + const config = SandboxRuntimeConfigSchema.parse( + convertToSandboxRuntimeConfig(settings), + ) + + expect(config.network).toEqual({ + allowedDomains: ['*.example.org', 'api.example.com:443'], + deniedDomains: ['blocked.example.com', '*:22'], + allowUnixSockets: ['/var/run/example.sock'], + allowAllUnixSockets: false, + allowLocalBinding: false, + httpProxyPort: 8080, + socksProxyPort: 1080, + }) + expect(config.ignoreViolations).toEqual({ '*': ['/dev/example'] }) + expect(config.enableWeakerNestedSandbox).toBe(false) + expect(config.enableWeakerNetworkIsolation).toBe(false) + expect(config.ripgrep).toEqual({ command: 'rg', args: ['--hidden'] }) + expect(config.filesystem.disabled).toBeUndefined() + expect(config.allowNestedUserNamespaces).toBeUndefined() + expect(config.credentials).toBeUndefined() + expect(config.windows).toBeUndefined() + + const policy = await inspectRuntimePolicy(config) + expect(policy.getNetworkRestrictionConfig).toEqual({ + allowedHosts: ['*.example.org', 'api.example.com:443'], + deniedHosts: ['blocked.example.com', '*:22'], + }) + expect(policy.getAllowUnixSockets).toEqual(['/var/run/example.sock']) + expect(policy.getAllowLocalBinding).toBe(false) + expect(policy.getIgnoreViolations).toEqual({ '*': ['/dev/example'] }) + expect(policy.getEnableWeakerNestedSandbox).toBe(false) + }, 10_000) + + test('loads filesystem rules from disk with their source path semantics', async () => { + const configDir = process.env.OPENCLAUDE_CONFIG_DIR! + await mkdir(configDir, { recursive: true }) + await writeFile(join(configDir, 'settings.json'), JSON.stringify({ + permissions: { + allow: ['Edit(/editable/**)'], + deny: ['Read(//private/**)', 'Edit(/locked/**)'], + }, + sandbox: { + filesystem: { + allowWrite: ['./build'], + denyWrite: [join(tempRoot, 'protected')], + denyRead: ['./secrets'], + allowRead: ['./secrets/public'], + }, + }, + })) + resetSettingsCache() + const settings = getSettingsForSource('userSettings')! + expect(settings.sandbox?.filesystem?.allowWrite).toEqual(['./build']) + const config = SandboxRuntimeConfigSchema.parse( + convertToSandboxRuntimeConfig(settings), + ) + const policy = await inspectRuntimePolicy(config) + const read = policy.getFsReadConfig + const write = policy.getFsWriteConfig + expect(read.denyOnly).toContain('/private') + expect(read.denyOnly).toContain(join(configDir, 'secrets')) + expect(read.allowWithinDeny).toContain(join(configDir, 'secrets/public')) + expect(write.allowOnly).toContain(join(configDir, 'editable')) + expect(write.allowOnly).toContain(join(configDir, 'build')) + expect(write.denyWithinAllow).toContain(join(configDir, 'locked')) + expect(write.denyWithinAllow).toContain(join(tempRoot, 'protected')) + expect(write.denyWithinAllow).toContain( + join(activeCwd, '.openclaude', 'settings.json'), + ) + }, 10_000) + + test('keeps managed domain and read exceptions exclusive without dropping denies', () => { + setCachedSettingsForSource('policySettings', SettingsSchema().parse({ + permissions: { allow: ['WebFetch(domain:managed.example.com)'] }, + sandbox: { + network: { + allowManagedDomainsOnly: true, + allowedDomains: ['policy.example.com'], + }, + filesystem: { + allowManagedReadPathsOnly: true, + allowRead: [join(tempRoot, 'policy-public')], + }, + }, + })) + const userSettings = SettingsSchema().parse({ + permissions: { + allow: ['WebFetch(domain:user.example.com)'], + deny: ['WebFetch(domain:blocked.example.com)'], + }, + sandbox: { + network: { allowedDomains: ['extra.example.com'] }, + filesystem: { + allowRead: [join(tempRoot, 'user-public')], + denyRead: [join(tempRoot, 'secrets')], + }, + }, + }) + setCachedSettingsForSource('userSettings', userSettings) + const config = SandboxRuntimeConfigSchema.parse( + convertToSandboxRuntimeConfig(userSettings), + ) + + expect(config.network.allowedDomains).toEqual([ + 'policy.example.com', 'managed.example.com', + ]) + expect(config.network.deniedDomains).toEqual(['blocked.example.com']) + expect(config.filesystem.allowRead).toEqual([join(tempRoot, 'policy-public')]) + expect(config.filesystem.denyRead).toContain(join(tempRoot, 'secrets')) + }) + + test('preserves an empty network allowlist through the runtime adapter', async () => { + const config = SandboxRuntimeConfigSchema.parse( + convertToSandboxRuntimeConfig(SettingsSchema().parse({})), + ) + const policy = await inspectRuntimePolicy(config) + + expect(policy.getNetworkRestrictionConfig).toEqual({ + allowedHosts: [], + }) + expect(policy.getAllowLocalBinding).toBeUndefined() + expect(policy.getAllowUnixSockets).toBeUndefined() + }, 10_000) + + test('exposes sanitized violation events and subscriptions through the adapter', () => { + const store = SandboxManager.getSandboxViolationStore() + const timestamp = new Date('2026-01-01T00:00:00Z') + const count = store.getTotalCount() + const lines: string[][] = [] + const unsubscribe = store.subscribe(events => { + lines.push(events.map(event => event.line)) + }) + try { + store.addViolation({ + line: 'deny\n', command: 'touch blocked', timestamp, + }) + expect(store.getViolations().at(-1)).toEqual({ + line: 'deny file-write', command: 'touch blocked', timestamp, + }) + expect(store.getTotalCount()).toBe(count + 1) + expect(lines.at(-1)).toEqual(['deny file-write']) + } finally { + unsubscribe() + store.clear() + } + }) }) From ff13baf587522f97b61d844768e8a40e2f0bc254 Mon Sep 17 00:00:00 2001 From: JATMN Date: Sat, 24 Oct 2026 11:31:34 -0700 Subject: [PATCH 086/121] chore(deps): replace cli-highlight with a highlight.js ANSI renderer (D-5) (#2396) * chore(deps): replace cli-highlight with a highlight.js ANSI renderer * fix(highlight): key fallback highlight cache by Chalk level Also correct the CONTRIBUTING highlighting note and move the qrcode retention test and note out of this change. --- CONTRIBUTING.md | 10 ++- bun.lock | 50 +----------- package.json | 2 +- scripts/externals.ts | 2 +- .../HighlightedCode/Fallback.test.tsx | 61 ++++++++++++++ src/components/HighlightedCode/Fallback.tsx | 5 +- src/ink/Ansi.tsx | 2 +- src/native-ts/color-diff/index.ts | 4 +- src/test/fixtures/cliHighlight.baseline.json | 80 +++++++++++++++++++ src/utils/cliHighlight.test.ts | 63 +++++++++++++++ src/utils/cliHighlight.ts | 66 ++++++++++----- 11 files changed, 269 insertions(+), 76 deletions(-) create mode 100644 src/components/HighlightedCode/Fallback.test.tsx create mode 100644 src/test/fixtures/cliHighlight.baseline.json create mode 100644 src/utils/cliHighlight.test.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e79c4effbc..b7afebe383 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -230,9 +230,15 @@ color detection tests must import in a fresh process. ### Dependency pins and overrides +Terminal syntax highlighting uses the exact `highlight.js` pin directly, with +the shared Chalk instance for ANSI colors. Keep the lazy loader and terminal +palette in `src/utils/cliHighlight.ts` for terminal code and Markdown highlighting +only. Structured diff rendering in `src/native-ts/color-diff/index.ts` loads +`highlight.js` separately. Run the highlighting and Markdown regression tests +when changing this dependency or its ANSI renderer. + Keep all direct dependencies and devDependencies pinned to exact versions in the -root and web manifests. The existing `sharp` range is the temporary exception -while its separate upgrade is in progress. Regenerate the relevant Bun lockfile +root and web manifests. Regenerate the relevant Bun lockfile when changing a pin. The root `overrides` block also controls transitive resolutions: diff --git a/bun.lock b/bun.lock index 0fe3c8953c..715fbe455a 100644 --- a/bun.lock +++ b/bun.lock @@ -38,7 +38,6 @@ "chalk": "6.0.1", "chokidar": "4.0.3", "cli-boxes": "4.0.1", - "cli-highlight": "2.1.11", "commander": "12.1.0", "cross-spawn": "7.0.6", "diff": "9.0.0", @@ -54,6 +53,7 @@ "google-auth-library": "11.3.1", "graphology": "0.26.0", "graphology-metrics": "2.4.2", + "highlight.js": "10.7.3", "https-proxy-agent": "9.1.0", "ignore": "7.0.13", "indent-string": "5.0.0", @@ -613,8 +613,6 @@ "ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], - "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], "astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="], @@ -663,8 +661,6 @@ "cli-boxes": ["cli-boxes@4.0.1", "", {}, "sha512-5IOn+jcCEHEraYolBPs/sT4BxYCe2nHg374OPiItB1O96KZFseS2gthU4twyYzeDcFew4DaUM/xwc5BQf08JJw=="], - "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], - "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], @@ -1019,8 +1015,6 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], - "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "needle": ["needle@3.5.0", "", { "dependencies": { "iconv-lite": "^0.6.3", "sax": "^1.2.4" }, "bin": { "needle": "bin/needle" } }, "sha512-jaQyPKKk2YokHrEg+vFDYxXIHTCBgiZwSHOoVx/8V3GIBS8/VN6NdVRmg8q1ERtPkMvmOvebsgga4sAj5hls/w=="], @@ -1067,10 +1061,6 @@ "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], - "parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], - - "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="], - "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], @@ -1199,10 +1189,6 @@ "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], - "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], - - "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], @@ -1503,10 +1489,6 @@ "bun-types/undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], - "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "cli-highlight/yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], - "cliui/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], "cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1537,8 +1519,6 @@ "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], - "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], - "proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "qrcode/yargs": ["yargs@15.4.1", "", { "dependencies": { "cliui": "^6.0.0", "decamelize": "^1.2.0", "find-up": "^4.1.0", "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", "string-width": "^4.2.0", "which-module": "^2.0.0", "y18n": "^4.0.0", "yargs-parser": "^18.1.2" } }, "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A=="], @@ -1581,16 +1561,6 @@ "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], - "cli-highlight/chalk/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "cli-highlight/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "cli-highlight/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], - - "cli-highlight/yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "cli-highlight/yargs/yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], - "cliui/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], @@ -1633,18 +1603,6 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - "cli-highlight/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - - "cli-highlight/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "cli-highlight/yargs/cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], - - "cli-highlight/yargs/string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "cli-highlight/yargs/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "cli-highlight/yargs/string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - "eslint/chalk/supports-color/has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], "qrcode/yargs/cliui/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], @@ -1661,12 +1619,6 @@ "yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "cli-highlight/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "cli-highlight/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "cli-highlight/yargs/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "qrcode/yargs/cliui/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "qrcode/yargs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], diff --git a/package.json b/package.json index ba0cfac106..b322b79753 100644 --- a/package.json +++ b/package.json @@ -118,7 +118,6 @@ "chalk": "6.0.1", "chokidar": "4.0.3", "cli-boxes": "4.0.1", - "cli-highlight": "2.1.11", "commander": "12.1.0", "cross-spawn": "7.0.6", "diff": "9.0.0", @@ -134,6 +133,7 @@ "google-auth-library": "11.3.1", "graphology": "0.26.0", "graphology-metrics": "2.4.2", + "highlight.js": "10.7.3", "https-proxy-agent": "9.1.0", "ignore": "7.0.13", "indent-string": "5.0.0", diff --git a/scripts/externals.ts b/scripts/externals.ts index 75309749da..bd398d2792 100644 --- a/scripts/externals.ts +++ b/scripts/externals.ts @@ -141,7 +141,7 @@ export const INTENTIONALLY_BUNDLED: string[] = [ 'bidi-js', 'chalk', 'cli-boxes', - 'cli-highlight', + 'highlight.js', 'commander', 'emoji-regex', 'env-paths', diff --git a/src/components/HighlightedCode/Fallback.test.tsx b/src/components/HighlightedCode/Fallback.test.tsx new file mode 100644 index 0000000000..5a2523a184 --- /dev/null +++ b/src/components/HighlightedCode/Fallback.test.tsx @@ -0,0 +1,61 @@ +import { expect, spyOn, test } from 'bun:test' +import chalk from 'chalk' +import { PassThrough } from 'node:stream' +import React from 'react' +import { Ansi, createRoot } from '../../ink.js' +import { + acquireSharedMutationLock, + releaseSharedMutationLock, +} from '../../test/sharedMutationLock.js' +import { getCliHighlightPromise } from '../../utils/cliHighlight.js' +import { HighlightedCodeFallback } from './Fallback.js' + +test('remounting with colors disabled does not reuse colored highlights', async () => { + await acquireSharedMutationLock('HighlightedCode/Fallback.test.tsx') + const previousLevel = chalk.level + const stdout = new PassThrough() + const stdin = new PassThrough() + const code = 'const cachedColorLevel = 42;' + const renderAnsi = Ansi.type + // Inspect the highlighted string before Ink can strip ANSI colors itself. + const ansi = spyOn(Ansi, 'type').mockImplementation(props => renderAnsi(props)) + const root = await createRoot({ + stdout: stdout as unknown as NodeJS.WriteStream, + stdin: stdin as unknown as NodeJS.ReadStream, + patchConsole: false, + }) + let timer: ReturnType | undefined + + try { + expect(await getCliHighlightPromise()).not.toBeNull() + chalk.level = 1 + const highlighted = Promise.withResolvers() + ansi.mockImplementation(props => { + if (props.children.includes('\x1b')) highlighted.resolve() + return renderAnsi(props) + }) + root.render() + await Promise.race([ + highlighted.promise, + new Promise((_, reject) => { + timer = setTimeout(() => reject(new Error('Highlighting did not complete')), 2500) + }), + ]) + expect(ansi.mock.calls.at(-1)![0].children).toContain('\x1b') + + chalk.level = 0 + ansi.mockClear() + root.render() + const output = ansi.mock.calls.at(-1)![0].children + expect(output).toBe(code) + expect(output).not.toContain('\x1b') + } finally { + clearTimeout(timer) + root.unmount() + ansi.mockRestore() + stdout.end() + stdin.end() + chalk.level = previousLevel + releaseSharedMutationLock() + } +}) diff --git a/src/components/HighlightedCode/Fallback.tsx b/src/components/HighlightedCode/Fallback.tsx index 4258077345..1457ca7c86 100644 --- a/src/components/HighlightedCode/Fallback.tsx +++ b/src/components/HighlightedCode/Fallback.tsx @@ -1,4 +1,5 @@ import { c as _c } from "react-compiler-runtime"; +import chalk from 'chalk'; import { extname } from 'path'; import React, { Suspense, use, useMemo } from 'react'; import { Ansi, Text } from '../../ink.js'; @@ -15,11 +16,11 @@ type Props = { // Module-level highlight cache — hl.highlight() is the hot cost on virtual- // scroll remounts. useMemo doesn't survive unmount→remount. Keyed by hash -// of code+language to avoid retaining full source strings (#24180 RSS fix). +// of code+language and Chalk level to avoid retaining full source strings (#24180 RSS fix). const HL_CACHE_MAX = 500; const hlCache = new Map(); function cachedHighlight(hl: NonNullable>>, code: string, language: string): string { - const key = hashPair(language, code); + const key = `${chalk.level}:${hashPair(language, code)}`; const hit = hlCache.get(key); if (hit !== undefined) { hlCache.delete(key); diff --git a/src/ink/Ansi.tsx b/src/ink/Ansi.tsx index c397b058fd..46c41ab546 100644 --- a/src/ink/Ansi.tsx +++ b/src/ink/Ansi.tsx @@ -25,7 +25,7 @@ type SpanProps = { * Component that parses ANSI escape codes and renders them using Text components. * * Use this as an escape hatch when you have pre-formatted ANSI strings from - * external tools (like cli-highlight) that need to be rendered in Ink. + * syntax highlighting that need to be rendered in Ink. * * Memoized to prevent re-renders when parent changes but children string is the same. */ diff --git a/src/native-ts/color-diff/index.ts b/src/native-ts/color-diff/index.ts index 2d81f27387..e582e54490 100644 --- a/src/native-ts/color-diff/index.ts +++ b/src/native-ts/color-diff/index.ts @@ -2,8 +2,8 @@ * Pure TypeScript port of vendor/color-diff-src. * * The Rust version uses syntect+bat for syntax highlighting and the similar - * crate for word diffing. This port uses highlight.js (already a dep via - * cli-highlight) and the diff npm package's diffArrays. + * crate for word diffing. This port uses highlight.js and the diff npm + * package's diffArrays. * * API matches vendor/color-diff-src/index.d.ts exactly so callers don't change. * diff --git a/src/test/fixtures/cliHighlight.baseline.json b/src/test/fixtures/cliHighlight.baseline.json new file mode 100644 index 0000000000..50d9d55487 --- /dev/null +++ b/src/test/fixtures/cliHighlight.baseline.json @@ -0,0 +1,80 @@ +[ + { + "name": "javascript", + "language": "javascript", + "code": "const value = true;\nconsole.log(\"<&> \\\"quotes\\\"\", 42); // comment\nfunction sum(a) { return /ab+/g.test(a); }", + "output": "\u001b[34mconst\u001b[39m value = \u001b[34mtrue\u001b[39m;\n\u001b[36mconsole\u001b[39m.log(\u001b[31m\"<&> \\\"quotes\\\"\"\u001b[39m, \u001b[32m42\u001b[39m); \u001b[32m// comment\u001b[39m\n\u001b[33m\u001b[34mfunction\u001b[39m\u001b[33m sum(a) \u001b[39m{ \u001b[34mreturn\u001b[39m \u001b[31m/ab+/g\u001b[39m.test(a); }" + }, + { + "name": "typescript", + "language": "ts", + "code": "interface Result { name: string; count: number }\nclass Example { value: Result | null = null; }", + "output": "\u001b[34minterface\u001b[39m Result { name: \u001b[36mstring\u001b[39m; count: \u001b[36mnumber\u001b[39m }\n\u001b[34m\u001b[34mclass\u001b[39m\u001b[34m Example \u001b[39m{ value: Result | \u001b[34mnull\u001b[39m = \u001b[34mnull\u001b[39m; }" + }, + { + "name": "json", + "language": "json", + "code": "{\"key\": \"value\", \"count\": 42, \"ok\": true}", + "output": "{\u001b[36m\"key\"\u001b[39m: \u001b[31m\"value\"\u001b[39m, \u001b[36m\"count\"\u001b[39m: \u001b[32m42\u001b[39m, \u001b[36m\"ok\"\u001b[39m: \u001b[34mtrue\u001b[39m}" + }, + { + "name": "markup", + "language": "xml", + "code": "
    \">hello & world
    ", + "output": "\u001b[90m<\u001b[34msection\u001b[39m\u001b[90m \u001b[36mtitle\u001b[39m\u001b[90m=\u001b[31m\"<&>\"\u001b[39m\u001b[90m>\u001b[39mhello & world\u001b[90m\u001b[39m" + }, + { + "name": "sublanguages", + "language": "xml", + "code": "\n", + "output": "\u001b[90m<\u001b[34mscript\u001b[39m\u001b[90m>\u001b[39m\u001b[34mconst\u001b[39m n = \u001b[32m2\u001b[39m;\u001b[90m\u001b[39m\n\u001b[90m<\u001b[34mstyle\u001b[39m\u001b[90m>\u001b[39m.item { color: red; }\u001b[90m\u001b[39m" + }, + { + "name": "templates", + "language": "javascript", + "code": "const value = `hello ${Math.max(1, 2)}`;", + "output": "\u001b[34mconst\u001b[39m value = \u001b[31m`hello ${\u001b[36mMath\u001b[39m\u001b[31m.max(\u001b[32m1\u001b[39m\u001b[31m, \u001b[32m2\u001b[39m\u001b[31m)}`\u001b[39m;" + }, + { + "name": "markdown styles", + "language": "markdown", + "code": "# Heading\n**bold** *italic* [label](target)\n> quote\n- list\n`code`", + "output": "# Heading\n\u001b[1m**bold**\u001b[22m \u001b[3m*italic*\u001b[23m [\u001b[31mlabel\u001b[39m](\u001b[4mtarget\u001b[24m)\n> quote\n- list\n`code`" + }, + { + "name": "diff colors", + "language": "diff", + "code": "@@ -1 +1 @@\n-removed\n+added", + "output": "@@ -1 +1 @@\n\u001b[31m-removed\u001b[39m\n\u001b[32m+added\u001b[39m" + }, + { + "name": "meta", + "language": "cpp", + "code": "#include \n#define COUNT 12\nint main() { return 0; }", + "output": "\u001b[90m#\u001b[90minclude\u001b[39m\u001b[90m \u001b[90m\u001b[39m\u001b[90m\u001b[39m\n\u001b[90m#\u001b[90mdefine\u001b[39m\u001b[90m COUNT 12\u001b[39m\n\u001b[33m\u001b[34mint\u001b[39m\u001b[33m main() \u001b[39m{ \u001b[34mreturn\u001b[39m \u001b[32m0\u001b[39m; }" + }, + { + "name": "type style", + "language": "haskell", + "code": "value :: Int\nvalue = 42", + "output": "value :: \u001b[36m\u001b[2mInt\u001b[22m\u001b[39m\nvalue = \u001b[32m42\u001b[39m" + }, + { + "name": "plaintext escapes", + "language": "plaintext", + "code": " & ' > ' 😀\n", + "output": " & ' > ' 😀\n" + }, + { + "name": "line endings", + "language": "plaintext", + "code": "first\r\nsecond\rthird\u0000last\n", + "output": "first\nsecond\nthirdlast\n" + }, + { + "name": "empty", + "language": "plaintext", + "code": "", + "output": "" + } +] diff --git a/src/utils/cliHighlight.test.ts b/src/utils/cliHighlight.test.ts new file mode 100644 index 0000000000..a49b50e322 --- /dev/null +++ b/src/utils/cliHighlight.test.ts @@ -0,0 +1,63 @@ +import { afterEach, beforeEach, describe, expect, test } from 'bun:test' +import chalk from 'chalk' +import { stripVTControlCharacters } from 'node:util' +import baseline from '../test/fixtures/cliHighlight.baseline.json' +import { getCliHighlightPromise, getLanguageName, type CliHighlight } from './cliHighlight.js' +import { applyMarkdown } from './markdown.js' + +let previousLevel: typeof chalk.level +let highlighter: CliHighlight + +beforeEach(async () => { + previousLevel = chalk.level + chalk.level = 1 + const loaded = await getCliHighlightPromise() + expect(loaded).not.toBeNull() + highlighter = loaded! +}) + +afterEach(() => { chalk.level = previousLevel }) + +// Captured from the previous renderer at color level 1. Compare ANSI bytes, +// including nested color restoration, rather than only the visible text. +describe('terminal syntax output compatibility', () => { + for (const fixture of baseline) { + test(fixture.name, () => { + expect(highlighter.highlight(fixture.code, { language: fixture.language })) + .toBe(fixture.output) + }) + } + + test('disabled colors preserve visible output', () => { + chalk.level = 0 + for (const fixture of baseline) { + expect(highlighter.highlight(fixture.code, { language: fixture.language })) + .toBe(stripVTControlCharacters(fixture.output)) + } + }) + + test('aliases and telemetry use the shared language registry', async () => { + expect(getCliHighlightPromise()).toBe(getCliHighlightPromise()) + expect(highlighter.supportsLanguage('ts')).toBe(true) + expect(highlighter.supportsLanguage('unsupported-language')).toBe(false) + expect(await getLanguageName('example.ts')).toBe('TypeScript') + expect(await getLanguageName('example.unsupported-language')).toBe('unknown') + expect(await getLanguageName('README')).toBe('unknown') + }) + + test('Markdown keeps the syntax palette in dark and light themes', () => { + const fixture = baseline.find(item => item.name === 'json')! + for (const theme of ['dark', 'light'] as const) { + expect(applyMarkdown('```json\n' + fixture.code + '\n```', theme, highlighter)) + .toBe(fixture.output) + } + }) + + test('unknown and missing fenced languages fall back to plaintext', () => { + const code = 'const value = "<&>"; // keep plain' + for (const language of ['unsupported-language', '']) { + expect(applyMarkdown('```' + language + '\n' + code + '\n```', 'dark', highlighter)) + .toBe(code) + } + }) +}) diff --git a/src/utils/cliHighlight.ts b/src/utils/cliHighlight.ts index 70504fa04a..086782aa80 100644 --- a/src/utils/cliHighlight.ts +++ b/src/utils/cliHighlight.ts @@ -1,34 +1,64 @@ -// highlight.js's type defs carry `/// `. SSETransport, -// mcp/client, ssh, dumpPrompts use DOM types (TextDecodeOptions, RequestInfo) -// that only typecheck because this file's `typeof import('highlight.js')` pulls -// lib.dom in. tsconfig has lib: ["ESNext"] only — fixing the actual DOM-type -// deps is a separate sweep; this ref preserves the status quo. -/// - +import chalk from 'chalk' import { extname } from 'path' export type CliHighlight = { - highlight: typeof import('cli-highlight').highlight - supportsLanguage: typeof import('cli-highlight').supportsLanguage + highlight: (code: string, options: { language: string }) => string + supportsLanguage: (name: string) => boolean +} + +// Preserve the terminal palette used by the code and Markdown renderers. +// Unlisted tokens, including titles, parameters and substitutions, are plain. +const tokenStyles = new Map string>([ + ['keyword', chalk.blue], ['literal', chalk.blue], ['class', chalk.blue], + ['built_in', chalk.cyan], ['type', chalk.cyan.dim], ['attr', chalk.cyan], + ['number', chalk.green], ['comment', chalk.green], ['doctag', chalk.green], + ['addition', chalk.green], ['regexp', chalk.red], ['string', chalk.red], + ['deletion', chalk.red], ['function', chalk.yellow], ['meta', chalk.gray], + ['tag', chalk.gray], ['name', chalk.blue], ['emphasis', chalk.italic], + ['strong', chalk.bold], ['link', chalk.underline], +]) + +const entities: Record = { + '&': '&', '<': '<', '>': '>', '"': '"', ''': "'", +} + +// Only consume highlight.js's generated spans and its five escaped entities. +// Source markup is escaped by highlight.js, so it cannot become a span here. +// A stack preserves nested styles and unprefixed sublanguage spans without a +// general HTML parser. Keep the previous renderer's CR/LF and NUL handling. +function terminalHighlight(html: string): string { + const stack: { token?: string; text: string }[] = [{ text: '' }] + for (const part of html.replace(/\r\n?/g, '\n').replace(/\0/g, '') + .split(/(|<\/span>)/g)) { + if (part.startsWith('