feat: compose TechLog static and mock adapters
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { test } from "vitest";
|
||||
|
||||
import type {
|
||||
PublicationAggregate,
|
||||
PublicPreview,
|
||||
ValidationReport,
|
||||
WorkingCopy,
|
||||
WorkingCopyDetail,
|
||||
} from "../../../src/features/tech-log/contracts/studio/contract.ts";
|
||||
import {
|
||||
deriveDocumentState,
|
||||
deriveNextAction,
|
||||
derivePreviewState,
|
||||
deriveValidationState,
|
||||
} from "../../../src/features/tech-log/domain/studio/document-state.ts";
|
||||
import { createLocalId } from "../../../src/features/tech-log/domain/studio/local-id.ts";
|
||||
|
||||
const now = "2026-08-14T12:00:00.000Z";
|
||||
const validUntil = "2026-08-14T12:30:00.000Z";
|
||||
const expiredAt = "2026-08-14T12:00:00.000Z";
|
||||
const futureExpiry = "2026-08-14T12:45:00.000Z";
|
||||
|
||||
function documentAt(
|
||||
version = 7,
|
||||
overrides: Partial<WorkingCopy> = {},
|
||||
): WorkingCopy {
|
||||
return {
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
kind: "CASE",
|
||||
version,
|
||||
title: "Fetch Join 경계",
|
||||
slug: "fetch-join-boundary",
|
||||
summary: "목록 경계를 검증합니다.",
|
||||
topicId: "22222222-2222-4222-8222-222222222222",
|
||||
projectId: null,
|
||||
relations: [],
|
||||
updatedAt: now,
|
||||
problem: "문제",
|
||||
conclusion: "결론",
|
||||
environment: "PostgreSQL",
|
||||
reproduction: "재현",
|
||||
lastVerifiedOn: "2026-08-14",
|
||||
bodyMarkdown: "## 본문",
|
||||
...overrides,
|
||||
} as WorkingCopy;
|
||||
}
|
||||
|
||||
function validationAt(
|
||||
version = 7,
|
||||
overrides: Partial<ValidationReport> = {},
|
||||
): ValidationReport {
|
||||
return {
|
||||
validationId: "33333333-3333-4333-8333-333333333333",
|
||||
documentId: "11111111-1111-4111-8111-111111111111",
|
||||
validatedVersion: version,
|
||||
status: "VALID",
|
||||
issues: [],
|
||||
validatedAt: "2026-08-14T11:50:00.000Z",
|
||||
validUntil,
|
||||
dependencyRevision: "catalog-1",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function previewAt(
|
||||
version = 7,
|
||||
validationId = validationAt(version).validationId,
|
||||
overrides: Partial<PublicPreview> = {},
|
||||
): PublicPreview {
|
||||
return {
|
||||
previewId: "44444444-4444-4444-8444-444444444444",
|
||||
documentId: "11111111-1111-4111-8111-111111111111",
|
||||
previewVersion: version,
|
||||
validationId,
|
||||
createdAt: "2026-08-14T11:55:00.000Z",
|
||||
expiresAt: futureExpiry,
|
||||
renderModel: {
|
||||
kind: "CASE",
|
||||
title: "Fetch Join 경계",
|
||||
slug: "fetch-join-boundary",
|
||||
summary: "목록 경계를 검증합니다.",
|
||||
topic: {
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
label: "JPA",
|
||||
publicPath: null,
|
||||
},
|
||||
project: null,
|
||||
relations: [],
|
||||
blocks: [],
|
||||
},
|
||||
...overrides,
|
||||
} as PublicPreview;
|
||||
}
|
||||
|
||||
function publishedAt(version = 7): PublicationAggregate {
|
||||
return {
|
||||
publicationId: "55555555-5555-4555-8555-555555555555",
|
||||
documentId: "11111111-1111-4111-8111-111111111111",
|
||||
status: "PUBLISHED",
|
||||
publishedVersion: version,
|
||||
publicationRevision: 1,
|
||||
latestEventId: "66666666-6666-4666-8666-666666666666",
|
||||
publicPath: "/cases/fetch-join-boundary",
|
||||
updatedAt: now,
|
||||
};
|
||||
}
|
||||
|
||||
type InputOverrides = Partial<Parameters<typeof deriveNextAction>[0]>;
|
||||
|
||||
function input(overrides: InputOverrides = {}) {
|
||||
return {
|
||||
document: documentAt(),
|
||||
validation: validationAt(),
|
||||
preview: previewAt(),
|
||||
publication: null,
|
||||
now,
|
||||
dependencyRevision: "catalog-1",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("same published version is NONE before stale validation checks", () => {
|
||||
assert.equal(
|
||||
deriveNextAction({
|
||||
document: documentAt(7),
|
||||
validation: validationAt(7, { validUntil: expiredAt }),
|
||||
preview: previewAt(7, "other-validation", { expiresAt: expiredAt }),
|
||||
publication: publishedAt(7),
|
||||
now,
|
||||
dependencyRevision: "catalog-2",
|
||||
}),
|
||||
"NONE",
|
||||
);
|
||||
});
|
||||
|
||||
test("validation freshness requires the saved version, a future expiry, and matching dependencies", () => {
|
||||
assert.deepEqual(deriveValidationState(input({ validation: null })), {
|
||||
result: "NOT_RUN",
|
||||
freshness: "NONE",
|
||||
});
|
||||
assert.deepEqual(
|
||||
deriveValidationState(input({ validation: validationAt(6) })),
|
||||
{ result: "VALID", freshness: "STALE" },
|
||||
);
|
||||
assert.deepEqual(
|
||||
deriveValidationState(
|
||||
input({ validation: validationAt(7, { validUntil: now }) }),
|
||||
),
|
||||
{ result: "VALID", freshness: "STALE" },
|
||||
);
|
||||
assert.deepEqual(
|
||||
deriveValidationState(input({ dependencyRevision: "catalog-2" })),
|
||||
{ result: "VALID", freshness: "STALE" },
|
||||
);
|
||||
assert.deepEqual(deriveValidationState(input()), {
|
||||
result: "VALID",
|
||||
freshness: "CURRENT",
|
||||
});
|
||||
});
|
||||
|
||||
test("preview expiry takes precedence and all remaining freshness checks are enforced", () => {
|
||||
const validation = validationAt();
|
||||
assert.equal(derivePreviewState(input({ preview: null })), "NONE");
|
||||
assert.equal(
|
||||
derivePreviewState(
|
||||
input({ preview: previewAt(6, validation.validationId) }),
|
||||
),
|
||||
"STALE",
|
||||
);
|
||||
assert.equal(
|
||||
derivePreviewState(input({ preview: previewAt(7, "other-validation") })),
|
||||
"STALE",
|
||||
);
|
||||
assert.equal(
|
||||
derivePreviewState(
|
||||
input({
|
||||
preview: previewAt(6, "other-validation", { expiresAt: now }),
|
||||
}),
|
||||
),
|
||||
"EXPIRED",
|
||||
);
|
||||
assert.equal(derivePreviewState(input()), "CURRENT");
|
||||
assert.equal(
|
||||
derivePreviewState(
|
||||
input({ validation: validationAt(7, { validUntil: now }) }),
|
||||
),
|
||||
"STALE",
|
||||
);
|
||||
assert.equal(
|
||||
derivePreviewState(input({ dependencyRevision: "catalog-2" })),
|
||||
"STALE",
|
||||
);
|
||||
});
|
||||
|
||||
test("save-reset, dependency staleness, invalid reports, warnings and preview gaps are ordered", () => {
|
||||
assert.equal(
|
||||
deriveNextAction(
|
||||
input({ document: documentAt(7, { title: "", slug: "", summary: "" }) }),
|
||||
),
|
||||
"CONTINUE_EDITING",
|
||||
);
|
||||
assert.equal(deriveNextAction(input({ validation: null })), "VALIDATE");
|
||||
assert.equal(
|
||||
deriveNextAction(input({ dependencyRevision: "catalog-2" })),
|
||||
"VALIDATE",
|
||||
);
|
||||
assert.equal(
|
||||
deriveNextAction(
|
||||
input({ validation: validationAt(7, { status: "INVALID" }) }),
|
||||
),
|
||||
"FIX_VALIDATION",
|
||||
);
|
||||
assert.equal(
|
||||
deriveNextAction(
|
||||
input({
|
||||
validation: validationAt(7, { status: "WARNINGS" }),
|
||||
preview: null,
|
||||
}),
|
||||
),
|
||||
"CREATE_PREVIEW",
|
||||
);
|
||||
assert.equal(
|
||||
deriveNextAction(
|
||||
input({ validation: validationAt(7, { status: "WARNINGS" }) }),
|
||||
),
|
||||
"PUBLISH",
|
||||
);
|
||||
assert.deepEqual(
|
||||
deriveValidationState(
|
||||
input({
|
||||
validation: validationAt(7, { status: "WARNINGS" }),
|
||||
dependencyRevision: "catalog-2",
|
||||
}),
|
||||
),
|
||||
{ result: "WARNINGS", freshness: "STALE" },
|
||||
);
|
||||
assert.equal(
|
||||
deriveNextAction(
|
||||
input({
|
||||
validation: validationAt(7, { status: "WARNINGS" }),
|
||||
dependencyRevision: "catalog-2",
|
||||
}),
|
||||
),
|
||||
"VALIDATE",
|
||||
);
|
||||
});
|
||||
|
||||
test("saving retains an old preview as stale and returns to validation", () => {
|
||||
const previousValidation = validationAt(7);
|
||||
const previousPreview = previewAt(7, previousValidation.validationId);
|
||||
const state = deriveDocumentState(
|
||||
input({
|
||||
document: documentAt(8),
|
||||
validation: null,
|
||||
preview: previousPreview,
|
||||
}),
|
||||
);
|
||||
|
||||
assert.deepEqual(state.validation, {
|
||||
result: "NOT_RUN",
|
||||
freshness: "NONE",
|
||||
});
|
||||
assert.equal(state.preview, "STALE");
|
||||
assert.equal(state.nextAction, "VALIDATE");
|
||||
});
|
||||
|
||||
test("unpublished or old-version publications follow the normal next-action flow", () => {
|
||||
assert.equal(
|
||||
deriveNextAction(
|
||||
input({ publication: { ...publishedAt(), status: "UNPUBLISHED" } }),
|
||||
),
|
||||
"PUBLISH",
|
||||
);
|
||||
assert.equal(
|
||||
deriveNextAction(input({ publication: publishedAt(6) })),
|
||||
"PUBLISH",
|
||||
);
|
||||
});
|
||||
|
||||
test("deriveDocumentState is consistent with a WorkingCopyDetail aggregate", () => {
|
||||
const detail: WorkingCopyDetail = {
|
||||
document: documentAt(),
|
||||
currentValidation: validationAt(),
|
||||
latestPreview: previewAt(),
|
||||
currentPublication: publishedAt(),
|
||||
dependencyRevision: "catalog-1",
|
||||
};
|
||||
const explicit = deriveDocumentState({
|
||||
document: detail.document,
|
||||
validation: detail.currentValidation,
|
||||
preview: detail.latestPreview,
|
||||
publication: detail.currentPublication,
|
||||
dependencyRevision: detail.dependencyRevision,
|
||||
now,
|
||||
});
|
||||
const fromDetail = deriveDocumentState({ ...detail, now });
|
||||
|
||||
assert.deepEqual(explicit, {
|
||||
validation: { result: "VALID", freshness: "CURRENT" },
|
||||
preview: "CURRENT",
|
||||
nextAction: "NONE",
|
||||
});
|
||||
assert.deepEqual(fromDetail, explicit);
|
||||
});
|
||||
|
||||
test("local Studio IDs are deterministic without Web Crypto and prefer UUIDs when present", () => {
|
||||
assert.equal(
|
||||
createLocalId("preview", null, () => 1234, () => 0.25),
|
||||
"preview-1234-250000000",
|
||||
);
|
||||
assert.equal(
|
||||
createLocalId("save", { randomUUID: () => "fixture-uuid" }),
|
||||
"save-fixture-uuid",
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user