1963 lines
68 KiB
TypeScript
1963 lines
68 KiB
TypeScript
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
||
import path from "node:path";
|
||
|
||
import {
|
||
parseNasCodexBootstrapCliArgs,
|
||
parseDeployObservationCliArgs,
|
||
parseGlobalReviewCliArgs,
|
||
parseNapcatRemoteDevHandoffCliArgs,
|
||
parseNapcatRuntimeReleaseReadinessCliArgs,
|
||
parseNapcatSyncCandidateReviewCliArgs,
|
||
parseNapcatUpstreamAuditCliArgs,
|
||
parseWorkstreamCloseoutCliArgs,
|
||
} from "./core/cli.js";
|
||
import { registeredToolNames } from "./core/constants.js";
|
||
import { buildBlockerResolution } from "./tools/blocker.js";
|
||
import { buildChangeDocSync } from "./tools/docSync.js";
|
||
import { resolveInsideRoot } from "./core/workspace.js";
|
||
import { hasEntityLoadTimeSerializationMutation } from "./tools/envRisk.js";
|
||
import { buildGuardrails } from "./tools/guardrails.js";
|
||
import {
|
||
readObsidianContext,
|
||
syncObsidianWorkflow,
|
||
validateObsidianVault,
|
||
} from "./tools/obsidian.js";
|
||
import {
|
||
cleanupHistoryArtifacts,
|
||
parseCliCleanupArgs,
|
||
} from "./tools/cleanup.js";
|
||
import { buildWorkflowLoopAudit } from "./tools/loop.js";
|
||
import { inspectProject } from "./tools/inspect.js";
|
||
import { prepareTask, readWorkflowContext } from "./tools/task.js";
|
||
import {
|
||
buildGlobalCodeReview,
|
||
findNapcatImageGovernanceFindings,
|
||
findQqbotNapcatCaptchaFlowFindings,
|
||
findQqbotCommandServiceTestImportFindings,
|
||
findQqbotPluginPackageProductionPathFindings,
|
||
findQqbotPluginSmokeImportFindings,
|
||
findQqbotStatusBoundaryFindings,
|
||
findTaskRecordGovernanceFindings,
|
||
isBenignCredentialReviewValue,
|
||
} from "./tools/review.js";
|
||
import {
|
||
napcatAutomationReasonCodes,
|
||
parseNapcatAutomationClassification,
|
||
upstreamAuditOutputSchema,
|
||
} from "./tools/napcatAutomation.types.js";
|
||
import {
|
||
buildNapcatCodexExecCommand,
|
||
buildNapcatRemoteDevHandoff,
|
||
buildNapcatRuntimeReleaseReadiness,
|
||
buildNapcatSyncCandidateReview,
|
||
buildNapcatUpstreamAudit,
|
||
buildNasCodexBootstrapPlan,
|
||
classifyNapcatUpstreamAudit,
|
||
napcatAutomationDefaults,
|
||
} from "./tools/napcatAutomation.js";
|
||
import { registerTools } from "./registerTools.js";
|
||
import {
|
||
loadNapcatAutomationPrompt,
|
||
type NapcatAutomationPromptName,
|
||
napcatAutomationPromptNames,
|
||
} from "./tools/napcatAutomation.prompts.js";
|
||
import {
|
||
buildBusinessTestPlan,
|
||
buildNapcatDeviceProfileCheck,
|
||
} from "./tools/testing.js";
|
||
import { buildVerificationPlan } from "./tools/verification.js";
|
||
import { buildWorkstreamCloseout } from "./tools/closeout.js";
|
||
import {
|
||
buildDeployObservation,
|
||
buildDeployObservationSectionMap,
|
||
normalizeDeployObservationEvidence,
|
||
} from "./tools/deployObservation.js";
|
||
import {
|
||
buildCommitPlan,
|
||
buildComponentWorkflow,
|
||
buildDbSyncPlan,
|
||
buildFinishTask,
|
||
buildPushPlan,
|
||
buildRemoteHealthCheck,
|
||
} from "./tools/workflow.js";
|
||
/**
|
||
* Runs ktWorkflow guardrail smoke checks from the CLI self-test entry; writes local evidence under the KT workspace and throws on any broken workflow contract.
|
||
* @returns A promise that resolves after all contract checks and evidence writing complete.
|
||
*/
|
||
export async function runSelfTest(): Promise<void> {
|
||
const napcatAuditSchemaSmoke = upstreamAuditOutputSchema.safeParse({
|
||
classification: "manual-review",
|
||
reasonCodes: ["HOT_ZONE_CHANGED"],
|
||
recommendedAction: "request-human-review",
|
||
summary: "Login hot-zone changed.",
|
||
});
|
||
if (!napcatAuditSchemaSmoke.success) {
|
||
throw new Error("NapCat upstream audit schema self-check failed");
|
||
}
|
||
const napcatAuditExtraPropertySmoke = upstreamAuditOutputSchema.safeParse({
|
||
classification: "manual-review",
|
||
reasonCodes: ["HOT_ZONE_CHANGED"],
|
||
recommendedAction: "request-human-review",
|
||
summary: "Login hot-zone changed.",
|
||
unexpectedField: "Codex output schema rejects this.",
|
||
});
|
||
if (napcatAuditExtraPropertySmoke.success) {
|
||
throw new Error("NapCat upstream audit schema strict self-check failed");
|
||
}
|
||
if (!napcatAutomationReasonCodes.includes("HOT_ZONE_CHANGED")) {
|
||
throw new Error("NapCat reason-code list self-check failed");
|
||
}
|
||
const napcatCli = parseNapcatUpstreamAuditCliArgs([
|
||
"node",
|
||
"server",
|
||
"--napcat-upstream-audit",
|
||
"--execute",
|
||
"--use-codex",
|
||
"--artifact-root",
|
||
"/vol1/docker/kt-codex/artifacts/napcat-upstream-sync",
|
||
]);
|
||
if (!napcatCli.execute || !napcatCli.useCodex) {
|
||
throw new Error("NapCat upstream audit CLI parser self-check failed");
|
||
}
|
||
if (
|
||
napcatCli.artifactRoot !==
|
||
"/vol1/docker/kt-codex/artifacts/napcat-upstream-sync"
|
||
) {
|
||
throw new Error("NapCat upstream audit artifactRoot CLI parser self-check failed");
|
||
}
|
||
const napcatCandidateCli = parseNapcatSyncCandidateReviewCliArgs([
|
||
"node",
|
||
"server",
|
||
"--napcat-sync-candidate-review",
|
||
]);
|
||
const napcatRuntimeCli = parseNapcatRuntimeReleaseReadinessCliArgs([
|
||
"node",
|
||
"server",
|
||
"--napcat-runtime-release-readiness",
|
||
]);
|
||
const napcatRemoteHandoffCli = parseNapcatRemoteDevHandoffCliArgs([
|
||
"node",
|
||
"server",
|
||
"--napcat-remote-dev-handoff",
|
||
]);
|
||
const nasBootstrapCli = parseNasCodexBootstrapCliArgs([
|
||
"node",
|
||
"server",
|
||
"--nas-codex-bootstrap",
|
||
]);
|
||
if (
|
||
napcatCandidateCli.execute ||
|
||
napcatRuntimeCli.execute ||
|
||
napcatRemoteHandoffCli.execute ||
|
||
nasBootstrapCli.execute
|
||
) {
|
||
throw new Error("NapCat auxiliary CLI dry-run default self-check failed");
|
||
}
|
||
const napcatCandidateDefaults = buildNapcatSyncCandidateReview({}).context;
|
||
const napcatRuntimeDefaults = buildNapcatRuntimeReleaseReadiness({}).context;
|
||
const napcatRemoteDefaults = buildNapcatRemoteDevHandoff({}).context;
|
||
const nasBootstrapDefaults = buildNasCodexBootstrapPlan({}).context;
|
||
if (
|
||
napcatCandidateDefaults.candidateBranch !==
|
||
napcatAutomationDefaults.candidateBranch ||
|
||
napcatCandidateDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
|
||
napcatCandidateDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
|
||
napcatCandidateDefaults.lastAcceptedUpstreamBase !==
|
||
napcatAutomationDefaults.lastAcceptedUpstreamBase ||
|
||
napcatCandidateDefaults.upstreamReleaseTag !==
|
||
napcatAutomationDefaults.upstreamReleaseRef ||
|
||
napcatCandidateDefaults.upstreamRepo !== napcatAutomationDefaults.upstreamRepo ||
|
||
napcatCandidateDefaults.workspaceRoot !==
|
||
napcatAutomationDefaults.workspaceRoot
|
||
) {
|
||
throw new Error("NapCat candidate review default alignment self-check failed");
|
||
}
|
||
if (
|
||
napcatRuntimeDefaults.apiImageTag !==
|
||
napcatAutomationDefaults.apiImageTag ||
|
||
napcatRuntimeDefaults.candidateBranch !==
|
||
napcatAutomationDefaults.candidateBranch ||
|
||
napcatRuntimeDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
|
||
napcatRuntimeDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
|
||
napcatRuntimeDefaults.napcatImageTag !==
|
||
napcatAutomationDefaults.runtimeImageTag ||
|
||
napcatRuntimeDefaults.profile !== napcatAutomationDefaults.runtimeProfile ||
|
||
napcatRuntimeDefaults.upstreamReleaseTag !==
|
||
napcatAutomationDefaults.upstreamReleaseRef ||
|
||
napcatRuntimeDefaults.workspaceRoot !==
|
||
napcatAutomationDefaults.workspaceRoot
|
||
) {
|
||
throw new Error("NapCat runtime readiness default alignment self-check failed");
|
||
}
|
||
if (
|
||
napcatRemoteDefaults.candidateBranch !==
|
||
napcatAutomationDefaults.candidateBranch ||
|
||
napcatRemoteDefaults.forkBranch !== napcatAutomationDefaults.forkBranch ||
|
||
napcatRemoteDefaults.forkRepo !== napcatAutomationDefaults.forkRepo ||
|
||
napcatRemoteDefaults.targetHost !== napcatAutomationDefaults.targetHost ||
|
||
napcatRemoteDefaults.upstreamReleaseTag !==
|
||
napcatAutomationDefaults.upstreamReleaseRef ||
|
||
napcatRemoteDefaults.workspaceRoot !== napcatAutomationDefaults.workspaceRoot
|
||
) {
|
||
throw new Error("NapCat remote handoff default alignment self-check failed");
|
||
}
|
||
if (
|
||
nasBootstrapDefaults.codexHome !== napcatAutomationDefaults.codexHome ||
|
||
nasBootstrapDefaults.serviceName !==
|
||
napcatAutomationDefaults.bootstrapServiceName ||
|
||
nasBootstrapDefaults.workspaceRoot !== napcatAutomationDefaults.workspaceRoot
|
||
) {
|
||
throw new Error("NAS Codex bootstrap default alignment self-check failed");
|
||
}
|
||
const bootstrap = buildNasCodexBootstrapPlan({ execute: false });
|
||
const bootstrapCommandText = bootstrap.commands
|
||
.map((item) => item.command)
|
||
.join("\n");
|
||
if (!bootstrapCommandText.includes("/vol1/docker/kt-codex/home/.codex")) {
|
||
throw new Error("NAS Codex bootstrap CODEX_HOME self-check failed");
|
||
}
|
||
if (bootstrapCommandText.includes("C:\\Users")) {
|
||
throw new Error("NAS Codex bootstrap Windows path guard self-check failed");
|
||
}
|
||
for (const expected of [
|
||
"node --version",
|
||
"corepack --version",
|
||
"pnpm --version",
|
||
"git --version",
|
||
"docker --version",
|
||
"codex --version",
|
||
"test -d /vol1/docker/kt-codex/workspace/KT",
|
||
"test -d /vol1/docker/kt-codex/home/.codex",
|
||
]) {
|
||
if (!bootstrapCommandText.includes(expected)) {
|
||
throw new Error(`NAS Codex bootstrap dry-run command missing: ${expected}`);
|
||
}
|
||
}
|
||
for (const command of bootstrap.commands) {
|
||
if (!command.readOnly) {
|
||
throw new Error("NAS Codex bootstrap dry-run command must be read-only");
|
||
}
|
||
}
|
||
const executeBootstrap = buildNasCodexBootstrapPlan({ execute: true });
|
||
const executeBootstrapCommandText = executeBootstrap.commands
|
||
.map((item) => item.command)
|
||
.join("\n");
|
||
if (
|
||
executeBootstrap.execute !== true ||
|
||
executeBootstrap.commands.every((command) => command.readOnly)
|
||
) {
|
||
throw new Error("NAS Codex bootstrap execute plan self-check failed");
|
||
}
|
||
if (
|
||
!executeBootstrapCommandText.includes("install -d -m 700 /vol1/docker/kt-codex/home/.codex") ||
|
||
!executeBootstrapCommandText.includes("npm install -g @openai/codex")
|
||
) {
|
||
throw new Error("NAS Codex bootstrap execute package plan self-check failed");
|
||
}
|
||
for (const forbidden of [
|
||
"git push",
|
||
"git merge",
|
||
"docker build",
|
||
"docker pull",
|
||
"kubectl",
|
||
"systemctl enable",
|
||
"systemctl start",
|
||
"cp ~/.codex",
|
||
"scp ~/.codex",
|
||
"C:\\Users",
|
||
]) {
|
||
if (executeBootstrapCommandText.includes(forbidden)) {
|
||
throw new Error(`NAS Codex bootstrap forbidden command leaked: ${forbidden}`);
|
||
}
|
||
}
|
||
let windowsBootstrapPathRejected = false;
|
||
try {
|
||
buildNasCodexBootstrapPlan({
|
||
codexHome: "C:\\Users\\example\\.codex",
|
||
execute: false,
|
||
});
|
||
} catch (error) {
|
||
windowsBootstrapPathRejected = String(error).includes(
|
||
"Unsafe NAS Codex bootstrap path",
|
||
);
|
||
}
|
||
if (!windowsBootstrapPathRejected) {
|
||
throw new Error("NAS Codex bootstrap unsafe Windows path self-check failed");
|
||
}
|
||
const napcatCandidateOverride = buildNapcatSyncCandidateReview({
|
||
candidateBranch: "custom-candidate",
|
||
lastAcceptedUpstreamBase: "custom-base",
|
||
});
|
||
const napcatCandidateOverrideCommandText =
|
||
napcatCandidateOverride.commands.map((item) => item.command).join("\n");
|
||
if (
|
||
!napcatCandidateOverrideCommandText.includes("custom-candidate") ||
|
||
!napcatCandidateOverrideCommandText.includes("custom-base") ||
|
||
napcatCandidateOverrideCommandText.includes(
|
||
napcatAutomationDefaults.candidateBranch,
|
||
) ||
|
||
napcatCandidateOverrideCommandText.includes(
|
||
napcatAutomationDefaults.lastAcceptedUpstreamBase,
|
||
)
|
||
) {
|
||
throw new Error("NapCat candidate command override self-check failed");
|
||
}
|
||
const napcatRuntimeOverride = buildNapcatRuntimeReleaseReadiness({
|
||
napcatImageTag: "custom-image",
|
||
});
|
||
const napcatRuntimeOverrideCommandText =
|
||
napcatRuntimeOverride.commands.map((item) => item.command).join("\n");
|
||
if (
|
||
!napcatRuntimeOverrideCommandText.includes("custom-image") ||
|
||
napcatRuntimeOverrideCommandText.includes(
|
||
napcatAutomationDefaults.runtimeImageTag,
|
||
)
|
||
) {
|
||
throw new Error("NapCat runtime command override self-check failed");
|
||
}
|
||
const napcatAuditOverride = buildNapcatUpstreamAudit({
|
||
dryMergeConflict: false,
|
||
forkBranch: "custom-fork",
|
||
forkPatchFiles: [],
|
||
lastAcceptedUpstreamBase: "custom-base",
|
||
upstreamChangedFiles: [],
|
||
upstreamReleaseTag: "custom-upstream",
|
||
});
|
||
const napcatAuditOverrideCommandText =
|
||
napcatAuditOverride.commands.map((item) => item.command).join("\n");
|
||
if (
|
||
!napcatAuditOverrideCommandText.includes("custom-fork") ||
|
||
!napcatAuditOverrideCommandText.includes("custom-base") ||
|
||
!napcatAuditOverrideCommandText.includes("custom-upstream") ||
|
||
napcatAuditOverrideCommandText.includes(napcatAutomationDefaults.forkBranch) ||
|
||
napcatAuditOverrideCommandText.includes(
|
||
napcatAutomationDefaults.lastAcceptedUpstreamBase,
|
||
) ||
|
||
napcatAuditOverrideCommandText.includes(
|
||
napcatAutomationDefaults.upstreamReleaseRef,
|
||
)
|
||
) {
|
||
throw new Error("NapCat upstream audit command override self-check failed");
|
||
}
|
||
const packageJson = JSON.parse(
|
||
readFileSync(resolveInsideRoot("mcp/ktWorkflow/package.json"), "utf8"),
|
||
) as { scripts?: Record<string, string> };
|
||
if (!packageJson.scripts?.["napcat-remote-dev-handoff"]) {
|
||
throw new Error("NapCat remote handoff npm script self-check failed");
|
||
}
|
||
const registeredByFakeServer: string[] = [];
|
||
const fakeMcpServer = {
|
||
/**
|
||
* Records registered MCP tool names so self-test proves the actual registerTools surface, not only the constants list.
|
||
* @param name - MCP tool name passed by `registerTools`.
|
||
* @returns Nothing; the name is appended to `registeredByFakeServer`.
|
||
*/
|
||
registerTool(name: string): void {
|
||
registeredByFakeServer.push(name);
|
||
},
|
||
};
|
||
registerTools(fakeMcpServer as never);
|
||
for (const toolName of [
|
||
"kt_napcat_upstream_audit",
|
||
"kt_napcat_sync_candidate_review",
|
||
"kt_napcat_runtime_release_readiness",
|
||
"kt_napcat_remote_dev_handoff",
|
||
"kt_nas_codex_bootstrap_plan",
|
||
] as const) {
|
||
if (!registeredToolNames.includes(toolName)) {
|
||
throw new Error(`NapCat MCP tool registry self-check failed: ${toolName}`);
|
||
}
|
||
if (!registeredByFakeServer.includes(toolName)) {
|
||
throw new Error(`NapCat MCP registerTools self-check failed: ${toolName}`);
|
||
}
|
||
}
|
||
if (parseNapcatAutomationClassification("manual-review") !== "manual-review") {
|
||
throw new Error("NapCat classification parser self-check failed");
|
||
}
|
||
const hotZoneAudit = classifyNapcatUpstreamAudit({
|
||
dryMergeConflict: false,
|
||
forkPatchFiles: ["packages/napcat-core/login/runtime.ts"],
|
||
upstreamChangedFiles: ["packages/napcat-core/login/runtime.ts"],
|
||
});
|
||
if (hotZoneAudit.classification !== "manual-review") {
|
||
throw new Error("NapCat hot-zone audit classification self-check failed");
|
||
}
|
||
if (!hotZoneAudit.reasonCodes.includes("HOT_ZONE_CHANGED")) {
|
||
throw new Error("NapCat hot-zone reason-code self-check failed");
|
||
}
|
||
const dryRunAudit = buildNapcatUpstreamAudit({
|
||
dryMergeConflict: false,
|
||
forkPatchFiles: ["packages/napcat-core/runtime/base.ts"],
|
||
upstreamChangedFiles: ["packages/napcat-core/runtime/upstream.ts"],
|
||
});
|
||
if (dryRunAudit.execute !== false || dryRunAudit.artifacts.length === 0) {
|
||
throw new Error(
|
||
"NapCat upstream audit dry-run artifact plan self-check failed",
|
||
);
|
||
}
|
||
const dryRunAuditDefaultCommands = dryRunAudit.commands.map(
|
||
(item) => item.command,
|
||
);
|
||
if (
|
||
dryRunAuditDefaultCommands.some((command) =>
|
||
command.includes("FORK_BRANCH:-HEAD"),
|
||
) ||
|
||
!dryRunAuditDefaultCommands.some((command) =>
|
||
command.includes(`FORK_BRANCH:-${napcatAutomationDefaults.forkBranch}`),
|
||
) ||
|
||
!dryRunAuditDefaultCommands.some((command) =>
|
||
command.includes(
|
||
`UPSTREAM_RELEASE_REF:-${napcatAutomationDefaults.upstreamReleaseRef}`,
|
||
),
|
||
)
|
||
) {
|
||
throw new Error("NapCat upstream audit command default alignment self-check failed");
|
||
}
|
||
const codexCommand = buildNapcatCodexExecCommand({
|
||
artifactRoot: "/vol1/docker/kt-codex/artifacts/smoke",
|
||
contextPacketPath:
|
||
"/vol1/docker/kt-codex/artifacts/smoke/context-packet.md",
|
||
outputSchemaPath:
|
||
"/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json",
|
||
promptName: "upstream-audit",
|
||
workspaceRoot: "/vol1/docker/kt-codex/workspace/KT",
|
||
});
|
||
if (!codexCommand.includes("--ask-for-approval never")) {
|
||
throw new Error("NapCat Codex runner approval policy self-check failed");
|
||
}
|
||
if (!codexCommand.includes("codex --ask-for-approval never exec")) {
|
||
throw new Error("NapCat Codex runner top-level approval order self-check failed");
|
||
}
|
||
if (codexCommand.includes("codex exec") || codexCommand.includes("$(cat")) {
|
||
throw new Error("NapCat Codex runner stdin safety self-check failed");
|
||
}
|
||
for (const expected of [
|
||
"CODEX_HOME='/vol1/docker/kt-codex/home/.codex'",
|
||
"--cd '/vol1/docker/kt-codex/workspace/KT'",
|
||
"--profile-v2 automation",
|
||
"--sandbox workspace-write",
|
||
"--json",
|
||
"--ephemeral",
|
||
"--output-schema '/vol1/docker/kt-codex/workspace/KT/mcp/ktWorkflow/prompts/napcat/schemas/upstream-audit.schema.json'",
|
||
"--output-last-message '/vol1/docker/kt-codex/artifacts/smoke/codex-upstream-audit.md'",
|
||
"- < '/vol1/docker/kt-codex/artifacts/smoke/context-packet.md'",
|
||
]) {
|
||
if (!codexCommand.includes(expected)) {
|
||
throw new Error(`NapCat Codex runner command missing: ${expected}`);
|
||
}
|
||
}
|
||
if (codexCommand.includes("git push") || codexCommand.includes("kubectl")) {
|
||
throw new Error("NapCat Codex runner mutation guard self-check failed");
|
||
}
|
||
const codexExecutionArtifactRoot = resolveInsideRoot(
|
||
".kt-workspace/test-artifacts/napcat-codex-runner-self-test",
|
||
);
|
||
const codexInvalidSchemaAudit = buildNapcatUpstreamAudit({
|
||
artifactRoot: codexExecutionArtifactRoot,
|
||
codexExecutor: () => ({
|
||
ok: true,
|
||
stderr: "schema warning",
|
||
stdout: "{\"classification\":\"surprising\"}\n",
|
||
}),
|
||
dryMergeConflict: false,
|
||
execute: true,
|
||
forkPatchFiles: [],
|
||
upstreamChangedFiles: [],
|
||
useCodex: true,
|
||
});
|
||
if (
|
||
codexInvalidSchemaAudit.classification !== "blocked" ||
|
||
!codexInvalidSchemaAudit.reasonCodes.includes("CODEX_SCHEMA_INVALID")
|
||
) {
|
||
throw new Error("NapCat Codex invalid schema fallback self-check failed");
|
||
}
|
||
if (
|
||
!existsSync(
|
||
path.join(codexExecutionArtifactRoot, "codex-upstream-audit.jsonl"),
|
||
) ||
|
||
!existsSync(path.join(codexExecutionArtifactRoot, "context-packet.md")) ||
|
||
readFileSync(
|
||
path.join(codexExecutionArtifactRoot, "codex-upstream-audit.stderr.log"),
|
||
"utf8",
|
||
) !== "schema warning"
|
||
) {
|
||
throw new Error("NapCat Codex artifact capture self-check failed");
|
||
}
|
||
const codexItemTextArtifactRoot = resolveInsideRoot(
|
||
".kt-workspace/test-artifacts/napcat-codex-runner-success-self-test",
|
||
);
|
||
const codexItemTextAudit = buildNapcatUpstreamAudit({
|
||
artifactRoot: codexItemTextArtifactRoot,
|
||
codexExecutor: () => ({
|
||
ok: true,
|
||
stderr: "",
|
||
stdout: `${JSON.stringify({
|
||
item: {
|
||
text: JSON.stringify({
|
||
classification: "safe-candidate",
|
||
reasonCodes: ["NO_UPSTREAM_CHANGE"],
|
||
recommendedAction: "create-candidate-branch",
|
||
summary: "ok",
|
||
}),
|
||
type: "agent_message",
|
||
},
|
||
type: "item.completed",
|
||
})}\n`,
|
||
}),
|
||
dryMergeConflict: false,
|
||
execute: true,
|
||
forkPatchFiles: [],
|
||
upstreamChangedFiles: [],
|
||
useCodex: true,
|
||
});
|
||
if (
|
||
codexItemTextAudit.classification !== "safe-candidate" ||
|
||
!codexItemTextAudit.reasonCodes.includes("NO_UPSTREAM_CHANGE") ||
|
||
codexItemTextAudit.recommendedAction !== "create-candidate-branch"
|
||
) {
|
||
throw new Error("NapCat Codex item.text JSONL success self-check failed");
|
||
}
|
||
const codexThrowArtifactRoot = resolveInsideRoot(
|
||
".kt-workspace/test-artifacts/napcat-codex-runner-throw-self-test",
|
||
);
|
||
const codexThrowAudit = buildNapcatUpstreamAudit({
|
||
artifactRoot: codexThrowArtifactRoot,
|
||
codexExecutor: () => {
|
||
throw new Error("codex timed out");
|
||
},
|
||
dryMergeConflict: false,
|
||
execute: true,
|
||
forkPatchFiles: [],
|
||
upstreamChangedFiles: [],
|
||
useCodex: true,
|
||
});
|
||
if (
|
||
codexThrowAudit.classification !== "blocked" ||
|
||
!readFileSync(
|
||
path.join(codexThrowArtifactRoot, "codex-upstream-audit.stderr.log"),
|
||
"utf8",
|
||
).includes("codex timed out")
|
||
) {
|
||
throw new Error("NapCat Codex thrown executor stderr self-check failed");
|
||
}
|
||
const unsafeArtifactRootPath = path.resolve("/tmp/not-kt");
|
||
const unsafeArtifactRootExisted = existsSync(unsafeArtifactRootPath);
|
||
let unsafeArtifactRootRejected = false;
|
||
let unsafeArtifactExecutorCalled = false;
|
||
try {
|
||
buildNapcatUpstreamAudit({
|
||
artifactRoot: "/tmp/not-kt",
|
||
codexExecutor: () => {
|
||
unsafeArtifactExecutorCalled = true;
|
||
return { ok: true, stderr: "", stdout: "{}\n" };
|
||
},
|
||
dryMergeConflict: false,
|
||
execute: true,
|
||
forkPatchFiles: [],
|
||
upstreamChangedFiles: [],
|
||
useCodex: true,
|
||
});
|
||
} catch (error) {
|
||
unsafeArtifactRootRejected = String(error).includes(
|
||
"Unsafe NapCat Codex artifactRoot",
|
||
);
|
||
} finally {
|
||
if (!unsafeArtifactRootExisted && existsSync(unsafeArtifactRootPath)) {
|
||
rmSync(unsafeArtifactRootPath, { force: true, recursive: true });
|
||
}
|
||
}
|
||
if (!unsafeArtifactRootRejected || unsafeArtifactExecutorCalled) {
|
||
throw new Error("NapCat Codex unsafe artifact root self-check failed");
|
||
}
|
||
const dryRunAuditCommands: string[] = [];
|
||
for (const command of dryRunAudit.commands) {
|
||
if (!command.readOnly) {
|
||
throw new Error("NapCat upstream audit dry-run read-only flag failed");
|
||
}
|
||
dryRunAuditCommands.push(command.command);
|
||
}
|
||
const dryRunAuditCommandText = dryRunAuditCommands.join("\n");
|
||
for (const expected of [
|
||
"git status --short --branch",
|
||
"git fetch --dry-run upstream --tags --prune",
|
||
"git fetch --dry-run origin --prune",
|
||
"git diff --name-only",
|
||
"git merge-tree",
|
||
]) {
|
||
if (!dryRunAuditCommandText.includes(expected)) {
|
||
throw new Error(
|
||
`NapCat upstream audit dry-run command missing: ${expected}`,
|
||
);
|
||
}
|
||
}
|
||
for (const command of dryRunAudit.commands) {
|
||
if (
|
||
command.readOnly &&
|
||
/^git\s+fetch(?:\s|$)/.test(command.command) &&
|
||
!/\s--dry-run(?:\s|$)/.test(command.command)
|
||
) {
|
||
throw new Error(
|
||
"NapCat upstream audit read-only fetch must use --dry-run",
|
||
);
|
||
}
|
||
}
|
||
for (const forbidden of [
|
||
{ label: "git merge", regex: /(^|\n)\s*git\s+merge(?:\s|$)/ },
|
||
{ label: "git push", regex: /(^|\n)\s*git\s+push(?:\s|$)/ },
|
||
{ label: "docker build", regex: /(^|\n)\s*docker\s+build(?:\s|$)/ },
|
||
{ label: "kubectl", regex: /(^|\n)\s*kubectl(?:\s|$)/ },
|
||
{ label: "deploy", regex: /(^|\n)\s*deploy(?:\s|$)/ },
|
||
]) {
|
||
if (forbidden.regex.test(dryRunAuditCommandText)) {
|
||
throw new Error(
|
||
`NapCat upstream audit dry-run command is not read-only: ${forbidden.label}`,
|
||
);
|
||
}
|
||
}
|
||
for (const invalidPathCase of [
|
||
{
|
||
forkPatchFiles: [],
|
||
name: "Windows absolute path",
|
||
upstreamChangedFiles: ["D:/repo/packages/napcat-core/login/runtime.ts"],
|
||
},
|
||
{
|
||
forkPatchFiles: [],
|
||
name: "Unix absolute path",
|
||
upstreamChangedFiles: ["/repo/packages/napcat-core/login/runtime.ts"],
|
||
},
|
||
{
|
||
forkPatchFiles: [],
|
||
name: "UNC path",
|
||
upstreamChangedFiles: [
|
||
"\\\\server\\share\\packages\\napcat-core\\login\\runtime.ts",
|
||
],
|
||
},
|
||
{
|
||
forkPatchFiles: [],
|
||
name: "Windows rooted path",
|
||
upstreamChangedFiles: [
|
||
"\\packages\\napcat-core\\login\\runtime.ts",
|
||
],
|
||
},
|
||
{
|
||
forkPatchFiles: [],
|
||
name: "Windows drive-relative path",
|
||
upstreamChangedFiles: ["C:packages/napcat-core/login/runtime.ts"],
|
||
},
|
||
{
|
||
forkPatchFiles: [],
|
||
name: "parent segment path",
|
||
upstreamChangedFiles: ["packages/../napcat-core/login/runtime.ts"],
|
||
},
|
||
{
|
||
forkPatchFiles: ["packages/../napcat-core/login/runtime.ts"],
|
||
name: "fork patch parent segment path",
|
||
upstreamChangedFiles: [],
|
||
},
|
||
]) {
|
||
const invalidPathAudit = classifyNapcatUpstreamAudit({
|
||
dryMergeConflict: false,
|
||
forkPatchFiles: invalidPathCase.forkPatchFiles,
|
||
upstreamChangedFiles: invalidPathCase.upstreamChangedFiles,
|
||
});
|
||
if (
|
||
invalidPathAudit.classification !== "blocked" ||
|
||
invalidPathAudit.recommendedAction !== "block" ||
|
||
!invalidPathAudit.reasonCodes.includes("METADATA_UNAVAILABLE")
|
||
) {
|
||
throw new Error(
|
||
`NapCat invalid audit path self-check failed: ${invalidPathCase.name}`,
|
||
);
|
||
}
|
||
}
|
||
for (const promptName of napcatAutomationPromptNames) {
|
||
const promptText = loadNapcatAutomationPrompt(promptName);
|
||
if (!promptText.includes("Do not edit files")) {
|
||
throw new Error(`NapCat prompt safety contract missing: ${promptName}`);
|
||
}
|
||
}
|
||
let traversalPromptRejected = false;
|
||
try {
|
||
loadNapcatAutomationPrompt("../../README" as NapcatAutomationPromptName);
|
||
} catch (error) {
|
||
traversalPromptRejected = String(error).includes(
|
||
"Unsupported NapCat automation prompt",
|
||
);
|
||
}
|
||
if (!traversalPromptRejected) {
|
||
throw new Error("NapCat prompt traversal self-check failed");
|
||
}
|
||
|
||
const reviewClassifier = {
|
||
localizedSecretLabelAllowed: isBenignCredentialReviewValue("密钥"),
|
||
realTokenRejected: !isBenignCredentialReviewValue(
|
||
"eqc2Sbp8ufP2AcS6I2YReLqxnu3XJKC6D7C6PUcx",
|
||
),
|
||
testTokenAllowed: isBenignCredentialReviewValue("token-test"),
|
||
};
|
||
if (
|
||
!reviewClassifier.localizedSecretLabelAllowed ||
|
||
!reviewClassifier.realTokenRejected ||
|
||
!reviewClassifier.testTokenAllowed
|
||
) {
|
||
throw new Error("review credential classifier self-check failed");
|
||
}
|
||
|
||
const taskRecordFindings = findTaskRecordGovernanceFindings([
|
||
"## 最近记录",
|
||
"### 2026-06-10:长记录污染",
|
||
"- 范围:root",
|
||
"- 关键词:TASKS 治理",
|
||
"- 页面测试用例:配置类任务,无页面测试。",
|
||
"- 验证:global-review",
|
||
"## 历史索引",
|
||
]);
|
||
if (
|
||
!taskRecordFindings.some(
|
||
(item) => item.category === "tasks-record-extra-field",
|
||
)
|
||
) {
|
||
throw new Error("TASKS record governance self-check failed");
|
||
}
|
||
|
||
const napcatImageFindings = findNapcatImageGovernanceFindings(
|
||
["QQBOT_NAPCAT_IMAGE=mlikiowa/napcat-docker:latest"],
|
||
{
|
||
file: ".env.example",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
);
|
||
if (
|
||
!napcatImageFindings.some((item) => item.category === "napcat-latest-image")
|
||
) {
|
||
throw new Error("NapCat image governance self-check failed");
|
||
}
|
||
|
||
const qqbotCommandServiceImportFindings =
|
||
findQqbotCommandServiceTestImportFindings(
|
||
[
|
||
"import { QqbotCommandService } from '@/qqbot/command/qqbot-command.service';",
|
||
],
|
||
{
|
||
file: "test/qqbot/command/qqbot-command-cooldown.service.spec.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
);
|
||
if (
|
||
!qqbotCommandServiceImportFindings.some(
|
||
(item) => item.category === "qqbot-command-service-heavy-test-import",
|
||
)
|
||
) {
|
||
throw new Error(
|
||
"QQBot command service test import governance self-check failed",
|
||
);
|
||
}
|
||
|
||
const qqbotPluginSmokeImportFindings = findQqbotPluginSmokeImportFindings(
|
||
[
|
||
"import { QqbotBangDreamPluginService } from '../../../../src/modules/qqbot/plugins/bangDream/qqbot-bangdream.plugin';",
|
||
],
|
||
{
|
||
file: "test/modules/qqbot/plugins/plugin-controller-http-smoke.spec.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
);
|
||
if (
|
||
!qqbotPluginSmokeImportFindings.some(
|
||
(item) => item.category === "qqbot-plugin-smoke-heavy-import",
|
||
)
|
||
) {
|
||
throw new Error("QQBot plugin smoke import governance self-check failed");
|
||
}
|
||
|
||
const qqbotPluginSmokeMockedFindings = findQqbotPluginSmokeImportFindings(
|
||
[
|
||
"jest.mock(",
|
||
" '@/modules/qqbot/plugins/bangDream/qqbot-bangdream.plugin',",
|
||
" () => ({}),",
|
||
");",
|
||
"import { QqbotBangDreamPluginService } from '../../../../src/modules/qqbot/plugins/bangDream/qqbot-bangdream.plugin';",
|
||
],
|
||
{
|
||
file: "test/modules/qqbot/plugins/plugin-controller-http-smoke.spec.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
);
|
||
if (qqbotPluginSmokeMockedFindings.length > 0) {
|
||
throw new Error(
|
||
"QQBot plugin smoke import mocked self-check produced false positive",
|
||
);
|
||
}
|
||
|
||
const qqbotPluginProductionPathFindings =
|
||
findQqbotPluginPackageProductionPathFindings(
|
||
[
|
||
"const DEFAULT_BUILTIN_PACKAGE_ROOT_SEGMENTS = [",
|
||
" ['src', 'modules', 'qqbot', 'plugins'],",
|
||
"];",
|
||
"private resolveEntryFile(entryFile: string) {",
|
||
" return entryFile;",
|
||
"}",
|
||
],
|
||
{
|
||
file: "src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
);
|
||
if (
|
||
!qqbotPluginProductionPathFindings.some(
|
||
(item) =>
|
||
item.category === "qqbot-plugin-production-dist-root-missing",
|
||
) ||
|
||
!qqbotPluginProductionPathFindings.some(
|
||
(item) =>
|
||
item.category === "qqbot-plugin-compiled-entry-fallback-missing",
|
||
)
|
||
) {
|
||
throw new Error("QQBot plugin production path governance self-check failed");
|
||
}
|
||
|
||
const qqbotPluginProductionPathValidFindings =
|
||
findQqbotPluginPackageProductionPathFindings(
|
||
[
|
||
"const DEFAULT_BUILTIN_PACKAGE_ROOT_SEGMENTS = [",
|
||
" ['src', 'modules', 'qqbot', 'plugins'],",
|
||
" ['dist', 'modules', 'qqbot', 'plugins'],",
|
||
"];",
|
||
"private resolveCompiledEntryFile(entryFile: string): string {",
|
||
" if (entryFile.endsWith('.ts')) {",
|
||
" return `${entryFile.slice(0, -3)}.js`;",
|
||
" }",
|
||
" return entryFile;",
|
||
"}",
|
||
],
|
||
{
|
||
file: "src/modules/qqbot/plugin-platform/infrastructure/integration/package/plugin-package-path-policy.service.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
);
|
||
if (qqbotPluginProductionPathValidFindings.length > 0) {
|
||
throw new Error(
|
||
"QQBot plugin production path governance valid self-check false positive",
|
||
);
|
||
}
|
||
|
||
const qqbotStatusBoundaryFindings = [
|
||
...findQqbotStatusBoundaryFindings(
|
||
[
|
||
"async markOnline(",
|
||
" selfId: string,",
|
||
" clientRole: QqbotConnectionRole,",
|
||
" lastError: null | string = null,",
|
||
") {}",
|
||
"if (account.connectStatus === 'online') return 'online';",
|
||
"return this.toTime(account.lastHeartbeatAt) > checkedAt;",
|
||
],
|
||
{
|
||
file: "src/qqbot/account/qqbot-account.service.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
),
|
||
...findQqbotStatusBoundaryFindings(
|
||
[
|
||
"ws.on('close', async () => {",
|
||
" this.connections.delete(key);",
|
||
" await this.accountService.markOffline(context.selfId);",
|
||
"});",
|
||
],
|
||
{
|
||
file: "src/qqbot/connection/qqbot-reverse-ws.service.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
),
|
||
];
|
||
for (const category of [
|
||
"qqbot-mark-online-clears-login-error",
|
||
"qqbot-connect-status-drives-login-status",
|
||
"qqbot-heartbeat-bypasses-login-check",
|
||
"qqbot-stale-ws-close-marks-offline",
|
||
]) {
|
||
if (!qqbotStatusBoundaryFindings.some((item) => item.category === category)) {
|
||
throw new Error(`QQBot status boundary self-check failed: ${category}`);
|
||
}
|
||
}
|
||
|
||
const qqbotCaptchaFlowFindings = findQqbotNapcatCaptchaFlowFindings(
|
||
[
|
||
"private async resolvePasswordCaptchaUrl(container, loginStatus, sinceMs) {",
|
||
" return await this.containerService.detectRuntimeCaptchaUrl(container, sinceMs);",
|
||
" return await this.waitForPasswordCaptchaUrl(container, sinceMs);",
|
||
"}",
|
||
"private async waitForPasswordCaptchaUrl(container, sinceMs) {",
|
||
" await this.toolsService.sleep(this.getLoginPollIntervalMs());",
|
||
" return await this.detectPasswordCaptchaUrl(container, sinceMs, true);",
|
||
"}",
|
||
"private async waitForPasswordLoginStatus(container) {",
|
||
" latestStatus = await this.getLoginStatus(container, true);",
|
||
"}",
|
||
"async status(sessionId) {",
|
||
" if (session.captchaUrl) {",
|
||
" return this.failCaptchaLogin(session, container, status.loginError);",
|
||
" }",
|
||
"}",
|
||
"if (captchaResult?.needNewDevice && captchaResult.jumpUrl) {",
|
||
" return this.keepSessionPending(session, '请在 NapCat WebUI 继续完成', true);",
|
||
"}",
|
||
"private async tryPasswordRelogin(session, container) {",
|
||
" await this.containerService.ensureRuntimeLoginEnv(container, { loginPassword: 'x' });",
|
||
" session.lastRestartedAt = Date.now();",
|
||
" loginStatus = await this.waitForPasswordLoginStatus(container, session.lastRestartedAt);",
|
||
" await this.resolvePasswordCaptchaUrl(container, loginStatus, session.lastRestartedAt);",
|
||
"}",
|
||
],
|
||
{
|
||
file: "src/qqbot/account/qqbot-napcat-login.service.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
);
|
||
for (const category of [
|
||
"qqbot-napcat-captcha-error-throws-before-pending",
|
||
"qqbot-napcat-captcha-log-single-read",
|
||
"qqbot-napcat-captcha-status-before-log-url",
|
||
"qqbot-napcat-captcha-status-clears-pending",
|
||
"qqbot-napcat-captcha-log-window-late",
|
||
"qqbot-napcat-new-device-drops-pending",
|
||
]) {
|
||
if (!qqbotCaptchaFlowFindings.some((item) => item.category === category)) {
|
||
throw new Error(`QQBot captcha flow self-check failed: ${category}`);
|
||
}
|
||
}
|
||
const qqbotCaptchaFlowValidFindings = findQqbotNapcatCaptchaFlowFindings(
|
||
[
|
||
"private async resolvePasswordCaptchaUrl(container, loginStatus, sinceMs) {",
|
||
" await this.detectPasswordCaptchaUrl(container, sinceMs, false);",
|
||
" await this.waitForPasswordCaptchaUrl(container, sinceMs);",
|
||
"}",
|
||
"private async waitForPasswordLoginStatus(container, sinceMs) {",
|
||
" if (this.toolsService.isNapcatCaptchaRequiredMessage(errorMessage)) {",
|
||
" await this.detectPasswordCaptchaUrl(container, sinceMs);",
|
||
" }",
|
||
"}",
|
||
"async status(sessionId) {",
|
||
" if (session.captchaUrl) {",
|
||
" if (this.isPasswordCaptchaStillRequired(status)) {",
|
||
" return this.keepPasswordCaptchaPending(session, session.captchaUrl, status.loginError);",
|
||
" }",
|
||
" }",
|
||
"}",
|
||
"if (captchaResult?.needNewDevice && captchaResult.jumpUrl) {",
|
||
" return this.startNewDeviceVerification(session, container, captchaResult);",
|
||
"}",
|
||
"private async startNewDeviceVerification(session, container, captchaResult) {",
|
||
" session.deviceVerifyUrl = captchaResult.jumpUrl;",
|
||
" session.captchaUrl = undefined;",
|
||
" session.newDeviceQrcode = await this.client.GetNewDeviceQRCode();",
|
||
" session.newDeviceStatus = 'qr-pending';",
|
||
"}",
|
||
"private async pollNewDeviceVerification(session) {",
|
||
" await this.client.PollNewDeviceQR();",
|
||
" await this.client.NewDeviceLogin();",
|
||
"}",
|
||
"private async detectPasswordCaptchaUrl(container, sinceMs, allowTailFallback = true) {",
|
||
" await this.containerService.detectRuntimeCaptchaUrl(container, sinceMs);",
|
||
" await this.containerService.detectRuntimeCaptchaUrl(container);",
|
||
"}",
|
||
"private async waitForPasswordCaptchaUrl(container, sinceMs) {",
|
||
" const attempts = 5;",
|
||
" for (let index = 0; index < attempts; index += 1) {",
|
||
" await this.toolsService.sleep(this.getLoginPollIntervalMs());",
|
||
" await this.detectPasswordCaptchaUrl(container, sinceMs, true);",
|
||
" }",
|
||
"}",
|
||
"private async tryPasswordRelogin(session, container) {",
|
||
" const passwordLogSinceMs = Date.now();",
|
||
" await this.containerService.ensureRuntimeLoginEnv(container, { loginPassword: 'x' });",
|
||
" loginStatus = await this.waitForPasswordLoginStatus(container, passwordLogSinceMs);",
|
||
" await this.resolvePasswordCaptchaUrl(container, loginStatus, passwordLogSinceMs);",
|
||
"}",
|
||
],
|
||
{
|
||
file: "src/qqbot/account/qqbot-napcat-login.service.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
);
|
||
if (
|
||
qqbotCaptchaFlowValidFindings.some(
|
||
(item) =>
|
||
item.category === "qqbot-napcat-captcha-log-window-late" ||
|
||
item.category === "qqbot-napcat-captcha-status-before-log-url" ||
|
||
item.category === "qqbot-napcat-captcha-status-clears-pending" ||
|
||
item.category === "qqbot-napcat-new-device-drops-pending",
|
||
)
|
||
) {
|
||
throw new Error("QQBot captcha flow valid self-check produced false positive");
|
||
}
|
||
const qqbotCaptchaLogTimeoutFindings = findQqbotNapcatCaptchaFlowFindings(
|
||
[
|
||
"async detectRuntimeCaptchaUrl(runtime, sinceMs) {",
|
||
" const result = await this.runProcess(",
|
||
" 'ssh',",
|
||
" [...this.getSshArgs(), 'sh -s'],",
|
||
" script,",
|
||
" undefined,",
|
||
" this.getRuntimeCheckTimeoutMs(),",
|
||
" );",
|
||
"}",
|
||
],
|
||
{
|
||
file: "src/qqbot/napcat/qqbot-napcat-container.service.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
);
|
||
if (
|
||
!qqbotCaptchaLogTimeoutFindings.some(
|
||
(item) => item.category === "qqbot-napcat-captcha-log-timeout-too-short",
|
||
)
|
||
) {
|
||
throw new Error("QQBot captcha log timeout self-check failed");
|
||
}
|
||
const qqbotCaptchaLogTimeoutValidFindings =
|
||
findQqbotNapcatCaptchaFlowFindings(
|
||
[
|
||
"async detectRuntimeCaptchaUrl(runtime, sinceMs) {",
|
||
" const result = await this.runProcess(",
|
||
" 'ssh',",
|
||
" [...this.getSshArgs(), 'sh -s'],",
|
||
" script,",
|
||
" undefined,",
|
||
" this.getCaptchaLogReadTimeoutMs(),",
|
||
" );",
|
||
"}",
|
||
],
|
||
{
|
||
file: "src/qqbot/napcat/qqbot-napcat-container.service.ts",
|
||
project: "Node/kt-template-online-api",
|
||
},
|
||
);
|
||
if (qqbotCaptchaLogTimeoutValidFindings.length) {
|
||
throw new Error("QQBot captcha log timeout valid self-check false positive");
|
||
}
|
||
|
||
const reviewCliParser = {
|
||
dashedAll: parseGlobalReviewCliArgs([
|
||
"node",
|
||
"server",
|
||
"--global-review",
|
||
"--content-scan-all",
|
||
]).contentScanMode,
|
||
keyValueAll: parseGlobalReviewCliArgs([
|
||
"node",
|
||
"server",
|
||
"--global-review",
|
||
"--contentScanMode=all",
|
||
]).contentScanMode,
|
||
splitAll: parseGlobalReviewCliArgs([
|
||
"node",
|
||
"server",
|
||
"--global-review",
|
||
"--content-scan-mode",
|
||
"all",
|
||
]).contentScanMode,
|
||
};
|
||
if (
|
||
reviewCliParser.dashedAll !== "all" ||
|
||
reviewCliParser.keyValueAll !== "all" ||
|
||
reviewCliParser.splitAll !== "all"
|
||
) {
|
||
throw new Error(
|
||
"global-review CLI content scan mode parser self-check failed",
|
||
);
|
||
}
|
||
|
||
const closeoutCliParser = parseWorkstreamCloseoutCliArgs([
|
||
"node",
|
||
"server",
|
||
"--workstream-closeout",
|
||
"--review",
|
||
"global-review findings=0",
|
||
"--superpowers-review",
|
||
"Superpowers reviewer completed",
|
||
]);
|
||
if (
|
||
closeoutCliParser.reviewEvidence?.[0] !== "global-review findings=0" ||
|
||
closeoutCliParser.superpowersReviewEvidence?.[0] !==
|
||
"Superpowers reviewer completed"
|
||
) {
|
||
throw new Error("workstream closeout review CLI parser self-check failed");
|
||
}
|
||
|
||
const deployObservationInput = parseDeployObservationCliArgs([
|
||
"node",
|
||
"server",
|
||
"--deploy-observation",
|
||
"--project",
|
||
"api",
|
||
"--job",
|
||
"kt-template-online-api",
|
||
"--build",
|
||
"132",
|
||
"--commit",
|
||
"abc1234",
|
||
"--image-tag",
|
||
"abc1234",
|
||
"--namespace",
|
||
"kt-prod",
|
||
"--deployment",
|
||
"kt-template-online-api",
|
||
"--container",
|
||
"api",
|
||
"--health-url",
|
||
"http://127.0.0.1:48085/health/runtime",
|
||
"--smoke",
|
||
"curl -fsS --max-time 8 http://127.0.0.1:48085/health/runtime",
|
||
]);
|
||
if (
|
||
deployObservationInput.project !== "api" ||
|
||
deployObservationInput.jobName !== "kt-template-online-api" ||
|
||
deployObservationInput.buildNumber !== "132" ||
|
||
deployObservationInput.expectedCommit !== "abc1234" ||
|
||
deployObservationInput.imageTag !== "abc1234" ||
|
||
deployObservationInput.namespace !== "kt-prod" ||
|
||
deployObservationInput.deployment !== "kt-template-online-api" ||
|
||
deployObservationInput.container !== "api" ||
|
||
deployObservationInput.healthUrl !==
|
||
"http://127.0.0.1:48085/health/runtime" ||
|
||
deployObservationInput.smoke !==
|
||
"curl -fsS --max-time 8 http://127.0.0.1:48085/health/runtime" ||
|
||
deployObservationInput.execute !== false
|
||
) {
|
||
throw new Error("deploy observation CLI parser self-check failed");
|
||
}
|
||
|
||
const buildDeployObservationTestSections = (overrides: {
|
||
desiredReplicas?: number;
|
||
healthStatus?: string;
|
||
jenkinsBuildXml?: string;
|
||
logFinishedStatus?: string;
|
||
logCommit?: string;
|
||
omitFinishedStatus?: boolean;
|
||
omitSmoke?: boolean;
|
||
readyReplicas?: number;
|
||
smokeExitCode?: number;
|
||
containerName?: string;
|
||
podContainerName?: string;
|
||
updatedReplicas?: number;
|
||
} = {}) =>
|
||
buildDeployObservationSectionMap([
|
||
"__KT_SECTION:jenkins_meta__",
|
||
"buildNumber=132",
|
||
"result=SUCCESS",
|
||
"__KT_SECTION:jenkins_log_tail__",
|
||
overrides.logCommit === ""
|
||
? `Finished: ${overrides.logFinishedStatus ?? "SUCCESS"}`
|
||
: `Checking out Revision ${overrides.logCommit ?? "abc1234"}`,
|
||
...(overrides.omitFinishedStatus
|
||
? []
|
||
: [`Finished: ${overrides.logFinishedStatus ?? "SUCCESS"}`]),
|
||
"__KT_SECTION:jenkins_build_xml__",
|
||
overrides.jenkinsBuildXml ?? "",
|
||
"__KT_SECTION:deployment_json__",
|
||
JSON.stringify({
|
||
metadata: { generation: 12 },
|
||
spec: {
|
||
replicas: overrides.desiredReplicas ?? 1,
|
||
template: {
|
||
spec: {
|
||
containers: [
|
||
{
|
||
image:
|
||
"k3d-kt-registry.localhost:5000/kt-template-online-api:abc1234",
|
||
name: overrides.containerName ?? "api",
|
||
},
|
||
],
|
||
},
|
||
},
|
||
},
|
||
status: {
|
||
observedGeneration: 12,
|
||
readyReplicas: overrides.readyReplicas ?? 1,
|
||
updatedReplicas: overrides.updatedReplicas ?? 1,
|
||
},
|
||
}),
|
||
"__KT_SECTION:pods_json__",
|
||
JSON.stringify({
|
||
items: [
|
||
{
|
||
metadata: {
|
||
name: "kt-template-online-api-abc",
|
||
},
|
||
spec: {
|
||
containers: [
|
||
{
|
||
image:
|
||
"k3d-kt-registry.localhost:5000/kt-template-online-api:abc1234",
|
||
name: overrides.podContainerName ?? "api",
|
||
},
|
||
],
|
||
},
|
||
status: {
|
||
containerStatuses: [
|
||
{
|
||
image:
|
||
"k3d-kt-registry.localhost:5000/kt-template-online-api:abc1234",
|
||
name: overrides.podContainerName ?? "api",
|
||
ready: true,
|
||
restartCount: 0,
|
||
},
|
||
],
|
||
phase: "Running",
|
||
startTime: "2026-06-14T00:00:00Z",
|
||
},
|
||
},
|
||
],
|
||
}),
|
||
"__KT_SECTION:health_json__",
|
||
JSON.stringify({
|
||
checkedAt: "2026-06-14T00:00:01.000Z",
|
||
checks: [
|
||
{
|
||
critical: true,
|
||
message: "NestJS process answered runtime health request",
|
||
name: "process",
|
||
status: "live",
|
||
},
|
||
],
|
||
service: "kt-template-online-api",
|
||
status: overrides.healthStatus ?? "ready",
|
||
}),
|
||
"__KT_SECTION:smoke_text__",
|
||
overrides.omitSmoke
|
||
? "no task smoke command provided"
|
||
: '{"service":"kt-template-online-api","status":"ready"}',
|
||
"__KT_SECTION:smoke_status__",
|
||
overrides.omitSmoke ? "exitCode=" : `exitCode=${overrides.smokeExitCode ?? 0}`,
|
||
].join("\n"));
|
||
const deployObservationSections = buildDeployObservationTestSections();
|
||
const deployObservationEvidence = normalizeDeployObservationEvidence({
|
||
checkedAt: new Date("2026-06-14T00:00:02.000Z"),
|
||
input: deployObservationInput,
|
||
sectionMap: deployObservationSections,
|
||
});
|
||
const deployObservationSmoke = deployObservationEvidence.details.smoke as {
|
||
exitCode?: number | null;
|
||
success?: boolean | null;
|
||
};
|
||
if (
|
||
deployObservationEvidence.status !== "passed" ||
|
||
deployObservationEvidence.details.deployment.image !==
|
||
"k3d-kt-registry.localhost:5000/kt-template-online-api:abc1234" ||
|
||
deployObservationEvidence.details.deployment.observedGeneration !== 12 ||
|
||
deployObservationEvidence.details.pod.name !==
|
||
"kt-template-online-api-abc" ||
|
||
deployObservationEvidence.details.pod.restartCount !== 0 ||
|
||
deployObservationEvidence.details.runtimeHealth.status !== "ready" ||
|
||
deployObservationSmoke.exitCode !== 0 ||
|
||
deployObservationSmoke.success !== true ||
|
||
!deployObservationEvidence.assertions.every((item) => item.passed)
|
||
) {
|
||
throw new Error("deploy observation evidence normalization self-check failed");
|
||
}
|
||
|
||
const deployObservationDegradedHealthEvidence =
|
||
normalizeDeployObservationEvidence({
|
||
checkedAt: new Date("2026-06-14T00:00:02.000Z"),
|
||
input: deployObservationInput,
|
||
sectionMap: buildDeployObservationTestSections({
|
||
healthStatus: "degraded",
|
||
}),
|
||
});
|
||
if (
|
||
deployObservationDegradedHealthEvidence.status !== "passed" ||
|
||
!deployObservationDegradedHealthEvidence.assertions.every(
|
||
(item) => item.passed,
|
||
)
|
||
) {
|
||
throw new Error("deploy observation degraded health self-check failed");
|
||
}
|
||
|
||
const deployObservationBuildXmlCommitEvidence =
|
||
normalizeDeployObservationEvidence({
|
||
checkedAt: new Date("2026-06-14T00:00:02.000Z"),
|
||
input: deployObservationInput,
|
||
sectionMap: buildDeployObservationTestSections({
|
||
jenkinsBuildXml: [
|
||
"<jenkins.scm.api.SCMRevisionAction>",
|
||
"<revision>",
|
||
"<hash>abc1234</hash>",
|
||
"</revision>",
|
||
"</jenkins.scm.api.SCMRevisionAction>",
|
||
].join("\n"),
|
||
logCommit: "",
|
||
}),
|
||
});
|
||
if (
|
||
deployObservationBuildXmlCommitEvidence.status !== "passed" ||
|
||
deployObservationBuildXmlCommitEvidence.details.jenkins.commitMatched !== true
|
||
) {
|
||
throw new Error("deploy observation build.xml commit self-check failed");
|
||
}
|
||
|
||
const deployObservationLogFailureBeatsXmlEvidence =
|
||
normalizeDeployObservationEvidence({
|
||
checkedAt: new Date("2026-06-14T00:00:02.000Z"),
|
||
input: deployObservationInput,
|
||
sectionMap: buildDeployObservationTestSections({
|
||
logFinishedStatus: "FAILURE",
|
||
}),
|
||
});
|
||
if (
|
||
deployObservationLogFailureBeatsXmlEvidence.status === "passed" ||
|
||
deployObservationLogFailureBeatsXmlEvidence.details.jenkins
|
||
.finishedStatus !== "FAILURE"
|
||
) {
|
||
throw new Error("deploy observation log failure precedence self-check failed");
|
||
}
|
||
|
||
const deployObservationRunningLogWithoutFinalStatusEvidence =
|
||
normalizeDeployObservationEvidence({
|
||
checkedAt: new Date("2026-06-14T00:00:02.000Z"),
|
||
input: deployObservationInput,
|
||
sectionMap: buildDeployObservationTestSections({
|
||
omitFinishedStatus: true,
|
||
}),
|
||
});
|
||
if (
|
||
deployObservationRunningLogWithoutFinalStatusEvidence.status === "passed" ||
|
||
deployObservationRunningLogWithoutFinalStatusEvidence.details.jenkins
|
||
.finishedStatus !== null
|
||
) {
|
||
throw new Error("deploy observation running log status self-check failed");
|
||
}
|
||
|
||
const deployObservationFailedSmokeEvidence =
|
||
normalizeDeployObservationEvidence({
|
||
checkedAt: new Date("2026-06-14T00:00:02.000Z"),
|
||
input: deployObservationInput,
|
||
sectionMap: buildDeployObservationTestSections({
|
||
smokeExitCode: 7,
|
||
}),
|
||
});
|
||
const deployObservationFailedSmoke =
|
||
deployObservationFailedSmokeEvidence.details.smoke as {
|
||
exitCode?: number | null;
|
||
success?: boolean | null;
|
||
};
|
||
if (
|
||
deployObservationFailedSmokeEvidence.status === "passed" ||
|
||
deployObservationFailedSmoke.exitCode !== 7 ||
|
||
deployObservationFailedSmoke.success !== false
|
||
) {
|
||
throw new Error("deploy observation failed smoke self-check failed");
|
||
}
|
||
|
||
const deployObservationReplicaLagEvidence =
|
||
normalizeDeployObservationEvidence({
|
||
checkedAt: new Date("2026-06-14T00:00:02.000Z"),
|
||
input: deployObservationInput,
|
||
sectionMap: buildDeployObservationTestSections({
|
||
desiredReplicas: 2,
|
||
readyReplicas: 1,
|
||
updatedReplicas: 2,
|
||
}),
|
||
});
|
||
if (deployObservationReplicaLagEvidence.status === "passed") {
|
||
throw new Error("deploy observation replica lag self-check failed");
|
||
}
|
||
|
||
const deployObservationDesiredReplicaLagEvidence =
|
||
normalizeDeployObservationEvidence({
|
||
checkedAt: new Date("2026-06-14T00:00:02.000Z"),
|
||
input: deployObservationInput,
|
||
sectionMap: buildDeployObservationTestSections({
|
||
desiredReplicas: 2,
|
||
readyReplicas: 1,
|
||
updatedReplicas: 1,
|
||
}),
|
||
});
|
||
if (deployObservationDesiredReplicaLagEvidence.status === "passed") {
|
||
throw new Error("deploy observation desired replica lag self-check failed");
|
||
}
|
||
|
||
const deployObservationWrongContainerEvidence =
|
||
normalizeDeployObservationEvidence({
|
||
checkedAt: new Date("2026-06-14T00:00:02.000Z"),
|
||
input: deployObservationInput,
|
||
sectionMap: buildDeployObservationTestSections({
|
||
containerName: "sidecar",
|
||
podContainerName: "sidecar",
|
||
}),
|
||
});
|
||
if (deployObservationWrongContainerEvidence.status === "passed") {
|
||
throw new Error("deploy observation wrong container self-check failed");
|
||
}
|
||
|
||
const deployObservationNoSmokeEvidence = normalizeDeployObservationEvidence({
|
||
checkedAt: new Date("2026-06-14T00:00:02.000Z"),
|
||
input: { ...deployObservationInput, smoke: undefined },
|
||
sectionMap: buildDeployObservationTestSections({
|
||
omitSmoke: true,
|
||
}),
|
||
});
|
||
const deployObservationNoSmoke = deployObservationNoSmokeEvidence.details
|
||
.smoke as {
|
||
exitCode?: number | null;
|
||
success?: boolean | null;
|
||
};
|
||
if (
|
||
deployObservationNoSmokeEvidence.status === "passed" ||
|
||
deployObservationNoSmoke.exitCode !== null ||
|
||
deployObservationNoSmoke.success !== false
|
||
) {
|
||
throw new Error("deploy observation no-smoke self-check failed");
|
||
}
|
||
|
||
const deployObservationDryRun = await buildDeployObservation({
|
||
deployment: "kt-template-online-api",
|
||
execute: false,
|
||
imageTag: "abc1234",
|
||
jobName: "kt-template-online-api",
|
||
namespace: "kt-prod",
|
||
project: "api",
|
||
smoke: "curl -fsS --max-time 8 http://127.0.0.1:48085/health/runtime",
|
||
});
|
||
const deployObservationCommandText = deployObservationDryRun.commands
|
||
.map((item) => item.command)
|
||
.join("\n");
|
||
if (
|
||
deployObservationDryRun.execute !== false ||
|
||
!deployObservationCommandText.includes("tr -d '\\015' | bash -s") ||
|
||
!deployObservationCommandText.includes("kubectl") ||
|
||
!deployObservationCommandText.includes(
|
||
"http://127.0.0.1:48085/health/runtime",
|
||
) ||
|
||
!deployObservationCommandText.includes("curl -fsS --max-time 8") ||
|
||
deployObservationDryRun.evidence
|
||
) {
|
||
throw new Error("deploy observation dry-run self-check failed");
|
||
}
|
||
for (const expected of [
|
||
"KUBECONFIG_ACTIVE",
|
||
"docker port k3d-kt-nas-serverlb 6443/tcp",
|
||
"/tmp/kt-deploy-observation-kubeconfig-$$.yaml",
|
||
"127.0.0.1:${K3D_API_PORT}",
|
||
'rm -f "$KUBECONFIG_TEMP"',
|
||
'kubectl --kubeconfig "$KUBECONFIG_ACTIVE"',
|
||
"section jenkins_build_xml",
|
||
]) {
|
||
if (!deployObservationCommandText.includes(expected)) {
|
||
throw new Error(`deploy observation kubeconfig self-check failed: ${expected}`);
|
||
}
|
||
}
|
||
if (
|
||
deployObservationCommandText.includes('sed -E "s#.*:([0-9]+)$#') ||
|
||
!deployObservationCommandText.includes("rev | cut -d: -f1 | rev")
|
||
) {
|
||
throw new Error("deploy observation docker port parser self-check failed");
|
||
}
|
||
|
||
const deployObservationDefaultDryRun = await buildDeployObservation({
|
||
deployment: "kt-template-online-api",
|
||
execute: false,
|
||
namespace: "kt-prod",
|
||
project: "api",
|
||
});
|
||
const deployObservationDefaultCommandText =
|
||
deployObservationDefaultDryRun.commands.map((item) => item.command).join("\n");
|
||
if (
|
||
!deployObservationDefaultCommandText.includes(
|
||
"/vol1/docker/jenkins/jenkins_home",
|
||
) ||
|
||
!deployObservationDefaultCommandText.includes(
|
||
"JOB_NAME='KT-Template/KT-Template-API/main'",
|
||
)
|
||
) {
|
||
throw new Error("deploy observation Jenkins defaults self-check failed");
|
||
}
|
||
for (const expected of [
|
||
'JOB_DIR="$JENKINS_HOME/jobs/$JOB_NAME/builds"',
|
||
'jobs/$JOB_FOLDER/jobs/$JOB_PIPELINE/branches/$JOB_BRANCH/builds',
|
||
'find "$JENKINS_HOME/jobs"',
|
||
]) {
|
||
if (!deployObservationDefaultCommandText.includes(expected)) {
|
||
throw new Error(
|
||
`deploy observation Jenkins path self-check failed: ${expected}`,
|
||
);
|
||
}
|
||
}
|
||
|
||
const deployObservationExplicitJenkinsDryRun = await buildDeployObservation({
|
||
execute: false,
|
||
jenkinsHome: "/custom/jenkins",
|
||
jobName: "custom-job",
|
||
});
|
||
const deployObservationExplicitJenkinsCommandText =
|
||
deployObservationExplicitJenkinsDryRun.commands
|
||
.map((item) => item.command)
|
||
.join("\n");
|
||
if (
|
||
!deployObservationExplicitJenkinsCommandText.includes(
|
||
"JENKINS_HOME='/custom/jenkins'",
|
||
) ||
|
||
!deployObservationExplicitJenkinsCommandText.includes("JOB_NAME='custom-job'")
|
||
) {
|
||
throw new Error("deploy observation explicit Jenkins input self-check failed");
|
||
}
|
||
|
||
const deployObservationExplicitHealthDryRun = await buildDeployObservation({
|
||
deployment: "kt-template-online-api",
|
||
execute: false,
|
||
healthUrl: "http://127.0.0.1:30085/health/runtime",
|
||
imageTag: "abc1234",
|
||
jobName: "kt-template-online-api",
|
||
namespace: "kt-prod",
|
||
project: "api",
|
||
});
|
||
const deployObservationExplicitHealthCommandText =
|
||
deployObservationExplicitHealthDryRun.commands
|
||
.map((item) => item.command)
|
||
.join("\n");
|
||
if (
|
||
!deployObservationExplicitHealthCommandText.includes(
|
||
"http://127.0.0.1:30085/health/runtime",
|
||
)
|
||
) {
|
||
throw new Error("deploy observation explicit health URL self-check failed");
|
||
}
|
||
|
||
const deployObservationUnsafeProjectDryRun = await buildDeployObservation({
|
||
deployment: "kt-template-online-api",
|
||
execute: false,
|
||
healthUrl: "http://127.0.0.1:48085/health/runtime",
|
||
imageTag: "abc1234",
|
||
jobName: "kt-template-online-api",
|
||
namespace: "kt-prod",
|
||
project: "../escape",
|
||
smoke: "curl -fsS --max-time 8 http://127.0.0.1:48085/health/runtime",
|
||
});
|
||
if (
|
||
deployObservationUnsafeProjectDryRun.execute !== false ||
|
||
deployObservationUnsafeProjectDryRun.artifactPath ||
|
||
deployObservationUnsafeProjectDryRun.evidence ||
|
||
JSON.stringify(deployObservationUnsafeProjectDryRun.commands).includes(
|
||
"../escape",
|
||
)
|
||
) {
|
||
throw new Error("deploy observation project dry-run safety self-check failed");
|
||
}
|
||
|
||
let unsupportedSmokeRejected = false;
|
||
try {
|
||
await buildDeployObservation({
|
||
execute: false,
|
||
smoke:
|
||
"curl -fsS --max-time 8 http://127.0.0.1:48085/health/runtime; rm -rf /",
|
||
});
|
||
} catch (error) {
|
||
unsupportedSmokeRejected = String(error).includes(
|
||
"Unsupported deploy observation smoke command",
|
||
);
|
||
}
|
||
if (!unsupportedSmokeRejected) {
|
||
throw new Error("deploy observation unsafe smoke self-check failed");
|
||
}
|
||
|
||
let unsafeHealthUrlRejected = false;
|
||
try {
|
||
await buildDeployObservation({
|
||
execute: false,
|
||
healthUrl: "http://user:pass@127.0.0.1:48085/health/runtime",
|
||
});
|
||
} catch (error) {
|
||
unsafeHealthUrlRejected = String(error).includes(
|
||
"Unsupported deploy observation healthUrl",
|
||
);
|
||
}
|
||
if (!unsafeHealthUrlRejected) {
|
||
throw new Error("deploy observation unsafe health URL self-check failed");
|
||
}
|
||
|
||
let unsafeSshTargetRejected = false;
|
||
try {
|
||
await buildDeployObservation({
|
||
execute: true,
|
||
sshTarget: "root@unexpected-host.example",
|
||
});
|
||
} catch (error) {
|
||
unsafeSshTargetRejected = String(error).includes(
|
||
'Unsafe deploy observation input "sshTarget"',
|
||
);
|
||
}
|
||
if (!unsafeSshTargetRejected) {
|
||
throw new Error("deploy observation unsafe ssh target self-check failed");
|
||
}
|
||
|
||
let unsafeInputRejected = false;
|
||
try {
|
||
await buildDeployObservation({
|
||
execute: false,
|
||
jobName: "bad\njob",
|
||
});
|
||
} catch (error) {
|
||
unsafeInputRejected = String(error).includes(
|
||
"Unsafe deploy observation input",
|
||
);
|
||
}
|
||
if (!unsafeInputRejected) {
|
||
throw new Error("deploy observation unsafe input self-check failed");
|
||
}
|
||
|
||
const refactorGuardrails = buildGuardrails({
|
||
project: "mcp",
|
||
taskType: "refactor",
|
||
userRequest: "重构 KT 工作流护栏",
|
||
});
|
||
const refactorGuardrailOk =
|
||
refactorGuardrails.guardrails.beforeEdit.some((item) =>
|
||
item.includes("重构前写清楚"),
|
||
) &&
|
||
refactorGuardrails.guardrails.verification.notes.some((item) =>
|
||
item.includes("重构验证必须覆盖原行为"),
|
||
);
|
||
if (!refactorGuardrailOk) {
|
||
throw new Error("refactor guardrail self-check failed");
|
||
}
|
||
|
||
const apiVerification = buildVerificationPlan({
|
||
changeType: "api",
|
||
project: "api",
|
||
});
|
||
const apiVerificationCommands = apiVerification.commands.join("\n");
|
||
if (
|
||
!apiVerificationCommands.includes("pnpm exec jest --runInBand") ||
|
||
apiVerificationCommands.includes("passWithNoTests")
|
||
) {
|
||
throw new Error("API Jest verification command self-check failed");
|
||
}
|
||
|
||
const timeSerializationRisk = {
|
||
bad: hasEntityLoadTimeSerializationMutation(
|
||
"import { AfterLoad } from 'typeorm';\nexport const FormatDateTime = () => {}; formatDateTimeFields(this);",
|
||
),
|
||
good: hasEntityLoadTimeSerializationMutation(
|
||
"import { Column } from 'typeorm';\nexport class KtDateTime extends Date {}\nexport const KtDateTimeColumn = () => Column({ transformer: { from: (value) => new KtDateTime(value), to: (value) => value } });",
|
||
),
|
||
};
|
||
if (!timeSerializationRisk.bad || timeSerializationRisk.good) {
|
||
throw new Error("time serialization mutation risk self-check failed");
|
||
}
|
||
|
||
const databaseVerification = buildVerificationPlan({
|
||
changeType: "database",
|
||
project: "api",
|
||
});
|
||
const databaseVerificationNotes = databaseVerification.notes.join("\n");
|
||
if (
|
||
!databaseVerificationNotes.includes("备份目标表") ||
|
||
!databaseVerificationNotes.includes("SHOW COLUMNS") ||
|
||
!databaseVerificationNotes.includes("真实接口 smoke")
|
||
) {
|
||
throw new Error("database migration verification self-check failed");
|
||
}
|
||
|
||
const qqbotAccountScanPlan = buildBusinessTestPlan({
|
||
flow: "qqbot-account-scan",
|
||
});
|
||
const qqbotAccountScanText = JSON.stringify(qqbotAccountScanPlan);
|
||
if (
|
||
!qqbotAccountScanText.includes("pending sessionId") ||
|
||
!qqbotAccountScanText.includes("第一轮 Docker run") ||
|
||
!qqbotAccountScanText.includes("不能造脏数据")
|
||
) {
|
||
throw new Error("QQBot account scan non-dirty smoke self-check failed");
|
||
}
|
||
|
||
const closeoutNeedsUpgrade = buildWorkstreamCloseout({
|
||
cleanupEvidence: ["cleanup-history dry-run deleted=0。"],
|
||
docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"],
|
||
reviewEvidence: ["global-review findings=0。"],
|
||
problemRecords: ["Jenkins 状态需要结合日志和 K8s 状态确认。"],
|
||
reusablePatterns: ["deploy-observation"],
|
||
stableSolutions: [
|
||
"收尾时固定观测 build、commit、镜像、Deployment、Pod 和 smoke。",
|
||
],
|
||
superpowersReviewEvidence: ["Superpowers reviewer completed; Important findings handled。"],
|
||
title: "发布闭环",
|
||
verificationEvidence: ["Jenkins #132 SUCCESS,K8s Pod Running。"],
|
||
});
|
||
if ((closeoutNeedsUpgrade.canReportComplete as boolean) !== false) {
|
||
throw new Error("workstream closeout upgrade gate self-check failed");
|
||
}
|
||
|
||
const closeoutComplete = buildWorkstreamCloseout({
|
||
cleanupEvidence: ["cleanup-history dry-run deleted=0。"],
|
||
docSyncEvidence: ["无需文档更新,已记录原因。"],
|
||
ktWorkflowUpdated: true,
|
||
problemRecords: ["无新卡点。"],
|
||
reviewEvidence: ["global-review findings=0。"],
|
||
reusablePatterns: ["none"],
|
||
stableSolutions: ["无新增稳定解法。"],
|
||
superpowersReviewEvidence: ["Superpowers reviewer completed; no Critical/Important findings。"],
|
||
title: "文档收尾",
|
||
verificationEvidence: ["typecheck/self-test/global-review 通过。"],
|
||
});
|
||
if ((closeoutComplete.canReportComplete as boolean) !== true) {
|
||
throw new Error("workstream closeout complete self-check failed");
|
||
}
|
||
|
||
const closeoutInvalidCleanup = buildWorkstreamCloseout({
|
||
cleanupEvidence: ["cleanup-history dry-run deleted=3。"],
|
||
docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"],
|
||
problemRecords: ["无新卡点。"],
|
||
reviewEvidence: ["global-review findings=0。"],
|
||
reusablePatterns: ["none"],
|
||
stableSolutions: ["无新增稳定解法。"],
|
||
superpowersReviewEvidence: ["Superpowers reviewer completed。"],
|
||
title: "无效清理证据",
|
||
verificationEvidence: ["typecheck 通过。"],
|
||
});
|
||
if ((closeoutInvalidCleanup.canReportComplete as boolean) !== false) {
|
||
throw new Error("workstream closeout cleanup evidence self-check failed");
|
||
}
|
||
|
||
const closeoutMissingSuperpowersReview = buildWorkstreamCloseout({
|
||
cleanupFinalDeleted: 0,
|
||
docSyncEvidence: ["无需文档更新,已记录原因。"],
|
||
problemRecords: ["无新卡点。"],
|
||
reviewEvidence: ["global-review findings=0。"],
|
||
reusablePatterns: ["none"],
|
||
stableSolutions: ["无新增稳定解法。"],
|
||
title: "缺 Superpowers 审查证据",
|
||
verificationEvidence: ["typecheck 通过。"],
|
||
});
|
||
if ((closeoutMissingSuperpowersReview.canReportComplete as boolean) !== false) {
|
||
throw new Error("workstream closeout Superpowers review gate self-check failed");
|
||
}
|
||
|
||
const obsidianContext = readObsidianContext({
|
||
maxDocuments: 6,
|
||
module: "ktWorkflow",
|
||
});
|
||
if (
|
||
(obsidianContext.summary as { returnedDocuments?: number })
|
||
.returnedDocuments === 0
|
||
) {
|
||
throw new Error("obsidian context self-check failed");
|
||
}
|
||
if ((obsidianContext.summary as { codeAnchors?: number }).codeAnchors === 0) {
|
||
throw new Error("obsidian code anchor self-check failed");
|
||
}
|
||
|
||
const obsidianValidation = validateObsidianVault({
|
||
includeStaleReferences: false,
|
||
});
|
||
if ((obsidianValidation.summary as { errors?: number }).errors !== 0) {
|
||
throw new Error("obsidian validation self-check failed");
|
||
}
|
||
|
||
const cleanupCliParser = {
|
||
defaultDryRun: parseCliCleanupArgs(["node", "server", "--cleanup-history"])
|
||
.dryRun,
|
||
executeDryRun: parseCliCleanupArgs([
|
||
"node",
|
||
"server",
|
||
"--cleanup-history",
|
||
"--execute",
|
||
]).dryRun,
|
||
};
|
||
if (!cleanupCliParser.defaultDryRun || cleanupCliParser.executeDryRun) {
|
||
throw new Error("cleanup CLI dry-run parser self-check failed");
|
||
}
|
||
|
||
const cleanupPreview = cleanupHistoryArtifacts({
|
||
dryRun: true,
|
||
keep: 3,
|
||
roots: [".kt-workspace/test-artifacts", ".kt-workspace"],
|
||
});
|
||
const cleanupRoots = cleanupPreview.roots as Array<{
|
||
preservedNames: string[];
|
||
root: string;
|
||
}>;
|
||
const testArtifactsRoot = cleanupRoots.find(
|
||
(item) => item.root === ".kt-workspace/test-artifacts",
|
||
);
|
||
const workspaceHistoryRoot = cleanupRoots.find(
|
||
(item) => item.root === ".kt-workspace",
|
||
);
|
||
if (!testArtifactsRoot?.preservedNames.includes("_templates")) {
|
||
throw new Error("cleanup template preservation self-check failed");
|
||
}
|
||
if (!workspaceHistoryRoot?.preservedNames.includes("test-artifacts")) {
|
||
throw new Error("cleanup workspace root preservation self-check failed");
|
||
}
|
||
|
||
const docSyncBangDream = await buildChangeDocSync({
|
||
changedFiles: [
|
||
"src/qqbot/plugins/bangDream/application/bangdream-application.service.ts",
|
||
],
|
||
project: "api",
|
||
});
|
||
const docSyncRequiredDocs = docSyncBangDream.requiredDocs as Array<{
|
||
path: string;
|
||
}>;
|
||
if (
|
||
!docSyncRequiredDocs.some(
|
||
(item) => item.path === "Node/kt-template-online-api/API.md",
|
||
)
|
||
) {
|
||
throw new Error("doc sync API self-check failed");
|
||
}
|
||
|
||
const loopAuditComplete = buildWorkflowLoopAudit({
|
||
cleanupExecuted: true,
|
||
cleanupFinalDeleted: 0,
|
||
cleanupPreviewDeleted: 3,
|
||
docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"],
|
||
ktWorkflowUpdated: true,
|
||
problemRecords: ["测试历史清理流程未自动执行。"],
|
||
reusablePattern: true,
|
||
reviewEvidence: ["global-review findings=0"],
|
||
stableSolutions: [
|
||
"cleanup-history 默认 dry-run,显式 --execute 后复验 deleted=0。",
|
||
],
|
||
superpowersReviewEvidence: ["Superpowers reviewer completed; no Critical/Important findings"],
|
||
taskTitle: "自动化闭环审计",
|
||
testCases: ["cleanup-history dry-run/execute 回归测试"],
|
||
verificationEvidence: ["typecheck/self-test/cleanup dry-run 通过"],
|
||
});
|
||
if ((loopAuditComplete.canReportComplete as boolean) !== true) {
|
||
throw new Error("workflow loop complete self-check failed");
|
||
}
|
||
|
||
const loopAuditBlocked = buildWorkflowLoopAudit({
|
||
cleanupPreviewDeleted: 1,
|
||
taskTitle: "缺证据闭环",
|
||
});
|
||
if ((loopAuditBlocked.canReportComplete as boolean) !== false) {
|
||
throw new Error("workflow loop blocked self-check failed");
|
||
}
|
||
|
||
const loopAuditMissingCleanup = buildWorkflowLoopAudit({
|
||
docSyncEvidence: ["kt_change_doc_sync requiredDocs 已处理。"],
|
||
reviewEvidence: ["global-review findings=0。"],
|
||
superpowersReviewEvidence: ["Superpowers reviewer completed。"],
|
||
taskTitle: "缺清理证据闭环",
|
||
testCases: ["self-test"],
|
||
verificationEvidence: ["typecheck 通过。"],
|
||
});
|
||
if ((loopAuditMissingCleanup.canReportComplete as boolean) !== false) {
|
||
throw new Error("workflow loop missing cleanup evidence self-check failed");
|
||
}
|
||
|
||
const dbSyncPlan = buildDbSyncPlan({});
|
||
const dbSyncCommands = (dbSyncPlan.commands as string[]).join("\n");
|
||
const backupName = dbSyncPlan.backupName as string;
|
||
if (
|
||
!backupName.includes("_") ||
|
||
!dbSyncCommands.includes(".kt-workspace/db-sync") ||
|
||
dbSyncCommands.includes("CREATE DATABASE IF NOT EXISTS")
|
||
) {
|
||
throw new Error("DB sync plan safety self-check failed");
|
||
}
|
||
|
||
const napcatDeviceProfileCheck = buildNapcatDeviceProfileCheck({
|
||
project: "api",
|
||
});
|
||
if ((napcatDeviceProfileCheck.ok as boolean) !== true) {
|
||
throw new Error("NapCat device profile guardrail self-check failed");
|
||
}
|
||
|
||
const data = {
|
||
context: readWorkflowContext({ taskRecordCount: 2 }),
|
||
guardrails: buildGuardrails({
|
||
project: "mcp",
|
||
taskType: "mcp",
|
||
userRequest: "扩展 KT 工作区 MCP 可复用能力",
|
||
}),
|
||
cleanupCliParser,
|
||
cleanupPreview,
|
||
docSyncBangDream,
|
||
loopAuditBlocked,
|
||
loopAuditComplete,
|
||
historyCleanup: cleanupHistoryArtifacts({
|
||
dryRun: true,
|
||
keep: 3,
|
||
}),
|
||
inspectAdmin: await inspectProject({ includeGit: false, project: "admin" }),
|
||
inspectKnife4j: await inspectProject({
|
||
includeGit: false,
|
||
project: "knife4j",
|
||
}),
|
||
prepareTask: await prepareTask({
|
||
includeGit: false,
|
||
project: "mcp",
|
||
taskType: "mcp",
|
||
userRequest: "扩展 KT 工作区 MCP 可复用能力",
|
||
}),
|
||
review: await buildGlobalCodeReview({
|
||
includeContentScan: true,
|
||
includeRootScan: true,
|
||
maxFindingsPerProject: 10,
|
||
projects: ["mcp"],
|
||
}),
|
||
reviewCliParser,
|
||
reviewClassifier,
|
||
refactorGuardrails,
|
||
apiVerification,
|
||
businessTestPlan: buildBusinessTestPlan({
|
||
flow: "system-log-visualization",
|
||
}),
|
||
napcatDeviceProfileCheck,
|
||
closeoutComplete,
|
||
closeoutNeedsUpgrade,
|
||
obsidianContext,
|
||
obsidianSync: syncObsidianWorkflow({
|
||
includeStaleReferences: false,
|
||
}),
|
||
obsidianValidation,
|
||
blockerResolution: buildBlockerResolution({
|
||
attempts: 2,
|
||
evidence: "self-test",
|
||
problem: "重复超时",
|
||
project: "mcp",
|
||
solution: "改用固定脚本和 timeout 后再验证",
|
||
}),
|
||
commitPlan: await buildCommitPlan({
|
||
projects: ["mcp"],
|
||
}),
|
||
componentWorkflow: buildComponentWorkflow({
|
||
target: "SystemLog",
|
||
}),
|
||
dbSyncPlan,
|
||
finishTask: await buildFinishTask({
|
||
cleanupHistory: false,
|
||
projects: ["mcp"],
|
||
runValidation: false,
|
||
}),
|
||
pushPlan: await buildPushPlan({
|
||
projects: ["mcp"],
|
||
}),
|
||
remoteHealthCheck: await buildRemoteHealthCheck({
|
||
execute: false,
|
||
services: ["ssh", "docker"],
|
||
}),
|
||
verification: buildVerificationPlan({
|
||
changeType: "mcp",
|
||
project: "mcp",
|
||
}),
|
||
};
|
||
const outputDir = resolveInsideRoot(".kt-workspace/verify/ktWorkflow");
|
||
mkdirSync(outputDir, { recursive: true });
|
||
writeFileSync(
|
||
path.join(outputDir, "self-test.json"),
|
||
JSON.stringify(data, null, 2),
|
||
"utf8",
|
||
);
|
||
console.log(
|
||
JSON.stringify(
|
||
{ ok: true, outputDir, tools: registeredToolNames.length },
|
||
null,
|
||
2,
|
||
),
|
||
);
|
||
}
|