Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions lib/mcp/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,15 @@ const AcceptedPlanOut = z.object({
test_status: z.string().nullable(),
run_command: z.string(),
// Custom scenarios only: the agreement itself, since an agent reads structuredContent, not the text summary.
// The agreement also carries launch: "pending" | "approved" | "not_applicable" (lib/policy.ts). It is deliberately
// NOT declared here: the tool listing is part of the Northstar snapshot (850971e), which must stay byte-identical.
// The schema isn't strict, so the field still reaches structured readers; test-scenario P1-3/P1-3b check it.
// Every field lib/policy.ts sends must be declared: the published schema has additionalProperties: false, so a
// validating client rejects the whole result over one undeclared field (test-mcp:custom).
agreement: z
.object({
company: z.string(),
asking_team: z.string(),
owning_team: z.string(),
status: z.enum(["agreed", "not_agreed"]),
launch: z.enum(["approved", "pending", "not_applicable"]), // agreed is not launch-approved (leadership decides)
plan_title: z.string(),
plan_text: z.string(),
owning_measures: z.array(z.string()),
Expand Down
4 changes: 3 additions & 1 deletion scripts/test-all.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# Build A gate runner. Usage: scripts/test-all.sh <port> (run from a lane or integration worktree)
# Checks frozen files, lint and types, starts a fresh dev server on <port>, then runs in fixed order:
# snapshot, test-flow, test-scenario (default fixture, then each scripts/fixtures/*.json), test-ui-loop, test-mcp,
# snapshot, test-flow, test-scenario (default fixture, then each scripts/fixtures/*.json), test-ui-loop, test-mcp (Northstar, then custom),
# test-capture, test-mapper. Capture and mapper add sessions
# that survive reset, so they run last. Then test-tenancy and ui-northstar, which start their own production
# servers on <port>.
Expand Down Expand Up @@ -68,6 +68,8 @@ for f in scripts/fixtures/*.json; do step "scenario:$(basename "$f" .json)" env
# The same custom loop in a browser: two team profiles by visible labels, plus a synthetic agent's wake-up. Needs Chrome.
step test-ui-loop node scripts/test-ui-loop.mjs "$BASE"
step test-mcp env ALIGN_MCP_TOKEN="$TOKEN" node scripts/test-mcp.mjs "$BASE/api/mcp"
# The same validating client on a custom scenario (its `agreement` output), then back to Northstar.
step test-mcp:custom env ALIGN_MCP_TOKEN="$TOKEN" node scripts/test-mcp-custom.mjs "$BASE"
step test-capture node scripts/test-capture.mjs "$BASE"
step test-mapper env ALIGN_URL="$BASE" node scripts/test-mapper.mjs
# Multi-workspace tests need their own server configs (creation closed, signup code, open create).
Expand Down
27 changes: 27 additions & 0 deletions scripts/test-mcp-custom.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// test-mcp against a custom scenario. The SDK client in test-mcp validates every tool's structuredContent against
// its outputSchema; the Northstar run never exercises the custom `agreement` object, so a field the schema doesn't
// declare there (additionalProperties: false) went unseen. Restores Northstar afterwards, since later gate steps
// (test-capture, test-mapper) run on it.
// Usage: ALIGN_MCP_TOKEN=... node scripts/test-mcp-custom.mjs <baseUrl> [fixture.json]
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { baseUrl, signIn } from "./lib/harness.mjs";

const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const BASE = baseUrl();
const FIXTURE = path.resolve(process.argv[3] || path.join(ROOT, "data/scenarios/example-custom.json"));

async function post(cookie, p, body) {
const r = await fetch(BASE + p, { method: "POST", headers: { cookie, "content-type": "application/json", origin: BASE }, body: JSON.stringify(body) });
if (r.status !== 200) throw new Error(`POST ${p} -> ${r.status} ${(await r.text()).slice(0, 200)}`);
}

const lead = await signIn(BASE, "leadership");
await post(lead, "/api/reset", { confirm: "reset" });
await post(lead, "/api/scenario", { ...JSON.parse(readFileSync(FIXTURE, "utf8")), confirm: "replace" });
const run = spawnSync(process.execPath, [path.join(ROOT, "scripts/test-mcp.mjs"), `${BASE}/api/mcp`], { stdio: "inherit", env: process.env });
await post(lead, "/api/reset", { confirm: "reset" });
console.log(run.status === 0 ? `test-mcp passes on the custom scenario ${path.basename(FIXTURE)}` : `test-mcp FAILED on the custom scenario ${path.basename(FIXTURE)}`);
process.exit(run.status ?? 1);
2 changes: 1 addition & 1 deletion test/snapshots/northstar-850971e/steps/01-initial.json

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Large diffs are not rendered by default.