chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed on it through 43 template commits, so it was missing all three rounds of adapter remediation — including files it never had, such as the shared `abortable-operation` primitive and the `exact-snapshot` decoder that later fixes are written against. Taking only the newest round was not possible for that reason: the delta is coherent only as a whole. The product had not touched `src/adapters` at all since materialization, so the 140-file delta applied with a three-way merge and no conflicts. `package.json` was the single overlap and merged cleanly: the product owns `name`, the template contributed `check:adapter-inventory`, `check:remediation-ledger` and the image-resolve-signal type fixture. All 24 product-owned files — README, index.html, CI workflow, i18n catalog, home page, generated schemas, evidence scripts, component and visual snapshots — are byte-identical to `main`. `template.lock.json` now pins the synced revision and tree. Verified in this repository, not inherited from the template: six type projects, lint, nine gates (adapter inventory, remediation ledger, registries, diagnostics, realtime boundaries, architecture, browser file/storage boundaries, optional recipes, documentation), the production build, and 2,054 of 2,073 tests. The 19 failures are all in `tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template records; four suites that failed once under parallel load pass in isolation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
002ba3624e
commit
4bff9ca151
@@ -290,7 +290,10 @@ function createWorker(
|
||||
return browserDataSuccess(undefined);
|
||||
},
|
||||
async cleanupTransaction() {
|
||||
return browserDataSuccess(undefined);
|
||||
return browserDataSuccess({ kind: "CLEANED" as const });
|
||||
},
|
||||
async abortPreparedPut() {
|
||||
return browserDataSuccess({ kind: "CLEANED" as const });
|
||||
},
|
||||
async finalizePut() {
|
||||
return browserDataSuccess(undefined);
|
||||
@@ -349,6 +352,123 @@ describe("OPFS byte-store coordinator", () => {
|
||||
expect(JSON.stringify(observations)).not.toContain("object_12345678");
|
||||
});
|
||||
|
||||
/**
|
||||
* NS-04. Awaiting `journal.complete` without checking it reported a settled
|
||||
* transaction that nobody settled: the caller saw plain success and success
|
||||
* telemetry while the durable row stayed `COMMITTED`, so the reconcile
|
||||
* backlog grew invisibly.
|
||||
*/
|
||||
it("does not report a settled write when the journal cannot complete it", async () => {
|
||||
const journal = createJournal();
|
||||
journal.complete = async () =>
|
||||
browserDataFailure("UNAVAILABLE", "INDEXEDDB_WRITE", {
|
||||
retryable: true,
|
||||
recovery: "RECONCILE",
|
||||
});
|
||||
const observations: { operation: string; outcome: string }[] = [];
|
||||
const adapter = createOpfsByteStoreAdapter({
|
||||
journal,
|
||||
worker: createWorker(),
|
||||
scope,
|
||||
storagePolicy,
|
||||
policy: {
|
||||
...resolveOpfsRuntimePolicy(),
|
||||
chunkSizeBytes: 64 * 1024,
|
||||
maxObjectBytes: 64 * 1024,
|
||||
maxChunkCount: 1,
|
||||
},
|
||||
createTransactionId: () => "transaction_12345678",
|
||||
now: () => 100,
|
||||
observer: (event) =>
|
||||
observations.push(event as { operation: string; outcome: string }),
|
||||
});
|
||||
|
||||
const result = await adapter.objects.put({
|
||||
objectId: "object_12345678",
|
||||
expectedGeneration: null,
|
||||
mediaType: "application/octet-stream",
|
||||
source: sourceFrom(new Uint8Array([1, 2, 3])),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
if (!result.ok) {
|
||||
// The payload is durable, so recovery is reconciliation, not a rewrite.
|
||||
expect(result.error.recovery).toBe("RECONCILE");
|
||||
expect(result.error.operation).toBe("OBJECT_WRITE");
|
||||
}
|
||||
expect(
|
||||
observations.some(
|
||||
(event) =>
|
||||
event.operation === "OBJECT_WRITE" && event.outcome === "SUCCEEDED",
|
||||
),
|
||||
).toBe(false);
|
||||
// The committed row stays the reconciler's authority.
|
||||
expect(journal.transactions.get("transaction_12345678")?.phase).toBe(
|
||||
"COMMITTED",
|
||||
);
|
||||
expect(journal.objects.get("object_12345678")).toBeDefined();
|
||||
});
|
||||
|
||||
it("records maintenance debt when a committed delete cannot be completed", async () => {
|
||||
const journal = createJournal();
|
||||
const observations: { operation: string; outcome: string }[] = [];
|
||||
const policy = {
|
||||
...resolveOpfsRuntimePolicy(),
|
||||
chunkSizeBytes: 64 * 1024,
|
||||
maxObjectBytes: 64 * 1024,
|
||||
maxChunkCount: 1,
|
||||
};
|
||||
const writer = createOpfsByteStoreAdapter({
|
||||
journal,
|
||||
worker: createWorker(),
|
||||
scope,
|
||||
storagePolicy,
|
||||
policy,
|
||||
createTransactionId: () => "transaction_12345678",
|
||||
now: () => 100,
|
||||
});
|
||||
await writer.objects.put({
|
||||
objectId: "object_12345678",
|
||||
expectedGeneration: null,
|
||||
mediaType: "application/octet-stream",
|
||||
source: sourceFrom(new Uint8Array([1, 2, 3])),
|
||||
});
|
||||
|
||||
// The journal can commit the deletion but cannot settle the transaction.
|
||||
const remover = createOpfsByteStoreAdapter({
|
||||
journal: {
|
||||
...journal,
|
||||
complete: async () =>
|
||||
browserDataFailure("UNAVAILABLE", "INDEXEDDB_WRITE", {
|
||||
retryable: true,
|
||||
recovery: "RECONCILE",
|
||||
}),
|
||||
},
|
||||
worker: createWorker(),
|
||||
scope,
|
||||
storagePolicy,
|
||||
policy,
|
||||
createTransactionId: () => "transaction_87654321",
|
||||
now: () => 200,
|
||||
observer: (event) =>
|
||||
observations.push(event as { operation: string; outcome: string }),
|
||||
});
|
||||
|
||||
// Logical deletion is already committed, so the caller must not be asked to
|
||||
// repeat a non-idempotent delete — but it is not a settled success either.
|
||||
const result = await remover.objects.remove({
|
||||
objectId: "object_12345678",
|
||||
expectedGeneration: 1,
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
const deleteOutcomes = observations
|
||||
.filter((event) => event.operation === "OBJECT_DELETE")
|
||||
.map((event) => event.outcome);
|
||||
expect(deleteOutcomes.at(-1)).toBe("DEGRADED");
|
||||
expect(deleteOutcomes).not.toContain("SUCCEEDED");
|
||||
});
|
||||
|
||||
it("deep-snapshots scope and policy at composition against caller mutation", async () => {
|
||||
const journal = createJournal();
|
||||
const mutableScope = { ...scope };
|
||||
@@ -501,7 +621,183 @@ describe("OPFS byte-store coordinator", () => {
|
||||
expect(replacementWorkerMethod).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps a committed journal row for reconciliation when cleanup fails", async () => {
|
||||
it("keeps PREPARING journal when compensating cleanup is aborted or unavailable", async () => {
|
||||
for (const failing of [
|
||||
{
|
||||
async abortPreparedPut() {
|
||||
return browserDataFailure("ABORTED", "OBJECT_RECONCILE");
|
||||
},
|
||||
},
|
||||
{
|
||||
async abortPreparedPut() {
|
||||
return browserDataSuccess({ kind: "EFFECT_UNKNOWN" as const });
|
||||
},
|
||||
},
|
||||
]) {
|
||||
const journal = createJournal();
|
||||
const worker = createWorker({
|
||||
async preparePut() {
|
||||
return browserDataFailure("QUOTA_EXCEEDED", "OBJECT_WRITE");
|
||||
},
|
||||
...failing,
|
||||
});
|
||||
const adapter = createOpfsByteStoreAdapter({
|
||||
journal,
|
||||
worker,
|
||||
scope,
|
||||
storagePolicy,
|
||||
createTransactionId: () => "transaction_12345678",
|
||||
now: () => 100,
|
||||
});
|
||||
|
||||
const result = await adapter.objects.put({
|
||||
objectId: "object_12345678",
|
||||
expectedGeneration: null,
|
||||
mediaType: "application/octet-stream",
|
||||
source: sourceFrom(new Uint8Array([1])),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
// The journal row is the only durable evidence that staging bytes may
|
||||
// still exist, so it survives an unconfirmed compensation.
|
||||
expect(
|
||||
journal.transactions.get("transaction_12345678")?.phase,
|
||||
).toBe("PREPARING");
|
||||
}
|
||||
});
|
||||
|
||||
it("does not roll back journal after an unknown worker mutation effect", async () => {
|
||||
const journal = createJournal();
|
||||
const worker = createWorker({
|
||||
async abortPreparedPut() {
|
||||
return browserDataSuccess({ kind: "EFFECT_UNKNOWN" as const });
|
||||
},
|
||||
});
|
||||
const rollback = vi.spyOn(journal, "rollback");
|
||||
vi.spyOn(journal, "markFilesReady").mockResolvedValueOnce(
|
||||
browserDataFailure("UNAVAILABLE", "OBJECT_WRITE"),
|
||||
);
|
||||
const adapter = createOpfsByteStoreAdapter({
|
||||
journal,
|
||||
worker,
|
||||
scope,
|
||||
storagePolicy,
|
||||
createTransactionId: () => "transaction_12345678",
|
||||
now: () => 100,
|
||||
});
|
||||
|
||||
const result = await adapter.objects.put({
|
||||
objectId: "object_12345678",
|
||||
expectedGeneration: null,
|
||||
mediaType: "application/octet-stream",
|
||||
source: sourceFrom(new Uint8Array([1])),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(false);
|
||||
expect(rollback).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("holds the OPFS mutation lease until exact physical cleanup completes", async () => {
|
||||
const journal = createJournal();
|
||||
const observed: Array<Readonly<Record<string, unknown>>> = [];
|
||||
const worker = createWorker({
|
||||
async preparePut() {
|
||||
return browserDataFailure("QUOTA_EXCEEDED", "OBJECT_WRITE");
|
||||
},
|
||||
async abortPreparedPut(request) {
|
||||
observed.push({ ...request });
|
||||
return browserDataSuccess({ kind: "CLEANED" as const });
|
||||
},
|
||||
});
|
||||
const compensation = new AbortController();
|
||||
const caller = new AbortController();
|
||||
caller.abort();
|
||||
const adapter = createOpfsByteStoreAdapter({
|
||||
journal,
|
||||
worker,
|
||||
scope,
|
||||
storagePolicy,
|
||||
createTransactionId: () => "transaction_12345678",
|
||||
createPhysicalGenerationId: () => "f".repeat(32) as never,
|
||||
compensationSignal: compensation.signal,
|
||||
now: () => 100,
|
||||
});
|
||||
|
||||
await adapter.objects.put({
|
||||
objectId: "object_12345678",
|
||||
expectedGeneration: null,
|
||||
mediaType: "application/octet-stream",
|
||||
source: sourceFrom(new Uint8Array([1])),
|
||||
});
|
||||
|
||||
expect(observed).toHaveLength(1);
|
||||
expect(observed[0]).toMatchObject({
|
||||
transactionId: "transaction_12345678",
|
||||
physicalGenerationId: "f".repeat(32),
|
||||
});
|
||||
// Compensation never inherits the caller signal.
|
||||
expect(observed[0]?.signal).toBe(compensation.signal);
|
||||
expect(
|
||||
journal.transactions.has("transaction_12345678"),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("delayed stale cleanup cannot delete a reused logical generation", async () => {
|
||||
const journal = createJournal();
|
||||
const issued: string[] = [];
|
||||
let nextToken = 0;
|
||||
const worker = createWorker({
|
||||
async abortPreparedPut(request) {
|
||||
issued.push(request.physicalGenerationId);
|
||||
return browserDataSuccess({ kind: "CLEANED" as const });
|
||||
},
|
||||
});
|
||||
let transaction = 0;
|
||||
// T1 abandons its prepared put at the same logical generation 1.
|
||||
vi.spyOn(journal, "markFilesReady").mockResolvedValueOnce(
|
||||
browserDataFailure("UNAVAILABLE", "OBJECT_WRITE"),
|
||||
);
|
||||
const adapter = createOpfsByteStoreAdapter({
|
||||
journal,
|
||||
worker,
|
||||
scope,
|
||||
storagePolicy,
|
||||
createTransactionId: () => `transaction_1234567${(transaction += 1)}`,
|
||||
createPhysicalGenerationId: () =>
|
||||
String(nextToken += 1).padStart(32, "0") as never,
|
||||
now: () => 100,
|
||||
});
|
||||
const first = await adapter.objects.put({
|
||||
objectId: "object_12345678",
|
||||
expectedGeneration: null,
|
||||
mediaType: "application/octet-stream",
|
||||
source: sourceFrom(new Uint8Array([1])),
|
||||
});
|
||||
expect(first.ok).toBe(false);
|
||||
|
||||
// T2 legitimately reuses logical generation 1 with a different token.
|
||||
const second = await adapter.objects.put({
|
||||
objectId: "object_12345678",
|
||||
expectedGeneration: null,
|
||||
mediaType: "application/octet-stream",
|
||||
source: sourceFrom(new Uint8Array([1])),
|
||||
});
|
||||
expect(second.ok).toBe(true);
|
||||
if (second.ok) expect(second.value.generation).toBe(1);
|
||||
|
||||
// The stale compensation targeted only T1's physical token.
|
||||
expect(issued).toEqual([String(1).padStart(32, "0")]);
|
||||
const stored = journal.objects.get("object_12345678");
|
||||
expect(stored?.physicalSchemaVersion).toBe(1);
|
||||
});
|
||||
|
||||
/**
|
||||
* STO-RR-01. Finalization runs after the commit fence, so the payload is
|
||||
* durable and the journal row must survive for reconciliation. What the
|
||||
* caller must not be told is that the write settled: the previous generation
|
||||
* and the staging directory are still there.
|
||||
*/
|
||||
it("reports a failed finalization instead of a plain success", async () => {
|
||||
const journal = createJournal();
|
||||
const worker = createWorker({
|
||||
async finalizePut() {
|
||||
@@ -533,7 +829,8 @@ describe("OPFS byte-store coordinator", () => {
|
||||
source: sourceFrom(new Uint8Array([1])),
|
||||
});
|
||||
|
||||
expect(result.ok).toBe(true);
|
||||
expect(result.ok).toBe(false);
|
||||
expect(result.ok ? null : result.error.operation).toBe("OBJECT_WRITE");
|
||||
expect(
|
||||
journal.transactions.get("transaction_12345678")?.phase,
|
||||
).toBe("COMMITTED");
|
||||
|
||||
Reference in New Issue
Block a user