a46b7b8240
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
127 lines
3.6 KiB
TypeScript
127 lines
3.6 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, test } from "bun:test"
|
|
import { mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs"
|
|
import { tmpdir } from "node:os"
|
|
import { join, resolve } from "node:path"
|
|
import { resolveFilePath, resolveFileReferencesInText } from "./file-reference-resolver"
|
|
|
|
describe("resolveFilePath", () => {
|
|
const cwd = "/skills/gsd"
|
|
|
|
test("expands bare environment variables before resolving absolute paths", () => {
|
|
//#given
|
|
const homeDir = process.env.HOME
|
|
if (!homeDir) {
|
|
throw new Error("HOME must be set for file reference resolver tests")
|
|
}
|
|
|
|
//#when
|
|
const resolved = resolveFilePath("$HOME/foo.md", cwd)
|
|
|
|
//#then
|
|
expect(resolved).toBe(resolve(homeDir, "foo.md"))
|
|
})
|
|
|
|
test("expands braced environment variables before resolving absolute paths", () => {
|
|
//#given
|
|
const homeDir = process.env.HOME
|
|
if (!homeDir) {
|
|
throw new Error("HOME must be set for file reference resolver tests")
|
|
}
|
|
|
|
//#when
|
|
const resolved = resolveFilePath("${HOME}/foo.md", cwd)
|
|
|
|
//#then
|
|
expect(resolved).toBe(resolve(homeDir, "foo.md"))
|
|
})
|
|
|
|
test("keeps absolute paths absolute", () => {
|
|
//#given
|
|
const absolutePath = "/abs/path.md"
|
|
|
|
//#when
|
|
const resolved = resolveFilePath(absolutePath, cwd)
|
|
|
|
//#then
|
|
expect(resolved).toBe(resolve(absolutePath))
|
|
})
|
|
|
|
test("resolves relative paths from cwd", () => {
|
|
//#given
|
|
const relativePath = "relative/path.md"
|
|
|
|
//#when
|
|
const resolved = resolveFilePath(relativePath, cwd)
|
|
|
|
//#then
|
|
expect(resolved).toBe(resolve(cwd, relativePath))
|
|
})
|
|
})
|
|
|
|
describe("resolveFileReferencesInText", () => {
|
|
const fixtureRoot = join(tmpdir(), `file-reference-resolver-${Date.now()}`)
|
|
const workspaceDir = join(fixtureRoot, "workspace")
|
|
const notesDir = join(workspaceDir, "notes")
|
|
const allowedFilePath = join(notesDir, "allowed.txt")
|
|
const linkedSecretPath = join(notesDir, "linked-secret.txt")
|
|
const outsideFilePath = join(fixtureRoot, "secret.txt")
|
|
|
|
beforeAll(() => {
|
|
mkdirSync(notesDir, { recursive: true })
|
|
writeFileSync(allowedFilePath, "allowed-content", "utf8")
|
|
writeFileSync(outsideFilePath, "secret-content", "utf8")
|
|
symlinkSync(outsideFilePath, linkedSecretPath)
|
|
})
|
|
|
|
afterAll(() => {
|
|
rmSync(fixtureRoot, { recursive: true, force: true })
|
|
})
|
|
|
|
test("resolves file references within cwd", async () => {
|
|
//#given
|
|
const input = "Read @notes/allowed.txt before continuing"
|
|
|
|
//#when
|
|
const resolved = await resolveFileReferencesInText(input, workspaceDir)
|
|
|
|
//#then
|
|
expect(resolved).toContain("allowed-content")
|
|
})
|
|
|
|
test("rejects traversal references that escape cwd", async () => {
|
|
//#given
|
|
const input = "Read @../secret.txt before continuing"
|
|
|
|
//#when
|
|
const resolved = await resolveFileReferencesInText(input, workspaceDir)
|
|
|
|
//#then
|
|
expect(resolved).toContain("[path rejected:")
|
|
expect(resolved).not.toContain("secret-content")
|
|
})
|
|
|
|
test("rejects absolute references outside cwd", async () => {
|
|
//#given
|
|
const input = `Read @${outsideFilePath} before continuing`
|
|
|
|
//#when
|
|
const resolved = await resolveFileReferencesInText(input, workspaceDir)
|
|
|
|
//#then
|
|
expect(resolved).toContain("[path rejected:")
|
|
expect(resolved).not.toContain("secret-content")
|
|
})
|
|
|
|
test("rejects symlink references that escape cwd", async () => {
|
|
//#given
|
|
const input = "Read @notes/linked-secret.txt before continuing"
|
|
|
|
//#when
|
|
const resolved = await resolveFileReferencesInText(input, workspaceDir)
|
|
|
|
//#then
|
|
expect(resolved).toContain("[path rejected:")
|
|
expect(resolved).not.toContain("secret-content")
|
|
})
|
|
})
|