Path: blob/main/tests/unit/project/file-information-cache.test.ts
12925 views
/*1* file-information-cache.test.ts2*3* Tests for fileInformationCache path normalization4* Related to issue #139555*6* Copyright (C) 2026 Posit Software, PBC7*/89import { unitTest } from "../../test.ts";10import { assert } from "testing/asserts";11import { asMappedString } from "../../../src/core/lib/mapped-text.ts";12import { existsSync } from "../../../src/deno_ral/fs.ts";13import { join, relative } from "../../../src/deno_ral/path.ts";14import {15ensureFileInformationCache,16FileInformationCacheMap,17} from "../../../src/project/project-shared.ts";18import { createMockProjectContext } from "./utils.ts";1920// deno-lint-ignore require-await21unitTest(22"fileInformationCache - same path returns same entry",23async () => {24const project = createMockProjectContext();2526// Use cross-platform absolute path (backslashes on Windows, forward on Linux)27const path1 = join(project.dir, "doc.qmd");28const path2 = join(project.dir, "doc.qmd");2930const entry1 = ensureFileInformationCache(project, path1);31const entry2 = ensureFileInformationCache(project, path2);3233assert(34entry1 === entry2,35"Same path should return same cache entry",36);37assert(38project.fileInformationCache.size === 1,39"Should have exactly one cache entry",40);41},42);4344// deno-lint-ignore require-await45unitTest(46"fileInformationCache - different paths create different entries",47async () => {48const project = createMockProjectContext();4950const path1 = join(project.dir, "doc1.qmd");51const path2 = join(project.dir, "doc2.qmd");5253const entry1 = ensureFileInformationCache(project, path1);54const entry2 = ensureFileInformationCache(project, path2);5556assert(57entry1 !== entry2,58"Different paths should return different cache entries",59);60assert(61project.fileInformationCache.size === 2,62"Should have two cache entries for different paths",63);64},65);6667// deno-lint-ignore require-await68unitTest(69"fileInformationCache - cache entry persists across calls",70async () => {71const project = createMockProjectContext();7273const path = join(project.dir, "doc.qmd");7475// First call creates entry76const entry1 = ensureFileInformationCache(project, path);77// Modify the entry78entry1.metadata = { title: "Test" };7980// Second call should return same entry with our modification81const entry2 = ensureFileInformationCache(project, path);8283assert(84entry2.metadata?.title === "Test",85"Cache entry should persist modifications",86);87assert(88entry1 === entry2,89"Should return same cache entry object",90);91},92);9394// deno-lint-ignore require-await95unitTest(96"ensureFileInformationCache - creates FileInformationCacheMap when cache is missing",97async () => {98const project = createMockProjectContext();99// Simulate minimal ProjectContext without cache (as in command-utils.ts)100// deno-lint-ignore no-explicit-any101(project as any).fileInformationCache = undefined;102103ensureFileInformationCache(project, join(project.dir, "doc.qmd"));104105assert(106project.fileInformationCache instanceof FileInformationCacheMap,107"Should create FileInformationCacheMap, not plain Map",108);109},110);111112// deno-lint-ignore require-await113unitTest(114"fileInformationCache - relative and absolute paths share same entry",115async () => {116const project = createMockProjectContext();117118const absolutePath = join(project.dir, "subdir", "page.qmd");119const relativePath = relative(Deno.cwd(), absolutePath);120121const entry1 = ensureFileInformationCache(project, relativePath);122const entry2 = ensureFileInformationCache(project, absolutePath);123124assert(125entry1 === entry2,126"Relative and absolute paths to same file should share a cache entry",127);128assert(129project.fileInformationCache.size === 1,130"Should have exactly one cache entry",131);132},133);134135// deno-lint-ignore require-await136unitTest(137"fileInformationCache - invalidateForFile deletes transient notebook file",138async () => {139const project = createMockProjectContext();140const sourcePath = join(project.dir, "doc.qmd");141142// Create a real temp file simulating a transient .quarto_ipynb143const notebookPath = join(project.dir, "doc.quarto_ipynb");144Deno.writeTextFileSync(notebookPath, '{"cells": []}');145assert(existsSync(notebookPath), "Temp notebook file should exist");146147// Populate cache entry with a transient target pointing to the file148const entry = ensureFileInformationCache(project, sourcePath);149entry.target = {150source: sourcePath,151input: notebookPath,152markdown: asMappedString(""),153metadata: {},154data: { transient: true, kernelspec: {} },155};156157// Invalidate the cache entry for this file158project.fileInformationCache.invalidateForFile(sourcePath);159160// The transient file should be deleted from disk161assert(162!existsSync(notebookPath),163"Transient notebook file should be deleted on invalidation",164);165// The cache entry should be removed166assert(167!project.fileInformationCache.has(sourcePath),168"Cache entry should be removed after invalidation",169);170},171);172173// deno-lint-ignore require-await174unitTest(175"fileInformationCache - invalidateForFile preserves non-transient files",176async () => {177const project = createMockProjectContext();178const sourcePath = join(project.dir, "notebook.ipynb");179180// Create a real file simulating a user's .ipynb (non-transient)181const notebookPath = join(project.dir, "notebook.ipynb");182Deno.writeTextFileSync(notebookPath, '{"cells": []}');183184// Populate cache entry with a non-transient target185const entry = ensureFileInformationCache(project, sourcePath);186entry.target = {187source: sourcePath,188input: notebookPath,189markdown: asMappedString(""),190metadata: {},191data: { transient: false, kernelspec: {} },192};193194// Invalidate the cache entry195project.fileInformationCache.invalidateForFile(sourcePath);196197// The non-transient file should NOT be deleted198assert(199existsSync(notebookPath),200"Non-transient file should be preserved on invalidation",201);202// But the cache entry should still be removed203assert(204!project.fileInformationCache.has(sourcePath),205"Cache entry should be removed after invalidation",206);207},208);209210// deno-lint-ignore require-await211unitTest(212"fileInformationCache - invalidateForFile handles entry with no target",213async () => {214const project = createMockProjectContext();215const sourcePath = join(project.dir, "doc.qmd");216217// Populate cache entry with metadata only (no target)218const entry = ensureFileInformationCache(project, sourcePath);219entry.metadata = { title: "Test" };220221// Should not throw222project.fileInformationCache.invalidateForFile(sourcePath);223224assert(225!project.fileInformationCache.has(sourcePath),226"Cache entry should be removed even without a target",227);228},229);230231// deno-lint-ignore require-await232unitTest(233"fileInformationCache - invalidateForFile is a no-op for missing keys",234async () => {235const project = createMockProjectContext();236237// Should not throw on a key that doesn't exist238project.fileInformationCache.invalidateForFile(239join(project.dir, "nonexistent.qmd"),240);241242assert(243project.fileInformationCache.size === 0,244"Cache should remain empty",245);246},247);248249250