test(channels): extend bundled artifact parity and add plugin-shape contract suite (#104618)

This commit is contained in:
Peter Steinberger
2026-07-11 11:44:34 -07:00
committed by GitHub
parent fe3884a7ad
commit 6bd14d9129
16 changed files with 404 additions and 31 deletions
+1
View File
@@ -101,6 +101,7 @@ Test wrapper runs end with a short `[test] passed|failed|skipped ... in ...` sum
- **TUI PTY tests:** `node scripts/run-vitest.mjs run --config test/vitest/vitest.tui-pty.config.ts` runs the fast fake-backend PTY lane. `OPENCLAW_TUI_PTY_INCLUDE_LOCAL=1` or `pnpm tui:pty:test:watch --mode local` runs the slower `tui --local` smoke, which mocks only the external model endpoint. Assert stable visible text or fixture calls, not raw ANSI snapshots.
- `pnpm test:extensions` and `pnpm test extensions` run all extension/plugin shards. Heavy channel plugins, the browser plugin, and OpenAI run as dedicated shards; other plugin groups stay batched. `pnpm test extensions/<id>` runs one bundled plugin lane.
- Source files with sibling tests map to that sibling before falling back to wider directory globs. Helper edits under `src/channels/plugins/contracts/test-helpers`, `src/plugin-sdk/test-helpers`, and `src/plugins/contracts` use a local import graph to run importing tests instead of broad-running every shard when the dependency path is precise.
- Contract directory targets fan out to their contract lanes: `pnpm test src/channels/plugins/contracts` runs the four channel contract configs and `pnpm test src/plugins/contracts` runs the plugin contracts config, since the generic `channels`/`plugins` projects exclude `contracts/**`.
- `auto-reply` splits into three dedicated configs (`core`, `top-level`, `reply`) so the reply harness does not dominate the lighter top-level status/token/helper tests.
- Selected `plugin-sdk` and `commands` test files route through dedicated light lanes that keep only `test/setup.ts`, leaving runtime-heavy cases on their existing lanes.
- Base Vitest config defaults to `pool: "threads"` and `isolate: false`, with the shared non-isolated runner enabled across repo configs.
+3 -2
View File
@@ -1702,8 +1702,9 @@ export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount, FeishuProbeResul
}
return { to: `chat:${parsed?.chatId ?? conversationId.trim()}` };
},
resolveSessionConversation: ({ kind, rawId }) =>
resolveFeishuSessionConversation({ kind, rawId }),
// Same function as the public session-key artifact so the pre-registry
// fast path cannot drift from plugin behavior (pinned by contract test).
resolveSessionConversation: resolveFeishuSessionConversation,
resolveOutboundSessionRoute: (params) => resolveFeishuOutboundSessionRoute(params),
targetResolver: {
looksLikeId: looksLikeFeishuId,
+1 -1
View File
@@ -32,7 +32,7 @@ export const mattermostSetupPlugin: ChannelPlugin<ResolvedMattermostAccount> = {
describeAccount: describeMattermostAccount,
},
gateway: {
resolveGatewayAuthBypassPaths: ({ cfg }) => resolveMattermostGatewayAuthBypassPaths(cfg),
resolveGatewayAuthBypassPaths: resolveMattermostGatewayAuthBypassPaths,
},
setup: mattermostSetupAdapter,
setupWizard: mattermostSetupWizard,
+3 -1
View File
@@ -868,7 +868,9 @@ export const mattermostPlugin: ChannelPlugin<ResolvedMattermostAccount> = create
}),
}),
gateway: {
resolveGatewayAuthBypassPaths: ({ cfg }) => resolveMattermostGatewayAuthBypassPaths(cfg),
// Same function as the public gateway-auth artifact so the pre-plugin
// fast path and the loaded plugin cannot drift (pinned by contract test).
resolveGatewayAuthBypassPaths: resolveMattermostGatewayAuthBypassPaths,
startAccount: async (ctx) => {
const account = ctx.account;
const statusSink = createAccountStatusSink({
@@ -18,16 +18,18 @@ describe("Mattermost gateway auth bypass paths", () => {
it("keeps only Mattermost channel callback paths", () => {
expect(
resolveMattermostGatewayAuthBypassPaths({
channels: {
mattermost: {
commands: {
callbackPath: "/api/channels/mattermost/command",
callbackUrl: "https://gateway.example.com/api/channels/nostr/default/profile",
},
accounts: {
work: {
commands: {
callbackPath: "/api/channels/mattermost/work",
cfg: {
channels: {
mattermost: {
commands: {
callbackPath: "/api/channels/mattermost/command",
callbackUrl: "https://gateway.example.com/api/channels/nostr/default/profile",
},
accounts: {
work: {
commands: {
callbackPath: "/api/channels/mattermost/work",
},
},
},
},
@@ -54,12 +54,17 @@ export function collectMattermostSlashCallbackPaths(
return [...paths];
}
export function resolveMattermostGatewayAuthBypassPaths(cfg: {
channels?: Record<string, unknown>;
// Params shape is the core gateway-auth artifact contract: core invokes the
// public `gateway-auth-api.js` export as `resolveGatewayAuthBypassPaths({ cfg })`
// (src/channels/plugins/gateway-auth-bypass.ts), so a positional `cfg` param
// would silently drop configured callback paths on the pre-plugin fast path.
export function resolveMattermostGatewayAuthBypassPaths(params: {
cfg: { channels?: Record<string, unknown> };
}): string[] {
const channels = params.cfg.channels;
const base =
cfg.channels?.mattermost && typeof cfg.channels.mattermost === "object"
? (cfg.channels.mattermost as MattermostConfigInput)
channels?.mattermost && typeof channels.mattermost === "object"
? (channels.mattermost as MattermostConfigInput)
: undefined;
const callbackPaths = new Set(
collectMattermostSlashCallbackPaths(readMattermostCommands(base?.commands)).filter(
+3 -2
View File
@@ -837,8 +837,9 @@ export const telegramPlugin = createChatChannelPlugin({
resolveTelegramInboundConversation({ to, conversationId, threadId }),
resolveDeliveryTarget: ({ conversationId, parentConversationId }) =>
resolveTelegramDeliveryTarget({ conversationId, parentConversationId }),
resolveSessionConversation: ({ kind, rawId }) =>
resolveTelegramSessionConversation({ kind, rawId }),
// Same function as the public session-key artifact so the pre-registry
// fast path cannot drift from plugin behavior (pinned by contract test).
resolveSessionConversation: resolveTelegramSessionConversation,
resolveSessionTarget: ({ kind, id }) => resolveTelegramSessionTarget({ kind, id }),
inferTargetChatType: ({ to }) => resolveTelegramRouteTarget(to).chatType,
preserveHeartbeatThreadIdForGroupRoute: true,
@@ -9,6 +9,8 @@ function listContractTestFiles(rootDir = "src/channels/plugins/contracts") {
const CONTRACT_FILE_WEIGHTS = new Map([
["channel-import-guardrails.test.ts", 18],
["outbound-payload.contract.test.ts", 18],
// Loads every bundled channel plugin surface in one file.
["plugin-shape.contract.test.ts", 48],
["plugins-core.catalog.paths.contract.test.ts", 28],
["plugins-core.catalog.entries.contract.test.ts", 16],
["session-binding.registry-backed.contract.test.ts", 40],
+23
View File
@@ -2344,6 +2344,7 @@ const CHANNEL_CONTRACT_CONFIG_PATTERNS = new Map([
"src/channels/plugins/contracts/channel-catalog.contract.test.ts",
"src/channels/plugins/contracts/channel-import-guardrails.test.ts",
"src/channels/plugins/contracts/group-policy.fallback.contract.test.ts",
"src/channels/plugins/contracts/message-tool-artifact.contract.test.ts",
"src/channels/plugins/contracts/outbound-payload.contract.test.ts",
"src/channels/plugins/contracts/*-shard-a.contract.test.ts",
"src/channels/plugins/contracts/*-shard-e.contract.test.ts",
@@ -2352,6 +2353,7 @@ const CHANNEL_CONTRACT_CONFIG_PATTERNS = new Map([
[
CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG,
[
"src/channels/plugins/contracts/gateway-auth-artifact.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.authorize-config-write.policy.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.authorize-config-write.targets.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.catalog.entries.contract.test.ts",
@@ -2362,6 +2364,7 @@ const CHANNEL_CONTRACT_CONFIG_PATTERNS = new Map([
[
CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG,
[
"src/channels/plugins/contracts/plugin-shape.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.catalog.paths.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.loader.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.registry.contract.test.ts",
@@ -2375,6 +2378,7 @@ const CHANNEL_CONTRACT_CONFIG_PATTERNS = new Map([
"src/channels/plugins/contracts/plugins-core.resolve-config-writes.contract.test.ts",
"src/channels/plugins/contracts/registry.contract.test.ts",
"src/channels/plugins/contracts/session-binding.registry-backed.contract.test.ts",
"src/channels/plugins/contracts/session-key-artifact.contract.test.ts",
"src/channels/plugins/contracts/thread-binding-artifact.contract.test.ts",
"src/channels/plugins/contracts/*-shard-d.contract.test.ts",
"src/channels/plugins/contracts/*-shard-h.contract.test.ts",
@@ -2714,6 +2718,22 @@ function expandExplicitSourceTestTargets(targetArgs, cwd) {
if (relative === "src/commands" && isExistingDirectoryTarget(targetArg, cwd)) {
return [COMMANDS_LIGHT_VITEST_CONFIG, COMMANDS_VITEST_CONFIG];
}
// Contract directory targets must fan out to the owning contract lanes; the
// generic channels/plugins projects exclude contracts/**, so routing a
// contracts directory there silently runs zero tests (passWithNoTests).
if (isExistingDirectoryTarget(targetArg, cwd)) {
if (isPathAtOrUnder(relative, "src/channels/plugins/contracts")) {
return [
CONTRACTS_CHANNEL_SURFACE_VITEST_CONFIG,
CONTRACTS_CHANNEL_CONFIG_VITEST_CONFIG,
CONTRACTS_CHANNEL_REGISTRY_VITEST_CONFIG,
CONTRACTS_CHANNEL_SESSION_VITEST_CONFIG,
];
}
if (isPathAtOrUnder(relative, "src/plugins/contracts")) {
return [CONTRACTS_PLUGIN_VITEST_CONFIG];
}
}
const exactDirectoryTargets = resolveExactSourceDirectoryTestTargets(targetArg, cwd);
if (exactDirectoryTargets) {
return exactDirectoryTargets;
@@ -3201,6 +3221,7 @@ function resolveChannelContractTargetKind(relative) {
"channel-catalog.contract.test.ts",
"channel-import-guardrails.test.ts",
"group-policy.fallback.contract.test.ts",
"message-tool-artifact.contract.test.ts",
"outbound-payload.contract.test.ts",
].includes(name)
) {
@@ -3208,6 +3229,7 @@ function resolveChannelContractTargetKind(relative) {
}
if (
[
"gateway-auth-artifact.contract.test.ts",
"plugins-core.authorize-config-write.policy.contract.test.ts",
"plugins-core.authorize-config-write.targets.contract.test.ts",
"plugins-core.catalog.entries.contract.test.ts",
@@ -3217,6 +3239,7 @@ function resolveChannelContractTargetKind(relative) {
}
if (
[
"plugin-shape.contract.test.ts",
"plugins-core.catalog.paths.contract.test.ts",
"plugins-core.loader.contract.test.ts",
"plugins-core.registry.contract.test.ts",
@@ -0,0 +1,61 @@
// Gateway-auth artifact parity contract for bundled channel plugins.
//
// Core resolves unauthenticated Gateway callback paths from lightweight
// `gateway-auth-api` artifacts (src/channels/plugins/gateway-auth-bypass.ts),
// which invoke the export with a `{ cfg }` params object. This suite pins each
// artifact export to the exact function the loaded plugin's gateway surface
// uses and pins the params-object call shape core relies on.
import { beforeAll, describe, expect, it } from "vitest";
import {
getBundledChannelGatewayAuthArtifactAsync,
getBundledChannelPluginAsync,
listBundledChannelPluginIds,
} from "./test-helpers/bundled-channel-plugin-loader.js";
// Bundled channels expected to ship a top-level gateway-auth artifact.
const GATEWAY_AUTH_ARTIFACT_PLUGIN_IDS = ["mattermost"] as const;
describe("bundled channel gateway-auth artifact parity", () => {
const artifactResolvers = new Map<string, unknown>();
beforeAll(async () => {
for (const id of listBundledChannelPluginIds()) {
const artifact = await getBundledChannelGatewayAuthArtifactAsync(id);
if (artifact) {
artifactResolvers.set(id, artifact.resolveGatewayAuthBypassPaths);
}
}
});
it("keeps the artifact table in sync with bundled channels that ship one", () => {
expect([...artifactResolvers.keys()].toSorted()).toEqual([...GATEWAY_AUTH_ARTIFACT_PLUGIN_IDS]);
});
it.each(GATEWAY_AUTH_ARTIFACT_PLUGIN_IDS)(
"keeps the %s artifact resolver identical to the plugin gateway surface",
async (id) => {
const resolveGatewayAuthBypassPaths = artifactResolvers.get(id);
expect(typeof resolveGatewayAuthBypassPaths).toBe("function");
const plugin = await getBundledChannelPluginAsync(id);
expect(plugin?.gateway?.resolveGatewayAuthBypassPaths).toBe(resolveGatewayAuthBypassPaths);
},
);
it("resolves mattermost bypass paths through core's { cfg } call shape", () => {
// Regression pin: the artifact once took `cfg` positionally, so core's
// `{ cfg }` invocation silently dropped configured callback paths.
const resolve = artifactResolvers.get("mattermost") as (params: {
cfg: { channels?: Record<string, unknown> };
}) => string[];
const cfg = {
channels: {
mattermost: {
commands: { callbackPath: "/api/channels/mattermost/custom" },
},
},
};
expect(resolve({ cfg })).toContain("/api/channels/mattermost/custom");
});
});
@@ -0,0 +1,46 @@
// Message-tool artifact parity contract for bundled channel plugins.
//
// Core discovers message-tool schemas from lightweight `message-tool-api`
// artifacts without loading full channel plugins
// (src/channels/plugins/message-tool-api.ts). This suite pins each artifact
// export to the exact function the loaded plugin's action surface uses so
// discovery cannot drift from runtime behavior.
import { beforeAll, describe, expect, it } from "vitest";
import {
getBundledChannelMessageToolArtifactAsync,
getBundledChannelPluginAsync,
listBundledChannelPluginIds,
} from "./test-helpers/bundled-channel-plugin-loader.js";
// Bundled channels expected to ship a top-level message-tool artifact.
const MESSAGE_TOOL_ARTIFACT_PLUGIN_IDS = ["imessage", "slack"] as const;
describe("bundled channel message-tool artifact parity", () => {
const artifactDescribers = new Map<string, unknown>();
beforeAll(async () => {
for (const id of listBundledChannelPluginIds()) {
const artifact = await getBundledChannelMessageToolArtifactAsync(id);
if (artifact) {
artifactDescribers.set(id, artifact.describeMessageTool);
}
}
});
it("keeps the artifact table in sync with bundled channels that ship one", () => {
expect([...artifactDescribers.keys()].toSorted()).toEqual([
...MESSAGE_TOOL_ARTIFACT_PLUGIN_IDS,
]);
});
it.each(MESSAGE_TOOL_ARTIFACT_PLUGIN_IDS)(
"keeps the %s artifact describer identical to the plugin action surface",
async (id) => {
const describeMessageTool = artifactDescribers.get(id);
expect(typeof describeMessageTool).toBe("function");
const plugin = await getBundledChannelPluginAsync(id);
expect(plugin?.actions?.describeMessageTool).toBe(describeMessageTool);
},
);
});
@@ -0,0 +1,71 @@
// Plugin-shape coherence contract for bundled channel plugins.
//
// Catalog routing keys off plugin ids, docs surfaces render `meta.docsPath`,
// and capability flags gate feature discovery, so every bundled channel must
// keep identity metadata aligned with its catalog id and capability flags
// coherent with the adapters that implement them. Per-surface behavior checks
// live in the registry-backed shard suites; this suite pins the static shape.
//
// Capability rules verified against every bundled channel before pinning.
// Dropped for legitimate exceptions rather than special-casing:
// - threads=true does not imply a threading adapter (clickclack/tlon bind
// threads through conversationBindings only).
// - nativeCommands=true does not imply a commands adapter (mattermost serves
// slash commands through gateway HTTP routes).
// - blockStreaming=true does not imply a streaming adapter (coalesce tuning
// is optional).
import { beforeAll, describe, expect, it } from "vitest";
import {
getBundledChannelPluginAsync,
listBundledChannelPluginIds,
} from "./test-helpers/bundled-channel-plugin-loader.js";
const CHAT_TYPES = new Set(["direct", "group", "channel", "thread"]);
const bundledChannelPluginIds = listBundledChannelPluginIds();
describe("bundled channel plugin shape coherence", () => {
const plugins = new Map<string, Awaited<ReturnType<typeof getBundledChannelPluginAsync>>>();
beforeAll(async () => {
for (const id of bundledChannelPluginIds) {
plugins.set(id, await getBundledChannelPluginAsync(id));
}
});
it("discovers bundled channel plugins from the catalog", () => {
expect(bundledChannelPluginIds.length).toBeGreaterThan(0);
});
describe.each(bundledChannelPluginIds)("%s", (id) => {
it("keeps plugin identity aligned with the catalog id", () => {
const plugin = plugins.get(id);
if (!plugin) {
throw new Error(`Missing bundled channel plugin for ${id}`);
}
expect(plugin.id).toBe(id);
expect(plugin.meta.id).toBe(id);
});
it("ships non-empty docs metadata", () => {
const plugin = plugins.get(id);
expect(plugin?.meta.docsPath.trim()).toBeTruthy();
});
it("declares known chat types", () => {
const chatTypes = plugins.get(id)?.capabilities.chatTypes ?? [];
expect(chatTypes.length).toBeGreaterThan(0);
expect(chatTypes.filter((chatType) => !CHAT_TYPES.has(chatType))).toEqual([]);
});
it("backs declared reactions with a message action surface", () => {
const plugin = plugins.get(id);
if (!plugin?.capabilities.reactions) {
return;
}
// Reactions are delivered through the shared `message` tool, so a channel
// declaring the capability without an actions adapter ships a dead flag.
expect(plugin.actions).toBeDefined();
expect(typeof plugin.actions?.describeMessageTool).toBe("function");
});
});
});
@@ -0,0 +1,77 @@
// Session-key artifact parity contract for bundled channel plugins.
//
// Core resolves threaded session conversations from lightweight `session-key-api`
// artifacts before full plugin loading (src/channels/plugins/session-conversation.ts).
// This suite pins each artifact export to the exact function the loaded plugin's
// messaging surface uses so the fast path cannot drift from plugin behavior.
//
// Artifact exports are intentionally non-uniform: telegram/feishu ship the
// `resolveSessionConversation` hook core probes, while discord ships only its
// explicit session-key normalizer. Pin what each channel ships instead of
// forcing one shape.
import { beforeAll, describe, expect, it } from "vitest";
import {
getBundledChannelPluginAsync,
getBundledChannelSessionKeyArtifactAsync,
listBundledChannelPluginIds,
} from "./test-helpers/bundled-channel-plugin-loader.js";
// Bundled channels expected to ship a top-level session-key artifact.
const SESSION_KEY_ARTIFACT_PLUGIN_IDS = ["discord", "feishu", "telegram"] as const;
const SESSION_CONVERSATION_ARTIFACT_PLUGIN_IDS = ["feishu", "telegram"] as const;
type ExplicitSessionKeyNormalizer = (
sessionKey: string,
ctx: { ChatType?: string; From?: string; SenderId?: string },
) => string;
describe("bundled channel session-key artifact parity", () => {
const artifacts = new Map<string, Record<string, unknown>>();
beforeAll(async () => {
for (const id of listBundledChannelPluginIds()) {
const artifact = await getBundledChannelSessionKeyArtifactAsync(id);
if (artifact) {
artifacts.set(id, artifact);
}
}
});
it("keeps the artifact table in sync with bundled channels that ship one", () => {
expect([...artifacts.keys()].toSorted()).toEqual([...SESSION_KEY_ARTIFACT_PLUGIN_IDS]);
});
it.each(SESSION_CONVERSATION_ARTIFACT_PLUGIN_IDS)(
"keeps the %s artifact resolver identical to the plugin messaging hook",
async (id) => {
const resolveSessionConversation = artifacts.get(id)?.resolveSessionConversation;
expect(typeof resolveSessionConversation).toBe("function");
const plugin = await getBundledChannelPluginAsync(id);
expect(plugin?.messaging?.resolveSessionConversation).toBe(resolveSessionConversation);
},
);
it("keeps the discord artifact normalizer behind the plugin messaging hook", async () => {
const normalize = artifacts.get("discord")?.normalizeExplicitDiscordSessionKey as
| ExplicitSessionKeyNormalizer
| undefined;
expect(typeof normalize).toBe("function");
const plugin = await getBundledChannelPluginAsync("discord");
const pluginNormalize = plugin?.messaging?.normalizeExplicitSessionKey;
expect(typeof pluginNormalize).toBe("function");
// The plugin hook adapts core's params-object shape onto the artifact's
// positional export, so pin behavioral parity over representative keys.
const cases = [
{ sessionKey: "discord:channel:123", ctx: { ChatType: "direct", SenderId: "123" } },
{ sessionKey: "discord:dm:42", ctx: { ChatType: "dm", From: "discord:42" } },
{ sessionKey: "agent:m:discord:channel:9", ctx: { ChatType: "direct", From: "discord:9" } },
{ sessionKey: "Discord:Channel:77", ctx: { ChatType: "group", SenderId: "77" } },
] as const;
for (const { sessionKey, ctx } of cases) {
expect(pluginNormalize?.({ sessionKey, ctx })).toBe(normalize?.(sessionKey, ctx));
}
});
});
@@ -296,25 +296,75 @@ export async function getBundledChannelDirectoryPluginAsync(
}
type ChannelThreadBindingArtifactModule = { defaultTopLevelPlacement?: unknown };
type ChannelSessionKeyArtifactModule = Record<string, unknown>;
type ChannelMessageToolArtifactModule = { describeMessageTool?: unknown };
type ChannelGatewayAuthArtifactModule = { resolveGatewayAuthBypassPaths?: unknown };
function isMissingBundledThreadBindingArtifact(error: unknown, id: ChannelId): boolean {
function isMissingBundledArtifact(
error: unknown,
id: ChannelId,
artifactBasename: string,
): boolean {
return (
error instanceof Error &&
error.message === `Unable to resolve bundled plugin public surface ${id}/thread-binding-api.js`
error.message === `Unable to resolve bundled plugin public surface ${id}/${artifactBasename}`
);
}
// Lightweight artifacts are optional per channel; contract suites discover the
// shipping set from the catalog, so a missing artifact resolves to null while
// present-but-broken artifacts still fail the load.
async function getOptionalBundledChannelArtifactAsync<T extends object>(
id: ChannelId,
artifactBasename: string,
): Promise<T | null> {
return await loadBundledPluginPublicSurface<T>({
pluginId: id,
artifactBasename,
}).catch((error: unknown) => {
if (isMissingBundledArtifact(error, id, artifactBasename)) {
return null;
}
throw error;
});
}
/** Returns a bundled channel's thread-binding artifact, or null when it ships none. */
export async function getBundledChannelThreadBindingArtifactAsync(
id: ChannelId,
): Promise<ChannelThreadBindingArtifactModule | null> {
return await loadBundledPluginPublicSurface<ChannelThreadBindingArtifactModule>({
pluginId: id,
artifactBasename: "thread-binding-api.js",
}).catch((error: unknown) => {
if (isMissingBundledThreadBindingArtifact(error, id)) {
return null;
}
throw error;
});
return await getOptionalBundledChannelArtifactAsync<ChannelThreadBindingArtifactModule>(
id,
"thread-binding-api.js",
);
}
/** Returns a bundled channel's session-key artifact, or null when it ships none. */
export async function getBundledChannelSessionKeyArtifactAsync(
id: ChannelId,
): Promise<ChannelSessionKeyArtifactModule | null> {
return await getOptionalBundledChannelArtifactAsync<ChannelSessionKeyArtifactModule>(
id,
"session-key-api.js",
);
}
/** Returns a bundled channel's message-tool artifact, or null when it ships none. */
export async function getBundledChannelMessageToolArtifactAsync(
id: ChannelId,
): Promise<ChannelMessageToolArtifactModule | null> {
return await getOptionalBundledChannelArtifactAsync<ChannelMessageToolArtifactModule>(
id,
"message-tool-api.js",
);
}
/** Returns a bundled channel's gateway-auth artifact, or null when it ships none. */
export async function getBundledChannelGatewayAuthArtifactAsync(
id: ChannelId,
): Promise<ChannelGatewayAuthArtifactModule | null> {
return await getOptionalBundledChannelArtifactAsync<ChannelGatewayAuthArtifactModule>(
id,
"gateway-auth-api.js",
);
}
+27
View File
@@ -2941,6 +2941,33 @@ describe("scripts/test-projects changed-target routing", () => {
]);
});
it("fans contract directory targets out to the owning contract lanes", () => {
// Regression: the generic channels project excludes contracts/**, so the
// directory target used to run zero tests and exit green.
const plans = buildVitestRunPlans(["src/channels/plugins/contracts"]);
expect(plans.map((plan) => plan.config)).toEqual([
"test/vitest/vitest.contracts-channel-surface.config.ts",
"test/vitest/vitest.contracts-channel-config.config.ts",
"test/vitest/vitest.contracts-channel-registry.config.ts",
"test/vitest/vitest.contracts-channel-session.config.ts",
]);
expect(plans.every((plan) => plan.includePatterns === null)).toBe(true);
});
it("routes the plugin contracts directory to the plugin contracts lane", () => {
const plans = buildVitestRunPlans(["src/plugins/contracts"]);
expect(plans).toEqual([
{
config: "test/vitest/vitest.contracts-plugin.config.ts",
forwardedArgs: [],
includePatterns: null,
watchMode: false,
},
]);
});
it("routes misc extensions to the misc extension shard", () => {
const plans = buildVitestRunPlans(["extensions/thread-ownership"], process.cwd());
+4
View File
@@ -11,12 +11,14 @@ export const channelSurfaceContractPatterns = [
"src/channels/plugins/contracts/channel-catalog.contract.test.ts",
"src/channels/plugins/contracts/channel-import-guardrails.test.ts",
"src/channels/plugins/contracts/group-policy.fallback.contract.test.ts",
"src/channels/plugins/contracts/message-tool-artifact.contract.test.ts",
"src/channels/plugins/contracts/outbound-payload.contract.test.ts",
"src/channels/plugins/contracts/*-shard-a.contract.test.ts",
"src/channels/plugins/contracts/*-shard-e.contract.test.ts",
];
export const channelConfigContractPatterns = [
"src/channels/plugins/contracts/gateway-auth-artifact.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.authorize-config-write.policy.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.authorize-config-write.targets.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.catalog.entries.contract.test.ts",
@@ -25,6 +27,7 @@ export const channelConfigContractPatterns = [
];
export const channelRegistryContractPatterns = [
"src/channels/plugins/contracts/plugin-shape.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.catalog.paths.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.loader.contract.test.ts",
"src/channels/plugins/contracts/plugins-core.registry.contract.test.ts",
@@ -36,6 +39,7 @@ export const channelSessionContractPatterns = [
"src/channels/plugins/contracts/plugins-core.resolve-config-writes.contract.test.ts",
"src/channels/plugins/contracts/registry.contract.test.ts",
"src/channels/plugins/contracts/session-binding.registry-backed.contract.test.ts",
"src/channels/plugins/contracts/session-key-artifact.contract.test.ts",
"src/channels/plugins/contracts/thread-binding-artifact.contract.test.ts",
"src/channels/plugins/contracts/*-shard-d.contract.test.ts",
"src/channels/plugins/contracts/*-shard-h.contract.test.ts",