Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 15 additions & 12 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.2
Comment on lines +35 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/ci.yml | head -200

Repository: sasazaki1994/TraceMap

Length of output: 5740


🌐 Web query:

pnpm/action-setup v4 Node.js runtime requirements

💡 Result:

The pnpm/action-setup v4 GitHub Action does not have strict Node.js runtime requirements specified in its documentation. It explicitly states: "This action does not setup Node.js for you, use actions/setup-node yourself."[1][2] You must separately configure Node.js using actions/setup-node@v4 (or equivalent), and pnpm/action-setup will use whatever Node.js runtime is available in the environment.[1][3] Key points from official sources: - v2 had compatibility issues with newer Node.js versions, prompting upgrade to v3+ (including v4).[1][2] - Recent v4 releases (v4.3.0, v4.4.0) updated the action's internal runtime to Node.js 24.[4][5] - Older v4 versions likely used Node.js 20 or earlier, but since v4.4.0, it runs on Node.js 24.[4] - Dependencies include @types/node ^20.x, indicating development against Node 20+.[6] - Example workflows use Node.js 20: uses: actions/setup-node@v4 with node-version: 20.[3][7] - Standalone mode (standalone: true) installs @pnpm/exe, a self-bundled executable that does not require Node.js.[1][2] - For pnpm versions like 11.x, Node.js 22+ is recommended due to pnpm requirements, but this is for the installed pnpm, not the action itself.[8] In practice, v4 works with Node.js 20+ in user workflows, with updates ensuring compatibility up to Node.js 24.[4][9] No minimum Node.js version is enforced by the action; compatibility depends on the pnpm version installed and GitHub Actions runner (ubuntu-latest supports Node 20-24 as of 2026).[9]

Citations:


🌐 Web query:

GitHub Actions Node.js 20 deprecation Node.js 24 June 2026

💡 Result:

As of today (2026-05-07), GitHub Actions is deprecating Node.js 20 and migrating the default JavaScript runtime to Node.js 24. 1) Key deprecation/migration dates (Node.js 20 -> Node.js 24) - GitHub’s published deprecation notice says that starting June 2, 2026, GitHub Actions runners begin using Node.js 24 by default. [1] - In that same notice, GitHub also states you can temporarily keep Node.js 20 after June 2, 2026 by setting ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true, but it will only work until Node 20 is removed later in the fall of 2026. [1] - GitHub notes that this deprecation process was started because Node.js 20 reaches end-of-life (EOL) in April 2026. [1] 2) How to react in your workflows - If you use JavaScript actions without pinning to a version that supports Node 24, you should upgrade the action versions. GitHub’s notice explicitly recommends updating actions to run on Node 24 and checking whether newer versions are available. [1] - A common way to control your app/test Node.js version (separate from the runtime used by the GitHub-provided “JavaScript actions” on the runner) is to use actions/setup-node with node-version. [2] 3) What your workflows might be telling you right now - Users commonly see a warning that Node.js 20 actions are deprecated and that actions will be forced to run with Node.js 24 by default starting June 2, 2026; the warning also mentions how to opt into Node 24 early (FORCE_JAVASCRIPT_ACTIONS_TO_NODE24=true) and how to opt out of the default using ACTIONS_ALLOW_USE_UNSECURE_NODE_VERSION=true. [3] If you tell me which specific GitHub Actions (e.g., actions/checkout@..., actions/setup-node@..., github/codeql-action@...) your workflows use, I can map them to what you should upgrade to avoid Node 20 runtime warnings.

Citations:


🌐 Web query:

pnpm/action-setup v6 current version recommended

💡 Result:

Recommended: use pnpm/action-setup@v6, pinned to the latest v6 patch release: pnpm/action-setup@v6.0.5 (published 2026-05-02). [1] Example (best practice: pin to an exact v6.x.y): uses: pnpm/action-setup@v6.0.5 Or, to always stay on the latest v6 line (un-pinned): uses: pnpm/action-setup@v6 Notes: - The action’s README shows usage with pnpm/action-setup@v6, and documents that you can omit the version input to use the version from the package.json packageManager field. [1] - There is a known issue where specifying package_json_file and relying on packageManager in that file may not work as expected on some v6 versions; the issue suggests a workaround of passing version explicitly. [2]

Citations:


pnpm/action-setup を v4 から v6 へアップグレードしてください

pnpm/action-setup@v6 が現在の推奨バージョンです(公式リポジトリのREADMEで明記されており、2026年5月2日に v6.0.5 がリリースされています)。3つのジョブすべてで同じアップグレードが必要です。

🔧 推奨される修正
-        uses: pnpm/action-setup@v4
+        uses: pnpm/action-setup@v6

3箇所(Line 36、Line 95、Line 154)すべてで同様の変更が必要です。

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.2
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
version: 10.18.2
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml around lines 35 - 38, Replace every occurrence of
the GitHub Action usage "uses: pnpm/action-setup@v4" with the current
recommended "uses: pnpm/action-setup@v6" (you can pin to v6.0.5 if desired);
there are three occurrences of "uses: pnpm/action-setup@v4" in the workflow and
you should update all three so each job uses the v6 action, leaving the existing
"with: version: 10.18.2" input unchanged.


- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Enable corepack
run: |
corepack enable
corepack prepare pnpm@10.18.2 --activate

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand Down Expand Up @@ -90,16 +91,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.2

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Enable corepack
run: |
corepack enable
corepack prepare pnpm@10.18.2 --activate

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand Down Expand Up @@ -148,16 +150,17 @@ jobs:
- name: Checkout
uses: actions/checkout@v4

- name: Setup pnpm
uses: pnpm/action-setup@v4
with:
version: 10.18.2

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm

- name: Enable corepack
run: |
corepack enable
corepack prepare pnpm@10.18.2 --activate

- name: Install dependencies
run: pnpm install --frozen-lockfile
Expand Down
1 change: 1 addition & 0 deletions acceptance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@
| Source Cache and Fetch Snapshot | `source-cache-and-fetch-snapshot.feature` |
| Run Cache | `run-cache.feature` |
| Source Intake and Fetching v0.1 | `source-intake-and-fetching.feature` |
| Source Discovery v0.1 | `source-discovery.feature` |
30 changes: 30 additions & 0 deletions acceptance/source-discovery.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
Feature: Source Discovery
As a TraceMap investigation user
I want TraceMap to discover likely source candidates from my research topic
So that investigations can proceed even when I do not paste URLs

Scenario: Source discovery is disabled by default
Given source discovery provider is not configured
When a user starts an investigation
Then the existing manual URL intake behavior should be preserved
And the run should not require discovered sources to complete

Scenario: Mock source discovery returns deterministic source candidates
Given the source discovery provider is set to mock
When a user starts an investigation without URLs
Then TraceMap should request discovered source candidates for the research topic
And discovered source URLs should be normalized and deduplicated
And valid discovered sources should be passed to the answer graph provider as source candidates

Scenario: Manual URLs are prioritized over discovered URLs
Given the research topic contains manual URLs
And source discovery also returns URLs
When source candidates are built
Then manual URL candidates should appear before discovered candidates
And duplicate URLs should appear only once

Scenario: Discovery failures do not fail the investigation
Given the source discovery provider returns a failure
When a user starts an investigation
Then the investigation should continue with any manually supplied URL candidates
And the discovery failure should be recorded as ignored source information
1 change: 1 addition & 0 deletions specs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,4 @@ Each feature spec should describe:
| Source Cache and Fetch Snapshot | [source-cache-and-fetch-snapshot.md](./source-cache-and-fetch-snapshot.md) |
| Run Cache | [run-cache.md](./run-cache.md) |
| Source Intake and Fetching v0.1 | [source-intake-and-fetching.md](./source-intake-and-fetching.md) |
| Source Discovery v0.1 | [source-discovery.md](./source-discovery.md) |
88 changes: 88 additions & 0 deletions specs/source-discovery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Source Discovery v0.1

## Purpose
Enable TraceMap to discover source candidates from a research topic even when the user does not provide manual URLs, while preserving the existing Source Intake / Fetch / Cache / Provider pipeline.

## User value
- Users can start an investigation from a plain research topic and still get evidence candidates.
- Manual URLs remain first-class and prioritized when present.
- Discovery can evolve from mock to real search providers without breaking provider integration.

## Scope
- Add `SourceDiscoveryProvider` boundary for pluggable source discovery.
- Add `disabled` and `mock` discovery providers.
- Add discovery hook from research topic (`question`) in source intake.
- Merge manual URLs and discovered URLs into one normalized, deduplicated intake list.
- Reuse existing source cache / fetch pipeline (`resolveSourceCacheForUrl`).
- Pass resulting `sourceCandidates` to answer graph providers.

## Non-goals
- Production external search API integration.
- RAG / embeddings / reranking.
- Background job orchestration.
- DB schema changes or Prisma migrations.
- Large UI redesign.
- Major OpenAI answer graph schema changes.
- Full-text crawling.
- Dedicated persistence table for search result history.

## Existing implementation constraints
- Keep `AnalysisRun.question` and form field `question` unchanged.
- Preserve existing Source Intake behavior for manual URLs.
- Keep run completion flow valid even when source discovery fails or yields no candidates.
- Do not bypass existing URL safety validation.

## Provider strategy
- Environment variable switch: `TRACEMAP_SOURCE_DISCOVERY_PROVIDER=disabled|mock`.
- Default is `disabled`.
- `mock` provider must return deterministic results from the research topic.
- Provider boundary is designed to allow future providers (e.g. web search backends) without changing intake contracts.

## Source candidate flow
1. Extract manual URLs from research topic.
2. Resolve source discovery provider.
3. If provider is enabled, discover additional URLs from the same topic.
4. Merge manual + discovered URLs (manual first).
5. Normalize, safety-check, dedupe by normalized URL.
6. Resolve cache/fetch metadata with `resolveSourceCacheForUrl`.
7. Build `SourceCandidate[]` and pass to answer graph provider input.

## Deduplication rules
- Deduplicate by normalized URL.
- Manual URL candidates are evaluated before discovered candidates.
- When duplicates exist, keep first occurrence (manual precedence).
- Do not fetch or process the same normalized URL more than once.

## Error handling
- Discovery provider failures do not fail the run.
- Per-URL cache/fetch failures do not fail the run.
- Discovery and URL failures are captured in `ignoredUrls` with reasons.
- Avoid excessive logging; never log secrets.

## Security constraints
- Discovery outputs are treated as untrusted input.
- All discovered URLs must pass existing normalization and safety checks.
- Unsafe URLs are ignored and recorded, not fetched.
- Existing SSRF guard behavior remains authoritative.

## Cost constraints
- `DEFAULT_DISCOVERY_MAX_RESULTS = 5`.
- `DEFAULT_SOURCE_CANDIDATE_MAX_RESULTS = 5`.
- Keep provider source context compact (no raw full HTML).
- Keep excerpt truncation behavior unchanged in answer graph providers.

## Test requirements
- Provider resolution defaults to disabled.
- Disabled provider yields no discovered candidates.
- Mock provider is deterministic and respects maxResults.
- Intake integration preserves manual URL-only behavior.
- Discovery can produce candidates when no manual URLs exist.
- Manual URLs are prioritized over discovered URLs.
- Duplicate URLs are deduplicated.
- Discovery failure does not fail intake.
- Unsafe discovered URLs are ignored.
- Provider integration remains valid when sourceCandidates is empty or discovered.

## Acceptance references
- `acceptance/source-discovery.feature`
- `acceptance/source-intake-and-fetching.feature`
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { SourceDiscoveryProvider } from "@/server/analysis/source-discovery/source-discovery-provider";

function toTopicSlug(topic: string): string {
const normalized = topic.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9-]/g, "");
return normalized.slice(0, 48) || "general-investigation";
}

export const mockSourceDiscoveryProvider: SourceDiscoveryProvider = {
id: "mock",
async discoverSources(input) {
const slug = toTopicSlug(input.researchTopic);
const base = [
{
title: `Mission dossier: ${slug}`,
url: `https://example.com/tracemap/mock-source-1?topic=${slug}`,
snippet: `Initial mission dossier for ${slug} with baseline facts and terminology.`,
sourceKind: "report" as const,
},
{
title: `Primary evidence registry: ${slug}`,
url: `https://example.com/tracemap/mock-source-2?topic=${slug}`,
snippet: `Cross-checkable references and claims matrix for ${slug}.`,
sourceKind: "documentation" as const,
},
{
title: `Risk and unknown map briefing: ${slug}`,
url: `https://example.com/tracemap/mock-source-3?topic=${slug}`,
snippet: `Potential uncertainties and unresolved questions related to ${slug}.`,
sourceKind: "news" as const,
},
].slice(0, Math.max(0, input.maxResults));

return { kind: "success", candidates: base.map((c) => ({ ...c, discoveredBy: "mock" as const })) };
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { mockSourceDiscoveryProvider } from "@/server/analysis/source-discovery/mock-source-discovery-provider";
import type { SourceDiscoveryProvider } from "@/server/analysis/source-discovery/source-discovery-provider";

const disabledProvider: SourceDiscoveryProvider = {
id: "disabled",
async discoverSources() {
return { kind: "success", candidates: [] };
},
};

export function resolveSourceDiscoveryProvider(): SourceDiscoveryProvider {
const configured = process.env.TRACEMAP_SOURCE_DISCOVERY_PROVIDER?.trim().toLowerCase() ?? "disabled";
if (configured === "mock") return mockSourceDiscoveryProvider;
return disabledProvider;
}
21 changes: 21 additions & 0 deletions src/server/analysis/source-discovery/source-discovery-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
export type SourceDiscoveryCandidate = {
title: string;
url: string;
snippet?: string | null;
sourceKind?: "official" | "news" | "documentation" | "report" | "paper" | "unknown";
discoveredBy: "mock" | "manual_url" | "search_provider";
};

export type SourceDiscoveryInput = {
researchTopic: string;
maxResults: number;
};

export type SourceDiscoveryResult =
| { kind: "success"; candidates: SourceDiscoveryCandidate[] }
| { kind: "failure"; errorMessage: string };

export type SourceDiscoveryProvider = {
id: "disabled" | "mock" | string;
discoverSources(input: SourceDiscoveryInput): Promise<SourceDiscoveryResult>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const DEFAULT_DISCOVERY_MAX_RESULTS = 5;
export const DEFAULT_SOURCE_CANDIDATE_MAX_RESULTS = 5;
42 changes: 33 additions & 9 deletions src/server/analysis/source-intake/source-intake-service.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,53 @@
import type { SourceCandidate, SourceIntakeResult } from "@/types/source-intake";
import { resolveSourceCacheForUrl } from "@/server/analysis/source-cache-service";
import { extractUrls } from "@/server/analysis/source-intake/extract-urls";
import { resolveSourceDiscoveryProvider } from "@/server/analysis/source-discovery/resolve-source-discovery-provider";
import { DEFAULT_DISCOVERY_MAX_RESULTS, DEFAULT_SOURCE_CANDIDATE_MAX_RESULTS } from "@/server/analysis/source-discovery/source-discovery-service";

export async function buildSourceIntakeFromQuestion(question: string): Promise<SourceIntakeResult> {
const rawUrls = extractUrls(question);
const candidates: SourceCandidate[] = [];
const manualUrls = extractUrls(question);
const discoveryProvider = resolveSourceDiscoveryProvider();
const ignoredUrls: SourceIntakeResult["ignoredUrls"] = [];

let discoveredUrls: string[] = [];
if (discoveryProvider.id !== "disabled") {
const discovery = await discoveryProvider.discoverSources({
researchTopic: question,
maxResults: DEFAULT_DISCOVERY_MAX_RESULTS,
});
if (discovery.kind === "failure") {
ignoredUrls.push({ url: "[source_discovery]", reason: discovery.errorMessage });
} else {
discoveredUrls = discovery.candidates.map((candidate) => candidate.url);
}
}
Comment on lines +13 to +23
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

discoverSources の例外時に調査を継続できるようにしてください。

現在は kind: "failure" のみを処理しており、discoverSources が throw するとこの関数全体が reject して調査フローが中断します。ここも ignoredUrls に記録して継続するのが安全です。

修正例
   let discoveredUrls: string[] = [];
   if (discoveryProvider.id !== "disabled") {
-    const discovery = await discoveryProvider.discoverSources({
-      researchTopic: question,
-      maxResults: DEFAULT_DISCOVERY_MAX_RESULTS,
-    });
-    if (discovery.kind === "failure") {
-      ignoredUrls.push({ url: "[source_discovery]", reason: discovery.errorMessage });
-    } else {
-      discoveredUrls = discovery.candidates.map((candidate) => candidate.url);
-    }
+    try {
+      const discovery = await discoveryProvider.discoverSources({
+        researchTopic: question,
+        maxResults: DEFAULT_DISCOVERY_MAX_RESULTS,
+      });
+      if (discovery.kind === "failure") {
+        ignoredUrls.push({ url: "[source_discovery]", reason: discovery.errorMessage });
+      } else {
+        discoveredUrls = discovery.candidates.map((candidate) => candidate.url);
+      }
+    } catch (error) {
+      ignoredUrls.push({
+        url: "[source_discovery]",
+        reason: error instanceof Error ? error.message : String(error),
+      });
+    }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (discoveryProvider.id !== "disabled") {
const discovery = await discoveryProvider.discoverSources({
researchTopic: question,
maxResults: DEFAULT_DISCOVERY_MAX_RESULTS,
});
if (discovery.kind === "failure") {
ignoredUrls.push({ url: "[source_discovery]", reason: discovery.errorMessage });
} else {
discoveredUrls = discovery.candidates.map((candidate) => candidate.url);
}
}
if (discoveryProvider.id !== "disabled") {
try {
const discovery = await discoveryProvider.discoverSources({
researchTopic: question,
maxResults: DEFAULT_DISCOVERY_MAX_RESULTS,
});
if (discovery.kind === "failure") {
ignoredUrls.push({ url: "[source_discovery]", reason: discovery.errorMessage });
} else {
discoveredUrls = discovery.candidates.map((candidate) => candidate.url);
}
} catch (error) {
ignoredUrls.push({
url: "[source_discovery]",
reason: error instanceof Error ? error.message : String(error),
});
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/server/analysis/source-intake/source-intake-service.ts` around lines 13 -
23, discoverSources
の呼び出しが例外を投げると処理全体が中断してしまうので、discoveryProvider.discoverSources(...) を try/catch
で囲み、例外発生時は ignoredUrls に { url: "[source_discovery]", reason: error.message ||
String(error) } を push して discoveredUrls の更新をスキップし処理を継続するように修正してください(参照箇所:
discoveryProvider, discoverSources, ignoredUrls, discoveredUrls,
DEFAULT_DISCOVERY_MAX_RESULTS, question)。


const merged = [
...manualUrls.map((url) => ({ url, origin: "manual_url" as const })),
...discoveredUrls.map((url) => ({ url, origin: "discovered" as const })),
];

const candidates: SourceCandidate[] = [];
const seen = new Set<string>();

for (const rawUrl of rawUrls) {
for (const item of merged) {
let result;
try {
result = await resolveSourceCacheForUrl(rawUrl);
result = await resolveSourceCacheForUrl(item.url);
} catch (error) {
ignoredUrls.push({
url: rawUrl,
url: item.url,
reason: error instanceof Error ? error.message : String(error),
});
continue;
}
if (result.kind === "invalid") {
ignoredUrls.push({ url: rawUrl, reason: result.errorMessage });
continue;
}
if (seen.has(result.normalizedUrl)) {
ignoredUrls.push({ url: item.url, reason: result.errorMessage });
continue;
}
if (seen.has(result.normalizedUrl)) continue;
seen.add(result.normalizedUrl);

candidates.push({
normalizedUrl: result.normalizedUrl,
originalUrl: result.originalUrl,
Expand All @@ -39,7 +60,10 @@ export async function buildSourceIntakeFromQuestion(question: string): Promise<S
sourceCacheEntryId: result.sourceCacheEntryId,
sourceFetchSnapshotId: result.sourceFetchSnapshotId,
fetchErrorMessage: result.verificationStatus !== "verified" ? `verification_status:${result.verificationStatus}` : null,
origin: item.origin,
});

if (candidates.length >= DEFAULT_SOURCE_CANDIDATE_MAX_RESULTS) break;
}

return { candidates, ignoredUrls };
Expand Down
3 changes: 3 additions & 0 deletions src/types/source-intake.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export type SourceCandidateOrigin = "manual_url" | "discovered";

export type SourceCandidate = {
normalizedUrl: string;
originalUrl: string;
Expand All @@ -10,6 +12,7 @@ export type SourceCandidate = {
sourceCacheEntryId?: string | null;
sourceFetchSnapshotId?: string | null;
fetchErrorMessage?: string | null;
origin?: SourceCandidateOrigin;
};

export type SourceIntakeResult = {
Expand Down
25 changes: 25 additions & 0 deletions tests/source-discovery-provider.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";

import { mockSourceDiscoveryProvider } from "@/server/analysis/source-discovery/mock-source-discovery-provider";
import { resolveSourceDiscoveryProvider } from "@/server/analysis/source-discovery/resolve-source-discovery-provider";

describe("source discovery provider", () => {
it("defaults to disabled and returns empty candidates", async () => {
delete process.env.TRACEMAP_SOURCE_DISCOVERY_PROVIDER;
const provider = resolveSourceDiscoveryProvider();
expect(provider.id).toBe("disabled");
await expect(provider.discoverSources({ researchTopic: "x", maxResults: 5 })).resolves.toEqual({ kind: "success", candidates: [] });
});

it("mock provider is deterministic", async () => {
const a = await mockSourceDiscoveryProvider.discoverSources({ researchTopic: "Acme Revenue", maxResults: 3 });
const b = await mockSourceDiscoveryProvider.discoverSources({ researchTopic: "Acme Revenue", maxResults: 3 });
expect(a).toEqual(b);
});

it("respects maxResults", async () => {
const result = await mockSourceDiscoveryProvider.discoverSources({ researchTopic: "Acme", maxResults: 2 });
expect(result.kind).toBe("success");
if (result.kind === "success") expect(result.candidates).toHaveLength(2);
});
});
Loading
Loading