import { gitMirrorFlushJobManifest, gitMirrorSyncJobManifest, gitMirrorV02SyncRequirement, hwlabG14MonitorStateFileName, parseGitMirrorStatusRefs, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets, v02PipelineServiceIds } from "./src/hwlab-g14"; function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); } function record(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; } assertCondition( hwlabG14MonitorStateFileName({ once: false, dryRun: false }) === "latest-monitor-job.json", "long-running monitor should own latest-monitor-job.json", ); assertCondition( hwlabG14MonitorStateFileName({ once: true, dryRun: false }) === "latest-once-job.json", "once jobs must not overwrite the long-running monitor pointer", ); assertCondition( hwlabG14MonitorStateFileName({ once: false, dryRun: true }) === "latest-dry-run-job.json", "dry-run monitor jobs must not overwrite the live monitor pointer", ); assertCondition( hwlabG14MonitorStateFileName({ once: true, dryRun: true }) === "latest-once-dry-run-job.json", "once dry-runs need a distinct pointer for low-noise diagnostics", ); const gitMirrorJob = gitMirrorSyncJobManifest("git-mirror-hwlab-sync-manual-test"); assertCondition(gitMirrorJob.kind === "Job", "git mirror sync must be a manual Job, not a CronJob", gitMirrorJob); assertCondition(record(gitMirrorJob.metadata).namespace === "devops-infra", "git mirror sync Job must target devops-infra", gitMirrorJob); assertCondition(record(record(gitMirrorJob.metadata).labels)["hwlab.pikastech.local/trigger"] === "manual-cli", "git mirror sync Job must be labeled as manual CLI triggered", gitMirrorJob); assertCondition(record(gitMirrorJob.spec).backoffLimit === 0, "git mirror sync Job should fail visibly instead of retrying in the background", gitMirrorJob); const gitMirrorFlushJob = gitMirrorFlushJobManifest("git-mirror-hwlab-flush-manual-test"); assertCondition(gitMirrorFlushJob.kind === "Job", "git mirror flush must be a manual Job", gitMirrorFlushJob); assertCondition(record(record(gitMirrorFlushJob.metadata).labels)["app.kubernetes.io/component"] === "flush-controller", "git mirror flush Job must be labeled separately from sync", gitMirrorFlushJob); assertCondition(record(record(gitMirrorFlushJob.metadata).labels)["hwlab.pikastech.local/trigger"] === "manual-cli", "git mirror flush Job must be labeled as manual CLI triggered", gitMirrorFlushJob); const flushTemplate = record(record(gitMirrorFlushJob.spec).template); const flushPodSpec = record(flushTemplate.spec); const flushContainer = record(Array.isArray(flushPodSpec.containers) ? flushPodSpec.containers[0] : null); assertCondition(JSON.stringify(flushContainer.command) === JSON.stringify(["/script/flush.sh"]), "git mirror flush Job must run the flush script", gitMirrorFlushJob); const gitMirrorStatusRaw = [ 'lastSync={"ok":true}', "lastWrite=", "lastFlush=", 'refs={"refs":{"localV02":"0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc","localG14":"1111111111111111111111111111111111111111","localGitops":"2222222222222222222222222222222222222222","githubGitops":"3333333333333333333333333333333333333333"},"pendingFlush":true}', ].join("\n"); const parsedGitMirrorRefs = parseGitMirrorStatusRefs(gitMirrorStatusRaw); assertCondition( parsedGitMirrorRefs.refs.localV02 === "0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc", "git mirror status parser must expose local v0.2 ref for trigger pre-sync", parsedGitMirrorRefs, ); assertCondition(parsedGitMirrorRefs.pendingFlush === true, "git mirror status parser must preserve pending flush signal", parsedGitMirrorRefs); assertCondition( gitMirrorV02SyncRequirement("0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc", gitMirrorStatusRaw).required === false, "trigger-current must not sync mirror when local v0.2 already matches source commit", ); assertCondition( gitMirrorV02SyncRequirement("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", gitMirrorStatusRaw).required === true, "trigger-current must sync mirror before creating PipelineRun when local v0.2 is stale", ); assertCondition( !v02PipelineServiceIds().includes("hwlab-cli"), "v0.2 PipelineRun service matrix must not build hwlab-cli because cli is short-connection source tool", v02PipelineServiceIds(), ); const prBody = [ "## 背景", "", "G14 DEV Cloud Workbench 中,内部 `message-trace-body` 的 `pre` 会在 trace 更新时被替换,导致用户滚到中间后被重置到顶部。", "", "## 修改", "", "- trace row patch 从 index/replace 改为 stable `data-trace-row-id` keyed reconciliation。", "- 已存在 row 只移动和原地更新 header/body,不替换 `li/pre`。", "- `message-trace-body` 内部滚动状态按 `traceUiKey + rowId` 记忆。", ].join("\n"); const semanticBullets = semanticChangelogBullets("fix: preserve inner trace scroll", prBody, { keyFiles: [ "modified web/hwlab-cloud-web/app.mjs", "modified web/hwlab-cloud-web/scripts/trace-scroll.test.mjs", ], }); assertCondition( semanticBullets.some((line) => line.includes("修复目标") && line.includes("滚到中间后被重置到顶部")), "semantic changelog should explain the user-visible bug being fixed", semanticBullets, ); assertCondition( semanticBullets.some((line) => line.includes("data-trace-row-id")), "semantic changelog should include PR body change bullets instead of only file stats", semanticBullets, ); const summaryBullets = semanticChangelogBullets("feat: preload gateway tran helper", [ "## 摘要", "- 新增 `/app/tools/hwlab-gateway-tran.mjs`,支持 `cmd`、`ps`、`upload`、`download`。", ].join("\n"), {}); assertCondition( summaryBullets.some((line) => line.includes("hwlab-gateway-tran.mjs") && line.includes("upload")), "semantic changelog should also extract Chinese summary sections", summaryBullets, ); const rolloutBody = rolloutRecordBody({ pr: { number: 506, title: "fix: preserve inner trace scroll", url: "https://github.com/pikasTech/HWLAB/pull/506" }, prData: { json: { title: "fix: preserve inner trace scroll", body: prBody, url: "https://github.com/pikasTech/HWLAB/pull/506" } }, fileSummary: { summary: { files: 2, additions: 268, deletions: 22, commits: 1 }, keyFiles: [ "modified web/hwlab-cloud-web/app.mjs", "modified web/hwlab-cloud-web/scripts/trace-scroll.test.mjs", ], }, sourceCommit: "1a3fa9e6fbc987142463ecff2cbcef240a6278f2", pipelineRun: "hwlab-g14-ci-poll-1a3fa9e6fbc9", gitopsRevision: "21462b78ce4e7dba4ea374398f60db690e290147", mergedAt: "2026-05-27T06:52:22Z", pipelineSucceededAt: "2026-05-27T06:55:38Z", finishedAt: "2026-05-27T06:55:47Z", rollout: { pipelineText: "True\nSucceeded", argoText: "21462\nSynced\nHealthy\nSucceeded\n21462", healthOk: true }, ciMetrics: parsePipelineTaskRunMetrics("hwlab-g14-ci-poll-test", [ "taskrun-a\tbuild-hwlab-cloud-api\t2026-05-27T06:54:43Z\t2026-05-27T06:54:52Z\tservice-id=hwlab-cloud-api;status=reused;build-backend=reused-catalog;", "taskrun-b\tbuild-hwlab-cloud-web\t2026-05-27T06:54:43Z\t2026-05-27T06:55:08Z\tservice-id=hwlab-cloud-web;status=published;build-backend=buildkit;", ].join("\n")), }); assertCondition( rolloutBody.includes("- 上线 changelog(语义化):"), "rollout record must label natural-language changelog separately", ); assertCondition( rolloutBody.includes("- 自动 diff 摘要:"), "rollout record must keep automatic diff summary separately", ); assertCondition( rolloutBody.indexOf("- 上线 changelog(语义化):") < rolloutBody.indexOf("- 自动 diff 摘要:"), "semantic changelog should appear before automatic diff summary", ); assertCondition( rolloutBody.includes("changed files: 2; +268 / -22; commits: 1"), "automatic diff summary should preserve file/stat evidence", ); assertCondition( rolloutBody.includes("lazy build reused: 1/2"), "rollout record should include lazy-build reused ratio", rolloutBody, ); assertCondition( rolloutBody.includes("reused services: hwlab-cloud-api") && rolloutBody.includes("rebuild services: hwlab-cloud-web"), "rollout record should list reused and rebuild services", rolloutBody, ); assertCondition( rolloutBody.includes("hwlab-cloud-api: reused, 9s") && rolloutBody.includes("hwlab-cloud-web: published, 25s"), "rollout record should include each service duration", rolloutBody, ); console.log(JSON.stringify({ ok: true, checks: [ "long-running monitor owns latest-monitor-job.json", "once jobs do not overwrite long-running monitor state", "dry-run jobs do not overwrite live monitor state", "once dry-run jobs have a distinct diagnostic state file", "git mirror sync is a manual devops-infra Job, not a CronJob", "git mirror flush is a manual devops-infra Job, not a CronJob", "trigger-current can decide whether v0.2 git mirror pre-sync is required", "v0.2 PipelineRun service matrix excludes hwlab-cli", "rollout brief includes natural-language changelog before automatic diff summary", "semantic changelog extracts Chinese summary sections", "rollout brief includes lazy-build reused/rebuild metrics and service durations", ], }, null, 2));