-
Notifications
You must be signed in to change notification settings - Fork 0
Emit 0.2 build manifests with supplementary file support, plan-driven archive assembly, and filesystem-identity validation
#456
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
eXamadeus
merged 1 commit into
main
from
julian/07-23-emit_0.2_build_manifests_with_supplementary_file_support_plan-driven_archive_assembly_and_filesystem-identity_validation
Jul 24, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
148
packages/cli/src/__tests__/candidate-archive.e2e.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,148 @@ | ||
| import { afterAll, beforeAll, describe, expect, test } from 'bun:test' | ||
| import { existsSync } from 'node:fs' | ||
| import { mkdtemp, rm } from 'node:fs/promises' | ||
| import { tmpdir } from 'node:os' | ||
| import { join, resolve } from 'node:path' | ||
| import { gunzipSync } from 'node:zlib' | ||
| import { type GunzipFn, parseFacetArchive, validateFacetArchive } from '@agent-facets/protocol' | ||
|
|
||
| /** | ||
| * Reproducible candidate `0.2` archive / interop path (task 11.6). | ||
| * | ||
| * Builds a representative facet — a skill with a text companion and a binary | ||
| * companion, plus an archive-only README — using the freshly compiled | ||
| * candidate CLI (`dist/facet`, produced by `test:e2e`), then verifies the | ||
| * emitted `.facet` through the SAME protocol verifier a registry stage uses | ||
| * (`validateFacetArchive`). This proves the candidate producer emits a | ||
| * verifiable `0.2` artifact with exact deterministic membership, WITHOUT | ||
| * publishing or releasing the CLI — no Changeset is involved, and the binary | ||
| * is the local compile. | ||
| * | ||
| * A registry stage acceptance run can reproduce this exact archive by running | ||
| * `facet build` on the same source tree; the deterministic tar layout makes | ||
| * the bytes stable across machines. | ||
| */ | ||
|
|
||
| let testDir: string | ||
|
|
||
| beforeAll(async () => { | ||
| testDir = await mkdtemp(join(tmpdir(), 'cli-candidate-archive-')) | ||
| }) | ||
|
|
||
| afterAll(async () => { | ||
| await rm(testDir, { recursive: true, force: true }) | ||
| }) | ||
|
|
||
| const CLI_PATH = resolve(import.meta.dir, '../../dist/facet') | ||
|
|
||
| if (!existsSync(CLI_PATH)) { | ||
| throw new Error(`[e2e] dist/facet not found at ${CLI_PATH}.\nBuild the CLI first: bun run --cwd packages/cli build`) | ||
| } | ||
|
|
||
| async function runCli(cwd: string, ...args: string[]) { | ||
| const facetDir = await mkdtemp(join(testDir, 'facet-dir-')) | ||
| const proc = Bun.spawn([CLI_PATH, ...args], { | ||
| cwd, | ||
| stdout: 'pipe', | ||
| stderr: 'pipe', | ||
| env: { ...process.env, NO_COLOR: '1', FACET_DIR: facetDir }, | ||
| }) | ||
| const stdout = await new Response(proc.stdout).text() | ||
| const stderr = await new Response(proc.stderr).text() | ||
| const exitCode = await proc.exited | ||
| return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode } | ||
| } | ||
|
|
||
| const REPRESENTATIVE_BINARY = new Uint8Array([0x89, 0x50, 0x4e, 0x47, 0x00, 0x01, 0xff, 0xfe]) | ||
|
|
||
| /** Write the representative facet source tree into `dir`. */ | ||
| async function writeRepresentativeFacet(dir: string): Promise<void> { | ||
| await Bun.write(join(dir, 'skills/planning/SKILL.md'), '# planning\n\nPlan things.\n') | ||
| await Bun.write(join(dir, 'skills/planning/references/api.md'), '# API reference\n') | ||
| await Bun.write(join(dir, 'skills/planning/assets/logo.bin'), REPRESENTATIVE_BINARY) | ||
| await Bun.write(join(dir, 'README.md'), '# Representative facet\n\nShips a README.\n') | ||
| await Bun.write( | ||
| join(dir, 'facet.json'), | ||
| JSON.stringify( | ||
| { | ||
| name: 'representative', | ||
| version: '1.0.0', | ||
| description: 'A representative 0.2 facet for stage interop', | ||
| files: ['README.md'], | ||
| skills: { | ||
| planning: { description: 'Planning skill', files: ['references/api.md', 'assets/logo.bin'] }, | ||
| }, | ||
| }, | ||
| null, | ||
| 2, | ||
| ), | ||
| ) | ||
| } | ||
|
|
||
| const EXPECTED_MEMBERSHIP = [ | ||
| 'README.md', | ||
| 'facet.json', | ||
| 'skills/planning/SKILL.md', | ||
| 'skills/planning/assets/logo.bin', | ||
| 'skills/planning/references/api.md', | ||
| ].sort() | ||
|
|
||
| const gunzip: GunzipFn = async (bytes) => { | ||
| try { | ||
| return { ok: true, bytes: new Uint8Array(gunzipSync(bytes)) } | ||
| } catch { | ||
| return { ok: false, reason: 'corrupt' } | ||
| } | ||
| } | ||
|
|
||
| describe('candidate 0.2 archive interop', () => { | ||
| test('the candidate CLI builds a verifiable 0.2 archive with exact membership', async () => { | ||
| const dir = await mkdtemp(join(testDir, 'build-')) | ||
| await writeRepresentativeFacet(dir) | ||
|
|
||
| const built = await runCli(dir, 'build') | ||
| expect(built.exitCode).toBe(0) | ||
|
|
||
| const archivePath = join(dir, 'dist/representative-1.0.0.facet') | ||
| expect(existsSync(archivePath)).toBe(true) | ||
| const outerBytes = new Uint8Array(await Bun.file(archivePath).arrayBuffer()) | ||
|
|
||
| // Parse the outer container: the build manifest must be current 0.2. | ||
| const parsed = parseFacetArchive(outerBytes) | ||
| if (!parsed.ok) expect.unreachable() | ||
| expect(parsed.data.manifest.facetVersion).toBe(0.2) | ||
|
|
||
| // Verify through the shared registry-grade verifier. | ||
| const verified = await validateFacetArchive(outerBytes, { gunzip }) | ||
| if (!verified.ok) expect.unreachable() | ||
| if (verified.data.archiveVersion !== 0.2) expect.unreachable() | ||
|
|
||
| // Exact 0.2 membership, including the archive-only README and both | ||
| // skill companions (text + binary). | ||
| const observed = verified.data.entries.map((e) => e.path).sort() | ||
| expect(observed).toEqual(EXPECTED_MEMBERSHIP) | ||
|
|
||
| // The archive-only README is classified as such (never a primary asset), | ||
| // and the binary companion is grouped with its owning skill. | ||
| const readme = verified.data.entries.find((e) => e.path === 'README.md') | ||
| expect(readme?.kind).toBe('archive-only') | ||
| const logo = verified.data.entries.find((e) => e.path === 'skills/planning/assets/logo.bin') | ||
| if (logo?.kind !== 'skill-companion') expect.unreachable() | ||
| expect(logo.skill).toBe('planning') | ||
| expect(logo.bytes).toEqual(REPRESENTATIVE_BINARY) | ||
| }) | ||
|
|
||
| test('two candidate builds of the same source are byte-identical (deterministic)', async () => { | ||
| const build = async (name: string): Promise<Uint8Array> => { | ||
| const dir = await mkdtemp(join(testDir, `${name}-`)) | ||
| await writeRepresentativeFacet(dir) | ||
| const built = await runCli(dir, 'build') | ||
| expect(built.exitCode).toBe(0) | ||
| const archivePath = join(dir, 'dist/representative-1.0.0.facet') | ||
| return new Uint8Array(await Bun.file(archivePath).arrayBuffer()) | ||
| } | ||
| const a = await build('det-a') | ||
| const b = await build('det-b') | ||
| expect(Array.from(a)).toEqual(Array.from(b)) | ||
| }) | ||
| }) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Assert at least one primary archive entry.
A regressed
files: ['facet.json']payload still passes, despite this fixture containing thehelperskill. Assert its primary path too.Proposed test update
📝 Committable suggestion