import { assertEquals, assertStringIncludes } from "jsr:@std/assert"; import { Project } from "ts-morph"; import { generateClient } from "../src/client.ts"; import type { Lexicon } from "../src/mod.ts"; function createTestProject() { return new Project({ useInMemoryFileSystem: true }); } Deno.test("generateClient - creates nested client structure", () => { const project = createTestProject(); const sourceFile = project.createSourceFile("test.ts", ""); const lexicons: Lexicon[] = [ { id: "com.example.post", definitions: { main: { type: "record", record: { type: "record", properties: { text: { type: "string" }, }, }, }, }, }, ]; generateClient(sourceFile, lexicons); const result = sourceFile.getFullText(); // Should create main AtProtoClient class assertStringIncludes(result, "export class AtProtoClient extends SlicesClient"); // Should create nested class structure assertStringIncludes(result, "class ComClient"); assertStringIncludes(result, "class PostExampleComClient"); // Should have nested properties assertStringIncludes(result, "readonly com: ComClient;"); assertStringIncludes(result, "readonly post: PostExampleComClient;"); // Should have OAuth client property assertStringIncludes(result, "readonly oauth?: OAuthClient | AuthProvider;"); }); Deno.test("generateClient - creates CRUD methods for records", () => { const project = createTestProject(); const sourceFile = project.createSourceFile("test.ts", ""); const lexicons: Lexicon[] = [ { id: "app.bsky.feed.post", definitions: { main: { type: "record", record: { type: "record", properties: { text: { type: "string" }, createdAt: { type: "string" }, }, }, }, }, }, ]; generateClient(sourceFile, lexicons); const result = sourceFile.getFullText(); // Should create CRUD methods assertStringIncludes(result, "async getRecords("); assertStringIncludes(result, "async getRecord("); assertStringIncludes(result, "async createRecord("); assertStringIncludes(result, "async updateRecord("); assertStringIncludes(result, "async deleteRecord("); assertStringIncludes(result, "async countRecords("); // Should have proper return types assertStringIncludes(result, "Promise>"); assertStringIncludes(result, "Promise>"); assertStringIncludes(result, 'return await this.client.getRecords(\'app.bsky.feed.post\', params);'); }); Deno.test("generateClient - creates constructors with proper inheritance", () => { const project = createTestProject(); const sourceFile = project.createSourceFile("test.ts", ""); const lexicons: Lexicon[] = [ { id: "com.example.test", definitions: { main: { type: "record", record: { type: "record", properties: { name: { type: "string" } }, }, }, }, }, ]; generateClient(sourceFile, lexicons); const result = sourceFile.getFullText(); // Main client constructor assertStringIncludes(result, "constructor(baseUrl: string, sliceUri: string, oauthClient?: OAuthClient | AuthProvider)"); assertStringIncludes(result, "super(baseUrl, sliceUri, oauthClient);"); // Nested class constructors assertStringIncludes(result, "constructor(client: SlicesClient)"); assertStringIncludes(result, "this.client = client;"); }); Deno.test("generateClient - creates client for network.slices lexicons", () => { const project = createTestProject(); const sourceFile = project.createSourceFile("test.ts", ""); const lexicons: Lexicon[] = [ { id: "network.slices.slice", definitions: { main: { type: "record", record: { type: "record", properties: { name: { type: "string" } }, }, }, }, }, ]; generateClient(sourceFile, lexicons); const result = sourceFile.getFullText(); // Should create proper client structure for network.slices assertStringIncludes(result, "export class AtProtoClient extends SlicesClient"); assertStringIncludes(result, "class NetworkClient"); assertStringIncludes(result, "class SlicesNetworkClient"); assertStringIncludes(result, "readonly network: NetworkClient;"); assertStringIncludes(result, "readonly slices: SlicesNetworkClient;"); // Should include standard CRUD methods assertStringIncludes(result, "async getRecords("); assertStringIncludes(result, "async createRecord("); }); Deno.test("generateClient - sanitizes hyphenated NSIDs", () => { const project = createTestProject(); const sourceFile = project.createSourceFile("test.ts", ""); const lexicons: Lexicon[] = [ { id: "network.slices.cyber-meteor-1637.actor.profile", definitions: { main: { type: "record", record: { type: "record", properties: { displayName: { type: "string" }, description: { type: "string" }, }, }, }, }, }, ]; generateClient(sourceFile, lexicons); const result = sourceFile.getFullText(); // Should sanitize hyphenated names to camelCase assertStringIncludes(result, "readonly cyberMeteor1637: CyberMeteor1637SlicesNetworkClient;"); assertStringIncludes(result, "class CyberMeteor1637SlicesNetworkClient"); assertStringIncludes(result, "class ActorCyberMeteor1637SlicesNetworkClient"); assertStringIncludes(result, "class ProfileActorCyberMeteor1637SlicesNetworkClient"); // Constructor should use sanitized property names assertStringIncludes(result, "this.cyberMeteor1637 = new CyberMeteor1637SlicesNetworkClient("); // But should preserve original NSID in API calls assertStringIncludes(result, "'network.slices.cyber-meteor-1637.actor.profile'"); }); Deno.test("generateClient - handles mixed lexicon types", () => { const project = createTestProject(); const sourceFile = project.createSourceFile("test.ts", ""); const lexicons: Lexicon[] = [ { id: "app.bsky.feed.post", definitions: { main: { type: "record", record: { type: "record", properties: { text: { type: "string" } }, }, }, }, }, { id: "network.slices.slice", definitions: { main: { type: "record", record: { type: "record", properties: { name: { type: "string" } }, }, }, }, }, ]; generateClient(sourceFile, lexicons); const result = sourceFile.getFullText(); // Should include both app.bsky and network.slices clients assertStringIncludes(result, "class AppClient"); assertStringIncludes(result, "class NetworkClient"); assertStringIncludes(result, "readonly app: AppClient;"); assertStringIncludes(result, "readonly network: NetworkClient;"); }); Deno.test("generateClient - handles deep nesting correctly", () => { const project = createTestProject(); const sourceFile = project.createSourceFile("test.ts", ""); const lexicons: Lexicon[] = [ { id: "app.bsky.feed.post", definitions: { main: { type: "record", record: { type: "record", properties: { text: { type: "string" } }, }, }, }, }, { id: "app.bsky.actor.profile", definitions: { main: { type: "record", record: { type: "record", properties: { displayName: { type: "string" } }, }, }, }, }, ]; generateClient(sourceFile, lexicons); const result = sourceFile.getFullText(); // Should create proper nesting: app.bsky.feed and app.bsky.actor assertStringIncludes(result, "class AppClient"); assertStringIncludes(result, "class BskyAppClient"); assertStringIncludes(result, "class FeedBskyAppClient"); assertStringIncludes(result, "class ActorBskyAppClient"); // Should have proper property chains assertStringIncludes(result, "readonly app: AppClient;"); assertStringIncludes(result, "readonly bsky: BskyAppClient;"); assertStringIncludes(result, "readonly feed: FeedBskyAppClient;"); assertStringIncludes(result, "readonly actor: ActorBskyAppClient;"); }); Deno.test("generateClient - creates no client when no record lexicons", () => { const project = createTestProject(); const sourceFile = project.createSourceFile("test.ts", ""); const lexicons: Lexicon[] = [ { id: "com.example.defs", definitions: { main: { type: "object", properties: { value: { type: "string" } }, }, }, }, ]; generateClient(sourceFile, lexicons); const result = sourceFile.getFullText(); // Should not create any client classes for non-record lexicons assertEquals(result.includes("export class AtProtoClient"), false); assertEquals(result.includes("class"), false); });