Files
0pt/tests/p0/source-query-service.test.ts
T
adminandClaude Sonnet 5 c89731115c feat: add 0pt.query_source, 0pt.document_ocr, 0pt.write_external_record capabilities
Three new, tenant-agnostic P0 capabilities:

- 0pt.query_source: bounded, timeout/row-capped ad-hoc SQL query execution
  against registered database sources, via a Fabric/OneLake adapter
  (Azure AD credential-based mssql connection), with AUD0 audit events
  and an app/api/0pt/sources/[sourceId]/query endpoint.

- 0pt.document_ocr (document capture + extraction): mailbox-based document
  intake via Microsoft Graph, OCR/field extraction via Azure Document
  Intelligence, with capture/extraction API endpoints and services that
  never fabricate extracted values.

- 0pt.write_external_record (external writeback): a generic writeback-adapter
  contract plus a concrete Odoo bills adapter that matches partners via
  Fabric, backing an external-writeback-service for pushing records to
  downstream systems of record.

Known interim gaps:
- src/adapters/local-blob-store.ts is an interim ST0RE substitute pending
  the real ST0RE artifact-store integration.
- mssql is used by the Fabric/OneLake adapter but deliberately not yet
  added to package.json (pending `pnpm add mssql` once the concurrent
  session's package.json/dependency changes settle).

A separate, concurrently-active session has unrelated in-flight changes
in this same working tree (data-platform catalog/lineage/feature-store,
dataset quality, lakehouse/Iceberg docs, deploy scaffolding, package.json/
pnpm-lock.yaml updates, etc.) — none of that is touched or included here.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-08 19:32:43 +02:00

315 lines
12 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { SourceService } from "@/src/services/source-service";
import type { SourceType } from "@/src/types/source";
const queryMock = vi.fn();
const closeMock = vi.fn().mockResolvedValue(undefined);
// Hoisted by Vitest above the static imports below, so
// source-query-service.ts (which statically imports FabricOneLakeAdapter)
// sees this mock too — no real mssql/AAD network calls happen in this file.
vi.mock("@/src/adapters/fabric-onelake-adapter", () => {
return {
// Must be a real `function` (not an arrow fn) so `new FabricOneLakeAdapter(...)`
// in source-query-service.ts works: a constructor that explicitly returns an
// object substitutes that object for `this`, per JS semantics.
FabricOneLakeAdapter: vi.fn().mockImplementation(function FakeFabricOneLakeAdapter() {
return { query: queryMock, close: closeMock };
}),
};
});
import {
SourceQueryService,
SourceQueryTimeoutError,
SourceQueryUnsupportedError,
SOURCE_QUERY_HARD_MAX_ROWS,
SOURCE_QUERY_HARD_MAX_TIMEOUT_MS,
} from "@/src/services/source-query-service";
const BASE_CONNECTION_CONFIG = {
credentials: { tenantId: "t", clientId: "c", clientSecret: "s" },
sqlEndpoint: "ep.datawarehouse.fabric.microsoft.com",
database: "lh_test",
};
async function registerSource(
sourceService: SourceService,
opts: { source_id: string; adapter_id?: string; source_type?: SourceType },
): Promise<string> {
await sourceService.createSource({
source_id: opts.source_id,
tenant_id: "t1",
project_id: "p1",
source_type: opts.source_type ?? "database",
name: "Fabric Warehouse",
adapter_id: opts.adapter_id ?? "fabric_onelake_sql",
});
return opts.source_id;
}
describe("SourceQueryService.querySource — bounded execution", () => {
beforeEach(() => {
queryMock.mockReset();
closeMock.mockClear();
});
it("executes a query against a registered fabric_onelake_sql database source and returns rows", async () => {
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_query_ok" });
queryMock.mockResolvedValue({ rows: [{ id: 1 }, { id: 2 }], rowCount: 2, durationMs: 5 });
const service = new SourceQueryService({ sourceService });
const result = await service.querySource({
source_id: sourceId,
query: "SELECT * FROM dbo.customers",
tenant_id: "t1",
project_id: "p1",
connection_config: BASE_CONNECTION_CONFIG,
});
expect(result.rows).toEqual([{ id: 1 }, { id: 2 }]);
expect(result.row_count).toBe(2);
expect(result.truncated).toBe(false);
expect(result.bounds.max_rows).toBeGreaterThan(0);
expect(closeMock).toHaveBeenCalledTimes(1);
});
it("rejects a non-database source with SourceQueryUnsupportedError", async () => {
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_file_1", source_type: "file", adapter_id: undefined });
const service = new SourceQueryService({ sourceService });
await expect(
service.querySource({ source_id: sourceId, query: "SELECT 1", tenant_id: "t1", project_id: "p1", connection_config: BASE_CONNECTION_CONFIG }),
).rejects.toBeInstanceOf(SourceQueryUnsupportedError);
});
it("rejects a database source with an unsupported adapter_id", async () => {
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_bad_adapter", adapter_id: "some_other_adapter" });
const service = new SourceQueryService({ sourceService });
await expect(
service.querySource({ source_id: sourceId, query: "SELECT 1", tenant_id: "t1", project_id: "p1", connection_config: BASE_CONNECTION_CONFIG }),
).rejects.toBeInstanceOf(SourceQueryUnsupportedError);
});
it("propagates a 'not found' error for an unregistered source, without ever building an adapter", async () => {
const sourceService = new SourceService();
const service = new SourceQueryService({ sourceService });
await expect(
service.querySource({ source_id: "does_not_exist", query: "SELECT 1", tenant_id: "t1", project_id: "p1", connection_config: BASE_CONNECTION_CONFIG }),
).rejects.toThrow(/not found/i);
expect(queryMock).not.toHaveBeenCalled();
});
it("truncates rows to max_rows and flags truncated:true", async () => {
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_truncate" });
const rows = Array.from({ length: 50 }, (_, i) => ({ id: i }));
queryMock.mockResolvedValue({ rows, rowCount: rows.length, durationMs: 3 });
const service = new SourceQueryService({ sourceService });
const result = await service.querySource({
source_id: sourceId,
query: "SELECT * FROM dbo.big_table",
tenant_id: "t1",
project_id: "p1",
max_rows: 10,
connection_config: BASE_CONNECTION_CONFIG,
});
expect(result.rows).toHaveLength(10);
expect(result.row_count).toBe(10);
expect(result.truncated).toBe(true);
expect(result.bounds.max_rows).toBe(10);
});
it("does not flag truncated when row count is exactly max_rows", async () => {
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_exact" });
const rows = Array.from({ length: 10 }, (_, i) => ({ id: i }));
queryMock.mockResolvedValue({ rows, rowCount: rows.length, durationMs: 1 });
const service = new SourceQueryService({ sourceService });
const result = await service.querySource({
source_id: sourceId,
query: "SELECT * FROM dbo.t",
tenant_id: "t1",
project_id: "p1",
max_rows: 10,
connection_config: BASE_CONNECTION_CONFIG,
});
expect(result.truncated).toBe(false);
expect(result.rows).toHaveLength(10);
});
it("clamps a caller-requested max_rows above the hard ceiling instead of allowing an unbounded read", async () => {
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_clamp_rows" });
queryMock.mockResolvedValue({ rows: [{ id: 1 }], rowCount: 1, durationMs: 1 });
const service = new SourceQueryService({ sourceService });
const result = await service.querySource({
source_id: sourceId,
query: "SELECT 1",
tenant_id: "t1",
project_id: "p1",
max_rows: SOURCE_QUERY_HARD_MAX_ROWS * 10,
connection_config: BASE_CONNECTION_CONFIG,
});
expect(result.bounds.max_rows).toBe(SOURCE_QUERY_HARD_MAX_ROWS);
});
it("clamps a caller-requested timeout_ms above the hard ceiling", async () => {
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_clamp_timeout" });
queryMock.mockResolvedValue({ rows: [], rowCount: 0, durationMs: 1 });
const service = new SourceQueryService({ sourceService });
const result = await service.querySource({
source_id: sourceId,
query: "SELECT 1",
tenant_id: "t1",
project_id: "p1",
timeout_ms: SOURCE_QUERY_HARD_MAX_TIMEOUT_MS * 100,
connection_config: BASE_CONNECTION_CONFIG,
});
expect(result.bounds.timeout_ms).toBe(SOURCE_QUERY_HARD_MAX_TIMEOUT_MS);
});
it("times out a query that runs longer than the bounded timeout, with SourceQueryTimeoutError", async () => {
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_timeout" });
queryMock.mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve({ rows: [], rowCount: 0, durationMs: 1 }), 300)),
);
const service = new SourceQueryService({ sourceService });
await expect(
service.querySource({
source_id: sourceId,
query: "SELECT * FROM dbo.slow_view",
tenant_id: "t1",
project_id: "p1",
timeout_ms: 20,
connection_config: BASE_CONNECTION_CONFIG,
}),
).rejects.toBeInstanceOf(SourceQueryTimeoutError);
});
it("still releases the adapter (calls close) after a timeout", async () => {
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_timeout_cleanup" });
queryMock.mockImplementation(
() => new Promise((resolve) => setTimeout(() => resolve({ rows: [], rowCount: 0, durationMs: 1 }), 300)),
);
const service = new SourceQueryService({ sourceService });
await expect(
service.querySource({
source_id: sourceId,
query: "SELECT 1",
tenant_id: "t1",
project_id: "p1",
timeout_ms: 20,
connection_config: BASE_CONNECTION_CONFIG,
}),
).rejects.toThrow();
expect(closeMock).toHaveBeenCalledTimes(1);
});
it("propagates adapter query errors as-is", async () => {
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_adapter_error" });
queryMock.mockRejectedValue(new Error("Invalid object name 'dbo.does_not_exist'"));
const service = new SourceQueryService({ sourceService });
await expect(
service.querySource({
source_id: sourceId,
query: "SELECT * FROM dbo.does_not_exist",
tenant_id: "t1",
project_id: "p1",
connection_config: BASE_CONNECTION_CONFIG,
}),
).rejects.toThrow(/Invalid object name/);
});
});
describe("SourceQueryService — AUD0 0pt.source_queried event emission", () => {
const originalFetch = global.fetch;
afterEach(() => {
global.fetch = originalFetch;
});
it("emits a 0pt.source_queried AUD0 event with ok:true on success", async () => {
queryMock.mockReset();
closeMock.mockClear();
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_aud0_ok" });
queryMock.mockResolvedValue({ rows: [{ id: 1 }], rowCount: 1, durationMs: 2 });
const calls: Array<{ url: string; body: Record<string, unknown> }> = [];
global.fetch = vi.fn(async (url: unknown, init: unknown) => {
calls.push({ url: String(url), body: JSON.parse(String((init as RequestInit).body)) });
return new Response("{}", { status: 200 });
}) as unknown as typeof fetch;
const service = new SourceQueryService({ sourceService, aud0BaseUrl: "http://aud0.test" });
await service.querySource({
source_id: sourceId,
query: "SELECT * FROM dbo.customers",
tenant_id: "t1",
project_id: "p1",
actor_id: "user_42",
correlation_id: "corr_abc",
connection_config: BASE_CONNECTION_CONFIG,
});
expect(calls).toHaveLength(1);
expect(calls[0].url).toBe("http://aud0.test/v1/aud0/events");
expect(calls[0].body.event_type).toBe("0pt.source_queried");
expect(calls[0].body.capability_id).toBe("0pt.query_source");
expect(calls[0].body.app_id).toBe("0pt");
expect(calls[0].body.resource_type).toBe("source");
expect(calls[0].body.resource_id).toBe(sourceId);
expect(calls[0].body.actor_id).toBe("user_42");
expect(calls[0].body.correlation_id).toBe("corr_abc");
const payload = calls[0].body.payload as Record<string, unknown>;
expect(payload.ok).toBe(true);
expect(payload.row_count).toBe(1);
expect(payload.adapter_id).toBe("fabric_onelake_sql");
});
it("emits a 0pt.source_queried AUD0 event with ok:false and the error message on failure", async () => {
queryMock.mockReset();
closeMock.mockClear();
const sourceService = new SourceService();
const sourceId = await registerSource(sourceService, { source_id: "src_aud0_fail" });
queryMock.mockRejectedValue(new Error("boom"));
const calls: Array<{ url: string; body: Record<string, unknown> }> = [];
global.fetch = vi.fn(async (url: unknown, init: unknown) => {
calls.push({ url: String(url), body: JSON.parse(String((init as RequestInit).body)) });
return new Response("{}", { status: 200 });
}) as unknown as typeof fetch;
const service = new SourceQueryService({ sourceService, aud0BaseUrl: "http://aud0.test" });
await expect(
service.querySource({ source_id: sourceId, query: "SELECT 1", tenant_id: "t1", project_id: "p1", connection_config: BASE_CONNECTION_CONFIG }),
).rejects.toThrow("boom");
expect(calls).toHaveLength(1);
const payload = calls[0].body.payload as Record<string, unknown>;
expect(payload.ok).toBe(false);
expect(payload.error).toBe("boom");
});
});