From e20dea504cbc3a5d10b1fbdf445857e9f325d229 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 20:14:41 +0000 Subject: [PATCH 1/3] test: align hook expectations with sorted normalization (#180) and the example's semantic event route (#182) Both features merged while main CI was already red, so their outdated sibling tests were never caught: normalizeHooks now orders hooks by stable id, the rsc-agent-runtime example declares one tool/after event route instead of per-host prebuilt hooks, and the workbench therefore exposes the simulatable Hooks capability page. --- .../tests/dev-provider.integration.test.ts | 16 ++++++++++------ packages/agent-bundle/tests/hooks.test.ts | 14 ++++++++------ .../tests/runtime-playground.e2e.test.ts | 7 +++---- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts b/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts index eda04178e..e9c2b2f9a 100644 --- a/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts +++ b/examples/rsc-agent-runtime/tests/dev-provider.integration.test.ts @@ -375,12 +375,16 @@ test('declares an optional runtime while keeping Claude and Codex artifacts buil provider: './src/dev/provider.ts', servers: [expect.objectContaining({ name: 'timeline', transport: 'stdio' })], }); - // One prebuilt hook declaration per host, each carrying its own `--host` - // argument. - expect(prepared.model?.hooks).toEqual(expect.arrayContaining([ - expect.objectContaining({ prebuiltPath: 'runtime/hook/index.js', targets: ['claude'] }), - expect.objectContaining({ prebuiltPath: 'runtime/hook/index.js', targets: ['codex'] }), - ])); + // The example declares one semantic event route (src/events/tool/after.tsx) + // that serves both hosts through the generated native wrappers. + expect(prepared.model?.hooks).toEqual([ + expect.objectContaining({ + event: 'afterTool', + eventRoute: expect.objectContaining({ event: 'tool/after' }), + id: 'hook:event-route:tool-after', + targets: ['claude', 'codex'], + }), + ]); const artifact = await new ArtifactService({ epochStore: new EpochStore({ projectRoot: root }) }).build(prepared); if (artifact.outcome !== 'succeeded') throw new Error(JSON.stringify(artifact.diagnostics)); diff --git a/packages/agent-bundle/tests/hooks.test.ts b/packages/agent-bundle/tests/hooks.test.ts index 11f5151e0..b1356b9a1 100644 --- a/packages/agent-bundle/tests/hooks.test.ts +++ b/packages/agent-bundle/tests/hooks.test.ts @@ -1306,13 +1306,15 @@ it('normalizes a mixed hook fixture and reports malformed hook declarations', as targets: hook.targets, timeout: hook.timeout, tools: hook.tools, + // Normalization orders hooks by their stable id, not by declaration + // order, so mixed configured and conventional hooks emit deterministically. }))).toEqual([ { - event: 'sessionStart', - name: 'session-start-session-start-7ab7e8a5', + event: 'afterTool', + name: 'after-tool-record-87785f02', targets: ['claude', 'codex'], timeout: undefined, - tools: [], + tools: ['file.write', 'shell'], }, { event: 'beforeTool', @@ -1329,11 +1331,11 @@ it('normalizes a mixed hook fixture and reports malformed hook declarations', as tools: ['shell'], }, { - event: 'afterTool', - name: 'after-tool-record-87785f02', + event: 'sessionStart', + name: 'session-start-session-start-7ab7e8a5', targets: ['claude', 'codex'], timeout: undefined, - tools: ['file.write', 'shell'], + tools: [], }, { event: 'stop', diff --git a/packages/workbench/tests/runtime-playground.e2e.test.ts b/packages/workbench/tests/runtime-playground.e2e.test.ts index 3dcfa39fb..d042897df 100644 --- a/packages/workbench/tests/runtime-playground.e2e.test.ts +++ b/packages/workbench/tests/runtime-playground.e2e.test.ts @@ -35,10 +35,9 @@ e2e('renders the capability-gated Runtime sibling in the real RSC workbench', { await expect(page.getByRole('link', { name: 'Inspector' })).toHaveCount(0, { timeout: browserTimeout }); await expect(page.getByRole('link', { name: 'Runtime' })).toBeVisible({ timeout: browserTimeout }); - // The example's hooks are prebuilt payload commands packaged like native - // hooks, so no simulatable hook wrappers exist and the Hooks and - // Playground capability pages stay hidden alongside them. - await expect(page.getByRole('link', { name: 'Hooks' })).toHaveCount(0, { timeout: browserTimeout }); + // The example declares a semantic event route rendered through generated + // native wrappers, so the simulatable Hooks capability page is available. + await expect(page.getByRole('link', { exact: true, name: 'Hooks' })).toBeVisible({ timeout: browserTimeout }); for (const sibling of ['artifacts', 'logs'] as const) { await page.goto(workbenchUrl(fixture.url, sibling)); await expect(page.locator(`#${sibling}`)).toBeVisible({ timeout: browserTimeout }); From f71e2d005a248fef200f6f1d8ef9444e75dafb17 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 20:40:17 +0000 Subject: [PATCH 2/3] fix(ci): keep route-unit proofs out of the example plain test pool --- examples/rsc-agent-runtime/rstest.config.ts | 4 + .../agent-bundle/src/core/project-context.ts | 80 ++++++++----------- 2 files changed, 36 insertions(+), 48 deletions(-) diff --git a/examples/rsc-agent-runtime/rstest.config.ts b/examples/rsc-agent-runtime/rstest.config.ts index 9f7777633..ee62079f1 100644 --- a/examples/rsc-agent-runtime/rstest.config.ts +++ b/examples/rsc-agent-runtime/rstest.config.ts @@ -3,6 +3,10 @@ import { defineConfig } from '@rstest/core'; import { timeScale } from './tests/support/time-scale.ts'; export default defineConfig({ + // Route-unit proofs need the framework-generated configuration + // (rstest.route-unit.config.ts via `pnpm test:routes`), so they are not + // swept into this plain Node pool where no test manifest is registered. + exclude: ['tests/route-unit/**'], include: ['tests/**/*.test.{ts,tsx}'], pool: { maxWorkers: 1 }, testEnvironment: 'node', diff --git a/packages/agent-bundle/src/core/project-context.ts b/packages/agent-bundle/src/core/project-context.ts index 0936d916f..02f92cc0a 100644 --- a/packages/agent-bundle/src/core/project-context.ts +++ b/packages/agent-bundle/src/core/project-context.ts @@ -1,13 +1,13 @@ import { readFileSync, realpathSync } from 'node:fs'; import { isAbsolute, join, relative, resolve } from 'node:path'; -import type { SkillHostDocument, SkillIr, SkillSidecarRef } from '../skills/ir.ts'; -import type { Diagnostic } from './diagnostics.ts'; import { digest } from './digest.ts'; import { deepFreeze } from './freeze.ts'; import { isInsideOrEqual } from './paths.ts'; import { snapshotStrictJsonValue } from './strict-json.ts'; +import type { Diagnostic } from './diagnostics.ts'; import type { NormalizedPlugin, SourceProvenance } from './types.ts'; +import type { SkillHostDocument, SkillIr, SkillSidecarRef } from '../skills/ir.ts'; /** One deterministic, byte-addressed authored input in a project identity. */ export interface ProjectSourceInput { @@ -211,45 +211,45 @@ const canonicalProvenance = (root: string, provenance: SourceProvenance): Source sourcePath: canonicalCompilerPath(root, provenance.sourcePath, 'Model provenance path'), }); -const canonicalDiagnostic = (root: string, diagnostic: Diagnostic): Diagnostic => ({ - ...diagnostic, - ...(diagnostic.generatedPath === undefined - ? {} - : { generatedPath: canonicalCompilerPath(root, diagnostic.generatedPath, 'Diagnostic generated path') }), - ...(diagnostic.sourcePath === undefined - ? {} - : { sourcePath: canonicalCompilerPath(root, diagnostic.sourcePath, 'Diagnostic source path') }), -}); +const canonicalSkillDiagnostics = (root: string, diagnostics: readonly Diagnostic[]): readonly Diagnostic[] => + diagnostics.map((diagnostic) => ({ + ...diagnostic, + ...(diagnostic.sourcePath === undefined + ? {} + : { sourcePath: canonicalCompilerPath(root, diagnostic.sourcePath, 'Skill diagnostic source path') }), + })); -const canonicalSkillSidecar = (root: string, sidecar: SkillSidecarRef): SkillSidecarRef => ({ - ...sidecar, - ...(sidecar.source === undefined - ? {} - : { source: canonicalCompilerPath(root, sidecar.source, 'Skill sidecar source path') }), -}); +const canonicalSkillSidecars = (root: string, sidecars: readonly SkillSidecarRef[]): readonly SkillSidecarRef[] => + sidecars.map((sidecar) => ({ + ...sidecar, + ...(sidecar.source === undefined + ? {} + : { source: canonicalCompilerPath(root, sidecar.source, 'Skill sidecar source path') }), + })); -const canonicalSkillIr = (root: string, skillIr: SkillIr): SkillIr => ({ - ...skillIr, - diagnostics: skillIr.diagnostics.map((diagnostic) => canonicalDiagnostic(root, diagnostic)), - resources: skillIr.resources.map((resource) => ({ +const canonicalSkillIr = (root: string, ir: SkillIr): SkillIr => ({ + ...ir, + diagnostics: canonicalSkillDiagnostics(root, ir.diagnostics), + resources: ir.resources.map((resource) => ({ ...resource, - source: canonicalCompilerPath(root, resource.source, 'Skill IR resource path'), + source: canonicalCompilerPath(root, resource.source, 'Skill resource path'), })), - sidecars: skillIr.sidecars.map((sidecar) => canonicalSkillSidecar(root, sidecar)), - source: canonicalCompilerPath(root, skillIr.source, 'Skill IR source path'), + sidecars: canonicalSkillSidecars(root, ir.sidecars), + source: canonicalCompilerPath(root, ir.source, 'Skill source path'), }); -const canonicalHostDocuments = ( +const canonicalSkillHostDocuments = ( root: string, - hostDocuments: Readonly>, -): Readonly> => - Object.fromEntries(Object.entries(hostDocuments) + documents: Readonly>, +): Readonly> => Object.fromEntries( + Object.entries(documents) .sort(([left], [right]) => left.localeCompare(right)) .map(([host, document]) => [host, { ...document, - diagnostics: document.diagnostics.map((diagnostic) => canonicalDiagnostic(root, diagnostic)), - sidecars: document.sidecars.map((sidecar) => canonicalSkillSidecar(root, sidecar)), - }])); + diagnostics: canonicalSkillDiagnostics(root, document.diagnostics), + sidecars: canonicalSkillSidecars(root, document.sidecars), + }]), +); const modelPathReferences = (model: NormalizedPlugin): readonly string[] => [ model.metadata.provenance.sourcePath, @@ -268,22 +268,6 @@ const modelPathReferences = (model: NormalizedPlugin): readonly string[] => [ skill.provenance.sourcePath, skill.source, ...skill.resources.map((resource) => resource.source), - ...(skill.skillIr === undefined - ? [] - : [ - skill.skillIr.source, - ...skill.skillIr.resources.map((resource) => resource.source), - ...skill.skillIr.sidecars.flatMap((sidecar) => sidecar.source === undefined ? [] : [sidecar.source]), - ...skill.skillIr.diagnostics.flatMap((diagnostic) => - diagnostic.sourcePath === undefined ? [] : [diagnostic.sourcePath]), - ]), - ...(skill.hostDocuments === undefined - ? [] - : Object.values(skill.hostDocuments).flatMap((document) => [ - ...document.diagnostics.flatMap((diagnostic) => - diagnostic.sourcePath === undefined ? [] : [diagnostic.sourcePath]), - ...document.sidecars.flatMap((sidecar) => sidecar.source === undefined ? [] : [sidecar.source]), - ])), ]), ...model.scripts.flatMap((script) => [script.provenance.sourcePath, script.source]), ...model.mcpServers.flatMap((server) => [ @@ -427,7 +411,7 @@ export const canonicalizeNormalizedModel = ( dir: canonicalCompilerPath(root, skill.dir, 'Skill directory path'), ...(skill.hostDocuments === undefined ? {} - : { hostDocuments: canonicalHostDocuments(root, skill.hostDocuments) }), + : { hostDocuments: canonicalSkillHostDocuments(root, skill.hostDocuments) }), provenance: canonicalProvenance(root, skill.provenance), resources: skill.resources.map((resource) => ({ ...resource, From 08de34771ab72c2485db5e65597fa3e4961bd056 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 20:40:53 +0000 Subject: [PATCH 3/3] fix(ci): defer to the mainline Skill IR digest canonicalization (#191) --- .../agent-bundle/src/core/project-context.ts | 80 +++++++++++-------- 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/packages/agent-bundle/src/core/project-context.ts b/packages/agent-bundle/src/core/project-context.ts index 02f92cc0a..0936d916f 100644 --- a/packages/agent-bundle/src/core/project-context.ts +++ b/packages/agent-bundle/src/core/project-context.ts @@ -1,13 +1,13 @@ import { readFileSync, realpathSync } from 'node:fs'; import { isAbsolute, join, relative, resolve } from 'node:path'; +import type { SkillHostDocument, SkillIr, SkillSidecarRef } from '../skills/ir.ts'; +import type { Diagnostic } from './diagnostics.ts'; import { digest } from './digest.ts'; import { deepFreeze } from './freeze.ts'; import { isInsideOrEqual } from './paths.ts'; import { snapshotStrictJsonValue } from './strict-json.ts'; -import type { Diagnostic } from './diagnostics.ts'; import type { NormalizedPlugin, SourceProvenance } from './types.ts'; -import type { SkillHostDocument, SkillIr, SkillSidecarRef } from '../skills/ir.ts'; /** One deterministic, byte-addressed authored input in a project identity. */ export interface ProjectSourceInput { @@ -211,45 +211,45 @@ const canonicalProvenance = (root: string, provenance: SourceProvenance): Source sourcePath: canonicalCompilerPath(root, provenance.sourcePath, 'Model provenance path'), }); -const canonicalSkillDiagnostics = (root: string, diagnostics: readonly Diagnostic[]): readonly Diagnostic[] => - diagnostics.map((diagnostic) => ({ - ...diagnostic, - ...(diagnostic.sourcePath === undefined - ? {} - : { sourcePath: canonicalCompilerPath(root, diagnostic.sourcePath, 'Skill diagnostic source path') }), - })); +const canonicalDiagnostic = (root: string, diagnostic: Diagnostic): Diagnostic => ({ + ...diagnostic, + ...(diagnostic.generatedPath === undefined + ? {} + : { generatedPath: canonicalCompilerPath(root, diagnostic.generatedPath, 'Diagnostic generated path') }), + ...(diagnostic.sourcePath === undefined + ? {} + : { sourcePath: canonicalCompilerPath(root, diagnostic.sourcePath, 'Diagnostic source path') }), +}); -const canonicalSkillSidecars = (root: string, sidecars: readonly SkillSidecarRef[]): readonly SkillSidecarRef[] => - sidecars.map((sidecar) => ({ - ...sidecar, - ...(sidecar.source === undefined - ? {} - : { source: canonicalCompilerPath(root, sidecar.source, 'Skill sidecar source path') }), - })); +const canonicalSkillSidecar = (root: string, sidecar: SkillSidecarRef): SkillSidecarRef => ({ + ...sidecar, + ...(sidecar.source === undefined + ? {} + : { source: canonicalCompilerPath(root, sidecar.source, 'Skill sidecar source path') }), +}); -const canonicalSkillIr = (root: string, ir: SkillIr): SkillIr => ({ - ...ir, - diagnostics: canonicalSkillDiagnostics(root, ir.diagnostics), - resources: ir.resources.map((resource) => ({ +const canonicalSkillIr = (root: string, skillIr: SkillIr): SkillIr => ({ + ...skillIr, + diagnostics: skillIr.diagnostics.map((diagnostic) => canonicalDiagnostic(root, diagnostic)), + resources: skillIr.resources.map((resource) => ({ ...resource, - source: canonicalCompilerPath(root, resource.source, 'Skill resource path'), + source: canonicalCompilerPath(root, resource.source, 'Skill IR resource path'), })), - sidecars: canonicalSkillSidecars(root, ir.sidecars), - source: canonicalCompilerPath(root, ir.source, 'Skill source path'), + sidecars: skillIr.sidecars.map((sidecar) => canonicalSkillSidecar(root, sidecar)), + source: canonicalCompilerPath(root, skillIr.source, 'Skill IR source path'), }); -const canonicalSkillHostDocuments = ( +const canonicalHostDocuments = ( root: string, - documents: Readonly>, -): Readonly> => Object.fromEntries( - Object.entries(documents) + hostDocuments: Readonly>, +): Readonly> => + Object.fromEntries(Object.entries(hostDocuments) .sort(([left], [right]) => left.localeCompare(right)) .map(([host, document]) => [host, { ...document, - diagnostics: canonicalSkillDiagnostics(root, document.diagnostics), - sidecars: canonicalSkillSidecars(root, document.sidecars), - }]), -); + diagnostics: document.diagnostics.map((diagnostic) => canonicalDiagnostic(root, diagnostic)), + sidecars: document.sidecars.map((sidecar) => canonicalSkillSidecar(root, sidecar)), + }])); const modelPathReferences = (model: NormalizedPlugin): readonly string[] => [ model.metadata.provenance.sourcePath, @@ -268,6 +268,22 @@ const modelPathReferences = (model: NormalizedPlugin): readonly string[] => [ skill.provenance.sourcePath, skill.source, ...skill.resources.map((resource) => resource.source), + ...(skill.skillIr === undefined + ? [] + : [ + skill.skillIr.source, + ...skill.skillIr.resources.map((resource) => resource.source), + ...skill.skillIr.sidecars.flatMap((sidecar) => sidecar.source === undefined ? [] : [sidecar.source]), + ...skill.skillIr.diagnostics.flatMap((diagnostic) => + diagnostic.sourcePath === undefined ? [] : [diagnostic.sourcePath]), + ]), + ...(skill.hostDocuments === undefined + ? [] + : Object.values(skill.hostDocuments).flatMap((document) => [ + ...document.diagnostics.flatMap((diagnostic) => + diagnostic.sourcePath === undefined ? [] : [diagnostic.sourcePath]), + ...document.sidecars.flatMap((sidecar) => sidecar.source === undefined ? [] : [sidecar.source]), + ])), ]), ...model.scripts.flatMap((script) => [script.provenance.sourcePath, script.source]), ...model.mcpServers.flatMap((server) => [ @@ -411,7 +427,7 @@ export const canonicalizeNormalizedModel = ( dir: canonicalCompilerPath(root, skill.dir, 'Skill directory path'), ...(skill.hostDocuments === undefined ? {} - : { hostDocuments: canonicalSkillHostDocuments(root, skill.hostDocuments) }), + : { hostDocuments: canonicalHostDocuments(root, skill.hostDocuments) }), provenance: canonicalProvenance(root, skill.provenance), resources: skill.resources.map((resource) => ({ ...resource,