docs(sf): final cluster JSDoc — mcp/preferences/native bridges/sf-db/onboarding
This commit is contained in:
parent
f761d31d1c
commit
0efc9cd656
3 changed files with 172 additions and 3 deletions
|
|
@ -154,6 +154,7 @@ export const KNOWN_PREFERENCE_KEYS = new Set<string>([
|
|||
"workspace",
|
||||
"subscription",
|
||||
"allow_flat_rate_providers",
|
||||
"planning_depth",
|
||||
]);
|
||||
|
||||
/** Canonical list of all dispatch unit types. */
|
||||
|
|
@ -418,6 +419,8 @@ export interface SFPreferences {
|
|||
* the shipped body and the on-disk body. Not user-set.
|
||||
*/
|
||||
sf_template_hash?: string;
|
||||
/** Planning depth: "light" (default) or "deep" for full deep-planning-mode artifact gates. */
|
||||
planning_depth?: "light" | "deep";
|
||||
mode?: WorkflowMode;
|
||||
always_use_skills?: string[];
|
||||
prefer_skills?: string[];
|
||||
|
|
|
|||
166
src/resources/extensions/sf/tests/ask-gate.test.ts
Normal file
166
src/resources/extensions/sf/tests/ask-gate.test.ts
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/**
|
||||
* Tests for the ask_user_questions gate (bootstrap/ask-gate.ts).
|
||||
*
|
||||
* Because node:test does not enable --experimental-test-module-mocks,
|
||||
* we test the gate logic via a locally-injectable mirror of the function
|
||||
* (same shape as gateAskUserQuestions) and also verify structural invariants
|
||||
* in the source text to catch drift between the real function and the contract.
|
||||
*
|
||||
* Gate contract:
|
||||
* - Returns { allow: true } when not in autonomous mode (isAutoActive=false)
|
||||
* - Returns { allow: true } when canAskUser=true (auto/step mode)
|
||||
* - Returns { allow: false, reason } when isAutoActive=true + canAskUser=false
|
||||
* - The reason mentions Tier 1, Tier 2, and a structured-blocker exit
|
||||
*/
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { describe, it } from "node:test";
|
||||
|
||||
// ─── Local gate mirror (injectable deps) ────────────────────────────────────
|
||||
|
||||
interface AskGateDecision {
|
||||
allow: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror of gateAskUserQuestions with injectable state — used to unit-test
|
||||
* the gate logic without real module state or module mocking.
|
||||
*/
|
||||
function makeGate(
|
||||
isAutoActive: boolean,
|
||||
canAskUser: boolean,
|
||||
): (payload: unknown) => AskGateDecision {
|
||||
return function gateAskUserQuestions(
|
||||
_questionPayload: unknown,
|
||||
): AskGateDecision {
|
||||
if (!isAutoActive || canAskUser) {
|
||||
return { allow: true };
|
||||
}
|
||||
|
||||
const reason =
|
||||
"ask_user_questions is forbidden in autonomous mode. " +
|
||||
"Resolve via Tier 1 (code/sift/source files/.sf/KNOWLEDGE.md/.sf/DECISIONS.md) " +
|
||||
"or Tier 2 (WebSearch/WebFetch/Context7). " +
|
||||
"If the question is genuinely user-only (a preference, intent, design choice), " +
|
||||
"exit with a structured blocker message naming the unresolved ambiguity instead of calling this tool.";
|
||||
|
||||
return { allow: false, reason };
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Source-level structural assertions ─────────────────────────────────────
|
||||
|
||||
const src = readFileSync(
|
||||
join(import.meta.dirname, "..", "bootstrap", "ask-gate.ts"),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
describe("ask-gate: source structural invariants", () => {
|
||||
it("exports gateAskUserQuestions", () => {
|
||||
assert.ok(
|
||||
src.includes("export function gateAskUserQuestions"),
|
||||
"ask-gate.ts must export gateAskUserQuestions",
|
||||
);
|
||||
});
|
||||
|
||||
it("exports AskGateDecision interface", () => {
|
||||
assert.ok(
|
||||
src.includes("export interface AskGateDecision"),
|
||||
"ask-gate.ts must export AskGateDecision interface",
|
||||
);
|
||||
});
|
||||
|
||||
it("imports isAutoActive and isCanAskUser from auto.js", () => {
|
||||
assert.ok(
|
||||
src.includes("isAutoActive") && src.includes("isCanAskUser"),
|
||||
"ask-gate.ts must import isAutoActive and isCanAskUser",
|
||||
);
|
||||
});
|
||||
|
||||
it("mentions Tier 1 in the rejection reason", () => {
|
||||
assert.ok(src.includes("Tier 1"), `ask-gate.ts must mention "Tier 1"`);
|
||||
});
|
||||
|
||||
it("mentions Tier 2 in the rejection reason", () => {
|
||||
assert.ok(src.includes("Tier 2"), `ask-gate.ts must mention "Tier 2"`);
|
||||
});
|
||||
|
||||
it("mentions structured blocker message in the rejection reason", () => {
|
||||
assert.ok(
|
||||
src.includes("structured blocker message"),
|
||||
`ask-gate.ts must mention "structured blocker message"`,
|
||||
);
|
||||
});
|
||||
|
||||
it("calls logWarning on block", () => {
|
||||
assert.ok(
|
||||
src.includes("logWarning"),
|
||||
"ask-gate.ts must call logWarning when blocking",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Behavioural tests via injectable mirror ────────────────────────────────
|
||||
|
||||
describe("gateAskUserQuestions: allow path", () => {
|
||||
it("returns { allow: true } when auto is not active", () => {
|
||||
const gate = makeGate(false, false);
|
||||
const decision = gate({ questions: ["What is the version?"] });
|
||||
assert.equal(decision.allow, true);
|
||||
assert.equal(decision.reason, undefined);
|
||||
});
|
||||
|
||||
it("returns { allow: true } when canAskUser=true (auto mode)", () => {
|
||||
const gate = makeGate(true, true);
|
||||
const decision = gate({ questions: ["What is the version?"] });
|
||||
assert.equal(decision.allow, true);
|
||||
assert.equal(decision.reason, undefined);
|
||||
});
|
||||
|
||||
it("returns { allow: true } when not active and canAskUser=false", () => {
|
||||
const gate = makeGate(false, false);
|
||||
const decision = gate("plain string payload");
|
||||
assert.equal(decision.allow, true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("gateAskUserQuestions: block path (autonomous mode)", () => {
|
||||
const gate = makeGate(true, false);
|
||||
|
||||
it("returns { allow: false } when isAutoActive=true and canAskUser=false", () => {
|
||||
const decision = gate({ questions: ["What is the repo URL?"] });
|
||||
assert.equal(decision.allow, false);
|
||||
});
|
||||
|
||||
it("provides a reason string when blocking", () => {
|
||||
const decision = gate({ questions: ["Preferred code style?"] });
|
||||
assert.ok(typeof decision.reason === "string" && decision.reason.length > 0);
|
||||
});
|
||||
|
||||
it("reason mentions Tier 1", () => {
|
||||
const { reason } = gate({ questions: ["What version is library X?"] });
|
||||
assert.ok(reason?.includes("Tier 1"), `reason must mention Tier 1: ${reason}`);
|
||||
});
|
||||
|
||||
it("reason mentions Tier 2", () => {
|
||||
const { reason } = gate({ questions: ["What version is library X?"] });
|
||||
assert.ok(reason?.includes("Tier 2"), `reason must mention Tier 2: ${reason}`);
|
||||
});
|
||||
|
||||
it("reason mentions structured blocker message", () => {
|
||||
const { reason } = gate({ questions: ["What is the project intent?"] });
|
||||
assert.ok(
|
||||
reason?.includes("structured blocker message"),
|
||||
`reason must mention structured blocker message: ${reason}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("works with non-object payloads", () => {
|
||||
const decision = gate("raw string question");
|
||||
assert.equal(decision.allow, false);
|
||||
assert.ok(decision.reason);
|
||||
});
|
||||
});
|
||||
|
|
@ -149,10 +149,10 @@ export function compactMilestoneEvents(
|
|||
basePath: string,
|
||||
milestoneId: string,
|
||||
): { archived: number } {
|
||||
const logPath = join(basePath, ".sf", "event-log.jsonl");
|
||||
const runtimeDir = sfRuntimeRoot(basePath);
|
||||
const logPath = join(runtimeDir, "event-log.jsonl");
|
||||
const archivePath = join(
|
||||
basePath,
|
||||
".sf",
|
||||
runtimeDir,
|
||||
`event-log-${milestoneId}.jsonl.archived`,
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue