Files
2026-07-13 12:31:29 +08:00

325 lines
13 KiB
C#

using System.Text.Json;
using Microsoft.Data.Sqlite;
namespace CodexProviderSync.Core.Tests;
internal sealed class TestCodexHomeFixture
{
private TestCodexHomeFixture(string root, string codexHome)
{
Root = root;
CodexHome = codexHome;
}
public string Root { get; }
public string CodexHome { get; }
public static async Task<TestCodexHomeFixture> CreateAsync()
{
string root = Path.Combine(Path.GetTempPath(), $"codex-provider-sync-{Guid.NewGuid():N}");
string codexHome = Path.Combine(root, ".codex");
Directory.CreateDirectory(Path.Combine(codexHome, "sessions", "2026", "03", "19"));
Directory.CreateDirectory(Path.Combine(codexHome, "archived_sessions", "2026", "03", "18"));
return await Task.FromResult(new TestCodexHomeFixture(root, codexHome));
}
public string RolloutPath(string directory, string fileName)
{
return Path.Combine(CodexHome, directory, "2026", "03", directory == "sessions" ? "19" : "18", fileName);
}
public string BackupRoot()
{
return Path.Combine(CodexHome, "backups_state", AppConstants.BackupNamespace);
}
public string BackupPath(string directoryName)
{
return Path.Combine(BackupRoot(), directoryName);
}
public string StateDbPath()
{
return Path.Combine(CodexHome, AppConstants.SqliteDirBasename, AppConstants.DbFileBasename);
}
public string LegacyStateDbPath()
{
return Path.Combine(CodexHome, AppConstants.DbFileBasename);
}
public async Task WriteConfigAsync(string modelProviderLine)
{
string prefix = string.IsNullOrWhiteSpace(modelProviderLine) ? string.Empty : modelProviderLine + "\n";
string configText = $"{prefix}sandbox_mode = \"danger-full-access\"\n\n[model_providers.apigather]\nbase_url = \"https://example.com\"\n";
await File.WriteAllTextAsync(Path.Combine(CodexHome, "config.toml"), configText);
}
public async Task WriteGlobalStateAsync(object state)
{
string json = JsonSerializer.Serialize(state, new JsonSerializerOptions { WriteIndented = true }) + "\n";
await File.WriteAllTextAsync(Path.Combine(CodexHome, AppConstants.GlobalStateFileBasename), json);
await File.WriteAllTextAsync(Path.Combine(CodexHome, AppConstants.GlobalStateBackupFileBasename), json);
}
public async Task WriteRolloutAsync(string filePath, string id, string provider, string cwd = "C:\\AITemp")
{
object payload = new
{
id,
timestamp = "2026-03-19T00:00:00.000Z",
cwd,
source = "cli",
cli_version = "0.115.0",
model_provider = provider
};
string first = JsonSerializer.Serialize(new
{
timestamp = "2026-03-19T00:00:00.000Z",
type = "session_meta",
payload
});
string second = JsonSerializer.Serialize(new
{
timestamp = "2026-03-19T00:00:00.000Z",
type = "event_msg",
payload = new
{
type = "user_message",
message = "hi"
}
});
await File.WriteAllTextAsync(filePath, $"{first}\n{second}\n");
}
public async Task AppendEncryptedContentAsync(string filePath)
{
await File.AppendAllTextAsync(filePath, "{\"type\":\"event_msg\",\"payload\":{\"encrypted_content\":\"gAAA\"}}\n");
}
public async Task<long> WriteBackupAsync(string directoryName, params (string RelativePath, string Content)[] files)
{
string backupDir = BackupPath(directoryName);
Directory.CreateDirectory(backupDir);
long totalBytes = 0;
if (!files.Any(file => string.Equals(file.RelativePath, "metadata.json", StringComparison.Ordinal)))
{
string metadataContent = $$"""
{
"version": 1,
"namespace": "provider-sync",
"codexHome": "{{CodexHome.Replace("\\", "\\\\")}}",
"targetProvider": "openai",
"createdAt": "2026-03-24T00:00:00.0000000+00:00",
"dbFiles": [],
"changedSessionFiles": 0
}
""";
string metadataPath = Path.Combine(backupDir, "metadata.json");
await File.WriteAllTextAsync(metadataPath, metadataContent);
totalBytes += new FileInfo(metadataPath).Length;
}
foreach ((string relativePath, string content) in files)
{
string fullPath = Path.Combine(backupDir, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);
await File.WriteAllTextAsync(fullPath, content);
totalBytes += new FileInfo(fullPath).Length;
}
return totalBytes;
}
public async Task WriteStateDbAsync(IEnumerable<(string Id, string ModelProvider, bool Archived)> rows)
{
await WriteStateDbAtAsync(StateDbPath(), rows);
}
public async Task WriteLegacyStateDbAsync(IEnumerable<(string Id, string ModelProvider, bool Archived)> rows)
{
await WriteStateDbAtAsync(LegacyStateDbPath(), rows);
}
private static async Task WriteStateDbAtAsync(string dbPath, IEnumerable<(string Id, string ModelProvider, bool Archived)> rows)
{
await using SqliteConnection connection = OpenSqliteConnection(dbPath);
await connection.OpenAsync();
SqliteCommand create = connection.CreateCommand();
create.CommandText = """
CREATE TABLE threads (
id TEXT PRIMARY KEY,
model_provider TEXT,
cwd TEXT NOT NULL DEFAULT '',
archived INTEGER NOT NULL DEFAULT 0,
first_user_message TEXT NOT NULL DEFAULT ''
)
""";
await create.ExecuteNonQueryAsync();
foreach ((string id, string modelProvider, bool archived) in rows)
{
SqliteCommand insert = connection.CreateCommand();
insert.CommandText = """
INSERT INTO threads (id, model_provider, cwd, archived, first_user_message)
VALUES ($id, $provider, 'C:\AITemp', $archived, 'hello')
""";
insert.Parameters.AddWithValue("$id", id);
insert.Parameters.AddWithValue("$provider", modelProvider);
insert.Parameters.AddWithValue("$archived", archived ? 1 : 0);
await insert.ExecuteNonQueryAsync();
}
}
public async Task WriteStateDbWithUserEventColumnAsync(IEnumerable<(string Id, string ModelProvider, bool Archived, bool HasUserEvent)> rows)
{
await using SqliteConnection connection = OpenSqliteConnection();
await connection.OpenAsync();
SqliteCommand create = connection.CreateCommand();
create.CommandText = """
CREATE TABLE threads (
id TEXT PRIMARY KEY,
model_provider TEXT,
cwd TEXT NOT NULL DEFAULT '',
archived INTEGER NOT NULL DEFAULT 0,
has_user_event INTEGER NOT NULL DEFAULT 0,
first_user_message TEXT NOT NULL DEFAULT ''
)
""";
await create.ExecuteNonQueryAsync();
foreach ((string id, string modelProvider, bool archived, bool hasUserEvent) in rows)
{
SqliteCommand insert = connection.CreateCommand();
insert.CommandText = """
INSERT INTO threads (id, model_provider, cwd, archived, has_user_event, first_user_message)
VALUES ($id, $provider, 'C:\AITemp', $archived, $hasUserEvent, 'hello')
""";
insert.Parameters.AddWithValue("$id", id);
insert.Parameters.AddWithValue("$provider", modelProvider);
insert.Parameters.AddWithValue("$archived", archived ? 1 : 0);
insert.Parameters.AddWithValue("$hasUserEvent", hasUserEvent ? 1 : 0);
await insert.ExecuteNonQueryAsync();
}
}
public async Task WriteStateDbWithUserEventAndCwdAsync(IEnumerable<(string Id, string ModelProvider, bool Archived, bool HasUserEvent, string Cwd)> rows)
{
await using SqliteConnection connection = OpenSqliteConnection();
await connection.OpenAsync();
SqliteCommand create = connection.CreateCommand();
create.CommandText = """
CREATE TABLE threads (
id TEXT PRIMARY KEY,
model_provider TEXT,
cwd TEXT NOT NULL DEFAULT '',
archived INTEGER NOT NULL DEFAULT 0,
has_user_event INTEGER NOT NULL DEFAULT 0,
first_user_message TEXT NOT NULL DEFAULT ''
)
""";
await create.ExecuteNonQueryAsync();
foreach ((string id, string modelProvider, bool archived, bool hasUserEvent, string cwd) in rows)
{
SqliteCommand insert = connection.CreateCommand();
insert.CommandText = """
INSERT INTO threads (id, model_provider, cwd, archived, has_user_event, first_user_message)
VALUES ($id, $provider, $cwd, $archived, $hasUserEvent, 'hello')
""";
insert.Parameters.AddWithValue("$id", id);
insert.Parameters.AddWithValue("$provider", modelProvider);
insert.Parameters.AddWithValue("$cwd", cwd);
insert.Parameters.AddWithValue("$archived", archived ? 1 : 0);
insert.Parameters.AddWithValue("$hasUserEvent", hasUserEvent ? 1 : 0);
await insert.ExecuteNonQueryAsync();
}
}
public async Task WriteStateDbWithCwdAsync(IEnumerable<(string Id, string ModelProvider, bool Archived, string Cwd)> rows)
{
await using SqliteConnection connection = OpenSqliteConnection();
await connection.OpenAsync();
SqliteCommand create = connection.CreateCommand();
create.CommandText = """
CREATE TABLE threads (
id TEXT PRIMARY KEY,
model_provider TEXT,
cwd TEXT NOT NULL DEFAULT '',
archived INTEGER NOT NULL DEFAULT 0,
first_user_message TEXT NOT NULL DEFAULT ''
)
""";
await create.ExecuteNonQueryAsync();
foreach ((string id, string modelProvider, bool archived, string cwd) in rows)
{
SqliteCommand insert = connection.CreateCommand();
insert.CommandText = """
INSERT INTO threads (id, model_provider, cwd, archived, first_user_message)
VALUES ($id, $provider, $cwd, $archived, 'hello')
""";
insert.Parameters.AddWithValue("$id", id);
insert.Parameters.AddWithValue("$provider", modelProvider);
insert.Parameters.AddWithValue("$cwd", cwd);
insert.Parameters.AddWithValue("$archived", archived ? 1 : 0);
await insert.ExecuteNonQueryAsync();
}
}
public async Task WriteStateDbForProjectVisibilityAsync(IEnumerable<(string Id, string ModelProvider, string Cwd, string Source, bool Archived, string FirstUserMessage, long UpdatedAtMs)> rows)
{
await using SqliteConnection connection = OpenSqliteConnection();
await connection.OpenAsync();
SqliteCommand create = connection.CreateCommand();
create.CommandText = """
CREATE TABLE threads (
id TEXT PRIMARY KEY,
model_provider TEXT,
cwd TEXT NOT NULL DEFAULT '',
source TEXT NOT NULL DEFAULT 'cli',
archived INTEGER NOT NULL DEFAULT 0,
first_user_message TEXT NOT NULL DEFAULT '',
updated_at_ms INTEGER NOT NULL DEFAULT 0
)
""";
await create.ExecuteNonQueryAsync();
foreach ((string id, string modelProvider, string cwd, string source, bool archived, string firstUserMessage, long updatedAtMs) in rows)
{
SqliteCommand insert = connection.CreateCommand();
insert.CommandText = """
INSERT INTO threads (id, model_provider, cwd, source, archived, first_user_message, updated_at_ms)
VALUES ($id, $provider, $cwd, $source, $archived, $firstUserMessage, $updatedAtMs)
""";
insert.Parameters.AddWithValue("$id", id);
insert.Parameters.AddWithValue("$provider", modelProvider);
insert.Parameters.AddWithValue("$cwd", cwd);
insert.Parameters.AddWithValue("$source", source);
insert.Parameters.AddWithValue("$archived", archived ? 1 : 0);
insert.Parameters.AddWithValue("$firstUserMessage", firstUserMessage);
insert.Parameters.AddWithValue("$updatedAtMs", updatedAtMs);
await insert.ExecuteNonQueryAsync();
}
}
public SqliteConnection OpenSqliteConnection()
{
return OpenSqliteConnection(StateDbPath());
}
public SqliteConnection OpenLegacySqliteConnection()
{
return OpenSqliteConnection(LegacyStateDbPath());
}
private static SqliteConnection OpenSqliteConnection(string dbPath)
{
Directory.CreateDirectory(Path.GetDirectoryName(dbPath)!);
return new SqliteConnection($"Data Source={dbPath};Mode=ReadWriteCreate;Pooling=False");
}
}