2026-08-14 12:57:06 +02:00
import { createServer } from 'node:net'
2026-08-22 16:02:10 +02:00
import { createServer as createHttpServer , type Server as HttpServer } from 'node:http'
2026-08-14 14:09:24 +02:00
import { randomUUID } from 'node:crypto'
2026-08-22 16:02:10 +02:00
import { readFileSync } from 'node:fs'
2026-08-14 12:57:06 +02:00
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import pg from 'pg'
2026-08-15 09:29:08 +02:00
import jwt from 'jsonwebtoken'
2026-08-14 12:57:06 +02:00
import { afterAll , beforeAll , describe , expect , it } from 'vitest'
2026-08-17 09:24:53 +02:00
import type { CaseState , DocumentExhibit , EventExhibit , FolderExhibit , NoteExhibit , PartyExhibit , TimelineView } from '../src/types.js'
2026-08-14 12:57:06 +02:00
import { runMigrations } from './migrations.js'
const { Client } = pg
const baseDatabaseUrl = process . env . TEST_DATABASE_URL
const suite = baseDatabaseUrl ? describe : describe.skip
const databaseName = `osint_api_test_ ${ process . pid } _ ${ Date . now () } `
let adminClient : InstanceType < typeof Client >
let appServer : Awaited < typeof import ( './index.js' ) >[ 'server' ]
let appPool : Awaited < typeof import ( './index.js' ) >[ 'pool' ]
let baseUrl = ''
2026-08-15 09:29:08 +02:00
let adminAuthorization = ''
2026-08-22 16:02:10 +02:00
let judgeServer : HttpServer
let judgeVerdict = { subject : 'target' , supports_claim :true , evidence_excerpt : 'Ada Example patented a pocket telescope' , confidence :.96 }
let judgeHttpStatus = 200
2026-08-15 09:29:08 +02:00
function adminFetch ( url : string , init : RequestInit = {}) {
const headers = new Headers ( init . headers )
headers . set ( 'authorization' , adminAuthorization )
return fetch ( url , { ... init , headers })
}
2026-08-14 12:57:06 +02:00
async function availablePort() {
return new Promise < number >(( resolve , reject ) => {
const probe = createServer ()
probe . once ( 'error' , reject )
probe . listen ( 0 , '127.0.0.1' , () => {
const address = probe . address ()
const port = typeof address === 'object' && address ? address.port : 0
probe . close ( error => error ? reject ( error ) : resolve ( port ))
})
})
}
2026-08-17 09:24:53 +02:00
const placed = ( x : number , y : number , width : number , height : number , zIndex = 1 ) => ({ x , y , width , height , rotation : 0 , zIndex , hidden : false })
suite ( 'normalized level persistence API' , () => {
2026-08-14 12:57:06 +02:00
beforeAll ( async () => {
const adminUrl = new URL ( baseDatabaseUrl ! )
adminUrl . pathname = '/postgres'
adminClient = new Client ({ connectionString : adminUrl.toString () })
await adminClient . connect ()
await adminClient . query ( `CREATE DATABASE " ${ databaseName } "` )
const testUrl = new URL ( baseDatabaseUrl ! )
testUrl . pathname = `/ ${ databaseName } `
const databaseUrl = testUrl . toString ()
2026-08-17 09:24:53 +02:00
await runMigrations ( databaseUrl , path . resolve ( path . dirname ( fileURLToPath ( import . meta . url )), '..' , 'migrations' ), () => undefined )
2026-08-14 12:57:06 +02:00
const port = await availablePort ()
process . env . DATABASE_URL = databaseUrl
process . env . LEVEL_EDITING_ENABLED = 'true'
2026-08-15 09:29:08 +02:00
process . env . JWT_SECRET = 'osint-integration-jwt-secret'
2026-08-17 09:24:53 +02:00
process . env . ASSET_STORAGE_DRIVER = 'memory'
2026-08-14 12:57:06 +02:00
process . env . PORT = String ( port )
2026-08-22 16:02:10 +02:00
judgeServer = createHttpServer (( _req , res ) => {
res . statusCode = judgeHttpStatus
res . setHeader ( 'content-type' , 'application/json' )
res . end ( JSON . stringify ({ content : [{ type : 'tool_use' , name : 'record_evidence_verdict' , input :judgeVerdict }] }))
})
await new Promise < void >(( resolve , reject ) => judgeServer . listen ( 0 , '127.0.0.1' , resolve ). once ( 'error' , reject ))
const judgeAddress = judgeServer . address ()
process . env . EVIDENCE_JUDGE_PROVIDER = 'anthropic'
process . env . EVIDENCE_JUDGE_MODEL = 'integration-haiku'
process . env . ANTHROPIC_API_KEY = 'integration-key'
process . env . ANTHROPIC_API_URL = `http://127.0.0.1: ${ typeof judgeAddress === 'object' && judgeAddress ? judgeAddress.port : 0 } `
2026-08-14 12:57:06 +02:00
const serverModule = await import ( './index.js' )
appServer = serverModule . server
appPool = serverModule . pool
baseUrl = `http://127.0.0.1: ${ port } `
2026-08-15 09:29:08 +02:00
adminAuthorization = `Bearer ${ jwt . sign ({ sub : 'integration-admin' , role : 'admin' } , process.env.JWT_SECRET)}`
2026-08-14 12:57:06 +02:00
})
afterAll ( async () => {
if ( appServer ) await new Promise < void >(( resolve , reject ) => appServer . close ( error => error ? reject ( error ) : resolve ()))
2026-08-22 16:02:10 +02:00
if ( judgeServer ) await new Promise < void >(( resolve , reject ) => judgeServer . close ( error => error ? reject ( error ) : resolve ()))
2026-08-14 12:57:06 +02:00
if ( appPool ) await appPool . end ()
if ( ! adminClient ) return
await adminClient . query ( `DROP DATABASE IF EXISTS " ${ databaseName } "` )
await adminClient . end ()
})
2026-08-17 09:24:53 +02:00
it ( 'round-trips exhibits, relations, board views, private objects, and template clones' , async () => {
2026-08-22 17:02:30 +02:00
expect ( await ( await fetch ( ` ${ baseUrl } /api/session` )). json ()). toEqual ({ authenticated : false , isAdmin : false , playerName : 'Player' })
2026-08-15 09:29:08 +02:00
const createResponse = await adminFetch ( ` ${ baseUrl } /api/levels` , {
2026-08-17 09:24:53 +02:00
method : 'POST' , headers : { 'content-type' : 'application/json' }, body : JSON.stringify ({ id : 'api-smoke-level' , title : 'API Smoke Level' }),
2026-08-14 12:57:06 +02:00
})
expect ( createResponse . status ). toBe ( 201 )
const state = await createResponse . json () as CaseState
2026-08-17 09:24:53 +02:00
const timeline = state . views . find (( view ) : view is TimelineView => view . type === 'timeline' ) !
timeline . rangeMode = 'fixed'
timeline . range = { start : '2021-04-01' , end : '2021-04-30' }
2026-08-14 12:57:06 +02:00
state . viewport = { x : 91 , y : - 42 , zoom : 0.85 }
2026-08-17 09:24:53 +02:00
const document : DocumentExhibit = { id : randomUUID (), type : 'document' , title : 'Evidence' , publishedAt : '2021-04-17T12:00:00Z' , body : [ 'Extracted body' ], regions : [{ id : 'stamp' , label : 'Date stamp' , excerpt : '17 April 2021' , date : '2021-04-17T09:00:00Z' }], fileType : 'image' , metadata : {}, ... placed ( 1051 , 417 , 174 , 145 , 2 ) }
2026-08-22 14:53:23 +02:00
const gatedDocument : DocumentExhibit = { id : randomUUID (), type : 'document' , title : 'Later tip' , body : [], regions : [], fileType : 'image' , metadata : {}, requiredFlags : [ 'tip.received' ], ... placed ( 1260 , 417 , 174 , 145 , 3 ) }
2026-08-17 09:24:53 +02:00
const folder : FolderExhibit = { id : randomUUID (), type : 'folder' , title : 'Folder' , content : 'Evidence folder' , isOpen : true , ... placed ( 685 , 417 , 260 , 166 ) }
const note : NoteExhibit = { id : randomUUID (), type : 'note' , title : 'Extract' , content : 'Date matters' , ... placed ( 420 , 300 , 108 , 154 ) }
const event : EventExhibit = { id : randomUUID (), type : 'event' , title : 'The meeting occurred' , content : 'The evidence places the meeting on 18 April.' , eventDate : '2021-04-18T14:30:00Z' , ... placed ( 520 , 610 , 270 , 174 ) }
const party : PartyExhibit = { id : randomUUID (), type : 'party' , partyKind : 'person' , title : 'Ada Lovelace' , content : 'Named as correspondent.' , aliases : [ 'A. A. L.' ], ... placed ( 720 , 250 , 280 , 190 ) }
2026-08-22 14:53:23 +02:00
state . exhibits = [ document , gatedDocument , folder , note , event , party ]
2026-08-17 09:24:53 +02:00
state . relations = [
{ id : randomUUID (), fromExhibitId : folder.id , toExhibitId : document.id , type : 'contains' , sortOrder : 0 },
{ id : randomUUID (), fromExhibitId : note.id , toExhibitId : document.id , type : 'source' , sourceRegionId : 'stamp' , sortOrder : 0 },
{ id : randomUUID (), fromExhibitId : event.id , toExhibitId : document.id , type : 'supports' , sortOrder : 0 },
{ id : randomUUID (), fromExhibitId : event.id , toExhibitId : note.id , type : 'supports' , sortOrder : 1 },
{ id : randomUUID (), fromExhibitId : party.id , toExhibitId : document.id , type : 'concerns' , sortOrder : 0 },
2026-08-14 14:17:54 +02:00
]
2026-08-17 09:24:53 +02:00
state . connections = [{ id : randomUUID (), fromExhibitId : folder.id , toExhibitId : document.id , label : 'Primary source' , tightness : 85 , tagStyle : 'compact' , tagPosition : 72 , tagOffset : - 12 }]
state . brief = { body : 'Identify Ada Lovelace.' , concepts : [{ id : randomUUID (), label : 'Ada Lovelace' , context : 'Named in evidence.' , expectedPartyKind : 'person' , resolvedPartyExhibitId : party.id }] }
2026-08-14 12:57:06 +02:00
2026-08-17 09:24:53 +02:00
const save = await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } ?edit=1` , { method : 'PUT' , headers : { 'content-type' : 'application/json' }, body : JSON.stringify ( state ) })
expect ( await save . json ()). toEqual ({ ok : true , mode : 'author' })
2026-08-15 09:29:08 +02:00
const loaded = await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } ?edit=1` )). json () as CaseState
2026-08-17 09:24:53 +02:00
expect ( loaded . views [ 0 ]). toMatchObject ({ type : 'timeline' , rangeMode : 'fixed' , range : timeline.range })
expect ( loaded . exhibits . find ( item => item . id === folder . id )). toMatchObject ({ x : 685 , y : 417 , isOpen : true })
expect ( loaded . relations ). toEqual ( expect . arrayContaining ([ expect . objectContaining ({ type : 'supports' , fromExhibitId : event.id , toExhibitId : note.id })]))
expect ( loaded . connections [ 0 ]). toMatchObject ({ fromExhibitId : folder.id , toExhibitId : document.id , label : 'Primary source' })
2026-08-22 14:53:23 +02:00
expect ( loaded . exhibits . find ( item => item . id === gatedDocument . id )). toMatchObject ({ requiredFlags : [ 'tip.received' ] })
const beforeFlag = await ( await fetch ( ` ${ baseUrl } /api/levels/ ${ state . id } ` )). json () as CaseState
expect ( beforeFlag . exhibits . map ( item => item . id )). not . toContain ( gatedDocument . id )
expect ( beforeFlag . newlyVisibleDocumentIds ). toContain ( document . id )
expect ( await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /flags` )). json ()). toEqual ([
{ key : 'tip.received' , gatedDocumentCount : 1 },
])
expect (( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /flags/tip.received` , { method : 'PUT' })). status ). toBe ( 200 )
const afterFlag = await ( await fetch ( ` ${ baseUrl } /api/levels/ ${ state . id } ` )). json () as CaseState
expect ( afterFlag . exhibits . map ( item => item . id )). toContain ( gatedDocument . id )
expect ( afterFlag . newlyVisibleDocumentIds ). toContain ( gatedDocument . id )
expect (( await fetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /reveals/seen` , { method : 'POST' , headers : { 'content-type' : 'application/json' }, body : JSON.stringify ({ documentIds : afterFlag.newlyVisibleDocumentIds }) })). status ). toBe ( 200 )
expect (( await ( await fetch ( ` ${ baseUrl } /api/levels/ ${ state . id } ` )). json () as CaseState ). newlyVisibleDocumentIds ). toEqual ([])
expect (( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /flags/tip.received` , { method : 'DELETE' })). status ). toBe ( 200 )
const matchRuleResponse = await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /evidence-match-rules` , {
method : 'POST' , headers : { 'content-type' : 'application/json' }, body : JSON.stringify ({
2026-08-22 16:02:10 +02:00
name : 'Smoke source passage' , sourceLabel : 'Archive smoke test' , sourceUri : 'https://example.test/archive/smoke' ,
flagKey : 'tip.received' , minimumAnchorMatches : 1 ,
2026-08-22 14:53:23 +02:00
anchors : [{ phrase : 'OSINT smoke evidence from the archive' , minimumSimilarity : 0.72 }],
}),
})
expect ( matchRuleResponse . status ). toBe ( 201 )
2026-08-22 16:02:10 +02:00
expect ( await matchRuleResponse . json ()). toMatchObject ({ name : 'Smoke source passage' , sourceLabel : 'Archive smoke test' ,
sourceUri : 'https://example.test/archive/smoke' , flagKey : 'tip.received' , anchors : [{ phrase : 'OSINT smoke evidence from the archive' }] })
2026-08-22 14:53:23 +02:00
expect ( await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /evidence-match-rules` )). json ()). toHaveLength ( 1 )
2026-08-22 16:02:10 +02:00
const goalResponse = await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /goals` , {
method : 'POST' , headers : { 'content-type' : 'application/json' }, body : JSON.stringify ({
key : 'smoke.prove-source' , title : 'Prove the source' , instructions : 'Paste a matching archival source.' ,
completionMessage : 'Source verified.' , requiredFlags : [ 'tip.received' ],
}),
})
expect ( goalResponse . status ). toBe ( 201 )
expect ( await goalResponse . json ()). toMatchObject ({ key : 'smoke.prove-source' , status : 'pending' , requiredFlags : [ 'tip.received' ] })
const playGoalBefore = ( await ( await fetch ( ` ${ baseUrl } /api/levels/ ${ state . id } ` )). json () as CaseState ). goals [ 0 ]
expect ( playGoalBefore ). toMatchObject ({ key : 'smoke.prove-source' , status : 'pending' , newlyCompleted : false })
expect ( playGoalBefore ). not . toHaveProperty ( 'id' )
expect ( playGoalBefore ). not . toHaveProperty ( 'requiredFlags' )
const semanticGoalResponse = await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /goals` , {
method : 'POST' , headers : { 'content-type' : 'application/json' }, body :JSON.stringify ({ key : 'smoke.semantic-proof' , title : 'Prove the semantic claim' ,
instructions : 'Upload another credible source.' , completionMessage : 'Claim verified.' , requiredFlags : [ 'semantic.proved' ] }),
})
expect ( semanticGoalResponse . status ). toBe ( 201 )
const semanticGoal = await semanticGoalResponse . json () as { id :string }
const semanticRuleResponse = await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /evidence-semantic-rules` , {
method : 'POST' , headers : { 'content-type' : 'application/json' }, body :JSON.stringify ({ goalId :semanticGoal.id , name : 'Ada inventor claim' ,
targetSubject : 'Ada Example' , relatedSubject : 'Ada Example Senior' , assertion : 'Ada Example was an inventor' , successFlagKey : 'semantic.proved' ,
relatedFlagKey : 'semantic.father' , minimumConfidence :.85 }),
})
expect ( semanticRuleResponse . status ). toBe ( 201 )
2026-08-14 13:43:37 +02:00
const upload = new FormData ()
2026-08-22 14:53:23 +02:00
upload . append ( 'file' , new Blob ([ 'OSINT smoke evidence from the archlve' ], { type : 'text/plain' }), 'smoke-evidence.txt' )
upload . append ( 'x' , '812' )
upload . append ( 'y' , '438' )
const uploadResponse = await fetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /documents` , { method : 'POST' , body : upload })
2026-08-14 13:43:37 +02:00
expect ( uploadResponse . status ). toBe ( 201 )
2026-08-22 16:02:10 +02:00
const uploaded = await uploadResponse . json () as DocumentExhibit & { analysis : { extractionStatus : string ; matchedFlags : string []; awardedFlags : string []; goals : CaseState [ 'goals' ] } }
2026-08-22 14:53:23 +02:00
expect ( uploaded ). toMatchObject ({ type : 'document' , fileName : 'smoke-evidence.txt' , fileType : 'text' , x : 812 , y : 438 ,
2026-08-22 16:02:10 +02:00
body : [ 'OSINT smoke evidence from the archlve' ], analysis : { extractionStatus : 'succeeded' , matchedFlags : [ 'tip.received' ], awardedFlags : [ 'tip.received' ],
goals : expect.arrayContaining ([ expect . objectContaining ({ key : 'smoke.prove-source' , status : 'complete' , newlyCompleted : true })]) } })
2026-08-22 14:53:23 +02:00
expect ( await ( await fetch ( ` ${ baseUrl } /api/assets/ ${ uploaded . assetId } ` )). text ()). toBe ( 'OSINT smoke evidence from the archlve' )
2026-08-17 09:24:53 +02:00
const assetRow = await appPool . query < { storage_provider : string ; content : Buffer | null ; object_key : string | null } > ( 'SELECT storage_provider,content,object_key FROM osint.assets WHERE id=$1' , [ uploaded . assetId ])
expect ( assetRow . rows [ 0 ]). toMatchObject ({ storage_provider : 's3' , content : null , object_key : expect.stringMatching ( /^assets\// ) })
2026-08-22 14:53:23 +02:00
const automaticallyRevealed = await ( await fetch ( ` ${ baseUrl } /api/levels/ ${ state . id } ` )). json () as CaseState
expect ( automaticallyRevealed . exhibits . map ( item => item . id )). toContain ( gatedDocument . id )
2026-08-22 16:02:10 +02:00
expect ( automaticallyRevealed . goals ). toEqual ( expect . arrayContaining ([
expect . objectContaining ({ key : 'smoke.prove-source' , status : 'complete' , newlyCompleted : false }),
]))
2026-08-22 14:53:23 +02:00
const evaluationRows = await appPool . query < { matched : boolean ; matched_anchor_count : number } > (
'SELECT matched,matched_anchor_count FROM osint.evidence_match_evaluations WHERE document_exhibit_id=$1' , [ uploaded . id ])
expect ( evaluationRows . rows ). toEqual ([{ matched : true , matched_anchor_count : 1 }])
2026-08-22 16:02:10 +02:00
const semanticUpload = new FormData ()
semanticUpload . append ( 'file' , new Blob ([ 'Archive entry: Ada Example patented a pocket telescope in 1948.' ], { type : 'text/plain' }), 'semantic-evidence.txt' )
const semanticDocumentResponse = await fetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /documents` , { method : 'POST' , body :semanticUpload })
expect ( semanticDocumentResponse . status ). toBe ( 201 )
const semanticDocument = await semanticDocumentResponse . json () as DocumentExhibit & { analysis : { goals :CaseState [ 'goals' ] } }
expect ( semanticDocument . analysis . goals . find ( goal => goal . key === 'smoke.semantic-proof' ) ? . status ). toBe ( 'pending' )
const judgedResponse = await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /documents/ ${ semanticDocument . id } /judge` , { method : 'POST' })
expect ( judgedResponse . status ). toBe ( 200 )
expect ( await judgedResponse . json ()). toMatchObject ({ status : 'succeeded' , subject : 'target' , supportsClaim :true , confidence :.96 ,
awardedFlags : [ 'semantic.proved' ], goals :expect.arrayContaining ([ expect . objectContaining ({ key : 'smoke.semantic-proof' , status : 'complete' , newlyCompleted :true })]) })
const semanticProvenance = await appPool . query < { flag_key :string ; awarded_by_semantic_evaluation_id :string | null } > (
'SELECT flag_key,awarded_by_semantic_evaluation_id FROM osint.level_flags WHERE level_id=(SELECT id FROM osint.levels WHERE slug=$1) AND flag_key=$2' ,
[ state . id , 'semantic.proved' ])
expect ( semanticProvenance . rows [ 0 ]). toMatchObject ({ flag_key : 'semantic.proved' , awarded_by_semantic_evaluation_id :expect.any ( String ) })
expect ( await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /documents/ ${ semanticDocument . id } /judge` , { method : 'POST' })). json ())
. toMatchObject ({ status : 'not_needed' , awardedFlags : [] })
2026-08-22 14:53:23 +02:00
const screenshot = new FormData ()
screenshot . append ( 'file' , new Blob ([ Buffer . from ( '89504e470d0a1a0a' , 'hex' )], { type : 'image/png' }), 'Screenshot 2026-08-22.png' )
const screenshotResponse = await fetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /documents` , { method : 'POST' , body : screenshot })
expect ( screenshotResponse . status ). toBe ( 201 )
expect ( await screenshotResponse . json ()). toMatchObject ({ type : 'document' , fileType : 'image' , fileName : 'Screenshot 2026-08-22.png' })
2026-08-14 13:43:37 +02:00
2026-08-17 09:24:53 +02:00
const templateResponse = await adminFetch ( ` ${ baseUrl } /api/levels/ ${ state . id } /templates?edit=1` , { method : 'POST' , headers : { 'content-type' : 'application/json' }, body : JSON.stringify ({ name : 'Smoke Template' }) })
2026-08-14 14:17:54 +02:00
expect ( templateResponse . status ). toBe ( 201 )
2026-08-17 09:24:53 +02:00
const cloneResponse = await adminFetch ( ` ${ baseUrl } /api/templates/smoke-template/levels?edit=1` , { method : 'POST' , headers : { 'content-type' : 'application/json' }, body : JSON.stringify ({ id : 'smoke-template-copy' , title : 'Playable copy' }) })
2026-08-14 14:17:54 +02:00
expect ( cloneResponse . status ). toBe ( 201 )
const clone = await cloneResponse . json () as CaseState
2026-08-17 09:24:53 +02:00
expect ( clone . views [ 0 ]). toMatchObject ({ type : 'timeline' , rangeMode : 'fixed' , range : timeline.range })
expect ( clone . exhibits . map ( item => item . id )). not . toContain ( folder . id )
expect ( clone . exhibits . find ( item => item . type === 'folder' )). toMatchObject ({ x : 685 , y : 417 })
expect ( clone . relations . filter ( relation => relation . type === 'supports' )). toHaveLength ( 2 )
expect ( clone . connections [ 0 ]). toMatchObject ({ label : 'Primary source' , tightness : 85 })
expect ( clone . brief . concepts [ 0 ]. resolvedPartyExhibitId ). not . toBe ( party . id )
2026-08-22 14:53:23 +02:00
const authoredClone = await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ clone . id } ?edit=1` )). json () as CaseState
expect ( authoredClone . exhibits . find ( item => item . type === 'document' && item . title === 'Later tip' )). toMatchObject ({ requiredFlags : [ 'tip.received' ] })
expect ( await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ clone . id } /evidence-match-rules` )). json ()). toEqual ([
2026-08-22 16:02:10 +02:00
expect . objectContaining ({ name : 'Smoke source passage' , sourceLabel : 'Archive smoke test' , sourceUri : 'https://example.test/archive/smoke' ,
flagKey : 'tip.received' , anchors : [ expect . objectContaining ({ phrase : 'OSINT smoke evidence from the archive' })] }),
])
expect ( await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ clone . id } /goals` )). json ()). toEqual ( expect . arrayContaining ([
expect . objectContaining ({ key : 'smoke.prove-source' , status : 'pending' , requiredFlags : [ 'tip.received' ] }),
expect . objectContaining ({ key : 'smoke.semantic-proof' , status : 'pending' , requiredFlags : [ 'semantic.proved' ] }),
]))
expect ( await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ clone . id } /evidence-semantic-rules` )). json ()). toEqual ([
expect . objectContaining ({ name : 'Ada inventor claim' , goalKey : 'smoke.semantic-proof' , successFlagKey : 'semantic.proved' }),
2026-08-22 14:53:23 +02:00
])
2026-08-14 12:57:06 +02:00
})
2026-08-22 16:02:10 +02:00
it ( 'imports the data-defined Scene 7 template with its private recognition rules' , async () => {
const { importMysteryTemplate } = await import ( '../scripts/importMysteryTemplate.js' )
const manifestPath = path . resolve ( path . dirname ( fileURLToPath ( import . meta . url )), '..' , 'mysteries' , 'barricelli-scene-7' , 'mystery.json' )
const imported = await importMysteryTemplate ( manifestPath , baseUrl , adminAuthorization . replace ( /^Bearer / , '' ))
2026-08-22 17:02:30 +02:00
expect ( imported . playableLevel ). toMatchObject ({ title : 'The Barricelli Files' , exhibits : [ expect . objectContaining ({ type : 'claim' , statement : 'Nils Aall Barricelli was an inventor.' })], report :expect.objectContaining ({
title : 'Barricelli Inventor Finding' , requiredForCompletion :true , status : 'draft' ,
}), goals : [ expect . objectContaining ({
2026-08-22 16:02:10 +02:00
key : 'barricelli.inventor-proof' , status : 'pending' , newlyCompleted :false ,
})] })
2026-08-22 17:16:10 +02:00
const sceneSevenRules = await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ imported . playableLevel . id } /evidence-match-rules` )). json ()
expect ( sceneSevenRules ). toHaveLength ( 2 )
expect ( sceneSevenRules ). toEqual ( expect . arrayContaining ([
2026-08-22 16:02:10 +02:00
expect . objectContaining ({ name : 'Google Patents · GB695913A' , sourceUri : 'https://patents.google.com/patent/GB695913A/en' ,
flagKey : 'scene7.nils_inventor_proved' , minimumAnchorMatches :2 }),
2026-08-22 17:16:10 +02:00
expect . objectContaining ({ name : 'Nasjonalbiblioteket · Patent 93585' ,
sourceUri : 'https://www.nb.no/items/921545b51acef0b05054cfc1f4666975?page=9&searchText=baricelli' ,
flagKey : 'scene7.nils_inventor_proved' , minimumAnchorMatches :2 }),
]))
2026-08-22 16:02:10 +02:00
expect ( await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ imported . playableLevel . id } /evidence-semantic-rules` )). json ()). toEqual ([
expect . objectContaining ({ goalKey : 'barricelli.inventor-proof' , targetSubject : 'Nils Aall Barricelli' ,
relatedFlagKey : 'scene7.father_inventor_discovered' }),
])
const fixtureDir = path . join ( path . dirname ( manifestPath ), 'fixtures' )
const fatherUpload = new FormData ()
fatherUpload . append ( 'file' , new Blob ([ readFileSync ( path . join ( fixtureDir , 'father-only-negative-ocr.txt' ))], { type : 'text/plain' }), 'father-source.txt' )
const fatherDocument = await ( await fetch ( ` ${ baseUrl } /api/levels/ ${ imported . playableLevel . id } /documents` , { method : 'POST' , body :fatherUpload })). json () as DocumentExhibit & { analysis : { goals :CaseState [ 'goals' ] } }
expect ( fatherDocument . analysis . goals [ 0 ]. status ). toBe ( 'pending' )
judgeVerdict = { subject : 'related' , supports_claim :true , evidence_excerpt : 'den italienske maler og opfinder Barricelli' , confidence :.96 }
judgeHttpStatus = 429
expect ( await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ imported . playableLevel . id } /documents/ ${ fatherDocument . id } /judge` , { method : 'POST' })). json ())
. toMatchObject ({ status : 'failed' , retryable :true , awardedFlags : [], goals : [ expect . objectContaining ({ status : 'pending' })] })
judgeHttpStatus = 200
const fatherJudgment = await ( await adminFetch ( ` ${ baseUrl } /api/levels/ ${ imported . playableLevel . id } /documents/ ${ fatherDocument . id } /judge` , { method : 'POST' })). json ()
expect ( fatherJudgment ). toMatchObject ({ status : 'succeeded' , subject : 'related' , awardedFlags : [ 'scene7.father_inventor_discovered' ],
goals : [ expect . objectContaining ({ key : 'barricelli.inventor-proof' , status : 'pending' })] })
const targetUpload = new FormData ()
targetUpload . append ( 'file' , new Blob ([ readFileSync ( path . join ( fixtureDir , 'google-patents-target-ocr.txt' ))], { type : 'text/plain' }), 'google-patents-source.txt' )
const targetResponse = await fetch ( ` ${ baseUrl } /api/levels/ ${ imported . playableLevel . id } /documents` , { method : 'POST' , body :targetUpload })
expect ( targetResponse . status ). toBe ( 201 )
2026-08-22 17:02:30 +02:00
const targetDocument = await targetResponse . json () as DocumentExhibit & { analysis : { awardedFlags :string []; goals :CaseState [ 'goals' ] } }
expect ( targetDocument ). toMatchObject ({ displayNumber :expect.any ( Number ), analysis : { awardedFlags : [ 'scene7.nils_inventor_proved' ],
2026-08-22 16:02:10 +02:00
goals : [ expect . objectContaining ({ key : 'barricelli.inventor-proof' , status : 'complete' , newlyCompleted :true })] } })
2026-08-22 17:02:30 +02:00
const reportState = await ( await fetch ( ` ${ baseUrl } /api/levels/ ${ imported . playableLevel . id } ` )). json () as CaseState
const claim = reportState . exhibits . find ( exhibit => exhibit . type === 'claim' ) !
const connectionId = randomUUID ()
reportState . connections . push ({ id :connectionId , fromExhibitId :claim.id , toExhibitId :targetDocument.id , label : 'Proof that…' , tightness :65 , tagStyle : 'luggage' , tagPosition :50 , tagOffset :0 })
expect (( await fetch ( ` ${ baseUrl } /api/levels/ ${ reportState . id } ` ,{ method : 'PUT' , headers : { 'content-type' : 'application/json' }, body :JSON.stringify ( reportState ) })). status ). toBe ( 200 )
const incomplete = await fetch ( ` ${ baseUrl } /api/levels/ ${ reportState . id } /report/submissions` ,{ method : 'POST' , headers : { 'content-type' : 'application/json' }, body :JSON.stringify ({
investigatorName : 'Test Player' , evidence : [{ connectionId , documentExhibitId :targetDocument.id , relationText : 'Proof that…' }],
}) })
expect ( incomplete . status ). toBe ( 201 )
expect ( await incomplete . json ()). toMatchObject ({ status : 'evidence_accepted_report_incomplete' , issues :expect.arrayContaining ([ 'unfinished_relation' , 'missing_date' , 'missing_source' ]),
feedback : "The evidence is good enough, but the report itself won't hold up in court. Add the date, cite the source, and provide the link if you can. Then we can accept it." })
const accepted = await fetch ( ` ${ baseUrl } /api/levels/ ${ reportState . id } /report/submissions` ,{ method : 'POST' , headers : { 'content-type' : 'application/json' }, body :JSON.stringify ({
investigatorName : 'Test Player' , evidence : [{ connectionId , documentExhibitId :targetDocument.id , relationText : 'Proof that Barricelli is named as the inventor on patent GB695913A.' ,
publishedAt : '1953-08-19' , sourceCitation : 'Google Patents · GB695913A' , sourceUri : 'https://patents.google.com/patent/GB695913A/en' }],
}) })
expect ( accepted . status ). toBe ( 201 )
expect ( await accepted . json ()). toMatchObject ({ status : 'accepted' , investigatorName : 'Test Player' , issues : [], claims : [ expect . objectContaining ({ evidence : [ expect . objectContaining ({
displayNumber :targetDocument.displayNumber , evidenceAccepted :true , sourceCitation : 'Google Patents · GB695913A' , publishedAt : '1953-08-19T00:00:00.000Z' ,
})] })] })
2026-08-22 16:02:10 +02:00
judgeVerdict = { subject : 'target' , supports_claim :true , evidence_excerpt : 'Ada Example patented a pocket telescope' , confidence :.96 }
judgeHttpStatus = 200
})
2026-08-14 12:57:06 +02:00
})