import fs from "node:fs/promises"; import path from "node:path"; import { afterEach, describe, expect, test } from "vitest"; import { loadAgentDeclarations } from "../src/agents/declarations.js"; import { temporaryProject, testDeclarationEnvironment } from "./helpers.js"; const roots: string[] = []; afterEach(async () => { await Promise.all(roots.splice(0).map((root) => fs.rm(root, { recursive: true, force: true }))); }); describe("agent declarations", () => { test("binds conceptualization contracts to graph events and strict JSON mode", async () => { const source = await fs.readFile( path.join(process.cwd(), "agents", "conceptualizer.yaml"), "utf8", ); const prompt = await fs.readFile( path.join(process.cwd(), "prompts", "conceptualizer.md"), "utf8", ); const cases = [ { name: "wrong-event", declaration: source.replace( " - stream.thought.derived.concept.graph", " - stream.thought.derived.topics", ), message: "must be selected together", }, { name: "wrong-contract", declaration: source.replace( "stream.thought.output.conceptualization", "stream.thought.output.observation", ), message: "must be selected together", }, { name: "conversation-text", declaration: source.replace( " model: gpt-4.1-mini", " model: gpt-4.1-mini\n outputMode: conversation-text", ), message: "strict JSON", }, ]; for (const current of cases) { const project = await temporaryProject(`thoughtstream-declaration-${current.name}-`); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); await fs.writeFile(path.join(agents, "conceptualizer.yaml"), current.declaration); await fs.writeFile(path.join(prompts, "conceptualizer.md"), prompt); await expect(loadAgentDeclarations(agents, {})).rejects.toThrow(current.message); } }); test("resolves a Tinker capability tier to a concrete persisted model", async () => { const declarations = await loadAgentDeclarations(path.join(process.cwd(), "agents"), testDeclarationEnvironment); expect(declarations.find((declaration) => declaration.id === "bluesky-enrichment-observer")).toMatchObject({ enabled: true, mode: "pi", provider: "tinker", providerProfile: "tinker-default", modelTier: "reasoning-small", model: "thinkingmachines/Inkling-Small", tools: ["atproto.fetch-markdown", "web.download-image"], }); expect(declarations.find((declaration) => declaration.id === "telegram-conversation")).toMatchObject({ version: 22, enabled: false, mode: "pi", provider: "tinker", providerProfile: "tinker-default", model: "thinkingmachines/Inkling-Small", thinkingLevel: "medium", outputMode: "conversation-text", sourcePatterns: ["telegram:thoughtstream-bot-webhook"], acceptedPrivacy: ["sensitive"], outputEventType: "stream.thought.derived.message.observation", payloadFields: ["text"], tools: [], proposals: ["memory-change", "self-correction", "focus-declaration"], initialReplay: "now", contextStrategy: "telegram-conversation", maxEvents: 100, conversationCompaction: { mode: "consume", agentId: "telegram-conversation-compactor", }, contextDocumentMaxChars: 64_000, contextDocumentSubscriptions: [{ source: "filesystem:telegram-agent-context", paths: ["identity.md", "memory.md"], required: true, }], conversationHistoryAgentIds: ["telegram-conversation"], accounting: { onExhaustion: "defer", reservation: { inputTokens: 80_000, outputTokens: 3_000, costMicrousd: 50_000 }, }, retry: { initialDelayMs: 5_000, maxDelayMs: 300_000 }, }); expect(declarations.find((declaration) => declaration.id === "output-repair")).toMatchObject({ enabled: true, mode: "pi", role: "repair", provider: "tinker", providerProfile: "tinker-default", modelTier: "escalation", model: "fixture/escalation-model", tools: [], }); expect(declarations.find((declaration) => declaration.id === "telegram-conversation-compactor")).toMatchObject({ version: 4, enabled: false, role: "compactor", mode: "pi", provider: "tinker", providerProfile: "tinker-default", model: "thinkingmachines/Inkling-Small", thinkingLevel: "off", maxOutputTokens: 1_200, outputMode: "compaction-text", maxEvents: 300, contextStrategy: "telegram-compaction", conversationHistoryAgentIds: ["telegram-conversation"], conversationCompaction: { mode: "produce", targetAgentId: "telegram-conversation", triggerInputChars: 80_000, retainInputChars: 40_000, }, outputEventType: "stream.thought.derived.conversation.compaction", tools: [], proposals: [], }); expect(declarations.find((declaration) => declaration.id === "stream-agent-conversation")).toMatchObject({ version: 1, enabled: true, role: "standard", mode: "pi", provider: "tinker", providerProfile: "tinker-default", model: "thinkingmachines/Inkling-Small", thinkingLevel: "medium", outputMode: "conversation-text", contextStrategy: "agent-conversation", eventTypes: ["stream.thought.source.agent.message"], sourcePatterns: ["agent-message:co"], acceptedPrivacy: ["sensitive"], initialReplay: "now", outputEventType: "stream.thought.derived.agent.message", contextDocumentSubscriptions: [{ source: "filesystem:telegram-agent-context", paths: ["identity.md", "memory.md"], required: true, }], tools: [], proposals: [], externalActions: false, }); expect(declarations.find((declaration) => declaration.id === "conceptualizer")).toMatchObject({ version: 14, enabled: true, mode: "pi", provider: "openai-compatible", providerProfile: "openai-json-default", model: "gpt-4.1-mini", outputContract: { id: "stream.thought.output.conceptualization", version: 1, sha256: expect.stringMatching(/^[a-f0-9]{64}$/), }, outputEventType: "stream.thought.derived.concept.graph", contextStrategy: "atproto-batch", atprotoObjectContext: true, tools: [], }); expect(declarations.find((declaration) => declaration.id === "post-training-course-tutor")).toMatchObject({ version: 1, enabled: true, mode: "pi", provider: "tinker", providerProfile: "tinker-default", model: "thinkingmachines/Inkling-Small", thinkingLevel: "medium", contextStrategy: "single-event", maxEvents: 1, payloadFields: ["courseId", "courseRevision", "lessonId", "lessonTitle", "sectionId", "sectionTitle", "question", "lessonContext"], eventTypes: ["stream.thought.source.course.question"], sourcePatterns: ["web-course:post-training-model-factory"], acceptedPrivacy: ["sensitive"], initialReplay: "now", outputEventType: "stream.thought.derived.message.observation", tools: [], proposals: [], externalActions: false, }); expect(declarations.find((declaration) => declaration.id === "resident-letta-conversation")).toMatchObject({ version: 6, enabled: true, mode: "letta-agent-sdk", provider: "letta-cloud", sourcePatterns: ["telegram:thoughtstream-bot-webhook", "batch:stream-activity"], acceptedPrivacy: ["sensitive", "private", "public-source"], contextStrategy: "activity-batch", maxEvents: 1, payloadFields: ["text"], lettaAgent: { backend: "cloud", agentIdEnv: "THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID", conversation: "main", responseMode: "conversation-text", batchResponseMode: "strict-json", permissionMode: "unrestricted", dreaming: { trigger: "off" }, sandbox: { ttlMinutes: 5, terminateOnClose: false }, }, accounting: { reservation: { inputTokens: 80_000, outputTokens: 2_000 }, limits: [ expect.objectContaining({ window: "rolling", durationMs: 300_000, maxCalls: 100, maxInputTokens: 100_000_000, maxOutputTokens: 10_000_000, }), expect.objectContaining({ window: "hour", maxCalls: 1_000, maxInputTokens: 1_000_000_000, maxOutputTokens: 100_000_000, }), expect.objectContaining({ window: "day", maxCalls: 10_000, maxInputTokens: 1_000_000_000, maxOutputTokens: 1_000_000_000, }), ], }, }); expect(declarations.find((declaration) => declaration.id === "cameron-bluesky-listener")).toMatchObject({ version: 2, enabled: false, mode: "letta-agent-sdk", provider: "letta-cloud", model: "chatgpt-plus-pro/gpt-5.6-luna", sourcePatterns: ["batch:cameron-atproto"], acceptedPrivacy: ["public-source"], privacyFloor: "private", contextStrategy: "atproto-batch", atprotoObjectContext: true, lettaAgent: { agentIdEnv: "THOUGHTSTREAM_LETTA_BLUESKY_LISTENER_AGENT_ID", responseMode: "strict-json", outputOnly: true, permissionMode: "strict", skillSources: [], }, tools: [], externalActions: false, }); expect(declarations.find((declaration) => declaration.id === "cameron-x-listener")).toMatchObject({ version: 1, enabled: false, model: "chatgpt-plus-pro/gpt-5.6-luna", sourcePatterns: ["batch:cameron-x-activity"], acceptedPrivacy: ["sensitive", "private", "public-source"], contextStrategy: "activity-batch", lettaAgent: { agentIdEnv: "THOUGHTSTREAM_LETTA_X_LISTENER_AGENT_ID", responseMode: "strict-json", outputOnly: true, permissionMode: "strict", skillSources: [], }, }); expect(declarations.find((declaration) => declaration.id === "cameron-social-listener")).toMatchObject({ version: 1, enabled: false, model: "chatgpt-plus-pro/gpt-5.6-terra", sourcePatterns: ["batch:cameron-social-observations"], contextStrategy: "activity-batch", requireCompletedAgentOutputs: true, lettaAgent: { agentIdEnv: "THOUGHTSTREAM_LETTA_SOCIAL_LISTENER_AGENT_ID", responseMode: "strict-json", outputOnly: true, permissionMode: "strict", skillSources: [], }, }); const directTelegramOwners = declarations.filter((declaration) => ( declaration.enabled && declaration.eventTypes.includes("stream.thought.source.telegram.message") && declaration.sourcePatterns.includes("telegram:thoughtstream-bot-webhook") && declaration.outputEventType === "stream.thought.derived.message.observation" )); expect(directTelegramOwners.map((declaration) => ({ id: declaration.id, mode: declaration.mode }))).toEqual([ { id: "resident-letta-conversation", mode: "letta-agent-sdk" }, ]); expect(declarations.find((declaration) => declaration.id === "resident-letta-conversation")?.lettaAgent?.agentId) .toBe("agent-telegram-fixture"); }); test("resolves an enabled Letta Cloud agent from its trusted environment reference", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile( path.join(process.cwd(), "agents", "resident-letta-conversation.yaml"), "utf8", ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), declaration, ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), path.join(prompts, "resident-letta-conversation.md"), ); const loaded = await loadAgentDeclarations(agents, { THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID: "agent-cloud-fixture", }); expect(loaded[0]).toMatchObject({ enabled: true, mode: "letta-agent-sdk", provider: "letta-cloud", lettaAgent: { agentId: "agent-cloud-fixture" }, }); }); test("activates each social listener only through its own environment gate and agent identity", async () => { const declarations = await loadAgentDeclarations(path.join(process.cwd(), "agents"), { ...testDeclarationEnvironment, THOUGHTSTREAM_LETTA_ENABLE_BLUESKY_LISTENER: "1", THOUGHTSTREAM_LETTA_BLUESKY_LISTENER_AGENT_ID: "agent-bluesky-listener-fixture", THOUGHTSTREAM_LETTA_ENABLE_X_LISTENER: "true", THOUGHTSTREAM_LETTA_X_LISTENER_AGENT_ID: "agent-x-listener-fixture", THOUGHTSTREAM_LETTA_ENABLE_SOCIAL_LISTENER: "1", THOUGHTSTREAM_LETTA_SOCIAL_LISTENER_AGENT_ID: "agent-social-listener-fixture", }); expect(declarations.filter((declaration) => declaration.id.startsWith("cameron-")).map((declaration) => ({ id: declaration.id, enabled: declaration.enabled, agentId: declaration.lettaAgent?.agentId, }))).toEqual([ { id: "cameron-bluesky-listener", enabled: true, agentId: "agent-bluesky-listener-fixture" }, { id: "cameron-social-listener", enabled: true, agentId: "agent-social-listener-fixture" }, { id: "cameron-x-listener", enabled: true, agentId: "agent-x-listener-fixture" }, ]); await expect(loadAgentDeclarations(path.join(process.cwd(), "agents"), { ...testDeclarationEnvironment, THOUGHTSTREAM_LETTA_ENABLE_X_LISTENER: "1", })).rejects.toThrow("THOUGHTSTREAM_LETTA_X_LISTENER_AGENT_ID"); await expect(loadAgentDeclarations(path.join(process.cwd(), "agents"), { ...testDeclarationEnvironment, THOUGHTSTREAM_LETTA_ENABLE_BLUESKY_LISTENER: "1", THOUGHTSTREAM_LETTA_BLUESKY_LISTENER_AGENT_ID: "agent-shared-listener-fixture", THOUGHTSTREAM_LETTA_ENABLE_X_LISTENER: "1", THOUGHTSTREAM_LETTA_X_LISTENER_AGENT_ID: "agent-shared-listener-fixture", })).rejects.toThrow("distinct agent identities"); }); test("activates the disabled output-only local Coil consumer only through its explicit environment gate", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); await fs.copyFile( path.join(process.cwd(), "agents", "coil-public-knowledge.yaml"), path.join(agents, "coil-public-knowledge.yaml"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "coil-public-knowledge.md"), path.join(prompts, "coil-public-knowledge.md"), ); const [inactive] = await loadAgentDeclarations(agents, {}); expect(inactive?.enabled).toBe(false); expect(inactive?.lettaAgent?.agentId).toBeUndefined(); const [active] = await loadAgentDeclarations(agents, { THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE: "1", THOUGHTSTREAM_LETTA_CO_AGENT_ID: "agent-co-fixture", THOUGHTSTREAM_LETTA_CO_MEMORY_DIR: "/tmp/co-memory", }); expect(active).toMatchObject({ id: "coil-public-knowledge", version: 3, enabled: true, provider: "letta-local", contextStrategy: "coil-public-knowledge", lettaAgent: { backend: "local", agentId: "agent-co-fixture", conversation: "per-document", outputOnly: true, proposalTool: "public-knowledge-diff", permissionMode: "strict", skillSources: [], }, }); await expect(loadAgentDeclarations(agents, { THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE: "sometimes", })).rejects.toThrow("must be one of 1, true, 0, or false"); const declarationPath = path.join(agents, "coil-public-knowledge.yaml"); await fs.writeFile( declarationPath, (await fs.readFile(declarationPath, "utf8")).replace(" replay: now", " replay: beginning"), ); await expect(loadAgentDeclarations(agents, { THOUGHTSTREAM_ENABLE_COIL_PUBLIC_KNOWLEDGE: "1", THOUGHTSTREAM_LETTA_CO_AGENT_ID: "agent-co-fixture", THOUGHTSTREAM_LETTA_CO_MEMORY_DIR: "/tmp/co-memory", })).rejects.toThrow("replay now"); }); test("rejects a cost limit when the resident declaration omits a cost reservation", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile( path.join(process.cwd(), "agents", "resident-letta-conversation.yaml"), "utf8", ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), declaration .replace("enabled: true", "enabled: false") .replace(" maxOutputTokens: 100000000", " maxOutputTokens: 100000000\n maxCostMicrousd: 10000000"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), path.join(prompts, "resident-letta-conversation.md"), ); await expect(loadAgentDeclarations(agents, {})) .rejects.toThrow("Window cost limit requires a cost reservation"); }); test("requires token limits when cost accounting is omitted", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile( path.join(process.cwd(), "agents", "resident-letta-conversation.yaml"), "utf8", ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), declaration .replace("enabled: true", "enabled: false") .replace(" maxOutputTokens: 100000000\n", ""), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), path.join(prompts, "resident-letta-conversation.md"), ); await expect(loadAgentDeclarations(agents, {})).rejects.toThrow("maxOutputTokens"); }); test("rejects Letta SDK declarations that replay synthetic history", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile( path.join(process.cwd(), "agents", "resident-letta-conversation.yaml"), "utf8", ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), declaration .replace("strategy: activity-batch", "strategy: telegram-conversation") .replace("maxEvents: 1", "maxEvents: 8"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), path.join(prompts, "resident-letta-conversation.md"), ); await expect(loadAgentDeclarations(agents, { THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID: "agent-cloud-fixture", })).rejects.toThrow("Letta Agent SDK declarations require one trigger event context"); }); test("accepts bounded concrete resident sources and rejects wildcard namespaces", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile( path.join(process.cwd(), "agents", "resident-letta-conversation.yaml"), "utf8", ); await fs.writeFile( path.join(agents, "resident-letta-conversation.yaml"), declaration .replace("- telegram:thoughtstream-bot-webhook", "- telegram:*"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "resident-letta-conversation.md"), path.join(prompts, "resident-letta-conversation.md"), ); await expect(loadAgentDeclarations(agents, { THOUGHTSTREAM_LETTA_TELEGRAM_AGENT_ID: "agent-cloud-fixture", })).rejects.toThrow("Letta Agent SDK declarations require one to eight concrete source namespaces"); }); test("fails closed when a repair declaration selects an unmapped escalation tier", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents, { recursive: true }); await fs.mkdir(prompts, { recursive: true }); const declaration = await fs.readFile(path.join(process.cwd(), "agents", "output-repair.yaml"), "utf8"); await fs.writeFile( path.join(agents, "output-repair.yaml"), declaration, ); await fs.copyFile( path.join(process.cwd(), "prompts", "output-repair.md"), path.join(prompts, "output-repair.md"), ); await expect(loadAgentDeclarations(agents, {})) .rejects.toThrow("No concrete model mapping for tinker/escalation"); }); test("loads the production repair declaration from its trusted-host escalation mapping", async () => { const declarations = await loadAgentDeclarations(path.join(process.cwd(), "agents"), testDeclarationEnvironment); expect(declarations.find((declaration) => declaration.id === "output-repair")).toMatchObject({ enabled: true, role: "repair", modelTier: "escalation", model: "fixture/escalation-model", }); }); test("rejects declaration-controlled provider URLs and credential selectors", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); await fs.mkdir(agents); await fs.mkdir(path.join(project, "prompts")); await fs.writeFile(path.join(project, "prompts", "agent.md"), "Read only.\n"); await fs.writeFile(path.join(agents, "agent.yaml"), [ "id: unsafe-provider", "version: 1", "description: Must fail before execution", "enabled: true", "subscribe: { types: ['*'], sources: ['*'], privacy: [private] }", "context: { maxEvents: 1, maxChars: 1000 }", "runner:", " kind: pi", " profile: tinker-default", " model: Qwen/Qwen3.5-4B", " baseUrl: https://attacker.invalid/v1", " apiKeyEnv: ATTACKER_KEY", " maxOutputTokens: 100", " timeoutMs: 1000", "prompt: prompts/agent.md", "emit: [stream.thought.derived.document.read]", "policy: { tools: [], externalActions: false }", ].join("\n")); await expect(loadAgentDeclarations(agents)).rejects.toThrow("Unrecognized keys"); }); test("requires a trusted provider profile for Pi declarations", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); await fs.mkdir(agents); await fs.mkdir(path.join(project, "prompts")); await fs.writeFile(path.join(project, "prompts", "agent.md"), "Read only.\n"); await fs.writeFile(path.join(agents, "agent.yaml"), [ "id: missing-profile", "version: 1", "description: Must fail before execution", "enabled: true", "subscribe: { types: ['*'], sources: ['*'], privacy: [private] }", "context: { maxEvents: 1, maxChars: 1000 }", "runner: { kind: pi, model: Qwen/Qwen3.5-4B, maxOutputTokens: 100, timeoutMs: 1000 }", "prompt: prompts/agent.md", "emit: [stream.thought.derived.document.read]", "policy: { tools: [], externalActions: false }", ].join("\n")); await expect(loadAgentDeclarations(agents)).rejects.toThrow("Pi agents require a trusted provider profile"); }); test("rejects output types outside the registered allowlist", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); await fs.mkdir(agents); await fs.mkdir(path.join(project, "prompts")); await fs.writeFile(path.join(project, "prompts", "agent.md"), "Read only.\n"); await fs.writeFile(path.join(agents, "agent.yaml"), [ "id: invalid-output", "version: 1", "description: Must fail before execution", "enabled: true", "subscribe:", " types: ['*']", " sources: ['*']", " privacy: [private]", "context: { maxEvents: 1, maxChars: 1000 }", "runner: { kind: deterministic, maxOutputTokens: 100, timeoutMs: 1000 }", "prompt: prompts/agent.md", "emit: [stream.thought.derived.not-registered]", "policy: { tools: [], externalActions: false }", ].join("\n")); await expect(loadAgentDeclarations(agents)).rejects.toThrow("unregistered event type"); }); test("rejects conversation-text output outside the scoped Telegram adapter", async () => { const project = await temporaryProject(); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents); await fs.mkdir(prompts); const declaration = await fs.readFile(path.join(process.cwd(), "agents", "telegram-conversation.yaml"), "utf8"); await fs.writeFile( path.join(agents, "invalid-conversation-text.yaml"), declaration.replace("strategy: telegram-conversation", "strategy: single-event"), ); await fs.copyFile( path.join(process.cwd(), "prompts", "telegram-conversation.md"), path.join(prompts, "telegram-conversation.md"), ); await expect(loadAgentDeclarations(agents)).rejects.toThrow( "Conversation-text output requires a standard Pi Telegram or private agent conversation declaration with no read-only model tools", ); }); test("rejects escaping, over-budget, and non-Telegram document subscriptions", async () => { const source = await fs.readFile(path.join(process.cwd(), "agents", "telegram-conversation.yaml"), "utf8"); const prompt = await fs.readFile(path.join(process.cwd(), "prompts", "telegram-conversation.md"), "utf8"); const cases = [ { name: "escaping-path", declaration: source.replace(" - identity.md", " - ../identity.md"), message: "normalized relative POSIX paths", }, { name: "document-budget", declaration: source.replace(" maxChars: 64000", " maxChars: 160000"), message: "leave at least 1024 characters", }, { name: "wrong-strategy", declaration: source.replace(" strategy: telegram-conversation", " strategy: single-event"), message: "supported only for Telegram conversations", }, ]; for (const current of cases) { const project = await temporaryProject(`thoughtstream-document-subscription-${current.name}-`); roots.push(project); const agents = path.join(project, "agents"); const prompts = path.join(project, "prompts"); await fs.mkdir(agents); await fs.mkdir(prompts); await fs.writeFile(path.join(agents, "telegram-conversation.yaml"), current.declaration); await fs.writeFile(path.join(prompts, "telegram-conversation.md"), prompt); await expect(loadAgentDeclarations(agents)).rejects.toThrow(current.message); } }); });