Prepare optional spell catalog inputs
This commit is contained in:
@@ -17,7 +17,7 @@ different contract.
|
||||
| Stage | Outcome | Status |
|
||||
| ---: | --- | --- |
|
||||
| 1 | Add the reference-selector and configuration vocabulary, including optional spell-catalog inputs. | Completed |
|
||||
| 2 | Materialize and inventory the optional spell catalog through the prepare and operator lifecycle. | Pending |
|
||||
| 2 | Materialize and inventory the optional spell catalog through the prepare and operator lifecycle. | Completed |
|
||||
| 3 | Centralize manifest-authoritative prepared-input resolution and migrate analyze to it. | Pending |
|
||||
| 4 | Add deterministic Notarius v0.6 reference arguments at the subprocess adapter boundary. | Pending |
|
||||
| 5 | Resolve references in extract and bind fingerprints, resume, and metadata to their identities. | Pending |
|
||||
|
||||
@@ -458,6 +458,63 @@ inputs:
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperatorCommandsReportConfiguredSpellCatalog(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
campaignBytes, err := os.ReadFile(campaignPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read campaign: %v", err)
|
||||
}
|
||||
campaignYAML := strings.Replace(string(campaignBytes), " party_file: ./party.yml\n", " party_file: ./party.yml\n spell_catalog_file: ./spells.json\n", 1)
|
||||
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||
t.Fatalf("write campaign: %v", err)
|
||||
}
|
||||
spellPath := filepath.Join(filepath.Dir(campaignPath), "spells.json")
|
||||
mustWriteTestFile(t, spellPath, "{\"spells\":[]}\n")
|
||||
|
||||
fake := &storage.FakeBackend{}
|
||||
var storeInitCalls int
|
||||
restoreAppConfigTestGlobals(t, fake, &storeInitCalls, []string{sessionPath})
|
||||
commonArgs := []string{
|
||||
"2026-05-03",
|
||||
"--config", pipelinePath,
|
||||
"--campaign-file", campaignPath,
|
||||
"--session", sessionPath,
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var stderr bytes.Buffer
|
||||
validateArgs := append([]string{"session", "validate"}, commonArgs...)
|
||||
if code := Execute(validateArgs, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("validate exit code = %d, want 0; stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "OK inputs spell_catalog: "+spellPath) {
|
||||
t.Fatalf("validate stdout = %q, want spell catalog finding", stdout.String())
|
||||
}
|
||||
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
statusArgs := append([]string{"session", "status"}, commonArgs...)
|
||||
if code := Execute(statusArgs, &stdout, &stderr); code != 0 {
|
||||
t.Fatalf("status exit code = %d, want 0; stdout=%q stderr=%q", code, stdout.String(), stderr.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "Stable input spell_catalog: "+spellPath) {
|
||||
t.Fatalf("status stdout = %q, want spell catalog inventory", stdout.String())
|
||||
}
|
||||
|
||||
if err := os.Remove(spellPath); err != nil {
|
||||
t.Fatalf("remove spell catalog: %v", err)
|
||||
}
|
||||
stdout.Reset()
|
||||
stderr.Reset()
|
||||
if code := Execute(validateArgs, &stdout, &stderr); code == 0 {
|
||||
t.Fatalf("validate missing spell catalog exit code = 0; stdout=%q", stdout.String())
|
||||
}
|
||||
if !strings.Contains(stdout.String(), "ERROR inputs spell_catalog missing:") {
|
||||
t.Fatalf("validate stdout = %q, want missing spell catalog finding", stdout.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteLocksAddListAndRemoveUseRemoteLockStore(t *testing.T) {
|
||||
workspaceRoot := t.TempDir()
|
||||
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||
|
||||
@@ -55,21 +55,26 @@ func inspectStableInputs(cfg *config.Config) []stableInputCheck {
|
||||
items := []struct {
|
||||
name string
|
||||
in config.ResolvedInputFile
|
||||
optional bool
|
||||
}{
|
||||
{name: "speakers", in: cfg.StableInputs.SpeakersFile},
|
||||
{name: "autocorrect", in: cfg.StableInputs.AutocorrectFile},
|
||||
{name: "glossary", in: cfg.StableInputs.GlossaryFile},
|
||||
{name: "players", in: cfg.StableInputs.PlayersFile},
|
||||
{name: "party", in: cfg.StableInputs.PartyFile},
|
||||
{name: "spell_catalog", in: cfg.StableInputs.SpellCatalogFile, optional: true},
|
||||
}
|
||||
out := make([]stableInputCheck, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.optional && strings.TrimSpace(item.in.Path) == "" {
|
||||
continue
|
||||
}
|
||||
path, err := resolveHelperConfigRelativePath(item.in)
|
||||
if err != nil {
|
||||
out = append(out, stableInputCheck{Name: item.name, Err: err})
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
if err := requireInspectionFile(path, item.name); err != nil {
|
||||
out = append(out, stableInputCheck{Name: item.name, Path: path, Err: err})
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -22,6 +22,32 @@ func RemoveAllUnderRoot(rootPath, target string) error {
|
||||
return removeConfinedEntry(root, targetName)
|
||||
}
|
||||
|
||||
// RemoveFileUnderRoot removes an exact regular-file target below root without
|
||||
// following symlinked ancestors or the leaf. A missing target is successful;
|
||||
// directories, symlinks, and other non-regular entries are rejected.
|
||||
func RemoveFileUnderRoot(rootPath, target string) error {
|
||||
root, targetName, err := openConfinedCleanupTarget(rootPath, target)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = root.Close() }()
|
||||
|
||||
info, err := root.Lstat(targetName)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect cleanup file %q: %w", targetName, err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("refusing to delete non-regular file path %q", targetName)
|
||||
}
|
||||
if err := root.Remove(targetName); err != nil {
|
||||
return fmt.Errorf("remove cleanup file %q: %w", targetName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func openConfinedCleanupTarget(rootPath, target string) (*os.Root, string, error) {
|
||||
if strings.TrimSpace(rootPath) == "" {
|
||||
return nil, "", fmt.Errorf("cleanup root is required")
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package fileops
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -77,3 +79,47 @@ func TestRemoveAllUnderRootRejectsSymlinkInTree(t *testing.T) {
|
||||
t.Fatalf("outside sentinel was changed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRemoveFileUnderRootRemovesOnlyRegularFile(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
target := filepath.Join(root, "inputs", "spell_catalog.json")
|
||||
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
|
||||
t.Fatalf("MkdirAll() error = %v", err)
|
||||
}
|
||||
if err := os.WriteFile(target, []byte("{}\n"), 0o644); err != nil {
|
||||
t.Fatalf("WriteFile() error = %v", err)
|
||||
}
|
||||
if err := RemoveFileUnderRoot(root, target); err != nil {
|
||||
t.Fatalf("RemoveFileUnderRoot() error = %v", err)
|
||||
}
|
||||
if _, err := os.Lstat(target); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("Lstat() error = %v, want not exist", err)
|
||||
}
|
||||
if err := RemoveFileUnderRoot(root, target); err != nil {
|
||||
t.Fatalf("RemoveFileUnderRoot(missing) error = %v", err)
|
||||
}
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
setup func(string) error
|
||||
}{
|
||||
{name: "directory", setup: func(path string) error { return os.Mkdir(path, 0o755) }},
|
||||
{name: "symlink", setup: func(path string) error { return os.Symlink(filepath.Join(root, "outside"), path) }},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := tt.setup(target); err != nil {
|
||||
t.Fatalf("setup target: %v", err)
|
||||
}
|
||||
err := RemoveFileUnderRoot(root, target)
|
||||
if err == nil || !strings.Contains(err.Error(), "non-regular") {
|
||||
t.Fatalf("RemoveFileUnderRoot() error = %v, want non-regular rejection", err)
|
||||
}
|
||||
if _, err := os.Lstat(target); err != nil {
|
||||
t.Fatalf("ambiguous target was removed: %v", err)
|
||||
}
|
||||
if err := os.Remove(target); err != nil {
|
||||
t.Fatalf("cleanup target: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/adapters/storage"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifactpolicy"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/audio"
|
||||
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||
@@ -64,6 +65,7 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
glossaryInput := stableInputSource(env.Config.StableInputs.GlossaryFile, env.Config.Session.Inputs.GlossaryFile, sessionSrc)
|
||||
playersInput := stableInputSource(env.Config.StableInputs.PlayersFile, env.Config.Session.Inputs.PlayersFile, sessionSrc)
|
||||
partyInput := stableInputSource(env.Config.StableInputs.PartyFile, env.Config.Session.Inputs.PartyFile, sessionSrc)
|
||||
spellCatalogInput := stableInputSource(env.Config.StableInputs.SpellCatalogFile, env.Config.Session.Inputs.SpellCatalogFile, sessionSrc)
|
||||
|
||||
speakersSrc, err := resolveConfigRelativePath(speakersInput)
|
||||
if err != nil {
|
||||
@@ -85,6 +87,17 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare: party path: %w", err)
|
||||
}
|
||||
spellCatalogConfigured := strings.TrimSpace(spellCatalogInput.Path) != ""
|
||||
spellCatalogSrc := ""
|
||||
if spellCatalogConfigured {
|
||||
spellCatalogSrc, err = resolveConfigRelativePath(spellCatalogInput)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare: spell catalog path: %w", err)
|
||||
}
|
||||
if err := requireRegularReadableFile(spellCatalogSrc, "spell catalog"); err != nil {
|
||||
return nil, fmt.Errorf("prepare: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
for _, required := range []struct {
|
||||
path string
|
||||
@@ -106,7 +119,7 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
return nil, fmt.Errorf("prepare: resolve audio inputs: %w", err)
|
||||
}
|
||||
|
||||
inputs := make([]manifest.InputRecord, 0, 8+len(resolvedLocalAudio))
|
||||
inputs := make([]manifest.InputRecord, 0, 9+len(resolvedLocalAudio))
|
||||
registerInput := func(kind, path, checksum string) {
|
||||
inputs = append(inputs, manifest.InputRecord{Kind: kind, Path: path, Checksum: checksum})
|
||||
}
|
||||
@@ -155,18 +168,36 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
||||
}
|
||||
registerInput("pipeline_resolved", pipelineDst, pipelineChecksum)
|
||||
|
||||
for _, cfgFile := range []struct {
|
||||
type preparedConfigFile struct {
|
||||
kind string
|
||||
src string
|
||||
dst string
|
||||
source string
|
||||
}{
|
||||
}
|
||||
preparedConfigFiles := []preparedConfigFile{
|
||||
{kind: "speakers", src: speakersSrc, dst: filepath.Join(paths.InputsDir, "speakers.yml"), source: speakersInput.Source},
|
||||
{kind: "autocorrect", src: autocorrectSrc, dst: filepath.Join(paths.InputsDir, "autocorrect.yml"), source: autocorrectInput.Source},
|
||||
{kind: "glossary", src: glossarySrc, dst: filepath.Join(paths.InputsDir, "glossary.yml"), source: glossaryInput.Source},
|
||||
{kind: "players", src: playersSrc, dst: filepath.Join(paths.InputsDir, "players.yml"), source: playersInput.Source},
|
||||
{kind: "party", src: partySrc, dst: filepath.Join(paths.InputsDir, "party.yml"), source: partyInput.Source},
|
||||
} {
|
||||
}
|
||||
spellDescriptor, ok := artifactpolicy.DescribePreparedInputSource(artifactpolicy.SourceInputSpellCatalog)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("prepare: spell catalog prepared-input descriptor is unavailable")
|
||||
}
|
||||
spellCatalogDst := filepath.Join(paths.InputsDir, spellDescriptor.Filename)
|
||||
if spellCatalogConfigured {
|
||||
preparedConfigFiles = append(preparedConfigFiles, preparedConfigFile{
|
||||
kind: spellDescriptor.ManifestKind,
|
||||
src: spellCatalogSrc,
|
||||
dst: spellCatalogDst,
|
||||
source: spellCatalogInput.Source,
|
||||
})
|
||||
} else if err := fileops.RemoveFileUnderRoot(paths.Root, spellCatalogDst); err != nil {
|
||||
return nil, fmt.Errorf("prepare: remove obsolete spell catalog: %w", err)
|
||||
}
|
||||
|
||||
for _, cfgFile := range preparedConfigFiles {
|
||||
checksum, err := copyFileIfChanged(env.ArtifactStore, cfgFile.src, cfgFile.dst)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("prepare: materialize %s: %w", cfgFile.kind, err)
|
||||
@@ -585,6 +616,28 @@ func requireFile(path string, label string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func requireRegularReadableFile(path string, label string) error {
|
||||
if strings.TrimSpace(path) == "" {
|
||||
return fmt.Errorf("%s path is required", label)
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open %s %q: %w", label, path, err)
|
||||
}
|
||||
info, statErr := file.Stat()
|
||||
closeErr := file.Close()
|
||||
if statErr != nil {
|
||||
return fmt.Errorf("stat %s %q: %w", label, path, statErr)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("%s %q is not a regular file", label, path)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return fmt.Errorf("close %s %q: %w", label, path, closeErr)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func isFlac(path string) bool {
|
||||
return strings.EqualFold(filepath.Ext(path), ".flac")
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package stage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -154,6 +156,167 @@ func TestPrepareStageIdempotent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareStageMaterializesSpellCatalogWithProvenance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
configPath func(*config.Config) string
|
||||
source string
|
||||
}{
|
||||
{name: "campaign", configPath: func(cfg *config.Config) string { return cfg.CampaignPath }, source: "campaign_config"},
|
||||
{name: "session", configPath: func(cfg *config.Config) string { return cfg.SessionPath }, source: "session_config"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
env, m := setupPrepareEnv(t)
|
||||
root := filepath.Dir(env.Config.SessionPath)
|
||||
writeFile(t, filepath.Join(root, "audio", "a.flac"), "audio")
|
||||
env.Config.Session.Inputs.AudioFiles = []string{"./audio/a.flac"}
|
||||
|
||||
payload := []byte(`{"spells":[{"name":"Fireball"}]}` + "\n")
|
||||
sourcePath := filepath.Join(filepath.Dir(tt.configPath(env.Config)), tt.name+"-spells.json")
|
||||
writeFile(t, sourcePath, string(payload))
|
||||
env.Config.StableInputs.SpellCatalogFile = config.ResolvedInputFile{
|
||||
Path: filepath.Base(sourcePath),
|
||||
ConfigPath: tt.configPath(env.Config),
|
||||
Source: tt.source,
|
||||
}
|
||||
|
||||
if _, err := (prepareStage{}).Run(context.Background(), env, m); err != nil {
|
||||
t.Fatalf("prepare.Run() error = %v", err)
|
||||
}
|
||||
|
||||
destination := filepath.Join(sessionPathsForEnv(env, m.SessionID).InputsDir, "spell_catalog.json")
|
||||
got, err := os.ReadFile(destination)
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(destination) error = %v", err)
|
||||
}
|
||||
if string(got) != string(payload) {
|
||||
t.Fatalf("destination bytes = %q, want %q", got, payload)
|
||||
}
|
||||
record := findManifestInput(t, m.Inputs, "spell_catalog")
|
||||
digest := sha256.Sum256(payload)
|
||||
wantChecksum := hex.EncodeToString(digest[:])
|
||||
if record.Path != destination || record.Source != tt.source || record.Checksum != wantChecksum {
|
||||
t.Fatalf("spell catalog record = %#v, want path=%q source=%q checksum=%q", record, destination, tt.source, wantChecksum)
|
||||
}
|
||||
assertManifestInputsSorted(t, m.Inputs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareStageSpellCatalogOptionalOmission(t *testing.T) {
|
||||
env, m := setupPrepareEnv(t)
|
||||
root := filepath.Dir(env.Config.SessionPath)
|
||||
writeFile(t, filepath.Join(root, "audio", "a.flac"), "audio")
|
||||
env.Config.Session.Inputs.AudioFiles = []string{"./audio/a.flac"}
|
||||
|
||||
if _, err := (prepareStage{}).Run(context.Background(), env, m); err != nil {
|
||||
t.Fatalf("prepare.Run() error = %v", err)
|
||||
}
|
||||
for _, input := range m.Inputs {
|
||||
if input.Kind == "spell_catalog" {
|
||||
t.Fatalf("unexpected spell catalog record: %#v", input)
|
||||
}
|
||||
}
|
||||
destination := filepath.Join(sessionPathsForEnv(env, m.SessionID).InputsDir, "spell_catalog.json")
|
||||
if _, err := os.Lstat(destination); !os.IsNotExist(err) {
|
||||
t.Fatalf("Lstat(destination) error = %v, want not exist", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareStageSpellCatalogMissingOrNonRegularSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setup func(string) error
|
||||
}{
|
||||
{name: "missing", setup: func(string) error { return nil }},
|
||||
{name: "directory", setup: func(path string) error { return os.Mkdir(path, 0o755) }},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
env, m := setupPrepareEnv(t)
|
||||
root := filepath.Dir(env.Config.SessionPath)
|
||||
writeFile(t, filepath.Join(root, "audio", "a.flac"), "audio")
|
||||
env.Config.Session.Inputs.AudioFiles = []string{"./audio/a.flac"}
|
||||
sourcePath := filepath.Join(root, "spells.json")
|
||||
if err := tt.setup(sourcePath); err != nil {
|
||||
t.Fatalf("setup source: %v", err)
|
||||
}
|
||||
env.Config.StableInputs.SpellCatalogFile = config.ResolvedInputFile{
|
||||
Path: "./spells.json",
|
||||
ConfigPath: env.Config.CampaignPath,
|
||||
Source: "campaign_config",
|
||||
}
|
||||
|
||||
_, err := (prepareStage{}).Run(context.Background(), env, m)
|
||||
if err == nil || !strings.Contains(err.Error(), "spell catalog") {
|
||||
t.Fatalf("prepare.Run() error = %v, want spell catalog source error", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareStageReplacesAndRemovesSpellCatalog(t *testing.T) {
|
||||
env, m := setupPrepareEnv(t)
|
||||
root := filepath.Dir(env.Config.SessionPath)
|
||||
writeFile(t, filepath.Join(root, "audio", "a.flac"), "audio")
|
||||
env.Config.Session.Inputs.AudioFiles = []string{"./audio/a.flac"}
|
||||
sourcePath := filepath.Join(root, "spells.json")
|
||||
writeFile(t, sourcePath, "first\n")
|
||||
env.Config.StableInputs.SpellCatalogFile = config.ResolvedInputFile{
|
||||
Path: "./spells.json",
|
||||
ConfigPath: env.Config.CampaignPath,
|
||||
Source: "campaign_config",
|
||||
}
|
||||
|
||||
stage := prepareStage{}
|
||||
if _, err := stage.Run(context.Background(), env, m); err != nil {
|
||||
t.Fatalf("first prepare.Run() error = %v", err)
|
||||
}
|
||||
firstChecksum := findManifestInput(t, m.Inputs, "spell_catalog").Checksum
|
||||
destination := filepath.Join(sessionPathsForEnv(env, m.SessionID).InputsDir, "spell_catalog.json")
|
||||
|
||||
writeFile(t, sourcePath, "second\n")
|
||||
if _, err := stage.Run(context.Background(), env, m); err != nil {
|
||||
t.Fatalf("replacement prepare.Run() error = %v", err)
|
||||
}
|
||||
secondChecksum := findManifestInput(t, m.Inputs, "spell_catalog").Checksum
|
||||
if secondChecksum == firstChecksum {
|
||||
t.Fatalf("replacement checksum = %q, want different from %q", secondChecksum, firstChecksum)
|
||||
}
|
||||
mustReadFileEquals(t, destination, "second\n")
|
||||
|
||||
env.Config.StableInputs.SpellCatalogFile = config.ResolvedInputFile{}
|
||||
env.Config.Session.Inputs.SpellCatalogFile = ""
|
||||
if _, err := stage.Run(context.Background(), env, m); err != nil {
|
||||
t.Fatalf("removal prepare.Run() error = %v", err)
|
||||
}
|
||||
if _, err := os.Lstat(destination); !os.IsNotExist(err) {
|
||||
t.Fatalf("Lstat(destination) error = %v, want removed", err)
|
||||
}
|
||||
for _, input := range m.Inputs {
|
||||
if input.Kind == "spell_catalog" {
|
||||
t.Fatalf("stale spell catalog record remains: %#v", input)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrepareStageRefusesAmbiguousObsoleteSpellCatalog(t *testing.T) {
|
||||
env, m := setupPrepareEnv(t)
|
||||
root := filepath.Dir(env.Config.SessionPath)
|
||||
writeFile(t, filepath.Join(root, "audio", "a.flac"), "audio")
|
||||
env.Config.Session.Inputs.AudioFiles = []string{"./audio/a.flac"}
|
||||
destination := filepath.Join(sessionPathsForEnv(env, m.SessionID).InputsDir, "spell_catalog.json")
|
||||
writeFile(t, filepath.Join(destination, "keep.txt"), "keep")
|
||||
|
||||
_, err := (prepareStage{}).Run(context.Background(), env, m)
|
||||
if err == nil || !strings.Contains(err.Error(), "remove obsolete spell catalog") || !strings.Contains(err.Error(), "non-regular") {
|
||||
t.Fatalf("prepare.Run() error = %v, want ambiguous destination rejection", err)
|
||||
}
|
||||
mustReadFileEquals(t, filepath.Join(destination, "keep.txt"), "keep")
|
||||
}
|
||||
|
||||
func TestPrepareStageRecordsLocalSessionProvenance(t *testing.T) {
|
||||
env, m := setupPrepareEnv(t)
|
||||
root := filepath.Dir(env.Config.SessionPath)
|
||||
@@ -719,6 +882,17 @@ func findManifestInput(t *testing.T, inputs []manifest.InputRecord, kind string)
|
||||
return manifest.InputRecord{}
|
||||
}
|
||||
|
||||
func assertManifestInputsSorted(t *testing.T, inputs []manifest.InputRecord) {
|
||||
t.Helper()
|
||||
for index := 1; index < len(inputs); index++ {
|
||||
previous := inputs[index-1]
|
||||
current := inputs[index]
|
||||
if previous.Kind > current.Kind || (previous.Kind == current.Kind && previous.Path > current.Path) {
|
||||
t.Fatalf("manifest inputs are not sorted at %d: %#v before %#v", index, previous, current)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mustReadFileEquals(t *testing.T, path, want string) {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(path)
|
||||
|
||||
Reference in New Issue
Block a user