mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-16 18:02:30 +00:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a42352081c | ||
|
|
070f4c5678 | ||
|
|
4b2df89935 |
@@ -1743,7 +1743,15 @@ async function extractStaleFromDB(
|
||||
// `page.updated_at.toISOString()` — the JS Date is ms-truncated, so the
|
||||
// µs-precision DB updated_at stayed strictly greater and the page never
|
||||
// cleared on Postgres. Stamping the exact value makes them equal.
|
||||
processedRefs.push({ slug: page.slug, source_id: page.source_id, extractedAt: page.updated_at_iso });
|
||||
//
|
||||
// Version-arm floor: a page last edited BEFORE LINK_EXTRACTOR_VERSION_TS
|
||||
// would otherwise be stamped below the version watermark and stay
|
||||
// permanently stale (`links_extracted_at < versionTs` re-fires every run).
|
||||
// Stamp max(updated_at, versionTs) — versionTs is always a past release
|
||||
// date, so a concurrent edit's now() still exceeds the stamp and D4 holds.
|
||||
// Tie at ms precision picks updated_at_iso (its µs ≥ versionTs's .000000).
|
||||
const stampTs = new Date(page.updated_at_iso) >= new Date(versionTs) ? page.updated_at_iso : versionTs;
|
||||
processedRefs.push({ slug: page.slug, source_id: page.source_id, extractedAt: stampTs });
|
||||
}
|
||||
|
||||
// Flush NON-swallowing (CDX-4): a throw here propagates out of the sweep so
|
||||
|
||||
@@ -908,10 +908,6 @@ export const KNOWN_CONFIG_KEYS: readonly string[] = [
|
||||
'facts.extraction_model',
|
||||
// #2113: output-token cap for the per-turn facts extractor (default 4000).
|
||||
'facts.extraction_max_tokens',
|
||||
// Owner opt-in: let the local stdio MCP pipe read this owner's private facts
|
||||
// (find_trajectory / recall). Default off; HTTP transport ignores it. See
|
||||
// src/core/facts/reader-trust.ts.
|
||||
'facts.trust_local_reads',
|
||||
// Dream cycle config
|
||||
'dream.synthesize.session_corpus_dir',
|
||||
'dream.synthesize.meeting_transcripts_dir',
|
||||
|
||||
@@ -591,8 +591,6 @@ export interface TrajectoryOpts {
|
||||
sourceIds?: string[];
|
||||
/** When true, filters to visibility='world' only. Set by MCP layer from ctx.remote. */
|
||||
remote?: boolean;
|
||||
/** Owner opt-in: read private facts despite `remote`. See facts/reader-trust.ts. */
|
||||
trustedFactReads?: boolean;
|
||||
/** Metric filter. When set, only facts with this canonical metric label participate. */
|
||||
metric?: string;
|
||||
/**
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import type { OperationContext } from './../operations.ts';
|
||||
import type { FactRow } from './../engine.ts';
|
||||
import { effectiveConfidence } from './decay.ts';
|
||||
import { readableFactVisibilities } from './reader-trust.ts';
|
||||
|
||||
const DEFAULT_TTL_MS = 30_000;
|
||||
const DEFAULT_TOP_K = 10;
|
||||
@@ -51,13 +50,7 @@ export async function getBrainHotMemoryMeta(
|
||||
const sessionId = (ctx as { source_session?: string }).source_session
|
||||
?? null;
|
||||
const allowListHash = hashAllowList(ctx.takesHoldersAllowList);
|
||||
// Visibility tier: untrusted remote → world-only; trusted local +
|
||||
// owner-trusted reads → all rows. Folded into the cache key (the header's
|
||||
// "cache entries don't bleed across tiers" invariant): trustedFactReads is
|
||||
// re-read from config per call, so a mid-session opt-out must not keep
|
||||
// serving a private-inclusive cached payload for the TTL window.
|
||||
const visibility = readableFactVisibilities(ctx);
|
||||
const cacheKey = `${sourceId}::${sessionId ?? '_'}::${allowListHash}::${visibility ? 'world' : 'all'}`;
|
||||
const cacheKey = `${sourceId}::${sessionId ?? '_'}::${allowListHash}`;
|
||||
|
||||
const ttl = Math.max(1000, opts.ttlMs ?? DEFAULT_TTL_MS);
|
||||
const topK = Math.max(1, Math.min(opts.topK ?? DEFAULT_TOP_K, 25));
|
||||
@@ -68,6 +61,10 @@ export async function getBrainHotMemoryMeta(
|
||||
return cached.payload;
|
||||
}
|
||||
|
||||
// Build a fresh payload. Visibility tier: remote → world-only;
|
||||
// local → all rows.
|
||||
const visibility = ctx.remote === false ? undefined : ['world'] as ('world' | 'private')[];
|
||||
|
||||
let rows: FactRow[] = [];
|
||||
if (sessionId) {
|
||||
rows = await ctx.engine.listFactsBySession(sourceId, sessionId, {
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* Fact-read visibility trust.
|
||||
*
|
||||
* Fact rows are tagged `private` | `world`. Remote/untrusted callers
|
||||
* (`remote === true`) see only `world` rows — the posture that keeps a
|
||||
* published or HTTP-served brain from leaking private claims to strangers.
|
||||
*
|
||||
* But the stdio MCP server is an unauthenticated LOCAL pipe: on a single-owner
|
||||
* machine the caller IS the owner, yet it still defaults `remote: true` for
|
||||
* safety, so the owner's own agent is denied the owner's own private facts
|
||||
* (e.g. `find_trajectory` returns empty over MCP even though the facts exist).
|
||||
*
|
||||
* `trustedFactReads` is a narrow, READ-ONLY trust elevation, deliberately
|
||||
* DECOUPLED from `remote` so every other remote protection — file_upload
|
||||
* confinement, source isolation, fence stripping, takes-holder scoping — stays
|
||||
* fully in force. The stdio MCP server sets it ONLY when the brain owner opts
|
||||
* in via the `facts.trust_local_reads` config (default off). The HTTP/published
|
||||
* transport never sets it, so a served brain stays world-only regardless.
|
||||
*/
|
||||
export interface FactReaderTrust {
|
||||
/**
|
||||
* Mirrors OperationContext.remote. FAIL-CLOSED: anything not strictly
|
||||
* `false` is treated as remote/untrusted (CLAUDE.md trust invariant).
|
||||
*/
|
||||
remote?: boolean;
|
||||
/** Owner opt-in: this remote caller may read private facts. */
|
||||
trustedFactReads?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the reader is restricted to `visibility = 'world'` rows.
|
||||
* Fail-closed: an unset/undefined `remote` is untrusted — only an explicit
|
||||
* `remote: false` (trusted local CLI) or an explicit owner opt-in
|
||||
* (`trustedFactReads: true`) reads private rows.
|
||||
*/
|
||||
export function factsWorldOnly(t: FactReaderTrust): boolean {
|
||||
return t.remote !== false && t.trustedFactReads !== true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Visibility filter for list-style fact reads: `['world']` when the reader is
|
||||
* world-only, `undefined` (no filter — all rows) when it is trusted.
|
||||
*/
|
||||
export function readableFactVisibilities(
|
||||
t: FactReaderTrust,
|
||||
): ('private' | 'world')[] | undefined {
|
||||
return factsWorldOnly(t) ? ['world'] : undefined;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ import { ensureWellFormed } from './text-safe.ts';
|
||||
* OR updated_at > links_extracted_at`. It is an ISO-8601 string (NOT a number) —
|
||||
* the column is TIMESTAMPTZ and the predicate binds it as `::timestamptz`.
|
||||
*/
|
||||
export const LINK_EXTRACTOR_VERSION_TS = '2026-05-31T00:00:00Z';
|
||||
export const LINK_EXTRACTOR_VERSION_TS = '2026-07-21T00:00:00Z';
|
||||
|
||||
// ─── Entity references ──────────────────────────────────────────
|
||||
|
||||
@@ -80,10 +80,10 @@ export type LinkResolutionType = 'qualified' | 'unqualified';
|
||||
* Directory prefix whitelist. These are the top-level slug dirs the extractor
|
||||
* recognizes as entity references. Upstream canonical + our extensions:
|
||||
* - Gbrain canonical: people, companies, meetings, concepts, deal, civic, project, source, media, yc, projects
|
||||
* - Our domain extensions: tech, finance, personal, openclaw (domain-organized wikis)
|
||||
* - Our domain extensions: tech, finance, personal, openclaw, ops (domain-organized wikis)
|
||||
* - Our entity prefix: entities (we kept some legacy entities/projects/ pages)
|
||||
*/
|
||||
const DIR_PATTERN = '(?:people|companies|meetings|concepts|deal|civic|project|projects|source|media|yc|tech|finance|personal|openclaw|entities)';
|
||||
const DIR_PATTERN = '(?:people|companies|meetings|concepts|deal|civic|project|projects|source|media|yc|tech|finance|personal|openclaw|entities|ops)';
|
||||
|
||||
/**
|
||||
* Match `[Name](path)` markdown links pointing to entity directories.
|
||||
@@ -865,7 +865,16 @@ export function queryBasenameIndex(idx: Map<string, string[]>, name: string): st
|
||||
if (!name || typeof name !== 'string') return [];
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return [];
|
||||
const hit = idx.get(trimmed) ?? idx.get(trimmed.toLowerCase()) ?? idx.get(normalizeBasename(trimmed));
|
||||
let hit = idx.get(trimmed) ?? idx.get(trimmed.toLowerCase()) ?? idx.get(normalizeBasename(trimmed));
|
||||
// Issue #2576 bug 2: path-style refs (`runbooks/2026-05-01-x`) from dirs
|
||||
// outside DIR_PATTERN reach here, but normalizeBasename strips slashes
|
||||
// into a garbage key (`runbooks2026-05-01-x`) that can never hit the
|
||||
// tail-keyed index. Fall back to the path tail so qualified refs resolve
|
||||
// by basename like everything else.
|
||||
if (!hit && trimmed.includes('/')) {
|
||||
const tail = trimmed.slice(trimmed.lastIndexOf('/') + 1).trim();
|
||||
if (tail) hit = idx.get(tail) ?? idx.get(tail.toLowerCase()) ?? idx.get(normalizeBasename(tail));
|
||||
}
|
||||
return hit ? [...hit].sort(basenameSort) : [];
|
||||
}
|
||||
|
||||
|
||||
+7
-14
@@ -18,7 +18,6 @@ import { captureEvalCandidate, isEvalCaptureEnabled, isEvalScrubEnabled } from '
|
||||
import type { HybridSearchMeta } from './types.ts';
|
||||
import { extractPageLinks, isAutoLinkEnabled, isAutoTimelineEnabled, isGlobalBasenameEnabled, parseTimelineEntries, makeResolver, type UnresolvedFrontmatterRef } from './link-extraction.ts';
|
||||
import { isFactsBackstopEligible } from './facts/eligibility.ts';
|
||||
import { readableFactVisibilities } from './facts/reader-trust.ts';
|
||||
import { stripTakesFence } from './takes-fence.ts';
|
||||
import { stripFactsFence } from './facts-fence.ts';
|
||||
import { getContentFlag } from './quarantine.ts';
|
||||
@@ -333,15 +332,6 @@ export interface OperationContext {
|
||||
* remote/untrusted (defense in depth in case the type is bypassed via cast).
|
||||
*/
|
||||
remote: boolean;
|
||||
/**
|
||||
* Owner opt-in (`facts.trust_local_reads`): allow this remote caller to read
|
||||
* `private` facts. A NARROW, read-only trust elevation decoupled from
|
||||
* `remote` — every other remote protection (file confinement, source
|
||||
* isolation, fence stripping, takes scoping) stays in force. Set ONLY by the
|
||||
* stdio MCP server when the config is on; the HTTP transport never sets it.
|
||||
* Consulted via `src/core/facts/reader-trust.ts`.
|
||||
*/
|
||||
trustedFactReads?: boolean;
|
||||
/**
|
||||
* Subagent runtime context (v0.16+). Set by the subagent tool dispatcher when
|
||||
* dispatching an op as a tool call from an LLM loop. Used to enforce per-op
|
||||
@@ -3639,7 +3629,6 @@ const find_trajectory: Operation = {
|
||||
entitySlug: p.entity_slug,
|
||||
...scope,
|
||||
remote: ctx.remote === true,
|
||||
trustedFactReads: ctx.trustedFactReads === true,
|
||||
metric,
|
||||
kind,
|
||||
since,
|
||||
@@ -4000,9 +3989,13 @@ const recall: Operation = {
|
||||
const includeExpired = p.include_expired === true;
|
||||
const grep = typeof p.grep === 'string' ? p.grep.toLowerCase() : null;
|
||||
|
||||
// Visibility filter: world-only for untrusted remote callers; all rows for
|
||||
// trusted local CLI and owner-trusted reads (facts.trust_local_reads).
|
||||
const visibility = readableFactVisibilities(ctx);
|
||||
// Visibility filter: remote callers see world-only unless their token
|
||||
// grants elevated visibility (future-proofing; v0.31 ships world-only
|
||||
// for remote, all for local CLI).
|
||||
const visibility =
|
||||
ctx.remote === false
|
||||
? undefined
|
||||
: ['world'] as ('private' | 'world')[];
|
||||
|
||||
let rows: Awaited<ReturnType<typeof ctx.engine.listFactsByEntity>> = [];
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
} from './engine.ts';
|
||||
import { MAX_SEARCH_LIMIT, clampSearchLimit } from './engine.ts';
|
||||
import { withRetry, BULK_RETRY_OPTS, resolveBulkRetryOpts, computeNextDelay, type BatchAuditSite } from './retry.ts';
|
||||
import { factsWorldOnly } from './facts/reader-trust.ts';
|
||||
import { logBatchRetry as auditLogBatchRetry, logBatchExhausted as auditLogBatchExhausted } from './audit/batch-retry-audit.ts';
|
||||
import { runMigrations } from './migrate.ts';
|
||||
import { PGLITE_SCHEMA_SQL, getPGLiteSchema } from './pglite-schema.ts';
|
||||
@@ -4319,10 +4318,7 @@ export class PGLiteEngine implements BrainEngine {
|
||||
const useArray = Array.isArray(opts.sourceIds) && opts.sourceIds.length > 0;
|
||||
const sourceIds = useArray ? opts.sourceIds! : null;
|
||||
const sourceId = opts.sourceId ?? 'default';
|
||||
// Direct-engine contract: unset `remote` here means a trusted in-process
|
||||
// caller (CLI/tests) — the fail-closed default lives in the op layer, which
|
||||
// always passes explicit booleans. Normalize before the fail-closed helper.
|
||||
const remoteFilter = factsWorldOnly({ remote: opts.remote === true, trustedFactReads: opts.trustedFactReads === true });
|
||||
const remoteFilter = opts.remote === true;
|
||||
|
||||
// Build SQL dynamically. PGLite uses $N positional params; we
|
||||
// assemble the WHERE clauses + params array in tandem to keep them
|
||||
|
||||
@@ -14,7 +14,6 @@ import type {
|
||||
SourceRow,
|
||||
} from './engine.ts';
|
||||
import { withRetry, BULK_RETRY_OPTS, resolveBulkRetryOpts, computeNextDelay, type BatchAuditSite } from './retry.ts';
|
||||
import { factsWorldOnly } from './facts/reader-trust.ts';
|
||||
import { logBatchRetry as auditLogBatchRetry, logBatchExhausted as auditLogBatchExhausted } from './audit/batch-retry-audit.ts';
|
||||
import type {
|
||||
DomainBankSampleOpts, CorpusSampleOpts, DomainBankRow,
|
||||
@@ -4533,10 +4532,7 @@ export class PostgresEngine implements BrainEngine {
|
||||
const useArray = Array.isArray(opts.sourceIds) && opts.sourceIds.length > 0;
|
||||
const sourceIds = useArray ? opts.sourceIds! : null;
|
||||
const sourceId = opts.sourceId ?? 'default';
|
||||
// Direct-engine contract: unset `remote` here means a trusted in-process
|
||||
// caller (CLI/tests) — the fail-closed default lives in the op layer, which
|
||||
// always passes explicit booleans. Normalize before the fail-closed helper.
|
||||
const remoteFilter = factsWorldOnly({ remote: opts.remote === true, trustedFactReads: opts.trustedFactReads === true });
|
||||
const remoteFilter = opts.remote === true;
|
||||
|
||||
// Source-scope predicate: array path (federated) wins over scalar.
|
||||
// Engine.ts contract: returns chronological points; regressions +
|
||||
|
||||
@@ -30,13 +30,6 @@ export interface ToolResult {
|
||||
export interface DispatchOpts {
|
||||
/** Defaults to true (remote/untrusted). Local CLI callers (`gbrain call`) pass false. */
|
||||
remote?: boolean;
|
||||
/**
|
||||
* Owner opt-in (`facts.trust_local_reads`): let this remote caller read
|
||||
* `private` facts. Set ONLY by the stdio MCP server; the HTTP transport
|
||||
* leaves it unset so a served brain stays world-only. See
|
||||
* `src/core/facts/reader-trust.ts`.
|
||||
*/
|
||||
trustedFactReads?: boolean;
|
||||
/** Override the default stderr logger (e.g. CLI uses console.* directly). */
|
||||
logger?: OperationContext['logger'];
|
||||
/**
|
||||
@@ -210,7 +203,6 @@ export function buildOperationContext(
|
||||
logger: opts.logger || stderrLogger,
|
||||
dryRun: !!params.dry_run,
|
||||
remote: opts.remote ?? true,
|
||||
trustedFactReads: opts.trustedFactReads === true,
|
||||
takesHoldersAllowList: opts.takesHoldersAllowList,
|
||||
// v0.34 D4: sourceId is REQUIRED at the type level. Auto-fill 'default'
|
||||
// for single-source brains and any caller who didn't resolve a sourceId.
|
||||
|
||||
@@ -35,16 +35,6 @@ export async function startMcpServer(engine: BrainEngine) {
|
||||
// shape and cast through `any` (the SDK accepts it via the ServerResult union).
|
||||
server.setRequestHandler(CallToolRequestSchema, async (request: any): Promise<any> => {
|
||||
const { name, arguments: params } = request.params;
|
||||
// Owner opt-in: the stdio pipe is local + unauthenticated, so on a
|
||||
// single-owner machine its caller is the owner. When facts.trust_local_reads
|
||||
// is on, let fact reads (find_trajectory / recall) see this owner's own
|
||||
// private facts. Narrow + read-only — every other remote protection stays
|
||||
// on (remote stays true). HTTP transport never sets this. Best-effort: a
|
||||
// config read blip falls back to the safe world-only default.
|
||||
let trustedFactReads = false;
|
||||
try {
|
||||
trustedFactReads = (await engine.getConfig('facts.trust_local_reads')) === 'true';
|
||||
} catch { /* keep world-only default */ }
|
||||
// v0.28: stdio MCP has no per-token auth (local pipe). Default the
|
||||
// takes-holder allow-list to ['world'] so agent-facing callers don't
|
||||
// see private hunches via takes_list / takes_search / query. Operators
|
||||
@@ -52,7 +42,6 @@ export async function startMcpServer(engine: BrainEngine) {
|
||||
// `gbrain call <op>` (sets remote=false in src/cli.ts).
|
||||
return dispatchToolCall(engine, name, params, {
|
||||
remote: true,
|
||||
trustedFactReads,
|
||||
takesHoldersAllowList: ['world'],
|
||||
// v0.31: source defaults to 'default' for stdio (no per-token scope).
|
||||
// Operators who want a different source on stdio MCP should set
|
||||
|
||||
@@ -165,22 +165,6 @@ describe('findTrajectory — visibility filter (D-CDX-1 / R6)', () => {
|
||||
const all = await engine.findTrajectory({ entitySlug: 'traj-vis-default' });
|
||||
expect(all.length).toBe(2);
|
||||
});
|
||||
|
||||
test('remote=true + trustedFactReads bypasses world-only (owner-trusted reads)', async () => {
|
||||
await insertTyped({ entity_slug: 'traj-vis-trusted', metric: 'mrr', value: 50000, visibility: 'private', valid_from: new Date('2026-01-15') });
|
||||
await insertTyped({ entity_slug: 'traj-vis-trusted', metric: 'mrr', value: 99999, visibility: 'world', valid_from: new Date('2026-04-12') });
|
||||
|
||||
// Untrusted remote: world only.
|
||||
const untrusted = await engine.findTrajectory({ entitySlug: 'traj-vis-trusted', remote: true });
|
||||
expect(untrusted.length).toBe(1);
|
||||
expect(untrusted[0].value).toBe(99999);
|
||||
|
||||
// Owner-trusted remote: sees the private point too. remote stays true — only
|
||||
// fact-read visibility is elevated.
|
||||
const trusted = await engine.findTrajectory({ entitySlug: 'traj-vis-trusted', remote: true, trustedFactReads: true });
|
||||
expect(trusted.length).toBe(2);
|
||||
expect(trusted.map(p => p.value).sort((a, b) => (a! - b!))).toEqual([50000, 99999]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('findTrajectory — metric + since + until filters', () => {
|
||||
|
||||
@@ -209,6 +209,24 @@ describe('gbrain extract --stale', () => {
|
||||
expect(usRows[0]?.eq).toBe(true);
|
||||
});
|
||||
|
||||
test('version-arm floor: page edited BEFORE LINK_EXTRACTOR_VERSION_TS clears after --stale (issue #2576 bug 3)', async () => {
|
||||
// A page whose updated_at predates the version watermark used to be
|
||||
// stamped at its updated_at (< versionTs), so the version arm re-fired
|
||||
// every run — permanently stale. The sweep now floors the stamp at
|
||||
// versionTs. (The #1768 test above also covers this since the v0.42.x
|
||||
// VERSION_TS bump moved its date below the watermark, but this pins the
|
||||
// behavior explicitly so a date "repair" there can't drop coverage.)
|
||||
await engine.putPage('people/alice', personPage('Alice'));
|
||||
await engine.executeRaw(`UPDATE pages SET updated_at = '2000-01-01T00:00:00Z' WHERE slug = 'people/alice'`);
|
||||
expect(await engine.countStalePagesForExtraction({ versionTs: LINK_EXTRACTOR_VERSION_TS })).toBe(1);
|
||||
|
||||
await runExtract(engine, ['--stale']);
|
||||
// Pre-floor this stayed 1 forever (stamp < versionTs → version arm re-fires).
|
||||
expect(await engine.countStalePagesForExtraction({ versionTs: LINK_EXTRACTOR_VERSION_TS })).toBe(0);
|
||||
await runExtract(engine, ['--stale']);
|
||||
expect(await engine.countStalePagesForExtraction({ versionTs: LINK_EXTRACTOR_VERSION_TS })).toBe(0);
|
||||
});
|
||||
|
||||
test('CDX-4 (D2): a link-flush throw aborts the sweep and leaves pages UNSTAMPED', async () => {
|
||||
await engine.putPage('people/alice', personPage('Alice'));
|
||||
await engine.putPage('companies/acme', companyPage('Acme', '[Alice](people/alice) founded [Acme](companies/acme).'));
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import {
|
||||
factsWorldOnly,
|
||||
readableFactVisibilities,
|
||||
} from '../src/core/facts/reader-trust.ts';
|
||||
|
||||
describe('factsWorldOnly', () => {
|
||||
test('FAIL-CLOSED: unset remote is untrusted (world-only)', () => {
|
||||
expect(factsWorldOnly({})).toBe(true);
|
||||
expect(factsWorldOnly({ remote: undefined })).toBe(true);
|
||||
expect(factsWorldOnly({ trustedFactReads: false })).toBe(true);
|
||||
});
|
||||
|
||||
test('explicit remote=false (trusted local CLI) sees all', () => {
|
||||
expect(factsWorldOnly({ remote: false })).toBe(false);
|
||||
expect(factsWorldOnly({ remote: false, trustedFactReads: false })).toBe(false);
|
||||
});
|
||||
|
||||
test('untrusted remote callers are world-only', () => {
|
||||
expect(factsWorldOnly({ remote: true })).toBe(true);
|
||||
expect(factsWorldOnly({ remote: true, trustedFactReads: false })).toBe(true);
|
||||
});
|
||||
|
||||
test('owner-trusted remote reads bypass the world-only filter', () => {
|
||||
expect(factsWorldOnly({ remote: true, trustedFactReads: true })).toBe(false);
|
||||
});
|
||||
|
||||
test('trustedFactReads is a no-op for an already-trusted local caller', () => {
|
||||
expect(factsWorldOnly({ remote: false, trustedFactReads: true })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readableFactVisibilities', () => {
|
||||
test("world-only readers get the ['world'] filter (incl. unset remote)", () => {
|
||||
expect(readableFactVisibilities({ remote: true })).toEqual(['world']);
|
||||
expect(readableFactVisibilities({})).toEqual(['world']);
|
||||
});
|
||||
|
||||
test('trusted readers get undefined (no filter — all rows)', () => {
|
||||
expect(readableFactVisibilities({ remote: false })).toBeUndefined();
|
||||
expect(readableFactVisibilities({ remote: true, trustedFactReads: true })).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -140,6 +140,15 @@ describe('extractEntityRefs', () => {
|
||||
expect(wikiRefs[0].needsResolution).toBe(true);
|
||||
});
|
||||
|
||||
test('recognizes ops/ qualified wikilinks (issue #2576 bug 2)', () => {
|
||||
// `ops` was missing from DIR_PATTERN, so [[ops/...]] fell through to
|
||||
// the generic 2c pass (needsResolution) instead of being a real ref.
|
||||
const refs = extractEntityRefs('Deployed via [[ops/services/pointer-agent]].');
|
||||
expect(refs.length).toBe(1);
|
||||
expect(refs[0].slug).toBe('ops/services/pointer-agent');
|
||||
expect(refs[0].needsResolution).toBeUndefined();
|
||||
});
|
||||
|
||||
test('skips qualified-syntax tokens (those belong to 2a)', () => {
|
||||
// [[wiki:topics/ai]] looks like 2a's qualified shape — even though
|
||||
// it wouldn't satisfy DIR_PATTERN, 2c must not claim it either
|
||||
@@ -1069,6 +1078,15 @@ describe('makeResolver — fallback chain', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
test('resolveBasenameMatches: path-style ref falls back to the tail (issue #2576 bug 2)', async () => {
|
||||
// normalizeBasename strips slashes, so `runbooks/2026-05-01-pointer-agent`
|
||||
// used to normalize to a garbage key that never hit the tail-keyed index.
|
||||
const engine = makeFakeEngineWithSlugs(['ops/changes/2026-05-01-pointer-agent']);
|
||||
const r = makeResolver(engine);
|
||||
expect(await r.resolveBasenameMatches!('runbooks/2026-05-01-pointer-agent'))
|
||||
.toEqual(['ops/changes/2026-05-01-pointer-agent']);
|
||||
});
|
||||
|
||||
test('resolveBasenameMatches: case-insensitive fallback', async () => {
|
||||
const engine = makeFakeEngineWithSlugs(['companies/fast-weigh']);
|
||||
const r = makeResolver(engine);
|
||||
|
||||
Reference in New Issue
Block a user