Skip to content

Commit cd7a1d1

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
test(design-diff): exercise pinned engine in cloud
1 parent ee213d9 commit cd7a1d1

2 files changed

Lines changed: 343 additions & 0 deletions

File tree

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
name: Design diff cloud smoke
2+
3+
on:
4+
push:
5+
branches: [codex/design-diff-cloud-smoke]
6+
7+
permissions:
8+
contents: read
9+
10+
concurrency:
11+
group: design-diff-smoke-${{ github.ref }}
12+
cancel-in-progress: true
13+
14+
jobs:
15+
smoke:
16+
runs-on: ${{ (vars.CI_PROVIDER == '' || vars.CI_PROVIDER == 'blacksmith') && 'blacksmith-2vcpu-ubuntu-2404' || 'ubuntu-latest' }}
17+
timeout-minutes: 15
18+
steps:
19+
- name: Checkout pinned engine
20+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
21+
with:
22+
ref: ee213d9a7e4ad5ec944d4d55003c378b2f2787d4
23+
path: engine
24+
fetch-depth: 0
25+
persist-credentials: false
26+
27+
- name: Checkout test runner
28+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
29+
with:
30+
ref: ${{ github.sha }}
31+
path: harness
32+
persist-credentials: false
33+
sparse-checkout: scripts/design-diff/cloud-smoke.ts
34+
sparse-checkout-cone-mode: false
35+
36+
- name: Setup Bun
37+
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
38+
with:
39+
bun-version: 1.4.1
40+
41+
- name: Install pinned dependencies
42+
working-directory: engine
43+
run: bun install --frozen-lockfile --ignore-scripts
44+
45+
- name: Exercise controlled diffs
46+
env:
47+
REPORT_DIRECTORY: ${{ runner.temp }}/design-diff-smoke
48+
run: bun --no-env-file harness/scripts/design-diff/cloud-smoke.ts --repo engine --output "$REPORT_DIRECTORY"
49+
50+
- name: Retain reports and test results
51+
if: ${{ always() }}
52+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
53+
with:
54+
name: design-diff-smoke-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
55+
path: ${{ runner.temp }}/design-diff-smoke/*.json
56+
retention-days: 7
57+
if-no-files-found: error

‎scripts/design-diff/cloud-smoke.ts‎

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
import assert from 'node:assert/strict'
2+
import { execFileSync, spawnSync } from 'node:child_process'
3+
import {
4+
closeSync,
5+
mkdirSync,
6+
mkdtempSync,
7+
openSync,
8+
readdirSync,
9+
readFileSync,
10+
rmSync,
11+
writeFileSync,
12+
} from 'node:fs'
13+
import { tmpdir } from 'node:os'
14+
import path from 'node:path'
15+
import { parseArgs } from 'node:util'
16+
import type { Category, Report } from '#design-diff/types'
17+
18+
const ENGINE = 'ee213d9a7e4ad5ec944d4d55003c378b2f2787d4'
19+
const BUTTON = 'packages/emcn/src/components/button/button.tsx'
20+
const CONSTANTS =
21+
'apps/sim/app/workspace/[workspaceId]/home/components/user-input/components/constants.ts'
22+
interface Scenario {
23+
name: string
24+
edit?: [file: string, before: string, after: string]
25+
category?: Category
26+
}
27+
interface Result {
28+
name: string
29+
head: string | null
30+
passed: boolean
31+
seconds?: number
32+
peakRssBytes?: number
33+
exitCode?: number | null
34+
signal?: string | null
35+
flagged?: boolean | null
36+
findingCount?: number
37+
failedCheck?: string
38+
}
39+
const scenarios: Scenario[] = [
40+
{
41+
name: 'shared-button-shape',
42+
edit: [BUTTON, 'rounded-[5px]', 'rounded-none'],
43+
category: 'shape-effects',
44+
},
45+
{
46+
name: 'local-button-colour',
47+
edit: [CONSTANTS, 'bg-[#383838]', 'bg-[#E11D48]'],
48+
category: 'colour',
49+
},
50+
{
51+
name: 'local-button-padding',
52+
edit: [CONSTANTS, 'rounded-full border-0 p-0', 'rounded-full border-0 p-2'],
53+
category: 'dimensions',
54+
},
55+
{
56+
name: 'comment-only',
57+
edit: [BUTTON, 'a section-header action.', 'an action in a section header.'],
58+
},
59+
{
60+
name: 'unproven-movement',
61+
edit: [CONSTANTS, "p-0 transition-colors'", "p-0 transition-colors translate-x-2'"],
62+
category: 'layout',
63+
},
64+
{ name: 'missing-revision' },
65+
]
66+
scenarios.push({ ...scenarios[0], name: 'shared-button-shape-repeat' })
67+
68+
/** Runs only the pinned analyzer; synthetic application edits stay in Git objects. */
69+
function main() {
70+
const { values } = parseArgs({
71+
options: { repo: { type: 'string' }, output: { type: 'string' } },
72+
strict: true,
73+
})
74+
assert(values.repo && values.output, 'Both --repo and --output are required')
75+
const repo = path.resolve(values.repo)
76+
const output = path.resolve(values.output)
77+
mkdirSync(output, { recursive: true })
78+
assert.equal(readdirSync(output).length, 0, 'Output directory must be empty')
79+
const temporary = mkdtempSync(path.join(tmpdir(), 'design-diff-smoke-'))
80+
const {
81+
DESIGN_DIFF_PR: _pr,
82+
DESIGN_DIFF_ENGINE_SHA: _engine,
83+
HEAD_SHA: _head,
84+
...environment
85+
} = process.env
86+
const env = { ...environment, LC_ALL: 'C', GIT_CONFIG_NOSYSTEM: '1', GIT_NO_REPLACE_OBJECTS: '1' }
87+
const summary = {
88+
engineSha: ENGINE,
89+
workflowSha: process.env.GITHUB_SHA ?? null,
90+
runId: process.env.GITHUB_RUN_ID ?? null,
91+
runAttempt: process.env.GITHUB_RUN_ATTEMPT ?? null,
92+
base: ENGINE,
93+
platform: process.platform,
94+
architecture: process.arch,
95+
bunVersion: execFileSync(process.execPath, ['--version']).toString().trim(),
96+
status: 'running',
97+
deterministic: false,
98+
results: [] as Result[],
99+
}
100+
const save = () =>
101+
writeFileSync(path.join(output, 'test-results.json'), `${JSON.stringify(summary, null, 2)}\n`)
102+
const git = (args: string[], input?: string, extraEnv = {}) =>
103+
execFileSync('git', args, {
104+
cwd: repo,
105+
env: { ...env, ...extraEnv },
106+
input,
107+
stdio: ['pipe', 'pipe', 'pipe'],
108+
})
109+
.toString()
110+
.trim()
111+
save()
112+
try {
113+
assert.equal(summary.bunVersion, '1.4.1', 'Use the repository Bun version')
114+
assert.equal(git(['rev-parse', 'HEAD']), ENGINE, 'Engine checkout must match the pin')
115+
assert.equal(
116+
git(['status', '--porcelain', '--untracked-files=no']),
117+
'',
118+
'Engine checkout must be clean'
119+
)
120+
for (const scenario of scenarios) {
121+
const result: Result = { name: scenario.name, head: null, passed: false }
122+
summary.results.push(result)
123+
save()
124+
let stage = 'prepare-comparison'
125+
try {
126+
let head = '0'.repeat(40)
127+
if (scenario.edit) {
128+
const [file, before, after] = scenario.edit
129+
const original = execFileSync('git', ['show', `${ENGINE}:${file}`], {
130+
cwd: repo,
131+
env,
132+
}).toString()
133+
assert.equal(
134+
original.split(before).length,
135+
2,
136+
'Fixture replacement must match exactly once'
137+
)
138+
const blob = git(['hash-object', '-w', '--stdin'], original.replace(before, after))
139+
const index = { GIT_INDEX_FILE: path.join(temporary, 'index') }
140+
git(['read-tree', ENGINE], undefined, index)
141+
const mode = git(['ls-tree', ENGINE, '--', file]).split(' ')[0]
142+
git(['update-index', '--add', '--cacheinfo', mode, blob, file], undefined, index)
143+
const tree = git(['write-tree'], undefined, index)
144+
head = git(['commit-tree', tree, '-p', ENGINE], 'Design diff isolated fixture\n', {
145+
GIT_AUTHOR_NAME: 'Design diff smoke',
146+
GIT_AUTHOR_EMAIL: 'design-diff@example.invalid',
147+
GIT_COMMITTER_NAME: 'Design diff smoke',
148+
GIT_COMMITTER_EMAIL: 'design-diff@example.invalid',
149+
GIT_AUTHOR_DATE: '2000-01-01T00:00:00Z',
150+
GIT_COMMITTER_DATE: '2000-01-01T00:00:00Z',
151+
})
152+
}
153+
result.head = head
154+
save()
155+
stage = 'engine-execution'
156+
const reportFile = path.join(output, `${scenario.name}.json`)
157+
const metricsFile = path.join(temporary, 'metrics')
158+
const descriptor = openSync(metricsFile, 'w', 0o600)
159+
const start = performance.now()
160+
const timing = process.platform === 'darwin' ? ['-l'] : ['-f', 'design-diff-peak-kib=%M']
161+
let execution: ReturnType<typeof spawnSync>
162+
try {
163+
execution = spawnSync(
164+
'/usr/bin/time',
165+
[
166+
...timing,
167+
process.execPath,
168+
'--no-env-file',
169+
'scripts/design-diff/cli.ts',
170+
'--base',
171+
ENGINE,
172+
'--head',
173+
head,
174+
'--output',
175+
reportFile,
176+
],
177+
{
178+
cwd: repo,
179+
env,
180+
stdio: ['ignore', 'ignore', descriptor],
181+
}
182+
)
183+
} finally {
184+
closeSync(descriptor)
185+
}
186+
result.seconds = Math.round((performance.now() - start) / 10) / 100
187+
result.exitCode = execution.status
188+
result.signal = execution.signal
189+
assert(!execution.error && !execution.signal, 'Analyzer execution must finish normally')
190+
stage = 'memory-measurement'
191+
const metrics = readFileSync(metricsFile, 'utf8')
192+
const peak =
193+
process.platform === 'darwin'
194+
? metrics.match(/(\d+)\s+maximum resident set size/)
195+
: metrics.match(/design-diff-peak-kib=(\d+)/)
196+
assert(peak, 'Peak memory measurement is required')
197+
result.peakRssBytes = Number(peak[1]) * (process.platform === 'darwin' ? 1 : 1024)
198+
stage = 'report-identity'
199+
const report = JSON.parse(readFileSync(reportFile, 'utf8')) as Report
200+
assert.equal(report.schemaVersion, '2.0.0')
201+
assert.equal(report.engineVersion, '0.2.0')
202+
assert.equal(report.policyVersion, '2.0.0')
203+
assert.equal(report.context, undefined)
204+
result.flagged = report.flagged
205+
result.findingCount = report.findings.length
206+
stage = 'expected-decision'
207+
if (!scenario.edit) {
208+
assert.equal(execution.status, 1)
209+
assert.equal(report.status, 'failed')
210+
assert.equal(report.flagged, null)
211+
assert.equal(report.commits, null)
212+
assert.equal(report.findings.length, 0)
213+
assert(report.error)
214+
} else {
215+
assert.equal(execution.status, 0)
216+
assert.equal(report.status, 'completed')
217+
assert.deepEqual(report.commits, { base: ENGINE, head, mergeBase: ENGINE })
218+
assert.equal(report.flagged, Boolean(scenario.category))
219+
assert(report.findings.every((finding) => ['flag', 'exempt'].includes(finding.decision)))
220+
assert.equal(report.findings.length, scenario.category ? 1 : 0)
221+
if (scenario.category) {
222+
stage = 'source-and-category'
223+
const finding = report.findings[0]
224+
assert.equal(finding.decision, 'flag')
225+
assert.equal(finding.source.before?.file, scenario.edit[0])
226+
assert.equal(finding.source.after?.file, scenario.edit[0])
227+
assert.equal(
228+
finding.source.before?.blob,
229+
git(['rev-parse', `${ENGINE}:${scenario.edit[0]}`])
230+
)
231+
assert.equal(
232+
finding.source.after?.blob,
233+
git(['rev-parse', `${head}:${scenario.edit[0]}`])
234+
)
235+
assert(finding.categories.includes(scenario.category))
236+
assert(finding.reason.length > 0)
237+
if (scenario.edit[0] === BUTTON) {
238+
stage = 'shared-consumers'
239+
assert.equal(finding.impact.basis, 'resolved-static-references')
240+
assert(finding.impact.after.fileCount > 1)
241+
assert(finding.impact.after.references.some((ref) => ref.location.file !== BUTTON))
242+
assert(finding.example)
243+
}
244+
}
245+
}
246+
if (scenario.name.endsWith('-repeat')) {
247+
stage = 'byte-identical-report'
248+
summary.deterministic = readFileSync(reportFile).equals(
249+
readFileSync(path.join(output, 'shared-button-shape.json'))
250+
)
251+
assert(summary.deterministic)
252+
}
253+
result.passed = true
254+
} catch {
255+
result.failedCheck = stage
256+
}
257+
save()
258+
process.stdout.write(
259+
`${result.name}: ${result.passed ? 'PASS' : 'FAIL'} (${result.seconds ?? 0}s, peak RSS ${result.peakRssBytes ?? 0} bytes)\n`
260+
)
261+
}
262+
assert.equal(
263+
git(['status', '--porcelain', '--untracked-files=no']),
264+
'',
265+
'Engine files must remain unchanged'
266+
)
267+
summary.status =
268+
summary.results.every((result) => result.passed) && summary.deterministic
269+
? 'passed'
270+
: 'failed'
271+
} finally {
272+
if (summary.status === 'running') summary.status = 'failed'
273+
save()
274+
rmSync(temporary, { recursive: true, force: true })
275+
}
276+
if (summary.status !== 'passed') process.exitCode = 1
277+
}
278+
279+
try {
280+
main()
281+
} catch {
282+
process.stderr.write(
283+
'Design diff smoke setup or execution failed. Check the retained test results.\n'
284+
)
285+
process.exitCode = 1
286+
}

0 commit comments

Comments
 (0)