@mytegroupinc/myte-core 0.0.54 → 0.0.56
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 +211 -182
- package/THIRD_PARTY_NOTICES.md +21 -21
- package/TRADEMARKS.md +10 -10
- package/cli.js +2822 -2489
- package/lib/certification-manifest.js +180 -178
- package/lib/mytecody-release-public-key.pem +11 -11
- package/lib/mytecody-splash.js +251 -251
- package/mytecody-cli.js +2040 -2040
- package/package.json +8 -8
- package/scripts/feedback-certification-cleanup.js +53 -53
- package/scripts/feedback-live-full-harness.js +1509 -1364
- package/scripts/mission-live-full-harness.js +10 -9
- package/scripts/project-assistant-live-full-harness.js +208 -0
- package/scripts/project-assistant-read-certification.js +470 -470
|
@@ -2,52 +2,53 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
4
|
const fs = require("node:fs");
|
|
5
|
-
const os = require("node:os");
|
|
6
|
-
const path = require("node:path");
|
|
7
|
-
const { spawnSync } = require("node:child_process");
|
|
8
|
-
const {
|
|
9
|
-
createCertificationManifest,
|
|
10
|
-
recordCertificationArtifact,
|
|
11
|
-
validateCertificationManifest,
|
|
12
|
-
} = require("../lib/certification-manifest");
|
|
13
|
-
const { readYamlFile } = require("../cli");
|
|
14
|
-
|
|
15
|
-
const CLI_PATH = path.resolve(__dirname, "..", "cli.js");
|
|
16
|
-
const LIVE_SCENARIO_MATRIX = [
|
|
17
|
-
{ id: "doctor", kind: "transport", mutation: "none", required: true },
|
|
18
|
-
{ id: "config", kind: "scope", mutation: "none", required: true },
|
|
19
|
-
{ id: "create-independent-prds", kind: "write", mutation: "test_feedback", required: true },
|
|
20
|
-
{ id: "create-three-document-prd", kind: "write", mutation: "test_feedback", required: true },
|
|
21
|
-
{ id: "feedback-sync", kind: "read/local-sync", mutation: "local_temp_only", required: true },
|
|
22
|
-
{ id: "feedback-
|
|
23
|
-
{ id: "feedback-
|
|
24
|
-
{ id: "feedback-comment
|
|
25
|
-
{
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
{ id: "feedback-
|
|
33
|
-
{ id: "feedback-
|
|
34
|
-
{ id: "feedback-
|
|
35
|
-
{ id: "feedback-
|
|
36
|
-
{ id: "feedback-
|
|
37
|
-
{ id: "feedback-
|
|
38
|
-
{ id: "feedback-
|
|
39
|
-
{ id: "feedback-
|
|
40
|
-
{ id: "feedback-
|
|
41
|
-
{ id: "feedback-
|
|
42
|
-
{ id: "
|
|
43
|
-
{
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
5
|
+
const os = require("node:os");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const { spawnSync } = require("node:child_process");
|
|
8
|
+
const {
|
|
9
|
+
createCertificationManifest,
|
|
10
|
+
recordCertificationArtifact,
|
|
11
|
+
validateCertificationManifest,
|
|
12
|
+
} = require("../lib/certification-manifest");
|
|
13
|
+
const { readYamlFile } = require("../cli");
|
|
14
|
+
|
|
15
|
+
const CLI_PATH = path.resolve(__dirname, "..", "cli.js");
|
|
16
|
+
const LIVE_SCENARIO_MATRIX = [
|
|
17
|
+
{ id: "doctor", kind: "transport", mutation: "none", required: true },
|
|
18
|
+
{ id: "config", kind: "scope", mutation: "none", required: true },
|
|
19
|
+
{ id: "create-independent-prds", kind: "write", mutation: "test_feedback", required: true },
|
|
20
|
+
{ id: "create-three-document-prd", kind: "write", mutation: "test_feedback", required: true },
|
|
21
|
+
{ id: "feedback-sync", kind: "read/local-sync", mutation: "local_temp_only", required: true },
|
|
22
|
+
{ id: "feedback-ticket-batch-lifecycle", kind: "synchronous-governed-write", mutation: "test_feedback_ticket", required: true },
|
|
23
|
+
{ id: "feedback-get-history", kind: "read", mutation: "none", required: true },
|
|
24
|
+
{ id: "feedback-comment", kind: "write", mutation: "test_comment", required: true },
|
|
25
|
+
{ id: "feedback-comment-sync-roundtrip", kind: "read/local-sync", mutation: "local_temp_only", required: true },
|
|
26
|
+
{
|
|
27
|
+
id: "feedback-comment-attachment-sync",
|
|
28
|
+
kind: "web-write/read/local-sync",
|
|
29
|
+
mutation: "test_comment_attachment",
|
|
30
|
+
required: "when --require-comment-attachment-sync is set",
|
|
31
|
+
},
|
|
32
|
+
{ id: "feedback-stale-edit-guard", kind: "conflict-safety", mutation: "test_feedback", required: true },
|
|
33
|
+
{ id: "feedback-forced-stale-override", kind: "owner-conflict-override", mutation: "test_feedback", required: true },
|
|
34
|
+
{ id: "feedback-move-undo", kind: "governed-write", mutation: "test_feedback", required: true },
|
|
35
|
+
{ id: "feedback-validate-submit", kind: "review", mutation: "test_review", required: true },
|
|
36
|
+
{ id: "feedback-review-list-detail", kind: "read", mutation: "none", required: true },
|
|
37
|
+
{ id: "feedback-request-changes-revise", kind: "review", mutation: "test_review", required: true },
|
|
38
|
+
{ id: "feedback-reject-cancel", kind: "review", mutation: "test_review", required: true },
|
|
39
|
+
{ id: "feedback-batch-review", kind: "governed-write", mutation: "test_review", required: true },
|
|
40
|
+
{ id: "feedback-document-refinement", kind: "review", mutation: "test_feedback", required: true },
|
|
41
|
+
{ id: "feedback-prd-versions-diff", kind: "read", mutation: "none", required: true },
|
|
42
|
+
{ id: "feedback-batch-move-archive", kind: "governed-write", mutation: "test_feedback", required: true },
|
|
43
|
+
{ id: "project-key-unarchive-denial", kind: "negative-permission", mutation: "none", required: true },
|
|
44
|
+
{
|
|
45
|
+
id: "owner-delegate-collaborator-matrix",
|
|
46
|
+
kind: "role-permission",
|
|
47
|
+
mutation: "test_feedback",
|
|
48
|
+
required: "when --require-role-matrix is set",
|
|
49
|
+
},
|
|
50
|
+
{ id: "exact-id-cleanup", kind: "cleanup", mutation: "archive_test_feedback", required: true },
|
|
51
|
+
];
|
|
51
52
|
|
|
52
53
|
function parseArgs(argv) {
|
|
53
54
|
const args = { _: [] };
|
|
@@ -69,32 +70,32 @@ function parseArgs(argv) {
|
|
|
69
70
|
return args;
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
function requireConfirm(args) {
|
|
73
|
-
if (!args["confirm-live"]) {
|
|
74
|
-
throw new Error("Refusing live feedback mutations. Re-run with --confirm-live after backend/web deploy.");
|
|
73
|
+
function requireConfirm(args) {
|
|
74
|
+
if (!args["confirm-live"]) {
|
|
75
|
+
throw new Error("Refusing live feedback mutations. Re-run with --confirm-live after backend/web deploy.");
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
!process.env.MYTE_OWNER_API_KEY
|
|
79
|
+
&& !process.env.MYTE_API_KEY
|
|
80
|
+
&& !process.env.MYTE_PROJECT_API_KEY
|
|
81
|
+
) {
|
|
82
|
+
throw new Error("Missing MYTE_OWNER_API_KEY, MYTE_API_KEY, or MYTE_PROJECT_API_KEY.");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
|
|
87
|
+
const env = { ...process.env };
|
|
88
|
+
delete env.MYTE_OWNER_API_KEY;
|
|
89
|
+
delete env.MYTE_DELEGATE_API_KEY;
|
|
90
|
+
delete env.MYTE_COLLABORATOR_API_KEY;
|
|
91
|
+
delete env.MYTE_WEB_ACCESS_TOKEN;
|
|
92
|
+
if (apiKey) {
|
|
93
|
+
env.MYTE_API_KEY = apiKey;
|
|
94
|
+
env.MYTE_PROJECT_API_KEY = apiKey;
|
|
75
95
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
&& !process.env.MYTE_PROJECT_API_KEY
|
|
80
|
-
) {
|
|
81
|
-
throw new Error("Missing MYTE_OWNER_API_KEY, MYTE_API_KEY, or MYTE_PROJECT_API_KEY.");
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
|
|
86
|
-
const env = { ...process.env };
|
|
87
|
-
delete env.MYTE_OWNER_API_KEY;
|
|
88
|
-
delete env.MYTE_DELEGATE_API_KEY;
|
|
89
|
-
delete env.MYTE_COLLABORATOR_API_KEY;
|
|
90
|
-
delete env.MYTE_WEB_ACCESS_TOKEN;
|
|
91
|
-
if (apiKey) {
|
|
92
|
-
env.MYTE_API_KEY = apiKey;
|
|
93
|
-
env.MYTE_PROJECT_API_KEY = apiKey;
|
|
94
|
-
}
|
|
95
|
-
const result = spawnSync(process.execPath, [CLI_PATH, ...cliArgs], {
|
|
96
|
-
cwd,
|
|
97
|
-
env,
|
|
96
|
+
const result = spawnSync(process.execPath, [CLI_PATH, ...cliArgs], {
|
|
97
|
+
cwd,
|
|
98
|
+
env,
|
|
98
99
|
encoding: "utf8",
|
|
99
100
|
stdio: ["ignore", "pipe", "pipe"],
|
|
100
101
|
});
|
|
@@ -113,75 +114,75 @@ function runCli(cliArgs, cwd, { allowFailure = false, apiKey } = {}) {
|
|
|
113
114
|
result.stderr || stdout || "(no output)",
|
|
114
115
|
].join("\n"));
|
|
115
116
|
}
|
|
116
|
-
return { status: result.status, stdout: parsed, stderr: String(result.stderr || "").trim() };
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function resolveHarnessApiBase(args) {
|
|
120
|
-
const raw = String(
|
|
121
|
-
args["base-url"]
|
|
122
|
-
|| process.env.MYTE_API_BASE
|
|
123
|
-
|| "https://api.myte.dev",
|
|
124
|
-
).trim().replace(/\/+$/, "");
|
|
125
|
-
return raw.endsWith("/api") ? raw : `${raw}/api`;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function createWebFeedbackCommentWithAttachment({
|
|
129
|
-
apiBase,
|
|
130
|
-
accessToken,
|
|
131
|
-
feedbackId,
|
|
132
|
-
projectId,
|
|
133
|
-
namespace,
|
|
134
|
-
timeoutMs,
|
|
135
|
-
}) {
|
|
136
|
-
const attachmentName = `${namespace}-comment-context.md`;
|
|
137
|
-
const attachmentContent = [
|
|
138
|
-
`# ${namespace} Comment Attachment`,
|
|
139
|
-
"",
|
|
140
|
-
"This disposable document proves that a web-created Feedback comment attachment",
|
|
141
|
-
"remains associated with its comment and becomes readable after Project Assistant sync.",
|
|
142
|
-
"",
|
|
143
|
-
].join("\n");
|
|
144
|
-
const controller = new AbortController();
|
|
145
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
146
|
-
let response;
|
|
147
|
-
let body;
|
|
148
|
-
try {
|
|
149
|
-
response = await fetch(`${apiBase}/feedbacks/${feedbackId}/comments`, {
|
|
150
|
-
method: "POST",
|
|
151
|
-
headers: {
|
|
152
|
-
Authorization: `Bearer ${accessToken}`,
|
|
153
|
-
"Content-Type": "application/json",
|
|
154
|
-
},
|
|
155
|
-
body: JSON.stringify({
|
|
156
|
-
project_id: projectId,
|
|
157
|
-
content: `${namespace} web attachment sync certification comment.`,
|
|
158
|
-
attachments: [
|
|
159
|
-
{
|
|
160
|
-
name: attachmentName,
|
|
161
|
-
data: `data:text/markdown;base64,${Buffer.from(attachmentContent, "utf8").toString("base64")}`,
|
|
162
|
-
size: Buffer.byteLength(attachmentContent, "utf8"),
|
|
163
|
-
type: "text/markdown",
|
|
164
|
-
},
|
|
165
|
-
],
|
|
166
|
-
}),
|
|
167
|
-
signal: controller.signal,
|
|
168
|
-
});
|
|
169
|
-
body = await response.json();
|
|
170
|
-
} finally {
|
|
171
|
-
clearTimeout(timeout);
|
|
172
|
-
}
|
|
173
|
-
if (!response.ok || body?.status !== "success" || !body?.data?.comment_id) {
|
|
174
|
-
throw new Error(
|
|
175
|
-
`Web Feedback comment attachment fixture failed (${response.status}): ${body?.message || "unknown response"}`,
|
|
176
|
-
);
|
|
177
|
-
}
|
|
178
|
-
return {
|
|
179
|
-
commentId: String(body.data.comment_id),
|
|
180
|
-
commentContent: String(body.data.content || ""),
|
|
181
|
-
attachmentName,
|
|
182
|
-
attachmentContent,
|
|
183
|
-
};
|
|
184
|
-
}
|
|
117
|
+
return { status: result.status, stdout: parsed, stderr: String(result.stderr || "").trim() };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function resolveHarnessApiBase(args) {
|
|
121
|
+
const raw = String(
|
|
122
|
+
args["base-url"]
|
|
123
|
+
|| process.env.MYTE_API_BASE
|
|
124
|
+
|| "https://api.myte.dev",
|
|
125
|
+
).trim().replace(/\/+$/, "");
|
|
126
|
+
return raw.endsWith("/api") ? raw : `${raw}/api`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
async function createWebFeedbackCommentWithAttachment({
|
|
130
|
+
apiBase,
|
|
131
|
+
accessToken,
|
|
132
|
+
feedbackId,
|
|
133
|
+
projectId,
|
|
134
|
+
namespace,
|
|
135
|
+
timeoutMs,
|
|
136
|
+
}) {
|
|
137
|
+
const attachmentName = `${namespace}-comment-context.md`;
|
|
138
|
+
const attachmentContent = [
|
|
139
|
+
`# ${namespace} Comment Attachment`,
|
|
140
|
+
"",
|
|
141
|
+
"This disposable document proves that a web-created Feedback comment attachment",
|
|
142
|
+
"remains associated with its comment and becomes readable after Project Assistant sync.",
|
|
143
|
+
"",
|
|
144
|
+
].join("\n");
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
147
|
+
let response;
|
|
148
|
+
let body;
|
|
149
|
+
try {
|
|
150
|
+
response = await fetch(`${apiBase}/feedbacks/${feedbackId}/comments`, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
headers: {
|
|
153
|
+
Authorization: `Bearer ${accessToken}`,
|
|
154
|
+
"Content-Type": "application/json",
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify({
|
|
157
|
+
project_id: projectId,
|
|
158
|
+
content: `${namespace} web attachment sync certification comment.`,
|
|
159
|
+
attachments: [
|
|
160
|
+
{
|
|
161
|
+
name: attachmentName,
|
|
162
|
+
data: `data:text/markdown;base64,${Buffer.from(attachmentContent, "utf8").toString("base64")}`,
|
|
163
|
+
size: Buffer.byteLength(attachmentContent, "utf8"),
|
|
164
|
+
type: "text/markdown",
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
}),
|
|
168
|
+
signal: controller.signal,
|
|
169
|
+
});
|
|
170
|
+
body = await response.json();
|
|
171
|
+
} finally {
|
|
172
|
+
clearTimeout(timeout);
|
|
173
|
+
}
|
|
174
|
+
if (!response.ok || body?.status !== "success" || !body?.data?.comment_id) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Web Feedback comment attachment fixture failed (${response.status}): ${body?.message || "unknown response"}`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
commentId: String(body.data.comment_id),
|
|
181
|
+
commentContent: String(body.data.content || ""),
|
|
182
|
+
attachmentName,
|
|
183
|
+
attachmentContent,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
185
186
|
|
|
186
187
|
function assert(condition, message) {
|
|
187
188
|
if (!condition) {
|
|
@@ -189,7 +190,7 @@ function assert(condition, message) {
|
|
|
189
190
|
}
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
function writePrd(workspace, title, index) {
|
|
193
|
+
function writePrd(workspace, title, index) {
|
|
193
194
|
const filePath = path.join(workspace, `feedback-live-harness-${index}.md`);
|
|
194
195
|
fs.writeFileSync(
|
|
195
196
|
filePath,
|
|
@@ -208,117 +209,117 @@ function writePrd(workspace, title, index) {
|
|
|
208
209
|
].join("\n"),
|
|
209
210
|
"utf8",
|
|
210
211
|
);
|
|
211
|
-
return filePath;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function writeCreateApproval(workspace, runId, prdFiles) {
|
|
215
|
-
const approvalPath = path.join(workspace, `feedback-live-harness-${runId}-approval.md`);
|
|
216
|
-
fs.writeFileSync(
|
|
217
|
-
approvalPath,
|
|
218
|
-
[
|
|
219
|
-
"# Myte Feedback Certification Approval",
|
|
220
|
-
"",
|
|
221
|
-
`run_id: ${runId}`,
|
|
222
|
-
"command: myte create-prd",
|
|
223
|
-
`batch_count: ${prdFiles.length}`,
|
|
224
|
-
"targets:",
|
|
225
|
-
...prdFiles.map((filePath) => `- ${path.basename(filePath)}`),
|
|
226
|
-
"proposed_change: Create disposable, namespaced TEST Feedback records for post-deploy certification.",
|
|
227
|
-
"reason: Verify the published CLI and deployed Feedback workflow before exact-ID archival.",
|
|
228
|
-
"",
|
|
229
|
-
].join("\n"),
|
|
230
|
-
"utf8",
|
|
231
|
-
);
|
|
232
|
-
return approvalPath;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
function writeCommentApproval(workspace, runId, feedbackId) {
|
|
236
|
-
const filePath = path.join(workspace, `feedback-live-harness-${runId}-comment.md`);
|
|
237
|
-
fs.writeFileSync(
|
|
238
|
-
filePath,
|
|
239
|
-
[
|
|
240
|
-
`# ${runId} Feedback certification comment`,
|
|
241
|
-
"",
|
|
242
|
-
`feedback_id: ${feedbackId}`,
|
|
243
|
-
`run_id: ${runId}`,
|
|
244
|
-
"proposed_change: Record a disposable, namespaced certification comment.",
|
|
245
|
-
"reason: Verify the project-scoped Feedback comment route and idempotency controls.",
|
|
246
|
-
"",
|
|
247
|
-
].join("\n"),
|
|
248
|
-
"utf8",
|
|
249
|
-
);
|
|
250
|
-
return filePath;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
function responseRequestId(payload) {
|
|
254
|
-
return String(
|
|
255
|
-
payload?.request?.request_id
|
|
256
|
-
|| payload?.request?._id
|
|
257
|
-
|| payload?.request_id
|
|
258
|
-
|| payload?._id
|
|
259
|
-
|| "",
|
|
260
|
-
).trim();
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function responseEventId(payload) {
|
|
264
|
-
return String(
|
|
265
|
-
payload?.event?.event_id
|
|
266
|
-
|| payload?.event?._id
|
|
267
|
-
|| payload?.event_id
|
|
268
|
-
|| "",
|
|
269
|
-
).trim();
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
function responseVersionId(version) {
|
|
273
|
-
return String(version?.version_id || version?._id || "").trim();
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function assertDocumentSetMaterialized(workspace, feedbackId, expectedTitles) {
|
|
277
|
-
const root = path.join(
|
|
278
|
-
workspace,
|
|
279
|
-
"MyteCommandCenter",
|
|
280
|
-
"PRD",
|
|
281
|
-
"feedback-sync",
|
|
282
|
-
feedbackId,
|
|
283
|
-
);
|
|
284
|
-
assert(fs.existsSync(root), `Missing synchronized PRD document directory for ${feedbackId}.`);
|
|
285
|
-
const files = fs.readdirSync(root)
|
|
286
|
-
.filter((name) => name.toLowerCase().endsWith(".md"))
|
|
287
|
-
.sort();
|
|
288
|
-
assert(files.length === expectedTitles.length, `Expected ${expectedTitles.length} synchronized PRD files, got ${files.length}.`);
|
|
289
|
-
files.forEach((name, index) => {
|
|
290
|
-
const content = fs.readFileSync(path.join(root, name), "utf8");
|
|
291
|
-
assert(
|
|
292
|
-
content.includes(`# ${expectedTitles[index]}`),
|
|
293
|
-
`Synchronized document ${name} did not preserve its expected title.`,
|
|
294
|
-
);
|
|
295
|
-
});
|
|
296
|
-
return files;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
function writeCertificationManifest(manifestPath, manifest) {
|
|
300
|
-
validateCertificationManifest(manifest);
|
|
301
|
-
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
302
|
-
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
function loadFeedbackManifest(workspace) {
|
|
212
|
+
return filePath;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function writeCreateApproval(workspace, runId, prdFiles) {
|
|
216
|
+
const approvalPath = path.join(workspace, `feedback-live-harness-${runId}-approval.md`);
|
|
217
|
+
fs.writeFileSync(
|
|
218
|
+
approvalPath,
|
|
219
|
+
[
|
|
220
|
+
"# Myte Feedback Certification Approval",
|
|
221
|
+
"",
|
|
222
|
+
`run_id: ${runId}`,
|
|
223
|
+
"command: myte create-prd",
|
|
224
|
+
`batch_count: ${prdFiles.length}`,
|
|
225
|
+
"targets:",
|
|
226
|
+
...prdFiles.map((filePath) => `- ${path.basename(filePath)}`),
|
|
227
|
+
"proposed_change: Create disposable, namespaced TEST Feedback records for post-deploy certification.",
|
|
228
|
+
"reason: Verify the published CLI and deployed Feedback workflow before exact-ID archival.",
|
|
229
|
+
"",
|
|
230
|
+
].join("\n"),
|
|
231
|
+
"utf8",
|
|
232
|
+
);
|
|
233
|
+
return approvalPath;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function writeCommentApproval(workspace, runId, feedbackId) {
|
|
237
|
+
const filePath = path.join(workspace, `feedback-live-harness-${runId}-comment.md`);
|
|
238
|
+
fs.writeFileSync(
|
|
239
|
+
filePath,
|
|
240
|
+
[
|
|
241
|
+
`# ${runId} Feedback certification comment`,
|
|
242
|
+
"",
|
|
243
|
+
`feedback_id: ${feedbackId}`,
|
|
244
|
+
`run_id: ${runId}`,
|
|
245
|
+
"proposed_change: Record a disposable, namespaced certification comment.",
|
|
246
|
+
"reason: Verify the project-scoped Feedback comment route and idempotency controls.",
|
|
247
|
+
"",
|
|
248
|
+
].join("\n"),
|
|
249
|
+
"utf8",
|
|
250
|
+
);
|
|
251
|
+
return filePath;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function responseRequestId(payload) {
|
|
255
|
+
return String(
|
|
256
|
+
payload?.request?.request_id
|
|
257
|
+
|| payload?.request?._id
|
|
258
|
+
|| payload?.request_id
|
|
259
|
+
|| payload?._id
|
|
260
|
+
|| "",
|
|
261
|
+
).trim();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function responseEventId(payload) {
|
|
265
|
+
return String(
|
|
266
|
+
payload?.event?.event_id
|
|
267
|
+
|| payload?.event?._id
|
|
268
|
+
|| payload?.event_id
|
|
269
|
+
|| "",
|
|
270
|
+
).trim();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function responseVersionId(version) {
|
|
274
|
+
return String(version?.version_id || version?._id || "").trim();
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function assertDocumentSetMaterialized(workspace, feedbackId, expectedTitles) {
|
|
278
|
+
const root = path.join(
|
|
279
|
+
workspace,
|
|
280
|
+
"MyteCommandCenter",
|
|
281
|
+
"PRD",
|
|
282
|
+
"feedback-sync",
|
|
283
|
+
feedbackId,
|
|
284
|
+
);
|
|
285
|
+
assert(fs.existsSync(root), `Missing synchronized PRD document directory for ${feedbackId}.`);
|
|
286
|
+
const files = fs.readdirSync(root)
|
|
287
|
+
.filter((name) => name.toLowerCase().endsWith(".md"))
|
|
288
|
+
.sort();
|
|
289
|
+
assert(files.length === expectedTitles.length, `Expected ${expectedTitles.length} synchronized PRD files, got ${files.length}.`);
|
|
290
|
+
files.forEach((name, index) => {
|
|
291
|
+
const content = fs.readFileSync(path.join(root, name), "utf8");
|
|
292
|
+
assert(
|
|
293
|
+
content.includes(`# ${expectedTitles[index]}`),
|
|
294
|
+
`Synchronized document ${name} did not preserve its expected title.`,
|
|
295
|
+
);
|
|
296
|
+
});
|
|
297
|
+
return files;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function writeCertificationManifest(manifestPath, manifest) {
|
|
301
|
+
validateCertificationManifest(manifest);
|
|
302
|
+
fs.mkdirSync(path.dirname(manifestPath), { recursive: true });
|
|
303
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function loadFeedbackManifest(workspace) {
|
|
306
307
|
const manifestPath = path.join(workspace, "MyteCommandCenter", "data", "feedback.yml");
|
|
307
308
|
if (!fs.existsSync(manifestPath)) {
|
|
308
309
|
return { items: [] };
|
|
309
|
-
}
|
|
310
|
-
try {
|
|
311
|
-
const parsed = readYamlFile(manifestPath);
|
|
312
|
-
if (parsed && typeof parsed === "object") {
|
|
313
|
-
return {
|
|
314
|
-
...parsed,
|
|
315
|
-
items: Array.isArray(parsed.items) ? parsed.items : [],
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
} catch (_err) {
|
|
319
|
-
// Keep a small fallback parser for a partially written legacy snapshot.
|
|
320
|
-
}
|
|
321
|
-
const text = fs.readFileSync(manifestPath, "utf8");
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const parsed = readYamlFile(manifestPath);
|
|
313
|
+
if (parsed && typeof parsed === "object") {
|
|
314
|
+
return {
|
|
315
|
+
...parsed,
|
|
316
|
+
items: Array.isArray(parsed.items) ? parsed.items : [],
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
} catch (_err) {
|
|
320
|
+
// Keep a small fallback parser for a partially written legacy snapshot.
|
|
321
|
+
}
|
|
322
|
+
const text = fs.readFileSync(manifestPath, "utf8");
|
|
322
323
|
const items = [];
|
|
323
324
|
let current = null;
|
|
324
325
|
for (const line of text.split(/\r?\n/)) {
|
|
@@ -340,62 +341,62 @@ function loadFeedbackManifest(workspace) {
|
|
|
340
341
|
}
|
|
341
342
|
}
|
|
342
343
|
if (current) items.push(current);
|
|
343
|
-
return { items };
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
function findFeedbackItem(workspace, feedbackId) {
|
|
347
|
-
const manifest = loadFeedbackManifest(workspace);
|
|
348
|
-
return (manifest.items || []).find(
|
|
349
|
-
(item) => String(item?.feedback_id || item?.id || "") === String(feedbackId),
|
|
350
|
-
) || null;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
function assertSynchronizedComment({
|
|
354
|
-
workspace,
|
|
355
|
-
feedbackId,
|
|
356
|
-
commentId,
|
|
357
|
-
expectedContent,
|
|
358
|
-
expectedAttachment,
|
|
359
|
-
}) {
|
|
360
|
-
const item = findFeedbackItem(workspace, feedbackId);
|
|
361
|
-
assert(item, `Feedback ${feedbackId} was missing while validating synchronized comments.`);
|
|
362
|
-
const turn = (item.conversation_turns || []).find(
|
|
363
|
-
(candidate) => String(candidate?.comment_id || "") === String(commentId),
|
|
364
|
-
);
|
|
365
|
-
assert(turn, `Comment ${commentId} was not linked to Feedback ${feedbackId} after sync.`);
|
|
366
|
-
assert(
|
|
367
|
-
String(turn.content || "").includes(expectedContent),
|
|
368
|
-
`Comment ${commentId} did not preserve its expected content after sync.`,
|
|
369
|
-
);
|
|
370
|
-
|
|
371
|
-
if (!expectedAttachment) return;
|
|
372
|
-
const attachment = (turn.attachments || []).find(
|
|
373
|
-
(candidate) => String(candidate?.name || "") === expectedAttachment.name,
|
|
374
|
-
);
|
|
375
|
-
assert(
|
|
376
|
-
attachment,
|
|
377
|
-
`Comment ${commentId} did not preserve attachment ${expectedAttachment.name}.`,
|
|
378
|
-
);
|
|
379
|
-
assert(
|
|
380
|
-
attachment.context_status === "readable",
|
|
381
|
-
`Comment attachment ${expectedAttachment.name} was not synchronized as readable context.`,
|
|
382
|
-
);
|
|
383
|
-
assert(
|
|
384
|
-
attachment.local_file,
|
|
385
|
-
`Comment attachment ${expectedAttachment.name} did not receive a local file reference.`,
|
|
386
|
-
);
|
|
387
|
-
const commandCenterRoot = path.resolve(workspace, "MyteCommandCenter");
|
|
388
|
-
const localPath = path.resolve(commandCenterRoot, String(attachment.local_file));
|
|
389
|
-
assert(
|
|
390
|
-
localPath.startsWith(`${commandCenterRoot}${path.sep}`),
|
|
391
|
-
`Comment attachment local path escaped MyteCommandCenter: ${attachment.local_file}`,
|
|
392
|
-
);
|
|
393
|
-
assert(fs.existsSync(localPath), `Synchronized comment attachment file is missing: ${localPath}`);
|
|
394
|
-
assert(
|
|
395
|
-
fs.readFileSync(localPath, "utf8").includes(expectedAttachment.contentMarker),
|
|
396
|
-
`Synchronized comment attachment ${expectedAttachment.name} lost its readable content.`,
|
|
397
|
-
);
|
|
398
|
-
}
|
|
344
|
+
return { items };
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function findFeedbackItem(workspace, feedbackId) {
|
|
348
|
+
const manifest = loadFeedbackManifest(workspace);
|
|
349
|
+
return (manifest.items || []).find(
|
|
350
|
+
(item) => String(item?.feedback_id || item?.id || "") === String(feedbackId),
|
|
351
|
+
) || null;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function assertSynchronizedComment({
|
|
355
|
+
workspace,
|
|
356
|
+
feedbackId,
|
|
357
|
+
commentId,
|
|
358
|
+
expectedContent,
|
|
359
|
+
expectedAttachment,
|
|
360
|
+
}) {
|
|
361
|
+
const item = findFeedbackItem(workspace, feedbackId);
|
|
362
|
+
assert(item, `Feedback ${feedbackId} was missing while validating synchronized comments.`);
|
|
363
|
+
const turn = (item.conversation_turns || []).find(
|
|
364
|
+
(candidate) => String(candidate?.comment_id || "") === String(commentId),
|
|
365
|
+
);
|
|
366
|
+
assert(turn, `Comment ${commentId} was not linked to Feedback ${feedbackId} after sync.`);
|
|
367
|
+
assert(
|
|
368
|
+
String(turn.content || "").includes(expectedContent),
|
|
369
|
+
`Comment ${commentId} did not preserve its expected content after sync.`,
|
|
370
|
+
);
|
|
371
|
+
|
|
372
|
+
if (!expectedAttachment) return;
|
|
373
|
+
const attachment = (turn.attachments || []).find(
|
|
374
|
+
(candidate) => String(candidate?.name || "") === expectedAttachment.name,
|
|
375
|
+
);
|
|
376
|
+
assert(
|
|
377
|
+
attachment,
|
|
378
|
+
`Comment ${commentId} did not preserve attachment ${expectedAttachment.name}.`,
|
|
379
|
+
);
|
|
380
|
+
assert(
|
|
381
|
+
attachment.context_status === "readable",
|
|
382
|
+
`Comment attachment ${expectedAttachment.name} was not synchronized as readable context.`,
|
|
383
|
+
);
|
|
384
|
+
assert(
|
|
385
|
+
attachment.local_file,
|
|
386
|
+
`Comment attachment ${expectedAttachment.name} did not receive a local file reference.`,
|
|
387
|
+
);
|
|
388
|
+
const commandCenterRoot = path.resolve(workspace, "MyteCommandCenter");
|
|
389
|
+
const localPath = path.resolve(commandCenterRoot, String(attachment.local_file));
|
|
390
|
+
assert(
|
|
391
|
+
localPath.startsWith(`${commandCenterRoot}${path.sep}`),
|
|
392
|
+
`Comment attachment local path escaped MyteCommandCenter: ${attachment.local_file}`,
|
|
393
|
+
);
|
|
394
|
+
assert(fs.existsSync(localPath), `Synchronized comment attachment file is missing: ${localPath}`);
|
|
395
|
+
assert(
|
|
396
|
+
fs.readFileSync(localPath, "utf8").includes(expectedAttachment.contentMarker),
|
|
397
|
+
`Synchronized comment attachment ${expectedAttachment.name} lost its readable content.`,
|
|
398
|
+
);
|
|
399
|
+
}
|
|
399
400
|
|
|
400
401
|
function findFeedbackState(workspace, feedbackId) {
|
|
401
402
|
const manifest = loadFeedbackManifest(workspace);
|
|
@@ -430,52 +431,52 @@ function assertFeedbackAbsent(workspace, feedbackIds) {
|
|
|
430
431
|
}
|
|
431
432
|
}
|
|
432
433
|
|
|
433
|
-
async function main() {
|
|
434
|
-
const args = parseArgs(process.argv.slice(2));
|
|
435
|
-
if (!args["confirm-live"]) {
|
|
436
|
-
console.log(JSON.stringify({
|
|
437
|
-
ok: true,
|
|
438
|
-
dry_run: true,
|
|
439
|
-
message: "No network, project mutation, or local certification artifact was created.",
|
|
440
|
-
required_live_flag: "--confirm-live",
|
|
441
|
-
optional_strict_role_flag: "--require-role-matrix",
|
|
442
|
-
optional_comment_attachment_flag: "--require-comment-attachment-sync",
|
|
443
|
-
required_role_environment: [
|
|
444
|
-
"MYTE_OWNER_API_KEY",
|
|
445
|
-
"MYTE_DELEGATE_API_KEY",
|
|
446
|
-
"MYTE_COLLABORATOR_API_KEY",
|
|
447
|
-
],
|
|
448
|
-
comment_attachment_environment: "MYTE_WEB_ACCESS_TOKEN",
|
|
449
|
-
scenario_matrix: LIVE_SCENARIO_MATRIX,
|
|
450
|
-
}, null, 2));
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
requireConfirm(args);
|
|
454
|
-
|
|
455
|
-
const ownerApiKey = String(
|
|
456
|
-
process.env.MYTE_OWNER_API_KEY
|
|
457
|
-
|| process.env.MYTE_API_KEY
|
|
458
|
-
|| process.env.MYTE_PROJECT_API_KEY
|
|
459
|
-
|| "",
|
|
460
|
-
).trim();
|
|
461
|
-
const delegateApiKey = String(process.env.MYTE_DELEGATE_API_KEY || "").trim();
|
|
462
|
-
const collaboratorApiKey = String(process.env.MYTE_COLLABORATOR_API_KEY || "").trim();
|
|
463
|
-
const webAccessToken = String(process.env.MYTE_WEB_ACCESS_TOKEN || "").trim();
|
|
464
|
-
process.env.MYTE_API_KEY = ownerApiKey;
|
|
465
|
-
process.env.MYTE_PROJECT_API_KEY = ownerApiKey;
|
|
466
|
-
if (
|
|
467
|
-
args["require-role-matrix"]
|
|
468
|
-
&& (!ownerApiKey || !delegateApiKey || !collaboratorApiKey)
|
|
469
|
-
) {
|
|
470
|
-
throw new Error(
|
|
471
|
-
"--require-role-matrix requires MYTE_OWNER_API_KEY, MYTE_DELEGATE_API_KEY, and MYTE_COLLABORATOR_API_KEY.",
|
|
472
|
-
);
|
|
473
|
-
}
|
|
474
|
-
if (args["require-comment-attachment-sync"] && !webAccessToken) {
|
|
475
|
-
throw new Error(
|
|
476
|
-
"--require-comment-attachment-sync requires an ephemeral MYTE_WEB_ACCESS_TOKEN for a user assigned to the target project.",
|
|
477
|
-
);
|
|
478
|
-
}
|
|
434
|
+
async function main() {
|
|
435
|
+
const args = parseArgs(process.argv.slice(2));
|
|
436
|
+
if (!args["confirm-live"]) {
|
|
437
|
+
console.log(JSON.stringify({
|
|
438
|
+
ok: true,
|
|
439
|
+
dry_run: true,
|
|
440
|
+
message: "No network, project mutation, or local certification artifact was created.",
|
|
441
|
+
required_live_flag: "--confirm-live",
|
|
442
|
+
optional_strict_role_flag: "--require-role-matrix",
|
|
443
|
+
optional_comment_attachment_flag: "--require-comment-attachment-sync",
|
|
444
|
+
required_role_environment: [
|
|
445
|
+
"MYTE_OWNER_API_KEY",
|
|
446
|
+
"MYTE_DELEGATE_API_KEY",
|
|
447
|
+
"MYTE_COLLABORATOR_API_KEY",
|
|
448
|
+
],
|
|
449
|
+
comment_attachment_environment: "MYTE_WEB_ACCESS_TOKEN",
|
|
450
|
+
scenario_matrix: LIVE_SCENARIO_MATRIX,
|
|
451
|
+
}, null, 2));
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
requireConfirm(args);
|
|
455
|
+
|
|
456
|
+
const ownerApiKey = String(
|
|
457
|
+
process.env.MYTE_OWNER_API_KEY
|
|
458
|
+
|| process.env.MYTE_API_KEY
|
|
459
|
+
|| process.env.MYTE_PROJECT_API_KEY
|
|
460
|
+
|| "",
|
|
461
|
+
).trim();
|
|
462
|
+
const delegateApiKey = String(process.env.MYTE_DELEGATE_API_KEY || "").trim();
|
|
463
|
+
const collaboratorApiKey = String(process.env.MYTE_COLLABORATOR_API_KEY || "").trim();
|
|
464
|
+
const webAccessToken = String(process.env.MYTE_WEB_ACCESS_TOKEN || "").trim();
|
|
465
|
+
process.env.MYTE_API_KEY = ownerApiKey;
|
|
466
|
+
process.env.MYTE_PROJECT_API_KEY = ownerApiKey;
|
|
467
|
+
if (
|
|
468
|
+
args["require-role-matrix"]
|
|
469
|
+
&& (!ownerApiKey || !delegateApiKey || !collaboratorApiKey)
|
|
470
|
+
) {
|
|
471
|
+
throw new Error(
|
|
472
|
+
"--require-role-matrix requires MYTE_OWNER_API_KEY, MYTE_DELEGATE_API_KEY, and MYTE_COLLABORATOR_API_KEY.",
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
if (args["require-comment-attachment-sync"] && !webAccessToken) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
"--require-comment-attachment-sync requires an ephemeral MYTE_WEB_ACCESS_TOKEN for a user assigned to the target project.",
|
|
478
|
+
);
|
|
479
|
+
}
|
|
479
480
|
|
|
480
481
|
const workspace = path.resolve(
|
|
481
482
|
args.workspace || fs.mkdtempSync(path.join(os.tmpdir(), "myte-feedback-full-harness-")),
|
|
@@ -487,494 +488,638 @@ async function main() {
|
|
|
487
488
|
baseArgs.push("--base-url", String(args["base-url"]));
|
|
488
489
|
}
|
|
489
490
|
|
|
490
|
-
const runId = String(args["run-id"] || new Date().toISOString().replace(/[:.]/g, "-"));
|
|
491
|
-
const namespace = `MYTE_TEST_${runId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
492
|
-
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
493
|
-
const reason = String(args.reason || "Disposable live Feedback harness verification");
|
|
494
|
-
const createdFeedbackIds = [];
|
|
495
|
-
const reviewRequestIds = [];
|
|
496
|
-
const reviewArtifactByRequestId = new Map();
|
|
497
|
-
let documentSetFeedbackId = null;
|
|
498
|
-
let documentSetRequestId = null;
|
|
499
|
-
const roleMatrix = {
|
|
500
|
-
required: Boolean(args["require-role-matrix"]),
|
|
501
|
-
owner: true,
|
|
502
|
-
delegate: Boolean(delegateApiKey),
|
|
503
|
-
collaborator: Boolean(collaboratorApiKey),
|
|
504
|
-
same_project: true,
|
|
505
|
-
collaborator_review_denied: null,
|
|
506
|
-
collaborator_direct_apply_denied: null,
|
|
507
|
-
collaborator_archive_denied: null,
|
|
508
|
-
collaborator_writes_used: false,
|
|
509
|
-
owner_review_used: false,
|
|
510
|
-
delegate_review_used: false,
|
|
511
|
-
};
|
|
512
|
-
let commentAttachmentSyncVerified = false;
|
|
513
|
-
const manifestPath = path.resolve(
|
|
514
|
-
args.manifest
|
|
515
|
-
|| path.join(workspace, "MyteCommandCenter", "certification", `${runId}.json`),
|
|
516
|
-
);
|
|
517
|
-
const manifest = createCertificationManifest({
|
|
518
|
-
runId,
|
|
519
|
-
namespace,
|
|
520
|
-
environment: String(args.environment || "dev"),
|
|
521
|
-
});
|
|
522
|
-
manifest.state = "running";
|
|
523
|
-
writeCertificationManifest(manifestPath, manifest);
|
|
524
|
-
|
|
525
|
-
try {
|
|
526
|
-
const doctor = runCli(
|
|
527
|
-
["doctor", "--json", "--timeout-ms", String(args["timeout-ms"] || 60000), ...baseArgs],
|
|
528
|
-
workspace,
|
|
529
|
-
{ apiKey: ownerApiKey },
|
|
530
|
-
).stdout;
|
|
531
|
-
assert(doctor.ok === true, `Myte doctor failed at ${doctor.failure_layer || "unknown"} layer.`);
|
|
532
|
-
|
|
533
|
-
const config = runCli(
|
|
534
|
-
["config", "--json", ...baseArgs],
|
|
535
|
-
workspace,
|
|
536
|
-
{ apiKey: ownerApiKey },
|
|
537
|
-
).stdout;
|
|
538
|
-
manifest.project_id = String(
|
|
539
|
-
config.project_id || config.project?.id || config.project?._id || "",
|
|
540
|
-
) || null;
|
|
541
|
-
for (const [role, apiKey] of [
|
|
542
|
-
["delegate", delegateApiKey],
|
|
543
|
-
["collaborator", collaboratorApiKey],
|
|
544
|
-
]) {
|
|
545
|
-
if (!apiKey) continue;
|
|
546
|
-
const roleConfig = runCli(
|
|
547
|
-
["config", "--json", ...baseArgs],
|
|
548
|
-
workspace,
|
|
549
|
-
{ apiKey },
|
|
550
|
-
).stdout;
|
|
551
|
-
const roleProjectId = String(
|
|
552
|
-
roleConfig.project_id || roleConfig.project?.id || roleConfig.project?._id || "",
|
|
553
|
-
).trim();
|
|
554
|
-
if (!manifest.project_id || roleProjectId !== manifest.project_id) {
|
|
555
|
-
roleMatrix.same_project = false;
|
|
556
|
-
throw new Error(`${role} certification key is not scoped to the owner key's project.`);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
writeCertificationManifest(manifestPath, manifest);
|
|
560
|
-
|
|
561
|
-
const prdFiles = [];
|
|
562
|
-
for (let index = 1; index <= 5; index += 1) {
|
|
563
|
-
const title = `${namespace} Feedback ${stamp} ${index}`;
|
|
564
|
-
prdFiles.push(writePrd(workspace, title, index));
|
|
565
|
-
}
|
|
566
|
-
const createApprovalPath = writeCreateApproval(workspace, runId, prdFiles);
|
|
567
|
-
|
|
568
|
-
const create = runCli([
|
|
569
|
-
"create-prd",
|
|
491
|
+
const runId = String(args["run-id"] || new Date().toISOString().replace(/[:.]/g, "-"));
|
|
492
|
+
const namespace = `MYTE_TEST_${runId.replace(/[^A-Za-z0-9_-]/g, "_")}`;
|
|
493
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
494
|
+
const reason = String(args.reason || "Disposable live Feedback harness verification");
|
|
495
|
+
const createdFeedbackIds = [];
|
|
496
|
+
const reviewRequestIds = [];
|
|
497
|
+
const reviewArtifactByRequestId = new Map();
|
|
498
|
+
let documentSetFeedbackId = null;
|
|
499
|
+
let documentSetRequestId = null;
|
|
500
|
+
const roleMatrix = {
|
|
501
|
+
required: Boolean(args["require-role-matrix"]),
|
|
502
|
+
owner: true,
|
|
503
|
+
delegate: Boolean(delegateApiKey),
|
|
504
|
+
collaborator: Boolean(collaboratorApiKey),
|
|
505
|
+
same_project: true,
|
|
506
|
+
collaborator_review_denied: null,
|
|
507
|
+
collaborator_direct_apply_denied: null,
|
|
508
|
+
collaborator_archive_denied: null,
|
|
509
|
+
collaborator_writes_used: false,
|
|
510
|
+
owner_review_used: false,
|
|
511
|
+
delegate_review_used: false,
|
|
512
|
+
};
|
|
513
|
+
let commentAttachmentSyncVerified = false;
|
|
514
|
+
const manifestPath = path.resolve(
|
|
515
|
+
args.manifest
|
|
516
|
+
|| path.join(workspace, "MyteCommandCenter", "certification", `${runId}.json`),
|
|
517
|
+
);
|
|
518
|
+
const manifest = createCertificationManifest({
|
|
519
|
+
runId,
|
|
520
|
+
namespace,
|
|
521
|
+
environment: String(args.environment || "dev"),
|
|
522
|
+
});
|
|
523
|
+
manifest.state = "running";
|
|
524
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
525
|
+
|
|
526
|
+
try {
|
|
527
|
+
const doctor = runCli(
|
|
528
|
+
["doctor", "--json", "--timeout-ms", String(args["timeout-ms"] || 60000), ...baseArgs],
|
|
529
|
+
workspace,
|
|
530
|
+
{ apiKey: ownerApiKey },
|
|
531
|
+
).stdout;
|
|
532
|
+
assert(doctor.ok === true, `Myte doctor failed at ${doctor.failure_layer || "unknown"} layer.`);
|
|
533
|
+
|
|
534
|
+
const config = runCli(
|
|
535
|
+
["config", "--json", ...baseArgs],
|
|
536
|
+
workspace,
|
|
537
|
+
{ apiKey: ownerApiKey },
|
|
538
|
+
).stdout;
|
|
539
|
+
manifest.project_id = String(
|
|
540
|
+
config.project_id || config.project?.id || config.project?._id || "",
|
|
541
|
+
) || null;
|
|
542
|
+
for (const [role, apiKey] of [
|
|
543
|
+
["delegate", delegateApiKey],
|
|
544
|
+
["collaborator", collaboratorApiKey],
|
|
545
|
+
]) {
|
|
546
|
+
if (!apiKey) continue;
|
|
547
|
+
const roleConfig = runCli(
|
|
548
|
+
["config", "--json", ...baseArgs],
|
|
549
|
+
workspace,
|
|
550
|
+
{ apiKey },
|
|
551
|
+
).stdout;
|
|
552
|
+
const roleProjectId = String(
|
|
553
|
+
roleConfig.project_id || roleConfig.project?.id || roleConfig.project?._id || "",
|
|
554
|
+
).trim();
|
|
555
|
+
if (!manifest.project_id || roleProjectId !== manifest.project_id) {
|
|
556
|
+
roleMatrix.same_project = false;
|
|
557
|
+
throw new Error(`${role} certification key is not scoped to the owner key's project.`);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
561
|
+
|
|
562
|
+
const prdFiles = [];
|
|
563
|
+
for (let index = 1; index <= 5; index += 1) {
|
|
564
|
+
const title = `${namespace} Feedback ${stamp} ${index}`;
|
|
565
|
+
prdFiles.push(writePrd(workspace, title, index));
|
|
566
|
+
}
|
|
567
|
+
const createApprovalPath = writeCreateApproval(workspace, runId, prdFiles);
|
|
568
|
+
|
|
569
|
+
const create = runCli([
|
|
570
|
+
"create-prd",
|
|
570
571
|
...prdFiles,
|
|
571
|
-
"--description",
|
|
572
|
-
`Disposable ${namespace} Feedback created by the live certification harness.`,
|
|
573
|
-
"--confirm-write",
|
|
574
|
-
"--approval-artifact",
|
|
575
|
-
createApprovalPath,
|
|
576
|
-
"--json",
|
|
572
|
+
"--description",
|
|
573
|
+
`Disposable ${namespace} Feedback created by the live certification harness.`,
|
|
574
|
+
"--confirm-write",
|
|
575
|
+
"--approval-artifact",
|
|
576
|
+
createApprovalPath,
|
|
577
|
+
"--json",
|
|
577
578
|
...baseArgs,
|
|
578
579
|
], workspace).stdout;
|
|
579
580
|
|
|
580
581
|
const createdItems = Array.isArray(create.items) ? create.items : [];
|
|
581
582
|
for (const item of createdItems) {
|
|
582
|
-
if (item.status === "created" && item.feedback_id) {
|
|
583
|
-
createdFeedbackIds.push(String(item.feedback_id));
|
|
584
|
-
recordCertificationArtifact(manifest, {
|
|
585
|
-
kind: "feedback",
|
|
586
|
-
id: String(item.feedback_id),
|
|
587
|
-
metadata: { source: "create-prd", cleanup: "archive" },
|
|
588
|
-
});
|
|
589
|
-
}
|
|
590
|
-
}
|
|
591
|
-
writeCertificationManifest(manifestPath, manifest);
|
|
592
|
-
assert(createdFeedbackIds.length === 5, `Expected 5 created TEST feedback items, got ${createdFeedbackIds.length}: ${JSON.stringify(create, null, 2)}`);
|
|
593
|
-
|
|
594
|
-
const documentSetTitles = [
|
|
595
|
-
`${namespace} Document Set Overview`,
|
|
596
|
-
`${namespace} Document Set Harness`,
|
|
597
|
-
`${namespace} Document Set Security`,
|
|
598
|
-
];
|
|
599
|
-
const documentSetFiles = documentSetTitles.map((title, index) =>
|
|
600
|
-
writePrd(workspace, title, 101 + index),
|
|
601
|
-
);
|
|
602
|
-
const documentSetApprovalPath = writeCreateApproval(
|
|
603
|
-
workspace,
|
|
604
|
-
`${runId}-document-set`,
|
|
605
|
-
documentSetFiles,
|
|
606
|
-
);
|
|
607
|
-
const documentSetCreate = runCli([
|
|
608
|
-
"create-prd",
|
|
609
|
-
...documentSetFiles,
|
|
610
|
-
"--document-set",
|
|
611
|
-
"--title",
|
|
612
|
-
`${namespace} Three Document PRD`,
|
|
613
|
-
"--description",
|
|
614
|
-
`Disposable ${namespace} three-document PRD certification record.`,
|
|
615
|
-
"--confirm-write",
|
|
616
|
-
"--approval-artifact",
|
|
617
|
-
documentSetApprovalPath,
|
|
618
|
-
"--json",
|
|
619
|
-
...baseArgs,
|
|
620
|
-
], workspace).stdout;
|
|
621
|
-
documentSetFeedbackId = String(documentSetCreate.feedback_id || "");
|
|
622
|
-
assert(documentSetFeedbackId, `Document-set creation did not return feedback_id: ${JSON.stringify(documentSetCreate, null, 2)}`);
|
|
623
|
-
assert(documentSetCreate.document_count === 3, `Expected three PRD documents: ${JSON.stringify(documentSetCreate, null, 2)}`);
|
|
624
|
-
assert(Array.isArray(documentSetCreate.documents) && documentSetCreate.documents.length === 3, "Document-set metadata did not contain three documents.");
|
|
625
|
-
recordCertificationArtifact(manifest, {
|
|
626
|
-
kind: "feedback",
|
|
627
|
-
id: documentSetFeedbackId,
|
|
628
|
-
metadata: {
|
|
629
|
-
source: "create-prd-document-set",
|
|
630
|
-
document_count: 3,
|
|
631
|
-
cleanup: "archive",
|
|
632
|
-
},
|
|
633
|
-
});
|
|
634
|
-
for (const document of documentSetCreate.documents) {
|
|
635
|
-
if (!document.document_id) continue;
|
|
636
|
-
recordCertificationArtifact(manifest, {
|
|
637
|
-
kind: "object",
|
|
638
|
-
id: String(document.document_id),
|
|
639
|
-
metadata: {
|
|
640
|
-
object_type: "prd_document",
|
|
641
|
-
feedback_id: documentSetFeedbackId,
|
|
642
|
-
cleanup: "retained_with_archived_parent",
|
|
643
|
-
},
|
|
644
|
-
});
|
|
645
|
-
}
|
|
646
|
-
writeCertificationManifest(manifestPath, manifest);
|
|
647
|
-
|
|
648
|
-
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
649
|
-
assertFeedbackPresent(workspace, createdFeedbackIds, "todo");
|
|
650
|
-
assertFeedbackPresent(workspace, [documentSetFeedbackId], "todo");
|
|
651
|
-
assertDocumentSetMaterialized(
|
|
652
|
-
workspace,
|
|
653
|
-
documentSetFeedbackId,
|
|
654
|
-
documentSetTitles,
|
|
655
|
-
);
|
|
656
|
-
|
|
657
|
-
const collaboratorWriteKey = collaboratorApiKey || ownerApiKey;
|
|
658
|
-
const operationalReviewKey = delegateApiKey || ownerApiKey;
|
|
659
|
-
roleMatrix.collaborator_writes_used = Boolean(collaboratorApiKey);
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
"
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
"
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
"feedback",
|
|
748
|
-
"
|
|
749
|
-
"--
|
|
750
|
-
|
|
751
|
-
"--
|
|
752
|
-
"
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
"
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
"
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
"
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
"--
|
|
784
|
-
|
|
785
|
-
"--
|
|
786
|
-
"
|
|
787
|
-
"--
|
|
788
|
-
"--
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
"
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
"
|
|
892
|
-
|
|
893
|
-
"--
|
|
894
|
-
|
|
895
|
-
"--
|
|
896
|
-
"
|
|
897
|
-
"--
|
|
898
|
-
"
|
|
899
|
-
"--
|
|
900
|
-
"
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
"
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
"
|
|
924
|
-
"
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
"
|
|
946
|
-
"
|
|
947
|
-
"--
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
{
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
583
|
+
if (item.status === "created" && item.feedback_id) {
|
|
584
|
+
createdFeedbackIds.push(String(item.feedback_id));
|
|
585
|
+
recordCertificationArtifact(manifest, {
|
|
586
|
+
kind: "feedback",
|
|
587
|
+
id: String(item.feedback_id),
|
|
588
|
+
metadata: { source: "create-prd", cleanup: "archive" },
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
593
|
+
assert(createdFeedbackIds.length === 5, `Expected 5 created TEST feedback items, got ${createdFeedbackIds.length}: ${JSON.stringify(create, null, 2)}`);
|
|
594
|
+
|
|
595
|
+
const documentSetTitles = [
|
|
596
|
+
`${namespace} Document Set Overview`,
|
|
597
|
+
`${namespace} Document Set Harness`,
|
|
598
|
+
`${namespace} Document Set Security`,
|
|
599
|
+
];
|
|
600
|
+
const documentSetFiles = documentSetTitles.map((title, index) =>
|
|
601
|
+
writePrd(workspace, title, 101 + index),
|
|
602
|
+
);
|
|
603
|
+
const documentSetApprovalPath = writeCreateApproval(
|
|
604
|
+
workspace,
|
|
605
|
+
`${runId}-document-set`,
|
|
606
|
+
documentSetFiles,
|
|
607
|
+
);
|
|
608
|
+
const documentSetCreate = runCli([
|
|
609
|
+
"create-prd",
|
|
610
|
+
...documentSetFiles,
|
|
611
|
+
"--document-set",
|
|
612
|
+
"--title",
|
|
613
|
+
`${namespace} Three Document PRD`,
|
|
614
|
+
"--description",
|
|
615
|
+
`Disposable ${namespace} three-document PRD certification record.`,
|
|
616
|
+
"--confirm-write",
|
|
617
|
+
"--approval-artifact",
|
|
618
|
+
documentSetApprovalPath,
|
|
619
|
+
"--json",
|
|
620
|
+
...baseArgs,
|
|
621
|
+
], workspace).stdout;
|
|
622
|
+
documentSetFeedbackId = String(documentSetCreate.feedback_id || "");
|
|
623
|
+
assert(documentSetFeedbackId, `Document-set creation did not return feedback_id: ${JSON.stringify(documentSetCreate, null, 2)}`);
|
|
624
|
+
assert(documentSetCreate.document_count === 3, `Expected three PRD documents: ${JSON.stringify(documentSetCreate, null, 2)}`);
|
|
625
|
+
assert(Array.isArray(documentSetCreate.documents) && documentSetCreate.documents.length === 3, "Document-set metadata did not contain three documents.");
|
|
626
|
+
recordCertificationArtifact(manifest, {
|
|
627
|
+
kind: "feedback",
|
|
628
|
+
id: documentSetFeedbackId,
|
|
629
|
+
metadata: {
|
|
630
|
+
source: "create-prd-document-set",
|
|
631
|
+
document_count: 3,
|
|
632
|
+
cleanup: "archive",
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
for (const document of documentSetCreate.documents) {
|
|
636
|
+
if (!document.document_id) continue;
|
|
637
|
+
recordCertificationArtifact(manifest, {
|
|
638
|
+
kind: "object",
|
|
639
|
+
id: String(document.document_id),
|
|
640
|
+
metadata: {
|
|
641
|
+
object_type: "prd_document",
|
|
642
|
+
feedback_id: documentSetFeedbackId,
|
|
643
|
+
cleanup: "retained_with_archived_parent",
|
|
644
|
+
},
|
|
645
|
+
});
|
|
646
|
+
}
|
|
647
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
648
|
+
|
|
649
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
650
|
+
assertFeedbackPresent(workspace, createdFeedbackIds, "todo");
|
|
651
|
+
assertFeedbackPresent(workspace, [documentSetFeedbackId], "todo");
|
|
652
|
+
assertDocumentSetMaterialized(
|
|
653
|
+
workspace,
|
|
654
|
+
documentSetFeedbackId,
|
|
655
|
+
documentSetTitles,
|
|
656
|
+
);
|
|
657
|
+
|
|
658
|
+
const collaboratorWriteKey = collaboratorApiKey || ownerApiKey;
|
|
659
|
+
const operationalReviewKey = delegateApiKey || ownerApiKey;
|
|
660
|
+
roleMatrix.collaborator_writes_used = Boolean(collaboratorApiKey);
|
|
661
|
+
|
|
662
|
+
const ticketVersions = runCli([
|
|
663
|
+
"feedback",
|
|
664
|
+
"prd-versions",
|
|
665
|
+
"--feedback-id",
|
|
666
|
+
documentSetFeedbackId,
|
|
667
|
+
"--json",
|
|
668
|
+
...baseArgs,
|
|
669
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
670
|
+
const ticketVersionId = String(
|
|
671
|
+
ticketVersions.active_prd_version_id
|
|
672
|
+
|| ticketVersions.versions?.[0]?.version_id
|
|
673
|
+
|| ticketVersions.versions?.[0]?._id
|
|
674
|
+
|| "",
|
|
675
|
+
);
|
|
676
|
+
const ticketDocumentId = String(documentSetCreate.documents?.[0]?.document_id || "");
|
|
677
|
+
assert(ticketVersionId && ticketDocumentId, "Ticket certification could not resolve a PRD version/document anchor.");
|
|
678
|
+
const ticketPayloadPath = path.join(workspace, `tickets-${runId}.json`);
|
|
679
|
+
const ticketPayload = {
|
|
680
|
+
items: [
|
|
681
|
+
{
|
|
682
|
+
ticket_key: `${namespace}-api`,
|
|
683
|
+
title: `${namespace} API subticket`,
|
|
684
|
+
summary: "Disposable batch lifecycle certification ticket.",
|
|
685
|
+
severity: "High",
|
|
686
|
+
prd_links: [{ version_id: ticketVersionId, document_id: ticketDocumentId }],
|
|
687
|
+
},
|
|
688
|
+
{
|
|
689
|
+
ticket_key: `${namespace}-web`,
|
|
690
|
+
title: `${namespace} Web subticket`,
|
|
691
|
+
summary: "Disposable batch lifecycle certification ticket.",
|
|
692
|
+
severity: "Medium",
|
|
693
|
+
prd_links: [{ version_id: ticketVersionId, document_id: ticketDocumentId }],
|
|
694
|
+
},
|
|
695
|
+
],
|
|
696
|
+
};
|
|
697
|
+
fs.writeFileSync(ticketPayloadPath, `${JSON.stringify(ticketPayload, null, 2)}\n`, "utf8");
|
|
698
|
+
const ticketApprovalPath = path.join(workspace, `tickets-${runId}-approval.json`);
|
|
699
|
+
fs.writeFileSync(ticketApprovalPath, `${JSON.stringify({
|
|
700
|
+
command: "myte feedback tickets create",
|
|
701
|
+
reason,
|
|
702
|
+
feedback_id: documentSetFeedbackId,
|
|
703
|
+
batch_count: 2,
|
|
704
|
+
tickets: ticketPayload.items,
|
|
705
|
+
}, null, 2)}\n`, "utf8");
|
|
706
|
+
const ticketValidation = runCli([
|
|
707
|
+
"feedback", "tickets", "create",
|
|
708
|
+
"--feedback-id", documentSetFeedbackId,
|
|
709
|
+
"--file", ticketPayloadPath,
|
|
710
|
+
"--dry-run", "--json", ...baseArgs,
|
|
711
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
712
|
+
assert(ticketValidation.valid === true && ticketValidation.item_count === 2, "Ticket batch validation did not certify both items.");
|
|
713
|
+
const ticketCreate = runCli([
|
|
714
|
+
"feedback", "tickets", "create",
|
|
715
|
+
"--feedback-id", documentSetFeedbackId,
|
|
716
|
+
"--file", ticketPayloadPath,
|
|
717
|
+
"--confirm-write", "--approval-artifact", ticketApprovalPath,
|
|
718
|
+
"--idempotency-key", `ticket-create-${runId}`,
|
|
719
|
+
"--json", ...baseArgs,
|
|
720
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
721
|
+
assert(ticketCreate.created_count === 2 && ticketCreate.receipts?.length === 2, "Ticket batch create did not return two receipts.");
|
|
722
|
+
const ticketIds = ticketCreate.receipts.map((receipt) => String(receipt.ticket_id));
|
|
723
|
+
for (const ticketId of ticketIds) {
|
|
724
|
+
recordCertificationArtifact(manifest, {
|
|
725
|
+
kind: "feedback_ticket",
|
|
726
|
+
id: ticketId,
|
|
727
|
+
metadata: { feedback_id: documentSetFeedbackId, cleanup: "retained_with_archived_parent" },
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
if (ticketCreate.batch_id) {
|
|
731
|
+
recordCertificationArtifact(manifest, {
|
|
732
|
+
kind: "ticket_batch",
|
|
733
|
+
id: String(ticketCreate.batch_id),
|
|
734
|
+
metadata: { operation: "create", feedback_id: documentSetFeedbackId },
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
const ticketUpdateApprovalPath = path.join(workspace, `tickets-${runId}-update.json`);
|
|
738
|
+
fs.writeFileSync(ticketUpdateApprovalPath, `${JSON.stringify({
|
|
739
|
+
command: "myte feedback tickets update",
|
|
740
|
+
reason,
|
|
741
|
+
feedback_id: documentSetFeedbackId,
|
|
742
|
+
ticket_ids: ticketIds,
|
|
743
|
+
batch_count: ticketIds.length,
|
|
744
|
+
proposed_change: { status: "in_progress" },
|
|
745
|
+
}, null, 2)}\n`, "utf8");
|
|
746
|
+
const ticketUpdate = runCli([
|
|
747
|
+
"feedback", "tickets", "update",
|
|
748
|
+
"--feedback-id", documentSetFeedbackId,
|
|
749
|
+
"--ticket-ids", ticketIds.join(","),
|
|
750
|
+
"--status", "in_progress",
|
|
751
|
+
"--confirm-write", "--approval-artifact", ticketUpdateApprovalPath,
|
|
752
|
+
"--idempotency-key", `ticket-update-${runId}`,
|
|
753
|
+
"--json", ...baseArgs,
|
|
754
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
755
|
+
assert(ticketUpdate.updated_count === 2, "Ticket batch update did not update both items.");
|
|
756
|
+
if (ticketUpdate.batch_id) {
|
|
757
|
+
recordCertificationArtifact(manifest, {
|
|
758
|
+
kind: "ticket_batch",
|
|
759
|
+
id: String(ticketUpdate.batch_id),
|
|
760
|
+
metadata: { operation: "update", feedback_id: documentSetFeedbackId },
|
|
761
|
+
});
|
|
762
|
+
}
|
|
763
|
+
const ticketList = runCli([
|
|
764
|
+
"feedback", "tickets", "list",
|
|
765
|
+
"--feedback-id", documentSetFeedbackId,
|
|
766
|
+
"--json", ...baseArgs,
|
|
767
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
768
|
+
assert(
|
|
769
|
+
ticketIds.every((ticketId) => ticketList.tickets?.some((ticket) => String(ticket._id || ticket.ticket_id) === ticketId && ticket.status === "in_progress")),
|
|
770
|
+
"Ticket list did not reflect the batch lifecycle update.",
|
|
771
|
+
);
|
|
772
|
+
const ticketCoverageApprovalPath = path.join(workspace, `tickets-${runId}-coverage.json`);
|
|
773
|
+
fs.writeFileSync(ticketCoverageApprovalPath, `${JSON.stringify({
|
|
774
|
+
command: "myte feedback tickets coverage",
|
|
775
|
+
reason,
|
|
776
|
+
feedback_id: documentSetFeedbackId,
|
|
777
|
+
ticket_id: ticketIds[0],
|
|
778
|
+
version_id: ticketVersionId,
|
|
779
|
+
document_id: ticketDocumentId,
|
|
780
|
+
coverage_status: "partially_covered",
|
|
781
|
+
}, null, 2)}\n`, "utf8");
|
|
782
|
+
const ticketCoverage = runCli([
|
|
783
|
+
"feedback", "tickets", "coverage",
|
|
784
|
+
"--feedback-id", documentSetFeedbackId,
|
|
785
|
+
"--ticket-id", ticketIds[0],
|
|
786
|
+
"--version-id", ticketVersionId,
|
|
787
|
+
"--document-id", ticketDocumentId,
|
|
788
|
+
"--coverage-status", "partially_covered",
|
|
789
|
+
"--confirm-write", "--approval-artifact", ticketCoverageApprovalPath,
|
|
790
|
+
"--idempotency-key", `ticket-coverage-${runId}`,
|
|
791
|
+
"--json", ...baseArgs,
|
|
792
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
793
|
+
assert(
|
|
794
|
+
ticketCoverage.prd_links?.some((link) => String(link.version_id) === ticketVersionId && String(link.document_id) === ticketDocumentId && link.coverage_status === "partially_covered"),
|
|
795
|
+
"Ticket coverage update did not persist on the expected PRD link.",
|
|
796
|
+
);
|
|
797
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace, { apiKey: collaboratorWriteKey });
|
|
798
|
+
const syncedTicketItem = findFeedbackItem(workspace, documentSetFeedbackId);
|
|
799
|
+
assert(
|
|
800
|
+
syncedTicketItem?.tickets?.length >= 2 && syncedTicketItem.ticket_count >= 2,
|
|
801
|
+
"feedback-sync did not hydrate ticket summaries.",
|
|
802
|
+
);
|
|
803
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
804
|
+
|
|
805
|
+
const commentApprovalPath = writeCommentApproval(
|
|
806
|
+
workspace,
|
|
807
|
+
runId,
|
|
808
|
+
createdFeedbackIds[0],
|
|
809
|
+
);
|
|
810
|
+
const comment = runCli([
|
|
811
|
+
"feedback",
|
|
812
|
+
"comment",
|
|
813
|
+
"--feedback-id",
|
|
814
|
+
createdFeedbackIds[0],
|
|
815
|
+
"--body-file",
|
|
816
|
+
commentApprovalPath,
|
|
817
|
+
"--confirm-write",
|
|
818
|
+
"--approval-artifact",
|
|
819
|
+
commentApprovalPath,
|
|
820
|
+
"--no-sync",
|
|
821
|
+
"--json",
|
|
822
|
+
...baseArgs,
|
|
823
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
824
|
+
assert(comment.comment_id, `Feedback comment did not return comment_id: ${JSON.stringify(comment, null, 2)}`);
|
|
825
|
+
recordCertificationArtifact(manifest, {
|
|
826
|
+
kind: "comment",
|
|
827
|
+
id: String(comment.comment_id),
|
|
828
|
+
metadata: {
|
|
829
|
+
feedback_id: createdFeedbackIds[0],
|
|
830
|
+
cleanup: "retained_with_archived_parent",
|
|
831
|
+
},
|
|
832
|
+
});
|
|
833
|
+
let webAttachmentFixture = null;
|
|
834
|
+
if (webAccessToken) {
|
|
835
|
+
webAttachmentFixture = await createWebFeedbackCommentWithAttachment({
|
|
836
|
+
apiBase: resolveHarnessApiBase(args),
|
|
837
|
+
accessToken: webAccessToken,
|
|
838
|
+
feedbackId: createdFeedbackIds[0],
|
|
839
|
+
projectId: manifest.project_id,
|
|
840
|
+
namespace,
|
|
841
|
+
timeoutMs: Number(args["timeout-ms"] || 60000),
|
|
842
|
+
});
|
|
843
|
+
recordCertificationArtifact(manifest, {
|
|
844
|
+
kind: "comment",
|
|
845
|
+
id: webAttachmentFixture.commentId,
|
|
846
|
+
metadata: {
|
|
847
|
+
feedback_id: createdFeedbackIds[0],
|
|
848
|
+
attachment_name: webAttachmentFixture.attachmentName,
|
|
849
|
+
cleanup: "retained_with_archived_parent",
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
855
|
+
assertSynchronizedComment({
|
|
856
|
+
workspace,
|
|
857
|
+
feedbackId: createdFeedbackIds[0],
|
|
858
|
+
commentId: String(comment.comment_id),
|
|
859
|
+
expectedContent: runId,
|
|
860
|
+
});
|
|
861
|
+
if (webAttachmentFixture) {
|
|
862
|
+
assertSynchronizedComment({
|
|
863
|
+
workspace,
|
|
864
|
+
feedbackId: createdFeedbackIds[0],
|
|
865
|
+
commentId: webAttachmentFixture.commentId,
|
|
866
|
+
expectedContent: namespace,
|
|
867
|
+
expectedAttachment: {
|
|
868
|
+
name: webAttachmentFixture.attachmentName,
|
|
869
|
+
contentMarker: `${namespace} Comment Attachment`,
|
|
870
|
+
},
|
|
871
|
+
});
|
|
872
|
+
commentAttachmentSyncVerified = true;
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
const staleGuardFeedbackId = createdFeedbackIds[4];
|
|
876
|
+
const staleProposalTitle = `${namespace} stale local proposal`;
|
|
877
|
+
const staleDraft = runCli([
|
|
878
|
+
"feedback",
|
|
879
|
+
"edit",
|
|
880
|
+
"--feedback-id",
|
|
881
|
+
staleGuardFeedbackId,
|
|
882
|
+
"--title",
|
|
883
|
+
staleProposalTitle,
|
|
884
|
+
"--reason",
|
|
885
|
+
"Harness creates this artifact before a remote revision.",
|
|
886
|
+
"--json",
|
|
887
|
+
...baseArgs,
|
|
888
|
+
], workspace).stdout;
|
|
889
|
+
assert(staleDraft.artifact_path, "Stale-edit guard did not create its initial local artifact.");
|
|
890
|
+
|
|
891
|
+
const staleRemoteMove = runCli([
|
|
892
|
+
"feedback",
|
|
893
|
+
"move",
|
|
894
|
+
"--feedback-id",
|
|
895
|
+
staleGuardFeedbackId,
|
|
896
|
+
"--from-state",
|
|
897
|
+
"todo",
|
|
898
|
+
"--to-state",
|
|
899
|
+
"in_progress",
|
|
900
|
+
"--reason",
|
|
901
|
+
"Harness advances the server revision after the local artifact was created.",
|
|
902
|
+
"--no-sync",
|
|
903
|
+
"--json",
|
|
904
|
+
...baseArgs,
|
|
905
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
906
|
+
const staleRemoteMoveEventId = responseEventId(staleRemoteMove);
|
|
907
|
+
assert(staleRemoteMoveEventId, "Stale-edit guard move did not return an event id.");
|
|
908
|
+
|
|
909
|
+
const staleValidation = runCli([
|
|
910
|
+
"feedback",
|
|
911
|
+
"validate",
|
|
912
|
+
"--file",
|
|
913
|
+
staleDraft.artifact_path,
|
|
914
|
+
"--json",
|
|
915
|
+
...baseArgs,
|
|
916
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
917
|
+
assert(staleValidation.ok === true && staleValidation.valid === false, "A stale local Feedback artifact unexpectedly validated.");
|
|
918
|
+
assert(
|
|
919
|
+
(staleValidation.errors || []).some((error) => error?.code === "snapshot_mismatch"),
|
|
920
|
+
`Stale artifact did not return snapshot_mismatch: ${JSON.stringify(staleValidation, null, 2)}`,
|
|
921
|
+
);
|
|
922
|
+
|
|
923
|
+
const staleGuardUndo = runCli([
|
|
924
|
+
"feedback",
|
|
925
|
+
"undo",
|
|
926
|
+
"--feedback-id",
|
|
927
|
+
staleGuardFeedbackId,
|
|
928
|
+
"--event-id",
|
|
929
|
+
staleRemoteMoveEventId,
|
|
930
|
+
"--reason",
|
|
931
|
+
"Harness restores the disposable card after stale-artifact certification.",
|
|
932
|
+
"--no-sync",
|
|
933
|
+
"--json",
|
|
934
|
+
...baseArgs,
|
|
935
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
936
|
+
assert(
|
|
937
|
+
String(staleGuardUndo.feedback_state || "") === "todo",
|
|
938
|
+
`Stale-edit guard cleanup did not restore todo: ${JSON.stringify(staleGuardUndo, null, 2)}`,
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
const forcedStaleApply = runCli([
|
|
942
|
+
"feedback",
|
|
943
|
+
"apply",
|
|
944
|
+
"--file",
|
|
945
|
+
staleDraft.artifact_path,
|
|
946
|
+
"--force",
|
|
947
|
+
"--no-sync",
|
|
948
|
+
"--json",
|
|
949
|
+
...baseArgs,
|
|
950
|
+
], workspace, { apiKey: ownerApiKey }).stdout;
|
|
951
|
+
assert(
|
|
952
|
+
String(forcedStaleApply.feedback?.title || "") === staleProposalTitle,
|
|
953
|
+
`Owner forced stale override did not apply the intended title: ${JSON.stringify(forcedStaleApply, null, 2)}`,
|
|
954
|
+
);
|
|
955
|
+
assert(
|
|
956
|
+
(forcedStaleApply.warnings || []).some(
|
|
957
|
+
(warning) => warning?.code === "snapshot_mismatch_forced",
|
|
958
|
+
),
|
|
959
|
+
`Owner forced stale override did not report snapshot_mismatch_forced: ${JSON.stringify(forcedStaleApply, null, 2)}`,
|
|
960
|
+
);
|
|
961
|
+
assert(
|
|
962
|
+
forcedStaleApply.history?.force === true
|
|
963
|
+
&& Boolean(String(forcedStaleApply.history?.reason || "").trim()),
|
|
964
|
+
`Owner forced stale override was not explicitly audited with a reason: ${JSON.stringify(forcedStaleApply, null, 2)}`,
|
|
965
|
+
);
|
|
966
|
+
|
|
967
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
968
|
+
const freshDraft = runCli([
|
|
969
|
+
"feedback",
|
|
970
|
+
"edit",
|
|
971
|
+
"--feedback-id",
|
|
972
|
+
staleGuardFeedbackId,
|
|
973
|
+
"--title",
|
|
974
|
+
`${namespace} fresh proposal after resync`,
|
|
975
|
+
"--reason",
|
|
976
|
+
"Harness confirms a fresh artifact can replace the rejected stale proposal.",
|
|
977
|
+
"--json",
|
|
978
|
+
...baseArgs,
|
|
979
|
+
], workspace).stdout;
|
|
980
|
+
const freshValidation = runCli([
|
|
981
|
+
"feedback",
|
|
982
|
+
"validate",
|
|
983
|
+
"--file",
|
|
984
|
+
freshDraft.artifact_path,
|
|
985
|
+
"--json",
|
|
986
|
+
...baseArgs,
|
|
987
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
988
|
+
assert(
|
|
989
|
+
freshValidation.ok === true && freshValidation.valid !== false,
|
|
990
|
+
`Fresh artifact did not validate after resync: ${JSON.stringify(freshValidation, null, 2)}`,
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
if (collaboratorApiKey) {
|
|
994
|
+
const directApplyDenied = runCli([
|
|
995
|
+
"feedback",
|
|
996
|
+
"apply",
|
|
997
|
+
"--file",
|
|
998
|
+
freshDraft.artifact_path,
|
|
999
|
+
"--no-sync",
|
|
1000
|
+
"--json",
|
|
1001
|
+
...baseArgs,
|
|
1002
|
+
], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
|
|
1003
|
+
roleMatrix.collaborator_direct_apply_denied =
|
|
1004
|
+
directApplyDenied.ok === false && directApplyDenied.status === 403;
|
|
1005
|
+
assert(
|
|
1006
|
+
roleMatrix.collaborator_direct_apply_denied,
|
|
1007
|
+
`Regular collaborator direct apply was not denied: ${JSON.stringify(directApplyDenied, null, 2)}`,
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
const collaboratorArchiveDenied = runCli([
|
|
1011
|
+
"feedback",
|
|
1012
|
+
"move",
|
|
1013
|
+
"--feedback-id",
|
|
1014
|
+
staleGuardFeedbackId,
|
|
1015
|
+
"--from-state",
|
|
1016
|
+
"todo",
|
|
1017
|
+
"--to-state",
|
|
1018
|
+
"archived",
|
|
1019
|
+
"--reason",
|
|
1020
|
+
"Harness expects regular collaborator archive to be denied.",
|
|
1021
|
+
"--no-sync",
|
|
1022
|
+
"--json",
|
|
1023
|
+
...baseArgs,
|
|
1024
|
+
], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
|
|
1025
|
+
roleMatrix.collaborator_archive_denied =
|
|
1026
|
+
collaboratorArchiveDenied.ok === false && collaboratorArchiveDenied.status === 403;
|
|
1027
|
+
assert(
|
|
1028
|
+
roleMatrix.collaborator_archive_denied,
|
|
1029
|
+
`Regular collaborator archive was not denied: ${JSON.stringify(collaboratorArchiveDenied, null, 2)}`,
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const directMove = runCli([
|
|
1034
|
+
"feedback",
|
|
1035
|
+
"move",
|
|
1036
|
+
"--feedback-id",
|
|
1037
|
+
createdFeedbackIds[0],
|
|
1038
|
+
"--from-state",
|
|
1039
|
+
"todo",
|
|
1040
|
+
"--to-state",
|
|
1041
|
+
"in_progress",
|
|
1042
|
+
"--reason",
|
|
1043
|
+
"Harness verifies a collaborator-safe active-state move.",
|
|
1044
|
+
"--no-sync",
|
|
1045
|
+
"--json",
|
|
1046
|
+
...baseArgs,
|
|
1047
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1048
|
+
const directMoveEventId = responseEventId(directMove);
|
|
1049
|
+
assert(directMoveEventId, `Feedback move did not return an event id: ${JSON.stringify(directMove, null, 2)}`);
|
|
1050
|
+
recordCertificationArtifact(manifest, {
|
|
1051
|
+
kind: "feedback_event",
|
|
1052
|
+
id: directMoveEventId,
|
|
1053
|
+
metadata: {
|
|
1054
|
+
feedback_id: createdFeedbackIds[0],
|
|
1055
|
+
action: "move",
|
|
1056
|
+
cleanup: "retain_audit",
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
|
|
1060
|
+
const undo = runCli([
|
|
1061
|
+
"feedback",
|
|
1062
|
+
"undo",
|
|
1063
|
+
"--feedback-id",
|
|
1064
|
+
createdFeedbackIds[0],
|
|
1065
|
+
"--event-id",
|
|
1066
|
+
directMoveEventId,
|
|
1067
|
+
"--reason",
|
|
1068
|
+
"Harness verifies exact event undo.",
|
|
1069
|
+
"--json",
|
|
1070
|
+
...baseArgs,
|
|
1071
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1072
|
+
assert(
|
|
1073
|
+
String(undo.feedback_state || "") === "todo",
|
|
1074
|
+
`Feedback undo did not restore todo: ${JSON.stringify(undo, null, 2)}`,
|
|
1075
|
+
);
|
|
1076
|
+
const undoEventId = responseEventId(undo);
|
|
1077
|
+
if (undoEventId) {
|
|
1078
|
+
recordCertificationArtifact(manifest, {
|
|
1079
|
+
kind: "feedback_event",
|
|
1080
|
+
id: undoEventId,
|
|
1081
|
+
metadata: {
|
|
1082
|
+
feedback_id: createdFeedbackIds[0],
|
|
1083
|
+
action: "undo",
|
|
1084
|
+
cleanup: "retain_audit",
|
|
1085
|
+
},
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
const getAfterUndo = runCli([
|
|
1090
|
+
"feedback",
|
|
1091
|
+
"get",
|
|
1092
|
+
"--feedback-id",
|
|
1093
|
+
createdFeedbackIds[0],
|
|
1094
|
+
"--json",
|
|
1095
|
+
...baseArgs,
|
|
1096
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1097
|
+
assert(
|
|
1098
|
+
String(getAfterUndo.feedback?.feedback_state || "") === "todo",
|
|
1099
|
+
`Feedback get did not return the restored state: ${JSON.stringify(getAfterUndo, null, 2)}`,
|
|
1100
|
+
);
|
|
1101
|
+
const historyAfterUndo = runCli([
|
|
1102
|
+
"feedback",
|
|
1103
|
+
"history",
|
|
1104
|
+
"--feedback-id",
|
|
1105
|
+
createdFeedbackIds[0],
|
|
1106
|
+
"--json",
|
|
1107
|
+
...baseArgs,
|
|
1108
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1109
|
+
assert(
|
|
1110
|
+
Array.isArray(historyAfterUndo.events) && historyAfterUndo.events.length >= 2,
|
|
1111
|
+
"Feedback history did not include move and undo evidence.",
|
|
1112
|
+
);
|
|
1113
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
1114
|
+
|
|
1115
|
+
runCli(
|
|
1116
|
+
["feedback-sync", "--json", ...baseArgs],
|
|
1117
|
+
workspace,
|
|
1118
|
+
{ apiKey: collaboratorWriteKey },
|
|
1119
|
+
);
|
|
1120
|
+
for (const feedbackId of createdFeedbackIds) {
|
|
1121
|
+
const draft = runCli([
|
|
1122
|
+
"feedback",
|
|
978
1123
|
"status",
|
|
979
1124
|
"--feedback-id",
|
|
980
1125
|
feedbackId,
|
|
@@ -984,177 +1129,177 @@ async function main() {
|
|
|
984
1129
|
reason,
|
|
985
1130
|
"--json",
|
|
986
1131
|
...baseArgs,
|
|
987
|
-
], workspace).stdout;
|
|
988
|
-
assert(draft.artifact_path, `feedback status did not return artifact_path for ${feedbackId}`);
|
|
989
|
-
|
|
990
|
-
const validation = runCli([
|
|
991
|
-
"feedback",
|
|
992
|
-
"validate",
|
|
993
|
-
"--file",
|
|
994
|
-
draft.artifact_path,
|
|
995
|
-
"--json",
|
|
996
|
-
...baseArgs,
|
|
997
|
-
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
998
|
-
assert(
|
|
999
|
-
validation.ok === true && validation.valid !== false,
|
|
1000
|
-
`feedback validate failed for ${feedbackId}: ${JSON.stringify(validation, null, 2)}`,
|
|
1001
|
-
);
|
|
1002
|
-
|
|
1003
|
-
const submit = runCli([
|
|
1132
|
+
], workspace).stdout;
|
|
1133
|
+
assert(draft.artifact_path, `feedback status did not return artifact_path for ${feedbackId}`);
|
|
1134
|
+
|
|
1135
|
+
const validation = runCli([
|
|
1136
|
+
"feedback",
|
|
1137
|
+
"validate",
|
|
1138
|
+
"--file",
|
|
1139
|
+
draft.artifact_path,
|
|
1140
|
+
"--json",
|
|
1141
|
+
...baseArgs,
|
|
1142
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1143
|
+
assert(
|
|
1144
|
+
validation.ok === true && validation.valid !== false,
|
|
1145
|
+
`feedback validate failed for ${feedbackId}: ${JSON.stringify(validation, null, 2)}`,
|
|
1146
|
+
);
|
|
1147
|
+
|
|
1148
|
+
const submit = runCli([
|
|
1004
1149
|
"feedback",
|
|
1005
1150
|
"submit",
|
|
1006
|
-
"--file",
|
|
1007
|
-
draft.artifact_path,
|
|
1008
|
-
"--confirm-write",
|
|
1009
|
-
"--approval-artifact",
|
|
1010
|
-
draft.artifact_path,
|
|
1011
|
-
"--json",
|
|
1012
|
-
...baseArgs,
|
|
1013
|
-
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1014
|
-
const requestId = responseRequestId(submit);
|
|
1151
|
+
"--file",
|
|
1152
|
+
draft.artifact_path,
|
|
1153
|
+
"--confirm-write",
|
|
1154
|
+
"--approval-artifact",
|
|
1155
|
+
draft.artifact_path,
|
|
1156
|
+
"--json",
|
|
1157
|
+
...baseArgs,
|
|
1158
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1159
|
+
const requestId = responseRequestId(submit);
|
|
1015
1160
|
assert(requestId, `feedback submit did not return request id for ${feedbackId}: ${JSON.stringify(submit, null, 2)}`);
|
|
1016
|
-
reviewRequestIds.push(requestId);
|
|
1017
|
-
reviewArtifactByRequestId.set(requestId, draft.artifact_path);
|
|
1018
|
-
recordCertificationArtifact(manifest, {
|
|
1019
|
-
kind: "review_request",
|
|
1020
|
-
id: requestId,
|
|
1021
|
-
metadata: { feedback_id: feedbackId },
|
|
1022
|
-
});
|
|
1023
|
-
writeCertificationManifest(manifestPath, manifest);
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
const openReviews = runCli([
|
|
1027
|
-
"feedback",
|
|
1028
|
-
"reviews",
|
|
1029
|
-
"--status",
|
|
1030
|
-
"open",
|
|
1031
|
-
"--json",
|
|
1032
|
-
...baseArgs,
|
|
1033
|
-
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1034
|
-
const openRequestIds = new Set(
|
|
1035
|
-
(openReviews.requests || []).map((request) => responseRequestId(request)).filter(Boolean),
|
|
1036
|
-
);
|
|
1037
|
-
assert(
|
|
1038
|
-
reviewRequestIds.every((requestId) => openRequestIds.has(requestId)),
|
|
1039
|
-
"Feedback review list did not include every submitted certification request.",
|
|
1040
|
-
);
|
|
1041
|
-
const reviewDetail = runCli([
|
|
1042
|
-
"feedback",
|
|
1043
|
-
"reviews",
|
|
1044
|
-
"--request-id",
|
|
1045
|
-
reviewRequestIds[0],
|
|
1046
|
-
"--json",
|
|
1047
|
-
...baseArgs,
|
|
1048
|
-
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1049
|
-
assert(
|
|
1050
|
-
responseRequestId(reviewDetail) === reviewRequestIds[0],
|
|
1051
|
-
"Feedback review detail returned the wrong request.",
|
|
1052
|
-
);
|
|
1053
|
-
|
|
1054
|
-
if (collaboratorApiKey) {
|
|
1055
|
-
const deniedReview = runCli([
|
|
1056
|
-
"feedback",
|
|
1057
|
-
"review",
|
|
1058
|
-
"--request-id",
|
|
1059
|
-
reviewRequestIds[0],
|
|
1060
|
-
"--action",
|
|
1061
|
-
"approve",
|
|
1062
|
-
"--reason",
|
|
1063
|
-
"Harness expects regular collaborator approval to be denied.",
|
|
1064
|
-
"--no-sync",
|
|
1065
|
-
"--json",
|
|
1066
|
-
...baseArgs,
|
|
1067
|
-
], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
|
|
1068
|
-
roleMatrix.collaborator_review_denied =
|
|
1069
|
-
deniedReview.ok === false && deniedReview.status === 403;
|
|
1070
|
-
assert(
|
|
1071
|
-
roleMatrix.collaborator_review_denied,
|
|
1072
|
-
`Regular collaborator review was not denied as expected: ${JSON.stringify(deniedReview, null, 2)}`,
|
|
1073
|
-
);
|
|
1074
|
-
}
|
|
1075
|
-
|
|
1076
|
-
const requestChanges = runCli([
|
|
1077
|
-
"feedback",
|
|
1078
|
-
"review",
|
|
1079
|
-
"--request-id",
|
|
1080
|
-
reviewRequestIds[0],
|
|
1081
|
-
"--action",
|
|
1082
|
-
"request_changes",
|
|
1083
|
-
"--reason",
|
|
1084
|
-
"Harness requests a submitter revision.",
|
|
1085
|
-
"--verification-evidence",
|
|
1086
|
-
"Initial validation completed; revision path remains to be certified.",
|
|
1087
|
-
"--no-sync",
|
|
1088
|
-
"--json",
|
|
1089
|
-
...baseArgs,
|
|
1090
|
-
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
1091
|
-
assert(
|
|
1092
|
-
String(requestChanges.request?.status || requestChanges.status || "") === "needs_changes",
|
|
1093
|
-
`Request-changes did not enter needs_changes: ${JSON.stringify(requestChanges, null, 2)}`,
|
|
1094
|
-
);
|
|
1095
|
-
|
|
1096
|
-
const revised = runCli([
|
|
1097
|
-
"feedback",
|
|
1098
|
-
"revise",
|
|
1099
|
-
"--request-id",
|
|
1100
|
-
reviewRequestIds[0],
|
|
1101
|
-
"--file",
|
|
1102
|
-
reviewArtifactByRequestId.get(reviewRequestIds[0]),
|
|
1103
|
-
"--confirm-write",
|
|
1104
|
-
"--approval-artifact",
|
|
1105
|
-
reviewArtifactByRequestId.get(reviewRequestIds[0]),
|
|
1106
|
-
"--json",
|
|
1107
|
-
...baseArgs,
|
|
1108
|
-
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1109
|
-
assert(
|
|
1110
|
-
responseRequestId(revised) === reviewRequestIds[0],
|
|
1111
|
-
`Feedback revise returned the wrong request: ${JSON.stringify(revised, null, 2)}`,
|
|
1112
|
-
);
|
|
1113
|
-
|
|
1114
|
-
const rejected = runCli([
|
|
1115
|
-
"feedback",
|
|
1116
|
-
"review",
|
|
1117
|
-
"--request-id",
|
|
1118
|
-
reviewRequestIds[1],
|
|
1119
|
-
"--action",
|
|
1120
|
-
"reject",
|
|
1121
|
-
"--reason",
|
|
1122
|
-
"Harness verifies terminal rejection without applying changes.",
|
|
1123
|
-
"--verification-evidence",
|
|
1124
|
-
"Negative review path certified.",
|
|
1125
|
-
"--no-sync",
|
|
1126
|
-
"--json",
|
|
1127
|
-
...baseArgs,
|
|
1128
|
-
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
1129
|
-
assert(
|
|
1130
|
-
String(rejected.request?.status || rejected.status || "") === "rejected",
|
|
1131
|
-
`Feedback rejection did not become terminal: ${JSON.stringify(rejected, null, 2)}`,
|
|
1132
|
-
);
|
|
1133
|
-
|
|
1134
|
-
const cancelled = runCli([
|
|
1135
|
-
"feedback",
|
|
1136
|
-
"review",
|
|
1137
|
-
"--request-id",
|
|
1138
|
-
reviewRequestIds[2],
|
|
1139
|
-
"--action",
|
|
1140
|
-
"cancel",
|
|
1141
|
-
"--reason",
|
|
1142
|
-
"Harness submitter cancels a disposable review.",
|
|
1143
|
-
"--no-sync",
|
|
1144
|
-
"--json",
|
|
1145
|
-
...baseArgs,
|
|
1146
|
-
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1147
|
-
assert(
|
|
1148
|
-
String(cancelled.request?.status || cancelled.status || "") === "cancelled",
|
|
1149
|
-
`Feedback cancellation did not become terminal: ${JSON.stringify(cancelled, null, 2)}`,
|
|
1150
|
-
);
|
|
1151
|
-
|
|
1152
|
-
const pendingApprovalRequestIds = [
|
|
1153
|
-
reviewRequestIds[0],
|
|
1154
|
-
reviewRequestIds[3],
|
|
1155
|
-
reviewRequestIds[4],
|
|
1156
|
-
];
|
|
1157
|
-
const blocked = runCli([
|
|
1161
|
+
reviewRequestIds.push(requestId);
|
|
1162
|
+
reviewArtifactByRequestId.set(requestId, draft.artifact_path);
|
|
1163
|
+
recordCertificationArtifact(manifest, {
|
|
1164
|
+
kind: "review_request",
|
|
1165
|
+
id: requestId,
|
|
1166
|
+
metadata: { feedback_id: feedbackId },
|
|
1167
|
+
});
|
|
1168
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
const openReviews = runCli([
|
|
1172
|
+
"feedback",
|
|
1173
|
+
"reviews",
|
|
1174
|
+
"--status",
|
|
1175
|
+
"open",
|
|
1176
|
+
"--json",
|
|
1177
|
+
...baseArgs,
|
|
1178
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1179
|
+
const openRequestIds = new Set(
|
|
1180
|
+
(openReviews.requests || []).map((request) => responseRequestId(request)).filter(Boolean),
|
|
1181
|
+
);
|
|
1182
|
+
assert(
|
|
1183
|
+
reviewRequestIds.every((requestId) => openRequestIds.has(requestId)),
|
|
1184
|
+
"Feedback review list did not include every submitted certification request.",
|
|
1185
|
+
);
|
|
1186
|
+
const reviewDetail = runCli([
|
|
1187
|
+
"feedback",
|
|
1188
|
+
"reviews",
|
|
1189
|
+
"--request-id",
|
|
1190
|
+
reviewRequestIds[0],
|
|
1191
|
+
"--json",
|
|
1192
|
+
...baseArgs,
|
|
1193
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1194
|
+
assert(
|
|
1195
|
+
responseRequestId(reviewDetail) === reviewRequestIds[0],
|
|
1196
|
+
"Feedback review detail returned the wrong request.",
|
|
1197
|
+
);
|
|
1198
|
+
|
|
1199
|
+
if (collaboratorApiKey) {
|
|
1200
|
+
const deniedReview = runCli([
|
|
1201
|
+
"feedback",
|
|
1202
|
+
"review",
|
|
1203
|
+
"--request-id",
|
|
1204
|
+
reviewRequestIds[0],
|
|
1205
|
+
"--action",
|
|
1206
|
+
"approve",
|
|
1207
|
+
"--reason",
|
|
1208
|
+
"Harness expects regular collaborator approval to be denied.",
|
|
1209
|
+
"--no-sync",
|
|
1210
|
+
"--json",
|
|
1211
|
+
...baseArgs,
|
|
1212
|
+
], workspace, { allowFailure: true, apiKey: collaboratorApiKey }).stdout;
|
|
1213
|
+
roleMatrix.collaborator_review_denied =
|
|
1214
|
+
deniedReview.ok === false && deniedReview.status === 403;
|
|
1215
|
+
assert(
|
|
1216
|
+
roleMatrix.collaborator_review_denied,
|
|
1217
|
+
`Regular collaborator review was not denied as expected: ${JSON.stringify(deniedReview, null, 2)}`,
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
const requestChanges = runCli([
|
|
1222
|
+
"feedback",
|
|
1223
|
+
"review",
|
|
1224
|
+
"--request-id",
|
|
1225
|
+
reviewRequestIds[0],
|
|
1226
|
+
"--action",
|
|
1227
|
+
"request_changes",
|
|
1228
|
+
"--reason",
|
|
1229
|
+
"Harness requests a submitter revision.",
|
|
1230
|
+
"--verification-evidence",
|
|
1231
|
+
"Initial validation completed; revision path remains to be certified.",
|
|
1232
|
+
"--no-sync",
|
|
1233
|
+
"--json",
|
|
1234
|
+
...baseArgs,
|
|
1235
|
+
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
1236
|
+
assert(
|
|
1237
|
+
String(requestChanges.request?.status || requestChanges.status || "") === "needs_changes",
|
|
1238
|
+
`Request-changes did not enter needs_changes: ${JSON.stringify(requestChanges, null, 2)}`,
|
|
1239
|
+
);
|
|
1240
|
+
|
|
1241
|
+
const revised = runCli([
|
|
1242
|
+
"feedback",
|
|
1243
|
+
"revise",
|
|
1244
|
+
"--request-id",
|
|
1245
|
+
reviewRequestIds[0],
|
|
1246
|
+
"--file",
|
|
1247
|
+
reviewArtifactByRequestId.get(reviewRequestIds[0]),
|
|
1248
|
+
"--confirm-write",
|
|
1249
|
+
"--approval-artifact",
|
|
1250
|
+
reviewArtifactByRequestId.get(reviewRequestIds[0]),
|
|
1251
|
+
"--json",
|
|
1252
|
+
...baseArgs,
|
|
1253
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1254
|
+
assert(
|
|
1255
|
+
responseRequestId(revised) === reviewRequestIds[0],
|
|
1256
|
+
`Feedback revise returned the wrong request: ${JSON.stringify(revised, null, 2)}`,
|
|
1257
|
+
);
|
|
1258
|
+
|
|
1259
|
+
const rejected = runCli([
|
|
1260
|
+
"feedback",
|
|
1261
|
+
"review",
|
|
1262
|
+
"--request-id",
|
|
1263
|
+
reviewRequestIds[1],
|
|
1264
|
+
"--action",
|
|
1265
|
+
"reject",
|
|
1266
|
+
"--reason",
|
|
1267
|
+
"Harness verifies terminal rejection without applying changes.",
|
|
1268
|
+
"--verification-evidence",
|
|
1269
|
+
"Negative review path certified.",
|
|
1270
|
+
"--no-sync",
|
|
1271
|
+
"--json",
|
|
1272
|
+
...baseArgs,
|
|
1273
|
+
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
1274
|
+
assert(
|
|
1275
|
+
String(rejected.request?.status || rejected.status || "") === "rejected",
|
|
1276
|
+
`Feedback rejection did not become terminal: ${JSON.stringify(rejected, null, 2)}`,
|
|
1277
|
+
);
|
|
1278
|
+
|
|
1279
|
+
const cancelled = runCli([
|
|
1280
|
+
"feedback",
|
|
1281
|
+
"review",
|
|
1282
|
+
"--request-id",
|
|
1283
|
+
reviewRequestIds[2],
|
|
1284
|
+
"--action",
|
|
1285
|
+
"cancel",
|
|
1286
|
+
"--reason",
|
|
1287
|
+
"Harness submitter cancels a disposable review.",
|
|
1288
|
+
"--no-sync",
|
|
1289
|
+
"--json",
|
|
1290
|
+
...baseArgs,
|
|
1291
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1292
|
+
assert(
|
|
1293
|
+
String(cancelled.request?.status || cancelled.status || "") === "cancelled",
|
|
1294
|
+
`Feedback cancellation did not become terminal: ${JSON.stringify(cancelled, null, 2)}`,
|
|
1295
|
+
);
|
|
1296
|
+
|
|
1297
|
+
const pendingApprovalRequestIds = [
|
|
1298
|
+
reviewRequestIds[0],
|
|
1299
|
+
reviewRequestIds[3],
|
|
1300
|
+
reviewRequestIds[4],
|
|
1301
|
+
];
|
|
1302
|
+
const blocked = runCli([
|
|
1158
1303
|
"feedback",
|
|
1159
1304
|
"move",
|
|
1160
1305
|
"--feedback-ids",
|
|
@@ -1173,69 +1318,69 @@ async function main() {
|
|
|
1173
1318
|
assert(blocked.status === 409, `Expected 409 pending review block, got ${blocked.status}.`);
|
|
1174
1319
|
assert(blocked.data?.code === "pending_feedback_review", `Expected pending_feedback_review, got ${JSON.stringify(blocked, null, 2)}.`);
|
|
1175
1320
|
|
|
1176
|
-
const ownerApproval = runCli([
|
|
1177
|
-
"feedback",
|
|
1178
|
-
"review",
|
|
1179
|
-
"--request-id",
|
|
1180
|
-
reviewRequestIds[0],
|
|
1181
|
-
"--action",
|
|
1182
|
-
"approve",
|
|
1183
|
-
"--reason",
|
|
1184
|
-
"Harness owner approval.",
|
|
1185
|
-
"--verification-evidence",
|
|
1186
|
-
"Stale-state, validation, role denial, request-changes, and revision checks passed.",
|
|
1187
|
-
"--deployment-evidence",
|
|
1188
|
-
"Dev post-deploy certification run; artifacts will be archived.",
|
|
1189
|
-
"--no-sync",
|
|
1190
|
-
"--json",
|
|
1191
|
-
...baseArgs,
|
|
1192
|
-
], workspace, { apiKey: ownerApiKey }).stdout;
|
|
1193
|
-
roleMatrix.owner_review_used = true;
|
|
1194
|
-
assert(
|
|
1195
|
-
String(ownerApproval.request?.status || ownerApproval.status || "") === "applied",
|
|
1196
|
-
`Owner approval did not apply: ${JSON.stringify(ownerApproval, null, 2)}`,
|
|
1197
|
-
);
|
|
1198
|
-
|
|
1199
|
-
const batchApprovalRequestIds = [
|
|
1200
|
-
reviewRequestIds[3],
|
|
1201
|
-
reviewRequestIds[4],
|
|
1202
|
-
];
|
|
1203
|
-
const review = runCli([
|
|
1204
|
-
"feedback",
|
|
1205
|
-
"review",
|
|
1206
|
-
"--request-ids",
|
|
1207
|
-
batchApprovalRequestIds.join(","),
|
|
1208
|
-
"--action",
|
|
1209
|
-
"approve",
|
|
1210
|
-
"--reason",
|
|
1211
|
-
"Harness batch approval.",
|
|
1212
|
-
"--verification-evidence",
|
|
1213
|
-
"Validation, role checks, request-changes, and direct board guards passed.",
|
|
1214
|
-
"--deployment-evidence",
|
|
1215
|
-
"Dev post-deploy certification run; artifacts will be archived.",
|
|
1216
|
-
"--json",
|
|
1217
|
-
...baseArgs,
|
|
1218
|
-
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
1219
|
-
roleMatrix.delegate_review_used = Boolean(delegateApiKey);
|
|
1220
|
-
assert(review.ok !== false && review.updated_count === 2, `batch review failed: ${JSON.stringify(review, null, 2)}`);
|
|
1221
|
-
|
|
1222
|
-
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
1223
|
-
assertFeedbackPresent(
|
|
1224
|
-
workspace,
|
|
1225
|
-
[createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
|
|
1226
|
-
"completed",
|
|
1227
|
-
);
|
|
1228
|
-
assertFeedbackPresent(
|
|
1229
|
-
workspace,
|
|
1230
|
-
[createdFeedbackIds[1], createdFeedbackIds[2]],
|
|
1231
|
-
"todo",
|
|
1232
|
-
);
|
|
1233
|
-
|
|
1234
|
-
const reopen = runCli([
|
|
1321
|
+
const ownerApproval = runCli([
|
|
1322
|
+
"feedback",
|
|
1323
|
+
"review",
|
|
1324
|
+
"--request-id",
|
|
1325
|
+
reviewRequestIds[0],
|
|
1326
|
+
"--action",
|
|
1327
|
+
"approve",
|
|
1328
|
+
"--reason",
|
|
1329
|
+
"Harness owner approval.",
|
|
1330
|
+
"--verification-evidence",
|
|
1331
|
+
"Stale-state, validation, role denial, request-changes, and revision checks passed.",
|
|
1332
|
+
"--deployment-evidence",
|
|
1333
|
+
"Dev post-deploy certification run; artifacts will be archived.",
|
|
1334
|
+
"--no-sync",
|
|
1335
|
+
"--json",
|
|
1336
|
+
...baseArgs,
|
|
1337
|
+
], workspace, { apiKey: ownerApiKey }).stdout;
|
|
1338
|
+
roleMatrix.owner_review_used = true;
|
|
1339
|
+
assert(
|
|
1340
|
+
String(ownerApproval.request?.status || ownerApproval.status || "") === "applied",
|
|
1341
|
+
`Owner approval did not apply: ${JSON.stringify(ownerApproval, null, 2)}`,
|
|
1342
|
+
);
|
|
1343
|
+
|
|
1344
|
+
const batchApprovalRequestIds = [
|
|
1345
|
+
reviewRequestIds[3],
|
|
1346
|
+
reviewRequestIds[4],
|
|
1347
|
+
];
|
|
1348
|
+
const review = runCli([
|
|
1349
|
+
"feedback",
|
|
1350
|
+
"review",
|
|
1351
|
+
"--request-ids",
|
|
1352
|
+
batchApprovalRequestIds.join(","),
|
|
1353
|
+
"--action",
|
|
1354
|
+
"approve",
|
|
1355
|
+
"--reason",
|
|
1356
|
+
"Harness batch approval.",
|
|
1357
|
+
"--verification-evidence",
|
|
1358
|
+
"Validation, role checks, request-changes, and direct board guards passed.",
|
|
1359
|
+
"--deployment-evidence",
|
|
1360
|
+
"Dev post-deploy certification run; artifacts will be archived.",
|
|
1361
|
+
"--json",
|
|
1362
|
+
...baseArgs,
|
|
1363
|
+
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
1364
|
+
roleMatrix.delegate_review_used = Boolean(delegateApiKey);
|
|
1365
|
+
assert(review.ok !== false && review.updated_count === 2, `batch review failed: ${JSON.stringify(review, null, 2)}`);
|
|
1366
|
+
|
|
1367
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
1368
|
+
assertFeedbackPresent(
|
|
1369
|
+
workspace,
|
|
1370
|
+
[createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
|
|
1371
|
+
"completed",
|
|
1372
|
+
);
|
|
1373
|
+
assertFeedbackPresent(
|
|
1374
|
+
workspace,
|
|
1375
|
+
[createdFeedbackIds[1], createdFeedbackIds[2]],
|
|
1376
|
+
"todo",
|
|
1377
|
+
);
|
|
1378
|
+
|
|
1379
|
+
const reopen = runCli([
|
|
1235
1380
|
"feedback",
|
|
1236
1381
|
"move",
|
|
1237
1382
|
"--feedback-ids",
|
|
1238
|
-
[createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]].join(","),
|
|
1383
|
+
[createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]].join(","),
|
|
1239
1384
|
"--from-state",
|
|
1240
1385
|
"completed",
|
|
1241
1386
|
"--to-state",
|
|
@@ -1245,206 +1390,206 @@ async function main() {
|
|
|
1245
1390
|
"--json",
|
|
1246
1391
|
...baseArgs,
|
|
1247
1392
|
], workspace).stdout;
|
|
1248
|
-
assert(reopen.ok !== false && reopen.updated_count === 3, `batch active move failed: ${JSON.stringify(reopen, null, 2)}`);
|
|
1249
|
-
|
|
1250
|
-
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
1251
|
-
assertFeedbackPresent(
|
|
1252
|
-
workspace,
|
|
1253
|
-
[createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
|
|
1254
|
-
"in_progress",
|
|
1255
|
-
);
|
|
1393
|
+
assert(reopen.ok !== false && reopen.updated_count === 3, `batch active move failed: ${JSON.stringify(reopen, null, 2)}`);
|
|
1394
|
+
|
|
1395
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
1396
|
+
assertFeedbackPresent(
|
|
1397
|
+
workspace,
|
|
1398
|
+
[createdFeedbackIds[0], createdFeedbackIds[3], createdFeedbackIds[4]],
|
|
1399
|
+
"in_progress",
|
|
1400
|
+
);
|
|
1256
1401
|
|
|
1257
1402
|
const archive = runCli([
|
|
1258
1403
|
"feedback",
|
|
1259
|
-
"move",
|
|
1260
|
-
"--feedback-ids",
|
|
1261
|
-
createdFeedbackIds.join(","),
|
|
1262
|
-
"--to-state",
|
|
1263
|
-
"archived",
|
|
1404
|
+
"move",
|
|
1405
|
+
"--feedback-ids",
|
|
1406
|
+
createdFeedbackIds.join(","),
|
|
1407
|
+
"--to-state",
|
|
1408
|
+
"archived",
|
|
1264
1409
|
"--reason",
|
|
1265
1410
|
"Harness cleanup archive.",
|
|
1266
1411
|
"--json",
|
|
1267
1412
|
...baseArgs,
|
|
1268
|
-
], workspace).stdout;
|
|
1269
|
-
assert(archive.ok !== false && archive.updated_count === 5, `batch archive failed: ${JSON.stringify(archive, null, 2)}`);
|
|
1270
|
-
|
|
1271
|
-
const selectedDocument = documentSetCreate.documents[1];
|
|
1272
|
-
const selectedDocumentId = String(selectedDocument?.document_id || "").trim();
|
|
1273
|
-
assert(selectedDocumentId, "Three-document PRD did not return a second document id.");
|
|
1274
|
-
const revisedDocumentTitle = `${namespace} Document Set Harness Revised`;
|
|
1275
|
-
const revisedDocumentPath = writePrd(
|
|
1276
|
-
workspace,
|
|
1277
|
-
revisedDocumentTitle,
|
|
1278
|
-
202,
|
|
1279
|
-
);
|
|
1280
|
-
const documentDraft = runCli([
|
|
1281
|
-
"feedback",
|
|
1282
|
-
"edit",
|
|
1283
|
-
"--feedback-id",
|
|
1284
|
-
documentSetFeedbackId,
|
|
1285
|
-
"--prd-file",
|
|
1286
|
-
revisedDocumentPath,
|
|
1287
|
-
"--document-id",
|
|
1288
|
-
selectedDocumentId,
|
|
1289
|
-
"--reason",
|
|
1290
|
-
"Harness verifies document-scoped refinement.",
|
|
1291
|
-
"--json",
|
|
1292
|
-
...baseArgs,
|
|
1293
|
-
], workspace).stdout;
|
|
1294
|
-
assert(documentDraft.artifact_path, "Document-scoped edit did not create a review artifact.");
|
|
1295
|
-
const documentValidation = runCli([
|
|
1296
|
-
"feedback",
|
|
1297
|
-
"validate",
|
|
1298
|
-
"--file",
|
|
1299
|
-
documentDraft.artifact_path,
|
|
1300
|
-
"--json",
|
|
1301
|
-
...baseArgs,
|
|
1302
|
-
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1303
|
-
assert(
|
|
1304
|
-
documentValidation.ok === true && documentValidation.valid !== false,
|
|
1305
|
-
`Document-scoped refinement did not validate: ${JSON.stringify(documentValidation, null, 2)}`,
|
|
1306
|
-
);
|
|
1307
|
-
const documentSubmit = runCli([
|
|
1308
|
-
"feedback",
|
|
1309
|
-
"submit",
|
|
1310
|
-
"--file",
|
|
1311
|
-
documentDraft.artifact_path,
|
|
1312
|
-
"--confirm-write",
|
|
1313
|
-
"--approval-artifact",
|
|
1314
|
-
documentDraft.artifact_path,
|
|
1315
|
-
"--json",
|
|
1316
|
-
...baseArgs,
|
|
1317
|
-
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1318
|
-
documentSetRequestId = responseRequestId(documentSubmit);
|
|
1319
|
-
assert(documentSetRequestId, "Document-scoped refinement did not return a review request id.");
|
|
1320
|
-
recordCertificationArtifact(manifest, {
|
|
1321
|
-
kind: "review_request",
|
|
1322
|
-
id: documentSetRequestId,
|
|
1323
|
-
metadata: {
|
|
1324
|
-
feedback_id: documentSetFeedbackId,
|
|
1325
|
-
document_id: selectedDocumentId,
|
|
1326
|
-
},
|
|
1327
|
-
});
|
|
1328
|
-
const documentApproval = runCli([
|
|
1329
|
-
"feedback",
|
|
1330
|
-
"review",
|
|
1331
|
-
"--request-id",
|
|
1332
|
-
documentSetRequestId,
|
|
1333
|
-
"--action",
|
|
1334
|
-
"approve",
|
|
1335
|
-
"--reason",
|
|
1336
|
-
"Harness approves one document without replacing its siblings.",
|
|
1337
|
-
"--verification-evidence",
|
|
1338
|
-
"Document-set validation and sibling-preservation checks passed.",
|
|
1339
|
-
"--deployment-evidence",
|
|
1340
|
-
"Dev post-deploy certification only.",
|
|
1341
|
-
"--no-sync",
|
|
1342
|
-
"--json",
|
|
1343
|
-
...baseArgs,
|
|
1344
|
-
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
1345
|
-
assert(
|
|
1346
|
-
String(documentApproval.request?.status || documentApproval.status || "") === "applied",
|
|
1347
|
-
`Document-scoped approval did not apply: ${JSON.stringify(documentApproval, null, 2)}`,
|
|
1348
|
-
);
|
|
1349
|
-
|
|
1350
|
-
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
1351
|
-
const synchronizedDocumentRoot = path.join(
|
|
1352
|
-
workspace,
|
|
1353
|
-
"MyteCommandCenter",
|
|
1354
|
-
"PRD",
|
|
1355
|
-
"feedback-sync",
|
|
1356
|
-
documentSetFeedbackId,
|
|
1357
|
-
);
|
|
1358
|
-
const synchronizedDocuments = fs.readdirSync(synchronizedDocumentRoot)
|
|
1359
|
-
.filter((name) => name.toLowerCase().endsWith(".md"))
|
|
1360
|
-
.sort();
|
|
1361
|
-
assert(synchronizedDocuments.length === 3, "Document refinement changed the document count.");
|
|
1362
|
-
assert(
|
|
1363
|
-
fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[0]), "utf8")
|
|
1364
|
-
.includes(`# ${documentSetTitles[0]}`),
|
|
1365
|
-
"Document refinement replaced the first sibling document.",
|
|
1366
|
-
);
|
|
1367
|
-
assert(
|
|
1368
|
-
fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[1]), "utf8")
|
|
1369
|
-
.includes(`# ${revisedDocumentTitle}`),
|
|
1370
|
-
"Document refinement did not update the selected second document.",
|
|
1371
|
-
);
|
|
1372
|
-
assert(
|
|
1373
|
-
fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[2]), "utf8")
|
|
1374
|
-
.includes(`# ${documentSetTitles[2]}`),
|
|
1375
|
-
"Document refinement replaced the third sibling document.",
|
|
1376
|
-
);
|
|
1377
|
-
|
|
1378
|
-
const versions = runCli([
|
|
1379
|
-
"feedback",
|
|
1380
|
-
"prd-versions",
|
|
1381
|
-
"--feedback-id",
|
|
1382
|
-
documentSetFeedbackId,
|
|
1383
|
-
"--json",
|
|
1384
|
-
...baseArgs,
|
|
1385
|
-
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1386
|
-
assert(
|
|
1387
|
-
Array.isArray(versions.versions) && versions.versions.length >= 2,
|
|
1388
|
-
`Document refinement did not retain PRD versions: ${JSON.stringify(versions, null, 2)}`,
|
|
1389
|
-
);
|
|
1390
|
-
const activeVersionId = String(versions.active_prd_version_id || "").trim();
|
|
1391
|
-
const targetVersion =
|
|
1392
|
-
versions.versions.find((version) => responseVersionId(version) !== activeVersionId)
|
|
1393
|
-
|| versions.versions[0];
|
|
1394
|
-
const targetVersionId = responseVersionId(targetVersion);
|
|
1395
|
-
assert(targetVersionId, "PRD version list did not return a target version id.");
|
|
1396
|
-
const versionDiff = runCli([
|
|
1397
|
-
"feedback",
|
|
1398
|
-
"prd-diff",
|
|
1399
|
-
"--feedback-id",
|
|
1400
|
-
documentSetFeedbackId,
|
|
1401
|
-
"--version-id",
|
|
1402
|
-
targetVersionId,
|
|
1403
|
-
"--document-id",
|
|
1404
|
-
selectedDocumentId,
|
|
1405
|
-
"--json",
|
|
1406
|
-
...baseArgs,
|
|
1407
|
-
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1408
|
-
assert(
|
|
1409
|
-
String(versionDiff.document_id || "") === selectedDocumentId,
|
|
1410
|
-
`PRD diff was not scoped to the selected document: ${JSON.stringify(versionDiff, null, 2)}`,
|
|
1411
|
-
);
|
|
1412
|
-
for (const version of versions.versions) {
|
|
1413
|
-
const versionId = responseVersionId(version);
|
|
1414
|
-
if (!versionId) continue;
|
|
1415
|
-
recordCertificationArtifact(manifest, {
|
|
1416
|
-
kind: "object",
|
|
1417
|
-
id: versionId,
|
|
1418
|
-
metadata: {
|
|
1419
|
-
object_type: "prd_version",
|
|
1420
|
-
feedback_id: documentSetFeedbackId,
|
|
1421
|
-
cleanup: "retain_audit",
|
|
1422
|
-
},
|
|
1423
|
-
});
|
|
1424
|
-
}
|
|
1425
|
-
writeCertificationManifest(manifestPath, manifest);
|
|
1426
|
-
|
|
1427
|
-
const archiveDocumentSet = runCli([
|
|
1428
|
-
"feedback",
|
|
1429
|
-
"move",
|
|
1430
|
-
"--feedback-id",
|
|
1431
|
-
documentSetFeedbackId,
|
|
1432
|
-
"--from-state",
|
|
1433
|
-
"todo",
|
|
1434
|
-
"--to-state",
|
|
1435
|
-
"archived",
|
|
1436
|
-
"--reason",
|
|
1437
|
-
"Harness cleanup archive for three-document PRD.",
|
|
1438
|
-
"--json",
|
|
1439
|
-
...baseArgs,
|
|
1440
|
-
], workspace).stdout;
|
|
1441
|
-
assert(
|
|
1442
|
-
archiveDocumentSet.ok !== false,
|
|
1443
|
-
`document-set archive failed: ${JSON.stringify(archiveDocumentSet, null, 2)}`,
|
|
1444
|
-
);
|
|
1445
|
-
|
|
1446
|
-
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
1447
|
-
assertFeedbackAbsent(workspace, [...createdFeedbackIds, documentSetFeedbackId]);
|
|
1413
|
+
], workspace).stdout;
|
|
1414
|
+
assert(archive.ok !== false && archive.updated_count === 5, `batch archive failed: ${JSON.stringify(archive, null, 2)}`);
|
|
1415
|
+
|
|
1416
|
+
const selectedDocument = documentSetCreate.documents[1];
|
|
1417
|
+
const selectedDocumentId = String(selectedDocument?.document_id || "").trim();
|
|
1418
|
+
assert(selectedDocumentId, "Three-document PRD did not return a second document id.");
|
|
1419
|
+
const revisedDocumentTitle = `${namespace} Document Set Harness Revised`;
|
|
1420
|
+
const revisedDocumentPath = writePrd(
|
|
1421
|
+
workspace,
|
|
1422
|
+
revisedDocumentTitle,
|
|
1423
|
+
202,
|
|
1424
|
+
);
|
|
1425
|
+
const documentDraft = runCli([
|
|
1426
|
+
"feedback",
|
|
1427
|
+
"edit",
|
|
1428
|
+
"--feedback-id",
|
|
1429
|
+
documentSetFeedbackId,
|
|
1430
|
+
"--prd-file",
|
|
1431
|
+
revisedDocumentPath,
|
|
1432
|
+
"--document-id",
|
|
1433
|
+
selectedDocumentId,
|
|
1434
|
+
"--reason",
|
|
1435
|
+
"Harness verifies document-scoped refinement.",
|
|
1436
|
+
"--json",
|
|
1437
|
+
...baseArgs,
|
|
1438
|
+
], workspace).stdout;
|
|
1439
|
+
assert(documentDraft.artifact_path, "Document-scoped edit did not create a review artifact.");
|
|
1440
|
+
const documentValidation = runCli([
|
|
1441
|
+
"feedback",
|
|
1442
|
+
"validate",
|
|
1443
|
+
"--file",
|
|
1444
|
+
documentDraft.artifact_path,
|
|
1445
|
+
"--json",
|
|
1446
|
+
...baseArgs,
|
|
1447
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1448
|
+
assert(
|
|
1449
|
+
documentValidation.ok === true && documentValidation.valid !== false,
|
|
1450
|
+
`Document-scoped refinement did not validate: ${JSON.stringify(documentValidation, null, 2)}`,
|
|
1451
|
+
);
|
|
1452
|
+
const documentSubmit = runCli([
|
|
1453
|
+
"feedback",
|
|
1454
|
+
"submit",
|
|
1455
|
+
"--file",
|
|
1456
|
+
documentDraft.artifact_path,
|
|
1457
|
+
"--confirm-write",
|
|
1458
|
+
"--approval-artifact",
|
|
1459
|
+
documentDraft.artifact_path,
|
|
1460
|
+
"--json",
|
|
1461
|
+
...baseArgs,
|
|
1462
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1463
|
+
documentSetRequestId = responseRequestId(documentSubmit);
|
|
1464
|
+
assert(documentSetRequestId, "Document-scoped refinement did not return a review request id.");
|
|
1465
|
+
recordCertificationArtifact(manifest, {
|
|
1466
|
+
kind: "review_request",
|
|
1467
|
+
id: documentSetRequestId,
|
|
1468
|
+
metadata: {
|
|
1469
|
+
feedback_id: documentSetFeedbackId,
|
|
1470
|
+
document_id: selectedDocumentId,
|
|
1471
|
+
},
|
|
1472
|
+
});
|
|
1473
|
+
const documentApproval = runCli([
|
|
1474
|
+
"feedback",
|
|
1475
|
+
"review",
|
|
1476
|
+
"--request-id",
|
|
1477
|
+
documentSetRequestId,
|
|
1478
|
+
"--action",
|
|
1479
|
+
"approve",
|
|
1480
|
+
"--reason",
|
|
1481
|
+
"Harness approves one document without replacing its siblings.",
|
|
1482
|
+
"--verification-evidence",
|
|
1483
|
+
"Document-set validation and sibling-preservation checks passed.",
|
|
1484
|
+
"--deployment-evidence",
|
|
1485
|
+
"Dev post-deploy certification only.",
|
|
1486
|
+
"--no-sync",
|
|
1487
|
+
"--json",
|
|
1488
|
+
...baseArgs,
|
|
1489
|
+
], workspace, { apiKey: operationalReviewKey }).stdout;
|
|
1490
|
+
assert(
|
|
1491
|
+
String(documentApproval.request?.status || documentApproval.status || "") === "applied",
|
|
1492
|
+
`Document-scoped approval did not apply: ${JSON.stringify(documentApproval, null, 2)}`,
|
|
1493
|
+
);
|
|
1494
|
+
|
|
1495
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
1496
|
+
const synchronizedDocumentRoot = path.join(
|
|
1497
|
+
workspace,
|
|
1498
|
+
"MyteCommandCenter",
|
|
1499
|
+
"PRD",
|
|
1500
|
+
"feedback-sync",
|
|
1501
|
+
documentSetFeedbackId,
|
|
1502
|
+
);
|
|
1503
|
+
const synchronizedDocuments = fs.readdirSync(synchronizedDocumentRoot)
|
|
1504
|
+
.filter((name) => name.toLowerCase().endsWith(".md"))
|
|
1505
|
+
.sort();
|
|
1506
|
+
assert(synchronizedDocuments.length === 3, "Document refinement changed the document count.");
|
|
1507
|
+
assert(
|
|
1508
|
+
fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[0]), "utf8")
|
|
1509
|
+
.includes(`# ${documentSetTitles[0]}`),
|
|
1510
|
+
"Document refinement replaced the first sibling document.",
|
|
1511
|
+
);
|
|
1512
|
+
assert(
|
|
1513
|
+
fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[1]), "utf8")
|
|
1514
|
+
.includes(`# ${revisedDocumentTitle}`),
|
|
1515
|
+
"Document refinement did not update the selected second document.",
|
|
1516
|
+
);
|
|
1517
|
+
assert(
|
|
1518
|
+
fs.readFileSync(path.join(synchronizedDocumentRoot, synchronizedDocuments[2]), "utf8")
|
|
1519
|
+
.includes(`# ${documentSetTitles[2]}`),
|
|
1520
|
+
"Document refinement replaced the third sibling document.",
|
|
1521
|
+
);
|
|
1522
|
+
|
|
1523
|
+
const versions = runCli([
|
|
1524
|
+
"feedback",
|
|
1525
|
+
"prd-versions",
|
|
1526
|
+
"--feedback-id",
|
|
1527
|
+
documentSetFeedbackId,
|
|
1528
|
+
"--json",
|
|
1529
|
+
...baseArgs,
|
|
1530
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1531
|
+
assert(
|
|
1532
|
+
Array.isArray(versions.versions) && versions.versions.length >= 2,
|
|
1533
|
+
`Document refinement did not retain PRD versions: ${JSON.stringify(versions, null, 2)}`,
|
|
1534
|
+
);
|
|
1535
|
+
const activeVersionId = String(versions.active_prd_version_id || "").trim();
|
|
1536
|
+
const targetVersion =
|
|
1537
|
+
versions.versions.find((version) => responseVersionId(version) !== activeVersionId)
|
|
1538
|
+
|| versions.versions[0];
|
|
1539
|
+
const targetVersionId = responseVersionId(targetVersion);
|
|
1540
|
+
assert(targetVersionId, "PRD version list did not return a target version id.");
|
|
1541
|
+
const versionDiff = runCli([
|
|
1542
|
+
"feedback",
|
|
1543
|
+
"prd-diff",
|
|
1544
|
+
"--feedback-id",
|
|
1545
|
+
documentSetFeedbackId,
|
|
1546
|
+
"--version-id",
|
|
1547
|
+
targetVersionId,
|
|
1548
|
+
"--document-id",
|
|
1549
|
+
selectedDocumentId,
|
|
1550
|
+
"--json",
|
|
1551
|
+
...baseArgs,
|
|
1552
|
+
], workspace, { apiKey: collaboratorWriteKey }).stdout;
|
|
1553
|
+
assert(
|
|
1554
|
+
String(versionDiff.document_id || "") === selectedDocumentId,
|
|
1555
|
+
`PRD diff was not scoped to the selected document: ${JSON.stringify(versionDiff, null, 2)}`,
|
|
1556
|
+
);
|
|
1557
|
+
for (const version of versions.versions) {
|
|
1558
|
+
const versionId = responseVersionId(version);
|
|
1559
|
+
if (!versionId) continue;
|
|
1560
|
+
recordCertificationArtifact(manifest, {
|
|
1561
|
+
kind: "object",
|
|
1562
|
+
id: versionId,
|
|
1563
|
+
metadata: {
|
|
1564
|
+
object_type: "prd_version",
|
|
1565
|
+
feedback_id: documentSetFeedbackId,
|
|
1566
|
+
cleanup: "retain_audit",
|
|
1567
|
+
},
|
|
1568
|
+
});
|
|
1569
|
+
}
|
|
1570
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
1571
|
+
|
|
1572
|
+
const archiveDocumentSet = runCli([
|
|
1573
|
+
"feedback",
|
|
1574
|
+
"move",
|
|
1575
|
+
"--feedback-id",
|
|
1576
|
+
documentSetFeedbackId,
|
|
1577
|
+
"--from-state",
|
|
1578
|
+
"todo",
|
|
1579
|
+
"--to-state",
|
|
1580
|
+
"archived",
|
|
1581
|
+
"--reason",
|
|
1582
|
+
"Harness cleanup archive for three-document PRD.",
|
|
1583
|
+
"--json",
|
|
1584
|
+
...baseArgs,
|
|
1585
|
+
], workspace).stdout;
|
|
1586
|
+
assert(
|
|
1587
|
+
archiveDocumentSet.ok !== false,
|
|
1588
|
+
`document-set archive failed: ${JSON.stringify(archiveDocumentSet, null, 2)}`,
|
|
1589
|
+
);
|
|
1590
|
+
|
|
1591
|
+
runCli(["feedback-sync", "--json", ...baseArgs], workspace);
|
|
1592
|
+
assertFeedbackAbsent(workspace, [...createdFeedbackIds, documentSetFeedbackId]);
|
|
1448
1593
|
|
|
1449
1594
|
const unarchive = runCli([
|
|
1450
1595
|
"feedback",
|
|
@@ -1462,90 +1607,90 @@ async function main() {
|
|
|
1462
1607
|
...baseArgs,
|
|
1463
1608
|
], workspace, { allowFailure: true }).stdout;
|
|
1464
1609
|
assert(unarchive.ok === false, "Project-key Feedback unarchive unexpectedly succeeded.");
|
|
1465
|
-
assert(unarchive.status === 403, `Expected project-key unarchive to return 403, got ${unarchive.status}.`);
|
|
1466
|
-
assert(unarchive.data?.code === "feedback_unarchive_not_supported_project_api", `Unexpected unarchive error: ${JSON.stringify(unarchive, null, 2)}`);
|
|
1467
|
-
|
|
1468
|
-
if (roleMatrix.required) {
|
|
1469
|
-
assert(roleMatrix.same_project, "Certification role keys were not scoped to one project.");
|
|
1470
|
-
assert(roleMatrix.collaborator_writes_used, "Regular collaborator writes were not certified.");
|
|
1471
|
-
assert(roleMatrix.collaborator_review_denied, "Collaborator approval denial was not certified.");
|
|
1472
|
-
assert(roleMatrix.collaborator_direct_apply_denied, "Collaborator direct-apply denial was not certified.");
|
|
1473
|
-
assert(roleMatrix.collaborator_archive_denied, "Collaborator archive denial was not certified.");
|
|
1474
|
-
assert(roleMatrix.owner_review_used, "Owner approval was not used during strict role certification.");
|
|
1475
|
-
assert(roleMatrix.delegate_review_used, "Delegate approval was not used during strict role certification.");
|
|
1476
|
-
}
|
|
1477
|
-
if (args["require-comment-attachment-sync"]) {
|
|
1478
|
-
assert(
|
|
1479
|
-
commentAttachmentSyncVerified,
|
|
1480
|
-
"Web-created Feedback comment attachment did not survive Project Assistant sync.",
|
|
1481
|
-
);
|
|
1482
|
-
}
|
|
1483
|
-
manifest.checks = LIVE_SCENARIO_MATRIX.map((scenario) => ({
|
|
1484
|
-
id: scenario.id,
|
|
1485
|
-
status: scenario.id === "owner-delegate-collaborator-matrix"
|
|
1486
|
-
? (
|
|
1487
|
-
roleMatrix.required
|
|
1488
|
-
? "passed"
|
|
1489
|
-
: delegateApiKey && collaboratorApiKey
|
|
1490
|
-
? "passed_optional"
|
|
1491
|
-
: "optional_not_requested"
|
|
1492
|
-
)
|
|
1493
|
-
: scenario.id === "feedback-comment-attachment-sync"
|
|
1494
|
-
? (
|
|
1495
|
-
commentAttachmentSyncVerified
|
|
1496
|
-
? "passed"
|
|
1497
|
-
: "optional_not_requested"
|
|
1498
|
-
)
|
|
1499
|
-
: "passed",
|
|
1500
|
-
checked_at: new Date().toISOString(),
|
|
1501
|
-
}));
|
|
1502
|
-
manifest.state = "cleaned";
|
|
1503
|
-
manifest.completed_at = new Date().toISOString();
|
|
1504
|
-
manifest.updated_at = manifest.completed_at;
|
|
1505
|
-
writeCertificationManifest(manifestPath, manifest);
|
|
1506
|
-
|
|
1507
|
-
console.log(JSON.stringify({
|
|
1508
|
-
ok: true,
|
|
1509
|
-
run_id: runId,
|
|
1510
|
-
namespace,
|
|
1511
|
-
workspace,
|
|
1512
|
-
manifest_path: manifestPath,
|
|
1513
|
-
created_feedback_ids: createdFeedbackIds,
|
|
1514
|
-
document_set_feedback_id: documentSetFeedbackId,
|
|
1515
|
-
document_set_review_request_id: documentSetRequestId,
|
|
1516
|
-
review_request_ids: reviewRequestIds,
|
|
1517
|
-
role_matrix: roleMatrix,
|
|
1518
|
-
comment_verified: true,
|
|
1519
|
-
comment_sync_roundtrip_verified: true,
|
|
1520
|
-
comment_attachment_sync_verified: commentAttachmentSyncVerified,
|
|
1521
|
-
stale_edit_rejection_and_resync_verified: true,
|
|
1522
|
-
forced_stale_override_verified: true,
|
|
1523
|
-
move_undo_history_verified: true,
|
|
1524
|
-
validation_verified: true,
|
|
1525
|
-
request_changes_revision_verified: true,
|
|
1526
|
-
rejection_and_cancellation_verified: true,
|
|
1527
|
-
pending_review_batch_block_verified: true,
|
|
1528
|
-
batch_review_verified: true,
|
|
1529
|
-
batch_active_move_verified: true,
|
|
1530
|
-
batch_archive_verified: true,
|
|
1531
|
-
three_document_prd_verified: true,
|
|
1532
|
-
document_scoped_refinement_verified: true,
|
|
1533
|
-
prd_version_diff_verified: true,
|
|
1534
|
-
archived_sync_exclusion_verified: true,
|
|
1610
|
+
assert(unarchive.status === 403, `Expected project-key unarchive to return 403, got ${unarchive.status}.`);
|
|
1611
|
+
assert(unarchive.data?.code === "feedback_unarchive_not_supported_project_api", `Unexpected unarchive error: ${JSON.stringify(unarchive, null, 2)}`);
|
|
1612
|
+
|
|
1613
|
+
if (roleMatrix.required) {
|
|
1614
|
+
assert(roleMatrix.same_project, "Certification role keys were not scoped to one project.");
|
|
1615
|
+
assert(roleMatrix.collaborator_writes_used, "Regular collaborator writes were not certified.");
|
|
1616
|
+
assert(roleMatrix.collaborator_review_denied, "Collaborator approval denial was not certified.");
|
|
1617
|
+
assert(roleMatrix.collaborator_direct_apply_denied, "Collaborator direct-apply denial was not certified.");
|
|
1618
|
+
assert(roleMatrix.collaborator_archive_denied, "Collaborator archive denial was not certified.");
|
|
1619
|
+
assert(roleMatrix.owner_review_used, "Owner approval was not used during strict role certification.");
|
|
1620
|
+
assert(roleMatrix.delegate_review_used, "Delegate approval was not used during strict role certification.");
|
|
1621
|
+
}
|
|
1622
|
+
if (args["require-comment-attachment-sync"]) {
|
|
1623
|
+
assert(
|
|
1624
|
+
commentAttachmentSyncVerified,
|
|
1625
|
+
"Web-created Feedback comment attachment did not survive Project Assistant sync.",
|
|
1626
|
+
);
|
|
1627
|
+
}
|
|
1628
|
+
manifest.checks = LIVE_SCENARIO_MATRIX.map((scenario) => ({
|
|
1629
|
+
id: scenario.id,
|
|
1630
|
+
status: scenario.id === "owner-delegate-collaborator-matrix"
|
|
1631
|
+
? (
|
|
1632
|
+
roleMatrix.required
|
|
1633
|
+
? "passed"
|
|
1634
|
+
: delegateApiKey && collaboratorApiKey
|
|
1635
|
+
? "passed_optional"
|
|
1636
|
+
: "optional_not_requested"
|
|
1637
|
+
)
|
|
1638
|
+
: scenario.id === "feedback-comment-attachment-sync"
|
|
1639
|
+
? (
|
|
1640
|
+
commentAttachmentSyncVerified
|
|
1641
|
+
? "passed"
|
|
1642
|
+
: "optional_not_requested"
|
|
1643
|
+
)
|
|
1644
|
+
: "passed",
|
|
1645
|
+
checked_at: new Date().toISOString(),
|
|
1646
|
+
}));
|
|
1647
|
+
manifest.state = "cleaned";
|
|
1648
|
+
manifest.completed_at = new Date().toISOString();
|
|
1649
|
+
manifest.updated_at = manifest.completed_at;
|
|
1650
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
1651
|
+
|
|
1652
|
+
console.log(JSON.stringify({
|
|
1653
|
+
ok: true,
|
|
1654
|
+
run_id: runId,
|
|
1655
|
+
namespace,
|
|
1656
|
+
workspace,
|
|
1657
|
+
manifest_path: manifestPath,
|
|
1658
|
+
created_feedback_ids: createdFeedbackIds,
|
|
1659
|
+
document_set_feedback_id: documentSetFeedbackId,
|
|
1660
|
+
document_set_review_request_id: documentSetRequestId,
|
|
1661
|
+
review_request_ids: reviewRequestIds,
|
|
1662
|
+
role_matrix: roleMatrix,
|
|
1663
|
+
comment_verified: true,
|
|
1664
|
+
comment_sync_roundtrip_verified: true,
|
|
1665
|
+
comment_attachment_sync_verified: commentAttachmentSyncVerified,
|
|
1666
|
+
stale_edit_rejection_and_resync_verified: true,
|
|
1667
|
+
forced_stale_override_verified: true,
|
|
1668
|
+
move_undo_history_verified: true,
|
|
1669
|
+
validation_verified: true,
|
|
1670
|
+
request_changes_revision_verified: true,
|
|
1671
|
+
rejection_and_cancellation_verified: true,
|
|
1672
|
+
pending_review_batch_block_verified: true,
|
|
1673
|
+
batch_review_verified: true,
|
|
1674
|
+
batch_active_move_verified: true,
|
|
1675
|
+
batch_archive_verified: true,
|
|
1676
|
+
three_document_prd_verified: true,
|
|
1677
|
+
document_scoped_refinement_verified: true,
|
|
1678
|
+
prd_version_diff_verified: true,
|
|
1679
|
+
archived_sync_exclusion_verified: true,
|
|
1535
1680
|
project_key_unarchive_block_verified: true,
|
|
1536
1681
|
}, null, 2));
|
|
1537
|
-
} catch (err) {
|
|
1538
|
-
const cleanupFeedbackIds = [
|
|
1539
|
-
...createdFeedbackIds,
|
|
1540
|
-
...(documentSetFeedbackId ? [documentSetFeedbackId] : []),
|
|
1541
|
-
];
|
|
1542
|
-
if (cleanupFeedbackIds.length) {
|
|
1543
|
-
try {
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
const cleanupFeedbackIds = [
|
|
1684
|
+
...createdFeedbackIds,
|
|
1685
|
+
...(documentSetFeedbackId ? [documentSetFeedbackId] : []),
|
|
1686
|
+
];
|
|
1687
|
+
if (cleanupFeedbackIds.length) {
|
|
1688
|
+
try {
|
|
1544
1689
|
runCli([
|
|
1545
1690
|
"feedback",
|
|
1546
|
-
"move",
|
|
1547
|
-
"--feedback-ids",
|
|
1548
|
-
cleanupFeedbackIds.join(","),
|
|
1691
|
+
"move",
|
|
1692
|
+
"--feedback-ids",
|
|
1693
|
+
cleanupFeedbackIds.join(","),
|
|
1549
1694
|
"--to-state",
|
|
1550
1695
|
"archived",
|
|
1551
1696
|
"--reason",
|
|
@@ -1556,29 +1701,29 @@ async function main() {
|
|
|
1556
1701
|
} catch (_cleanupErr) {
|
|
1557
1702
|
// Keep the original error. Cleanup is best effort and must not hide failure evidence.
|
|
1558
1703
|
}
|
|
1559
|
-
}
|
|
1560
|
-
manifest.state = "cleanup_required";
|
|
1561
|
-
manifest.failure = {
|
|
1562
|
-
message: String(err?.message || err).slice(0, 2000),
|
|
1563
|
-
recorded_at: new Date().toISOString(),
|
|
1564
|
-
};
|
|
1565
|
-
manifest.updated_at = new Date().toISOString();
|
|
1566
|
-
try {
|
|
1567
|
-
writeCertificationManifest(manifestPath, manifest);
|
|
1568
|
-
} catch (_manifestErr) {
|
|
1569
|
-
// Preserve the primary harness failure.
|
|
1570
|
-
}
|
|
1571
|
-
const failure = {
|
|
1572
|
-
ok: false,
|
|
1573
|
-
run_id: runId,
|
|
1574
|
-
namespace,
|
|
1575
|
-
workspace,
|
|
1576
|
-
manifest_path: manifestPath,
|
|
1577
|
-
created_feedback_ids: createdFeedbackIds,
|
|
1578
|
-
document_set_feedback_id: documentSetFeedbackId,
|
|
1579
|
-
document_set_review_request_id: documentSetRequestId,
|
|
1580
|
-
review_request_ids: reviewRequestIds,
|
|
1581
|
-
role_matrix: roleMatrix,
|
|
1704
|
+
}
|
|
1705
|
+
manifest.state = "cleanup_required";
|
|
1706
|
+
manifest.failure = {
|
|
1707
|
+
message: String(err?.message || err).slice(0, 2000),
|
|
1708
|
+
recorded_at: new Date().toISOString(),
|
|
1709
|
+
};
|
|
1710
|
+
manifest.updated_at = new Date().toISOString();
|
|
1711
|
+
try {
|
|
1712
|
+
writeCertificationManifest(manifestPath, manifest);
|
|
1713
|
+
} catch (_manifestErr) {
|
|
1714
|
+
// Preserve the primary harness failure.
|
|
1715
|
+
}
|
|
1716
|
+
const failure = {
|
|
1717
|
+
ok: false,
|
|
1718
|
+
run_id: runId,
|
|
1719
|
+
namespace,
|
|
1720
|
+
workspace,
|
|
1721
|
+
manifest_path: manifestPath,
|
|
1722
|
+
created_feedback_ids: createdFeedbackIds,
|
|
1723
|
+
document_set_feedback_id: documentSetFeedbackId,
|
|
1724
|
+
document_set_review_request_id: documentSetRequestId,
|
|
1725
|
+
review_request_ids: reviewRequestIds,
|
|
1726
|
+
role_matrix: roleMatrix,
|
|
1582
1727
|
message: err?.message || String(err),
|
|
1583
1728
|
};
|
|
1584
1729
|
console.error(JSON.stringify(failure, null, 2));
|
|
@@ -1586,10 +1731,10 @@ async function main() {
|
|
|
1586
1731
|
}
|
|
1587
1732
|
}
|
|
1588
1733
|
|
|
1589
|
-
main().catch((err) => {
|
|
1590
|
-
console.error(JSON.stringify({
|
|
1591
|
-
ok: false,
|
|
1592
|
-
message: err?.message || String(err),
|
|
1593
|
-
}, null, 2));
|
|
1594
|
-
process.exitCode = 1;
|
|
1595
|
-
});
|
|
1734
|
+
main().catch((err) => {
|
|
1735
|
+
console.error(JSON.stringify({
|
|
1736
|
+
ok: false,
|
|
1737
|
+
message: err?.message || String(err),
|
|
1738
|
+
}, null, 2));
|
|
1739
|
+
process.exitCode = 1;
|
|
1740
|
+
});
|