1173 lines
40 KiB
TypeScript
1173 lines
40 KiB
TypeScript
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 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 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,
|
||
),
|
||
);
|
||
}
|