mirror of
https://github.com/larksuite/cli.git
synced 2026-07-10 11:04:26 +08:00
fix: require svglide bundle receipts before publish
This commit is contained in:
@@ -43,6 +43,11 @@ type svglideManifestSize struct {
|
||||
|
||||
type svglideManifestReceipts struct {
|
||||
ValidateSVGDeck string `json:"validate_svg_deck"`
|
||||
PromptReadTrace string `json:"prompt_read_trace"`
|
||||
AssetReadiness string `json:"asset_readiness"`
|
||||
TypographyPlan string `json:"typography_plan"`
|
||||
FrameDiscipline string `json:"frame_discipline"`
|
||||
ParserContract string `json:"parser_contract"`
|
||||
}
|
||||
|
||||
type svglideManifestPage struct {
|
||||
@@ -160,6 +165,11 @@ type svglideValidateSVGDeckReceipt struct {
|
||||
TotalErrors *int `json:"totalErrors"`
|
||||
}
|
||||
|
||||
type svglideZeroErrorReceipt struct {
|
||||
TotalErrors *int `json:"totalErrors"`
|
||||
OK *bool `json:"ok"`
|
||||
}
|
||||
|
||||
func validateSVGlideBundleReceipt(runtime *common.RuntimeContext, baseDir string, receipts svglideManifestReceipts) error {
|
||||
receiptPath, err := svglideManifestChildPath("receipts.validate_svg_deck", baseDir, receipts.ValidateSVGDeck)
|
||||
if err != nil {
|
||||
@@ -176,6 +186,42 @@ func validateSVGlideBundleReceipt(runtime *common.RuntimeContext, baseDir string
|
||||
if receipt.TotalErrors == nil || *receipt.TotalErrors != 0 {
|
||||
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "--manifest validate_svg_deck receipt must have totalErrors=0").WithParam("--manifest")
|
||||
}
|
||||
required := []struct {
|
||||
label string
|
||||
rel string
|
||||
}{
|
||||
{"receipts.prompt_read_trace", receipts.PromptReadTrace},
|
||||
{"receipts.asset_readiness", receipts.AssetReadiness},
|
||||
{"receipts.typography_plan", receipts.TypographyPlan},
|
||||
{"receipts.frame_discipline", receipts.FrameDiscipline},
|
||||
{"receipts.parser_contract", receipts.ParserContract},
|
||||
}
|
||||
for _, item := range required {
|
||||
label, rel := item.label, item.rel
|
||||
if strings.TrimSpace(rel) == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--manifest %s is required", label).WithParam("--manifest")
|
||||
}
|
||||
joined, err := svglideManifestChildPath(label, baseDir, rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
raw, err := readSVGlideManifestInput(runtime, joined, "--manifest %s receipt not found: %s", label, rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if label == "receipts.frame_discipline" || label == "receipts.parser_contract" {
|
||||
var zero svglideZeroErrorReceipt
|
||||
if err := json.Unmarshal(raw, &zero); err != nil {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--manifest %s receipt invalid JSON: %v", label, err).WithParam("--manifest").WithCause(err)
|
||||
}
|
||||
if zero.TotalErrors != nil && *zero.TotalErrors != 0 {
|
||||
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "--manifest %s receipt must have totalErrors=0", label).WithParam("--manifest")
|
||||
}
|
||||
if zero.OK != nil && !*zero.OK {
|
||||
return errs.NewValidationError(errs.SubtypeFailedPrecondition, "--manifest %s receipt must have ok=true", label).WithParam("--manifest")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -22,13 +22,19 @@ import (
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
const (
|
||||
svglideValidateOnlyReceiptsJSON = `"validate_svg_deck":"receipts/validate_svg_deck.json"`
|
||||
svglideRequiredReceiptsJSON = svglideValidateOnlyReceiptsJSON + `,"prompt_read_trace":"receipts/prompt_read_trace.json","asset_readiness":"receipts/asset_readiness.json","typography_plan":"receipts/typography_plan.json","frame_discipline":"receipts/frame_discipline.json","parser_contract":"receipts/parser_contract.json"`
|
||||
)
|
||||
|
||||
func TestPrepareSVGlidePublishSpecReadsManifestAndPages(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
withSlidesTestWorkingDir(t, dir)
|
||||
page := `<svg xmlns="http://www.w3.org/2000/svg" xmlns:slide="https://slides.bytedance.com/ns" slide:role="slide" id="page-001" viewBox="0 0 960 540"><rect slide:role="background" x="0" y="0" width="960" height="540" fill="rgba(255,255,255,1)"/></svg>`
|
||||
mustWriteSVGlideTestFile(t, "pages/page-001.svg", page)
|
||||
mustWriteSVGlideTestFile(t, "receipts/validate_svg_deck.json", `{"totalErrors":0}`)
|
||||
mustWriteSVGlideTestFile(t, "manifest.json", fmt.Sprintf(`{
|
||||
mustWriteSVGlideRequiredNonValidateReceipts(t)
|
||||
mustWriteSVGlideTestFile(t, "manifest.json", withSVGlideRequiredReceipts(fmt.Sprintf(`{
|
||||
"version": "svglide.manifest.v1",
|
||||
"protocol": "svg-slides.v1",
|
||||
"title": "Manifest Title",
|
||||
@@ -37,7 +43,7 @@ func TestPrepareSVGlidePublishSpecReadsManifestAndPages(t *testing.T) {
|
||||
"published": false,
|
||||
"pages": [{"id": "page-001", "index": 1, "file": "pages/page-001.svg", "sha256": %q}],
|
||||
"receipts": {"validate_svg_deck": "receipts/validate_svg_deck.json"}
|
||||
}`, sha256Hex([]byte(page))))
|
||||
}`, sha256Hex([]byte(page)))))
|
||||
|
||||
runtime, _ := newSVGlideTestRuntime(t)
|
||||
spec, err := prepareSVGlidePublishSpec(runtime, "manifest.json", "")
|
||||
@@ -64,7 +70,8 @@ func TestPrepareSVGlidePublishSpecTitleOverride(t *testing.T) {
|
||||
page := `<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540"></svg>`
|
||||
mustWriteSVGlideTestFile(t, "page.svg", page)
|
||||
mustWriteSVGlideTestFile(t, "receipts/validate_svg_deck.json", `{"totalErrors":0}`)
|
||||
mustWriteSVGlideTestFile(t, "manifest.json", fmt.Sprintf(`{
|
||||
mustWriteSVGlideRequiredNonValidateReceipts(t)
|
||||
mustWriteSVGlideTestFile(t, "manifest.json", withSVGlideRequiredReceipts(fmt.Sprintf(`{
|
||||
"version": "svglide.manifest.v1",
|
||||
"protocol": "svg-slides.v1",
|
||||
"title": "Manifest Title",
|
||||
@@ -73,7 +80,7 @@ func TestPrepareSVGlidePublishSpecTitleOverride(t *testing.T) {
|
||||
"published": false,
|
||||
"pages": [{"id": "p1", "index": 1, "file": "page.svg", "sha256": %q}],
|
||||
"receipts": {"validate_svg_deck": "receipts/validate_svg_deck.json"}
|
||||
}`, sha256Hex([]byte(page))))
|
||||
}`, sha256Hex([]byte(page)))))
|
||||
|
||||
runtime, _ := newSVGlideTestRuntime(t)
|
||||
spec, err := prepareSVGlidePublishSpec(runtime, "manifest.json", "CLI Title")
|
||||
@@ -87,10 +94,11 @@ func TestPrepareSVGlidePublishSpecTitleOverride(t *testing.T) {
|
||||
|
||||
func TestPrepareSVGlidePublishSpecValidationErrors(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
manifest string
|
||||
pageFiles map[string]string
|
||||
wantMessage string
|
||||
name string
|
||||
manifest string
|
||||
pageFiles map[string]string
|
||||
skipDefaultReceipts bool
|
||||
wantMessage string
|
||||
}{
|
||||
{
|
||||
name: "wrong version",
|
||||
@@ -140,6 +148,20 @@ func TestPrepareSVGlidePublishSpecValidationErrors(t *testing.T) {
|
||||
pageFiles: map[string]string{"page.svg": `<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540"></svg>`, "receipts/validate_svg_deck.json": `{"totalErrors":1}`},
|
||||
wantMessage: "--manifest validate_svg_deck receipt must have totalErrors=0",
|
||||
},
|
||||
{
|
||||
name: "missing parser contract receipt",
|
||||
manifest: `{"version":"svglide.manifest.v1","protocol":"svg-slides.v1","title":"x","size":{"width":960,"height":540},"publish_ready":true,"published":false,"receipts":{"validate_svg_deck":"receipts/validate_svg_deck.json","prompt_read_trace":"receipts/prompt_read_trace.json","asset_readiness":"receipts/asset_readiness.json","typography_plan":"receipts/typography_plan.json","frame_discipline":"receipts/frame_discipline.json"},"pages":[{"id":"p1","index":1,"file":"page.svg","sha256":"<valid-sha>"}]}`,
|
||||
pageFiles: map[string]string{
|
||||
"page.svg": `<svg xmlns="http://www.w3.org/2000/svg" width="960" height="540"></svg>`,
|
||||
"receipts/validate_svg_deck.json": `{"totalErrors":0}`,
|
||||
"receipts/prompt_read_trace.json": `{"ok":true}`,
|
||||
"receipts/asset_readiness.json": `{"ok":true}`,
|
||||
"receipts/typography_plan.json": `{"ok":true}`,
|
||||
"receipts/frame_discipline.json": `{"ok":true,"totalErrors":0}`,
|
||||
},
|
||||
skipDefaultReceipts: true,
|
||||
wantMessage: "--manifest receipts.parser_contract is required",
|
||||
},
|
||||
{
|
||||
name: "sha mismatch",
|
||||
manifest: `{"version":"svglide.manifest.v1","protocol":"svg-slides.v1","title":"x","size":{"width":960,"height":540},"publish_ready":true,"receipts":{"validate_svg_deck":"receipts/validate_svg_deck.json"},"pages":[{"id":"p1","index":1,"file":"page.svg","sha256":"bad"}]}`,
|
||||
@@ -161,6 +183,10 @@ func TestPrepareSVGlidePublishSpecValidationErrors(t *testing.T) {
|
||||
for path, body := range tt.pageFiles {
|
||||
mustWriteSVGlideTestFile(t, path, body)
|
||||
}
|
||||
if !tt.skipDefaultReceipts {
|
||||
mustWriteSVGlideRequiredNonValidateReceipts(t)
|
||||
tt.manifest = withSVGlideRequiredReceipts(tt.manifest)
|
||||
}
|
||||
if body, ok := tt.pageFiles["page.svg"]; ok {
|
||||
tt.manifest = strings.ReplaceAll(tt.manifest, "<valid-sha>", sha256Hex([]byte(body)))
|
||||
}
|
||||
@@ -194,6 +220,29 @@ func mustWriteSVGlideTestFile(t *testing.T, path string, body string) {
|
||||
}
|
||||
}
|
||||
|
||||
func withSVGlideRequiredReceipts(manifest string) string {
|
||||
manifest = strings.ReplaceAll(manifest, `"receipts":{`+svglideValidateOnlyReceiptsJSON+`}`, `"receipts":{`+svglideRequiredReceiptsJSON+`}`)
|
||||
manifest = strings.ReplaceAll(manifest, `"receipts":{"validate_svg_deck":"receipts/validate_svg_deck.json"}`, `"receipts":{`+svglideRequiredReceiptsJSON+`}`)
|
||||
manifest = strings.ReplaceAll(manifest, `"receipts": {"validate_svg_deck": "receipts/validate_svg_deck.json"}`, `"receipts": {`+svglideRequiredReceiptsJSON+`}`)
|
||||
return strings.ReplaceAll(manifest, `"receipts": {`+svglideValidateOnlyReceiptsJSON+`}`, `"receipts": {`+svglideRequiredReceiptsJSON+`}`)
|
||||
}
|
||||
|
||||
func mustWriteSVGlideRequiredNonValidateReceipts(t *testing.T) {
|
||||
t.Helper()
|
||||
for path, body := range map[string]string{
|
||||
"receipts/prompt_read_trace.json": `{"ok":true}`,
|
||||
"receipts/asset_readiness.json": `{"ok":true}`,
|
||||
"receipts/typography_plan.json": `{"ok":true}`,
|
||||
"receipts/frame_discipline.json": `{"ok":true,"totalErrors":0}`,
|
||||
"receipts/parser_contract.json": `{"ok":true,"totalErrors":0}`,
|
||||
} {
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
continue
|
||||
}
|
||||
mustWriteSVGlideTestFile(t, path, body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanSVGlideImageRewritesResolvesAndDedupes(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
withSlidesTestWorkingDir(t, dir)
|
||||
@@ -415,7 +464,8 @@ func mustWriteSVGlideBundle(t *testing.T, title string, page string) {
|
||||
t.Helper()
|
||||
mustWriteSVGlideTestFile(t, "pages/page-001.svg", page)
|
||||
mustWriteSVGlideTestFile(t, "receipts/validate_svg_deck.json", `{"totalErrors":0}`)
|
||||
mustWriteSVGlideTestFile(t, "manifest.json", fmt.Sprintf(`{
|
||||
mustWriteSVGlideRequiredNonValidateReceipts(t)
|
||||
mustWriteSVGlideTestFile(t, "manifest.json", withSVGlideRequiredReceipts(fmt.Sprintf(`{
|
||||
"version": "svglide.manifest.v1",
|
||||
"protocol": "svg-slides.v1",
|
||||
"title": %q,
|
||||
@@ -424,7 +474,7 @@ func mustWriteSVGlideBundle(t *testing.T, title string, page string) {
|
||||
"published": false,
|
||||
"pages": [{"id": "page-001", "index": 1, "file": "pages/page-001.svg", "sha256": %q}],
|
||||
"receipts": {"validate_svg_deck": "receipts/validate_svg_deck.json"}
|
||||
}`, title, sha256Hex([]byte(page))))
|
||||
}`, title, sha256Hex([]byte(page)))))
|
||||
}
|
||||
|
||||
func newSVGlideTestRuntime(t *testing.T) (*common.RuntimeContext, *httpmock.Registry) {
|
||||
|
||||
@@ -60,6 +60,13 @@ if (!validate.stdout.trim()) {
|
||||
const receipt = JSON.parse(validate.stdout);
|
||||
fs.mkdirSync(path.join(root, "receipts"), { recursive: true });
|
||||
fs.writeFileSync(path.join(root, "receipts", "validate_svg_deck.json"), `${JSON.stringify(receipt, null, 2)}\n`);
|
||||
const parserContractReceipt = {
|
||||
version: "svg-slides.parser-contract.v1",
|
||||
ok: receipt.totalErrors === 0,
|
||||
source: "validate_svg_deck.mjs",
|
||||
totalErrors: receipt.totalErrors,
|
||||
};
|
||||
fs.writeFileSync(path.join(root, "receipts", "parser_contract.json"), `${JSON.stringify(parserContractReceipt, null, 2)}\n`);
|
||||
if (receipt.totalErrors !== 0) {
|
||||
fail(`SVG deck is not publish-ready: ${receipt.totalErrors} validation error(s)`, 1);
|
||||
}
|
||||
@@ -141,7 +148,8 @@ const manifest = {
|
||||
prompt_read_trace: "receipts/prompt_read_trace.json",
|
||||
asset_readiness: "receipts/asset_readiness.json",
|
||||
typography_plan: "receipts/typography_plan.json",
|
||||
frame_discipline: "receipts/frame_discipline.json"
|
||||
frame_discipline: "receipts/frame_discipline.json",
|
||||
parser_contract: "receipts/parser_contract.json"
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -172,11 +172,15 @@ test("bundle builder writes manifest and validation receipt", () => {
|
||||
assert.equal(manifest.receipts.asset_readiness, "receipts/asset_readiness.json");
|
||||
assert.equal(manifest.receipts.typography_plan, "receipts/typography_plan.json");
|
||||
assert.equal(manifest.receipts.frame_discipline, "receipts/frame_discipline.json");
|
||||
assert.equal(manifest.receipts.parser_contract, "receipts/parser_contract.json");
|
||||
assert.equal(manifest.pages.length, 1);
|
||||
assert.match(manifest.pages[0].sha256, /^[a-f0-9]{64}$/);
|
||||
const receipt = JSON.parse(fs.readFileSync(path.join(root, "receipts", "validate_svg_deck.json"), "utf8"));
|
||||
assert.equal(receipt.totalErrors, 0);
|
||||
assert.equal(fs.existsSync(path.join(root, "receipts", "frame_discipline.json")), true);
|
||||
const parserContract = JSON.parse(fs.readFileSync(path.join(root, "receipts", "parser_contract.json"), "utf8"));
|
||||
assert.equal(parserContract.totalErrors, 0);
|
||||
assert.equal(parserContract.ok, true);
|
||||
});
|
||||
|
||||
test("bundle builder rejects missing asset readiness receipt", () => {
|
||||
@@ -327,4 +331,8 @@ test("bundle builder accepts chart deck with deck and chart prompt reads", () =>
|
||||
assert.equal(manifest.receipts.asset_readiness, "receipts/asset_readiness.json");
|
||||
assert.equal(manifest.receipts.typography_plan, "receipts/typography_plan.json");
|
||||
assert.equal(manifest.receipts.frame_discipline, "receipts/frame_discipline.json");
|
||||
assert.equal(manifest.receipts.parser_contract, "receipts/parser_contract.json");
|
||||
const parserContract = JSON.parse(fs.readFileSync(path.join(root, "receipts", "parser_contract.json"), "utf8"));
|
||||
assert.equal(parserContract.totalErrors, 0);
|
||||
assert.equal(parserContract.ok, true);
|
||||
});
|
||||
|
||||
@@ -43,6 +43,11 @@ func prepareSVGlideImageSmokeBundle(t *testing.T, title string) string {
|
||||
|
||||
validateOut := runSVGlideNodeTool(t, root, "skills/lark-slides/scripts/validate_svg_deck.mjs", dir, "--json")
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "receipts", "validate_svg_deck.json"), validateOut, 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "receipts", "prompt_read_trace.json"), []byte(`{"ok":true}`), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "receipts", "asset_readiness.json"), []byte(`{"ok":true}`), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "receipts", "typography_plan.json"), []byte(`{"ok":true}`), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "receipts", "frame_discipline.json"), []byte(`{"ok":true,"totalErrors":0}`), 0o644))
|
||||
require.NoError(t, os.WriteFile(filepath.Join(dir, "receipts", "parser_contract.json"), []byte(`{"ok":true,"totalErrors":0}`), 0o644))
|
||||
|
||||
manifest := map[string]any{
|
||||
"version": "svglide.manifest.v1",
|
||||
@@ -57,7 +62,14 @@ func prepareSVGlideImageSmokeBundle(t *testing.T, title string) string {
|
||||
"file": "slides/page-001.svg",
|
||||
"sha256": sha256HexForE2E([]byte(page)),
|
||||
}},
|
||||
"receipts": map[string]any{"validate_svg_deck": "receipts/validate_svg_deck.json"},
|
||||
"receipts": map[string]any{
|
||||
"validate_svg_deck": "receipts/validate_svg_deck.json",
|
||||
"prompt_read_trace": "receipts/prompt_read_trace.json",
|
||||
"asset_readiness": "receipts/asset_readiness.json",
|
||||
"typography_plan": "receipts/typography_plan.json",
|
||||
"frame_discipline": "receipts/frame_discipline.json",
|
||||
"parser_contract": "receipts/parser_contract.json",
|
||||
},
|
||||
}
|
||||
raw, err := json.MarshalIndent(manifest, "", " ")
|
||||
require.NoError(t, err)
|
||||
|
||||
Reference in New Issue
Block a user