This commit is contained in:
2026-07-12 14:54:50 +08:00
commit a769e4ae58
99 changed files with 9278 additions and 0 deletions
View File
Binary file not shown.
+10
View File
@@ -0,0 +1,10 @@
import os
import sys
import pytest
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
@pytest.fixture
def data_dir():
return os.path.join(os.path.dirname(__file__), 'test_data')
+68
View File
@@ -0,0 +1,68 @@
import json
from unittest.mock import patch, MagicMock
from agent.api_client import APIClient
def test_parse_json_valid():
text = '{"groups": {"g1": {"records": []}}}'
result = APIClient._parse_json(text)
assert result is not None
assert 'groups' in result
def test_parse_json_with_markdown_wrapper():
text = '```json\n{"key": "value"}\n```'
result = APIClient._parse_json(text)
assert result is not None
assert result['key'] == 'value'
def test_parse_json_with_text_before():
text = '说明\n{"key": "value"}\n以上'
result = APIClient._parse_json(text)
assert result is not None
assert result['key'] == 'value'
def test_parse_json_invalid():
text = '这不是有效的JSON。'
result = APIClient._parse_json(text)
assert result is None
@patch('agent.api_client.requests.post')
def test_generate_success(mock_post):
mock_response = MagicMock()
mock_response.json.return_value = {
'choices': [{'message': {'content': '{"groups": {"g1": {"type": "json"}}}'}}]
}
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
client = APIClient(api_key='test-key')
result = client.generate("test prompt")
assert result['groups']['g1']['type'] == 'json'
@patch('agent.api_client.requests.post')
def test_generate_retry_on_json_error(mock_post):
bad_response = MagicMock()
bad_response.json.return_value = {
'choices': [{'message': {'content': 'invalid response'}}]
}
bad_response.raise_for_status = MagicMock()
good_response = MagicMock()
good_response.json.return_value = {
'choices': [{'message': {'content': '{"result": "ok"}'}}]
}
good_response.raise_for_status = MagicMock()
mock_post.side_effect = [bad_response, good_response]
client = APIClient(api_key='test-key', max_retries=3)
result = client.generate("test")
assert result['result'] == 'ok'
assert mock_post.call_count == 2
+87
View File
@@ -0,0 +1,87 @@
# tests/test_copy_parser.py
import os
import tempfile
from agent.input_parser import InputParser
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
def test_parse_single_copybook():
parser = InputParser(
design_md_path='dummy.md', source_cbl_path='dummy.cbl',
file_db_md_path='dummy.md', cpy_dir=FIXTURE_DIR,
db_md_path='dummy_db.md'
)
fields = parser._parse_single_copybook(
os.path.join(FIXTURE_DIR, 'ZAN01REC.cpy'),
prefix='R01'
)
assert len(fields) == 8
f0 = fields[0]
assert f0.name == 'R01-APPL-ID'
assert f0.raw_name == '(A)APPL-ID'
assert 'X' in f0.pic_type
assert f0.pic_bytes == 8
f1 = fields[1]
assert f1.name == 'R01-EMP-ID'
assert '9' in f1.pic_type
assert f1.pic_bytes == 8
f3 = fields[3]
assert f3.name == 'R01-START-TIME'
assert f3.pic_bytes == 4
f7 = fields[7]
assert 'FILLER' in f7.name
assert f7.pic_bytes == 46
def test_extract_copy_replacing():
source_text = """
FD R01INNFIL.
01 R01INNREC.
COPY ZAN01REC REPLACING ==(A)== BY ==R01==.
FD R02INNFIL.
01 R02INNREC.
COPY ZAN04REC REPLACING ==(A)== BY ==R02==.
"""
parser = InputParser(
design_md_path='dummy.md', source_cbl_path='dummy.cbl',
file_db_md_path='dummy.md', cpy_dir=FIXTURE_DIR,
db_md_path='dummy_db.md'
)
parser._source_text = source_text
result = parser._extract_copy_replacing()
assert len(result) == 2
assert result[0] == ('ZAN01REC', 'R01')
assert result[1] == ('ZAN04REC', 'R02')
def test_calculate_pic_bytes_x():
assert InputParser._calculate_pic_bytes('X(008)') == 8
assert InputParser._calculate_pic_bytes('X(10)') == 10
def test_calculate_pic_bytes_9():
assert InputParser._calculate_pic_bytes('9(008)') == 8
assert InputParser._calculate_pic_bytes('9(004)') == 4
def test_calculate_pic_bytes_comp3():
assert InputParser._calculate_pic_bytes('S9(009) COMP-3') == 5
assert InputParser._calculate_pic_bytes('S9(7) COMP-3') == 4
assert InputParser._calculate_pic_bytes('9(7) COMP-3') == 4
def test_calculate_pic_bytes_comp():
assert InputParser._calculate_pic_bytes('S9(4) COMP') == 2
assert InputParser._calculate_pic_bytes('S9(9) COMP') == 4
def test_calculate_pic_bytes_decimal():
assert InputParser._calculate_pic_bytes('S9(7)V9(2)') == 9
+347
View File
@@ -0,0 +1,347 @@
# DB定義書
## 変更履歴
| No | 変更内容 | 担当者 | 変更日 | 承認者 | 備考 |
|----|---------|--------|--------|--------|------|
| 1 | 新規作成(全8テーブル) | AI | 2026/06/23 | | サブシステムA(6) + B(2) |
| 2 | SALARYDB 4テーブル追加 | AI | 2026/07/08 | | サブシステムC(給与計算) |
---
# EMP_MASTER — 社員マスタ
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | EMP_MASTER | — | PK: EMP_ID |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 部署ID | DEPT_ID | CHAR | 4 | | 4 | NOT NULL | | | | | | |
| 3 | 氏名 | EMP_NAME | VARCHAR | 50 | | 50 | NOT NULL | | | | | | |
| 4 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '1'=在籍, '9'=退職 |
### 備考
- サブシステムA: 参照のみ(SELECT)
- サブシステムB: 使用しない
---
# LEAVE_RECORDS — 休暇申請記録
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | LEAVE_RECORDS | — | PK: APPLICATION_ID |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 申請番号 | APPLICATION_ID | INTEGER | 10 | | 4 | NOT NULL | | ✓ | | | | 自動採番 |
| 2 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | | | | | |
| 3 | 休暇種別 | LEAVE_TYPE | CHAR | 2 | | 2 | NOT NULL | | | | | | 01/02/03/04 |
| 4 | 開始日 | START_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD |
| 5 | 開始時刻 | START_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM |
| 6 | 終了日 | END_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD |
| 7 | 終了時刻 | END_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM |
| 8 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '1'=有効, '9'=取消 |
### 備考
- KIN02UPDがINSERT/DELETEを実行
- KIN03EXPがSELECT(日付展開用)
- 取消はDELETE FROM で物理削除
---
# HOLIDAY_CALENDAR — 休日カレンダー
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | HOLIDAY_CALENDAR | — | PK: HOLIDAY_DATE |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 休日日付 | HOLIDAY_DATE | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD |
| 2 | 説明 | DESCRIPTION | VARCHAR | 50 | | 50 | NULL許可 | NULL | | | | | 例:「建国記念の日」 |
### 備考
- 土日は曜日判定で処理するため、このテーブルには祝日のみ格納
- KIN03EXP, KIN06CLDがSELECTで参照
---
# SICK_LEAVE_RATE — 病欠控除率
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | SICK_LEAVE_RATE | — | PK: LEAVE_TYPE |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 休暇種別 | LEAVE_TYPE | CHAR | 2 | | 2 | NOT NULL | | ✓ | | | | '04'固定 |
| 2 | 控除率 | DEDUCTION_RATE | DECIMAL | 3 | 2 | 3 | NOT NULL | | | | | | 例:0.5050%控除) |
### 備考
- 全社員共通の設定値
- 現時点では参照プログラム未実装(設計上定義)
---
# DAILY_RECORDS — 日別勤怠記録
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | DAILY_RECORDS | — | PK: (EMP_ID, TARGET_DATE) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 対象日 | TARGET_DATE | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD |
| 3 | 出勤時刻 | TIME_IN | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM, '0000'=打刻なし |
| 4 | 退勤時刻 | TIME_OUT | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM, '0000'=打刻なし |
| 5 | 年休時間 | ANNUAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 |
| 6 | 事假時間 | PERSONAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 |
| 7 | 因公特批假時間 | OFFICIAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 |
| 8 | 病欠時間 | SICK_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 |
| 9 | 未申請欠勤時間 | UNAPPROVED_ABSENT_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | 0.1h単位 |
| 10 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | DEFAULT CURRENT_TIMESTAMP |
### 備考
- KIN08DBUがKIN07DAIの出力ファイルからINSERT
- 1社員1日=1レコード。出勤日のみ存在。
---
# MONTHLY_ABSENCE — 月次統計
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 勤怠休暇管理DB | MONTHLY_ABSENCE | — | PK: (EMP_ID, YEAR_MONTH) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 対象年月 | YEAR_MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM |
| 3 | 年休合計 | ANNUAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 4 | 事假合計 | PERSONAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 5 | 因公特批假合計 | OFFICIAL_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 6 | 病欠合計 | SICK_LEAVE_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 7 | 未申請欠勤合計 | UNAPPROVED_ABSENT_H | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 8 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | DEFAULT CURRENT_TIMESTAMP |
### 備考
- KIN08DBUが月次集計後にUPSERTMERGE
- サブシステムC(給与計算)への連携元
---
# OVT_APPLICATIONS — 個別加班申請テーブル
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 残業統計管理DB | OVT_APPLICATIONS | — | PK: APPL_ID |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 申請番号 | APPL_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | YYYYMMDD+SEQ |
| 2 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | | | | | |
| 3 | 申請日 | APPL_DATE | CHAR | 8 | | 8 | NOT NULL | | | | | | YYYYMMDD |
| 4 | 残業種別 | OVT_TYPE | CHAR | 1 | | 1 | NOT NULL | | | | | | W=平日, H=休日 |
| 5 | 開始時刻 | START_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM |
| 6 | 終了時刻 | END_TIME | CHAR | 4 | | 4 | NOT NULL | | | | | | HHMM |
| 7 | 加班時間 | OVT_HOURS | DECIMAL | 4 | 1 | 4 | NOT NULL | | | | | | 0.1h単位 |
| 8 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | 0=有効, 9=取消 |
| 9 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | |
### 備考
- ZAN06UPDがINSERT/UPDATEを実行
- STATUS=9で取消(物理削除はしない)
---
# OVT_MONTHLY — 月次集計テーブル
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 残業統計管理DB | OVT_MONTHLY | — | PK: (EMP_ID, YEAR_MONTH, OVT_TYPE) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP_ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 対象年月 | YEAR_MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM |
| 3 | 残業種別 | OVT_TYPE | CHAR | 1 | | 1 | NOT NULL | | ✓ | | | | W=平日, H=休日 |
| 4 | 加班時間合計 | OVT_HOURS | DECIMAL | 6 | 1 | 5 | NOT NULL | | | | | | |
| 5 | 加班回数 | OVT_COUNT | INTEGER | 10 | | 4 | NOT NULL | | | | | | |
| 6 | 更新日時 | UPDATED_AT | TIMESTAMP | — | | 10 | NOT NULL | | | | | | |
### 備考
- ZAN06UPDが月次集計結果をUPSERT
---
# EMP-MASTERSALARYDB — 社員マスタ(給与計算用)
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 給与計算DB | EMP-MASTER | KYU01REC | PK: EMP-ID |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP-ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 氏名 | EMP-NAME | VARCHAR | 40 | | 40 | NOT NULL | | | | | | |
| 3 | 部署コード | DEPT-CODE | CHAR | 2 | | 2 | NOT NULL | | | | | | |
| 4 | 地域コード | REGION-CODE | CHAR | 2 | | 2 | NOT NULL | | | | | | |
| 5 | 職種コード | CATEGORY-CODE | CHAR | 3 | | 3 | NOT NULL | | | | | | |
| 6 | 基本給 | BASE-SALARY | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 7 | 時給 | HOURLY-RATE | DECIMAL | 7 | 0 | 4 | NOT NULL | | | | | | |
| 8 | 扶養人数 | DEPENDENT-COUNT | SMALLINT | — | | 2 | NOT NULL | | | | | | |
| 9 | ステータス | STATUS | CHAR | 1 | | 1 | NOT NULL | | | | | | '0'=在籍, '9'=退職 |
| 10 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | |
### 備考
- KYU02REGがINSERT/UPSERTを実行
- KYU04CALがSELECTで参照
---
# TAX-TABLESALARYDB — 所得税率テーブル
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 給与計算DB | TAX-TABLE | — | PK: (TAX-FROM, TAX-TO) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 課税下限 | TAX-FROM | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | |
| 2 | 課税上限 | TAX-TO | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | |
| 3 | 税率 | TAX-RATE | DECIMAL | 5 | 4 | 3 | NOT NULL | | | | | | |
| 4 | 控除額 | DEDUCTION | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
### 備考
- KYU05DEDがSELECT ... BETWEENで参照
- 課税所得に対する超過累進税率
---
# INSURANCE-TABLESALARYDB — 社会保険料率テーブル
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 給与計算DB | INSURANCE-TABLE | — | PK: (INS-INCOME-FROM, INS-INCOME-TO) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 所得下限 | INS-INCOME-FROM | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | |
| 2 | 所得上限 | INS-INCOME-TO | DECIMAL | 9 | 0 | 5 | NOT NULL | | ✓ | | | | |
| 3 | 健康保険料率 | INS-HEALTH-RATE | DECIMAL | 7 | 6 | 4 | NOT NULL | | | | | | |
| 4 | 厚生年金保険料率 | INS-PENSION-RATE | DECIMAL | 7 | 6 | 4 | NOT NULL | | | | | | |
| 5 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | |
### 備考
- KYU05DEDが内部テーブル(SEARCH ALL)に取込んで参照
- 被保険者負担分のみ
---
# SALARY-RESULTSSALARYDB — 給与計算結果テーブル
## Layout(テーブル定義)
### DB基本情報
| DB名 | DB ID | COPY ID | INDEX |
|------|-------|---------|-------|
| 給与計算DB | SALARY-RESULTS | — | PK: (EMP-ID, YEAR-MONTH) |
### カラム定義
| No | 項目名 | 項目名(英字名) | TYPE | 最大長 | 小数桁 | バイト数 | NULL | DEFAULT | PK | INDEX1 | INDEX2 | INDEX3 | 備考 |
|----|--------|---------------|------|--------|--------|---------|------|---------|----|--------|--------|--------|------|
| 1 | 社員番号 | EMP-ID | CHAR | 8 | | 8 | NOT NULL | | ✓ | | | | |
| 2 | 対象年月 | YEAR-MONTH | CHAR | 6 | | 6 | NOT NULL | | ✓ | | | | YYYYMM |
| 3 | 総支給額 | GROSS-PAYMENT | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 4 | 源泉所得税 | INCOME-TAX | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 5 | 社会保険料 | INSURANCE | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 6 | 住民税 | RESIDENT-TAX | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 7 | 差引支給額 | NET-PAYMENT | DECIMAL | 9 | 0 | 5 | NOT NULL | | | | | | |
| 8 | 更新日時 | UPDATED-AT | TIMESTAMP | — | | 10 | NOT NULL | CURRENT TIMESTAMP | | | | | |
### 備考
- KYU06UPDがINSERT/UPSERTを実行
+11
View File
@@ -0,0 +1,11 @@
*
* 加班申請レコード 80B
*
03 (A)APPL-ID PIC X(008).
03 (A)EMP-ID PIC 9(008).
03 (A)APPL-DATE PIC 9(008).
03 (A)START-TIME PIC 9(004).
03 (A)END-TIME PIC 9(004).
03 (A)STATUS PIC X(001).
03 (A)OVT-TYPE PIC X(001).
03 (A)FILLER PIC X(046).
+12
View File
@@ -0,0 +1,12 @@
*
* マッチング結果レコード 80B
*
03 (A)APPL-ID PIC X(008).
03 (A)EMP-ID PIC 9(008).
03 (A)APPL-DATE PIC 9(008).
03 (A)START-TIME PIC 9(004).
03 (A)END-TIME PIC 9(004).
03 (A)STATUS PIC X(001).
03 (A)OVT-TYPE PIC X(001).
03 (A)PROC-SEQ PIC 9(002).
03 (A)FILLER PIC X(044).
+5
View File
@@ -0,0 +1,5 @@
*
* DBCLEAN対象レコード 80B
*
03 (A)APPL-ID PIC X(008).
03 (A)FILLER PIC X(072).
+144
View File
@@ -0,0 +1,144 @@
# 詳細設計書
## 基本情報
| # | 項目 | 内容 |
|---|------|------|
| 1 | システム名 | 残業統計管理システム |
| 2 | プログラムID | ZAN04MAT |
| 3 | プログラム名 | 取消マッチング処理 |
| 4 | PGMタイプ | メイン |
| 5 | PGMパターン | マッチング(1:1) |
| 6 | 機能概要 | OVT-SORTED(有効申請)とOVT-CSORT(取消申請)を申請番号(APPL-ID)で1:1マッチングし、結果を振り分ける。 |
| 7 | | 申請番号一致(取消済):監査証跡としてERROR-LOGに記録 |
| 8 | | 申請のみ(取消なし):OVT-MATCHEDに出力(処理番号=1 |
| 9 | | 取消のみ(既DB登録済):OVT-DBCLEANに出力(DB削除用) |
※PGMタイプ:メイン、サブ
※PGMパターン:マッチング(1:1、1:N、N:1、M:N)、レイアウト編集のみ(GETPUT)、振り分け(IF文、EVALUATE文)、キーブレイク(集計、集約、集計・集約の以外)、DB更新
### 前提条件
| NO | 対象ファイル | 条件 |
|----|-------------|------|
| 1 | ファイルR01OVT-SORTED | 申請番号(APPL-ID)で昇順ソート済、重複なし |
| 2 | ファイルR02OVT-CSORT | 申請番号(APPL-ID)で昇順ソート済、重複なし |
### 使用ファイル一覧
| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 形式 | ブロック | レコード長 | 媒体 | 備考 |
|----|------------------|--------|------|-----|--------|------|---------|-----------|------|------|
| 1 | OVT-SORTED | R01 | ZAN04R01 | I | ZAN01REC | FB | | 80 | PS | 有効申請(APPL-ID昇順) |
| 2 | OVT-CSORT | R02 | ZAN04R02 | I | ZAN01REC | FB | | 80 | PS | 取消申請(APPL-ID昇順) |
| 3 | OVT-MATCHED | W01 | ZAN04W01 | O | ZAN02REC | FB | | 80 | PS | マッチング結果(処理番号付) |
| 4 | OVT-DBCLEAN | W02 | ZAN04W02 | O | ZAN04REC | FB | | 80 | PS | DB削除用(APPL-IDのみ) |
| 5 | ERROR-LOG | W03 | ZAN04W03 | O | ZAN05REC | VB | | 200 | PS | 監査証跡(取消マッチ記録) |
### キー項目一覧
| NO | ファイル名 | ソート条件(キー項目) | キー条件(マッチング/キーブレイク) |
|----|-----------|---------------------|-------------------------------------------|
| 1 | ファイルR01 | APPL-ID(重複NG | APPL-ID |
| 2 | ファイルR02 | APPL-ID(重複NG | APPL-ID |
### 使用モジュール一覧
| NO | 機能 | プログラムID | 使用COPY名 |
|----|------|-------------|-----------|
| 1 | 運用日付取得SUB | SUB01DAT | ZANDATAC |
| 2 | メッセージ編集出力SUB | SUB02MSG | ZANMSGAC |
| 3 | ABEND処理SUB | SUB03END | ZANENDAC |
---
## 処理詳細
```
1.初期処理(1000ITTSOR
1-1.開始メッセージ出力
【メッセージ編集】
メッセージ番号:1(開始メッセージ)
1-2.コンパイル日時出力
【メッセージ編集】
メッセージ番号:33(コンパイル日時)
PARM1:コンパイル日時
PARM2'COMPILED'
1-3.ワークエリアの初期化
1-4.運用日付取得SUB(SUB01DAT)により運用日を取得する。
復帰コード≠ZEROの場合、メッセージを出力し、ABEND処理SUBを呼び出し異常終了する。
【メッセージ編集】
メッセージ番号:5(サブエラー)
PARM1'SUB01DAT'
PARM2:復帰コード
【ABEND処理SUB】
ABENDコード:999
1-5.使用ファイルのオープン
1-6.R01を読み込む。(1100R01INNSOR)(1回目)
1-7.R02を読み込む。(1200R02INNSOR)(1回目)
2.主処理(2000MAJSOR)(R01、R02を全て読み終えるまで下記を繰り返す)
2-1.マッチの場合(R01.APPL-ID = R02.APPL-ID
取消済みの申請のため、OVT-MATCHEDには出力しない。
監査証跡としてERROR-LOGにマッチ情報を出力する。(2100MATCHSOR)
【STRING編集】
ERR-CATEGORY = 04
ERR-DETAIL = 'CANCEL-MATCH: ' + 申請番号 + 社員番号 + 申請日 + 開始時刻 + 終了時刻
R01を読み込む。(1100R01INNSOR)(2件目以降)
R02を読み込む。(1200R02INNSOR)(2件目以降)
2-2.R01のみの場合(R01.APPL-ID < R02.APPL-ID
R01をOVT-MATCHEDにSTRING編集して出力する。(2200R01OUTSOR)
【STRING編集】
W01 = 申請番号 + 社員番号 + 申請日 + 開始時刻 + 終了時刻 + ステータス + OVT-TYPE + 処理番号(01)
R01を読み込む。(1100R01INNSOR)(2件目以降)
2-3.R02のみの場合(R01.APPL-ID > R02.APPL-ID
R02をOVT-DBCLEANに出力する。(2300R02OUTSOR)
W02.APPL-ID = R02.APPL-ID
R02を読み込む。(1200R02INNSOR)(2件目以降)
3.終了処理(3000STPSOR
3-1.入出力ファイルのクローズ
3-2.入出力件数出力メッセージ出力
【入力メッセージ編集】
メッセージ番号:6(入力件数メッセージ)
PARM1:当該入力ファイルのDD名
PARM2:当該入力ファイルの件数
【出力メッセージ編集】
メッセージ番号:7(出力件数メッセージ)
PARM1:当該出力ファイルのDD名
PARM2:当該出力ファイルの件数
3-3.終了メッセージ出力
【メッセージ編集】
メッセージ番号:2(終了メッセージ)
```
---
## 出力レコード定義
### 出力ファイル1W01/OVT-MATCHED
| No | 項目名 | 設定元 | 備考 |
|----|--------|--------|------|
| 1 | APPL-ID | R01.APPL-ID | |
| 2 | EMP-ID | R01.EMP-ID | |
| 3 | APPL-DATE | R01.APPL-DATE | |
| 4 | START-TIME | R01.START-TIME | |
| 5 | END-TIME | R01.END-TIME | |
| 6 | STATUS | R01.STATUS | |
| 7 | OVT-TYPE | R01.OVT-TYPE | |
| 8 | PROC-SEQ | 01固定 | 同一申請番号グループ内連番 |
| 9 | FILLER | 初期値 | |
### 出力ファイル2W02/OVT-DBCLEAN
| No | 項目名 | 設定元 | 備考 |
|----|--------|--------|------|
| 1 | APPL-ID | R02.APPL-ID | DB削除対象申請番号 |
| 2 | FILLER | 初期値 | |
### 出力ファイル3W03/ERROR-LOG
| No | 項目名 | 設定元 | 備考 |
|----|--------|--------|------|
| 1 | ERR-CATEGORY | 04で固定 | 取消マッチ監査証跡 |
| 2 | ERR-DETAIL | STRINGで編集 | CANCEL-MATCH: + 申請番号 + 社員番号 + 申請日 + 開始時刻 + 終了時刻 |
+53
View File
@@ -0,0 +1,53 @@
# tests/test_db_parser.py
import os
from agent.input_parser import InputParser
from agent.models import ProgramMeta
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
def test_parse_db_definition():
parser = InputParser(
design_md_path='dummy.md', source_cbl_path='dummy.cbl',
file_db_md_path='dummy.md', cpy_dir='dummy_cpy',
db_md_path=os.path.join(FIXTURE_DIR, 'DB定義書.md')
)
meta = ProgramMeta(
program_id='TEST', program_name='テスト', system_name='',
pgm_type='メイン', pgm_pattern='DB更新',
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='mixed',
copy_fields={}, db_tables={}
)
parser._parse_db_definition(meta)
assert len(meta.db_tables) > 0
assert 'EMP_MASTER' in meta.db_tables
emp = meta.db_tables['EMP_MASTER']
assert emp.db_id == 'EMP_MASTER'
assert len(emp.columns) >= 3
assert emp.pk_columns == ['EMP_ID']
def test_parse_db_skipped_for_file_type():
parser = InputParser(
design_md_path='dummy.md', source_cbl_path='dummy.cbl',
file_db_md_path='dummy.md', cpy_dir='dummy_cpy',
db_md_path=os.path.join(FIXTURE_DIR, 'DB定義書.md')
)
meta = ProgramMeta(
program_id='TEST', program_name='テスト', system_name='',
pgm_type='メイン', pgm_pattern='マッチング(1:1)',
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='file',
copy_fields={}, db_tables={}
)
parser._parse_db_definition(meta)
assert len(meta.db_tables) == 0
+149
View File
@@ -0,0 +1,149 @@
# tests/test_input_parser.py
import os
from agent.input_parser import InputParser
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
def test_parse_design_basic_info():
parser = InputParser(
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
source_cbl_path='dummy.cbl',
file_db_md_path='dummy.md',
cpy_dir='dummy_cpy',
db_md_path='dummy_db.md'
)
parser._design_text = parser._read_file(parser.design_md_path)
from agent.models import ProgramMeta
meta = ProgramMeta(program_id='', program_name='', system_name='',
pgm_type='', pgm_pattern='',
summary_lines=[], prerequisites=[],
files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='file', copy_fields={}, db_tables={})
parser._parse_basic_info(meta)
assert meta.program_id == 'ZAN04MAT'
assert meta.pgm_pattern == 'マッチング(1:1)'
assert meta.pgm_type == 'メイン'
assert '残業統計管理システム' in meta.system_name
def test_parse_use_files():
parser = InputParser(
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
source_cbl_path='dummy.cbl', file_db_md_path='dummy.md',
cpy_dir='dummy_cpy', db_md_path='dummy_db.md'
)
parser._design_text = parser._read_file(parser.design_md_path)
from agent.models import ProgramMeta
meta = ProgramMeta(program_id='', program_name='', system_name='',
pgm_type='', pgm_pattern='',
summary_lines=[], prerequisites=[],
files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='file', copy_fields={}, db_tables={})
parser._parse_use_files(meta)
assert len(meta.files) == 5
r01 = [f for f in meta.files if f.identifier == 'R01'][0]
assert r01.file_db_name == 'OVT-SORTED'
assert r01.copy_group == 'ZAN01REC'
assert r01.medium == 'PS'
assert r01.io == 'I'
def test_determine_input_type_file():
parser = InputParser(
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
source_cbl_path='dummy.cbl', file_db_md_path='dummy.md',
cpy_dir='dummy_cpy', db_md_path='dummy_db.md'
)
from agent.models import ProgramMeta, FileInfo
meta = ProgramMeta(program_id='ZAN04MAT', program_name='', system_name='',
pgm_type='', pgm_pattern='',
summary_lines=[], prerequisites=[],
files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='file', copy_fields={}, db_tables={})
meta.files = [
FileInfo(no=1, file_db_name='F1', identifier='R01', dd_name='DD1',
io='I', copy_group='C1', format='FB', record_len=80,
medium='PS', remarks=''),
FileInfo(no=2, file_db_name='F2', identifier='W01', dd_name='DD2',
io='O', copy_group='C2', format='FB', record_len=80,
medium='PS', remarks=''),
]
parser._determine_input_type(meta)
assert meta.input_type == 'file'
def test_determine_input_type_mixed():
parser = InputParser(
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
source_cbl_path='dummy.cbl', file_db_md_path='dummy.md',
cpy_dir='dummy_cpy', db_md_path='dummy_db.md'
)
from agent.models import ProgramMeta, FileInfo
meta = ProgramMeta(program_id='TEST', program_name='', system_name='',
pgm_type='', pgm_pattern='',
summary_lines=[], prerequisites=[],
files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='file', copy_fields={}, db_tables={})
meta.files = [
FileInfo(no=1, file_db_name='F1', identifier='R01', dd_name='DD1',
io='I', copy_group='C1', format='FB', record_len=80,
medium='PS', remarks=''),
FileInfo(no=2, file_db_name='DB1', identifier='DB', dd_name='',
io='I', copy_group='', format='', record_len=0,
medium='DB', remarks=''),
]
parser._determine_input_type(meta)
assert meta.input_type == 'mixed'
def test_parse_process_detail():
parser = InputParser(
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
source_cbl_path='dummy.cbl', file_db_md_path='dummy.md',
cpy_dir='dummy_cpy', db_md_path='dummy_db.md'
)
parser._design_text = parser._read_file(parser.design_md_path)
from agent.models import ProgramMeta
meta = ProgramMeta(program_id='', program_name='', system_name='',
pgm_type='', pgm_pattern='',
summary_lines=[], prerequisites=[],
files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='file', copy_fields={}, db_tables={})
parser._parse_process_detail(meta)
assert '1000ITTSOR' in meta.process_detail
assert 'マッチの場合' in meta.process_detail
def test_parse_output_records():
parser = InputParser(
design_md_path=os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md'),
source_cbl_path='dummy.cbl', file_db_md_path='dummy.md',
cpy_dir='dummy_cpy', db_md_path='dummy_db.md'
)
parser._design_text = parser._read_file(parser.design_md_path)
from agent.models import ProgramMeta
meta = ProgramMeta(program_id='', program_name='', system_name='',
pgm_type='', pgm_pattern='',
summary_lines=[], prerequisites=[],
files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='file', copy_fields={}, db_tables={})
parser._parse_output_records(meta)
assert 'OVT-MATCHED' in meta.output_records
assert 'OVT-DBCLEAN' in meta.output_records
+77
View File
@@ -0,0 +1,77 @@
# tests/test_integration.py
import os
import tempfile
from unittest.mock import patch, MagicMock
from agent import generate
FIXTURE_DIR = os.path.join(os.path.dirname(__file__), 'test_data')
@patch('agent.api_client.requests.post')
def test_full_pipeline_mock_api(mock_post):
"""完整流水线测试,API 调用使用 mock。"""
mock_response = MagicMock()
mock_response.json.return_value = {
'choices': [{'message': {'content': '''{
"groups": {
"g1": {
"program": "ZAN04MAT",
"records": [
{
"input": {
"R01INNFIL": {
"R01-APPL-ID": "A0000001",
"R01-EMP-ID": "00000101",
"R01-APPL-DATE": "20260101",
"R01-START-TIME": "0900",
"R01-END-TIME": "1800",
"R01-STATUS": "0",
"R01-OVT-TYPE": "W",
"R01-FILLER": "D000000000000000000000000000000000000000000001"
}
}
}
]
}
}
}'''}}]
}
mock_response.raise_for_status = MagicMock()
mock_post.return_value = mock_response
cpy_dir = FIXTURE_DIR
output_dir = tempfile.mkdtemp()
rules_dir = os.path.join(os.path.dirname(__file__), '..', 'rules')
design_md = os.path.join(FIXTURE_DIR, '詳細設計書_ZAN04MAT.md')
source_cbl = os.path.join(FIXTURE_DIR, '..', '..', '..',
'cobol-tna-system', 'src', 'ZAN04MAT.cbl')
# Fallback if real source not found
if not os.path.exists(source_cbl):
source_cbl = os.path.join(output_dir, 'dummy.cbl')
with open(source_cbl, 'w', encoding='utf-8') as f:
f.write("COPY ZAN01REC REPLACING ==(A)== BY ==R01==.\n")
try:
result = generate(
design_md=design_md,
source_cbl=source_cbl,
file_db_md='dummy.md',
cpy_dir=cpy_dir,
db_md='dummy_db.md',
output_dir=output_dir,
api_key='test-key',
rules_dir=rules_dir,
)
assert result['program_id'] == 'ZAN04MAT'
assert result['groups'] > 0
assert len(result['output_files']) > 0
for path in result['output_files'].values():
assert os.path.exists(path), f"输出文件不存在: {path}"
finally:
if source_cbl.endswith('dummy.cbl') and os.path.exists(source_cbl):
os.remove(source_cbl)
+65
View File
@@ -0,0 +1,65 @@
# tests/test_markdown_utils.py
from agent.markdown_utils import extract_section, parse_table_rows, parse_table_from_section, find_row_by_key
SAMPLE_MD = """
## 基本情報
| # | 項目 | 内容 |
|---|------|------|
| 1 | システム名 | 残業統計管理システム |
| 4 | PGMパターン | マッチング(1:1) |
| 5 | 機能概要 | 取消マッチング処理 |
## 使用ファイル一覧
| NO | 使用ファイル/DB名 | 識別子 | DD名 | I/O | COPY群 | 媒体 | 備考 |
|----|------------------|--------|------|-----|--------|------|------|
| 1 | OVT-SORTED | R01 | ZAN04R01 | I | ZAN01REC | PS | |
| 2 | ERROR-LOG | W01 | ZAN04W01 | O | ZAN05REC | PS | |
## 出力レコード定義
### 出力ファイル1W01/OVT-MATCHED
| No | 項目名 | 設定元 | 備考 |
|----|--------|--------|------|
| 1 | APPL-ID | R01.APPL-ID | |
| 2 | EMP-ID | R01.EMP-ID | |
"""
def test_extract_section():
result = extract_section(SAMPLE_MD, "基本情報")
assert "残業統計管理システム" in result
assert "使用ファイル一覧" not in result
def test_extract_section_not_found():
result = extract_section(SAMPLE_MD, "存在しないセクション")
assert result == ''
def test_parse_table_rows():
rows = parse_table_from_section(SAMPLE_MD, "基本情報")
assert len(rows) >= 3
assert rows[0]['項目'] == 'システム名'
def test_find_row_by_key():
rows = parse_table_from_section(SAMPLE_MD, "基本情報")
row = find_row_by_key(rows, '項目', 'PGMパターン')
assert row is not None
assert row['内容'] == 'マッチング(1:1)'
def test_parse_use_file_table():
rows = parse_table_from_section(SAMPLE_MD, "使用ファイル一覧")
assert len(rows) == 2
assert rows[0]['識別子'] == 'R01'
assert rows[0]['DD名'] == 'ZAN04R01'
def test_extract_output_records_section():
result = extract_section(SAMPLE_MD, "出力レコード定義")
assert "出力ファイル1" in result
assert "W01/OVT-MATCHED" in result
+59
View File
@@ -0,0 +1,59 @@
from agent.models import FileInfo, CopyField, ProgramMeta, KeyInfo, ModuleInfo, TableColumn, TableInfo
def test_file_info():
f = FileInfo(
no=1, file_db_name="OVT-SORTED", identifier="R01",
dd_name="ZAN04R01", io="I", copy_group="ZAN01REC",
format="FB", record_len=80, medium="PS", remarks="有效申请"
)
assert f.identifier == "R01"
assert f.medium == "PS"
def test_copy_field():
cf = CopyField(level=3, name="R01-APPL-ID", raw_name="(A)APPL-ID",
pic_type="X(008)", pic_bytes=8)
assert cf.name == "R01-APPL-ID"
assert cf.pic_bytes == 8
def test_program_meta_defaults():
meta = ProgramMeta(
program_id="TEST", program_name="", system_name="",
pgm_type="", pgm_pattern="",
summary_lines=[], prerequisites=[],
files=[], keys=[], modules=[],
process_detail="", output_records="",
input_type="file",
copy_fields={}, db_tables={}
)
assert meta.program_id == "TEST"
assert meta.input_type == "file"
def test_table_info():
col = TableColumn(no=1, name_jp="社員番号", name_en="EMP_ID",
type="CHAR", max_len="8", decimal_digits="",
byte_count="8", nullable=False, is_pk=True)
table = TableInfo(table_name="EMP_MASTER", db_id="EMP_MASTER",
copy_id="", columns=[col], pk_columns=["EMP_ID"])
assert table.pk_columns == ["EMP_ID"]
assert len(table.columns) == 1
def test_key_info():
k = KeyInfo(no=1, file_name="ファイルR01",
sort_condition="A001>A002>A003",
key_condition="A001>A002>A003")
assert k.no == 1
assert k.file_name == "ファイルR01"
assert "A001" in k.sort_condition
def test_module_info():
m = ModuleInfo(no=1, function="メッセージ編集出力SUB",
program_id="SUB02MSG", copy_name="ZANMSGAC")
assert m.function == "メッセージ編集出力SUB"
assert m.program_id == "SUB02MSG"
assert m.copy_name == "ZANMSGAC"
+83
View File
@@ -0,0 +1,83 @@
import json
import os
import tempfile
from agent.output_writer import OutputWriter
def test_write_json_files():
with tempfile.TemporaryDirectory() as tmpdir:
writer = OutputWriter(tmpdir)
ai_result = {
"groups": {
"g1": {
"program": "TEST",
"records": [
{"input": {"R01": {"R01-FIELD": "A001"}}}
]
}
}
}
written = writer.write("TESTPGM", ai_result, input_type="file")
assert len(written) == 1
json_path = written["g1/json"]
assert os.path.exists(json_path)
with open(json_path, 'r', encoding='utf-8') as f:
content = json.load(f)
assert content['program'] == 'TEST'
assert len(content['records']) == 1
def test_write_mixed_type():
with tempfile.TemporaryDirectory() as tmpdir:
writer = OutputWriter(tmpdir)
ai_result = {
"groups": {
"g1": {
"program": "MIXED",
"records": [{"input": {"R01": {"F1": "X"}}}],
"sql": "INSERT INTO T VALUES ('x');"
}
}
}
written = writer.write("MIXEDPGM", ai_result, input_type="mixed")
assert len(written) == 2
assert os.path.exists(written["g1/json"])
assert os.path.exists(written["g1/sql"])
def test_write_multiple_groups():
with tempfile.TemporaryDirectory() as tmpdir:
writer = OutputWriter(tmpdir)
ai_result = {
"groups": {
"g1": {"records": [{}]},
"g2": {"records": [{}]},
"g3": {"records": [{}]},
}
}
written = writer.write("MULTI", ai_result, input_type="file")
assert len(written) == 3
for g in ['g1', 'g2', 'g3']:
assert f'{g}/json' in written
assert os.path.exists(written[f'{g}/json'])
def test_directory_structure():
with tempfile.TemporaryDirectory() as tmpdir:
writer = OutputWriter(tmpdir)
ai_result = {"groups": {"g1": {"records": [{}]}}}
writer.write("MYPROG", ai_result, input_type="file")
expected_dir = os.path.join(tmpdir, "MYPROG", "g1")
assert os.path.isdir(expected_dir)
assert os.path.isfile(os.path.join(expected_dir, "MYPROG_g1.json"))
+96
View File
@@ -0,0 +1,96 @@
from agent.prompt_builder import PromptBuilder
from agent.models import ProgramMeta, FileInfo, CopyField
def test_build_basic_prompt():
meta = ProgramMeta(
program_id='ZAN04MAT', program_name='取消マッチング処理',
system_name='残業統計管理システム',
pgm_type='メイン', pgm_pattern='マッチング(1:1)',
summary_lines=['取消申請のマッチング'], prerequisites=[],
files=[], keys=[], modules=[],
process_detail='1.初期処理...\n2.主処理...',
output_records='### 出力ファイル1...',
input_type='file', copy_fields={}, db_tables={}
)
builder = PromptBuilder()
prompt = builder.build(
meta=meta,
rules_text='# マッチング(1:1) データ生成規則\n...',
group_descriptions=['两端不一致', '逆方向两端不一致', '中间不一致'],
group_count=3
)
assert 'ZAN04MAT' in prompt
assert 'マッチング(1:1)' in prompt
assert '1.初期処理' in prompt
assert '生成するグループ数: 3' in prompt
assert 'g1: 两端不一致' in prompt
assert 'g2: 逆方向两端不一致' in prompt
assert 'g3: 中间不一致' in prompt
assert 'JSON形式' in prompt
def test_build_with_copy_fields():
fields = {
'R01': [
CopyField(level=3, name='R01-APPL-ID', raw_name='(A)APPL-ID',
pic_type='X(008)', pic_bytes=8),
CopyField(level=3, name='R01-EMP-ID', raw_name='(A)EMP-ID',
pic_type='9(008)', pic_bytes=8),
]
}
meta = ProgramMeta(
program_id='TEST', program_name='', system_name='',
pgm_type='メイン', pgm_pattern='マッチング(1:1)',
summary_lines=[], prerequisites=[],
files=[
FileInfo(no=1, file_db_name='INPUT-FILE', identifier='R01',
dd_name='TESTR01', io='I', copy_group='TESTREC',
format='FB', record_len=80, medium='PS', remarks='')
],
keys=[], modules=[],
process_detail='', output_records='',
input_type='file', copy_fields=fields, db_tables={}
)
builder = PromptBuilder()
prompt = builder.build(
meta=meta,
rules_text='# 規則',
group_descriptions=['テスト'],
group_count=1
)
assert 'R01-APPL-ID' in prompt
assert 'X(008)' in prompt
assert 'R01-EMP-ID' in prompt
def test_build_db_prompt():
meta = ProgramMeta(
program_id='TESTDB', program_name='DB更新', system_name='',
pgm_type='メイン', pgm_pattern='DB更新',
summary_lines=[], prerequisites=[],
files=[
FileInfo(no=1, file_db_name='DB-TABLE', identifier='DB',
dd_name='', io='I', copy_group='',
format='', record_len=0, medium='DB', remarks='')
],
keys=[], modules=[],
process_detail='', output_records='',
input_type='db', copy_fields={}, db_tables={}
)
builder = PromptBuilder()
prompt = builder.build(
meta=meta,
rules_text='# DB更新規則',
group_descriptions=['テスト'],
group_count=1
)
assert 'DB' in prompt or 'SQL' in prompt
assert 'INSERT' in prompt
+90
View File
@@ -0,0 +1,90 @@
import os
from agent.rule_loader import RuleLoader, PGM_PATTERN_MAP
from agent.models import ProgramMeta
RULES_DIR = os.path.join(os.path.dirname(__file__), '..', 'rules')
def test_pgm_pattern_map_has_known_patterns():
assert 'マッチング(1:1)' in PGM_PATTERN_MAP
assert 'マッチング(1:N)' in PGM_PATTERN_MAP
assert 'DB更新' in PGM_PATTERN_MAP
def test_load_matching_1_1_rule():
loader = RuleLoader(RULES_DIR)
meta = ProgramMeta(
program_id='TEST', program_name='', system_name='',
pgm_type='メイン', pgm_pattern='マッチング(1:1)',
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='file', copy_fields={}, db_tables={}
)
combined, descriptions, count = loader.load(meta)
assert count > 0
assert len(combined) > 0
assert 'マッチング(1:1)' in combined
assert '数据生成' in combined
def test_load_matching_1_n_rule():
loader = RuleLoader(RULES_DIR)
meta = ProgramMeta(
program_id='TEST', program_name='', system_name='',
pgm_type='メイン', pgm_pattern='マッチング(1:N)',
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='file', copy_fields={}, db_tables={}
)
combined, descriptions, count = loader.load(meta)
assert count > 0
assert len(combined) > 0
assert 'マッチング(1:N)' in combined
def test_load_nonexistent_pattern_raises():
loader = RuleLoader(RULES_DIR)
meta = ProgramMeta(
program_id='TEST', program_name='', system_name='',
pgm_type='メイン', pgm_pattern='存在しないパターン',
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
process_detail='', output_records='',
input_type='file', copy_fields={}, db_tables={}
)
try:
loader.load(meta)
assert False, 'Should have raised FileNotFoundError'
except FileNotFoundError:
pass
def test_detect_conditional_branch():
process = """
2-1-2.ロジック分岐判定(EVALUATE)
2-1-2-1.STATUS='1'の場合
INSERT処理
"""
assert RuleLoader._detect_feature(process, ['場合', 'EVALUATE', 'IF']) is True
def test_no_keyword_match():
process = "1.初期処理\n2.主処理\n3.終了処理"
assert RuleLoader._detect_feature(process, ['EVALUATE']) is False
def test_rule_with_conditional_feature():
loader = RuleLoader(RULES_DIR)
meta = ProgramMeta(
program_id='TEST', program_name='', system_name='',
pgm_type='メイン', pgm_pattern='マッチング(1:1)',
summary_lines=[], prerequisites=[], files=[], keys=[], modules=[],
process_detail='2-1-2.EVALUATEで条件分岐する。',
output_records='',
input_type='file', copy_fields={}, db_tables={}
)
combined, descriptions, count = loader.load(meta)
assert count > 0
assert '条件分支' in combined