also looking at this
test(registry): qualify Zig packages against a real native consumer
#261
1 file
+73
-0
| @@ -1,10 +1,14 @@ | ||
| 1 | 1 | import { afterEach, beforeEach, describe, expect, it } from 'bun:test' |
| 2 | 2 | import { createHash } from 'node:crypto' |
| 3 | import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' | |
| 4 | import { tmpdir } from 'node:os' | |
| 5 | import { join } from 'node:path' | |
| 3 | 6 | import { InMemoryAnalytics } from './analytics' |
| 4 | 7 | import { createLocalRegistry } from './registry' |
| 5 | 8 | import { createServer } from './server' |
| 6 | 9 | import { getAvailablePort } from './test-utils' |
| 7 | 10 | import { InMemoryZigStorage } from './zig' |
| 11 | import { managedRegistryZig } from './zig-native-hash' | |
| 8 | 12 | import { zigPackageFixture } from './zig-package-fixture' |
| 9 | 13 | |
| 10 | 14 | // These routes execute the declared native compiler with a 120-second budget. |
| @@ -76,6 +80,75 @@ describe('Zig registry routes', () => { | ||
| 76 | 80 | expect(body.dependency).toContain('.@"graphics-package-consumer"') |
| 77 | 81 | }, 130_000) |
| 78 | 82 | |
| 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 | ||
| 79 | 152 | it('retrieves latest and exact version metadata and lists versions', async () => { |
| 80 | 153 | expect((await publish('1.0.0')).status).toBe(201) |
| 81 | 154 | expect((await publish('2.0.0')).status).toBe(201) |