Reject unversioned party documents
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user