singularity-forge/packages/native/src/__tests__/diff.test.mjs
ace-pm f92ee8d64c
Rename @sf-run/* → @singularity-forge/* package scope
- All 373 source files updated
- Package.json scopes in all workspace packages
- Loader workspace symlink dir updated
- RpcClient import unified from pi-coding-agent (fixes type mismatch)
- Scripts, configs, flake.nix updated
- Workspace symlinks rebuilt
2026-04-15 22:56:33 +02:00

189 lines
6.4 KiB
JavaScript

import { test, describe } from "node:test";
import assert from "node:assert/strict";
import { createRequire } from "node:module";
import * as path from "node:path";
import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
// Load the native addon directly
const addonDir = path.resolve(
__dirname,
"..",
"..",
"..",
"..",
"native",
"addon",
);
const platformTag = `${process.platform}-${process.arch}`;
const candidates = [
path.join(addonDir, `forge_engine.${platformTag}.node`),
path.join(addonDir, "forge_engine.dev.node"),
];
let native;
for (const candidate of candidates) {
try {
native = require(candidate);
break;
} catch {
// try next
}
}
if (!native) {
console.error(
"Native addon not found. Run `npm run build:native -w @singularity-forge/native` first.",
);
process.exit(1);
}
// ── normalizeForFuzzyMatch ──────────────────────────────────────────────
describe("normalizeForFuzzyMatch", () => {
test("strips trailing whitespace per line", () => {
assert.equal(native.normalizeForFuzzyMatch("hello \nworld "), "hello\nworld");
});
test("normalizes smart quotes to ASCII", () => {
assert.equal(
native.normalizeForFuzzyMatch("\u201Chello\u201D \u2018world\u2019"),
'"hello" \'world\'',
);
});
test("normalizes dashes to ASCII hyphen", () => {
assert.equal(native.normalizeForFuzzyMatch("a\u2013b\u2014c"), "a-b-c");
});
test("normalizes special spaces to regular space", () => {
assert.equal(native.normalizeForFuzzyMatch("a\u00A0b\u3000c"), "a b c");
});
test("handles empty string", () => {
assert.equal(native.normalizeForFuzzyMatch(""), "");
});
test("preserves leading whitespace", () => {
assert.equal(native.normalizeForFuzzyMatch(" hello "), " hello");
});
});
// ── fuzzyFindText ───────────────────────────────────────────────────────
describe("fuzzyFindText", () => {
test("finds exact match", () => {
const result = native.fuzzyFindText("hello world", "world");
assert.equal(result.found, true);
assert.equal(result.index, 6);
assert.equal(result.matchLength, 5);
assert.equal(result.usedFuzzyMatch, false);
assert.equal(result.contentForReplacement, "hello world");
});
test("finds fuzzy match with smart quotes", () => {
const content = 'let x = \u201Chello\u201D;';
const oldText = 'let x = "hello";';
const result = native.fuzzyFindText(content, oldText);
assert.equal(result.found, true);
assert.equal(result.usedFuzzyMatch, true);
});
test("returns not found for missing text", () => {
const result = native.fuzzyFindText("hello world", "xyz");
assert.equal(result.found, false);
assert.equal(result.index, -1);
assert.equal(result.matchLength, 0);
});
test("returns correct UTF-16 index for non-ASCII content", () => {
// Emoji U+1F600 is 2 UTF-16 code units (surrogate pair), 4 UTF-8 bytes
const content = "\u{1F600}hello";
const result = native.fuzzyFindText(content, "hello");
assert.equal(result.found, true);
// Emoji is 2 UTF-16 code units, so "hello" starts at index 2
assert.equal(result.index, 2);
assert.equal(result.matchLength, 5);
});
test("index is compatible with JS substring()", () => {
const content = "abc\u{1F600}def";
const result = native.fuzzyFindText(content, "def");
assert.equal(result.found, true);
// "abc" = 3, emoji = 2 UTF-16 code units → index 5
assert.equal(result.index, 5);
// Verify substring works correctly with the returned index
const extracted = result.contentForReplacement.substring(
result.index,
result.index + result.matchLength,
);
assert.equal(extracted, "def");
});
test("fuzzy match with trailing whitespace differences", () => {
const content = "hello \nworld ";
const oldText = "hello\nworld";
const result = native.fuzzyFindText(content, oldText);
assert.equal(result.found, true);
assert.equal(result.usedFuzzyMatch, true);
});
});
// ── generateDiff ────────────────────────────────────────────────────────
describe("generateDiff", () => {
test("generates diff for a line change", () => {
const old = "line1\nline2\nline3";
const newText = "line1\nmodified\nline3";
const result = native.generateDiff(old, newText);
assert.ok(result.diff.includes("line2"));
assert.ok(result.diff.includes("modified"));
assert.ok(result.diff.includes("-"));
assert.ok(result.diff.includes("+"));
assert.notEqual(result.firstChangedLine, null);
});
test("generates diff for an addition", () => {
const old = "line1\nline3";
const newText = "line1\nline2\nline3";
const result = native.generateDiff(old, newText);
assert.ok(result.diff.includes("+"));
assert.ok(result.diff.includes("line2"));
});
test("generates diff for a deletion", () => {
const old = "line1\nline2\nline3";
const newText = "line1\nline3";
const result = native.generateDiff(old, newText);
assert.ok(result.diff.includes("-"));
assert.ok(result.diff.includes("line2"));
});
test("returns empty diff for identical content", () => {
const result = native.generateDiff("same", "same");
assert.equal(result.diff, "");
// napi-rs maps Option::None to undefined (not null)
assert.equal(result.firstChangedLine, undefined);
});
test("respects context lines parameter", () => {
const lines = Array.from({ length: 20 }, (_, i) => `line${i + 1}`);
const old = lines.join("\n");
lines[10] = "modified";
const newText = lines.join("\n");
const result = native.generateDiff(old, newText, 2);
assert.ok(result.diff.includes("..."));
});
test("default context is 4 lines", () => {
const lines = Array.from({ length: 20 }, (_, i) => `line${i + 1}`);
const old = lines.join("\n");
lines[10] = "modified";
const newText = lines.join("\n");
const result = native.generateDiff(old, newText);
// Should show 4 context lines before and after
assert.ok(result.diff.length > 0);
});
});