Add previous-session workspace path helpers and layout support
This commit is contained in:
@@ -17,7 +17,8 @@ Outputs:
|
|||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
Owns:
|
Owns:
|
||||||
- Session-level path layout (`inputs/`, `audio/`, `transcripts/`, `artifacts/`, `reports/`, `logs/`, `config/`, `current/`, `runs/`)
|
- Session-level path layout (`inputs/`, `audio/`, `transcripts/`, `artifacts/`, `reports/`, `logs/`, `config/`, `current/`, `runs/`, `previous/`)
|
||||||
|
- `previous/manifest.json` and `previous/artifacts/**` are reserved for prepared previous-session state
|
||||||
- Run-local stage sandbox layout under `runs/{run_id}/{stage}/`
|
- Run-local stage sandbox layout under `runs/{run_id}/{stage}/`
|
||||||
- Session lock acquisition/release (`.lock`)
|
- Session lock acquisition/release (`.lock`)
|
||||||
|
|
||||||
@@ -65,4 +66,5 @@ None directly in this subsystem. Stages may use object storage adapters and then
|
|||||||
- Session root is campaign-aware: `{workspace.root}/work/{campaign}/{session_id}`.
|
- Session root is campaign-aware: `{workspace.root}/work/{campaign}/{session_id}`.
|
||||||
- Run roots are always nested: `runs/{run_id}` under the session root.
|
- Run roots are always nested: `runs/{run_id}` under the session root.
|
||||||
- Run-local output promotion must end in canonical session paths.
|
- Run-local output promotion must end in canonical session paths.
|
||||||
|
- `previous/**` is session-durable state and must not be treated as run-local output scratch state.
|
||||||
- Cleanup only targets run-scoped directories and must never delete configured root directories.
|
- Cleanup only targets run-scoped directories and must never delete configured root directories.
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ func TestPostArchiveCleanupWorkdirOnly(t *testing.T) {
|
|||||||
|
|
||||||
assertExists(t, cfg.Pipeline.Workspace.Root)
|
assertExists(t, cfg.Pipeline.Workspace.Root)
|
||||||
assertExists(t, seed.otherRunDir)
|
assertExists(t, seed.otherRunDir)
|
||||||
|
assertExists(t, seed.previousCachePath)
|
||||||
assertMissing(t, seed.runWorkDir)
|
assertMissing(t, seed.runWorkDir)
|
||||||
assertExists(t, seed.spoolAudioDir)
|
assertExists(t, seed.spoolAudioDir)
|
||||||
}
|
}
|
||||||
@@ -98,6 +99,7 @@ func TestPostArchiveCleanupBothPolicies(t *testing.T) {
|
|||||||
assertMissing(t, seed.spoolAudioDir)
|
assertMissing(t, seed.spoolAudioDir)
|
||||||
assertMissing(t, seed.runWorkDir)
|
assertMissing(t, seed.runWorkDir)
|
||||||
assertExists(t, seed.otherRunDir)
|
assertExists(t, seed.otherRunDir)
|
||||||
|
assertExists(t, seed.previousCachePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPostArchiveCleanupNotRunWhenArchiveFails(t *testing.T) {
|
func TestPostArchiveCleanupNotRunWhenArchiveFails(t *testing.T) {
|
||||||
@@ -256,11 +258,12 @@ func TestPostArchiveCleanupNotRunWhenCurrentPointerUploadFails(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type cleanupSeed struct {
|
type cleanupSeed struct {
|
||||||
runWorkDir string
|
runWorkDir string
|
||||||
otherRunDir string
|
otherRunDir string
|
||||||
spoolAudioDir string
|
spoolAudioDir string
|
||||||
localSourceAudio string
|
localSourceAudio string
|
||||||
sessionPrefix string
|
previousCachePath string
|
||||||
|
sessionPrefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func cleanupFixtureConfig(t *testing.T) (*config.Config, cleanupSeed) {
|
func cleanupFixtureConfig(t *testing.T) (*config.Config, cleanupSeed) {
|
||||||
@@ -274,11 +277,18 @@ func cleanupFixtureConfig(t *testing.T) (*config.Config, cleanupSeed) {
|
|||||||
runWorkDir := artifacts.SessionRunRootForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, runID)
|
runWorkDir := artifacts.SessionRunRootForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, runID)
|
||||||
otherRunDir := artifacts.SessionRunRootForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, "20260516T010204Z-5e6f7a8b")
|
otherRunDir := artifacts.SessionRunRootForCampaign(cfg.Pipeline.Workspace.Root, cfg.Session.Campaign, cfg.Session.SessionID, "20260516T010204Z-5e6f7a8b")
|
||||||
spoolAudioDir := artifacts.SessionSpoolAudioDir(cfg.Pipeline.Spool.Root, cfg.Session.Campaign, cfg.Session.SessionID, runID)
|
spoolAudioDir := artifacts.SessionSpoolAudioDir(cfg.Pipeline.Spool.Root, cfg.Session.Campaign, cfg.Session.SessionID, runID)
|
||||||
|
previousCachePath := artifacts.SessionPreviousArtifactPathForCampaign(
|
||||||
|
cfg.Pipeline.Workspace.Root,
|
||||||
|
cfg.Session.Campaign,
|
||||||
|
cfg.Session.SessionID,
|
||||||
|
"session_recap.md",
|
||||||
|
)
|
||||||
|
|
||||||
mustWriteFile(t, filepath.Join(runWorkDir, "manifest.json"), "{}\n")
|
mustWriteFile(t, filepath.Join(runWorkDir, "manifest.json"), "{}\n")
|
||||||
mustWriteFile(t, filepath.Join(runWorkDir, "logs", "stage.log"), "log\n")
|
mustWriteFile(t, filepath.Join(runWorkDir, "logs", "stage.log"), "log\n")
|
||||||
mustWriteFile(t, filepath.Join(otherRunDir, "logs", "stage.log"), "other\n")
|
mustWriteFile(t, filepath.Join(otherRunDir, "logs", "stage.log"), "other\n")
|
||||||
mustWriteFile(t, filepath.Join(spoolAudioDir, "speaker.flac"), "flac\n")
|
mustWriteFile(t, filepath.Join(spoolAudioDir, "speaker.flac"), "flac\n")
|
||||||
|
mustWriteFile(t, previousCachePath, "# previous recap\n")
|
||||||
|
|
||||||
localSourceAudio := filepath.Join(filepath.Dir(cfg.SessionPath), "audio", "alice.flac")
|
localSourceAudio := filepath.Join(filepath.Dir(cfg.SessionPath), "audio", "alice.flac")
|
||||||
mustWriteFile(t, localSourceAudio, "source\n")
|
mustWriteFile(t, localSourceAudio, "source\n")
|
||||||
@@ -301,11 +311,12 @@ func cleanupFixtureConfig(t *testing.T) (*config.Config, cleanupSeed) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return cfg, cleanupSeed{
|
return cfg, cleanupSeed{
|
||||||
runWorkDir: runWorkDir,
|
runWorkDir: runWorkDir,
|
||||||
otherRunDir: otherRunDir,
|
otherRunDir: otherRunDir,
|
||||||
spoolAudioDir: spoolAudioDir,
|
spoolAudioDir: spoolAudioDir,
|
||||||
localSourceAudio: localSourceAudio,
|
localSourceAudio: localSourceAudio,
|
||||||
sessionPrefix: seed.S3SessionPrefix,
|
previousCachePath: previousCachePath,
|
||||||
|
sessionPrefix: seed.S3SessionPrefix,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ func (s *LocalStore) ensureLayout(paths SessionPaths) (SessionPaths, error) {
|
|||||||
paths.LogsDir,
|
paths.LogsDir,
|
||||||
paths.CurrentDir,
|
paths.CurrentDir,
|
||||||
paths.RunsDir,
|
paths.RunsDir,
|
||||||
|
paths.PreviousDir,
|
||||||
|
paths.PreviousArtifactsDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, dir := range dirs {
|
for _, dir := range dirs {
|
||||||
|
|||||||
@@ -27,10 +27,15 @@ func TestEnsureLayoutCreatesExpectedDirectories(t *testing.T) {
|
|||||||
checkDirExists(t, paths.LogsDir)
|
checkDirExists(t, paths.LogsDir)
|
||||||
checkDirExists(t, paths.CurrentDir)
|
checkDirExists(t, paths.CurrentDir)
|
||||||
checkDirExists(t, paths.RunsDir)
|
checkDirExists(t, paths.RunsDir)
|
||||||
|
checkDirExists(t, paths.PreviousDir)
|
||||||
|
checkDirExists(t, paths.PreviousArtifactsDir)
|
||||||
|
|
||||||
if filepath.Base(paths.ManifestPath) != "manifest.json" {
|
if filepath.Base(paths.ManifestPath) != "manifest.json" {
|
||||||
t.Fatalf("ManifestPath = %q, want basename manifest.json", paths.ManifestPath)
|
t.Fatalf("ManifestPath = %q, want basename manifest.json", paths.ManifestPath)
|
||||||
}
|
}
|
||||||
|
if filepath.Base(paths.PreviousManifestPath) != "manifest.json" {
|
||||||
|
t.Fatalf("PreviousManifestPath = %q, want basename manifest.json", paths.PreviousManifestPath)
|
||||||
|
}
|
||||||
if filepath.Base(paths.LockPath) != ".lock" {
|
if filepath.Base(paths.LockPath) != ".lock" {
|
||||||
t.Fatalf("LockPath = %q, want basename .lock", paths.LockPath)
|
t.Fatalf("LockPath = %q, want basename .lock", paths.LockPath)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ type SessionPaths struct {
|
|||||||
LogsDir string
|
LogsDir string
|
||||||
CurrentDir string
|
CurrentDir string
|
||||||
RunsDir string
|
RunsDir string
|
||||||
|
PreviousDir string
|
||||||
|
PreviousManifestPath string
|
||||||
|
PreviousArtifactsDir string
|
||||||
ManifestPath string
|
ManifestPath string
|
||||||
LockPath string
|
LockPath string
|
||||||
}
|
}
|
||||||
@@ -42,6 +45,29 @@ func SessionRunsDirForCampaign(rootDir, campaign, sessionID string) string {
|
|||||||
return filepath.Join(SessionWorkDirForCampaign(rootDir, campaign, sessionID), config.PathRunsDirSegment)
|
return filepath.Join(SessionWorkDirForCampaign(rootDir, campaign, sessionID), config.PathRunsDirSegment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SessionPreviousDirForCampaign returns the canonical previous-session state directory.
|
||||||
|
func SessionPreviousDirForCampaign(rootDir, campaign, sessionID string) string {
|
||||||
|
return filepath.Join(SessionWorkDirForCampaign(rootDir, campaign, sessionID), config.PathPreviousDirSegment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionPreviousManifestPathForCampaign returns the canonical previous-session manifest cache path.
|
||||||
|
func SessionPreviousManifestPathForCampaign(rootDir, campaign, sessionID string) string {
|
||||||
|
return filepath.Join(SessionPreviousDirForCampaign(rootDir, campaign, sessionID), config.PathManifestFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionPreviousArtifactsDirForCampaign returns the canonical previous-session artifact cache directory.
|
||||||
|
func SessionPreviousArtifactsDirForCampaign(rootDir, campaign, sessionID string) string {
|
||||||
|
return filepath.Join(SessionPreviousDirForCampaign(rootDir, campaign, sessionID), config.PathArtifactsDirSegment)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionPreviousArtifactPathForCampaign returns a path under previous/artifacts for one artifact.
|
||||||
|
func SessionPreviousArtifactPathForCampaign(rootDir, campaign, sessionID, artifactRelativePath string) string {
|
||||||
|
return filepath.Join(
|
||||||
|
SessionPreviousArtifactsDirForCampaign(rootDir, campaign, sessionID),
|
||||||
|
filepath.FromSlash(artifactRelativePath),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// SessionRunRootForCampaign returns the canonical run root under runs/{run_id}.
|
// SessionRunRootForCampaign returns the canonical run root under runs/{run_id}.
|
||||||
func SessionRunRootForCampaign(rootDir, campaign, sessionID, runID string) string {
|
func SessionRunRootForCampaign(rootDir, campaign, sessionID, runID string) string {
|
||||||
return filepath.Join(SessionRunsDirForCampaign(rootDir, campaign, sessionID), runID)
|
return filepath.Join(SessionRunsDirForCampaign(rootDir, campaign, sessionID), runID)
|
||||||
@@ -62,6 +88,26 @@ func SessionSpoolAudioDir(spoolRoot, campaign, sessionID, runID string) string {
|
|||||||
return filepath.Join(spoolRoot, campaign, sessionID, runID, config.PathAudioDirSegment)
|
return filepath.Join(spoolRoot, campaign, sessionID, runID, config.PathAudioDirSegment)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SessionPreviousDir returns the previous-session state directory for already-resolved session paths.
|
||||||
|
func SessionPreviousDir(paths SessionPaths) string {
|
||||||
|
return paths.PreviousDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionPreviousManifestPath returns the previous-session manifest path for already-resolved session paths.
|
||||||
|
func SessionPreviousManifestPath(paths SessionPaths) string {
|
||||||
|
return paths.PreviousManifestPath
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionPreviousArtifactsDir returns the previous-session artifact directory for already-resolved session paths.
|
||||||
|
func SessionPreviousArtifactsDir(paths SessionPaths) string {
|
||||||
|
return paths.PreviousArtifactsDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// SessionPreviousArtifactPath returns a path under previous/artifacts for already-resolved session paths.
|
||||||
|
func SessionPreviousArtifactPath(paths SessionPaths, artifactRelativePath string) string {
|
||||||
|
return filepath.Join(paths.PreviousArtifactsDir, filepath.FromSlash(artifactRelativePath))
|
||||||
|
}
|
||||||
|
|
||||||
func buildSessionPaths(workspaceRoot, campaign, sessionID string) SessionPaths {
|
func buildSessionPaths(workspaceRoot, campaign, sessionID string) SessionPaths {
|
||||||
root := SessionWorkDirForCampaign(workspaceRoot, campaign, sessionID)
|
root := SessionWorkDirForCampaign(workspaceRoot, campaign, sessionID)
|
||||||
return buildSessionPathsFromRoot(workspaceRoot, campaign, sessionID, root)
|
return buildSessionPathsFromRoot(workspaceRoot, campaign, sessionID, root)
|
||||||
@@ -84,6 +130,9 @@ func buildSessionPathsFromRoot(workspaceRoot, campaign, sessionID, root string)
|
|||||||
LogsDir: filepath.Join(root, config.PathLogsDirSegment),
|
LogsDir: filepath.Join(root, config.PathLogsDirSegment),
|
||||||
CurrentDir: filepath.Join(root, config.PathCurrentDirSegment),
|
CurrentDir: filepath.Join(root, config.PathCurrentDirSegment),
|
||||||
RunsDir: filepath.Join(root, config.PathRunsDirSegment),
|
RunsDir: filepath.Join(root, config.PathRunsDirSegment),
|
||||||
|
PreviousDir: filepath.Join(root, config.PathPreviousDirSegment),
|
||||||
|
PreviousManifestPath: filepath.Join(root, config.PathPreviousDirSegment, config.PathManifestFile),
|
||||||
|
PreviousArtifactsDir: filepath.Join(root, config.PathPreviousDirSegment, config.PathArtifactsDirSegment),
|
||||||
ManifestPath: filepath.Join(root, config.PathManifestFile),
|
ManifestPath: filepath.Join(root, config.PathManifestFile),
|
||||||
LockPath: filepath.Join(root, config.PathLockFile),
|
LockPath: filepath.Join(root, config.PathLockFile),
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,52 @@ func TestSessionRunManifestPathForCampaign(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSessionPreviousPathsForCampaign(t *testing.T) {
|
||||||
|
root := "/tmp/workspace"
|
||||||
|
previousDir := SessionPreviousDirForCampaign(root, "forsaken", "2026-04-19")
|
||||||
|
wantPreviousDir := filepath.Join(root, "work", "forsaken", "2026-04-19", "previous")
|
||||||
|
if previousDir != wantPreviousDir {
|
||||||
|
t.Fatalf("SessionPreviousDirForCampaign() = %q, want %q", previousDir, wantPreviousDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestPath := SessionPreviousManifestPathForCampaign(root, "forsaken", "2026-04-19")
|
||||||
|
wantManifestPath := filepath.Join(root, "work", "forsaken", "2026-04-19", "previous", "manifest.json")
|
||||||
|
if manifestPath != wantManifestPath {
|
||||||
|
t.Fatalf("SessionPreviousManifestPathForCampaign() = %q, want %q", manifestPath, wantManifestPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
artifactsDir := SessionPreviousArtifactsDirForCampaign(root, "forsaken", "2026-04-19")
|
||||||
|
wantArtifactsDir := filepath.Join(root, "work", "forsaken", "2026-04-19", "previous", "artifacts")
|
||||||
|
if artifactsDir != wantArtifactsDir {
|
||||||
|
t.Fatalf("SessionPreviousArtifactsDirForCampaign() = %q, want %q", artifactsDir, wantArtifactsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
artifactPath := SessionPreviousArtifactPathForCampaign(root, "forsaken", "2026-04-19", "session_recap.md")
|
||||||
|
wantArtifactPath := filepath.Join(root, "work", "forsaken", "2026-04-19", "previous", "artifacts", "session_recap.md")
|
||||||
|
if artifactPath != wantArtifactPath {
|
||||||
|
t.Fatalf("SessionPreviousArtifactPathForCampaign() = %q, want %q", artifactPath, wantArtifactPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSessionPreviousPathsFromSessionPaths(t *testing.T) {
|
||||||
|
paths := buildSessionPaths("/tmp/workspace", "forsaken", "2026-04-19")
|
||||||
|
if got := SessionPreviousDir(paths); got != paths.PreviousDir {
|
||||||
|
t.Fatalf("SessionPreviousDir() = %q, want %q", got, paths.PreviousDir)
|
||||||
|
}
|
||||||
|
if got := SessionPreviousManifestPath(paths); got != paths.PreviousManifestPath {
|
||||||
|
t.Fatalf("SessionPreviousManifestPath() = %q, want %q", got, paths.PreviousManifestPath)
|
||||||
|
}
|
||||||
|
if got := SessionPreviousArtifactsDir(paths); got != paths.PreviousArtifactsDir {
|
||||||
|
t.Fatalf("SessionPreviousArtifactsDir() = %q, want %q", got, paths.PreviousArtifactsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
got := SessionPreviousArtifactPath(paths, "quest_log.json")
|
||||||
|
want := filepath.Join(paths.PreviousArtifactsDir, "quest_log.json")
|
||||||
|
if got != want {
|
||||||
|
t.Fatalf("SessionPreviousArtifactPath() = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSessionSpoolAudioDir(t *testing.T) {
|
func TestSessionSpoolAudioDir(t *testing.T) {
|
||||||
root := "/var/spool/narratio"
|
root := "/var/spool/narratio"
|
||||||
got := SessionSpoolAudioDir(root, "forsaken", "2026-04-19", "20260515T031522Z-a1b2c3d4")
|
got := SessionSpoolAudioDir(root, "forsaken", "2026-04-19", "20260515T031522Z-a1b2c3d4")
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ const (
|
|||||||
PathLogsDirSegment = "logs"
|
PathLogsDirSegment = "logs"
|
||||||
PathCurrentDirSegment = "current"
|
PathCurrentDirSegment = "current"
|
||||||
PathRunsDirSegment = "runs"
|
PathRunsDirSegment = "runs"
|
||||||
|
PathPreviousDirSegment = "previous"
|
||||||
PathManifestFile = "manifest.json"
|
PathManifestFile = "manifest.json"
|
||||||
PathLockFile = ".lock"
|
PathLockFile = ".lock"
|
||||||
PathTranscriptMerged = "transcripts/merged.json"
|
PathTranscriptMerged = "transcripts/merged.json"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
"gitea.maximumdirect.net/eric/narratio/internal/artifacts"
|
||||||
|
"gitea.maximumdirect.net/eric/narratio/internal/config"
|
||||||
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
"gitea.maximumdirect.net/eric/narratio/internal/manifest"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,6 +100,9 @@ func runLocalPathForCanonical(layout runStageLayout, sessionPaths artifacts.Sess
|
|||||||
if rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
if rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||||
return "", fmt.Errorf("canonical path %q is outside session root %q", cleanCanonical, sessionPaths.Root)
|
return "", fmt.Errorf("canonical path %q is outside session root %q", cleanCanonical, sessionPaths.Root)
|
||||||
}
|
}
|
||||||
|
if rel == config.PathPreviousDirSegment || strings.HasPrefix(rel, config.PathPreviousDirSegment+string(filepath.Separator)) {
|
||||||
|
return cleanCanonical, nil
|
||||||
|
}
|
||||||
localPath := filepath.Join(layout.OutputsDir, rel)
|
localPath := filepath.Join(layout.OutputsDir, rel)
|
||||||
if err := os.MkdirAll(filepath.Dir(localPath), 0o755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(localPath), 0o755); err != nil {
|
||||||
return "", fmt.Errorf("create run-local output parent for %q: %w", localPath, err)
|
return "", fmt.Errorf("create run-local output parent for %q: %w", localPath, err)
|
||||||
|
|||||||
@@ -33,3 +33,24 @@ func TestRunLocalPathForCanonicalCreatesParentDirectories(t *testing.T) {
|
|||||||
t.Fatalf("expected run-local parent directory to exist: %v", err)
|
t.Fatalf("expected run-local parent directory to exist: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRunLocalPathForCanonicalKeepsPreviousStateSessionDurable(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
sessionRoot := filepath.Join(root, "work", "dilfs", "2026-05-17")
|
||||||
|
layout := runStageLayout{
|
||||||
|
Enabled: true,
|
||||||
|
OutputsDir: filepath.Join(sessionRoot, "runs", "run-1", "prepare", "outputs"),
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(layout.OutputsDir, 0o755); err != nil {
|
||||||
|
t.Fatalf("mkdir outputs dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
canonical := filepath.Join(sessionRoot, "previous", "artifacts", "session_recap.md")
|
||||||
|
got, err := runLocalPathForCanonical(layout, artifacts.SessionPaths{Root: sessionRoot}, canonical)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("runLocalPathForCanonical() error = %v", err)
|
||||||
|
}
|
||||||
|
if got != canonical {
|
||||||
|
t.Fatalf("runLocalPathForCanonical() = %q, want canonical %q", got, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user