mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-25 02:26:12 +02:00
Merge the tested repository candidate into qualification reporting
Co-Authored-By: Paperclip <noreply@paperclip.ing> * codex/dependable-repository-conversion: fix: validate actual task roots and unfinished response waits test: qualify conversion prerequisites before runner selection
This commit is contained in:
commit
9a8c50ca5d
10 files changed
+165
-19
No files matched your search
@@ -122,3 +122,24 @@ the recorded Codex and Claude models. All twelve new cells declare development
|
||||
API-key authentication and per-turn lifecycle differences. None counts as staging
|
||||
subscription, warm lifecycle, Slack, or the complete 24-cell milestone. Fresh
|
||||
provider measurement remains required; catalog registration is not acceptance.
|
||||
The fifth development attempt uses the recorded Codex model. It has no monitor,
|
||||
but selects a response wake and then reaches productive-continuation recovery.
|
||||
Its next admission still fails at Git setup. The first plain-root regression had
|
||||
used a `task_directory` strategy that the runtime does not persist. The production
|
||||
runtime records `project_primary`, task ownership, and a `task_session` source.
|
||||
A corrected real-database reproduction fails with that recorded representation.
|
||||
The repair now checks those current ownership/source fields and absence of a root
|
||||
repository or configured project source. Shared and isolated task directories are
|
||||
covered; configured, agent, worktree, and adapter-managed roots stay strict. The
|
||||
second Git helper caller was inspected, but is not established as this attempt's
|
||||
cause. The fifth failure remains retained, including the distinct response-wake
|
||||
selection. Live same-agent acceptance is still open.
|
||||
|
||||
|
||||
That fifth attempt also exposes an independent completion-validation gap: a
|
||||
provider can report an unsatisfied objective, leave remainingWork empty, and yield
|
||||
to response_wake without any pending response. The existing gate checked only
|
||||
blocking remainingWork entries. A real-database reproduction accepts that invalid
|
||||
report before repair. The same gate now also checks an unfinished objective.
|
||||
Current questions, approvals, pause holds, conversations, and authorized external
|
||||
response waits retain their earlier checks. No accepted result is rewritten.
|
||||
@@ -334,7 +334,25 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
expect(acquisition.materialize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["sponsored", "sponsored_git_residue", "local_board", "local_board_jwt", "routine"] as const)("prepares public Git from an idle workspace for a trusted %s task and reuses verified bytes after trust downgrade", async principal => {
|
||||
it.each([
|
||||
{ name: "missing task owner", patch: { sourceIssueId: null } },
|
||||
{ name: "configured directory source", patch: { metadata: { source: "project_primary" } } },
|
||||
{ name: "agent directory source", patch: { metadata: { source: "agent_home" } } },
|
||||
{ name: "worktree strategy", patch: { strategyType: "git_worktree" } },
|
||||
{ name: "adapter-managed mode", patch: { mode: "adapter_managed" } },
|
||||
{ name: "configured root repository", patch: { repoUrl: "https://github.com/public/anchor" } },
|
||||
])("keeps strict Git validation for $name", async ({ patch }) => {
|
||||
const f = await fixture();
|
||||
await f.request({ kind: "url", url: "https://github.com/public/example" });
|
||||
await db.update(executionWorkspaces).set({ strategyType: "project_primary", sourceIssueId: f.issueId,
|
||||
metadata: { source: "task_session" }, ...patch }).where(eq(executionWorkspaces.id, f.workspace.id));
|
||||
await mkdir(path.join(f.admission.cwd, ".git", "info"), { recursive: true });
|
||||
await writeFile(path.join(f.admission.cwd, ".git", "info", "exclude"), "/.paperclip-repositories/\n");
|
||||
await expect(f.service.prepareForAdmission(f.admission)).rejects.toThrow("not a git repository");
|
||||
expect(acquisition.materialize).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each(["sponsored", "sponsored_git_residue", "sponsored_shared_git_residue", "local_board", "local_board_jwt", "routine"] as const)("prepares public Git from an idle workspace for a trusted %s task and reuses verified bytes after trust downgrade", async principal => {
|
||||
const f = await fixture();
|
||||
await db.update(executionWorkspaces).set({ status: "idle" }).where(eq(executionWorkspaces.id, f.workspace.id));
|
||||
const localBoard = principal === "local_board" || principal === "local_board_jwt";
|
||||
@@ -366,7 +384,9 @@ const support = await getEmbeddedPostgresTestSupport();
|
||||
await exec("git", ["-C", cwd, "remote", "set-url", "origin", input.repoUrl]);
|
||||
return result;
|
||||
});
|
||||
if (principal === "sponsored_git_residue") {
|
||||
if (principal === "sponsored_git_residue" || principal === "sponsored_shared_git_residue") {
|
||||
await db.update(executionWorkspaces).set({ strategyType: "project_primary", sourceIssueId: f.issueId,
|
||||
mode: principal === "sponsored_shared_git_residue" ? "shared_workspace" : "isolated_workspace", metadata: { source: "task_session" } }).where(eq(executionWorkspaces.id, f.workspace.id));
|
||||
await mkdir(path.join(f.admission.cwd, ".git", "info"), { recursive: true });
|
||||
await writeFile(path.join(f.admission.cwd, ".git", "info", "exclude"), "/.paperclip-repositories/\n");
|
||||
}
|
||||
|
||||
@@ -362,7 +362,9 @@ export function executionWorkspaceRepositoryService(db: Db) {
|
||||
await fs.rename(ignoreTemporary, ignorePath);
|
||||
} finally { await fs.rm(ignoreTemporary, { force: true }); }
|
||||
await measureRunPhase("workspace.repository.ignore_rules", () => ensureManagedRepositoriesIgnored(root, {
|
||||
plainTaskDirectory: workspace.strategyType === "task_directory" && !workspace.repoUrl && !workspace.projectWorkspaceId,
|
||||
plainTaskDirectory: workspace.strategyType === "project_primary" && workspace.metadata?.source === "task_session"
|
||||
&& workspace.sourceIssueId === input.issueId && ["shared_workspace", "isolated_workspace"].includes(workspace.mode)
|
||||
&& !workspace.repoUrl && !workspace.projectId && !workspace.projectWorkspaceId,
|
||||
}));
|
||||
}
|
||||
return stagingParent;
|
||||
|
||||
@@ -212,6 +212,29 @@ describe("native final-response feedback", () => {
|
||||
const feedback = await nativeCompletionFeedback(db, current.runId, done);
|
||||
expect(feedback).not.toContain("#document-"); expect(feedback).not.toContain("foreign.example");
|
||||
});
|
||||
it("requires a real response wait when an unfinished objective omits remaining work", async () => {
|
||||
const value = await fixture();
|
||||
const report: PrpStructuredRunResult = { ...done, reportedWorkDisposition: "yielded",
|
||||
completionClaim: { ...done.completionClaim, objectiveSatisfied: false, remainingWork: [],
|
||||
criteria: [{ criterionId: "objective", status: "not_satisfied", evidenceRefs: [] }] },
|
||||
continuation: { kind: "response_wake", summary: "Repository preparation needs the next admission", idempotencyKey: "repository-admission" },
|
||||
};
|
||||
await expect(nativeCompletionFeedback(db, value.runId, report)).rejects.toThrow("without a pending wait condition");
|
||||
const [workspace] = await db.insert(executionWorkspaces).values({ companyId: value.companyId, sourceIssueId: value.issueId,
|
||||
mode: "shared_workspace", strategyType: "project_primary", metadata: { source: "task_session" }, name: "Task files" }).returning();
|
||||
await db.insert(executionWorkspaceRepositories).values({ companyId: value.companyId, executionWorkspaceId: workspace.id,
|
||||
requestedByIssueId: value.issueId, repositoryIdentity: "public/example", repoUrl: "https://github.com/public/example",
|
||||
relativePath: ".paperclip-repositories/example", requestKey: "prepare" });
|
||||
await db.update(issues).set({ executionWorkspaceId: workspace.id }).where(eq(issues.id, value.issueId));
|
||||
await expect(nativeCompletionFeedback(db, value.runId, report)).rejects.toThrow("omit continuation (or use null)");
|
||||
const [hold] = await db.insert(issueTreeHolds).values({ companyId: value.companyId, rootIssueId: value.issueId, mode: "pause" }).returning();
|
||||
await expect(nativeCompletionFeedback(db, value.runId, report)).resolves.toContain("task is paused");
|
||||
await db.update(issueTreeHolds).set({ status: "released" }).where(eq(issueTreeHolds.id, hold.id));
|
||||
const [question] = await db.insert(issueThreadInteractions).values({ companyId: value.companyId, issueId: value.issueId,
|
||||
sourceRunId: value.runId, createdByAgentId: value.agentId, kind: "ask_user_questions", status: "pending", continuationPolicy: "wake_assignee",
|
||||
payload: { version: 1, questions: [{ id: "branch", prompt: "Which private branch?", selectionMode: "single", required: true, allowOther: true, options: [] }] } }).returning();
|
||||
await expect(nativeCompletionFeedback(db, value.runId, report)).resolves.toContain(`Pending request: ${question.id}`);
|
||||
});
|
||||
it("corrects a repository-admission blocker before accepting the result", async () => {
|
||||
const value = await fixture();
|
||||
const [workspace] = await db.insert(executionWorkspaces).values({ companyId: value.companyId,
|
||||
|
||||
@@ -27,6 +27,8 @@ import {
|
||||
type PrpStructuredRunResult,
|
||||
} from "../../vendor/paperclip-runner/index.js";
|
||||
|
||||
const repositoryYieldCorrection = 'This task has a saved repository preparation for its next admission. This is routine setup, not a blocker. Call paperclip_finish with reportedWorkDisposition="yielded" and omit continuation (or use null); Paperclip will select the saved workspace continuation. Preserve the completed work and explain what remains. Do not repeat setup, set a timer, or ask the user to restart.';
|
||||
|
||||
function record(value: unknown): Record<string, unknown> {
|
||||
return value && typeof value === "object" && !Array.isArray(value)
|
||||
? value as Record<string, unknown> : {};
|
||||
@@ -250,7 +252,7 @@ export async function nativeCompletionFeedback(
|
||||
const repository = await pendingTaskRepository(db, { companyId: run.companyId, issueId: issue.id, workspaceId: issue.executionWorkspaceId, repositoryIds });
|
||||
if (repository &&
|
||||
!(await issueTreeControlService(db).getActivePauseHoldGate(run.companyId, issue.id))) {
|
||||
throw new Error('This task has a saved repository preparation for its next admission. This is routine setup, not a blocker. Call paperclip_finish with reportedWorkDisposition="yielded" and omit continuation (or use null); Paperclip will select the saved workspace continuation. Preserve the completed work and explain what remains. Do not repeat setup, set a timer, or ask the user to restart.');
|
||||
throw new Error(repositoryYieldCorrection);
|
||||
}
|
||||
}
|
||||
if (result.reportedWorkDisposition === "yielded" && result.continuation?.kind === "monitor") {
|
||||
@@ -260,7 +262,7 @@ export async function nativeCompletionFeedback(
|
||||
!isConversation(issue) &&
|
||||
result.reportedWorkDisposition === "yielded" &&
|
||||
result.continuation?.kind === "response_wake" &&
|
||||
result.completionClaim.remainingWork.some((entry) => entry.blocksCompletion) &&
|
||||
(!result.completionClaim.objectiveSatisfied || result.completionClaim.remainingWork.some((entry) => entry.blocksCompletion)) &&
|
||||
signals.actionableAttentionRequests.length === 0
|
||||
) {
|
||||
const pause = await issueTreeControlService(db).getActivePauseHoldGate(
|
||||
@@ -275,8 +277,10 @@ export async function nativeCompletionFeedback(
|
||||
});
|
||||
// Conversation turns and revoked chat authority retain their own lifecycle.
|
||||
if (chatWait === "not_applicable") {
|
||||
const repository = await pendingTaskRepository(db, { companyId: run.companyId, issueId: issue.id, workspaceId: issue.executionWorkspaceId });
|
||||
if (repository) throw new Error(repositoryYieldCorrection);
|
||||
throw new Error(
|
||||
"The response_wake report includes blocking remaining work without a pending wait condition. Answering a user comment does not pause the task. Continue the authorized work, register a same_agent continuation, or report the concrete blocker or required question. Do not repeat completed work.",
|
||||
"The response_wake report includes an unfinished objective or blocking remaining work without a pending wait condition. Answering a user comment does not pause the task. Continue the authorized work, or report the concrete blocker or required question. Do not repeat completed work.",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,6 +66,11 @@ hard stops. The conversion journey creates a real legacy agent and selects
|
||||
Paperclip Runner through the public agent update API. It checks retained
|
||||
configuration, encrypted secret references, environment, instructions, and
|
||||
corresponding permission policy before submitting work through the browser.
|
||||
The disposable legacy profile starts without optional CLI-only extra arguments;
|
||||
the journey does not remove settings from an existing user agent. For either
|
||||
provider, Daytona's public environment probe must save platform qualification
|
||||
before the same agent selects Runner. Probe leases and uncertain probe accounting
|
||||
remain included in attempt evidence and cleanup.
|
||||
|
||||
The observer seeds a random reference in the fixture-owned Git checkout before
|
||||
task creation. It checks the downloaded JSON against the original file bytes,
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { runnerSuites } from "./catalog.js";
|
||||
import { convertRepositoryProofAgent } from "./repository-conversion-setup.js";
|
||||
|
||||
function fixtures(driver = "sandbox"): Parameters<typeof convertRepositoryProofAgent>[1] {
|
||||
return { agent: { id: "agent", name: "Fixture Agent", companyId: "company" }, company: { id: "company", name: "Fixture Company" }, environment: { id: "environment", driver,
|
||||
config: { image: `image@sha256:${"a".repeat(64)}` } } };
|
||||
}
|
||||
|
||||
describe("real legacy conversion prerequisites", () => {
|
||||
it("constructs compatible legacy settings before recording a retention baseline", () => {
|
||||
const suite = runnerSuites.find(suite => suite.id === "legacy-conversion")!;
|
||||
for (const profile of suite.profiles) {
|
||||
const agent = profile.buildAgent({ environmentId: "environment", environmentFixtureId: "local", workspacePath: "/owned/workspace",
|
||||
executionId: "fixture", secretRefs: { OPENAI_API_KEY: { type: "secret_ref", secretId: "codex-reference", version: "latest" },
|
||||
ANTHROPIC_API_KEY: { type: "secret_ref", secretId: "claude-reference", version: "latest" } } });
|
||||
expect(agent.runner).toBe("legacy");
|
||||
expect(agent.adapterType).toBe(profile.provider === "codex" ? "codex_local" : "claude_local");
|
||||
expect(agent.adapterConfig).toMatchObject({ model: profile.model, timeoutSec: 360, cwd: "/owned/workspace" });
|
||||
expect(agent.adapterConfig).not.toHaveProperty("extraArgs");
|
||||
expect((agent.adapterConfig as Record<string, any>).env).toMatchObject(profile.provider === "codex"
|
||||
? { OPENAI_API_KEY: { type: "secret_ref", secretId: "codex-reference", version: "latest" } }
|
||||
: { ANTHROPIC_API_KEY: { type: "secret_ref", secretId: "claude-reference", version: "latest" } });
|
||||
}
|
||||
});
|
||||
it("qualifies the sandbox before selecting Runner for either provider", async () => {
|
||||
const fixture = fixtures(); let qualified = false;
|
||||
const api = { post: vi.fn(async () => { qualified = true; return { ok: true, details: { platformQualificationRecorded: true, leaseId: "lease" } }; }),
|
||||
patch: vi.fn(async () => { if (!qualified) throw new Error("agent_runner_unavailable"); return {}; }) };
|
||||
await expect(convertRepositoryProofAgent(api as any, fixture)).resolves.toBeUndefined();
|
||||
expect(api.post).toHaveBeenCalledWith("/api/environments/environment/probe?companyId=company", {}, { timeout: 330_000 });
|
||||
expect(fixture.setupProbe).toMatchObject({ leaseId: "lease" });
|
||||
expect(api.patch).toHaveBeenCalledExactlyOnceWith("/api/agents/agent", { runner: "paperclip" });
|
||||
});
|
||||
it("does not attempt conversion when platform qualification is absent", async () => {
|
||||
const fixture = fixtures(); const api = { post: vi.fn(async () => ({ ok: true, details: { leaseId: "lease" } })), patch: vi.fn() };
|
||||
await expect(convertRepositoryProofAgent(api as any, fixture)).rejects.toThrow("qualification");
|
||||
expect(api.patch).not.toHaveBeenCalled();
|
||||
expect(fixture.setupProbe?.leaseId).toBe("lease");
|
||||
});
|
||||
it("retains uncertain probe accounting when the response is lost", async () => {
|
||||
const fixture = fixtures(); const api = { post: vi.fn(async () => { throw new Error("connection lost"); }), patch: vi.fn() };
|
||||
await expect(convertRepositoryProofAgent(api as any, fixture)).rejects.toThrow("connection lost");
|
||||
expect(api.patch).not.toHaveBeenCalled();
|
||||
expect(fixture.setupLeaseAccountingIncomplete).toBe(true);
|
||||
});
|
||||
it.each([{ image: "image:latest" }, { image: `image@sha256:${"a".repeat(64)}`, snapshot: "snapshot" }])("refuses an unqualified image before creating a sandbox", async config => {
|
||||
const fixture = fixtures(); fixture.environment.config = config;
|
||||
const api = { post: vi.fn(), patch: vi.fn() };
|
||||
await expect(convertRepositoryProofAgent(api as any, fixture)).rejects.toThrow("immutable");
|
||||
expect(api.post).not.toHaveBeenCalled(); expect(api.patch).not.toHaveBeenCalled();
|
||||
});
|
||||
it("converts a local agent without creating a remote sandbox", async () => {
|
||||
const api = { post: vi.fn(), patch: vi.fn(async () => ({})) };
|
||||
await convertRepositoryProofAgent(api as any, fixtures("local"));
|
||||
expect(api.post).not.toHaveBeenCalled(); expect(api.patch).toHaveBeenCalledOnce();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,20 @@
|
||||
import type { RunnerApi } from "./api.js";
|
||||
import type { LiveFixtureValues } from "./live-fixtures.js";
|
||||
|
||||
export async function convertRepositoryProofAgent(api: Pick<RunnerApi, "post" | "patch">, fixtures: Pick<LiveFixtureValues, "environment" | "agent" | "company" | "setupProbe" | "setupLeaseAccountingIncomplete">) {
|
||||
if (fixtures.environment.driver === "sandbox") {
|
||||
if (!/^.+@sha256:[a-f0-9]{64}$/i.test(String(fixtures.environment.config?.image ?? "")) || fixtures.environment.config?.snapshot)
|
||||
throw new Error("Legacy conversion requires the immutable fixture image");
|
||||
const startedAt = Date.now(), previousAccounting = fixtures.setupLeaseAccountingIncomplete;
|
||||
fixtures.setupLeaseAccountingIncomplete = true;
|
||||
const probe = await api.post<{ ok: boolean; details?: { platformQualificationRecorded?: boolean; leaseId?: string } }>(
|
||||
`/api/environments/${fixtures.environment.id}/probe?companyId=${encodeURIComponent(fixtures.company.id)}`, {}, { timeout: 330_000 });
|
||||
if (probe.details?.leaseId) {
|
||||
fixtures.setupProbe = { leaseId: probe.details.leaseId, durationMs: Date.now() - startedAt };
|
||||
fixtures.setupLeaseAccountingIncomplete = previousAccounting;
|
||||
}
|
||||
if (!probe.ok || !probe.details?.platformQualificationRecorded || !probe.details.leaseId)
|
||||
throw new Error("Legacy conversion platform qualification was not recorded");
|
||||
}
|
||||
await api.patch(`/api/agents/${fixtures.agent.id}`, { runner: "paperclip" });
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import path from "node:path";
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
import { pollUntil, type RunnerApi } from "./api.js";
|
||||
import { createTaskThroughUi } from "./user-actions.js";
|
||||
import { convertRepositoryProofAgent } from "./repository-conversion-setup.js";
|
||||
import { readRegisteredArtifacts } from "./registered-artifact.js";
|
||||
import { gradeRepositoryProof, repositoryConfigurationDigest, repositoryProofFiles, type RepositoryProofObservation } from "./repository-verification.js";
|
||||
import type { LiveFixtureValues } from "./live-fixtures.js";
|
||||
@@ -85,7 +86,7 @@ export async function runRepositoryVerificationFlow(input: {
|
||||
observation.originalAdapter = before.adapterType;
|
||||
observation.configurationBefore = repositoryConfigurationDigest(before);
|
||||
observation.instructionsBefore = await instructionsDigest();
|
||||
if (conversion) await api.patch(`/api/agents/${fixtures.agent.id}`, { runner: "paperclip" });
|
||||
if (conversion) await convertRepositoryProofAgent(api, fixtures);
|
||||
const after = await api.get<Row>(`/api/agents/${fixtures.agent.id}`);
|
||||
observation.finalAdapter = after.adapterType;
|
||||
observation.configurationAfter = repositoryConfigurationDigest(after);
|
||||
@@ -95,15 +96,6 @@ export async function runRepositoryVerificationFlow(input: {
|
||||
if (after.defaultEnvironmentId !== fixtures.environment.id || observation.configurationBefore !== observation.configurationAfter ||
|
||||
observation.instructionsBefore !== observation.instructionsAfter || !observation.permissionsPreserved || after.adapterType !== "paperclip_runner")
|
||||
throw new Error("Production runner selection did not retain the configured agent context");
|
||||
if (conversion && execution.environment.id === "daytona" && execution.profile.provider === "codex") {
|
||||
if (!/^.+@sha256:[a-f0-9]{64}$/i.test(String(fixtures.environment.config?.image ?? "")) || fixtures.environment.config?.snapshot)
|
||||
throw new Error("Converted Codex requires the immutable fixture image");
|
||||
const startedAt = Date.now();
|
||||
const probe = await api.post<{ ok: boolean; details?: { platformQualificationRecorded?: boolean; leaseId?: string } }>(
|
||||
`/api/environments/${fixtures.environment.id}/probe?companyId=${encodeURIComponent(fixtures.company.id)}`, {}, { timeout: 330_000 });
|
||||
if (!probe.ok || !probe.details?.platformQualificationRecorded || !probe.details.leaseId) throw new Error("Converted Codex platform qualification was not recorded");
|
||||
fixtures.setupProbe = { leaseId: probe.details.leaseId, durationMs: Date.now() - startedAt };
|
||||
}
|
||||
await save("configured");
|
||||
const created = await createTaskThroughUi({ page, issuePrefix: fixtures.company.issuePrefix!, agentName: fixtures.agent.name,
|
||||
projectName: fixtures.project.name, title: execution.task.buildTitle(nonce), prompt: execution.task.buildPrompt(nonce), workMode: "standard", requireExplicitTitle: true });
|
||||
|
||||
@@ -9,8 +9,9 @@ export function repositoryProofFiles(nonce: string) {
|
||||
export function repositoryProofProfile(native: RunnerProfileFixture, legacy?: RunnerProfileFixture): RunnerProfileFixture {
|
||||
return { ...native, buildAgent(input) {
|
||||
const { instructionsBundle: _manual, ...agent } = (legacy ?? native).buildAgent(input);
|
||||
return { ...agent, ...(legacy ? { runner: "legacy" } : {}), budgetMonthlyCents: 1000,
|
||||
adapterConfig: { ...(agent.adapterConfig as Record<string, unknown>), model: native.model } };
|
||||
const config = { ...(agent.adapterConfig as Record<string, unknown>), model: native.model };
|
||||
if (legacy) delete (config as Record<string, unknown>).extraArgs;
|
||||
return { ...agent, ...(legacy ? { runner: "legacy" } : {}), budgetMonthlyCents: 1000, adapterConfig: config };
|
||||
} };
|
||||
}
|
||||
|
||||
@@ -95,7 +96,7 @@ export function repositoryConfigurationDigest(agent: Record<string, any>) {
|
||||
|
||||
export function repositoryProofDigest() {
|
||||
const hash = createHash("sha256");
|
||||
for (const file of ["repository-verification.ts", "repository-verification-flow.ts", "registered-artifact.ts", "live-fixtures.ts", "runner.spec.ts", "catalog.ts", "stock-harness.ts"])
|
||||
for (const file of ["repository-verification.ts", "repository-verification-flow.ts", "repository-conversion-setup.ts", "registered-artifact.ts", "live-fixtures.ts", "runner.spec.ts", "catalog.ts", "stock-harness.ts"])
|
||||
hash.update(file).update(readFileSync(new URL(file, import.meta.url)));
|
||||
return hash.digest("hex");
|
||||
}
|
||||
Reference in new issue
Block a user