diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 13e4514..690ef17 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -53,7 +53,7 @@ jobs:
cmd /c mklink /D "$localData" "$tempData" | Out-Null
Write-Host "rustc: $(rustc --version)"
Write-Host "cargo: $(cargo --version)"
- - run: cargo test --all-targets -- --test-threads=4 --nocapture
+ - run: cargo test --lib --tests --bins --examples -- --test-threads=4 --nocapture
fmt:
name: Rustfmt
diff --git a/AGENTS.md b/AGENTS.md
index 8a1d96a..93f8707 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -60,6 +60,13 @@ Skill Runtime 全生命周期已落地(含依赖管理 Schema v15),Schema
- Schema 迁移前自动生成 `backup-YYYYMMDD-HHMMSS.db` 快照
- Registry 支持 `export`/`import` 用于用户自主备份
+## 许可证策略
+
+- **主许可证**: AGPL-3.0-or-later (`LICENSE`)
+- **商业授权**: 双许可模式,闭源/专有 SaaS 使用需联系作者 (`LICENSE-COMMERCIAL.md`)
+- **Cargo.toml**: `license = "AGPL-3.0-or-later"`
+- **SPDX 头**: 新增源文件应在顶部包含 AGPL-3.0 声明(见 `LICENSE` 末尾 "How to Apply" 部分)
+
## 架构状态(Wave 15b 完成)
| 维度 | 状态 |
@@ -103,6 +110,13 @@ grep -rn "dirs::data_local_dir\|std::env::var_os\|std::env::var(\"LOCALAPPDATA\"
- 文件系统测试必须使用 `tempfile` + 注入式路径,禁止直接操作 `%LOCALAPPDATA%` 或 `~/.config`。
- Tantivy / SQLite 文件系统测试必须获取 `SEARCH_TEST_LOCK`(或同等级串行化机制)。
+**子规则(来自 PR #4 教训)**:
+- **R2.1 禁止 `DEVBASE_DATA_DIR` 全局注入**:并行测试中 `std::env::set_var("DEVBASE_DATA_DIR", ...)` 导致竞态;必须使用 `TempStorageBackend` 注入式替代。
+- **R2.2 Windows 路径双端规范化**:`TempDir` 可能返回短文件名(`TEMP~1`),而 `dunce::canonicalize` 返回长文件名;路径比较前必须对**双方**调用 `dunce::canonicalize`。
+- **R2.3 `git2` 测试显式身份 + 显式分支**:
+ - CI runner 无全局 `user.name`/`user.email` → `repo.signature()` 会 panic;必须改用 `git2::Signature::now("Test", "test@example.com")`。
+ - `git2::Repository::init` 的默认分支在不同平台可能为 `master` 或 `main`;必须显式 `repo.set_head("refs/heads/main")` 并 commit 到 `"refs/heads/main"`。
+
**Fitness Function**:
```bash
# 高并发下 100% 通过,无 flaky
diff --git a/Cargo.lock b/Cargo.lock
index 73b34c4..78e65fb 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3112,15 +3112,14 @@ checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e"
[[package]]
name = "openssl"
-version = "0.10.78"
+version = "0.10.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
+checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
dependencies = [
"bitflags 2.11.1",
"cfg-if",
"foreign-types",
"libc",
- "once_cell",
"openssl-macros",
"openssl-sys",
]
@@ -3150,9 +3149,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
-version = "0.9.114"
+version = "0.9.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
+checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
dependencies = [
"cc",
"libc",
diff --git a/Cargo.toml b/Cargo.toml
index ffb6a76..e7da488 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -92,7 +92,7 @@ resolver = "2"
version = "0.14.3"
authors = ["juice094 <160722440+juice094@users.noreply.github.com>"]
edition = "2024"
-license = "MIT"
+license = "AGPL-3.0-or-later"
repository = "https://github.com/juice094/devbase"
diff --git a/LICENSE b/LICENSE
index 262467f..ef54a5c 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,21 +1,665 @@
-MIT License
-
-Copyright (c) 2026 juice094
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc.
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include
+a requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through
+a publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a party who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under
+this License and any other pertinent obligations, then as a consequence
+you may not convey it at all. For example, if you agree to terms that
+obligate you to collect a royalty for further conveying from those to
+whom you convey the Program, the only way you could satisfy both those
+terms and this License would be to refrain entirely from conveying the
+Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions
+of the GNU Affero General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero
+General Public License "or any later version" applies to it, you have
+the option of following the terms and conditions either of that
+numbered version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever
+published by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that
+proxy's public statement of acceptance of a version permanently
+authorizes you to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT
+WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND
+PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE
+DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR
+CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES
+AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR
+DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL
+DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM
+(INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED
+INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE
+OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH
+HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these
+terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+
+ Copyright (C)
+
+ This program is free software: you can redistribute it and/or modify
+ it under the terms of the GNU Affero General Public License as published by
+ the Free Software Foundation, either version 3 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU Affero General Public License for more details.
+
+ You should have received a copy of the GNU Affero General Public License
+ along with this program. If not, see .
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+.
diff --git a/LICENSE-COMMERCIAL.md b/LICENSE-COMMERCIAL.md
new file mode 100644
index 0000000..fb05839
--- /dev/null
+++ b/LICENSE-COMMERCIAL.md
@@ -0,0 +1,53 @@
+# Commercial License for devbase
+
+## Dual Licensing Notice
+
+devbase is licensed under the **GNU Affero General Public License v3.0 or later**
+(AGPL-3.0+). A copy of the AGPL-3.0 license is included in the [`LICENSE`](./LICENSE)
+file.
+
+The AGPL-3.0 license grants broad freedoms for personal, academic, and
+open-source use. However, it requires that any modified version deployed as a
+network service (e.g., SaaS, hosted MCP server) must also be made available
+under the AGPL-3.0 license, including full source code.
+
+## Commercial Use & Alternative Licensing
+
+If you wish to use devbase in a **closed-source product**, **proprietary SaaS**,
+or any scenario where the AGPL-3.0 obligations are incompatible with your
+business model, a commercial license is available.
+
+Commercial licensing provides:
+
+- **Exemption from AGPL-3.0 copyleft obligations** for your specific use case
+- **Right to modify and distribute without source code disclosure**
+- **Priority support and consultation** (subject to agreement terms)
+- **Patent and liability protections** beyond the scope of AGPL-3.0
+
+## Contact
+
+To inquire about commercial licensing terms, pricing, or to request a
+commercial license agreement, please contact:
+
+**Email:** `juice094@protonmail.com`
+**GitHub:** [@juice094](https://github.com/juice094)
+
+Please include:
+- Your company / organization name
+- Intended use case (internal tool, SaaS product, embedded solution, etc.)
+- Estimated deployment scale
+
+## Contributor Notice
+
+By contributing code, documentation, or other materials to the devbase
+project, you agree that your contributions may be distributed under both the
+AGPL-3.0 license and, at the discretion of the copyright holder, under
+commercial license terms.
+
+The copyright holder reserves the right to relicense future versions of
+devbase under different terms. A Contributor License Agreement (CLA) may be
+required for substantial contributions.
+
+---
+
+*Last updated: 2026-05-06*
diff --git a/README.md b/README.md
index 767f4ab..ba856ad 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://github.com/juice094/devbase/releases)
[](./AGENTS.md)
[](./AGENTS.md)
-[](./LICENSE)
+[](./LICENSE)
[](https://www.rust-lang.org)
**本地优先的 AI Skill 编排基础设施**
@@ -349,4 +349,12 @@ token = ""
## 许可证
-[MIT](./LICENSE)
+本软件采用 **双许可证 (Dual License)** 模式:
+
+- **开源使用**: [GNU Affero General Public License v3.0 or later (AGPL-3.0+)](./LICENSE)
+ - 适用于个人、学术、及遵守 AGPL-3.0 义务的开源项目
+ - **核心约束**: 若将修改版部署为网络服务(SaaS、托管 MCP Server 等),必须向用户公开完整源代码
+
+- **商业使用**: 如需在闭源产品、专有 SaaS 或无法遵守 AGPL-3.0 的场景中使用,可联系作者获取商业授权
+ - 详见 [`LICENSE-COMMERCIAL.md`](./LICENSE-COMMERCIAL.md)
+ - 联系方式: `juice094@protonmail.com`
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..c4b61b7
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,28 @@
+# Security Policy
+
+## Supported Versions
+
+The following versions of devbase currently receive security updates:
+
+| Version | Supported |
+| ------- | ------------------ |
+| 0.14.x | :white_check_mark: |
+| < 0.14 | :x: |
+
+## Reporting a Vulnerability
+
+If you discover a security vulnerability in devbase, please report it responsibly.
+
+**Preferred method:** Use [GitHub Private Vulnerability Reporting](https://github.com/juice094/devbase/security/advisories/new) if enabled. This allows you to disclose the issue privately and collaborate on a fix before public disclosure.
+
+**Alternative method:** Open a GitHub Issue with the title prefix `[SECURITY]` and mark it with the `security` label. Please do not include exploit details in the initial public report — we will reach out to you for technical details.
+
+## Disclosure Policy
+
+- We aim to acknowledge receipt of a security report within **72 hours**.
+- We will work to validate the vulnerability and provide a timeline for a fix within **7 days**.
+- Once a fix is released, we will publish a security advisory and credit the reporter (with their permission).
+
+## Scope
+
+This security policy covers the devbase CLI, MCP server, TUI, and all first-party skills shipped in this repository. Third-party dependencies should be reported to their respective maintainers.
diff --git a/spike/candle-embedding/Cargo.lock b/spike/candle-embedding/Cargo.lock
index 2918137..df6a9b3 100644
--- a/spike/candle-embedding/Cargo.lock
+++ b/spike/candle-embedding/Cargo.lock
@@ -1515,15 +1515,14 @@ dependencies = [
[[package]]
name = "openssl"
-version = "0.10.78"
+version = "0.10.79"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222"
+checksum = "bf0b434746ee2832f4f0baf10137e1cabb18cbe6912c69e2e33263c45250f542"
dependencies = [
"bitflags",
"cfg-if",
"foreign-types",
"libc",
- "once_cell",
"openssl-macros",
"openssl-sys",
]
@@ -1547,9 +1546,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "openssl-sys"
-version = "0.9.114"
+version = "0.9.115"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6"
+checksum = "158fe5b292746440aa6e7a7e690e55aeb72d41505e2804c23c6973ad0e9c9781"
dependencies = [
"cc",
"libc",
diff --git a/src/mcp/tests.rs b/src/mcp/tests.rs
index e5970aa..5bb051b 100644
--- a/src/mcp/tests.rs
+++ b/src/mcp/tests.rs
@@ -1,13 +1,13 @@
// SPDX-License-Identifier: MIT
// Copyright (c) 2026 juice094
use super::*;
+use crate::storage::StorageBackend;
fn test_ctx() -> (crate::storage::AppContext, tempfile::TempDir) {
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let ctx = crate::storage::AppContext::with_storage(backend).unwrap();
+ // Return a dummy TempDir to keep call-site destructuring compatible.
let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let ctx = crate::storage::AppContext::with_defaults().unwrap();
(ctx, tmp)
}
@@ -421,20 +421,14 @@ fn test_nl_filter_repos_fallback_finds_by_language() {
#[test]
fn test_nl_filter_repos_tantivy_finds_devbase() {
- let _guard = NL_FILTER_TEST_LOCK.lock().unwrap();
- let _search_guard = crate::search::SEARCH_TEST_LOCK.lock().unwrap();
-
- let tmp = tempfile::tempdir().unwrap();
- let old = std::env::var("DEVBASE_DATA_DIR").ok();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let index_path = backend.index_path().unwrap();
- // Ensure DB schema exists in temp dir
- let conn = crate::registry::WorkspaceRegistry::init_in_memory().unwrap();
+ // Ensure DB schema exists
+ let conn = crate::registry::WorkspaceRegistry::init_db_with(&*backend).unwrap();
// Populate Tantivy index with devbase doc
- let (index, _reader) = crate::search::init_index().unwrap();
+ let (index, _reader) = crate::search::init_index_at(&index_path).unwrap();
let mut writer = crate::search::get_writer(&index).unwrap();
let schema = index.schema();
crate::search::add_repo_doc(
@@ -461,20 +455,15 @@ fn test_nl_filter_repos_tantivy_finds_devbase() {
remotes: vec![],
}];
- let results =
- crate::mcp::tools::repo::nl_filter_repos("developer workspace", &repos, &conn).unwrap();
+ let results = crate::mcp::tools::repo::nl_filter_repos_at(
+ &index_path,
+ "developer workspace",
+ &repos,
+ &conn,
+ )
+ .unwrap();
assert!(!results.is_empty(), "tantivy path should find devbase");
assert_eq!(results[0].id, "devbase");
-
- if let Some(v) = old {
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", v);
- }
- } else {
- unsafe {
- std::env::remove_var("DEVBASE_DATA_DIR");
- }
- }
}
#[test]
diff --git a/src/mcp/tools/relations.rs b/src/mcp/tools/relations.rs
index f80a76f..70ee592 100644
--- a/src/mcp/tools/relations.rs
+++ b/src/mcp/tools/relations.rs
@@ -325,11 +325,8 @@ mod tests {
#[tokio::test]
async fn test_relation_store_and_query_roundtrip() {
- let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();
// Pre-seed entities to satisfy FK constraint
let conn = ctx.conn().unwrap();
@@ -394,11 +391,8 @@ mod tests {
#[tokio::test]
async fn test_relation_store_missing_required_fields() {
- let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();
let tool = DevkitRelationStoreTool;
let result = tool
@@ -413,11 +407,8 @@ mod tests {
#[tokio::test]
async fn test_relation_query_bidirectional() {
- let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();
let conn = ctx.conn().unwrap();
let now = chrono::Utc::now().to_rfc3339();
diff --git a/src/mcp/tools/repo.rs b/src/mcp/tools/repo.rs
index def4f7d..a623845 100644
--- a/src/mcp/tools/repo.rs
+++ b/src/mcp/tools/repo.rs
@@ -6,7 +6,7 @@ use crate::registry::RepoEntry;
use crate::repository::health::HealthRepository;
use crate::repository::repo::RepoRepository;
use crate::repository::workspace::WorkspaceRepository;
-use crate::storage::AppContext;
+use crate::storage::{AppContext, StorageBackend};
use anyhow::Context;
#[derive(Clone)]
@@ -554,6 +554,34 @@ pub(crate) fn nl_filter_repos(
query: &str,
repos: &[RepoEntry],
conn: &rusqlite::Connection,
+) -> anyhow::Result> {
+ let backend = crate::storage::DefaultStorageBackend {};
+ let index_path = match backend.index_path() {
+ Ok(p) => p,
+ Err(e) => {
+ tracing::warn!("Failed to resolve index path: {}", e);
+ // Fallback to non-Tantivy path
+ let q = query.to_lowercase();
+ let stars_cond = parse_stars_condition(&q);
+ let explicit_tag = extract_tag_from_query(&q);
+ let mut results = Vec::new();
+ for repo in repos {
+ if apply_nl_filters(repo, &q, stars_cond, explicit_tag.as_deref(), conn)? {
+ results.push(repo.clone());
+ }
+ }
+ return Ok(results);
+ }
+ };
+ nl_filter_repos_at(&index_path, query, repos, conn)
+}
+
+/// Filter repos using an explicit Tantivy index path, bypassing global storage backend.
+pub(crate) fn nl_filter_repos_at(
+ index_path: &std::path::Path,
+ query: &str,
+ repos: &[RepoEntry],
+ conn: &rusqlite::Connection,
) -> anyhow::Result> {
let q = query.to_lowercase();
let stars_cond = parse_stars_condition(&q);
@@ -569,7 +597,7 @@ pub(crate) fn nl_filter_repos(
|| q.contains("uptodate");
// Try Tantivy search first if index is not empty
- let use_tantivy = match crate::search::index_is_empty() {
+ let use_tantivy = match crate::search::index_is_empty_at(index_path) {
Ok(empty) => !empty,
Err(e) => {
tracing::warn!("Failed to check search index: {}", e);
@@ -579,7 +607,7 @@ pub(crate) fn nl_filter_repos(
if use_tantivy && !query.trim().is_empty() {
let limit = repos.len().max(1000);
- match crate::search::search_repos(query, limit) {
+ match crate::search::search_repos_at(index_path, query, limit) {
Ok(search_results) => {
let repo_map: std::collections::HashMap<_, _> =
repos.iter().map(|r| (r.id.clone(), r)).collect();
diff --git a/src/mcp/tools/vault.rs b/src/mcp/tools/vault.rs
index 9e915ac..8b90f9a 100644
--- a/src/mcp/tools/vault.rs
+++ b/src/mcp/tools/vault.rs
@@ -203,7 +203,7 @@ Returns: JSON with success status and the written file path."#,
async fn invoke(
&self,
args: serde_json::Value,
- _ctx: &mut crate::storage::AppContext,
+ ctx: &mut crate::storage::AppContext,
) -> anyhow::Result {
let path = args
.get("path")
@@ -215,7 +215,9 @@ Returns: JSON with success status and the written file path."#,
.context("Missing required argument: content")?;
let append = args.get("append").and_then(|v| v.as_bool()).unwrap_or(false);
- let vault_root = crate::registry::WorkspaceRegistry::workspace_dir()
+ let vault_root = ctx
+ .storage
+ .workspace_dir()
.map(|ws| ws.join("vault"))
.unwrap_or_else(|_| std::path::PathBuf::from("vault"));
let target = resolve_vault_path(path, &vault_root)?;
@@ -316,19 +318,18 @@ Returns: JSON array of backlinking notes, each with id, title, and path."#,
async fn invoke(
&self,
args: serde_json::Value,
- _ctx: &mut crate::storage::AppContext,
+ ctx: &mut crate::storage::AppContext,
) -> anyhow::Result {
let note_id = args
.get("note_id")
.and_then(|v| v.as_str())
.context("Missing required argument: note_id")?;
+ let vault_dir = ctx.storage.workspace_dir().ok().map(|ws| ws.join("vault"));
let backlinks = tokio::task::spawn_blocking({
let note_id = note_id.to_string();
+ let vault_dir = vault_dir.clone();
move || {
- let vault_dir = crate::registry::WorkspaceRegistry::workspace_dir()
- .ok()
- .map(|ws| ws.join("vault"));
if let Some(vd) = vault_dir {
match crate::vault::backlinks::build_backlink_index(&vd) {
Ok(index) => crate::vault::backlinks::get_backlinks(&index, ¬e_id),
@@ -389,16 +390,19 @@ Returns: JSON with success status and the generated file path."#,
let config = ctx.config.clone();
let i18n = ctx.i18n;
+ let vault_root = ctx
+ .storage
+ .workspace_dir()
+ .map(|ws| ws.join("vault"))
+ .unwrap_or_else(|_| std::path::PathBuf::from("vault"));
let file_path = tokio::task::spawn_blocking({
let rel_path = rel_path.clone();
let today = today.clone();
+ let vault_root = vault_root.clone();
move || {
let conn = pool.get()?;
let digest = crate::digest::generate_daily_digest(&conn, &config, &i18n)?;
- let vault_root = crate::registry::WorkspaceRegistry::workspace_dir()
- .map(|ws| ws.join("vault"))
- .unwrap_or_else(|_| std::path::PathBuf::from("vault"));
let target = resolve_vault_path(&rel_path, &vault_root)?;
if let Some(parent) = target.parent() {
@@ -460,16 +464,15 @@ Returns: JSON with nodes (id, title) and edges (source, target)."#,
async fn invoke(
&self,
args: serde_json::Value,
- _ctx: &mut crate::storage::AppContext,
+ ctx: &mut crate::storage::AppContext,
) -> anyhow::Result {
let repo_id = args.get("repo_id").and_then(|v| v.as_str()).map(|s| s.to_string());
+ let vault_dir = ctx.storage.workspace_dir().ok().map(|ws| ws.join("vault"));
let graph = tokio::task::spawn_blocking({
let repo_id = repo_id.clone();
+ let vault_dir = vault_dir.clone();
move || {
- let vault_dir = crate::registry::WorkspaceRegistry::workspace_dir()
- .ok()
- .map(|ws| ws.join("vault"));
let Some(vd) = vault_dir else {
return anyhow::Ok(serde_json::json!({
"success": true,
@@ -577,6 +580,7 @@ Returns: JSON with nodes (id, title) and edges (source, target)."#,
#[cfg(test)]
mod tests {
use super::*;
+ use crate::storage::StorageBackend;
use std::time::Instant;
fn vault_root() -> std::path::PathBuf {
@@ -654,11 +658,8 @@ mod tests {
#[tokio::test]
async fn test_vault_daily_creates_file() {
- let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();
let tool = DevkitVaultDailyTool;
let result = tool.invoke(serde_json::json!({}), &mut ctx).await.unwrap();
@@ -676,11 +677,8 @@ mod tests {
#[tokio::test]
async fn test_vault_daily_appends_to_existing() {
- let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();
let tool = DevkitVaultDailyTool;
// First call creates the file
@@ -698,12 +696,9 @@ mod tests {
#[tokio::test]
async fn test_vault_graph_basic() {
- let tmp = tempfile::tempdir().unwrap();
- let vault_dir = tmp.path().join("devbase").join("workspace").join("vault");
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let vault_dir = backend.workspace_dir().unwrap().join("vault");
std::fs::create_dir_all(&vault_dir).unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
std::fs::write(
vault_dir.join("a.md"),
@@ -714,7 +709,7 @@ mod tests {
.unwrap();
std::fs::write(vault_dir.join("c.md"), "---\ntitle: Note C\n---\n\nNo links.\n").unwrap();
- let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
+ let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();
let tool = DevkitVaultGraphTool;
let result = tool.invoke(serde_json::json!({}), &mut ctx).await.unwrap();
@@ -735,12 +730,9 @@ mod tests {
#[tokio::test]
async fn test_vault_graph_filtered_by_repo() {
- let tmp = tempfile::tempdir().unwrap();
- let vault_dir = tmp.path().join("devbase").join("workspace").join("vault");
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let vault_dir = backend.workspace_dir().unwrap().join("vault");
std::fs::create_dir_all(&vault_dir).unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
std::fs::write(
vault_dir.join("repo-a-note.md"),
@@ -753,7 +745,7 @@ mod tests {
)
.unwrap();
- let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
+ let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();
let tool = DevkitVaultGraphTool;
let result =
tool.invoke(serde_json::json!({ "repo_id": "repo-a" }), &mut ctx).await.unwrap();
diff --git a/src/mcp/tools/workflow.rs b/src/mcp/tools/workflow.rs
index 5deb428..bb17bc7 100644
--- a/src/mcp/tools/workflow.rs
+++ b/src/mcp/tools/workflow.rs
@@ -254,11 +254,8 @@ mod tests {
#[tokio::test]
async fn test_workflow_list_empty_registry() {
- let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();
let tool = DevkitWorkflowListTool;
let result = tool.invoke(serde_json::json!({}), &mut ctx).await.unwrap();
@@ -268,11 +265,8 @@ mod tests {
#[tokio::test]
async fn test_workflow_run_not_found() {
- let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();
let tool = DevkitWorkflowRunTool;
let result = tool
@@ -284,11 +278,8 @@ mod tests {
#[tokio::test]
async fn test_workflow_status_invalid_id() {
- let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let mut ctx = crate::storage::AppContext::with_defaults().unwrap();
+ let backend = std::sync::Arc::new(crate::storage::TempStorageBackend::new());
+ let mut ctx = crate::storage::AppContext::with_storage(backend).unwrap();
let tool = DevkitWorkflowStatusTool;
let result = tool.invoke(serde_json::json!({"execution_id": -1}), &mut ctx).await.unwrap();
diff --git a/src/scan.rs b/src/scan.rs
index 198e957..1bd58be 100644
--- a/src/scan.rs
+++ b/src/scan.rs
@@ -213,6 +213,10 @@ fn discover_repos(
exclude_paths: &[String],
exclude_patterns: &[String],
) -> anyhow::Result> {
+ // Canonicalize root to ensure consistent path comparison on Windows,
+ // where TempDir may return short (8.3) filenames while canonicalize expands them.
+ let root = dunce::canonicalize(root).unwrap_or_else(|_| root.to_path_buf());
+
let mut ignored_dirs: Vec = Vec::new();
// Scanning must NOT skip .git directories, otherwise no git repos can be discovered.
@@ -220,32 +224,34 @@ fn discover_repos(
exclude_patterns.iter().filter(|&p| p != ".git").cloned().collect();
// First pass: collect all directories containing .devbase-ignore
- for entry in WalkDir::new(root)
+ for entry in WalkDir::new(&root)
.follow_links(false)
.into_iter()
.filter_entry(|e| !crate::semantic_index::should_skip_dir(e.path(), &scan_exclude_patterns))
.filter_map(|e| e.ok())
{
if entry.file_type().is_dir() && entry.path().join(".devbase-ignore").exists() {
- ignored_dirs.push(entry.path().to_path_buf());
+ let ig =
+ dunce::canonicalize(entry.path()).unwrap_or_else(|_| entry.path().to_path_buf());
+ ignored_dirs.push(ig);
}
}
let is_ignored = |path: &Path| {
ignored_dirs.iter().any(|ig| path.starts_with(ig))
- || is_excluded_path(path, exclude_paths, Some(root))
+ || is_excluded_path(path, exclude_paths, Some(&root))
};
let mut git_repos = Vec::new();
- for entry in WalkDir::new(root)
+ for entry in WalkDir::new(&root)
.follow_links(false)
.into_iter()
.filter_entry(|e| !crate::semantic_index::should_skip_dir(e.path(), &scan_exclude_patterns))
.filter_map(|e| e.ok())
{
if entry.file_name() == ".git" && entry.file_type().is_dir() {
- let repo_path = entry.path().parent().unwrap_or(root).to_path_buf();
+ let repo_path = entry.path().parent().unwrap_or(&root).to_path_buf();
let repo_path = dunce::canonicalize(&repo_path).unwrap_or(repo_path);
// Skip nested .git inside submodules if possible
@@ -265,7 +271,7 @@ fn discover_repos(
// Discover non-git workspaces by marker files
let mut non_git_repos = Vec::new();
- for entry in WalkDir::new(root)
+ for entry in WalkDir::new(&root)
.follow_links(false)
.into_iter()
.filter_entry(|e| !crate::semantic_index::should_skip_dir(e.path(), &scan_exclude_patterns))
@@ -277,7 +283,7 @@ fn discover_repos(
if !is_marker {
continue;
}
- let ws_path = entry.path().parent().unwrap_or(root).to_path_buf();
+ let ws_path = entry.path().parent().unwrap_or(&root).to_path_buf();
// Skip if already inside a known git repo
if is_nested_submodule(&ws_path, &git_repos) {
continue;
diff --git a/src/search.rs b/src/search.rs
index e6e71ab..a6ac84b 100644
--- a/src/search.rs
+++ b/src/search.rs
@@ -33,20 +33,26 @@ fn build_schema() -> Schema {
pub fn init_index() -> Result<(Index, IndexReader), TantivyError> {
let path = index_path()?;
- std::fs::create_dir_all(&path)?;
+ init_index_at(&path)
+}
+
+/// Initialize a Tantivy index at an explicit path, bypassing the global storage backend.
+/// Used by tests and any code that needs hermetic index isolation.
+pub fn init_index_at(path: &std::path::Path) -> Result<(Index, IndexReader), TantivyError> {
+ std::fs::create_dir_all(path)?;
let schema = build_schema();
- let index = match Index::open_in_dir(&path) {
+ let index = match Index::open_in_dir(path) {
Ok(idx) => {
if idx.schema() == schema {
idx
} else {
drop(idx);
- let _ = std::fs::remove_dir_all(&path);
- std::fs::create_dir_all(&path)?;
- Index::create_in_dir(&path, schema)?
+ let _ = std::fs::remove_dir_all(path);
+ std::fs::create_dir_all(path)?;
+ Index::create_in_dir(path, schema)?
}
}
- Err(_) => Index::create_in_dir(&path, schema)?,
+ Err(_) => Index::create_in_dir(path, schema)?,
};
let reader = index.reader_builder().reload_policy(ReloadPolicy::Manual).try_into()?;
Ok((index, reader))
@@ -131,7 +137,12 @@ pub fn commit_writer(writer: &mut IndexWriter) -> Result<(), TantivyError> {
}
pub fn index_is_empty() -> Result {
- let (_index, reader) = init_index()?;
+ let path = index_path()?;
+ index_is_empty_at(&path)
+}
+
+pub fn index_is_empty_at(path: &std::path::Path) -> Result {
+ let (_index, reader) = init_index_at(path)?;
let searcher = reader.searcher();
Ok(searcher.num_docs() == 0)
}
@@ -140,6 +151,14 @@ pub fn index_is_empty() -> Result {
/// Used by startup consistency scan to detect orphan documents.
pub fn list_indexed_repo_ids() -> Result, TantivyError> {
let (index, reader) = init_index()?;
+ list_indexed_repo_ids_with_reader(&reader, &index)
+}
+
+/// List indexed repo IDs using an already-opened index reader.
+fn list_indexed_repo_ids_with_reader(
+ reader: &IndexReader,
+ index: &Index,
+) -> Result, TantivyError> {
let searcher = reader.searcher();
let schema = index.schema();
let id_field = schema.get_field("id").expect("schema field 'id' defined in init_index");
@@ -160,10 +179,26 @@ pub fn list_indexed_repo_ids() -> Result, TantivyError> {
Ok(ids)
}
+/// List indexed repo IDs from an index at an explicit path.
+pub fn list_indexed_repo_ids_at(path: &std::path::Path) -> Result, TantivyError> {
+ let (index, reader) = init_index_at(path)?;
+ list_indexed_repo_ids_with_reader(&reader, &index)
+}
+
/// Scan Tantivy index against SQLite entities table and remove orphan documents.
/// Returns the number of deleted orphan documents.
pub fn sync_index_to_db(conn: &rusqlite::Connection) -> anyhow::Result {
- let tantivy_ids = list_indexed_repo_ids()?;
+ let index_path = index_path()?;
+ sync_index_to_db_at(&index_path, conn)
+}
+
+/// Sync Tantivy index to DB at an explicit index path, bypassing global storage backend.
+pub fn sync_index_to_db_at(
+ index_path: &std::path::Path,
+ conn: &rusqlite::Connection,
+) -> anyhow::Result {
+ let (index, reader) = init_index_at(index_path)?;
+ let tantivy_ids = list_indexed_repo_ids_with_reader(&reader, &index)?;
let db_ids: Vec = conn
.prepare("SELECT id FROM entities WHERE entity_type = ?1")?
.query_map([crate::registry::ENTITY_TYPE_REPO], |row| row.get(0))?
@@ -174,7 +209,7 @@ pub fn sync_index_to_db(conn: &rusqlite::Connection) -> anyhow::Result {
if orphans.is_empty() {
return Ok(0);
}
- let (index, schema) = open_index()?;
+ let (index, schema) = open_index_at(index_path)?;
let mut writer = get_writer(&index)?;
for repo_id in &orphans {
delete_repo_doc(&mut writer, &schema, repo_id)?;
@@ -184,7 +219,16 @@ pub fn sync_index_to_db(conn: &rusqlite::Connection) -> anyhow::Result {
}
pub fn search_repos(query_str: &str, limit: usize) -> Result, TantivyError> {
- search_by_doc_type(query_str, limit, None)
+ let path = index_path()?;
+ search_repos_at(&path, query_str, limit)
+}
+
+pub fn search_repos_at(
+ path: &std::path::Path,
+ query_str: &str,
+ limit: usize,
+) -> Result, TantivyError> {
+ search_by_doc_type_at(path, query_str, limit, None)
}
pub fn search_vault(query_str: &str, limit: usize) -> Result, TantivyError> {
@@ -196,7 +240,17 @@ fn search_by_doc_type(
limit: usize,
doc_type_filter: Option<&str>,
) -> Result, TantivyError> {
- let (index, reader) = init_index()?;
+ let path = index_path()?;
+ search_by_doc_type_at(&path, query_str, limit, doc_type_filter)
+}
+
+fn search_by_doc_type_at(
+ path: &std::path::Path,
+ query_str: &str,
+ limit: usize,
+ doc_type_filter: Option<&str>,
+) -> Result, TantivyError> {
+ let (index, reader) = init_index_at(path)?;
search_with_reader(&index, &reader, query_str, limit, doc_type_filter)
}
@@ -254,8 +308,12 @@ fn search_with_reader(
fn open_index() -> Result<(Index, Schema), TantivyError> {
let path = index_path()?;
+ open_index_at(&path)
+}
+
+fn open_index_at(path: &std::path::Path) -> Result<(Index, Schema), TantivyError> {
let schema = build_schema();
- let dir = tantivy::directory::MmapDirectory::open(&path)?;
+ let dir = tantivy::directory::MmapDirectory::open(path)?;
let idx = Index::open_or_create(dir, schema.clone())?;
Ok((idx, schema))
}
@@ -463,20 +521,15 @@ mod tests {
#[test]
fn test_sync_index_to_db_removes_orphans() {
- let _guard = super::SEARCH_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
- let tmp = tempfile::tempdir().unwrap();
- let old = std::env::var("DEVBASE_DATA_DIR").ok();
- // SAFETY: Test-only DEVBASE_DATA_DIR override. Guarded by SEARCH_TEST_LOCK.
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
+ let backend = crate::storage::TempStorageBackend::new();
+ let index_path = backend.index_path().unwrap();
+ let db_path = backend.db_path().unwrap();
// Initialize DB with full schema
- let db_path = tmp.path().join("registry.db");
let conn = crate::registry::WorkspaceRegistry::init_db_at(&db_path).unwrap();
// Add 2 repo docs to Tantivy
- let (index, _reader) = crate::search::init_index().unwrap();
+ let (index, _reader) = crate::search::init_index_at(&index_path).unwrap();
let mut writer = crate::search::get_writer(&index).unwrap();
let schema = index.schema();
crate::search::add_repo_doc(&mut writer, &schema, "foo", "Foo", "foo content", &[])
@@ -498,27 +551,16 @@ mod tests {
.unwrap();
// Sync should delete bar (orphan)
- let deleted = crate::search::sync_index_to_db(&conn).unwrap();
+ let deleted = crate::search::sync_index_to_db_at(&index_path, &conn).unwrap();
assert_eq!(deleted, 1);
// Windows may need extra time before reopening the index
std::thread::sleep(std::time::Duration::from_millis(1500));
// Verify only foo remains in index
- let remaining = crate::search::list_indexed_repo_ids().unwrap();
+ let remaining = crate::search::list_indexed_repo_ids_at(&index_path).unwrap();
assert_eq!(remaining.len(), 1);
assert_eq!(remaining[0], "foo");
-
- // Restore env
- if let Some(old) = old {
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", old);
- }
- } else {
- unsafe {
- std::env::remove_var("DEVBASE_DATA_DIR");
- }
- }
}
// Helper for test_list_indexed_repo_ids that works with an existing reader
diff --git a/src/skill_runtime/executor.rs b/src/skill_runtime/executor.rs
index 29a605b..68b6fba 100644
--- a/src/skill_runtime/executor.rs
+++ b/src/skill_runtime/executor.rs
@@ -371,7 +371,7 @@ sys.exit(0)
};
let conn = crate::registry::WorkspaceRegistry::init_in_memory().unwrap();
- let result = run_skill(&conn, &skill, &[], std::time::Duration::from_secs(5)).unwrap();
+ let result = run_skill(&conn, &skill, &[], std::time::Duration::from_secs(30)).unwrap();
assert_eq!(result.status, ExecutionStatus::Success);
assert_eq!(result.exit_code, Some(0));
assert!(result.stdout.contains("hello"));
diff --git a/src/storage.rs b/src/storage.rs
index 2bb03cc..8822965 100644
--- a/src/storage.rs
+++ b/src/storage.rs
@@ -145,7 +145,23 @@ impl AppContext {
/// Startup consistency scan: detect Tantivy documents whose repo no longer exists in SQLite.
/// Inserts orphan records into `orphan_tantivy_docs` for lazy cleanup during next index.
pub(crate) fn repair_tantivy_consistency(conn: &mut rusqlite::Connection) -> anyhow::Result {
- let tantivy_ids = match crate::search::list_indexed_repo_ids() {
+ let backend = crate::storage::DefaultStorageBackend {};
+ let index_path = match backend.index_path() {
+ Ok(p) => p,
+ Err(e) => {
+ tracing::warn!("Failed to resolve index path: {}", e);
+ return Ok(0);
+ }
+ };
+ repair_tantivy_consistency_at(&index_path, conn)
+}
+
+/// Repair Tantivy consistency at an explicit index path, bypassing global storage backend.
+pub(crate) fn repair_tantivy_consistency_at(
+ index_path: &std::path::Path,
+ conn: &mut rusqlite::Connection,
+) -> anyhow::Result {
+ let tantivy_ids = match crate::search::list_indexed_repo_ids_at(index_path) {
Ok(ids) => ids,
Err(e) => {
tracing::warn!("Failed to list Tantivy repo IDs: {}", e);
@@ -607,39 +623,47 @@ impl crate::clients::RegistryClient for AppContext {
}
}
+/// Test-only storage backend that uses an independent temporary directory.
+/// Eliminates `DEVBASE_DATA_DIR` environment-variable races during parallel tests.
#[cfg(test)]
-mod tests {
- use super::*;
- use std::path::PathBuf;
-
- struct TempStorageBackend {
- dir: tempfile::TempDir,
- }
+pub(crate) struct TempStorageBackend {
+ dir: tempfile::TempDir,
+}
- impl TempStorageBackend {
- fn new() -> Self {
- Self {
- dir: tempfile::tempdir().unwrap(),
- }
+#[cfg(test)]
+impl TempStorageBackend {
+ pub fn new() -> Self {
+ Self {
+ dir: tempfile::tempdir().unwrap(),
}
}
+}
- impl StorageBackend for TempStorageBackend {
- fn db_path(&self) -> anyhow::Result {
- Ok(self.dir.path().join("registry.db"))
- }
- fn workspace_dir(&self) -> anyhow::Result {
- let ws = self.dir.path().join("workspace");
- std::fs::create_dir_all(&ws)?;
- Ok(ws)
- }
- fn index_path(&self) -> anyhow::Result {
- Ok(self.dir.path().join("search_index"))
- }
- fn backup_dir(&self) -> anyhow::Result {
- Ok(self.dir.path().join("backups"))
- }
+#[cfg(test)]
+impl StorageBackend for TempStorageBackend {
+ fn db_path(&self) -> anyhow::Result {
+ Ok(self.dir.path().join("registry.db"))
+ }
+ fn workspace_dir(&self) -> anyhow::Result {
+ let ws = self.dir.path().join("workspace");
+ std::fs::create_dir_all(&ws)?;
+ std::fs::create_dir_all(ws.join("vault"))?;
+ std::fs::create_dir_all(ws.join("assets"))?;
+ Ok(ws)
}
+ fn index_path(&self) -> anyhow::Result {
+ let dir = self.dir.path();
+ std::fs::create_dir_all(dir)?;
+ Ok(dir.join("search_index"))
+ }
+ fn backup_dir(&self) -> anyhow::Result {
+ Ok(self.dir.path().join("backups"))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
#[test]
fn test_app_context_with_temp_storage() {
@@ -653,20 +677,15 @@ mod tests {
#[test]
fn test_repair_tantivy_consistency_detects_orphan() {
- let _guard = crate::search::SEARCH_TEST_LOCK.lock().unwrap_or_else(|p| p.into_inner());
- let tmp = tempfile::tempdir().unwrap();
- let old = std::env::var("DEVBASE_DATA_DIR").ok();
- // SAFETY: Test-only DEVBASE_DATA_DIR override. Guarded by SEARCH_TEST_LOCK.
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
+ let backend = crate::storage::TempStorageBackend::new();
+ let index_path = backend.index_path().unwrap();
+ let db_path = backend.db_path().unwrap();
// Initialize DB with full schema
- let db_path = tmp.path().join("registry.db");
let mut conn = crate::registry::WorkspaceRegistry::init_db_at(&db_path).unwrap();
// Add a Tantivy doc for a repo that does NOT exist in SQLite
- let (index, _reader) = crate::search::init_index().unwrap();
+ let (index, _reader) = crate::search::init_index_at(&index_path).unwrap();
let mut writer = crate::search::get_writer(&index).unwrap();
let schema = index.schema();
crate::search::add_repo_doc(
@@ -686,7 +705,7 @@ mod tests {
std::thread::sleep(std::time::Duration::from_millis(800));
// Repair should detect the orphan
- let count = repair_tantivy_consistency(&mut conn).unwrap();
+ let count = repair_tantivy_consistency_at(&index_path, &mut conn).unwrap();
assert_eq!(count, 1);
let orphan_exists: bool = conn
@@ -704,7 +723,7 @@ mod tests {
).unwrap();
// Repair should now find zero orphans and clear the record
- let count2 = repair_tantivy_consistency(&mut conn).unwrap();
+ let count2 = repair_tantivy_consistency_at(&index_path, &mut conn).unwrap();
assert_eq!(count2, 0);
let orphan_still_exists: bool = conn
@@ -713,16 +732,5 @@ mod tests {
})
.unwrap_or(false);
assert!(!orphan_still_exists);
-
- // Restore env
- if let Some(old) = old {
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", old);
- }
- } else {
- unsafe {
- std::env::remove_var("DEVBASE_DATA_DIR");
- }
- }
}
}
diff --git a/src/sync/tests.rs b/src/sync/tests.rs
index d121d76..7f25de8 100644
--- a/src/sync/tests.rs
+++ b/src/sync/tests.rs
@@ -7,7 +7,7 @@ use std::fs;
use tempfile::TempDir;
fn create_commit(repo: &Repository, message: &str) -> git2::Oid {
- let sig = repo.signature().unwrap();
+ let sig = git2::Signature::now("Test", "test@example.com").unwrap();
let tree_id = {
let mut index = repo.index().unwrap();
index.write_tree().unwrap()
@@ -19,8 +19,10 @@ fn create_commit(repo: &Repository, message: &str) -> git2::Oid {
.and_then(|h| h.target())
.and_then(|oid| repo.find_commit(oid).ok());
match parent {
- Some(ref p) => repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[p]).unwrap(),
- None => repo.commit(Some("HEAD"), &sig, &sig, message, &tree, &[]).unwrap(),
+ Some(ref p) => {
+ repo.commit(Some("refs/heads/main"), &sig, &sig, message, &tree, &[p]).unwrap()
+ }
+ None => repo.commit(Some("refs/heads/main"), &sig, &sig, message, &tree, &[]).unwrap(),
}
}
@@ -30,22 +32,24 @@ fn setup_repo_with_remote_commits(
) -> (TempDir, Repository) {
let dir = TempDir::new().unwrap();
let repo = Repository::init(&dir).unwrap();
+ repo.set_head("refs/heads/main").unwrap();
// Initial commit on main
fs::write(dir.path().join("file.txt"), "base").unwrap();
let mut index = repo.index().unwrap();
index.add_path(std::path::Path::new("file.txt")).unwrap();
index.write().unwrap();
- let sig = repo.signature().unwrap();
+ let sig = git2::Signature::now("Test", "test@example.com").unwrap();
let tree_id = index.write_tree().unwrap();
{
let tree = repo.find_tree(tree_id).unwrap();
- repo.commit(Some("HEAD"), &sig, &sig, "base", &tree, &[]).unwrap();
+ repo.commit(Some("refs/heads/main"), &sig, &sig, "base", &tree, &[]).unwrap();
}
// Create origin remote pointing to a bare repo
let bare_dir = TempDir::new().unwrap();
- let _bare_repo = Repository::init_bare(&bare_dir).unwrap();
+ let bare_repo = Repository::init_bare(&bare_dir).unwrap();
+ bare_repo.set_head("refs/heads/main").unwrap();
repo.remote("origin", bare_dir.path().to_str().unwrap()).unwrap();
// Push base to origin/main
@@ -62,13 +66,20 @@ fn setup_repo_with_remote_commits(
let mut hindex = helper.index().unwrap();
hindex.add_path(std::path::Path::new("file.txt")).unwrap();
hindex.write().unwrap();
- let hsig = helper.signature().unwrap();
+ let hsig = git2::Signature::now("Test", "test@example.com").unwrap();
let htree_id = hindex.write_tree().unwrap();
{
let htree = helper.find_tree(htree_id).unwrap();
let hparent = helper.head().unwrap().peel_to_commit().unwrap();
helper
- .commit(Some("HEAD"), &hsig, &hsig, &format!("remote{}", i), &htree, &[&hparent])
+ .commit(
+ Some("refs/heads/main"),
+ &hsig,
+ &hsig,
+ &format!("remote{}", i),
+ &htree,
+ &[&hparent],
+ )
.unwrap();
}
}
@@ -145,10 +156,11 @@ fn test_assess_safety_up_to_date() {
fn test_assess_safety_no_upstream() {
let dir = TempDir::new().unwrap();
let _repo = Repository::init(&dir).unwrap();
- let sig = _repo.signature().unwrap();
+ let sig = git2::Signature::now("Test", "test@example.com").unwrap();
let tree_id = _repo.index().unwrap().write_tree().unwrap();
let tree = _repo.find_tree(tree_id).unwrap();
- _repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[]).unwrap();
+ _repo.commit(Some("refs/heads/main"), &sig, &sig, "init", &tree, &[]).unwrap();
+ _repo.set_head("refs/heads/main").unwrap();
let (safety, _, _) = assess_safety(dir.path().to_str().unwrap(), SyncPolicy::from_tags(""));
assert_eq!(safety, SyncSafety::NoUpstream);
}
@@ -187,10 +199,10 @@ fn test_write_syncdone_marker() {
fn test_sync_repo_skip_no_syncdone() {
let dir = TempDir::new().unwrap();
let repo = Repository::init(&dir).unwrap();
- let sig = repo.signature().unwrap();
+ let sig = git2::Signature::now("Test", "test@example.com").unwrap();
let tree_id = repo.index().unwrap().write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
- repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[]).unwrap();
+ repo.commit(Some("refs/heads/main"), &sig, &sig, "init", &tree, &[]).unwrap();
// Simulate a SKIP summary: write_syncdone_marker should NOT be called
let syncdone_path = dir.path().join(".devbase").join("syncdone");
diff --git a/src/workflow/executor.rs b/src/workflow/executor.rs
index 8dad620..4fe58d7 100644
--- a/src/workflow/executor.rs
+++ b/src/workflow/executor.rs
@@ -492,7 +492,6 @@ mod tests {
use super::*;
use crate::skill_runtime::registry::install_skill;
use crate::skill_runtime::{SkillMeta, SkillType};
- use crate::storage::StorageBackend;
use crate::workflow::model::{ErrorPolicy, StepDefinition, StepType, WorkflowDefinition};
use std::collections::HashMap;
@@ -502,11 +501,7 @@ mod tests {
rusqlite::Connection,
) {
let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let backend = crate::storage::DefaultStorageBackend {};
- let path = backend.db_path().unwrap();
+ let path = tmp.path().join("registry.db");
let conn = crate::registry::WorkspaceRegistry::init_db_at(&path).unwrap();
let manager = r2d2_sqlite::SqliteConnectionManager::file(&path).with_init(|c| {
c.execute("PRAGMA foreign_keys = ON", [])?;
diff --git a/src/workflow/state.rs b/src/workflow/state.rs
index d166a3f..7c8b150 100644
--- a/src/workflow/state.rs
+++ b/src/workflow/state.rs
@@ -132,7 +132,6 @@ fn parse_status(s: &str) -> ExecutionStatus {
mod tests {
use super::*;
use crate::registry::WorkspaceRegistry;
- use crate::storage::StorageBackend;
use std::collections::HashMap;
fn dummy_wf() -> WorkflowDefinition {
@@ -176,11 +175,7 @@ mod tests {
use crate::workflow::model::{ErrorPolicy, StepDefinition, StepType};
use crate::workflow::{execute_workflow, validate_workflow};
let tmp = tempfile::tempdir().unwrap();
- unsafe {
- std::env::set_var("DEVBASE_DATA_DIR", tmp.path());
- }
- let backend = crate::storage::DefaultStorageBackend {};
- let path = backend.db_path().unwrap();
+ let path = tmp.path().join("registry.db");
let conn = crate::registry::WorkspaceRegistry::init_db_at(&path).unwrap();
let manager = r2d2_sqlite::SqliteConnectionManager::file(&path).with_init(|c| {
c.execute("PRAGMA foreign_keys = ON", [])?;
diff --git a/tests/cli.rs b/tests/cli.rs
index 79850e6..4b073f2 100644
--- a/tests/cli.rs
+++ b/tests/cli.rs
@@ -79,10 +79,11 @@ fn test_scan_git_repo() {
let repo_dir = tmp.path().join("test-repo");
fs::create_dir(&repo_dir).unwrap();
let repo = git2::Repository::init(&repo_dir).unwrap();
- let sig = repo.signature().unwrap();
+ let sig = git2::Signature::now("Test", "test@example.com").unwrap();
let tree_id = repo.index().unwrap().write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
- repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[]).unwrap();
+ repo.commit(Some("refs/heads/main"), &sig, &sig, "init", &tree, &[]).unwrap();
+ repo.set_head("refs/heads/main").unwrap();
cmd.args(["scan", repo_dir.to_str().unwrap(), "--register"]);
cmd.assert().success().stdout(predicate::str::contains("test-repo"));
@@ -140,10 +141,11 @@ fn test_backup_export() {
/// Helper: create a minimal git repo with one commit.
fn init_git_repo(path: &std::path::Path) {
let repo = git2::Repository::init(path).unwrap();
- let sig = repo.signature().unwrap();
+ let sig = git2::Signature::now("Test", "test@example.com").unwrap();
let tree_id = repo.index().unwrap().write_tree().unwrap();
let tree = repo.find_tree(tree_id).unwrap();
- repo.commit(Some("HEAD"), &sig, &sig, "init", &tree, &[]).unwrap();
+ repo.commit(Some("refs/heads/main"), &sig, &sig, "init", &tree, &[]).unwrap();
+ repo.set_head("refs/heads/main").unwrap();
}
#[test]