Skip to content

Commit 975b7c6

Browse files
Merge issue-224-deterministic-fixtures: stabilize calendar and search test fixtures (#224)
2 parents 241c8df + 3365055 commit 975b7c6

6 files changed

Lines changed: 60 additions & 53 deletions

File tree

backend/e2e/canonical-route-parity.spec.js

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ const { test, expect } = require('@playwright/test');
22
const { recordCapabilityEvidence } = require('./helpers/capability-evidence');
33
const AxeBuilder = require('@axe-core/playwright').default;
44
const path = require('path');
5+
const { offsetBusinessDate } = require('./helpers/business-date');
56

67
const SHOTS = path.resolve(__dirname, '..', '..', '.tmp', 'screenshots', 'issue-156');
78

@@ -16,7 +17,7 @@ function berlinToday() {
1617
return `${parts.year}-${parts.month}-${parts.day}`;
1718
}
1819

19-
async function createFixtures(request) {
20+
async function createFixtures(request, referenceTime = Date.now()) {
2021
const id = suffix();
2122
const card = (await (await request.post('/api/cards', {
2223
data: { title: `Route workflow ${id}`, anchorDate: '2026-08-11' },
@@ -34,10 +35,10 @@ async function createFixtures(request) {
3435
data: { source: 'manual', title: `Blocked intake ${id}`, note: 'Waiting on a synthetic reply', dataClass: 'internal' },
3536
})).json()).item;
3637
await request.post(`/api/intake/${blocked.id}/block`, {
37-
data: { reason: 'Need a response', waitingFor: 'Synthetic partner', followUpAt: '2026-08-01T09:00:00.000Z' },
38+
data: { reason: 'Need a response', waitingFor: 'Synthetic partner', followUpAt: `${offsetBusinessDate(referenceTime, -2)}T09:00:00.000Z` },
3839
});
3940
await request.post(`/api/intake/${blocked.id}/follow-up-sent`, {
40-
data: { note: 'Sent a synthetic reminder', nextFollowUpAt: '2026-08-10T09:00:00.000Z', channel: 'email' },
41+
data: { note: 'Sent a synthetic reminder', nextFollowUpAt: `${offsetBusinessDate(referenceTime, -1)}T09:00:00.000Z`, channel: 'email' },
4142
});
4243
const filteredOut = (await (await request.post('/api/intake', {
4344
data: { source: 'manual', title: `Archived intake ${id}`, note: 'Genuinely outside the actionable filter', dataClass: 'internal' },
@@ -640,7 +641,8 @@ test.describe('issue 156 canonical route and operator parity', () => {
640641
});
641642

642643
test('provides mobile Inbox, dismissal, recurring delete, sign-out, a11y, and exact screenshot evidence', async ({ page, request }, testInfo) => {
643-
const fixture = await createFixtures(request);
644+
const referenceTime = Date.now();
645+
const fixture = await createFixtures(request, referenceTime);
644646
const createStateIntake = async (label) => (await (await request.post('/api/intake', {
645647
data: { source: 'manual', title: `${label} ${fixture.id}`, note: `Synthetic ${label.toLowerCase()} context`, dataClass: 'internal' },
646648
})).json()).item;
@@ -662,7 +664,7 @@ test.describe('issue 156 canonical route and operator parity', () => {
662664
})).json()).item;
663665
const futureBlockedSource = await createStateIntake('Future blocked intake');
664666
const futureBlocked = (await (await request.post(`/api/intake/${futureBlockedSource.id}/block`, {
665-
data: { reason: 'Future synthetic wait', waitingFor: 'Synthetic partner', followUpAt: '2026-09-01T09:00:00.000Z' },
667+
data: { reason: 'Future synthetic wait', waitingFor: 'Synthetic partner', followUpAt: `${offsetBusinessDate(referenceTime, 2)}T09:00:00.000Z` },
666668
})).json()).item;
667669

668670
await page.setViewportSize({ width: 1440, height: 900 });

backend/tests/conversational-execution.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { after, before, describe, it } from 'node:test';
22
import assert from 'node:assert';
33
import { GetCommand, QueryCommand, type DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
4+
import { useFixedDate } from './helpers/fixed-date';
45

56
import { getClient } from '../src/db/client';
67
import {
@@ -112,6 +113,7 @@ function event(
112113
}
113114

114115
describe('transactional conversational approval and durable execution', () => {
116+
useFixedDate(NOW);
115117
let client: DynamoDBDocumentClient;
116118
let sequence = 0;
117119

backend/tests/conversational-state.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import assert from 'node:assert';
33
import fs from 'fs/promises';
44
import path from 'path';
55
import type { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
6+
import { useFixedDate } from './helpers/fixed-date';
67

78
import { getClient } from '../src/db/client';
89
import { startLocal, stopLocal } from '../scripts/local-dynamodb';
@@ -163,6 +164,7 @@ function attempt(conversationId: string): ExecutionAttempt {
163164
}
164165

165166
describe('conversational state persistence', () => {
167+
useFixedDate(NOW_DATE);
166168
let client: DynamoDBDocumentClient;
167169
const generatedDirs: string[] = [];
168170

backend/tests/docs-search.test.ts

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { describe, it, before, after } from 'node:test';
22
import assert from 'node:assert';
33
import { mkdtempSync, rmSync } from 'node:fs';
44
import { tmpdir } from 'node:os';
5-
import { join, resolve } from 'node:path';
5+
import { join } from 'node:path';
66

77
import {
88
createSearchIndex,
@@ -13,10 +13,8 @@ import {
1313
type SearchDocument,
1414
type SearchResult,
1515
} from '../src/docs/searchIndex';
16-
import { extractDoc, iterContentDocs } from '../src/docs/search/extract';
17-
import { findContentRoot } from './helpers/content';
16+
import { extractDoc } from '../src/docs/search/extract';
1817

19-
const CONTENT_DIR = findContentRoot();
2018
const paths = (results: SearchResult[]): string[] => results.map((r) => String(r.path));
2119

2220
describe('docs search - field config (parity with docs_index.py)', () => {
@@ -144,51 +142,39 @@ describe('docs search - save/load round-trip (json-1)', () => {
144142
});
145143
});
146144

147-
// Smoke fixtures updated for TF-IDF (minsearch) -> BM25-lite (zerosearch).
148-
// Captured against a minsearch top-k baseline: top-1 is identical for every
149-
// query below; ordering shifts inside the relevant set, recall stays on par
150-
// (see issue #85 notes). Each fixture asserts the BM25-lite top-1 plus a doc
151-
// that must remain in the top-5 (recall guard).
152-
const SMOKE: { query: string; top1: string; recall: string }[] = [
153-
{
154-
query: 'podcast intake',
155-
top1: 'content/media/podcast/templates/podcast-share-the-podcast-page-template.md',
156-
recall: 'content/tasks/templates/podcast.md',
157-
},
158-
{
159-
query: 'newsletter sponsor',
160-
top1: 'content/overview/reference/newsletter.md',
161-
recall: 'content/newsletter/sponsorship/sops/creating-a-document-for-sponsored-content-for-a-newsletter.md',
162-
},
163-
{
164-
query: 'course certificate',
165-
top1: 'content/tasks/templates/course.md',
166-
recall: 'content/courses/reference/course-guide.md',
167-
},
168-
{
169-
query: 'youtube upload',
170-
top1: 'content/media/open-source-spotlight/reference/for-update-download-open-source-spotlight-video-from-zoom-and-upload-it-to-youtube.md',
171-
recall: 'content/media/video-youtube/sops/downloading-and-uploading-videos-from-loom-to-youtube.md',
172-
},
145+
// Public synthetic relevance fixtures: title/summary matches should outrank
146+
// incidental single-word matches, while related body text remains discoverable.
147+
const SMOKE = [
148+
{ query: 'podcast intake', title: 'Podcast intake checklist', related: 'Recording preparation', distractor: 'Podcast audio settings' },
149+
{ query: 'newsletter sponsor', title: 'Newsletter sponsor overview', related: 'Publication planning', distractor: 'Newsletter typography' },
150+
{ query: 'course certificate', title: 'Course certificate guide', related: 'Completion records', distractor: 'Course exercises' },
151+
{ query: 'youtube upload', title: 'YouTube upload checklist', related: 'Video preparation', distractor: 'YouTube analytics' },
173152
];
174153

175-
describe('docs search - smoke query relevance over the document corpus (BM25-lite)', () => {
176-
// The corpus lives in the private knowledge repository, so these rank real
177-
// documents only when it is checked out.
178-
let index: ReturnType<typeof createSearchIndex>;
179-
before(() => {
180-
if (!CONTENT_DIR) return;
181-
const docs = iterContentDocs(CONTENT_DIR);
182-
index = createSearchIndex().fit(docs);
183-
});
184-
185-
for (const { query, top1, recall } of SMOKE) {
186-
it(`ranks the expected doc first and keeps recall for "${query}"`, (t) => {
187-
if (!CONTENT_DIR) return t.skip('knowledge repository not checked out');
154+
describe('docs search - smoke query relevance over a public synthetic corpus (BM25-lite)', () => {
155+
const corpus: SearchDocument[] = SMOKE.flatMap(({ query, title, related, distractor }, index) => [
156+
{
157+
path: `synthetic/topic-${index}/overview.md`, id: `overview-${index}`,
158+
title, summary: `A synthetic overview of ${query}.`,
159+
body: 'Reference material for a test topic.',
160+
},
161+
{
162+
path: `synthetic/topic-${index}/related.md`, id: `related-${index}`,
163+
title: related, body: `This synthetic reference also discusses ${query}.`,
164+
},
165+
{
166+
path: `synthetic/topic-${index}/incidental.md`, id: `incidental-${index}`,
167+
title: distractor, body: 'A separate topic with only an incidental keyword match.',
168+
},
169+
]);
170+
const index = createSearchIndex().fit(corpus);
171+
172+
for (const [topic, { query }] of SMOKE.entries()) {
173+
it(`ranks the expected doc first and keeps recall for "${query}"`, () => {
188174
const results = index.search(query, { numResults: 5 });
189-
assert.ok(results.length > 0, 'expected at least one result');
190-
assert.strictEqual(results[0].path, top1, `top-1 for "${query}"`);
191-
assert.ok(paths(results).includes(recall), `"${recall}" should be in top-5 for "${query}"`);
175+
assert.strictEqual(results[0]?.path, `synthetic/topic-${topic}/overview.md`, `top-1 for "${query}"`);
176+
assert.ok(paths(results).includes(`synthetic/topic-${topic}/related.md`), 'related body-text match remains in top-5');
177+
assert.ok(paths(results).includes(`synthetic/topic-${topic}/incidental.md`), 'a competing single-term document was also searched');
192178
});
193179
}
194180
});
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { after, before, mock } from 'node:test';
2+
3+
/** Fix only wall-clock Date inside this suite; async/database timers stay real. */
4+
export function useFixedDate(now: Date): void {
5+
// Runtime CI uses Node 20+, whose Date-only timer API is newer than the
6+
// project's Node 18 type declarations. Keep this type bridge test-local.
7+
const timers = mock.timers as unknown as {
8+
enable(options: { apis: ['Date']; now: Date }): void;
9+
reset(): void;
10+
};
11+
before(() => timers.enable({ apis: ['Date'], now }));
12+
after(() => timers.reset());
13+
}

backend/tests/telegram-conversational.test.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { mkdir, readdir, rm, symlink, utimes, writeFile } from 'fs/promises';
44
import path from 'path';
55
import { encode as encodeJpeg } from 'jpeg-js';
66
import { PutCommand, QueryCommand, type DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
7+
import { useFixedDate } from './helpers/fixed-date';
78

89
import { getClient } from '../src/db/client';
910
import { startLocal, stopLocal } from '../scripts/local-dynamodb';
@@ -188,6 +189,7 @@ class FakeCore implements TelegramCoreRuntime {
188189
}
189190

190191
describe('private conversational Telegram adapter', () => {
192+
useFixedDate(NOW);
191193
let client: Awaited<ReturnType<typeof getClient>>;
192194
let telegram: FakeTelegram;
193195
let core: FakeCore;
@@ -1138,9 +1140,9 @@ describe('private conversational Telegram adapter', () => {
11381140
})
11391141
)) as typeof fetch;
11401142
const hangingTelegram = new HttpTelegramClient('fake', 100, 1024, hangingFetch);
1141-
const started = Date.now();
1143+
const started = performance.now();
11421144
await assert.rejects(() => hangingTelegram.getFile('safe-file'));
1143-
assert.ok(Date.now() - started < 1_000);
1145+
assert.ok(performance.now() - started < 1_000);
11441146

11451147
let telegramCalls = 0;
11461148
const oversizedFetch = (async () => {

0 commit comments

Comments
 (0)