ReviewOS

also looking at this

pantry-pm/pantry

test(registry): qualify Zig packages against a real native consumer

#261
Merged glennmichael123 wants to merge fix/248-zig-package-hash into main
1 file +73 -0
packages/registry/src/zig-routes.test.tsmodified+73-0
Changes to packages/registry/src/zig-routes.test.ts
@@ -1,10 +1,14 @@
11import { afterEach, beforeEach, describe, expect, it } from 'bun:test'
22import { createHash } from 'node:crypto'
3import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
4import { tmpdir } from 'node:os'
5import { join } from 'node:path'
36import { InMemoryAnalytics } from './analytics'
47import { createLocalRegistry } from './registry'
58import { createServer } from './server'
69import { getAvailablePort } from './test-utils'
710import { InMemoryZigStorage } from './zig'
11import { managedRegistryZig } from './zig-native-hash'
812import { zigPackageFixture } from './zig-package-fixture'
913
1014// These routes execute the declared native compiler with a 120-second budget.
@@ -76,6 +80,75 @@ describe('Zig registry routes', () => {
7680 expect(body.dependency).toContain('.@"graphics-package-consumer"')
7781 }, 130_000)
7882
83 it('publishes a dependency entry a clean native Zig consumer resolves', async () => {
84 // The qualification #248 asked for. The registry used to advertise the
85 // tarball's SHA-256 under a `1220` prefix, which is a different identity
86 // from a Zig package hash: the archive downloaded and matched that digest,
87 // and a consumer still refused the dependency with "invalid hash:
88 // incomplete". Nothing in this suite noticed, because every assertion
89 // compared the registry against itself. This one hands the advertised
90 // entry to the compiler and lets it judge.
91 const zig = process.env.PANTRY_ZIG_BIN || managedRegistryZig() || Bun.which('zig')
92 expect(zig).toBeTruthy()
93
94 const body = await (await publish()).json() as any
95 expect(body.hash).not.toMatch(/^1220/)
96
97 // A cache of its own, so a hit from another test cannot stand in for a
98 // fetch that would otherwise fail.
99 const workspace = await mkdtemp(join(tmpdir(), 'pantry-zig-consumer-'))
100 const cache = join(workspace, 'cache')
101 try {
102 // Spawned asynchronously, never with spawnSync: the registry under test
103 // serves the tarball from THIS process, so blocking the loop while the
104 // compiler fetches deadlocks the pair until the test times out.
105 async function zigRun(...args: string[]): Promise<{ code: number, out: string, err: string }> {
106 const child = Bun.spawn([zig!, ...args], {
107 cwd: workspace,
108 env: { ...process.env, ZIG_GLOBAL_CACHE_DIR: cache },
109 stdout: 'pipe',
110 stderr: 'pipe',
111 })
112 const [out, err, code] = await Promise.all([
113 new Response(child.stdout).text(),
114 new Response(child.stderr).text(),
115 child.exited,
116 ])
117 return { code, out, err }
118 }
119
120 // What the compiler computes for the served archive has to be exactly
121 // what the registry told consumers to write down.
122 const fetched = await zigRun('fetch', body.tarballUrl)
123 // Not an empty stderr: the compiler writes housekeeping warnings there
124 // (a temp dir it could not remove, for one), and failing on those tests
125 // the host rather than the hash. The exit status and the hash are the
126 // contract.
127 expect(fetched.err).not.toContain('error:')
128 expect(fetched.code).toBe(0)
129 expect(fetched.out.trim()).toBe(body.hash)
130
131 // And the identity a consumer actually records has to be that same
132 // hash: `zig fetch --save` writes the dependency into build.zig.zon
133 // exactly as a person adding this package would.
134 // `zig fetch --save` edits a real project, so it needs both files.
135 await writeFile(join(workspace, 'build.zig'), 'pub fn build(_: *.Build) void {}\n')
136 await writeFile(
137 join(workspace, 'build.zig.zon'),
138 // The fingerprint is derived from the package name, so it is fixed for
139 // `.consumer` rather than arbitrary — the compiler rejects a wrong one
140 // and prints the value it wants, which is where this came from.
141 `.{\n .name = .consumer,\n .version = "0.0.0",\n .fingerprint = 0x705b372777268903,\n .paths = .{""},\n}\n`,
142 )
143 const saved = await zigRun('fetch', '--save=dep', body.tarballUrl)
144 expect(`${saved.code} ${saved.err}`.replace(/warning\(fetch\):[^\n]*\n?/g, '').trim()).toBe('0')
145 const manifest = await readFile(join(workspace, 'build.zig.zon'), 'utf8')
146 expect(manifest).toContain(body.hash)
147 expect(manifest).toContain(body.tarballUrl)
148 }
149 finally { await rm(workspace, { recursive: true, force: true }) }
150 }, 130_000)
151
79152 it('retrieves latest and exact version metadata and lists versions', async () => {
80153 expect((await publish('1.0.0')).status).toBe(201)
81154 expect((await publish('2.0.0')).status).toBe(201)