tests/integration/dashboard-adapter.test.ts
Rama main · réplica de lectura del repositorio original.
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
let proc: ReturnType<typeof Bun.spawn>, upstream: ReturnType<typeof Bun.serve>, port=0, upstreamPort=0, base=""; const staticRoot=mkdtempSync(join(tmpdir(),"titen-dashboard-adapter-"));
const forwarded: Array<{authorization:string|null;body:Record<string,unknown>}>=[];
const forwardedMemoryQueries: Array<{authorization:string|null;query:string}> = [];
let probeReply: {status:number;body:unknown}|undefined;
const forwardedRoutes: Array<{path:string;method:string;authorization:string|null;body?:Record<string,unknown>}> = [];
beforeAll(async()=>{ mkdirSync(`${staticRoot}/dashboard`,{recursive:true}); writeFileSync(`${staticRoot}/index.html`,"ROOT_REDIRECT"); writeFileSync(`${staticRoot}/dashboard/index.html`,"DASHBOARD_OK"); try{symlinkSync("/etc/passwd",`${staticRoot}/passwd-link`)}catch{} upstream=Bun.serve({hostname:"127.0.0.1",port:0,async fetch(request){const url=new URL(request.url);const path=url.pathname;if(path==="/healthz")return Response.json({data:{status:"ok"}});if(path==="/readyz")return Response.json({data:{ready:true}});if(path==="/v1/memories"){forwardedMemoryQueries.push({authorization:request.headers.get("authorization"),query:url.search});return Response.json({data:{items:[],page:{limit:25,has_more:false,next_cursor:null},query:{},authorization:{principal_id:"owner",access_mode:"principal"}}})}if((path.startsWith("/v1/knowledge-releases/")||path.startsWith("/v1/claim-approvals/"))&&probeReply)return Response.json(probeReply.body,{status:probeReply.status});if(path==="/v1/models/probe"&&probeReply)return Response.json(probeReply.body,{status:probeReply.status});if(path==="/v1/memory-views/compile"){const body=await request.json() as Record<string,unknown>;forwarded.push({authorization:request.headers.get("authorization"),body});return Response.json({data:{lens:body.lens,focus_id:body.focus_id??null,nodes:[],edges:[],metadata:{}}})}const body=request.method==="DELETE"?undefined:await request.json().catch(()=>undefined) as Record<string,unknown>|undefined;forwardedRoutes.push({path:path+url.search,method:request.method,authorization:request.headers.get("authorization"),...(body?{body}:{})});return Response.json({data:{ok:true}})}}); upstreamPort=upstream.port!; const reservation=Bun.serve({hostname:"127.0.0.1",port:0,fetch:()=>new Response("reserved")}); port=reservation.port!; base=`http://127.0.0.1:${port}`; await reservation.stop(true); proc=Bun.spawn({cmd:[process.execPath,"scripts/dashboard-adapter.ts"],env:{...process.env,TITEN_DASHBOARD_LIVE:"true",TITEN_API_URL:`http://127.0.0.1:${upstreamPort}`,TITEN_API_KEY:"adapter-test-key",TITEN_DASHBOARD_PORT:String(port),TITEN_DASHBOARD_ORIGIN:"https://benchmark-host.example",TITEN_DASHBOARD_ROOT:staticRoot},stdout:"ignore",stderr:"pipe"}); for(let i=0;i<250;i++){try{if((await fetch(`${base}/dashboard`)).ok)return}catch{} await Bun.sleep(20)} throw new Error("adapter did not start")});
afterAll(()=>{proc?.kill();upstream?.stop(true);rmSync(staticRoot,{recursive:true,force:true})});
describe("dashboard adapter boundary",()=>{
test("serves root and both dashboard routes with hardened headers",async()=>{for(const [p,body] of [["/","ROOT_REDIRECT"],["/dashboard","DASHBOARD_OK"],["/dashboard/","DASHBOARD_OK"]]){const r=await fetch(base+p);expect(await r.text()).toBe(body);const csp=r.headers.get("content-security-policy");expect(csp).toContain("connect-src 'self'");expect(csp).toContain("img-src 'self' data:")}});
test("reports the configured live service",async()=>{expect(await(await fetch(base+"/dashboard-api/status")).json()).toEqual({mode:"live",endpoint:`127.0.0.1:${upstreamPort}`,authentication:"server",authenticated:true});expect((await fetch(base+"/dashboard-api/health")).status).toBe(200);expect((await fetch(base+"/dashboard-api/readiness")).status).toBe(200)});
test("validates and forwards governance, graph, and administrator lenses",async()=>{const call=(body:Record<string,unknown>)=>fetch(base+"/dashboard-api/atlas/compile",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(body)});expect((await call({lens:"scope_preview",limit:50})).status).toBe(400);expect((await call({lens:"neighborhood",subject_id:"subject",access_mode:"organization_admin",limit:50})).status).toBe(400);for(const body of [{lens:"scope_preview",focus_id:"principal_ops",limit:50},{lens:"knowledge_release",limit:25},{lens:"workspace_graph",workspace_id:"ws_1",max_nodes:80,limit:50},{lens:"neighborhood",subject_id:"subject",access_mode:"organization_admin",administrator_reason:"recovery",limit:10}])expect((await call(body)).status).toBe(200);expect(forwarded).toEqual([{authorization:"Bearer adapter-test-key",body:{lens:"scope_preview",focus_id:"principal_ops",limit:50}},{authorization:"Bearer adapter-test-key",body:{lens:"knowledge_release",limit:25}},{authorization:"Bearer adapter-test-key",body:{lens:"workspace_graph",workspace_id:"ws_1",limit:50,max_nodes:80}},{authorization:"Bearer adapter-test-key",body:{lens:"neighborhood",subject_id:"subject",access_mode:"organization_admin",administrator_reason:"recovery",limit:10}}])});
test("allowlists and forwards memory list queries",async()=>{const response=await fetch(base+"/dashboard-api/memories?q=rollback%20smoke&status=disputed&limit=25&after=cursor");expect(response.status).toBe(200);expect(await response.json()).toEqual({data:{items:[],page:{limit:25,has_more:false,next_cursor:null},query:{},authorization:{principal_id:"owner",access_mode:"principal"}}});expect(forwardedMemoryQueries).toEqual([{authorization:"Bearer adapter-test-key",query:"?q=rollback+smoke&status=disputed&limit=25&after=cursor"}]);expect((await fetch(base+"/dashboard-api/memories?unsupported=yes")).status).toBe(400)});
test("allowlists directory reads and same-origin mutations",async()=>{expect((await fetch(base+"/dashboard-api/projects?q=repo&limit=20")).status).toBe(200);expect((await fetch(base+"/dashboard-api/projects/project_1/references")).status).toBe(200);expect((await fetch(base+"/dashboard-api/access/simulate",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({principal_id:"agent_1",resource_type:"claim",resource_id:"claim_1",operation:"read"})})).status).toBe(200);expect((await fetch(base+"/dashboard-api/access/grants/grant_1",{method:"DELETE"})).status).toBe(200);expect((await fetch(base+"/dashboard-api/models/probe",{method:"POST",headers:{origin:"https://evil.test","content-type":"application/json"},body:"{}"})).status).toBe(403);expect(forwardedRoutes).toEqual([{path:"/v1/projects?q=repo&limit=20",method:"GET",authorization:"Bearer adapter-test-key"},{path:"/v1/projects/project_1/references",method:"GET",authorization:"Bearer adapter-test-key"},{path:"/v1/access/simulate",method:"POST",authorization:"Bearer adapter-test-key",body:{principal_id:"agent_1",resource_type:"claim",resource_id:"claim_1",operation:"read"}},{path:"/v1/grants/grant_1",method:"DELETE",authorization:"Bearer adapter-test-key"}])});
for(const p of ["//etc/passwd","/%2fetc/passwd","/%252fetc/passwd","/../etc/passwd","/%2e%2e/etc/passwd","/%252e%252e/etc/passwd","/..%5cetc%5cpasswd","/%255c..%255cetc/passwd","/etc/passwd%00","/passwd-link"]) test(`blocks ${p}`,async()=>{const r=await fetch(base+p); const b=await r.text(); expect(r.status).not.toBe(200); expect(b).not.toContain("root:x:")});
test("forwards key list filters for the Access clamp preview",async()=>{const response=await fetch(base+"/dashboard-api/governance/keys?status=active&principal_id=agent_1&limit=500");expect(response.status).toBe(200);expect(forwardedRoutes.at(-1)).toEqual({path:"/v1/keys?status=active&principal_id=agent_1&limit=500",method:"GET",authorization:"Bearer adapter-test-key"});expect((await fetch(base+"/dashboard-api/governance/keys?status=active&issued_by=agent_1&limit=500")).status).toBe(200);expect(forwardedRoutes.at(-1)).toEqual({path:"/v1/keys?status=active&issued_by=agent_1&limit=500",method:"GET",authorization:"Bearer adapter-test-key"});expect((await fetch(base+"/dashboard-api/governance/keys?scope=all")).status).toBe(400)});
test("model probe failures keep their real cause without forwarding upstream text",async()=>{const probe=()=>fetch(base+"/dashboard-api/models/probe",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({group:"extraction"})});probeReply={status:429,body:{error:{code:"RATE_LIMITED",message:"Wait ten seconds before probing this provider again."}}};let response=await probe();expect(response.status).toBe(429);expect(await response.json()).toEqual({error:{code:"PROBE_RATE_LIMITED",message:"Wait ten seconds before probing a model provider again."}});probeReply={status:503,body:{error:{code:"UNAVAILABLE",message:"The extraction provider is not enabled."}}};response=await probe();expect(response.status).toBe(503);expect(await response.json()).toEqual({error:{code:"PROVIDER_NOT_ENABLED",message:"This model provider is not enabled on this deployment."}});probeReply={status:503,body:{error:{code:"UNAVAILABLE",message:"secret upstream detail"}}};response=await probe();const generic=await response.json() as {error:{code:string;message:string}};expect(generic.error.code).toBe("UPSTREAM_503");expect(JSON.stringify(generic)).not.toContain("secret upstream detail");probeReply={status:429,body:{error:{code:"OTHER",message:"x"}}};response=await probe();expect(((await response.json()) as {error:{message:string}}).error.message).not.toContain("sign-in");probeReply=undefined});
test("a claim submitter refusal names the independent-approval rule",async()=>{const decide=()=>fetch(base+"/dashboard-api/approvals/apr_1/decide",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({decision:"approve",expected_version:1,reason:"ok"})});probeReply={status:403,body:{error:{code:"FORBIDDEN",message:"The submitter cannot decide this approval."}}};let response=await decide();expect(response.status).toBe(403);expect(await response.json()).toEqual({error:{code:"APPROVAL_SUBMITTER",message:"You submitted this claim; another approver must decide it."}});probeReply={status:403,body:{error:{code:"FORBIDDEN",message:"other upstream detail"}}};response=await decide();const generic=await response.json() as {error:{code:string}};expect(generic.error.code).toBe("UPSTREAM_403");expect(JSON.stringify(generic)).not.toContain("other upstream detail");probeReply=undefined});
test("a release proposer refusal names the separation-of-duties rule",async()=>{const approve=()=>fetch(base+"/dashboard-api/releases/rel_1/approve",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({expected_version:1,reason:"ok"})});probeReply={status:403,body:{error:{code:"FORBIDDEN",message:"The release proposer cannot approve it."}}};let response=await approve();expect(response.status).toBe(403);expect(await response.json()).toEqual({error:{code:"RELEASE_PROPOSER",message:"You proposed this release; another approver must approve it."}});probeReply={status:403,body:{error:{code:"FORBIDDEN",message:"other upstream detail"}}};response=await approve();const generic=await response.json() as {error:{code:string}};expect(generic.error.code).toBe("UPSTREAM_403");expect(JSON.stringify(generic)).not.toContain("other upstream detail");probeReply=undefined});
test("reference ids are encoded exactly once on the way upstream",async()=>{expect((await fetch(base+"/dashboard-api/subjects/user%3Aalice/references")).status).toBe(200);expect(forwardedRoutes.at(-1)).toEqual({path:"/v1/subjects/user%3Aalice/references",method:"GET",authorization:"Bearer adapter-test-key"});expect((await fetch(base+"/dashboard-api/projects/team%2Fweb/references")).status).toBe(200);expect(forwardedRoutes.at(-1)?.path).toBe("/v1/projects/team%2Fweb/references");expect((await fetch(base+"/dashboard-api/subjects/bad%E0%A4%A/references")).status).toBe(400)});
test("rejects foreign host and origin",async()=>{expect((await fetch(base+"/dashboard-api/status",{headers:{host:"evil.test"}})).status).toBe(403); expect((await fetch(base+"/dashboard-api/status",{headers:{origin:"https://evil.test"}})).status).toBe(403)});
test("accepts only the configured reverse-proxy origin",async()=>{expect((await fetch(base+"/dashboard-api/status",{headers:{host:"benchmark-host.example",origin:"https://benchmark-host.example"}})).status).toBe(200);expect((await fetch(base+"/dashboard-api/status",{headers:{host:"benchmark-host.example",origin:"https://evil.test"}})).status).toBe(403)});
});