openpetswithchatandmcp/apps/desktop/tests/knowledge-store.test.ts
OpenPets Dev 6ab3bb64d8 feat(rebrand): rename OpenPets to FamiliarOS and pets to familiars
- Rename all user-facing and technical identifiers from OpenPets/Pet to FamiliarOS/Familiar.
- Rename packages from @open-pets/* to @familiaros/*; rename install-pet/pet-format packages.
- Rename plugin IDs and directories from openpets.* to familiaros.*.
- Rename IPC namespace from openpets:* to familiaros:* and state filenames from openpets-* to familiaros-* with legacy migration.
- Rename source files (pet-window, built-in-pet, default-pet-controller, etc.) to familiar equivalents.
- Update locales (en, es-419, ja, ko, pt-BR, zh-Hans, zh-Hant) and tray/pet context menu strings.
- Add Familiar naming feature: preference, settings input, tray menu display.
- Update assets and packaging config; all desktop tests pass.
2026-06-17 01:42:08 +00:00

117 lines
6.7 KiB
TypeScript

import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { KnowledgeStore } from "../src/knowledge-store-core.js";
function makeTempDir(): string {
return mkdtempSync(join(tmpdir(), "familiaros-knowledge-"));
}
function cleanTempDir(dir: string): void {
try {
rmSync(dir, { recursive: true, force: true });
} catch {
// ignore cleanup failures
}
}
const tempDir = makeTempDir();
const store = new KnowledgeStore(tempDir);
assert.deepEqual(store.listFiles(), [], "New store should have no files.");
assert.equal(store.buildRelevantContext("hello"), undefined, "Empty store should produce no context.");
const text = await store.storeFile({ name: "hello.txt", data: Buffer.from("Hello world, this is FamiliarOS knowledge store.") });
assert.ok(text.id.startsWith("kfile-"), "Stored file should have a kfile id.");
assert.equal(text.name, "hello.txt", "Stored file should keep its name.");
assert.equal(text.mimeType, "text/plain", "TXT files should be detected as text/plain.");
assert.equal(text.isText, true, "TXT files should be flagged as text.");
assert.equal(text.extractedText, "Hello world, this is FamiliarOS knowledge store.", "Text should be extracted.");
const binary = await store.storeFile({ name: "blob.bin", data: Buffer.from([0x00, 0x01, 0x02, 0x03]) });
assert.equal(binary.mimeType, "application/octet-stream", "Unknown extensions default to octet-stream.");
assert.equal(binary.isText, false, "Binary files should not be flagged as text.");
assert.equal(binary.extractedText, undefined, "Binary files should have no extracted text.");
await new Promise((resolve) => setTimeout(resolve, 10));
const markdown = await store.storeFile({ name: "notes.md", data: Buffer.from("# FamiliarOS\n\nKnowledge store notes.") });
assert.equal(markdown.mimeType, "text/markdown", "MD extension should infer markdown.");
// Listing and ordering
const listed = store.listFiles(10);
assert.equal(listed.length, 3, "All three files should be listed.");
assert.equal(listed[0].id, markdown.id, "Most recent file should appear first.");
// Searching
const hits = store.searchFiles("FamiliarOS");
assert.equal(hits.length, 2, "Search should find files mentioning FamiliarOS.");
assert.ok(hits.every((hit) => hit.score > 0), "All returned hits should have a positive score.");
const noHits = store.searchFiles("quantum physics");
assert.equal(noHits.length, 0, "Unrelated query should return no hits.");
const emptyQuery = store.searchFiles(" ");
assert.equal(emptyQuery.length, 3, "Empty query should fall back to listing.");
// Relevant context building
const context = store.buildRelevantContext("FamiliarOS knowledge");
assert.ok(context, "Relevant context should be built for matching query.");
assert.ok(context?.includes("hello.txt"), "Context should include the matching file name.");
assert.ok(context?.includes("Use the above files only when they help answer the current request."), "Context should include guidance footer.");
// File path retrieval
const filePath = store.getFilePath(text.id);
assert.ok(filePath, "Stored file should have a path.");
assert.equal(readFileSync(filePath, "utf8"), "Hello world, this is FamiliarOS knowledge store.", "Stored file content should match.");
assert.equal(store.getFilePath("kfile-missing"), null, "Missing id should return null.");
// Deletion
assert.equal(await store.deleteFile(binary.id), true, "Existing file should be deleted.");
assert.equal(await store.deleteFile(binary.id), false, "Deleting same id again should return false.");
assert.equal(store.listFiles().length, 2, "Deleted file should no longer be listed.");
assert.equal(store.getFilePath(binary.id), null, "Deleted file path should be null.");
// Invalid inputs
await assert.rejects(store.storeFile({ name: "", data: Buffer.from("x") }), /File name is required/, "Empty file name should be rejected.");
await assert.rejects(store.storeFile({ name: "x.txt", data: Buffer.alloc(0) }), /File cannot be empty/, "Empty file data should be rejected.");
await assert.rejects(store.storeFile({ name: "x.txt", data: new Uint8Array([120]) } as unknown as { name: string; data: Buffer }), /File data must be a Buffer/, "Non-Buffer data should be rejected.");
await assert.rejects(store.storeFile({ name: "huge.txt", data: Buffer.alloc(6 * 1024 * 1024) }), /too large/, "Oversized file should be rejected.");
await assert.rejects(store.deleteFile(" "), /id is required/, "Empty id delete should throw.");
// Persistence: a fresh store instance should load the same index
const reloaded = new KnowledgeStore(tempDir);
assert.equal(reloaded.listFiles().length, 2, "Reloaded store should preserve files.");
const reloadedText = reloaded.listFiles().find((file) => file.id === text.id);
assert.ok(reloadedText, "Reloaded store should include the text file.");
assert.equal(reloadedText?.extractedText, text.extractedText, "Extracted text should survive reload.");
// File count pruning (limit is 100; we'll just verify old entries survive below limit)
for (let index = 0; index < 5; index += 1) {
await store.storeFile({ name: `note-${index}.txt`, data: Buffer.from(`Note ${index}`) });
}
assert.equal(store.listFiles(200).length, 7, "Stored files below the limit should all be present.");
// Sanitization of dangerous names
const dangerous = await store.storeFile({ name: "../../etc/passwd", data: Buffer.from("safe content") });
assert.equal(dangerous.name, "passwd", "Path traversal characters should be stripped.");
assert.ok(!dangerous.name.includes("/"), "Sanitized name should not contain separators.");
// Relevant context should handle non-text files gracefully
const picture = await store.storeFile({ name: "screenshot.png", data: Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]) });
assert.equal(picture.mimeType, "image/png", "PNG extension should infer image/png.");
const nonTextContext = store.buildRelevantContext("screenshot");
assert.ok(nonTextContext?.includes("screenshot.png"), "Context should include the matching non-text file name.");
assert.ok(nonTextContext?.includes("[Non-text file: content not shown.]"), "Non-text files should show a placeholder in context.");
// Phrase search in file content should rank highly
const phraseFile = await store.storeFile({ name: "deep-thoughts.txt", data: Buffer.from("The answer to life, the universe, and everything is forty-two.") });
const phraseHits = store.searchFiles("forty-two");
assert.ok(phraseHits.some((hit) => hit.file.id === phraseFile.id), "Phrase search should find content containing the exact phrase.");
console.log("Knowledge store validation passed.");
// Cleanup happens at the end so failures still leave the directory for inspection.
cleanTempDir(tempDir);