agentstatedeveloper_cli/lib.rs
1//! agentstatedeveloper-cli — library surface.
2//!
3//! The `asd` binary is a thin wrapper over this crate. `asd-pro`
4//! (commercial) imports this as a library and extends it: it reuses
5//! [`run_oss_command`] for OSS subcommands and provides its own
6//! handlers for the ratify / audit-verify commands that OSS stubs out.
7
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10use std::sync::OnceLock;
11
12use anyhow::Result;
13use clap::{Parser, Subcommand};
14
15use agentstatedeveloper_core::{AuditSink, RatifyOps};
16
17pub mod commands;
18pub mod config;
19// Vendored agent-skillgen; the CLI uses only part of its surface.
20#[allow(dead_code, unused_imports)]
21pub(crate) mod skillgen;
22
23pub use config::Config;
24
25/// Process-wide audit sink override. `asd-pro` sets this at startup
26/// to swap in `JsonlFileSink` when `--audit-log` / `ASD_AUDIT_LOG` is
27/// configured. OSS `asd` leaves it unset, so subcommands fall back to
28/// the default `NullSink` (with a warning if a log path was configured).
29static AUDIT_SINK_OVERRIDE: OnceLock<Arc<dyn AuditSink>> = OnceLock::new();
30
31pub fn set_audit_sink_override(sink: Arc<dyn AuditSink>) {
32 // First-write-wins: OnceLock::set returns Err if already initialized.
33 // `asd-pro` installs once at startup; later calls (test re-init, etc.)
34 // are intentional no-ops — the first sink remains authoritative.
35 let _ = AUDIT_SINK_OVERRIDE.set(sink);
36}
37
38pub(crate) fn audit_sink_override() -> Option<Arc<dyn AuditSink>> {
39 AUDIT_SINK_OVERRIDE.get().cloned()
40}
41
42/// Process-wide ratify ops override. `asd-pro` installs `RatifyOpsImpl`
43/// here before dispatch. `open_engine` wires it into every Engine instance.
44static RATIFY_OVERRIDE: OnceLock<Arc<dyn RatifyOps>> = OnceLock::new();
45
46pub fn set_ratify_ops_override(ratify: Arc<dyn RatifyOps>) {
47 // First-write-wins (see set_audit_sink_override): the OSS->pro
48 // installer runs once; double-set is a no-op by design.
49 let _ = RATIFY_OVERRIDE.set(ratify);
50}
51
52pub(crate) fn ratify_ops_override() -> Option<Arc<dyn RatifyOps>> {
53 RATIFY_OVERRIDE.get().cloned()
54}
55
56/// AgentStateDeveloper — code-level context and audit overlay.
57#[derive(Debug, Parser)]
58#[command(
59 name = "asd",
60 version,
61 about = "AgentStateDeveloper CLI",
62 // We ship our own `help` subcommand (on-demand asd/ctx feature docs), so the
63 // auto-generated clap `help` subcommand is disabled. `-h`/`--help` still work.
64 disable_help_subcommand = true,
65 long_about = "AgentStateDeveloper CLI — code-level context and audit overlay.
66
67Bootstrap a NEW repo (no sidecar yet) — manual sequence:
68 asd init # install git hooks, create .asd/ directory
69 asd index <path> # walk source files; build FTS + ASG state
70 asd sync # write .asd/v1/ sidecar (commit this)
71 asd status # verify (expect state: fresh, sidecar: hydrated)
72
73Bootstrap a FRESH CLONE (sidecar already in repo) — manual sequence:
74 asd init # install git hooks
75 asd hydrate --verify # restore ASG from sidecar; --verify catches drift
76 asd status # verify (index_consistency.consistent == true)
77
78One-shot alternative (recommended): `asd onboard` runs the right sequence
79automatically for either case above. Idempotent — safe to re-run.
80
81Daily loop:
82 asd prepare-change \"<task>\" # one-call context package (use BEFORE editing)
83 asd search \"<query>\" --agent # ranked semantic search
84 asd impact <symbol> # blast radius for a planned change
85 asd ledger append ... # record a decision / invariant / hazard
86
87Which command when (they overlap — pick by intent):
88 Cold repo, don't know the layout -> asd architecture (languages, layers, hotspots)
89 \"Where is the code that does X?\" -> asd search \"X\" (ranked symbols)
90 \"Explain feature X end-to-end\" -> asd investigate X (search + call chains + hazards)
91 About to edit — give me everything -> asd prepare-change \"<task>\" (one-call package)
92 I know the symbol; what breaks? -> asd impact <symbol> (blast radius)
93 Pre-edit safety checklist -> asd checklist \"<task>\"
94 Reviewing a diff / PR -> asd since <base-sha>
95 Pull full context on named symbols -> asd context-for a,b,c
96
97When source changes outside ASD's commit hooks, re-run `asd index <path>`
98to refresh the FTS cache. `asd status` will flag staleness if the index
99is more than an hour behind."
100)]
101pub struct Cli {
102 /// Path to the ASD SQLite database. Overrides `ASD_DB` env var.
103 #[arg(long, global = true)]
104 pub db: Option<PathBuf>,
105
106 /// Path to a JSON policy file evaluated against ledger/effect writes.
107 /// Overrides `ASD_POLICY` env var. When absent, solo-dev default is
108 /// permissive (everything Allow).
109 #[arg(long, global = true)]
110 pub policy: Option<PathBuf>,
111
112 /// Path to an append-only JSONL audit log. Every ledger/effect
113 /// mutation emits one event. Overrides `ASD_AUDIT_LOG` env var.
114 /// When absent, the audit sink discards events.
115 #[arg(long, global = true)]
116 pub audit_log: Option<PathBuf>,
117
118 /// Plan D t-001: emit compact output projecting each command down to
119 /// load-bearing fields (qname, file:line, signature, first doc line).
120 /// 60-80% token reduction vs default. Also honors `ASD_FORMAT=brief`.
121 /// `--json` (default) keeps the structured payload.
122 #[arg(long, global = true)]
123 pub brief: bool,
124
125 #[command(subcommand)]
126 pub cmd: Command,
127}
128
129#[derive(Debug, Subcommand)]
130pub enum Command {
131 /// On-demand feature docs: syntax, examples, gotchas. `asd help` lists the
132 /// catalog; `asd help <feature>` details one; `--publish` writes the shared
133 /// cross-tool help index.
134 Help(commands::help::HelpArgs),
135
136 /// Initialize an ASD repository and install git hooks.
137 Init(commands::init::InitArgs),
138
139 /// Plan K t-005: one-shot post-clone setup. Runs `init → index →
140 /// conclusions import` in the right order so a new developer (or
141 /// their agent) gets a fully usable ASD project in one command.
142 /// Idempotent — re-runs are safe.
143 Onboard(commands::onboard::OnboardArgs),
144
145 /// Show installed ASD git hooks and their current status.
146 Hooks(commands::hooks::HooksArgs),
147
148 /// Walk source files under a directory and build the FTS + ASG
149 /// index. Re-runnable; idempotent. (Aliased as `reindex` to
150 /// match the MCP `mcp__asd__reindex` tool name.)
151 #[command(alias = "reindex")]
152 Index(commands::index::IndexArgs),
153
154 /// Read a symbol, its effects, and recent ledger entries.
155 /// Plan D t-003: also accepts `code_read` (MCP-era alias).
156 #[command(alias = "code_read")]
157 Read(commands::read::ReadArgs),
158
159 /// Ledger operations.
160 #[command(subcommand)]
161 Ledger(commands::ledger::LedgerCmd),
162
163 /// Policy introspection (requires --policy / ASD_POLICY).
164 #[command(subcommand)]
165 Policy(commands::policy::PolicyCmd),
166
167 /// Verify a symbol's declared effects against its source. Reports each as
168 /// verified or unverified (unverified = source unreadable or no adapter for
169 /// that language). `--write` persists results. Use before trusting
170 /// `impact`'s effect set on a symbol you're about to edit.
171 VerifyEffects(commands::verify_effects::VerifyEffectsArgs),
172
173 /// Run a program under the ASD runtime tracer and ingest observed effects
174 /// (Python only — uses sys.settrace via tools/asd_tracer.py).
175 Trace(commands::trace::TraceArgs),
176
177 /// Mirror ASG state into the `.asd/v1/` on-disk sidecar so it can
178 /// travel with `git commit`.
179 Sync(commands::sync::SyncArgs),
180
181 /// Read the `.asd/v1/` sidecar back into ASG. Inverse of `sync`.
182 /// Used to restore state after a fresh `git clone`.
183 Hydrate(commands::hydrate::HydrateArgs),
184
185 /// Read back audit events (ledger mutations, policy evaluations,
186 /// effect declarations) emitted to the JSONL audit log.
187 #[command(subcommand)]
188 Audit(commands::audit::AuditCmd),
189
190 /// Install, uninstall, or check asd-mcp registration in agent tools.
191 #[command(subcommand)]
192 Mcp(commands::mcp::McpCmd),
193
194 /// Install ASD's agent Skill (SKILL.md) into detected agent skill dirs.
195 Skill(commands::skill::SkillArgs),
196
197 /// Print a paste-into-your-agent block that installs + connects ASD (+ CTX).
198 Bootstrap(commands::bootstrap::BootstrapArgs),
199
200 /// Watch the repo and re-index automatically on source changes.
201 Watch(commands::watch::WatchArgs),
202
203 /// List indexed symbols, effects, or ledger entries.
204 List(commands::list::ListArgs),
205
206 /// Show symbols that call the given symbol (direct or transitive).
207 /// Plan D t-003: also accepts `callers_of` (MCP-era alias).
208 #[command(alias = "callers_of")]
209 Callers(commands::graph::CallersArgs),
210
211 /// Show symbols called by the given symbol (direct or transitive).
212 /// Plan D t-003: also accepts `callees_of` (MCP-era alias).
213 #[command(alias = "callees_of")]
214 Callees(commands::graph::CalleesArgs),
215
216 /// Assemble agent query context for one or more symbols.
217 /// Returns signature, callers/callees, effects, invariants, hazards, and ledger.
218 #[command(name = "context-for")]
219 ContextFor(commands::context_for::ContextForArgs),
220
221 /// Scan the ASG for integrity issues (orphaned refs, malformed blobs, stale
222 /// call graph edges) and optionally apply safe auto-corrections.
223 /// By default runs read-only (dry-run); pass `--fix` to apply corrections.
224 Repair(commands::repair::RepairArgs),
225
226 /// Garbage-collect the ASG store: preview by default, reporting what a
227 /// sweep would reclaim; `--sweep` deletes, `--vacuum` shrinks the file, and
228 /// `--unpin-legacy` releases milestones distilled before AgentStateGraph
229 /// v1.2.2. Takes the store's write lock while sweeping, so other ASD
230 /// writers wait rather than lose data.
231 Gc(commands::gc::GcArgs),
232
233 /// Working notes scoped to a symbol or investigation, with a
234 /// promote-to-ledger path. Local-only; not synced by `asd sync`.
235 #[command(subcommand)]
236 Scratch(commands::scratch::ScratchCmd),
237
238 /// Plan-scoped git worktrees: isolated files + HEAD per unit of work so
239 /// parallel agents can't clobber each other. `start`, work, then `finish`
240 /// to merge back + tear down.
241 #[command(subcommand)]
242 Worktree(commands::worktree::WorktreeCmd),
243
244 /// Ranked concept search over indexed symbols.
245 /// Plan D t-003: also accepts `code_search` and `code_query` (MCP-era aliases).
246 #[command(aliases = ["code_search", "code_query"])]
247 Search(commands::search::SearchArgs),
248
249 /// Exact-symbol references via literal text scan + index definition lookup.
250 /// Use this when you want rg-style completeness on a concrete identifier
251 /// (no tokenization, no BM25). Requires `rg` on PATH.
252 References(commands::references::ReferencesArgs),
253
254 /// List named scope aliases defined in `.asd/scopes.toml`. Discoverability
255 /// for the `--scope` and `--paths` flags supported by search and friends.
256 #[command(subcommand)]
257 Scopes(commands::scopes::ScopesCmd),
258
259 /// View ledger entries bucketed by the six Plan B conclusion classes
260 /// (decisions, classifications, mappings, hazards, recipes, followups).
261 #[command(subcommand)]
262 Conclusions(commands::conclusions::ConclusionsCmd),
263
264 /// Plan C t-004: structured change-intent recipes. Returns per-file
265 /// action plans (Delete / Gate / Run / KeepAsCovered / Review) for
266 /// known task families.
267 #[command(subcommand)]
268 Recipe(commands::recipe::RecipeCmd),
269
270 /// Plan C t-007: initial-read project summary. Walks the indexed
271 /// project, identifies package boundaries, and tags test files
272 /// (fast-test / diagnostic-test). Writes Ownership ledger entries
273 /// with role tags so the next session inherits the mental model.
274 Map(commands::map::MapArgs),
275
276 /// Plan G t-003: capture agent thinking. Subcommands: speculate
277 /// (Hypothesis), model (MentalModel), failed (FailedAttempt),
278 /// question (OpenQuestion), list. See docs/initial-read-prompt.md.
279 #[command(subcommand)]
280 Think(commands::think::ThinkCmd),
281
282 /// Sidecar utilities. `migrate` flips a repo from the legacy `.asd/v1/`
283 /// layout (Plan A) to the compact `.asd/conclusions/` layout (Plan B).
284 #[command(subcommand)]
285 Sidecar(commands::sidecar::SidecarCmd),
286
287 /// Broad feature archaeology: search → expand call chains, invariants,
288 /// hazards, and effects for the top matching entry points in one pass.
289 Investigate(commands::investigate::InvestigateArgs),
290
291 /// Show index health: age, symbol count, and optionally dirty source files.
292 Status(commands::status::StatusArgs),
293
294 /// Blast-radius analysis before editing a symbol: transitive callers,
295 /// aggregated effects, invariants/hazards, affected tests, and recent git touches.
296 Impact(commands::impact::ImpactArgs),
297
298 /// Structured pre-edit checklist: files to inspect, invariants to preserve,
299 /// tests to run, known hazards, and effects to verify. Markdown or JSON output.
300 Checklist(commands::checklist::ChecklistArgs),
301
302 /// Record, list, and remove invariants attached to symbols.
303 /// Shortcut for `asd ledger {append,list,withdraw} --kind invariant`.
304 #[command(subcommand)]
305 Invariant(commands::invariant::InvariantCmd),
306
307 /// One-call agent-ready context package for a planned change: design invariants,
308 /// layer-grouped entry points, likely edit files, affected tests, effects, and
309 /// recent git touches — all composed in a single JSON response.
310 #[command(name = "prepare-change")]
311 PrepareChange(commands::prepare_change::PrepareChangeArgs),
312
313 /// Symbols in files changed since a commit + combined blast radius.
314 /// PR-review workflow: pass the base SHA to get full impact without knowing
315 /// any symbol names upfront. Supports --agent, --intent, --depth.
316 Since(commands::since::SinceArgs),
317
318 /// List cross-service endpoints (HTTP routes/clients, pub-sub) and match
319 /// in-repo client calls to the routes that serve them. A client call with NO
320 /// matching route is a candidate contract-drift signal (a caller reaching an
321 /// endpoint the server no longer serves). `--export` emits a service manifest
322 /// for cross-repo matching.
323 Endpoints(commands::endpoints::EndpointsArgs),
324
325 /// One-call "orient me" overview: languages, packages, layers, routes, and
326 /// call-graph hotspots for a cold agent. Supports --agent.
327 Architecture(commands::architecture::ArchitectureArgs),
328
329 /// Functions/methods with no inbound call edges (CANDIDATE dead code).
330 /// Excludes route handlers, tests, and main/dunder methods. Candidates only —
331 /// a symbol may still be reached via reflection, a framework entrypoint, or a
332 /// cross-language boundary the call graph doesn't span; confirm with
333 /// `asd references <name>` before deleting. Supports --agent.
334 DeadCode(commands::dead_code::DeadCodeArgs),
335
336 /// Read test-runner output on stdin; emit a compact failures-only summary
337 /// (cargo/pytest parsed precisely, others via generic scan). Supports --agent.
338 TestSummary(commands::test_summary::TestSummaryArgs),
339
340 /// Record and list search-quality feedback verdicts for (query, symbol) pairs.
341 #[command(subcommand)]
342 Feedback(commands::feedback::FeedbackCmd),
343
344 /// Derive ledger annotations from a git commit and optionally write them.
345 /// Reads changed files and the commit message, resolves touched symbols,
346 /// and suggests (or records) decisions, invariants, proofs, and hazards.
347 #[command(name = "annotate-commit")]
348 AnnotateCommit(commands::annotate_commit::AnnotateCommitArgs),
349
350 /// Close an active task: write proof and optional validation entries to the
351 /// ledger for all symbols affected by HEAD, tagged with CTX plan/task provenance.
352 #[command(name = "task-close")]
353 TaskClose(commands::task_close::TaskCloseArgs),
354
355 /// Benchmark scorecard across the five ASD dimensions:
356 /// truth, feedback, change, uncertainty, and workflow.
357 Scorecard(commands::scorecard::ScorecardArgs),
358
359 /// Golden benchmark harness: run structural assertions against ASD
360 /// command output to catch ranking/classification regressions.
361 Probe(commands::probe::ProbeCmd),
362
363 /// State Trust Score: machine-readable rollup of index freshness, sidecar
364 /// status, ledger density, dirty files, and concept gaps. Answers: "can I
365 /// rely on ASD for the current task?" in a single call.
366 Trust(commands::trust::TrustArgs),
367
368 /// Task workflow session history: evidence quality, workflow type, and
369 /// missing steps across recent `asd task-close` invocations.
370 Workflow(commands::workflow::WorkflowArgs),
371
372 /// Manage the shared ASD repo registry at ~/.config/asd/repos.toml.
373 /// Subcommands: add, list, use, rm, show.
374 #[command(subcommand)]
375 Repo(commands::repo::RepoCmd),
376}
377
378/// Resolve [`Config`] from the parsed CLI flags.
379pub fn config_from_cli(cli: &Cli) -> Config {
380 // init/onboard/index/hydrate create or (re)populate a db in the current
381 // location, so they must resolve to `./.asd-state.db` (or --db/ASD_DB) only —
382 // never the walk-up parent or the registry's active repo (Plan Q t-002
383 // scoping). hydrate was missing from this list: run in a directory holding
384 // only a sidecar, it fell through to the registry's active repo and loaded
385 // one project's symbols and ledger into another project's store.
386 let local_only = matches!(
387 cli.cmd,
388 Command::Init(_) | Command::Onboard(_) | Command::Index(_) | Command::Hydrate(_)
389 );
390 // `hydrate --dir <d>` fills the store beside the sidecar it reads, and
391 // `index <dir>` uses the store of the directory it indexes — not one in
392 // whatever directory `asd` ran from. An explicit --db or ASD_DB still wins.
393 let explicit_db = cli.db.clone().or_else(|| match &cli.cmd {
394 Command::Hydrate(args) if std::env::var_os("ASD_DB").is_none() => {
395 args.dir.as_ref().map(|d| d.join(".asd-state.db"))
396 }
397 Command::Index(args) if std::env::var_os("ASD_DB").is_none() => std::env::current_dir()
398 .ok()
399 .map(|cwd| index_store(&args.path, &cwd)),
400 _ => None,
401 });
402 Config::resolve_with_brief(
403 explicit_db,
404 cli.policy.clone(),
405 cli.audit_log.clone(),
406 cli.brief,
407 local_only,
408 )
409}
410
411/// The store `asd index <path>` uses when neither `--db` nor `ASD_DB` names
412/// one. It was always `./.asd-state.db`, so indexing a project from its parent
413/// directory (`asd index SessionDrift-ios` from `Apps/`) built and registered a
414/// new store in the parent instead of using the one the project has.
415///
416/// - the current directory, or a file: `./.asd-state.db`, as before;
417/// - a directory with a store of its own: that store;
418/// - a directory inside the current project (the current directory has a
419/// store): `./.asd-state.db`, as before;
420/// - any other directory: a new store in it, beside what it indexes.
421fn index_store(path: &Path, cwd: &Path) -> PathBuf {
422 let here = PathBuf::from("./.asd-state.db");
423 let (Ok(dir), Ok(cwd)) = (cwd.join(path).canonicalize(), cwd.canonicalize()) else {
424 return here;
425 };
426 if !dir.is_dir() || dir == cwd {
427 return here;
428 }
429 let own = dir.join(".asd-state.db");
430 if own.exists() {
431 return own;
432 }
433 if cwd.join(".asd-state.db").exists() && dir.starts_with(&cwd) {
434 return here;
435 }
436 own
437}
438
439/// Dispatch the OSS command set. `asd-pro` can call this for any
440/// subcommand that does not require a commercial override.
441pub fn run(cli: Cli) -> Result<()> {
442 let cfg = config_from_cli(&cli);
443 run_with_config(&cfg, cli.cmd)
444}
445
446/// Same as [`run`] but uses an already-resolved [`Config`]. `asd-pro`
447/// uses this after customizing the audit sink / ledger store.
448pub fn run_with_config(cfg: &Config, cmd: Command) -> Result<()> {
449 use commands::*;
450 match cmd {
451 Command::Help(args) => help::run(cfg, args),
452 Command::Init(args) => init::run(cfg, args),
453 Command::Onboard(args) => onboard::run(cfg, args),
454 Command::Hooks(args) => hooks::run(cfg, args),
455 Command::Index(args) => index::run(cfg, args),
456 Command::Read(args) => read::run(cfg, args),
457 Command::Ledger(sub) => ledger::run(cfg, sub),
458 Command::Policy(sub) => policy::run(cfg, sub),
459 Command::VerifyEffects(args) => verify_effects::run(cfg, args),
460 Command::Trace(args) => trace::run(cfg, args),
461 Command::Sync(args) => sync::run(cfg, args),
462 Command::Gc(args) => gc::run(cfg, args),
463 Command::Hydrate(args) => hydrate::run(cfg, args),
464 Command::Audit(sub) => audit::run(cfg, sub),
465 Command::Mcp(sub) => mcp::run(cfg, sub),
466 Command::Skill(args) => skill::run(args),
467 Command::Bootstrap(args) => bootstrap::run(args),
468 Command::Watch(args) => watch::run(cfg, args),
469 Command::List(args) => list::run(cfg, args),
470 Command::Callers(args) => graph::run_callers(cfg, args),
471 Command::Callees(args) => graph::run_callees(cfg, args),
472 Command::ContextFor(args) => context_for::run(cfg, args),
473 Command::Repair(args) => repair::run(cfg, args),
474 Command::Scratch(cmd) => scratch::run(cfg, cmd),
475 Command::Worktree(cmd) => worktree::run(cmd),
476 Command::Search(args) => search::run(cfg, args),
477 Command::References(args) => references::run(cfg, args),
478 Command::Scopes(cmd) => scopes::run(cfg, cmd),
479 Command::Conclusions(cmd) => conclusions::run(cfg, cmd),
480 Command::Recipe(cmd) => recipe::run(cfg, cmd),
481 Command::Map(args) => map::run(cfg, args),
482 Command::Think(cmd) => think::run(cfg, cmd),
483 Command::Sidecar(cmd) => sidecar::run(cfg, cmd),
484 Command::Investigate(args) => investigate::run(cfg, args),
485 Command::Status(args) => status::run(cfg, args),
486 Command::Impact(args) => impact::run(cfg, args),
487 Command::Checklist(args) => checklist::run(cfg, args),
488 Command::Invariant(sub) => invariant::run(cfg, sub),
489 Command::PrepareChange(args) => prepare_change::run(cfg, args),
490 Command::Since(args) => since::run(cfg, args),
491 Command::Endpoints(args) => endpoints::run(cfg, args),
492 Command::Architecture(args) => architecture::run(cfg, args),
493 Command::DeadCode(args) => dead_code::run(cfg, args),
494 Command::TestSummary(args) => test_summary::run(cfg, args),
495 Command::Feedback(sub) => feedback::run(cfg, sub),
496 Command::AnnotateCommit(args) => annotate_commit::run(cfg, args),
497 Command::TaskClose(args) => task_close::run(cfg, args),
498 Command::Scorecard(args) => scorecard::run(cfg, args),
499 Command::Probe(cmd) => probe::run(cfg, cmd),
500 Command::Trust(args) => trust::run(cfg, args),
501 Command::Workflow(args) => workflow::run(cfg, args),
502 Command::Repo(cmd) => repo::run(cfg, cmd),
503 }
504}
505
506#[cfg(test)]
507mod index_store_tests {
508 use super::index_store;
509 use std::path::PathBuf;
510
511 fn touch_store(dir: &std::path::Path) {
512 std::fs::create_dir_all(dir).unwrap();
513 std::fs::write(dir.join(".asd-state.db"), b"").unwrap();
514 }
515
516 #[test]
517 fn the_current_directory_and_files_keep_the_local_store() {
518 let tmp = tempfile::tempdir().unwrap();
519 let cwd = tmp.path().canonicalize().unwrap();
520 std::fs::write(cwd.join("a.py"), b"").unwrap();
521 let here = PathBuf::from("./.asd-state.db");
522 assert_eq!(index_store(&PathBuf::from("."), &cwd), here);
523 assert_eq!(index_store(&cwd, &cwd), here);
524 assert_eq!(index_store(&PathBuf::from("a.py"), &cwd), here);
525 }
526
527 #[test]
528 fn a_project_indexed_from_its_parent_uses_its_own_store() {
529 let tmp = tempfile::tempdir().unwrap();
530 let apps = tmp.path().canonicalize().unwrap();
531 touch_store(&apps.join("proj"));
532 assert_eq!(
533 index_store(&PathBuf::from("proj"), &apps),
534 apps.join("proj/.asd-state.db")
535 );
536 // From anywhere, by absolute path, even with a store of your own here.
537 touch_store(&apps.join("other"));
538 assert_eq!(
539 index_store(&apps.join("proj"), &apps.join("other")),
540 apps.join("proj/.asd-state.db")
541 );
542 }
543
544 #[test]
545 fn a_subdirectory_of_the_current_project_uses_its_store() {
546 let tmp = tempfile::tempdir().unwrap();
547 let proj = tmp.path().canonicalize().unwrap();
548 touch_store(&proj);
549 std::fs::create_dir_all(proj.join("src")).unwrap();
550 assert_eq!(
551 index_store(&PathBuf::from("src"), &proj),
552 PathBuf::from("./.asd-state.db")
553 );
554 }
555
556 #[test]
557 fn a_directory_with_no_store_gets_one_of_its_own() {
558 let tmp = tempfile::tempdir().unwrap();
559 let apps = tmp.path().canonicalize().unwrap();
560 std::fs::create_dir_all(apps.join("fresh")).unwrap();
561 assert_eq!(
562 index_store(&PathBuf::from("fresh"), &apps),
563 apps.join("fresh/.asd-state.db"),
564 "would have built a store in the parent"
565 );
566 }
567}