Add campaign configuration support
This commit is contained in:
21
docs/cli.md
21
docs/cli.md
@@ -6,7 +6,7 @@
|
|||||||
narratio run --session-id 2026-04-04
|
narratio run --session-id 2026-04-04
|
||||||
```
|
```
|
||||||
|
|
||||||
This command uses default discovery for `pipeline.yml` and `session.yml`; both files must be discoverable unless you pass explicit `--config` and `--session` paths.
|
This command uses default discovery for `pipeline.yml`, `campaign.yml`, and `session.yml`; all three files must be discoverable unless you pass explicit `--config`, `--campaign`, and `--session` paths.
|
||||||
|
|
||||||
## Command Overview
|
## Command Overview
|
||||||
|
|
||||||
@@ -28,6 +28,7 @@ For config semantics, see [docs/config.md](./config.md). For operator lifecycle
|
|||||||
### `run`
|
### `run`
|
||||||
|
|
||||||
- `--config <path>`: optional explicit `pipeline.yml` path.
|
- `--config <path>`: optional explicit `pipeline.yml` path.
|
||||||
|
- `--campaign <path>`: optional explicit `campaign.yml` path.
|
||||||
- `--session <path>`: optional explicit `session.yml` path.
|
- `--session <path>`: optional explicit `session.yml` path.
|
||||||
- `--session-id <value>`: session template variable value.
|
- `--session-id <value>`: session template variable value.
|
||||||
- `--previous-session-id <value>`: previous-session template variable value.
|
- `--previous-session-id <value>`: previous-session template variable value.
|
||||||
@@ -37,6 +38,7 @@ For config semantics, see [docs/config.md](./config.md). For operator lifecycle
|
|||||||
### `plan`
|
### `plan`
|
||||||
|
|
||||||
- `--config <path>`
|
- `--config <path>`
|
||||||
|
- `--campaign <path>`
|
||||||
- `--session <path>`
|
- `--session <path>`
|
||||||
- `--session-id <value>`
|
- `--session-id <value>`
|
||||||
- `--previous-session-id <value>`
|
- `--previous-session-id <value>`
|
||||||
@@ -45,6 +47,7 @@ For config semantics, see [docs/config.md](./config.md). For operator lifecycle
|
|||||||
### `resume`
|
### `resume`
|
||||||
|
|
||||||
- `--config <path>`
|
- `--config <path>`
|
||||||
|
- `--campaign <path>`
|
||||||
- `--session <path>`
|
- `--session <path>`
|
||||||
- `--session-id <value>`
|
- `--session-id <value>`
|
||||||
- `--previous-session-id <value>`
|
- `--previous-session-id <value>`
|
||||||
@@ -54,6 +57,7 @@ For config semantics, see [docs/config.md](./config.md). For operator lifecycle
|
|||||||
### `run-stage`
|
### `run-stage`
|
||||||
|
|
||||||
- `--config <path>`
|
- `--config <path>`
|
||||||
|
- `--campaign <path>`
|
||||||
- `--session <path>`
|
- `--session <path>`
|
||||||
- `--session-id <value>`
|
- `--session-id <value>`
|
||||||
- `--previous-session-id <value>`
|
- `--previous-session-id <value>`
|
||||||
@@ -76,6 +80,7 @@ Valid stage names:
|
|||||||
### `restore`
|
### `restore`
|
||||||
|
|
||||||
- `--config <path>`
|
- `--config <path>`
|
||||||
|
- `--campaign <path>`
|
||||||
- `--session <path>`
|
- `--session <path>`
|
||||||
- `--session-id <value>`
|
- `--session-id <value>`
|
||||||
- `--previous-session-id <value>`
|
- `--previous-session-id <value>`
|
||||||
@@ -97,14 +102,14 @@ Purpose:
|
|||||||
Syntax:
|
Syntax:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
narratio run [--config <pipeline.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--force] [--artifacts <name[,name...]>]
|
narratio run [--config <pipeline.yml>] [--campaign <campaign.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--force] [--artifacts <name[,name...]>]
|
||||||
```
|
```
|
||||||
|
|
||||||
Success output:
|
Success output:
|
||||||
- `narratio run: session <session_id>; executed=<n> skipped=<n>; manifest=<path>`
|
- `narratio run: session <session_id>; executed=<n> skipped=<n>; manifest=<path>`
|
||||||
|
|
||||||
Common failure cases:
|
Common failure cases:
|
||||||
- missing default config/session paths when flags omitted.
|
- missing default config/campaign/session paths when flags omitted.
|
||||||
- invalid template/rendered session mismatch.
|
- invalid template/rendered session mismatch.
|
||||||
- unknown/invalid `--artifacts` value.
|
- unknown/invalid `--artifacts` value.
|
||||||
- `--artifacts` with unknown configured artifact key.
|
- `--artifacts` with unknown configured artifact key.
|
||||||
@@ -117,7 +122,7 @@ Purpose:
|
|||||||
Syntax:
|
Syntax:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
narratio plan [--config <pipeline.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--force]
|
narratio plan [--config <pipeline.yml>] [--campaign <campaign.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--force]
|
||||||
```
|
```
|
||||||
|
|
||||||
Success output includes:
|
Success output includes:
|
||||||
@@ -126,7 +131,7 @@ Success output includes:
|
|||||||
- `totals: run=<n> skip=<n>`
|
- `totals: run=<n> skip=<n>`
|
||||||
|
|
||||||
Common failure cases:
|
Common failure cases:
|
||||||
- same config/session discovery and validation failures as `run`.
|
- same config/campaign/session discovery and validation failures as `run`.
|
||||||
- secrets directory read failures when `pipeline.secrets.env_dir` is configured.
|
- secrets directory read failures when `pipeline.secrets.env_dir` is configured.
|
||||||
|
|
||||||
### `resume`
|
### `resume`
|
||||||
@@ -137,7 +142,7 @@ Purpose:
|
|||||||
Syntax:
|
Syntax:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
narratio resume [--config <pipeline.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--force] [--artifacts <name[,name...]>]
|
narratio resume [--config <pipeline.yml>] [--campaign <campaign.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--force] [--artifacts <name[,name...]>]
|
||||||
```
|
```
|
||||||
|
|
||||||
Success output:
|
Success output:
|
||||||
@@ -177,7 +182,7 @@ Purpose:
|
|||||||
Syntax:
|
Syntax:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
narratio run-stage [--config <pipeline.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--force] [--artifacts <name[,name...]>] <stage>
|
narratio run-stage [--config <pipeline.yml>] [--campaign <campaign.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--force] [--artifacts <name[,name...]>] <stage>
|
||||||
```
|
```
|
||||||
|
|
||||||
Success output:
|
Success output:
|
||||||
@@ -201,7 +206,7 @@ Purpose:
|
|||||||
Syntax:
|
Syntax:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
narratio restore [--config <pipeline.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--dry-run] [--force] [--include-audio]
|
narratio restore [--config <pipeline.yml>] [--campaign <campaign.yml>] [--session <session.yml>] [--session-id <id>] [--previous-session-id <id>] [--dry-run] [--force] [--include-audio]
|
||||||
```
|
```
|
||||||
|
|
||||||
Success output (dry-run):
|
Success output (dry-run):
|
||||||
|
|||||||
@@ -2,12 +2,13 @@
|
|||||||
|
|
||||||
## 1. Overview
|
## 1. Overview
|
||||||
|
|
||||||
Narratio loads two YAML files:
|
Narratio loads three YAML files:
|
||||||
|
|
||||||
- `pipeline.yml`: pipeline-level runtime configuration.
|
- `pipeline.yml`: pipeline-level runtime configuration.
|
||||||
|
- `campaign.yml`: stable campaign identity and campaign-level input defaults.
|
||||||
- `session.yml`: per-session metadata and input selection.
|
- `session.yml`: per-session metadata and input selection.
|
||||||
|
|
||||||
These commands load and validate both files before running:
|
These commands load and validate all three files before running:
|
||||||
|
|
||||||
- `narratio run`
|
- `narratio run`
|
||||||
- `narratio plan`
|
- `narratio plan`
|
||||||
@@ -20,6 +21,8 @@ Behavior:
|
|||||||
- strict YAML decode is enabled (`KnownFields(true)`): unknown fields fail.
|
- strict YAML decode is enabled (`KnownFields(true)`): unknown fields fail.
|
||||||
- session templates render before session YAML decode.
|
- session templates render before session YAML decode.
|
||||||
- defaults are applied for optional pipeline fields.
|
- defaults are applied for optional pipeline fields.
|
||||||
|
- campaign-level stable input paths fill missing session input paths.
|
||||||
|
- session-level stable input paths override campaign-level input paths.
|
||||||
- validation enforces required fields, value formats, and cross-field constraints.
|
- validation enforces required fields, value formats, and cross-field constraints.
|
||||||
|
|
||||||
## 2. Config file discovery
|
## 2. Config file discovery
|
||||||
@@ -32,6 +35,15 @@ Pipeline config lookup for `run`, `plan`, `resume`, `run-stage`, and `restore`:
|
|||||||
2. `/etc/narratio/pipeline.yml`
|
2. `/etc/narratio/pipeline.yml`
|
||||||
- first existing file wins.
|
- first existing file wins.
|
||||||
|
|
||||||
|
Campaign config lookup for `run`, `plan`, `resume`, `run-stage`, and `restore`:
|
||||||
|
|
||||||
|
- if `--campaign <path>` is provided, that path is used.
|
||||||
|
- if omitted, Narratio searches in order:
|
||||||
|
1. `./campaign.yml`
|
||||||
|
2. `/usr/local/etc/narratio/campaign.yml`
|
||||||
|
3. `/etc/narratio/campaign.yml`
|
||||||
|
- first existing file wins.
|
||||||
|
|
||||||
## 3. Session file discovery and templating
|
## 3. Session file discovery and templating
|
||||||
|
|
||||||
Session config lookup for `run`, `plan`, `resume`, `run-stage`, and `restore`:
|
Session config lookup for `run`, `plan`, `resume`, `run-stage`, and `restore`:
|
||||||
@@ -71,20 +83,28 @@ Why this is sufficient:
|
|||||||
|
|
||||||
## 5. Minimal session template
|
## 5. Minimal session template
|
||||||
|
|
||||||
|
`campaign.yml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
session_id: "{{ session_id }}"
|
|
||||||
campaign: sample-campaign
|
campaign: sample-campaign
|
||||||
|
inputs:
|
||||||
|
speakers_file: ./speakers.yml
|
||||||
|
autocorrect_file: ./autocorrect.yml
|
||||||
|
glossary_file: ./glossary.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
`session.yml`:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
session_id: "{{ session_id }}"
|
||||||
inputs:
|
inputs:
|
||||||
audio_dir: ./audio
|
audio_dir: ./audio
|
||||||
speakers_file: ./examples/speakers.yml
|
|
||||||
autocorrect_file: ./examples/autocorrect.yml
|
|
||||||
glossary_file: ./examples/glossary.yml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
narratio run --config /path/to/pipeline.yml --session ./session.yml --session-id 2026-05-03
|
narratio run --config /path/to/pipeline.yml --campaign ./campaign.yml --session ./session.yml --session-id 2026-05-03
|
||||||
```
|
```
|
||||||
|
|
||||||
Previous-session-enabled variant:
|
Previous-session-enabled variant:
|
||||||
@@ -92,16 +112,12 @@ Previous-session-enabled variant:
|
|||||||
```yaml
|
```yaml
|
||||||
session_id: "{{ session_id }}"
|
session_id: "{{ session_id }}"
|
||||||
previous_session_id: "{{ previous_session_id }}"
|
previous_session_id: "{{ previous_session_id }}"
|
||||||
campaign: sample-campaign
|
|
||||||
inputs:
|
inputs:
|
||||||
audio_dir: ./audio
|
audio_dir: ./audio
|
||||||
speakers_file: ./examples/speakers.yml
|
|
||||||
autocorrect_file: ./examples/autocorrect.yml
|
|
||||||
glossary_file: ./examples/glossary.yml
|
|
||||||
```
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
narratio run --config /path/to/pipeline.yml --session ./session.yml --session-id 2026-05-03 --previous-session-id 2026-04-26
|
narratio run --config /path/to/pipeline.yml --campaign ./campaign.yml --session ./session.yml --session-id 2026-05-03 --previous-session-id 2026-04-26
|
||||||
```
|
```
|
||||||
|
|
||||||
## 6. Production-oriented config
|
## 6. Production-oriented config
|
||||||
@@ -158,7 +174,7 @@ Operational notes:
|
|||||||
- archive promotion is explicit and source-based via `archive.promote_artifacts`.
|
- archive promotion is explicit and source-based via `archive.promote_artifacts`.
|
||||||
- `source` is required; `dest` is optional and derived when omitted.
|
- `source` is required; `dest` is optional and derived when omitted.
|
||||||
- Narratio does not auto-promote all generated analyze artifacts.
|
- Narratio does not auto-promote all generated analyze artifacts.
|
||||||
- `restore` reads the same config/session inputs and restore scope is bounded by committed archive current state.
|
- `restore` reads the same config/campaign/session inputs and restore scope is bounded by committed archive current state.
|
||||||
|
|
||||||
## 7. Full pipeline reference
|
## 7. Full pipeline reference
|
||||||
|
|
||||||
@@ -299,21 +315,34 @@ Restore-related implications:
|
|||||||
- restore remote identity requires archive S3 identity to resolve (`pipeline.storage.s3.bucket` and session prefix derivation inputs).
|
- restore remote identity requires archive S3 identity to resolve (`pipeline.storage.s3.bucket` and session prefix derivation inputs).
|
||||||
- restore scope considers committed current state and durable paths (`manifest.json`, `transcripts/**`, `artifacts/**`, `previous/**`, optional `audio/**`).
|
- restore scope considers committed current state and durable paths (`manifest.json`, `transcripts/**`, `artifacts/**`, `previous/**`, optional `audio/**`).
|
||||||
|
|
||||||
## 8. Full session reference
|
## 8. Full campaign reference
|
||||||
|
|
||||||
|
| Path | Type | Required | Default |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `campaign.campaign` | string | Yes | none |
|
||||||
|
| `campaign.inputs.speakers_file` | string | Yes | none |
|
||||||
|
| `campaign.inputs.autocorrect_file` | string | Yes | none |
|
||||||
|
| `campaign.inputs.glossary_file` | string | Yes | none |
|
||||||
|
|
||||||
|
Campaign input paths may be absolute or relative. Relative paths resolve from the directory containing `campaign.yml`.
|
||||||
|
|
||||||
|
## 9. Full session reference
|
||||||
|
|
||||||
| Path | Type | Required | Default |
|
| Path | Type | Required | Default |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| `session.session_id` | string | Yes | none |
|
| `session.session_id` | string | Yes | none |
|
||||||
| `session.previous_session_id` | string | No | empty |
|
| `session.previous_session_id` | string | No | empty |
|
||||||
| `session.campaign` | string | Yes | none |
|
| `session.campaign` | string | No | `campaign.campaign` |
|
||||||
| `session.date` | string | No | empty |
|
| `session.date` | string | No | empty |
|
||||||
| `session.title` | string | No | empty |
|
| `session.title` | string | No | empty |
|
||||||
| `session.inputs.audio_dir` | string | Conditional | empty |
|
| `session.inputs.audio_dir` | string | Conditional | empty |
|
||||||
| `session.inputs.audio_files[]` | list[string] | Conditional | empty |
|
| `session.inputs.audio_files[]` | list[string] | Conditional | empty |
|
||||||
| `session.inputs.audio_s3.prefix` | string | Conditional | none |
|
| `session.inputs.audio_s3.prefix` | string | Conditional | none |
|
||||||
| `session.inputs.speakers_file` | string | Yes | none |
|
| `session.inputs.speakers_file` | string | No | `campaign.inputs.speakers_file` |
|
||||||
| `session.inputs.autocorrect_file` | string | Yes | none |
|
| `session.inputs.autocorrect_file` | string | No | `campaign.inputs.autocorrect_file` |
|
||||||
| `session.inputs.glossary_file` | string | Yes | none |
|
| `session.inputs.glossary_file` | string | No | `campaign.inputs.glossary_file` |
|
||||||
|
|
||||||
|
Session input paths may be absolute or relative. Relative audio paths and session-level stable input overrides resolve from the directory containing `session.yml`. If both `campaign.yml` and `session.yml` specify campaign identity, the values must match.
|
||||||
|
|
||||||
Audio-source rule:
|
Audio-source rule:
|
||||||
|
|
||||||
@@ -328,7 +357,7 @@ Previous-session rule:
|
|||||||
- if `session.previous_session_id` is set, it must not equal `session.session_id`.
|
- if `session.previous_session_id` is set, it must not equal `session.session_id`.
|
||||||
- canonical previous-session sources (`narratio.previous_session.artifact.<name>`) are hydrated during `prepare` from archive current state when required by enabled configured artifacts.
|
- canonical previous-session sources (`narratio.previous_session.artifact.<name>`) are hydrated during `prepare` from archive current state when required by enabled configured artifacts.
|
||||||
|
|
||||||
## 9. Secrets
|
## 10. Secrets
|
||||||
|
|
||||||
Narratio supports filesystem-based secret injection via `pipeline.secrets.env_dir`.
|
Narratio supports filesystem-based secret injection via `pipeline.secrets.env_dir`.
|
||||||
|
|
||||||
@@ -347,13 +376,14 @@ Guidance:
|
|||||||
- do not put secret values directly in YAML.
|
- do not put secret values directly in YAML.
|
||||||
- configure env var names in config and provide values via env/secrets files.
|
- configure env var names in config and provide values via env/secrets files.
|
||||||
|
|
||||||
## 10. Examples
|
## 11. Examples
|
||||||
|
|
||||||
Maintained examples:
|
Maintained examples:
|
||||||
|
|
||||||
- `examples/pipeline.minimal.yml`
|
- `examples/pipeline.minimal.yml`
|
||||||
- `examples/pipeline.production.yml`
|
- `examples/pipeline.production.yml`
|
||||||
- `examples/pipeline.full.annotated.yml`
|
- `examples/pipeline.full.annotated.yml`
|
||||||
|
- `examples/campaign.yml`
|
||||||
- `examples/session.template.yml`
|
- `examples/session.template.yml`
|
||||||
- `examples/session.local-audio.yml`
|
- `examples/session.local-audio.yml`
|
||||||
- `examples/session.s3-audio.yml`
|
- `examples/session.s3-audio.yml`
|
||||||
|
|||||||
@@ -10,8 +10,8 @@ Prepare owns:
|
|||||||
|
|
||||||
## Inputs and outputs
|
## Inputs and outputs
|
||||||
Inputs:
|
Inputs:
|
||||||
- resolved config/session (`pipeline.yml`, `session.yml`);
|
- resolved config/campaign/session (`pipeline.yml`, `campaign.yml`, `session.yml`);
|
||||||
- session-local input files (`speakers`, `autocorrect`, `glossary`);
|
- campaign or session input files (`speakers`, `autocorrect`, `glossary`);
|
||||||
- audio source:
|
- audio source:
|
||||||
- local: `session.inputs.audio_dir` or `session.inputs.audio_files`;
|
- local: `session.inputs.audio_dir` or `session.inputs.audio_files`;
|
||||||
- S3: `session.inputs.audio_s3.prefix`;
|
- S3: `session.inputs.audio_s3.prefix`;
|
||||||
@@ -19,6 +19,7 @@ Inputs:
|
|||||||
- remote previous-session current archive state when previous hydration is required.
|
- remote previous-session current archive state when previous hydration is required.
|
||||||
|
|
||||||
Outputs:
|
Outputs:
|
||||||
|
- `inputs/campaign.yml`;
|
||||||
- `inputs/session.yml`;
|
- `inputs/session.yml`;
|
||||||
- `inputs/pipeline.resolved.yml`;
|
- `inputs/pipeline.resolved.yml`;
|
||||||
- `inputs/speakers.yml`;
|
- `inputs/speakers.yml`;
|
||||||
@@ -58,6 +59,10 @@ Does not own:
|
|||||||
- `pipeline.scriptorium.artifacts.<name>.enabled`
|
- `pipeline.scriptorium.artifacts.<name>.enabled`
|
||||||
- `pipeline.scriptorium.artifacts.<name>.inputs.<key>.source`
|
- `pipeline.scriptorium.artifacts.<name>.inputs.<key>.source`
|
||||||
- `pipeline.scriptorium.artifacts.<name>.inputs.<key>.required`
|
- `pipeline.scriptorium.artifacts.<name>.inputs.<key>.required`
|
||||||
|
- `campaign.campaign`
|
||||||
|
- `campaign.inputs.speakers_file`
|
||||||
|
- `campaign.inputs.autocorrect_file`
|
||||||
|
- `campaign.inputs.glossary_file`
|
||||||
|
|
||||||
## External adapters used
|
## External adapters used
|
||||||
- `storage.ObjectStore` for:
|
- `storage.ObjectStore` for:
|
||||||
@@ -67,6 +72,8 @@ Does not own:
|
|||||||
## State and manifest behavior
|
## State and manifest behavior
|
||||||
- Ensures workspace layout exists.
|
- Ensures workspace layout exists.
|
||||||
- Materializes canonical input files and audio files.
|
- Materializes canonical input files and audio files.
|
||||||
|
- Resolves campaign-provided stable input paths relative to `campaign.yml`.
|
||||||
|
- Resolves session-provided stable input overrides relative to `session.yml`.
|
||||||
- Scans enabled configured artifact inputs for canonical sources:
|
- Scans enabled configured artifact inputs for canonical sources:
|
||||||
- `narratio.previous_session.artifact.<artifact_key>`
|
- `narratio.previous_session.artifact.<artifact_key>`
|
||||||
- If one or more canonical previous-session requirements exist:
|
- If one or more canonical previous-session requirements exist:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ narratio run --session-id 2026-04-04
|
|||||||
- use `manifest=<path>` with `status` for inspection.
|
- use `manifest=<path>` with `status` for inspection.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- default config/session discovery applies unless `--config` and `--session` are passed.
|
- default config/campaign/session discovery applies unless `--config`, `--campaign`, and `--session` are passed.
|
||||||
- S3 audio mode requires `session.inputs.audio_s3.prefix` and valid object-store access.
|
- S3 audio mode requires `session.inputs.audio_s3.prefix` and valid object-store access.
|
||||||
|
|
||||||
## Restore workflow
|
## Restore workflow
|
||||||
@@ -61,6 +61,7 @@ Primary state:
|
|||||||
- `manifest.json`: session-level stage state.
|
- `manifest.json`: session-level stage state.
|
||||||
- `runs/{run_id}/manifest.json`: invocation-level state.
|
- `runs/{run_id}/manifest.json`: invocation-level state.
|
||||||
- `.lock`: session lock while a modifying command is active.
|
- `.lock`: session lock while a modifying command is active.
|
||||||
|
- `inputs/campaign.yml`, `inputs/session.yml`, and `inputs/pipeline.resolved.yml`: materialized config inputs for the run.
|
||||||
|
|
||||||
Canonical session directories:
|
Canonical session directories:
|
||||||
- `inputs/`
|
- `inputs/`
|
||||||
|
|||||||
5
examples/campaign.yml
Normal file
5
examples/campaign.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
campaign: sample-campaign
|
||||||
|
inputs:
|
||||||
|
speakers_file: ./speakers.yml
|
||||||
|
autocorrect_file: ./autocorrect.yml
|
||||||
|
glossary_file: ./glossary.yml
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
session_id: 2026-05-03
|
session_id: 2026-05-03
|
||||||
campaign: sample-campaign
|
|
||||||
date: 2026-05-03
|
date: 2026-05-03
|
||||||
title: Sample Session
|
title: Sample Session
|
||||||
inputs:
|
inputs:
|
||||||
audio_dir: ./audio
|
audio_dir: ./audio
|
||||||
speakers_file: ./examples/speakers.yml
|
|
||||||
autocorrect_file: ./examples/autocorrect.yml
|
|
||||||
glossary_file: ./examples/glossary.yml
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
session_id: 2026-05-03
|
session_id: 2026-05-03
|
||||||
campaign: sample-campaign
|
|
||||||
date: 2026-05-03
|
date: 2026-05-03
|
||||||
title: Sample Session
|
title: Sample Session
|
||||||
inputs:
|
inputs:
|
||||||
audio_s3:
|
audio_s3:
|
||||||
prefix: audio/
|
prefix: audio/
|
||||||
speakers_file: ./examples/speakers.yml
|
|
||||||
autocorrect_file: ./examples/autocorrect.yml
|
|
||||||
glossary_file: ./examples/glossary.yml
|
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
session_id: "{{ session_id }}"
|
session_id: "{{ session_id }}"
|
||||||
campaign: sample-campaign
|
|
||||||
inputs:
|
inputs:
|
||||||
audio_dir: ./audio
|
audio_dir: ./audio
|
||||||
speakers_file: ./examples/speakers.yml
|
|
||||||
autocorrect_file: ./examples/autocorrect.yml
|
|
||||||
glossary_file: ./examples/glossary.yml
|
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ import (
|
|||||||
|
|
||||||
func TestExecuteRunStageArtifactsNonAnalyzeFails(t *testing.T) {
|
func TestExecuteRunStageArtifactsNonAnalyzeFails(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute(
|
code := Execute(
|
||||||
[]string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "--artifacts", "session_recap", "polish"},
|
[]string{"run-stage", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--artifacts", "session_recap", "polish"},
|
||||||
&stdout,
|
&stdout,
|
||||||
&stderr,
|
&stderr,
|
||||||
)
|
)
|
||||||
@@ -33,12 +33,12 @@ func TestExecuteRunStageArtifactsNonAnalyzeFails(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteUnknownArtifactsFailValidation(t *testing.T) {
|
func TestExecuteUnknownArtifactsFailValidation(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute(
|
code := Execute(
|
||||||
[]string{"run", "--config", pipelinePath, "--session", sessionPath, "--artifacts", "unknown_artifact"},
|
[]string{"run", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--artifacts", "unknown_artifact"},
|
||||||
&stdout,
|
&stdout,
|
||||||
&stderr,
|
&stderr,
|
||||||
)
|
)
|
||||||
@@ -52,7 +52,7 @@ func TestExecuteUnknownArtifactsFailValidation(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunStageArtifactsDoesNotImplyForce(t *testing.T) {
|
func TestRunStageArtifactsDoesNotImplyForce(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
|
|
||||||
store := &manifest.LocalStore{}
|
store := &manifest.LocalStore{}
|
||||||
@@ -65,7 +65,7 @@ func TestRunStageArtifactsDoesNotImplyForce(t *testing.T) {
|
|||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := RunStage(
|
err := RunStage(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
[]string{"--config", pipelinePath, "--session", sessionPath, "--artifacts", "session_recap,session_recap", "analyze"},
|
[]string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--artifacts", "session_recap,session_recap", "analyze"},
|
||||||
&out,
|
&out,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -78,7 +78,7 @@ func TestRunStageArtifactsDoesNotImplyForce(t *testing.T) {
|
|||||||
|
|
||||||
func TestResumeArtifactsWithSucceededAnalyzeSkipsUnlessForced(t *testing.T) {
|
func TestResumeArtifactsWithSucceededAnalyzeSkipsUnlessForced(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
|
|
||||||
store := &manifest.LocalStore{}
|
store := &manifest.LocalStore{}
|
||||||
@@ -93,7 +93,7 @@ func TestResumeArtifactsWithSucceededAnalyzeSkipsUnlessForced(t *testing.T) {
|
|||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := Resume(
|
err := Resume(
|
||||||
context.Background(),
|
context.Background(),
|
||||||
[]string{"--config", pipelinePath, "--session", sessionPath, "--artifacts", "session_recap"},
|
[]string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--artifacts", "session_recap"},
|
||||||
&out,
|
&out,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -104,10 +104,10 @@ func TestResumeArtifactsWithSucceededAnalyzeSkipsUnlessForced(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeValidConfigFilesWithScriptoriumArtifacts(t *testing.T, workspaceRoot string) (string, string) {
|
func writeValidConfigFilesWithScriptoriumArtifacts(t *testing.T, workspaceRoot string) (string, string, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
f, err := os.OpenFile(pipelinePath, os.O_APPEND|os.O_WRONLY, 0)
|
f, err := os.OpenFile(pipelinePath, os.O_APPEND|os.O_WRONLY, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("open pipeline config for append: %v", err)
|
t.Fatalf("open pipeline config for append: %v", err)
|
||||||
@@ -136,5 +136,5 @@ scriptorium:
|
|||||||
if _, err := f.WriteString(extra); err != nil {
|
if _, err := f.WriteString(extra); err != nil {
|
||||||
t.Fatalf("append scriptorium config: %v", err)
|
t.Fatalf("append scriptorium config: %v", err)
|
||||||
}
|
}
|
||||||
return pipelinePath, sessionPath
|
return pipelinePath, campaignPath, sessionPath
|
||||||
}
|
}
|
||||||
|
|||||||
49
internal/app/campaign_config_path.go
Normal file
49
internal/app/campaign_config_path.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolveCampaignConfigPath(flagValue string) (string, error) {
|
||||||
|
return resolveCampaignConfigPathWithCandidates(flagValue, config.DefaultCampaignConfigSearchPaths)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveCampaignConfigPathWithCandidates(flagValue string, candidates []string) (string, error) {
|
||||||
|
if explicit := strings.TrimSpace(flagValue); explicit != "" {
|
||||||
|
return explicit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ordered := make([]string, 0, len(candidates))
|
||||||
|
for _, raw := range candidates {
|
||||||
|
path := strings.TrimSpace(raw)
|
||||||
|
if path == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ordered = append(ordered, path)
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err == nil {
|
||||||
|
if info.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return filepath.Clean(path), nil
|
||||||
|
}
|
||||||
|
if errors.Is(err, os.ErrNotExist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("check default campaign config %q: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(ordered) == 0 {
|
||||||
|
return "", fmt.Errorf("no campaign config path provided and no default locations configured")
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"no campaign config path provided and no default campaign config found; searched: %s; pass --campaign to use an explicit path",
|
||||||
|
strings.Join(ordered, ", "),
|
||||||
|
)
|
||||||
|
}
|
||||||
49
internal/app/campaign_config_path_test.go
Normal file
49
internal/app/campaign_config_path_test.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResolveCampaignConfigPathExplicitWins(t *testing.T) {
|
||||||
|
explicit := filepath.Join(t.TempDir(), "custom-campaign.yml")
|
||||||
|
got, err := resolveCampaignConfigPathWithCandidates(explicit, []string{filepath.Join(t.TempDir(), "campaign.yml")})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveCampaignConfigPathWithCandidates() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != explicit {
|
||||||
|
t.Fatalf("path = %q, want explicit path %q", got, explicit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveCampaignConfigPathUsesFirstExistingDefault(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
missing := filepath.Join(dir, "missing.yml")
|
||||||
|
found := filepath.Join(dir, "campaign.yml")
|
||||||
|
if err := os.WriteFile(found, []byte("campaign: sample-campaign\n"), 0o644); err != nil {
|
||||||
|
t.Fatalf("write campaign.yml: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := resolveCampaignConfigPathWithCandidates("", []string{missing, found})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("resolveCampaignConfigPathWithCandidates() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != filepath.Clean(found) {
|
||||||
|
t.Fatalf("path = %q, want %q", got, filepath.Clean(found))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResolveCampaignConfigPathErrorIncludesSearchedPaths(t *testing.T) {
|
||||||
|
_, err := resolveCampaignConfigPathWithCandidates("", []string{"./campaign.yml", "/usr/local/etc/narratio/campaign.yml", "/etc/narratio/campaign.yml"})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "searched") {
|
||||||
|
t.Fatalf("error = %q, want searched paths", err.Error())
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "pass --campaign") {
|
||||||
|
t.Fatalf("error = %q, want explicit-campaign guidance", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ func TestExecuteValidCommands(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
||||||
manifestPath := writeManifestPathForExecute(t)
|
manifestPath := writeManifestPathForExecute(t)
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
@@ -32,11 +32,11 @@ func TestExecuteValidCommands(t *testing.T) {
|
|||||||
args []string
|
args []string
|
||||||
wantOut string
|
wantOut string
|
||||||
}{
|
}{
|
||||||
{name: "run", args: []string{"run", "--config", pipelinePath, "--session", sessionPath}, wantOut: "narratio run: session 2026-05-03; executed=9 skipped=0; manifest="},
|
{name: "run", args: []string{"run", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, wantOut: "narratio run: session 2026-05-03; executed=9 skipped=0; manifest="},
|
||||||
{name: "plan", args: []string{"plan", "--config", pipelinePath, "--session", sessionPath}, wantOut: "prepare: skip\ntranscribe: skip\nmerge: skip\npolish: skip\nnormalize: skip\ntrim: skip\nanalyze: skip\narchive: skip\nnotify: skip"},
|
{name: "plan", args: []string{"plan", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, wantOut: "prepare: skip\ntranscribe: skip\nmerge: skip\npolish: skip\nnormalize: skip\ntrim: skip\nanalyze: skip\narchive: skip\nnotify: skip"},
|
||||||
{name: "status", args: []string{"status", "--manifest", manifestPath}, wantOut: "session_id: 2026-05-03"},
|
{name: "status", args: []string{"status", "--manifest", manifestPath}, wantOut: "session_id: 2026-05-03"},
|
||||||
{name: "resume", args: []string{"resume", "--config", pipelinePath, "--session", sessionPath}, wantOut: "narratio resume: session 2026-05-03 has no remaining stages"},
|
{name: "resume", args: []string{"resume", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, wantOut: "narratio resume: session 2026-05-03 has no remaining stages"},
|
||||||
{name: "run-stage", args: []string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "polish"}, wantOut: "narratio run-stage: stage=polish executed=0 skipped=1 force=false; manifest="},
|
{name: "run-stage", args: []string{"run-stage", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "polish"}, wantOut: "narratio run-stage: stage=polish executed=0 skipped=1 force=false; manifest="},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -94,12 +94,12 @@ func TestExecuteMissingRequiredFlags(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRunStageUnknownFails(t *testing.T) {
|
func TestExecuteRunStageUnknownFails(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe")
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe")
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
code := Execute([]string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "unknown"}, &stdout, &stderr)
|
code := Execute([]string{"run-stage", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "unknown"}, &stdout, &stderr)
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
t.Fatal("exit code = 0, want non-zero")
|
t.Fatal("exit code = 0, want non-zero")
|
||||||
}
|
}
|
||||||
@@ -110,14 +110,14 @@ func TestExecuteRunStageUnknownFails(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRunStageNormalizeIsAccepted(t *testing.T) {
|
func TestExecuteRunStageNormalizeIsAccepted(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe")
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, "https://example.com/transcribe")
|
||||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1}]}`)
|
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1}]}`)
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
code := Execute([]string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "normalize"}, &stdout, &stderr)
|
code := Execute([]string{"run-stage", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "normalize"}, &stdout, &stderr)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
@@ -136,19 +136,19 @@ func TestExecuteRunStageTranscribeUsesConfiguredWhisperXServer(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
|
|
||||||
code := Execute([]string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "prepare"}, &stdout, &stderr)
|
code := Execute([]string{"run-stage", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "prepare"}, &stdout, &stderr)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("prepare exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("prepare exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
stdout.Reset()
|
stdout.Reset()
|
||||||
stderr.Reset()
|
stderr.Reset()
|
||||||
|
|
||||||
code = Execute([]string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "--force", "transcribe"}, &stdout, &stderr)
|
code = Execute([]string{"run-stage", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--force", "transcribe"}, &stdout, &stderr)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("transcribe exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("transcribe exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
@@ -188,6 +188,7 @@ func TestExecuteRunStagePolishLoadsCredentialFromSecretsDir(t *testing.T) {
|
|||||||
t.Setenv("GO_WANT_APP_AUDITA_HELPER", "1")
|
t.Setenv("GO_WANT_APP_AUDITA_HELPER", "1")
|
||||||
|
|
||||||
pipelinePath := filepath.Join(configDir, "pipeline.yml")
|
pipelinePath := filepath.Join(configDir, "pipeline.yml")
|
||||||
|
campaignPath := writeAppTestCampaignConfig(t, configDir)
|
||||||
sessionPath := filepath.Join(configDir, "session.yml")
|
sessionPath := filepath.Join(configDir, "session.yml")
|
||||||
pipelineYAML := `workspace:
|
pipelineYAML := `workspace:
|
||||||
root: ` + workspaceRoot + `
|
root: ` + workspaceRoot + `
|
||||||
@@ -239,7 +240,7 @@ inputs:
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"run-stage", "--config", pipelinePath, "--session", sessionPath, "--force", "polish"}, &stdout, &stderr)
|
code := Execute([]string{"run-stage", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--force", "polish"}, &stdout, &stderr)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
@@ -252,6 +253,7 @@ func TestExecuteRunFailsWhenConfiguredSecretsDirMissing(t *testing.T) {
|
|||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
configDir := t.TempDir()
|
configDir := t.TempDir()
|
||||||
pipelinePath := filepath.Join(configDir, "pipeline.yml")
|
pipelinePath := filepath.Join(configDir, "pipeline.yml")
|
||||||
|
campaignPath := writeAppTestCampaignConfig(t, configDir)
|
||||||
sessionPath := filepath.Join(configDir, "session.yml")
|
sessionPath := filepath.Join(configDir, "session.yml")
|
||||||
|
|
||||||
pipelineYAML := `workspace:
|
pipelineYAML := `workspace:
|
||||||
@@ -288,7 +290,7 @@ inputs:
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"run", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
|
code := Execute([]string{"run", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
t.Fatal("exit code = 0, want non-zero")
|
t.Fatal("exit code = 0, want non-zero")
|
||||||
}
|
}
|
||||||
@@ -305,11 +307,14 @@ func TestExecuteUsesDefaultPipelineConfigPathWhenConfigFlagOmitted(t *testing.T)
|
|||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
||||||
originalDefaults := append([]string(nil), config.DefaultPipelineConfigSearchPaths...)
|
originalDefaults := append([]string(nil), config.DefaultPipelineConfigSearchPaths...)
|
||||||
|
originalCampaignDefaults := append([]string(nil), config.DefaultCampaignConfigSearchPaths...)
|
||||||
config.DefaultPipelineConfigSearchPaths = []string{pipelinePath}
|
config.DefaultPipelineConfigSearchPaths = []string{pipelinePath}
|
||||||
|
config.DefaultCampaignConfigSearchPaths = []string{campaignPath}
|
||||||
defer func() {
|
defer func() {
|
||||||
config.DefaultPipelineConfigSearchPaths = originalDefaults
|
config.DefaultPipelineConfigSearchPaths = originalDefaults
|
||||||
|
config.DefaultCampaignConfigSearchPaths = originalCampaignDefaults
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
@@ -323,6 +328,33 @@ func TestExecuteUsesDefaultPipelineConfigPathWhenConfigFlagOmitted(t *testing.T)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecuteMissingCampaignConfigReportsSearchedPaths(t *testing.T) {
|
||||||
|
workspaceRoot := t.TempDir()
|
||||||
|
pipelinePath, _, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
missingCampaignPath := filepath.Join(t.TempDir(), "campaign.yml")
|
||||||
|
originalCampaignDefaults := append([]string(nil), config.DefaultCampaignConfigSearchPaths...)
|
||||||
|
config.DefaultCampaignConfigSearchPaths = []string{missingCampaignPath}
|
||||||
|
defer func() {
|
||||||
|
config.DefaultCampaignConfigSearchPaths = originalCampaignDefaults
|
||||||
|
}()
|
||||||
|
|
||||||
|
var stdout bytes.Buffer
|
||||||
|
var stderr bytes.Buffer
|
||||||
|
code := Execute([]string{"run", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
|
||||||
|
if code == 0 {
|
||||||
|
t.Fatal("exit code = 0, want non-zero")
|
||||||
|
}
|
||||||
|
if stdout.Len() != 0 {
|
||||||
|
t.Fatalf("stdout = %q, want empty", stdout.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), "no campaign config path provided and no default campaign config found; searched:") {
|
||||||
|
t.Fatalf("stderr = %q, want campaign discovery failure", stderr.String())
|
||||||
|
}
|
||||||
|
if !strings.Contains(stderr.String(), "pass --campaign") {
|
||||||
|
t.Fatalf("stderr = %q, want explicit campaign guidance", stderr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestExecuteInvalidCommand(t *testing.T) {
|
func TestExecuteInvalidCommand(t *testing.T) {
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
@@ -359,11 +391,12 @@ func TestExecuteMissingCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeValidConfigFiles(t *testing.T, workspaceRoot string, transcribeURL ...string) (string, string) {
|
func writeValidConfigFiles(t *testing.T, workspaceRoot string, transcribeURL ...string) (string, string, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||||
|
campaignPath := filepath.Join(dir, "campaign.yml")
|
||||||
sessionPath := filepath.Join(dir, "session.yml")
|
sessionPath := filepath.Join(dir, "session.yml")
|
||||||
url := "https://example.com/transcribe"
|
url := "https://example.com/transcribe"
|
||||||
if len(transcribeURL) > 0 && strings.TrimSpace(transcribeURL[0]) != "" {
|
if len(transcribeURL) > 0 && strings.TrimSpace(transcribeURL[0]) != "" {
|
||||||
@@ -407,9 +440,11 @@ notification:
|
|||||||
`
|
`
|
||||||
|
|
||||||
sessionYAML := `session_id: 2026-05-03
|
sessionYAML := `session_id: 2026-05-03
|
||||||
campaign: sample-campaign
|
|
||||||
inputs:
|
inputs:
|
||||||
audio_dir: ./audio
|
audio_dir: ./audio
|
||||||
|
`
|
||||||
|
campaignYAML := `campaign: sample-campaign
|
||||||
|
inputs:
|
||||||
speakers_file: ./speakers.yml
|
speakers_file: ./speakers.yml
|
||||||
autocorrect_file: ./autocorrect.yml
|
autocorrect_file: ./autocorrect.yml
|
||||||
glossary_file: ./glossary.yml
|
glossary_file: ./glossary.yml
|
||||||
@@ -418,6 +453,9 @@ inputs:
|
|||||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||||
t.Fatalf("write pipeline config: %v", err)
|
t.Fatalf("write pipeline config: %v", err)
|
||||||
}
|
}
|
||||||
|
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||||
|
t.Fatalf("write campaign config: %v", err)
|
||||||
|
}
|
||||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||||
t.Fatalf("write session config: %v", err)
|
t.Fatalf("write session config: %v", err)
|
||||||
}
|
}
|
||||||
@@ -427,7 +465,22 @@ inputs:
|
|||||||
mustWriteTestFile(t, filepath.Join(dir, "glossary.yml"), "[]\n")
|
mustWriteTestFile(t, filepath.Join(dir, "glossary.yml"), "[]\n")
|
||||||
mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes")
|
mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes")
|
||||||
|
|
||||||
return pipelinePath, sessionPath
|
return pipelinePath, campaignPath, sessionPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAppTestCampaignConfig(t *testing.T, dir string) string {
|
||||||
|
t.Helper()
|
||||||
|
campaignPath := filepath.Join(dir, "campaign.yml")
|
||||||
|
campaignYAML := `campaign: sample-campaign
|
||||||
|
inputs:
|
||||||
|
speakers_file: ./speakers.yml
|
||||||
|
autocorrect_file: ./autocorrect.yml
|
||||||
|
glossary_file: ./glossary.yml
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||||
|
t.Fatalf("write campaign.yml: %v", err)
|
||||||
|
}
|
||||||
|
return campaignPath
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeManifestPathForExecute(t *testing.T) string {
|
func writeManifestPathForExecute(t *testing.T) string {
|
||||||
|
|||||||
@@ -20,11 +20,13 @@ func Plan(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
fs.SetOutput(io.Discard)
|
fs.SetOutput(io.Discard)
|
||||||
|
|
||||||
var pipelinePath string
|
var pipelinePath string
|
||||||
|
var campaignPath string
|
||||||
var sessionPath string
|
var sessionPath string
|
||||||
var sessionID string
|
var sessionID string
|
||||||
var previousSessionID string
|
var previousSessionID string
|
||||||
var force bool
|
var force bool
|
||||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||||
|
fs.StringVar(&campaignPath, "campaign", "", "path to campaign.yml (optional; defaults searched)")
|
||||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||||
fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates")
|
fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates")
|
||||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier for session.yml templates")
|
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier for session.yml templates")
|
||||||
@@ -40,12 +42,16 @@ func Plan(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("plan: %w", err)
|
return fmt.Errorf("plan: %w", err)
|
||||||
}
|
}
|
||||||
|
resolvedCampaignPath, err := resolveCampaignConfigPath(campaignPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("plan: %w", err)
|
||||||
|
}
|
||||||
resolvedSessionPath, err := resolveSessionConfigPath(sessionPath)
|
resolvedSessionPath, err := resolveSessionConfigPath(sessionPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("plan: %w", err)
|
return fmt.Errorf("plan: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedSessionPath, config.SessionLoadOptions{
|
cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedCampaignPath, resolvedSessionPath, config.SessionLoadOptions{
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
PreviousSessionID: previousSessionID,
|
PreviousSessionID: previousSessionID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import (
|
|||||||
|
|
||||||
func TestPlanCreatesAndReusesWorkdir(t *testing.T) {
|
func TestPlanCreatesAndReusesWorkdir(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
args := []string{"--config", pipelinePath, "--session", sessionPath}
|
args := []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}
|
||||||
|
|
||||||
if err := Plan(context.Background(), args, &out); err != nil {
|
if err := Plan(context.Background(), args, &out); err != nil {
|
||||||
t.Fatalf("first Plan() error = %v", err)
|
t.Fatalf("first Plan() error = %v", err)
|
||||||
@@ -62,7 +62,7 @@ func TestPlanCreatesAndReusesWorkdir(t *testing.T) {
|
|||||||
|
|
||||||
func TestPlanShowsRunAndSkipFromManifest(t *testing.T) {
|
func TestPlanShowsRunAndSkipFromManifest(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
|
|
||||||
store := &manifest.LocalStore{}
|
store := &manifest.LocalStore{}
|
||||||
@@ -74,7 +74,7 @@ func TestPlanShowsRunAndSkipFromManifest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
if err := Plan(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath}, &out); err != nil {
|
if err := Plan(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &out); err != nil {
|
||||||
t.Fatalf("Plan() error = %v", err)
|
t.Fatalf("Plan() error = %v", err)
|
||||||
}
|
}
|
||||||
got := out.String()
|
got := out.String()
|
||||||
@@ -93,6 +93,7 @@ func TestPlanFailsWhenConfiguredSecretsDirMissing(t *testing.T) {
|
|||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
configDir := t.TempDir()
|
configDir := t.TempDir()
|
||||||
pipelinePath := filepath.Join(configDir, "pipeline.yml")
|
pipelinePath := filepath.Join(configDir, "pipeline.yml")
|
||||||
|
campaignPath := writeAppTestCampaignConfig(t, configDir)
|
||||||
sessionPath := filepath.Join(configDir, "session.yml")
|
sessionPath := filepath.Join(configDir, "session.yml")
|
||||||
|
|
||||||
pipelineYAML := `workspace:
|
pipelineYAML := `workspace:
|
||||||
@@ -128,7 +129,7 @@ inputs:
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := Plan(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath}, &out)
|
err := Plan(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &out)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ func Restore(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
fs.SetOutput(out)
|
fs.SetOutput(out)
|
||||||
|
|
||||||
var pipelinePath string
|
var pipelinePath string
|
||||||
|
var campaignPath string
|
||||||
var sessionPath string
|
var sessionPath string
|
||||||
var sessionID string
|
var sessionID string
|
||||||
var previousSessionID string
|
var previousSessionID string
|
||||||
@@ -33,6 +34,7 @@ func Restore(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
var force bool
|
var force bool
|
||||||
var includeAudio bool
|
var includeAudio bool
|
||||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||||
|
fs.StringVar(&campaignPath, "campaign", "", "path to campaign.yml (optional; defaults searched)")
|
||||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||||
fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates")
|
fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates")
|
||||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier for session.yml templates")
|
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier for session.yml templates")
|
||||||
@@ -40,7 +42,7 @@ func Restore(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
fs.BoolVar(&force, "force", false, "overwrite local conflicts with remote state")
|
fs.BoolVar(&force, "force", false, "overwrite local conflicts with remote state")
|
||||||
fs.BoolVar(&includeAudio, "include-audio", false, "include archived session-level audio objects")
|
fs.BoolVar(&includeAudio, "include-audio", false, "include archived session-level audio objects")
|
||||||
fs.Usage = func() {
|
fs.Usage = func() {
|
||||||
_, _ = fmt.Fprintln(out, "Usage: narratio restore [--config <path>] [--session <path>] [--session-id <value>] [--previous-session-id <value>] [--dry-run] [--force] [--include-audio]")
|
_, _ = fmt.Fprintln(out, "Usage: narratio restore [--config <path>] [--campaign <path>] [--session <path>] [--session-id <value>] [--previous-session-id <value>] [--dry-run] [--force] [--include-audio]")
|
||||||
_, _ = fmt.Fprintln(out)
|
_, _ = fmt.Fprintln(out)
|
||||||
_, _ = fmt.Fprintln(out, "Flags:")
|
_, _ = fmt.Fprintln(out, "Flags:")
|
||||||
fs.PrintDefaults()
|
fs.PrintDefaults()
|
||||||
@@ -59,12 +61,16 @@ func Restore(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("restore: %w", err)
|
return fmt.Errorf("restore: %w", err)
|
||||||
}
|
}
|
||||||
|
resolvedCampaignPath, err := resolveCampaignConfigPath(campaignPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("restore: %w", err)
|
||||||
|
}
|
||||||
resolvedSessionPath, err := resolveSessionConfigPath(sessionPath)
|
resolvedSessionPath, err := resolveSessionConfigPath(sessionPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("restore: %w", err)
|
return fmt.Errorf("restore: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedSessionPath, config.SessionLoadOptions{
|
cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedCampaignPath, resolvedSessionPath, config.SessionLoadOptions{
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
PreviousSessionID: previousSessionID,
|
PreviousSessionID: previousSessionID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import (
|
|||||||
|
|
||||||
func TestExecuteRestoreNonDryRunRestoresDurableFiles(t *testing.T) {
|
func TestExecuteRestoreNonDryRunRestoresDurableFiles(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
fake := &storage.FakeBackend{}
|
fake := &storage.FakeBackend{}
|
||||||
cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fake, pipelinePath, sessionPath)
|
cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath)
|
||||||
seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte(`{"segments":[1,2,3]}`))
|
seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte(`{"segments":[1,2,3]}`))
|
||||||
seedRestoreObject(fake, sessionPrefix+"artifacts/session_recap.md", []byte("# recap\n"))
|
seedRestoreObject(fake, sessionPrefix+"artifacts/session_recap.md", []byte("# recap\n"))
|
||||||
seedRestoreObject(fake, sessionPrefix+"audio/alice.flac", []byte("remote-audio"))
|
seedRestoreObject(fake, sessionPrefix+"audio/alice.flac", []byte("remote-audio"))
|
||||||
@@ -32,7 +32,7 @@ func TestExecuteRestoreNonDryRunRestoresDurableFiles(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
@@ -64,17 +64,17 @@ func TestExecuteRestoreNonDryRunRestoresDurableFiles(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRestoreIncludeAudioRestoresAudio(t *testing.T) {
|
func TestExecuteRestoreIncludeAudioRestoresAudio(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
fake := &storage.FakeBackend{}
|
fake := &storage.FakeBackend{}
|
||||||
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, sessionPath)
|
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath)
|
||||||
seedRestoreObject(fake, sessionPrefix+"audio/alice.flac", []byte("remote-audio"))
|
seedRestoreObject(fake, sessionPrefix+"audio/alice.flac", []byte("remote-audio"))
|
||||||
|
|
||||||
restoreWithStoreAndRealPhases(t, fake)
|
restoreWithStoreAndRealPhases(t, fake)
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath, "--include-audio"}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--include-audio"}, &stdout, &stderr)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
@@ -89,10 +89,10 @@ func TestExecuteRestoreIncludeAudioRestoresAudio(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRestoreRestoresPreviousCacheWhenPresent(t *testing.T) {
|
func TestExecuteRestoreRestoresPreviousCacheWhenPresent(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
fake := &storage.FakeBackend{}
|
fake := &storage.FakeBackend{}
|
||||||
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, sessionPath)
|
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath)
|
||||||
seedRestoreObject(fake, sessionPrefix+"previous/manifest.json", []byte(`{"session_id":"2026-04-26"}`))
|
seedRestoreObject(fake, sessionPrefix+"previous/manifest.json", []byte(`{"session_id":"2026-04-26"}`))
|
||||||
seedRestoreObject(fake, sessionPrefix+"previous/artifacts/session_recap.md", []byte("# previous recap\n"))
|
seedRestoreObject(fake, sessionPrefix+"previous/artifacts/session_recap.md", []byte("# previous recap\n"))
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ func TestExecuteRestoreRestoresPreviousCacheWhenPresent(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
@@ -116,10 +116,10 @@ func TestExecuteRestoreRestoresPreviousCacheWhenPresent(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRestoreConflictWithoutForceDoesNotOverwrite(t *testing.T) {
|
func TestExecuteRestoreConflictWithoutForceDoesNotOverwrite(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
fake := &storage.FakeBackend{}
|
fake := &storage.FakeBackend{}
|
||||||
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, sessionPath)
|
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath)
|
||||||
seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte("remote-transcript"))
|
seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte("remote-transcript"))
|
||||||
|
|
||||||
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||||
@@ -129,7 +129,7 @@ func TestExecuteRestoreConflictWithoutForceDoesNotOverwrite(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
t.Fatal("exit code = 0, want non-zero")
|
t.Fatal("exit code = 0, want non-zero")
|
||||||
}
|
}
|
||||||
@@ -148,10 +148,10 @@ func TestExecuteRestoreConflictWithoutForceDoesNotOverwrite(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRestoreForceOverwritesDifferingFile(t *testing.T) {
|
func TestExecuteRestoreForceOverwritesDifferingFile(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
fake := &storage.FakeBackend{}
|
fake := &storage.FakeBackend{}
|
||||||
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, sessionPath)
|
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath)
|
||||||
seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte("remote-transcript"))
|
seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte("remote-transcript"))
|
||||||
|
|
||||||
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||||
@@ -161,7 +161,7 @@ func TestExecuteRestoreForceOverwritesDifferingFile(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath, "--force"}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
@@ -174,10 +174,10 @@ func TestExecuteRestoreForceOverwritesDifferingFile(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRestoreForceOverwritesDifferingPreviousCacheFile(t *testing.T) {
|
func TestExecuteRestoreForceOverwritesDifferingPreviousCacheFile(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
fake := &storage.FakeBackend{}
|
fake := &storage.FakeBackend{}
|
||||||
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, sessionPath)
|
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath)
|
||||||
seedRestoreObject(fake, sessionPrefix+"previous/artifacts/session_recap.md", []byte("# remote previous recap\n"))
|
seedRestoreObject(fake, sessionPrefix+"previous/artifacts/session_recap.md", []byte("# remote previous recap\n"))
|
||||||
|
|
||||||
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
sessionRoot := artifacts.SessionWorkDirForCampaign(workspaceRoot, cfg.Session.Campaign, cfg.Session.SessionID)
|
||||||
@@ -187,7 +187,7 @@ func TestExecuteRestoreForceOverwritesDifferingPreviousCacheFile(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath, "--force"}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
@@ -196,10 +196,10 @@ func TestExecuteRestoreForceOverwritesDifferingPreviousCacheFile(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRestoreLockConflictFailsAndWritesNothing(t *testing.T) {
|
func TestExecuteRestoreLockConflictFailsAndWritesNothing(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
fake := &storage.FakeBackend{}
|
fake := &storage.FakeBackend{}
|
||||||
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, sessionPath)
|
cfg, sessionPrefix, _, _ := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath)
|
||||||
seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte("remote-transcript"))
|
seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte("remote-transcript"))
|
||||||
|
|
||||||
store := artifacts.NewLocalStore(workspaceRoot)
|
store := artifacts.NewLocalStore(workspaceRoot)
|
||||||
@@ -213,7 +213,7 @@ func TestExecuteRestoreLockConflictFailsAndWritesNothing(t *testing.T) {
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
t.Fatal("exit code = 0, want non-zero")
|
t.Fatal("exit code = 0, want non-zero")
|
||||||
}
|
}
|
||||||
@@ -229,10 +229,10 @@ func TestExecuteRestoreLockConflictFailsAndWritesNothing(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRestoreInvalidManifestDoesNotCorruptExistingManifest(t *testing.T) {
|
func TestExecuteRestoreInvalidManifestDoesNotCorruptExistingManifest(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
base := &storage.FakeBackend{}
|
base := &storage.FakeBackend{}
|
||||||
cfg, sessionPrefix, manifestKey, _ := seedRestoreCommittedState(t, base, pipelinePath, sessionPath)
|
cfg, sessionPrefix, manifestKey, _ := seedRestoreCommittedState(t, base, pipelinePath, campaignPath, sessionPath)
|
||||||
seedRestoreObject(base, sessionPrefix+"transcripts/full.json", []byte("remote-transcript"))
|
seedRestoreObject(base, sessionPrefix+"transcripts/full.json", []byte("remote-transcript"))
|
||||||
|
|
||||||
toggled := &stagedManifestDownloadStore{
|
toggled := &stagedManifestDownloadStore{
|
||||||
@@ -260,7 +260,7 @@ func TestExecuteRestoreInvalidManifestDoesNotCorruptExistingManifest(t *testing.
|
|||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath, "--force"}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr)
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
t.Fatal("exit code = 0, want non-zero")
|
t.Fatal("exit code = 0, want non-zero")
|
||||||
}
|
}
|
||||||
@@ -344,9 +344,9 @@ func restoreWithStoreAndRealPhases(t *testing.T, objectStore storage.ObjectStore
|
|||||||
executeRestorePlanFn = executeRestorePlan
|
executeRestorePlanFn = executeRestorePlan
|
||||||
}
|
}
|
||||||
|
|
||||||
func seedRestoreCommittedState(t *testing.T, fake *storage.FakeBackend, pipelinePath, sessionPath string) (*config.Config, string, string, string) {
|
func seedRestoreCommittedState(t *testing.T, fake *storage.FakeBackend, pipelinePath, campaignPath, sessionPath string) (*config.Config, string, string, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cfg, err := config.LoadWithSessionOptions(pipelinePath, sessionPath, config.SessionLoadOptions{})
|
cfg, err := config.LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, config.SessionLoadOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ func TestExecuteRestoreHelp(t *testing.T) {
|
|||||||
if !strings.Contains(out, "--include-audio") {
|
if !strings.Contains(out, "--include-audio") {
|
||||||
t.Fatalf("stdout = %q, want --include-audio flag", out)
|
t.Fatalf("stdout = %q, want --include-audio flag", out)
|
||||||
}
|
}
|
||||||
|
if !strings.Contains(out, "--campaign") {
|
||||||
|
t.Fatalf("stdout = %q, want --campaign flag", out)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) {
|
func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) {
|
||||||
@@ -70,7 +73,7 @@ func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
@@ -78,6 +81,7 @@ func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) {
|
|||||||
[]string{
|
[]string{
|
||||||
"restore",
|
"restore",
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
|
"--campaign", campaignPath,
|
||||||
"--session", sessionPath,
|
"--session", sessionPath,
|
||||||
"--session-id", "2026-05-03",
|
"--session-id", "2026-05-03",
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
@@ -116,11 +120,11 @@ func TestExecuteRestoreRecognizedAndReturnsNYI(t *testing.T) {
|
|||||||
|
|
||||||
func TestExecuteRestoreRejectsUnexpectedPositionalArguments(t *testing.T) {
|
func TestExecuteRestoreRejectsUnexpectedPositionalArguments(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath, "extra"}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "extra"}, &stdout, &stderr)
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
t.Fatal("exit code = 0, want non-zero")
|
t.Fatal("exit code = 0, want non-zero")
|
||||||
}
|
}
|
||||||
@@ -138,11 +142,11 @@ func TestExecuteRestoreFailsWhenStorageBackendNotConfigured(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeRestoreConfigWithoutStorage(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeRestoreConfigWithoutStorage(t, workspaceRoot)
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
t.Fatal("exit code = 0, want non-zero")
|
t.Fatal("exit code = 0, want non-zero")
|
||||||
}
|
}
|
||||||
@@ -166,11 +170,11 @@ func TestExecuteRestoreDiscoveryErrorSurfaced(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
t.Fatal("exit code = 0, want non-zero")
|
t.Fatal("exit code = 0, want non-zero")
|
||||||
}
|
}
|
||||||
@@ -208,7 +212,7 @@ func TestExecuteRestoreLoadsSecretsBeforeObjectStoreInit(t *testing.T) {
|
|||||||
restoreEnv(secretKeyEnv)
|
restoreEnv(secretKeyEnv)
|
||||||
|
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
secretsDir := filepath.Join(t.TempDir(), "secrets")
|
secretsDir := filepath.Join(t.TempDir(), "secrets")
|
||||||
mustWriteTestFile(t, filepath.Join(secretsDir, accessKeyEnv), "test-access-key-id\n")
|
mustWriteTestFile(t, filepath.Join(secretsDir, accessKeyEnv), "test-access-key-id\n")
|
||||||
mustWriteTestFile(t, filepath.Join(secretsDir, secretKeyEnv), "test-secret-key\n")
|
mustWriteTestFile(t, filepath.Join(secretsDir, secretKeyEnv), "test-secret-key\n")
|
||||||
@@ -261,6 +265,7 @@ func TestExecuteRestoreLoadsSecretsBeforeObjectStoreInit(t *testing.T) {
|
|||||||
[]string{
|
[]string{
|
||||||
"restore",
|
"restore",
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
|
"--campaign", campaignPath,
|
||||||
"--session", sessionPath,
|
"--session", sessionPath,
|
||||||
"--session-id", "2026-05-03",
|
"--session-id", "2026-05-03",
|
||||||
"--dry-run",
|
"--dry-run",
|
||||||
@@ -307,10 +312,10 @@ func TestExecuteRestoreNonDryRunConflictFailsBeforeNYI(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &stdout, &stderr)
|
||||||
if code == 0 {
|
if code == 0 {
|
||||||
t.Fatal("exit code = 0, want non-zero")
|
t.Fatal("exit code = 0, want non-zero")
|
||||||
}
|
}
|
||||||
@@ -359,10 +364,10 @@ func TestExecuteRestoreNonDryRunForceExecutesPlan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
var stdout bytes.Buffer
|
var stdout bytes.Buffer
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Execute([]string{"restore", "--config", pipelinePath, "--session", sessionPath, "--force"}, &stdout, &stderr)
|
code := Execute([]string{"restore", "--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--force"}, &stdout, &stderr)
|
||||||
if code != 0 {
|
if code != 0 {
|
||||||
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
t.Fatalf("exit code = %d, want 0; stderr=%q", code, stderr.String())
|
||||||
}
|
}
|
||||||
@@ -374,11 +379,12 @@ func TestExecuteRestoreNonDryRunForceExecutesPlan(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeRestoreConfigWithoutStorage(t *testing.T, workspaceRoot string) (string, string) {
|
func writeRestoreConfigWithoutStorage(t *testing.T, workspaceRoot string) (string, string, string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||||
|
campaignPath := writeAppTestCampaignConfig(t, dir)
|
||||||
sessionPath := filepath.Join(dir, "session.yml")
|
sessionPath := filepath.Join(dir, "session.yml")
|
||||||
|
|
||||||
pipelineYAML := `workspace:
|
pipelineYAML := `workspace:
|
||||||
@@ -405,5 +411,5 @@ inputs:
|
|||||||
mustWriteTestFile(t, filepath.Join(dir, "glossary.yml"), "[]\n")
|
mustWriteTestFile(t, filepath.Join(dir, "glossary.yml"), "[]\n")
|
||||||
mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes")
|
mustWriteTestFile(t, filepath.Join(dir, "audio", "alice.flac"), "audio-bytes")
|
||||||
|
|
||||||
return pipelinePath, sessionPath
|
return pipelinePath, campaignPath, sessionPath
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,10 +19,10 @@ import (
|
|||||||
|
|
||||||
func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
|
func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFilesWithScriptoriumArtifacts(t, workspaceRoot)
|
||||||
|
|
||||||
fake := &storage.FakeBackend{}
|
fake := &storage.FakeBackend{}
|
||||||
cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fake, pipelinePath, sessionPath)
|
cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fake, pipelinePath, campaignPath, sessionPath)
|
||||||
seedRestoreObject(fake, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n"))
|
seedRestoreObject(fake, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n"))
|
||||||
seedRestoreObject(fake, manifestKey, restoreWorkflowManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign))
|
seedRestoreObject(fake, manifestKey, restoreWorkflowManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign))
|
||||||
seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte(`{"segments":[1,2,3]}`+"\n"))
|
seedRestoreObject(fake, sessionPrefix+"transcripts/full.json", []byte(`{"segments":[1,2,3]}`+"\n"))
|
||||||
@@ -48,6 +48,7 @@ func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
|
|||||||
[]string{
|
[]string{
|
||||||
"restore",
|
"restore",
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
|
"--campaign", campaignPath,
|
||||||
"--session", sessionPath,
|
"--session", sessionPath,
|
||||||
"--session-id", cfg.Session.SessionID,
|
"--session-id", cfg.Session.SessionID,
|
||||||
},
|
},
|
||||||
@@ -86,6 +87,7 @@ func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
|
|||||||
[]string{
|
[]string{
|
||||||
"run-stage",
|
"run-stage",
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
|
"--campaign", campaignPath,
|
||||||
"--session", sessionPath,
|
"--session", sessionPath,
|
||||||
"--session-id", cfg.Session.SessionID,
|
"--session-id", cfg.Session.SessionID,
|
||||||
"--force",
|
"--force",
|
||||||
@@ -157,7 +159,7 @@ func TestRestoreThenRunStageForceAnalyzeUsesRestoredDurableState(t *testing.T) {
|
|||||||
|
|
||||||
func TestRestoreThenAnalyzeUsesRestoredPreviousCacheWithoutObjectStore(t *testing.T) {
|
func TestRestoreThenAnalyzeUsesRestoredPreviousCacheWithoutObjectStore(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
appendRestoreWorkflowScriptoriumConfig(t, pipelinePath, `
|
appendRestoreWorkflowScriptoriumConfig(t, pipelinePath, `
|
||||||
scriptorium:
|
scriptorium:
|
||||||
binary: scriptorium
|
binary: scriptorium
|
||||||
@@ -176,7 +178,7 @@ scriptorium:
|
|||||||
`)
|
`)
|
||||||
|
|
||||||
fakeStore := &storage.FakeBackend{}
|
fakeStore := &storage.FakeBackend{}
|
||||||
cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fakeStore, pipelinePath, sessionPath)
|
cfg, sessionPrefix, manifestKey, runIDKey := seedRestoreCommittedState(t, fakeStore, pipelinePath, campaignPath, sessionPath)
|
||||||
seedRestoreObject(fakeStore, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n"))
|
seedRestoreObject(fakeStore, runIDKey, []byte("20260519T010203Z-a1b2c3d4\n"))
|
||||||
seedRestoreObject(fakeStore, manifestKey, restoreWorkflowManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign))
|
seedRestoreObject(fakeStore, manifestKey, restoreWorkflowManifestJSON(t, cfg.Session.SessionID, cfg.Session.Campaign))
|
||||||
seedRestoreObject(fakeStore, sessionPrefix+"transcripts/trimmed.json", []byte(`{"segments":[]}`+"\n"))
|
seedRestoreObject(fakeStore, sessionPrefix+"transcripts/trimmed.json", []byte(`{"segments":[]}`+"\n"))
|
||||||
@@ -191,6 +193,7 @@ scriptorium:
|
|||||||
[]string{
|
[]string{
|
||||||
"restore",
|
"restore",
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
|
"--campaign", campaignPath,
|
||||||
"--session", sessionPath,
|
"--session", sessionPath,
|
||||||
"--session-id", cfg.Session.SessionID,
|
"--session-id", cfg.Session.SessionID,
|
||||||
},
|
},
|
||||||
@@ -235,6 +238,7 @@ scriptorium:
|
|||||||
[]string{
|
[]string{
|
||||||
"run-stage",
|
"run-stage",
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
|
"--campaign", campaignPath,
|
||||||
"--session", sessionPath,
|
"--session", sessionPath,
|
||||||
"--session-id", cfg.Session.SessionID,
|
"--session-id", cfg.Session.SessionID,
|
||||||
"--force",
|
"--force",
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ func Resume(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
fs.SetOutput(io.Discard)
|
fs.SetOutput(io.Discard)
|
||||||
|
|
||||||
var pipelinePath string
|
var pipelinePath string
|
||||||
|
var campaignPath string
|
||||||
var sessionPath string
|
var sessionPath string
|
||||||
var sessionID string
|
var sessionID string
|
||||||
var previousSessionID string
|
var previousSessionID string
|
||||||
var force bool
|
var force bool
|
||||||
var selectedArtifacts artifactSelectionFlag
|
var selectedArtifacts artifactSelectionFlag
|
||||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||||
|
fs.StringVar(&campaignPath, "campaign", "", "path to campaign.yml (optional; defaults searched)")
|
||||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||||
fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates")
|
fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates")
|
||||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier for session.yml templates")
|
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier for session.yml templates")
|
||||||
@@ -39,12 +41,16 @@ func Resume(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resume: %w", err)
|
return fmt.Errorf("resume: %w", err)
|
||||||
}
|
}
|
||||||
|
resolvedCampaignPath, err := resolveCampaignConfigPath(campaignPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("resume: %w", err)
|
||||||
|
}
|
||||||
resolvedSessionPath, err := resolveSessionConfigPath(sessionPath)
|
resolvedSessionPath, err := resolveSessionConfigPath(sessionPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("resume: %w", err)
|
return fmt.Errorf("resume: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedSessionPath, config.SessionLoadOptions{
|
cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedCampaignPath, resolvedSessionPath, config.SessionLoadOptions{
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
PreviousSessionID: previousSessionID,
|
PreviousSessionID: previousSessionID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
func TestResumeStartsAfterCompletedStages(t *testing.T) {
|
func TestResumeStartsAfterCompletedStages(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
|
|
||||||
store := &manifest.LocalStore{}
|
store := &manifest.LocalStore{}
|
||||||
@@ -32,7 +32,7 @@ func TestResumeStartsAfterCompletedStages(t *testing.T) {
|
|||||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
|
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := Resume(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath}, &out)
|
err := Resume(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Resume() error = %v", err)
|
t.Fatalf("Resume() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -51,7 +51,7 @@ func TestResumeStartsAfterCompletedStages(t *testing.T) {
|
|||||||
|
|
||||||
func TestResumeNoRemainingStages(t *testing.T) {
|
func TestResumeNoRemainingStages(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
|
|
||||||
store := &manifest.LocalStore{}
|
store := &manifest.LocalStore{}
|
||||||
@@ -64,7 +64,7 @@ func TestResumeNoRemainingStages(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := Resume(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath}, &out)
|
err := Resume(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Resume() error = %v", err)
|
t.Fatalf("Resume() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ func TestResumeForceRerunsSucceeded(t *testing.T) {
|
|||||||
_, _ = w.Write([]byte(`{"source":"resume-force-test","segments":[{"speaker":"alice"}]}`))
|
_, _ = w.Write([]byte(`{"source":"resume-force-test","segments":[{"speaker":"alice"}]}`))
|
||||||
}))
|
}))
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot, srv.URL)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
|
|
||||||
store := &manifest.LocalStore{}
|
store := &manifest.LocalStore{}
|
||||||
@@ -93,7 +93,7 @@ func TestResumeForceRerunsSucceeded(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := Resume(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "--force"}, &out)
|
err := Resume(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--force"}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Resume() error = %v", err)
|
t.Fatalf("Resume() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -104,14 +104,14 @@ func TestResumeForceRerunsSucceeded(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunStageExecutesOnlySelectedStage(t *testing.T) {
|
func TestRunStageExecutesOnlySelectedStage(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`)
|
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`)
|
||||||
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
|
mustWriteTestFile(t, filepath.Join(workRoot, "inputs", "glossary.yml"), "terms: []\n")
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "polish"}, &out)
|
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "polish"}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("RunStage() error = %v", err)
|
t.Fatalf("RunStage() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ func TestRunStageExecutesOnlySelectedStage(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunStageSkipAndForce(t *testing.T) {
|
func TestRunStageSkipAndForce(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`)
|
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`)
|
||||||
@@ -148,7 +148,7 @@ func TestRunStageSkipAndForce(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "polish"}, &out)
|
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "polish"}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("RunStage() error = %v", err)
|
t.Fatalf("RunStage() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,7 @@ func TestRunStageSkipAndForce(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
out.Reset()
|
out.Reset()
|
||||||
err = RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "--force", "polish"}, &out)
|
err = RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--force", "polish"}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("RunStage(force) error = %v", err)
|
t.Fatalf("RunStage(force) error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -168,7 +168,7 @@ func TestRunStageSkipAndForce(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunStageForceMarksDownstreamStaleAndResumeContinuesFromStale(t *testing.T) {
|
func TestRunStageForceMarksDownstreamStaleAndResumeContinuesFromStale(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`)
|
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "merged.json"), `{"segments":[]}`)
|
||||||
@@ -184,7 +184,7 @@ func TestRunStageForceMarksDownstreamStaleAndResumeContinuesFromStale(t *testing
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "--force", "polish"}, &out)
|
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--force", "polish"}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("RunStage(force) error = %v", err)
|
t.Fatalf("RunStage(force) error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -203,7 +203,7 @@ func TestRunStageForceMarksDownstreamStaleAndResumeContinuesFromStale(t *testing
|
|||||||
}
|
}
|
||||||
|
|
||||||
out.Reset()
|
out.Reset()
|
||||||
err = Resume(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath}, &out)
|
err = Resume(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Resume() error = %v", err)
|
t.Fatalf("Resume() error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -214,13 +214,13 @@ func TestRunStageForceMarksDownstreamStaleAndResumeContinuesFromStale(t *testing
|
|||||||
|
|
||||||
func TestRunStageTrimExecutes(t *testing.T) {
|
func TestRunStageTrimExecutes(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "normalized.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "trim"}, &out)
|
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "trim"}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("RunStage(trim) error = %v", err)
|
t.Fatalf("RunStage(trim) error = %v", err)
|
||||||
}
|
}
|
||||||
@@ -243,13 +243,13 @@ func TestRunStageTrimExecutes(t *testing.T) {
|
|||||||
|
|
||||||
func TestRunStageNormalizeExecutes(t *testing.T) {
|
func TestRunStageNormalizeExecutes(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
manifestPath := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03", "manifest.json")
|
||||||
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
workRoot := filepath.Join(workspaceRoot, "work", "sample-campaign", "2026-05-03")
|
||||||
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
mustWriteTestFile(t, filepath.Join(workRoot, "transcripts", "processed.json"), `{"segments":[{"id":1},{"id":2}]}`)
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "normalize"}, &out)
|
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "normalize"}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("RunStage(normalize) error = %v", err)
|
t.Fatalf("RunStage(normalize) error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ func Run(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
fs.SetOutput(io.Discard)
|
fs.SetOutput(io.Discard)
|
||||||
|
|
||||||
var pipelinePath string
|
var pipelinePath string
|
||||||
|
var campaignPath string
|
||||||
var sessionPath string
|
var sessionPath string
|
||||||
var sessionID string
|
var sessionID string
|
||||||
var previousSessionID string
|
var previousSessionID string
|
||||||
var force bool
|
var force bool
|
||||||
var selectedArtifacts artifactSelectionFlag
|
var selectedArtifacts artifactSelectionFlag
|
||||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||||
|
fs.StringVar(&campaignPath, "campaign", "", "path to campaign.yml (optional; defaults searched)")
|
||||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||||
fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates")
|
fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates")
|
||||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier for session.yml templates")
|
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier for session.yml templates")
|
||||||
@@ -37,12 +39,16 @@ func Run(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run: %w", err)
|
return fmt.Errorf("run: %w", err)
|
||||||
}
|
}
|
||||||
|
resolvedCampaignPath, err := resolveCampaignConfigPath(campaignPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("run: %w", err)
|
||||||
|
}
|
||||||
resolvedSessionPath, err := resolveSessionConfigPath(sessionPath)
|
resolvedSessionPath, err := resolveSessionConfigPath(sessionPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run: %w", err)
|
return fmt.Errorf("run: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedSessionPath, config.SessionLoadOptions{
|
cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedCampaignPath, resolvedSessionPath, config.SessionLoadOptions{
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
PreviousSessionID: previousSessionID,
|
PreviousSessionID: previousSessionID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -15,12 +15,14 @@ func RunStage(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
fs.SetOutput(io.Discard)
|
fs.SetOutput(io.Discard)
|
||||||
|
|
||||||
var pipelinePath string
|
var pipelinePath string
|
||||||
|
var campaignPath string
|
||||||
var sessionPath string
|
var sessionPath string
|
||||||
var sessionID string
|
var sessionID string
|
||||||
var previousSessionID string
|
var previousSessionID string
|
||||||
var force bool
|
var force bool
|
||||||
var selectedArtifacts artifactSelectionFlag
|
var selectedArtifacts artifactSelectionFlag
|
||||||
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
fs.StringVar(&pipelinePath, "config", "", "path to pipeline.yml (optional; defaults searched)")
|
||||||
|
fs.StringVar(&campaignPath, "campaign", "", "path to campaign.yml (optional; defaults searched)")
|
||||||
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
fs.StringVar(&sessionPath, "session", "", "path to session.yml")
|
||||||
fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates")
|
fs.StringVar(&sessionID, "session-id", "", "session identifier for session.yml templates")
|
||||||
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier for session.yml templates")
|
fs.StringVar(&previousSessionID, "previous-session-id", "", "previous session identifier for session.yml templates")
|
||||||
@@ -50,12 +52,16 @@ func RunStage(ctx context.Context, args []string, out io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run-stage: %w", err)
|
return fmt.Errorf("run-stage: %w", err)
|
||||||
}
|
}
|
||||||
|
resolvedCampaignPath, err := resolveCampaignConfigPath(campaignPath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("run-stage: %w", err)
|
||||||
|
}
|
||||||
resolvedSessionPath, err := resolveSessionConfigPath(sessionPath)
|
resolvedSessionPath, err := resolveSessionConfigPath(sessionPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("run-stage: %w", err)
|
return fmt.Errorf("run-stage: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedSessionPath, config.SessionLoadOptions{
|
cfg, err := config.LoadWithSessionOptions(resolvedPipelinePath, resolvedCampaignPath, resolvedSessionPath, config.SessionLoadOptions{
|
||||||
SessionID: sessionID,
|
SessionID: sessionID,
|
||||||
PreviousSessionID: previousSessionID,
|
PreviousSessionID: previousSessionID,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -994,10 +994,12 @@ func testConfig(t *testing.T) *config.Config {
|
|||||||
workspace := t.TempDir()
|
workspace := t.TempDir()
|
||||||
cfgDir := t.TempDir()
|
cfgDir := t.TempDir()
|
||||||
sessionPath := filepath.Join(cfgDir, "session.yml")
|
sessionPath := filepath.Join(cfgDir, "session.yml")
|
||||||
|
campaignPath := filepath.Join(cfgDir, "campaign.yml")
|
||||||
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
||||||
|
|
||||||
mustWriteFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
mustWriteFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
||||||
mustWriteFile(t, sessionPath, "session_id: 2026-05-03\ncampaign: sample-campaign\n")
|
mustWriteFile(t, campaignPath, "campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.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, "speakers.yml"), "alice: alice.flac\n")
|
||||||
mustWriteFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
mustWriteFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||||
mustWriteFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
mustWriteFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
||||||
@@ -1005,8 +1007,27 @@ func testConfig(t *testing.T) *config.Config {
|
|||||||
|
|
||||||
return &config.Config{
|
return &config.Config{
|
||||||
Pipeline: &config.PipelineConfig{Workspace: config.WorkspaceConfig{Root: workspace}},
|
Pipeline: &config.PipelineConfig{Workspace: config.WorkspaceConfig{Root: workspace}},
|
||||||
|
Campaign: &config.CampaignConfig{Campaign: "sample-campaign"},
|
||||||
PipelinePath: pipelinePath,
|
PipelinePath: pipelinePath,
|
||||||
|
CampaignPath: campaignPath,
|
||||||
SessionPath: sessionPath,
|
SessionPath: sessionPath,
|
||||||
|
StableInputs: config.ResolvedStableInputs{
|
||||||
|
SpeakersFile: config.ResolvedInputFile{
|
||||||
|
Path: "./speakers.yml",
|
||||||
|
ConfigPath: campaignPath,
|
||||||
|
Source: "campaign_config",
|
||||||
|
},
|
||||||
|
AutocorrectFile: config.ResolvedInputFile{
|
||||||
|
Path: "./autocorrect.yml",
|
||||||
|
ConfigPath: campaignPath,
|
||||||
|
Source: "campaign_config",
|
||||||
|
},
|
||||||
|
GlossaryFile: config.ResolvedInputFile{
|
||||||
|
Path: "./glossary.yml",
|
||||||
|
ConfigPath: campaignPath,
|
||||||
|
Source: "campaign_config",
|
||||||
|
},
|
||||||
|
},
|
||||||
Session: &config.SessionConfig{
|
Session: &config.SessionConfig{
|
||||||
SessionID: "2026-05-03",
|
SessionID: "2026-05-03",
|
||||||
Campaign: "sample-campaign",
|
Campaign: "sample-campaign",
|
||||||
@@ -1023,6 +1044,7 @@ func testConfig(t *testing.T) *config.Config {
|
|||||||
func TestBuildDefaultRunnersWithOmittedToolSections(t *testing.T) {
|
func TestBuildDefaultRunnersWithOmittedToolSections(t *testing.T) {
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||||
|
campaignPath := filepath.Join(dir, "campaign.yml")
|
||||||
sessionPath := filepath.Join(dir, "session.yml")
|
sessionPath := filepath.Join(dir, "session.yml")
|
||||||
pipelineYAML := `workspace:
|
pipelineYAML := `workspace:
|
||||||
root: ` + t.TempDir() + `
|
root: ` + t.TempDir() + `
|
||||||
@@ -1032,6 +1054,12 @@ analyzer:
|
|||||||
timeout: 20m
|
timeout: 20m
|
||||||
notification:
|
notification:
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
|
`
|
||||||
|
campaignYAML := `campaign: sample-campaign
|
||||||
|
inputs:
|
||||||
|
speakers_file: ./speakers.yml
|
||||||
|
autocorrect_file: ./autocorrect.yml
|
||||||
|
glossary_file: ./glossary.yml
|
||||||
`
|
`
|
||||||
sessionYAML := `session_id: 2026-05-03
|
sessionYAML := `session_id: 2026-05-03
|
||||||
campaign: sample-campaign
|
campaign: sample-campaign
|
||||||
@@ -1042,6 +1070,7 @@ inputs:
|
|||||||
glossary_file: ./glossary.yml
|
glossary_file: ./glossary.yml
|
||||||
`
|
`
|
||||||
mustWriteFile(t, pipelinePath, pipelineYAML)
|
mustWriteFile(t, pipelinePath, pipelineYAML)
|
||||||
|
mustWriteFile(t, campaignPath, campaignYAML)
|
||||||
mustWriteFile(t, sessionPath, sessionYAML)
|
mustWriteFile(t, sessionPath, sessionYAML)
|
||||||
|
|
||||||
cfg, err := config.Load(pipelinePath, sessionPath)
|
cfg, err := config.Load(pipelinePath, sessionPath)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
|
|
||||||
func TestPlanUsesDiscoveredSessionTemplateWithSessionIDs(t *testing.T) {
|
func TestPlanUsesDiscoveredSessionTemplateWithSessionIDs(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, _, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
sessionTemplate := `session_id: "{{ session_id }}"
|
sessionTemplate := `session_id: "{{ session_id }}"
|
||||||
previous_session_id: "{{ previous_session_id }}"
|
previous_session_id: "{{ previous_session_id }}"
|
||||||
@@ -51,10 +51,10 @@ inputs:
|
|||||||
|
|
||||||
func TestPlanFailsWhenSessionIDMismatchesConcreteSession(t *testing.T) {
|
func TestPlanFailsWhenSessionIDMismatchesConcreteSession(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := Plan(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "--session-id", "2026-04-04"}, &out)
|
err := Plan(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--session-id", "2026-04-04"}, &out)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Fatal("expected error, got nil")
|
t.Fatal("expected error, got nil")
|
||||||
}
|
}
|
||||||
@@ -65,7 +65,7 @@ func TestPlanFailsWhenSessionIDMismatchesConcreteSession(t *testing.T) {
|
|||||||
|
|
||||||
func TestPlanFailsWhenPreviousSessionIDMismatchesConcreteSession(t *testing.T) {
|
func TestPlanFailsWhenPreviousSessionIDMismatchesConcreteSession(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
sessionYAML := `session_id: 2026-05-03
|
sessionYAML := `session_id: 2026-05-03
|
||||||
previous_session_id: 2026-04-26
|
previous_session_id: 2026-04-26
|
||||||
@@ -83,6 +83,7 @@ inputs:
|
|||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := Plan(context.Background(), []string{
|
err := Plan(context.Background(), []string{
|
||||||
"--config", pipelinePath,
|
"--config", pipelinePath,
|
||||||
|
"--campaign", campaignPath,
|
||||||
"--session", sessionPath,
|
"--session", sessionPath,
|
||||||
"--session-id", "2026-05-03",
|
"--session-id", "2026-05-03",
|
||||||
"--previous-session-id", "2026-04-25",
|
"--previous-session-id", "2026-04-25",
|
||||||
@@ -97,10 +98,10 @@ inputs:
|
|||||||
|
|
||||||
func TestRunStageAcceptsSessionIDFlagAndParsesStageName(t *testing.T) {
|
func TestRunStageAcceptsSessionIDFlagAndParsesStageName(t *testing.T) {
|
||||||
workspaceRoot := t.TempDir()
|
workspaceRoot := t.TempDir()
|
||||||
pipelinePath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
pipelinePath, campaignPath, sessionPath := writeValidConfigFiles(t, workspaceRoot)
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--session", sessionPath, "--session-id", "2026-05-03", "prepare"}, &out)
|
err := RunStage(context.Background(), []string{"--config", pipelinePath, "--campaign", campaignPath, "--session", sessionPath, "--session-id", "2026-05-03", "prepare"}, &out)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("RunStage() error = %v", err)
|
t.Fatalf("RunStage() error = %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
138
internal/config/campaign_config_test.go
Normal file
138
internal/config/campaign_config_test.go
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCampaignConfigDefaultSearchOrder(t *testing.T) {
|
||||||
|
want := []string{
|
||||||
|
"./campaign.yml",
|
||||||
|
"/usr/local/etc/narratio/campaign.yml",
|
||||||
|
"/etc/narratio/campaign.yml",
|
||||||
|
}
|
||||||
|
if len(DefaultCampaignConfigSearchPaths) != len(want) {
|
||||||
|
t.Fatalf("DefaultCampaignConfigSearchPaths = %#v, want %#v", DefaultCampaignConfigSearchPaths, want)
|
||||||
|
}
|
||||||
|
for i := range want {
|
||||||
|
if DefaultCampaignConfigSearchPaths[i] != want[i] {
|
||||||
|
t.Fatalf("DefaultCampaignConfigSearchPaths[%d] = %q, want %q", i, DefaultCampaignConfigSearchPaths[i], want[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCampaignStrictDecodeRejectsUnknownFields(t *testing.T) {
|
||||||
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||||
|
"campaign: sample-campaign\nunknown: true\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.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(), "campaign file") || !strings.Contains(err.Error(), "strict decode failed") {
|
||||||
|
t.Fatalf("error = %q, want campaign strict decode context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCampaignSessionMergeFillsStableInputs(t *testing.T) {
|
||||||
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||||
|
"campaign: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.yml\n",
|
||||||
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||||
|
)
|
||||||
|
|
||||||
|
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.Session.Campaign != "sample-campaign" {
|
||||||
|
t.Fatalf("session campaign = %q, want campaign config value", cfg.Session.Campaign)
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCampaignSessionMergeSessionOverridesStableInputs(t *testing.T) {
|
||||||
|
pipelinePath, campaignPath, sessionPath := writeCampaignConfigTestFiles(t,
|
||||||
|
"campaign: sample-campaign\ninputs:\n speakers_file: ./campaign-speakers.yml\n autocorrect_file: ./campaign-autocorrect.yml\n glossary_file: ./campaign-glossary.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{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("LoadWithSessionOptions() error = %v", err)
|
||||||
|
}
|
||||||
|
if err := Validate(cfg); err != nil {
|
||||||
|
t.Fatalf("Validate() error = %v", err)
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCampaignSessionMismatchFails(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",
|
||||||
|
"session_id: 2026-05-03\ncampaign: other-campaign\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(), "does not match campaign config") {
|
||||||
|
t.Fatalf("error = %q, want campaign mismatch context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadMissingCampaignFileFails(t *testing.T) {
|
||||||
|
pipelinePath, _, sessionPath := writeCampaignConfigTestFiles(t,
|
||||||
|
"campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n",
|
||||||
|
"session_id: 2026-05-03\ninputs:\n audio_dir: ./audio\n",
|
||||||
|
)
|
||||||
|
missingCampaignPath := filepath.Join(filepath.Dir(sessionPath), "missing-campaign.yml")
|
||||||
|
|
||||||
|
_, err := LoadWithSessionOptions(pipelinePath, missingCampaignPath, sessionPath, SessionLoadOptions{})
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected load error, got nil")
|
||||||
|
}
|
||||||
|
if !strings.Contains(err.Error(), "load campaign config") {
|
||||||
|
t.Fatalf("error = %q, want campaign load context", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeCampaignConfigTestFiles(t *testing.T, campaignYAML, sessionYAML string) (string, string, string) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
dir := t.TempDir()
|
||||||
|
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||||
|
campaignPath := filepath.Join(dir, "campaign.yml")
|
||||||
|
sessionPath := filepath.Join(dir, "session.yml")
|
||||||
|
|
||||||
|
pipelineYAML := "workspace:\n root: " + filepath.ToSlash(filepath.Join(dir, "work")) + "\nwhisperx:\n transcribe_url: https://example.com/transcribe\nanalyzer:\n timeout: 20m\nnotification:\n timeout: 10s\n"
|
||||||
|
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||||
|
t.Fatalf("write pipeline.yml: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||||
|
t.Fatalf("write campaign.yml: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||||
|
t.Fatalf("write session.yml: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pipelinePath, campaignPath, sessionPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func assertResolvedStableInput(t *testing.T, got ResolvedInputFile, wantPath, wantConfigPath, wantSource string) {
|
||||||
|
t.Helper()
|
||||||
|
if got.Path != wantPath || got.ConfigPath != wantConfigPath || got.Source != wantSource {
|
||||||
|
t.Fatalf("resolved input = %#v, want path=%q config_path=%q source=%q", got, wantPath, wantConfigPath, wantSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
// Config is the resolved combined configuration from pipeline.yml and session.yml.
|
// Config is the resolved combined configuration from pipeline.yml,
|
||||||
|
// campaign.yml, and session.yml.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Pipeline *PipelineConfig
|
Pipeline *PipelineConfig
|
||||||
|
Campaign *CampaignConfig
|
||||||
Session *SessionConfig
|
Session *SessionConfig
|
||||||
PipelinePath string
|
PipelinePath string
|
||||||
|
CampaignPath string
|
||||||
SessionPath string
|
SessionPath string
|
||||||
|
|
||||||
|
StableInputs ResolvedStableInputs
|
||||||
}
|
}
|
||||||
|
|
||||||
// PipelineConfig contains durable pipeline-level settings.
|
// PipelineConfig contains durable pipeline-level settings.
|
||||||
@@ -25,6 +30,19 @@ type PipelineConfig struct {
|
|||||||
Notification NotificationConfig `yaml:"notification"`
|
Notification NotificationConfig `yaml:"notification"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CampaignConfig contains stable campaign-level identity and input defaults.
|
||||||
|
type CampaignConfig struct {
|
||||||
|
Campaign string `yaml:"campaign"`
|
||||||
|
Inputs CampaignInputsConfig `yaml:"inputs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CampaignInputsConfig contains stable campaign-level input file references.
|
||||||
|
type CampaignInputsConfig struct {
|
||||||
|
SpeakersFile string `yaml:"speakers_file"`
|
||||||
|
AutocorrectFile string `yaml:"autocorrect_file"`
|
||||||
|
GlossaryFile string `yaml:"glossary_file"`
|
||||||
|
}
|
||||||
|
|
||||||
// SessionConfig contains per-session inputs and metadata.
|
// SessionConfig contains per-session inputs and metadata.
|
||||||
type SessionConfig struct {
|
type SessionConfig struct {
|
||||||
SessionID string `yaml:"session_id"`
|
SessionID string `yaml:"session_id"`
|
||||||
@@ -229,3 +247,18 @@ type SessionInputsConfig struct {
|
|||||||
type SessionAudioS3Input struct {
|
type SessionAudioS3Input struct {
|
||||||
Prefix string `yaml:"prefix"`
|
Prefix string `yaml:"prefix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolvedStableInputs records where stable input file paths came from after
|
||||||
|
// campaign/session merge.
|
||||||
|
type ResolvedStableInputs struct {
|
||||||
|
SpeakersFile ResolvedInputFile
|
||||||
|
AutocorrectFile ResolvedInputFile
|
||||||
|
GlossaryFile ResolvedInputFile
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvedInputFile records one merged config path and its source config file.
|
||||||
|
type ResolvedInputFile struct {
|
||||||
|
Path string
|
||||||
|
ConfigPath string
|
||||||
|
Source string
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ package config
|
|||||||
const (
|
const (
|
||||||
DefaultPipelineConfigPathUsrLocal = "/usr/local/etc/narratio/pipeline.yml"
|
DefaultPipelineConfigPathUsrLocal = "/usr/local/etc/narratio/pipeline.yml"
|
||||||
DefaultPipelineConfigPathEtc = "/etc/narratio/pipeline.yml"
|
DefaultPipelineConfigPathEtc = "/etc/narratio/pipeline.yml"
|
||||||
|
DefaultCampaignConfigPathLocal = "./campaign.yml"
|
||||||
|
DefaultCampaignConfigPathUsrLocal = "/usr/local/etc/narratio/campaign.yml"
|
||||||
|
DefaultCampaignConfigPathEtc = "/etc/narratio/campaign.yml"
|
||||||
DefaultSessionConfigPathLocal = "./session.yml"
|
DefaultSessionConfigPathLocal = "./session.yml"
|
||||||
DefaultSessionConfigPathUsrLocal = "/usr/local/etc/narratio/session.yml"
|
DefaultSessionConfigPathUsrLocal = "/usr/local/etc/narratio/session.yml"
|
||||||
DefaultSessionConfigPathEtc = "/etc/narratio/session.yml"
|
DefaultSessionConfigPathEtc = "/etc/narratio/session.yml"
|
||||||
@@ -88,6 +91,17 @@ var DefaultPipelineConfigSearchPaths = []string{
|
|||||||
DefaultPipelineConfigPathEtc,
|
DefaultPipelineConfigPathEtc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DefaultCampaignConfigSearchPaths defines the default search order for
|
||||||
|
// campaign.yml when callers do not provide an explicit path.
|
||||||
|
//
|
||||||
|
// Keep this in a variable so future defaults can be extended without changing
|
||||||
|
// call sites.
|
||||||
|
var DefaultCampaignConfigSearchPaths = []string{
|
||||||
|
DefaultCampaignConfigPathLocal,
|
||||||
|
DefaultCampaignConfigPathUsrLocal,
|
||||||
|
DefaultCampaignConfigPathEtc,
|
||||||
|
}
|
||||||
|
|
||||||
// DefaultSessionConfigSearchPaths defines the default search order for
|
// DefaultSessionConfigSearchPaths defines the default search order for
|
||||||
// session.yml when callers do not provide an explicit path.
|
// session.yml when callers do not provide an explicit path.
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -22,6 +22,15 @@ func LoadPipeline(path string) (*PipelineConfig, error) {
|
|||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadCampaign loads campaign configuration from a YAML file with strict field checking.
|
||||||
|
func LoadCampaign(path string) (*CampaignConfig, error) {
|
||||||
|
var cfg CampaignConfig
|
||||||
|
if err := decodeStrictYAML("campaign", path, &cfg); err != nil {
|
||||||
|
return nil, fmt.Errorf("load campaign config: %w", err)
|
||||||
|
}
|
||||||
|
return &cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
// LoadSession loads session configuration from a YAML file with strict field checking.
|
// LoadSession loads session configuration from a YAML file with strict field checking.
|
||||||
func LoadSession(path string) (*SessionConfig, error) {
|
func LoadSession(path string) (*SessionConfig, error) {
|
||||||
return LoadSessionWithOptions(path, SessionLoadOptions{})
|
return LoadSessionWithOptions(path, SessionLoadOptions{})
|
||||||
@@ -71,32 +80,128 @@ func LoadSessionWithOptions(path string, opts SessionLoadOptions) (*SessionConfi
|
|||||||
return &cfg, nil
|
return &cfg, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load loads and resolves combined pipeline and session configuration.
|
// Load loads and resolves combined pipeline, campaign, and session configuration.
|
||||||
func Load(pipelinePath, sessionPath string) (*Config, error) {
|
// Passing only a session path is supported for package-internal compatibility;
|
||||||
return LoadWithSessionOptions(pipelinePath, sessionPath, SessionLoadOptions{})
|
// in that form campaign.yml is expected next to the session file.
|
||||||
|
func Load(pipelinePath string, paths ...string) (*Config, error) {
|
||||||
|
campaignPath, sessionPath, err := campaignSessionPaths(paths...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, SessionLoadOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadWithSessionOptions loads and resolves combined pipeline and session
|
// LoadWithSessionOptions loads and resolves combined pipeline, campaign, and
|
||||||
// configuration with session template options.
|
// session configuration with session template options.
|
||||||
func LoadWithSessionOptions(pipelinePath, sessionPath string, sessionOpts SessionLoadOptions) (*Config, error) {
|
func LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath string, sessionOpts SessionLoadOptions) (*Config, error) {
|
||||||
pipelineCfg, err := LoadPipeline(pipelinePath)
|
pipelineCfg, err := LoadPipeline(pipelinePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
campaignCfg, err := LoadCampaign(campaignPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
sessionCfg, err := LoadSessionWithOptions(sessionPath, sessionOpts)
|
sessionCfg, err := LoadSessionWithOptions(sessionPath, sessionOpts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stableInputs, err := mergeCampaignSession(campaignCfg, sessionCfg, campaignPath, sessionPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
Pipeline: pipelineCfg,
|
Pipeline: pipelineCfg,
|
||||||
|
Campaign: campaignCfg,
|
||||||
Session: sessionCfg,
|
Session: sessionCfg,
|
||||||
PipelinePath: pipelinePath,
|
PipelinePath: pipelinePath,
|
||||||
|
CampaignPath: campaignPath,
|
||||||
SessionPath: sessionPath,
|
SessionPath: sessionPath,
|
||||||
|
StableInputs: stableInputs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func campaignSessionPaths(paths ...string) (campaignPath, sessionPath string, err error) {
|
||||||
|
switch len(paths) {
|
||||||
|
case 1:
|
||||||
|
sessionPath = paths[0]
|
||||||
|
campaignPath = filepath.Join(filepath.Dir(sessionPath), "campaign.yml")
|
||||||
|
case 2:
|
||||||
|
campaignPath = paths[0]
|
||||||
|
sessionPath = paths[1]
|
||||||
|
default:
|
||||||
|
return "", "", fmt.Errorf("load config: expected session path or campaign and session paths")
|
||||||
|
}
|
||||||
|
return campaignPath, sessionPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func mergeCampaignSession(campaignCfg *CampaignConfig, sessionCfg *SessionConfig, campaignPath, sessionPath string) (ResolvedStableInputs, error) {
|
||||||
|
if campaignCfg == nil {
|
||||||
|
return ResolvedStableInputs{}, fmt.Errorf("campaign config is required")
|
||||||
|
}
|
||||||
|
if sessionCfg == nil {
|
||||||
|
return ResolvedStableInputs{}, fmt.Errorf("session config is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
campaignName := strings.TrimSpace(campaignCfg.Campaign)
|
||||||
|
sessionCampaign := strings.TrimSpace(sessionCfg.Campaign)
|
||||||
|
if sessionCampaign != "" && campaignName != "" && sessionCampaign != campaignName {
|
||||||
|
return ResolvedStableInputs{}, fmt.Errorf(
|
||||||
|
"campaign/session config invalid: session campaign %q does not match campaign config %q",
|
||||||
|
sessionCampaign,
|
||||||
|
campaignName,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if sessionCampaign == "" {
|
||||||
|
sessionCfg.Campaign = campaignName
|
||||||
|
}
|
||||||
|
|
||||||
|
stable := ResolvedStableInputs{
|
||||||
|
SpeakersFile: selectStableInput(
|
||||||
|
campaignCfg.Inputs.SpeakersFile,
|
||||||
|
sessionCfg.Inputs.SpeakersFile,
|
||||||
|
campaignPath,
|
||||||
|
sessionPath,
|
||||||
|
),
|
||||||
|
AutocorrectFile: selectStableInput(
|
||||||
|
campaignCfg.Inputs.AutocorrectFile,
|
||||||
|
sessionCfg.Inputs.AutocorrectFile,
|
||||||
|
campaignPath,
|
||||||
|
sessionPath,
|
||||||
|
),
|
||||||
|
GlossaryFile: selectStableInput(
|
||||||
|
campaignCfg.Inputs.GlossaryFile,
|
||||||
|
sessionCfg.Inputs.GlossaryFile,
|
||||||
|
campaignPath,
|
||||||
|
sessionPath,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionCfg.Inputs.SpeakersFile = stable.SpeakersFile.Path
|
||||||
|
sessionCfg.Inputs.AutocorrectFile = stable.AutocorrectFile.Path
|
||||||
|
sessionCfg.Inputs.GlossaryFile = stable.GlossaryFile.Path
|
||||||
|
return stable, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func selectStableInput(campaignValue, sessionValue, campaignPath, sessionPath string) ResolvedInputFile {
|
||||||
|
if strings.TrimSpace(sessionValue) != "" {
|
||||||
|
return ResolvedInputFile{
|
||||||
|
Path: sessionValue,
|
||||||
|
ConfigPath: sessionPath,
|
||||||
|
Source: "session_config",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ResolvedInputFile{
|
||||||
|
Path: campaignValue,
|
||||||
|
ConfigPath: campaignPath,
|
||||||
|
Source: "campaign_config",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func decodeStrictYAML(kind, path string, out any) error {
|
func decodeStrictYAML(kind, path string, out any) error {
|
||||||
f, err := os.Open(path)
|
f, err := os.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -904,6 +904,7 @@ func TestValidateMissingAudioSource(t *testing.T) {
|
|||||||
Report: boolPtr(true),
|
Report: boolPtr(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
Campaign: &CampaignConfig{Campaign: "sample-campaign"},
|
||||||
Session: &SessionConfig{
|
Session: &SessionConfig{
|
||||||
SessionID: "2026-05-03",
|
SessionID: "2026-05-03",
|
||||||
Campaign: "sample-campaign",
|
Campaign: "sample-campaign",
|
||||||
@@ -963,6 +964,7 @@ func TestExamplesLoadAndValidate(t *testing.T) {
|
|||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
pipelinePath := filepath.Join(examplesDir, tt.pipelineFile)
|
pipelinePath := filepath.Join(examplesDir, tt.pipelineFile)
|
||||||
|
campaignPath := filepath.Join(examplesDir, "campaign.yml")
|
||||||
sessionPath := filepath.Join(examplesDir, tt.sessionFile)
|
sessionPath := filepath.Join(examplesDir, tt.sessionFile)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -972,7 +974,7 @@ func TestExamplesLoadAndValidate(t *testing.T) {
|
|||||||
if strings.TrimSpace(tt.sessionOpts.SessionID) == "" {
|
if strings.TrimSpace(tt.sessionOpts.SessionID) == "" {
|
||||||
cfg, err = Load(pipelinePath, sessionPath)
|
cfg, err = Load(pipelinePath, sessionPath)
|
||||||
} else {
|
} else {
|
||||||
cfg, err = LoadWithSessionOptions(pipelinePath, sessionPath, tt.sessionOpts)
|
cfg, err = LoadWithSessionOptions(pipelinePath, campaignPath, sessionPath, tt.sessionOpts)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("load example config error = %v", err)
|
t.Fatalf("load example config error = %v", err)
|
||||||
@@ -1001,14 +1003,34 @@ func writeConfigFiles(t *testing.T, pipelineYAML, sessionYAML string) (string, s
|
|||||||
|
|
||||||
dir := t.TempDir()
|
dir := t.TempDir()
|
||||||
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
pipelinePath := filepath.Join(dir, "pipeline.yml")
|
||||||
|
campaignPath := filepath.Join(dir, "campaign.yml")
|
||||||
sessionPath := filepath.Join(dir, "session.yml")
|
sessionPath := filepath.Join(dir, "session.yml")
|
||||||
|
|
||||||
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
if err := os.WriteFile(pipelinePath, []byte(pipelineYAML), 0o644); err != nil {
|
||||||
t.Fatalf("write pipeline.yml: %v", err)
|
t.Fatalf("write pipeline.yml: %v", err)
|
||||||
}
|
}
|
||||||
|
campaignYAML := `campaign: ` + campaignNameFromSessionYAML(sessionYAML) + `
|
||||||
|
inputs:
|
||||||
|
speakers_file: ./speakers.yml
|
||||||
|
autocorrect_file: ./autocorrect.yml
|
||||||
|
glossary_file: ./glossary.yml
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(campaignPath, []byte(campaignYAML), 0o644); err != nil {
|
||||||
|
t.Fatalf("write campaign.yml: %v", err)
|
||||||
|
}
|
||||||
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
if err := os.WriteFile(sessionPath, []byte(sessionYAML), 0o644); err != nil {
|
||||||
t.Fatalf("write session.yml: %v", err)
|
t.Fatalf("write session.yml: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return pipelinePath, sessionPath
|
return pipelinePath, sessionPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func campaignNameFromSessionYAML(sessionYAML string) string {
|
||||||
|
for _, line := range strings.Split(sessionYAML, "\n") {
|
||||||
|
trimmed := strings.TrimSpace(line)
|
||||||
|
if strings.HasPrefix(trimmed, "campaign:") {
|
||||||
|
return strings.Trim(strings.TrimSpace(strings.TrimPrefix(trimmed, "campaign:")), `"'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "sample-campaign"
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,9 @@ func Validate(cfg *Config) error {
|
|||||||
if cfg.Pipeline == nil {
|
if cfg.Pipeline == nil {
|
||||||
return fmt.Errorf("pipeline config is required")
|
return fmt.Errorf("pipeline config is required")
|
||||||
}
|
}
|
||||||
|
if cfg.Campaign == nil {
|
||||||
|
return fmt.Errorf("campaign config is required")
|
||||||
|
}
|
||||||
if cfg.Session == nil {
|
if cfg.Session == nil {
|
||||||
return fmt.Errorf("session config is required")
|
return fmt.Errorf("session config is required")
|
||||||
}
|
}
|
||||||
@@ -24,6 +27,9 @@ func Validate(cfg *Config) error {
|
|||||||
if err := validatePipeline(cfg.Pipeline); err != nil {
|
if err := validatePipeline(cfg.Pipeline); err != nil {
|
||||||
return fmt.Errorf("pipeline config %q invalid: %w", shortName(cfg.PipelinePath, "pipeline.yml"), err)
|
return fmt.Errorf("pipeline config %q invalid: %w", shortName(cfg.PipelinePath, "pipeline.yml"), err)
|
||||||
}
|
}
|
||||||
|
if err := validateCampaign(cfg.Campaign); err != nil {
|
||||||
|
return fmt.Errorf("campaign config %q invalid: %w", shortName(cfg.CampaignPath, "campaign.yml"), err)
|
||||||
|
}
|
||||||
if err := validateSession(cfg.Session); err != nil {
|
if err := validateSession(cfg.Session); err != nil {
|
||||||
return fmt.Errorf("session config %q invalid: %w", shortName(cfg.SessionPath, "session.yml"), err)
|
return fmt.Errorf("session config %q invalid: %w", shortName(cfg.SessionPath, "session.yml"), err)
|
||||||
}
|
}
|
||||||
@@ -34,6 +40,16 @@ func Validate(cfg *Config) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateCampaign(cfg *CampaignConfig) error {
|
||||||
|
if cfg == nil {
|
||||||
|
return fmt.Errorf("campaign config is required")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(cfg.Campaign) == "" {
|
||||||
|
return fmt.Errorf("campaign.campaign is required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func validatePipeline(cfg *PipelineConfig) error {
|
func validatePipeline(cfg *PipelineConfig) error {
|
||||||
if strings.TrimSpace(cfg.Workspace.Root) == "" {
|
if strings.TrimSpace(cfg.Workspace.Root) == "" {
|
||||||
return fmt.Errorf("pipeline.workspace.root is required")
|
return fmt.Errorf("pipeline.workspace.root is required")
|
||||||
|
|||||||
@@ -30,8 +30,10 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
|
|||||||
store := artifacts.NewLocalStore(root)
|
store := artifacts.NewLocalStore(root)
|
||||||
cfgDir := t.TempDir()
|
cfgDir := t.TempDir()
|
||||||
sessionPath := filepath.Join(cfgDir, "session.yml")
|
sessionPath := filepath.Join(cfgDir, "session.yml")
|
||||||
|
campaignPath := filepath.Join(cfgDir, "campaign.yml")
|
||||||
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
||||||
writeStageTestFile(t, sessionPath, "session_id: 2026-05-03\n")
|
writeStageTestFile(t, sessionPath, "session_id: 2026-05-03\n")
|
||||||
|
writeStageTestFile(t, campaignPath, "campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n")
|
||||||
writeStageTestFile(t, pipelinePath, "workspace:\n root: "+root+"\n")
|
writeStageTestFile(t, pipelinePath, "workspace:\n root: "+root+"\n")
|
||||||
writeStageTestFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
writeStageTestFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
||||||
writeStageTestFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
writeStageTestFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||||
@@ -48,7 +50,9 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
|
|||||||
env := &Env{
|
env := &Env{
|
||||||
Config: &config.Config{
|
Config: &config.Config{
|
||||||
SessionPath: sessionPath,
|
SessionPath: sessionPath,
|
||||||
|
CampaignPath: campaignPath,
|
||||||
PipelinePath: pipelinePath,
|
PipelinePath: pipelinePath,
|
||||||
|
Campaign: &config.CampaignConfig{Campaign: "sample-campaign"},
|
||||||
Pipeline: &config.PipelineConfig{
|
Pipeline: &config.PipelineConfig{
|
||||||
Workspace: config.WorkspaceConfig{Root: root},
|
Workspace: config.WorkspaceConfig{Root: root},
|
||||||
Storage: config.StorageConfig{
|
Storage: config.StorageConfig{
|
||||||
@@ -62,14 +66,28 @@ func TestStagesReturnExpectedMetadata(t *testing.T) {
|
|||||||
UploadRun: boolPtr(true),
|
UploadRun: boolPtr(true),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
StableInputs: config.ResolvedStableInputs{
|
||||||
|
SpeakersFile: config.ResolvedInputFile{
|
||||||
|
Path: "./speakers.yml",
|
||||||
|
ConfigPath: campaignPath,
|
||||||
|
Source: "campaign_config",
|
||||||
|
},
|
||||||
|
AutocorrectFile: config.ResolvedInputFile{
|
||||||
|
Path: "./autocorrect.yml",
|
||||||
|
ConfigPath: campaignPath,
|
||||||
|
Source: "campaign_config",
|
||||||
|
},
|
||||||
|
GlossaryFile: config.ResolvedInputFile{
|
||||||
|
Path: "./glossary.yml",
|
||||||
|
ConfigPath: campaignPath,
|
||||||
|
Source: "campaign_config",
|
||||||
|
},
|
||||||
|
},
|
||||||
Session: &config.SessionConfig{
|
Session: &config.SessionConfig{
|
||||||
SessionID: "2026-05-03",
|
SessionID: "2026-05-03",
|
||||||
Campaign: "sample-campaign",
|
Campaign: "sample-campaign",
|
||||||
Inputs: config.SessionInputsConfig{
|
Inputs: config.SessionInputsConfig{
|
||||||
AudioDir: "./audio",
|
AudioDir: "./audio",
|
||||||
SpeakersFile: "./speakers.yml",
|
|
||||||
AutocorrectFile: "./autocorrect.yml",
|
|
||||||
GlossaryFile: "./glossary.yml",
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func (prepareStage) Name() string { return "prepare" }
|
|||||||
func (prepareStage) Declares() IODecl {
|
func (prepareStage) Declares() IODecl {
|
||||||
return IODecl{
|
return IODecl{
|
||||||
Inputs: []artifacts.Ref{
|
Inputs: []artifacts.Ref{
|
||||||
|
{Kind: "config", Category: "inputs", RelativePath: "campaign.yml"},
|
||||||
{Kind: "config", Category: "inputs", RelativePath: "session.yml"},
|
{Kind: "config", Category: "inputs", RelativePath: "session.yml"},
|
||||||
{Kind: "config", Category: "inputs", RelativePath: "pipeline.resolved.yml"},
|
{Kind: "config", Category: "inputs", RelativePath: "pipeline.resolved.yml"},
|
||||||
{Kind: "config", Category: "inputs", RelativePath: "speakers.yml"},
|
{Kind: "config", Category: "inputs", RelativePath: "speakers.yml"},
|
||||||
@@ -59,21 +60,30 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
return nil, fmt.Errorf("prepare: ensure workdir layout: %w", err)
|
return nil, fmt.Errorf("prepare: ensure workdir layout: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
campaignSrc := env.Config.CampaignPath
|
||||||
|
if err := requireFile(campaignSrc, "campaign.yml"); err != nil {
|
||||||
|
return nil, fmt.Errorf("prepare: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
sessionSrc := env.Config.SessionPath
|
sessionSrc := env.Config.SessionPath
|
||||||
if err := requireFile(sessionSrc, "session.yml"); err != nil {
|
if err := requireFile(sessionSrc, "session.yml"); err != nil {
|
||||||
return nil, fmt.Errorf("prepare: %w", err)
|
return nil, fmt.Errorf("prepare: %w", err)
|
||||||
}
|
}
|
||||||
sessionDir := filepath.Dir(sessionSrc)
|
sessionDir := filepath.Dir(sessionSrc)
|
||||||
|
|
||||||
speakersSrc, err := resolvePath(sessionDir, env.Config.Session.Inputs.SpeakersFile)
|
speakersInput := stableInputSource(env.Config.StableInputs.SpeakersFile, env.Config.Session.Inputs.SpeakersFile, sessionSrc)
|
||||||
|
autocorrectInput := stableInputSource(env.Config.StableInputs.AutocorrectFile, env.Config.Session.Inputs.AutocorrectFile, sessionSrc)
|
||||||
|
glossaryInput := stableInputSource(env.Config.StableInputs.GlossaryFile, env.Config.Session.Inputs.GlossaryFile, sessionSrc)
|
||||||
|
|
||||||
|
speakersSrc, err := resolveConfigRelativePath(speakersInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("prepare: speakers path: %w", err)
|
return nil, fmt.Errorf("prepare: speakers path: %w", err)
|
||||||
}
|
}
|
||||||
autocorrectSrc, err := resolvePath(sessionDir, env.Config.Session.Inputs.AutocorrectFile)
|
autocorrectSrc, err := resolveConfigRelativePath(autocorrectInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("prepare: autocorrect path: %w", err)
|
return nil, fmt.Errorf("prepare: autocorrect path: %w", err)
|
||||||
}
|
}
|
||||||
glossarySrc, err := resolvePath(sessionDir, env.Config.Session.Inputs.GlossaryFile)
|
glossarySrc, err := resolveConfigRelativePath(glossaryInput)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("prepare: glossary path: %w", err)
|
return nil, fmt.Errorf("prepare: glossary path: %w", err)
|
||||||
}
|
}
|
||||||
@@ -96,17 +106,27 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
return nil, fmt.Errorf("prepare: resolve audio inputs: %w", err)
|
return nil, fmt.Errorf("prepare: resolve audio inputs: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
inputs := make([]manifest.InputRecord, 0, 5+len(resolvedLocalAudio))
|
inputs := make([]manifest.InputRecord, 0, 6+len(resolvedLocalAudio))
|
||||||
registerInput := func(kind, path, checksum string) {
|
registerInput := func(kind, path, checksum string) {
|
||||||
inputs = append(inputs, manifest.InputRecord{Kind: kind, Path: path, Checksum: checksum})
|
inputs = append(inputs, manifest.InputRecord{Kind: kind, Path: path, Checksum: checksum})
|
||||||
}
|
}
|
||||||
|
registerConfigInput := func(kind, path, checksum, source string) {
|
||||||
|
inputs = append(inputs, manifest.InputRecord{Kind: kind, Path: path, Checksum: checksum, Source: source})
|
||||||
|
}
|
||||||
|
|
||||||
|
campaignDst := filepath.Join(paths.InputsDir, "campaign.yml")
|
||||||
|
campaignChecksum, err := copyFileIfChanged(env.ArtifactStore, campaignSrc, campaignDst)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("prepare: materialize campaign.yml: %w", err)
|
||||||
|
}
|
||||||
|
registerConfigInput("campaign_config", campaignDst, campaignChecksum, "campaign_config")
|
||||||
|
|
||||||
sessionDst := filepath.Join(paths.InputsDir, "session.yml")
|
sessionDst := filepath.Join(paths.InputsDir, "session.yml")
|
||||||
sessionChecksum, err := copyFileIfChanged(env.ArtifactStore, sessionSrc, sessionDst)
|
sessionChecksum, err := copyFileIfChanged(env.ArtifactStore, sessionSrc, sessionDst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("prepare: materialize session.yml: %w", err)
|
return nil, fmt.Errorf("prepare: materialize session.yml: %w", err)
|
||||||
}
|
}
|
||||||
registerInput("session_config", sessionDst, sessionChecksum)
|
registerConfigInput("session_config", sessionDst, sessionChecksum, "session_config")
|
||||||
|
|
||||||
pipelineResolvedBytes, err := renderResolvedPipeline(env.Config.Pipeline)
|
pipelineResolvedBytes, err := renderResolvedPipeline(env.Config.Pipeline)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,19 +140,20 @@ func (prepareStage) Run(ctx context.Context, env *Env, m *manifest.Manifest) (*S
|
|||||||
registerInput("pipeline_resolved", pipelineDst, pipelineChecksum)
|
registerInput("pipeline_resolved", pipelineDst, pipelineChecksum)
|
||||||
|
|
||||||
for _, cfgFile := range []struct {
|
for _, cfgFile := range []struct {
|
||||||
kind string
|
kind string
|
||||||
src string
|
src string
|
||||||
dst string
|
dst string
|
||||||
|
source string
|
||||||
}{
|
}{
|
||||||
{kind: "speakers", src: speakersSrc, dst: filepath.Join(paths.InputsDir, "speakers.yml")},
|
{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")},
|
{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")},
|
{kind: "glossary", src: glossarySrc, dst: filepath.Join(paths.InputsDir, "glossary.yml"), source: glossaryInput.Source},
|
||||||
} {
|
} {
|
||||||
checksum, err := copyFileIfChanged(env.ArtifactStore, cfgFile.src, cfgFile.dst)
|
checksum, err := copyFileIfChanged(env.ArtifactStore, cfgFile.src, cfgFile.dst)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("prepare: materialize %s: %w", cfgFile.kind, err)
|
return nil, fmt.Errorf("prepare: materialize %s: %w", cfgFile.kind, err)
|
||||||
}
|
}
|
||||||
registerInput(cfgFile.kind, cfgFile.dst, checksum)
|
registerConfigInput(cfgFile.kind, cfgFile.dst, checksum, cfgFile.source)
|
||||||
}
|
}
|
||||||
|
|
||||||
if useS3Audio {
|
if useS3Audio {
|
||||||
@@ -195,6 +216,31 @@ func renderResolvedPipeline(cfg *config.PipelineConfig) ([]byte, error) {
|
|||||||
return yaml.Marshal(cfg)
|
return yaml.Marshal(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func stableInputSource(resolved config.ResolvedInputFile, fallbackPath, fallbackConfigPath string) config.ResolvedInputFile {
|
||||||
|
if strings.TrimSpace(resolved.Path) != "" || strings.TrimSpace(resolved.ConfigPath) != "" || strings.TrimSpace(resolved.Source) != "" {
|
||||||
|
if strings.TrimSpace(resolved.ConfigPath) == "" {
|
||||||
|
resolved.ConfigPath = fallbackConfigPath
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(resolved.Source) == "" {
|
||||||
|
resolved.Source = "session_config"
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
return config.ResolvedInputFile{
|
||||||
|
Path: fallbackPath,
|
||||||
|
ConfigPath: fallbackConfigPath,
|
||||||
|
Source: "session_config",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveConfigRelativePath(input config.ResolvedInputFile) (string, error) {
|
||||||
|
basePath := strings.TrimSpace(input.ConfigPath)
|
||||||
|
if basePath == "" {
|
||||||
|
return "", fmt.Errorf("source config path is required")
|
||||||
|
}
|
||||||
|
return resolvePath(filepath.Dir(basePath), input.Path)
|
||||||
|
}
|
||||||
|
|
||||||
func resolveAudioInputs(sessionDir string, inputs config.SessionInputsConfig) ([]string, bool, error) {
|
func resolveAudioInputs(sessionDir string, inputs config.SessionInputsConfig) ([]string, bool, error) {
|
||||||
hasLocal := strings.TrimSpace(inputs.AudioDir) != "" || len(inputs.AudioFiles) > 0
|
hasLocal := strings.TrimSpace(inputs.AudioDir) != "" || len(inputs.AudioFiles) > 0
|
||||||
if inputs.AudioS3 != nil {
|
if inputs.AudioS3 != nil {
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ func TestPrepareStageExplicitAudioFiles(t *testing.T) {
|
|||||||
|
|
||||||
paths := sessionPathsForEnv(env, m.SessionID)
|
paths := sessionPathsForEnv(env, m.SessionID)
|
||||||
for _, p := range []string{
|
for _, p := range []string{
|
||||||
|
filepath.Join(paths.InputsDir, "campaign.yml"),
|
||||||
filepath.Join(paths.InputsDir, "session.yml"),
|
filepath.Join(paths.InputsDir, "session.yml"),
|
||||||
filepath.Join(paths.InputsDir, "pipeline.resolved.yml"),
|
filepath.Join(paths.InputsDir, "pipeline.resolved.yml"),
|
||||||
filepath.Join(paths.InputsDir, "speakers.yml"),
|
filepath.Join(paths.InputsDir, "speakers.yml"),
|
||||||
@@ -48,8 +49,8 @@ func TestPrepareStageExplicitAudioFiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.Inputs) != 7 {
|
if len(m.Inputs) != 8 {
|
||||||
t.Fatalf("manifest inputs len = %d, want 7", len(m.Inputs))
|
t.Fatalf("manifest inputs len = %d, want 8", len(m.Inputs))
|
||||||
}
|
}
|
||||||
for _, in := range m.Inputs {
|
for _, in := range m.Inputs {
|
||||||
if in.Checksum == "" {
|
if in.Checksum == "" {
|
||||||
@@ -473,8 +474,15 @@ func setupPrepareEnv(t *testing.T) (*Env, *manifest.Manifest) {
|
|||||||
|
|
||||||
sessionPath := filepath.Join(cfgDir, "session.yml")
|
sessionPath := filepath.Join(cfgDir, "session.yml")
|
||||||
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
||||||
|
campaignPath := filepath.Join(cfgDir, "campaign.yml")
|
||||||
|
|
||||||
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
||||||
|
writeFile(t, campaignPath, `campaign: sample-campaign
|
||||||
|
inputs:
|
||||||
|
speakers_file: ./speakers.yml
|
||||||
|
autocorrect_file: ./autocorrect.yml
|
||||||
|
glossary_file: ./glossary.yml
|
||||||
|
`)
|
||||||
writeFile(t, sessionPath, "session_id: 2026-05-03\n")
|
writeFile(t, sessionPath, "session_id: 2026-05-03\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
writeFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||||
@@ -482,16 +490,32 @@ func setupPrepareEnv(t *testing.T) (*Env, *manifest.Manifest) {
|
|||||||
|
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
Pipeline: &config.PipelineConfig{Workspace: config.WorkspaceConfig{Root: workspace}},
|
Pipeline: &config.PipelineConfig{Workspace: config.WorkspaceConfig{Root: workspace}},
|
||||||
|
Campaign: &config.CampaignConfig{Campaign: "sample-campaign"},
|
||||||
SessionPath: sessionPath,
|
SessionPath: sessionPath,
|
||||||
|
CampaignPath: campaignPath,
|
||||||
PipelinePath: pipelinePath,
|
PipelinePath: pipelinePath,
|
||||||
Session: &config.SessionConfig{
|
Session: &config.SessionConfig{
|
||||||
SessionID: "2026-05-03",
|
SessionID: "2026-05-03",
|
||||||
Campaign: "sample-campaign",
|
Campaign: "sample-campaign",
|
||||||
Inputs: config.SessionInputsConfig{
|
Inputs: config.SessionInputsConfig{
|
||||||
AudioDir: "./audio",
|
AudioDir: "./audio",
|
||||||
SpeakersFile: "./speakers.yml",
|
},
|
||||||
AutocorrectFile: "./autocorrect.yml",
|
},
|
||||||
GlossaryFile: "./glossary.yml",
|
StableInputs: config.ResolvedStableInputs{
|
||||||
|
SpeakersFile: config.ResolvedInputFile{
|
||||||
|
Path: "./speakers.yml",
|
||||||
|
ConfigPath: campaignPath,
|
||||||
|
Source: "campaign_config",
|
||||||
|
},
|
||||||
|
AutocorrectFile: config.ResolvedInputFile{
|
||||||
|
Path: "./autocorrect.yml",
|
||||||
|
ConfigPath: campaignPath,
|
||||||
|
Source: "campaign_config",
|
||||||
|
},
|
||||||
|
GlossaryFile: config.ResolvedInputFile{
|
||||||
|
Path: "./glossary.yml",
|
||||||
|
ConfigPath: campaignPath,
|
||||||
|
Source: "campaign_config",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -233,8 +233,10 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
|
|||||||
|
|
||||||
sessionPath := filepath.Join(cfgDir, "session.yml")
|
sessionPath := filepath.Join(cfgDir, "session.yml")
|
||||||
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
pipelinePath := filepath.Join(cfgDir, "pipeline.yml")
|
||||||
|
campaignPath := filepath.Join(cfgDir, "campaign.yml")
|
||||||
writeFile(t, sessionPath, "session_id: 2026-05-03\ncampaign: sample-campaign\n")
|
writeFile(t, sessionPath, "session_id: 2026-05-03\ncampaign: sample-campaign\n")
|
||||||
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
writeFile(t, pipelinePath, "workspace:\n root: "+workspace+"\n")
|
||||||
|
writeFile(t, campaignPath, "campaign: sample-campaign\ninputs:\n speakers_file: ./speakers.yml\n autocorrect_file: ./autocorrect.yml\n glossary_file: ./glossary.yml\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
writeFile(t, filepath.Join(cfgDir, "speakers.yml"), "alice: alice.flac\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
writeFile(t, filepath.Join(cfgDir, "autocorrect.yml"), "[]\n")
|
||||||
writeFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
writeFile(t, filepath.Join(cfgDir, "glossary.yml"), "[]\n")
|
||||||
@@ -243,7 +245,9 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
|
|||||||
concurrency := 2
|
concurrency := 2
|
||||||
cfg := &config.Config{
|
cfg := &config.Config{
|
||||||
PipelinePath: pipelinePath,
|
PipelinePath: pipelinePath,
|
||||||
|
CampaignPath: campaignPath,
|
||||||
SessionPath: sessionPath,
|
SessionPath: sessionPath,
|
||||||
|
Campaign: &config.CampaignConfig{Campaign: "sample-campaign"},
|
||||||
Pipeline: &config.PipelineConfig{
|
Pipeline: &config.PipelineConfig{
|
||||||
Workspace: config.WorkspaceConfig{Root: workspace},
|
Workspace: config.WorkspaceConfig{Root: workspace},
|
||||||
WhisperX: config.WhisperXConfig{
|
WhisperX: config.WhisperXConfig{
|
||||||
@@ -265,6 +269,11 @@ func setupTranscribeEnv(t *testing.T, audioFiles []string) (*Env, *manifest.Mani
|
|||||||
GlossaryFile: "./glossary.yml",
|
GlossaryFile: "./glossary.yml",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
StableInputs: config.ResolvedStableInputs{
|
||||||
|
SpeakersFile: config.ResolvedInputFile{Path: "./speakers.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
||||||
|
AutocorrectFile: config.ResolvedInputFile{Path: "./autocorrect.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
||||||
|
GlossaryFile: config.ResolvedInputFile{Path: "./glossary.yml", ConfigPath: campaignPath, Source: "campaign_config"},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
env := &Env{
|
env := &Env{
|
||||||
|
|||||||
Reference in New Issue
Block a user