mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-24 18:19:05 +02:00
## Thinking Path > - Paperclip manages agents and their work. > - Product E2E tests check complete user journeys. > - The current harness prepares self-hosted instances. > - Hosted workspaces need separate authentication, setup, and cleanup. > - The same in-app story should run on either deployment. > - This pull request adds deployment targets and explicit prerequisites. > - The private Cloud adapter owns fleet access and lifecycle operations. ## Linked Issues or Issue Description **Subsystem affected** Product E2E harness, fixture registry, and reports. **Problem or motivation** Self-hosted test setup cannot qualify a deployed Cloud workspace. Copying each story into a second harness would make its assertions diverge. **Proposed solution** Add explicit deployment targets, setup recipes, and ordered campaigns. Reuse a no-model task-properties story on both deployments. Let fresh Cloud onboarding provide verified context for that same story. Keep Cloud lifecycle and credentials behind a private target provider. **Roadmap alignment** This supports Cloud deployment qualification and the existing agent eval system. It adds no application endpoint or scheduler. Related work: #17112. The separate onboarding gateway UI prerequisite is #17253. ## What Changed - Add explicit target configuration and a credential-free preparation preview. Keep historical selectors and the current `--all` matrix unchanged. - Resolve requested fixture prerequisites, including a run-owned backlog task prepared before measurement. Reuse verified onboarding context without counting recipe setup as onboarding coverage. - Add task assignment persistence and the actual onboarding parent/child first-task journey, with an independent artifact oracle and native trace requirement. - Preserve failed boundaries and blocked dependents, deployment identity, costs, phase outcomes, and private evidence in the existing dashboard. - Validate success assertions and accounting before successful cleanup. Bound collection by the campaign deadline and cleanup/evidence hooks by a separate ten-minute grace, with cancellation and stop-only fallback. - Stage dashboard rendering separately so a timed-out renderer cannot overwrite retained failure results. Add evidence finalization hooks so the private provider can save evidence before target deletion and lease release. - Reject private journey bundles in the public history publisher. Browser-only results have no model or provider. ## Verification - The unchanged task-properties story passed on managed self-hosted and a registered testing Cloud workspace. Authentication, setup, journey, collection, and cleanup passed. Model calls: zero. Earlier attempts remain private and retained. - Final journey and report tests: 96 passed in eight files. Catalog compatibility: 39 passed. The wider focused run exposed three stale master catalog expectations; these now match the already shipped case-handoff v4 metadata and four-cell matrix. The default 385-cell selection is unchanged. - Review regressions reproduced premature success for a zero-request receipt and a missing required assertion. Both now fail before success cleanup while retaining cost receipts. The initial lifecycle fixes passed 33 checks. A subsequent review exposed a late renderer write that could erase saved failures; rendering now writes into isolated staging and publishes HTML only after it settles. Combined campaign, fixture-contract, and retained-report checks: 67 passed, including late writes, delayed timer callbacks, stalled collection, release, stop, evidence hooks, and browser close. Harness typecheck passed after the fixes. - A broader harness unit attempt completed 3,601 passing cases, two failures, and two skips. The failures exposed a stale Pi count and an omitted active Copilot fixture in the Daytona image input closure. The focused compatibility fixes passed 83 tests. After all source fixes, the full harness support suite passed: 160 files, 3,621 tests, two skips, plus 128 native-source checks. The earlier failures remain recorded. - Harness typecheck, repository typecheck, repository build, map checks, and Storybook catalog checks passed on the rebased branch. - The required full `pnpm test:run` attempt timed out after 35 minutes. It had 1,195 completed passing cases and one chat integration failure before its first suite completed. That case and nine adjacent cases pass in isolation. The original failure remains unresolved and retained; this is not a full-suite pass. - Current-head remote checks (`39b2a1ed8c49222fabd1ba8e225cac81bff5c6f7`): 53 passed, two skipped, none pending or failing. Greptile completed successfully at 5/5 with no unresolved review threads. - Fresh paid Cloud onboarding, dependent reuse, and disposable cleanup have not been qualified live. They require deployment of the separate UI prerequisite and actual Cloud gateway budget admission. The trusted Cloud workflow remains disabled. - Final live verification at `39b2a1ed8c`: the unchanged shared story passed on managed self-hosted and the registered testing Cloud stack using the prepared backlog-task recipe. Both cleaned up successfully with zero model calls. Independent private S3 checks confirmed final evidence and released QA identity/stack leases. Cloud evidence remains private. ## Risks - A deployed UI or Cloud control-plane change can invalidate selectors or provider contracts. Version drift fails the attempt. - Cloud evidence and lifecycle recovery are private-provider responsibilities. A pending cleanup or evidence upload must keep the target unavailable for reuse. - No release is deployed or pinned by this change. No automatic story retry is added. The initial pilot retains its explicit $10, one disposable workspace, and 30-minute bounds. ## Model Used OpenAI GPT-6 through Codex, with reasoning, repository tools, code execution, and delegated agents. The runtime does not expose a more specific deployment model identifier or context-window size. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
135 lines
5.2 KiB
TypeScript
135 lines
5.2 KiB
TypeScript
import { validateRetainedRunnerResult } from "./result-validation.js";
|
|
import { createHash } from "node:crypto";
|
|
import type { MatrixExecution, RunnerE2EResult } from "./types.js";
|
|
|
|
/** Display-only metadata: never load or reconstruct executable branch fixtures. */
|
|
export interface ReportExecution {
|
|
id: string;
|
|
suiteDefinitionHash: string;
|
|
suite: Pick<
|
|
MatrixExecution["suite"],
|
|
"id" | "label" | "description" | "expectedMatrixSize"
|
|
>;
|
|
profile: { id: string; label: string; generation: "legacy" | "native" | "none"; provider: string | null; model: string | null; expectedRuntimeMode: "legacy" | "native" | "none" };
|
|
environment: Pick<
|
|
MatrixExecution["environment"],
|
|
"id" | "label" | "provider" | "expectedExecutionTarget"
|
|
>;
|
|
task: Pick<MatrixExecution["task"], "id" | "label">;
|
|
}
|
|
|
|
export function parseReportExecutionId(id: string) {
|
|
// These identities also become evidence paths. Reject path traversal and any
|
|
// syntax other than catalog identities, even in imported JSON. Deployment
|
|
// journeys add one explicit target segment; all historical IDs stay intact.
|
|
if (typeof id !== "string")
|
|
throw new Error("Report execution identity must be a string");
|
|
const journey = /^(product-journeys)\.([a-z0-9][a-z0-9_-]*)\.([a-z0-9][a-z0-9_-]*)\.(local|daytona)\.([a-z0-9][a-z0-9_-]*)$/.exec(id);
|
|
if (journey && id.length <= 512) return { suiteId: journey[1]!, targetId: journey[2]!, profileId: journey[3]!, environmentId: journey[4]! as "local" | "daytona", caseId: journey[5]! };
|
|
const match =
|
|
/^([a-z0-9][a-z0-9_-]*)\.([a-z0-9][a-z0-9_-]*)\.(local|daytona)\.([a-z0-9][a-z0-9_-]*)$/.exec(
|
|
id,
|
|
);
|
|
if (!match || id.length > 512)
|
|
throw new Error(
|
|
`Invalid report execution identity: ${String(id).slice(0, 512)}`,
|
|
);
|
|
return {
|
|
suiteId: match[1]!,
|
|
profileId: match[2]!,
|
|
environmentId: match[3]! as "local" | "daytona",
|
|
caseId: match[4]!,
|
|
targetId: undefined,
|
|
};
|
|
}
|
|
|
|
export function validateReportResultIdentity(result: RunnerE2EResult) {
|
|
validateRetainedRunnerResult(result);
|
|
const identity = parseReportExecutionId(result.executionId);
|
|
if (
|
|
(result.suiteId !== undefined && result.suiteId !== identity.suiteId) ||
|
|
result.profileId !== identity.profileId ||
|
|
result.environmentId !== identity.environmentId ||
|
|
result.caseId !== identity.caseId
|
|
|| identity.targetId !== undefined && result.productJourney?.targetId !== identity.targetId
|
|
) {
|
|
throw new Error(
|
|
`Result identity does not match execution ID: ${result.executionId}`,
|
|
);
|
|
}
|
|
return identity;
|
|
}
|
|
|
|
export function discoverReportCatalog(input: {
|
|
catalog: readonly ReportExecution[];
|
|
expected: readonly string[];
|
|
results: readonly RunnerE2EResult[];
|
|
}): ReportExecution[] {
|
|
const expected = new Set(input.expected);
|
|
if (expected.size !== input.expected.length)
|
|
throw new Error("Report expected identities must be unique");
|
|
const identities = new Map(
|
|
input.expected.map((id) => [id, parseReportExecutionId(id)]),
|
|
);
|
|
const results = new Map<string, RunnerE2EResult>();
|
|
for (const result of input.results) {
|
|
validateReportResultIdentity(result);
|
|
if (expected.has(result.executionId))
|
|
results.set(result.executionId, result);
|
|
}
|
|
const catalog = [...input.catalog];
|
|
const knownIds = new Set(catalog.map((execution) => execution.id));
|
|
const knownSuites = new Map(
|
|
catalog.map((execution) => [execution.suite.id, execution.suite]),
|
|
);
|
|
for (const [id, identity] of identities) {
|
|
if (knownIds.has(id)) continue;
|
|
const result = results.get(id);
|
|
const suiteIds = input.expected
|
|
.filter(
|
|
(candidate) => identities.get(candidate)?.suiteId === identity.suiteId,
|
|
)
|
|
.sort();
|
|
const suite = knownSuites.get(identity.suiteId) ?? {
|
|
id: identity.suiteId,
|
|
label: identity.suiteId,
|
|
description:
|
|
"Suite discovered from retained campaign identities; full suite size is not known to this publisher.",
|
|
expectedMatrixSize: suiteIds.length,
|
|
};
|
|
const knownProfile = input.catalog.find(
|
|
(execution) => execution.profile.id === identity.profileId,
|
|
)?.profile;
|
|
const knownEnvironment = input.catalog.find(
|
|
(execution) => execution.environment.id === identity.environmentId,
|
|
)?.environment;
|
|
const runtimeMode = result?.runtimeMode ?? "native";
|
|
catalog.push({
|
|
id,
|
|
suite,
|
|
suiteDefinitionHash:
|
|
result?.suiteDefinitionHash ??
|
|
`selection-${createHash("sha256").update(JSON.stringify(suiteIds)).digest("hex")}`,
|
|
profile: knownProfile ?? {
|
|
id: identity.profileId,
|
|
label: identity.profileId,
|
|
generation: runtimeMode,
|
|
expectedRuntimeMode: runtimeMode,
|
|
provider:
|
|
result?.runtimeMode === "none" ? null : result?.provider ?? "unknown",
|
|
model: result?.runtimeMode === "none" ? null : result?.model ?? "unknown",
|
|
},
|
|
environment: knownEnvironment ?? {
|
|
id: identity.environmentId,
|
|
label: identity.environmentId,
|
|
provider: identity.environmentId,
|
|
expectedExecutionTarget: {
|
|
kind: identity.environmentId === "local" ? "local" : "remote",
|
|
},
|
|
},
|
|
task: { id: identity.caseId, label: identity.caseId },
|
|
});
|
|
}
|
|
return catalog;
|
|
}
|