- 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.
117 lines
6.7 KiB
TypeScript
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);
|