/** * Tests for `buildVisibleAppContext`, the resolver behind the `visible_app:` * turn-context line. * * The client reports only the id of the app it has on screen; the daemon * resolves the name and source directory so the assistant can act on the app * without a lookup. A stale or malformed id must degrade to "no app in view" * rather than failing the turn. */ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { createApp, deleteApp } from "../apps/app-store.js"; import { clearConversations, setConversation, } from "../daemon/conversation-registry.js"; import { applyRuntimeInjections, buildVisibleAppContext, } from "../daemon/conversation-runtime-assembly.js"; import { registerDefaultPluginInjectors } from "../plugins/defaults/index.js"; import type { Message } from "../providers/types.js"; import { getWorkspacePluginsDir } from "../util/platform.js"; import { asConversation } from "./helpers/mock-conversation.js"; // The injector chain is registered by the daemon bootstrap in production; do // the same here so `applyRuntimeInjections` walks a non-empty chain. registerDefaultPluginInjectors(); let testDataDir: string; // Restored after each test so the workspace override never outlives this file: // leaving it pointed at a directory we just deleted breaks whatever module a // sibling test file loads next. const originalWorkspaceDir = process.env.VELLUM_WORKSPACE_DIR; beforeEach(() => { testDataDir = join( tmpdir(), `vellum-visible-app-test-${Date.now()}-${Math.random().toString(36).slice(2)}`, ); process.env.VELLUM_WORKSPACE_DIR = testDataDir; }); afterEach(() => { if (existsSync(testDataDir)) { rmSync(testDataDir, { recursive: true, force: true }); } if (originalWorkspaceDir === undefined) { delete process.env.VELLUM_WORKSPACE_DIR; } else { process.env.VELLUM_WORKSPACE_DIR = originalWorkspaceDir; } }); describe("buildVisibleAppContext", () => { test("resolves the app's name and readable slug, without a host path", () => { // A workspace app id is an opaque UUID; the slug is the handle a human // (or the model) recognizes, so both have to come back. const app = createApp({ name: "Grocery List", schemaJson: "{}", htmlDefinition: "

Hello

", }); expect(buildVisibleAppContext(app.id)).toEqual({ appId: app.id, name: "Grocery List", slug: "grocery-list", }); }); test("carries no resolved directory, only derivable handles", () => { // Only derivable handles ship: a workspace app's directory is the // workspace `Root:` joined with the app-builder skill's // `data/apps//` layout, so no value here may be an absolute path. const app = createApp({ name: "Grocery List", schemaJson: "{}", htmlDefinition: "

Hello

", }); const context = buildVisibleAppContext(app.id); expect(context).not.toBeNull(); for (const value of Object.values(context ?? {})) { expect(value.startsWith("/")).toBe(false); } }); test("returns null when no app is in view", () => { expect(buildVisibleAppContext(undefined)).toBeNull(); expect(buildVisibleAppContext("")).toBeNull(); }); test("returns null for an app that no longer exists", () => { const app = createApp({ name: "Deleted App", schemaJson: "{}", htmlDefinition: "

Hello

", }); deleteApp(app.id); expect(buildVisibleAppContext(app.id)).toBeNull(); }); test("returns null for a traversal-shaped id instead of throwing", () => { expect(buildVisibleAppContext("../../etc/passwd")).toBeNull(); }); test("resolves a plugin-bundled app, which the viewer can open too", () => { const pluginDir = join(getWorkspacePluginsDir(), "acme"); mkdirSync(join(pluginDir, "apps", "acme-dashboard"), { recursive: true }); writeFileSync( join(pluginDir, "package.json"), JSON.stringify({ name: "acme", version: "1.0.0" }), ); writeFileSync( join(pluginDir, "apps", "acme-dashboard", "index.html"), "

Plugin app

", ); expect(buildVisibleAppContext("plugins~acme~acme-dashboard")).toEqual({ appId: "plugins~acme~acme-dashboard", name: "acme-dashboard", slug: "acme-dashboard", pluginName: "acme", }); }); test("returns null for a plugin app whose plugin is not installed", () => { expect(buildVisibleAppContext("plugins~not-a-plugin~x")).toBeNull(); }); }); // --------------------------------------------------------------------------- // End-to-end through the injector chain: the app frozen on the live // conversation at turn start must reach the rendered `` block. // --------------------------------------------------------------------------- describe("visible_app injection", () => { const CONVERSATION_ID = "visible-app-injection-conv"; const baseMessages: Message[] = [ { role: "user", content: [{ type: "text", text: "make the header bigger" }], }, ]; function seedConversation(currentTurnVisibleAppId: string | undefined): void { setConversation( CONVERSATION_ID, asConversation({ conversationId: CONVERSATION_ID, workingDir: "/sandbox", workspaceTopLevelContext: "", workspaceTopLevelDirty: false, surfaceState: new Map(), currentTurnVisibleAppId, currentTurnTemporalSnapshot: { clientTimezone: null, timeSinceLastMessage: null, }, }), ); } afterEach(() => { clearConversations(); }); async function injectedText(): Promise { const { messages } = await applyRuntimeInjections(baseMessages, { conversationId: CONVERSATION_ID, requestId: "visible-app-req", turnIndex: 0, trust: { sourceChannel: "vellum" as const, trustClass: "guardian" as const, }, }); return messages[0].content .filter((b): b is { type: "text"; text: string } => b.type === "text") .map((b) => b.text) .join("\n"); } test("renders the frozen visible app inside ", async () => { const app = createApp({ name: "Grocery List", schemaJson: "{}", htmlDefinition: "

Hello

", }); seedConversation(app.id); const text = await injectedText(); expect(text).toContain(""); expect(text).toContain(`visible_app: "Grocery List" (app_id: "${app.id}"`); }); test("emits no visible_app line when no app was in view", async () => { seedConversation(undefined); const text = await injectedText(); expect(text).toContain(""); expect(text).not.toContain("visible_app:"); }); });