fix(gsd): unlock depth verification outside guided flow (#4058)
This commit is contained in:
parent
65ba0fc30b
commit
1d8e7c95ff
2 changed files with 105 additions and 10 deletions
|
|
@ -181,14 +181,10 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|||
// Only gate-shaped ask_user_questions calls should block execution.
|
||||
// The gate stays pending until the user selects the approval option.
|
||||
if (event.toolName === "ask_user_questions") {
|
||||
const milestoneId = getDiscussionMilestoneId(discussionBasePath);
|
||||
const inDiscussion = milestoneId !== null || isQueuePhaseActive();
|
||||
if (inDiscussion) {
|
||||
const questions: any[] = (event.input as any)?.questions ?? [];
|
||||
const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
|
||||
if (typeof questionId === "string") {
|
||||
setPendingGate(questionId);
|
||||
}
|
||||
const questions: any[] = (event.input as any)?.questions ?? [];
|
||||
const questionId = questions.find((question) => typeof question?.id === "string" && isGateQuestionId(question.id))?.id;
|
||||
if (typeof questionId === "string") {
|
||||
setPendingGate(questionId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -286,7 +282,6 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|||
if (event.toolName !== "ask_user_questions") return;
|
||||
const milestoneId = getDiscussionMilestoneId(process.cwd());
|
||||
const queueActive = isQueuePhaseActive();
|
||||
if (!milestoneId && !queueActive) return;
|
||||
|
||||
const details = event.details as any;
|
||||
|
||||
|
|
@ -319,13 +314,16 @@ export function registerHooks(pi: ExtensionAPI): void {
|
|||
// Only unlock the gate if the user selected the first option (confirmation).
|
||||
// Cross-references against the question's defined options to reject free-form "Other" text.
|
||||
const answer = details.response?.answers?.[question.id];
|
||||
const inferredMilestoneId = extractDepthVerificationMilestoneId(question.id) ?? milestoneId;
|
||||
if (isDepthConfirmationAnswer(answer?.selected, question.options)) {
|
||||
markDepthVerified(extractDepthVerificationMilestoneId(question.id) ?? milestoneId);
|
||||
markDepthVerified(inferredMilestoneId);
|
||||
clearPendingGate();
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!milestoneId && !queueActive) return;
|
||||
if (!milestoneId) return;
|
||||
|
||||
const basePath = process.cwd();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdirSync, rmSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { tmpdir } from "node:os";
|
||||
|
||||
import { registerHooks } from "../bootstrap/register-hooks.ts";
|
||||
import {
|
||||
getPendingGate,
|
||||
resetWriteGateState,
|
||||
shouldBlockContextArtifactSave,
|
||||
} from "../bootstrap/write-gate.ts";
|
||||
|
||||
function makeTempDir(prefix: string): string {
|
||||
const dir = join(
|
||||
tmpdir(),
|
||||
`gsd-depth-gate-${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
);
|
||||
mkdirSync(dir, { recursive: true });
|
||||
return dir;
|
||||
}
|
||||
|
||||
test("register-hooks unlocks milestone depth verification from question id without guided-flow state (#4047)", async (t) => {
|
||||
const dir = makeTempDir("manual");
|
||||
const originalCwd = process.cwd();
|
||||
process.chdir(dir);
|
||||
resetWriteGateState();
|
||||
|
||||
t.after(() => {
|
||||
resetWriteGateState();
|
||||
process.chdir(originalCwd);
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
const handlers = new Map<string, Array<(event: any, ctx?: any) => Promise<void> | void>>();
|
||||
const pi = {
|
||||
on(event: string, handler: (event: any, ctx?: any) => Promise<void> | void) {
|
||||
const existing = handlers.get(event) ?? [];
|
||||
existing.push(handler);
|
||||
handlers.set(event, existing);
|
||||
},
|
||||
} as any;
|
||||
|
||||
registerHooks(pi);
|
||||
|
||||
const questionId = "depth_verification_M001_confirm";
|
||||
const questions = [
|
||||
{
|
||||
id: questionId,
|
||||
question: "Do you agree?",
|
||||
options: [
|
||||
{ label: "Yes, you got it (Recommended)" },
|
||||
{ label: "Needs adjustment" },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const toolCallHandlers = handlers.get("tool_call");
|
||||
const toolResultHandlers = handlers.get("tool_result");
|
||||
assert.ok(toolCallHandlers?.length, "tool_call handler should be registered");
|
||||
assert.ok(toolResultHandlers?.length, "tool_result handler should be registered");
|
||||
|
||||
for (const handler of toolCallHandlers ?? []) {
|
||||
await handler({
|
||||
toolName: "ask_user_questions",
|
||||
input: { questions },
|
||||
});
|
||||
}
|
||||
|
||||
assert.equal(getPendingGate(), questionId, "gate should be set even without guided-flow state");
|
||||
assert.equal(
|
||||
shouldBlockContextArtifactSave("CONTEXT", "M001").block,
|
||||
true,
|
||||
"milestone context should still be blocked before confirmation",
|
||||
);
|
||||
|
||||
for (const handler of toolResultHandlers ?? []) {
|
||||
await handler({
|
||||
toolName: "ask_user_questions",
|
||||
input: { questions },
|
||||
details: {
|
||||
response: {
|
||||
answers: {
|
||||
[questionId]: { selected: "Yes, you got it (Recommended)" },
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
assert.equal(getPendingGate(), null, "confirming the depth question should clear the pending gate");
|
||||
assert.equal(
|
||||
shouldBlockContextArtifactSave("CONTEXT", "M001").block,
|
||||
false,
|
||||
"question-id milestone inference should unlock the matching milestone context write",
|
||||
);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue