Files
paseo/packages/protocol/tests/validation/ws-outbound.test.ts
T
a8ebd390fa feat(forge): pluggable forge abstraction + GitLab and Gitea/Forgejo/Codeberg (#1913)
* refactor(forge): forge-neutral foundation (GitHub-only)

Decouple git-hosting from GitHub behind a neutral abstraction (issue #1616), GitHub-only for now; existing GitHub behaviour is unchanged.

- Forge manifest, neutral ForgeService contract, forge registry + resolver, and a client forge-module registry.
- GitHub code renamed to the neutral shape; PR/Issue attachment wording preserved.
- forge.search.response enums parse tolerantly (unknown kind/auth state degrade instead of breaking the client).
- createPullRequest reports typed CLI/auth errors instead of a generic message.
- forge-resolver host/remote caches are LRU-bounded.
- Forge host trust is explicit: only a known cloud host or a CLI-authenticated host is ever talked to; an unauthenticated GitHub Enterprise host fails resolution instead of routing to github.com.
- Docs: forge-providers guide, glossary and i18n forge-copy conventions, architecture and rpc-namespacing terminology.
- Vitest React Native mocks (unistyles, svg, linking, lucide) consolidated into shared aliased test-stubs.

* feat(forge): GitLab adapter, forge-aware UI, pipelines and approvals

GitLab adapter over the glab CLI on the neutral contracts: MR status, forge-aware UI, pipeline tree, and N-of-M approvals.

- threadIsResolved is part of the neutral timeline item.
- Pipeline load failures show an error instead of an empty section.
- Manual pipeline jobs render as pending.
- Fork/detached MR head pipelines are fetched by MR iid (glab ci get --merge-request).

* feat(forge): Gitea family adapter (Gitea, Forgejo, Codeberg)

One adapter over the tea CLI serving Gitea, Forgejo, and Codeberg on the neutral contracts.

- CI status aggregates commit statuses and Actions runs together.
- Gitea's terminal "warning" state maps to failure on server and client.
- Gitea Actions check details are reachable from the PR pane by workflowRunId.

* refactor(forge): localize compatibility handling

* test(forge): expect normalized GitLab facts

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-07-17 15:03:26 +08:00

200 lines
6.8 KiB
TypeScript

import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { createJiti } from "jiti";
import { describe, expect, it } from "vitest";
import { WSOutboundMessageSchema as GeneratedWSOutboundMessageSchema } from "../../src/generated/validation/ws-outbound.aot.js";
interface GeneratedSchema {
safeParse(input: unknown): { success: boolean; data?: unknown };
}
const protocolRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const generatedWSOutboundPath = resolve(
protocolRoot,
"src/generated/validation/ws-outbound.aot.ts",
);
const require = createRequire(import.meta.url);
async function compileInlineSchema(sourceSchema: string): Promise<GeneratedSchema> {
const scratchRoot = resolve(protocolRoot, "../../.tmp");
await mkdir(scratchRoot, { recursive: true });
const tempDir = await mkdtemp(join(scratchRoot, "paseo-zod-aot-"));
try {
const sourcePath = join(tempDir, "schema.source.js");
const outputPath = join(tempDir, "schema.generated.ts");
await writeFile(join(tempDir, "package.json"), '{"type":"module"}\n');
await writeFile(
sourcePath,
[
'import { z } from "zod";',
'import { compile } from "zod-aot";',
sourceSchema,
"export const Schema = compile(SourceSchema);",
"",
].join("\n"),
);
const zodAotEntry = require.resolve("zod-aot");
const zodAotRoot = resolve(dirname(zodAotEntry), "..");
const [{ discoverSchemas }, { compileSchemas }, { generateCompiledFileContent }] =
await Promise.all([
import(pathToFileURL(resolve(zodAotRoot, "dist/discovery.js")).href),
import(pathToFileURL(resolve(zodAotRoot, "dist/core/pipeline.js")).href),
import(pathToFileURL(resolve(zodAotRoot, "dist/cli/emitter.js")).href),
]);
const schemas = await discoverSchemas(sourcePath, { cacheBust: true });
const compiled = compileSchemas(schemas, { mode: "inline" });
const content = generateCompiledFileContent(compiled, "./schema.source.js", {
zodCompat: false,
});
await writeFile(outputPath, content);
const jiti = createJiti(import.meta.url, { moduleCache: false });
const generated = await jiti.import(outputPath);
return generated.Schema as GeneratedSchema;
} finally {
await rm(tempDir, { recursive: true, force: true });
}
}
describe("WS outbound zod-aot validation", () => {
it("applies defaults inside discriminated-union branches", async () => {
const schema = await compileInlineSchema(`
const SourceSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("with_default"),
enabled: z.boolean().default(true),
}),
z.object({
type: z.literal("without_default"),
label: z.string(),
}),
]);
`);
expect(schema.safeParse({ type: "with_default" })).toMatchObject({
success: true,
data: { type: "with_default", enabled: true },
});
});
it("routes tool-call-like status unions through the current sequential item union", async () => {
const schema = await compileInlineSchema(`
const ToolCallItemSchema = z.discriminatedUnion("status", [
z.object({ type: z.literal("tool_call"), status: z.literal("running"), callId: z.string() }),
z.object({ type: z.literal("tool_call"), status: z.literal("completed"), callId: z.string(), output: z.string() }),
z.object({ type: z.literal("tool_call"), status: z.literal("failed"), callId: z.string(), error: z.string() }),
z.object({ type: z.literal("tool_call"), status: z.literal("canceled"), callId: z.string() }),
]);
const TimelineItemSchema = z.union([
z.object({ type: z.literal("assistant_message"), text: z.string() }),
ToolCallItemSchema,
]);
const SourceSchema = z.object({
item: TimelineItemSchema,
});
`);
expect(
schema.safeParse({ item: { type: "tool_call", status: "running", callId: "run" } }),
).toMatchObject({
success: true,
data: { item: { type: "tool_call", status: "running", callId: "run" } },
});
expect(
schema.safeParse({
item: { type: "tool_call", status: "completed", callId: "done", output: "ok" },
}),
).toMatchObject({
success: true,
data: { item: { type: "tool_call", status: "completed", callId: "done", output: "ok" } },
});
expect(
schema.safeParse({
item: { type: "tool_call", status: "failed", callId: "fail", error: "boom" },
}),
).toMatchObject({
success: true,
data: { item: { type: "tool_call", status: "failed", callId: "fail", error: "boom" } },
});
expect(
schema.safeParse({ item: { type: "tool_call", status: "canceled", callId: "stop" } }),
).toMatchObject({
success: true,
data: { item: { type: "tool_call", status: "canceled", callId: "stop" } },
});
});
it("accepts a minimal valid envelope and rejects a corrupted envelope", () => {
expect(GeneratedWSOutboundMessageSchema.safeParse({ type: "pong" }).success).toBe(true);
expect(GeneratedWSOutboundMessageSchema.safeParse({ type: "not_a_message" }).success).toBe(
false,
);
});
it("emits runtime imports with .js extensions", async () => {
const generated = await readFile(generatedWSOutboundPath, "utf8");
expect(generated).toContain('from "../../validation/ws-outbound-schema-metadata.js"');
});
it("accepts a forge.search.response envelope", () => {
const result = GeneratedWSOutboundMessageSchema.safeParse({
type: "session",
message: {
type: "forge.search.response",
payload: {
items: [
{
kind: "change_request",
number: 17,
title: "Fix search",
url: "https://gitlab.com/acme/repo/-/merge_requests/17",
state: "open",
body: null,
labels: [],
},
],
authState: "authenticated",
error: null,
requestId: "search-forge",
},
},
});
expect(result.success).toBe(true);
});
it("accepts a legacy github_search_response envelope", () => {
const result = GeneratedWSOutboundMessageSchema.safeParse({
type: "session",
message: {
type: "github_search_response",
payload: {
items: [
{
kind: "pr",
number: 42,
title: "Legacy PR",
url: "https://github.com/acme/repo/pull/42",
state: "open",
body: null,
labels: [],
},
],
featuresEnabled: true,
githubFeaturesEnabled: true,
authState: "authenticated",
error: null,
requestId: "search-github",
},
},
});
expect(result.success).toBe(true);
});
});