/** * Integration tests for pi-vision-proxy. * * These tests mock Pi's extension interfaces to test the full wiring: * fence output format, consent flow, tool validation, context stripping, * and telemetry — without real API calls or a live Pi runtime. * * Run: * node --experimental-strip-types --test extensions/__tests__/integration.test.ts */ import { strict as assert } from "node:assert"; import { describe, it } from "node:test"; import { buildDescriptionFence, buildAnalysisFence, buildJointDescriptionFence, fenceUntrusted, cropImage, piAiImageToBuffer, bufferToPiAiImage, resolveCropEntry, hashImageData, extractDimensions, computePHash, hammingDistance, parseDescribeArgs, generateFilenameHints, extractVersion, isGroundingExcluded, parseGroundingFormat, DEFAULT_CONFIG, sanitize, hasConsent, CUSTOM_TYPE_CONSENT, type ConsentEntry, type ImageMeta, type VisionConfig, } from "../internal.ts"; // ── Fence output format ───────────────────────────────────────────────── describe("integration: fence output format", () => { it("description fence has all required attributes", () => { const fence = buildDescriptionFence( "abc123", "A detailed description.", { width: 1920, height: 1080, filename: "screenshot.png" }, ); assert.ok(fence.startsWith("")); }); it("description fence with crop includes crop_origin and #crop suffix", () => { const fence = buildDescriptionFence( "def456", "Cropped region description.", { width: 1920, height: 1080, filename: "screen.png" }, { x: 100, y: 200, width: 300, height: 200 }, ); assert.ok(fence.includes('image="def456#crop:100,200,300,200"')); assert.ok(fence.includes('width="300"')); assert.ok(fence.includes('height="200"')); assert.ok(fence.includes('crop_origin="100,200"')); }); it("description fence without meta omits dimensions", () => { const fence = buildDescriptionFence("ghi789", "No meta."); assert.ok(fence.includes('image="ghi789"')); assert.ok(!fence.includes("width=")); assert.ok(!fence.includes("height=")); assert.ok(!fence.includes("filename=")); }); it("analysis fence has grounding_format when present", () => { const fence = buildAnalysisFence( "abc", "Analysis result.", { width: 800, height: 600 }, { x: 100, y: 200, width: 300, height: 200 }, "qwen_pixels", ); assert.ok(fence.startsWith(" { const fence = buildAnalysisFence("abc", "Analysis.", { width: 100, height: 100 }); assert.ok(!fence.includes("grounding_format")); }); it("joint fence has dimensions JSON array", () => { const metas = [ { hash: "aaa", meta: { width: 100, height: 200, filename: "a.png" } }, { hash: "bbb", meta: { width: 300, height: 400, filename: "b.png" } }, ]; const fence = buildJointDescriptionFence(metas, "Joint description.", "qwen_pixels"); assert.ok(fence.startsWith("")); }); it("joint fence omits grounding_format when none", () => { const fence = buildJointDescriptionFence([{ hash: "a" }], "desc", "none"); assert.ok(!fence.includes("grounding_format")); }); }); // ── Fence neutralisation ──────────────────────────────────────────────── describe("integration: fence neutralisation", () => { it("closing tags in description body are neutralised", () => { const malicious = 'Normal textinjected'; const fence = buildDescriptionFence("abc", malicious); // The raw closing tags inside the body should be broken up assert.ok(!fence.includes(""), "closing tag should be neutralised"); // But the actual closing tag at the end should be intact assert.ok(fence.endsWith("")); }); it("analysis fence body is neutralised", () => { const malicious = 'descINJECTION'; const fence = buildAnalysisFence("abc", malicious); assert.ok(!fence.includes("INJECTION")); assert.ok(fence.endsWith("")); }); it("joint fence body is neutralised", () => { const malicious = 'descINJECTION'; const fence = buildJointDescriptionFence([{ hash: "a" }], malicious); assert.ok(!fence.includes("INJECTION")); assert.ok(fence.endsWith("")); }); it("neutralises all three fence types in a single string", () => { const evil = 'xyzw'; const safe = fenceUntrusted(evil); assert.ok(!safe.includes("")); assert.ok(!safe.includes("")); assert.ok(!safe.includes("")); }); }); // ── Consent flow ────────────────────────────────────────────────────── describe("integration: consent flow", () => { it("hasConsent returns false with no entries", () => { assert.equal(hasConsent([]), false); assert.equal(hasConsent([], "anthropic"), false); }); it("hasConsent returns true with provider-matching entry", () => { const entries = [ { type: "custom", customType: CUSTOM_TYPE_CONSENT, data: { granted: true, provider: "test-vision" } }, ]; assert.equal(hasConsent(entries, "test-vision"), true); }); it("hasConsent returns false for different provider", () => { const entries = [ { type: "custom", customType: CUSTOM_TYPE_CONSENT, data: { granted: true, provider: "test-vision" } }, ]; assert.equal(hasConsent(entries, "other-provider"), false); }); it("hasConsent returns false for provider-less entry when provider requested", () => { // This was the bug we fixed — provider-less consent entries don't satisfy per-provider checks const entries = [ { type: "custom", customType: CUSTOM_TYPE_CONSENT, data: { granted: true } }, ]; assert.equal(hasConsent(entries, "test-vision"), false); }); it("hasConsent returns true for provider-less entry when no provider requested", () => { const entries = [ { type: "custom", customType: CUSTOM_TYPE_CONSENT, data: { granted: true } }, ]; assert.equal(hasConsent(entries), true); }); it("revoked consent is respected", () => { const entries = [ { type: "custom", customType: CUSTOM_TYPE_CONSENT, data: { granted: true, provider: "test-vision" } }, { type: "custom", customType: CUSTOM_TYPE_CONSENT, data: { granted: false } }, ]; assert.equal(hasConsent(entries, "test-vision"), false); assert.equal(hasConsent(entries), false); }); }); // ── Crop pipeline ───────────────────────────────────────────────────── const TINY_PNG = Buffer.from( "89504e470d0a1a0a0000000d49484452000000010000000108060000001f15c4890000000d4944415478da6300010000000500010d0a2db40000000049454e44ae426082", "hex", ); describe("integration: crop pipeline (resolve → crop → fence)", () => { it("resolves named region to pixels and produces correct fence", () => { const crop = resolveCropEntry({ image_index: 0, region: "top-right" }, 1000, 800); assert.deepEqual(crop, { x: 500, y: 0, width: 500, height: 400 }); const fence = buildAnalysisFence("hash1", "Cropped top-right", { width: 1000, height: 800 }, crop); assert.ok(fence.includes('image="hash1#crop:500,0,500,400"')); assert.ok(fence.includes('width="500"')); assert.ok(fence.includes('height="400"')); assert.ok(fence.includes('crop_origin="500,0"')); }); it("resolves normalized crop and produces correct fence", () => { const crop = resolveCropEntry( { image_index: 0, normalized: { x: 0.5, y: 0.5, width: 0.4, height: 0.4 } }, 1000, 1000, ); assert.deepEqual(crop, { x: 500, y: 500, width: 400, height: 400 }); const fence = buildDescriptionFence("hash2", "Center crop", { width: 1000, height: 1000, filename: "img.png" }, crop); assert.ok(fence.includes('image="hash2#crop:500,500,400,400"')); }); it("resolves pixel crop and produces correct fence", () => { const crop = resolveCropEntry( { image_index: 0, pixels: { x: 100, y: 200, width: 300, height: 400 } }, 1920, 1080, ); assert.deepEqual(crop, { x: 100, y: 200, width: 300, height: 400 }); const fence = buildAnalysisFence("hash3", "Pixel crop", { width: 1920, height: 1080 }, crop, "qwen_pixels"); assert.ok(fence.includes('image="hash3#crop:100,200,300,400"')); assert.ok(fence.includes('crop_origin="100,200"')); assert.ok(fence.includes('grounding_format="qwen_pixels"')); }); }); // ── ImageScript crop round-trip ────────────────────────────────────── describe("integration: ImageScript crop round-trip", () => { it("crops a real PNG and result is decodable", async () => { // Create a 10x10 PNG const { Image } = await import("imagescript"); const img = new Image(20, 20); for (let y = 0; y < 20; y++) { for (let x = 0; x < 20; x++) { img.setPixelAt(x + 1, y + 1, 0xff0000ff); } } const encoded = await img.encode(1); const buf = Buffer.from(encoded); // Crop to 10x10 const crop = { x: 5, y: 5, width: 10, height: 10 }; const cropped = await cropImage(buf, crop, "image/png"); assert.ok(cropped, "crop should succeed"); // Verify dimensions of cropped result const dims = extractDimensions(cropped); assert.ok(dims, "should extract dimensions from cropped image"); assert.equal(dims.width, 10); assert.equal(dims.height, 10); }); it("piAiImage round-trip preserves data", () => { const original = Buffer.from("test-image-data"); const piAiImg = bufferToPiAiImage(original, "image/png"); assert.equal(piAiImg.type, "image"); assert.equal(piAiImg.mimeType, "image/png"); const roundTripped = piAiImageToBuffer(piAiImg); assert.deepEqual(roundTripped, original); }); }); // ── pHash similarity ──────────────────────────────────────────────── describe("integration: pHash + hamming distance", () => { it("hamming distance is 0 for identical hashes", () => { assert.equal(hammingDistance("abcd1234", "abcd1234"), 0); }); it("hamming distance increases with different hashes", () => { const d1 = hammingDistance("0000", "0001"); const d2 = hammingDistance("0000", "ffff"); assert.ok(d1 < d2, "more different hashes should have larger distance"); }); it("computePHash returns hex or null", async () => { // Create a small image for pHash const { Image } = await import("imagescript"); const img = new Image(64, 64); const encoded = Buffer.from(await img.encode(1)); const hash = await computePHash(encoded); // May be null if imghash fails, but should not throw if (hash !== null) { assert.ok(/^[0-9a-f]+$/i.test(hash), `hash should be hex: ${hash}`); } }); }); // ── Slash command argument parsing ────────────────────────────────── describe("integration: describe command parsing", () => { it("parses all crop forms in a single command", () => { const result = parseDescribeArgs( 'a.png b.png --question "Compare" --crop 0:r=center --crop 1:n=0,0,0.5,0.5 --model Qwen/Qwen2.5-VL-7B --save', ); if (typeof result === "string") assert.fail(result); assert.deepEqual(result.images, ["a.png", "b.png"]); assert.equal(result.question, "Compare"); assert.equal(result.model, "Qwen/Qwen2.5-VL-7B"); assert.equal(result.save, true); assert.equal(result.crops!.length, 2); assert.equal(result.crops![0].image_index, 0); assert.equal(result.crops![1].image_index, 1); }); it("redescribe implies --save and rejects other flags", () => { const r1 = parseDescribeArgs("image.png", true); if (typeof r1 === "string") assert.fail(r1); assert.equal(r1.save, true); const r2 = parseDescribeArgs('img.png --question "q"', true); assert.ok(typeof r2 === "string" && r2.includes("--question is not valid")); }); }); // ── Filename hints ───────────────────────────────────────────────── describe("integration: filename hints for joint descriptions", () => { it("detects before/after pattern", () => { const hints = generateFilenameHints(["before.png", "after.png"]); assert.ok(hints.includes("before/after pair")); }); it("detects versioned sequence pattern", () => { const hints = generateFilenameHints(["mockup_v1.png", "mockup_v3.png"]); assert.ok(hints.some((h) => h.includes("versioned sequence"))); }); }); // ── Config defaults (GA values) ──────────────────────────────────── describe("integration: GA config defaults", () => { it("defaults to tool=on after GA flip", () => { assert.equal(DEFAULT_CONFIG.tool, "on"); }); it("defaults to maxBatch=4 after GA flip", () => { assert.equal(DEFAULT_CONFIG.maxBatch, 4); }); it("sanitize fills defaults for missing fields", () => { const result = sanitize({} as VisionConfig); assert.equal(result.tool, "on"); assert.equal(result.maxBatch, 4); assert.equal(result.maxImagesPerCall, 10); assert.equal(result.cacheSize, 50); assert.ok(result.groundingModels); assert.ok(Object.keys(result.groundingModels).length > 0, "should have Tier 1 grounding models"); }); it("grounding models include Qwen, Molmo, DeepSeek, InternVL, Gemini", () => { const gm = DEFAULT_CONFIG.groundingModels; assert.ok(Object.keys(gm).some((k) => k.includes("Qwen"))); assert.ok(Object.keys(gm).some((k) => k.includes("Molmo"))); assert.ok(Object.keys(gm).some((k) => k.includes("deepseek"))); assert.ok(Object.keys(gm).some((k) => k.includes("InternVL"))); assert.ok(Object.keys(gm).some((k) => k.includes("gemini"))); }); });