Reject unversioned party documents
This commit is contained in:
@@ -227,8 +227,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline.yml: %v", err)
|
||||
@@ -292,8 +290,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline.yml: %v", err)
|
||||
@@ -389,14 +385,13 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`)
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "speakers.yml"), "match:\n - speaker: Alice\n match: [\"alice\"]\n")
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "glossary.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "players.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "party.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(otherDir, "party.yml"), "schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n")
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
@@ -522,7 +517,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
|
||||
@@ -542,8 +536,7 @@ inputs:
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "speakers.yml"), "match:\n - speaker: Alice\n match: [\"alice\"]\n")
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "glossary.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "players.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "party.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(campaignDir, "party.yml"), "schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes")
|
||||
|
||||
return pipelinePath, campaignPath, sessionPath
|
||||
@@ -568,13 +561,12 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||
t.Fatalf("write campaign.yml: %v", err)
|
||||
}
|
||||
mustWriteTestFile(t, filepath.Join(dir, "party.yml"), "legacy: party\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "party.yml"), "schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n")
|
||||
return campaignPath
|
||||
}
|
||||
|
||||
|
||||
@@ -400,12 +400,12 @@ func TestConfigSourcesReportsStableOwnership(t *testing.T) {
|
||||
"imports: none",
|
||||
"profile: name=testing selection=cli overlay=",
|
||||
"campaign: id=sample-campaign source=",
|
||||
"party: mode=legacy source=",
|
||||
"party: mode=canonical source=",
|
||||
"digest: ",
|
||||
"path\trole\tsource\n",
|
||||
"workspace.root\troot\t",
|
||||
"whisperx.language\tprofile\t",
|
||||
"campaign.inputs.players_file\tlegacy_player\t",
|
||||
"derived.players\tparty\t",
|
||||
"trim.enabled\tdefault\tdefault",
|
||||
} {
|
||||
if !strings.Contains(output, want) {
|
||||
|
||||
@@ -210,7 +210,7 @@ func TestExtractLifecyclePrepareBindsVerifiedReferenceSnapshots(t *testing.T) {
|
||||
|
||||
func TestExtractLifecyclePreparedReferenceChangeRerunsExtractionAndInvalidatesDownstream(t *testing.T) {
|
||||
cfg, env, runner := extractionLifecycleFixture(t, true)
|
||||
originalPaths := configureLifecycleReferences(t, cfg)
|
||||
configureLifecycleReferences(t, cfg)
|
||||
if _, err := executeStages(context.Background(), cfg, prepareExtractLifecyclePlan(t), RunOptions{Env: env}); err != nil {
|
||||
t.Fatalf("initial executeStages() error = %v", err)
|
||||
}
|
||||
@@ -222,9 +222,18 @@ func TestExtractLifecyclePreparedReferenceChangeRerunsExtractionAndInvalidatesDo
|
||||
if err := (&manifest.LocalStore{}).Save(context.Background(), manifestPathFor(cfg), before); err != nil {
|
||||
t.Fatalf("Save(downstream success) error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(originalPaths[artifactpolicy.SourceInputParty], []byte("changed party bytes\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(party source) error = %v", err)
|
||||
partyDocument, err := config.ParseParty([]byte(`schema_version: narratio.party.v1
|
||||
characters:
|
||||
changed:
|
||||
player: {name: Eric}
|
||||
character:
|
||||
name: Changed
|
||||
classes: [{name: wizard}]
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatalf("ParseParty(changed party) error = %v", err)
|
||||
}
|
||||
cfg.Party.Canonical = partyDocument.Canonical
|
||||
|
||||
prepared := loadLifecycleManifest(t, cfg)
|
||||
prepare, err := stage.Select("prepare")
|
||||
@@ -266,17 +275,22 @@ func TestExtractLifecyclePreparedReferenceChangeRerunsExtractionAndInvalidatesDo
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractLifecycleSessionOverrideBytesReachCanonicalReference(t *testing.T) {
|
||||
func TestExtractLifecycleCanonicalPartyBytesReachCanonicalReference(t *testing.T) {
|
||||
cfg, env, runner := extractionLifecycleFixture(t, true)
|
||||
configureLifecycleReferences(t, cfg)
|
||||
overridePath := filepath.Join(filepath.Dir(cfg.SessionPath), "session-party.yml")
|
||||
if err := os.WriteFile(overridePath, []byte("session override party\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile(session override) error = %v", err)
|
||||
partyBytes := []byte(`schema_version: narratio.party.v1
|
||||
characters:
|
||||
session:
|
||||
player: {name: Eric}
|
||||
character:
|
||||
name: Session
|
||||
classes: [{name: wizard}]
|
||||
`)
|
||||
partyDocument, err := config.ParseParty(partyBytes)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseParty(canonical party) error = %v", err)
|
||||
}
|
||||
cfg.StableInputs.PartyFile = config.ResolvedInputFile{
|
||||
Path: "./session-party.yml", ConfigPath: cfg.SessionPath, Source: "session_config",
|
||||
}
|
||||
cfg.Session.Inputs.PartyFile = "./session-party.yml"
|
||||
cfg.Party.Canonical = partyDocument.Canonical
|
||||
|
||||
if _, err := executeStages(context.Background(), cfg, prepareExtractLifecyclePlan(t), RunOptions{Env: env}); err != nil {
|
||||
t.Fatalf("executeStages() error = %v", err)
|
||||
@@ -294,8 +308,8 @@ func TestExtractLifecycleSessionOverrideBytesReachCanonicalReference(t *testing.
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(prepared party) error = %v", err)
|
||||
}
|
||||
if string(contents) != "session override party\n" || partyPath == overridePath {
|
||||
t.Fatalf("prepared party path=%q contents=%q override=%q", partyPath, contents, overridePath)
|
||||
if string(contents) != string(partyBytes) || partyPath == cfg.Party.Source.Path {
|
||||
t.Fatalf("prepared party path=%q contents=%q source=%q", partyPath, contents, cfg.Party.Source.Path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -113,7 +113,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`), 0o644); err != nil {
|
||||
t.Fatalf("write explicit campaign: %v", err)
|
||||
@@ -122,7 +121,7 @@ inputs:
|
||||
mustWriteTestFile(t, filepath.Join(explicitDir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(explicitDir, "glossary.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(explicitDir, "players.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(explicitDir, "party.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(explicitDir, "party.yml"), "schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n")
|
||||
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
|
||||
@@ -66,6 +66,10 @@ func inspectStableInputs(cfg *config.Config) []stableInputCheck {
|
||||
}
|
||||
out := make([]stableInputCheck, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.name == "players" && item.in.Source == "derived_from_party" {
|
||||
out = append(out, stableInputCheck{Name: item.name, Path: "derived from canonical party"})
|
||||
continue
|
||||
}
|
||||
if item.optional && strings.TrimSpace(item.in.Path) == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(campaignPath, []byte(campaign), 0o644); err != nil {
|
||||
|
||||
@@ -117,8 +117,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline.yml: %v", err)
|
||||
|
||||
@@ -436,8 +436,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||
t.Fatalf("write pipeline config: %v", err)
|
||||
@@ -448,8 +446,7 @@ inputs:
|
||||
mustWriteTestFile(t, filepath.Join(dir, "speakers.yml"), "alice: alice.flac\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "glossary.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "players.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "party.yml"), "[]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "party.yml"), "schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n")
|
||||
mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes")
|
||||
|
||||
return pipelinePath, campaignPath, sessionPath
|
||||
|
||||
@@ -1316,14 +1316,17 @@ func testConfig(t *testing.T) *config.Config {
|
||||
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
||||
|
||||
mustWriteFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
||||
mustWriteFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n")
|
||||
mustWriteFile(t, campaignPath, "campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n")
|
||||
mustWriteFile(t, sessionPath, "session_id: 2026-05-03\ncampaign: sample-campaign\ninputs:\n audio_dir: ./audio\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "players.yml"), "[]\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "party.yml"), "[]\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "party.yml"), "schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n")
|
||||
mustWriteFile(t, filepath.Join(cfgDir, "audio", "alice.flac"), "audio")
|
||||
partyDocument, err := config.ParseParty([]byte("schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("parse canonical party: %v", err)
|
||||
}
|
||||
|
||||
return &config.Config{
|
||||
Pipeline: &config.PipelineConfig{Workspace: config.WorkspaceConfig{Root: workspace}},
|
||||
@@ -1331,6 +1334,15 @@ func testConfig(t *testing.T) *config.Config {
|
||||
PipelinePath: pipelinePath,
|
||||
CampaignPath: campaignPath,
|
||||
SessionPath: sessionPath,
|
||||
Party: config.ResolvedParty{
|
||||
Mode: config.PartyModeCanonical,
|
||||
Canonical: partyDocument.Canonical,
|
||||
Source: config.PartySource{
|
||||
Path: filepath.Join(cfgDir, "party.yml"),
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
},
|
||||
StableInputs: config.ResolvedStableInputs{
|
||||
SpeakersFile: config.ResolvedInputFile{
|
||||
Path: "./speakers.yml",
|
||||
@@ -1347,11 +1359,7 @@ func testConfig(t *testing.T) *config.Config {
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
PlayersFile: config.ResolvedInputFile{
|
||||
Path: "./players.yml",
|
||||
ConfigPath: campaignPath,
|
||||
Source: "campaign_config",
|
||||
},
|
||||
PlayersFile: config.ResolvedInputFile{Source: "derived_from_party"},
|
||||
PartyFile: config.ResolvedInputFile{
|
||||
Path: "./party.yml",
|
||||
ConfigPath: campaignPath,
|
||||
@@ -1366,8 +1374,6 @@ func testConfig(t *testing.T) *config.Config {
|
||||
SpeakersFile: "./speakers.yml",
|
||||
AutocorrectFile: "./autocorrect.yml",
|
||||
GlossaryFile: "./glossary.yml",
|
||||
PlayersFile: "./players.yml",
|
||||
PartyFile: "./party.yml",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1390,7 +1396,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
sessionYAML := `session_id: 2026-05-03
|
||||
@@ -1400,13 +1405,11 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
mustWriteFile(t, pipelinePath, pipelineYAML)
|
||||
mustWriteFile(t, campaignPath, campaignYAML)
|
||||
mustWriteFile(t, sessionPath, sessionYAML)
|
||||
mustWriteFile(t, filepath.Join(dir, "party.yml"), "legacy: party\n")
|
||||
mustWriteFile(t, filepath.Join(dir, "party.yml"), "schema_version: narratio.party.v1\ncharacters:\n arannis:\n player: {name: Eric}\n character:\n name: Arannis\n classes: [{name: wizard}]\n")
|
||||
|
||||
cfg, err := config.Load(pipelinePath, sessionPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,8 +22,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionTemplate), 0o644); err != nil {
|
||||
t.Fatalf("write session template: %v", err)
|
||||
@@ -77,8 +75,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
|
||||
@@ -123,7 +123,7 @@ func TestArtifactFamiliesRejectInvalidExpansionDeclarations(t *testing.T) {
|
||||
families string
|
||||
wantError string
|
||||
}{
|
||||
{name: "legacy party", party: "legacy: party\n", families: validFamilyYAML, wantError: "requires a canonical campaign party"},
|
||||
{name: "unversioned party", party: "characters: {}\n", families: validFamilyYAML, wantError: "schema_version is required"},
|
||||
{name: "invalid iteration", party: canonicalPartyFixture, families: strings.Replace(validFamilyYAML, "party.characters", "party.players", 1), wantError: ".for_each must be"},
|
||||
{name: "invalid token", party: canonicalPartyFixture, families: strings.Replace(validFamilyYAML, "{character_id}", "{character_name}", 1), wantError: "must contain exactly one"},
|
||||
{name: "repeated token", party: canonicalPartyFixture, families: strings.Replace(validFamilyYAML, "meta.md", "{character_id}.md", 1), wantError: "must contain exactly one"},
|
||||
|
||||
@@ -37,7 +37,7 @@ notification:
|
||||
|
||||
func TestCampaignStrictDecodeAcceptsCampaignID(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestCampaignStrictDecodeAcceptsCampaignID(t *testing.T) {
|
||||
|
||||
func TestCampaignStrictDecodeRejectsLegacyCampaignField(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestCampaignStrictDecodeRejectsLegacyCampaignField(t *testing.T) {
|
||||
|
||||
func TestCampaignStrictDecodeRejectsUnknownFields(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\nunknown: true\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"campaign_id: sample-campaign\nunknown: true\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -82,7 +82,7 @@ func TestCampaignStrictDecodeRejectsUnknownFields(t *testing.T) {
|
||||
|
||||
func TestCampaignStrictDecodeAcceptsSessionTemplateFile(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\nsession_template_file: ./session.template.yml\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"campaign_id: sample-campaign\nsession_template_file: ./session.template.yml\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -97,7 +97,7 @@ func TestCampaignStrictDecodeAcceptsSessionTemplateFile(t *testing.T) {
|
||||
|
||||
func TestCampaignSessionMergeFillsStableInputs(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n players_file: ./campaign-players.yml\n party_file: ./campaign-party.yml\n",
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n party_file: ./campaign-party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -114,14 +114,14 @@ func TestCampaignSessionMergeFillsStableInputs(t *testing.T) {
|
||||
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./campaign-speakers.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PlayersFile, "./campaign-players.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PlayersFile, "", "", "derived_from_party")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./campaign-party.yml", campaignPath, "campaign_config")
|
||||
}
|
||||
|
||||
func TestCampaignSessionMergeSessionOverridesStableInputs(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n players_file: ./campaign-players.yml\n party_file: ./campaign-party.yml\n",
|
||||
"session_id: 2026-05-03\ncampaign: sample-campaign\ninputs:\n audio_dir: ./audio\n speakers_file: ./session-speakers.yml\n players_file: ./session-players.yml\n party_file: ./session-party.yml\n",
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n party_file: ./campaign-party.yml\n",
|
||||
"session_id: 2026-05-03\ncampaign: sample-campaign\ninputs:\n audio_dir: ./audio\n speakers_file: ./session-speakers.yml\n",
|
||||
)
|
||||
|
||||
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
@@ -134,8 +134,8 @@ func TestCampaignSessionMergeSessionOverridesStableInputs(t *testing.T) {
|
||||
assertResolvedStableInput(t, cfg.StableInputs.SpeakersFile, "./session-speakers.yml", sessionPath, "session_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.AutocorrectFile, "./campaign-autocorrect.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.GlossaryFile, "./campaign-glossary.yml", campaignPath, "campaign_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PlayersFile, "./session-players.yml", sessionPath, "session_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./session-party.yml", sessionPath, "session_config")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PlayersFile, "", "", "derived_from_party")
|
||||
assertResolvedStableInput(t, cfg.StableInputs.PartyFile, "./campaign-party.yml", campaignPath, "campaign_config")
|
||||
}
|
||||
|
||||
func TestCampaignSessionMergeSpellCatalog(t *testing.T) {
|
||||
@@ -162,7 +162,7 @@ func TestCampaignSessionMergeSpellCatalog(t *testing.T) {
|
||||
sessionSpell = " spell_catalog_file: " + tt.sessionValue + "\n"
|
||||
}
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n"+campaignSpell,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n"+campaignSpell,
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n"+sessionSpell,
|
||||
)
|
||||
|
||||
@@ -195,7 +195,7 @@ func TestCampaignSessionMergeRejectsWhitespaceSpellCatalog(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n"+tt.campaignLine,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n"+tt.campaignLine,
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n"+tt.sessionLine,
|
||||
)
|
||||
_, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
@@ -206,24 +206,20 @@ func TestCampaignSessionMergeRejectsWhitespaceSpellCatalog(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyCampaignRequiresPlayersInput(t *testing.T) {
|
||||
func TestCampaignWithCanonicalPartyDoesNotRequirePlayersInput(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
_, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
if err == nil {
|
||||
t.Fatal("expected load error, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "players_file is required with a legacy party") {
|
||||
t.Fatalf("error = %q, want players_file required", err.Error())
|
||||
if _, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{}); err != nil {
|
||||
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCampaignSessionMismatchFails(t *testing.T) {
|
||||
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ncampaign: other-campaign\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
|
||||
@@ -238,7 +234,7 @@ func TestCampaignSessionMismatchFails(t *testing.T) {
|
||||
|
||||
func TestLoadMissingCampaignFileFails(t *testing.T) {
|
||||
pipelinePath, _, sessionPath := writeCampaignConfigTestFiles(t,
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n players_file: ./players.yml\n party_file: ./party.yml\n",
|
||||
"campaign_id: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n party_file: ./party.yml\n",
|
||||
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||
)
|
||||
missingCampaignPath := filepath.Join(filepath.Dir(sessionPath), "missing-campaign.yml")
|
||||
@@ -271,7 +267,7 @@ func writeCampaignConfigTestFiles(t *testing.T, campaignYAML, sessionYAML string
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
}
|
||||
for _, name := range []string{"party.yml", "campaign-party.yml", "session-party.yml"} {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte("legacy: party\n"), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(canonicalPartyFixture), 0o644); err != nil {
|
||||
t.Fatalf("write %s: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,7 +327,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||
@@ -336,7 +335,7 @@ inputs:
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
t.Fatalf("write session.yml: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "party.yml"), []byte("legacy: party\n"), 0o644); err != nil {
|
||||
if err := os.WriteFile(filepath.Join(dir, "party.yml"), []byte(canonicalPartyFixture), 0o644); err != nil {
|
||||
t.Fatalf("write party.yml: %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -83,17 +83,12 @@ type PartyClass struct {
|
||||
Level *int
|
||||
}
|
||||
|
||||
// ParseParty classifies and, when selected by schema_version, strictly parses
|
||||
// a party source. An unversioned source takes the deliberately opaque legacy
|
||||
// compatibility path; see party_legacy.go.
|
||||
// ParseParty strictly parses one supported party document.
|
||||
func ParseParty(data []byte) (*PartyDocument, error) {
|
||||
root, decoder, err := parsePartyFirstDocument(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !partyDocumentSelectsCanonical(root) {
|
||||
return classifyLegacyPartyDocument(), nil
|
||||
}
|
||||
if err := requireNoTrailingPartyDocuments(decoder); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -119,25 +114,12 @@ func parsePartyFirstDocument(data []byte) (*yaml.Node, *yaml.Decoder, error) {
|
||||
return document.Content[0], decoder, nil
|
||||
}
|
||||
|
||||
func partyDocumentSelectsCanonical(root *yaml.Node) bool {
|
||||
if root == nil || root.Kind != yaml.MappingNode {
|
||||
return false
|
||||
}
|
||||
for index := 0; index+1 < len(root.Content); index += 2 {
|
||||
key := root.Content[index]
|
||||
if key.Kind == yaml.ScalarNode && key.Tag == "!!str" && key.Value == "schema_version" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func requireNoTrailingPartyDocuments(decoder *yaml.Decoder) error {
|
||||
var trailing yaml.Node
|
||||
if err := decoder.Decode(&trailing); err == nil {
|
||||
return fmt.Errorf("canonical party must contain exactly one YAML document")
|
||||
return fmt.Errorf("party must contain exactly one YAML document")
|
||||
} else if err != io.EOF {
|
||||
return fmt.Errorf("decode trailing canonical party YAML: %w", err)
|
||||
return fmt.Errorf("decode trailing party YAML: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -147,17 +129,21 @@ func parseCanonicalParty(root *yaml.Node, raw []byte) (*CanonicalParty, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version, ok := fields["schema_version"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("party.schema_version is required; supported schema_version is %q", PartySchemaVersion)
|
||||
}
|
||||
schemaVersion, err := partyRequiredString(map[string]*yaml.Node{"schema_version": version}, "schema_version", "party")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("party.schema_version must be a non-empty string; supported schema_version is %q", PartySchemaVersion)
|
||||
}
|
||||
if schemaVersion != PartySchemaVersion {
|
||||
return nil, fmt.Errorf("party.schema_version %q is unsupported; supported schema_version is %q", schemaVersion, PartySchemaVersion)
|
||||
}
|
||||
if err := partyKnownFields(fields, "party", "schema_version", "characters"); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
version, err := partyRequiredString(fields, "schema_version", "party")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if version != PartySchemaVersion {
|
||||
return nil, fmt.Errorf("party.schema_version %q is unsupported", version)
|
||||
}
|
||||
charactersNode, ok := fields["characters"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("party.characters is required")
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package config
|
||||
|
||||
// classifyLegacyPartyDocument is the bounded compatibility path for an
|
||||
// unversioned party source. It intentionally retains no parsed roster data and
|
||||
// must be removed once legacy campaign inputs are no longer supported.
|
||||
func classifyLegacyPartyDocument() *PartyDocument {
|
||||
return &PartyDocument{Mode: PartyModeLegacy}
|
||||
}
|
||||
|
||||
// resolveLegacyParty preserves the deliberately opaque legacy party mode.
|
||||
// This small compatibility surface is intended for removal once legacy
|
||||
// campaign inputs are no longer supported.
|
||||
func resolveLegacyParty(resolved ResolvedParty) (ResolvedParty, error) {
|
||||
resolved.Mode = PartyModeLegacy
|
||||
resolved.Canonical = nil
|
||||
return resolved, nil
|
||||
}
|
||||
@@ -32,19 +32,15 @@ func resolvePartyInput(input ResolvedInputFile) (ResolvedParty, error) {
|
||||
if err != nil {
|
||||
return ResolvedParty{}, fmt.Errorf("parse party input %q: %w", path, err)
|
||||
}
|
||||
resolved := ResolvedParty{
|
||||
Mode: document.Mode,
|
||||
return ResolvedParty{
|
||||
Mode: PartyModeCanonical,
|
||||
Source: PartySource{
|
||||
Path: path,
|
||||
ConfigPath: configPath,
|
||||
Source: input.Source,
|
||||
},
|
||||
Canonical: document.Canonical,
|
||||
}
|
||||
if document.IsCanonical() {
|
||||
return resolved, nil
|
||||
}
|
||||
return resolveLegacyParty(resolved)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func campaignPartyInput(campaignPath string, campaign *CampaignConfig) (ResolvedInputFile, error) {
|
||||
|
||||
@@ -85,44 +85,7 @@ func TestCanonicalPartyRejectsSeparatePlayersAndSessionPartyOverrides(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyPartyPreservesCampaignAndSessionInputOverrides(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePartyResolutionFile(t, filepath.Join(dir, "party.yml"), "legacy: campaign\n")
|
||||
writePartyResolutionFile(t, filepath.Join(dir, "session-party.yml"), "legacy: session\n")
|
||||
pipelinePath, campaignPath, sessionPath := writePartyResolutionConfig(t, dir, `campaign_id: campaign
|
||||
inputs:
|
||||
speakers_file: speakers.yml
|
||||
autocorrect_file: autocorrect.yml
|
||||
glossary_file: glossary.yml
|
||||
players_file: campaign-players.yml
|
||||
party_file: party.yml
|
||||
`, `session_id: session
|
||||
campaign: campaign
|
||||
inputs:
|
||||
audio_dir: audio
|
||||
players_file: session-players.yml
|
||||
party_file: session-party.yml
|
||||
`)
|
||||
|
||||
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
||||
}
|
||||
if err := Validate(cfg); err != nil {
|
||||
t.Fatalf("Validate() error = %v", err)
|
||||
}
|
||||
if cfg.Party.Mode != PartyModeLegacy || cfg.Party.Canonical != nil {
|
||||
t.Fatalf("party = %#v, want opaque legacy party", cfg.Party)
|
||||
}
|
||||
if cfg.Party.Source.Source != "session_config" || filepath.Base(cfg.Party.Source.Path) != "session-party.yml" {
|
||||
t.Fatalf("party source = %#v, want session override", cfg.Party.Source)
|
||||
}
|
||||
if got := cfg.StableInputs.PlayersFile; got.Path != "session-players.yml" || got.Source != "session_config" {
|
||||
t.Fatalf("players input = %#v, want session override", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyPartyAcceptsAnEffectiveSessionPlayersOverride(t *testing.T) {
|
||||
func TestUnversionedPartyIsRejectedBeforeResolution(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
writePartyResolutionFile(t, filepath.Join(dir, "party.yml"), "legacy: campaign\n")
|
||||
pipelinePath, campaignPath, sessionPath := writePartyResolutionConfig(t, dir, `campaign_id: campaign
|
||||
@@ -135,49 +98,10 @@ inputs:
|
||||
campaign: campaign
|
||||
inputs:
|
||||
audio_dir: audio
|
||||
players_file: session-players.yml
|
||||
`)
|
||||
cfg, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
if err != nil {
|
||||
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
||||
}
|
||||
if got := cfg.StableInputs.PlayersFile; got.Path != "session-players.yml" || got.Source != "session_config" {
|
||||
t.Fatalf("players input = %#v, want effective session override", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLegacyPartyRequiresPlayersAndPartyMustBeReadableRegularFile(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
partySetup func(t *testing.T, dir string)
|
||||
partyFile string
|
||||
playersFile string
|
||||
want string
|
||||
}{
|
||||
{name: "missing players", partySetup: func(t *testing.T, dir string) {
|
||||
writePartyResolutionFile(t, filepath.Join(dir, "party.yml"), "legacy: party\n")
|
||||
}, partyFile: "party.yml", want: "players_file is required with a legacy party"},
|
||||
{name: "missing party file", partySetup: func(t *testing.T, dir string) {}, partyFile: "missing.yml", playersFile: "players.yml", want: "read party input"},
|
||||
{name: "non-regular party file", partySetup: func(t *testing.T, dir string) {
|
||||
if err := os.Mkdir(filepath.Join(dir, "party-dir"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}, partyFile: "party-dir", playersFile: "players.yml", want: "read party input"},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
test.partySetup(t, dir)
|
||||
players := ""
|
||||
if test.playersFile != "" {
|
||||
players = " players_file: " + test.playersFile + "\n"
|
||||
}
|
||||
pipelinePath, campaignPath, sessionPath := writePartyResolutionConfig(t, dir, "campaign_id: campaign\ninputs:\n speakers_file: speakers.yml\n autocorrect_file: autocorrect.yml\n glossary_file: glossary.yml\n"+players+" party_file: "+test.partyFile+"\n", "session_id: session\ncampaign: campaign\ninputs:\n audio_dir: audio\n")
|
||||
_, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||
if err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("LoadWithSessionOptions() error = %v, want %q", err, test.want)
|
||||
}
|
||||
})
|
||||
if _, err := LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{}); err == nil || !strings.Contains(err.Error(), "schema_version is required") {
|
||||
t.Fatalf("LoadWithSessionOptions() error = %v, want schema rejection before resolution", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -210,13 +210,20 @@ characters:
|
||||
}
|
||||
}
|
||||
|
||||
func TestParsePartyClassifiesUnversionedDocumentAsLegacy(t *testing.T) {
|
||||
document, err := ParseParty([]byte("players:\n - not canonical\n---\nlegacy: remains opaque\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("ParseParty() error = %v", err)
|
||||
}
|
||||
if document.Mode != PartyModeLegacy || document.Canonical != nil || document.IsCanonical() {
|
||||
t.Fatalf("document = %#v, want opaque legacy classification", document)
|
||||
func TestParsePartyRejectsMissingOrUnknownSchemaVersion(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
yaml string
|
||||
want string
|
||||
}{
|
||||
{name: "missing", yaml: "characters: {}\n", want: "schema_version is required"},
|
||||
{name: "unknown", yaml: "schema_version: narratio.party.v2\ncharacters: {}\n", want: "supported schema_version is \"narratio.party.v1\""},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if _, err := ParseParty([]byte(test.yaml)); err == nil || !strings.Contains(err.Error(), test.want) {
|
||||
t.Fatalf("ParseParty() error = %v, want %q", err, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -276,11 +283,4 @@ func TestCanonicalPartyPlayersYAMLRequiresCanonicalParty(t *testing.T) {
|
||||
if _, err := party.PlayersYAML(); err == nil {
|
||||
t.Fatal("PlayersYAML() error = nil, want canonical party requirement")
|
||||
}
|
||||
legacy, err := ParseParty([]byte("legacy: party\n"))
|
||||
if err != nil {
|
||||
t.Fatalf("ParseParty() legacy error = %v", err)
|
||||
}
|
||||
if _, err := legacy.PlayersYAML(); err == nil {
|
||||
t.Fatal("legacy PlayersYAML() error = nil, want canonical party requirement")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,8 +429,8 @@ func TestLoadWithSessionOptionsCarriesExplicitProfilePresence(t *testing.T) {
|
||||
rootPath := writeProfilePipeline(t, dir, profileComposition("production", "production", "testing"))
|
||||
writePipelineSource(t, dir, "production.yml", "workspace:\n root: /production\n")
|
||||
writePipelineSource(t, dir, "testing.yml", "workspace:\n root: /testing\n")
|
||||
campaignPath := writePipelineSource(t, dir, "campaign.yml", "campaign_id: campaign\ninputs:\n speakers_file: speakers.yml\n autocorrect_file: autocorrect.yml\n glossary_file: glossary.yml\n players_file: players.yml\n party_file: party.yml\n")
|
||||
writePipelineSource(t, dir, "party.yml", "legacy: party\n")
|
||||
campaignPath := writePipelineSource(t, dir, "campaign.yml", "campaign_id: campaign\ninputs:\n speakers_file: speakers.yml\n autocorrect_file: autocorrect.yml\n glossary_file: glossary.yml\n party_file: party.yml\n")
|
||||
writePipelineSource(t, dir, "party.yml", canonicalPartyFixture)
|
||||
sessionPath := writePipelineSource(t, dir, "session.yml", "session_id: session\ncampaign: campaign\n")
|
||||
selected := "testing"
|
||||
cfg, err := LoadWithSessionOptions(rootPath, campaignPath, sessionPath, SessionLoadOptions{Profile: &selected})
|
||||
|
||||
@@ -627,6 +627,4 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
|
||||
@@ -17,7 +17,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
@@ -42,7 +41,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
@@ -69,7 +67,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
@@ -99,7 +96,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
@@ -150,7 +146,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
@@ -176,7 +171,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||
|
||||
@@ -699,8 +699,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
},
|
||||
{
|
||||
@@ -713,8 +711,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantErr: "session.inputs.audio_s3.prefix must be a relative path",
|
||||
},
|
||||
@@ -728,8 +724,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantErr: "session.inputs.audio_s3.prefix must not contain path traversal",
|
||||
},
|
||||
@@ -744,8 +738,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`,
|
||||
wantErr: "mutually exclusive",
|
||||
},
|
||||
@@ -793,8 +785,6 @@ inputs:
|
||||
speakers_file: ./speakers.yml
|
||||
autocorrect_file: ./autocorrect.yml
|
||||
glossary_file: ./glossary.yml
|
||||
players_file: ./players.yml
|
||||
party_file: ./party.yml
|
||||
`
|
||||
pipelinePath, sessionPath := writeConfigFiles(t, pipelineYAML, sessionYAML)
|
||||
cfg, err := Load(pipelinePath, sessionPath)
|
||||
|
||||
@@ -108,7 +108,7 @@ func buildPrepareSemanticConfig(env *Env) (prepareSemanticConfig, error) {
|
||||
}
|
||||
partyMode := cfg.Party.Mode
|
||||
if partyMode == "" {
|
||||
partyMode = config.PartyModeLegacy
|
||||
return prepareSemanticConfig{}, fmt.Errorf("prepare semantic configuration requires a resolved canonical party")
|
||||
}
|
||||
payload := prepareSemanticConfig{
|
||||
CampaignID: strings.TrimSpace(cfg.Session.Campaign),
|
||||
|
||||
@@ -26,9 +26,6 @@ func TestPrepareSemanticConfigSensitivity(t *testing.T) {
|
||||
{name: "stable input owner", mutate: func(env *Env) {
|
||||
env.Config.StableInputs.PartyFile.Source = "campaign_config"
|
||||
}},
|
||||
{name: "party mode", mutate: func(env *Env) {
|
||||
env.Config.Party.Mode = config.PartyModeCanonical
|
||||
}},
|
||||
})
|
||||
|
||||
assertSemanticFingerprintUnchanged(t, prepareStage{}, initialSemanticEnv(), []struct {
|
||||
@@ -129,6 +126,7 @@ func initialSemanticEnv() *Env {
|
||||
StableInputs: config.ResolvedStableInputs{
|
||||
PartyFile: config.ResolvedInputFile{Path: "/campaign/party.yml", ConfigPath: "/campaign/campaign.yml", Source: "session_config"},
|
||||
},
|
||||
Party: config.ResolvedParty{Mode: config.PartyModeCanonical},
|
||||
}}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user