UNPKG

@simplepg/repo

Version:

SimplePage repository

3,040 lines 132 kB
import { jest } from '@jest/globals'
import 'fake-indexeddb/auto'
import { IDBFactory } from "fake-indexeddb";
import { globSource } from '@helia/unixfs'
import all from 'it-all'
import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts'
import { Buffer } from 'node:buffer';
import { join } from 'path';
import { CID } from 'multiformats/cid'
import { JSDOM } from 'jsdom'
import { resolveEnsDomain } from '@simplepg/common'
import { MockStorage, TestEnvironmentNode } from '@simplepg/test-utils';

import { Repo } from '../src/repo.js';


// Mock DOMParser for Node.js environment
const dom = new JSDOM()
global.DOMParser = dom.window.DOMParser

const resetIDB = () => global.indexedDB = new IDBFactory()
const checkMeta = (doc, name, content) => {
  const meta = doc.querySelector(`meta[name="${name}"]`)
  expect(meta).toBeDefined()
  expect(meta.content).toBe(content)
}

const cat = async (kubo, path) => {
  const content = await all(await kubo.cat(path))
  return new TextDecoder().decode(content[0])
}

const catBytes = async (kubo, path) => {
  const content = await all(await kubo.cat(path))
  if (content.length === 1) return content[0]
  const length = content.reduce((sum, chunk) => sum + chunk.length, 0)
  const merged = new Uint8Array(length)
  let offset = 0
  for (const chunk of content) {
    merged.set(chunk, offset)
    offset += chunk.length
  }
  return merged
}

const ls = async (kubo, path) => {
  const files = await all(await kubo.ls(path))
  return files.map(file => file.name)
}

jest.setTimeout(10000);

import Parser from 'rss-parser';

describe('Repo Integration Tests', () => {
  let testEnv;
  let addresses;
  let client;
  let walletClient;
  let storage;
  let repo;
  let templateCid;
  let testDataCid;
  let parser;

  beforeAll(async () => {
    testEnv = new TestEnvironmentNode();
    await testEnv.start();
    addresses = testEnv.addresses;
    parser = new DOMParser()
    
    // Mint subscriptions for new.simplepage.eth and test.eth
    testEnv.evm.mintPage('new.simplepage.eth', 365 * 24 * 60 * 60, '0x0000000000000000000000000000000000000001');
    testEnv.evm.mintPage('test.eth', 365 * 24 * 60 * 60, '0x0000000000000000000000000000000000000001');

    // Set up the resolver for new.simplepage.eth (template domain)
    testEnv.evm.setResolver(addresses.universalResolver, 'new.simplepage.eth', addresses.resolver1);
    testEnv.evm.setTextRecord(addresses.resolver1, 'new.simplepage.eth', 'dservice', testEnv.dserviceUrl);
    
    // Set up a test domain
    testEnv.evm.setResolver(addresses.universalResolver, 'test.eth', addresses.resolver1);
    
    // Add some test content to IPFS for the template
    async function loadFixtures(path) {
      const glob = globSource(join(process.cwd(), path), '**/*')
      const entries = await all(glob)
      const result = await all(await testEnv.kubo.kuboApi.addAll(entries, { wrapWithDirectory: true }))
      return result[result.length - 1].cid.toV1()
    }
    templateCid = await loadFixtures('./test/__fixtures__/new.simplepage.eth')
    testDataCid = await loadFixtures('./test/__fixtures__/test.eth')

    client = createPublicClient({
      transport: http(testEnv.evm.url)
    });

    const account = privateKeyToAccount(testEnv.evm.secretKey)

    walletClient = createWalletClient({
      chain: testEnv.evm.chain,
      transport: http(testEnv.evm.url),
      account
    });
  });

  afterAll(async () => {
    await testEnv.stop();
  });

  beforeEach(async () => {
    storage = new MockStorage();
    repo = new Repo('test.eth', storage);
  });

  afterEach(async () => {
    storage.clear();
    await repo.close()
  });

  describe('Constructor and init', () => {
    it('should construct Repo with domain and storage', () => {
      expect(repo.domain).toBe('test.eth');
      expect(repo.storage).toBe(storage);
      expect(repo.dservice).toBeDefined();
    });

    it('should fail initialization if test.eth has no contenthash', async () => {
      await expect(repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      })).rejects.toThrow('Repo root not found for test.eth');
    });

    it('should fail to initialize repo with viem client when test domain has no contenthash', async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'new.simplepage.eth', templateCid.toString());
      
      await expect(repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      })).rejects.toThrow('Repo root not found');
    });

    it('should initialize repo with viem client when test domain has contenthash', async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'test.eth', testDataCid.toString());
      
      await repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      expect(repo.viemClient).toBe(client);
      expect(repo.chainId).toBe(parseInt(testEnv.evm.chainId));
      expect(repo.universalResolver).toBe(addresses.universalResolver);
      expect(repo.repoRoot).toBeDefined();
      expect(repo.templateRoot).toBeDefined();
    });
  });

  describe('Changes', () => {
    beforeAll(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'new.simplepage.eth', templateCid.toString());
      testEnv.evm.setContenthash(addresses.resolver1, 'test.eth', templateCid.toString());
    });

    beforeEach(async () => {
      await repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });
    });

    it('should get markdown without active edits', async () => {
      const markdown = await repo.getMarkdown('/');
      expect(markdown).toBeDefined();
      expect(typeof markdown).toBe('string');
    });

    it('should get markdown with active page edits', async () => {
      const testMarkdown = '# Test Page\n\nThis is a test.';
      const testBody = '<h1>Test Page</h1><p>This is a test.</p>';
      
      await repo.setPageEdit('/', testMarkdown, testBody);
      
      const markdown = await repo.getMarkdown('/');
      expect(markdown).toBe(testMarkdown);
    });

    it('should get HTML body without active edits', async () => {
      const htmlBody = await repo.getHtmlBody('/');
      expect(htmlBody).toBeDefined();
      expect(typeof htmlBody).toBe('string');
    });

    it('should get HTML body with active edits', async () => {
      const testMarkdown = '# Test Page\n\nThis is a test.';
      const testBody = '<h1>Test Page</h1><p>This is a test.</p>';
      
      await repo.setPageEdit('/', testMarkdown, testBody);
      
      const htmlBody = await repo.getHtmlBody('/');
      expect(htmlBody).toBe(testBody);
    });

    it('should get HTML body ignoring edits when requested', async () => {
      const testMarkdown = '# Test Page\n\nThis is a test.';
      const testBody = '<h1>Test Page</h1><p>This is a test.</p>';
      
      await repo.setPageEdit('/', testMarkdown, testBody);
      
      const htmlBody = await repo.getHtmlBody('/', true);
      expect(htmlBody).not.toBe(testBody);
      expect(htmlBody).toBeDefined();
    });

    it('should set page edits correctly', async () => {
      const testMarkdown = '# Test Page\n\nThis is a test.';
      const testBody = '<h1>Test Page</h1><p>This is a test.</p>';
      
      await repo.setPageEdit('/', testMarkdown, testBody);
      
      const storedData = storage.getItem('spg_edit_/');
      expect(storedData).toBeDefined();
      
      const parsedData = JSON.parse(storedData);
      expect(parsedData.markdown).toBe(testMarkdown);
      expect(parsedData.body).toBe(testBody);
      expect(parsedData.root).toBe(repo.repoRoot.cid.toString());
    });

    it('should list staged edits', async () => {
      const testMarkdown = '# Test Page\n\nThis is a test.';
      const testBody = '<h1>Test Page</h1><p>This is a test.</p>';
      
      await repo.setPageEdit('/', testMarkdown, testBody);
      await repo.setPageEdit('/about/', '# About\n\nAbout page content.', '<h1>About</h1><p>About page content.</p>');
      
      const changes = await repo.getChanges();
      const paths = changes.map(change => change.path);
      expect(paths).toContain('/');
      expect(paths).toContain('/about/');
      expect(changes.length).toBe(2);
      expect(changes[0].type).toBe('edit');
      expect(changes[1].type).toBe('new');
    });

    it('should validate path format in setPageEdit', async () => {
      const testMarkdown = '# Test Page\n\nThis is a test.';
      const testBody = '<h1>Test Page</h1><p>This is a test.</p>';
      
      // Should throw for invalid paths
      await expect(repo.setPageEdit('invalid', testMarkdown, testBody)).rejects.toThrow('Path must start with /');
      await expect(repo.setPageEdit('/invalid', testMarkdown, testBody)).rejects.toThrow('Path must end with /');
      
      // Should work for valid paths
      await expect(repo.setPageEdit('/', testMarkdown, testBody)).resolves.not.toThrow();
      await expect(repo.setPageEdit('/about/', testMarkdown, testBody)).resolves.not.toThrow();
      await expect(repo.setPageEdit('/rss/', testMarkdown, testBody)).resolves.not.toThrow();
      await expect(repo.setPageEdit('/rss.xml/', testMarkdown, testBody)).rejects.toThrow('Cannot create page at reserved path /rss.xml or /feed');
      await expect(repo.setPageEdit('/feed/', testMarkdown, testBody)).rejects.toThrow('Cannot create page at reserved path /rss.xml or /feed');
    });

    it('should handle restorePage functionality', async () => {
      // Create initial pages
      await repo.setPageEdit('/about/', '# About\n\nAbout content.', '<h1>About</h1><p>About content.</p>');
      await repo.setPageEdit('/blog/', '# Blog\n\nBlog content.', '<h1>Blog</h1><p>Blog content.</p>');

      const currentBlock = Number(testEnv.evm.getBlockNumber());
      await testEnv.waitUntilBlockIsIndexed(currentBlock);
      
      // Stage and commit initial pages
      const firstResult = await repo.stage('test.eth', false);
      const hash1 = await walletClient.writeContract(firstResult.prepTx);
      await client.waitForTransactionReceipt({ hash: hash1 });
      await new Promise(resolve => setTimeout(resolve, 1000));
      await repo.finalizeCommit(firstResult.cid);
      
      // Delete a page
      await repo.deletePage('/about/');
      
      // Verify deletion is staged
      let changes = await repo.getChanges();
      expect(changes.length).toBe(1);
      expect(changes[0].type).toBe('delete');
      expect(changes[0].path).toBe('/about/');
      
      // Restore the deleted page
      await repo.restorePage('/about/');
      
      // Verify the deletion is removed from changes
      changes = await repo.getChanges();
      expect(changes.length).toBe(0);
    });

    it('should handle restoreAllPages functionality', async () => {
      // Create multiple pages with edits
      await repo.setPageEdit('/about/', '# About\n\nAbout content.', '<h1>About</h1><p>About content.</p>');
      await repo.setPageEdit('/blog/', '# Blog\n\nBlog content.', '<h1>Blog</h1><p>Blog content.</p>');
      await repo.setPageEdit('/contact/', '# Contact\n\nContact info.', '<h1>Contact</h1><p>Contact info.</p>');
      await repo.setPageEdit('/docs/', '# Docs\n\nDocumentation.', '<h1>Docs</h1><p>Documentation.</p>');
      
      // Verify all edits exist
      let changes = await repo.getChanges();
      expect(changes.length).toBe(4);
      expect(changes.map(c => c.path)).toContain('/about/');
      expect(changes.map(c => c.path)).toContain('/blog/');
      expect(changes.map(c => c.path)).toContain('/contact/');
      expect(changes.map(c => c.path)).toContain('/docs/');
      
      // Restore all pages
      repo.restoreAllPages();
      
      // Verify all edits are removed
      changes = await repo.getChanges();
      expect(changes.length).toBe(0);
      
      // Verify individual pages are also restored
      const aboutData = storage.getItem('spg_edit_/about/');
      expect(aboutData).toBeNull();
      
      const blogData = storage.getItem('spg_edit_/blog/');
      expect(blogData).toBeNull();
      
      const contactData = storage.getItem('spg_edit_/contact/');
      expect(contactData).toBeNull();
      
      const docsData = storage.getItem('spg_edit_/docs/');
      expect(docsData).toBeNull();
    });
  });

  describe('Version Management', () => {
    beforeAll(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'new.simplepage.eth', templateCid.toString());
    });

    it('should check if new version is available', async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'test.eth', templateCid.toString());
      await repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });
      const versionInfo = await repo.isNewVersionAvailable();
      
      expect(versionInfo).toHaveProperty('templateVersion');
      expect(versionInfo).toHaveProperty('currentVersion');
      expect(versionInfo).toHaveProperty('canUpdate');
      expect(typeof versionInfo.templateVersion).toBe('string');
      expect(typeof versionInfo.currentVersion).toBe('string');
      expect(versionInfo.canUpdate).toBe(false);
    });

    it('should detect when template version is newer', async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'test.eth', testDataCid.toString());
      await repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });
      
      const versionInfo = await repo.isNewVersionAvailable();
      expect(versionInfo.canUpdate).toBe(true);
    });

    it('should persist pages across version updates', async () => {
      // Start with test.eth using testDataCid
      testEnv.evm.setContenthash(addresses.resolver1, 'test.eth', testDataCid.toString());
      await repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      // Add a few pages and commit without version update
      const pages = {
        about: {
          path: '/about/',
          markdown: `---
title: About
description: About page content.
---

# About

About page content.`,
          body: '<h1>About</h1><p>About page content.</p>'
        },
        blog: {
          path: '/blog/',
          markdown: `---
title: Blog
description: Blog index page.
sidebar-toc: true
---

# Blog

Blog index page.`,
          body: '<h1>Blog</h1><p>Blog index page.</p>'
        },
        contact: {
          path: '/contact/',
          markdown: `---
title: Contact
description: Contact information.
sidebar-toc: true
sidebar-nav-prio: 100
---

# Contact

Contact information.`,
          body: '<h1>Contact</h1><p>Contact information.</p>'
        }
      };

      // Set page edits
      for (const page of Object.values(pages)) {
        await repo.setPageEdit(page.path, page.markdown, page.body);
      }

      expect(await repo.getMetadata('/about/')).toEqual({
        title: 'About',
        description: 'About page content.',
      })
      
      expect(await repo.getMetadata('/blog/')).toEqual({
        title: 'Blog',
        description: 'Blog index page.',
        'sidebar-toc': true
      })
      
      
      expect(await repo.getMetadata('/contact/')).toEqual({
        title: 'Contact',
        description: 'Contact information.',
        'sidebar-toc': true,
        'sidebar-nav-prio': 100
      })
      

      // Stage and commit without version update
      const firstResult = await repo.stage('test.eth', false);
      expect(firstResult).toHaveProperty('cid');
      expect(firstResult.cid instanceof CID).toBe(true);

      // Verify all pages are staged correctly
      for (const page of Object.values(pages)) {
        const markdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}${page.path}index.md`);
        expect(markdown).toBe(page.markdown);

        const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}${page.path}index.html`);
        expect(html).toContain(page.body);
      }

      // Commit the first version
      await repo.finalizeCommit(firstResult.cid);

      // Edit the root page and commit with version update
      const updatedRootMarkdown = '# Updated Home Page\n\nThis is the updated home page content.';
      const updatedRootBody = '<h1>Updated Home Page</h1><p>This is the updated home page content.</p>';
      
      await repo.setPageEdit('/', updatedRootMarkdown, updatedRootBody);
      
      const secondResult = await repo.stage('test.eth', true);
      expect(secondResult).toHaveProperty('cid');
      expect(secondResult.cid instanceof CID).toBe(true);

      // Verify the root page is updated
      const updatedRootMarkdownContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/index.md`);
      expect(updatedRootMarkdownContent).toBe(updatedRootMarkdown);

      const updatedRootHtmlContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/index.html`);
      expect(updatedRootHtmlContent).toContain(updatedRootBody);

      // Verify that pages added before are still there
      for (const page of Object.values(pages)) {
        const markdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}${page.path}index.md`);
        expect(markdown).toBe(page.markdown);

        const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}${page.path}index.html`);
        expect(html).toContain(page.body);
      }

      // Verify version is updated in the template
      const templateContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_template.html`);
      expect(templateContent).toBeDefined();
      const templateDoc = parser.parseFromString(templateContent, 'text/html');
      checkMeta(templateDoc, 'version', '0.5.0');

      // Verify _assets and _js folders are updated
      const assetsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_assets`);
      expect(assetsContent).toBe('folder-updated');

      const jsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_js`);
      expect(jsContent).toBe('folder-updated');

      // Commit the second version
      const hash2 = await walletClient.writeContract(secondResult.prepTx);
      expect(hash2).toBeDefined();
      const transaction2 = await client.waitForTransactionReceipt({ hash: hash2 });
      expect(transaction2.status).toBe('success');

      // Verify the final state through ENS resolution
      const { cid: finalRoot } = await resolveEnsDomain(client, 'test.eth', addresses.universalResolver);
      expect(finalRoot.toString()).toBe(secondResult.cid.toString());

      expect(await repo.getMetadata('/about/')).toEqual({
        title: 'About',
        description: 'About page content.',
      })
      
      expect(await repo.getMetadata('/blog/')).toEqual({
        title: 'Blog',
        description: 'Blog index page.',
        'sidebar-toc': true
      })
      
      
      expect(await repo.getMetadata('/contact/')).toEqual({
        title: 'Contact',
        description: 'Contact information.',
        'sidebar-toc': true,
        'sidebar-nav-prio': 100
      })
    });
  });

  describe('Staging and Finalization', () => {
    beforeAll(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'new.simplepage.eth', templateCid.toString());
    });

    beforeEach(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'test.eth', testDataCid.toString());
      await repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });
    });

    it('should stage and commit changes without template update', async () => {
      const testMarkdown = '# Test Page\n\nThis is a test.';
      const testBody = '<h1>Test Page</h1><p>This is a test.</p>';
      
      await repo.setPageEdit('/', testMarkdown, testBody);
      
      const result = await repo.stage('test.eth', false);
      
      expect(result).toHaveProperty('cid');
      expect(result).toHaveProperty('prepTx');
      expect(result.cid instanceof CID).toBe(true);
      expect(result.prepTx).toHaveProperty('address');
      expect(result.prepTx).toHaveProperty('functionName');
      expect(result.prepTx.functionName).toBe('setContenthash');

      // verify the markdown content
      const markdown = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/index.md')
      expect(markdown).toBe(testMarkdown)

      // verify the html content
      const html = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/index.html')

      // verify the testBody is inside the root div of the html
      expect(html).toContain(testBody)
      expect(html).toMatch(/^<!DOCTYPE html>?/)
      expect(html).toContain('<title>test.eth</title>')

      // verify meta tags: version, ens-domain, description
      const doc = parser.parseFromString(html, 'text/html')
      checkMeta(doc, 'version', '0.4.0')
      checkMeta(doc, 'ens-domain', 'test.eth')
      checkMeta(doc, 'description', 'A SimplePage by test.eth')

      // verify favicon path
      const favicon = doc.querySelector('link[rel="icon"]')
      expect(favicon).toBeDefined()
      expect(favicon.href).toBe('/_assets/images/favicon-light.png')

      // verify the repo root is updated
      // uses viemClient to submit the prepTx
      const hash = await walletClient.writeContract(result.prepTx)
      expect(hash).toBeDefined()
      const transaction = await client.waitForTransactionReceipt({ hash })
      expect(transaction).toBeDefined()
      expect(transaction.status).toBe('success')
      
      const { cid: updatedRoot } = await resolveEnsDomain(client, 'test.eth', addresses.universalResolver)

      // verify the repo root is updated
      expect(updatedRoot.toString()).toBe(result.cid.toString())

      // verify the _assets folders are not updated
      const assetsContent = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/_assets')
      expect(assetsContent).toBe('folder-not-updated')

      const jsContent = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/_js')
      expect(jsContent).toBe('folder-not-updated')

      const templateContent = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/_template.html')
      expect(templateContent).toBeDefined()
      const templateDoc = parser.parseFromString(templateContent, 'text/html')
      checkMeta(templateDoc, 'version', '0.4.0')
    });

    it('should stage and commit changes with template update', async () => {
      const testMarkdown = '# Test Page\n\nThis is a test.';
      const testBody = '<h1>Test Page</h1><p>This is a test.</p>';
      
      await repo.setPageEdit('/', testMarkdown, testBody);
      
      const result = await repo.stage('test.eth', true);
      
      expect(result).toHaveProperty('cid');
      expect(result).toHaveProperty('prepTx');
      expect(result.cid instanceof CID).toBe(true);
      expect(result.prepTx).toHaveProperty('address');
      expect(result.prepTx).toHaveProperty('functionName');
      expect(result.prepTx.functionName).toBe('setContenthash');

      // verify the markdown content
      const markdown = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/index.md')
      expect(markdown).toBe(testMarkdown)

      // verify the html content
      const html = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/index.html')

      // verify the testBody is inside the root div of the html
      expect(html).toContain(testBody)
      expect(html).toContain('<title>test.eth</title>')

      // verify meta tags: version, ens-domain, description
      const doc = parser.parseFromString(html, 'text/html')
      checkMeta(doc, 'version', '0.5.0')

      // verify the _assets folders are updated
      const assetsContent = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/_assets')
      expect(assetsContent).toBe('folder-updated')

      const jsContent = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/_js')
      expect(jsContent).toBe('folder-updated')

      const templateContent = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/_template.html')
      expect(templateContent).toBeDefined()
      const templateDoc = parser.parseFromString(templateContent, 'text/html')
      checkMeta(templateDoc, 'version', '0.5.0')

      // verify the repo root is updated
      // uses viemClient to submit the prepTx
      const hash = await walletClient.writeContract(result.prepTx)
      expect(hash).toBeDefined()
      const transaction = await client.waitForTransactionReceipt({ hash })
      expect(transaction).toBeDefined()
      expect(transaction.status).toBe('success')
      
      const { cid: updatedRoot } = await resolveEnsDomain(client, 'test.eth', addresses.universalResolver)

      // verify the repo root is updated
      expect(updatedRoot.toString()).toBe(result.cid.toString())
    });

    it('should properly populate template with title and description in markdown preamble', async () => {
      const testMarkdown = `---
title: My Custom Title
description: This is a custom description for the page
---

# Test Page

This is a test page with custom title and description.`;
      
      const testBody = '<h1>Test Page</h1><p>This is a test page with custom title and description.</p>';
      
      await repo.setPageEdit('/', testMarkdown, testBody);
      
      const result = await repo.stage('test.eth', false);
      
      expect(result).toHaveProperty('cid');
      expect(result).toHaveProperty('prepTx');
      expect(result.cid instanceof CID).toBe(true);

      // verify the markdown content is preserved
      const markdown = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/index.md')
      expect(markdown).toBe(testMarkdown)

      // verify the html content
      const html = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/index.html')

      // verify the testBody is inside the root div of the html
      expect(html).toContain(testBody)

      // verify meta tags are populated with custom title and description
      const parser = new DOMParser()
      const doc = parser.parseFromString(html, 'text/html')
      
      // Check that title is set to custom title from frontmatter
      const titleElement = doc.querySelector('title')
      expect(titleElement).toBeDefined()
      expect(titleElement.textContent).toBe('My Custom Title')
      
      checkMeta(doc, 'version', '0.4.0')
      checkMeta(doc, 'ens-domain', 'test.eth')
      checkMeta(doc, 'description', 'This is a custom description for the page')
      
      // Check Open Graph and Twitter meta tags
      const ogTitle = doc.querySelector('meta[property="og:title"]')
      expect(ogTitle).toBeDefined()
      expect(ogTitle.content).toBe('My Custom Title')
      
      const ogSiteName = doc.querySelector('meta[property="og:site_name"]')
      expect(ogSiteName).toBeDefined()
      expect(ogSiteName.content).toBe('test.eth')

      const ogDescription = doc.querySelector('meta[property="og:description"]')
      expect(ogDescription).toBeDefined()
      expect(ogDescription.content).toBe('This is a custom description for the page')
      
      const twitterTitle = doc.querySelector('meta[name="twitter:title"]')
      expect(twitterTitle).toBeDefined()
      expect(twitterTitle.content).toBe('My Custom Title')
      
      const twitterDescription = doc.querySelector('meta[name="twitter:description"]')
      expect(twitterDescription).toBeDefined()
      expect(twitterDescription.content).toBe('This is a custom description for the page')

      // verify favicon path
      const favicon = doc.querySelector('link[rel="icon"]')
      expect(favicon).toBeDefined()
      expect(favicon.href).toBe('/_assets/images/favicon-light.png')

      // verify the repo root is updated
      const hash = await walletClient.writeContract(result.prepTx)
      expect(hash).toBeDefined()
      const transaction = await client.waitForTransactionReceipt({ hash })
      expect(transaction).toBeDefined()
      expect(transaction.status).toBe('success')
      
      const { cid: updatedRoot } = await resolveEnsDomain(client, 'test.eth', addresses.universalResolver)
      expect(updatedRoot.toString()).toBe(result.cid.toString())
    });

    it('should stage and commit changes (multiple edits)', async () => {
      const pages = {
        root: {
          path: '/',
          markdown: '# Test Page\n\nThis is a test.',
          body: '<h1>Test Page</h1><p>This is a test.</p>'
        },
        about: {
          path: '/about/',
          markdown: '# About\n\nAbout page content.',
          body: '<h1>About</h1><p>About page content.</p>'
        },
        blog: {
          path: '/blog/',
          markdown: '# Blog\n\nBlog index page.',
          body: '<h1>Blog</h1><p>Blog index page.</p>'
        },
        blogOne: {
          path: '/blog/one/',
          markdown: '# Blog Post One\n\nFirst blog post.',
          body: '<h1>Blog Post One</h1><p>First blog post.</p>'
        },
        blogTwo: {
          path: '/blog/two/',
          markdown: '# Blog Post Two\n\nSecond blog post.',
          body: '<h1>Blog Post Two</h1><p>Second blog post.</p>'
        }
      };
      
      // Set page edits
      for (const page of Object.values(pages)) {
        await repo.setPageEdit(page.path, page.markdown, page.body);
      }
      
      // Verify edits exist
      const changes = await repo.getChanges();
      const paths = changes.map(change => change.path);
      expect(changes.length).toBe(5);
      expect(paths).toContain('/');
      expect(paths).toContain('/about/');
      expect(paths).toContain('/blog/');
      expect(paths).toContain('/blog/one/');
      expect(paths).toContain('/blog/two/');
      
      // Stage and finalize
      const result = await repo.stage('test.eth', false);
      expect(result).toHaveProperty('cid');
      expect(result).toHaveProperty('prepTx');
      expect(result.cid instanceof CID).toBe(true);
      
      // Verify all pages are staged correctly
      for (const page of Object.values(pages)) {
        const markdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}${page.path}index.md`);
        expect(markdown).toBe(page.markdown);

        const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}${page.path}index.html`);
        expect(html).toContain(page.body);

        const doc = parser.parseFromString(html, 'text/html');
        const favicon = doc.querySelector('link[rel="icon"]');
        expect(favicon).toBeDefined();
        
        expect(favicon.href).toBe('/_assets/images/favicon-light.png');
      }
    });

    it('should use _prev directory as the root for the previous version', async () => {
      // First update: stage changes without template update
      const firstMarkdown = '# First Version\n\nThis is the first version.';
      const firstBody = '<h1>First Version</h1><p>This is the first version.</p>';
      
      await repo.setPageEdit('/', firstMarkdown, firstBody);
      
      const firstResult = await repo.stage('test.eth', false);
      expect(firstResult).toHaveProperty('cid');
      expect(firstResult.cid instanceof CID).toBe(true);
      await repo.finalizeCommit(firstResult.cid);
      
      // Verify first version content
      const firstMarkdownContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}/index.md`);
      expect(firstMarkdownContent).toBe(firstMarkdown);
      
      const firstHtmlContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}/index.html`);
      expect(firstHtmlContent).toContain(firstBody);

      const firstTemplateMdContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}/_template.html`);

      // verify the template (testDataCid) content is in the _prev/0 directory
      const actualTemplateMdContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${testDataCid.toString()}/_template.html`);
      const prevTemplateMdContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}/_prev/0/_template.html`);
      expect(actualTemplateMdContent).toBe(prevTemplateMdContent);
      expect(actualTemplateMdContent).toBe(firstTemplateMdContent);
      
      const actualIndexMdContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${testDataCid.toString()}/index.md`);
      const prevIndexMdContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}/_prev/0/index.md`);
      expect(actualIndexMdContent).toBe(prevIndexMdContent);

      const actualIndexHtmlContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${testDataCid.toString()}/index.html`);
      const prevIndexHtmlContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}/_prev/0/index.html`);
      expect(actualIndexHtmlContent).toBe(prevIndexHtmlContent);

      
      // Second update: stage changes with template update
      const secondMarkdown = '# Second Version\n\nThis is the second version with template update.';
      const secondBody = '<h1>Second Version</h1><p>This is the second version with template update.</p>';
      
      await repo.setPageEdit('/', secondMarkdown, secondBody);
      
      const secondResult = await repo.stage('test.eth', true);
      expect(secondResult).toHaveProperty('cid');
      expect(secondResult.cid instanceof CID).toBe(true);
      await repo.finalizeCommit(secondResult.cid);
      

      // Verify second version content
      const secondMarkdownContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/index.md`);
      expect(secondMarkdownContent).toBe(secondMarkdown);
      
      const secondHtmlContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/index.html`);
      expect(secondHtmlContent).toContain(secondBody);
      const secondHtmlDoc = parser.parseFromString(secondHtmlContent, 'text/html');
      checkMeta(secondHtmlDoc, 'version', '0.5.0');
      

      // verify the template (testDataCid) content is in the _prev/0/_prev/0 directory
      const prevPrevTemplateMdContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_prev/0/_prev/0/_template.html`);
      expect(actualTemplateMdContent).toBe(prevPrevTemplateMdContent);
      
      const prevPrevIndexMdContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_prev/0/_prev/0/index.md`);
      expect(actualIndexMdContent).toBe(prevPrevIndexMdContent);

      const prevPrevIndexHtmlContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_prev/0/_prev/0/index.html`);
      expect(actualIndexHtmlContent).toBe(prevPrevIndexHtmlContent);
      const prevPrevHtmlDoc = parser.parseFromString(prevPrevIndexHtmlContent, 'text/html');
      checkMeta(prevPrevHtmlDoc, 'version', '0.4.0');
      
      
      // verify the previous version of the app is in the _prev/0 directory
      const prevSecondResultTemplateMdContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_prev/0/_template.html`);
      expect(prevSecondResultTemplateMdContent).toBe(firstTemplateMdContent);

      const prevSecondResultMarkdownContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_prev/0/index.md`);
      expect(prevSecondResultMarkdownContent).toBe(firstMarkdownContent);
      expect(prevSecondResultMarkdownContent).toBe(firstMarkdown);

      const prevSecondResultHtmlContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_prev/0/index.html`);
      expect(prevSecondResultHtmlContent).toBe(firstHtmlContent);
      expect(prevSecondResultHtmlContent).toContain(firstBody);
      const prevSecondResultHtmlDoc = parser.parseFromString(prevSecondResultHtmlContent, 'text/html');
      checkMeta(prevSecondResultHtmlDoc, 'version', '0.4.0');


      // Verify _assets are updated in second version
      const secondAssetsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_assets`);
      expect(secondAssetsContent).toBe('folder-updated');
      const prev0AssetsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_prev/0/_assets`);
      expect(prev0AssetsContent).toBe('folder-not-updated');
      const prevPrevPrevAssetsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_prev/0/_prev/0/_assets`);
      expect(prevPrevPrevAssetsContent).toBe('folder-not-updated');

      // Verify _js are updated in second version
      const secondJsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_js`);
      expect(secondJsContent).toBe('folder-updated');
      const prev0JsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_prev/0/_js`);
      expect(prev0JsContent).toBe('folder-not-updated');
      const prevPrevPrevJsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/_prev/0/_prev/0/_js`);
      expect(prevPrevPrevJsContent).toBe('folder-not-updated');
    })

    it('should handle page deletion correctly', async () => {
      // Test 1: deleting page at '/' shouldn't work
      await expect(repo.deletePage('/')).rejects.toThrow('Cannot delete root page');

      // Test 2: delete a page that only exists as an edit
      const testMarkdown = '# Test Page\n\nThis is a test.';
      const testBody = '<h1>Test Page</h1><p>This is a test.</p>';
      
      await repo.setPageEdit('/about/', testMarkdown, testBody);
      await repo.setPageEdit('/blog/', '# Blog\n\nBlog content.', '<h1>Blog</h1><p>Blog content.</p>');
      await repo.setPageEdit('/contact/', '# Contact\n\nContact info.', '<h1>Contact</h1><p>Contact info.</p>');
      
      // Verify edits exist
      let changes = await repo.getChanges();
      expect(changes.length).toBe(3);
      expect(changes.map(c => c.path)).toContain('/about/');
      expect(changes.map(c => c.path)).toContain('/blog/');
      expect(changes.map(c => c.path)).toContain('/contact/');
      
      // Delete a page that only exists as an edit
      await repo.deletePage('/contact/');
      
      // Verify the edit was removed
      changes = await repo.getChanges();
      expect(changes.length).toBe(2);
      expect(changes.map(c => c.path)).toContain('/about/');
      expect(changes.map(c => c.path)).toContain('/blog/');
      expect(changes.map(c => c.path)).not.toContain('/contact/');
      
      // Stage and commit the new pages
      const firstResult = await repo.stage('test.eth', false);
      expect(firstResult).toHaveProperty('cid');
      expect(firstResult.cid instanceof CID).toBe(true);
      
      // Verify the pages are staged correctly
      const aboutMarkdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}/about/index.md`);
      expect(aboutMarkdown).toBe(testMarkdown);
      
      const blogMarkdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}/blog/index.md`);
      expect(blogMarkdown).toBe('# Blog\n\nBlog content.');
      
      // Verify contact page doesn't exist
      await expect(cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}/contact/index.md`)).rejects.toThrow();
      
      // Commit the first version
      const hash1 = await walletClient.writeContract(firstResult.prepTx);
      expect(hash1).toBeDefined();
      const transaction1 = await client.waitForTransactionReceipt({ hash: hash1 });
      expect(transaction1.status).toBe('success');
      await repo.finalizeCommit(firstResult.cid);
      
      // Delete one of the newly committed pages
      await repo.deletePage('/about/');
      
      // Verify the deletion is staged
      changes = await repo.getChanges();
      expect(changes.length).toBe(1);
      expect(changes[0].type).toBe('delete');
      expect(changes[0].path).toBe('/about/');
      
      // Stage and commit the deletion
      const secondResult = await repo.stage('test.eth', false);
      expect(secondResult).toHaveProperty('cid');
      expect(secondResult.cid instanceof CID).toBe(true);
      
      // Verify the about page is deleted
      await expect(cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/about/index.md`)).rejects.toThrow();
      
      // Verify the blog page still exists
      const blogMarkdownAfterDelete = await cat(testEnv.kubo.kuboApi, `/ipfs/${secondResult.cid.toString()}/blog/index.md`);
      expect(blogMarkdownAfterDelete).toBe('# Blog\n\nBlog content.');
      
      // Commit the second version
      const hash2 = await walletClient.writeContract(secondResult.prepTx);
      expect(hash2).toBeDefined();
      const transaction2 = await client.waitForTransactionReceipt({ hash: hash2 });
      expect(transaction2.status).toBe('success');
      
      // Verify the final state through ENS resolution
      const { cid: finalRoot } = await resolveEnsDomain(client, 'test.eth', addresses.universalResolver);
      expect(finalRoot.toString()).toBe(secondResult.cid.toString());
    });

    it('should handle deletion with multiple directory depths', async () => {
      // Create a nested structure
      await repo.setPageEdit('/docs/', '# Documentation\n\nMain docs.', '<h1>Documentation</h1><p>Main docs.</p>');
      await repo.setPageEdit('/docs/guides/', '# Guides\n\nUser guides.', '<h1>Guides</h1><p>User guides.</p>');
      await repo.setPageEdit('/docs/guides/getting-started/', '# Getting Started\n\nGetting started guide.', '<h1>Getting Started</h1><p>Getting started guide.</p>');
      await repo.setPageEdit('/docs/guides/advanced/', '# Advanced\n\nAdvanced guide.', '<h1>Advanced</h1><p>Advanced guide.</p>');
      await repo.setPageEdit('/docs-v2/api/', '# API Docs\n\nAPI documentation.', '<h1>API Docs</h1><p>API documentation.</p>');
      
      // Stage and commit the initial structure
      const initialResult = await repo.stage('test.eth', false);
      expect(initialResult).toHaveProperty('cid');
      expect(initialResult.cid instanceof CID).toBe(true);
      
      // Verify all pages are staged correctly
      const docsMarkdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${initialResult.cid.toString()}/docs/index.md`);
      expect(docsMarkdown).toBe('# Documentation\n\nMain docs.');
      
      const guidesMarkdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${initialResult.cid.toString()}/docs/guides/index.md`);
      expect(guidesMarkdown).toBe('# Guides\n\nUser guides.');
      
      const gettingStartedMarkdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${initialResult.cid.toString()}/docs/guides/getting-started/index.md`);
      expect(gettingStartedMarkdown).toBe('# Getting Started\n\nGetting started guide.');
      
      const advancedMarkdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${initialResult.cid.toString()}/docs/guides/advanced/index.md`);
      expect(advancedMarkdown).toBe('# Advanced\n\nAdvanced guide.');
      
      const apiMarkdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${initialResult.cid.toString()}/docs-v2/api/index.md`);
      expect(apiMarkdown).toBe('# API Docs\n\nAPI documentation.');
      
      // Commit the initial version
      const hash1 = await walletClient.writeContract(initialResult.prepTx);
      expect(hash1).toBeDefined();
      const transaction1 = await client.waitForTransactionReceipt({ hash: hash1 });
      expect(transaction1.status).toBe('success');
      await repo.finalizeCommit(initialResult.cid);
      
      // Test 1: Delete /docs-v2/api/ - should remove api page and empty parent directory
      await repo.deletePage('/docs-v2/api/');
      
      let changes = await repo.getChanges();
      expect(changes.length).toBe(1);
      expect(changes[0].type).toBe('delete');
      expect(changes[0].path).toBe('/docs-v2/api/');
      
      const deleteApiResult = await repo.stage('test.eth', false);
      expect(deleteApiResult).toHaveProperty('cid');
      expect(deleteApiResult.cid instanceof CID).toBe(true);
      
      // Verify api page is deleted
      await expect(cat(testEnv.kubo.kuboApi, `/ipfs/${deleteApiResult.cid.toString()}/docs-v2/api/index.md`)).rejects.toThrow();
      
      // Verify docs-v2 directory is also removed (empty parent directory)
      expect(await ls(testEnv.kubo.kuboApi, `/ipfs/${deleteApiResult.cid.toString()}/`)).toBeDefined();
      // await expect(ls(testEnv.kubo.kuboApi, `/ipfs/${deleteApiResult.cid.toString()}/docs-v2/api/not-exists`)).rejects.toThrow();
      await expect(ls(testEnv.kubo.kuboApi, `/ipfs/${deleteApiResult.cid.toString()}/docs-v2/api`)).rejects.toThrow();
      await expect(ls(testEnv.kubo.kuboApi, `/ipfs/${deleteApiResult.cid.toString()}/docs-v2`)).rejects.toThrow();
      
      // Verify other pages still exist
      const docsMarkdownAfterApiDelete = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteApiResult.cid.toString()}/docs/index.md`);
      expect(docsMarkdownAfterApiDelete).toBe('# Documentation\n\nMain docs.');
      
      const guidesMarkdownAfterApiDelete = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteApiResult.cid.toString()}/docs/guides/index.md`);
      expect(guidesMarkdownAfterApiDelete).toBe('# Guides\n\nUser guides.');
      
      // // Commit the api deletion
      await repo.finalizeCommit(deleteApiResult.cid);

      let pages = await repo.getAllPages();
      expect(pages.length).toBe(5);
      expect(pages).toContain('/');
      expect(pages).toContain('/docs/');
      expect(pages).toContain('/docs/guides/');
      expect(pages).toContain('/docs/guides/getting-started/');
      expect(pages).toContain('/docs/guides/advanced/');
      expect(pages).not.toContain('/docs-v2/api/');
      
      // Test 2: Delete /docs/guides/ - should remove guides content but keep children
      await repo.deletePage('/docs/guides/');
      
      changes = await repo.getChanges();
      expect(changes.length).toBe(1);
      expect(changes[0].type).toBe('delete');
      expect(changes[0].path).toBe('/docs/guides/');
      
      const deleteGuidesResult = await repo.stage('test.eth', false);
      expect(deleteGuidesResult).toHaveProperty('cid');
      expect(deleteGuidesResult.cid instanceof CID).toBe(true);

      
      // Verify guides index page is deleted
      await expect(cat(testEnv.kubo.kuboApi, `/ipfs/${deleteGuidesResult.cid.toString()}/docs/guides/index.md`)).rejects.toThrow();
      await expect(cat(testEnv.kubo.kuboApi, `/ipfs/${deleteGuidesResult.cid.toString()}/docs/guides/index.html`)).rejects.toThrow();
      
      // Verify children pages still exist
      const gettingStartedMarkdownAfterGuidesDelete = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteGuidesResult.cid.toString()}/docs/guides/getting-started/index.md`);
      expect(gettingStartedMarkdownAfterGuidesDelete).toBe('# Getting Started\n\nGetting started guide.');
      
      const advancedMarkdownAfterGuidesDelete = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteGuidesResult.cid.toString()}/docs/guides/advanced/index.md`);
      expect(advancedMarkdownAfterGuidesDelete).toBe('# Advanced\n\nAdvanced guide.');
      
      // Verify docs page still exists
      const docsMarkdownAfterGuidesDelete = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteGuidesResult.cid.toString()}/docs/index.md`);
      expect(docsMarkdownAfterGuidesDelete).toBe('# Documentation\n\nMain docs.');
      
      // Commit the guides deletion
      await repo.finalizeCommit(deleteGuidesResult.cid);

      pages = await repo.getAllPages();
      expect(pages.length).toBe(4);
      expect(pages).toContain('/');
      expect(pages).toContain('/docs/');
      expect(pages).toContain('/docs/guides/getting-started/');
      expect(pages).toContain('/docs/guides/advanced/');
      
      // Test 3: Delete /docs/guides/advanced/ - should remove only the specified page
      await repo.deletePage('/docs/guides/advanced/');
      
      changes = await repo.getChanges();
      expect(changes.length).toBe(1);
      expect(changes[0].type).toBe('delete');
      expect(changes[0].path).toBe('/docs/guides/advanced/');
      
      const deleteAdvancedResult = await repo.stage('test.eth', false);
      expect(deleteAdvancedResult).toHaveProperty('cid');
      expect(deleteAdvancedResult.cid instanceof CID).toBe(true);
      
      // Verify advanced page is deleted
      await expect(cat(testEnv.kubo.kuboApi, `/ipfs/${deleteAdvancedResult.cid.toString()}/docs/guides/advanced/index.md`)).rejects.toThrow();
      
      // Verify sibling page still exists
      const gettingStartedMarkdownAfterAdvancedDelete = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteAdvancedResult.cid.toString()}/docs/guides/getting-started/index.md`);
      expect(gettingStartedMarkdownAfterAdvancedDelete).toBe('# Getting Started\n\nGetting started guide.');
      
      // Verify parent pages still exist
      const docsMarkdownAfterAdvancedDelete = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteAdvancedResult.cid.toString()}/docs/index.md`);
      expect(docsMarkdownAfterAdvancedDelete).toBe('# Documentation\n\nMain docs.');
      
      // Commit the final version
      await repo.finalizeCommit(deleteAdvancedResult.cid);

      pages = await repo.getAllPages();
      expect(pages.length).toBe(3);
      expect(pages).toContain('/');
      expect(pages).toContain('/docs/');
      expect(pages).toContain('/docs/guides/getting-started/');
      expect(pages).not.toContain('/docs/guides/advanced/');
    });

    it('should handle staging with no edits', async () => {
      await expect(repo.stage('test.eth', false)).rejects.toThrow('No edits to stage');
    });

    it('should stage with template update when there are no file or page edits', async () => {
      // Stage with template update should work
      const result = await repo.stage('test.eth', true);
      expect(result).toHaveProperty('cid');
      expect(result).toHaveProperty('prepTx');
      expect(result.cid instanceof CID).toBe(true);
      
      // Verify template is updated to version 0.5.0
      const templateContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_template.html`);
      expect(templateContent).toBeDefined();
      const templateDoc = parser.parseFromString(templateContent, 'text/html');
      checkMeta(templateDoc, 'version', '0.5.0');
      
      // Verify _assets and _js folders are updated
      const assetsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_assets`);
      expect(assetsContent).toBe('folder-updated');
      
      const jsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_js`);
      expect(jsContent).toBe('folder-updated');
    });

    it('should include manifest.json and _redirects files during staging', async () => {
      const testMarkdown = `---
title: My Custom Title
description: This is a custom description for the page
---

# Test Page

This is a test.`;
      const testBody = '<h1>Test Page</h1><p>This is a test.</p>';
      
      await repo.setPageEdit('/', testMarkdown, testBody);
      await repo.setPageEdit('/about/', testMarkdown, testBody);
      
      const result = await repo.stage('test.eth', false);
      
      expect(result).toHaveProperty('cid');
      expect(result.cid instanceof CID).toBe(true);

      // verify the manifest.json file is included
      const manifest = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/manifest.json');
      expect(manifest).toBeDefined();
      
      // parse and verify manifest content
      const manifestData = JSON.parse(manifest);
      expect(manifestData.name).toBe('My Custom Title');
      expect(manifestData.short_name).toBe('test.eth');
      expect(manifestData.description).toBe('This is a custom description for the page');
      expect(manifestData.icons).toBeDefined();
      expect(manifestData.icons.length).toBe(1);
      expect(manifestData.icons[0].src).toBe('/_assets/images/logo.png');
      expect(manifestData.icons[0].type).toBe('image/png');
      expect(manifestData.icons[0].sizes).toBe('256x256');
      expect(manifestData.dapp_repository).toBe('https://github.com/stigmergic-org/simplepage');
      expect(manifestData.dapp_contracts).toEqual([]);

      // verify the _redirects file is included
      const redirects = await cat(testEnv.kubo.kuboApi, '/ipfs/' + result.cid.toString() + '/_redirects');
      expect(redirects).toBeDefined();
      expect(redirects.trim()).toContain('/* / 200');
      expect(redirects.trim()).toContain('/about/* /about/ 200');
    });

    it('should include sidenav.json during staging', async () => {
      const rootMarkdown = `---
title: Home
sidebar-nav-prio: 1
---

# Home`;
      const rootBody = '<h1>Home</h1>';
      const aboutMarkdown = `---
title: About
sidebar-nav-prio: 2
---

# About`;
      const aboutBody = '<h1>About</h1>';
      const docsMarkdown = `---
title: Guides
sidebar-nav-prio: 3
---

# Guides`;
      const docsBody = '<h1>Guides</h1>';

      await repo.setPageEdit('/', rootMarkdown, rootBody);
      await repo.setPageEdit('/about/', aboutMarkdown, aboutBody);
      await repo.setPageEdit('/docs/guides/', docsMarkdown, docsBody);

      const result = await repo.stage('test.eth', false);

      const sidenavJson = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/sidenav.json`);
      const sidenavData = JSON.parse(sidenavJson);
      expect(Array.isArray(sidenavData.items)).toBe(true);

      const collectPaths = (items) => items.reduce((acc, item) => {
        acc.push(item.path);
        return acc.concat(collectPaths(item.children || []));
      }, []);

      const paths = collectPaths(sidenavData.items);
      expect(paths).toContain('/');
      expect(paths).toContain('/about/');
      expect(paths).toContain('/docs/');
      expect(paths).toContain('/docs/guides/');
    });

    it('should include sidebar-toc meta in staged html', async () => {
      const rootMarkdown = `---
title: Home
sidebar-toc: true
---

# Home`;
      const rootBody = '<h1>Home</h1>';
      const aboutMarkdown = '# About';
      const aboutBody = '<h1>About</h1>';

      await repo.setPageEdit('/', rootMarkdown, rootBody);
      await repo.setPageEdit('/about/', aboutMarkdown, aboutBody);

      const result = await repo.stage('test.eth', false);

      const rootHtml = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/index.html`);
      const aboutHtml = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/about/index.html`);
      const rootDoc = parser.parseFromString(rootHtml, 'text/html');
      const aboutDoc = parser.parseFromString(aboutHtml, 'text/html');

      checkMeta(rootDoc, 'sidebar-toc', 'true');
      checkMeta(aboutDoc, 'sidebar-toc', 'false');
    });

    it('should include empty sidenav.json when no sidebar nav is configured', async () => {
      const rootMarkdown = '# Home';
      const rootBody = '<h1>Home</h1>';

      await repo.setPageEdit('/', rootMarkdown, rootBody);

      const result = await repo.stage('test.eth', false);

      const sidenavJson = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/sidenav.json`);
      const sidenavData = JSON.parse(sidenavJson);
      expect(Array.isArray(sidenavData.items)).toBe(true);
      expect(sidenavData.items.length).toBe(0);
    });

    it('should persist committed pages and allow loading from a new Repo instance', async () => {
      // Add two pages
      const page1 = {
        path: '/foo/',
        markdown: '# Foo\n\nFoo page content.',
        body: '<h1>Foo</h1><p>Foo page content.</p>'
      };
      const page2 = {
        path: '/bar/',
        markdown: '# Bar\n\nBar page content.',
        body: '<h1>Bar</h1><p>Bar page content.</p>'
      };
      // Add multi-depth: /foo/bar/ (with content at both /foo/ and /foo/bar/)
      const pageFooBar = {
        path: '/foo/bar/',
        markdown: '# FooBar\n\nFooBar page content.',
        body: '<h1>FooBar</h1><p>FooBar page content.</p>'
      };
      // Add multi-depth: /baz/bar/ (with content only at /baz/bar/)
      const pageBazBar = {
        path: '/baz/bar/',
        markdown: '# BazBar\n\nBazBar page content.',
        body: '<h1>BazBar</h1><p>BazBar page content.</p>'
      };
      await repo.setPageEdit(page1.path, page1.markdown, page1.body);
      await repo.setPageEdit(page2.path, page2.markdown, page2.body);
      await repo.setPageEdit(pageFooBar.path, pageFooBar.markdown, pageFooBar.body);
      await repo.setPageEdit(pageBazBar.path, pageBazBar.markdown, pageBazBar.body);

      // Stage and commit
      const result = await repo.stage('test.eth', false);
      const hash = await walletClient.writeContract(result.prepTx);
      await client.waitForTransactionReceipt({ hash });
      await repo.finalizeCommit(result.cid);

      // Create a new Repo instance with a fresh storage
      const newStorage = new MockStorage();
      const newRepo = new Repo('test.eth', newStorage);
      await newRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      // Load all pages and verify content
      const allPages = await newRepo.getAllPages();
      expect(allPages).toContain('/');
      expect(allPages).toContain('/foo/');
      expect(allPages).toContain('/bar/');
      expect(allPages).toContain('/foo/bar/');
      expect(allPages).toContain('/baz/bar/');
      // /baz/ should not be present as a page
      expect(allPages).not.toContain('/baz/');

      const fooMarkdown = await newRepo.getMarkdown('/foo/');
      expect(fooMarkdown).toBe(page1.markdown);
      const fooHtml = await newRepo.getHtmlBody('/foo/');
      expect(fooHtml).toContain('Foo page content.');

      const barMarkdown = await newRepo.getMarkdown('/bar/');
      expect(barMarkdown).toBe(page2.markdown);
      const barHtml = await newRepo.getHtmlBody('/bar/');
      expect(barHtml).toContain('Bar page content.');

      const fooBarMarkdown = await newRepo.getMarkdown('/foo/bar/');
      expect(fooBarMarkdown).toBe(pageFooBar.markdown);
      const fooBarHtml = await newRepo.getHtmlBody('/foo/bar/');
      expect(fooBarHtml).toContain('FooBar page content.');

      const bazBarMarkdown = await newRepo.getMarkdown('/baz/bar/');
      expect(bazBarMarkdown).toBe(pageBazBar.markdown);
      const bazBarHtml = await newRepo.getHtmlBody('/baz/bar/');
      expect(bazBarHtml).toContain('BazBar page content.');

      // /baz/ should throw or be undefined
      await expect(newRepo.getMarkdown('/baz/')).rejects.toThrow();
      await newRepo.close()
    });

    it('should update all pages to latest version when staging with template update', async () => {
      // Create initial pages at different depths
      const pages = {
        root: {
          path: '/',
          markdown: '# Home Page\n\nWelcome to the home page.',
          body: '<h1>Home Page</h1><p>Welcome to the home page.</p>'
        },
        about: {
          path: '/about/',
          markdown: '# About\n\nAbout page content.',
          body: '<h1>About</h1><p>About page content.</p>'
        },
        docs: {
          path: '/docs/',
          markdown: '# Documentation\n\nMain documentation.',
          body: '<h1>Documentation</h1><p>Main documentation.</p>'
        },
        docsGuides: {
          path: '/docs/guides/',
          markdown: '# Guides\n\nUser guides.',
          body: '<h1>Guides</h1><p>User guides.</p>'
        },
        docsGuidesGettingStarted: {
          path: '/docs/guides/getting-started/',
          markdown: '# Getting Started\n\nGetting started guide.',
          body: '<h1>Getting Started</h1><p>Getting started guide.</p>'
        },
        docsGuidesAdvanced: {
          path: '/docs/guides/advanced/',
          markdown: '# Advanced\n\nAdvanced guide.',
          body: '<h1>Advanced</h1><p>Advanced guide.</p>'
        },
        blog: {
          path: '/blog/',
          markdown: '# Blog\n\nBlog index page.',
          body: '<h1>Blog</h1><p>Blog index page.</p>'
        },
        blogPost1: {
          path: '/blog/post-1/',
          markdown: '# Blog Post 1\n\nFirst blog post content.',
          body: '<h1>Blog Post 1</h1><p>First blog post content.</p>'
        },
        blogPost2: {
          path: '/blog/post-2/',
          markdown: '# Blog Post 2\n\nSecond blog post content.',
          body: '<h1>Blog Post 2</h1><p>Second blog post content.</p>'
        },
        contact: {
          path: '/contact/',
          markdown: '# Contact\n\nContact information.',
          body: '<h1>Contact</h1><p>Contact information.</p>'
        }
      };

      // Set page edits for all pages
      for (const page of Object.values(pages)) {
        await repo.setPageEdit(page.path, page.markdown, page.body);
      }

      // Stage and commit initial version without template update
      const initialResult = await repo.stage('test.eth', false);
      expect(initialResult).toHaveProperty('cid');
      expect(initialResult.cid instanceof CID).toBe(true);

      // Verify all pages are staged correctly with version 0.4.0
      for (const page of Object.values(pages)) {
        const markdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${initialResult.cid.toString()}${page.path}index.md`);
        expect(markdown).toBe(page.markdown);

        const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${initialResult.cid.toString()}${page.path}index.html`);
        expect(html).toContain(page.body);

        // Verify version is 0.4.0 for all pages
        const doc = parser.parseFromString(html, 'text/html');
        checkMeta(doc, 'version', '0.4.0');
      }

      // Commit the initial version
      const hash1 = await walletClient.writeContract(initialResult.prepTx);
      expect(hash1).toBeDefined();
      const transaction1 = await client.waitForTransactionReceipt({ hash: hash1 });
      expect(transaction1.status).toBe('success');
      await repo.finalizeCommit(initialResult.cid);

      // Create a new Repo instance to simulate a fresh start
      const newStorage = new MockStorage();
      const newRepo = new Repo('test.eth', newStorage);
      await newRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      // Make a simple change to the root page
      const updatedRootMarkdown = '# Updated Home Page\n\nThis is the updated home page content.';
      const updatedRootBody = '<h1>Updated Home Page</h1><p>This is the updated home page content.</p>';
      
      await newRepo.setPageEdit('/', updatedRootMarkdown, updatedRootBody);

      // Stage and commit with template update (should update all pages to version 0.5.0)
      const updateResult = await newRepo.stage('test.eth', true);
      expect(updateResult).toHaveProperty('cid');
      expect(updateResult.cid instanceof CID).toBe(true);

      // Verify the root page is updated with new content
      const updatedRootMarkdownContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${updateResult.cid.toString()}/index.md`);
      expect(updatedRootMarkdownContent).toBe(updatedRootMarkdown);

      const updatedRootHtmlContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${updateResult.cid.toString()}/index.html`);
      expect(updatedRootHtmlContent).toContain(updatedRootBody);

      // Verify all other pages are updated to version 0.5.0 but keep their original content
      for (const [key, page] of Object.entries(pages)) {
        if (key === 'root') continue // ignore root as it has been changed

        const markdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${updateResult.cid.toString()}${page.path}index.md`);
        expect(markdown).toBe(page.markdown);

        const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${updateResult.cid.toString()}${page.path}index.html`);
        expect(html).toContain(page.body);

        // Verify all pages now use version 0.5.0
        const doc = parser.parseFromString(html, 'text/html');
        checkMeta(doc, 'version', '0.5.0');
      }

      // Verify template is updated to version 0.5.0
      const templateContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${updateResult.cid.toString()}/_template.html`);
      expect(templateContent).toBeDefined();
      const templateDoc = parser.parseFromString(templateContent, 'text/html');
      checkMeta(templateDoc, 'version', '0.5.0');

      // Verify _assets and _js folders are updated
      const assetsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${updateResult.cid.toString()}/_assets`);
      expect(assetsContent).toBe('folder-updated');

      const jsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${updateResult.cid.toString()}/_js`);
      expect(jsContent).toBe('folder-updated');

      // Commit the final version
      const hash2 = await walletClient.writeContract(updateResult.prepTx);
      expect(hash2).toBeDefined();
      const transaction2 = await client.waitForTransactionReceipt({ hash: hash2 });
      expect(transaction2.status).toBe('success');

      // Verify the final state through ENS resolution
      const { cid: finalRoot } = await resolveEnsDomain(client, 'test.eth', addresses.universalResolver);
      expect(finalRoot.toString()).toBe(updateResult.cid.toString());
      await newRepo.close()
    });

    it('should handle file deletion through staging and finalization', async () => {
      // Create repo and add some pages
      const pages = {
        root: {
          path: '/',
          markdown: '# Home Page\n\nWelcome to the home page.',
          body: '<h1>Home Page</h1><p>Welcome to the home page.</p>'
        },
        about: {
          path: '/about/',
          markdown: '# About\n\nAbout page content.',
          body: '<h1>About</h1><p>About page content.</p>'
        },
        contact: {
          path: '/contact/',
          markdown: '# Contact\n\nContact information.',
          body: '<h1>Contact</h1><p>Contact information.</p>'
        }
      };

      // Add all pages to the repo
      for (const page of Object.values(pages)) {
        await repo.setPageEdit(page.path, page.markdown, page.body);
      }

      // Stage and commit the initial pages
      const initialResult = await repo.stage('test.eth', false);
      expect(initialResult).toHaveProperty('cid');
      expect(initialResult.cid instanceof CID).toBe(true);

      // Verify all pages are staged correctly
      for (const page of Object.values(pages)) {
        const markdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${initialResult.cid.toString()}${page.path}index.md`);
        expect(markdown).toBe(page.markdown);

        const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${initialResult.cid.toString()}${page.path}index.html`);
        expect(html).toContain(page.body);
      }

      // Commit the initial version
      const hash1 = await walletClient.writeContract(initialResult.prepTx);
      expect(hash1).toBeDefined();
      const transaction1 = await client.waitForTransactionReceipt({ hash: hash1 });
      expect(transaction1.status).toBe('success');
      await repo.finalizeCommit(initialResult.cid);

      // Delete a file (the contact page)
      await repo.deletePage('/contact/');

      // Verify the deletion is staged
      const changes = await repo.getChanges();
      expect(changes.length).toBe(1);
      expect(changes[0].type).toBe('delete');
      expect(changes[0].path).toBe('/contact/');

      // Stage (with update template) and commit the deletion
      const deleteResult = await repo.stage('test.eth', true);
      expect(deleteResult).toHaveProperty('cid');
      expect(deleteResult.cid instanceof CID).toBe(true);

      // Ensure the deleted file is not present in the new commit
      await expect(cat(testEnv.kubo.kuboApi, `/ipfs/${deleteResult.cid.toString()}/contact/index.md`)).rejects.toThrow();
      await expect(cat(testEnv.kubo.kuboApi, `/ipfs/${deleteResult.cid.toString()}/contact/index.html`)).rejects.toThrow();

      // Verify other pages still exist
      const rootMarkdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteResult.cid.toString()}/index.md`);
      expect(rootMarkdown).toBe(pages.root.markdown);

      const aboutMarkdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteResult.cid.toString()}/about/index.md`);
      expect(aboutMarkdown).toBe(pages.about.markdown);

      // Verify template update occurred (version 0.5.0)
      const templateContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteResult.cid.toString()}/_template.html`);
      expect(templateContent).toBeDefined();
      const templateDoc = parser.parseFromString(templateContent, 'text/html');
      checkMeta(templateDoc, 'version', '0.5.0');

      // Verify _assets and _js folders are updated
      const assetsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteResult.cid.toString()}/_assets`);
      expect(assetsContent).toBe('folder-updated');

      const jsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${deleteResult.cid.toString()}/_js`);
      expect(jsContent).toBe('folder-updated');

      // Commit the final version
      const hash2 = await walletClient.writeContract(deleteResult.prepTx);
      expect(hash2).toBeDefined();
      const transaction2 = await client.waitForTransactionReceipt({ hash: hash2 });
      expect(transaction2.status).toBe('success');

      // Verify the final state through ENS resolution
      const { cid: finalRoot } = await resolveEnsDomain(client, 'test.eth', addresses.universalResolver);
      expect(finalRoot.toString()).toBe(deleteResult.cid.toString());
    });
  });

  describe('Files Tests', () => {
    beforeAll(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'new.simplepage.eth', templateCid.toString());
    });

    beforeEach(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'test.eth', testDataCid.toString());
      await repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });
    });

    afterEach(async () => {
      resetIDB()
      await repo.close()
    });

    // Helper function to verify file entry properties
    const verifyFileEntry = (file, basePath, expectedType = 'file') => {
      expect(file).toHaveProperty('name');
      expect(file).toHaveProperty('cid');
      expect(file).toHaveProperty('size');
      expect(file).toHaveProperty('path');
      expect(file).toHaveProperty('type');
      expect(typeof file.name).toBe('string');
      expect(file.cid).toBeInstanceOf(CID);
      expect(typeof file.size).toBe('number');
      expect(typeof file.path).toBe('string');
      expect(file.path.split('/').filter(Boolean).length).toBe(basePath.split('/').filter(Boolean).length + 1);
      expect(file.type).toBe(expectedType);
      // Files should not have change property since they're committed
      expect(file).not.toHaveProperty('change');
    };

    // Helper function to verify directory contents and properties
    const verifyDirectory = async (repoInstance, path, expectedFiles) => {
      const files = await repoInstance.files.ls(path);
      for (const file of files) {
        expect(expectedFiles).toContain(file.name);
        const expectedType = file.name.split('.').length > 1 ? 'file' : 'directory';
        verifyFileEntry(file, path, expectedType);
      }
      return files;
    };

    it('should add files and stage+commit, should be stored by dservice', async () => {
      // Create files in multiple folders and subfolders
      const files = {
        '/images/logo.png': new TextEncoder().encode('fake-png-data'),
        '/images/icons/favicon.ico': new TextEncoder().encode('fake-ico-data'),
        '/images/icons/apple-touch-icon.png': new TextEncoder().encode('fake-apple-icon-data'),
        '/documents/resume.pdf': new TextEncoder().encode('fake-pdf-data'),
        '/documents/contracts/agreement.pdf': new TextEncoder().encode('fake-agreement-data'),
        '/documents/contracts/terms.pdf': new TextEncoder().encode('fake-terms-data'),
        '/assets/css/style.css': new TextEncoder().encode('body { color: red; }'),
        '/assets/js/app.js': new TextEncoder().encode('console.log("Hello World");'),
        '/assets/js/utils/helper.js': new TextEncoder().encode('function helper() { return true; }'),
        '/data/config.json': new TextEncoder().encode('{"setting": "value"}'),
        '/data/users/profiles.json': new TextEncoder().encode('{"users": []}'),
        '/backups/old-file.txt': new TextEncoder().encode('old content'),
        '/backups/archive/very-old-file.txt': new TextEncoder().encode('very old content')
      };

      // Add all files to the repo
      for (const [path, content] of Object.entries(files)) {
        await repo.files.add(path, content);
      }


      // Stage and commit the changes
      const result = await repo.stage('test.eth', false);
      expect(result).toHaveProperty('cid');
      expect(result.cid instanceof CID).toBe(true);

      // Verify all files are stored in kubo
      for (const [path, expectedContent] of Object.entries(files)) {
        const storedContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files${path}`);
        expect(storedContent).toBe(new TextDecoder().decode(expectedContent));
      }

      // Verify the file structure is correct in kubo
      const filesList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files`);
      expect(filesList).toContain('images');
      expect(filesList).toContain('documents');
      expect(filesList).toContain('assets');
      expect(filesList).toContain('data');
      expect(filesList).toContain('backups');

      // Check nested directories
      const imagesList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/images`);
      expect(imagesList).toContain('logo.png');
      expect(imagesList).toContain('icons');

      const iconsList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/images/icons`);
      expect(iconsList).toContain('favicon.ico');
      expect(iconsList).toContain('apple-touch-icon.png');

      const documentsList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/documents`);
      expect(documentsList).toContain('resume.pdf');
      expect(documentsList).toContain('contracts');

      const contractsList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/documents/contracts`);
      expect(contractsList).toContain('agreement.pdf');
      expect(contractsList).toContain('terms.pdf');

      const assetsList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/assets`);
      expect(assetsList).toContain('css');
      expect(assetsList).toContain('js');

      const jsList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/assets/js`);
      expect(jsList).toContain('app.js');
      expect(jsList).toContain('utils');

      const utilsList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/assets/js/utils`);
      expect(utilsList).toContain('helper.js');

      const dataList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/data`);
      expect(dataList).toContain('config.json');
      expect(dataList).toContain('users');

      const usersList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/data/users`);
      expect(usersList).toContain('profiles.json');

      const backupsList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/backups`);
      expect(backupsList).toContain('old-file.txt');
      expect(backupsList).toContain('archive');

      const archiveList = await ls(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/_files/backups/archive`);
      expect(archiveList).toContain('very-old-file.txt');
    });

    it('files should be retrieved from dservice when catting from new repo instances', async () => {
      // Create files in multiple folders and subfolders
      const files = {
        'images/logo.png': new TextEncoder().encode('fake-png-data'),
        'images/icons/favicon.ico': new TextEncoder().encode('fake-ico-data'),
        'images/icons/apple-touch-icon.png': new TextEncoder().encode('fake-apple-icon-data'),
        'documents/resume.pdf': new TextEncoder().encode('fake-pdf-data'),
        'documents/contracts/agreement.pdf': new TextEncoder().encode('fake-agreement-data'),
        'documents/contracts/terms.pdf': new TextEncoder().encode('fake-terms-data'),
        'assets/css/style.css': new TextEncoder().encode('body { color: red; }'),
        'assets/js/app.js': new TextEncoder().encode('console.log("Hello World");'),
        'assets/js/utils/helper.js': new TextEncoder().encode('function helper() { return true; }'),
        'data/config.json': new TextEncoder().encode('{"setting": "value"}'),
        'data/users/profiles.json': new TextEncoder().encode('{"users": []}'),
        'backups/old-file.txt': new TextEncoder().encode('old content'),
        'backups/archive/very-old-file.txt': new TextEncoder().encode('very old content')
      };

      // Add all files to the repo
      for (const [path, content] of Object.entries(files)) {
        await repo.files.add(path, content);
      }

      // Stage and commit the changes
      const result = await repo.stage('test.eth', false);
      expect(result).toHaveProperty('cid');
      expect(result.cid instanceof CID).toBe(true);

      // Commit the changes
      const hash = await walletClient.writeContract(result.prepTx);
      expect(hash).toBeDefined();
      const transaction = await client.waitForTransactionReceipt({ hash });
      expect(transaction.status).toBe('success');
      await repo.finalizeCommit(result.cid);

      // Create a new Repo instance with fresh storage
      resetIDB()
      const newStorage = new MockStorage();
      const newRepo = new Repo('test.eth', newStorage);
      await newRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      // Spy on the dservice logger to track file requests
      const loggerSpy = jest.spyOn(testEnv.dservice.logger, 'info');
      let fileEndpointCallCount = 0

      // Intercept logger calls to track file requests
      const originalInfo = testEnv.dservice.logger.info;
      testEnv.dservice.logger.info = function(msg, { url, _method } = {}) {
        // Track requests to the /file endpoint
        if (msg === 'Incoming request' && url && url.includes('/file')) {
          fileEndpointCallCount++;
        }
      };

      // Verify all files can be read via repo.files.cat
      for (const [path, expectedContent] of Object.entries(files)) {
        const fileContent = await newRepo.files.cat(path);
        expect(fileContent).toEqual(expectedContent);
      }

      // Verify that dservice received file requests for each file
      expect(fileEndpointCallCount).toBe(Object.keys(files).length);

      // Restore the original logger
      testEnv.dservice.logger.info = originalInfo;
      loggerSpy.mockRestore();
      await newRepo.close()
    });

    it('files should be ls-able across repo instances', async () => {
      // Create files in multiple folders and subfolders
      const files = {
        'images/logo.png': new TextEncoder().encode('fake-png-data'),
        'images/icons/favicon.ico': new TextEncoder().encode('fake-ico-data'),
        'images/icons/apple-touch-icon.png': new TextEncoder().encode('fake-apple-icon-data'),
        'documents/resume.pdf': new TextEncoder().encode('fake-pdf-data'),
        'documents/contracts/agreement.pdf': new TextEncoder().encode('fake-agreement-data'),
        'documents/contracts/terms.pdf': new TextEncoder().encode('fake-terms-data'),
        'assets/css/style.css': new TextEncoder().encode('body { color: red; }'),
        'assets/js/app.js': new TextEncoder().encode('console.log("Hello World");'),
        'assets/js/utils/helper.js': new TextEncoder().encode('function helper() { return true; }'),
        'data/config.json': new TextEncoder().encode('{"setting": "value"}'),
        'data/users/profiles.json': new TextEncoder().encode('{"users": []}'),
        'backups/old-file.txt': new TextEncoder().encode('old content'),
        'backups/archive/very-old-file.txt': new TextEncoder().encode('very old content')
      };

      // Add all files to the repo
      for (const [path, content] of Object.entries(files)) {
        await repo.files.add(path, content);
      }

      // Stage and commit the changes
      const result = await repo.stage('test.eth', false);
      expect(result).toHaveProperty('cid');
      expect(result.cid instanceof CID).toBe(true);

      // Commit the changes
      const hash = await walletClient.writeContract(result.prepTx);
      expect(hash).toBeDefined();
      const transaction = await client.waitForTransactionReceipt({ hash });
      expect(transaction.status).toBe('success');
      await repo.finalizeCommit(result.cid);

      // Create a new Repo instance with fresh storage
      const newStorage = new MockStorage();
      const newRepo = new Repo('test.eth', newStorage);
      await newRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      // Verify the file structure using ls and check all properties
      await verifyDirectory(newRepo, '/', ['images', 'documents', 'assets', 'data', 'backups']);
      await verifyDirectory(newRepo, '/images', ['logo.png', 'icons']);
      await verifyDirectory(newRepo, '/images/icons', ['favicon.ico', 'apple-touch-icon.png']);
      await verifyDirectory(newRepo, '/documents', ['resume.pdf', 'contracts']);
      await verifyDirectory(newRepo, '/documents/contracts', ['agreement.pdf', 'terms.pdf']);
      await verifyDirectory(newRepo, '/assets', ['css', 'js']);
      await verifyDirectory(newRepo, '/assets/js', ['app.js', 'utils']);
      await verifyDirectory(newRepo, '/assets/js/utils', ['helper.js']);
      await verifyDirectory(newRepo, '/data', ['config.json', 'users']);
      await verifyDirectory(newRepo, '/data/users', ['profiles.json']);
      await verifyDirectory(newRepo, '/backups', ['old-file.txt', 'archive']);
      await verifyDirectory(newRepo, '/backups/archive', ['very-old-file.txt']);

      await newRepo.close()
    });

    it('should handle file updates and deletions across multiple folders', async () => {
      // Create initial files
      const initialFiles = {
        '/images/logo.png': new TextEncoder().encode('original-logo-data'),
        '/documents/resume.pdf': new TextEncoder().encode('original-resume-data'),
        '/assets/css/style.css': new TextEncoder().encode('original-css-data'),
        '/data/config.json': new TextEncoder().encode('{"original": "value"}')
      };

      // Add initial files
      for (const [path, content] of Object.entries(initialFiles)) {
        await repo.files.add(path, content);
      }

      // Stage and commit initial files
      const initialResult = await repo.stage('test.eth', false);
      const initialHash = await walletClient.writeContract(initialResult.prepTx);
      await client.waitForTransactionReceipt({ hash: initialHash });
      await repo.finalizeCommit(initialResult.cid);

      // Update some files and add new ones
      const updatedFiles = {
        '/images/logo.png': new TextEncoder().encode('updated-logo-data'),
        '/assets/css/style.css': new TextEncoder().encode('updated-css-data'),
        '/images/icons/new-icon.png': new TextEncoder().encode('new-icon-data'),
        '/documents/contracts/new-agreement.pdf': new TextEncoder().encode('new-agreement-data')
      };

      // Add updated and new files
      for (const [path, content] of Object.entries(updatedFiles)) {
        await repo.files.add(path, content);
      }

      // Delete a file
      await repo.files.rm('data/config.json');

      // Stage and commit changes
      const updateResult = await repo.stage('test.eth', false);
      const updateHash = await walletClient.writeContract(updateResult.prepTx);
      await client.waitForTransactionReceipt({ hash: updateHash });
      await repo.finalizeCommit(updateResult.cid);

      // Create new repo instance and verify changes
      const newStorage = new MockStorage();
      const newRepo = new Repo('test.eth', newStorage);
      await newRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      // Verify updated files
      const updatedLogoContent = await newRepo.files.cat('/images/logo.png');
      expect(updatedLogoContent).toEqual(updatedFiles['/images/logo.png']);

      const updatedCssContent = await newRepo.files.cat('/assets/css/style.css');
      expect(updatedCssContent).toEqual(updatedFiles['/assets/css/style.css']);

      // Verify new files
      const newIconContent = await newRepo.files.cat('/images/icons/new-icon.png');
      expect(newIconContent).toEqual(updatedFiles['/images/icons/new-icon.png']);

      const newAgreementContent = await newRepo.files.cat('/documents/contracts/new-agreement.pdf');
      expect(newAgreementContent).toEqual(updatedFiles['/documents/contracts/new-agreement.pdf']);

      // Verify unchanged files
      const unchangedResumeContent = await newRepo.files.cat('/documents/resume.pdf');
      expect(unchangedResumeContent).toEqual(initialFiles['/documents/resume.pdf']);

      // Verify deleted file is not accessible
      await expect(newRepo.files.cat('/data/config.json')).rejects.toThrow();

      // Verify file structure using ls and check all properties
      await verifyDirectory(newRepo, '/', ['images', 'documents', 'assets', 'data']);
      await verifyDirectory(newRepo, '/data', []);
      await verifyDirectory(newRepo, '/images', ['logo.png', 'icons']);
      await verifyDirectory(newRepo, '/images/icons', ['new-icon.png']);
      await verifyDirectory(newRepo, '/documents', ['resume.pdf', 'contracts']);
      await verifyDirectory(newRepo, '/documents/contracts', ['new-agreement.pdf']);
      await verifyDirectory(newRepo, '/assets', ['css']);
      await verifyDirectory(newRepo, '/assets/css', ['style.css']);

      // Verify data directory is empty (config.json was deleted)
      const dataFiles = await newRepo.files.ls('/data');
      expect(dataFiles.length).toBe(0);

      await newRepo.close()
    });

    it('should handle realistic scenario with files and pages across multiple updates', async () => {
      // === FIRST UPDATE: Create initial website with files and pages ===
      
      // Create initial pages
      const initialPages = {
        home: {
          path: '/',
          markdown: `---
title: My Portfolio
description: Welcome to my personal portfolio website
---

# Welcome to My Portfolio

This is my personal website showcasing my work and skills.`,
          body: '<h1>Welcome to My Portfolio</h1><p>This is my personal website showcasing my work and skills.</p>'
        },
        about: {
          path: '/about/',
          markdown: `# About Me

I'm a passionate developer with expertise in web technologies.`,
          body: '<h1>About Me</h1><p>I\'m a passionate developer with expertise in web technologies.</p>'
        },
        projects: {
          path: '/projects/',
          markdown: `# My Projects

Here are some of my recent projects.`,
          body: '<h1>My Projects</h1><p>Here are some of my recent projects.</p>'
        }
      };

      // Create initial files
      const initialFiles = {
        '/images/logo.png': new TextEncoder().encode('original-logo-data'),
        '/images/profile.jpg': new TextEncoder().encode('profile-photo-data'),
        '/assets/css/main.css': new TextEncoder().encode('body { font-family: Arial; }'),
        '/assets/js/app.js': new TextEncoder().encode('console.log("App loaded");'),
        '/documents/resume.pdf': new TextEncoder().encode('resume-content'),
        '/data/site-config.json': new TextEncoder().encode('{"theme": "light", "analytics": false}')
      };

      // Add initial pages and files
      for (const page of Object.values(initialPages)) {
        await repo.setPageEdit(page.path, page.markdown, page.body);
      }
      for (const [path, content] of Object.entries(initialFiles)) {
        await repo.files.add(path, content);
      }

      // Stage and commit first version
      const firstResult = await repo.stage('test.eth', false);
      const firstHash = await walletClient.writeContract(firstResult.prepTx);
      await client.waitForTransactionReceipt({ hash: firstHash });
      await repo.finalizeCommit(firstResult.cid);

      // Verify first version content
      for (const page of Object.values(initialPages)) {
        const markdown = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}${page.path}index.md`);
        expect(markdown).toBe(page.markdown);
      }
      for (const [path, expectedContent] of Object.entries(initialFiles)) {
        const storedContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${firstResult.cid.toString()}/_files${path}`);
        expect(storedContent).toBe(new TextDecoder().decode(expectedContent));
      }

      // === SECOND UPDATE: Modify content, add new pages/files, delete some ===
      
      // Update existing pages
      const updatedPages = {
        home: {
          path: '/',
          markdown: `---
title: My Updated Portfolio
description: Welcome to my updated personal portfolio website
---

# Welcome to My Updated Portfolio

This is my updated personal website with new features and content.`,
          body: '<h1>Welcome to My Updated Portfolio</h1><p>This is my updated personal website with new features and content.</p>'
        },
        about: {
          path: '/about/',
          markdown: `# About Me

I'm a passionate developer with expertise in web technologies and blockchain development.`,
          body: '<h1>About Me</h1><p>I\'m a passionate developer with expertise in web technologies and blockchain development.</p>'
        }
      };

      // Add new pages
      const newPages = {
        contact: {
          path: '/contact/',
          markdown: `# Contact Me

Get in touch with me for collaborations and opportunities.`,
          body: '<h1>Contact Me</h1><p>Get in touch with me for collaborations and opportunities.</p>'
        },
        blog: {
          path: '/blog/',
          markdown: `# Blog

Thoughts and insights about technology and development.`,
          body: '<h1>Blog</h1><p>Thoughts and insights about technology and development.</p>'
        }
      };

      // Update existing files
      const updatedFiles = {
        '/images/logo.png': new TextEncoder().encode('updated-logo-data'),
        '/assets/css/main.css': new TextEncoder().encode('body { font-family: Arial; color: #333; }'),
        '/assets/js/app.js': new TextEncoder().encode('console.log("Updated app loaded"); initAnalytics();')
      };

      // Add new files
      const newFiles = {
        '/images/icons/favicon.ico': new TextEncoder().encode('favicon-data'),
        '/images/icons/apple-touch-icon.png': new TextEncoder().encode('apple-icon-data'),
        '/assets/css/dark-theme.css': new TextEncoder().encode('body { background: #222; color: #fff; }'),
        '/assets/js/analytics.js': new TextEncoder().encode('function initAnalytics() { console.log("Analytics initialized"); }'),
        '/documents/portfolio.pdf': new TextEncoder().encode('portfolio-content'),
        '/data/analytics-config.json': new TextEncoder().encode('{"enabled": true, "provider": "google"}')
      };

      // Delete some files
      const filesToDelete = ['/data/site-config.json'];

      // Apply all changes
      for (const page of Object.values(updatedPages)) {
        await repo.setPageEdit(page.path, page.markdown, page.body);
      }
      for (const page of Object.values(newPages)) {
        await repo.setPageEdit(page.path, page.markdown, page.body);
      }
      for (const [path, content] of Object.entries(updatedFiles)) {
        await repo.files.add(path, content);
      }
      for (const [path, content] of Object.entries(newFiles)) {
        await repo.files.add(path, content);
      }
      for (const filePath of filesToDelete) {
        await repo.files.rm(filePath);
      }

      // Delete a page
      await repo.deletePage('/projects/');

      // Stage and commit second version
      const secondResult = await repo.stage('test.eth', false);
      const secondHash = await walletClient.writeContract(secondResult.prepTx);
      await client.waitForTransactionReceipt({ hash: secondHash });
      await repo.finalizeCommit(secondResult.cid);

      // === FINAL VERIFICATION: Create new repo instance and verify everything ===
      
      const newStorage = new MockStorage();
      const newRepo = new Repo('test.eth', newStorage);
      await newRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      // Verify all pages exist and have correct content
      const allPages = await newRepo.getAllPages();
      expect(allPages).toContain('/');
      expect(allPages).toContain('/about/');
      expect(allPages).toContain('/contact/');
      expect(allPages).toContain('/blog/');
      expect(allPages).not.toContain('/projects/'); // Should be deleted

      // Verify updated pages
      const homeMarkdown = await newRepo.getMarkdown('/');
      expect(homeMarkdown).toBe(updatedPages.home.markdown);
      expect(homeMarkdown).toContain('Updated Portfolio');

      const aboutMarkdown = await newRepo.getMarkdown('/about/');
      expect(aboutMarkdown).toBe(updatedPages.about.markdown);
      expect(aboutMarkdown).toContain('blockchain development');

      // Verify new pages
      const contactMarkdown = await newRepo.getMarkdown('/contact/');
      expect(contactMarkdown).toBe(newPages.contact.markdown);

      const blogMarkdown = await newRepo.getMarkdown('/blog/');
      expect(blogMarkdown).toBe(newPages.blog.markdown);

      // Verify deleted page is not accessible
      await expect(newRepo.getMarkdown('/projects/')).rejects.toThrow();

      // Verify updated files
      const updatedLogoContent = await newRepo.files.cat('/images/logo.png');
      expect(updatedLogoContent).toEqual(updatedFiles['/images/logo.png']);

      const updatedCssContent = await newRepo.files.cat('/assets/css/main.css');
      expect(updatedCssContent).toEqual(updatedFiles['/assets/css/main.css']);

      const updatedJsContent = await newRepo.files.cat('/assets/js/app.js');
      expect(updatedJsContent).toEqual(updatedFiles['/assets/js/app.js']);

      // Verify new files
      const faviconContent = await newRepo.files.cat('/images/icons/favicon.ico');
      expect(faviconContent).toEqual(newFiles['/images/icons/favicon.ico']);

      const darkThemeContent = await newRepo.files.cat('/assets/css/dark-theme.css');
      expect(darkThemeContent).toEqual(newFiles['/assets/css/dark-theme.css']);

      const analyticsContent = await newRepo.files.cat('/assets/js/analytics.js');
      expect(analyticsContent).toEqual(newFiles['/assets/js/analytics.js']);

      const portfolioContent = await newRepo.files.cat('/documents/portfolio.pdf');
      expect(portfolioContent).toEqual(newFiles['/documents/portfolio.pdf']);

      const analyticsConfigContent = await newRepo.files.cat('/data/analytics-config.json');
      expect(analyticsConfigContent).toEqual(newFiles['/data/analytics-config.json']);

      // Verify unchanged files (from first version)
      const profileContent = await newRepo.files.cat('/images/profile.jpg');
      expect(profileContent).toEqual(initialFiles['/images/profile.jpg']);

      const resumeContent = await newRepo.files.cat('/documents/resume.pdf');
      expect(resumeContent).toEqual(initialFiles['/documents/resume.pdf']);

      // Verify deleted files are not accessible
      await expect(newRepo.files.cat('/data/site-config.json')).rejects.toThrow();

      // Verify file structure using ls and check all properties
      await verifyDirectory(newRepo, '/', ['images', 'assets', 'documents', 'data']);
      await verifyDirectory(newRepo, '/images', ['logo.png', 'profile.jpg', 'icons']);
      await verifyDirectory(newRepo, '/images/icons', ['favicon.ico', 'apple-touch-icon.png']);
      await verifyDirectory(newRepo, '/assets', ['css', 'js']);
      await verifyDirectory(newRepo, '/assets/css', ['main.css', 'dark-theme.css']);
      await verifyDirectory(newRepo, '/assets/js', ['app.js', 'analytics.js']);
      await verifyDirectory(newRepo, '/documents', ['resume.pdf', 'portfolio.pdf']);
      await verifyDirectory(newRepo, '/data', ['analytics-config.json']);

      // Verify the final state through ENS resolution
      const { cid: finalRoot } = await resolveEnsDomain(client, 'test.eth', addresses.universalResolver);
      expect(finalRoot.toString()).toBe(secondResult.cid.toString());

      await newRepo.close()
    });

    it('should handle non-cached files', async () => {
      // Create some files and folders
      const files = {
        '/images/logo.png': new TextEncoder().encode('fake-png-data'),
        '/documents/resume.pdf': new TextEncoder().encode('fake-pdf-data'),
        '/assets/css/style.css': new TextEncoder().encode('body { color: red; }'),
        '/data/config.json': new TextEncoder().encode('{"setting": "value"}'),
        '/backups/old-file.txt': new TextEncoder().encode('old content')
      };

      // Add all files to the repo
      for (const [path, content] of Object.entries(files)) {
        await repo.files.add(path, content);
      }

      // Stage and finalize the changes
      const result = await repo.stage('test.eth', false);
      expect(result).toHaveProperty('cid');
      expect(result.cid instanceof CID).toBe(true);

      // Commit the changes
      const hash = await walletClient.writeContract(result.prepTx);
      expect(hash).toBeDefined();
      const transaction = await client.waitForTransactionReceipt({ hash });
      expect(transaction.status).toBe('success');
      await repo.finalizeCommit(result.cid);

      // Create new files instance with new unixfs and blockstore
      resetIDB();
      const newStorage = new MockStorage();
      const newRepo = new Repo('test.eth', newStorage);
      await newRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      // Stage again without changes
      const secondResult = await newRepo.stage('test.eth', true);
      expect(secondResult).toHaveProperty('cid');
      expect(secondResult.cid instanceof CID).toBe(true);

      await newRepo.close();
    });

    it('should set avatar and update favicon for all pages after staging', async () => {
      // Create initial pages
      const pages = {
        root: {
          path: '/',
          markdown: '# Home Page\n\nWelcome to the home page.',
          body: '<h1>Home Page</h1><p>Welcome to the home page.</p>'
        },
        about: {
          path: '/about/',
          markdown: '# About\n\nAbout page content.',
          body: '<h1>About</h1><p>About page content.</p>'
        },
        blog: {
          path: '/blog/',
          markdown: '# Blog\n\nBlog index page.',
          body: '<h1>Blog</h1><p>Blog index page.</p>'
        },
        contact: {
          path: '/contact/',
          markdown: '# Contact\n\nContact information.',
          body: '<h1>Contact</h1><p>Contact information.</p>'
        }
      };

      // Add all pages to the repo
      for (const page of Object.values(pages)) {
        await repo.setPageEdit(page.path, page.markdown, page.body);
      }

      // Stage and commit the initial pages
      const initialResult = await repo.stage('test.eth', false);
      expect(initialResult).toHaveProperty('cid');
      expect(initialResult.cid instanceof CID).toBe(true);

      // Verify all pages are staged correctly with default favicon
      for (const page of Object.values(pages)) {
        const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${initialResult.cid.toString()}${page.path}index.html`);
        expect(html).toContain(page.body);
        
        // Check that default favicon is set
        const doc = parser.parseFromString(html, 'text/html');
        const favicon = doc.querySelector('link[rel="icon"]');
        expect(favicon).toBeDefined();
        expect(favicon.href).toBe('/_assets/images/favicon-light.png');
      }

      // Commit the initial version
      await repo.finalizeCommit(initialResult.cid);

      // Set an avatar
      const avatarPngBase64 = 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR4nGNgYAAAAAMAASsJTYQAAAAASUVORK5CYII=';
      const avatarContent = Uint8Array.from(Buffer.from(avatarPngBase64, 'base64'));
      await repo.files.setArtifacts({
        ensAvatar: avatarContent,
        ensFavicon: avatarContent,
        foamAvatar: new TextEncoder().encode('foam-avatar'),
        foamFaviconLight: new TextEncoder().encode('foam-favicon-light'),
        foamFaviconDark: new TextEncoder().encode('foam-favicon-dark'),
      });

      // Make a small edit to one page to trigger staging
      await repo.setPageEdit('/', '# Updated Home Page\n\nWelcome to the updated home page.', '<h1>Updated Home Page</h1><p>Welcome to the updated home page.</p>');

      // Stage the changes (including avatar)
      const avatarResult = await repo.stage('test.eth', false);
      expect(avatarResult).toHaveProperty('cid');
      expect(avatarResult.cid instanceof CID).toBe(true);

      // Verify avatar file is stored in the _files directory
      const storedAvatarContent = await catBytes(testEnv.kubo.kuboApi, `/ipfs/${avatarResult.cid.toString()}/_files/.artifacts/ensAvatar.png`);
      expect(storedAvatarContent).toEqual(avatarContent);

      // Verify that ALL pages now have the avatar as favicon, Open Graph image, and Twitter image
      for (const page of Object.values(pages)) {
        const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${avatarResult.cid.toString()}${page.path}index.html`);
        
        // Parse HTML and check favicon
        const doc = parser.parseFromString(html, 'text/html');
        const favicon = doc.querySelector('link[rel="icon"]');
        expect(favicon).toBeDefined();
        
        // The favicon should now point to the avatar
        expect(favicon.href).toBe('/_files/.artifacts/ensFavicon.png');
        
        // Check Open Graph image meta tag
        const ogImage = doc.querySelector('meta[property="og:image"]');
        expect(ogImage).toBeDefined();
        expect(ogImage.content).toBe(`https://test.eth.link/_files/.artifacts/ensAvatar.png`);
        
        // Check Twitter image meta tag
        const twitterImage = doc.querySelector('meta[name="twitter:image"]');
        expect(twitterImage).toBeDefined();
        expect(twitterImage.content).toBe(`https://test.eth.link/_files/.artifacts/ensAvatar.png`);
        
        // Check Twitter card type (should be 'summary' when using avatar)
        const twitterCard = doc.querySelector('meta[name="twitter:card"]');
        expect(twitterCard).toBeDefined();
        expect(twitterCard.content).toBe('summary');
        
        // Verify page content is still correct
        if (page.path === '/') {
          // Root page should have updated content
          expect(html).toContain('Updated Home Page');
        } else {
          // Other pages should have original content
          expect(html).toContain(page.body);
        }
      }

      // Verify that the webapp manifest also includes the avatar as an icon
      const manifest = await cat(testEnv.kubo.kuboApi, `/ipfs/${avatarResult.cid.toString()}/manifest.json`);
      expect(manifest).toBeDefined();
      
      const manifestData = JSON.parse(manifest);
      expect(manifestData.icons).toBeDefined();
      expect(manifestData.icons.length).toBe(1);
      expect(manifestData.icons[0].src).toBe('/_files/.artifacts/ensAvatar.png');
      expect(manifestData.icons[0].type).toBe('image/png');
      expect(manifestData.icons[0].sizes).toBe('1x1');
    });

    it('should use first img tag in page content for social media previews', async () => {
      // Create a page with an image
      const pageWithImage = {
        path: '/',
        markdown: '# Page with Image\n\nThis page has an image.',
        body: '<h1>Page with Image</h1><p>This page has an image.</p><img src="/images/hero.jpg" alt="Hero image" /><p>More content here.</p>'
      };

      // Add the page to the repo
      await repo.setPageEdit(pageWithImage.path, pageWithImage.markdown, pageWithImage.body);

      // Stage and commit the page
      const result = await repo.stage('test.eth', false);
      expect(result).toHaveProperty('cid');
      expect(result.cid instanceof CID).toBe(true);

      // Verify the page HTML contains the image
      const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}${pageWithImage.path}index.html`);

      // Parse HTML and check social media meta tags
      const doc = parser.parseFromString(html, 'text/html');
      
      const imgTag = doc.querySelector('img');
      expect(imgTag).toBeDefined();
      expect(imgTag.src).toBe('/images/hero.jpg');
      expect(imgTag.alt).toBe('Hero image');

      // Check Open Graph image meta tag - should use the first img tag
      const ogImage = doc.querySelector('meta[property="og:image"]');
      expect(ogImage).toBeDefined();
      expect(ogImage.content).toBe('https://test.eth.link/images/hero.jpg');
      
      // Check Twitter image meta tag - should use the first img tag
      const twitterImage = doc.querySelector('meta[name="twitter:image"]');
      expect(twitterImage).toBeDefined();
      expect(twitterImage.content).toBe('https://test.eth.link/images/hero.jpg');
      
      // Check Twitter card type - should be 'summary_large_image' when using page image
      const twitterCard = doc.querySelector('meta[name="twitter:card"]');
      expect(twitterCard).toBeDefined();
      expect(twitterCard.content).toBe('summary_large_image');
      
      // Check that favicon still uses default (no avatar set)
      const favicon = doc.querySelector('link[rel="icon"]');
      expect(favicon).toBeDefined();
      expect(favicon.href).toBe('/_assets/images/favicon-light.png');
    });

    it('should prioritize page images over avatar for social media previews', async () => {
      // First set an avatar
      const avatarContent = new TextEncoder().encode('fake-avatar-png-data');
      await repo.files.setArtifacts({
        ensAvatar: avatarContent,
        ensFavicon: avatarContent,
        foamAvatar: new TextEncoder().encode('foam-avatar'),
        foamFaviconLight: new TextEncoder().encode('foam-favicon-light'),
        foamFaviconDark: new TextEncoder().encode('foam-favicon-dark'),
      });

      // Create a page with an image
      const pageWithImage = {
        path: '/',
        markdown: '# Page with Image\n\nThis page has an image.',
        body: '<h1>Page with Image</h1><p>This page has an image.</p><img src="/images/hero.jpg" alt="Hero image" /><p>More content here.</p>'
      };

      // Add the page to the repo
      await repo.setPageEdit(pageWithImage.path, pageWithImage.markdown, pageWithImage.body);

      // Stage and commit the page
      const result = await repo.stage('test.eth', false);
      expect(result).toHaveProperty('cid');
      expect(result.cid instanceof CID).toBe(true);

      // Verify the page HTML contains the image
      const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}${pageWithImage.path}index.html`);

      // Parse HTML and check social media meta tags
      const doc = parser.parseFromString(html, 'text/html');

      const imgTag = doc.querySelector('img');
      expect(imgTag).toBeDefined();
      expect(imgTag.src).toBe('/images/hero.jpg');
      expect(imgTag.alt).toBe('Hero image');
      
      // Check Open Graph image meta tag - should use the page image, not avatar
      const ogImage = doc.querySelector('meta[property="og:image"]');
      expect(ogImage).toBeDefined();
      expect(ogImage.content).toBe('https://test.eth.link/images/hero.jpg');
      
      // Check Twitter image meta tag - should use the page image, not avatar
      const twitterImage = doc.querySelector('meta[name="twitter:image"]');
      expect(twitterImage).toBeDefined();
      expect(twitterImage.content).toBe('https://test.eth.link/images/hero.jpg');
      
      // Check Twitter card type - should be 'summary_large_image' when using page image
      const twitterCard = doc.querySelector('meta[name="twitter:card"]');
      expect(twitterCard).toBeDefined();
      expect(twitterCard.content).toBe('summary_large_image');
      
      // Check that favicon still uses avatar (since avatar was set)
      const favicon = doc.querySelector('link[rel="icon"]');
      expect(favicon).toBeDefined();
      expect(favicon.href).toBe('/_files/.artifacts/ensFavicon.png');
    });

    it('should handle case with no avatar and no images in page content', async () => {
      // Create a page without any images and no avatar set
      const pageWithoutImage = {
        path: '/',
        markdown: '# Page without Image\n\nThis page has no images.',
        body: '<h1>Page without Image</h1><p>This page has no images.</p><p>Just text content here.</p>'
      };

      // Add the page to the repo
      await repo.setPageEdit(pageWithoutImage.path, pageWithoutImage.markdown, pageWithoutImage.body);

      // Stage and commit the page
      const result = await repo.stage('test.eth', false);
      expect(result).toHaveProperty('cid');
      expect(result.cid instanceof CID).toBe(true);

      // Verify the page HTML contains the content but no images
      const html = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}${pageWithoutImage.path}index.html`);
      expect(html).toContain(pageWithoutImage.body);
      expect(html).not.toContain('<img');

      // Parse HTML and check social media meta tags
      const doc = parser.parseFromString(html, 'text/html');
      
      // Check Open Graph image meta tag - should be null when no avatar and no images
      const ogImage = doc.querySelector('meta[property="og:image"]');
      expect(ogImage).toBeDefined();
      expect(ogImage.content).toBe('https://test.eth.link/_assets/images/logo.png');
      
      // Check Twitter image meta tag - should be null when no avatar and no images
      const twitterImage = doc.querySelector('meta[name="twitter:image"]');
      expect(twitterImage).toBeDefined();
      expect(twitterImage.content).toBe('https://test.eth.link/_assets/images/logo.png');
      
      // Check Twitter card type - should be 'summary' when no images
      const twitterCard = doc.querySelector('meta[name="twitter:card"]');
      expect(twitterCard).toBeDefined();
      expect(twitterCard.content).toBe('summary');
      
      // Check that favicon uses default
      const favicon = doc.querySelector('link[rel="icon"]');
      expect(favicon).toBeDefined();
      expect(favicon.href).toBe('/_assets/images/favicon-light.png');
    });
  });

  describe('Settings Tests', () => {
    beforeAll(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'new.simplepage.eth', templateCid.toString());
    });

    beforeEach(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'test.eth', testDataCid.toString());
      await repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });
    });

    afterEach(async () => {
      resetIDB()
      await repo.close()
    });

    it('should handle complete settings integration flow', async () => {
      // 1. Read/write some settings
      const initialSettings = {
        theme: 'dark',
        language: 'en',
        notifications: true,
        analytics: {
          enabled: true,
          provider: 'google'
        }
      };

      await repo.settings.write(initialSettings);
      
      // Verify settings are written
      const readSettings = await repo.settings.read();
      expect(readSettings).toEqual(initialSettings);
      
      // Read individual properties
      const theme = await repo.settings.readProperty('theme');
      expect(theme).toBe('dark');
      
      // Read nested properties from the full settings object
      const allSettings = await repo.settings.read();
      expect(allSettings.analytics.enabled).toBe(true);

      // 2. Stage and commit
      const result = await repo.stage('test.eth', false);
      expect(result).toHaveProperty('cid');
      expect(result.cid instanceof CID).toBe(true);

      // Verify settings are staged
      const settingsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${result.cid.toString()}/settings.json`);
      expect(settingsContent).toBeDefined();
      
      const parsedSettings = JSON.parse(settingsContent);
      expect(parsedSettings.theme).toBe('dark');
      expect(parsedSettings.analytics.enabled).toBe(true);

      // Commit the changes
      const hash = await walletClient.writeContract(result.prepTx);
      expect(hash).toBeDefined();
      const transaction = await client.waitForTransactionReceipt({ hash });
      expect(transaction.status).toBe('success');
      await repo.finalizeCommit(result.cid);

      // 3. Load new repo instance
      const newStorage = new MockStorage();
      const newRepo = new Repo('test.eth', newStorage);
      await newRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      // 4. Read settings from new instance
      const loadedSettings = await newRepo.settings.read();
      expect(loadedSettings).toEqual(initialSettings);
      
      const loadedTheme = await newRepo.settings.readProperty('theme');
      expect(loadedTheme).toBe('dark');

      // 5. Write new settings
      await newRepo.settings.writeProperty('theme', 'light');
      await newRepo.settings.writeProperty('custom', 'new-value');
      
      // Verify changes are detected
      expect(await newRepo.settings.hasChanges()).toBe(true);
      
      // Verify new values
      const updatedTheme = await newRepo.settings.readProperty('theme');
      const customValue = await newRepo.settings.readProperty('custom');
      expect(updatedTheme).toBe('light');
      expect(customValue).toBe('new-value');

      await newRepo.close();
    });
  })

  describe('History Integration', () => {
    const historyDomain = 'history.eth';
    let historyStorage;
    let historyRepo;

    beforeAll(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'new.simplepage.eth', templateCid.toString());
      testEnv.evm.mintPage(historyDomain, 365 * 24 * 60 * 60, '0x0000000000000000000000000000000000000001');
      testEnv.evm.setResolver(addresses.universalResolver, historyDomain, addresses.resolver1);
      await testEnv.waitUntilBlockIsIndexed(Number(testEnv.evm.getBlockNumber()));
    });

    beforeEach(async () => {
      historyStorage = new MockStorage();
      historyRepo = new Repo(historyDomain, historyStorage);
      testEnv.evm.setContenthash(addresses.resolver1, historyDomain, testDataCid.toString());
      await historyRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });
    });

    afterEach(async () => {
      historyStorage.clear();
      resetIDB();
      await historyRepo.close();
    });

    it('should retrieve history with parent relationships', async () => {
      const previousRoot = historyRepo.repoRoot.cid;

      await historyRepo.setPageEdit('/', '# Updated', '<h1>Updated</h1>');
      const result = await historyRepo.stage(historyDomain, false);
      const hash = await walletClient.writeContract(result.prepTx);
      const receipt = await client.waitForTransactionReceipt({ hash });
      await historyRepo.finalizeCommit(result.cid);
      await testEnv.waitUntilBlockIsIndexed(Number(receipt.blockNumber));

      const history = await historyRepo.history.get();
      const entriesByCid = new Map(history.map(entry => [entry.cid.toString(), entry]));

      const latestEntry = entriesByCid.get(result.cid.toString());
      expect(latestEntry).toBeDefined();
      expect(latestEntry.parents.map(parent => parent.toString())).toContain(previousRoot.toString());

      const previousEntry = entriesByCid.get(previousRoot.toString());
      expect(previousEntry).toBeDefined();
      expect(latestEntry.blockNumber).toBeGreaterThan(previousEntry.blockNumber);
    });
  });

  describe('Error Handling Tests', () => {
    it('should handle initialization errors gracefully', async () => {
      const invalidRepo = new Repo('invalid.eth', storage, {
        apiEndpoint: testEnv.dserviceUrl
      });
      
      await expect(invalidRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      })).rejects.toThrow();

      await invalidRepo.close()
    });

    it('should handle staging without initialization', async () => {
      const uninitializedRepo = new Repo('test.eth', storage, {
        apiEndpoint: testEnv.dserviceUrl
      });
      
      await expect(uninitializedRepo.stage('test.eth', false)).rejects.toThrow();

      await uninitializedRepo.close()
    });

    it('should throw error when staging with update=true and missing template', async () => {
      // Set up test domain with content
      testEnv.evm.setContenthash(addresses.resolver1, 'test.eth', testDataCid.toString());
      
      // Clear template domain contenthash
      testEnv.evm.clearContenthash(addresses.resolver1, 'new.simplepage.eth');
      
      const repo = new Repo('test.eth', storage, {
        apiEndpoint: testEnv.dserviceUrl
      });

      // Initialize repo (should succeed since test.eth has content)
      await repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      // Make an edit so we can stage
      await repo.setPageEdit('/', '# Test', '<h1>Test</h1>');

      // Attempt to stage with update=true should fail
      await expect(repo.stage('test.eth', true)).rejects.toThrow('Template root not found');

      await repo.close()
    });

    it('should reject staging when subscription is expired', async () => {
      const expiredDomain = 'expired.eth'
      const expiredStorage = new MockStorage();
      resetIDB();

      const shortDurationSeconds = 2
      testEnv.evm.mintPage(expiredDomain, shortDurationSeconds, '0x0000000000000000000000000000000000000001');
      testEnv.evm.setResolver(addresses.universalResolver, expiredDomain, addresses.resolver1);
      testEnv.evm.setContenthash(addresses.resolver1, expiredDomain, testDataCid.toString());
      testEnv.evm.setContenthash(addresses.resolver1, 'new.simplepage.eth', templateCid.toString());

      const currentBlock = Number(testEnv.evm.getBlockNumber());
      const expiryBlock = currentBlock + 5;
      await testEnv.waitUntilBlockIsIndexed(expiryBlock);

      const expiredRepo = new Repo(expiredDomain, expiredStorage, {
        apiEndpoint: testEnv.dserviceUrl
      });

      await expiredRepo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });

      await expiredRepo.setPageEdit('/', '# Expired', '<h1>Expired</h1>');

      await expect(expiredRepo.stage(expiredDomain, false)).rejects.toThrow('401')

      await expiredRepo.close()
      expiredStorage.clear();
    });
  });

  describe('RSS Feed Generation', () => {
    const rssParser = new Parser();
    
    beforeAll(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'new.simplepage.eth', templateCid.toString());
    });

    beforeEach(async () => {
      testEnv.evm.setContenthash(addresses.resolver1, 'test.eth', testDataCid.toString());
      await repo.init(client, {
        chainId: parseInt(testEnv.evm.chainId),
        universalResolver: addresses.universalResolver
      });
    });

    it('should generate RSS feed with items when pages have rss: true', async () => {
      // Set up root page with metadata
      const rootMarkdown = `---
title: My Test Blog
description: A blog about testing
language: en
---
# Welcome to my blog`;
      await repo.setPageEdit('/', rootMarkdown, '<h1>Welcome to my blog</h1>');

      // Add a blog post with RSS enabled
      const blogPost1 = `---
title: First Blog Post
description: A short summary of my first post
rss: true
created: 2024-01-15T10:00:00Z
updated: 2024-01-16T12:00:00Z
tags:
  - tutorial
  - introduction
---
# First Blog Post

This is my first post with an image!

![Test Image](/images/test.png)

Some content here.`;
      await repo.setPageEdit('/blog/post1/', blogPost1, '<h1>First Blog Post</h1><p>This is my first post with an image!</p><img src="/images/test.png" /><p>Some content here.</p>');

      // Add another blog post with RSS enabled
      const blogPost2 = `---
title: Second Blog Post
description: My second post
rss: true
created: 2024-01-20T10:00:00Z
tags:
  - update
---
# Second Blog Post

More content.`;
      await repo.setPageEdit('/blog/post2/', blogPost2, '<h1>Second Blog Post</h1><p>More content.</p>');

      // Add a page without RSS
      const aboutPage = `---
title: About
description: About page
---
# About

This page should not appear in RSS.`;
      await repo.setPageEdit('/about/', aboutPage, '<h1>About</h1><p>This page should not appear in RSS.</p>');

      // Stage the changes
      const { cid } = await repo.stage('test.eth', false);

      // Verify RSS file was created
      const files = await ls(testEnv.kubo.kuboApi, `/ipfs/${cid.toString()}`);
      expect(files).toContain('rss.xml');

      // Read and verify RSS content
      const rssContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${cid.toString()}/rss.xml`);
      
      // Verify XML structure
      expect(rssContent).toContain('<?xml version="1.0" encoding="UTF-8"?>');
      expect(rssContent).toContain('<rss version="2.0"');
      expect(rssContent).toContain('xmlns:atom="http://www.w3.org/2005/Atom"');
      expect(rssContent).toContain('xmlns:content="http://purl.org/rss/1.0/modules/content/"');
      expect(rssContent).toContain('xmlns:dc="http://purl.org/dc/elements/1.1/"');
      expect(rssContent).toContain('xmlns:media="http://search.yahoo.com/mrss/"');

      // Verify channel metadata
      expect(rssContent).toContain('<title>My Test Blog</title>');
      expect(rssContent).toContain('<description>A blog about testing</description>');
      expect(rssContent).toContain('<language>en</language>');
      expect(rssContent).toContain('https://test.eth.link');
      expect(rssContent).toContain('<atom:link href="https://test.eth.link/rss.xml" rel="self" type="application/rss+xml" />');

      // Verify items are present (should be ordered by date descending)
      expect(rssContent).toContain('<title>Second Blog Post</title>'); // Newer post
      expect(rssContent).toContain('<title>First Blog Post</title>');
      
      // Verify item details for first post
      expect(rssContent).toContain('<link>https://test.eth.link/blog/post1</link>');
      expect(rssContent).toContain('<description>A short summary of my first post</description>');
      expect(rssContent).toContain('<atom:updated>2024-01-16T12:00:00Z</atom:updated>');
      expect(rssContent).toContain('<dc:modified>2024-01-16T12:00:00Z</dc:modified>');
      expect(rssContent).toContain('<category>tutorial</category>');
      expect(rssContent).toContain('<category>introduction</category>');
      expect(rssContent).toContain('<guid isPermaLink="true">https://test.eth.link/blog/post1</guid>');
      
      // Verify NO enclosure tag for images (images are in content:encoded instead)
      expect(rssContent).not.toContain('enclosure url="https://test.eth.link/images/test.png"');
      
      // Verify HTML content is in CDATA
      expect(rssContent).toContain('<content:encoded><![CDATA[');

      // Validate RSS XML structure using rss-parser
      const feed = await rssParser.parseString(rssContent);
      
      // Verify channel structure
      expect(feed.title).toBe('My Test Blog');
      expect(feed.link).toBe('https://test.eth.link');
      expect(feed.description).toBe('A blog about testing');
      expect(Array.isArray(feed.items)).toBe(true);
      expect(feed.items.length).toBe(2); // Two blog posts
      
      // Verify feed has atom:link self reference
      expect(rssContent).toContain('<atom:link href="https://test.eth.link/rss.xml" rel="self" type="application/rss+xml" />');
      
      // Verify first post (has image, updated date, and tags)
      const firstPost = feed.items.find(item => item.link.includes('post1'));
      expect(firstPost).toBeDefined();
      expect(firstPost.title).toBe('First Blog Post');
      expect(firstPost.content).toBeDefined(); // content:encoded should exist
      // The image is in content:encoded XML but rss-parser doesn't expose it in item.content
      // So verify it's in the raw XML instead
      expect(rssContent).toContain('<img src="/images/test.png"');
      expect(firstPost.categories).toContain('tutorial');
      expect(firstPost.categories).toContain('introduction');
      expect(rssContent).not.toContain('<enclosure'); // Images don't use enclosure
      
      // Verify second post (simpler)
      const secondPost = feed.items.find(item => item.link.includes('post2'));
      expect(secondPost).toBeDefined();
      expect(secondPost.title).toBe('Second Blog Post');
      expect(secondPost.categories).toContain('update');

      // Verify about page is NOT in RSS
      expect(rssContent).not.toContain('About page');
      
      // Verify _redirects contains /feed redirect
      const redirectsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${cid.toString()}/_redirects`);
      expect(redirectsContent).toContain('/feed  /rss.xml  301');
    });

    it('should not generate RSS feed when no pages have rss: true', async () => {
      // Add pages without RSS
      const page1 = `---
title: Page 1
description: First page
---
# Page 1`;
      await repo.setPageEdit('/page1/', page1, '<h1>Page 1</h1>');

      const page2 = `---
title: Page 2
description: Second page
---
# Page 2`;
      await repo.setPageEdit('/page2/', page2, '<h1>Page 2</h1>');

      // Stage the changes
      const { cid } = await repo.stage('test.eth', false);

      // Verify RSS file was NOT created
      const files = await ls(testEnv.kubo.kuboApi, `/ipfs/${cid.toString()}`);
      expect(files).not.toContain('rss.xml');
      
      // Verify _redirects still contains /feed redirect (defensive)
      const redirectsContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${cid.toString()}/_redirects`);
      expect(redirectsContent).toContain('/feed  /rss.xml  301');
    });

    it('should skip RSS items without created date', async () => {
      // Add a blog post with RSS but no created date
      const invalidPost = `---
title: Invalid Post
description: Missing created date
rss: true
---
# Invalid Post`;
      await repo.setPageEdit('/blog/invalid/', invalidPost, '<h1>Invalid Post</h1>');

      // Add a valid post
      const validPost = `---
title: Valid Post
rss: true
created: 2024-01-15T10:00:00Z
---
# Valid Post`;
      await repo.setPageEdit('/blog/valid/', validPost, '<h1>Valid Post</h1>');

      // Stage the changes
      const { cid } = await repo.stage('test.eth', false);

      // Verify RSS file was created
      const files = await ls(testEnv.kubo.kuboApi, `/ipfs/${cid.toString()}`);
      expect(files).toContain('rss.xml');

      // Read RSS content
      const rssContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${cid.toString()}/rss.xml`);
      
      // Verify only valid post is in feed
      expect(rssContent).toContain('Valid Post');
      expect(rssContent).not.toContain('Invalid Post');
      
      // Validate RSS structure using rss-parser
      const feed = await rssParser.parseString(rssContent);
      expect(feed.items.length).toBe(1);
      expect(feed.items[0].title).toBe('Valid Post');
      expect(feed.items[0].content).toBeDefined(); // Should have content:encoded
      // Should NOT have categories or enclosures (no media in this post)
      expect(rssContent).not.toContain('<category>');
      expect(rssContent).not.toContain('<enclosure');
    });

    it('should handle audio/video media with enclosure tags', async () => {
      // Add a blog post with audio
      const audioPost = `---
title: Podcast Episode
rss: true
created: 2024-01-15T10:00:00Z
---
# Podcast Episode

Listen to my podcast:

![Episode](/audio/episode1.mp3)`;
      await repo.setPageEdit('/podcast/ep1/', audioPost, '<h1>Podcast Episode</h1>');

      // Stage the changes
      const { cid } = await repo.stage('test.eth', false);

      // Read RSS content
      const rssContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${cid.toString()}/rss.xml`);
      
      // Verify enclosure tag for audio (images and video should use enclosure too)
      expect(rssContent).toContain('<enclosure url="https://test.eth.link/audio/episode1.mp3" type="audio/mpeg"');
      
      // Validate RSS with enclosure using rss-parser
      const feed = await rssParser.parseString(rssContent);
      expect(feed.items.length).toBe(1);
      expect(feed.items[0].title).toBe('Podcast Episode');
      expect(rssContent).toContain('<enclosure url="https://test.eth.link/audio/episode1.mp3" type="audio/mpeg"'); // Should have enclosure for audio
    });

    it('should add RSS autodiscovery link to HTML pages', async () => {
      // Add a page
      const page = `---
title: Test Page
---
# Test Page`;
      await repo.setPageEdit('/test/', page, '<h1>Test Page</h1>');

      // Stage the changes
      const { cid } = await repo.stage('test.eth', false);

      // Read HTML content
      const htmlContent = await cat(testEnv.kubo.kuboApi, `/ipfs/${cid.toString()}/test/index.html`);
      
      // Parse HTML
      const doc = parser.parseFromString(htmlContent, 'text/html');
      
      // Verify RSS link exists
      const rssLink = doc.querySelector('link[rel="alternate"][type="application/rss+xml"]');
      expect(rssLink).toBeTruthy();
      expect(rssLink.getAttribute('href')).toBe('/rss.xml');
      expect(rssLink.getAttribute('title')).toContain('RSS Feed');
    });
  });
});