monorepo wit docs
This commit is contained in:
parent
88ddcfba39
commit
e3e4d1823d
43 changed files with 1585 additions and 29 deletions
99
apps/patch/test/discord.test.ts
Normal file
99
apps/patch/test/discord.test.ts
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
import { buildDiscordPayload, notifyDiscord, parseDiscordConfig } from "../src/discord";
|
||||
import type { FeedSignal } from "../src/types";
|
||||
|
||||
const feedSignal: FeedSignal = {
|
||||
sourceId: "github-openai-codex-main",
|
||||
provider: "github",
|
||||
event: "push",
|
||||
entryId: "tag:github.com,2008:Grit::Commit/0123456789abcdef0123456789abcdef01234567",
|
||||
title: "Tighten sandbox setup",
|
||||
url: "https://github.com/openai/codex/commit/0123456789abcdef0123456789abcdef01234567",
|
||||
author: "bookholt-oai",
|
||||
publishedAt: "2026-05-12T21:00:00.000Z",
|
||||
repo: {
|
||||
owner: "openai",
|
||||
name: "codex",
|
||||
fullName: "openai/codex",
|
||||
webUrl: "https://github.com/openai/codex",
|
||||
defaultBranch: "main",
|
||||
},
|
||||
ref: "refs/heads/main",
|
||||
sha: "0123456789abcdef0123456789abcdef01234567",
|
||||
target: {
|
||||
provider: "github",
|
||||
repoFullName: "peezy-tech/codex",
|
||||
branch: "main",
|
||||
mode: "notify_only",
|
||||
},
|
||||
raw: {},
|
||||
};
|
||||
|
||||
describe("discord notifications", () => {
|
||||
test("parses default notify events", () => {
|
||||
const config = parseDiscordConfig({});
|
||||
expect(config.enabled).toBe(false);
|
||||
expect(config.notifyEvents.has("push")).toBe(true);
|
||||
expect(config.notifyEvents.has("release")).toBe(true);
|
||||
expect(config.notifyEvents.has("ping")).toBe(false);
|
||||
});
|
||||
|
||||
test("parses explicit enable flag", () => {
|
||||
expect(parseDiscordConfig({ enabled: "true" }).enabled).toBe(true);
|
||||
expect(parseDiscordConfig({ enabled: "1" }).enabled).toBe(true);
|
||||
expect(parseDiscordConfig({ enabled: "yes" }).enabled).toBe(true);
|
||||
expect(parseDiscordConfig({ enabled: "false" }).enabled).toBe(false);
|
||||
});
|
||||
|
||||
test("builds readable feed embeds", () => {
|
||||
const payload = buildDiscordPayload({ signal: feedSignal });
|
||||
expect(payload.username).toBe("patch");
|
||||
expect(payload.embeds[0].title).toBe("[github] openai/codex upstream update on main");
|
||||
expect(payload.embeds[0].description).toBe("Tighten sandbox setup");
|
||||
expect(payload.embeds[0].url).toBe("https://github.com/openai/codex/commit/0123456789abcdef0123456789abcdef01234567");
|
||||
expect(payload.embeds[0].footer.text).toBe("feed watcher");
|
||||
});
|
||||
|
||||
test("does nothing without a webhook URL", async () => {
|
||||
let calls = 0;
|
||||
await notifyDiscord(parseDiscordConfig({ enabled: "true" }), { signal: feedSignal }, async () => {
|
||||
calls += 1;
|
||||
return new Response(null, { status: 204 });
|
||||
});
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
test("does nothing when Discord output is disabled", async () => {
|
||||
let calls = 0;
|
||||
await notifyDiscord(parseDiscordConfig({ webhookUrl: "https://discord.example/webhook", notifyEvents: "push" }), { signal: feedSignal }, async () => {
|
||||
calls += 1;
|
||||
return new Response(null, { status: 204 });
|
||||
});
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
test("skips unconfigured events", async () => {
|
||||
let calls = 0;
|
||||
await notifyDiscord(parseDiscordConfig({ enabled: "true", webhookUrl: "https://discord.example/webhook", notifyEvents: "release" }), { signal: feedSignal }, async () => {
|
||||
calls += 1;
|
||||
return new Response(null, { status: 204 });
|
||||
});
|
||||
expect(calls).toBe(0);
|
||||
});
|
||||
|
||||
test("posts configured events", async () => {
|
||||
let body = "";
|
||||
await notifyDiscord(parseDiscordConfig({ enabled: "true", webhookUrl: "https://discord.example/webhook", notifyEvents: "push" }), { signal: feedSignal }, async (_url, init) => {
|
||||
body = String(init?.body);
|
||||
return new Response(null, { status: 204 });
|
||||
});
|
||||
|
||||
expect(JSON.parse(body).embeds[0].title).toBe("[github] openai/codex upstream update on main");
|
||||
});
|
||||
|
||||
test("throws on Discord failure", async () => {
|
||||
await expect(notifyDiscord(parseDiscordConfig({ enabled: "true", webhookUrl: "https://discord.example/webhook" }), { signal: feedSignal }, async () => {
|
||||
return new Response("bad", { status: 500 });
|
||||
})).rejects.toThrow("Discord webhook returned 500");
|
||||
});
|
||||
});
|
||||
340
apps/patch/test/feed.test.ts
Normal file
340
apps/patch/test/feed.test.ts
Normal file
|
|
@ -0,0 +1,340 @@
|
|||
import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { loadSources, parseFeedEntries, pollFeedsOnce, signalFromEntry } from "../src/feed";
|
||||
import { dispatchFlowEvent, patchUpstreamReleaseEvent } from "../src/flow";
|
||||
import type { FeedSourceConfig } from "../src/types";
|
||||
|
||||
const atom = `<?xml version="1.0"?>
|
||||
<feed>
|
||||
<entry>
|
||||
<id>tag:github.com,2008:Grit::Commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/openai/codex/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"/>
|
||||
<title>Update main</title>
|
||||
<updated>2026-05-12T10:00:00Z</updated>
|
||||
<author><name>alice</name></author>
|
||||
</entry>
|
||||
<entry>
|
||||
<id>tag:github.com,2008:Grit::Commit/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb</id>
|
||||
<link type="text/html" rel="alternate" href="https://github.com/openai/codex/commit/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"/>
|
||||
<title>Older update</title>
|
||||
<updated>2026-05-12T09:00:00Z</updated>
|
||||
<author><name>bob</name></author>
|
||||
</entry>
|
||||
</feed>`;
|
||||
|
||||
const rss = `<?xml version="1.0"?>
|
||||
<rss><channel>
|
||||
<item>
|
||||
<title>v1.2.3</title>
|
||||
<link>https://codeberg.org/forgejo/forgejo/releases/tag/v1.2.3</link>
|
||||
<guid>release-123</guid>
|
||||
<author>release-team</author>
|
||||
<pubDate>Tue, 12 May 2026 10:00:00 +0000</pubDate>
|
||||
</item>
|
||||
</channel></rss>`;
|
||||
|
||||
const source: FeedSourceConfig = {
|
||||
id: "github-openai-codex-main",
|
||||
provider: "github",
|
||||
url: "https://github.com/openai/codex/commits/main.atom",
|
||||
event: "push",
|
||||
repo: {
|
||||
owner: "openai",
|
||||
name: "codex",
|
||||
fullName: "openai/codex",
|
||||
webUrl: "https://github.com/openai/codex",
|
||||
defaultBranch: "main",
|
||||
},
|
||||
target: {
|
||||
provider: "github",
|
||||
repoFullName: "peezy-tech/codex",
|
||||
branch: "main",
|
||||
mode: "notify_only",
|
||||
},
|
||||
};
|
||||
|
||||
describe("feed watcher", () => {
|
||||
test("parses Atom and RSS feed entries", () => {
|
||||
expect(parseFeedEntries(atom)[0]).toMatchObject({
|
||||
title: "Update main",
|
||||
author: "alice",
|
||||
url: "https://github.com/openai/codex/commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
});
|
||||
expect(parseFeedEntries(rss)[0]).toMatchObject({
|
||||
id: "release-123",
|
||||
title: "v1.2.3",
|
||||
author: "release-team",
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes commit feed entries into push signals", () => {
|
||||
const signal = signalFromEntry(source, parseFeedEntries(atom)[0]);
|
||||
expect(signal).toMatchObject({
|
||||
sourceId: "github-openai-codex-main",
|
||||
provider: "github",
|
||||
event: "push",
|
||||
ref: "refs/heads/main",
|
||||
sha: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
repo: { fullName: "openai/codex" },
|
||||
});
|
||||
});
|
||||
|
||||
test("loads configured feed sources", async () => {
|
||||
const sources = await loadSources(join(import.meta.dir, "..", "feed-sources.json"));
|
||||
expect(sources.map((item) => item.id)).toEqual([
|
||||
"codeberg-forgejo-branch",
|
||||
"codeberg-forgejo-releases",
|
||||
"github-openai-codex-main",
|
||||
"github-openai-codex-releases",
|
||||
]);
|
||||
});
|
||||
|
||||
test("first poll primes state without emitting old entries", async () => {
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "patch-feed-"));
|
||||
const sourcesPath = join(dataDir, "sources.json");
|
||||
await writeFile(sourcesPath, JSON.stringify({ sources: [source] }), "utf8");
|
||||
|
||||
await pollFeedsOnce({ dataDir, sourcesPath, discord: { enabled: true, webhookUrl: "https://discord.example/webhook", notifyEvents: new Set(["push"]) } }, async () => {
|
||||
return new Response(atom, { status: 200 });
|
||||
});
|
||||
|
||||
const state = JSON.parse(await readFile(join(dataDir, "feed-state.json"), "utf8"));
|
||||
expect(state["github-openai-codex-main"].lastSeenId).toBe("tag:github.com,2008:Grit::Commit/aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
await expect(readFile(join(dataDir, "feed-events.jsonl"), "utf8")).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("later polls emit new entries and release fork sync jobs", async () => {
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "patch-feed-"));
|
||||
const sourcesPath = join(dataDir, "sources.json");
|
||||
const releaseSource: FeedSourceConfig = {
|
||||
...source,
|
||||
id: "github-openai-codex-releases",
|
||||
url: "https://github.com/openai/codex/releases.atom",
|
||||
event: "release",
|
||||
target: {
|
||||
provider: "github",
|
||||
repoFullName: "peezy-tech/codex",
|
||||
branch: "main",
|
||||
mode: "fork_sync",
|
||||
},
|
||||
};
|
||||
await writeFile(sourcesPath, JSON.stringify({ sources: [releaseSource] }), "utf8");
|
||||
await writeFile(join(dataDir, "feed-state.json"), JSON.stringify({
|
||||
"github-openai-codex-releases": {
|
||||
lastSeenId: "older-release",
|
||||
lastCheckedAt: "2026-05-12T09:00:00.000Z",
|
||||
},
|
||||
}), "utf8");
|
||||
|
||||
let feedCalls = 0;
|
||||
await pollFeedsOnce({ dataDir, sourcesPath, discord: { enabled: false, notifyEvents: new Set(["release"]) } }, async () => {
|
||||
feedCalls += 1;
|
||||
return new Response(rss, { status: 200 });
|
||||
});
|
||||
|
||||
expect(await readFile(join(dataDir, "feed-events.jsonl"), "utf8")).toContain("\"event\":\"release\"");
|
||||
expect(await readFile(join(dataDir, "feed-jobs.jsonl"), "utf8")).toContain("\"kind\":\"fork_sync\"");
|
||||
expect(feedCalls).toBe(1);
|
||||
});
|
||||
|
||||
test("later polls dispatch generic flow events", async () => {
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "patch-feed-"));
|
||||
const sourcesPath = join(dataDir, "sources.json");
|
||||
const releaseSource: FeedSourceConfig = {
|
||||
...source,
|
||||
id: "github-openai-codex-releases",
|
||||
url: "https://github.com/openai/codex/releases.atom",
|
||||
event: "release",
|
||||
target: {
|
||||
mode: "flow_dispatch",
|
||||
eventType: "upstream.release",
|
||||
dispatchUrlEnv: "FLOW_URL",
|
||||
dispatchSecretEnv: "FLOW_SECRET",
|
||||
payload: {
|
||||
repo: "openai/codex",
|
||||
provider: "github",
|
||||
},
|
||||
},
|
||||
};
|
||||
await writeFile(sourcesPath, JSON.stringify({ sources: [releaseSource] }), "utf8");
|
||||
await writeFile(join(dataDir, "feed-state.json"), JSON.stringify({
|
||||
"github-openai-codex-releases": {
|
||||
lastSeenId: "older-release",
|
||||
lastCheckedAt: "2026-05-12T09:00:00.000Z",
|
||||
},
|
||||
}), "utf8");
|
||||
|
||||
let dispatchedBody = "";
|
||||
let dispatchedSignature = "";
|
||||
await pollFeedsOnce({
|
||||
dataDir,
|
||||
sourcesPath,
|
||||
discord: { enabled: false, notifyEvents: new Set(["release"]) },
|
||||
flowDispatch: {
|
||||
env: {
|
||||
FLOW_URL: "https://flow.example/events",
|
||||
FLOW_SECRET: "secret",
|
||||
},
|
||||
fetchImpl: async (_url, init) => {
|
||||
dispatchedBody = String(init.body);
|
||||
dispatchedSignature = headerValue(init.headers, "x-flow-signature-256");
|
||||
return Response.json({ status: "accepted", eventId: "event-1", runIds: [], matched: 0 }, { status: 202 });
|
||||
},
|
||||
},
|
||||
}, async () => {
|
||||
return new Response(rss, { status: 200 });
|
||||
});
|
||||
|
||||
const flowEventText = await readFile(join(dataDir, "flow-events.jsonl"), "utf8");
|
||||
const flowEvent = JSON.parse(flowEventText.trim()) as Record<string, any>;
|
||||
expect(flowEvent.type).toBe("upstream.release");
|
||||
expect(flowEvent.source).toBe("patch");
|
||||
expect(flowEvent.payload.repo).toBe("openai/codex");
|
||||
expect(flowEvent.payload.tag).toBe("v1.2.3");
|
||||
expect(JSON.parse(dispatchedBody).id).toBe(flowEvent.id);
|
||||
expect(dispatchedSignature).toMatch(/^sha256=[0-9a-f]{64}$/);
|
||||
expect(await readFile(join(dataDir, "flow-dispatches.jsonl"), "utf8")).toContain("\"status\":\"dispatched\"");
|
||||
});
|
||||
|
||||
test("flow dispatch uses default Patch env names", async () => {
|
||||
let dispatchedUrl = "";
|
||||
let dispatchedSignature = "";
|
||||
|
||||
const record = await dispatchFlowEvent({
|
||||
id: "patch:source:entry:upstream.release",
|
||||
type: "upstream.release",
|
||||
source: "patch",
|
||||
receivedAt: "2026-05-13T00:00:00.000Z",
|
||||
payload: { repo: "openai/codex", tag: "v1.2.3" },
|
||||
}, {}, {
|
||||
env: {
|
||||
PATCH_FLOW_DISPATCH_URL: "https://flow.example/events",
|
||||
PATCH_FLOW_DISPATCH_SECRET: "secret",
|
||||
},
|
||||
fetchImpl: async (url, init) => {
|
||||
dispatchedUrl = url;
|
||||
dispatchedSignature = headerValue(init.headers, "x-flow-signature-256");
|
||||
return Response.json({ status: "accepted", eventId: "event-1", runIds: [], matched: 0 }, { status: 202 });
|
||||
},
|
||||
});
|
||||
|
||||
expect(record.status).toBe("dispatched");
|
||||
expect(dispatchedUrl).toBe("https://flow.example/events");
|
||||
expect(dispatchedSignature).toMatch(/^sha256=[0-9a-f]{64}$/);
|
||||
});
|
||||
|
||||
test("flow dispatch records backend HTTP failures", async () => {
|
||||
const record = await dispatchFlowEvent({
|
||||
id: "patch:source:entry:upstream.release",
|
||||
type: "upstream.release",
|
||||
source: "patch",
|
||||
receivedAt: "2026-05-13T00:00:00.000Z",
|
||||
payload: { repo: "openai/codex", tag: "v1.2.3" },
|
||||
}, {}, {
|
||||
env: {
|
||||
PATCH_FLOW_DISPATCH_URL: "https://flow.example/events",
|
||||
},
|
||||
fetchImpl: async () => Response.json({ error: "bad" }, { status: 500 }),
|
||||
});
|
||||
|
||||
expect(record).toMatchObject({
|
||||
eventId: "patch:source:entry:upstream.release",
|
||||
status: "failed",
|
||||
httpStatus: 500,
|
||||
});
|
||||
});
|
||||
|
||||
test("flow dispatch uses local mode when no backend URL is configured", async () => {
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "patch-flow-local-"));
|
||||
await writeDemoFlow(dataDir);
|
||||
|
||||
const record = await dispatchFlowEvent({
|
||||
id: "patch:local:demo",
|
||||
type: "demo.event",
|
||||
source: "patch",
|
||||
receivedAt: "2026-05-15T00:00:00.000Z",
|
||||
payload: { name: "Ada" },
|
||||
}, {}, {
|
||||
cwd: dataDir,
|
||||
env: {},
|
||||
});
|
||||
|
||||
expect(record).toMatchObject({
|
||||
eventId: "patch:local:demo",
|
||||
eventType: "demo.event",
|
||||
status: "dispatched",
|
||||
});
|
||||
expect(record.url).toBeUndefined();
|
||||
expect(JSON.parse(await readFile(join(dataDir, "local-flow-output.json"), "utf8"))).toEqual({
|
||||
name: "Ada",
|
||||
});
|
||||
});
|
||||
|
||||
test("Patch upstream release helper creates deterministic product events", () => {
|
||||
expect(patchUpstreamReleaseEvent({
|
||||
repo: "openai/codex",
|
||||
tag: "rust-v1.2.3",
|
||||
receivedAt: "2026-05-15T00:00:00.000Z",
|
||||
})).toEqual({
|
||||
id: "patch:upstream.release:openai/codex:rust-v1.2.3",
|
||||
type: "upstream.release",
|
||||
source: "patch",
|
||||
receivedAt: "2026-05-15T00:00:00.000Z",
|
||||
payload: {
|
||||
repo: "openai/codex",
|
||||
tag: "rust-v1.2.3",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function headerValue(headers: HeadersInit | undefined, name: string): string {
|
||||
return new Headers(headers).get(name) ?? "";
|
||||
}
|
||||
|
||||
async function writeDemoFlow(root: string): Promise<void> {
|
||||
const flowRoot = join(root, "flows/demo");
|
||||
await mkdir(join(flowRoot, "exec"), { recursive: true });
|
||||
await mkdir(join(flowRoot, "schemas"), { recursive: true });
|
||||
await writeFile(
|
||||
join(flowRoot, "flow.toml"),
|
||||
[
|
||||
'name = "demo"',
|
||||
"version = 1",
|
||||
'description = "demo"',
|
||||
"",
|
||||
"[[steps]]",
|
||||
'name = "hello"',
|
||||
'runner = "bun"',
|
||||
'script = "exec/hello.ts"',
|
||||
"timeout_ms = 30000",
|
||||
"",
|
||||
"[steps.trigger]",
|
||||
'type = "demo.event"',
|
||||
'schema = "schemas/demo-event.schema.json"',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
await writeFile(
|
||||
join(flowRoot, "schemas/demo-event.schema.json"),
|
||||
JSON.stringify({
|
||||
type: "object",
|
||||
required: ["name"],
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
},
|
||||
}),
|
||||
);
|
||||
await writeFile(
|
||||
join(flowRoot, "exec/hello.ts"),
|
||||
[
|
||||
'import { writeFileSync } from "node:fs";',
|
||||
"const context = JSON.parse(await Bun.stdin.text());",
|
||||
'writeFileSync("../../local-flow-output.json", JSON.stringify({ name: context.flow.event.payload.name }));',
|
||||
'console.log(`FLOW_RESULT ${JSON.stringify({ status: "completed" })}`);',
|
||||
"",
|
||||
].join("\n"),
|
||||
);
|
||||
}
|
||||
113
apps/patch/test/server.test.ts
Normal file
113
apps/patch/test/server.test.ts
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
import { mkdtemp } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import { EventStore } from "../src/queue";
|
||||
import { createHandler } from "../src/server";
|
||||
|
||||
describe("server", () => {
|
||||
test("healthz returns ok", async () => {
|
||||
const handler = createHandler({ dataDir: await mkdtemp(join(tmpdir(), "patch-")) });
|
||||
const response = await handler(new Request("http://localhost/healthz"));
|
||||
expect(response.status).toBe(200);
|
||||
expect(await response.text()).toBe("ok\n");
|
||||
});
|
||||
|
||||
test("does not serve provider intake routes", async () => {
|
||||
const handler = createHandler({ dataDir: await mkdtemp(join(tmpdir(), "patch-")) });
|
||||
const github = await handler(new Request("http://localhost/github", { method: "POST", body: "{}" }));
|
||||
const jojo = await handler(new Request("http://localhost/jojo", { method: "POST", body: "{}" }));
|
||||
const prefixedJojo = await handler(new Request("http://localhost/prefix/jojo", { method: "POST", body: "{}" }));
|
||||
const prefixedGithub = await handler(new Request("http://localhost/prefix/github", { method: "POST", body: "{}" }));
|
||||
expect(github.status).toBe(404);
|
||||
expect(jojo.status).toBe(404);
|
||||
expect(prefixedJojo.status).toBe(404);
|
||||
expect(prefixedGithub.status).toBe(404);
|
||||
});
|
||||
|
||||
test("lists, retries, and replays stored flow events behind admin auth", async () => {
|
||||
const originalFetch = globalThis.fetch;
|
||||
const originalDispatchUrl = process.env.PATCH_FLOW_DISPATCH_URL;
|
||||
const originalDispatchSecret = process.env.PATCH_FLOW_DISPATCH_SECRET;
|
||||
const dataDir = await mkdtemp(join(tmpdir(), "patch-"));
|
||||
const store = new EventStore(dataDir);
|
||||
const event = {
|
||||
id: "patch:source:entry:upstream.release",
|
||||
type: "upstream.release",
|
||||
source: "patch",
|
||||
receivedAt: "2026-05-13T00:00:00.000Z",
|
||||
payload: { repo: "openai/codex", tag: "v1.2.3" },
|
||||
};
|
||||
await store.appendFlowEvent(event);
|
||||
await store.appendFlowDispatch({
|
||||
eventId: event.id,
|
||||
eventType: event.type,
|
||||
status: "failed",
|
||||
error: "network",
|
||||
createdAt: "2026-05-13T00:00:01.000Z",
|
||||
});
|
||||
|
||||
const calls: Array<{ url: string; body: string; headers: Headers }> = [];
|
||||
process.env.PATCH_FLOW_DISPATCH_URL = "http://172.20.0.1:7345/events";
|
||||
process.env.PATCH_FLOW_DISPATCH_SECRET = "secret";
|
||||
globalThis.fetch = (async (url: string | URL | Request, init?: RequestInit) => {
|
||||
calls.push({
|
||||
url: String(url),
|
||||
body: String(init?.body ?? ""),
|
||||
headers: new Headers(init?.headers),
|
||||
});
|
||||
return Response.json({ status: "accepted", eventId: event.id, runIds: [], matched: 0 }, { status: 202 });
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
try {
|
||||
const handler = createHandler({
|
||||
dataDir,
|
||||
adminToken: "admin",
|
||||
});
|
||||
|
||||
const unauthorized = await handler(new Request("http://localhost/flow-events"));
|
||||
expect(unauthorized.status).toBe(401);
|
||||
|
||||
const list = await handler(new Request("http://localhost/flow-events", {
|
||||
headers: { authorization: "Bearer admin" },
|
||||
}));
|
||||
expect(list.status).toBe(200);
|
||||
expect(await list.json()).toMatchObject({ events: [{ id: event.id, type: event.type }] });
|
||||
|
||||
const dispatches = await handler(new Request("http://localhost/flow-dispatches?status=failed", {
|
||||
headers: { "x-patch-admin-token": "admin" },
|
||||
}));
|
||||
expect(dispatches.status).toBe(200);
|
||||
expect(await dispatches.json()).toMatchObject({ dispatches: [{ status: "failed", eventId: event.id }] });
|
||||
|
||||
const retry = await handler(new Request(`http://localhost/flow-events/${encodeURIComponent(event.id)}/retry`, {
|
||||
method: "POST",
|
||||
headers: { authorization: "Bearer admin" },
|
||||
}));
|
||||
expect(retry.status).toBe(202);
|
||||
expect(calls.at(-1)?.url).toBe("http://172.20.0.1:7345/events");
|
||||
expect(JSON.parse(calls.at(-1)?.body ?? "{}")).toMatchObject({ id: event.id });
|
||||
expect(calls.at(-1)?.headers.get("x-flow-signature-256")).toMatch(/^sha256=[0-9a-f]{64}$/);
|
||||
|
||||
const replay = await handler(new Request(`http://localhost/flow-events/${encodeURIComponent(event.id)}/replay`, {
|
||||
method: "POST",
|
||||
headers: { authorization: "Bearer admin" },
|
||||
}));
|
||||
expect(replay.status).toBe(202);
|
||||
expect(calls.at(-1)?.url).toBe(`http://172.20.0.1:7345/events/${encodeURIComponent(event.id)}/replay`);
|
||||
expect(JSON.parse(calls.at(-1)?.body ?? "{}")).toEqual({ wait: false });
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
if (originalDispatchUrl === undefined) {
|
||||
delete process.env.PATCH_FLOW_DISPATCH_URL;
|
||||
} else {
|
||||
process.env.PATCH_FLOW_DISPATCH_URL = originalDispatchUrl;
|
||||
}
|
||||
if (originalDispatchSecret === undefined) {
|
||||
delete process.env.PATCH_FLOW_DISPATCH_SECRET;
|
||||
} else {
|
||||
process.env.PATCH_FLOW_DISPATCH_SECRET = originalDispatchSecret;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
10
apps/patch/test/signatures.test.ts
Normal file
10
apps/patch/test/signatures.test.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { describe, expect, test } from "bun:test";
|
||||
import { hmacSha256Hex } from "../src/signatures";
|
||||
|
||||
describe("flow signatures", () => {
|
||||
test("builds HMAC-SHA256 digests for flow dispatch signing", async () => {
|
||||
expect(await hmacSha256Hex("secret", "payload")).toBe(
|
||||
"b82fcb791acec57859b989b430a826488ce2e479fdf92326bd0a2e8375a42ba4",
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue