Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 43 additions & 28 deletions ProjectVG.Application/Models/Chat/ChatSegment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,25 @@

namespace ProjectVG.Application.Models.Chat
{
public record ChatSegment
public sealed class ChatSegment : IDisposable
{

public string Content { get; init; } = string.Empty;
public string Content { get; private set; } = string.Empty;

public int Order { get; init; }
public int Order { get; private set; }

public string? Emotion { get; init; }
public string? Emotion { get; private set; }

public List<string>? Actions { get; init; }
public List<string>? Actions { get; private set; }

public byte[]? AudioData { get; init; }
public string? AudioContentType { get; init; }
public float? AudioLength { get; init; }
public byte[]? AudioData { get; private set; }
public string? AudioContentType { get; private set; }
public float? AudioLength { get; private set; }

// 스트림 기반 음성 데이터 처리를 위한 새로운 프로퍼티
public IMemoryOwner<byte>? AudioMemoryOwner { get; init; }
public int AudioDataSize { get; init; }
// LOH 방지를 위한 ArrayPool 기반 메모리 관리
internal IMemoryOwner<byte>? AudioMemoryOwner { get; private set; }
internal int AudioDataSize { get; private set; }
private bool _disposed;



Expand All @@ -30,14 +31,13 @@ public record ChatSegment
public bool HasEmotion => !string.IsNullOrEmpty(Emotion);
public bool HasActions => Actions != null && Actions.Any();

/// <summary>
/// 메모리 효율적인 방식으로 음성 데이터에 접근합니다
/// </summary>
public ReadOnlySpan<byte> GetAudioSpan()
{
if (AudioMemoryOwner != null && AudioDataSize > 0)
{
return AudioMemoryOwner.Memory.Span.Slice(0, AudioDataSize);
var memory = AudioMemoryOwner.Memory;
var safeSize = Math.Min(AudioDataSize, memory.Length);
return memory.Span.Slice(0, safeSize);
}
if (AudioData != null)
{
Expand All @@ -48,6 +48,8 @@ public ReadOnlySpan<byte> GetAudioSpan()



private ChatSegment() { }

public static ChatSegment Create(string content, string? emotion = null, List<string>? actions = null, int order = 0)
{
return new ChatSegment
Expand All @@ -69,36 +71,50 @@ public static ChatSegment CreateAction(string action, int order = 0)
return Create("", null, new List<string> { action }, order);
}

// Method to add audio data (returns new record instance)
public ChatSegment WithAudioData(byte[] audioData, string audioContentType, float audioLength)
{
return this with
return new ChatSegment
{
Content = this.Content,
Order = this.Order,
Emotion = this.Emotion,
Actions = this.Actions,
AudioData = audioData,
AudioContentType = audioContentType,
AudioLength = audioLength
};
}

/// <summary>
/// 메모리 효율적인 방식으로 음성 데이터를 추가합니다 (LOH 방지)
/// </summary>
// 소유권 이전: 호출자는 audioMemoryOwner를 해제하지 말 것
public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audioDataSize, string audioContentType, float audioLength)
{
return this with
if (audioMemoryOwner is null)
throw new ArgumentNullException(nameof(audioMemoryOwner));

if (audioDataSize < 0 || audioDataSize > audioMemoryOwner.Memory.Length)
throw new ArgumentOutOfRangeException(
nameof(audioDataSize),
audioDataSize,
$"audioDataSize는 0 이상 {audioMemoryOwner.Memory.Length} 이하여야 합니다.");

// 기존 소유자 해제
this.AudioMemoryOwner?.Dispose();

return new ChatSegment
{
Content = this.Content,
Order = this.Order,
Emotion = this.Emotion,
Actions = this.Actions,
AudioMemoryOwner = audioMemoryOwner,
AudioDataSize = audioDataSize,
AudioContentType = audioContentType,
AudioLength = audioLength,
// 기존 AudioData는 null로 설정하여 중복 저장 방지
AudioData = null
};
}

/// <summary>
/// 음성 데이터를 배열로 변환합니다 (필요한 경우에만 사용)
/// </summary>
// 필요시만 사용 - LOH 위험 있음
public byte[]? GetAudioDataAsArray()
{
if (AudioData != null)
Expand All @@ -115,12 +131,11 @@ public ChatSegment WithAudioMemory(IMemoryOwner<byte> audioMemoryOwner, int audi
return null;
}

/// <summary>
/// 리소스 해제 (IMemoryOwner 해제)
/// </summary>
public void Dispose()
{
if (_disposed) return;
AudioMemoryOwner?.Dispose();
_disposed = true;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,14 @@ public async Task ProcessAsync(ChatProcessContext context)
var processedCount = 0;

foreach (var (idx, ttsResult) in ttsResults.OrderBy(x => x.idx)) {
if (ttsResult.Success == true && ttsResult.AudioData != null) {
if (ttsResult.Success == true && ttsResult.AudioMemoryOwner != null) {
var segment = context.Segments?[idx];
if (segment != null && context.Segments != null) {
context.Segments[idx] = segment.WithAudioData(ttsResult.AudioData, ttsResult.ContentType!, ttsResult.AudioLength ?? 0f);
context.Segments[idx] = segment.WithAudioMemory(
ttsResult.AudioMemoryOwner,
ttsResult.AudioDataSize,
ttsResult.ContentType!,
ttsResult.AudioLength ?? 0f);
}

if (ttsResult.AudioLength.HasValue) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using System.Buffers;

namespace ProjectVG.Infrastructure.Integrations.TextToSpeechClient.Models
{
Expand All @@ -17,11 +18,23 @@ public class TextToSpeechResponse
public string? ErrorMessage { get; set; }

/// <summary>
/// 오디오 데이터 (바이트 배열)
/// 오디오 데이터 (바이트 배열) - 레거시 호환성용
/// </summary>
[JsonIgnore]
public byte[]? AudioData { get; set; }

/// <summary>
/// ArrayPool 기반 오디오 메모리 소유자 (LOH 방지)
/// </summary>
[JsonIgnore]
public IMemoryOwner<byte>? AudioMemoryOwner { get; set; }

/// <summary>
/// 실제 오디오 데이터 크기
/// </summary>
[JsonIgnore]
public int AudioDataSize { get; set; }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

IMemoryOwner 소유권 명시 필요 (잠재적 누수/이중 해제 예방 가이드 추가 권장)

응답 DTO가 IMemoryOwner를 public으로 노출하므로 소유권/수명 규약을 문서화하거나 안전한 전달 API를 제공하는 편이 좋습니다. 예: 소비자가 세그먼트에 붙이지 않고 버릴 때 직접 해제해야 함을 명시하거나, 응답에서 “소유권 가져가기” 메서드를 제공해 중복 해제를 방지하세요.

아래 보조 메서드 추가를 제안드립니다(응답 → 세그먼트로 안전 이전):

// 클래스 내부에 추가
public bool TryTakeAudioOwner(out IMemoryOwner<byte>? owner, out int size)
{
    owner = AudioMemoryOwner;
    size = AudioDataSize;
    AudioMemoryOwner = null;
    AudioDataSize = 0;
    return owner != null;
}
🤖 Prompt for AI Agents
In
ProjectVG.Infrastructure/Integrations/TextToSpeechClient/Models/TextToSpeechResponse.cs
around lines 26 to 36, the DTO publicly exposes IMemoryOwner<byte> which risks
ownership confusion, leaks or double-dispose; add a clear ownership-transfer API
and docs: implement a TryTakeAudioOwner(out IMemoryOwner<byte>? owner, out int
size) method that returns the current AudioMemoryOwner and AudioDataSize, then
sets AudioMemoryOwner to null and AudioDataSize to 0 so ownership is transferred
safely, update the XML comments to state callers must dispose the returned
owner, and keep the original properties JsonIgnored and internal/private as
appropriate to prevent accidental external disposal.

Comment on lines +33 to +40
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

IMemoryOwner/크기 세터 접근 축소로 오·남용 방지

소비자가 임의로 교체하면 이중 Dispose/누수 가능성이 있습니다. 동일 어셈블리에서만 설정되도록 setter를 internal로 축소하세요.

-        public IMemoryOwner<byte>? AudioMemoryOwner { get; set; }
+        public IMemoryOwner<byte>? AudioMemoryOwner { get; internal set; }

-        public int AudioDataSize { get; set; }
+        public int AudioDataSize { get; internal set; }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[JsonIgnore]
public IMemoryOwner<byte>? AudioMemoryOwner { get; set; }
/// <summary>
/// 실제 오디오 데이터 크기
/// </summary>
[JsonIgnore]
public int AudioDataSize { get; set; }
[JsonIgnore]
public IMemoryOwner<byte>? AudioMemoryOwner { get; internal set; }
/// <summary>
/// 실제 오디오 데이터 크기
/// </summary>
[JsonIgnore]
public int AudioDataSize { get; internal set; }
🤖 Prompt for AI Agents
In
ProjectVG.Infrastructure/Integrations/TextToSpeechClient/Models/TextToSpeechResponse.cs
around lines 33 to 40, the public setters for AudioMemoryOwner and AudioDataSize
should be restricted to prevent external replacement and potential
double-dispose/leak; change both properties so only code within the same
assembly can set them (make their setters internal) while keeping their getters
public.


/// <summary>
/// 오디오 길이 (초)
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,10 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re
return voiceResponse;
}

// 스트림 기반으로 음성 데이터 읽기 (LOH 방지)
voiceResponse.AudioData = await ReadAudioDataWithPoolAsync(response.Content);
// ArrayPool 기반으로 음성 데이터 읽기 (LOH 방지)
var (memoryOwner, dataSize) = await ReadAudioDataWithPoolAsync(response.Content);
voiceResponse.AudioMemoryOwner = memoryOwner;
voiceResponse.AudioDataSize = dataSize;
voiceResponse.ContentType = response.Content.Headers.ContentType?.ToString();

if (response.Headers.Contains("X-Audio-Length"))
Expand All @@ -64,7 +66,7 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re
}

_logger.LogDebug("[TTS][Response] 오디오 길이: {AudioLength:F2}초, ContentType: {ContentType}, 바이트: {Length}, 소요시간: {Elapsed}ms",
voiceResponse.AudioLength, voiceResponse.ContentType, voiceResponse.AudioData?.Length ?? 0, elapsed);
voiceResponse.AudioLength, voiceResponse.ContentType, voiceResponse.AudioDataSize, elapsed);

return voiceResponse;
}
Expand All @@ -82,42 +84,50 @@ public async Task<TextToSpeechResponse> TextToSpeechAsync(TextToSpeechRequest re
/// <summary>
/// ArrayPool을 사용하여 스트림 기반으로 음성 데이터를 읽습니다 (LOH 할당 방지)
/// </summary>
private async Task<byte[]?> ReadAudioDataWithPoolAsync(HttpContent content)
private async Task<(IMemoryOwner<byte>?, int)> ReadAudioDataWithPoolAsync(HttpContent content)
{
const int chunkSize = 32768; // 32KB 청크 크기
byte[]? buffer = null;
byte[]? readBuffer = null;
MemoryStream? memoryStream = null;

try
{
buffer = _arrayPool.Rent(chunkSize);
readBuffer = _arrayPool.Rent(chunkSize);
memoryStream = new MemoryStream();

using var stream = await content.ReadAsStreamAsync();
int bytesRead;

// 청크 단위로 데이터 읽어서 MemoryStream에 복사
while ((bytesRead = await stream.ReadAsync(buffer, 0, chunkSize)) > 0)
while ((bytesRead = await stream.ReadAsync(readBuffer, 0, chunkSize)) > 0)
{
await memoryStream.WriteAsync(buffer, 0, bytesRead);
await memoryStream.WriteAsync(readBuffer, 0, bytesRead);
}

var result = memoryStream.ToArray();
var totalSize = (int)memoryStream.Length;

// ArrayPool에서 최종 데이터 크기만큼 메모리 할당
var resultMemoryOwner = MemoryPool<byte>.Shared.Rent(totalSize);

// MemoryStream에서 최종 메모리로 복사
memoryStream.Position = 0;
await memoryStream.ReadAsync(resultMemoryOwner.Memory.Slice(0, totalSize));

_logger.LogDebug("[TTS][ArrayPool] 음성 데이터 읽기 완료: {Size} bytes, 청크 크기: {ChunkSize}",
result.Length, chunkSize);
totalSize, chunkSize);

return result;
return (resultMemoryOwner, totalSize);
}
catch (Exception ex)
{
_logger.LogError(ex, "[TTS][ArrayPool] 음성 데이터 읽기 실패");
return null;
return (null, 0);
}
finally
{
if (buffer != null)
if (readBuffer != null)
{
_arrayPool.Return(buffer);
_arrayPool.Return(readBuffer);
}
memoryStream?.Dispose();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,104 @@ public void ChatSegment_MemoryOwner_vs_ByteArray_Test()
Assert.Equal(segment1.GetAudioSpan().ToArray(), segment2.GetAudioSpan().ToArray());

_output.WriteLine($"기존 방식 - HasAudio: {segment1.HasAudio}, 데이터 크기: {segment1.AudioData?.Length ?? 0}");
_output.WriteLine($"최적화 방식 - HasAudio: {segment2.HasAudio}, 데이터 크기: {segment2.AudioDataSize}");
_output.WriteLine($"최적화 방식 - HasAudio: {segment2.HasAudio}, 데이터 크기: {segment2.GetAudioSpan().Length}");
}

[Fact]
public void ChatSegment_GetAudioSpan_SafetyBoundaryTest()
{
// 경계 조건 테스트: 유효한 범위 내에서의 메모리 접근 안전성 검증
var testData = GenerateTestAudioData(1000);
using var memoryOwner = MemoryPool<byte>.Shared.Rent(500); // 더 작은 메모리 할당
var actualMemorySize = memoryOwner.Memory.Length; // 실제 할당된 메모리 크기
var copySize = Math.Min(500, actualMemorySize);
testData.AsSpan(0, copySize).CopyTo(memoryOwner.Memory.Span);

// 유효한 크기로 설정 (실제 메모리 크기 이하)
var validSize = actualMemorySize - 10; // 안전한 크기
var segment = ChatSegment.CreateText("Test content")
.WithAudioMemory(memoryOwner, validSize, "audio/wav", 5.0f);

// GetAudioSpan이 정확한 크기를 반환해야 함
var span = segment.GetAudioSpan();

// 요청한 크기만큼 반환되어야 함
Assert.Equal(validSize, span.Length);
_output.WriteLine($"요청 크기: {validSize}, 실제 메모리: {actualMemorySize}, 반환된 span 크기: {span.Length}");
}
Comment on lines +91 to +112
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

소유권 이전 규약 위반: WithAudioMemory 사용 시 memoryOwner를 using으로 해제하면 이중 Dispose 위험

WithAudioMemory는 소유권을 ChatSegment로 이전합니다. 현재 using var memoryOwner는 테스트 종료 시 메모리 소유자를 다시 해제하므로 규약을 위반합니다.

적용 예(요지: using 제거, segment.Dispose()만 호출):

-using var memoryOwner = MemoryPool<byte>.Shared.Rent(500); // 더 작은 메모리 할당
+var memoryOwner = MemoryPool<byte>.Shared.Rent(500); // 소유권은 ChatSegment로 이전됨
 ...
-var segment = ChatSegment.CreateText("Test content")
-    .WithAudioMemory(memoryOwner, validSize, "audio/wav", 5.0f);
+var segment = ChatSegment.CreateText("Test content")
+    .WithAudioMemory(memoryOwner, validSize, "audio/wav", 5.0f);
+try
+{
+    var span = segment.GetAudioSpan();
+    Assert.Equal(validSize, span.Length);
+}
+finally
+{
+    segment.Dispose();
+}

레포 전역에서 동일 패턴을 점검하려면 다음을 실행해주세요:


🏁 Script executed:

#!/bin/bash
# WithAudioMemory 사용 주변의 using var memoryOwner 패턴 탐지
rg -nP -C3 $'using\\s+var\\s+\\w+\\s*=\\s*MemoryPool<byte>\\.Shared\\.Rent\\(.+\\)|\\.WithAudioMemory\\(' --type cs

Length of output: 9773


소유권 이전 규약 위반 — WithAudioMemory에 IMemoryOwner 전달 시 memoryOwner를 using으로 해제하면 이중 Dispose(오류) 발생 위험

WithAudioMemory가 메모리 소유권을 ChatSegment로 이전한다고 가정하면, 현재 테스트들이 MemoryPool에서 Rent한 IMemoryOwner를 using var로 감싼 뒤 WithAudioMemory(...)에 넘기고 있어 범위 종료 시 이중 Dispose(또는 ObjectDisposed 예외)가 발생할 가능성이 큽니다. 동일 패턴이 ProjectVG.Tests/Infrastructure/Integrations/MemoryPoolingPerformanceTests.cs 여러 위치(예: l.74, l.93, l.120, l.137, l.167)에서 발견됨 — 테스트 전체에서 패턴 수정 필요.

수정 방안(예): memoryOwner를 using으로 해제하지 말고 ChatSegment가 소유권을 해제하도록 segment.Dispose()를 호출(또는 ChatSegment를 using으로 감싸 자동 Dispose). 예전 제안된 변경(diff)은 유효합니다:

-using var memoryOwner = MemoryPool<byte>.Shared.Rent(500); // 더 작은 메모리 할당
+var memoryOwner = MemoryPool<byte>.Shared.Rent(500); // 소유권은 ChatSegment로 이전됨
 ...
-var segment = ChatSegment.CreateText("Test content")
-    .WithAudioMemory(memoryOwner, validSize, "audio/wav", 5.0f);
+var segment = ChatSegment.CreateText("Test content")
+    .WithAudioMemory(memoryOwner, validSize, "audio/wav", 5.0f);
+try
+{
+    var span = segment.GetAudioSpan();
+    Assert.Equal(validSize, span.Length);
+}
+finally
+{
+    segment.Dispose();
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
[Fact]
public void ChatSegment_GetAudioSpan_SafetyBoundaryTest()
{
// 경계 조건 테스트: 유효한 범위 내에서의 메모리 접근 안전성 검증
var testData = GenerateTestAudioData(1000);
using var memoryOwner = MemoryPool<byte>.Shared.Rent(500); // 더 작은 메모리 할당
var actualMemorySize = memoryOwner.Memory.Length; // 실제 할당된 메모리 크기
var copySize = Math.Min(500, actualMemorySize);
testData.AsSpan(0, copySize).CopyTo(memoryOwner.Memory.Span);
// 유효한 크기로 설정 (실제 메모리 크기 이하)
var validSize = actualMemorySize - 10; // 안전한 크기
var segment = ChatSegment.CreateText("Test content")
.WithAudioMemory(memoryOwner, validSize, "audio/wav", 5.0f);
// GetAudioSpan이 정확한 크기를 반환해야 함
var span = segment.GetAudioSpan();
// 요청한 크기만큼 반환되어야 함
Assert.Equal(validSize, span.Length);
_output.WriteLine($"요청 크기: {validSize}, 실제 메모리: {actualMemorySize}, 반환된 span 크기: {span.Length}");
}
[Fact]
public void ChatSegment_GetAudioSpan_SafetyBoundaryTest()
{
// 경계 조건 테스트: 유효한 범위 내에서의 메모리 접근 안전성 검증
var testData = GenerateTestAudioData(1000);
var memoryOwner = MemoryPool<byte>.Shared.Rent(500); // 소유권은 ChatSegment로 이전됨
var actualMemorySize = memoryOwner.Memory.Length; // 실제 할당된 메모리 크기
var copySize = Math.Min(500, actualMemorySize);
testData.AsSpan(0, copySize).CopyTo(memoryOwner.Memory.Span);
// 유효한 크기로 설정 (실제 메모리 크기 이하)
var validSize = actualMemorySize - 10; // 안전한 크기
var segment = ChatSegment.CreateText("Test content")
.WithAudioMemory(memoryOwner, validSize, "audio/wav", 5.0f);
try
{
// GetAudioSpan이 정확한 크기를 반환해야 함
var span = segment.GetAudioSpan();
// 요청한 크기만큼 반환되어야 함
Assert.Equal(validSize, span.Length);
_output.WriteLine($"요청 크기: {validSize}, 실제 메모리: {actualMemorySize}, 반환된 span 크기: {span.Length}");
}
finally
{
segment.Dispose();
}
}
🤖 Prompt for AI Agents
ProjectVG.Tests/Infrastructure/Integrations/MemoryPoolingPerformanceTests.cs
lines 88-109: the test currently wraps the IMemoryOwner returned by
MemoryPool<byte>.Shared.Rent(...) in a using, then passes that memoryOwner into
ChatSegment.WithAudioMemory, which transfers ownership and causes a
double-dispose risk; remove the using around memoryOwner (do not dispose it
directly) and instead ensure the ChatSegment created takes responsibility for
disposal by either calling segment.Dispose() at the end of the test or wrapping
the segment in a using so ChatSegment will release the IMemoryOwner; apply the
same pattern to the other occurrences noted (around lines 74, 93, 120, 137,
167).


[Fact]
public void ChatSegment_GetAudioSpan_EmptyAndNullSafetyTest()
{
// null AudioMemoryOwner는 이제 예외가 발생해야 함 (ArgumentNullException)
var nullException = Assert.Throws<ArgumentNullException>(() =>
ChatSegment.CreateText("Test").WithAudioMemory(null!, 100, "audio/wav", 1.0f));
Assert.Equal("audioMemoryOwner", nullException.ParamName);

// AudioDataSize가 0인 경우는 여전히 정상 작동해야 함
using var memoryOwner = MemoryPool<byte>.Shared.Rent(100);
var segment2 = ChatSegment.CreateText("Test").WithAudioMemory(memoryOwner, 0, "audio/wav", 1.0f);
var span2 = segment2.GetAudioSpan();
Assert.True(span2.IsEmpty);

// 기존 AudioData 방식 (null 허용)
var segment3 = ChatSegment.CreateText("Test").WithAudioData(null!, "audio/wav", 1.0f);
var span3 = segment3.GetAudioSpan();
Assert.True(span3.IsEmpty);

_output.WriteLine("null 검증과 빈 케이스가 모두 안전하게 처리됨");
}

[Fact]
public void ChatSegment_WithAudioMemory_ValidationTest()
{
var testData = GenerateTestAudioData(100);
using var memoryOwner = MemoryPool<byte>.Shared.Rent(100);
testData.CopyTo(memoryOwner.Memory.Span);

// 정상 케이스
var validSegment = ChatSegment.CreateText("Test")
.WithAudioMemory(memoryOwner, 50, "audio/wav", 1.0f);
Assert.Equal(50, validSegment.GetAudioSpan().Length);

// null audioMemoryOwner 테스트
var nullException = Assert.Throws<ArgumentNullException>(() =>
ChatSegment.CreateText("Test").WithAudioMemory(null!, 100, "audio/wav", 1.0f));
Assert.Equal("audioMemoryOwner", nullException.ParamName);

// audioDataSize < 0 테스트
var negativeException = Assert.Throws<ArgumentOutOfRangeException>(() =>
ChatSegment.CreateText("Test").WithAudioMemory(memoryOwner, -1, "audio/wav", 1.0f));
Assert.Equal("audioDataSize", negativeException.ParamName);

// audioDataSize > memory.Length 테스트
var oversizeException = Assert.Throws<ArgumentOutOfRangeException>(() =>
ChatSegment.CreateText("Test").WithAudioMemory(memoryOwner, memoryOwner.Memory.Length + 1, "audio/wav", 1.0f));
Assert.Equal("audioDataSize", oversizeException.ParamName);

_output.WriteLine("모든 소유권 이전 검증 테스트 통과");
}

[Fact]
public void ChatSegment_Dispose_MemoryOwnerReleaseTest()
{
var testData = GenerateTestAudioData(100);
using var memoryOwner = MemoryPool<byte>.Shared.Rent(100);
testData.CopyTo(memoryOwner.Memory.Span);

var segment = ChatSegment.CreateText("Test")
.WithAudioMemory(memoryOwner, 100, "audio/wav", 1.0f);

// Dispose 호출 전에는 정상 접근 가능
Assert.True(segment.HasAudio);
Assert.Equal(100, segment.GetAudioSpan().Length);

// Dispose 호출
segment.Dispose();

// 메모리가 해제되었으므로 ObjectDisposedException 발생할 수 있음
// (실제 구현에 따라 다를 수 있음)
_output.WriteLine("Dispose 호출 완료 - 메모리 소유자 해제됨");
}

private byte[] GenerateTestAudioData(int size)
Expand Down
Loading