源码快照 @ 2026-09-02 (source-only, 剔除非源码素材)
This commit is contained in:
@@ -0,0 +1,184 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { X509Certificate } from "node:crypto";
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "node:test";
|
||||
|
||||
import {
|
||||
createBlipSchedule,
|
||||
denyHostsFromOutboundManifest,
|
||||
opensslCertificateCommands,
|
||||
opensslFlavor,
|
||||
outboundManifestFromUnknown,
|
||||
parseClientHelloVersions,
|
||||
resolveEgressProfileConfig,
|
||||
startEgressLab,
|
||||
} from "../src/egress.ts";
|
||||
import { productDiagnosticsPrecondition } from "@openwork/behaviors";
|
||||
import { matchVerdictExpectations } from "@openwork/matchers";
|
||||
|
||||
function hi(value: number): number {
|
||||
return (value >> 8) & 0xff;
|
||||
}
|
||||
|
||||
function lo(value: number): number {
|
||||
return value & 0xff;
|
||||
}
|
||||
|
||||
function clientHelloFixture(versions: number[]): Buffer {
|
||||
const supportedVersions = versions.flatMap((version) => [hi(version), lo(version)]);
|
||||
const extensions = supportedVersions.length > 0
|
||||
? [0x00, 0x2b, 0x00, supportedVersions.length + 1, supportedVersions.length, ...supportedVersions]
|
||||
: [];
|
||||
const body = [
|
||||
0x03,
|
||||
0x03,
|
||||
...Array.from({ length: 32 }, () => 0),
|
||||
0x00,
|
||||
0x00,
|
||||
0x02,
|
||||
0x00,
|
||||
0x2f,
|
||||
0x01,
|
||||
0x00,
|
||||
hi(extensions.length),
|
||||
lo(extensions.length),
|
||||
...extensions,
|
||||
];
|
||||
const handshakeLength = body.length;
|
||||
const recordLength = handshakeLength + 4;
|
||||
return Buffer.from([
|
||||
0x16,
|
||||
0x03,
|
||||
0x01,
|
||||
hi(recordLength),
|
||||
lo(recordLength),
|
||||
0x01,
|
||||
0x00,
|
||||
hi(handshakeLength),
|
||||
lo(handshakeLength),
|
||||
...body,
|
||||
]);
|
||||
}
|
||||
|
||||
test("ClientHello parser distinguishes TLS 1.2-only from TLS 1.3-capable clients", () => {
|
||||
const tls12 = parseClientHelloVersions(clientHelloFixture([0x0303]));
|
||||
if (tls12.kind !== "parsed") throw new Error(`unexpected parse result: ${tls12.kind}`);
|
||||
assert.equal(tls12.offersTls13, false);
|
||||
assert.deepEqual(tls12.supportedVersionLabels, ["TLSv1.2"]);
|
||||
|
||||
const tls13 = parseClientHelloVersions(clientHelloFixture([0x0304, 0x0303]));
|
||||
if (tls13.kind !== "parsed") throw new Error(`unexpected parse result: ${tls13.kind}`);
|
||||
assert.equal(tls13.offersTls13, true);
|
||||
assert.deepEqual(tls13.supportedVersionLabels, ["TLSv1.3", "TLSv1.2"]);
|
||||
});
|
||||
|
||||
test("profile config resolution normalizes defaults", () => {
|
||||
const config = resolveEgressProfileConfig({
|
||||
profile: "deny",
|
||||
port: 0,
|
||||
denyHosts: ["GitHub.com", "github.com.", ""],
|
||||
denyMode: "blackhole",
|
||||
slow: { totalBytes: 10, chunkBytes: 4, delayMs: 0, latencyMs: 1 },
|
||||
blip: { route: "api/den", count: 2, fault: "reset" },
|
||||
});
|
||||
|
||||
assert.equal(config.port, null);
|
||||
assert.deepEqual(config.denyHosts, ["github.com"]);
|
||||
assert.equal(config.denyMode, "blackhole");
|
||||
assert.equal(config.slow.totalBytes, 10);
|
||||
assert.equal(config.blip.route, "/api/den");
|
||||
assert.equal(config.blip.count, 2);
|
||||
assert.equal(config.blip.fault, "reset");
|
||||
});
|
||||
|
||||
test("openssl argv construction includes CA, AIA, and fullchain prerequisites", () => {
|
||||
const commands = opensslCertificateCommands({
|
||||
dir: "/tmp/openwork-egress-test",
|
||||
hostname: "localhost",
|
||||
aiaUrl: "http://127.0.0.1:9000/__egress-lab/intermediate.der",
|
||||
corporateIssuer: true,
|
||||
});
|
||||
const labels = commands.map((command) => command.label);
|
||||
|
||||
assert.deepEqual(labels, [
|
||||
"root-key",
|
||||
"root-csr",
|
||||
"root-cert",
|
||||
"intermediate-key",
|
||||
"intermediate-csr",
|
||||
"intermediate-cert",
|
||||
"intermediate-der",
|
||||
"leaf-key",
|
||||
"leaf-csr",
|
||||
"leaf-cert",
|
||||
]);
|
||||
assert.ok(commands.some((command) => command.args.includes("/CN=OpenWork Egress Lab Corporate Interception CA")));
|
||||
assert.ok(commands.some((command) => command.args.includes("-extfile")));
|
||||
const rootCert = commands.find((command) => command.label === "root-cert");
|
||||
assert.ok(rootCert, "root-cert command must exist");
|
||||
assert.ok(
|
||||
rootCert.args.includes("-extfile"),
|
||||
"root CA extensions must come from -extfile; openssl 1.1.1 ignores `req -x509 -addext`",
|
||||
);
|
||||
assert.ok(!rootCert.args.includes("-addext"), "root-cert must not depend on -addext");
|
||||
});
|
||||
|
||||
test("openssl flavor probe returns a known value", async () => {
|
||||
const flavor = await opensslFlavor();
|
||||
assert.ok(["openssl", "libressl", "unknown"].includes(flavor));
|
||||
});
|
||||
|
||||
test("deny-list seeding reads installer-critical hosts from the outbound manifest", () => {
|
||||
const manifest = outboundManifestFromUnknown({
|
||||
hosts: [
|
||||
{ host: "github.com", kind: "fetched", components: ["installer"], requirement: "required-in-practice", blockedEffect: "installer dead" },
|
||||
{ host: "release-assets.githubusercontent.com", kind: "redirect-target", components: ["installer"], requirement: "required-in-practice", blockedEffect: "download fails" },
|
||||
{ host: "cdn.simpleicons.org", kind: "fetched", components: ["renderer"], requirement: "optional", blockedEffect: "icons missing" },
|
||||
],
|
||||
});
|
||||
if (!manifest) throw new Error("manifest fixture did not parse");
|
||||
|
||||
assert.deepEqual(denyHostsFromOutboundManifest(manifest), ["github.com", "release-assets.githubusercontent.com"]);
|
||||
});
|
||||
|
||||
test("blip schedule fires once, then stops", () => {
|
||||
const schedule = createBlipSchedule({ route: "/v1/me", count: 1, fault: "401", status: 401, body: "proxy" });
|
||||
const first = schedule.next("/v1/me");
|
||||
const second = schedule.next("/v1/me");
|
||||
|
||||
assert.deepEqual(first, { kind: "status", status: 401, body: "proxy" });
|
||||
assert.deepEqual(second, { kind: "pass" });
|
||||
assert.equal(schedule.snapshot()[0]?.remaining, 0);
|
||||
});
|
||||
|
||||
test("verdict matcher names injected faults and rejects vague output", () => {
|
||||
assert.equal(matchVerdictExpectations("LIKELY TLS VERSION/HANDSHAKE FAULT: TLS 1.3 timed out while TLS 1.2 completed", "tls12-only").ok, true);
|
||||
assert.equal(matchVerdictExpectations("endpoint_tls_handshake_timeout_tls12_comparison_failed: TLS 1.3 timed out and the runtime TLS 1.2 comparison also timed out", "tls12-only").ok, true);
|
||||
assert.equal(matchVerdictExpectations("LIKELY MISSING INTERMEDIATE OR UNTRUSTED ROOT", "broken-chain").ok, true);
|
||||
assert.equal(matchVerdictExpectations("LIKELY TLS INTERCEPTION: endpoint leaf is re-signed by Corporate CA", "intercept").ok, true);
|
||||
assert.equal(matchVerdictExpectations("BLOCKED HOST / PROXY DENY: github.com is blocked", "deny").ok, true);
|
||||
assert.deepEqual(matchVerdictExpectations("TypeError: fetch failed", ["tls12-only", "broken-chain"]), {
|
||||
ok: false,
|
||||
missing: ["tls12-only", "broken-chain"],
|
||||
});
|
||||
});
|
||||
|
||||
test("product diagnostics precondition skips clearly when Bun is unavailable", () => {
|
||||
const reason = productDiagnosticsPrecondition({ PATH: "/definitely-no-bun-here" });
|
||||
|
||||
if (!reason) throw new Error("Expected a missing-Bun skip reason");
|
||||
assert.ok(reason.includes("Bun is required"));
|
||||
assert.ok(reason.includes("product-verdict egress proofs"));
|
||||
});
|
||||
|
||||
test("generated egress lab CA certificates carry basicConstraints on every openssl build", async () => {
|
||||
await using lab = await startEgressLab({ profile: "tls12-only" });
|
||||
assert.ok(lab.rootPem, "lab must expose its root PEM");
|
||||
const root = new X509Certificate(lab.rootPem);
|
||||
// openssl 1.1.1 silently produced a non-CA root, which broke chain building
|
||||
// only on runners shipping 1.1.1 (macos-14).
|
||||
assert.equal(root.ca, true, `root must be a CA (subject: ${root.subject})`);
|
||||
assert.ok(lab.intermediatePemPath, "lab must expose its intermediate path");
|
||||
const intermediate = new X509Certificate(readFileSync(lab.intermediatePemPath, "utf8"));
|
||||
assert.equal(intermediate.ca, true, `intermediate must be a CA (subject: ${intermediate.subject})`);
|
||||
});
|
||||
@@ -0,0 +1,198 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { X509Certificate } from "node:crypto";
|
||||
import { access, chmod, mkdtemp, readFile, rm, stat, symlink, writeFile } from "node:fs/promises";
|
||||
import http from "node:http";
|
||||
import https from "node:https";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import tls from "node:tls";
|
||||
import { test } from "node:test";
|
||||
|
||||
import { stageTrustedEnterpriseTlsRoot, startEnterpriseTlsReverseEdge } from "../src/egress.ts";
|
||||
|
||||
function listen(server: http.Server): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") return reject(new Error("server did not bind"));
|
||||
resolve(address.port);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function close(server: http.Server): Promise<void> {
|
||||
return new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
||||
}
|
||||
|
||||
function request(url: string, options: { ca?: string; method?: string; path?: string; body?: string } = {}): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const target = new URL(url);
|
||||
const outgoing = https.request({
|
||||
hostname: target.hostname,
|
||||
port: target.port,
|
||||
ca: options.ca,
|
||||
method: options.method,
|
||||
path: options.path ?? `${target.pathname}${target.search}`,
|
||||
}, (response) => {
|
||||
const chunks: Buffer[] = [];
|
||||
response.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
|
||||
response.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
||||
});
|
||||
outgoing.on("error", reject);
|
||||
outgoing.end(options.body);
|
||||
});
|
||||
}
|
||||
|
||||
function peerCertificatePem(url: string, ca: string): Promise<string> {
|
||||
const target = new URL(url);
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = tls.connect({ host: target.hostname, port: Number(target.port), ca, servername: "localhost" }, () => {
|
||||
const raw = socket.getPeerCertificate().raw;
|
||||
socket.end();
|
||||
if (!raw) reject(new Error("peer did not provide a certificate"));
|
||||
else resolve(new X509Certificate(raw).toString());
|
||||
});
|
||||
socket.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function manifest(rootPemPath: string): string {
|
||||
return `${JSON.stringify({
|
||||
pid: process.pid,
|
||||
candidateUrl: "https://localhost:8443",
|
||||
negativeUrl: "https://localhost:9443",
|
||||
adminUrl: "http://127.0.0.1:8445",
|
||||
rootPemPath,
|
||||
})}\n`;
|
||||
}
|
||||
|
||||
test("enterprise TLS privileged trust material is validated and staged", async (t) => {
|
||||
const edge = await startEnterpriseTlsReverseEdge({ upstream: "http://127.0.0.1:1" });
|
||||
const dir = await mkdtemp(path.join(tmpdir(), "openwork-enterprise-tls-validation-test-"));
|
||||
const uid = typeof process.getuid === "function" ? process.getuid() : 0;
|
||||
try {
|
||||
await t.test("accepts an owned regular manifest and CA PEM", async () => {
|
||||
const rootPemPath = path.join(dir, "happy-root.pem");
|
||||
const manifestPath = path.join(dir, "happy-manifest.json");
|
||||
await writeFile(rootPemPath, edge.rootPem, { mode: 0o600 });
|
||||
await writeFile(manifestPath, manifest(rootPemPath), { mode: 0o600 });
|
||||
const staged = await stageTrustedEnterpriseTlsRoot(manifestPath, [uid]);
|
||||
try {
|
||||
assert.deepEqual(staged.rootPem, Buffer.from(edge.rootPem));
|
||||
assert.deepEqual(await readFile(staged.stagedRootPemPath), staged.rootPem);
|
||||
assert.notEqual(staged.stagedRootPemPath, rootPemPath);
|
||||
assert.equal((await stat(staged.stagedRootPemPath)).mode & 0o777, 0o600);
|
||||
} finally {
|
||||
await staged.cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
await t.test("rejects a symlinked manifest", async () => {
|
||||
const rootPemPath = path.join(dir, "manifest-link-root.pem");
|
||||
const targetPath = path.join(dir, "manifest-target.json");
|
||||
const manifestPath = path.join(dir, "manifest-link.json");
|
||||
await writeFile(rootPemPath, edge.rootPem, { mode: 0o600 });
|
||||
await writeFile(targetPath, manifest(rootPemPath), { mode: 0o600 });
|
||||
await symlink(targetPath, manifestPath);
|
||||
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), { name: "ENTERPRISE_TLS_MANIFEST_UNTRUSTED" });
|
||||
});
|
||||
|
||||
await t.test("rejects a symlinked PEM", async () => {
|
||||
const targetPath = path.join(dir, "pem-target.pem");
|
||||
const rootPemPath = path.join(dir, "pem-link.pem");
|
||||
const manifestPath = path.join(dir, "pem-link-manifest.json");
|
||||
await writeFile(targetPath, edge.rootPem, { mode: 0o600 });
|
||||
await symlink(targetPath, rootPemPath);
|
||||
await writeFile(manifestPath, manifest(rootPemPath), { mode: 0o600 });
|
||||
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), { name: "ENTERPRISE_TLS_ROOT_PEM_UNTRUSTED" });
|
||||
});
|
||||
|
||||
await t.test("rejects a world-writable PEM", async () => {
|
||||
const rootPemPath = path.join(dir, "writable-root.pem");
|
||||
const manifestPath = path.join(dir, "writable-manifest.json");
|
||||
await writeFile(rootPemPath, edge.rootPem, { mode: 0o600 });
|
||||
await chmod(rootPemPath, 0o666);
|
||||
await writeFile(manifestPath, manifest(rootPemPath), { mode: 0o600 });
|
||||
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), { name: "ENTERPRISE_TLS_ROOT_PEM_UNTRUSTED" });
|
||||
});
|
||||
|
||||
await t.test("rejects non-CA and unparseable PEM files", async () => {
|
||||
const manifestPath = path.join(dir, "invalid-manifest.json");
|
||||
const rootPemPath = path.join(dir, "invalid-root.pem");
|
||||
await writeFile(manifestPath, manifest(rootPemPath), { mode: 0o600 });
|
||||
await writeFile(rootPemPath, await peerCertificatePem(edge.candidateUrl, edge.rootPem), { mode: 0o600 });
|
||||
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), /certificate is not a CA/u);
|
||||
await writeFile(rootPemPath, "not a certificate\n", { mode: 0o600 });
|
||||
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), { name: "ENTERPRISE_TLS_ROOT_PEM_UNTRUSTED" });
|
||||
});
|
||||
|
||||
await t.test("rejects a relative rootPemPath", async () => {
|
||||
const manifestPath = path.join(dir, "relative-manifest.json");
|
||||
await writeFile(manifestPath, manifest("relative-root.pem"), { mode: 0o600 });
|
||||
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid]), /rootPemPath must be absolute/u);
|
||||
});
|
||||
|
||||
await t.test("rejects an owner outside the explicit allowed uid set", async () => {
|
||||
const rootPemPath = path.join(dir, "owner-root.pem");
|
||||
const manifestPath = path.join(dir, "owner-manifest.json");
|
||||
await writeFile(rootPemPath, edge.rootPem, { mode: 0o600 });
|
||||
await writeFile(manifestPath, manifest(rootPemPath), { mode: 0o600 });
|
||||
await assert.rejects(stageTrustedEnterpriseTlsRoot(manifestPath, [uid + 1]), /owner uid .* is not allowed/u);
|
||||
});
|
||||
} finally {
|
||||
await edge.stop();
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("enterprise TLS edge pins its upstream and exposes selective trust", async () => {
|
||||
const upstreamRequests: { method: string; url: string; body: string }[] = [];
|
||||
const upstream = http.createServer(async (incoming, response) => {
|
||||
const chunks: Buffer[] = [];
|
||||
for await (const chunk of incoming) chunks.push(Buffer.from(chunk));
|
||||
upstreamRequests.push({ method: incoming.method ?? "GET", url: incoming.url ?? "/", body: Buffer.concat(chunks).toString("utf8") });
|
||||
response.end("from-pinned-upstream");
|
||||
});
|
||||
const attackerHits: string[] = [];
|
||||
const attacker = http.createServer((incoming, response) => {
|
||||
attackerHits.push(incoming.url ?? "/");
|
||||
response.end("attacker");
|
||||
});
|
||||
const upstreamPort = await listen(upstream);
|
||||
const attackerPort = await listen(attacker);
|
||||
const edge = await startEnterpriseTlsReverseEdge({ upstream: `http://127.0.0.1:${upstreamPort}` });
|
||||
const rootPath = edge.rootPemPath;
|
||||
try {
|
||||
await assert.rejects(request(`${edge.candidateUrl}/default-trust`));
|
||||
assert.equal(await request(`${edge.candidateUrl}/v1/me?full=1`, { ca: edge.rootPem, method: "POST", body: "payload" }), "from-pinned-upstream");
|
||||
await assert.rejects(request(`${edge.negativeUrl}/negative`, { ca: edge.rootPem }));
|
||||
|
||||
const absoluteTarget = `http://127.0.0.1:${attackerPort}/stolen?token=yes`;
|
||||
assert.equal(await request(edge.candidateUrl, { ca: edge.rootPem, path: absoluteTarget }), "from-pinned-upstream");
|
||||
assert.deepEqual(attackerHits, []);
|
||||
assert.deepEqual(upstreamRequests, [
|
||||
{ method: "POST", url: "/v1/me?full=1", body: "payload" },
|
||||
{ method: "GET", url: "/stolen?token=yes", body: "" },
|
||||
]);
|
||||
assert.deepEqual(edge.requests.map(({ endpoint, method, path, body }) => ({ endpoint, method, path, body })), [
|
||||
{ endpoint: "trusted-candidate", method: "POST", path: "/v1/me?full=1", body: "payload" },
|
||||
{ endpoint: "trusted-candidate", method: "GET", path: "/stolen?token=yes", body: "" },
|
||||
]);
|
||||
assert.equal(edge.linuxTrust.restartApplication, true);
|
||||
assert.equal(edge.linuxTrust.install()[1]?.file, "/usr/sbin/update-ca-certificates");
|
||||
assert.equal(edge.linuxTrust.prerequisiteFailures.root, "ENTERPRISE_TLS_LINUX_ROOT_REQUIRED");
|
||||
const prerequisites = await edge.linuxTrust.checkPrerequisites();
|
||||
if (!prerequisites.ok) {
|
||||
assert.ok([
|
||||
"ENTERPRISE_TLS_LINUX_ROOT_REQUIRED",
|
||||
"ENTERPRISE_TLS_UPDATE_CA_CERTIFICATES_REQUIRED",
|
||||
].includes(prerequisites.failure));
|
||||
}
|
||||
} finally {
|
||||
await edge.stop();
|
||||
await Promise.all([close(upstream), close(attacker)]);
|
||||
}
|
||||
await assert.rejects(access(rootPath));
|
||||
await assert.rejects(request(edge.candidateUrl, { ca: edge.rootPem }));
|
||||
});
|
||||
@@ -0,0 +1,124 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import {
|
||||
buildBlockedUserResponse,
|
||||
buildOidcClaims,
|
||||
buildRs256SigningArgv,
|
||||
buildWrongDomainEmail,
|
||||
matchSsoExpectation,
|
||||
normalizeCertificateMaterial,
|
||||
normalizeDomain,
|
||||
normalizeMockIdpConfig,
|
||||
startMockIdpLab,
|
||||
subjectWithKnobs,
|
||||
validateSsoConfiguration,
|
||||
} from "../src/idp.ts";
|
||||
|
||||
test("mock IdP config normalization trims issuer, domain, and credentials", () => {
|
||||
const config = normalizeMockIdpConfig({
|
||||
issuer: " https://idp.example.test/ ",
|
||||
domain: " @Acme.TEST. ",
|
||||
clientId: " client-one ",
|
||||
clientSecret: " secret-one ",
|
||||
});
|
||||
|
||||
assert.equal(config.issuer, "https://idp.example.test");
|
||||
assert.equal(config.domain, "acme.test");
|
||||
assert.equal(config.clientId, "client-one");
|
||||
assert.equal(config.clientSecret, "secret-one");
|
||||
assert.equal(config.defaultSubject.email, "sso.user@acme.test");
|
||||
});
|
||||
|
||||
test("certificate normalizer names the trailing-newline paste bug", () => {
|
||||
const cert = "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----\n";
|
||||
const normalized = normalizeCertificateMaterial(cert);
|
||||
const validation = validateSsoConfiguration({ configuredDomain: "acme.test", cert, subjectEmail: "maya@acme.test" });
|
||||
const match = matchSsoExpectation(validation, { code: "sso_cert_trailing_newline", includes: ["trailing newline", "certificate"] });
|
||||
|
||||
assert.equal(normalized.hadTrailingNewline, true);
|
||||
assert.equal(normalized.value.endsWith("\n"), false);
|
||||
assert.equal(validation.ok, false);
|
||||
assert.equal(match.passed, true);
|
||||
});
|
||||
|
||||
test("domain mismatch normalizer names IdP email domains that do not match the org", () => {
|
||||
const wrongEmail = buildWrongDomainEmail("maya@acme.test", "acme.test");
|
||||
const validation = validateSsoConfiguration({ configuredDomain: "ACME.test", subjectEmail: wrongEmail });
|
||||
const match = matchSsoExpectation(validation, { code: "sso_domain_mismatch", includes: ["wrong-acme.test", "acme.test"] });
|
||||
|
||||
assert.equal(wrongEmail, "maya@wrong-acme.test");
|
||||
assert.equal(normalizeDomain(" @ACME.test. "), "acme.test");
|
||||
assert.equal(validation.normalizedDomain, "acme.test");
|
||||
assert.equal(match.passed, true);
|
||||
});
|
||||
|
||||
test("claim builder supports present, absent, and unexpected group claim shapes", () => {
|
||||
const baseConfig = normalizeMockIdpConfig({ domain: "acme.test" });
|
||||
const subject = subjectWithKnobs(baseConfig, { email: "dev@acme.test", name: "Dev User" });
|
||||
const present = buildOidcClaims({ issuer: baseConfig.issuer, clientId: baseConfig.clientId, subject, knobs: baseConfig.knobs });
|
||||
const absentConfig = normalizeMockIdpConfig({ knobs: { groupClaims: "absent" } });
|
||||
const absent = buildOidcClaims({ issuer: absentConfig.issuer, clientId: absentConfig.clientId, subject, knobs: absentConfig.knobs });
|
||||
const oddConfig = normalizeMockIdpConfig({ knobs: { groupClaims: "unexpected-shape" } });
|
||||
const odd = buildOidcClaims({ issuer: oddConfig.issuer, clientId: oddConfig.clientId, subject, knobs: oddConfig.knobs });
|
||||
|
||||
assert.deepEqual(present.groups, ["Engineering", "OpenWork Lab"]);
|
||||
assert.equal("groups" in absent, false);
|
||||
assert.deepEqual(odd.groups, { primary: "Engineering", all: ["Engineering", "OpenWork Lab"] });
|
||||
});
|
||||
|
||||
test("email mismatch and guest-user knobs shape SSO subjects", () => {
|
||||
const mismatchConfig = normalizeMockIdpConfig({ knobs: { emailMismatch: "other@acme.test" } });
|
||||
const guestConfig = normalizeMockIdpConfig({ knobs: { guestUser: true } });
|
||||
|
||||
assert.equal(subjectWithKnobs(mismatchConfig, { email: "invited@acme.test" }).email, "other@acme.test");
|
||||
assert.equal(subjectWithKnobs(guestConfig, { email: "guest@external.test" }).sub, "guest:guest@external.test");
|
||||
});
|
||||
|
||||
test("RS256 signing argv documents the dependency-free node:crypto path", () => {
|
||||
assert.deepEqual(buildRs256SigningArgv({ keyId: "kid-one", payload: "abc" }), [
|
||||
"node:crypto",
|
||||
"createSign",
|
||||
"RSA-SHA256",
|
||||
"--kid",
|
||||
"kid-one",
|
||||
"--payload-bytes",
|
||||
"3",
|
||||
]);
|
||||
});
|
||||
|
||||
test("blocked-user response uses the Entra-style policy wording", () => {
|
||||
const response = buildBlockedUserResponse({ email: "blocked@acme.test" });
|
||||
|
||||
assert.equal(response.status, 403);
|
||||
assert.equal(response.error, "access_denied");
|
||||
assert.match(response.errorDescription, /administrator has configured the application to block users/);
|
||||
assert.match(response.message, /IdP administrator/);
|
||||
assert.match(response.html, /OpenWork Mock IdP policy/);
|
||||
});
|
||||
|
||||
test("mock IdP escapes request-derived blocked-user HTML", async () => {
|
||||
const loginHint = "blocked@acme.test<script>alert(1)</script>";
|
||||
await using lab = await startMockIdpLab({ knobs: { blockedUser: loginHint } });
|
||||
const url = new URL(lab.authorizationEndpoint);
|
||||
url.searchParams.set("client_id", lab.config.clientId);
|
||||
url.searchParams.set("redirect_uri", "http://127.0.0.1/callback");
|
||||
url.searchParams.set("login_hint", loginHint);
|
||||
|
||||
const response = await fetch(url);
|
||||
const body = await response.text();
|
||||
assert.equal(response.status, 403);
|
||||
// Plain substring checks: stricter than a tag regex (`<script` also catches
|
||||
// `<script src=...`) and they cannot be mistaken for a tag-filtering sanitizer.
|
||||
assert.ok(!body.includes("<script"), `escaped body must not contain raw markup: ${body}`);
|
||||
assert.ok(body.includes("<script>"), `escaped body must contain the escaped payload: ${body}`);
|
||||
});
|
||||
|
||||
test("expectation matcher fails with actionable detail when the named error is missing", () => {
|
||||
const validation = validateSsoConfiguration({ configuredDomain: "acme.test", subjectEmail: "maya@acme.test" });
|
||||
const match = matchSsoExpectation(validation, { code: "sso_domain_mismatch" });
|
||||
|
||||
assert.equal(match.passed, false);
|
||||
assert.deepEqual(match.actualCodes, []);
|
||||
assert.match(match.detail, /Expected sso_domain_mismatch/);
|
||||
});
|
||||
@@ -0,0 +1,338 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { createHash } from "node:crypto";
|
||||
import { createServer } from "node:http";
|
||||
import type { Server } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
import test from "node:test";
|
||||
|
||||
import { startMockGoogle } from "../src/mock-google.ts";
|
||||
import type { MockGoogleHandle } from "../src/mock-google.ts";
|
||||
|
||||
interface CallbackLab {
|
||||
url: string;
|
||||
callbacks: URL[];
|
||||
stop(): Promise<void>;
|
||||
[Symbol.asyncDispose](): Promise<void>;
|
||||
}
|
||||
|
||||
interface OAuthTokens {
|
||||
accessToken: string;
|
||||
idToken: string;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function isAddressInfo(value: ReturnType<Server["address"]>): value is AddressInfo {
|
||||
return typeof value === "object"
|
||||
&& value !== null
|
||||
&& typeof value.address === "string"
|
||||
&& typeof value.family === "string"
|
||||
&& typeof value.port === "number";
|
||||
}
|
||||
|
||||
function close(server: Server): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
server.close((error) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
server.closeIdleConnections();
|
||||
});
|
||||
}
|
||||
|
||||
async function startCallbackLab(): Promise<CallbackLab> {
|
||||
const callbacks: URL[] = [];
|
||||
const server = createServer((request, response) => {
|
||||
callbacks.push(new URL(request.url ?? "/", "http://127.0.0.1"));
|
||||
response.writeHead(200, { "content-type": "text/plain" });
|
||||
response.end("OAuth callback served");
|
||||
});
|
||||
const address = await new Promise<AddressInfo>((resolve, reject) => {
|
||||
server.once("error", reject);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
server.off("error", reject);
|
||||
const value = server.address();
|
||||
if (isAddressInfo(value)) {
|
||||
resolve(value);
|
||||
return;
|
||||
}
|
||||
reject(new Error("Callback lab did not expose a TCP port."));
|
||||
});
|
||||
});
|
||||
let stopped = false;
|
||||
const stop = async (): Promise<void> => {
|
||||
if (stopped) return;
|
||||
stopped = true;
|
||||
await close(server);
|
||||
};
|
||||
return {
|
||||
url: `http://127.0.0.1:${address.port}/callback`,
|
||||
callbacks,
|
||||
stop,
|
||||
[Symbol.asyncDispose]: stop,
|
||||
};
|
||||
}
|
||||
|
||||
function tokenFingerprint(accessToken: string): string {
|
||||
return createHash("sha256").update(accessToken).digest("hex").slice(0, 12);
|
||||
}
|
||||
|
||||
async function authorize(
|
||||
google: MockGoogleHandle,
|
||||
callback: CallbackLab,
|
||||
email: string,
|
||||
clientId: string,
|
||||
): Promise<OAuthTokens> {
|
||||
const verifier = `verifier-${clientId}`;
|
||||
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
||||
const authorizeUrl = new URL(google.authorizeUrl);
|
||||
authorizeUrl.searchParams.set("client_id", clientId);
|
||||
authorizeUrl.searchParams.set("redirect_uri", callback.url);
|
||||
authorizeUrl.searchParams.set("response_type", "code");
|
||||
authorizeUrl.searchParams.set("scope", "openid email profile https://www.googleapis.com/auth/gmail.compose");
|
||||
authorizeUrl.searchParams.set("state", `state-${clientId}`);
|
||||
authorizeUrl.searchParams.set("prompt", "consent select_account");
|
||||
authorizeUrl.searchParams.set("code_challenge", challenge);
|
||||
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
||||
const since = new Date().toISOString();
|
||||
const callbackCount = callback.callbacks.length;
|
||||
const chooser = await fetch(authorizeUrl, { redirect: "manual" });
|
||||
const chooserHtml = await chooser.text();
|
||||
|
||||
assert.equal(chooser.status, 200);
|
||||
assert.match(chooserHtml, /Choose an account/);
|
||||
assert.match(chooserHtml, new RegExp(`data-account-email="${email.replaceAll(".", "\\.")}"`));
|
||||
const observed = await google.authorizeRequestSince(since, { timeoutMs: 2_000 });
|
||||
assert.equal(observed.params.get("prompt"), "consent select_account");
|
||||
await google.chooseAccount(email, { timeoutMs: 2_000 });
|
||||
assert.equal(callback.callbacks.length, callbackCount + 1, "chooseAccount must wait until the callback server receives the request");
|
||||
const callbackUrl = callback.callbacks.at(-1);
|
||||
const code = callbackUrl?.searchParams.get("code");
|
||||
assert.ok(code, "callback must carry an authorization code");
|
||||
|
||||
const response = await fetch(google.tokenUrl, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/x-www-form-urlencoded" },
|
||||
body: new URLSearchParams({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
code_verifier: verifier,
|
||||
client_id: clientId,
|
||||
client_secret: `secret-${clientId}`,
|
||||
redirect_uri: callback.url,
|
||||
}),
|
||||
});
|
||||
const body: unknown = await response.json();
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(isRecord(body));
|
||||
if (typeof body.access_token !== "string" || typeof body.id_token !== "string") {
|
||||
throw new Error(`Token response omitted access_token or id_token: ${JSON.stringify(body)}`);
|
||||
}
|
||||
return { accessToken: body.access_token, idToken: body.id_token };
|
||||
}
|
||||
|
||||
function idTokenPayload(idToken: string): Record<string, unknown> {
|
||||
const payload = idToken.split(".")[1];
|
||||
assert.ok(payload, "id_token must have a JWT payload");
|
||||
const parsed: unknown = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
||||
assert.ok(isRecord(parsed));
|
||||
return parsed;
|
||||
}
|
||||
|
||||
async function userinfo(google: MockGoogleHandle, accessToken: string): Promise<Record<string, unknown>> {
|
||||
const response = await fetch(google.userinfoUrl, { headers: { authorization: `Bearer ${accessToken}` } });
|
||||
const body: unknown = await response.json();
|
||||
assert.equal(response.status, 200);
|
||||
assert.ok(isRecord(body));
|
||||
return body;
|
||||
}
|
||||
|
||||
async function submitDraft(
|
||||
google: MockGoogleHandle,
|
||||
accessToken: string,
|
||||
message: string,
|
||||
threadId?: string,
|
||||
): Promise<void> {
|
||||
const requestMessage: { raw: string; threadId?: string } = {
|
||||
raw: Buffer.from(message, "utf8").toString("base64url"),
|
||||
};
|
||||
if (threadId) requestMessage.threadId = threadId;
|
||||
const response = await fetch(`${google.apiUrl}/gmail/v1/users/me/drafts`, {
|
||||
method: "POST",
|
||||
headers: { authorization: `Bearer ${accessToken}`, "content-type": "application/json" },
|
||||
body: JSON.stringify({ message: requestMessage }),
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
}
|
||||
|
||||
async function createDraft(
|
||||
google: MockGoogleHandle,
|
||||
accessToken: string,
|
||||
to: string,
|
||||
body: string,
|
||||
threadId?: string,
|
||||
): Promise<void> {
|
||||
const message = [
|
||||
`To: ${to}`,
|
||||
"Subject: Fixture witness",
|
||||
"MIME-Version: 1.0",
|
||||
'Content-Type: text/plain; charset="UTF-8"',
|
||||
"Content-Transfer-Encoding: base64",
|
||||
"",
|
||||
Buffer.from(body, "utf8").toString("base64"),
|
||||
].join("\r\n");
|
||||
await submitDraft(google, accessToken, message, threadId);
|
||||
}
|
||||
|
||||
test("mock Google issues independent account identities and credential fingerprints", async () => {
|
||||
const accountA = "jordan@acme.test";
|
||||
const accountB = "jordan@acmelabs.test";
|
||||
await using google = await startMockGoogle({ accounts: [accountA, accountB], port: 0, autoApprove: false });
|
||||
await using callback = await startCallbackLab();
|
||||
|
||||
const tokensA = await authorize(google, callback, accountA, "client-a");
|
||||
const tokensB = await authorize(google, callback, accountB, "client-b");
|
||||
assert.notEqual(tokenFingerprint(tokensA.accessToken), tokenFingerprint(tokensB.accessToken));
|
||||
assert.equal(idTokenPayload(tokensA.idToken).email, accountA);
|
||||
assert.equal(idTokenPayload(tokensB.idToken).email, accountB);
|
||||
assert.equal((await userinfo(google, tokensA.accessToken)).email, accountA);
|
||||
assert.equal((await userinfo(google, tokensB.accessToken)).email, accountB);
|
||||
|
||||
await createDraft(google, tokensA.accessToken, "archive@acme.test", "Account A", "thread-a");
|
||||
await createDraft(google, tokensB.accessToken, "archive@acmelabs.test", "Account B", "thread-b");
|
||||
const draftsA = await google.draftsFor(accountA);
|
||||
const draftsB = await google.draftsFor(accountB);
|
||||
assert.equal(draftsA[0]?.tokenId, tokenFingerprint(tokensA.accessToken));
|
||||
assert.equal(draftsB[0]?.tokenId, tokenFingerprint(tokensB.accessToken));
|
||||
assert.notEqual(draftsA[0]?.tokenId, draftsB[0]?.tokenId);
|
||||
});
|
||||
|
||||
test("mock Google attributes drafts to one mailbox and returns an empty mailbox promptly", async () => {
|
||||
const accountA = "jordan@acme.test";
|
||||
const accountB = "jordan@acmelabs.test";
|
||||
await using google = await startMockGoogle({ accounts: [accountA, accountB], port: 0, autoApprove: false });
|
||||
await using callback = await startCallbackLab();
|
||||
const tokensA = await authorize(google, callback, accountA, "client-a");
|
||||
const since = new Date().toISOString();
|
||||
await createDraft(google, tokensA.accessToken, "supplier@parts.test", "Mailbox isolation witness", "thread-one");
|
||||
|
||||
const draftsA = await google.draftsFor(accountA, { since, atLeast: 1, timeoutMs: 2_000 });
|
||||
const startedAt = Date.now();
|
||||
const draftsB = await google.draftsFor(accountB, { since, timeoutMs: 5_000 });
|
||||
const elapsedMs = Date.now() - startedAt;
|
||||
assert.deepEqual(draftsB, []);
|
||||
assert.ok(elapsedMs < 1_000, `empty mailbox read blocked for ${elapsedMs}ms`);
|
||||
assert.equal(draftsA.length, 1);
|
||||
assert.deepEqual(draftsA[0], {
|
||||
to: "supplier@parts.test",
|
||||
body: "Mailbox isolation witness",
|
||||
threadId: "thread-one",
|
||||
tokenId: tokenFingerprint(tokensA.accessToken),
|
||||
at: draftsA[0]?.at,
|
||||
});
|
||||
});
|
||||
|
||||
test("mock Google decodes text/plain nested inside mixed and alternative MIME", async () => {
|
||||
const account = "jordan@acme.test";
|
||||
await using google = await startMockGoogle({ accounts: [account], port: 0, autoApprove: false });
|
||||
await using callback = await startCallbackLab();
|
||||
const tokens = await authorize(google, callback, account, "client-nested-mime");
|
||||
const plainBody = "Nested multipart body marker";
|
||||
const mixedBoundary = "fixture-mixed";
|
||||
const alternativeBoundary = "fixture-alternative";
|
||||
const message = [
|
||||
"To: archive@acme.test",
|
||||
"Subject: Nested MIME witness",
|
||||
"MIME-Version: 1.0",
|
||||
`Content-Type: multipart/mixed; boundary="${mixedBoundary}"`,
|
||||
"",
|
||||
`--${mixedBoundary}`,
|
||||
`Content-Type: multipart/alternative; boundary="${alternativeBoundary}"`,
|
||||
"",
|
||||
`--${alternativeBoundary}`,
|
||||
'Content-Type: text/plain; charset="UTF-8"',
|
||||
"Content-Transfer-Encoding: base64",
|
||||
"",
|
||||
Buffer.from(plainBody, "utf8").toString("base64"),
|
||||
`--${alternativeBoundary}`,
|
||||
'Content-Type: text/html; charset="UTF-8"',
|
||||
"Content-Transfer-Encoding: base64",
|
||||
"",
|
||||
Buffer.from(`<div>${plainBody}</div>`, "utf8").toString("base64"),
|
||||
`--${alternativeBoundary}--`,
|
||||
`--${mixedBoundary}`,
|
||||
'Content-Type: application/octet-stream; name="witness.bin"',
|
||||
'Content-Disposition: attachment; filename="witness.bin"',
|
||||
"Content-Transfer-Encoding: base64",
|
||||
"",
|
||||
Buffer.from("attachment bytes", "utf8").toString("base64"),
|
||||
`--${mixedBoundary}--`,
|
||||
"",
|
||||
].join("\r\n");
|
||||
|
||||
await submitDraft(google, tokens.accessToken, message);
|
||||
|
||||
const drafts = await google.draftsFor(account);
|
||||
assert.equal(drafts[0]?.body, plainBody);
|
||||
});
|
||||
|
||||
test("mock Google witnesses exact Gmail attachment and Drive upload bytes", async () => {
|
||||
const account = "jordan@acme.test";
|
||||
const fixture = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x00, 0xff, 0x80, 0x13, 0x0a]);
|
||||
await using google = await startMockGoogle({ accounts: [account], port: 0, autoApprove: false });
|
||||
await using callback = await startCallbackLab();
|
||||
const tokens = await authorize(google, callback, account, "client-binary-witness");
|
||||
|
||||
const mixedBoundary = "fixture-mixed-binary";
|
||||
const message = [
|
||||
"To: archive@acme.test",
|
||||
"Subject: Binary witness",
|
||||
"MIME-Version: 1.0",
|
||||
`Content-Type: multipart/mixed; boundary="${mixedBoundary}"`,
|
||||
"",
|
||||
`--${mixedBoundary}`,
|
||||
'Content-Type: text/plain; charset="UTF-8"',
|
||||
"Content-Transfer-Encoding: base64",
|
||||
"",
|
||||
Buffer.from("Binary body", "utf8").toString("base64"),
|
||||
`--${mixedBoundary}`,
|
||||
'Content-Type: image/png; name="witness.png"',
|
||||
'Content-Disposition: attachment; filename="witness.png"',
|
||||
"Content-Transfer-Encoding: base64",
|
||||
"",
|
||||
fixture.toString("base64"),
|
||||
`--${mixedBoundary}--`,
|
||||
"",
|
||||
].join("\r\n");
|
||||
await submitDraft(google, tokens.accessToken, message);
|
||||
const drafts = await google.draftsFor(account, { atLeast: 1, timeoutMs: 2_000 });
|
||||
assert.equal(drafts[0]?.attachments?.[0]?.filename, "witness.png");
|
||||
assert.equal(drafts[0]?.attachments?.[0]?.mimeType, "image/png");
|
||||
assert.deepEqual(drafts[0]?.attachments?.[0]?.content, fixture);
|
||||
|
||||
const driveBoundary = "fixture-drive-binary";
|
||||
const metadata = Buffer.from(`--${driveBoundary}\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n{"name":"witness.png"}\r\n`, "utf8");
|
||||
const contentHeader = Buffer.from(`--${driveBoundary}\r\nContent-Type: image/png\r\n\r\n`, "utf8");
|
||||
const footer = Buffer.from(`\r\n--${driveBoundary}--\r\n`, "utf8");
|
||||
const uploadBody = Buffer.concat([metadata, contentHeader, fixture, footer]);
|
||||
const uploadBytes = new Uint8Array(uploadBody.byteLength);
|
||||
uploadBytes.set(uploadBody);
|
||||
const response = await fetch(`${google.apiUrl}/upload/drive/v3/files?uploadType=multipart`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
authorization: `Bearer ${tokens.accessToken}`,
|
||||
"content-type": `multipart/related; boundary=${driveBoundary}`,
|
||||
},
|
||||
body: uploadBytes,
|
||||
});
|
||||
assert.equal(response.status, 200);
|
||||
const uploads = await google.driveUploadsFor(account, { atLeast: 1, timeoutMs: 2_000 });
|
||||
assert.equal(uploads[0]?.filename, "witness.png");
|
||||
assert.equal(uploads[0]?.mimeType, "image/png");
|
||||
assert.deepEqual(uploads[0]?.content, fixture);
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { allocateFreePort } from "@openwork/cdp";
|
||||
import { startMockMcp } from "../src/mock-mcp.ts";
|
||||
|
||||
test("records an unauthenticated initialize attempt as a handshake", async () => {
|
||||
await using mock = await startMockMcp({ port: await allocateFreePort() });
|
||||
const startedAt = new Date().toISOString();
|
||||
|
||||
const response = await fetch(mock.mcpUrl, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
jsonrpc: "2.0",
|
||||
id: 1,
|
||||
method: "initialize",
|
||||
params: {
|
||||
protocolVersion: "2025-06-18",
|
||||
capabilities: {},
|
||||
clientInfo: { name: "mock-handshake-test", version: "1.0.0" },
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(response.status, 401);
|
||||
const handshakes = await mock.handshakes({ sinceIso: startedAt });
|
||||
assert.equal(handshakes.length, 1);
|
||||
assert.equal(handshakes[0]?.method, "POST");
|
||||
assert.equal(handshakes[0]?.path, "/mcp");
|
||||
});
|
||||
|
||||
function completionBody(marker: string, completedTools: number): Record<string, unknown> {
|
||||
return {
|
||||
model: "mock-agent-workload-model",
|
||||
stream: true,
|
||||
tools: [
|
||||
{ type: "function", function: { name: "write", parameters: { type: "object" } } },
|
||||
{ type: "function", function: { name: "read", parameters: { type: "object" } } },
|
||||
],
|
||||
messages: [
|
||||
{ role: "user", content: `run ${marker}` },
|
||||
...Array.from({ length: completedTools }, (_, index) => ({
|
||||
role: "tool",
|
||||
tool_call_id: `call-${index}`,
|
||||
content: `tool result ${index}`,
|
||||
})),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
test("scripts and records deterministic OpenAI-compatible agent tool rounds", async () => {
|
||||
const marker = "agent-workload-unit-marker";
|
||||
await using mock = await startMockMcp({
|
||||
port: await allocateFreePort(),
|
||||
agentWorkloads: [{
|
||||
promptMarker: marker,
|
||||
finalReply: "unit workload complete",
|
||||
steps: [
|
||||
{ tool: "write", arguments: { filePath: "/tmp/unit.txt", content: marker } },
|
||||
{ tool: "read", arguments: { filePath: "/tmp/unit.txt" } },
|
||||
],
|
||||
}],
|
||||
});
|
||||
const startedAt = new Date().toISOString();
|
||||
|
||||
const first = await fetch(`${mock.url}/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(completionBody(marker, 0)),
|
||||
});
|
||||
const firstText = await first.text();
|
||||
assert.equal(first.status, 200);
|
||||
assert.match(firstText, /"name":"write"/);
|
||||
assert.match(firstText, /unit\.txt/);
|
||||
|
||||
const second = await fetch(`${mock.url}/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(completionBody(marker, 1)),
|
||||
});
|
||||
assert.match(await second.text(), /"name":"read"/);
|
||||
|
||||
const final = await fetch(`${mock.url}/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify(completionBody(marker, 2)),
|
||||
});
|
||||
assert.match(await final.text(), /unit workload complete/);
|
||||
|
||||
const requests = await mock.agentRequests({ promptMarker: marker, sinceIso: startedAt, atLeast: 3, timeoutMs: 5_000 });
|
||||
assert.deepEqual(requests.map((request) => request.kind), ["tool", "tool", "final"]);
|
||||
assert.deepEqual(requests.map((request) => request.completedTools), [0, 1, 2]);
|
||||
assert.deepEqual(requests.map((request) => request.matchedMarkers), [[marker], [marker], [marker]]);
|
||||
});
|
||||
Reference in New Issue
Block a user