增加Skill每日自动更新和使用记录
This commit is contained in:
@@ -3,3 +3,6 @@ __pycache__/
|
||||
outputs/
|
||||
scripts/config.json
|
||||
scripts/*.token
|
||||
scripts/update-state.json
|
||||
scripts/update-state.json.*
|
||||
scripts/update-state.lock
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# oracle-jump-query Skill Release Standard
|
||||
|
||||
Release 1.5.51: CLI 每次启动记录最后使用时间,按本机日期每日首次使用自动抓取 origin/main 并仅快进安装,成功后重新启动当前命令;本机状态与并发锁不提交 Git。保留本机配置,源码修改、暂存修改、分支分叉或配置冲突时暂停安装;失败不阻止业务命令。本次新增本机更新逻辑,不修改 Agent、transit-server、HTTP 接口、登录配置或审计 action、字段及保存策略,命令数保持 32。发布时同步私有主库、安装目录和无私有历史公共镜像。
|
||||
|
||||
取号 CODE 定义在 `AD_SEQUENCE` 表;1.5.50 的编号规则分析路径为 `DOCNO -> AD_COLUMN.SEQUENCENAME -> AD_SEQUENCE`。
|
||||
|
||||
Release 1.5.50: 明确取单号函数 `Get_SequenceNo('<CODE>', AD_CLIENT_ID)`,默认 `CODE` 取单据 `DOCNO` 字段对应的 `AD_COLUMN.SEQUENCENAME`,客户 ID 使用单据实际值;取号仅生成 SQL 示例,不通过查询通道调用。本次仅更新 Skill 文档,不修改 Agent、transit-server、CLI/HTTP 接口、配置或审计 action、字段及保存策略;命令数保持 32。发布时同步私有主库、安装目录和无私有历史公共镜像。
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
## 1.5.51 (2026-09-30)
|
||||
|
||||
- CLI 每次启动记录最后使用时间;按本机日期,每个使用日首次启动自动抓取 `origin/main`,仅快进安装更新并重新启动当前命令,同日不重复检测。
|
||||
- 使用独立本机状态文件和并发锁,保留本机配置;源码修改、暂存修改、分支分叉或配置冲突时暂停安装,抓取失败和超时继续业务命令,更新提示不污染 JSON 输出。
|
||||
- 本次新增 Skill 本地更新逻辑,不修改 Agent、transit-server、HTTP 接口、登录配置格式或审计 action、字段及保存策略;CLI 命令数保持 32。使用状态仅本机保存,不写入中转审计库。
|
||||
|
||||
## 1.5.50 (2026-09-30)
|
||||
|
||||
- 明确取单号函数 `Get_SequenceNo('<CODE>', AD_CLIENT_ID)`,默认 `CODE` 来自该单据 `DOCNO` 字段对应的 `AD_COLUMN.SEQUENCENAME`;客户 ID 使用单据实际值,不固定为 `37`。
|
||||
|
||||
@@ -306,6 +306,7 @@ python scripts/oracle_skill.py ops_report HENLO
|
||||
- 如果表存在 `DOCNO` 字段,先到 `AD_COLUMN` 查询该字段的 `SEQUENCENAME`。
|
||||
- 取单号函数为 `Get_SequenceNo('<CODE>', AD_CLIENT_ID)`;默认 `CODE` 取该单据 `DOCNO` 字段对应的 `AD_COLUMN.SEQUENCENAME` 值,第二个参数取单据实际的 `AD_CLIENT_ID`,确认是 `37` 时才写 `37`。配置为空时不猜测 `CODE`,特殊业务按实际配置和过程逻辑核实;只生成 SQL 示例,不通过查询通道调用取号函数。
|
||||
- 取号 `CODE` 的定义在 `AD_SEQUENCE` 表,分析编号规则时沿 `DOCNO -> AD_COLUMN.SEQUENCENAME -> AD_SEQUENCE` 核实。
|
||||
|
||||
- 如果表存在 `AD_CLIENT_ID` 字段,默认值为 `37`。
|
||||
- 如果表存在 `AD_ORG_ID` 字段,默认值为 `27`。
|
||||
- 子表 `AD_CLIENT_ID`、`AD_ORG_ID` 默认继承父表对应字段值。
|
||||
@@ -353,6 +354,14 @@ python scripts/oracle_skill.py version agent --all --json
|
||||
|
||||
## 从 Git 更新 Skill
|
||||
|
||||
### 每日自动更新与使用记录
|
||||
|
||||
CLI 启动时记录最后使用时间;按运行电脑本地日期,每个使用日首次启动自动 `fetch origin main`,检测到新版本时仅在 `main` 分支执行快进安装,并重新启动当前命令使用新版本。同一天后续启动不重复抓取;未使用的日期不运行后台任务。每次开始应用 Skill 时先运行 `python scripts/oracle_skill.py capabilities --json`,使只参考业务知识的任务也触发检测。
|
||||
|
||||
本机状态文件为 `scripts/update-state.json`:`last_used_at` 记录最后使用时间,`last_checked_at` / `last_check_date` 记录检测时间和日期,`status` 记录 `checking`、`up_to_date`、`updated`、`update_blocked`、`check_failed` 或 `not_git`;检测完成时可包含本机和远端提交 ID。状态、锁和临时文件不提交 Git,不保存 token、密钥、SQL 或查询结果。
|
||||
|
||||
自动更新保留本机 `config.json` 和 `scripts/config.json`;源码有未提交修改、存在暂存修改、分支分叉或配置冲突时暂停安装并继续原命令。抓取超时为 10 秒,认证和网络失败不阻止业务命令,也不在当天每次调用时重试;可手动执行 `git pull --ff-only origin main` 重试。提示写入 stderr,stdout 的 JSON 输出不受影响。Git checkout 使用自己的 `origin`,私有和公共安装均可使用;无 `.git` 的下载目录只记录使用时间。
|
||||
|
||||
Skill 仅通过 Gitea Git 仓库更新,不使用 OSS 或 `skill_update` 命令。源码仓库完成修改并推送私有库后,必须同步更新无历史公共镜像;随后在已安装目录执行:
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -5,10 +5,12 @@ description: Oracle 跳板查询技能。通过中转服务查询远程 Oracle
|
||||
|
||||
# Oracle 跳板查询
|
||||
|
||||
> **版本:v1.5.50** · [更新日志](./CHANGELOG.md)
|
||||
> **版本:v1.5.51** · [更新日志](./CHANGELOG.md)
|
||||
|
||||
## 使用原则
|
||||
|
||||
每次开始使用本 Skill(包括仅参考业务知识)时,先执行 `python scripts/oracle_skill.py capabilities --json`。CLI 每次启动记录最后使用时间;按运行电脑本地日期,当天第一次启动自动从当前 `origin/main` 抓取更新并尝试快进安装,成功后自动重新启动当前命令。无需登录即可检测;当天后续启动只记录使用时间。没有使用的日期不启动后台任务。
|
||||
|
||||
使用本 skill 时,先确认当前登录状态和 client。用户指定“恒诺 / 品小二 / 未芮 / 康奈”等服务器时,可以用 `switch <clientCode|clientName>` 按 code 或名称切换;找不到目标服务器时,skill 会先刷新 client 列表再重试匹配。若名称不够准确并匹配到多个 client,只列出候选项,请用户指定更准确的 client code,不要直接猜测切换。
|
||||
|
||||
本 skill 的数据库通道默认只读。可以生成 SQL 和说明,但不得通过 `query` / `qperm` 执行写库语句,包括 `INSERT`、`UPDATE`、`DELETE`、`MERGE`、`DDL`、提交存储过程和其他会改变业务数据的调用。用户要求写库时,只生成 SQL,并明确说明未执行。
|
||||
@@ -216,6 +218,8 @@ python scripts/oracle_skill.py sys_functions BOSNDS3
|
||||
|
||||
## Skill 更新方式
|
||||
|
||||
每日检测和使用记录保存在本机 `scripts/update-state.json`,包含 `last_used_at`、`last_checked_at`、`last_check_date`、`status` 及检测到的提交 ID;文件和并发锁均不提交 Git,不保存登录凭证或查询内容。检测失败仍记为当天已尝试,下次使用日再自动检测;可随时执行下面的手动拉取命令重试。自动安装只允许 `main` 分支快进:源码有未提交修改、存在暂存修改、分支分叉或配置冲突时保留本机文件并继续原命令;本机 `config.json`、`scripts/config.json` 保留原内容。网络抓取设有 10 秒超时,提示只输出到 stderr,`--json` 的 stdout 保持可解析。没有 `.git` 的下载目录仍记录使用时间,但需先安装为 Git checkout 才能自动更新。
|
||||
|
||||
Skill 仅通过 Gitea Git 仓库更新,不使用 OSS 发布或 `skill_update` 命令。源码修改、提交和推送均在源码仓库完成;私有库推送后必须同步无历史公共镜像,已安装目录仅执行拉取与验证:
|
||||
|
||||
```powershell
|
||||
|
||||
@@ -3823,4 +3823,7 @@ def main():
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from skill_update import on_use
|
||||
if on_use(SKILL_DIR):
|
||||
os.execv(sys.executable, [sys.executable] + sys.argv)
|
||||
main()
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
"""Daily Git update checks and local usage timestamps, independent of login config."""
|
||||
|
||||
import contextlib
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
GIT_TIMEOUT = 10
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _state_lock(path):
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with path.open("a+b") as handle:
|
||||
if os.name == "nt":
|
||||
import msvcrt
|
||||
handle.seek(0, os.SEEK_END)
|
||||
if not handle.tell():
|
||||
handle.write(b"0")
|
||||
handle.flush()
|
||||
handle.seek(0)
|
||||
msvcrt.locking(handle.fileno(), msvcrt.LK_LOCK, 1)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
handle.seek(0)
|
||||
msvcrt.locking(handle.fileno(), msvcrt.LK_UNLCK, 1)
|
||||
else:
|
||||
import fcntl
|
||||
fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
|
||||
def _read_state(path):
|
||||
try:
|
||||
with path.open(encoding="utf-8") as handle:
|
||||
state = json.load(handle)
|
||||
return state if isinstance(state, dict) else {}
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def _write_state(path, state):
|
||||
fd, temporary = tempfile.mkstemp(prefix=path.name + ".", dir=str(path.parent))
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as handle:
|
||||
json.dump(state, handle, ensure_ascii=False, indent=2)
|
||||
handle.write("\n")
|
||||
os.replace(temporary, str(path))
|
||||
finally:
|
||||
if os.path.exists(temporary):
|
||||
os.unlink(temporary)
|
||||
|
||||
|
||||
def _git(root, *args):
|
||||
env = os.environ.copy()
|
||||
env["GIT_TERMINAL_PROMPT"] = "0"
|
||||
env["GCM_INTERACTIVE"] = "never"
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(root), *args], stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE, timeout=GIT_TIMEOUT, env=env,
|
||||
)
|
||||
if result.returncode:
|
||||
# Git output can contain authenticated remote URLs; never store or print it.
|
||||
raise RuntimeError("git command failed")
|
||||
return result.stdout.decode("utf-8", errors="replace").strip()
|
||||
|
||||
|
||||
def _check_remote(root):
|
||||
# A downloaded directory may sit inside an unrelated parent repository.
|
||||
if not (root / ".git").exists():
|
||||
return {"status": "not_git"}
|
||||
_git(root, "fetch", "--quiet", "origin", "main")
|
||||
local = _git(root, "rev-parse", "HEAD")
|
||||
remote = _git(root, "rev-parse", "FETCH_HEAD")
|
||||
outcome = {
|
||||
"status": "up_to_date" if local == remote else "update_available",
|
||||
"local_commit": local,
|
||||
"remote_commit": remote,
|
||||
}
|
||||
if local == remote:
|
||||
return outcome
|
||||
if _git(root, "rev-parse", "--abbrev-ref", "HEAD") != "main":
|
||||
outcome["status"] = "update_blocked"
|
||||
return outcome
|
||||
changed = _git(root, "diff", "--name-only").splitlines()
|
||||
if any(name not in ("config.json", "scripts/config.json") for name in changed) or _git(root, "diff", "--cached", "--name-only"):
|
||||
outcome["status"] = "update_blocked"
|
||||
return outcome
|
||||
try:
|
||||
_git(root, "merge-base", "--is-ancestor", local, remote)
|
||||
except RuntimeError:
|
||||
outcome["status"] = "update_blocked"
|
||||
return outcome
|
||||
# Keep configuration bytes local, including clean tracked template config.
|
||||
saved = {}
|
||||
for name in ("config.json", "scripts/config.json"):
|
||||
path = root / name
|
||||
if path.exists():
|
||||
saved[path] = path.read_bytes()
|
||||
try:
|
||||
_git(root, "merge", "--ff-only", "--quiet", remote)
|
||||
except RuntimeError:
|
||||
outcome["status"] = "update_blocked"
|
||||
return outcome
|
||||
finally:
|
||||
for path, data in saved.items():
|
||||
if not path.exists() or path.read_bytes() != data:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(data)
|
||||
outcome["status"] = "updated"
|
||||
outcome["local_commit"] = remote
|
||||
return outcome
|
||||
|
||||
|
||||
def on_use(skill_dir, now=None):
|
||||
"""Record every invocation; claim today's check before the network call."""
|
||||
root = Path(skill_dir).resolve()
|
||||
state_path = root / "scripts" / "update-state.json"
|
||||
lock_path = root / "scripts" / "update-state.lock"
|
||||
now = now or datetime.now().astimezone()
|
||||
stamp, day = now.isoformat(timespec="seconds"), now.date().isoformat()
|
||||
try:
|
||||
with _state_lock(lock_path):
|
||||
state = _read_state(state_path)
|
||||
state["last_used_at"] = stamp
|
||||
should_check = state.get("last_check_date") != day
|
||||
if should_check:
|
||||
state.update(last_check_date=day, last_checked_at=stamp, status="checking")
|
||||
state.pop("local_commit", None)
|
||||
state.pop("remote_commit", None)
|
||||
_write_state(state_path, state)
|
||||
if not should_check:
|
||||
return False
|
||||
try:
|
||||
outcome = _check_remote(root)
|
||||
except (OSError, subprocess.SubprocessError, RuntimeError):
|
||||
outcome = {"status": "check_failed"}
|
||||
with _state_lock(lock_path):
|
||||
state = _read_state(state_path)
|
||||
# Preserve usage from concurrent invocations, and a newer day's claim.
|
||||
if state.get("last_check_date") == day:
|
||||
state.update(outcome)
|
||||
_write_state(state_path, state)
|
||||
if outcome["status"] == "updated":
|
||||
print("[Skill] 已快进安装 Git 新版本,重新启动命令。", file=sys.stderr)
|
||||
elif outcome["status"] == "update_blocked":
|
||||
print("[Skill] 检测到远端版本变化,本机修改或分支状态阻止快进更新;继续执行原命令。", file=sys.stderr)
|
||||
elif outcome["status"] == "check_failed":
|
||||
print("[Skill] 今日更新检测失败,继续执行原命令;下次使用日自动重试。", file=sys.stderr)
|
||||
return outcome["status"] == "updated"
|
||||
except OSError:
|
||||
print("[Skill] 无法保存本机使用记录,继续执行原命令。", file=sys.stderr)
|
||||
return False
|
||||
@@ -0,0 +1,160 @@
|
||||
import contextlib
|
||||
import io
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest import mock
|
||||
|
||||
from scripts import skill_update
|
||||
|
||||
|
||||
class DailyUpdateTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.temp = tempfile.TemporaryDirectory()
|
||||
self.addCleanup(self.temp.cleanup)
|
||||
self.root = pathlib.Path(self.temp.name)
|
||||
self.now = datetime(2026, 9, 30, 9, tzinfo=timezone(timedelta(hours=8)))
|
||||
|
||||
def state(self, root=None):
|
||||
return json.loads(((root or self.root) / "scripts/update-state.json").read_text(encoding="utf-8"))
|
||||
|
||||
def git(self, root, *args):
|
||||
result = subprocess.run(
|
||||
["git", "-C", str(root), *args], check=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
|
||||
)
|
||||
return result.stdout.decode().strip()
|
||||
|
||||
def commit(self, root, message):
|
||||
self.git(root, "add", "--all")
|
||||
self.git(root, "-c", "user.name=Update Test", "-c", "user.email=update@example.test", "commit", "-m", message)
|
||||
|
||||
def repository(self):
|
||||
remote, source, installed = [self.root / name for name in ("remote.git", "source", "installed")]
|
||||
self.git(self.root, "init", "--bare", str(remote))
|
||||
self.git(self.root, "init", "-b", "main", str(source))
|
||||
(source / "config.json").write_bytes(b'{"local":"keep"}\n')
|
||||
(source / "VERSION").write_text("1.0.0\n")
|
||||
(source / ".gitignore").write_text("scripts/update-state*\nscripts/config.json\n")
|
||||
self.commit(source, "initial")
|
||||
self.git(source, "remote", "add", "origin", str(remote))
|
||||
self.git(source, "push", "origin", "main")
|
||||
self.git(self.root, "clone", "-b", "main", str(remote), str(installed))
|
||||
(installed / "scripts").mkdir()
|
||||
(installed / "scripts/config.json").write_bytes(b'{"access_token":"local-only"}')
|
||||
return source, installed
|
||||
|
||||
def publish(self, source, config=False):
|
||||
(source / "VERSION").write_text("1.0.1\n")
|
||||
if config:
|
||||
(source / "config.json").write_bytes(b'{"new":"template"}\n')
|
||||
self.commit(source, "update")
|
||||
self.git(source, "push", "origin", "main")
|
||||
|
||||
def test_once_per_local_day_and_every_usage_timestamp(self):
|
||||
with mock.patch.object(skill_update, "_check_remote", return_value={"status": "up_to_date"}) as check:
|
||||
skill_update.on_use(self.root, self.now)
|
||||
skill_update.on_use(self.root, self.now + timedelta(hours=2))
|
||||
self.assertEqual(check.call_count, 1)
|
||||
self.assertEqual(self.state()["last_used_at"], "2026-09-30T11:00:00+08:00")
|
||||
skill_update.on_use(self.root, self.now + timedelta(days=1))
|
||||
self.assertEqual(check.call_count, 2)
|
||||
|
||||
def test_failed_check_does_not_block_or_repeat_today_or_leak_output(self):
|
||||
with mock.patch.object(skill_update, "_check_remote", side_effect=RuntimeError("secret-url")) as check:
|
||||
output, error = io.StringIO(), io.StringIO()
|
||||
with contextlib.redirect_stdout(output), contextlib.redirect_stderr(error):
|
||||
self.assertFalse(skill_update.on_use(self.root, self.now))
|
||||
skill_update.on_use(self.root, self.now)
|
||||
self.assertEqual(check.call_count, 1)
|
||||
self.assertEqual(output.getvalue(), "")
|
||||
self.assertNotIn("secret-url", error.getvalue())
|
||||
self.assertNotIn("secret-url", json.dumps(self.state()))
|
||||
self.assertEqual(self.state()["status"], "check_failed")
|
||||
|
||||
def test_fast_forward_keeps_modified_local_login_configuration(self):
|
||||
source, installed = self.repository()
|
||||
(installed / "config.json").write_bytes(b'{"local":"modified"}')
|
||||
original = (installed / "config.json").read_bytes()
|
||||
self.publish(source)
|
||||
self.assertTrue(skill_update.on_use(installed, self.now))
|
||||
self.assertEqual(self.git(installed, "rev-parse", "HEAD"), self.git(source, "rev-parse", "HEAD"))
|
||||
self.assertEqual((installed / "config.json").read_bytes(), original)
|
||||
self.assertEqual((installed / "scripts/config.json").read_bytes(), b'{"access_token":"local-only"}')
|
||||
self.assertEqual(self.state(installed)["status"], "updated")
|
||||
|
||||
def test_clean_config_preserved_when_upstream_template_changes(self):
|
||||
source, installed = self.repository()
|
||||
original = (installed / "config.json").read_bytes()
|
||||
self.publish(source, config=True)
|
||||
self.assertTrue(skill_update.on_use(installed, self.now))
|
||||
self.assertEqual((installed / "config.json").read_bytes(), original)
|
||||
self.assertEqual((installed / "VERSION").read_text(), "1.0.1\n")
|
||||
|
||||
def test_source_edits_block_install_but_do_not_discard_changes(self):
|
||||
source, installed = self.repository()
|
||||
self.publish(source)
|
||||
(installed / "VERSION").write_text("local edit\n")
|
||||
head = self.git(installed, "rev-parse", "HEAD")
|
||||
self.assertFalse(skill_update.on_use(installed, self.now))
|
||||
self.assertEqual(self.git(installed, "rev-parse", "HEAD"), head)
|
||||
self.assertEqual((installed / "VERSION").read_text(), "local edit\n")
|
||||
self.assertEqual(self.state(installed)["status"], "update_blocked")
|
||||
|
||||
def test_conflicting_remote_config_keeps_dirty_local_config(self):
|
||||
source, installed = self.repository()
|
||||
(installed / "config.json").write_bytes(b'{"local":"changed"}')
|
||||
self.publish(source, config=True)
|
||||
head = self.git(installed, "rev-parse", "HEAD")
|
||||
self.assertFalse(skill_update.on_use(installed, self.now))
|
||||
self.assertEqual(self.git(installed, "rev-parse", "HEAD"), head)
|
||||
self.assertEqual((installed / "config.json").read_bytes(), b'{"local":"changed"}')
|
||||
self.assertEqual(self.state(installed)["status"], "update_blocked")
|
||||
|
||||
def test_cli_restarts_into_new_version_and_keeps_stdout_json(self):
|
||||
source, installed = self.repository()
|
||||
(source / "scripts").mkdir()
|
||||
scripts = pathlib.Path(skill_update.__file__).parent
|
||||
for name in ("oracle_skill.py", "skill_update.py"):
|
||||
shutil.copyfile(str(scripts / name), str(source / "scripts" / name))
|
||||
self.commit(source, "add cli")
|
||||
self.git(source, "push", "origin", "main")
|
||||
self.git(installed, "pull", "--ff-only", "origin", "main")
|
||||
self.publish(source)
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(installed / "scripts/oracle_skill.py"), "capabilities", "--json"],
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, timeout=30,
|
||||
)
|
||||
self.assertEqual(json.loads(result.stdout.decode("utf-8"))["version"], "1.0.1")
|
||||
self.assertEqual(self.state(installed)["status"], "updated")
|
||||
self.assertEqual(self.git(installed, "rev-parse", "HEAD"), self.git(source, "rev-parse", "HEAD"))
|
||||
|
||||
def test_diverged_branch_never_resets_local_commit(self):
|
||||
source, installed = self.repository()
|
||||
(installed / "VERSION").write_text("local branch\n")
|
||||
self.commit(installed, "local")
|
||||
self.publish(source)
|
||||
head = self.git(installed, "rev-parse", "HEAD")
|
||||
self.assertFalse(skill_update.on_use(installed, self.now))
|
||||
self.assertEqual(self.git(installed, "rev-parse", "HEAD"), head)
|
||||
self.assertEqual(self.state(installed)["status"], "update_blocked")
|
||||
|
||||
def test_download_without_git_does_not_fetch_parent_repository(self):
|
||||
with mock.patch.object(skill_update, "_git") as git:
|
||||
self.assertFalse(skill_update.on_use(self.root, self.now))
|
||||
git.assert_not_called()
|
||||
self.assertEqual(self.state()["status"], "not_git")
|
||||
|
||||
def test_concurrent_invocation_does_not_fetch_twice_or_lose_latest_usage(self):
|
||||
def checking(root):
|
||||
skill_update.on_use(root, self.now + timedelta(minutes=1))
|
||||
return {"status": "up_to_date"}
|
||||
with mock.patch.object(skill_update, "_check_remote", side_effect=checking) as check:
|
||||
skill_update.on_use(self.root, self.now)
|
||||
self.assertEqual(check.call_count, 1)
|
||||
self.assertEqual(self.state()["last_used_at"], "2026-09-30T09:01:00+08:00")
|
||||
Reference in New Issue
Block a user