diff --git a/.changeset/fix-equal-length-target-layout.md b/.changeset/fix-equal-length-target-layout.md new file mode 100644 index 000000000..cf1e06617 --- /dev/null +++ b/.changeset/fix-equal-length-target-layout.md @@ -0,0 +1,5 @@ +--- +"agent-bundle": patch +--- + +Prevent artifact validation from attributing compiler outputs to a different target whose name has the same length. diff --git a/packages/agent-bundle/src/build/artifact-layout.ts b/packages/agent-bundle/src/build/artifact-layout.ts index 42df5511d..cd7202ef9 100644 --- a/packages/agent-bundle/src/build/artifact-layout.ts +++ b/packages/agent-bundle/src/build/artifact-layout.ts @@ -25,4 +25,5 @@ export const pathInTargetOutputLayout = ( targetPath: string, target: string, layout: TargetArtifactOutputLayout | undefined, -): boolean => isDirectOutputLayoutPath(targetPath.slice(target.length + 1), layout); +): boolean => targetPath.startsWith(`${target}/`) && + isDirectOutputLayoutPath(targetPath.slice(target.length + 1), layout); diff --git a/packages/agent-bundle/tests/artifact-validator.test.ts b/packages/agent-bundle/tests/artifact-validator.test.ts index b61a1bc1a..330a8d3dd 100644 --- a/packages/agent-bundle/tests/artifact-validator.test.ts +++ b/packages/agent-bundle/tests/artifact-validator.test.ts @@ -902,6 +902,52 @@ it('reports an orphan compiler MCP output after the artifact is rehashed', async } }); +it('does not attribute compiler MCP outputs to an equal-length sibling target', async () => { + const siblingTarget = 'neighbor'; + const siblingMetadata = Object.freeze({ + adapterRevision: 'neighbor-adapter-v1', + capabilityRevision: 'neighbor-capabilities-v1', + capabilitySha256: 'e'.repeat(64), + observedVersion: 'neighbor-observed-v1', + schemas: Object.freeze([]), + }); + const registry = coherenceRegistry().register({ + artifactLayout: { mcpEntries: { allowedSuffixes: ['.mjs'], directory: 'mcp' } }, + capabilities: { mcp: true }, + mcpRuntime: createTargetMcpRuntime({ + manifestPath: 'native/servers.json', + remoteTypes: ['streamable-http'], + resolveValue: createMcpPathTokenResolver({ target: siblingTarget, tokens: {} }), + }), + metadata: siblingMetadata, + name: siblingTarget, + plan: () => ({ diagnostics: [], entries: [] }), + } satisfies TargetAdapter); + const root = await writeArtifact([ + { + contents: '{"mcpServers":{"server":{"args":["mcp/mcp-server-deadbeef.mjs"],"command":"node","type":"stdio"}}}\n', + kind: 'generated', + path: 'coherent/native/servers.json', + }, + { contents: 'export const coherent = true;\n', kind: 'bundle', path: 'coherent/mcp/mcp-server-deadbeef.mjs' }, + { + contents: '{"mcpServers":{"server":{"args":["mcp/mcp-server-deadbeef.mjs"],"command":"node","type":"stdio"}}}\n', + kind: 'generated', + path: 'neighbor/native/servers.json', + }, + { contents: 'export const neighbor = true;\n', kind: 'bundle', path: 'neighbor/mcp/mcp-server-deadbeef.mjs' }, + ], true, [coherenceManifestTarget, Object.freeze({ ...siblingMetadata, name: siblingTarget })]); + + try { + expect(coherenceTarget).toHaveLength(siblingTarget.length); + expect(await validateArtifact({ artifactRoot: root, registry })).not.toEqual(expect.arrayContaining([ + expect.objectContaining({ code: 'AB6017' }), + ])); + } finally { + await rm(root, { force: true, recursive: true }); + } +}); + it.each([ ['backslash local path', 'scripts\\missing.mjs', true], ['local option assignment', '--config=./scripts/missing.mjs', true],