74일차 정답 공개 (LAB-A · VIBE-A · answers · tver), Lab 03 예제 3 이해하기 보강
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2ae33432a9
commit
d663d045e3
86 files changed
+16144
No files matched your search
@@ -0,0 +1,33 @@
|
||||
import json
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
DATA = Path(__file__).parent / "data"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def v1():
|
||||
return json.loads((DATA / "helpdesk-v1.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def v2():
|
||||
return json.loads((DATA / "helpdesk-v2.json").read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def work(tmp_path):
|
||||
"""A my-work folder with v1 as helpdesk.json, a note and a cases file (not workflows)."""
|
||||
folder = tmp_path / "my-work"
|
||||
folder.mkdir()
|
||||
shutil.copy(DATA / "helpdesk-v1.json", folder / "helpdesk.json")
|
||||
(folder / "PROMPTS.md").write_text("# prompts\n", encoding="utf-8")
|
||||
(folder / "cases.json").write_text('{"webhook": "x", "cases": []}', encoding="utf-8")
|
||||
return folder
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def store_dir(tmp_path):
|
||||
return tmp_path / "store"
|
||||
@@ -0,0 +1,286 @@
|
||||
{
|
||||
"name": "74 IT helpdesk v1 (provided)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Question webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "rel-v1",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "0709e90d-e8ea-509c-8c91-1721c012d260",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "d087d97a-46d1-544f-b742-78984643db81"
|
||||
},
|
||||
{
|
||||
"name": "Has question?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.body.question }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "notEmpty",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "0cafcebc-1ba4-546e-aa2e-22f24643ca5a"
|
||||
},
|
||||
{
|
||||
"name": "Reject empty",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "{ \"error\": \"question is required\" }",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
560
|
||||
],
|
||||
"id": "f60c3d44-3793-50e4-89c1-96c15c0075b0"
|
||||
},
|
||||
{
|
||||
"name": "Search FAQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/faq",
|
||||
"options": {},
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "q",
|
||||
"value": "={{ $('Question webhook').first().json.body.question }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "7bf6486f-83e4-56c6-9052-ee94305aa025"
|
||||
},
|
||||
{
|
||||
"name": "Get ticket",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "requester",
|
||||
"value": "={{ $('Question webhook').first().json.body.user ?? '' }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "9f1e31b4-c9b1-5848-8bc7-4f9f5c5b905b"
|
||||
},
|
||||
{
|
||||
"name": "Answer",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "=질문: {{ $('Question webhook').first().json.body.question }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}",
|
||||
"options": {
|
||||
"systemMessage": "You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \"찾지 못했습니다\" and that a staff member will help. Use 합니다체.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"id": "28d25947-3225-58fb-adec-112be9f61a60"
|
||||
},
|
||||
{
|
||||
"name": "Answer model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-4.1-mini"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
520
|
||||
],
|
||||
"id": "008c5ad6-673c-5e09-ae15-bbcf04777148"
|
||||
},
|
||||
{
|
||||
"name": "Build result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const results = $('Search FAQ').first().json.results ?? [];\nconst result = { route: '자동 답변', answer: String($json.output ?? '').trim(), sources: results.map((r) => r.id) };\nreturn [{ json: result }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"id": "5eabccbd-9c75-59f6-ac72-9070ef5f3a89"
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
300
|
||||
],
|
||||
"id": "95e15847-a230-567c-a1f5-827a415a52a4"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Question webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Has question?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Has question?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Search FAQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Reject empty",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Search FAQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get ticket",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get ticket": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Build result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Build result": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
{
|
||||
"name": "74 IT helpdesk v2 (provided)",
|
||||
"nodes": [
|
||||
{
|
||||
"name": "Question webhook",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "rel-v2",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"webhookId": "4dd9ca0d-6e35-58d9-afb9-36689beca2c7",
|
||||
"position": [
|
||||
0,
|
||||
300
|
||||
],
|
||||
"id": "edf3da10-d647-5671-a906-292126b204df"
|
||||
},
|
||||
{
|
||||
"name": "Has question?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "c1",
|
||||
"leftValue": "={{ $json.body.question }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "notEmpty",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
220,
|
||||
300
|
||||
],
|
||||
"id": "cf8a9038-ff3e-56a3-a17f-9c062bdd0391"
|
||||
},
|
||||
{
|
||||
"name": "Reject empty",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "{ \"error\": \"question is required\" }",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
560
|
||||
],
|
||||
"id": "3e81f84e-0040-52a4-80a2-e093464dea75"
|
||||
},
|
||||
{
|
||||
"name": "Search FAQ",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "http://data-api:8800/faq",
|
||||
"options": {},
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "q",
|
||||
"value": "={{ $('Question webhook').first().json.body.question }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
440,
|
||||
300
|
||||
],
|
||||
"id": "7e858b12-8c3b-5b72-aa02-09276f0d63e0"
|
||||
},
|
||||
{
|
||||
"name": "Get ticket",
|
||||
"type": "n8n-nodes-base.httpRequest",
|
||||
"typeVersion": 4.2,
|
||||
"parameters": {
|
||||
"url": "=http://data-api:8800/tickets/{{ (($('Question webhook').first().json.body.question.match(/T-?\\d{4}/i) || ['NONE'])[0]).toUpperCase().replace(/^T(\\d)/, 'T-$1') }}",
|
||||
"options": {
|
||||
"response": {
|
||||
"response": {
|
||||
"neverError": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"sendQuery": true,
|
||||
"queryParameters": {
|
||||
"parameters": [
|
||||
{
|
||||
"name": "requester",
|
||||
"value": "={{ $('Question webhook').first().json.body.user ?? '' }}"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
660,
|
||||
300
|
||||
],
|
||||
"id": "9265daab-0313-5c12-aa77-7a596f23eae5"
|
||||
},
|
||||
{
|
||||
"name": "Answer",
|
||||
"type": "@n8n/n8n-nodes-langchain.agent",
|
||||
"typeVersion": 3.1,
|
||||
"parameters": {
|
||||
"promptType": "define",
|
||||
"text": "=질문: {{ $('Question webhook').first().json.body.question }}\nFAQ: {{ JSON.stringify($('Search FAQ').first().json.results ?? []) }}\n접수 건: {{ JSON.stringify($('Get ticket').first().json) }}",
|
||||
"options": {
|
||||
"systemMessage": "You are the company IT help desk. Answer in Korean in at most 3 sentences, using only the FAQ entries and the ticket record given. Never invent steps, dates or names. For a ticket, say its status and note. If the data does not answer the question, say \"찾지 못했습니다\" and that a staff member will help. Use 합니다체.\nWhen the answer comes from a FAQ entry, end with its id in brackets, for example [FAQ-003]. Keep answers short.",
|
||||
"maxIterations": 1
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
300
|
||||
],
|
||||
"id": "027a9c55-9b22-5122-85e5-9f8e347fe189"
|
||||
},
|
||||
{
|
||||
"name": "Answer model",
|
||||
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
|
||||
"typeVersion": 1.2,
|
||||
"parameters": {
|
||||
"model": {
|
||||
"__rl": true,
|
||||
"mode": "id",
|
||||
"value": "gpt-5.4-nano"
|
||||
},
|
||||
"options": {
|
||||
"temperature": 0
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"openAiApi": {
|
||||
"id": "openAiCourse0001",
|
||||
"name": "OpenAI (.env)"
|
||||
}
|
||||
},
|
||||
"position": [
|
||||
880,
|
||||
520
|
||||
],
|
||||
"id": "bfe53f6e-ecec-5876-8943-03d0b6dad9a5"
|
||||
},
|
||||
{
|
||||
"name": "Review check",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "let answer = String($json.output ?? '').trim();\nif (answer.length > 400) answer = answer.slice(0, 400);\nreturn [{ json: { output: answer } }];"
|
||||
},
|
||||
"position": [
|
||||
1100,
|
||||
300
|
||||
],
|
||||
"id": "0d0c0486-e241-521a-83ca-66b725fd05d0"
|
||||
},
|
||||
{
|
||||
"name": "Build result",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 2,
|
||||
"parameters": {
|
||||
"jsCode": "const results = $('Search FAQ').first().json.results ?? [];\nconst result = { route: '자동 답변', answer: String($json.output ?? '').trim(), sources: results.map((r) => r.id) };\nreturn [{ json: result }];"
|
||||
},
|
||||
"position": [
|
||||
1320,
|
||||
300
|
||||
],
|
||||
"id": "27c64881-2145-56c1-ab88-8f0a7515dbd2"
|
||||
},
|
||||
{
|
||||
"name": "Send result",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ $json }}",
|
||||
"options": {}
|
||||
},
|
||||
"position": [
|
||||
1540,
|
||||
300
|
||||
],
|
||||
"id": "5a7d65a6-3b4e-562d-bf7e-1be3756a8cde"
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Question webhook": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Has question?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Has question?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Search FAQ",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Reject empty",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Search FAQ": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get ticket",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Get ticket": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer model": {
|
||||
"ai_languageModel": [
|
||||
[
|
||||
{
|
||||
"node": "Answer",
|
||||
"type": "ai_languageModel",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Answer": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Review check",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Review check": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Build result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Build result": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Send result",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
"""R8: versions list, change tree, detail of the chosen change, [s] saves."""
|
||||
import asyncio
|
||||
import json
|
||||
|
||||
from textual.widgets import ListView, Tree
|
||||
|
||||
from tver.app import VersionApp
|
||||
from tver.store import Store
|
||||
|
||||
|
||||
async def wait_for(pilot, check):
|
||||
for _ in range(60):
|
||||
await asyncio.sleep(0.05)
|
||||
await pilot.pause()
|
||||
if check():
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
async def test_versions_tree_and_save(work, store_dir, v2):
|
||||
store = Store(store_dir)
|
||||
store.save(work / "helpdesk.json")
|
||||
(work / "helpdesk.json").write_text(json.dumps(v2), encoding="utf-8")
|
||||
app = VersionApp(str(work / "helpdesk.json"), store)
|
||||
async with app.run_test() as pilot:
|
||||
assert len(app.query_one(ListView).children) == 1
|
||||
await pilot.press("s")
|
||||
assert await wait_for(pilot, lambda: len(app.query_one(ListView).children) == 2)
|
||||
assert await wait_for(pilot, lambda: "model 1 · prompt 1 · flow 5" in str(app.query_one(Tree).root.label))
|
||||
tree = app.query_one(Tree)
|
||||
tree.focus()
|
||||
await pilot.press("down", "down")
|
||||
await pilot.pause()
|
||||
assert str(app.query_one("#detail").render()).strip() != ""
|
||||
@@ -0,0 +1,60 @@
|
||||
"""R1 · R2 · R3 · R4 · R11 · R12 with the day 74 v1 → v2 files."""
|
||||
import copy
|
||||
import json
|
||||
|
||||
from tver import diff
|
||||
from tver.cli import main
|
||||
from tver.workflow import clean, dumps
|
||||
|
||||
|
||||
def test_v1_to_v2_is_seven_changes_like_flow_py(v1, v2):
|
||||
changes = diff.compare(v1, v2)
|
||||
titles = [c.title() for c in changes]
|
||||
assert "+ node 'Review check' (code)" in titles
|
||||
assert any(t.startswith("~ 'Answer' parameters.options.systemMessage: changed") for t in titles)
|
||||
assert "~ 'Answer model' parameters.model.value: gpt-4.1-mini → gpt-5.4-nano" in titles
|
||||
assert "~ 'Question webhook' parameters.path: rel-v1 → rel-v2" in titles
|
||||
s = diff.summary(changes)
|
||||
assert (s["total"], s["model"], s["prompt"], s["flow"]) == (7, 1, 1, 5)
|
||||
assert s["links"] == {"+": 2, "-": 1}
|
||||
|
||||
|
||||
def test_noise_is_not_a_change(v1):
|
||||
moved = copy.deepcopy(v1)
|
||||
for node in moved["nodes"]:
|
||||
node["position"] = [node["position"][0] + 100, 0]
|
||||
node["id"] = "other-" + node["name"]
|
||||
moved["nodes"].reverse()
|
||||
assert diff.compare(v1, moved) == []
|
||||
assert dumps(v1) == dumps(moved)
|
||||
assert all("position" not in n and "id" not in n for n in clean(v1)["nodes"])
|
||||
|
||||
|
||||
def test_long_prompt_gets_line_diff(v1, v2):
|
||||
change = next(c for c in diff.compare(v1, v2) if c.key.endswith("systemMessage"))
|
||||
assert change.category == "prompt"
|
||||
assert any(row.startswith("+") and "[FAQ-003]" in row for row in change.lines)
|
||||
|
||||
|
||||
def test_renamed_node_is_paired(v1):
|
||||
renamed = copy.deepcopy(v1)
|
||||
for node in renamed["nodes"]:
|
||||
if node["name"] == "Search FAQ":
|
||||
node["name"] = "Find FAQ"
|
||||
conn = json.dumps(renamed["connections"]).replace('"Search FAQ"', '"Find FAQ"')
|
||||
renamed["connections"] = json.loads(conn)
|
||||
changes = diff.compare(v1, renamed)
|
||||
assert [c.op for c in changes] == ["renamed"]
|
||||
assert (changes[0].before, changes[0].after) == ("Search FAQ", "Find FAQ")
|
||||
|
||||
|
||||
def test_cli_diff_exit_codes_and_json(tmp_path, v1, v2, capsys):
|
||||
a, b = tmp_path / "a.json", tmp_path / "b.json"
|
||||
a.write_text(json.dumps(v1), encoding="utf-8")
|
||||
b.write_text(json.dumps(v2), encoding="utf-8")
|
||||
assert main(["diff", str(a), str(a)]) == 0
|
||||
capsys.readouterr()
|
||||
assert main(["diff", str(a), str(b), "--json"]) == 1
|
||||
out = json.loads(capsys.readouterr().out)
|
||||
assert out["summary"]["total"] == 7
|
||||
assert main(["diff", str(a), str(tmp_path / "missing.json")]) == 2
|
||||
@@ -0,0 +1,57 @@
|
||||
"""R5 save · R6 log/diff of versions · R7 no secrets · R10 restore · R13 folder."""
|
||||
import json
|
||||
|
||||
import pytest
|
||||
|
||||
from tver.cli import main
|
||||
from tver.store import Store, StoreError
|
||||
|
||||
FAKE_KEY = "sk-" + "test-" + "a" * 24 # made here, never stored in a file of this repository
|
||||
|
||||
|
||||
def test_save_twice_makes_one_commit(work, store_dir):
|
||||
store = Store(store_dir)
|
||||
assert store.save(work / "helpdesk.json")[0][1] == "saved"
|
||||
assert store.save(work / "helpdesk.json")[0][1] == "no change"
|
||||
assert len(store.log("helpdesk.json")) == 1
|
||||
|
||||
|
||||
def test_versions_and_diff_between_them(work, store_dir, v2, capsys):
|
||||
store = Store(store_dir)
|
||||
store.save(work / "helpdesk.json")
|
||||
(work / "helpdesk.json").write_text(json.dumps(v2), encoding="utf-8")
|
||||
name, status, message = store.save(work / "helpdesk.json")[0]
|
||||
assert status == "saved" and "model 1 · prompt 1 · flow 5" in message
|
||||
versions = store.log("helpdesk.json")
|
||||
assert [v.message.split(": ")[0] for v in versions] == ["helpdesk.json", "helpdesk.json"]
|
||||
assert main(["--repo", str(store_dir), "diff", versions[1].rev, versions[0].rev, "helpdesk.json", "--short"]) == 1
|
||||
assert "7 change(s) · model 1 · prompt 1 · flow 5" in capsys.readouterr().out
|
||||
|
||||
|
||||
def test_secret_is_not_saved(work, store_dir):
|
||||
data = json.loads((work / "helpdesk.json").read_text(encoding="utf-8"))
|
||||
data["nodes"][0]["parameters"]["note"] = FAKE_KEY
|
||||
(work / "helpdesk.json").write_text(json.dumps(data), encoding="utf-8")
|
||||
with pytest.raises(StoreError) as error:
|
||||
Store(store_dir).save(work / "helpdesk.json")
|
||||
assert FAKE_KEY not in str(error.value)
|
||||
assert not (store_dir / "helpdesk.json").exists()
|
||||
|
||||
|
||||
def test_restore_writes_a_file_and_keeps_the_store(work, store_dir, v2, tmp_path):
|
||||
store = Store(store_dir)
|
||||
store.save(work / "helpdesk.json")
|
||||
(work / "helpdesk.json").write_text(json.dumps(v2), encoding="utf-8")
|
||||
store.save(work / "helpdesk.json")
|
||||
oldest = store.log("helpdesk.json")[-1].rev
|
||||
out = store.restore(oldest, "helpdesk.json", tmp_path / "back.json")
|
||||
restored = json.loads(out.read_text(encoding="utf-8"))
|
||||
model = next(n for n in restored["nodes"] if n["name"] == "Answer model")
|
||||
assert model["parameters"]["model"]["value"] == "gpt-4.1-mini"
|
||||
assert len(store.log("helpdesk.json")) == 2
|
||||
|
||||
|
||||
def test_folder_save_skips_files_that_are_not_workflows(work, store_dir):
|
||||
rows = Store(store_dir).save(work)
|
||||
assert ("cases.json", "skipped", "not a workflow") in rows
|
||||
assert [r[0] for r in rows if r[1] == "saved"] == ["helpdesk.json"]
|
||||
Reference in new issue
Block a user