🌳
pt0/deployF/testsF/recordTestResultAI.mts
1import { readFileSync, existsSync } from 'fs'
5import { ptenvLocal, ptenvTestprod, type Ptenv } from '../../sharedF/ptenvF.mts'
7type ActionHistoryRow = {
8 action?: string
9 ep?: string
10 suites?: Record<string, { passed: boolean }>
11 flags?: Record<string, boolean>
12 ts?: string
13 gitSha?: string
14 imageReused?: boolean
15 ptenv?: string
16 durationSec?: number
17 builder?: string
20export const recordTestResult = ({ ep, ptenv, ranSuites, durationSec, flags }: {
21 ep: string
22 ptenv: Ptenv
23 ranSuites: Record<string, { passed: boolean; message: string; durationSec?: number }>
24 durationSec?: number
25 flags?: Record<string, boolean>
26}) => {
27 const suites: Record<string, { passed: boolean; message: string; durationSec?: number }> = {}
28 let allPassed = true
29 for (const [name, r] of Object.entries(ranSuites)) {
30 suites[name] = { passed: r.passed, message: r.message, durationSec: r.durationSec }
31 if (!r.passed) allPassed = false
32 }
34 ep: toPtRelPath(ep),
35 action: 'runtests',
36 ts: new Date().toISOString(),
37 durationSec: durationSec || 0,
38 success: allPassed,
39 gitSha: getGitShaFull(),
40 ptenv,
41 suites,
42 flags,
43 })
46export const getLastPassingSha = ({ep, suite, preferBuilt}: {ep: string, suite?: string, preferBuilt?: boolean}): string | null => {
47 if (!existsSync(ledgerPath)) return null
48 let lines: string[]
49 try { lines = readFileSync(ledgerPath, 'utf8').trim().split('\n').filter(Boolean) }
50 catch { return null }
52 const epRel = toPtRelPath(ep)
53 const rows = lines.map(l => JSON.parse(l) as ActionHistoryRow).filter(r => r.ep === epRel && r.action === 'runtests')
55 const findPassing = (skipCached: boolean) => {
56 for (const row of rows.toReversed()) {
57 const allPassed = suite
58 ? row.suites?.[suite]?.passed
59 : Object.values(row.suites || {}).every((s: {passed: boolean}) => s.passed)
60 if (!allPassed || !row.gitSha) continue
61 if (skipCached && row.imageReused) continue
62 const deployRow = lines.map(l => JSON.parse(l) as ActionHistoryRow).find(r => r.gitSha === row.gitSha && r.ep === epRel && (r.action === 'apply' || r.action === 'testdeploy') && r.imageReused)
63 if (skipCached && deployRow) continue
64 return row.gitSha
65 }
66 return null
67 }
69 if (preferBuilt) {
70 const built = findPassing(true)
71 if (built) return built
72 }
73 return findPassing(false)
76export const historyHeaderMarker = 'SHA'
78export type DeployStatsRow = { gitSha: string; value: number }
80let historyPrinted = false
81export const applyHistoryGuard = async (ep: string | undefined, statsRows?: DeployStatsRow[] | null, statsLabel?: string): Promise<boolean> => {
82 if (historyPrinted) return false
83 historyPrinted = true
84 printTestHistory({ep, actions: ['apply'], statsRows, statsLabel})
85 return true
88export const printTestHistory = ({ ep, suites, actions, statsRows, statsLabel = 'REVENUE', flagCols }: { ep?: string; suites?: string[]; actions?: string[]; statsRows?: DeployStatsRow[] | null; statsLabel?: string; flagCols?: string[] } = {}) => {
89 let lines: string[]
90 if (!existsSync(ledgerPath)) { console.log(chalkGray('No history found.')); return }
91 try { lines = readFileSync(ledgerPath, 'utf8').trim().split('\n').filter(Boolean) }
92 catch { console.log(chalkGray('No history found.')); return }
94 const filterActions = actions || ['runtests', 'buildapk']
95 let rows: ActionHistoryRow[] = lines.map(l => JSON.parse(l) as ActionHistoryRow).filter(r => filterActions.includes(r.action || ''))
96 if (ep) {
97 const epRel = toPtRelPath(ep)
98 rows = rows.filter(r => r.ep === epRel)
99 }
101 const last30 = rows.slice(-30).toReversed()
102 if (!last30.length) { console.log(chalkGray('No matching history.')); return }
104 const allSuiteNames = suites?.length
105 ? suites
106 : [...new Set(last30.flatMap(r => Object.keys(r.suites || {})))]
108 const hasBuilder = last30.some(r => r.builder)
109 const hasStats = statsRows && statsRows.length > 0
110 const hasFlags = flagCols && flagCols.length > 0
111 const statsBySha = hasStats ? Object.fromEntries(statsRows!.map(r => [r.gitSha, r.value])) : null
112 const shaW = 9, actionW = 10, dateW = 12, agoW = 6, elapsedW = 7, builderW = 20, suiteW = 8, statsW = 10, flagW = 8
113 const padSha = (s: string) => s.slice(0, 8).padEnd(shaW), padAction = (s: string) => s.padEnd(actionW), padDate = (s: string) => s.padEnd(dateW), padAgo = (s: string) => s.padEnd(agoW), padElapsed = (s: string) => s.padEnd(elapsedW), padBuilder = (s: string) => s.padEnd(builderW), padSuite = (s: string) => s.padEnd(suiteW), padStats = (s: string) => s.padEnd(statsW), padFlag = (s: string) => s.padEnd(flagW)
114 const headerCols = [padSha('SHA'), padAction('ACTION'), padDate('DATE'), padAgo('AGO'), padElapsed('ELAPSED'), ...(hasBuilder ? [padBuilder('BUILDER')] : []), ...(hasStats ? [padStats(statsLabel.slice(0, 9))] : []), ...(hasFlags ? flagCols!.map(f => padFlag(f.slice(0, 7))) : []), ...allSuiteNames.map(s => padSuite(s.slice(0, 7)))]
115 console.log(chalkCyan(headerCols.join('')))
117 for (const row of last30) {
118 const datePart = row.ts?.slice(5, 16).replace('T', ' ') || '?'
119 const agoPart = row.ts ? chalkGray(padAgo(fmtAgo(row.ts))) : chalkGray(padAgo('-'))
120 const elapsedPart = row.durationSec ? chalkGray(padElapsed(fmtDuration(row.durationSec))) : chalkGray(padElapsed('-'))
121 const actionLabel = row.action === 'apply' ? chalkCyan(padAction('apply'))
122 : row.action === 'buildapk' ? chalkMagenta(padAction('buildapk'))
123 : row.action === 'testdeploy' ? chalkMagenta(padAction('testdploy'))
124 : row.ptenv === ptenvTestprod ? chalkGreen(padAction('testprod'))
125 : row.ptenv === ptenvLocal ? chalkYellow(padAction('testlocal'))
126 : padAction(row.ptenv || '?')
127 const cols = [
128 padSha((row.gitSha || '?') + (row.imageReused ? '*' : '')),
129 actionLabel,
130 padDate(datePart),
131 agoPart,
132 elapsedPart,
133 ...(hasBuilder && row.builder ? [padBuilder(row.builder)] : hasBuilder ? [padBuilder('')] : []),
134 ...(hasStats ? [padStats(statsBySha![row.gitSha || ''] != null ? (statsLabel.startsWith('#') ? String(statsBySha![row.gitSha!]) : `$${statsBySha![row.gitSha!].toFixed(0)}`) : '')] : []),
135 ...(hasFlags ? flagCols!.map(f => { const v = row.flags?.[f]; return v == null ? chalkGray(padFlag('-')) : v ? chalkGreen(padFlag('yes')) : chalkYellow(padFlag('no')) }) : []),
136 ...allSuiteNames.map(s => {
137 const sr = row.suites?.[s]
138 if (!sr) return chalkGray(padSuite('-'))
139 return sr.passed ? chalkGreen(padSuite('PASS')) : chalkRed(padSuite('FAIL'))
140 }),
141 ]
142 console.log(cols.join(''))
143 }