chore(testing): Phase 9 — bun:test suite + CI gating with no-DB fakes
CI / typecheck + build (turbo) (push) Canceled after 0s

Added a real test suite (32 tests, 0 external services) using bun:test with
in-process module mocking for @mcpedia/db, @mcpedia/queue, @mcpedia/core.

Enablers:
- apps/api: extracted createApp(deps?) factory + dashboard.ts module from
  index.ts so the HTTP surface is unit-testable (real queue is lazy-imported).
- packages/core: exported shouldCreateRevision pure predicate; restoreRevision
  gained an opts.reindex seam for the chunk-rebuild contract.
- apps/mcp: renamed smoke.test.ts -> smoke.ts (bun test now owns .test.ts),
  updated stale assertions (10 tools, 4 docs in docs section).
- infra: turbo test task (cache:false), test scripts across packages,
  @types/bun + tsconfig base types, CI 'Test' step after Build.

Packages with tests: embeddings(5), parser(5), search(8), core(4),
mcp(6 auth-gates), api(8 contracts).

All green: typecheck(4/4), test(6/6 pkgs), build(web). Live API verified
/health, /metrics, /dashboard, /hooks/* auth gate on temp port.
This commit is contained in:
asepharyana
2026-08-20 11:12:32 +07:00
parent 0cdf261d40
commit 76f778c10d
25 changed files with 1055 additions and 193 deletions
+44
View File
@@ -0,0 +1,44 @@
import { test, expect } from "bun:test";
import { shouldCreateRevision } from "../src/index.service";
/**
* Phase 9: unit tests for the revision-dedup decision rule.
*
* `shouldCreateRevision` is the pure predicate that `indexContentFile` consults
* before writing a new row to `document_revisions`. It's extracted because the
* dedup correctness is the single most important guarantee of the revision
* system ("metadata-only edits don't bloat history"), and it must hold without
* a database.
*
* The DB-backed paths (`snapshotRevision`, `restoreRevision`) are exercised
* end-to-end by the existing manual e2e (`bun run index` + restore via the web
* /api/revisions/restore route, see PHASES.md Phase 4 verification). Here we
* lock the decision invariant in CI.
*/
test("shouldCreateRevision: first snapshot when no prior revision exists", () => {
// No prior revision row → always snapshot the first version.
expect(shouldCreateRevision(null, "body text")).toBe(true);
expect(shouldCreateRevision(undefined, "body text")).toBe(true);
});
test("shouldCreateRevision: identical body creates no new revision (dedup)", () => {
// The exact dedup rule that prevents metadata-only edits from bloating
// history: if the body is byte-identical to the latest revision's body,
// skip the snapshot.
expect(shouldCreateRevision("same body", "same body")).toBe(false);
// The dedup rule applies regardless of body length — large identical bodies
// also skip the snapshot.
expect(shouldCreateRevision("a".repeat(5000), "a".repeat(5000))).toBe(false);
});
test("shouldCreateRevision: changed body creates a new revision", () => {
expect(shouldCreateRevision("old body", "new body")).toBe(true);
// Whitespace / trailing newline changes count as a real body change.
expect(shouldCreateRevision("body", "body\n")).toBe(true);
});
test("shouldCreateRevision: empty-string vs non-empty counts as a change", () => {
expect(shouldCreateRevision("", "content")).toBe(true);
expect(shouldCreateRevision("content", "")).toBe(true);
});
+16
View File
@@ -84,6 +84,22 @@ export async function indexContentFile(
return { indexed: true, chunks, revision };
}
/**
* Pure decision rule for the revision system: create a new revision only when
* the body genuinely changed vs the latest snapshot.
* - no prior revision (latestBody null) -> true (first snapshot)
* - identical body -> false (no noise)
* - different body -> true
*
* Exported separately so it can be unit-tested without a database.
*/
export function shouldCreateRevision(
latestBody: string | null | undefined,
body: string,
): boolean {
return latestBody == null || latestBody !== body;
}
/**
* Compare the incoming body against the latest revision's body; if different
* (or no prior revision exists), create a new revision with an incremented
+12 -2
View File
@@ -76,9 +76,18 @@ export async function getRevision(
};
}
/** Restore a revision: write its body+metadata back into the live `documents` row. */
/**
* Restore a revision: write its body+metadata back into the live `documents` row.
*
* @param id revision UUID
* @param opts optional seam for testing — override the chunk-rebuild step so
* tests can assert it's invoked without touching embeddings.
*/
export async function restoreRevision(
id: string,
opts?: {
reindex?: (slug: string) => Promise<number>;
},
): Promise<{ slug: string; documentId: string } | null> {
const [rev] = await db
.select({
@@ -118,7 +127,8 @@ export async function restoreRevision(
// Rebuild semantic chunks + embeddings from the restored body so semantic
// and hybrid search stay consistent (otherwise document_chunks would hold
// the NEW body's chunks while documents.body holds the OLD/restore body).
await reindexChunks(rev.slug);
const reindex = opts?.reindex ?? reindexChunks;
await reindex(rev.slug);
return { slug: rev.slug, documentId: rev.documentId };
}