From fc4439ce03aa22dfaf1e3ff74fa8eae0401b60b9 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 6 Apr 2026 22:27:32 +0000 Subject: [PATCH] refactoring continued --- .../blobDegradationProcessor.test.ts | 21 ++++++---- .../processors/blobDegradationProcessor.ts | 14 +++---- .../semanticCompressionProcessor.ts | 2 +- .../processors/toolMaskingProcessor.test.ts | 19 +++++----- .../processors/toolMaskingProcessor.ts | 16 ++++---- .../src/context/sidecar/SidecarLoader.test.ts | 38 +++++++------------ .../core/src/context/sidecar/SidecarLoader.ts | 18 +++++---- .../core/src/context/sidecar/environment.ts | 33 +++++++++------- .../src/context/sidecar/environmentImpl.ts | 10 +++++ .../core/src/context/system/IFileSystem.ts | 6 ++- .../src/context/system/InMemoryFileSystem.ts | 28 ++++++++------ .../core/src/context/system/NodeFileSystem.ts | 17 ++++++++- .../src/context/testing/contextTestUtils.ts | 5 +++ 13 files changed, 130 insertions(+), 97 deletions(-) diff --git a/packages/core/src/context/processors/blobDegradationProcessor.test.ts b/packages/core/src/context/processors/blobDegradationProcessor.test.ts index 9c57095113..0a5b31187b 100644 --- a/packages/core/src/context/processors/blobDegradationProcessor.test.ts +++ b/packages/core/src/context/processors/blobDegradationProcessor.test.ts @@ -9,17 +9,19 @@ import { BlobDegradationProcessor } from './blobDegradationProcessor.js'; import type { Episode, UserPrompt } from '../ir/types.js'; import type { ContextAccountingState } from '../pipeline.js'; import { randomUUID } from 'node:crypto'; -import * as fsPromises from 'node:fs/promises'; - -vi.mock('node:fs/promises'); +import type { ContextEnvironment } from '../sidecar/environment.js'; +import { InMemoryFileSystem } from '../system/InMemoryFileSystem.js'; describe('BlobDegradationProcessor', () => { let processor: BlobDegradationProcessor; + let env: ContextEnvironment; + let fileSystem: InMemoryFileSystem; beforeEach(() => { vi.resetAllMocks(); - - processor = new BlobDegradationProcessor(createMockEnvironment()); + env = createMockEnvironment(); + fileSystem = env.fileSystem as InMemoryFileSystem; + processor = new BlobDegradationProcessor(env); }); const getDummyState = ( @@ -61,7 +63,6 @@ describe('BlobDegradationProcessor', () => { steps: [], }; - // Fake token calculator says inlineData costs 258 tokens, text costs 10 const state = getDummyState(false, 500, new Set()); const result = await processor.process([ep], state); @@ -79,7 +80,11 @@ describe('BlobDegradationProcessor', () => { 'degraded to text to preserve context window', ); - expect(fsPromises.writeFile).toHaveBeenCalledTimes(1); + // Verify it was written to fake FS + expect(fileSystem.getFiles().size).toBeGreaterThan(0); + const files = Array.from(fileSystem.getFiles().keys()); + expect(files[0]).toContain('.gemini/tool-outputs/degraded-blobs/session-mock-session/blob_'); + expect(result[0].trigger.metadata.transformations.length).toBe(1); }); @@ -118,6 +123,6 @@ describe('BlobDegradationProcessor', () => { 'Original URI: gs://fake-bucket/doc.pdf', ); - expect(fsPromises.writeFile).not.toHaveBeenCalled(); + expect(fileSystem.getFiles().size).toBe(0); }); }); diff --git a/packages/core/src/context/processors/blobDegradationProcessor.ts b/packages/core/src/context/processors/blobDegradationProcessor.ts index 11a8e26b98..93b9305064 100644 --- a/packages/core/src/context/processors/blobDegradationProcessor.ts +++ b/packages/core/src/context/processors/blobDegradationProcessor.ts @@ -8,8 +8,6 @@ import type { ContextAccountingState, ContextProcessor } from '../pipeline.js'; import type { ContextEnvironment } from '../sidecar/environment.js'; import { sanitizeFilenamePart } from '../../utils/fileUtils.js'; -import * as fsPromises from 'node:fs/promises'; -import path from 'node:path'; import type { Part } from '@google/genai'; export class BlobDegradationProcessor implements ContextProcessor { @@ -32,13 +30,13 @@ export class BlobDegradationProcessor implements ContextProcessor { const newEpisodes = [...episodes]; let directoryCreated = false; - let blobOutputsDir = path.join( + let blobOutputsDir = this.env.fileSystem.join( this.env.projectTempDir, 'degraded-blobs', ); const sessionId = this.env.sessionId; if (sessionId) { - blobOutputsDir = path.join( + blobOutputsDir = this.env.fileSystem.join( blobOutputsDir, `session-${sanitizeFilenamePart(sessionId)}`, ); @@ -46,7 +44,7 @@ export class BlobDegradationProcessor implements ContextProcessor { const ensureDir = async () => { if (!directoryCreated) { - await fsPromises.mkdir(blobOutputsDir, { recursive: true }); + await this.env.fileSystem.mkdir(blobOutputsDir, { recursive: true }); directoryCreated = true; } }; @@ -69,12 +67,12 @@ export class BlobDegradationProcessor implements ContextProcessor { if (part.type === 'inline_data') { await ensureDir(); const ext = part.mimeType.split('/')[1] || 'bin'; - const fileName = `blob_${Date.now()}_${Math.random().toString(36).substring(7)}.${ext}`; - const filePath = path.join(blobOutputsDir, fileName); + const fileName = `blob_${Date.now()}_${this.env.idGenerator.generateId()}.${ext}`; + const filePath = this.env.fileSystem.join(blobOutputsDir, fileName); // Base64 to buffer const buffer = Buffer.from(part.data, 'base64'); - await fsPromises.writeFile(filePath, buffer); + await this.env.fileSystem.writeFile(filePath, buffer); const mb = (buffer.byteLength / 1024 / 1024).toFixed(2); newText = `[Multi-Modal Blob (${part.mimeType}, ${mb}MB) degraded to text to preserve context window. Saved to: ${filePath}]`; diff --git a/packages/core/src/context/processors/semanticCompressionProcessor.ts b/packages/core/src/context/processors/semanticCompressionProcessor.ts index f82f2c6652..79fe3ec8bb 100644 --- a/packages/core/src/context/processors/semanticCompressionProcessor.ts +++ b/packages/core/src/context/processors/semanticCompressionProcessor.ts @@ -37,7 +37,7 @@ export class SemanticCompressionProcessor implements ContextProcessor { const semanticConfig = this.options; const limitTokens = semanticConfig.nodeThresholdTokens; - const thresholdChars = limitTokens * this.env.charsPerToken; + const thresholdChars = this.env.tokenCalculator.tokensToChars(limitTokens); this.modelToUse = 'gemini-2.5-flash'; let currentDeficit = state.deficitTokens; diff --git a/packages/core/src/context/processors/toolMaskingProcessor.test.ts b/packages/core/src/context/processors/toolMaskingProcessor.test.ts index 6ba240710b..c63dbf40af 100644 --- a/packages/core/src/context/processors/toolMaskingProcessor.test.ts +++ b/packages/core/src/context/processors/toolMaskingProcessor.test.ts @@ -10,17 +10,20 @@ import { ToolMaskingProcessor } from './toolMaskingProcessor.js'; import type { Episode, ToolExecution } from '../ir/types.js'; import type { ContextAccountingState } from '../pipeline.js'; import { randomUUID } from 'node:crypto'; -import * as fsPromises from 'node:fs/promises'; - -vi.mock('node:fs/promises'); +import type { ContextEnvironment } from '../sidecar/environment.js'; +import { InMemoryFileSystem } from '../system/InMemoryFileSystem.js'; describe('ToolMaskingProcessor', () => { let processor: ToolMaskingProcessor; + let env: ContextEnvironment; + let fileSystem: InMemoryFileSystem; beforeEach(() => { vi.resetAllMocks(); + env = createMockEnvironment(); + fileSystem = env.fileSystem as InMemoryFileSystem; - processor = new ToolMaskingProcessor(createMockEnvironment(), { + processor = new ToolMaskingProcessor(env, { stringLengthThresholdTokens: 100, }); }); @@ -76,10 +79,6 @@ describe('ToolMaskingProcessor', () => { const state = getDummyState(true); const result = await processor.process(episodes, state); - require('fs').appendFileSync( - '/tmp/debug.json', - '\n\n' + JSON.stringify({ res: result[0].steps[0] }, null, 2), - ); expect(result).toStrictEqual(episodes); expect((result[0].steps[0] as ToolExecution).presentation).toBeUndefined(); @@ -124,7 +123,7 @@ describe('ToolMaskingProcessor', () => { ); expect((maskedObs as { error: string }).error).toBeNull(); - // Check disk writes occurred - expect(fsPromises.writeFile).toHaveBeenCalledTimes(2); + // Check disk writes occurred to fake FS + expect(fileSystem.getFiles().size).toBe(2); }); }); diff --git a/packages/core/src/context/processors/toolMaskingProcessor.ts b/packages/core/src/context/processors/toolMaskingProcessor.ts index cc560392cd..6331dab2a5 100644 --- a/packages/core/src/context/processors/toolMaskingProcessor.ts +++ b/packages/core/src/context/processors/toolMaskingProcessor.ts @@ -8,8 +8,6 @@ import type { ContextAccountingState, ContextProcessor } from '../pipeline.js'; import type { ContextEnvironment } from '../sidecar/environment.js'; import { sanitizeFilenamePart } from '../../utils/fileUtils.js'; -import * as fsPromises from 'node:fs/promises'; -import path from 'node:path'; import { ACTIVATE_SKILL_TOOL_NAME, MEMORY_TOOL_NAME, @@ -50,15 +48,15 @@ export class ToolMaskingProcessor implements ContextProcessor { const newEpisodes = [...episodes]; let currentDeficit = state.deficitTokens; - const limitChars = maskingConfig.stringLengthThresholdTokens * this.env.charsPerToken; + const limitChars = this.env.tokenCalculator.tokensToChars(maskingConfig.stringLengthThresholdTokens); - let toolOutputsDir = path.join( + let toolOutputsDir = this.env.fileSystem.join( this.env.projectTempDir, 'tool-outputs', ); const sessionId = this.env.sessionId; if (sessionId) { - toolOutputsDir = path.join( + toolOutputsDir = this.env.fileSystem.join( toolOutputsDir, `session-${sanitizeFilenamePart(sessionId)}`, ); @@ -75,14 +73,14 @@ export class ToolMaskingProcessor implements ContextProcessor { nodeType: string, ): Promise => { if (!directoryCreated) { - await fsPromises.mkdir(toolOutputsDir, { recursive: true }); + await this.env.fileSystem.mkdir(toolOutputsDir, { recursive: true }); directoryCreated = true; } - const fileName = `${sanitizeFilenamePart(toolName).toLowerCase()}_${sanitizeFilenamePart(callId).toLowerCase()}_${nodeType}_${Math.random().toString(36).substring(7)}.txt`; - const filePath = path.join(toolOutputsDir, fileName); + const fileName = `${sanitizeFilenamePart(toolName).toLowerCase()}_${sanitizeFilenamePart(callId).toLowerCase()}_${nodeType}_${this.env.idGenerator.generateId()}.txt`; + const filePath = this.env.fileSystem.join(toolOutputsDir, fileName); - await fsPromises.writeFile(filePath, content, 'utf-8'); + await this.env.fileSystem.writeFile(filePath, content); const fileSizeMB = ( Buffer.byteLength(content, 'utf8') / diff --git a/packages/core/src/context/sidecar/SidecarLoader.test.ts b/packages/core/src/context/sidecar/SidecarLoader.test.ts index 8d1fd2d8eb..76c3f98b87 100644 --- a/packages/core/src/context/sidecar/SidecarLoader.test.ts +++ b/packages/core/src/context/sidecar/SidecarLoader.test.ts @@ -1,13 +1,13 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest'; -import * as fs from 'node:fs'; +import { describe, it, expect, beforeEach } from 'vitest'; import { SidecarLoader } from './SidecarLoader.js'; import { defaultSidecarProfile } from './profiles.js'; +import { InMemoryFileSystem } from '../system/InMemoryFileSystem.js'; -vi.mock('node:fs'); +describe('SidecarLoader (Fake FS)', () => { + let fileSystem: InMemoryFileSystem; -describe('SidecarLoader', () => { beforeEach(() => { - vi.resetAllMocks(); + fileSystem = new InMemoryFileSystem(); }); const mockConfig = { @@ -15,48 +15,38 @@ describe('SidecarLoader', () => { } as any; it('returns default profile if file does not exist', () => { - vi.mocked(fs.existsSync).mockReturnValue(false); - const result = SidecarLoader.fromConfig(mockConfig); + const result = SidecarLoader.fromConfig(mockConfig, fileSystem); expect(result).toBe(defaultSidecarProfile); }); it('returns default profile if file exists but is 0 bytes', () => { - vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.statSync).mockReturnValue({ size: 0 } as any); - const result = SidecarLoader.fromConfig(mockConfig); + fileSystem.setFile('/path/to/sidecar.json', ''); + const result = SidecarLoader.fromConfig(mockConfig, fileSystem); expect(result).toBe(defaultSidecarProfile); }); it('throws an error if file is empty whitespace', () => { - vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.statSync).mockReturnValue({ size: 5 } as any); - vi.mocked(fs.readFileSync).mockReturnValue(' \n '); - - expect(() => SidecarLoader.fromConfig(mockConfig)).toThrow('is empty'); + fileSystem.setFile('/path/to/sidecar.json', ' \n '); + expect(() => SidecarLoader.fromConfig(mockConfig, fileSystem)).toThrow('is empty'); }); it('returns parsed config if file is valid', () => { - vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.statSync).mockReturnValue({ size: 100 } as any); const validConfig = { budget: { retainedTokens: 1000, maxTokens: 2000 }, gcBackstop: { strategy: 'truncate', target: 'max' }, pipelines: [] }; - vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(validConfig)); - const result = SidecarLoader.fromConfig(mockConfig); + fileSystem.setFile('/path/to/sidecar.json', JSON.stringify(validConfig)); + const result = SidecarLoader.fromConfig(mockConfig, fileSystem); expect(result).toEqual(validConfig); }); it('throws an error if schema validation fails', () => { - vi.mocked(fs.existsSync).mockReturnValue(true); - vi.mocked(fs.statSync).mockReturnValue({ size: 100 } as any); const invalidConfig = { budget: { retainedTokens: "invalid string" }, // Invalid type pipelines: [] }; - vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(invalidConfig)); - - expect(() => SidecarLoader.fromConfig(mockConfig)).toThrow('Validation error:'); + fileSystem.setFile('/path/to/sidecar.json', JSON.stringify(invalidConfig)); + expect(() => SidecarLoader.fromConfig(mockConfig, fileSystem)).toThrow('Validation error:'); }); }); diff --git a/packages/core/src/context/sidecar/SidecarLoader.ts b/packages/core/src/context/sidecar/SidecarLoader.ts index 4fa66b8ed2..123aa133c7 100644 --- a/packages/core/src/context/sidecar/SidecarLoader.ts +++ b/packages/core/src/context/sidecar/SidecarLoader.ts @@ -4,19 +4,21 @@ * SPDX-License-Identifier: Apache-2.0 */ -import * as fs from 'node:fs'; import type { Config } from '../../config/config.js'; import type { SidecarConfig } from './types.js'; import { defaultSidecarProfile } from './profiles.js'; import { SchemaValidator } from '../../utils/schemaValidator.js'; import { sidecarConfigSchema } from './schema.js'; +import type { IFileSystem } from '../system/IFileSystem.js'; +import { NodeFileSystem } from '../system/NodeFileSystem.js'; + export class SidecarLoader { /** * Loads and validates a sidecar config from a specific file path. * Throws an error if the file cannot be read, parsed, or fails schema validation. */ - static loadFromFile(sidecarPath: string): SidecarConfig { - const fileContent = fs.readFileSync(sidecarPath, 'utf8'); + static loadFromFile(sidecarPath: string, fileSystem: IFileSystem = new NodeFileSystem()): SidecarConfig { + const fileContent = fileSystem.readFileSync(sidecarPath, 'utf8'); if (!fileContent.trim()) { throw new Error(`Sidecar configuration file at ${sidecarPath} is empty.`); @@ -49,18 +51,18 @@ export class SidecarLoader { * Generates a Sidecar JSON graph from the experimental config file path or defaults. * If a config file is present but invalid, this will THROW to prevent silent misconfiguration. */ - static fromConfig(config: Config): SidecarConfig { + static fromConfig(config: Config, fileSystem: IFileSystem = new NodeFileSystem()): SidecarConfig { const sidecarPath = config.getExperimentalContextSidecarConfig(); - if (sidecarPath && fs.existsSync(sidecarPath)) { - const stat = fs.statSync(sidecarPath); + if (sidecarPath && fileSystem.existsSync(sidecarPath)) { + const size = fileSystem.statSyncSize(sidecarPath); // If the file exists but is completely empty (0 bytes), it's safe to fallback. - if (stat.size === 0) { + if (size === 0) { return defaultSidecarProfile; } // If the file has content, enforce strict validation and throw on failure. - return this.loadFromFile(sidecarPath); + return this.loadFromFile(sidecarPath, fileSystem); } return defaultSidecarProfile; diff --git a/packages/core/src/context/sidecar/environment.ts b/packages/core/src/context/sidecar/environment.ts index a113fe707f..369ae93933 100644 --- a/packages/core/src/context/sidecar/environment.ts +++ b/packages/core/src/context/sidecar/environment.ts @@ -4,20 +4,25 @@ * SPDX-License-Identifier: Apache-2.0 */ import type { BaseLlmClient } from '../../core/baseLlmClient.js'; - import type { ContextTracer } from '../tracer.js'; - import type { ContextEventBus } from '../eventBus.js'; +import type { ContextEventBus } from '../eventBus.js'; import type { ContextTokenCalculator } from '../utils/contextTokenCalculator.js'; - export type { ContextTracer, ContextEventBus }; +import type { ContextTracer } from '../tracer.js'; +import type { IFileSystem } from '../system/IFileSystem.js'; +import type { IIdGenerator } from '../system/IIdGenerator.js'; - export interface ContextEnvironment { - readonly llmClient: BaseLlmClient; - readonly promptId: string; - readonly sessionId: string; - readonly traceDir: string; - readonly projectTempDir: string; - readonly tracer: ContextTracer; - readonly charsPerToken: number; - readonly tokenCalculator: ContextTokenCalculator; - - eventBus: ContextEventBus; +export type { ContextTracer, ContextEventBus }; + +export interface ContextEnvironment { + readonly llmClient: BaseLlmClient; + readonly promptId: string; + readonly sessionId: string; + readonly traceDir: string; + readonly projectTempDir: string; + readonly tracer: ContextTracer; + readonly charsPerToken: number; + readonly tokenCalculator: ContextTokenCalculator; + readonly fileSystem: IFileSystem; + readonly idGenerator: IIdGenerator; + + eventBus: ContextEventBus; } diff --git a/packages/core/src/context/sidecar/environmentImpl.ts b/packages/core/src/context/sidecar/environmentImpl.ts index 52bf9dce0b..db62b5aa18 100644 --- a/packages/core/src/context/sidecar/environmentImpl.ts +++ b/packages/core/src/context/sidecar/environmentImpl.ts @@ -11,9 +11,15 @@ import type { ContextEnvironment } from './environment.js'; import type { ContextEventBus } from '../eventBus.js'; import { ContextTokenCalculator } from '../utils/contextTokenCalculator.js'; +import type { IFileSystem } from '../system/IFileSystem.js'; +import { NodeFileSystem } from '../system/NodeFileSystem.js'; +import type { IIdGenerator } from '../system/IIdGenerator.js'; +import { NodeIdGenerator } from '../system/NodeIdGenerator.js'; export class ContextEnvironmentImpl implements ContextEnvironment { public readonly tokenCalculator: ContextTokenCalculator; + public readonly fileSystem: IFileSystem; + public readonly idGenerator: IIdGenerator; constructor( public readonly llmClient: BaseLlmClient, @@ -24,7 +30,11 @@ export class ContextEnvironmentImpl implements ContextEnvironment { public readonly tracer: ContextTracer, public readonly charsPerToken: number, public readonly eventBus: ContextEventBus, + fileSystem?: IFileSystem, + idGenerator?: IIdGenerator, ) { this.tokenCalculator = new ContextTokenCalculator(this.charsPerToken); + this.fileSystem = fileSystem || new NodeFileSystem(); + this.idGenerator = idGenerator || new NodeIdGenerator(); } } diff --git a/packages/core/src/context/system/IFileSystem.ts b/packages/core/src/context/system/IFileSystem.ts index ab53eccc40..bb5ede4054 100644 --- a/packages/core/src/context/system/IFileSystem.ts +++ b/packages/core/src/context/system/IFileSystem.ts @@ -8,9 +8,13 @@ export interface IFileSystem { existsSync(path: string): boolean; statSyncSize(path: string): number; readFileSync(path: string, encoding: 'utf8'): string; - writeFileSync(path: string, data: string, encoding: 'utf-8'): void; + writeFileSync(path: string, data: string | Buffer, encoding?: 'utf-8'): void; appendFileSync(path: string, data: string, encoding: 'utf-8'): void; mkdirSync(path: string, options?: { recursive?: boolean }): void; + + writeFile(path: string, data: string | Buffer): Promise; + mkdir(path: string, options?: { recursive?: boolean }): Promise; + join(...paths: string[]): string; dirname(path: string): string; } diff --git a/packages/core/src/context/system/InMemoryFileSystem.ts b/packages/core/src/context/system/InMemoryFileSystem.ts index 5bd713b525..7b6b4886bc 100644 --- a/packages/core/src/context/system/InMemoryFileSystem.ts +++ b/packages/core/src/context/system/InMemoryFileSystem.ts @@ -7,21 +7,17 @@ import type { IFileSystem } from './IFileSystem.js'; export class InMemoryFileSystem implements IFileSystem { - private files = new Map(); + private files = new Map(); - // Helper for tests - getFiles(): ReadonlyMap { + getFiles(): ReadonlyMap { return this.files; } - // Helper for tests - setFile(path: string, content: string) { + setFile(path: string, content: string | Buffer) { this.files.set(this.normalize(path), content); } private normalize(p: string): string { - // A very naive normalization for testing purposes. - // Ensures '/foo/bar' and '/foo//bar' map to the same key. return p.replace(/\/+/g, '/'); } @@ -34,7 +30,7 @@ export class InMemoryFileSystem implements IFileSystem { if (content === undefined) { throw new Error(`ENOENT: no such file or directory, stat '${p}'`); } - return content.length; // Naive char length = byte size for testing + return Buffer.isBuffer(content) ? content.byteLength : Buffer.byteLength(content, 'utf8'); } readFileSync(p: string, encoding: 'utf8'): string { @@ -42,23 +38,31 @@ export class InMemoryFileSystem implements IFileSystem { if (content === undefined) { throw new Error(`ENOENT: no such file or directory, open '${p}'`); } + if (Buffer.isBuffer(content)) { + return content.toString(encoding); + } return content; } - writeFileSync(p: string, data: string, encoding: 'utf-8'): void { + writeFileSync(p: string, data: string | Buffer, encoding?: 'utf-8'): void { this.files.set(this.normalize(p), data); } appendFileSync(p: string, data: string, encoding: 'utf-8'): void { const norm = this.normalize(p); const existing = this.files.get(norm) || ''; - this.files.set(norm, existing + data); + const existingStr = Buffer.isBuffer(existing) ? existing.toString('utf8') : existing; + this.files.set(norm, existingStr + data); } - mkdirSync(p: string, options?: { recursive?: boolean }): void { - // In-memory fake doesn't track directories separately from files for our simple use cases + mkdirSync(p: string, options?: { recursive?: boolean }): void {} + + async writeFile(p: string, data: string | Buffer): Promise { + this.writeFileSync(p, data); } + async mkdir(p: string, options?: { recursive?: boolean }): Promise {} + join(...paths: string[]): string { return this.normalize(paths.join('/')); } diff --git a/packages/core/src/context/system/NodeFileSystem.ts b/packages/core/src/context/system/NodeFileSystem.ts index 4eae7b1b90..bd455b94f5 100644 --- a/packages/core/src/context/system/NodeFileSystem.ts +++ b/packages/core/src/context/system/NodeFileSystem.ts @@ -5,6 +5,7 @@ */ import * as fs from 'node:fs'; +import * as fsPromises from 'node:fs/promises'; import * as path from 'node:path'; import type { IFileSystem } from './IFileSystem.js'; @@ -21,8 +22,12 @@ export class NodeFileSystem implements IFileSystem { return fs.readFileSync(p, encoding); } - writeFileSync(p: string, data: string, encoding: 'utf-8'): void { - fs.writeFileSync(p, data, encoding); + writeFileSync(p: string, data: string | Buffer, encoding?: 'utf-8'): void { + if (Buffer.isBuffer(data)) { + fs.writeFileSync(p, data); + } else { + fs.writeFileSync(p, data, encoding); + } } appendFileSync(p: string, data: string, encoding: 'utf-8'): void { @@ -33,6 +38,14 @@ export class NodeFileSystem implements IFileSystem { fs.mkdirSync(p, options); } + async writeFile(p: string, data: string | Buffer): Promise { + await fsPromises.writeFile(p, data); + } + + async mkdir(p: string, options?: { recursive?: boolean }): Promise { + await fsPromises.mkdir(p, options); + } + join(...paths: string[]): string { return path.join(...paths); } diff --git a/packages/core/src/context/testing/contextTestUtils.ts b/packages/core/src/context/testing/contextTestUtils.ts index a55c916a20..aa47419b19 100644 --- a/packages/core/src/context/testing/contextTestUtils.ts +++ b/packages/core/src/context/testing/contextTestUtils.ts @@ -11,6 +11,9 @@ import type { Content } from '@google/genai'; import { AgentChatHistory } from '../../core/agentChatHistory.js'; import { ContextManager } from '../contextManager.js'; +import { InMemoryFileSystem } from '../system/InMemoryFileSystem.js'; +import { DeterministicIdGenerator } from '../system/DeterministicIdGenerator.js'; + export function createMockEnvironment(): ContextEnvironment { return { // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion @@ -27,6 +30,8 @@ export function createMockEnvironment(): ContextEnvironment { tracer: new ContextTracer({ targetDir: '/tmp', sessionId: 'mock-session' }), charsPerToken: 1, tokenCalculator: new ContextTokenCalculator(1), + fileSystem: new InMemoryFileSystem(), + idGenerator: new DeterministicIdGenerator('mock-uuid-'), }; }