atmn-test 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +24 -0
- package/package.json +15 -0
package/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import{mkdirSync as Oz,readFileSync as Qz,writeFileSync as Xz}from"node:fs";import{homedir as Yz}from"node:os";import{dirname as Zz,join as $z}from"node:path";var m=process.env.TEST_RUNNER_CONFIG_PATH?.trim()||$z(Yz(),".autumn-cloud","config.json");var Vz="http://localhost:8090",g=process.env.TEST_RUNNER_URL?.trim()||Vz,y=()=>{try{return JSON.parse(Qz(m,"utf8"))}catch{return null}},s=(z)=>{Oz(Zz(m),{recursive:!0}),Xz(m,JSON.stringify(z,null,2),{mode:384})},c=()=>{return g},b=async(z,H)=>{let X=c(),Q=process.env.TEST_RUNNER_API_KEY?.trim(),W=y(),V=new Headers(H?.headers??{});if(!V.has("content-type")&&H?.body)V.set("content-type","application/json");if(Q)V.set("x-api-key",Q);else if(W?.apiKey)V.set("x-api-key",W.apiKey);else if(W?.accessToken)V.set("authorization",`Bearer ${W.accessToken}`);else throw Error("Not logged in. Run: bun cli/index.ts login");return fetch(`${X}${z}`,{...H,headers:V})},f=async(z,H)=>{let X=await b(z,H);if(!X.ok)throw Error(`${z} failed (${X.status}): ${await X.text()}`);return await X.json()},q=(z)=>`\x1B[2m${z}\x1B[0m`,x=(z)=>`\x1B[1m${z}\x1B[0m`,P=(z)=>`\x1B[32m${z}\x1B[0m`,M=(z)=>`\x1B[31m${z}\x1B[0m`,U=(z)=>`\x1B[33m${z}\x1B[0m`,a=(z)=>`\x1B[36m${z}\x1B[0m`,d=(z)=>`\x1B[${z}A`,Wz="\x1B[2K",n="dev",Jz=()=>Buffer.from(crypto.getRandomValues(new Uint8Array(16))).toString("hex"),Tz=async(z)=>{let H=process.platform,X=H==="darwin"?[["open",z]]:H==="win32"?[["cmd","/c","start","",z]]:[["xdg-open",z]];for(let Q of X)try{if(await Bun.spawn(Q,{stdout:"ignore",stderr:"ignore"}).exited===0)return}catch{}console.log(`Open this URL in your browser:
|
|
3
|
+
${z}`)},Bz=({handler:z})=>{let H=Number(process.env.TEST_RUNNER_CALLBACK_PORT??"44767"),X=200;for(let Q=0;Q<200;Q+=1){let W=H+Q;try{return{server:Bun.serve({port:W,fetch:z}),port:W}}catch{}}throw Error(`Could not bind a localhost callback port (${H}-${H+200-1})`)},Gz=async()=>{let z=g.replace(/\/$/,""),H=Jz(),X=null,Q=null,W=new Promise((N,G)=>{X=N,Q=G}),{server:V,port:k}=Bz({handler(N){let G=new URL(N.url);if(G.pathname!=="/callback")return new Response("Not found",{status:404});let K=G.searchParams.get("code"),C=G.searchParams.get("state");if(!K||!C){if(Q)Q(Error("Missing code or state in callback"));return new Response("Missing code/state",{status:400})}if(X)X({code:K,state:C});return new Response("Login complete. You can close this tab and return to your terminal.")}}),I=`http://127.0.0.1:${k}/callback`,T=new URL(`${z}/cli/login`);T.searchParams.set("redirect_uri",I),T.searchParams.set("state",H),console.log("Opening browser for login..."),await Tz(T.toString());let $=setTimeout(()=>{if(Q)Q(Error("Login timed out after 5 minutes"))},300000);try{let N=await W;if(N.state!==H)throw Error("State mismatch in callback");let G=await fetch(`${z}/cli/auth/exchange`,{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify(N)});if(!G.ok)throw Error(`Token exchange failed (${G.status}): ${await G.text()}`);let K=await G.json(),C=y();s({runnerUrl:z,accessToken:K.accessToken,expiresAt:K.expiresAt,userEmail:K.user.email,apiKey:C?.apiKey}),console.log(`Logged in as ${K.user.email}`)}finally{clearTimeout($),V.stop()}},Kz=async()=>{if(!y()&&!process.env.TEST_RUNNER_API_KEY){console.log("Not logged in");return}let H=await b("/test-runner/whoami");if(!H.ok)throw Error(`whoami failed (${H.status}): ${await H.text()}`);let X=await H.json();console.log(`${X.actor.email} (${X.actor.source})`)},_z=async()=>{let z=y();if(!z?.accessToken)throw Error("You must be logged in to create an API key. Run: bun cli/index.ts login");let H=c(),X=await fetch(`${H}/api/auth/api-key/create`,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${z.accessToken}`},body:JSON.stringify({name:"test-runner-cli"})});if(!X.ok)throw Error(`Failed to create API key (${X.status}): ${await X.text()}`);let Q=await X.json();console.log(`API key created: ${Q.key}`),console.log(q("Save this key — it will not be shown again.")),console.log(""),console.log("Usage options:"),console.log(` 1. Set env var: ${x(`TEST_RUNNER_API_KEY=${Q.key}`)}`),console.log(` 2. Save to config: ${x("bun cli/index.ts api-key save <key>")}`)},qz=async({key:z})=>{let H=y();s({runnerUrl:H?.runnerUrl??g,accessToken:H?.accessToken??"",expiresAt:H?.expiresAt??0,userEmail:H?.userEmail??"",apiKey:z}),console.log("API key saved to config.")},S=async()=>{let z=c();try{let H=await fetch(`${z}/health`);if(!H.ok)throw Error(`Healthcheck returned ${H.status}`)}catch{throw Error(`Cannot reach test-runner at ${z}. Start it with: bun run dev:server`)}},jz=String.fromCharCode(27),kz=(z)=>z.includes(jz),Mz=({source:z,line:H})=>{if(z==="test:stdout"||z==="test:stderr"){if(kz(H))return H;if(H.includes("✓")||H.includes("passed"))return P(H);if(H.includes("✗")||H.includes("FAILED")||H.includes("failed"))return H.includes("will retry")?U(H):M(H);if(H.includes("SUMMARY:"))return x(H);return H}if(z==="system")return a(`[system] ${H}`);return null},p=({ms:z})=>{if(z<1000)return`${Math.round(z)}ms`;return`${(z/1000).toFixed(1)}s`},o=({str:z,maxLen:H})=>{if(z.length<=H)return z;return`${z.substring(0,H-3)}...`},i=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"],Nz=new RegExp(`${String.fromCharCode(27)}\\[[0-9;]*m`,"g"),u=(z)=>z.replace(Nz,""),Az=async({runId:z})=>{let H=await b(`/test-runner/runs/${z}/stream`);if(!H.ok)throw Error(`Failed to stream run: ${H.status} ${await H.text()}`);if(!H.body)throw Error("No response body from stream endpoint");let X=H.body.getReader(),Q=new TextDecoder,W="",V=!1,k="",I=null,T=null,$=(N)=>console.log(N);while(!V){let{done:N,value:G}=await X.read();if(N)break;W+=Q.decode(G,{stream:!0});let K=W.split(`
|
|
4
|
+
|
|
5
|
+
`);W=K.pop()??"";for(let C of K){let D=C.trim();if(!D.startsWith("data: "))continue;let h=D.slice(6),_;try{_=JSON.parse(h)}catch{continue}if(_.kind==="phase"){$(`[phase] ${_.phase}: ${_.message}`);continue}if(_.kind==="log"){if(_.source==="system")$(`[system] ${_.line}`);continue}if(_.kind==="test_event"){let Y=_.event;if(Y.type==="test_file_start")$(`[start] ${Y.file} (attempt ${Y.attempt}, org ${Y.orgSlug})`);else if(Y.type==="test_result"){if(Y.status==="failed"){if($(` [FAIL] ${Y.testName} (${Y.duration}ms)`),Y.error){if($(` ${Y.error.message}`),Y.error.location)$(` at ${Y.error.location}`)}}}else if(Y.type==="test_file_done"){let L=p({ms:Y.duration});if(Y.status==="passed")$(`[PASS] ${Y.file} (${Y.passCount} passed, ${L})`);else if(Y.status==="crashed"){if($(`[CRASHED] ${Y.file} (${L})`),Y.crashError){let A=u(Y.crashError).split(`
|
|
6
|
+
`).slice(0,30);for(let E of A)$(` | ${E}`)}}else if($(`[FAIL] ${Y.file} (${Y.passCount} passed, ${Y.failCount} failed, ${L})`),Y.crashError){let A=u(Y.crashError).split(`
|
|
7
|
+
`).slice(0,30);for(let E of A)$(` | ${E}`)}}else if(Y.type==="test_retry_start")$(`[retry] ${Y.file} (${Y.failedTests.length} failed tests)`);else if(Y.type==="test_file_retry_done"){let L=p({ms:Y.duration});if(Y.passedOnRetry)$(`[PASS on retry] ${Y.file} (${Y.passCount} passed, ${L})`);else $(`[FAIL after retry] ${Y.file} (${Y.passCount} passed, ${Y.failCount} failed, ${L})`)}else if(Y.type==="test_summary"){let L=p({ms:Y.duration});$(""),$("========== SUMMARY =========="),$(`Total files: ${Y.totalFiles}`),$(`Passed: ${Y.passed}`),$(`Failed: ${Y.failed}`),$(`Crashed: ${Y.crashed}`),$(`Retried: ${Y.retriedFiles}`),$(`Passed on retry: ${Y.passedOnRetry}`),$(`Duration: ${L}`),$("=============================")}continue}if(_.kind==="done"){k=_.status,I=_.exitCode,T=_.error,V=!0;break}}}if($(""),k==="success")$(`RESULT: SUCCESS (run ${z})`);else if($(`RESULT: ${k} exit_code=${I} (run ${z})`),T)$(`ERROR: ${T}`)},Lz=async({runId:z})=>{let H=process.argv.includes("--verbose")||process.argv.includes("-v");if(process.argv.includes("--agent")||process.argv.includes("-a"))return Az({runId:z});let Q=await b(`/test-runner/runs/${z}/stream`);if(!Q.ok)throw Error(`Failed to stream run: ${Q.status} ${await Q.text()}`);if(!Q.body)throw Error("No response body from stream endpoint");let W=Q.body.getReader(),V=new TextDecoder,k="",I="",T=null,$=null,N=!1,G=new Map,K=0,C=0,D=0,h=0,_=0,Y=0,L=0,A=()=>{if(K>0){process.stdout.write(d(K));for(let O=0;O<K;O++)process.stdout.write(`${Wz}
|
|
8
|
+
`);process.stdout.write(d(K)),K=0}},E=()=>{A();let O=[];for(let[B,j]of G)if(j.status==="running"||j.status==="retrying")O.push({file:B,state:j});if(O.length===0&&D===0)return;let Z=[];Z.push(q("─".repeat(60)));for(let{file:B,state:j}of O){let F=i[C%i.length],R=j.status==="retrying"?U("retrying"):"running",J=j.passCount>0||j.failCount>0?` ${P(`${j.passCount} passed`)} ${j.failCount>0?M(`${j.failCount} failed`):""}`:"";Z.push(` ${U(F??"⠋")} ${B} ${q(`(${R})`)}${J}`)}if(D>0){let B=[`${h}/${D} files`];if(B.push(`${O.length} running`),_>0)B.push(P(`${_} passed`));if(Y>0)B.push(U(`${Y} to retry`));if(L>0)B.push(M(`${L} crashed`));Z.push(q(" ")+B.join(q(" | ")))}for(let B of Z)process.stdout.write(`${B}
|
|
9
|
+
`);K=Z.length,C++},t=({file:O,state:Z,willRetry:B})=>{A();let j=p({ms:Z.duration});if(Z.status==="passed")console.log(` ${P("✓")} ${q(`${O} ${Z.passCount} passed (${j})`)}`);else if(Z.status==="crashed"){if(console.log(` ${M("✗")} ${O} ${M("CRASHED")} ${q(`(${j})`)}`),Z.crashError){let R=u(Z.crashError).split(`
|
|
10
|
+
`).filter((J)=>J.trim().length>0).slice(0,5);for(let J of R)console.log(` ${q(J)}`)}}else{let F=B?U("✗"):M("✗"),R=B?q(" (will retry)"):"";console.log(` ${F} ${O} ${P(`${Z.passCount} passed`)} ${B?U(`${Z.failCount} failed`):M(`${Z.failCount} failed`)} ${q(`(${j})`)}${R}`);for(let J of Z.failedTests){let w=B?U:M,Hz=J.errorMessage?` ${U(`— ${o({str:J.errorMessage,maxLen:70})}`)}`:"";if(console.log(` ${w("✗")} ${o({str:J.name,maxLen:60})}${Hz}`),J.location)console.log(` ${a(J.location)}`)}}E()},e=setInterval(()=>{if(Array.from(G.values()).some((Z)=>Z.status==="running"||Z.status==="retrying"))E()},100),zz=({event:O})=>{if(O.type==="test_file_start"){if(G.set(O.file,{status:O.attempt>1?"retrying":"running",passCount:0,failCount:0,duration:0,attempt:O.attempt,orgSlug:O.orgSlug,passedOnRetry:!1,failedTests:[]}),O.attempt<=1)D=Math.max(D,G.size);E();return}if(O.type==="test_result"){let Z=G.get(O.file);if(!Z)return;if(O.status==="passed")Z.passCount++;else Z.failCount++,Z.failedTests.push({name:O.testName,errorMessage:O.error?.message,location:O.error?.location});return}if(O.type==="test_file_done"){let Z=G.get(O.file);if(!Z)return;if(Z.status=O.status,Z.passCount=O.passCount,Z.failCount=O.failCount,Z.duration=O.duration,Z.crashError=O.crashError,h++,O.status==="passed")_++;else if(O.status==="crashed")L++;else if(O.status==="failed")Y++;t({file:O.file,state:Z,willRetry:Z.failCount>0});return}if(O.type==="test_retry_start"){A(),console.log(` ${U("↻")} ${O.file} ${U("retrying")} ${q(`(${O.failedTests.length} failed test${O.failedTests.length>1?"s":""})`)}`),E();return}if(O.type==="test_file_retry_done"){let Z=G.get(O.file);if(!Z)return;if(Z.status=O.passedOnRetry?"passed":"failed",Z.passCount=O.passCount,Z.failCount=O.failCount,Z.duration=O.duration,Z.passedOnRetry=O.passedOnRetry,A(),O.passedOnRetry)console.log(` ${P("↳ ✓")} ${P(`${O.file} passed on retry`)}`);else console.log(` ${M("↳ ✗")} ${M(`${O.file} still failed after retry`)}`);E();return}if(O.type==="test_summary"){A();let Z=p({ms:O.duration});if(console.log(""),console.log(x("═".repeat(60))),O.failed===0&&O.crashed===0){let B=O.passedOnRetry>0?` ${U(`(${O.passedOnRetry} on retry)`)}`:"";console.log(x(P(` ALL ${O.passed} FILE(S) PASSED (${Z})`))+B)}else console.log(x(M(` ${O.failed+O.crashed} FILE(S) FAILED`))),console.log(` ${P(`${O.passed} passed`)} | ${M(`${O.failed} failed`)} | ${O.crashed>0?`${M(`${O.crashed} crashed`)} | `:""}${q(Z)}`);console.log(x("═".repeat(60)))}};while(!N){let{done:O,value:Z}=await W.read();if(O)break;k+=V.decode(Z,{stream:!0});let B=k.split(`
|
|
11
|
+
|
|
12
|
+
`);k=B.pop()??"";for(let j of B){let F=j.trim();if(!F.startsWith("data: "))continue;let R=F.slice(6),J;try{J=JSON.parse(R)}catch{continue}if(J.kind==="phase"){A(),console.log(U(`[${J.phase}] ${J.message}`));let w=J.message.match(/Running (\d+) test file/);if(w?.[1])D=Number.parseInt(w[1],10);E();continue}if(J.kind==="log"){if(J.source==="system"){let w=J.line.match(/Found (\d+) test file/);if(w?.[1])D=Number.parseInt(w[1],10)}if(H){A();let w=q(`[${J.source}]`);console.log(`${w} ${J.line}`),E()}else{let w=Mz({source:J.source,line:J.line});if(w!==null)A(),console.log(w),E()}continue}if(J.kind==="test_event"){zz({event:J.event});continue}if(J.kind==="summary")continue;if(J.kind==="done"){I=J.status,T=J.exitCode,$=J.error,N=!0;break}}}if(clearInterval(e),A(),console.log(""),I==="success")console.log(P(x(`Run ${z} finished successfully`)));else if(console.log(M(x(`Run ${z} finished with status=${I} exitCode=${T}`))),$)console.log(M(`error: ${$}`))},r=async({request:z})=>{await S();let H=await b("/test-runner/runs",{method:"POST",body:JSON.stringify(z)});if(!H.ok)throw Error(`Failed to create run: ${H.status} ${await H.text()}`);let X=await H.json();console.log(q(`[test-runner] created run ${X.run.id} — streaming logs...
|
|
13
|
+
`)),await Lz({runId:X.run.id})},Ez=new Set(["--concurrency","-c","--branch","-b"]),wz=new Set(["--verbose","-v","--agent","-a"]),l=()=>{let z=process.argv.slice(2),H=[],X,Q;for(let W=0;W<z.length;W++){let V=z[W]??"";if(V.startsWith("--concurrency=")){X=Number.parseInt(V.split("=")[1]??"2",10);continue}if(V.startsWith("--branch=")){Q=V.split("=")[1];continue}if(Ez.has(V)){let k=z[W+1];if(V==="--concurrency"||V==="-c"){if(k)X=Number.parseInt(k,10)}if(V==="--branch"||V==="-b"){if(k)Q=k}W++;continue}if(wz.has(V))continue;if(V.startsWith("-"))continue;H.push(V)}return{positional:H,concurrency:X,branch:Q}},Uz=async({names:z,concurrency:H,branch:X})=>{await S();let Q=await f("/test-runner/resolve-groups",{method:"POST",body:JSON.stringify({names:z})}),W;for(let T of Q.groups)if(T.maxConcurrency)W=W===void 0?T.maxConcurrency:Math.min(W,T.maxConcurrency);console.log(`[test-runner] ${Q.groups.length} target(s):`);for(let T of Q.groups){let $=T.source==="shell"?" [shell]":"";if(T.source==="path")console.log(` ${T.name} (directory)`);else console.log(` ${T.name} (${T.paths.length} test paths${$})`)}console.log("");let V=H??W,I={gitRef:X??n,serverCommand:["bun","scripts/dev.ts","--server-only"],healthcheckUrl:"http://localhost:8080/",testTargets:Q.allPaths,concurrency:V,groupName:Q.groups.length===1?Q.groups[0]?.name:Q.groups.map((T)=>T.name).join("+")};await r({request:I})},Iz=async({filePath:z,filterPattern:H,branch:X})=>{if(console.log(`[test-runner] file: ${z}`),H)console.log(`[test-runner] filter: ${H}`);console.log("");let Q={};if(H)Q.TEST_NAME_PATTERN=H;let V={gitRef:X??n,serverCommand:["bun","scripts/dev.ts","--server-only"],healthcheckUrl:"http://localhost:8080/",testTargets:[z],concurrency:1,extraEnv:Q,groupName:z.split("/").pop()??z};await r({request:V})},Dz=async()=>{await S();let z=await f("/test-runner/groups");console.log(`Available test groups (TS):
|
|
14
|
+
`);for(let H of z.tsGroups){let X=H.pathCount>0?`${H.pathCount} paths`:q("empty"),Q=H.maxConcurrency?` max-concurrency=${H.maxConcurrency}`:"";console.log(` ${H.name} [${H.tier}] (${X}${Q})`),console.log(` ${q(H.description)}`)}if(z.suites.length>0){console.log(`
|
|
15
|
+
Available suites:
|
|
16
|
+
`);for(let H of z.suites)console.log(` ${H.name} → ${H.groups.join(", ")}`),console.log(` ${q(H.description)}`)}if(z.shellGroups.length>0){console.log(`
|
|
17
|
+
Legacy shell groups:
|
|
18
|
+
`);for(let H of z.shellGroups)console.log(` ${H.name} (${H.pathCount} paths)`)}},Pz=async({runId:z})=>{await S();let H=await f(`/test-runner/runs/${z}`);console.log(JSON.stringify(H.run,null,2))},Cz=async()=>{await S();let z=await f("/test-runner/runs");for(let H of z.runs)console.log(`${H.id} ${H.status} ${H.gitRef} ${H.testTargets.join(",")}`)},xz=async()=>{await S(),console.log(`Refreshing Stripe webhook endpoint...
|
|
19
|
+
`);let z=await b("/test-runner/refresh-stripe",{method:"POST"});if(!z.ok)throw Error(`Failed to refresh Stripe: ${z.status} ${await z.text()}`);let H=await z.json();console.log(`Deleted ${H.deleted} old endpoint(s)`),console.log(`Created new endpoint: ${H.newEndpointId}`),console.log("Infisical updated with new webhook secret.")},Fz=async()=>{await S();let z=await b("/test-runner/kill-server",{method:"POST"});if(!z.ok)throw Error(`Failed to kill server: ${z.status} ${await z.text()}`);console.log("Autumn server killed.")},v=()=>{console.log(`
|
|
20
|
+
Usage:
|
|
21
|
+
`),console.log(" Auth:"),console.log(" login Log in via Google OAuth"),console.log(" whoami Show current user"),console.log(" api-key create Create an API key"),console.log(` api-key save <key> Save an API key to config
|
|
22
|
+
`),console.log(" Test runs:"),console.log(" <group> Run a test group (e.g. core, balances)"),console.log(" <g1> <g2> ... Run multiple groups/suites/dirs"),console.log(" <group> -c N Set org concurrency (default 2)"),console.log(" <group> -b <branch> Git branch to test (default: dev)"),console.log(" <group> --verbose Run with full server logs"),console.log(" <group> --agent Plain-text output (no TUI)"),console.log(` test <file> [-t <pattern>] Run a single test file
|
|
23
|
+
`),console.log(" Info:"),console.log(" groups List available test groups/suites"),console.log(" get-run <run-id> Get run details"),console.log(" runs List all runs"),console.log(" kill-server Kill the autumn server process"),console.log(` refresh-stripe Refresh Stripe webhook endpoint
|
|
24
|
+
`)},Rz=async()=>{let z=process.argv.slice(2),H=z[0];if(!H){v();return}switch(H){case"login":await Gz();return;case"whoami":await Kz();return;case"api-key":{let X=z[1];if(X==="create"){await _z();return}if(X==="save"){let Q=z[2];if(!Q)throw Error("Usage: bun cli/index.ts api-key save <key>");await qz({key:Q});return}console.log("Usage: bun cli/index.ts api-key <create|save>");return}case"test":{let X=z[1];if(!X)throw Error("Missing file path. Usage: test <file> [-t <pattern>]");let Q,W=z.indexOf("-t");if(W!==-1){if(Q=z[W+1],!Q)throw Error("Missing pattern after -t")}let{branch:V}=l();await Iz({filePath:X,filterPattern:Q,branch:V});return}case"groups":await Dz();return;case"get-run":{let X=z[1];if(!X)throw Error("Missing run ID. Usage: get-run <run-id>");await Pz({runId:X});return}case"runs":await Cz();return;case"kill-server":await Fz();return;case"refresh-stripe":await xz();return;case"help":v();return;default:{let{positional:X,concurrency:Q,branch:W}=l();if(X.length===0){v();return}await Uz({names:X,concurrency:Q,branch:W})}}};Rz().catch((z)=>{console.error(z instanceof Error?z.message:String(z)),process.exit(1)});
|
package/package.json
ADDED