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 @@ [![Version](https://img.shields.io/badge/version-v0.14.3-blue)](https://github.com/juice094/devbase/releases) [![Tests](https://img.shields.io/badge/tests-390%20passed-brightgreen)](./AGENTS.md) [![Clippy](https://img.shields.io/badge/clippy-0%20warnings-green)](./AGENTS.md) -[![License](https://img.shields.io/badge/license-MIT-orange)](./LICENSE) +[![License](https://img.shields.io/badge/license-AGPL--3.0-orange)](./LICENSE) [![Rust](https://img.shields.io/badge/rust-1.94%2B-9cf)](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]