ktworkflow-mcp/src/selfTest.ts

1173 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { mkdirSync, writeFileSync } from "node:fs";
import path from "node:path";
import {
parseDeployObservationCliArgs,
parseGlobalReviewCliArgs,
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,
findQqbotStatusBoundaryFindings,
findTaskRecordGovernanceFindings,
isBenignCredentialReviewValue,
} from "./tools/review.js";
import { buildBusinessTestPlan } 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";
export async function runSelfTest(): Promise<void> {
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 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.keepDeviceVerifyPending(session, captchaResult.jumpUrl, '验证码已通过');",
"}",
"private keepDeviceVerifyPending(session, deviceVerifyUrl) {",
" session.deviceVerifyUrl = deviceVerifyUrl;",
" session.captchaUrl = undefined;",
"}",
"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",
)
) {
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;
logCommit?: string;
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: SUCCESS"
: `Checking out Revision ${overrides.logCommit ?? "abc1234"}`,
"Finished: 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 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 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 SUCCESSK8s 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 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",
}),
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,
),
);
}