| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334 |
- import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
- import { mkdtemp, rm, mkdir, writeFile } from 'node:fs/promises';
- import { join } from 'node:path';
- import { tmpdir } from 'node:os';
- import {
- classifyBundledFile,
- findPackageRoot,
- getBundledFilePath,
- listBundledFiles,
- bundledFileExists,
- type BundledFileType,
- } from './bundled.js';
- // ── classifyBundledFile ───────────────────────────────────────────────────────
- // Pure function — no I/O, no setup needed.
- describe('classifyBundledFile', () => {
- // ✅ Positive: agent prefix
- test('returns "agent" for .opencode/agent/ paths', () => {
- // Arrange
- const path = '.opencode/agent/core/openagent.md';
- // Act
- const result: BundledFileType = classifyBundledFile(path);
- // Assert
- expect(result).toBe('agent');
- });
- // ✅ Positive: agent prefix — nested deeply
- test('returns "agent" for deeply nested agent paths', () => {
- expect(classifyBundledFile('.opencode/agent/sub/dir/file.md')).toBe('agent');
- });
- // ✅ Positive: context prefix
- test('returns "context" for .opencode/context/ paths', () => {
- expect(classifyBundledFile('.opencode/context/standards.md')).toBe('context');
- });
- // ✅ Positive: context prefix — nested
- test('returns "context" for nested context paths', () => {
- expect(classifyBundledFile('.opencode/context/sub/file.md')).toBe('context');
- });
- // ✅ Positive: skill prefix
- test('returns "skill" for .opencode/skills/ paths', () => {
- expect(classifyBundledFile('.opencode/skills/my-skill.md')).toBe('skill');
- });
- // ✅ Positive: skill prefix — nested
- test('returns "skill" for nested skills paths', () => {
- expect(classifyBundledFile('.opencode/skills/category/skill.md')).toBe('skill');
- });
- // ✅ Positive: config fallback — arbitrary path
- test('returns "config" for unrecognised paths', () => {
- expect(classifyBundledFile('some/other/file.json')).toBe('config');
- });
- // ✅ Positive: config fallback — root-level file
- test('returns "config" for a root-level file', () => {
- expect(classifyBundledFile('README.md')).toBe('config');
- });
- // ❌ Negative: path that starts with .opencode/ but not a known subdir
- test('returns "config" for .opencode/ paths with unknown subdir', () => {
- expect(classifyBundledFile('.opencode/unknown/file.md')).toBe('config');
- });
- // ❌ Negative: partial prefix match should NOT classify as agent
- test('returns "config" for path that only partially matches agent prefix', () => {
- // ".opencode/agentX/" is NOT ".opencode/agent/"
- expect(classifyBundledFile('.opencode/agentX/file.md')).toBe('config');
- });
- // ❌ Negative: partial prefix match should NOT classify as context
- test('returns "config" for path that only partially matches context prefix', () => {
- expect(classifyBundledFile('.opencode/contexts/file.md')).toBe('config');
- });
- // ❌ Negative: empty string
- test('returns "config" for an empty string', () => {
- expect(classifyBundledFile('')).toBe('config');
- });
- });
- // ── getBundledFilePath ────────────────────────────────────────────────────────
- // Pure function — no I/O.
- describe('getBundledFilePath', () => {
- // ✅ Positive: joins packageRoot and relativePath
- test('joins packageRoot and relativePath correctly', () => {
- // Arrange
- const packageRoot = '/usr/local/lib/oac';
- const relativePath = '.opencode/agent/core/openagent.md';
- // Act
- const result = getBundledFilePath(packageRoot, relativePath);
- // Assert
- expect(result).toBe('/usr/local/lib/oac/.opencode/agent/core/openagent.md');
- });
- // ✅ Positive: works with nested relative paths
- test('handles nested relative paths', () => {
- const result = getBundledFilePath('/root', '.opencode/skills/sub/skill.md');
- expect(result).toBe('/root/.opencode/skills/sub/skill.md');
- });
- // ❌ Negative: empty relative path returns just the packageRoot
- test('returns packageRoot when relativePath is empty', () => {
- const result = getBundledFilePath('/root', '');
- expect(result).toBe('/root');
- });
- });
- // ── findPackageRoot ───────────────────────────────────────────────────────────
- describe('findPackageRoot', () => {
- let tmpDir: string;
- beforeAll(async () => {
- tmpDir = await mkdtemp(join(tmpdir(), 'oac-bundled-test-'));
- });
- afterAll(async () => {
- await rm(tmpDir, { recursive: true, force: true });
- });
- // ✅ Positive: finds a directory that has both .opencode/ and package.json
- test('returns the directory that has both .opencode/ and package.json', async () => {
- // Arrange — create a fake package root
- const fakeRoot = join(tmpDir, 'fake-pkg');
- await mkdir(join(fakeRoot, '.opencode'), { recursive: true });
- await writeFile(join(fakeRoot, 'package.json'), '{}', 'utf8');
- // Also create a subdirectory to start the walk from
- const startDir = join(fakeRoot, 'dist', 'lib');
- await mkdir(startDir, { recursive: true });
- // Act
- const result = findPackageRoot(startDir);
- // Assert
- expect(result).toBe(fakeRoot);
- });
- // ✅ Positive: finds root when starting exactly at the package root
- test('returns the start directory itself when it is the package root', async () => {
- // Arrange
- const fakeRoot = join(tmpDir, 'exact-root');
- await mkdir(join(fakeRoot, '.opencode'), { recursive: true });
- await writeFile(join(fakeRoot, 'package.json'), '{}', 'utf8');
- // Act
- const result = findPackageRoot(fakeRoot);
- // Assert
- expect(result).toBe(fakeRoot);
- });
- // ❌ Negative: throws when no package root is found (isolated tmp dir with no markers)
- test('throws an error when no package root is found walking to filesystem root', async () => {
- // Arrange — a directory with neither .opencode/ nor package.json
- const isolated = join(tmpDir, 'isolated-no-markers');
- await mkdir(isolated, { recursive: true });
- // Act & Assert — we cannot actually walk to the real filesystem root in a
- // test (it would find the monorepo's package.json), so we test the error
- // message shape by checking that a directory missing .opencode throws when
- // the walk terminates. We use a path that IS the filesystem root equivalent
- // by mocking: instead, we verify the thrown error message format by calling
- // with a path that has package.json but no .opencode, and one that has
- // .opencode but no package.json — neither should match, but the walk will
- // eventually reach the real monorepo root. So we test the error path by
- // verifying the function throws when given a path that cannot possibly
- // resolve (we use the OS tmpdir itself, which has no .opencode).
- //
- // The safest approach: create a temp dir tree that is self-contained and
- // has no .opencode anywhere. We can't prevent the walk from going above
- // tmpdir, so we test the error message by checking it contains the
- // expected substring when we know it will throw.
- //
- // NOTE: In CI / a clean environment this will throw because there is no
- // .opencode above the tmpdir. In a monorepo dev environment the walk may
- // find the repo root. We therefore test the error *shape* by directly
- // calling with a path that we know will fail: the filesystem root '/'.
- expect(() => findPackageRoot('/')).toThrow(
- 'getPackageRoot: could not find a directory with ".opencode/" and "package.json"',
- );
- });
- // ❌ Negative: error message includes the start directory
- test('error message includes the starting directory', () => {
- // Arrange & Act & Assert
- let thrownMessage = '';
- try {
- findPackageRoot('/');
- } catch (err) {
- thrownMessage = err instanceof Error ? err.message : String(err);
- }
- expect(thrownMessage).toContain('"/"');
- });
- // ❌ Negative: directory with only package.json (no .opencode) does not match
- test('does not match a directory that has package.json but no .opencode', async () => {
- // Arrange — a directory with only package.json, no .opencode
- const noOpencode = join(tmpDir, 'no-opencode');
- await mkdir(noOpencode, { recursive: true });
- await writeFile(join(noOpencode, 'package.json'), '{}', 'utf8');
- // Start from a child — the walk will pass through noOpencode (no match)
- // and continue upward until it finds the monorepo root or throws.
- // We just verify it does NOT return noOpencode.
- let result: string | undefined;
- try {
- result = findPackageRoot(noOpencode);
- } catch {
- result = undefined;
- }
- // If it found something, it must NOT be noOpencode (which lacks .opencode)
- if (result !== undefined) {
- expect(result).not.toBe(noOpencode);
- }
- // Either it threw (correct) or found a higher-level root (also acceptable)
- expect(true).toBe(true); // test passes either way — the key is it didn't return noOpencode
- });
- });
- // ── listBundledFiles ──────────────────────────────────────────────────────────
- describe('listBundledFiles', () => {
- let packageRoot: string;
- beforeAll(async () => {
- packageRoot = await mkdtemp(join(tmpdir(), 'oac-list-bundled-'));
- // Create a fake package structure with files in all three subdirs
- await mkdir(join(packageRoot, '.opencode', 'agent', 'core'), { recursive: true });
- await mkdir(join(packageRoot, '.opencode', 'context'), { recursive: true });
- await mkdir(join(packageRoot, '.opencode', 'skills', 'sub'), { recursive: true });
- await writeFile(join(packageRoot, '.opencode', 'agent', 'core', 'openagent.md'), '# Agent', 'utf8');
- await writeFile(join(packageRoot, '.opencode', 'agent', 'helper.md'), '# Helper', 'utf8');
- await writeFile(join(packageRoot, '.opencode', 'context', 'standards.md'), '# Standards', 'utf8');
- await writeFile(join(packageRoot, '.opencode', 'skills', 'sub', 'skill.md'), '# Skill', 'utf8');
- });
- afterAll(async () => {
- await rm(packageRoot, { recursive: true, force: true });
- });
- // ✅ Positive: returns relative paths for all files in all three subdirs
- test('returns relative paths for all bundled files', async () => {
- // Act
- const files = await listBundledFiles(packageRoot);
- // Assert — all four files should be present
- expect(files).toContain('.opencode/agent/core/openagent.md');
- expect(files).toContain('.opencode/agent/helper.md');
- expect(files).toContain('.opencode/context/standards.md');
- expect(files).toContain('.opencode/skills/sub/skill.md');
- expect(files).toHaveLength(4);
- });
- // ✅ Positive: paths are relative (not absolute)
- test('returns relative paths, not absolute paths', async () => {
- const files = await listBundledFiles(packageRoot);
- for (const f of files) {
- expect(f.startsWith('/')).toBe(false);
- }
- });
- // ✅ Positive: paths start with .opencode/
- test('all returned paths start with .opencode/', async () => {
- const files = await listBundledFiles(packageRoot);
- for (const f of files) {
- expect(f.startsWith('.opencode/')).toBe(true);
- }
- });
- // ❌ Negative: missing subdirectories are silently skipped
- test('silently skips subdirectories that do not exist', async () => {
- // Arrange — a package root with only the agent subdir
- const sparseRoot = await mkdtemp(join(tmpdir(), 'oac-sparse-pkg-'));
- try {
- await mkdir(join(sparseRoot, '.opencode', 'agent'), { recursive: true });
- await writeFile(join(sparseRoot, '.opencode', 'agent', 'only.md'), '# Only', 'utf8');
- // Act — context/ and skills/ don't exist
- const files = await listBundledFiles(sparseRoot);
- // Assert — only the agent file, no errors
- expect(files).toHaveLength(1);
- expect(files[0]).toBe('.opencode/agent/only.md');
- } finally {
- await rm(sparseRoot, { recursive: true, force: true });
- }
- });
- // ❌ Negative: empty package root returns empty array
- test('returns empty array when no bundled subdirs exist', async () => {
- // Arrange — completely empty package root
- const emptyRoot = await mkdtemp(join(tmpdir(), 'oac-empty-pkg-'));
- try {
- const files = await listBundledFiles(emptyRoot);
- expect(files).toHaveLength(0);
- } finally {
- await rm(emptyRoot, { recursive: true, force: true });
- }
- });
- });
- // ── bundledFileExists ─────────────────────────────────────────────────────────
- describe('bundledFileExists', () => {
- let packageRoot: string;
- beforeAll(async () => {
- packageRoot = await mkdtemp(join(tmpdir(), 'oac-exists-test-'));
- await mkdir(join(packageRoot, '.opencode', 'agent'), { recursive: true });
- await writeFile(join(packageRoot, '.opencode', 'agent', 'present.md'), '# Present', 'utf8');
- });
- afterAll(async () => {
- await rm(packageRoot, { recursive: true, force: true });
- });
- // ✅ Positive: returns true for a file that exists
- test('returns true when the bundled file exists', async () => {
- const exists = await bundledFileExists(packageRoot, '.opencode/agent/present.md');
- expect(exists).toBe(true);
- });
- // ❌ Negative: returns false for a file that does not exist
- test('returns false when the bundled file does not exist', async () => {
- const exists = await bundledFileExists(packageRoot, '.opencode/agent/missing.md');
- expect(exists).toBe(false);
- });
- });
|