spb/social-runtime-crawler
Public
TypeScript 91.8%
HTML 3.2%
JavaScript 3%
SQL 1.4%
CSS 0.7%
1import { describe, expect, it } from "vitest";2import type { ObservedEntity, PageState, SemanticAction } from "@src/shared";3import { WorldModel } from "./WorldModel.ts";4import { LoopDetector } from "./LoopDetector.ts";5import { scoreActions, relevanceOf } from "./InformationGain.ts";6import { HeuristicPlanner } from "./planner.ts";78const ent = (ref: string, name: string, type: ObservedEntity["type"] = "video"): ObservedEntity => ({ ref, type, platform: "youtube", platform_id: ref, url: `https://youtube.com/watch?v=${ref}`, name, fields: {}, provenance: [{ surface: "dom", confidence: 0.9 }, { surface: "network", confidence: 0.95 }], fingerprint: `youtube:${type}:${ref}` });910const state = (entities: ObservedEntity[], actions: SemanticAction[]): PageState => ({ url: "https://youtube.com/results?search_query=ai+quebec", title: "results", platform: "youtube", classification: { page_type: "SEARCH_RESULTS", confidence: 0.95, signals: [] }, entities, actions, media: [], summary_text: "AI Québec results", fingerprint: "fp", captured_at: new Date().toISOString() });1112describe("information gain + planner", () => {13 it("prefers relevant, unvisited, multi-surface entities and penalizes visited ones", async () => {14 const world = new WorldModel();15 const e1 = ent("v1", "Intelligence artificielle au Québec : table ronde");16 const e2 = ent("v2", "Recette de tarte aux pommes");17 world.observe([e1, e2], 0);18 world.markVisited(e2.fingerprint, e2.url);19 const actions: SemanticAction[] = [20 { id: "A1", type: "OPEN_VIDEO", target_ref: "v1", target_url: e1.url, label: "open v1", cost: 2 },21 { id: "A2", type: "OPEN_VIDEO", target_ref: "v2", target_url: e2.url, label: "open v2", cost: 2 },22 { id: "A3", type: "SCROLL_DOWN", label: "scroll", cost: 1 },23 { id: "A4", type: "END_SESSION", label: "end", cost: 0.5 },24 ];25 const st = state([e1, e2], actions);26 const scores = scoreActions({ goal: "intelligence artificielle Québec", mode: "research", world, state: st, recentActionTypes: [], recentUrls: [], stepsWithoutNewEntities: 0 }, actions);27 expect(scores[0]!.action_id).toBe("A1");28 const visited = scores.find((s) => s.action_id === "A2")!;29 expect(visited.penalties).toContain("already_seen");30 expect(scores.at(-1)!.action_id).toBe("A4");31 const d = await new HeuristicPlanner().plan({ goal: "intelligence artificielle Québec", mode: "research", world, state: st, recentActionTypes: [], recentUrls: [], stepsWithoutNewEntities: 0 }, 1);32 expect(d.chosen_action.id).toBe("A1");33 expect(d.reason.length).toBeGreaterThan(0);34 });3536 it("relevance follows lexical overlap with the goal", () => {37 expect(relevanceOf("AI Quebec panel", "AI Quebec")).toBeGreaterThan(relevanceOf("Apple pie recipe", "AI Quebec"));38 });3940 it("observe mode suppresses navigation", () => {41 const world = new WorldModel();42 const e1 = ent("v1", "AI");43 const actions: SemanticAction[] = [{ id: "A1", type: "OPEN_VIDEO", target_ref: "v1", target_url: e1.url, label: "open", cost: 2 }, { id: "A2", type: "SCROLL_DOWN", label: "scroll", cost: 1 }];44 const scores = scoreActions({ goal: "AI", mode: "observe", world, state: state([e1], actions), recentActionTypes: [], recentUrls: [], stepsWithoutNewEntities: 0 }, actions);45 expect(scores[0]!.action_id).toBe("A2");46 });47});4849describe("LoopDetector", () => {50 it("detects ABAB", () => {51 const l = new LoopDetector();52 for (const f of ["a", "b", "a", "b"]) l.record(f, "OPEN_VIDEO:x");53 expect(l.detect().loop).toBe(true);54 });55 it("tolerates repeated scrolling", () => {56 const l = new LoopDetector();57 for (let i = 0; i < 6; i++) l.record(`p${i}`, "SCROLL_DOWN:");58 expect(l.detect().loop).toBe(false);59 });60});6162describe("WorldModel novelty", () => {63 it("scores near-duplicates as low novelty", () => {64 const w = new WorldModel();65 w.observe([ent("a", "Intelligence artificielle Québec conférence 2026")], 0);66 expect(w.novelty(ent("b", "Intelligence artificielle Québec conférence 2026 partie 2"))).toBeLessThan(0.5);67 expect(w.novelty(ent("c", "Tarte aux pommes maison"))).toBeGreaterThan(0.9);68 });69});70