Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
// Exercise the shipped delivery view with synthetic status and read-only API fixtures.
// Exercise owner contract readback and unavailable operation controls with synthetic sources.
import assert from "node:assert/strict";
import {mkdir} from "node:fs/promises";
import { execFileSync, spawn } from "node:child_process";
Expand Down Expand Up @@ -39,9 +39,12 @@ try {
const writes = [];
let response = structuredClone(snapshot);
let responseStatus = 200;
let authorityStatus = 200;
await page.addInitScript(language => localStorage.setItem("loopx-pw-locale", language), locale);
await page.route("**/api/**", route => {
if (route.request().method() !== "GET") writes.push(route.request().url());
// Model-option discovery is an existing read-only POST. Every effect
// endpoint, including the new acceptance operation, remains in this ledger.
if (route.request().method() !== "GET" && new URL(route.request().url()).pathname !== "/api/chat/agent-model-options") writes.push(route.request().url());
return route.fulfill({ status: 404, json: { error: "Unavailable in read-only fixture" } });
});
await page.route(url => url.pathname === "/status.json", route => route.fulfill({ json: payload }));
Expand All @@ -50,6 +53,11 @@ try {
assert.equal(new URL(route.request().url()).searchParams.get("goal_id"), "release-demo");
return route.fulfill({ status: responseStatus, json: response });
});
await page.route("**/api/actions?*", route => route.fulfill({json: {ok: true, schema_version: "loopx_chat_action_list_v1", proposals: []}}));
await page.route("**/api/chat/goal-acceptance?*", route => route.fulfill({status: authorityStatus, json: {
ok: true, goal_id: response.acceptance.goal_id, provider_revision: "fixture:current",
goal_acceptance_contract: response.acceptance.goal_acceptance_contract ?? {enabled: false},
}}));
await page.goto(`http://127.0.0.1:${port}/${packaged ? "chat/" : ""}?statusUrl=/status.json`, { waitUntil: "networkidle" });
const navigation = page.getByRole("button", { name: locale === "en" ? "Open Goal navigation" : "打开 Goal 导航" });
if (await navigation.isVisible()) await navigation.click();
Expand Down Expand Up @@ -82,12 +90,25 @@ try {
const section = review.locator(".delivery-acceptance-contract");
const refresh = review.getByRole("button", { name: locale === "en" ? "Refresh snapshot" : "刷新快照", exact: true });
const exportButton = review.getByRole("button", { name: locale === "en" ? "Export delivery snapshot" : "导出交付快照", exact: true });
const openAdvanced = async () => {
const advanced = section.locator(".delivery-check-advanced");
if (await advanced.isVisible() && await advanced.getAttribute("open") === null) {
await advanced.locator(":scope > summary").focus();
await page.keyboard.press("Enter");
assert.notEqual(await advanced.getAttribute("open"), null, "Optional diagnostics are keyboard accessible");
}
};
const refreshSnapshot = async value => {
response = { ...snapshot, acceptance: { ...acceptance, goal_acceptance_contract: value } };
const read = page.waitForResponse(url => url.url().includes("/api/chat/delivery-review?"));
await refresh.click();
await read;
await page.waitForFunction(() => !document.querySelector(".delivery-review-toolbar button")?.disabled);
// The native check reader is independent of the delivery-map snapshot.
// Reload verifies both read owners; polling/recovery is covered by the
// real SQLite/HTTP/CLI journey.
if (responseStatus === 200) await page.reload({waitUntil: "networkidle"});
await openAdvanced();
};
await review.locator(".delivery-chain").waitFor();
const baseline = await review.innerText();
Expand All @@ -98,35 +119,28 @@ try {
assert.equal(await review.innerText(), baseline, "Absent/off contract must preserve the existing view");
}
await refreshSnapshot(contract);
assert.equal(await section.getAttribute("open"), null, "Contract starts collapsed");
const summary = section.locator(":scope > summary");
await summary.focus();
await page.keyboard.press("Enter");
assert.notEqual(await section.getAttribute("open"), null, "Disclosure is keyboard accessible");
const text = await section.innerText();
for (const value of [contract.objective, contract.digest, "release-demo", "todo_confirmed", "todo_unbound", "todo_stale", "recovery"]) assert.ok(text.includes(value));
for (const value of locale === "en" ? ["Task association confirmed", "Task association missing", "Task association stale", "Task associations require confirmation", "Outside the current task gate"] : ["任务关联已确认", "任务关联缺失", "任务关联已过期", "任务关联需要确认", "不属于当前任务门禁范围"]) assert.ok(text.includes(value), value);
assert.equal(await section.locator("button,input,select,textarea").count(), 0, "Readback must offer no mutation controls");
await section.locator("details > summary").last().click();
await section.getByText(locale === "en" ? /The local Goal owner/ : /本地 Goal 所有者/).waitFor();
await section.getByText(locale === "en" ? /Import an existing JSON contract/ : /导入现有 JSON 合同/).waitFor();
for (const [status, en, zh] of [["unverified", "Artifact checks not verified", "产物检查未验证"], ["failed", "Artifact checks failed", "产物检查失败"], ["stale", "Artifact checks stale", "产物检查已过期"], ["partial", "Task checks passed; Goal-wide verification unknown", "任务检查通过;Goal 整体验证未知"], ["accepted", "Artifact checks passed", "产物检查通过"]]) {
await refreshSnapshot(verifiedContract(status));
await section.getByText(locale === "en" ? en : zh, { exact: true }).waitFor();
assert.ok((await section.innerText()).includes(locale === "en" ? "neither automatically approves or completes the Goal" : "均不会自动批准或完成 Goal"));
await section.getByText(locale === "en" ? en : zh, { exact: true }).first().waitFor();
assert.ok((await section.innerText()).includes(locale === "en" ? "independent review" : "独立复核"));
}
await refreshSnapshot({ ...contract, scope: { kind: "selected_work", todo_ids: contract.tasks.map(task => task.todo_id) } });
await section.getByText(locale === "en" ? "Only explicitly selected tasks" : "仅明确选定的任务", { exact: true }).waitFor();
await section.locator("dl").first().scrollIntoViewIfNeeded();
await section.locator(".delivery-check-advanced dl").scrollIntoViewIfNeeded();
await page.screenshot({ path: process.env.LOOPX_ACCEPTANCE_SCOPE_SCREENSHOT_DIR
? `${process.env.LOOPX_ACCEPTANCE_SCOPE_SCREENSHOT_DIR}/${locale}.png` : `/tmp/loopx-acceptance-scope-${locale}.png` });
const next = { ...verifiedContract("stale"), revision: 8, digest: "b".repeat(64) };
await refreshSnapshot(next);
await section.getByText(next.digest, { exact: true }).waitFor();
assert.equal(await section.getByText(contract.digest, { exact: true }).count(), 0);
assert.match(await section.locator("dl").first().innerText(), /8/);
assert.match(await section.locator(".delivery-check-advanced dl").innerText(), /8/);
await section.locator("details > summary").first().click();
await section.getByText("c".repeat(64), { exact: true }).waitFor();
assert.match(await section.locator("dl").last().innerText(), /6/);
assert.match(await section.locator(".delivery-check-result dl").innerText(), /6/);
assert.equal(await section.evaluate(element => element.scrollWidth > element.clientWidth + 2), false, "Long digests must wrap at narrow widths");
const downloadPromise = page.waitForEvent("download");
await exportButton.click();
Expand All @@ -136,16 +150,20 @@ try {
for await (const chunk of stream) markdown += chunk.toString();
assert.ok(markdown.includes(next.digest) && markdown.includes("8") && markdown.includes(contract.objective));
responseStatus = 503;
authorityStatus = 503;
await refreshSnapshot(next);
await section.getByText(locale === "en" ? /Retained snapshot/ : /当前保留旧快照/).waitFor();
await section.getByText(locale === "en" ? /Retained snapshot/ : /当前保留旧快照/).first().waitFor();
assert.equal(await section.getByRole("button", {name: locale === "en" ? "Check delivery" : "检查交付", exact: true}).count(), 0,
"An unavailable current authority must not offer a command from retained state");
assert.equal(await exportButton.isDisabled(), true, "Failed refresh must not export retained contract as current");
responseStatus = 200;
authorityStatus = 200;
response = { ...snapshot, acceptance: { ...acceptance, goal_id: "other-goal", goal_acceptance_contract: next } };
const mismatch = page.waitForResponse(url => url.url().includes("/api/chat/delivery-review?"));
await refresh.click();
await mismatch;
await page.waitForFunction(() => !document.querySelector(".delivery-review-toolbar button")?.disabled);
await section.getByText(locale === "en" ? /Retained snapshot/ : /当前保留旧快照/).waitFor();
await section.getByText(locale === "en" ? /Retained snapshot/ : /当前保留旧快照/).first().waitFor();
assert.equal(await section.getByText("other-goal", { exact: true }).count(), 0);
await refreshSnapshot({ enabled: false });
assert.equal(await section.count(), 0, "Disabling removes the previous enabled readback");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,15 @@ assert.notEqual(next.acceptance.goal_acceptance_contract.digest, contract.digest
assert.deepEqual(contract, before, "Rendering must not mutate the supplied contract");
console.log("Goal acceptance contract API/export: off parity, association/verification distinction, source/revision and negative cases passed");

const {projectGoalAcceptanceCriteria} = await import("../node_modules/.cache/delivery-review/data/goal-acceptance-observation.js");
const {goalAcceptanceDisplayStatus, projectGoalAcceptanceCriteria} = await import("../node_modules/.cache/delivery-review/data/goal-acceptance-observation.js");
for (const [observation, expected] of Object.entries({current: "accepted", stale: "stale", failed: "failed", recorded: "unverified", unavailable: "unverified"})) {
const value = structuredClone(verifiedContract("accepted"));
value.criteria[0].oracle = {state: observation};
const before = structuredClone(value);
assert.equal(goalAcceptanceDisplayStatus(value), expected, "Historical success cannot override current file observations");
assert.equal(projectGoalAcceptanceCriteria(value)[0].state, expected);
assert.deepEqual(value, before, "Display freshness cannot mutate canonical acceptance");
}
for (const status of ["stale", "held", "partial"]) {
const value = verifiedContract(status);
value.verification.contract_revision = value.revision;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,26 @@ try {
await verificationGap.scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-verifier-gap-mobile.png"), animations: "disabled"});
await page.setViewportSize({width: 1512, height: 980});
// A local pinned check is useful evidence, but cannot authenticate an
// independent reviewer. Keep the existing gap alongside the optional receipt.
validation = {...currentValidation, oracle_checks: [{criterion_id: "report-format", source_id: "report-checker",
source_digest: "sha256:" + "8".repeat(64), checked_at: checkTime,
input_versions: [], output_versions: outputVersions}]};
await page.route("**/api/chat/sessions/*/loopx", checkedRead);
await evidence.getByRole("button", {name: "核验关联执行", exact: true}).click();
const correction = evidence.getByRole("region", {name: "纠偏证据路径", exact: true});
await correction.getByText("负责人指定的版本固定检查器", {exact: true}).waitFor();
await verificationGap.waitFor();
await correction.getByText("检查在声明文件的隔离副本上执行;不代表远程复核者身份或异议已解决。", {exact: true}).waitFor();
await selectedOutput().waitFor();
await verificationGap.scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-local-checker-gap-desktop.png"), animations: "disabled"});
await page.setViewportSize({width: 390, height: 844});
assert.ok(await dialog.evaluate(el => el.scrollWidth <= el.clientWidth));
await verificationGap.scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "team-local-checker-gap-mobile.png"), animations: "disabled"});
await page.setViewportSize({width: 1512, height: 980});
await page.unroute("**/api/chat/sessions/*/loopx", checkedRead);
// A completed source can retain its output bytes while its dependency
// chain becomes unavailable. Reading an adoption candidate must withdraw
// the earlier correction trace and must never offer a confirmation.
Expand Down
4 changes: 3 additions & 1 deletion apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type {OracleEvidence} from "./goal-acceptance-observation";
import { HANDOFF_MODES, EXECUTION_HANDOFF_MODES } from "../../../../../loopx/control_plane/coordination/handoff_mode_vocabulary.js";
import { normalizeGoalDraft } from "../../../../../loopx/control_plane/collaboration/goal_draft.js";
import { parseTurnStep, type TurnStep } from "./turn-steps.js";
Expand Down Expand Up @@ -410,6 +411,7 @@ export class ChatApiError extends Error {
}

export const typedActionKindSchema = z.enum([
"goal.acceptance.check",
"goal.create",
"goal.update",
"goal.lifecycle",
Expand Down Expand Up @@ -1174,7 +1176,7 @@ export type DelegationReadback = {
artifacts?: Array<{ref: string; sha256: string; text: string}>; error?: string;
validation?: {source: "goal_acceptance" | "todo_validation"; basis_sha256: string;
check_count: number; pinned_file_count: number; checked_at?: string;
output_versions?: Array<{ref: string; sha256: string}>};
output_versions?: Array<{ref: string; sha256: string}>; oracle_checks?: Array<OracleEvidence & {criterion_id: string}>};
task_failure?: {stage: "task_postcondition"; status: "failed" | "inconclusive" | "unavailable";
recovery_kind: "repair_required" | "replan_required"; summary: string; validator_kind: string;
turn_key: string; resume_mode: "revalidate_cached_result"};
Expand Down
4 changes: 2 additions & 2 deletions apps/presentation/dashboard/src/data/delivery-review.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { z } from "zod";
import { goalAcceptanceObservationSchema, type GoalAcceptanceContract } from "./goal-acceptance-observation.js";
import { goalAcceptanceDisplayStatus, goalAcceptanceObservationSchema, type GoalAcceptanceContract } from "./goal-acceptance-observation.js";

const refsSchema = z.record(z.string(), z.array(z.string()));
const nodeSchema = z.object({
Expand Down Expand Up @@ -211,7 +211,7 @@ export function deliveryReviewMarkdown(snapshot: DeliveryReviewSnapshot, labels:
if (task.reason) rows.push(` ${line(task.reason)}`);
if (task.applicable === false) rows.push(` ${copy.notApplicable}`);
}
rows.push("", `### ${copy.verification}`, copy.verificationState[contract.status]);
rows.push("", `### ${copy.verification}`, copy.verificationState[goalAcceptanceDisplayStatus(contract)]);
if (contract.held_todo_ids.length) rows.push(`${copy.heldTasks}: ${line(contract.held_todo_ids.join(", "))}`);
rows.push("", `### ${copy.receipt}`);
const receipt = contract.verification;
Expand Down
41 changes: 41 additions & 0 deletions apps/presentation/dashboard/src/data/goal-acceptance-actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {z} from "zod";
import {goalAcceptanceContractSchema} from "./goal-acceptance-observation";
import type {TypedActionProposal} from "./chat";
const responseSchema = z.object({ok: z.literal(true), goal_id: z.string(), provider_revision: z.string(),
goal_acceptance_contract: goalAcceptanceContractSchema, checks_passed: z.boolean().optional()});
export type AcceptanceReadback = z.infer<typeof responseSchema>;
export async function acceptanceRequest(goalId: string, operation?: Record<string, unknown>, signal?: AbortSignal): Promise<AcceptanceReadback> {
const url = "/api/chat/goal-acceptance";
const response = await fetch(operation ? url : `${url}?goal_id=${encodeURIComponent(goalId)}`, operation
? {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({...operation, goal_id: goalId}), signal} : {signal});
if (!response.ok) throw new Error("Acceptance unavailable");
const result = responseSchema.parse(await response.json());
if (result.goal_id !== goalId) throw new Error("Acceptance source changed");
return result;
}

// Read model only. The existing typed acceptance owner validates the complete
// document and its command/file bindings again at apply time.
const preparedDocument = z.object({objective: z.string().min(1),
scope: z.discriminatedUnion("kind", [z.object({kind: z.literal("all_advancement")}),
z.object({kind: z.literal("selected_work"), todo_ids: z.array(z.string()).min(1)})]),
criteria: z.array(z.object({id: z.string(), description: z.string(),
validation_argv: z.array(z.string()).min(1),
oracle: z.object({output_refs: z.array(z.string())}).passthrough().optional(),
}).passthrough()).min(1),
}).passthrough();
export function preparedAcceptancePlan(proposals: TypedActionProposal[], goalId: string) {
// Never resurrect an older draft after the newest plan was applied, cancelled
// or invalidated. Ambiguous creation order requires a fresh Agent proposal.
const matching = proposals.filter(proposal => proposal.action_kind === "goal.acceptance.check"
&& proposal.context.kind === "goal" && proposal.context.goal_id === goalId
&& proposal.normalized_parameters.goal_id === goalId)
.sort((a, b) => b.created_at.localeCompare(a.created_at));
const proposal = matching[0];
if (!proposal || matching[1]?.created_at === proposal.created_at) return null;
if (proposal.permission_classification !== "local_command_execution"
|| !["preview_ready", "deferred"].includes(proposal.status) || proposal.gate || proposal.error
|| !proposal.available_transitions.includes("apply")) return null;
const parsed = preparedDocument.safeParse(proposal.normalized_parameters.document);
return parsed.success ? {proposal, document: parsed.data} : null;
}
Loading
Loading