Files
Playa/tests/Volcanic.AgenticCore.Telegram.Tests/ChatSettingsStoreTests.cs
EugeneTes 5b780fac36 feat(telegram): per-chat time zones with /timezone, set_timezone and Telegram:DefaultTimeZone
ChatSettingsStore keeps each chat's IANA zone in <AgentDir>/telegram/chat-settings.json;
ChatTimeZoneService is the one path for a change (store, live session, scheduler re-evaluation,
reply text). ChatRegistry.TimeZoneFor now reads the store and new sessions start in the chat's zone.
/timezone, the set_timezone tool (ChatSettingsExtension), /status zone line, DEFAULT_TIMEZONE.

The store updates its cache under the write gate so the last file written is the newest state.
2026-09-27 10:29:29 +02:00

86 lines
3.1 KiB
C#

using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Time.Testing;
using Volcanic.AgenticCore.Telegram.Chats;
namespace Volcanic.AgenticCore.Telegram.Tests;
public sealed class ChatSettingsStoreTests : IDisposable
{
private static readonly TimeZoneInfo Berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
private readonly string _root = Directory.CreateTempSubdirectory("chat-settings-").FullName;
private string FilePath => ChatSettingsStore.DefaultPath(_root);
private ChatSettingsStore Store() =>
new(FilePath, Berlin, NullLogger<ChatSettingsStore>.Instance, new FakeTimeProvider(new DateTimeOffset(2026, 9, 26, 14, 3, 0, TimeSpan.Zero)));
public void Dispose()
{
try { Directory.Delete(_root, recursive: true); } catch (IOException) { }
}
[Fact]
public void Missing_file_uses_the_default()
{
var store = Store();
Assert.Same(Berlin, store.TimeZoneFor(1001));
Assert.False(store.HasOwnZone(1001));
Assert.EndsWith(Path.Combine("telegram", "chat-settings.json"), FilePath);
}
[Fact]
public async Task Set_persists_and_a_new_store_reads_it()
{
await Store().SetTimeZoneAsync(1001, "Asia/Tokyo", CancellationToken.None);
var reloaded = Store();
Assert.Equal("Asia/Tokyo", reloaded.TimeZoneFor(1001).Id);
Assert.True(reloaded.HasOwnZone(1001));
Assert.Same(Berlin, reloaded.TimeZoneFor(2002));
Assert.Equal(["chat-settings.json"], Directory.GetFiles(Path.GetDirectoryName(FilePath)!).Select(Path.GetFileName)); // no temp file left
}
[Fact]
public async Task File_format_is_versioned_camel_case()
{
await Store().SetTimeZoneAsync(1001, "Asia/Tokyo", CancellationToken.None);
var json = await File.ReadAllTextAsync(FilePath);
Assert.Contains("\"version\": 1", json);
Assert.Contains("\"1001\": {", json);
Assert.Contains("\"timeZone\": \"Asia/Tokyo\"", json);
}
[Fact]
public async Task Null_removes_the_entry()
{
var store = Store();
await store.SetTimeZoneAsync(1001, "Asia/Tokyo", CancellationToken.None);
await store.SetTimeZoneAsync(1001, null, CancellationToken.None);
Assert.False(store.HasOwnZone(1001));
Assert.False(Store().HasOwnZone(1001));
}
[Fact]
public void Corrupt_file_is_set_aside_and_loading_starts_empty()
{
Directory.CreateDirectory(Path.GetDirectoryName(FilePath)!);
File.WriteAllText(FilePath, "{ nope");
var store = Store();
Assert.False(store.HasOwnZone(1001));
Assert.True(File.Exists(FilePath + ".corrupt-20260926140300"));
}
[Fact]
public void Unknown_stored_id_is_ignored()
{
Directory.CreateDirectory(Path.GetDirectoryName(FilePath)!);
File.WriteAllText(FilePath, """{ "version": 1, "chats": { "1001": { "timeZone": "Mars/Olympus" }, "2002": { "timeZone": "Asia/Tokyo" } } }""");
var store = Store();
Assert.Same(Berlin, store.TimeZoneFor(1001));
Assert.Equal("Asia/Tokyo", store.TimeZoneFor(2002).Id);
}
}