From 55197a8ccf66e3e2c157335aec289fad99ec57e0 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 18:51:27 +0000 Subject: [PATCH 1/8] Allow read-only Linear and GitHub MCP tools without prompts The Linear MCP server registers as 'Linear' (capitalized), so the existing lowercase mcp__linear__* allowlist entries never match its tools. Add capitalized twins for the approved read tools, plus the read-only GitHub MCP tools that remote sessions use for PR reads and the session-start index-cache fetch. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UFceD1sbstj9M4K8xRL62R --- .claude/settings.json | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.claude/settings.json b/.claude/settings.json index bd09a945ce1..246396fb135 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -18,6 +18,23 @@ "mcp__linear__get_user", "mcp__linear__list_projects", "mcp__linear__list_issue_statuses", + "mcp__Linear__get_issue", + "mcp__Linear__list_comments", + "mcp__Linear__get_project", + "mcp__Linear__get_user", + "mcp__Linear__list_projects", + "mcp__Linear__list_issue_statuses", + "mcp__Linear__list_issues", + "mcp__github__pull_request_read", + "mcp__github__list_pull_requests", + "mcp__github__get_file_contents", + "mcp__github__search_code", + "mcp__github__list_commits", + "mcp__github__get_commit", + "mcp__github__get_me", + "mcp__github__actions_list", + "mcp__github__actions_get", + "mcp__github__get_job_logs", "mcp__chrome-devtools__take_screenshot" ] }, From 6099e28dce13d2fc752ceab06d0f27e8b58d4119 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 19:37:41 +0000 Subject: [PATCH 2/8] Scan production index in invalidation fan-out itemsThatReference only scanned boxel_index_working and prerendered_html, so a dependent whose row exists only in production boxel_index (index imported from a production-tables-only dump, or rows staged under a since-changed schema) was never re-rendered when a module it depends on changed. Its indexed computed values stayed stale until the file was touched individually. Scan production too, excluding promoted tombstones, and collapse duplicates in the existing (url, type) dedup. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UFceD1sbstj9M4K8xRL62R --- packages/host/tests/unit/index-writer-test.ts | 60 +++++++++++++++++++ packages/runtime-common/index-writer.ts | 40 +++++++++++-- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/packages/host/tests/unit/index-writer-test.ts b/packages/host/tests/unit/index-writer-test.ts index 28392dbc39d..9ce02992526 100644 --- a/packages/host/tests/unit/index-writer-test.ts +++ b/packages/host/tests/unit/index-writer-test.ts @@ -435,6 +435,66 @@ module('Unit | index-writer', function (hooks) { ]); }); + test('invalidations reach dependents whose rows exist only in the production index', async function (assert) { + // A production row without a working counterpart is a reachable state: + // the working table is a staging area that only gains a row once a visit + // writes one in this deployment's lifetime — an index imported from a + // dump that carries only the production tables, or rows staged under a + // since-changed schema. The dependency fan-out must find such dependents + // or a module edit leaves their indexed computed values stale until each + // file is touched individually. + await setupIndex( + adapter, + [{ realm_url: testRealmURL, current_generation: 1 }], + { + working: [], + production: [ + { + url: `${testRealmURL}1.json`, + file_alias: `${testRealmURL}1.json`, + type: 'instance', + generation: 1, + realm_url: testRealmURL, + deps: [`${testRealmURL}person`], + }, + // Transitive: discoverable only through 1.json's production row. + { + url: `${testRealmURL}2.json`, + file_alias: `${testRealmURL}2.json`, + type: 'instance', + generation: 1, + realm_url: testRealmURL, + deps: [`${testRealmURL}1.json`], + }, + // A promoted tombstone is a deleted file — it stays out of the + // fan-out. + { + url: `${testRealmURL}3.json`, + file_alias: `${testRealmURL}3.json`, + type: 'instance', + generation: 1, + realm_url: testRealmURL, + is_deleted: true, + deps: [`${testRealmURL}person`], + }, + ], + }, + { prerenderedHtml: false }, + ); + + let batch = await indexWriter.createBatch( + new URL(testRealmURL), + virtualNetwork, + ); + await batch.invalidate([new URL(`${testRealmURL}person.gts`)]); + + assert.deepEqual(batch.invalidations.sort(), [ + `${testRealmURL}1.json`, + `${testRealmURL}2.json`, + `${testRealmURL}person.gts`, + ]); + }); + test("invalidations don't cross realm boundaries", async function (assert) { await setupIndex( adapter, diff --git a/packages/runtime-common/index-writer.ts b/packages/runtime-common/index-writer.ts index 00ba3da02e4..dae6c46b5b5 100644 --- a/packages/runtime-common/index-writer.ts +++ b/packages/runtime-common/index-writer.ts @@ -2420,7 +2420,7 @@ export class Batch { unresolvedPath !== resolvedPath && this.isRegisteredPrefix(unresolvedPath); let scanTable = async ( - tableName: 'boxel_index_working' | 'prerendered_html', + tableName: 'boxel_index_working' | 'prerendered_html' | 'boxel_index', ) => { let results: (Pick & { type: BoxelIndexTable['type']; @@ -2476,6 +2476,19 @@ export class Batch { // probably need to reevaluate this condition when we get to cross // realm invalidation [`i.realm_url =`, param(this.realmURL.href)], + // A promoted tombstone is a deleted file — it has nothing to + // re-render, and pulling it into the fan-out would visit a path + // with no backing file. The working/prerendered scans carry + // tombstones from in-flight batches whose skip logic handles + // them, so only the production scan filters. + ...(tableName === 'boxel_index' + ? [ + any([ + ['i.is_deleted = false'], + ['i.is_deleted IS NULL'], + ]) as Expression, + ] + : []), ]), `LIMIT ${pageSize} OFFSET ${pageNumber * pageSize}`, ] as Expression)) as (Pick & { @@ -2492,15 +2505,30 @@ export class Batch { // — a rendered non-searchable link, the scoped-CSS artifacts of linked // instances. Both edge sets must feed the fan-out or a change to a // render-only dependency would never re-render its consumers. - let [workingScan, prerenderedScan] = await Promise.all([ + // + // Production `boxel_index` is scanned as well because the working table + // is a staging area whose coverage is not guaranteed: a row only exists + // there once a visit has written it in this deployment's lifetime, so + // promoted rows can lack a working counterpart (an index imported from a + // dump that carries only the production tables, or rows staged under a + // since-changed schema). Without the production scan, a module edit + // never reaches such dependents — their indexed computed values sit + // stale until each file is touched individually. Rows present in both + // tables collapse in the (url, type) dedup below. + let [workingScan, prerenderedScan, productionScan] = await Promise.all([ scanTable('boxel_index_working'), scanTable('prerendered_html'), + scanTable('boxel_index'), ]); let seen = new Set(); let results: (Pick & { type: BoxelIndexTable['type']; })[] = []; - for (let row of [...workingScan.results, ...prerenderedScan.results]) { + for (let row of [ + ...workingScan.results, + ...prerenderedScan.results, + ...productionScan.results, + ]) { let key = `${row.url}|${row.type}`; if (seen.has(key)) { continue; @@ -2511,7 +2539,11 @@ export class Batch { this.#perfLog.debug( `${jobIdentity(this.jobInfo)} time to determine items that reference ${resolvedPath} ${ Date.now() - start - } ms (page count=${workingScan.pageNumber + prerenderedScan.pageNumber})`, + } ms (page count=${ + workingScan.pageNumber + + prerenderedScan.pageNumber + + productionScan.pageNumber + })`, ); return results.map(({ url, file_alias, type }) => ({ url, From 5e884fec664346d0b35a92e16c00b7341ce79c6b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 19:37:52 +0000 Subject: [PATCH 3/8] Serve write-time content meta over indexed values in file-meta docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A render that runs inside the very batch that re-indexes a file reads the pre-swap production index row, so a card linking the file indexed the file's previous contentHash/contentSize — and any reindex re-serialized the same stale values instead of healing them. realm_file_meta is written in the same critical section as the file's bytes, so prefer it when serving file-meta documents; the indexed values remain fallbacks for files never hashed at write time. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UFceD1sbstj9M4K8xRL62R --- .../realm-server/tests/helpers/indexing.ts | 24 +++++ packages/realm-server/tests/indexing-test.ts | 94 +++++++++++++++++++ packages/runtime-common/realm.ts | 27 ++++-- 3 files changed, 136 insertions(+), 9 deletions(-) diff --git a/packages/realm-server/tests/helpers/indexing.ts b/packages/realm-server/tests/helpers/indexing.ts index b24306bf11f..3f202ad7f19 100644 --- a/packages/realm-server/tests/helpers/indexing.ts +++ b/packages/realm-server/tests/helpers/indexing.ts @@ -348,6 +348,30 @@ export async function depsForIndexEntry( ]; } +// The production `search_doc` for a row, parsed. Undefined when the URL has +// no row of that type or the row carries no search doc. +export async function searchDocForIndexEntry( + dbAdapter: DBAdapter, + url: string, + type: 'instance' | 'file' = 'instance', +): Promise | undefined> { + let rows = (await query(dbAdapter, [ + `SELECT search_doc FROM boxel_index WHERE`, + ...every([ + ['url =', param(url)], + ['type =', param(type)], + ]), + `ORDER BY generation DESC LIMIT 1`, + ] as Expression)) as { search_doc: Record | string | null }[]; + let raw = rows[0]?.search_doc; + if (raw == null) { + return undefined; + } + return typeof raw === 'string' + ? (JSON.parse(raw) as Record) + : raw; +} + export async function indexedAtForIndexEntry( dbAdapter: DBAdapter, url: string, diff --git a/packages/realm-server/tests/indexing-test.ts b/packages/realm-server/tests/indexing-test.ts index 18035ce9c65..fa732c483ce 100644 --- a/packages/realm-server/tests/indexing-test.ts +++ b/packages/realm-server/tests/indexing-test.ts @@ -39,11 +39,13 @@ import { errorDocForIndexEntry, indexedAtForIndexEntry, maxPrerenderHtmlJobId, + searchDocForIndexEntry, settlePrerenderHtmlJobs, typeForIndexEntry, } from './helpers/indexing.ts'; import stripScopedCSSAttributes from '@cardstack/runtime-common/helpers/strip-scoped-css-attributes'; import { basename } from 'path'; +import { createHash } from 'crypto'; import type { PgAdapter } from '@cardstack/postgres'; function trimCardContainer(text: string) { @@ -4489,6 +4491,98 @@ module(basename(import.meta.filename), function () { ); }); + test('consumers of a searchable FileDef target index the rewritten bytes when re-rendered in the same batch as the rewrite', async function (assert) { + await realm.write( + 'searchable-file-consumer.gts', + ` + import { CardDef, field, linksTo } from "@cardstack/base/card-api"; + import { FileDef } from "@cardstack/base/file-api"; + + export class SearchableFileConsumer extends CardDef { + @field primaryFile = linksTo(() => FileDef, { searchable: true }); + } + `, + ); + await realm.write('searchable-note.txt', 'note v1'); + await realm.write( + 'searchable-file-consumer.json', + JSON.stringify({ + data: { + relationships: { + primaryFile: { links: { self: './searchable-note.txt' } }, + }, + meta: { + adoptsFrom: { + module: rri('./searchable-file-consumer'), + name: 'SearchableFileConsumer', + }, + }, + }, + } as LooseSingleCardDocument), + ); + + let consumerURL = `${testRealm}searchable-file-consumer.json`; + let before = await searchDocForIndexEntry(testDbAdapter, consumerURL); + assert.strictEqual( + before?.primaryFile?.contentSize, + 'note v1'.length, + 'consumer indexes the original file bytes', + ); + + // The rewrite's incremental batch re-renders the consumer in the + // same pass that re-indexes the file (the consumer's deps include the + // file). That render fetches the file's meta doc from the realm, + // which must reflect the just-written bytes — the file's own updated + // index row is not promoted to production until the batch completes, + // so serving from the index alone would hand the consumer the + // previous bytes' contentHash/contentSize. + let rewritten = 'note v2 with more bytes'; + await realm.write('searchable-note.txt', rewritten); + + let after = await searchDocForIndexEntry(testDbAdapter, consumerURL); + assert.strictEqual( + after?.primaryFile?.contentSize, + rewritten.length, + 'consumer indexes the rewritten contentSize', + ); + assert.strictEqual( + after?.primaryFile?.contentHash, + createHash('md5').update(rewritten).digest('hex'), + 'consumer indexes the rewritten contentHash', + ); + }); + + test('file meta serves write-time contentHash/contentSize over indexed values', async function (assert) { + let content = 'bytes behind the file meta doc'; + await realm.write('meta-note.txt', content); + + // Emulate the state a mid-batch render observes: the production + // index row still carries the previous bytes' content values while + // `realm_file_meta` (written in the same critical section as the + // bytes) already has the current ones. The pristine resource is + // dropped so the served doc must choose between the indexed search + // doc and the persisted meta. + await testDbAdapter.execute( + `UPDATE boxel_index SET search_doc = COALESCE(search_doc, '{}'::jsonb) || '{"contentHash":"stale-hash","contentSize":1}'::jsonb, pristine_doc = NULL WHERE url = '${testRealm}meta-note.txt' AND type = 'file'`, + ); + + let response = await fetch(`${testRealm}meta-note.txt`, { + headers: { Accept: SupportedMimeType.FileMeta }, + }); + assert.strictEqual(response.status, 200, 'file meta response is ok'); + let doc = (await response.json()) as LooseSingleCardDocument; + assert.strictEqual( + doc.data.attributes?.contentHash, + createHash('md5').update(content).digest('hex'), + 'contentHash reflects the bytes on disk, not the indexed value', + ); + assert.strictEqual( + doc.data.attributes?.contentSize, + content.length, + 'contentSize reflects the bytes on disk, not the indexed value', + ); + }); + test('can incrementally index deleted instance', async function (assert) { await realm.delete('mango.json'); diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index 913f6e49794..17e4c8e7690 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -4479,13 +4479,19 @@ export class Realm { typeof searchDoc.contentSize === 'number' ? searchDoc.contentSize : undefined; - // Only hit the DB when the indexed searchDoc is missing a value. - let persistedMeta = - (searchHash === undefined || searchSize === undefined) && this.#dbAdapter - ? await getContentMeta(this.#dbAdapter, this.url, localPath) - : { contentHash: undefined, contentSize: undefined }; - let contentHash = searchHash ?? persistedMeta.contentHash; - let contentSize = searchSize ?? persistedMeta.contentSize; + // `realm_file_meta` is written in the same critical section as the file's + // bytes, so it is authoritative for content-derived values. The indexed + // values lag one batch promotion behind: a render inside the very batch + // that re-indexes this file reads the pre-swap production row, so a card + // linking the file would otherwise index the file's PREVIOUS + // contentHash/contentSize — and a reindex re-serializes the same stale + // values rather than healing them. The indexed values remain as + // fallbacks for files whose bytes were never hashed at write time. + let persistedMeta = this.#dbAdapter + ? await getContentMeta(this.#dbAdapter, this.url, localPath) + : { contentHash: undefined, contentSize: undefined }; + let contentHash = persistedMeta.contentHash ?? searchHash; + let contentSize = persistedMeta.contentSize ?? searchSize; let adoptsFrom = codeRefFromInternalKey(fileEntry.types?.[0]) ?? (isCodeRef(fileEntry.resource?.meta?.adoptsFrom) @@ -4501,8 +4507,11 @@ export class Realm { resourceAttributes.contentType ?? searchDoc.contentType ?? inferredContentType, - contentHash: resourceAttributes.contentHash ?? contentHash, - contentSize: resourceAttributes.contentSize ?? contentSize, + // The persisted write-time values win over the indexed resource's for + // the same staleness reason as above; the resource's extract-computed + // values cover files that predate write-time hashing. + contentHash: contentHash ?? resourceAttributes.contentHash, + contentSize: contentSize ?? resourceAttributes.contentSize, lastModified: fileEntry.lastModified ?? unixTime(Date.now()), createdAt: createdAt ?? unixTime(Date.now()), }; From b33adb3352867c000d4cc6e8593603d7319c98cf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 20:47:00 +0000 Subject: [PATCH 4/8] Establish FileDef-consumer test baseline via dependency fan-out MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An instance's initial index visit can expand a searchable FileDef link as { id } only, so the baseline snapshot now comes from the fan-out re-render that follows the file's first write — the same path the rewrite assertion exercises. Also let QUNIT_FILTER narrow a run to individual tests, complementing TEST_MODULES/TEST_FILES. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UFceD1sbstj9M4K8xRL62R --- packages/realm-server/tests/index.ts | 4 ++++ packages/realm-server/tests/indexing-test.ts | 16 ++++++++-------- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/realm-server/tests/index.ts b/packages/realm-server/tests/index.ts index c8ec404bb0c..b26ada915c6 100644 --- a/packages/realm-server/tests/index.ts +++ b/packages/realm-server/tests/index.ts @@ -111,6 +111,10 @@ if (testModules) { } } +if (process.env.QUNIT_FILTER) { + QUnit.config.filter = process.env.QUNIT_FILTER; +} + // Cleanup here ensures lingering servers/prerenderers/queues don't keep the // Node event loop alive after tests finish — and equivalently, don't leave // hardcoded test ports (4444-4471, etc.) bound after a test is aborted by diff --git a/packages/realm-server/tests/indexing-test.ts b/packages/realm-server/tests/indexing-test.ts index fa732c483ce..93f9894241f 100644 --- a/packages/realm-server/tests/indexing-test.ts +++ b/packages/realm-server/tests/indexing-test.ts @@ -4503,7 +4503,6 @@ module(basename(import.meta.filename), function () { } `, ); - await realm.write('searchable-note.txt', 'note v1'); await realm.write( 'searchable-file-consumer.json', JSON.stringify({ @@ -4521,7 +4520,15 @@ module(basename(import.meta.filename), function () { } as LooseSingleCardDocument), ); + // Each write of the file re-renders the consumer through the + // dependency fan-out, inside the same batch that re-indexes the file + // itself. That render fetches the file's meta doc from the realm, + // which must reflect the just-written bytes — the file's own updated + // index row is not promoted to production until the batch completes, + // so serving from the index alone would hand the consumer the + // previous bytes' contentHash/contentSize. let consumerURL = `${testRealm}searchable-file-consumer.json`; + await realm.write('searchable-note.txt', 'note v1'); let before = await searchDocForIndexEntry(testDbAdapter, consumerURL); assert.strictEqual( before?.primaryFile?.contentSize, @@ -4529,13 +4536,6 @@ module(basename(import.meta.filename), function () { 'consumer indexes the original file bytes', ); - // The rewrite's incremental batch re-renders the consumer in the - // same pass that re-indexes the file (the consumer's deps include the - // file). That render fetches the file's meta doc from the realm, - // which must reflect the just-written bytes — the file's own updated - // index row is not promoted to production until the batch completes, - // so serving from the index alone would hand the consumer the - // previous bytes' contentHash/contentSize. let rewritten = 'note v2 with more bytes'; await realm.write('searchable-note.txt', rewritten); From 1acbfb58f04e1caff105aae0b3494c71756f9816 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 21:06:56 +0000 Subject: [PATCH 5/8] Tighten file-meta precedence comment to state the invariant State why realm_file_meta is authoritative and how the indexed values lag, rather than describing what would go wrong without the preference. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UFceD1sbstj9M4K8xRL62R --- packages/runtime-common/realm.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/packages/runtime-common/realm.ts b/packages/runtime-common/realm.ts index 17e4c8e7690..c7c1d444b72 100644 --- a/packages/runtime-common/realm.ts +++ b/packages/runtime-common/realm.ts @@ -4480,12 +4480,11 @@ export class Realm { ? searchDoc.contentSize : undefined; // `realm_file_meta` is written in the same critical section as the file's - // bytes, so it is authoritative for content-derived values. The indexed - // values lag one batch promotion behind: a render inside the very batch - // that re-indexes this file reads the pre-swap production row, so a card - // linking the file would otherwise index the file's PREVIOUS - // contentHash/contentSize — and a reindex re-serializes the same stale - // values rather than healing them. The indexed values remain as + // bytes, so it is authoritative for content-derived values. It is + // preferred over the indexed values, which lag one batch promotion behind: + // a render inside the batch that re-indexes this file reads the pre-swap + // production row, so a card linking the file sees the file's index row at + // its previous contentHash/contentSize. The indexed values remain as // fallbacks for files whose bytes were never hashed at write time. let persistedMeta = this.#dbAdapter ? await getContentMeta(this.#dbAdapter, this.url, localPath) From 328e80b6957b8802144b4101b56e847932f47044 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 21:09:37 +0000 Subject: [PATCH 6/8] Ignore a blank QUNIT_FILTER instead of running zero tests A whitespace-only value became an active filter matching nothing, silently running no tests. Trim and only apply when non-empty, matching the TEST_MODULES handling. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UFceD1sbstj9M4K8xRL62R --- packages/realm-server/tests/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/realm-server/tests/index.ts b/packages/realm-server/tests/index.ts index b26ada915c6..61b13d5d638 100644 --- a/packages/realm-server/tests/index.ts +++ b/packages/realm-server/tests/index.ts @@ -111,8 +111,9 @@ if (testModules) { } } -if (process.env.QUNIT_FILTER) { - QUnit.config.filter = process.env.QUNIT_FILTER; +const qunitFilter = process.env.QUNIT_FILTER?.trim(); +if (qunitFilter) { + QUnit.config.filter = qunitFilter; } // Cleanup here ensures lingering servers/prerenderers/queues don't keep the From ec16ba0bc7b8adaf12aa4cbc93270b29d4222815 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 23 Jul 2026 21:09:40 +0000 Subject: [PATCH 7/8] Consolidate Linear MCP allowlist to one canonical spelling The tools resolve as mcp__Linear__* in this environment; the lowercase mcp__linear__* twins never matched. Keep the capitalized set and drop the dead duplicates. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UFceD1sbstj9M4K8xRL62R --- .claude/settings.json | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.claude/settings.json b/.claude/settings.json index 246396fb135..45ecb0c64e5 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -12,12 +12,6 @@ "Bash(node --check *)", "Bash(npx vitest run *)", "Bash(pnpm exec prettier *)", - "mcp__linear__list_comments", - "mcp__linear__get_issue", - "mcp__linear__get_project", - "mcp__linear__get_user", - "mcp__linear__list_projects", - "mcp__linear__list_issue_statuses", "mcp__Linear__get_issue", "mcp__Linear__list_comments", "mcp__Linear__get_project", From 3ecc57d726e8ceb387dab762235d074810acf2b7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Jul 2026 00:01:28 +0000 Subject: [PATCH 8/8] Drop the full-reindex test that didn't fail without the fix The end-to-end full-reindex test passed with or without the file-meta serving change, so it never exercised the regression it claimed to guard. Bug #2 is guarded by the serving-precedence test, which fails without the fix. Remove the redundant test and its now-unused searchDocForIndexEntry helper. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UFceD1sbstj9M4K8xRL62R --- .../realm-server/tests/helpers/indexing.ts | 24 ------- packages/realm-server/tests/indexing-test.ts | 62 ------------------- 2 files changed, 86 deletions(-) diff --git a/packages/realm-server/tests/helpers/indexing.ts b/packages/realm-server/tests/helpers/indexing.ts index 3f202ad7f19..b24306bf11f 100644 --- a/packages/realm-server/tests/helpers/indexing.ts +++ b/packages/realm-server/tests/helpers/indexing.ts @@ -348,30 +348,6 @@ export async function depsForIndexEntry( ]; } -// The production `search_doc` for a row, parsed. Undefined when the URL has -// no row of that type or the row carries no search doc. -export async function searchDocForIndexEntry( - dbAdapter: DBAdapter, - url: string, - type: 'instance' | 'file' = 'instance', -): Promise | undefined> { - let rows = (await query(dbAdapter, [ - `SELECT search_doc FROM boxel_index WHERE`, - ...every([ - ['url =', param(url)], - ['type =', param(type)], - ]), - `ORDER BY generation DESC LIMIT 1`, - ] as Expression)) as { search_doc: Record | string | null }[]; - let raw = rows[0]?.search_doc; - if (raw == null) { - return undefined; - } - return typeof raw === 'string' - ? (JSON.parse(raw) as Record) - : raw; -} - export async function indexedAtForIndexEntry( dbAdapter: DBAdapter, url: string, diff --git a/packages/realm-server/tests/indexing-test.ts b/packages/realm-server/tests/indexing-test.ts index 93f9894241f..567dc5efd35 100644 --- a/packages/realm-server/tests/indexing-test.ts +++ b/packages/realm-server/tests/indexing-test.ts @@ -39,7 +39,6 @@ import { errorDocForIndexEntry, indexedAtForIndexEntry, maxPrerenderHtmlJobId, - searchDocForIndexEntry, settlePrerenderHtmlJobs, typeForIndexEntry, } from './helpers/indexing.ts'; @@ -4491,67 +4490,6 @@ module(basename(import.meta.filename), function () { ); }); - test('consumers of a searchable FileDef target index the rewritten bytes when re-rendered in the same batch as the rewrite', async function (assert) { - await realm.write( - 'searchable-file-consumer.gts', - ` - import { CardDef, field, linksTo } from "@cardstack/base/card-api"; - import { FileDef } from "@cardstack/base/file-api"; - - export class SearchableFileConsumer extends CardDef { - @field primaryFile = linksTo(() => FileDef, { searchable: true }); - } - `, - ); - await realm.write( - 'searchable-file-consumer.json', - JSON.stringify({ - data: { - relationships: { - primaryFile: { links: { self: './searchable-note.txt' } }, - }, - meta: { - adoptsFrom: { - module: rri('./searchable-file-consumer'), - name: 'SearchableFileConsumer', - }, - }, - }, - } as LooseSingleCardDocument), - ); - - // Each write of the file re-renders the consumer through the - // dependency fan-out, inside the same batch that re-indexes the file - // itself. That render fetches the file's meta doc from the realm, - // which must reflect the just-written bytes — the file's own updated - // index row is not promoted to production until the batch completes, - // so serving from the index alone would hand the consumer the - // previous bytes' contentHash/contentSize. - let consumerURL = `${testRealm}searchable-file-consumer.json`; - await realm.write('searchable-note.txt', 'note v1'); - let before = await searchDocForIndexEntry(testDbAdapter, consumerURL); - assert.strictEqual( - before?.primaryFile?.contentSize, - 'note v1'.length, - 'consumer indexes the original file bytes', - ); - - let rewritten = 'note v2 with more bytes'; - await realm.write('searchable-note.txt', rewritten); - - let after = await searchDocForIndexEntry(testDbAdapter, consumerURL); - assert.strictEqual( - after?.primaryFile?.contentSize, - rewritten.length, - 'consumer indexes the rewritten contentSize', - ); - assert.strictEqual( - after?.primaryFile?.contentHash, - createHash('md5').update(rewritten).digest('hex'), - 'consumer indexes the rewritten contentHash', - ); - }); - test('file meta serves write-time contentHash/contentSize over indexed values', async function (assert) { let content = 'bytes behind the file meta doc'; await realm.write('meta-note.txt', content);