test(uok,slice-3b): cover ctx propagation through gate-runner, phases, plan-slice
Adds focused unit tests for the slice-3b wiring:
- UokGateRunner.run emits surface/runControl/permissionProfile/
parentTrace on all three trace paths (normal, unknown-gate,
circuit-breaker-blocked) and omits them when absent.
- buildAutonomousUokContext pins surface=autonomous + runControl=
autonomous and derives permissionProfile from session/prefs
(YOLO → low, prefs.permissionLevel honored, "high" default).
- emitAutonomousGate forwards the schema-v2 ctx into UokGateRunner
(covers the phases-pre-dispatch / phases-guards call sites via
the new shared helper).
- handlePlanSlice options.uokContext lands on every seeded Q3-Q8
quality_gates row; without it, rows stay in the legacy null shape.
Refactors phases-pre-dispatch and phases-guards to call the new
emitAutonomousGate helper so the three sites stay in sync going
forward. phases-finalize keeps its inline UokGateRunner because the
verification gate's execute callback isn't a static verdict.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
95ea9eecee
commit
7794208340
4 changed files with 591 additions and 40 deletions
|
|
@ -82,8 +82,10 @@ import {
|
|||
resetRunawayGuardState,
|
||||
} from "../uok/auto-runaway-guard.js";
|
||||
import { resolveUokFlags } from "../uok/flags.js";
|
||||
import { buildAutonomousUokContext } from "../uok/auto-uok-ctx.js";
|
||||
import { UokGateRunner } from "../uok/gate-runner.js";
|
||||
import {
|
||||
buildAutonomousUokContext,
|
||||
emitAutonomousGate,
|
||||
} from "../uok/auto-uok-ctx.js";
|
||||
import { emitModelAutoResolvedEvent } from "../uok/model-route-evidence.js";
|
||||
import {
|
||||
ensurePlanV2Graph as ensurePlanningFlowGraph,
|
||||
|
|
@ -405,16 +407,6 @@ export async function runGuards(ic, mid, unitType, unitId, sliceId) {
|
|||
planGateOutcome = "fail";
|
||||
planGateRationale = `Slice ${sliceId} has no tasks defined`;
|
||||
}
|
||||
const planGateRunner = new UokGateRunner();
|
||||
planGateRunner.register({
|
||||
id: "plan-gate",
|
||||
type: "policy",
|
||||
execute: async () => ({
|
||||
outcome: planGateOutcome,
|
||||
failureClass: planGateOutcome === "pass" ? "none" : "input",
|
||||
rationale: planGateRationale || "Plan files verified",
|
||||
}),
|
||||
});
|
||||
// Schema-v2 run-context: this gate runs inside the autonomous
|
||||
// loop's guard phase, so surface/runControl are "autonomous" and
|
||||
// permissionProfile follows session/prefs. parentTrace points at
|
||||
|
|
@ -430,18 +422,20 @@ export async function runGuards(ic, mid, unitType, unitId, sliceId) {
|
|||
milestoneId: mid,
|
||||
sliceId,
|
||||
});
|
||||
const planGateResult = await planGateRunner.run("plan-gate", {
|
||||
const planGateResult = await emitAutonomousGate({
|
||||
basePath: s.basePath,
|
||||
gateId: "plan-gate",
|
||||
gateType: "policy",
|
||||
outcome: planGateOutcome,
|
||||
failureClass: planGateOutcome === "pass" ? "none" : "input",
|
||||
rationale: planGateRationale || "Plan files verified",
|
||||
traceId: `guard:${ic.flowId}`,
|
||||
turnId: `iter-${ic.iteration}`,
|
||||
milestoneId: mid,
|
||||
sliceId,
|
||||
unitType,
|
||||
unitId,
|
||||
surface: v2Ctx?.surface,
|
||||
runControl: v2Ctx?.runControl,
|
||||
permissionProfile: v2Ctx?.permissionProfile,
|
||||
parentTrace: v2Ctx?.parentTrace,
|
||||
uokContext: v2Ctx,
|
||||
});
|
||||
if (planGateResult.outcome !== "pass") {
|
||||
ctx.ui.notify(
|
||||
|
|
|
|||
|
|
@ -82,9 +82,11 @@ import {
|
|||
countChangedFiles,
|
||||
resetRunawayGuardState,
|
||||
} from "../uok/auto-runaway-guard.js";
|
||||
import { buildAutonomousUokContext } from "../uok/auto-uok-ctx.js";
|
||||
import {
|
||||
buildAutonomousUokContext,
|
||||
emitAutonomousGate,
|
||||
} from "../uok/auto-uok-ctx.js";
|
||||
import { resolveUokFlags } from "../uok/flags.js";
|
||||
import { UokGateRunner } from "../uok/gate-runner.js";
|
||||
import { emitModelAutoResolvedEvent } from "../uok/model-route-evidence.js";
|
||||
import {
|
||||
ensurePlanV2Graph as ensurePlanningFlowGraph,
|
||||
|
|
@ -176,32 +178,20 @@ export async function runPreDispatch(ic, loopState) {
|
|||
});
|
||||
const runPreDispatchGate = async (input) => {
|
||||
if (!uokFlags.gates) return;
|
||||
const gateRunner = new UokGateRunner();
|
||||
gateRunner.register({
|
||||
id: input.gateId,
|
||||
type: input.gateType,
|
||||
execute: async () => ({
|
||||
outcome: input.outcome,
|
||||
failureClass: input.failureClass,
|
||||
rationale: input.rationale,
|
||||
findings: input.findings ?? "",
|
||||
}),
|
||||
});
|
||||
await gateRunner.run(input.gateId, {
|
||||
await emitAutonomousGate({
|
||||
basePath: s.basePath,
|
||||
gateId: input.gateId,
|
||||
gateType: input.gateType,
|
||||
outcome: input.outcome,
|
||||
failureClass: input.failureClass,
|
||||
rationale: input.rationale,
|
||||
findings: input.findings,
|
||||
traceId: `pre-dispatch:${ic.flowId}`,
|
||||
turnId: `iter-${ic.iteration}`,
|
||||
milestoneId: input.milestoneId ?? s.currentMilestoneId ?? undefined,
|
||||
unitType: "pre-dispatch",
|
||||
unitId: `iter-${ic.iteration}`,
|
||||
// Schema-v2 fields propagated from the iteration-level ctx. When
|
||||
// buildUokRunContext returned null (impossible for the values we
|
||||
// pass today, but defensive), these stay undefined and the gate
|
||||
// classifies as "legacy" as it did before this slice.
|
||||
surface: v2Ctx?.surface,
|
||||
runControl: v2Ctx?.runControl,
|
||||
permissionProfile: v2Ctx?.permissionProfile,
|
||||
parentTrace: v2Ctx?.parentTrace,
|
||||
milestoneId: input.milestoneId ?? s.currentMilestoneId ?? undefined,
|
||||
uokContext: v2Ctx,
|
||||
});
|
||||
};
|
||||
// Resource version guard
|
||||
|
|
|
|||
|
|
@ -0,0 +1,502 @@
|
|||
/**
|
||||
* Slice 3b of "Make UOK the SF Control Plane".
|
||||
*
|
||||
* Verifies that the autonomous loop's gate emissions now carry the
|
||||
* schema-v2 run-context fields (surface, runControl, permissionProfile,
|
||||
* parentTrace) through to the trace events that `sf headless status
|
||||
* uok --json` reads — flipping the gate's coverageStatus from
|
||||
* "legacy" to "ok".
|
||||
*
|
||||
* Layers covered here:
|
||||
* 1. UokGateRunner.run reads ctx.surface/runControl/permissionProfile/
|
||||
* parentTrace and writes them into every gate_run trace event
|
||||
* (unknown-gate, normal, and circuit-breaker-blocked paths).
|
||||
* 2. buildAutonomousUokContext pins surface="autonomous" / runControl=
|
||||
* "autonomous" and derives permissionProfile from session+prefs.
|
||||
* 3. plan-slice's handlePlanSlice forwards an options.uokContext to
|
||||
* sf-db-gates.insertGateRow.
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
} from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { afterEach, beforeEach, describe, test } from "vitest";
|
||||
import {
|
||||
closeDatabase,
|
||||
insertMilestone,
|
||||
insertSlice,
|
||||
openDatabase,
|
||||
updateGateCircuitBreaker,
|
||||
} from "../sf-db.js";
|
||||
import { _getAdapter } from "../sf-db/sf-db-core.js";
|
||||
import {
|
||||
buildAutonomousUokContext,
|
||||
derivePermissionProfile,
|
||||
emitAutonomousGate,
|
||||
} from "../uok/auto-uok-ctx.js";
|
||||
import { UokGateRunner } from "../uok/gate-runner.js";
|
||||
|
||||
const tmpRoots = [];
|
||||
|
||||
function makeProject() {
|
||||
const root = mkdtempSync(join(tmpdir(), "sf-uok-slice3b-"));
|
||||
mkdirSync(join(root, ".sf", "traces"), { recursive: true });
|
||||
mkdirSync(join(root, ".sf", "milestones", "M1", "slices", "S1"), {
|
||||
recursive: true,
|
||||
});
|
||||
mkdirSync(join(root, ".sf", "milestones", "M2", "slices", "S2"), {
|
||||
recursive: true,
|
||||
});
|
||||
tmpRoots.push(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
function readGateRunEvents(basePath) {
|
||||
const dir = join(basePath, ".sf", "traces");
|
||||
const events = [];
|
||||
for (const name of readdirSync(dir)) {
|
||||
if (!name.endsWith(".jsonl")) continue;
|
||||
const lines = readFileSync(join(dir, name), "utf-8")
|
||||
.split("\n")
|
||||
.filter(Boolean);
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const ev = JSON.parse(line);
|
||||
if (ev.type === "gate_run") events.push(ev);
|
||||
} catch {
|
||||
/* skip malformed lines */
|
||||
}
|
||||
}
|
||||
}
|
||||
return events;
|
||||
}
|
||||
|
||||
function planningMeeting() {
|
||||
return {
|
||||
trigger: "test",
|
||||
pm: "Plan the slice.",
|
||||
researcher: "Schema exists.",
|
||||
partner: "Existing tools.",
|
||||
combatant: "Avoid duplication.",
|
||||
architect: "SQLite-backed.",
|
||||
moderator: "Proceed.",
|
||||
recommendedRoute: "planning",
|
||||
confidenceSummary: "high",
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
openDatabase(":memory:");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
closeDatabase();
|
||||
for (const dir of tmpRoots.splice(0)) {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
// ─── 1. UokGateRunner.run propagates schema-v2 ctx into gate_run events ───
|
||||
|
||||
describe("UokGateRunner emits schema-v2 fields into gate_run trace events", () => {
|
||||
test("normal-pass path carries surface/runControl/permissionProfile/parentTrace", async () => {
|
||||
const basePath = makeProject();
|
||||
const runner = new UokGateRunner();
|
||||
runner.register({
|
||||
id: "demo-gate",
|
||||
type: "verification",
|
||||
execute: async () => ({ outcome: "pass", rationale: "ok" }),
|
||||
});
|
||||
await runner.run("demo-gate", {
|
||||
basePath,
|
||||
traceId: "trace-1",
|
||||
turnId: "turn-1",
|
||||
unitType: "execute-task",
|
||||
unitId: "M1/S1/T1",
|
||||
milestoneId: "M1",
|
||||
sliceId: "S1",
|
||||
taskId: "T1",
|
||||
surface: "autonomous",
|
||||
runControl: "autonomous",
|
||||
permissionProfile: "high",
|
||||
parentTrace: "flow-parent",
|
||||
});
|
||||
const events = readGateRunEvents(basePath);
|
||||
assert.equal(events.length, 1);
|
||||
const ev = events[0];
|
||||
assert.equal(ev.surface, "autonomous");
|
||||
assert.equal(ev.runControl, "autonomous");
|
||||
assert.equal(ev.permissionProfile, "high");
|
||||
assert.equal(ev.parentTrace, "flow-parent");
|
||||
assert.equal(ev.gateId, "demo-gate");
|
||||
assert.equal(ev.outcome, "pass");
|
||||
});
|
||||
|
||||
test("legacy ctx (no v2 fields) omits them from gate_run event", async () => {
|
||||
const basePath = makeProject();
|
||||
const runner = new UokGateRunner();
|
||||
runner.register({
|
||||
id: "legacy-gate",
|
||||
type: "verification",
|
||||
execute: async () => ({ outcome: "pass", rationale: "ok" }),
|
||||
});
|
||||
await runner.run("legacy-gate", {
|
||||
basePath,
|
||||
traceId: "trace-legacy",
|
||||
turnId: "turn-1",
|
||||
});
|
||||
const events = readGateRunEvents(basePath);
|
||||
assert.equal(events.length, 1);
|
||||
const ev = events[0];
|
||||
assert.equal(ev.surface, undefined);
|
||||
assert.equal(ev.runControl, undefined);
|
||||
assert.equal(ev.permissionProfile, undefined);
|
||||
assert.equal(ev.parentTrace, undefined);
|
||||
});
|
||||
|
||||
test("unknown-gate path also carries schema-v2 ctx fields", async () => {
|
||||
const basePath = makeProject();
|
||||
const runner = new UokGateRunner();
|
||||
const result = await runner.run("not-registered", {
|
||||
basePath,
|
||||
traceId: "trace-unknown",
|
||||
turnId: "turn-1",
|
||||
surface: "autonomous",
|
||||
runControl: "autonomous",
|
||||
permissionProfile: "low",
|
||||
parentTrace: "flow-x",
|
||||
});
|
||||
assert.equal(result.outcome, "manual-attention");
|
||||
const events = readGateRunEvents(basePath);
|
||||
assert.equal(events.length, 1);
|
||||
assert.equal(events[0].surface, "autonomous");
|
||||
assert.equal(events[0].runControl, "autonomous");
|
||||
assert.equal(events[0].permissionProfile, "low");
|
||||
assert.equal(events[0].parentTrace, "flow-x");
|
||||
});
|
||||
|
||||
test("circuit-breaker-blocked path also carries schema-v2 ctx fields", async () => {
|
||||
const basePath = makeProject();
|
||||
const runner = new UokGateRunner();
|
||||
runner.register({
|
||||
id: "cb-block-v2",
|
||||
type: "verification",
|
||||
execute: async () => ({ outcome: "pass", rationale: "ok" }),
|
||||
});
|
||||
updateGateCircuitBreaker("cb-block-v2", {
|
||||
state: "open",
|
||||
failureStreak: 5,
|
||||
openedAt: new Date().toISOString(),
|
||||
});
|
||||
await runner.run("cb-block-v2", {
|
||||
basePath,
|
||||
traceId: "trace-cb",
|
||||
turnId: "turn-1",
|
||||
surface: "autonomous",
|
||||
runControl: "autonomous",
|
||||
permissionProfile: "high",
|
||||
});
|
||||
const events = readGateRunEvents(basePath);
|
||||
assert.equal(events.length, 1);
|
||||
assert.equal(events[0].surface, "autonomous");
|
||||
assert.equal(events[0].runControl, "autonomous");
|
||||
assert.equal(events[0].permissionProfile, "high");
|
||||
assert.equal(events[0].outcome, "fail");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 2. buildAutonomousUokContext pins autonomous defaults ────────────────
|
||||
|
||||
describe("buildAutonomousUokContext autonomous-loop defaults", () => {
|
||||
test("returns surface=autonomous and runControl=autonomous", () => {
|
||||
const ctx = buildAutonomousUokContext({
|
||||
s: { isYolo: () => false },
|
||||
prefs: undefined,
|
||||
traceId: "flow-1",
|
||||
});
|
||||
assert.equal(ctx?.surface, "autonomous");
|
||||
assert.equal(ctx?.runControl, "autonomous");
|
||||
assert.equal(ctx?.permissionProfile, "high");
|
||||
assert.equal(ctx?.traceId, "flow-1");
|
||||
});
|
||||
|
||||
test("YOLO session forces permissionProfile=low", () => {
|
||||
const ctx = buildAutonomousUokContext({
|
||||
s: { isYolo: () => true },
|
||||
prefs: undefined,
|
||||
traceId: "flow-yolo",
|
||||
});
|
||||
assert.equal(ctx?.permissionProfile, "low");
|
||||
});
|
||||
|
||||
test("prefs.permissionLevel=medium propagates to permissionProfile", () => {
|
||||
const ctx = buildAutonomousUokContext({
|
||||
s: {},
|
||||
prefs: { permissionLevel: "medium" },
|
||||
traceId: "flow-med",
|
||||
});
|
||||
assert.equal(ctx?.permissionProfile, "medium");
|
||||
});
|
||||
|
||||
test("prefs.permissionLevel=low or minimal propagates to permissionProfile=low", () => {
|
||||
assert.equal(
|
||||
buildAutonomousUokContext({
|
||||
s: {},
|
||||
prefs: { permissionLevel: "low" },
|
||||
traceId: "flow-low",
|
||||
})?.permissionProfile,
|
||||
"low",
|
||||
);
|
||||
assert.equal(
|
||||
buildAutonomousUokContext({
|
||||
s: {},
|
||||
prefs: { permissionLevel: "minimal" },
|
||||
traceId: "flow-min",
|
||||
})?.permissionProfile,
|
||||
"low",
|
||||
);
|
||||
});
|
||||
|
||||
test("returns null when traceId is missing", () => {
|
||||
assert.equal(buildAutonomousUokContext({ s: {}, prefs: {} }), null);
|
||||
});
|
||||
|
||||
test("derivePermissionProfile session probe failure falls back to prefs", () => {
|
||||
const throwingSession = {
|
||||
isYolo: () => {
|
||||
throw new Error("session probe boom");
|
||||
},
|
||||
};
|
||||
assert.equal(
|
||||
derivePermissionProfile(throwingSession, { permissionLevel: "medium" }),
|
||||
"medium",
|
||||
);
|
||||
assert.equal(derivePermissionProfile(throwingSession, undefined), "high");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 3. emitAutonomousGate / phase wiring contract ────────────────────────
|
||||
|
||||
describe("emitAutonomousGate forwards uokContext into UokGateRunner.run", () => {
|
||||
test("emitted gate_run trace event carries surface/runControl/permissionProfile/parentTrace", async () => {
|
||||
const basePath = makeProject();
|
||||
const uokContext = buildAutonomousUokContext({
|
||||
s: { isYolo: () => false },
|
||||
prefs: undefined,
|
||||
traceId: "pre-dispatch:flow-A",
|
||||
parentTrace: "flow-A",
|
||||
unitType: "pre-dispatch",
|
||||
unitId: "iter-3",
|
||||
});
|
||||
assert.ok(uokContext);
|
||||
const result = await emitAutonomousGate({
|
||||
basePath,
|
||||
gateId: "resource-version-guard",
|
||||
gateType: "policy",
|
||||
outcome: "pass",
|
||||
failureClass: "none",
|
||||
rationale: "ok",
|
||||
traceId: "pre-dispatch:flow-A",
|
||||
turnId: "iter-3",
|
||||
unitType: "pre-dispatch",
|
||||
unitId: "iter-3",
|
||||
milestoneId: "M1",
|
||||
uokContext,
|
||||
});
|
||||
assert.equal(result.outcome, "pass");
|
||||
const events = readGateRunEvents(basePath);
|
||||
assert.equal(events.length, 1);
|
||||
const ev = events[0];
|
||||
assert.equal(ev.surface, "autonomous");
|
||||
assert.equal(ev.runControl, "autonomous");
|
||||
assert.equal(ev.permissionProfile, "high");
|
||||
assert.equal(ev.parentTrace, "flow-A");
|
||||
assert.equal(ev.gateId, "resource-version-guard");
|
||||
assert.equal(ev.unitType, "pre-dispatch");
|
||||
assert.equal(ev.unitId, "iter-3");
|
||||
assert.equal(ev.milestoneId, "M1");
|
||||
});
|
||||
|
||||
test("runnerOverride seam: phase code passes the same ctx every time", async () => {
|
||||
// Spy: record each .run call so we can assert the phases always
|
||||
// pass the v2 ctx through. This is the dependency-injection
|
||||
// contract the slice 3b plan calls for.
|
||||
const spy = {
|
||||
calls: [],
|
||||
register() {},
|
||||
async run(id, ctx) {
|
||||
this.calls.push({ id, ctx });
|
||||
return { outcome: "pass", gateId: id, gateType: "policy" };
|
||||
},
|
||||
};
|
||||
const uokContext = buildAutonomousUokContext({
|
||||
s: { isYolo: () => true },
|
||||
prefs: { permissionLevel: "medium" },
|
||||
traceId: "guard:flow-B",
|
||||
parentTrace: "flow-B",
|
||||
});
|
||||
assert.equal(uokContext?.permissionProfile, "low"); // YOLO wins over prefs
|
||||
await emitAutonomousGate({
|
||||
basePath: "/tmp/no-write",
|
||||
gateId: "plan-gate",
|
||||
gateType: "policy",
|
||||
outcome: "pass",
|
||||
traceId: "guard:flow-B",
|
||||
turnId: "iter-1",
|
||||
uokContext,
|
||||
runnerOverride: spy,
|
||||
});
|
||||
assert.equal(spy.calls.length, 1);
|
||||
assert.equal(spy.calls[0].id, "plan-gate");
|
||||
assert.equal(spy.calls[0].ctx.surface, "autonomous");
|
||||
assert.equal(spy.calls[0].ctx.runControl, "autonomous");
|
||||
assert.equal(spy.calls[0].ctx.permissionProfile, "low");
|
||||
assert.equal(spy.calls[0].ctx.parentTrace, "flow-B");
|
||||
});
|
||||
});
|
||||
|
||||
// ─── 4. plan-slice forwards uokContext to insertGateRow ───────────────────
|
||||
|
||||
describe("plan-slice forwards uokContext through to insertGateRow", () => {
|
||||
test("handlePlanSlice options.uokContext lands on every seeded gate row", async () => {
|
||||
// Real sf-db setup so the transaction commits and we can inspect
|
||||
// the seeded quality_gates rows.
|
||||
closeDatabase();
|
||||
const project = makeProject();
|
||||
openDatabase(join(project, ".sf", "sf.db"));
|
||||
insertMilestone({ id: "M1", title: "demo", status: "active" });
|
||||
insertSlice({
|
||||
milestoneId: "M1",
|
||||
id: "S1",
|
||||
title: "demo slice",
|
||||
status: "pending",
|
||||
});
|
||||
const { handlePlanSlice } = await import("../tools/plan-slice.js");
|
||||
const uokContext = buildAutonomousUokContext({
|
||||
s: { isYolo: () => false },
|
||||
prefs: undefined,
|
||||
traceId: "flow-from-test",
|
||||
parentTrace: "flow-from-test",
|
||||
});
|
||||
assert.ok(uokContext, "test setup: buildAutonomousUokContext must return a ctx");
|
||||
await handlePlanSlice(
|
||||
{
|
||||
milestoneId: "M1",
|
||||
sliceId: "S1",
|
||||
goal: "do the thing",
|
||||
successCriteria: "it works",
|
||||
proofLevel: "tested",
|
||||
integrationClosure: "merged",
|
||||
observabilityImpact: "logs",
|
||||
adversarialReview: {
|
||||
partner: "P review",
|
||||
combatant: "C review",
|
||||
architect: "A review",
|
||||
},
|
||||
planningMeeting: planningMeeting(),
|
||||
tasks: [
|
||||
{
|
||||
taskId: "T01",
|
||||
title: "Task one",
|
||||
description: "Do task one carefully",
|
||||
estimate: "small",
|
||||
files: ["src/foo.ts"],
|
||||
verify: "tests pass",
|
||||
inputs: ["spec"],
|
||||
expectedOutput: ["green tests"],
|
||||
observabilityImpact: "more logs",
|
||||
},
|
||||
],
|
||||
},
|
||||
project,
|
||||
{ uokContext },
|
||||
);
|
||||
const db = _getAdapter();
|
||||
const rows = db
|
||||
.prepare(
|
||||
"SELECT gate_id, surface, run_control, permission_profile, trace_id, parent_trace FROM quality_gates WHERE milestone_id = ? AND slice_id = ? ORDER BY gate_id, task_id",
|
||||
)
|
||||
.all("M1", "S1");
|
||||
assert.ok(rows.length >= 4, `expected ≥4 gate rows, got ${rows.length}`);
|
||||
for (const row of rows) {
|
||||
assert.equal(
|
||||
row.surface,
|
||||
"autonomous",
|
||||
`gate ${row.gate_id} missing surface`,
|
||||
);
|
||||
assert.equal(row.run_control, "autonomous");
|
||||
assert.equal(row.permission_profile, "high");
|
||||
assert.equal(row.trace_id, "flow-from-test");
|
||||
assert.equal(row.parent_trace, "flow-from-test");
|
||||
}
|
||||
});
|
||||
|
||||
test("handlePlanSlice without uokContext leaves gate rows in legacy null shape", async () => {
|
||||
closeDatabase();
|
||||
const project = makeProject();
|
||||
openDatabase(join(project, ".sf", "sf.db"));
|
||||
insertMilestone({ id: "M2", title: "legacy", status: "active" });
|
||||
insertSlice({
|
||||
milestoneId: "M2",
|
||||
id: "S2",
|
||||
title: "legacy slice",
|
||||
status: "pending",
|
||||
});
|
||||
const { handlePlanSlice } = await import("../tools/plan-slice.js");
|
||||
await handlePlanSlice(
|
||||
{
|
||||
milestoneId: "M2",
|
||||
sliceId: "S2",
|
||||
goal: "legacy path",
|
||||
successCriteria: "ok",
|
||||
proofLevel: "tested",
|
||||
integrationClosure: "merged",
|
||||
observabilityImpact: "logs",
|
||||
adversarialReview: {
|
||||
partner: "P review",
|
||||
combatant: "C review",
|
||||
architect: "A review",
|
||||
},
|
||||
planningMeeting: planningMeeting(),
|
||||
tasks: [
|
||||
{
|
||||
taskId: "T01",
|
||||
title: "legacy task",
|
||||
description: "describes legacy task",
|
||||
estimate: "small",
|
||||
files: ["x.ts"],
|
||||
verify: "ok",
|
||||
inputs: ["i"],
|
||||
expectedOutput: ["o"],
|
||||
},
|
||||
],
|
||||
},
|
||||
project,
|
||||
// no options.uokContext
|
||||
);
|
||||
const db = _getAdapter();
|
||||
const rows = db
|
||||
.prepare(
|
||||
"SELECT gate_id, surface, run_control, permission_profile, trace_id FROM quality_gates WHERE milestone_id = ? AND slice_id = ? ORDER BY gate_id",
|
||||
)
|
||||
.all("M2", "S2");
|
||||
assert.ok(rows.length >= 4);
|
||||
for (const row of rows) {
|
||||
assert.equal(
|
||||
row.surface,
|
||||
null,
|
||||
`gate ${row.gate_id} unexpectedly carries surface`,
|
||||
);
|
||||
assert.equal(row.run_control, null);
|
||||
assert.equal(row.permission_profile, null);
|
||||
assert.equal(row.trace_id, null);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
* Slice 3b of "Make UOK the SF Control Plane".
|
||||
*/
|
||||
|
||||
import { UokGateRunner } from "./gate-runner.js";
|
||||
import { buildUokRunContext } from "./run-context.js";
|
||||
|
||||
/**
|
||||
|
|
@ -76,3 +77,67 @@ export function buildAutonomousUokContext(opts) {
|
|||
taskId: opts.taskId,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a one-shot UOK gate via UokGateRunner, with the schema-v2
|
||||
* run-context already attached.
|
||||
*
|
||||
* Why this exists: the autonomous-loop phases register a gate that
|
||||
* just returns the verdict the phase already computed (it's a recording
|
||||
* gate, not a decision gate), then immediately invokes runner.run.
|
||||
* Centralizing that pattern means every call site forwards the same
|
||||
* surface/runControl/permissionProfile/parentTrace fields without
|
||||
* having to duplicate the 6-line setup, which is what was making the
|
||||
* phases drift to "legacy" before this slice.
|
||||
*
|
||||
* The optional UokGateRunner argument lets tests inject a mock; the
|
||||
* default constructs a fresh runner per emission (matches the
|
||||
* existing pre-dispatch pattern).
|
||||
*
|
||||
* @param {object} opts
|
||||
* @param {string} opts.basePath .sf base path for trace writes.
|
||||
* @param {string} opts.gateId Gate id (e.g. "pre-dispatch-health-gate").
|
||||
* @param {string} opts.gateType UOK gate type ("policy", "execution", ...).
|
||||
* @param {string} opts.outcome Decision: "pass" / "fail" / "manual-attention".
|
||||
* @param {string} [opts.failureClass] Failure class when outcome != "pass".
|
||||
* @param {string} [opts.rationale]
|
||||
* @param {string} [opts.findings]
|
||||
* @param {string} opts.traceId Phase trace id.
|
||||
* @param {string} opts.turnId Per-iteration turn id.
|
||||
* @param {object} [opts.uokContext] Schema-v2 ctx (from buildAutonomousUokContext).
|
||||
* @param {string} [opts.unitType]
|
||||
* @param {string} [opts.unitId]
|
||||
* @param {string} [opts.milestoneId]
|
||||
* @param {string} [opts.sliceId]
|
||||
* @param {string} [opts.taskId]
|
||||
* @param {UokGateRunner} [opts.runnerOverride] Test seam.
|
||||
*
|
||||
* @returns The runner's result object.
|
||||
*/
|
||||
export async function emitAutonomousGate(opts) {
|
||||
const runner = opts.runnerOverride ?? new UokGateRunner();
|
||||
runner.register({
|
||||
id: opts.gateId,
|
||||
type: opts.gateType,
|
||||
execute: async () => ({
|
||||
outcome: opts.outcome,
|
||||
failureClass: opts.failureClass,
|
||||
rationale: opts.rationale,
|
||||
findings: opts.findings ?? "",
|
||||
}),
|
||||
});
|
||||
return runner.run(opts.gateId, {
|
||||
basePath: opts.basePath,
|
||||
traceId: opts.traceId,
|
||||
turnId: opts.turnId,
|
||||
milestoneId: opts.milestoneId,
|
||||
unitType: opts.unitType,
|
||||
unitId: opts.unitId,
|
||||
sliceId: opts.sliceId,
|
||||
taskId: opts.taskId,
|
||||
surface: opts.uokContext?.surface,
|
||||
runControl: opts.uokContext?.runControl,
|
||||
permissionProfile: opts.uokContext?.permissionProfile,
|
||||
parentTrace: opts.uokContext?.parentTrace,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue