Files
oh-my-pi/packages/coding-agent/test/model-registry-create.test.ts
T
oldschoolaandcan1357 21d152c70c feat(coding-agent): async ConfigFile + ModelRegistry.create() factory; fix MemorySessionStorage O(N^2) appends
F6: defer the JSON -> YAML migration out of ConfigFile's constructor and
add an async path so the boot sequence stops blocking the event loop on
sync I/O. New ConfigFile.tryLoadAsync/loadAsync/loadOrDefaultAsync/
getMtimeMsAsync and static ConfigFile.warmup. The migration is now
idempotent (per-process cache) so relocate() does not re-run it.
ModelRegistry.create(authStorage, modelsPath?) is a new async factory
that runs the warmup before the sync constructor's bundled-model load.
Production call sites (main.ts, sdk.ts, task/executor.ts, commit
pipelines, SDK example) all switched. Sync new ModelRegistry(...)
constructor is still supported for tests.

F2: rewrite MemorySessionStorage's mirror as { chunks: string[]; byteLen;
mtimeMs } so writeLineSync appends a single chunk in O(1) instead of
read-modify-writing the entire file (which was O(N) per append, O(N^2)
per session). statSync now reports true UTF-8 byte length instead of
character count. readTextPrefix walks chunks until the byte budget is
exhausted instead of materialising the full mirror.

Also rolls in per-package CHANGELOG entries for F1-F8.
2026-05-31 04:46:06 +02:00

72 lines
2.8 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
import { AuthStorage } from "@oh-my-pi/pi-coding-agent/session/auth-storage";
import { TempDir } from "@oh-my-pi/pi-utils";
import { ConfigFile } from "../src/config/config-file";
import { ModelRegistry } from "../src/config/model-registry";
import { ModelsConfigSchema } from "../src/config/models-config-schema";
describe("ModelRegistry.create() factory (F6)", () => {
let tempDir: TempDir;
beforeEach(() => {
tempDir = TempDir.createSync("@model-registry-create-");
});
afterEach(async () => {
// On Windows the cache SQLite handle inside the registry may briefly hold
// the dir; treat cleanup errors as best-effort like TempDir's Symbol.dispose.
await tempDir.remove().catch(() => {});
});
test("produces an instance whose authStorage matches and that exposes bundled models", async () => {
const authStorage = await AuthStorage.create(path.join(tempDir.path(), "auth.db"));
try {
const registry = await ModelRegistry.create(authStorage, path.join(tempDir.path(), "models.yml"));
expect(registry.authStorage).toBe(authStorage);
// The constructor's bundled-model load runs after warmup, so the
// factory's returned instance must be queryable immediately.
const claude = registry.find("anthropic", "claude-sonnet-4-5");
expect(claude).toBeDefined();
expect(claude?.id).toBe("claude-sonnet-4-5");
} finally {
authStorage.close();
}
});
test("migrates legacy models.json → models.yml ahead of the sync constructor", async () => {
const yml = path.join(tempDir.path(), "models.yml");
const json = path.join(tempDir.path(), "models.json");
// Seed a legacy JSON config; factory should migrate it asynchronously
// before the sync constructor reads from the yml path.
await Bun.write(json, JSON.stringify({ models: [] }));
expect(fs.existsSync(yml)).toBe(false);
const authStorage = await AuthStorage.create(path.join(tempDir.path(), "auth.db"));
try {
await ModelRegistry.create(authStorage, yml);
expect(fs.existsSync(yml)).toBe(true);
} finally {
authStorage.close();
}
});
test("ConfigFile.warmup is idempotent — second call is a no-op", async () => {
const yml = path.join(tempDir.path(), "models.yml");
const json = path.join(tempDir.path(), "models.json");
await Bun.write(json, JSON.stringify({ models: [] }));
const cf = new ConfigFile("models", ModelsConfigSchema, yml);
await ConfigFile.warmup(cf);
expect(fs.existsSync(yml)).toBe(true);
const mtime1 = fs.statSync(yml).mtimeMs;
// Second warmup should not rewrite the file (idempotent path).
await ConfigFile.warmup(cf);
const mtime2 = fs.statSync(yml).mtimeMs;
expect(mtime2).toBe(mtime1);
});
});