@bf-meishijian/aip 0.0.31 → 0.0.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -18
- package/dist/aip-submit-outcome/SKILL.md +20 -47
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/coding-run-manager.d.ts +2 -3
- package/dist/coding-run-manager.d.ts.map +1 -1
- package/dist/coding-run-manager.js +52 -49
- package/dist/coding-run-manager.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +8 -3
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-tools.d.ts.map +1 -1
- package/dist/delivery-tools.js +2 -0
- package/dist/delivery-tools.js.map +1 -1
- package/dist/diagnostics.d.ts +1 -1
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +1 -1
- package/dist/diagnostics.js.map +1 -1
- package/dist/general-run-client.d.ts.map +1 -1
- package/dist/general-run-manager.d.ts.map +1 -1
- package/dist/general-run-manager.js +0 -1
- package/dist/general-run-manager.js.map +1 -1
- package/dist/general-run-tools.d.ts +1 -1
- package/dist/general-run-tools.d.ts.map +1 -1
- package/dist/general-run-tools.js +1 -1
- package/dist/general-run-tools.js.map +1 -1
- package/dist/generated/qa-schemas.js +1123 -0
- package/dist/handoff-cli.d.ts +2 -0
- package/dist/handoff-cli.d.ts.map +1 -0
- package/dist/handoff-cli.js +86 -0
- package/dist/handoff-cli.js.map +1 -0
- package/dist/host-log.d.ts +2 -0
- package/dist/host-log.d.ts.map +1 -1
- package/dist/host-log.js +5 -2
- package/dist/host-log.js.map +1 -1
- package/dist/host-shell.d.ts +2 -0
- package/dist/host-shell.d.ts.map +1 -0
- package/dist/host-shell.js +11 -0
- package/dist/host-shell.js.map +1 -0
- package/dist/host-status.d.ts.map +1 -1
- package/dist/live-employee.js +2 -2
- package/dist/live-employee.js.map +1 -1
- package/dist/local-employee-runtime.d.ts +1 -1
- package/dist/local-employee-runtime.d.ts.map +1 -1
- package/dist/local-employee-runtime.js +1 -1
- package/dist/local-employee-runtime.js.map +1 -1
- package/dist/local-handoff-workspace.d.ts +8 -1
- package/dist/local-handoff-workspace.d.ts.map +1 -1
- package/dist/local-handoff-workspace.js +14 -34
- package/dist/local-handoff-workspace.js.map +1 -1
- package/dist/outcome-cli.d.ts +1 -0
- package/dist/outcome-cli.d.ts.map +1 -1
- package/dist/outcome-cli.js +43 -54
- package/dist/outcome-cli.js.map +1 -1
- package/dist/pi-message-queue.d.ts +5 -0
- package/dist/pi-message-queue.d.ts.map +1 -1
- package/dist/pi-message-queue.js +21 -0
- package/dist/pi-message-queue.js.map +1 -1
- package/dist/pi-model-service.d.ts.map +1 -1
- package/dist/platform-client.d.ts +6 -0
- package/dist/platform-client.d.ts.map +1 -1
- package/dist/platform-client.js +18 -2
- package/dist/platform-client.js.map +1 -1
- package/dist/project-facts.d.ts.map +1 -1
- package/dist/project-facts.js +5 -0
- package/dist/project-facts.js.map +1 -1
- package/dist/qa-ajv.js +9 -0
- package/dist/qa-contract.d.ts +1 -1
- package/dist/qa-contract.d.ts.map +1 -1
- package/dist/qa-contract.js +18 -90
- package/dist/qa-contract.js.map +1 -1
- package/dist/qa-http-validation.js +5 -133
- package/dist/qa-project-checkout.d.ts +6 -2
- package/dist/qa-project-checkout.d.ts.map +1 -1
- package/dist/qa-project-checkout.js +19 -12
- package/dist/qa-project-checkout.js.map +1 -1
- package/dist/qa-session-manager.d.ts +3 -6
- package/dist/qa-session-manager.d.ts.map +1 -1
- package/dist/qa-session-manager.js +116 -90
- package/dist/qa-session-manager.js.map +1 -1
- package/dist/qa-tool-arguments.js +336 -0
- package/dist/stage-delivery.js +5 -3
- package/dist/state.d.ts +2 -2
- package/dist/state.js +2 -2
- package/dist/test-cli.d.ts.map +1 -1
- package/dist/test-cli.js +1 -3
- package/dist/test-cli.js.map +1 -1
- package/dist/test-executor.d.ts +2 -2
- package/dist/test-executor.d.ts.map +1 -1
- package/dist/test-executor.js +14 -9
- package/dist/test-executor.js.map +1 -1
- package/dist/test-reports.d.ts +13 -1
- package/dist/test-reports.d.ts.map +1 -1
- package/dist/tool-dispatch.d.ts.map +1 -1
- package/dist/tool-dispatch.js +3 -0
- package/dist/tool-dispatch.js.map +1 -1
- package/package.json +5 -4
|
@@ -1,107 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { qaEvidenceReferencesSchema, qaHttpExchangeSchema } from "./generated/qa-schemas.js";
|
|
2
|
+
import { createQaAjv } from "./qa-ajv.js";
|
|
2
3
|
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
additionalProperties: false,
|
|
6
|
-
properties: {
|
|
7
|
-
step: { type: "string", minLength: 1 },
|
|
8
|
-
id: {
|
|
9
|
-
type: "string",
|
|
10
|
-
},
|
|
11
|
-
sequence: {
|
|
12
|
-
type: "integer",
|
|
13
|
-
minimum: 1,
|
|
14
|
-
},
|
|
15
|
-
startedAt: {
|
|
16
|
-
type: "string",
|
|
17
|
-
},
|
|
18
|
-
durationMs: {
|
|
19
|
-
type: "integer",
|
|
20
|
-
minimum: 0,
|
|
21
|
-
},
|
|
22
|
-
request: {
|
|
23
|
-
type: "object",
|
|
24
|
-
additionalProperties: false,
|
|
25
|
-
properties: {
|
|
26
|
-
method: {
|
|
27
|
-
type: "string",
|
|
28
|
-
},
|
|
29
|
-
url: {
|
|
30
|
-
type: "string",
|
|
31
|
-
},
|
|
32
|
-
headers: {
|
|
33
|
-
type: "object",
|
|
34
|
-
additionalProperties: {
|
|
35
|
-
type: "array",
|
|
36
|
-
items: {
|
|
37
|
-
type: "string",
|
|
38
|
-
},
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
body: {
|
|
42
|
-
anyOf: [
|
|
43
|
-
{
|
|
44
|
-
type: "string",
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
type: "null",
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
},
|
|
51
|
-
bodyAttachment: {
|
|
52
|
-
type: "string",
|
|
53
|
-
},
|
|
54
|
-
},
|
|
55
|
-
required: ["method", "url", "headers", "body"],
|
|
56
|
-
},
|
|
57
|
-
response: {
|
|
58
|
-
anyOf: [
|
|
59
|
-
{
|
|
60
|
-
type: "object",
|
|
61
|
-
additionalProperties: false,
|
|
62
|
-
properties: {
|
|
63
|
-
status: {
|
|
64
|
-
type: "integer",
|
|
65
|
-
minimum: 100,
|
|
66
|
-
maximum: 599,
|
|
67
|
-
},
|
|
68
|
-
headers: {
|
|
69
|
-
type: "object",
|
|
70
|
-
additionalProperties: {
|
|
71
|
-
type: "array",
|
|
72
|
-
items: {
|
|
73
|
-
type: "string",
|
|
74
|
-
},
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
|
-
body: {
|
|
78
|
-
anyOf: [
|
|
79
|
-
{
|
|
80
|
-
type: "string",
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
type: "null",
|
|
84
|
-
},
|
|
85
|
-
],
|
|
86
|
-
},
|
|
87
|
-
bodyAttachment: {
|
|
88
|
-
type: "string",
|
|
89
|
-
},
|
|
90
|
-
},
|
|
91
|
-
required: ["status", "headers", "body"],
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
type: "null",
|
|
95
|
-
},
|
|
96
|
-
],
|
|
97
|
-
},
|
|
98
|
-
error: {
|
|
99
|
-
type: "string",
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
required: ["id", "sequence", "startedAt", "durationMs", "request", "response"],
|
|
103
|
-
} ;
|
|
104
|
-
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
4
|
+
export { qaEvidenceReferencesSchema, qaHttpExchangeSchema } from "./generated/qa-schemas.js";
|
|
5
|
+
const ajv = createQaAjv();
|
|
105
6
|
const validate = ajv.compile(qaHttpExchangeSchema);
|
|
106
7
|
export function parseQaHttpExchange(value ) {
|
|
107
8
|
if (!validate(value))
|
|
@@ -109,36 +10,7 @@ export function parseQaHttpExchange(value ) {
|
|
|
109
10
|
return value ;
|
|
110
11
|
}
|
|
111
12
|
|
|
112
|
-
|
|
113
|
-
type: "array",
|
|
114
|
-
items: {
|
|
115
|
-
oneOf: [
|
|
116
|
-
{
|
|
117
|
-
type: "object",
|
|
118
|
-
additionalProperties: false,
|
|
119
|
-
required: ["kind", "exchangeId", "side", "label"],
|
|
120
|
-
properties: {
|
|
121
|
-
kind: { const: "http" },
|
|
122
|
-
exchangeId: { type: "string", minLength: 1 },
|
|
123
|
-
side: { enum: ["request", "response"] },
|
|
124
|
-
label: { type: "string" },
|
|
125
|
-
},
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
type: "object",
|
|
129
|
-
additionalProperties: false,
|
|
130
|
-
required: ["kind", "filename", "label"],
|
|
131
|
-
properties: {
|
|
132
|
-
kind: { const: "attachment" },
|
|
133
|
-
filename: { type: "string", minLength: 1 },
|
|
134
|
-
label: { type: "string" },
|
|
135
|
-
},
|
|
136
|
-
},
|
|
137
|
-
],
|
|
138
|
-
},
|
|
139
|
-
} ;
|
|
140
|
-
|
|
141
|
-
const validateReferences = new Ajv({ strict: false }).compile(qaEvidenceReferencesSchema);
|
|
13
|
+
const validateReferences = createQaAjv().compile(qaEvidenceReferencesSchema);
|
|
142
14
|
export function parseQaEvidenceReferences(value ) {
|
|
143
15
|
if (!validateReferences(value)) throw new Error("QA_EVIDENCE_REFERENCES_INVALID");
|
|
144
16
|
return value ;
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { QaHostCommandPayload } from "@ai-platform/api-contracts";
|
|
2
2
|
type Project = Omit<NonNullable<QaHostCommandPayload["project"]>, "mainBranch">;
|
|
3
3
|
export declare function qaProjectDirectory(project: Project): string;
|
|
4
|
-
|
|
4
|
+
/** QA 检出复用平台受控 Git 执行器:超时、中断清理与输出上限同公共实现,不自动重试。 */
|
|
5
|
+
export declare function gitQa(args: string[], cwd?: string, signal?: AbortSignal): Promise<string>;
|
|
5
6
|
/** 同一 Host 的同项目任务排队;文件锁同时保护 CLI 与 daemon。 */
|
|
6
7
|
export declare function withQaProject<T>(project: Project, work: () => Promise<T>): Promise<T>;
|
|
7
8
|
/** 仅更新 Host 专用检出,不接触开发者工作目录。调用期间必须持有项目锁。 */
|
|
8
9
|
export declare function prepareQaCheckout({ project }: {
|
|
9
10
|
project: Project;
|
|
10
|
-
}): Promise<
|
|
11
|
+
}): Promise<{
|
|
12
|
+
directory: string;
|
|
13
|
+
commitSha: string;
|
|
14
|
+
}>;
|
|
11
15
|
export {};
|
|
12
16
|
//# sourceMappingURL=qa-project-checkout.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qa-project-checkout.d.ts","sourceRoot":"","sources":["../src/qa-project-checkout.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"qa-project-checkout.d.ts","sourceRoot":"","sources":["../src/qa-project-checkout.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC;AAOvE,KAAK,OAAO,GAAG,IAAI,CAAC,WAAW,CAAC,oBAAoB,CAAC,SAAS,CAAC,CAAC,EAAE,YAAY,CAAC,CAAC;AAEhF,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,UAGlD;AACD,mDAAmD;AACnD,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,WAAW,mBAe7E;AAED,6CAA6C;AAC7C,wBAAsB,aAAa,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAoB3F;AAED,4CAA4C;AAC5C,wBAAsB,iBAAiB,CAAC,EAAE,OAAO,EAAE,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE;;;GAqBxE"}
|
|
@@ -1,19 +1,30 @@
|
|
|
1
|
-
import { execFile } from "node:child_process";
|
|
2
1
|
import { access, mkdir, realpath } from "node:fs/promises";
|
|
3
2
|
import { join, resolve } from "node:path";
|
|
4
|
-
import { promisify } from "node:util";
|
|
5
3
|
import lockfile from "proper-lockfile";
|
|
4
|
+
import { execGitFile, GitProcessError, gitFailure } from "./git-process.js";
|
|
6
5
|
import { ensureSshOrigin, gitTransportUrl } from "./git-transport.js";
|
|
6
|
+
import { sanitizeGitOutput } from "./host-log.js";
|
|
7
7
|
import { paths } from "./paths.js";
|
|
8
|
-
const exec = promisify(execFile);
|
|
9
8
|
const pending = new Map();
|
|
10
9
|
export function qaProjectDirectory(project) {
|
|
11
10
|
if (!/^[a-zA-Z0-9_-]+$/u.test(project.id))
|
|
12
11
|
throw new Error("QA_PROJECT_ID_INVALID");
|
|
13
12
|
return join(paths().workspaceRoot, "qa-projects", project.id);
|
|
14
13
|
}
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
/** QA 检出复用平台受控 Git 执行器:超时、中断清理与输出上限同公共实现,不自动重试。 */
|
|
15
|
+
export async function gitQa(args, cwd, signal) {
|
|
16
|
+
try {
|
|
17
|
+
const { stdout } = await execGitFile("git", args, {
|
|
18
|
+
...(cwd ? { cwd } : {}),
|
|
19
|
+
...(signal ? { signal } : {}),
|
|
20
|
+
});
|
|
21
|
+
return stdout.trim();
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
// 失败信息保留脱敏后的 Git 输出,只带子命令名,不回显参数或仓库地址。
|
|
25
|
+
const stderr = error instanceof GitProcessError ? sanitizeGitOutput(error.stderr).trim() : "";
|
|
26
|
+
throw gitFailure(`QA_REPOSITORY_GIT_FAILED(git ${args[0] ?? ""})${stderr ? `:${stderr.slice(-2000)}` : ""}`, error);
|
|
27
|
+
}
|
|
17
28
|
}
|
|
18
29
|
/** 同一 Host 的同项目任务排队;文件锁同时保护 CLI 与 daemon。 */
|
|
19
30
|
export async function withQaProject(project, work) {
|
|
@@ -42,8 +53,6 @@ export async function withQaProject(project, work) {
|
|
|
42
53
|
}
|
|
43
54
|
/** 仅更新 Host 专用检出,不接触开发者工作目录。调用期间必须持有项目锁。 */
|
|
44
55
|
export async function prepareQaCheckout({ project }) {
|
|
45
|
-
if (!project.commitSha || !/^[a-f\d]{40}$/iu.test(project.commitSha))
|
|
46
|
-
throw new Error("QA_COMMIT_REQUIRED");
|
|
47
56
|
if (!project.remoteUrl || project.remoteUrl.startsWith("-"))
|
|
48
57
|
throw new Error("QA_REMOTE_INVALID");
|
|
49
58
|
const transport = gitTransportUrl(project.remoteUrl, project.sshRemoteUrl);
|
|
@@ -62,12 +71,10 @@ export async function prepareQaCheckout({ project }) {
|
|
|
62
71
|
if ((await realpath(resolve(repository, common))) !== (await realpath(join(repository, ".git"))))
|
|
63
72
|
throw new Error("QA_REPOSITORY_NOT_ISOLATED");
|
|
64
73
|
await ensureSshOrigin(repository, project.remoteUrl, project.sshRemoteUrl);
|
|
65
|
-
await gitQa(["fetch", "--no-tags", "origin", project.
|
|
74
|
+
await gitQa(["fetch", "--no-tags", "origin", `refs/heads/${project.branch}`], repository);
|
|
66
75
|
// 上一次报告已独立保存;清理专用仓库内产物,避免将旧结果当成本次结果。
|
|
67
76
|
await gitQa(["clean", "-ffdx"], repository);
|
|
68
|
-
await gitQa(["checkout", "--detach", "--force",
|
|
69
|
-
|
|
70
|
-
throw new Error("QA_CHECKOUT_MISMATCH");
|
|
71
|
-
return repository;
|
|
77
|
+
await gitQa(["checkout", "--detach", "--force", "FETCH_HEAD"], repository);
|
|
78
|
+
return { directory: repository, commitSha: await gitQa(["rev-parse", "HEAD"], repository) };
|
|
72
79
|
}
|
|
73
80
|
//# sourceMappingURL=qa-project-checkout.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qa-project-checkout.js","sourceRoot":"","sources":["../src/qa-project-checkout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,
|
|
1
|
+
{"version":3,"file":"qa-project-checkout.js","sourceRoot":"","sources":["../src/qa-project-checkout.ts"],"names":[],"mappings":"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"}
|
|
@@ -43,6 +43,8 @@ export interface QaSessionClient {
|
|
|
43
43
|
contentBase64: string;
|
|
44
44
|
}): Promise<QaAttachmentView>;
|
|
45
45
|
qaSubmitVerification: PlatformClient["qaSubmitVerification"];
|
|
46
|
+
qaReuseVerification: PlatformClient["qaReuseVerification"];
|
|
47
|
+
qaRequestWaiver: PlatformClient["qaRequestWaiver"];
|
|
46
48
|
qaSubmitConclusion: PlatformClient["qaSubmitConclusion"];
|
|
47
49
|
}
|
|
48
50
|
export declare class QaSessionManager {
|
|
@@ -66,13 +68,8 @@ export declare function readQaAssetState(cwd: string, remoteUrl: string, expecte
|
|
|
66
68
|
sourceCommitSha: string;
|
|
67
69
|
mainCommitSha: string;
|
|
68
70
|
hasChanges: boolean;
|
|
71
|
+
hasLocalChanges: boolean;
|
|
69
72
|
}>;
|
|
70
|
-
export declare function verifyQaAssetMerge(cwd: string, remoteUrl: string, expectedBranch: string, mainBranch: string, sshRemoteUrl?: string | null): Promise<{
|
|
71
|
-
sourceCommitSha: string;
|
|
72
|
-
mainCommitSha: string;
|
|
73
|
-
}>;
|
|
74
|
-
/** 对照批准内容及实际合入结果;主分支其他文件的并行改动不属于本次 QA。 */
|
|
75
|
-
export declare function qaAssetContentChanged(cwd: string, approvedSource: string, approvedMain: string, source: string, main: string): Promise<boolean>;
|
|
76
73
|
export declare function readQaScreenshot(cwd: string, filename: string): Promise<{
|
|
77
74
|
filename: string;
|
|
78
75
|
mediaType: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"qa-session-manager.d.ts","sourceRoot":"","sources":["../src/qa-session-manager.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EACV,6BAA6B,EAC7B,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,KAAK,EAAgB,gBAAgB,EAAE,MAAM,iCAAiC,CAAC;
|
|
1
|
+
{"version":3,"file":"qa-session-manager.d.ts","sourceRoot":"","sources":["../src/qa-session-manager.ts"],"names":[],"mappings":"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"}
|
|
@@ -2,13 +2,14 @@ import { execFile } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { mkdir, open, realpath, stat } from "node:fs/promises";
|
|
5
|
-
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { openPiSession, prepareDeploymentProjects, redactHostText } from "./coding-run-manager.js";
|
|
8
8
|
import { ensureSshOrigin, gitTransportUrl, matchesRepository, SSH_ACCESS_HINT, } from "./git-transport.js";
|
|
9
9
|
import { logGitPreparationFailure, logHostEvent } from "./host-log.js";
|
|
10
10
|
import { liveEmployee } from "./live-employee.js";
|
|
11
11
|
import { localModelUsage } from "./local-model-usage.js";
|
|
12
|
+
import { prepareMergeRequest } from "./merge-requests.js";
|
|
12
13
|
import { paths } from "./paths.js";
|
|
13
14
|
import { PiMessageQueue } from "./pi-message-queue.js";
|
|
14
15
|
import { PlatformClientError } from "./platform-client.js";
|
|
@@ -97,11 +98,22 @@ export class QaSessionManager {
|
|
|
97
98
|
return;
|
|
98
99
|
}
|
|
99
100
|
if (previous.session && !previous.session.isIdle && previous.queue) {
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
101
|
+
const queue = previous.queue;
|
|
102
|
+
const mode = input.message.mode;
|
|
103
|
+
const submission = (async () => {
|
|
104
|
+
await previous.emit?.("MESSAGE_QUEUE_UPDATED", "", {
|
|
105
|
+
messageId: input.message.id,
|
|
106
|
+
deliveryStatus: "QUEUED",
|
|
107
|
+
});
|
|
108
|
+
await queue.submit(input.message.id, mode, input.message.contentMarkdown);
|
|
109
|
+
})();
|
|
110
|
+
previous.submissions.add(submission);
|
|
111
|
+
try {
|
|
112
|
+
await submission;
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
previous.submissions.delete(submission);
|
|
116
|
+
}
|
|
105
117
|
return;
|
|
106
118
|
}
|
|
107
119
|
await previous.done;
|
|
@@ -112,6 +124,7 @@ export class QaSessionManager {
|
|
|
112
124
|
});
|
|
113
125
|
const active = {
|
|
114
126
|
stopping: false,
|
|
127
|
+
submissions: new Set(),
|
|
115
128
|
done: Promise.resolve(),
|
|
116
129
|
ready,
|
|
117
130
|
markReady,
|
|
@@ -187,7 +200,7 @@ export class QaSessionManager {
|
|
|
187
200
|
const live = liveEmployee({
|
|
188
201
|
employeeConfig: client.qaEmployeeConfig.bind(client),
|
|
189
202
|
employeeTool: client.qaEmployeeTool.bind(client),
|
|
190
|
-
}, id, employeeBinding, initial, cwd, false, false, "DEVELOPMENT", () => active.stopping, () => `工单:${workItem.key} ${workItem.title}\n验证方式:${input.verificationMode ?? "AUTO"}\n测试仓库:${preparedWorkspace.testProject?.projectName ?? "未准备;当前目录用于工单证据"}\n当前目录:${cwd}\n业务源码参考(测试分支独立副本,不代表部署环境的运行版本):\n${preparedWorkspace.sourceProjects.map((p) => `${p.projectName}:${p.directory},分支 ${p.branch},提交 ${p.commitSha}`).join("\n") || "未配置业务项目"}`, () => { }, {});
|
|
203
|
+
}, id, employeeBinding, initial, cwd, false, false, "DEVELOPMENT", () => active.stopping, () => `工单:${workItem.key} ${workItem.title}\n当前任务:${input.releaseAssetTask ? `上线准备,仅处理测试资产 ${input.releaseAssetTask.sourceBranch} → ${input.releaseAssetTask.targetBranch},通过 report_qa_asset_merge 提交 MR;不启动 QA 测试。` : "QA 验证"}\n验证方式:${input.verificationMode ?? "AUTO"}\n测试仓库:${preparedWorkspace.testProject?.projectName ?? "未准备;当前目录用于工单证据"}\n当前目录:${cwd}\n业务源码参考(测试分支独立副本,不代表部署环境的运行版本):\n${preparedWorkspace.sourceProjects.map((p) => `${p.projectName}:${p.directory},分支 ${p.branch},提交 ${p.commitSha}`).join("\n") || "未配置业务项目"}`, () => { }, {});
|
|
191
204
|
const opened = await openPiSession({
|
|
192
205
|
cwd,
|
|
193
206
|
sessionDir: join(paths().piSessions, "qa"),
|
|
@@ -275,6 +288,23 @@ export class QaSessionManager {
|
|
|
275
288
|
const prompted = opened.session.prompt(message.contentMarkdown);
|
|
276
289
|
active.markReady();
|
|
277
290
|
await prompted;
|
|
291
|
+
// Pi 1.4.0 只在本次运行收尾前续跑已入队消息;运行中判定后才入队的补充消息会留在
|
|
292
|
+
// 空闲会话队列里。按空闲会话的既有处理方式由同一 QA 继续处理,不取消、不另建队列。
|
|
293
|
+
for (;;) {
|
|
294
|
+
await Promise.allSettled([...active.submissions]);
|
|
295
|
+
const unread = active.stopping ? [] : (active.queue?.takeUnread() ?? []);
|
|
296
|
+
if (!unread.length)
|
|
297
|
+
break;
|
|
298
|
+
for (const item of unread) {
|
|
299
|
+
if (active.stopping)
|
|
300
|
+
break;
|
|
301
|
+
await emit("MESSAGE_QUEUE_UPDATED", "", {
|
|
302
|
+
messageId: item.id,
|
|
303
|
+
deliveryStatus: "READ",
|
|
304
|
+
});
|
|
305
|
+
await opened.session.prompt(item.text);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
278
308
|
}
|
|
279
309
|
await pending;
|
|
280
310
|
if (modelError && !active.stopping)
|
|
@@ -286,6 +316,8 @@ export class QaSessionManager {
|
|
|
286
316
|
throw error;
|
|
287
317
|
}
|
|
288
318
|
finally {
|
|
319
|
+
// 停止或失败时在途补充消息也须先进入 Pi,再如实标记未执行,不遗留为已排队。
|
|
320
|
+
await Promise.allSettled([...active.submissions]);
|
|
289
321
|
for (const messageId of active.queue?.recall() ?? [])
|
|
290
322
|
await emit("MESSAGE_QUEUE_UPDATED", "", { messageId, deliveryStatus: "CANCELLED" }).catch(() => undefined);
|
|
291
323
|
unsubscribe?.();
|
|
@@ -342,15 +374,15 @@ export async function prepareQaProject(input) {
|
|
|
342
374
|
workspaceId: input.workspaceId,
|
|
343
375
|
workItemId: input.workItem.id,
|
|
344
376
|
});
|
|
345
|
-
const cwd = join(workRoot, input.project
|
|
377
|
+
const cwd = join(workRoot, input.project ? qaProjectName(input.project) : "evidence");
|
|
346
378
|
await claimQaDirectory(cwd, {
|
|
347
|
-
projectId: input.project
|
|
379
|
+
projectId: input.project ? input.project.id : "evidence",
|
|
348
380
|
});
|
|
349
|
-
if (!input.project
|
|
381
|
+
if (!input.project) {
|
|
350
382
|
await mkdir(cwd, { recursive: true, mode: 0o700 });
|
|
351
383
|
return cwd;
|
|
352
384
|
}
|
|
353
|
-
const branch = `
|
|
385
|
+
const branch = `aip/${input.workItem.key}`;
|
|
354
386
|
await git("git", ["check-ref-format", "--branch", branch]);
|
|
355
387
|
if (!input.project.mainBranch)
|
|
356
388
|
throw new Error("QA_MAIN_BRANCH_REQUIRED");
|
|
@@ -419,7 +451,7 @@ export async function prepareQaWorkspace(input, cwd) {
|
|
|
419
451
|
sourceProjects.push(await readPreparedProject(project.id, project.name, directory));
|
|
420
452
|
}
|
|
421
453
|
return {
|
|
422
|
-
testProject: input.project
|
|
454
|
+
testProject: input.project
|
|
423
455
|
? await readPreparedProject(input.project.id, input.project.name, cwd)
|
|
424
456
|
: null,
|
|
425
457
|
sourceProjects,
|
|
@@ -465,47 +497,26 @@ export async function readQaAssetState(cwd, remoteUrl, expectedBranch, mainBranc
|
|
|
465
497
|
const sourceCommitSha = source.trim();
|
|
466
498
|
const mainCommitSha = main.trim();
|
|
467
499
|
const remoteSource = remote.trim().split(/\s+/)[0];
|
|
468
|
-
let hasChanges =
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
await git("git", ["merge-base", "--is-ancestor", sourceCommitSha, mainCommitSha], { cwd });
|
|
472
|
-
}
|
|
473
|
-
catch (error) {
|
|
474
|
-
if (error.code !== 1)
|
|
475
|
-
throw error;
|
|
476
|
-
hasChanges = true;
|
|
477
|
-
}
|
|
500
|
+
let hasChanges = false;
|
|
501
|
+
try {
|
|
502
|
+
await git("git", ["merge-base", "--is-ancestor", sourceCommitSha, mainCommitSha], { cwd });
|
|
478
503
|
}
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
const { hasChanges, ...commits } = await readQaAssetState(cwd, remoteUrl, expectedBranch, mainBranch, sshRemoteUrl);
|
|
483
|
-
if (hasChanges)
|
|
484
|
-
throw new Error("QA_TEST_ASSET_NOT_MERGED:存在未提交、未推送或远端主分支尚未包含的测试资产");
|
|
485
|
-
return commits;
|
|
486
|
-
}
|
|
487
|
-
/** 对照批准内容及实际合入结果;主分支其他文件的并行改动不属于本次 QA。 */
|
|
488
|
-
export async function qaAssetContentChanged(cwd, approvedSource, approvedMain, source, main) {
|
|
489
|
-
async function differs(from, to, files = []) {
|
|
504
|
+
catch (error) {
|
|
505
|
+
if (error.code !== 1)
|
|
506
|
+
throw error;
|
|
490
507
|
try {
|
|
491
|
-
await git("git", ["
|
|
492
|
-
cwd,
|
|
493
|
-
});
|
|
494
|
-
return false;
|
|
508
|
+
await git("git", ["diff", "--quiet", `${mainCommitSha}...${sourceCommitSha}`, "--"], { cwd });
|
|
495
509
|
}
|
|
496
510
|
catch (error) {
|
|
497
511
|
if (error.code !== 1)
|
|
498
512
|
throw error;
|
|
499
|
-
|
|
513
|
+
hasChanges = true;
|
|
500
514
|
}
|
|
501
515
|
}
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
const { stdout } = await git("git", ["diff", "--name-only", "--no-renames", "-z", base.trim(), approvedSource, "--"], { cwd });
|
|
507
|
-
const files = stdout.split("\0").filter(Boolean);
|
|
508
|
-
return files.length > 0 && (await differs(approvedSource, main, files));
|
|
516
|
+
const hasLocalChanges = Boolean(status.trim()) ||
|
|
517
|
+
Boolean(remoteSource && remoteSource !== sourceCommitSha) ||
|
|
518
|
+
(!remoteSource && hasChanges);
|
|
519
|
+
return { sourceCommitSha, mainCommitSha, hasChanges, hasLocalChanges };
|
|
509
520
|
}
|
|
510
521
|
export async function readQaScreenshot(cwd, filename) {
|
|
511
522
|
const root = await realpath(cwd), path = await realpath(resolve(cwd, filename));
|
|
@@ -541,7 +552,7 @@ export function qaTools(client, input, binding, cwd, definitions) {
|
|
|
541
552
|
throw new Error("QA_SESSION_COMMAND_INVALID");
|
|
542
553
|
const workItem = input.workItem;
|
|
543
554
|
const requireTestProject = () => {
|
|
544
|
-
if (!input.project
|
|
555
|
+
if (!input.project)
|
|
545
556
|
throw new Error("QA_TEST_PROJECT_REQUIRED:当前会话未准备测试项目,请补齐配置后继续");
|
|
546
557
|
return cwd;
|
|
547
558
|
};
|
|
@@ -594,45 +605,47 @@ export function qaTools(client, input, binding, cwd, definitions) {
|
|
|
594
605
|
}));
|
|
595
606
|
}, api));
|
|
596
607
|
api.registerTool(bindHostTool(definitions, "submit_qa_verification", async (callId, args) => {
|
|
597
|
-
if (args.sourceVerificationId)
|
|
598
|
-
return result(await client.qaSubmitVerification(id, {
|
|
599
|
-
...binding,
|
|
600
|
-
toolCallId: callId,
|
|
601
|
-
sourceVerificationId: args.sourceVerificationId,
|
|
602
|
-
testItemIds: args.testItemIds,
|
|
603
|
-
...(args.content === undefined ? {} : { content: args.content }),
|
|
604
|
-
}));
|
|
605
|
-
if (!args.content?.trim() || !args.conclusion)
|
|
606
|
-
throw new Error("QA_VERIFICATION_INVALID:请填写真实验证说明与结论,或引用 sourceVerificationId");
|
|
607
608
|
if (args.conclusion === "UNVERIFIABLE" && !args.unverifiableReason?.trim())
|
|
608
|
-
throw new Error("QA_VERIFICATION_DETAILS_REQUIRED
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
609
|
+
throw new Error("QA_VERIFICATION_DETAILS_REQUIRED:unverifiableReason 无法验证时须填写真实原因");
|
|
610
|
+
const screenshots = [];
|
|
611
|
+
for (const [index, file] of args.screenshots.entries()) {
|
|
612
|
+
try {
|
|
613
|
+
screenshots.push(await readQaScreenshot(cwd, file));
|
|
614
|
+
}
|
|
615
|
+
catch (error) {
|
|
616
|
+
const code = error.code ?? error.message;
|
|
617
|
+
const feedback = {
|
|
618
|
+
ENOENT: "文件不存在,请确认截图已保存并填写当前工作目录中的真实路径。",
|
|
619
|
+
EACCES: "文件不可读,请检查当前 Host 的文件读取权限。",
|
|
620
|
+
EPERM: "文件不可读,请检查当前 Host 的文件读取权限。",
|
|
621
|
+
QA_SCREENSHOT_OUTSIDE_WORKSPACE: "路径不在当前工作目录中,请先将真实截图保存在当前工作目录内。",
|
|
622
|
+
QA_SCREENSHOT_TYPE_INVALID: "文件类型不支持,请提供 PNG、JPEG 或 WebP 截图。",
|
|
623
|
+
QA_SCREENSHOT_SIZE_INVALID: "不是普通文件或超过 10 MiB,请提供符合大小限制的截图文件。",
|
|
624
|
+
};
|
|
625
|
+
if (code && feedback[code])
|
|
626
|
+
throw new Error(`QA_SCREENSHOT_INVALID:screenshots[${index}] ${feedback[code]}`);
|
|
627
|
+
throw error;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
612
630
|
const attachmentIds = [];
|
|
613
|
-
for (const
|
|
614
|
-
attachmentIds.push((await client.qaUploadSessionAttachment(id, {
|
|
615
|
-
...binding,
|
|
616
|
-
...(await readQaScreenshot(cwd, file)),
|
|
617
|
-
})).id);
|
|
631
|
+
for (const screenshot of screenshots)
|
|
632
|
+
attachmentIds.push((await client.qaUploadSessionAttachment(id, { ...binding, ...screenshot })).id);
|
|
618
633
|
return result(await client.qaSubmitVerification(id, {
|
|
619
634
|
...binding,
|
|
620
635
|
toolCallId: callId,
|
|
621
|
-
content: args.content,
|
|
636
|
+
...(args.content === undefined ? {} : { content: args.content }),
|
|
622
637
|
testItemIds: args.testItemIds,
|
|
623
|
-
|
|
624
|
-
|
|
638
|
+
expected: args.expected,
|
|
639
|
+
actual: args.actual,
|
|
625
640
|
...(args.checkpoints === undefined ? {} : { checkpoints: args.checkpoints }),
|
|
626
|
-
...(args.waiverReason === undefined ? {} : { waiverReason: args.waiverReason }),
|
|
627
|
-
...(args.waiverRisk === undefined ? {} : { waiverRisk: args.waiverRisk }),
|
|
628
|
-
...(args.waiverScope === undefined ? {} : { waiverScope: args.waiverScope }),
|
|
629
641
|
...(args.unverifiableReason ? { unverifiableReason: args.unverifiableReason } : {}),
|
|
630
|
-
|
|
642
|
+
steps: args.steps,
|
|
631
643
|
conclusion: args.conclusion,
|
|
632
644
|
attachmentIds,
|
|
633
|
-
...(args.ac ? { ac: args.ac } : {}),
|
|
634
645
|
}));
|
|
635
646
|
}, api));
|
|
647
|
+
api.registerTool(bindHostTool(definitions, "reuse_qa_verification", async (callId, args) => result(await client.qaReuseVerification(id, { ...args, ...binding, toolCallId: callId })), api));
|
|
648
|
+
api.registerTool(bindHostTool(definitions, "request_qa_waiver", async (callId, args) => result(await client.qaRequestWaiver(id, { ...args, ...binding, toolCallId: callId })), api));
|
|
636
649
|
api.registerTool(bindHostTool(definitions, "report_qa_defects", async (callId, args) => {
|
|
637
650
|
return result(await client.qaReportDefects(id, {
|
|
638
651
|
defects: args.defects,
|
|
@@ -657,9 +670,9 @@ export function qaTools(client, input, binding, cwd, definitions) {
|
|
|
657
670
|
api.registerTool(bindHostTool(definitions, "complete_qa_review", async (callId, args) => {
|
|
658
671
|
return result(await client.qaCompleteReview(id, {
|
|
659
672
|
summary: args.summary,
|
|
660
|
-
...(input.project
|
|
673
|
+
...(input.project
|
|
661
674
|
? {
|
|
662
|
-
assetState: await readQaAssetState(cwd, input.project.remoteUrl, `
|
|
675
|
+
assetState: await readQaAssetState(cwd, input.project.remoteUrl, `aip/${workItem.key}`, requireQaMainBranch(input), input.project.sshRemoteUrl),
|
|
663
676
|
}
|
|
664
677
|
: {}),
|
|
665
678
|
...binding,
|
|
@@ -667,26 +680,39 @@ export function qaTools(client, input, binding, cwd, definitions) {
|
|
|
667
680
|
}));
|
|
668
681
|
}, api));
|
|
669
682
|
api.registerTool(bindHostTool(definitions, "report_qa_asset_merge", async (callId, args) => {
|
|
670
|
-
if (!input.acceptanceDeliveryId)
|
|
671
|
-
throw new Error("QA_ACCEPTANCE_CONFIRMATION_REQUIRED");
|
|
672
683
|
requireTestProject();
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
let
|
|
679
|
-
if (
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
684
|
+
const task = input.releaseAssetTask;
|
|
685
|
+
if (!input.project || !task)
|
|
686
|
+
throw new Error("QA_RELEASE_TASK_REQUIRED:当前没有测试资产上线准备任务");
|
|
687
|
+
if (task.projectId !== input.project.id)
|
|
688
|
+
throw new Error("QA_RELEASE_PROJECT_MISMATCH");
|
|
689
|
+
let mergeRequest;
|
|
690
|
+
if (args.status === "SUCCEEDED") {
|
|
691
|
+
const status = await git("git", ["status", "--porcelain", "--untracked-files=all"], {
|
|
692
|
+
cwd,
|
|
693
|
+
});
|
|
694
|
+
if (status.stdout.trim())
|
|
695
|
+
throw new Error("QA_RELEASE_WORKTREE_DIRTY:请提交测试资产修改后再准备 MR");
|
|
696
|
+
mergeRequest = await prepareMergeRequest({
|
|
697
|
+
project: {
|
|
698
|
+
id: task.projectId,
|
|
699
|
+
name: input.project.name,
|
|
700
|
+
repositoryUrl: input.project.remoteUrl,
|
|
701
|
+
devBranch: task.sourceBranch,
|
|
702
|
+
baselineBranch: task.targetBranch,
|
|
703
|
+
},
|
|
704
|
+
cwd,
|
|
705
|
+
title: args.title ?? `${workItem.key} 测试资产`,
|
|
706
|
+
description: args.description ?? args.summary,
|
|
707
|
+
updateExisting: args.updateExisting === true,
|
|
708
|
+
pendingFile: join(dirname(cwd), `release-mr-${task.id}.json`),
|
|
709
|
+
});
|
|
683
710
|
}
|
|
684
711
|
return result(await client.qaReportAssetMerge(id, {
|
|
685
|
-
|
|
686
|
-
...(args.status === "SUCCEEDED" ? { contentChanged } : {}),
|
|
712
|
+
releaseAssetTaskId: task.id,
|
|
687
713
|
status: args.status,
|
|
688
714
|
summary: args.summary,
|
|
689
|
-
...
|
|
715
|
+
...(mergeRequest ? { mergeRequest } : {}),
|
|
690
716
|
...binding,
|
|
691
717
|
toolCallId: callId,
|
|
692
718
|
}));
|